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

【Axure高保真原型】引导弹窗
今天和大家中分享引导弹窗的原型模板,载入页面后,会显示引导弹窗,适用于引导用户使用页面,点击完成后,会显示下一个引导弹窗,直至最后一个引导弹窗完成后进入首页。具体效果可以点击下方视频观看或打开下方…...

未来机器人的大脑:如何用神经网络模拟器实现更智能的决策?
编辑:陈萍萍的公主一点人工一点智能 未来机器人的大脑:如何用神经网络模拟器实现更智能的决策?RWM通过双自回归机制有效解决了复合误差、部分可观测性和随机动力学等关键挑战,在不依赖领域特定归纳偏见的条件下实现了卓越的预测准…...

MPNet:旋转机械轻量化故障诊断模型详解python代码复现
目录 一、问题背景与挑战 二、MPNet核心架构 2.1 多分支特征融合模块(MBFM) 2.2 残差注意力金字塔模块(RAPM) 2.2.1 空间金字塔注意力(SPA) 2.2.2 金字塔残差块(PRBlock) 2.3 分类器设计 三、关键技术突破 3.1 多尺度特征融合 3.2 轻量化设计策略 3.3 抗噪声…...

简易版抽奖活动的设计技术方案
1.前言 本技术方案旨在设计一套完整且可靠的抽奖活动逻辑,确保抽奖活动能够公平、公正、公开地进行,同时满足高并发访问、数据安全存储与高效处理等需求,为用户提供流畅的抽奖体验,助力业务顺利开展。本方案将涵盖抽奖活动的整体架构设计、核心流程逻辑、关键功能实现以及…...

以下是对华为 HarmonyOS NETX 5属性动画(ArkTS)文档的结构化整理,通过层级标题、表格和代码块提升可读性:
一、属性动画概述NETX 作用:实现组件通用属性的渐变过渡效果,提升用户体验。支持属性:width、height、backgroundColor、opacity、scale、rotate、translate等。注意事项: 布局类属性(如宽高)变化时&#…...

边缘计算医疗风险自查APP开发方案
核心目标:在便携设备(智能手表/家用检测仪)部署轻量化疾病预测模型,实现低延迟、隐私安全的实时健康风险评估。 一、技术架构设计 #mermaid-svg-iuNaeeLK2YoFKfao {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg…...

STM32标准库-DMA直接存储器存取
文章目录 一、DMA1.1简介1.2存储器映像1.3DMA框图1.4DMA基本结构1.5DMA请求1.6数据宽度与对齐1.7数据转运DMA1.8ADC扫描模式DMA 二、数据转运DMA2.1接线图2.2代码2.3相关API 一、DMA 1.1简介 DMA(Direct Memory Access)直接存储器存取 DMA可以提供外设…...
【ROS】Nav2源码之nav2_behavior_tree-行为树节点列表
1、行为树节点分类 在 Nav2(Navigation2)的行为树框架中,行为树节点插件按照功能分为 Action(动作节点)、Condition(条件节点)、Control(控制节点) 和 Decorator(装饰节点) 四类。 1.1 动作节点 Action 执行具体的机器人操作或任务,直接与硬件、传感器或外部系统…...

Java-41 深入浅出 Spring - 声明式事务的支持 事务配置 XML模式 XML+注解模式
点一下关注吧!!!非常感谢!!持续更新!!! 🚀 AI篇持续更新中!(长期更新) 目前2025年06月05日更新到: AI炼丹日志-28 - Aud…...
Spring AI 入门:Java 开发者的生成式 AI 实践之路
一、Spring AI 简介 在人工智能技术快速迭代的今天,Spring AI 作为 Spring 生态系统的新生力量,正在成为 Java 开发者拥抱生成式 AI 的最佳选择。该框架通过模块化设计实现了与主流 AI 服务(如 OpenAI、Anthropic)的无缝对接&…...