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

文章首发于公众号:Keegan小钢


前一篇文章已经对 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 最容易理解,我们来看看其代码实现:

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;
}

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

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

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

csharp 复制代码
// 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 定义了其类型,还定义了一些函数,如下所示:

php 复制代码
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 中有定义了以下函数:

scss 复制代码
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 还缺少了关键的路由合约。

相关推荐
徐征63920 小时前
邮件安全防护体系技术拆解:从入口过滤到大模型研判的检测流水线 北京方向标
web3·生活
米小虾1 天前
币安 63 亿美元被盗:被攻破的不是私钥,是那台负责签名的机器
黑客·web3
OxYGC3 天前
[Web3] 的⛽ Gas 革命:五大协议群组如何终结“Gas 痛点
web3·1024程序员节·layer2·gas·erc-4337
OxYGC3 天前
[Web3] 去中心化基石 - BIP铁三角(BIP32/39/44)
web3·1024程序员节·钱包·bip32·bip44
磐链科技3 天前
IM聊天Web3钱包定制开发指南:从业务场景梳理、系统功能规划到产品迭代与生态扩展
web3
limit00755 天前
三维 WebGIS 工具选型:加载、样式、分析、交付,一套界面能做完什么
android·前端·web3·安卓·webgl
木西5 天前
dAppOS 意图执行网络核心技术剖析:从智能账户、生息资产金库到 OMS 乐观质押仲裁的 Solidity 0.8.28 实现
web3·智能合约·solidity
limit00755 天前
公开 WMTS / XYZ 瓦片接入 Cesium:6 类常见问题与排查顺序
chrome·低代码·arcgis·web3·旅游
limit00756 天前
三维模型悬空与切片偏移的排查顺序
低代码·3d·arcgis·web3·安卓·webgl
磐链科技9 天前
元链科技:随着区块链技术持续发展,Web3逐渐成为数字资产管理、去中心化应用和数字经济创新的重要技术方向
科技·web3·区块链