{"content":{"title":"剖析DeFi交易产品之UniswapV4：合约结构篇","body":"本文首发于公众号：[Keegan小钢](https://mp.weixin.qq.com/s?__biz=MzA5OTI1NDE0Mw==&mid=2652494685&idx=1&sn=8baae46a47a1282f71f1c3b7a4ea30e2&chksm=8b68514dbc1fd85b317f8b0abf423ea862a819a827e982a2073a8605903495bbe152b600c777&token=1859759885&lang=zh_CN#rd)\r\n***\r\n[前一篇文章](https://mp.weixin.qq.com/s?__biz=MzA5OTI1NDE0Mw==&mid=2652494678&idx=1&sn=74456ffc9b0ca32161e4b95c8ca99398&chksm=8b685146bc1fd850fdd7c698ba984fe3e09d50a0b366c256d4ef8e725b8056f219417e6da98e&token=619684707&lang=zh_CN#rd)已经对 UniswapV4 做了简单的概述，了解了其主要特性。从本篇开始，我们要深入合约实现了，先看看其合约结构。\r\n\r\nUniswapV4 的合约项目，还是和之前的版本一样，分为了 [v4-core](https://github.com/Uniswap/v4-core) 和 [v4-periphery](https://github.com/Uniswap/v4-periphery) 两个 repo。另外，之前的版本，合约项目框架是用 **Hardhat** 搭建的，而这回，你会发现改用 **Foundry** 了。Foundry 正在慢慢变成开发新合约项目的主流框架，因为 Foundry 相比 Hardhat，写单元测试和脚本都和写合约一样，可以统一用 **solidity** 来编写，这对于不太精通 **JavaScript/TypeScript** 的合约工程师来说就会更方便了。\r\n\r\n还有，目前的合约实现其实还不是最终版，近期依然在不断提交更新。\r\n\r\n当前，**v4-core** 的合约目录结构如下图所示：\r\n\r\n![image.png](https://img.learnblockchain.cn/attachments/2023/11/Am6B8LYw6561f2117cd63.png)\r\n\r\n**interfaces** 定义了所有接口合约，**libraries** 存放的是所有库合约，**test** 目录是测试用的，我们不用关心。**types** 值得介绍一下，在以前的版本中没有这个。其实就是封装了几种特定类型，包括 4 种类型：\r\n\r\n* **BalanceDelta**\r\n* **Currency**\r\n* **PoolId**\r\n* **PoolKey**\r\n\r\n`PoolKey` 最容易理解，我们来看看其代码实现：\r\n\r\n```C++\r\n// SPDX-License-Identifier: MIT\r\npragma solidity ^0.8.19;\r\n\r\nimport {Currency} from \"./Currency.sol\";\r\nimport {IHooks} from \"../interfaces/IHooks.sol\";\r\n\r\n/// @notice Returns the key for identifying a pool\r\nstruct PoolKey {\r\n    /// @notice The lower currency of the pool, sorted numerically\r\n    Currency currency0;\r\n    /// @notice The higher currency of the pool, sorted numerically\r\n    Currency currency1;\r\n    /// @notice The pool swap fee, capped at 1_000_000. The upper 4 bits determine if the hook sets any fees.\r\n    uint24 fee;\r\n    /// @notice Ticks that involve positions must be a multiple of tick spacing\r\n    int24 tickSpacing;\r\n    /// @notice The hooks of the pool\r\n    IHooks hooks;\r\n}\r\n```\r\n\r\n其实就是定义了一个结构体，包含了五个字段，这些字段加在一起就是一个池子的唯一标识。其中，`currency0` 和 `currency1` 就是之前版本的 `token0` 和 `token1`，只是变成了 `Currency` 类型。`Currency` 类型其实本质上也是地址类型，是由地址类型声明的**用户自定义值类型**。待会我们再展开介绍什么是用户自定义值类型。\r\n\r\n`PoolKey` 相比 **UniswapV3** 时多了一个 `hooks`，这其实就是要指定的 Hooks 合约地址。\r\n\r\n`PoolId` 就是一种**用户自定义值类型**，我们来看看其代码实现：\r\n\r\n```C++\r\n// SPDX-License-Identifier: MIT\r\npragma solidity ^0.8.20;\r\n\r\nimport {PoolKey} from \"./PoolKey.sol\";\r\n\r\ntype PoolId is bytes32;\r\n\r\n/// @notice Library for computing the ID of a pool\r\nlibrary PoolIdLibrary {\r\n    function toId(PoolKey memory poolKey) internal pure returns (PoolId) {\r\n        return PoolId.wrap(keccak256(abi.encode(poolKey)));\r\n    }\r\n}\r\n```\r\n\r\n最关键的一行就是 `type PoolId is bytes32`。这就是用户自定义值类型的用法，使用 `type C is V` 的方式进行定义。`V` 被称为基础类型，可以是布尔型、整型、地址型、字节型等值类型的一种，但不能是 mapping、数据、结构体等引用类型。`C` 就是所要定义的新类型名称，有点类似于是 `V` 的别名，但会有严格的类型检查。\r\n\r\n用户自定义值类型有两个内置函数可用于与基础类型之间进行转换。`wrap()` 函数可以将基础类型转为自定义类型，比如上面代码通过调用 `PoolId.wrap()` 函数就将一个 bytes32 类型的值转为了 `PoolId` 类型。还有个 `unwrap()` 函数则可以将自定义类型转为基础类型。\r\n\r\n这种自定义类型是在 `solidity 0.8.8` 开始引入的，所以也只能在 `0.8.8` 及以上的编译版本中使用。\r\n\r\n`PoolId` 其实就是用于定义一个池子的唯一 ID，从 `PoolIdLibrary` 的 `toId()` 函数可以看出，其实就是将 poolKey 进行编码后计算得出的哈希值，然后通过 `wrap` 函数将这个 `bytes32` 类型的哈希值转为了 `PoolId` 类型。\r\n\r\n`Currency` 和 `BalanceDelta` 也是和 `PoolId` 一样的用户自定义值类型。`Currency` 的基础类型是 `address` 类型，用来表示池子里的资产。`BalanceDelta` 的基础类型是 `int256`，用来表示净余额。\r\n\r\n`Currency` 的实现不只是简单地用 type 定义了其类型，还定义了一些函数，如下所示：\r\n\r\n```C++\r\ntype Currency is address;\r\n\r\nusing {greaterThan as >, lessThan as <, greaterThanOrEqualTo as >=, equals as ==} for Currency global;\r\n\r\nfunction equals(Currency currency, Currency other) pure returns (bool) {\r\n    return Currency.unwrap(currency) == Currency.unwrap(other);\r\n}\r\n\r\nfunction greaterThan(Currency currency, Currency other) pure returns (bool) {\r\n    return Currency.unwrap(currency) > Currency.unwrap(other);\r\n}\r\n\r\nfunction lessThan(Currency currency, Currency other) pure returns (bool) {\r\n    return Currency.unwrap(currency) < Currency.unwrap(other);\r\n}\r\n\r\nfunction greaterThanOrEqualTo(Currency currency, Currency other) pure returns (bool) {\r\n    return Currency.unwrap(currency) >= Currency.unwrap(other);\r\n}\r\n```\r\n\r\n自定义类型虽然是基于基础值类型而定义的，但因为类型检查，是没办法直接使用基础类型本身的用法的，包括比较符和基础类型本身的内置函数。虽然 `Currency` 类型的基础类型是 `address`，而我们知道 `address` 类型的两个变量是可以直接使用 `>、<、>=、==` 这些比较符去比较两个地址类型的大小的。但 `Currency` 类型则不能直接使用，类型检查无法通过。因此，需要再额外定义四个函数，分别用于对应的四个比较符，再通过 `using` 语句把这四个函数作为各自的比较符进行使用。如此一来，就可以把 `Currency` 类型用于大小比较了。\r\n\r\n另外，与 `Currency` 配置使用的还有库合约 `CurrencyLibrary`，其封装了转账、查询余额、是否原生代币等函数。需要对自定义类型添加额外的功能函数时，通常都是为其封装对应的库合约，`PoolId` 对应的有 `PoolIdLibrary`，`BalanceDelta` 对应的有 `BalanceDeltaLibrary`。\r\n\r\n`BalanceDelta` 需要说明一下，它是用于表示净余额的，它其实是将两个代币的数额组装到一起的。在 `BalanceDelta` 中有定义了以下函数：\r\n\r\n```C++\r\nfunction toBalanceDelta(int128 _amount0, int128 _amount1) pure returns (BalanceDelta balanceDelta) {\r\n    /// @solidity memory-safe-assembly\r\n    assembly {\r\n        balanceDelta :=\r\n            or(shl(128, _amount0), and(0x00000000000000000000000000000000ffffffffffffffffffffffffffffffff, _amount1))\r\n    }\r\n}\r\n```\r\n\r\n该函数就是将两个代币的金额一起转成 `BalanceDelta` 类型。可看到其实现使用了内联汇编，其实就是前 128 位用于存放 `amount0`，后 128 位用于存放 `amount1`。\r\n\r\n`BalanceDeltaLibrary` 库合约中则封装了 `amount0()` 和 `amount1()`，可从 `BalanceDelta` 中分别读取出 `amount0` 和 `amount1`。\r\n\r\n至此，关于 `types` 目录的就讲解这么多了。回到 **v4-core** 的合约目录结构，可看到根目录下有 5 个合约文件：\r\n\r\n* `Claims.sol`\r\n* `Fees.sol`\r\n* `NoDelegateCall.sol`\r\n* `Owned.sol`\r\n* `PoolManager.sol`\r\n\r\n最核心的就是 `PoolManager.sol`，也就是统一管理所有池的单例合约。其他几个合约都是被 `PoolManager` 所继承的子合约。关于 `PoolManager` 合约的具体实现我们下一篇文章再讲解。\r\n\r\n关于 `Claims` 合约，很有必要说明一下。其实两个星期前，即 11 月中旬之前，`PoolManager` 还是继承了 `ERC1155` 的，用于额外的代币记账。但是，我发现 11 月 14 号有一个[提交](https://github.com/Uniswap/v4-core/commit/3921fad6eb075b7853258a54fae2ce0cb5ba40b0#diff-84227e9327ad53c4ad37657de3ed4ee30143b70e038ddabd7e6214eba4f0865c)，移除了 `ERC1155` 部分，改为了继承自 `Claims` 合约。\r\n\r\n所以 `Claims` 合约其实就是用于替代 `ERC1155` 来实现额外记账功能的。其实现了 balanceOf 和 transfer 两个开放函数，以及 `_mint` 和 `_burn` 两个内部函数。具体实现比较简单，这里就不贴代码了。\r\n\r\n`Fees` 封装了费用相关的函数和状态变量，包括获取协议费用、获取 Hook 费用、获取动态交易费用，以及提取协议费用、提取 Hoos 费用等。\r\n\r\n`NoDelegateCall` 和 UniswapV3 中使用的 `NoDelegateCall` 一样的，是为了防止代理调用。\r\n\r\n`Owned` 则是用于设置和检查 owner 权限的。\r\n\r\n接着，来看看 **v4-periphery** 的合约代码结构。其根目录下有三个目录和一个文件：\r\n\r\n* `hooks/examples`\r\n* `interfaces`\r\n* `libraries`\r\n* `BaseHook.sol`\r\n\r\n`hooks/examples` 里是几个实现不同应用场景的示例代码，目前包括：\r\n\r\n* `FullRange`\r\n* `LimitOrder`\r\n* `TWAMM`\r\n* `GeomeanOracle`\r\n* `VolatilityOracle`\r\n\r\n后面会用其他篇章一一剖析这几个实现，目前我们就不展开了。\r\n\r\n`BaseHook` 是所有 Hooks 的基础合约，封装了最简单的实现。\r\n\r\n实际上，我个人觉得这个 **v4-periphery** 应该是还没完成全部实现的，因为目前该 repo 还缺少了关键的路由合约。"},"author":{"user":"https://learnblockchain.cn/people/96","address":null},"history":null,"timestamp":1700917852,"version":1}