剖析DeFi交易产品之UniswapV4:合约结构篇

2023-11-27 15:18:40 浏览数 (2)

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 最容易理解,我们来看看其代码实现:

代码语言:javascript复制
// 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;
}

其实就是定义了一个结构体,包含了五个字段,这些字段加在一起就是一个池子的唯一标识。其中,currency0currency1 就是之前版本的 token0token1,只是变成了 Currency 类型。Currency 类型其实本质上也是地址类型,是由地址类型声明的用户自定义值类型。待会我们再展开介绍什么是用户自定义值类型。

PoolKey 相比 UniswapV3 时多了一个 hooks,这其实就是要指定的 Hooks 合约地址。

PoolId 就是一种用户自定义值类型,我们来看看其代码实现:

代码语言:javascript复制
// 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,从 PoolIdLibrarytoId() 函数可以看出,其实就是将 poolKey 进行编码后计算得出的哈希值,然后通过 wrap 函数将这个 bytes32 类型的哈希值转为了 PoolId 类型。

CurrencyBalanceDelta 也是和 PoolId 一样的用户自定义值类型。Currency 的基础类型是 address 类型,用来表示池子里的资产。BalanceDelta 的基础类型是 int256,用来表示净余额。

Currency 的实现不只是简单地用 type 定义了其类型,还定义了一些函数,如下所示:

代码语言:javascript复制
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 对应的有 PoolIdLibraryBalanceDelta 对应的有 BalanceDeltaLibrary

BalanceDelta 需要说明一下,它是用于表示净余额的,它其实是将两个代币的数额组装到一起的。在 BalanceDelta 中有定义了以下函数:

代码语言:javascript复制
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 中分别读取出 amount0amount1

至此,关于 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 还缺少了关键的路由合约。

0 人点赞