前一篇文章已经对UniswapV4做了简单的概述,了解了其主要特性。从本篇开始,我们要深入合约实现了,先看看其合约结构。
本文首发于公众号:Keegan小钢
前一篇文章已经对 UniswapV4 做了简单的概述,了解了其主要特性。从本篇开始,我们要深入合约实现了,先看看其合约结构。
UniswapV4 的合约项目,还是和之前的版本一样,分为了 v4-core 和 v4-periphery 两个 repo。另外,之前的版本,合约项目框架是用 Hardhat 搭建的,而这回,你会发现改用 Foundry 了。Foundry 正在慢慢变成开发新合约项目的主流框架,因为 Foundry 相比 Hardhat,写单元测试和脚本都和写合约一样,可以统一用 solidity 来编写,这对于不太精通 JavaScript/TypeScript 的合约工程师来说就会更方便了。
还有,目前的合约实现其实还不是最终版,近期依然在不断提交更新。
当前,v4-core 的合约目录结构如下图所示:

interfaces 定义了所有接口合约,libraries 存放的是所有库合约,test 目录是测试用的,我们不用关心。types 值得介绍一下,在以前的版本中没有这个。其实就是封装了几种特定类型,包括 4 种类型:
PoolKey 最容易理解,我们来看看其代码实现:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {Currency} from "./Currency.sol";
import {IHooks} from "../interfaces/IHooks.sol";
/// @notice Returns the key for identifying a pool
struct PoolKey {
/// @notice The lower currency of the pool, sorted numerically
Currency currency0;
/// @notice The higher currency of the pool, sorted numerically
Currency currency1;
/// @notice The pool swap fee, capped at 1_000_000. The upper 4 bits determine if the hook sets any fees.
uint24 fee;
/// @notice Ticks that involve positions must be a multiple of tick spacing
int24 tickSpacing;
/// @notice The hooks of the pool
IHooks hooks;
}
其实就是定义了一个结构体,包含了五个字段,这些字段加在一起就是一个池子的唯一标识。其中,currency0 和 currency1 就是之前版本的 token0 和 token1,只是变成了 Currency 类型。Currency 类型其实本质上也是地址类型,是由地址类型声明的用户自定义值类型。待会我们再展开介绍什么是用户自定义值类型。
PoolKey 相比 UniswapV3 时多了一个 hooks,这其实就是要指定的 Hooks 合约地址。
PoolId 就是一种用户自定义值类型,我们来看看其代码实现:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {PoolKey} from "./PoolKey.sol";
type PoolId is bytes32;
/// @notice Library for computing the ID of a pool
library PoolIdLibrary {
function toId(PoolKey memory poolKey) internal pure returns (PoolId) {
return PoolId.wrap(keccak256(abi.encode(poolKey)));
}
}
最关键的一行就是 type PoolId is bytes32。这就是用户自定义值类型的用法,使用 type C is V 的方式进行定义。V 被称为基础类型,可以是布尔型、整型、地址型、字节型等值类型的一种,但不能是 mapping、数据、结构体等引用类型。C 就是所要定义的新类型名称,有点类似于是 V 的别名,但会有严格的类型检查。
用户自定义值类型有两个内置函数可用于与基础类型之间进行转换。wrap() 函数可以将基础类型转为自定义类型,比如上面代码通过调用 PoolId.wrap() 函数就将一个 bytes32 类型的值转为了 PoolId 类型。还有个 unwrap() 函数则可以将自定义类型转为基础类型。
这种自定义类型是在 solidity 0.8.8 开始引入的,所以也只能在 0.8.8 及以上的编译版本中使用。
PoolId 其实就是用于定义一个池子的唯一 ID,从 PoolIdLibrary 的 toId() 函数可以看出,其实就是将 poolKey 进行编码后计算得出的哈希值,然后通过 wrap 函数将这个 bytes32 类型的哈希值转为了 PoolId 类型。
Currency 和 BalanceDelta 也是和 PoolId 一样的用户自定义值类型。Currency 的基础类型是 address 类型,用来表示池子里的资产。BalanceDelta 的基础类型是 int256,用来表示净余额。
Currency 的实现不只是简单地用 type 定义了其类型,还定义了一些函数,如下所示:
type Currency is address;
using {greaterThan as >, lessThan as <, greaterThanOrEqualTo as >=, equals as ==} for Currency global;
function equals(Currency currency, Currency other) pure returns (bool) {
return Currency.unwrap(currency) == Currency.unwrap(other);
}
function greaterThan(Currency currency, Currency other) pure returns (bool) {
return Currency.unwrap(currency) > Currency.unwrap(other);
}
function lessThan(Currency currency, Currency other) pure returns (bool) {
return Currency.unwrap(currency) < Currency.unwrap(other);
}
function greaterThanOrEqualTo(Currency currency, Currency other) pure returns (bool) {
return Currency.unwrap(currency) >= Currency.unwrap(other);
}
自定义类型虽然是基于基础值类型而定义的,但因为类型检查,是没办法直接使用基础类型本身的用法的,包括比较符和基础类型本身的内置函数。虽然 Currency 类型的基础类型是 address,而我们知道 address 类型的两个变量是可以直接使用 >、<、>=、== 这些比较符去比较两个地址类型的大小的。但 Currency 类型则不能直接使用,类型检查无法通过。因此,需要再额外定义四个函数,分别用于对应的四个比较符,再通过 using 语句把这四个函数作为各自的比较符进行使用。如此一来,就可以把 Currency 类型用于大小比较了。
另外,与 Currency 配置使用的还有库合约 CurrencyLibrary,其封装了转账、查询余额、是否原生代币等函数。需要对自定义类型添加额外的功能函数时,通常都是为其封装对应的库合约,PoolId 对应的有 PoolIdLibrary,BalanceDelta 对应的有 BalanceDeltaLibrary。
BalanceDelta 需要说明一下,它是用于表示净余额的,它其实是将两个代币的数额组装到一起的。在 BalanceDelta 中有定义了以下函数:
function toBalanceDelta(int128 _amount0, int128 _amount1) pure returns (BalanceDelta balanceDelta) {
/// @solidity memory-safe-assembly
assembly {
balanceDelta :=
or(shl(128, _amount0), and(0x00000000000000000000000000000000ffffffffffffffffffffffffffffffff, _amount1))
}
}
该函数就是将两个代币的金额一起转成 BalanceDelta 类型。可看到其实现使用了内联汇编,其实就是前 128 位用于存放 amount0,后 128 位用于存放 amount1。
BalanceDeltaLibrary 库合约中则封装了 amount0() 和 amount1(),可从 BalanceDelta 中分别读取出 amount0 和 amount1。
至此,关于 types 目录的就讲解这么多了。回到 v4-core 的合约目录结构,可看到根目录下有 5 个合约文件:
Claims.solFees.solNoDelegateCall.solOwned.solPoolManager.sol最核心的就是 PoolManager.sol,也就是统一管理所有池的单例合约。其他几个合约都是被 PoolManager 所继承的子合约。关于 PoolManager 合约的具体实现我们下一篇文章再讲解。
关于 Claims 合约,很有必要说明一下。其实两个星期前,即 11 月中旬之前,PoolManager 还是继承了 ERC1155 的,用于额外的代币记账。但是,我发现 11 月 14 号有一个提交,移除了 ERC1155 部分,改为了继承自 Claims 合约。
所以 Claims 合约其实就是用于替代 ERC1155 来实现额外记账功能的。其实现了 balanceOf 和 transfer 两个开放函数,以及 _mint 和 _burn 两个内部函数。具体实现比较简单,这里就不贴代码了。
Fees 封装了费用相关的函数和状态变量,包括获取协议费用、获取 Hook 费用、获取动态交易费用,以及提取协议费用、提取 Hoos 费用等。
NoDelegateCall 和 UniswapV3 中使用的 NoDelegateCall 一样的,是为了防止代理调用。
Owned 则是用于设置和检查 owner 权限的。
接着,来看看 v4-periphery 的合约代码结构。其根目录下有三个目录和一个文件:
hooks/examplesinterfaceslibrariesBaseHook.solhooks/examples 里是几个实现不同应用场景的示例代码,目前包括:
FullRangeLimitOrderTWAMMGeomeanOracleVolatilityOracle后面会用其他篇章一一剖析这几个实现,目前我们就不展开了。
BaseHook 是所有 Hooks 的基础合约,封装了最简单的实现。
实际上,我个人觉得这个 v4-periphery 应该是还没完成全部实现的,因为目前该 repo 还缺少了关键的路由合约。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!