这是我们研发「BlockLever」的第 6 篇研发日志,前 5 篇如下:
- BlockLever实战营日志 #1 | 开营
- BlockLever实战营日志 #2 | 产品需求梳理
- BlockLever实战营日志 #3 | 合约开发阶段
- BlockLever实战营日志 #4 | 完成第一个里程碑
- BlockLever实战营日志 #5 | 重新梳理文档
前天,在做 fork 主网的集成测试的时候,发现在路由合约中引入的 QuoterV3 合约无法正常工作,昨天集中精力对其进行了调试,终于发现了问题,修复后集成测试也通过了。
背景介绍
很多了解过 UniswapV3 的技术人员都知道,其有个 QuoterV2 合约提供了系列报价函数,最核心的就是 4 个函数:
- quoteExactInputSingle
- quoteExactOutputSingle
- quoteExactInput
- quoteExactOutput
然而,因为底层实现时会直接调用 pool 合约的 swap 函数,所以这几个 quote 函数都无法被声明为 view 函数,这就给很多合约接入的项目带来了诸多不便。
在「BlockLever 」项目中,我们没有接入 UniswapV3,而是接入 PancakeSwapV3 。而 PancakeSwapV3 是在 UniswapV3 的基础上稍微做了很小的一点改动而部署的,其 Quoter 合约也是和 UniswapV3 的一样,几个报价函数也不是只读的 view 函数。
「BlockLever」的路由合约定义了几个预览函数如下:
- previewBorrowToBuy
- previewSellToRepay
- previewBorrowToSell
- previewBuyToRepay
这几个预览函数需要调用 Quoter 合约查询报价,如果我们直接使用以上说的 quote 函数,那我们这几个 preview 函数也一样无法声明为 view 函数。
在 DeFi 协议中,preview 类型的函数并不仅仅是"开发时方便调试用"。它们往往承担着几个非常重要的角色:
- 前端报价预览
- 区块浏览器直接查询
- 第三方合约只读调用
- 风控与策略模拟
如果这些函数不是 view,那么:
- 前端需要通过 transaction 调用,体验极差
- 区块浏览器无法直接查询
- 第三方协议难以安全集成
- 用户无法低成本进行策略预估
因此,在 BlockLever 中,我们非常明确地将所有 preview 函数定义为 纯只读函数,这是一个产品级别的设计决策,而不仅是技术实现细节。
有没有办法解决这个问题呢?其实是有的,我们不用 QuoterV2,改用 QuoterV3 即可,这就要用到下面介绍的这个库了。
view-quoter-v3
其实 Uniswap 后来是有实现 v3 版本的 Quoter 合约 的,只不过它是以一个独立仓库的形式存在,因此并没有像 QuoterV2 那样被广泛使用和认知。
该项目的 GitHub 地址如下:
这个项目中的 Quoter 合约,对外提供了与 QuoterV2 完全一致的 4 个核心报价函数,同时还额外增加了两个支持直接指定 Pool 的函数:
- quoteExactInputSingleWithPool
- quoteExactOutputSingleWithPool
最关键的一点在于:所有报价函数都被声明为 view 函数,这正是我们在 BlockLever 中所需要的能力。
其核心实现思路,是将原本位于 UniswapV3Pool 中、依赖 swap 执行过程的计算逻辑,重新抽离并改写为纯计算逻辑,从而避免任何状态修改,使其可以安全地以只读方式调用。
Uniswap 官方其实也已经在 BNB Chain 上部署了该合约。但由于 BlockLever 接入的并不是 Uniswap,而是 PancakeSwapV3,因此无法直接复用该实现。
而 PancakeSwap 官方目前也并没有提供对应的 view Quoter 合约,于是我们选择 fork 该项目,并对其进行适配 PancakeSwapV3 的修改,以满足路由合约中预览函数的调用需求。
事实上,在第一期实战营项目 「BlockETF」 中,我们就已经实现过一版 适配 PancakeSwap 的只读 Quoter 合约。
不过,BlockETF 中的预览逻辑并不完全依赖 Quoter 报价结果,而是同时结合了 价格预言机的数据 进行计算。因此,即使 Quoter 合约本身存在问题,BlockETF 在实际运行中依然不会出现明显异常。
也正因如此,如果不是这次在 BlockLever 的主网 fork 集成测试 中完整跑通真实调用链路,我们很可能会一直误以为 BlockETF 中使用的 Quoter 实现是完全可用的。
这次问题的暴露,也再次验证了一个事实:只有在真实环境下进行系统级集成测试,才能发现那些被业务逻辑"掩盖"的底层隐患。
适配 PancakeSwap 的核心改动
将 view-quoter-v3 适配为支持 PancakeSwapV3 ,整体改动其实并不多,主要集中在 Pool 地址计算逻辑 以及 Pool 接口定义差异 这两个方面。
1. PoolAddress 库的适配
首先需要修改的是 PoolAddress 库合约,该合约中存在两个必须调整的关键点。
第一个修改点 是 POOL_INIT_CODE_HASH。该常量需要从 UniswapV3Pool 的 POOL_INIT_CODE_HASH,替换为 PancakeSwapV3Pool 对应的值,否则在计算 Pool 地址时会直接得到错误结果。
第二个修改点 位于 computeAddress 函数中。在 UniswapV3 中,计算 Pool 地址时使用的是 Factory 合约地址 ;而在 PancakeSwapV3 中,用于计算 Pool 地址的并不是工厂合约,而是 Deployer 合约地址。
因此,computeAddress 函数中的 factory 参数需要整体替换为 PancakeSwapV3 的 deployer 地址。
这两处差异,都可以直接通过对比 PancakeSwap 官方仓库中的 PoolAddress 实现来确认。
2. Pool 接口类型的替换
第二类改动,发生在多个引用 IUniswapV3Pool 接口的合约中。
在这些地方,我们统一将接口替换为 IPancakeV3Pool 。这一问题,正是在本次 BlockLever 的主网 fork 集成测试 中被真正暴露出来的。
其根本原因在于,两者虽然接口名和函数结构高度相似,但在 细节定义上并不完全一致。
一个非常典型的差异点,是 slot0() 函数的返回值类型:
- 在 IUniswapV3Pool 中,
feeProtocol字段的类型为 uint8 - 而在 IPancakeV3Pool 中,对应字段的类型为 uint32
这一差异在单元测试或模拟环境中并不一定会立刻触发问题,但在 fork 主网、读取真实 Pool 状态时,就会导致 ABI 解码错误或潜在的逻辑异常。
3. 为什么这个问题容易被忽略
从表面上看,UniswapV3 与 PancakeSwapV3 的 Pool 接口"几乎一致",但正是这种"高度相似",反而容易让人在实现时 下意识地直接复用接口定义。
如果没有在真实链上环境中进行完整的集成测试,这类 类型级别的不一致 很容易长期潜伏在系统中,而不被察觉。
这也是为什么,本次问题并不是通过单元测试发现的,而是依赖 fork 主网后的系统级验证 才被准确定位。
小结
这次 view-quoter-v3 的适配过程再次印证了一点:
在 DeFi 协议集成中,"接口看起来一样"并不等于"可以安全复用"。
即便是源自同一套设计体系的协议分支,也必须以 真实部署实现 为准,逐项验证其接口、参数和数据类型,而不能仅凭经验或文档假设其行为一致。
集成测试的真正价值
回过头来看,这次 Quoter 相关的问题,并不是一个"代码写错了"的问题,而是一个只有在系统级集成测试中才会暴露的问题。
如果只停留在单个合约、单个函数的单元测试层面,所有逻辑看起来都是正确的;但当这些合约真正与真实链上协议、真实 Pool 状态交互时,细微的实现差异才会被放大。
这也是为什么,在 BlockLever 的研发过程中,我们将 fork 主网后的集成测试 视为一个必须完成的阶段,而不是可选项。
对实战营的学员来说,这一阶段同样重要:真正的 DeFi 工程能力,并不止于"写出合约",而在于让一整套系统在真实环境中 稳定、可预期、可验证地运行。
参考阅读: