剖析DeFi交易产品之UniswapV4:创建池子

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


创建池子的底层函数是 PoolManager 合约的 initialize 函数,其代码实现并不复杂,如下所示:

function initialize(PoolKey memory key, uint160 sqrtPriceX96, bytes calldata hookData)
    external
    override
    onlyByLocker
    returns (int24 tick)
{
    if (key.fee.isStaticFeeTooLarge()) revert FeeTooLarge();

    // see TickBitmap.sol for overflow conditions that can arise from tick spacing being too large
    if (key.tickSpacing > MAX_TICK_SPACING) revert TickSpacingTooLarge();
    if (key.tickSpacing < MIN_TICK_SPACING) revert TickSpacingTooSmall();
    if (key.currency0 >= key.currency1) revert CurrenciesOutOfOrderOrEqual();
    if (!key.hooks.isValidHookAddress(key.fee)) revert Hooks.HookAddressNotValid(address(key.hooks));

    if (key.hooks.shouldCallBeforeInitialize()) {
        if (key.hooks.beforeInitialize(msg.sender, key, sqrtPriceX96, hookData) != IHooks.beforeInitialize.selector)
        {
            revert Hooks.InvalidHookResponse();
        }
    }

    PoolId id = key.toId();

    uint24 swapFee = key.fee.isDynamicFee() ? _fetchDynamicSwapFee(key) : key.fee.getStaticFee();

    tick = pools[id].initialize(sqrtPriceX96, _fetchProtocolFees(key), _fetchHookFees(key), swapFee);

    if (key.hooks.shouldCallAfterInitialize()) {
        if (
            key.hooks.afterInitialize(msg.sender, key, sqrtPriceX96, tick, hookData)
                != IHooks.afterInitialize.selector
        ) {
            revert Hooks.InvalidHookResponse();
        }
    }

    // On intitalize we emit the key's fee, which tells us all fee settings a pool can have: either a static swap fee or dynamic swap fee and if the hook has enabled swap or withdraw fees.
    emit Initialize(id, key.currency0, key.currency1, key.fee, key.tickSpacing, key.hooks);
}

不过,里面有很多信息,我们需要一一拆解才能理解。

先来看入参,有三个:keysqrtPriceX96hookDatakey 指定了一个池子的唯一组成,sqrtPriceX96 是要初始化的根号价格,hookData 是需要传给 hooks 合约的初始化数据。

关于池子的唯一组成,前文我们已经讲过,PoolKey 包含了五个字段:

  • currency0:token0
  • currency1:token1
  • fee:费率
  • tickSpacing:tick 间隔
  • hooks:hooks 地址

currency0currency1 和以前版本的 token0token1 一样,是经过排序的,currency0 为数值较小的代币,currency1 则为数值较大的代币。tickSpacing 和 UniswapV3 的一样,就不再解释了。hooks 是自定义的地址,具体如何实现后面再细说。

fee 则和之前的版本不一样了。UniswapV3 的 fee 只指定了固定的交易费率,但 UniswapV4 的 fee 其实还包含了动态费用、hook 交易费用、hook 提现费用等标志。fee 总共 24 位(bit),前 4 位用来作为不同的标志位,具体解析在 FeeLibrary 里实现,以下是其代码实现:

// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.20;

library FeeLibrary {
    // 静态费率掩码
    uint24 public constant STATIC_FEE_MASK = 0x0FFFFF;
    // 支持动态费用的标志位
    uint24 public constant DYNAMIC_FEE_FLAG = 0x800000; // 1000
    // 支持hook交易费用的标志位
    uint24 public constant HOOK_SWAP_FEE_FLAG = 0x400000; // 0100
    // 支持hook提现费用的标志位
    uint24 public constant HOOK_WITHDRAW_FEE_FLAG = 0x200000; // 0010

    // 是否支持动态费用
    function isDynamicFee(uint24 self) internal pure returns (bool) {
        return self & DYNAMIC_FEE_FLAG != 0;
    }
    // 是否支持hook交易费用
    function hasHookSwapFee(uint24 self) internal pure returns (bool) {
        return self & HOOK_SWAP_FEE_FLAG != 0;
    }
    // 是否支持hook提现费用
    function hasHookWithdrawFee(uint24 self) internal pure returns (bool) {
        return self & HOOK_WITHDRAW_FEE_FLAG != 0;
    }
    // 静态费率是否超过最大值
    function isStaticFeeTooLarge(uint24 self) internal pure returns (bool) {
        return self & STATIC_FEE_MASK >= 1000000;
    }
    // 获取出静态手续费率
    function getStaticFee(uint24 self) internal pure returns (uint24) {
        return self & STATIC_FEE_MASK;
    }
}

静态费率最大值为 1000000,表示 100% 费用。那么要设置 0.3% 的费率的话那就是 3000,这个精度和 UniswapV3 是一致的。

那如果是要支持静态费率,就假设静态费率为 0.3%,同时又要支持 hook 交易费和提现费,则需要同时设置这两个标志位,那 fee 字段用 16 进制表示的值为 0xC01778。其二进制表示为:11000000000101110111000,前面两个 1 就是两个标志位,后面的 101110111000 其实就是十进制数 3000 的二进制数。

另外,UniswapV3 的费率只能在指定支持的几个费率中选择一个,而 UniswapV4 取消了这个限制,费率完全放开了,由池子的创建者自己去决定要设置多少费率。

回到 initialize 函数,函数声明里还有一个函数修饰器 onlyByLocker,这也是需要展开说明的一个地方。我们先来看这个函数修饰器的代码:

modifier onlyByLocker() {
    address locker = Lockers.getCurrentLocker();
    if (msg.sender != locker) revert LockedBy(locker);
    _;
}

它要求调用者需是当前的 locker。要成为 locker,需要调用 PoolManager 合约的 lock() 函数。以下是 lock() 函数的实现:

function lock(bytes calldata data) external override returns (bytes memory result) {
    //把调用者添加到locker队列里
    Lockers.push(msg.sender);

    //需在这个回调函数里完成所有事情,包括支付等操作
    result = ILockCallback(msg.sender).lockAcquired(data);

    if (Lockers.length() == 1) {//只有一个locker的情况下,做清理操作
        if (Lockers.nonzeroDeltaCount() != 0) revert CurrencyNotSettled();
        Lockers.clear();
    } else {//不止一个locker的情况下,移出顶部的locker
        Lockers.pop();
    }
}

其中,Lockers 是封装了锁定操作的库合约,push() 函数会把当前调用者添加到锁定者队列里,具体实现用到了 EIP-1153 所引入的 tstore 瞬态存储操作码。具体原理不在这里展开。

而下一步是调用了 msg.sender 的回调函数 lockAcquired(),这一步非常关键,透露出很多信息。首先,这说明了,调用者需是一个合约才行,而不能是一个 EOA 账户。然后,调用者需实现 ILockCallback 接口,该接口只定义了一个函数,就是 lockAcquired() 函数。最后,调用者合约需在 lockAcquired() 函数里实现所有事情,包括完成支付和各种不同的交易场景,其实也包括了调用 initialize 函数。

我的理解,lock() 函数调用者应该是一个路由合约,或不同功能模块用不同的合约实现,比如可以加一个工厂合约用于完成创建池子的操作,但目前 UniswapV4 还没看到关于路由合约或工厂合约的实现,所以具体逻辑不得而知。

总而言之,到了这里,我们就已经知道了,创建池子的调用者需是一个实现了 ILockCallback 接口的合约,先调用 lock() 函数成为 locker,再通过 lockAcquired() 回调函数调其 initialize 函数来完成初始化池子。

回到 initialize 函数的具体实现。前面是一些基本的校验,我们摘出来看一下:

// 静态费率不能超过最大值
if (key.fee.isStaticFeeTooLarge()) revert FeeTooLarge();
// tickSpacing需在限定的有效范围内
if (key.tickSpacing > MAX_TICK_SPACING) revert TickSpacingTooLarge();
if (key.tickSpacing < MIN_TICK_SPACING) revert TickSpacingTooSmall();
// currency0需小于currency1
if (key.currency0 >= key.currency1) revert CurrenciesOutOfOrderOrEqual();
// hooks地址需是符合条件的有效地址
if (!key.hooks.isValidHookAddress(key.fee)) revert Hooks.HookAddressNotValid(address(key.hooks));

接着,判断是否需要调用 beforeInitialize 的钩子函数,如下:

if (key.hooks.shouldCallBeforeInitialize()) {
    if (key.hooks.beforeInitialize(msg.sender, key, sqrtPriceX96, hookData) != IHooks.beforeInitialize.selector)
    {
        revert Hooks.InvalidHookResponse();
    }
}

钩子函数需返回该函数的 selector

之后的三行代码实现初始化逻辑,代码如下:

// 把key转为id
PoolId id = key.toId();
// 读取出交易费率
uint24 swapFee = key.fee.isDynamicFee() ? _fetchDynamicSwapFee(key) : key.fee.getStaticFee();
// 执行实际的初始化操作
tick = pools[id].initialize(sqrtPriceX96, _fetchProtocolFees(key), _fetchHookFees(key), swapFee);

这里面有好几个跟费用相关的函数,有必要说明一下。

isDynamicFee() 就是前面所说的 FeeLibrary 库合约的函数,判断是否设置了支持动态费用的标志位。如果不支持,则通过 getStaticFee() 读取出静态费率;如果支持动态费用,则通过 _fetchDynamicSwapFee() 获取费率。 _fetchDynamicSwapFee() 函数是在抽象合约 Fees 里实现的,其实现非常简单,就两行代码,如下所示:

function _fetchDynamicSwapFee(PoolKey memory key) internal view returns (uint24 dynamicSwapFee) {
    dynamicSwapFee = IDynamicFeeManager(address(key.hooks)).getFee(msg.sender, key);
    if (dynamicSwapFee >= MAX_SWAP_FEE) revert FeeTooLarge();
}

可见,其实是调用了 hooks 合约的 getFee() 函数。即是说,要支持动态费用,则 hooks 合约需要实现 IDynamicFeeManager 接口的 getFee() 函数。

_fetchHookFees() 函数也类似,需要 hooks 合约实现 IHookFeeManager 接口的 getHookFees() 函数。不过 getHookFees() 的返回值里其实是由两个费用组合而成的,一个是交易费,一个是提现费。返回值是 24 位,前 12 位是交易费,后 12 位是提现费。

_fetchProtocolFees() 函数则是用于获取协议费,这就和 hooks 合约没有关系了,是由一个实现了 IProtocolFeeController 接口的合约进行管理的。只有合约 owner 可以设置这个合约地址。目前 UniswapV4 还没有提供关于该合约的实现,短期内应该也不会开启收取协议费。

最后,通过调用 pools[id].initialize() 函数完成内部的初始化工作。这里的关键就是 pools 状态变量,新建的池子状态最终其实也是存储在了 pools 里。它是一个 mapping 类型的变量,如下:

mapping(PoolId id => Pool.State) public pools;

其 value 存的是一个 Pool.State 对象,这是一个定义在 Pool 库合约里的结构体,具体包含了如下数据:

struct State {
    Slot0 slot0;
    uint256 feeGrowthGlobal0X128;
    uint256 feeGrowthGlobal1X128;
    uint128 liquidity;
    mapping(int24 => TickInfo) ticks;
    mapping(int16 => uint256) tickBitmap;
    mapping(bytes32 => Position.Info) positions;
}

如果和 UniswapV3 对比就会发现,其实就是将 UniswapV3Pool 里的大部分状态变量移到了 State 里。另外,slot0 的字段与 UniswapV3Pool 的有所不同,以下是其具体字段:

struct Slot0 {
    // the current price
    uint160 sqrtPriceX96;
    // the current tick
    int24 tick;
    uint24 protocolFees;
    uint24 hookFees;
    // used for the swap fee, either static at initialize or dynamic via hook
    uint24 swapFee;
}

可看到,与 UniswapV3Pool 的 Slot0 相比,没有了预言机相关的状态数据。另外,关于费用的字段总共有三个:protocolFeeshookFeesswapFee

pools[id].initialize() 函数的实现是在 Pool 库合约里,其代码逻辑很简单,就是初始化了 slot0,代码如下:

function initialize(State storage self, uint160 sqrtPriceX96, uint24 protocolFees, uint24 hookFees, uint24 swapFee)
    internal
    returns (int24 tick)
{
    //当前状态下的根号价格不为0,说明已经初始化过了
    if (self.slot0.sqrtPriceX96 != 0) revert PoolAlreadyInitialized();
    //根据根号价格算出tick
    tick = TickMath.getTickAtSqrtRatio(sqrtPriceX96);
    //初始化slot0
    self.slot0 = Slot0({
        sqrtPriceX96: sqrtPriceX96,
        tick: tick,
        protocolFees: protocolFees,
        hookFees: hookFees,
        swapFee: swapFee
    });
}

再回到 PoolManager 合约自身的 initialize() 函数,还剩下最后一段代码如下:

if (key.hooks.shouldCallAfterInitialize()) {
    if (
        key.hooks.afterInitialize(msg.sender, key, sqrtPriceX96, tick, hookData)
            != IHooks.afterInitialize.selector
    ) {
        revert Hooks.InvalidHookResponse();
    }
}
//发送事件
emit Initialize(id, key.currency0, key.currency1, key.fee, key.tickSpacing, key.hooks);

完成了 PoolManager 自身的初始化逻辑之后,就是判断是否需要再调用 hooks 合约的 afterInitialize 钩子函数了。最后发送事件,整个创建池子的流程就完成了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/765461.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

MySQL:设计数据库与操作

设计数据库 1. 数据建模1.1 概念模型1.2 逻辑模型1.3 实体模型主键外键外键约束 2. 标准化2.1 第一范式2.2 链接表2.3 第二范式2.4 第三范式 3. 数据库模型修改3.1 模型的正向工程3.2 同步数据库模型3.3 模型的逆向工程3.4 实际应用建议 4. 数据库实体模型4.1 创建和删除数据库…

10.8K star!史上最强Web应用防火墙雷池WAF

长亭雷池SafeLine是长亭科技耗时近 10 年倾情打造的WAF(Web Application Firewall)&#xff0c; 一款敢打出口号 “不让黑客越雷池一步” 的 WAF&#xff0c;愿称之为史上最强的一款Web应用防火墙&#xff0c;足够简单、足够好用、足够强的免费且开源的 WAF&#xff0c;基于业…

leetcode-20-回溯-切割、子集

一、[131]分割回文串 给定一个字符串 s&#xff0c;将 s 分割成一些子串&#xff0c;使每个子串都是回文串。 返回 s 所有可能的分割方案。 示例: 输入: "aab" 输出: [ ["aa","b"], ["a","a","b"] ] 分析&…

JAVA连接FastGPT实现流式请求SSE效果

FastGPT 是一个基于 LLM 大语言模型的知识库问答系统&#xff0c;提供开箱即用的数据处理、模型调用等能力。同时可以通过 Flow 可视化进行工作流编排&#xff0c;从而实现复杂的问答场景&#xff01; 一、先看效果 真正实流式请求&#xff0c;SSE效果&#xff0c;SSE解释&am…

一切为了安全丨2024中国应急(消防)品牌巡展武汉站成功召开!

消防品牌巡展武汉站 6月28日&#xff0c;由中国安全产业协会指导&#xff0c;中国安全产业协会应急创新分会、应急救援产业网联合主办&#xff0c;湖北消防协会协办的“一切为了安全”2024年中国应急(消防)品牌巡展-武汉站成功举办。该巡展旨在展示中国应急&#xff08;消防&am…

Python基础002

Python数据类型 1、字符串&#xff08;str&#xff09; str3 """I miss you so much""" print("str3 ", str3,type(str3)) str3 I miss you so much <class str>2、整数&#xff08;int&#xff09; str1 55 print(&quo…

【面试题】TLS和SSL协议的区别

TLS&#xff08;Transport Layer Security&#xff09;和SSL&#xff08;Secure Sockets Layer&#xff09;协议都是用于在网络上建立安全通信连接的协议&#xff0c;但它们在多个方面存在区别。以下是TLS和SSL协议之间区别的详细分析&#xff1a; 1. 发展历程与标准化 SSL&a…

如何找BMS算法、BMS软件的实习

之前一直忙&#xff0c;好久没有更新了&#xff0c;今天就来写一篇文章来介绍如何找BMS方向的实习&#xff0c;以及需要具备哪些条件&#xff0c;我的实习经历都是在读研阶段找的&#xff0c;读研期间两段的实习经历再加上最高影响因子9.4分的论文&#xff0c;我的秋招可以说是…

分子AI预测赛Task2笔记

下面所述比较官方的内容都来自官方文档 ‍‌⁠‌‍​​​‌​​⁠​​​​​&#xfeff;​​​&#xfeff;‍‬​​‍⁠‍‍​​‬​&#xfeff;‌​​​‌‍‬​​​​​​‍‌Task2&#xff1a;赛题深入解析 - 飞书云文档 (feishu.cn) 赛题背景 强调了人工智能在科研领域&…

探囊取物之多形式注册页面(基于BootStrap4)

基于BootStrap4的注册页面&#xff0c;支持手机验证码注册、账号密码注册 低配置云服务器&#xff0c;首次加载速度较慢&#xff0c;请耐心等候&#xff1b;演练页面可点击查看源码 预览页面&#xff1a;http://www.daelui.com/#/tigerlair/saas/preview/ly4gax38ub9j 演练页…

晚上睡觉要不要关路由器?一语中的

前言 前几天小白去了一个朋友家&#xff0c;有朋友说&#xff1a;路由器不关机的话会影响睡眠吗&#xff1f; 这个影响睡眠嘛&#xff0c;确实是会的。毕竟一时冲浪一时爽&#xff0c;一直冲浪一直爽……刷剧刷抖音刷到根本停不下来&#xff0c;肯定影响睡眠。 所以晚上睡觉要…

MQTT协议详述

MQTT 概述 消息队列遥测传输&#xff08;英语&#xff1a;Message Queuing Telemetry Transport&#xff0c;缩写&#xff1a;MQTT&#xff09;&#xff0c;是基于发布&#xff08;Publish&#xff09;/订阅&#xff08;Subscribe&#xff09;范式的消息协议&#xff0c;位于…

BurpSuite抓IOS设备HTTPS流量

一、简述&#xff1a; Burp 这个工具做过 web 安全的人都应该用过&#xff0c;是个非常强大的抓包工具。在 PC 的浏览器上直接配置代理就行了&#xff0c;本篇文章就来介绍一下如何用 Burp 抓 IOS 设备上的流量&#xff0c;很多文章都介绍过怎么抓包&#xff0c;但是很多坑都没…

Linux驱动开发实战宝典:设备模型、模块编程、I2C/SPI/USB外设精讲

摘要: 本文将带你走进 Linux 驱动开发的世界,从设备驱动模型、内核模块开发基础开始,逐步深入 I2C、SPI、USB 等常用外设的驱动编写,结合实际案例,助你掌握 Linux 驱动开发技能。 关键词: Linux 驱动,设备驱动模型,内核模块,I2C,SPI,USB 一、Linux 设备驱动模型 Li…

java反射和注解

反射 获取class对象的三种方法 ①&#xff1a;Class.forName("全类名"); ②&#xff1a;类名.class ③&#xff1a;对象.getclass(); 代码样例 package com.ithema;public class Main {public static void main(String[] args) throws ClassNotFoundException {//第…

【JavaEE精炼宝库】多线程进阶(2)synchronized原理、JUC类——深度理解多线程编程

一、synchronized 原理 1.1 基本特点&#xff1a; 结合上面的锁策略&#xff0c;我们就可以总结出&#xff0c;synchronized 具有以下特性(只考虑 JDK 1.8)&#xff1a; 开始时是乐观锁&#xff0c;如果锁冲突频繁&#xff0c;就转换为悲观锁。 开始是轻量级锁实现&#xff…

QT+winodow 代码适配调试总结(二)

已经好多年了&#xff0c; linux环境下不同版本的QT程序开发和部署&#xff0c;突然需要适配window环境程序调试&#xff0c;一堆大坑&#xff0c;还真是一个艰巨的任务&#xff0c;可是kpi下的任务计划&#xff0c;开始吧&#xff01;&#xff01; 1、首先我们自定义的动态库…

【STM32HAL库学习】通信方式:USART、IIC、SPI

通信的目的&#xff1a;将一个设备的数据传送到另一个设备&#xff0c;扩展硬件系统 通信接口区别 名称引脚双工时钟电平设备USARTTX、RX全双工异步单端点对点I2CSCL、SDA半双工同步单端多设备SPISCLK、MOSI、MISO、CS全双工同步单端多设备CANCAN_H、CAN_L半双工异步差分多设…

【数值计算库-超长笔记】Python-Mpmath库:高精度数值计算

原文链接&#xff1a;https://www.cnblogs.com/aksoam/p/18279394 更多精彩&#xff0c;关注博客园主页&#xff0c;不断学习&#xff01;不断进步&#xff01; 我的主页 csdn很少看私信&#xff0c;有事请b站私信 博客园主页-发文字笔记-常用 有限元鹰的主页 内容&#xf…

类与对象完结

1.匿名类 先看一个例子 class A { public:A(int a 0):_a(a){cout << "A(int a)" << endl;}~A(){cout << "~A()" << endl;}void Print(){cout << _a << endl;} private:int _a; };int main() {A aa1(10);aa1.Print(…