剖析DeFi交易产品之UniswapV4:合约结构篇
前一篇文章已经对 UniswapV4 做了简单的概述,了解了其主要特性。从本篇开始,我们要深入合约实现了,先看看其合约结构。
UniswapV4 的合约项目,还是和之前的版本一样,分为了 v4-core 和 v4-periphery 两个 repo。另外,之前的版本,合约项目框架是用 Hardhat 搭建的,而这回,你会发现改用 Foundry 了。Foundry 正在慢慢变成开发新合约项目的主流框架,因为 Foundry 相比 Hardhat,写单元测试和脚本都和写合约一样,可以统一用 solidity 来编写,这对于不太精通 JavaScript/TypeScript 的合约工程师来说就会更方便了。
还有,目前的合约实现其实还不是最终版,近期依然在不断提交更新。
当前,v4-core 的合约目录结构如下图所示:
interfaces 定义了所有接口合约,libraries 存放的是所有库合约,test 目录是测试用的,我们不用关心。types 值得介绍一下,在以前的版本中没有这个。其实就是封装了几种特定类型,包括 4 种类型:
BalanceDelta
Currency
PoolId
PoolKey
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.sol
Fees.sol
NoDelegateCall.sol
Owned.sol
PoolManager.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/examples
interfaces
libraries
BaseHook.sol
hooks/examples
里是几个实现不同应用场景的示例代码,目前包括:
FullRange
LimitOrder
TWAMM
GeomeanOracle
VolatilityOracle
后面会用其他篇章一一剖析这几个实现,目前我们就不展开了。
BaseHook
是所有 Hooks 的基础合约,封装了最简单的实现。
实际上,我个人觉得这个 v4-periphery 应该是还没完成全部实现的,因为目前该 repo 还缺少了关键的路由合约。