剖析DeFi交易产品之UniswapV4:创建池子
本文首发于公众号:Keegan小钢
创建池子的底层函数是 PoolManager 合约的 initialize
函数,其代码实现并不复杂,如下所示:
function initialize(PoolKey memory key, uint160 sqrtPriceX96, bytes calldata hookData)externaloverrideonlyByLockerreturns (int24 tick)
{if (key.fee.isStaticFeeTooLarge()) revert FeeTooLarge();// see TickBitmap.sol for overflow conditions that can arise from tick spacing being too largeif (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);
}
不过,里面有很多信息,我们需要一一拆解才能理解。
先来看入参,有三个:key
、sqrtPriceX96
、hookData
。key
指定了一个池子的唯一组成,sqrtPriceX96
是要初始化的根号价格,hookData
是需要传给 hooks 合约的初始化数据。
关于池子的唯一组成,前文我们已经讲过,PoolKey
包含了五个字段:
currency0
:token0currency1
:token1fee
:费率tickSpacing
:tick 间隔hooks
:hooks 地址
currency0
和 currency1
和以前版本的 token0
和 token1
一样,是经过排序的,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的情况下,移出顶部的lockerLockers.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 priceuint160 sqrtPriceX96;// the current tickint24 tick;uint24 protocolFees;uint24 hookFees;// used for the swap fee, either static at initialize or dynamic via hookuint24 swapFee;
}
可看到,与 UniswapV3Pool 的 Slot0
相比,没有了预言机相关的状态数据。另外,关于费用的字段总共有三个:protocolFees
、 hookFees
和 swapFee
。
pools[id].initialize()
函数的实现是在 Pool
库合约里,其代码逻辑很简单,就是初始化了 slot0
,代码如下:
function initialize(State storage self, uint160 sqrtPriceX96, uint24 protocolFees, uint24 hookFees, uint24 swapFee)internalreturns (int24 tick)
{//当前状态下的根号价格不为0,说明已经初始化过了if (self.slot0.sqrtPriceX96 != 0) revert PoolAlreadyInitialized();//根据根号价格算出ticktick = TickMath.getTickAtSqrtRatio(sqrtPriceX96);//初始化slot0self.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
钩子函数了。最后发送事件,整个创建池子的流程就完成了。
相关文章:

剖析DeFi交易产品之UniswapV4:创建池子
本文首发于公众号:Keegan小钢 创建池子的底层函数是 PoolManager 合约的 initialize 函数,其代码实现并不复杂,如下所示: function initialize(PoolKey memory key, uint160 sqrtPriceX96, bytes calldata hookData)externalover…...

速盾:cdn内容分发服务有哪些优势?
CDN(Content Delivery Network)是指内容分发网络,是一种将网络内容分发到全球各个地点的技术和架构。在现代互联网架构中,CDN已经变得非常重要。CDN通过将内容分发到靠近用户的服务器上,提供高速、高效的服务。下面是C…...

如何利用React和Python构建强大的网络爬虫应用
如何利用React和Python构建强大的网络爬虫应用 引言: 网络爬虫是一种自动化程序,用于通过互联网抓取网页数据。随着互联网的不断发展和数据的爆炸式增长,网络爬虫越来越受欢迎。本文将介绍如何利用React和Python这两种流行的技术,…...

炎黄数智人:招商局集团推出AI数字员工“招小影”
引言 在全球数字化浪潮的推动下,招商局集团开启了一项具有里程碑意义的项目。招商局集团将引入AI数字员工“招小影”,这一举措不仅彰显了招商局集团在智能化转型方面的坚定决心,也为企业管理模式的创新注入了新的活力。 “招小影”是一款集成…...

【开发篇】明明配置跨域声明,为什么却仍可以发送HTTP请求
一、问题 在SpringBoot项目中,明确指定仅允许指定网站跨域访问: 为什么开发人员却仍旧可以通过HTTP工具调用接口? 二、为什么 在回答这个问题之前,我们首先要了解一下什么是CORS! 1、什么是CORS CORS的全称为跨域资源…...

单片机中有FLASH为啥还需要EEROM?
在开始前刚好我有一些资料,是我根据网友给的问题精心整理了一份「单片机的资料从专业入门到高级教程」, 点个关注在评论区回复“888”之后私信回复“888”,全部无偿共享给大家!!! 一是EEPROM操作简单&…...

Qt的源码目录集合(V5.12.12版本)
目录 1.QObject实现源码 2.qml中的ListModel实现源码 3.qml中的JS运行时的环境和数据类型源码 1.QObject实现源码 .\Qt\Qt5.12.12\5.12.12\Src\qtbase\src\corelib\kernel\qobject.h .\Qt\Qt5.12.12\5.12.12\Src\qtbase\src\corelib\kernel\qobject.cpp .\Qt\Qt5.12.12\5…...

记因hive配置文件参数运用不当导致 sqoop MySQL导入数据到hive 失败的案例
sqoop MySQL导入数据到hive报错 ERROR tool.ImportTool: Encountered IOException running import job: java.io.IOException: Hive exited with status 64 报错解释: 这个错误表明Sqoop在尝试导入数据到Hive时遇到了问题,导致Hive进程异常退出。状态码…...

自动化邮件通知:批处理脚本的通讯增强
自动化邮件通知:批处理脚本的通讯增强 引言 批处理脚本在自动化任务中扮演着重要角色,无论是在系统管理、数据处理还是日常任务调度中。然而,批处理脚本的自动化能力可以通过集成邮件通知功能得到显著增强。当脚本执行完毕或在执行过程中遇…...

236、二叉树的最近公共祖先
前提: 所有 Node.val 互不相同 。p ! qp 和 q 均存在于给定的二叉树中。 代码如下: class Solution { public:TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {if (root q || root p || root NULL) return root;TreeN…...

WebStorm 2024 for Mac JavaScript前端开发工具
Mac分享吧 文章目录 效果一、下载软件二、开始安装1、双击运行软件(适合自己的M芯片版或Intel芯片版),将其从左侧拖入右侧文件夹中,等待安装完毕2、应用程序显示软件图标,表示安装成功3、打开访达,点击【文…...

【Redis7】零基础篇
1 课程概述 2 Redis入门概述 2.1 是什么 Redis是基于内存的KV键值对内存数据库 Redis:Remote Dictionary Server(远程字典服务)是完全开源的,使用ANSIC语言编写遵守BSD协议,是一个高性能的Key-Value数据库提供了丰富的数据结构,…...

[ROS 系列学习教程] 建模与仿真 - 使用 ros_control 控制差速轮式机器人
ROS 系列学习教程(总目录) 本文目录 一、差速轮式机器人二、差速驱动机器人运动学模型三、对外接口3.1 输入接口3.2 输出接口 四、控制器参数五、配置控制器参数六、编写硬件抽象接口七、控制机器人移动八、源码 ros_control 提供了多种控制器,其中 diff_drive_cont…...

Ubuntu22.04使用Systemd设置ROS 2开机自启动遇到的问题
在查找网上的各种开机自启动资料配置好开机自启动后,使用ros2 topic list不能显示话题。 1、问题解决:用户问题与domenID问题2、ROS2开机自启动服务教程3、多个ROS2开机自启动服务教程 1、问题解决:用户问题与domenID问题 在root用户下能看到…...

AI安全研究滞后?清华专家团来支招
在21世纪的科技浪潮中,人工智能(AI)无疑是最为耀眼的一抹亮色。随着技术的不断突破,AI正以前所未有的速度融入我们的日常生活,重塑着社会、经济乃至人类文明的面貌。然而,在这股汹涌澎湃的发展洪流中&#…...

12寸FAB 信息部内外工作职责的一些划分构思
FAB的信息部,也常被称为IT部门或信息化部门,承担着确保整个制造工厂的信息技术系统高效、安全运行的职责。以下是 一、FAB信息部的一些关键部门职责: 1. 战略规划:制定和实施信息技术战略,以支持FAB的长期业务目标和增…...

css做旋转星球可举一反三
<!DOCTYPE html> <html lang"en"><head> <meta charset"UTF-8" /> <title>旋转的星球</title> <style type"text/css">.box {/*position: relative;*/position: absolute;width: 139px;height: 139p…...

AcWing 1256:扩展二叉树
【题目来源】https://www.acwing.com/problem/content/1258/【题目描述】 由于先序、中序和后序序列中的任一个都不能唯一确定一棵二叉树,所以对二叉树做如下处理,将二叉树的空结点用 补齐,如图所示。 我们把这样处理后的二叉树称为原二叉树…...

三维家:SaaS的IT规模化降本之道|OceanBase 《DB大咖说》(十一)
OceanBase《DB大咖说》第 11 期,我们邀请到了三维家的技术总监庄建超,来分享他对数据库技术的理解,以及典型 SaaS 场景在数据库如何实现规模化降本的经验与体会。 庄建超,身为三维家的技术总监,独挑大梁,负…...

ai智能语音机器人是如何影响客户体验的?电销机器人部署
随着人工智能技术的进步,越来越多的企业在寻求如何将人工智能技术融合到现有的商业模式上,进而实现自动化、智能化。在通信行业大量使用智能语音机器人、聊天机器人、客服机器人时,它能和“客户体验”并驾齐驱吗,还是可以让客户体…...

vue3使用v-html实现文本关键词变色
首先看应用场景 这有一段文本内容,是项目的简介,想要实现将文本中的关键词进行变色处理 有如下关键词 实现思路 遍历文本内容,找到关键词,并使用某种方法更改其字体样式。经过搜寻资料决定采用v-html实现,但是v-h…...

C#面:举列 a=10,b=15,在不用第三方变量的前提下,把a,b的值互换
要在不使用第三方变量的前提下交换a和b的值,可以使用异或运算。异或运算的特性是,对于两个相同的数进行异或运算,结果为0;对于任意数与0进行异或运算,结果为该数本身。因此,可以通过多次异或运算来实现变量…...

编写动态库
1.创建库.c .h文件 2.编写Makefile文件 3.make之后形成.so文件 4.make output,形成mylib 5.把mylib拷贝到test里面 mv mylib /test 6.编译 gcc main.c -I mylib/include -L mylib/lib -lmymethod形成a.out 但是直接执行会出现以下问题 很显然没有找到动态库 7.解决加载找不…...

记一次阿里云服务器java应用无法响应且无法远程连接的问题排查
问题表现 java服务无响应,无法远程链接到服务器。 今天中午12点多,应用直接崩溃。后续进入到服务器,发现java进程都不在了, 排查过程 先安装atop工具 安装、配置并使用atop监控工具 等下次再出现时看相关时间点日志ÿ…...

雷池WAF+Modsecurity安装防护及系统加固
君衍. 一、雷池WAF1、什么是雷池2、什么是WAF3、雷池的功能4、WAF部署架构5、整体检测流程 二、雷池WAF环境依赖1、查看本地CPU架构2、Docker安装2.1 卸载旧版本2.2 安装yum-utils工具包2.3 设置镜像仓库2.4 安装docker2.5 启动docker并查看版本 3、Docker Compose安装3.1 卸载…...

【Python】已解决:SyntaxError: positional argument follows keyword argument
文章目录 一、分析问题背景二、可能出错的原因三、错误代码示例四、正确代码示例五、注意事项 已解决:SyntaxError: positional argument follows keyword argument 一、分析问题背景 在Python编程中,当我们在调用函数时混合使用位置参数(p…...

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

利用深度学习模型进行语音障碍自动评估
语音的产生涉及器官的复杂协调,因此,语音包含了有关身体各个方面的信息,从认知状态和心理状态到呼吸条件。近十年来,研究者致力于发现和利用语音生物标志物——即与特定疾病相关的语音特征,用于诊断。随着人工智能&…...

TP8 JS(html2canvas) 把DIV内容生成二维码并与背景图、文字组合生成分享海报
方法一:前端JS生成(推荐) 注意: 1.这个网页只能截图图片效果代码,其它任何html效果都不能有,不然截图就不准确 2.如果要生成的图片DIV内容中引用了第三个方的图片,就是不使用同一个域名下的图片,需要把后…...

计算机科学中的接口(Interface)介绍
计算机科学中的接口(Interface)介绍 计算机科学中,接口是一个广泛的概念,在不同上下文中有不同含义: 1.任何两电路或设备间的连接电路,用于连接CPU与内存、CPU与外设之间。这是一个重要的硬件层面的接口概…...