剖析DeFi交易产品之UniswapV4:Swap

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


Swap 可分为两种场景:单池交易跨池交易。在 PoolManager 合约里,要完成交易流程,会涉及到 lock()swap()settle()take() 四个函数。单池交易时只需要调一次 swap() 函数,而跨池交易时则需要多次调用 swap() 函数来完成。

我们先来聊聊单池交易如何实现,以下是流程图:

image.png

第一步,和其他操作一样,先执行 lock(),锁定住接下来的系列操作。

第二步,就是在 lockAcquired() 回调函数里执行 swap() 函数。这一步执行完之后,记账系统中会记录用户欠池子的资产数量,即用户需要支付的代币;以及池子欠用户的资产数量,即用户此次交易可得的代币。

第三步,执行 settle() 函数,完成代币的支付。

第四步,执行 take() 函数,取回所得的代币。

最后,lock() 函数完成,返回结果。

而如果是跨池交易的话,则需要在 Router 层面确定好交易路径,然后根据路径执行多次 swap。举个例子,现在要用 A 兑换成 C,但是 A 和 C 之间没有直接配对的池子,但是有中间代币 B,存在 A 和 B 配对的池子,也存在 B 和 C 配对的池子。那交易路径就可以先用 A 换成 B,再将 B 换成 C,最终实现了 A 换成 C。而不管中间经过了多少次 swap,最后,只需要完成一次 settle 操作,即支付 A,也只需要执行一次 take 操作,即取回最后所得的 C。整个流程大致如下图所示:

image.png

下面,我们主要剖析讲解 swap() 函数的内部实现。

首先,看看其函数声明,如下:

function swap(PoolKey memory key, IPoolManager.SwapParams memory params, bytes calldata hookData)externaloverridenoDelegateCallonlyByLockerreturns (BalanceDelta delta)

key 指定了要进行交易的池子,params 是具体的交易参数,hookData 即需要回传给 hooks 合约的数据。

来看看 params 具体有哪些参数:

struct SwapParams {bool zeroForOne;int256 amountSpecified;uint160 sqrtPriceLimitX96;
}

zeroForOne 指名了要用 currency0 兑换 currency1,为 false 的话则反过来用 currency1 兑换 currency0amountSpecified 是指定的确定数额,正数表示输入,负数表示输出。sqrtPriceLimitX96 是滑点保护的限定价格。如果之前已经了解过 UniswapV3,那对这几个字段应该不陌生。

两个函数修饰器 noDelegateCallonlyByLocker,和之前文章介绍的一样,就不赘述了。

返回值 delta,其组成里的两个数,正常情况下就是一个正数,一个负数。

接下来,看看函数体了。先看前面一段代码:

PoolId id = key.toId();
_checkPoolInitialized(id);if (key.hooks.shouldCallBeforeSwap()) {bytes4 selector = key.hooks.beforeSwap(msg.sender, key, params, hookData);// Sentinel return value used to signify that a NoOp occurred.if (key.hooks.isValidNoOpCall(selector)) return BalanceDeltaLibrary.MAXIMUM_DELTA;else if (selector != IHooks.beforeSwap.selector) revert Hooks.InvalidHookResponse();
}

这部分逻辑很简单,前两行代码,检查池子是否已经初始化过了,未初始化的则 revert。之后是执行 hooks 合约的 beforeSwap 钩子函数。

接下来这段代码是执行 swap 的内部函数:

uint256 feeForProtocol;
uint256 feeForHook;
uint24 swapFee;
Pool.SwapState memory state;
(delta, feeForProtocol, feeForHook, swapFee, state) = pools[id].swap(Pool.SwapParams({tickSpacing: key.tickSpacing,zeroForOne: params.zeroForOne,amountSpecified: params.amountSpecified,sqrtPriceLimitX96: params.sqrtPriceLimitX96})
);

这个内部函数的具体实现比较复杂,我们待会再讲,先继续讲完外部函数剩下的代码。

接下来一行代码就是进行记账了:

_accountPoolBalanceDelta(key, delta);

之后是对协议费和 hook 费用的处理:

unchecked {if (feeForProtocol > 0) {protocolFeesAccrued[params.zeroForOne ? key.currency0 : key.currency1] += feeForProtocol;}if (feeForHook > 0) {hookFeesAccrued[address(key.hooks)][params.zeroForOne ? key.currency0 : key.currency1] += feeForHook;}
}

接着执行 afterSwap 的钩子函数:

if (key.hooks.shouldCallAfterSwap()) {if (key.hooks.afterSwap(msg.sender, key, params, delta, hookData) != IHooks.afterSwap.selector) {revert Hooks.InvalidHookResponse();}
}

最后,发送事件:

emit Swap(id, msg.sender, delta.amount0(), delta.amount1(), state.sqrtPriceX96, state.liquidity, state.tick, swapFee
);

整个外部函数的逻辑还是比较清晰的。复杂的其实是内部函数的实现。下面就来看看 swap 内部函数的实现逻辑。还是先看函数声明:

function swap(State storage self, SwapParams memory params)internalreturns (BalanceDelta result,uint256 feeForProtocol,uint256 feeForHook,uint24 swapFee,SwapState memory state)

selfstorage 类型的,其实就是外部函数的 pools[id]。而第二个参数的 SwapParams 不同于外部函数的同名参数,这个内部函数的此参数具体如下:

struct SwapParams {int24 tickSpacing;bool zeroForOne;int256 amountSpecified;uint160 sqrtPriceLimitX96;
}

相比外部函数的此参数,多了 tickSpacing,其他参数则和外部函数的一样。

返回值比较多。result 就是变动的净余额,feeForProtocol 是协议费,feeForHook 是 hook 费用,包括 hook 交易费用和提现费用,swapFee 就是池子本身的交易费,最后的 state 是最新的状态。

接着,开始查看函数体的代码实现,先看前面一段:

// 指定价格不能为0
if (params.amountSpecified == 0) revert SwapAmountCannotBeZero();
// 读取出swap前的状态
Slot0 memory slot0Start = self.slot0;
swapFee = slot0Start.swapFee;
if (params.zeroForOne) { // token0兑换token1// 滑点价格的判断if (params.sqrtPriceLimitX96 >= slot0Start.sqrtPriceX96) {revert PriceLimitAlreadyExceeded(slot0Start.sqrtPriceX96, params.sqrtPriceLimitX96);}if (params.sqrtPriceLimitX96 <= TickMath.MIN_SQRT_RATIO) {revert PriceLimitOutOfBounds(params.sqrtPriceLimitX96);}
} else { // token1兑换token0// 滑点价格的判断if (params.sqrtPriceLimitX96 <= slot0Start.sqrtPriceX96) {revert PriceLimitAlreadyExceeded(slot0Start.sqrtPriceX96, params.sqrtPriceLimitX96);}if (params.sqrtPriceLimitX96 >= TickMath.MAX_SQRT_RATIO) {revert PriceLimitOutOfBounds(params.sqrtPriceLimitX96);}
}

接下来是这段代码:

// 临时的缓存数据
SwapCache memory cache = SwapCache({liquidityStart: self.liquidity,protocolFee: params.zeroForOne? (getSwapFee(slot0Start.protocolFees) % 64): (getSwapFee(slot0Start.protocolFees) >> 6),hookFee: params.zeroForOne ? (getSwapFee(slot0Start.hookFees) % 64) : (getSwapFee(slot0Start.hookFees) >> 6)
});
// 是否为确定的输入
bool exactInput = params.amountSpecified > 0;
// 初始化返回值的state
state = SwapState({amountSpecifiedRemaining: params.amountSpecified,amountCalculated: 0,sqrtPriceX96: slot0Start.sqrtPriceX96,tick: slot0Start.tick,feeGrowthGlobalX128: params.zeroForOne ? self.feeGrowthGlobal0X128 : self.feeGrowthGlobal1X128,liquidity: cache.liquidityStart
});

cache 是一个临时状态的缓存数据,包括三个字段:

  • liquidityStart:流动性
  • protocolFee:协议费用
  • hookFee:hook 费用

amountSpecified 大于 0 则说明是指定的输入,即 exactInputtrue

初始化返回值 state 也都是用当前状态的值进行初始化。这里前两个字段需要介绍一下,即 amountSpecifiedRemainingamountCalculated。第一个字段表示当前还有多少指定的金额未进行交易计算的,第二个字段表示已经交易计算累加的数额。为了理解这两个字段,我们举个例子来说明。假设用户指定的是输出的数额,假设为 1000,那 amountSpecifiedRemaining 初始值即为 1000。但是,当前有效的流动性剩余量并不足 1000,假设只剩下 400,所以在当前 tick 下的计算只能用到 400,假设计算所得的输入数额为 200,那么,次轮计算后,amountSpecifiedRemaining 剩下 1000 - 400 = 600,而 amountCalculated 变为 200。之后,tick 会移动到下一个有流动性的区间内。剩下的 600 继续计算所得,假设这时的流动性剩余已经超过 600 了,这 600 计算所得的输入值为 250,那计算完后的 amountSpecifiedRemaining 就变成了 0,而 amountCalculated 则为 200 + 250 = 450,计算结束。这就是这两个字段的作用。

之后的代码会做循环判断,就是上面所说的计算逻辑:

StepComputations memory step;
// continue swapping as long as we haven't used the entire input/output and haven't reached the price limit
while (state.amountSpecifiedRemaining != 0 && state.sqrtPriceX96 != params.sqrtPriceLimitX96) {...
}

while 条件里除了判断 amountSpecifiedRemaining 不为 0 之外,还判断了最新价格不能等于滑点价格。如果等于滑点价格了,也会结束循环。

step 用来存储 while 循环里每一步的计算用到的临时变量,具体包含以下字段:

struct StepComputations {// the price at the beginning of the stepuint160 sqrtPriceStartX96;// the next tick to swap to from the current tick in the swap directionint24 tickNext;// whether tickNext is initialized or notbool initialized;// sqrt(price) for the next tick (1/0)uint160 sqrtPriceNextX96;// how much is being swapped in in this stepuint256 amountIn;// how much is being swapped outuint256 amountOut;// how much fee is being paid inuint256 feeAmount;
}

接着,来看看 while 循环里面的逻辑,先来看前面一段代码:

// 初始化当前这一步的价格
step.sqrtPriceStartX96 = state.sqrtPriceX96;
// 获取出下一个tick
(step.tickNext, step.initialized) =self.tickBitmap.nextInitializedTickWithinOneWord(state.tick, params.tickSpacing, params.zeroForOne);
// 确保下一个tick不会超出边界
if (step.tickNext < TickMath.MIN_TICK) {step.tickNext = TickMath.MIN_TICK;
} else if (step.tickNext > TickMath.MAX_TICK) {step.tickNext = TickMath.MAX_TICK;
}
// 计算出下一个tick对应的根号价格
step.sqrtPriceNextX96 = TickMath.getSqrtRatioAtTick(step.tickNext);

之后,执行当前这步的具体计算:

// compute values to swap to the target tick, price limit, or point where input/output amount is exhausted
(state.sqrtPriceX96, step.amountIn, step.amountOut, step.feeAmount) = SwapMath.computeSwapStep(state.sqrtPriceX96,(params.zeroForOne? step.sqrtPriceNextX96 < params.sqrtPriceLimitX96: step.sqrtPriceNextX96 > params.sqrtPriceLimitX96) ? params.sqrtPriceLimitX96 : step.sqrtPriceNextX96,state.liquidity,state.amountSpecifiedRemaining,swapFee
);

计算返回四个值,sqrtPriceX96 为计算后的最新价格,amountIn 为输入的数额,amountOut 为输出的金额,feeAmount 为需要支付的手续费。

继续看下一段代码:

if (exactInput) { //指定输入时unchecked {//remaining减去输入额和手续费state.amountSpecifiedRemaining -= (step.amountIn + step.feeAmount).toInt256();}//calculated加上输出额,因为amountOut为负数,所以用减法state.amountCalculated = state.amountCalculated - step.amountOut.toInt256();
} else { //指定输出时unchecked {//remaining减去输出额,因为amountOut为负数,所以用加法state.amountSpecifiedRemaining += step.amountOut.toInt256();}//calculated加上输入额和手续费state.amountCalculated = state.amountCalculated + (step.amountIn + step.feeAmount).toInt256();
}

之后的一段代码则是计算几个费用了:

// 协议费用
if (cache.protocolFee > 0) {// A: calculate the amount of the fee that should go to the protocoluint256 delta = step.feeAmount / cache.protocolFee;// A: subtract it from the regular fee and add it to the protocol feeunchecked {step.feeAmount -= delta;feeForProtocol += delta;}
}
// hook费用
if (cache.hookFee > 0) {// step.feeAmount has already been updated to account for the protocol feeuint256 delta = step.feeAmount / cache.hookFee;unchecked {step.feeAmount -= delta;feeForHook += delta;}
}
// 更新全局费用跟踪器
if (state.liquidity > 0) {unchecked {state.feeGrowthGlobalX128 += FullMath.mulDiv(step.feeAmount, FixedPoint128.Q128, state.liquidity);}
}

while 循环体里的最后一段代码则如下:

// 如果计算后的新价格到达下一个tick价格就移动tick
if (state.sqrtPriceX96 == step.sqrtPriceNextX96) {// 如果tick已经初始化,则执行移动tickif (step.initialized) {int128 liquidityNet = Pool.crossTick(self,step.tickNext,(params.zeroForOne ? state.feeGrowthGlobalX128 : self.feeGrowthGlobal0X128),(params.zeroForOne ? self.feeGrowthGlobal1X128 : state.feeGrowthGlobalX128));// 如果向左移动,把liquidityNet理解为相反的符号unchecked {if (params.zeroForOne) liquidityNet = -liquidityNet;}// 更新流动性state.liquidity = liquidityNet < 0? state.liquidity - uint128(-liquidityNet): state.liquidity + uint128(liquidityNet);}// 更新tickunchecked {state.tick = params.zeroForOne ? step.tickNext - 1 : step.tickNext;}
} else if (state.sqrtPriceX96 != step.sqrtPriceStartX96) {// 重新计算,除非我们处于较低的刻度边界(即已经转换过刻度),并且没有移动state.tick = TickMath.getTickAtSqrtRatio(state.sqrtPriceX96);
}

整个 while 循环跑完之后,一般来说,可能会存在两种情况。第一种,指定的金额全部完成兑换,即 amountSpecifiedRemaining 没有剩余。第二种,兑换到一半,触发到了滑点保护价格,那 amountSpecifiedRemaining 将会有剩余,只有部分成交。

那么,循环结束之后,整个内部的 swap 函数就只剩下最后的一部分代码了,如下:

// 将临时状态的价格和tick转为storage状态
(self.slot0.sqrtPriceX96, self.slot0.tick) = (state.sqrtPriceX96, state.tick);// 更新storage状态的流动性
if (cache.liquidityStart != state.liquidity) self.liquidity = state.liquidity;// 更新全局的手续费跟踪器
if (params.zeroForOne) {self.feeGrowthGlobal0X128 = state.feeGrowthGlobalX128;
} else {self.feeGrowthGlobal1X128 = state.feeGrowthGlobalX128;
}
// 净余额变动值赋值给返回值result
unchecked {if (params.zeroForOne == exactInput) {result = toBalanceDelta((params.amountSpecified - state.amountSpecifiedRemaining).toInt128(),state.amountCalculated.toInt128());} else {result = toBalanceDelta(state.amountCalculated.toInt128(),(params.amountSpecified - state.amountSpecifiedRemaining).toInt128());}
}

至此,就完成了 swap 的全部代码逻辑讲解了。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【面向就业的Linux基础】从入门到熟练,探索Linux的秘密(七)-shell语法(5)

shell语法的一些知识和练习&#xff0c;可以当作笔记收藏一下&#xff01;&#xff01; 文章目录 前言 一、shell 二、shell语法 1.文件重定向 2.引入外部脚本 3.作业 总结 前言 shell语法的一些知识和练习&#xff0c;可以当作笔记收藏一下&#xff01;&#xff01; 提示&…

七种大模型微调方法:让你的Offer拿到爽

在当今的人工智能和机器学习领域&#xff0c;大型预训练模型&#xff08;如GPT、BERT等&#xff09;已成为解决自然语言处理&#xff08;NLP&#xff09;任务的强大工具。然而&#xff0c;要让这些模型更好地适应特定任务或领域&#xff0c;往往需要进行微调。本文将详细介绍七…

手把手教你:如何在51建模网免费下载3D模型?

作为国内领先的3D互动展示平台&#xff0c;51建模网不仅汇聚了庞大的3D模型资源库&#xff0c;供用户免费下载&#xff0c;更集成了在线编辑、格式转换、内嵌展示及互动体验等一站式功能&#xff0c;为3D创作者及爱好者搭建起梦想与现实的桥梁。 如何在51建模网免费下载3D模型…

鸿蒙认证值得考吗?

鸿蒙认证值得考吗&#xff1f; 鸿蒙认证&#xff08;HarmonyOS Certification&#xff09;是华为为了培养和认证开发者在鸿蒙操作系统&#xff08;HarmonyOS&#xff09;领域的专业技能而设立的一系列认证项目。这些认证旨在帮助开发者和企业工程师提升在鸿蒙生态中的专业技能…

linux——IPC 进程间通信

IPC 进程间通信 interprocess communicate IPC&#xff08;Inter-Process Communication&#xff09;&#xff0c;即进程间通信&#xff0c;其产生的原因主要可以归纳为以下几点&#xff1a; 进程空间的独立性 资源隔离&#xff1a;在现代操作系统中&#xff0c;每个进程都…

图解 Kafka 架构

写在前面 Kafka 是一个可横向扩展&#xff0c;高可靠的实时消息中间件&#xff0c;常用于服务解耦、流量削峰。 好像是 LinkedIn 团队开发的&#xff0c;后面捐赠给apache基金会了。 kafka 总体架构图 Producer&#xff1a;生产者&#xff0c;消息的产生者&#xff0c;是消息的…

怎么把录音转文字?推荐几个简单易操作的方法

在小暑这个节气里&#xff0c;炎热的天气让人分外渴望效率up&#xff01;Up&#xff01;Up&#xff01; 对于那些在会议或课堂中急需记录信息的朋友们&#xff0c;手写笔记的速度往往难以跟上讲话的节奏。此时&#xff0c;电脑录音转文字软件就像一阵及时雨&#xff0c;让记录…

PHP pwn 学习 (1)

文章目录 A. PHP extensions for C1. 运行环境与工作目录初始化2. 构建与加载3. 关键结构定义PHP_FUNCTIONINTERNAL_FUNCTION_PARAMETERSzend_execute_data等ZEND_PARSE_PARAMETERS_START等zend_parse_arg_stringzend_module_entryzend_function_entry等PHP类相关 原文链接&…

Python 作业题1 (猜数字)

题目 你要根据线索猜出一个三位数。游戏会根据你的猜测给出以下提示之一&#xff1a;如果你猜对一位数字但数字位置不对&#xff0c;则会提示“Pico”&#xff1b;如果你同时猜对了一位数字及其位置&#xff0c;则会提示“Fermi”&#xff1b;如果你猜测的数字及其位置都不对&…

Flower花所:稳定运营的数字货币交易所

Flower花所是一家稳定运营的数字货币交易所&#xff0c;致力于为全球用户提供安全、高效的数字资产交易服务。作为一家长期稳定运营的数字货币交易平台&#xff0c;Flower花所以其可靠的技术基础和优质的客户服务而闻名。 平台稳定性与可靠性&#xff1a; 持续运营&#xff1a;…

Vue前端练习

此练习项目只涉及前端&#xff0c;主要是vue和ElementUI框架的使用。&#xff08;ElementUI官网&#xff1a;Element - The worlds most popular Vue UI framework&#xff09; 一、环境准备 安装idea 安装Node.js 一键式安装(不需要做任何配置) npm -v&#xff08;也可用nod…

mysql-sql-第十五周

学习目标&#xff1a; sql 学习内容&#xff1a; 41.查询没有学全所有课程的同学的信息 select *from students where students.stunm not in (select score.stunm from score group by score.stunm having count(score.counm) (select count(counm) from course)) 42.查询…

数据结构_线性表

线性表的定义和特点 线性表是具有相同特性的数据元素的一个有限序列 :线性起点/起始节点 :的直接前驱 :的直接后继 :线性终点/终端节点 n:元素总个数,表长 下标:是元素的序号,表示元素在表中的位置 n0时称为空表 线性表 由n(n>0)个数据元素(结点),组成的有限序列 将…

安卓模拟器如何修改ip地址

最近很多老铁玩游戏的&#xff0c;想多开模拟器一个窗口一个IP&#xff0c;若模拟器窗口开多了&#xff0c;IP一样会受到限制&#xff0c;那么怎么更换自己电脑手机模拟器IP地址呢&#xff0c;今天就教大家一个修改模拟器IP地址的方法&#xff01;废话不多说&#xff0c;直接上…

alibaba EasyExcel 简单导出数据到Excel

导入依赖 <dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</artifactId><version>4.0.1</version> </dependency> 1、alibaba.excel.EasyExcel导出工具类 import com.alibaba.excel.EasyExcel; import …

探索哈希函数:数据完整性的守护者

引言 银行在处理数以百万计的交易时&#xff0c;如何确保每一笔交易都没有出错&#xff1f;快递公司如何跟踪成千上万的包裹&#xff0c;确保每个包裹在运输过程中没有丢失或被替换&#xff1f;医院和诊所为庞大的患者提供有效的医疗保健服务&#xff0c;如何确保每个患者的医疗…

假阳性和假阴性、真阳性和真阴性

在深度学习的分类问题中&#xff0c;真阳性、真阴性、假阳性和假阴性是评估模型性能的重要指标。它们的定义和计算如下&#xff1a; 真阳性&#xff08;True Positive, TP&#xff09;&#xff1a; 定义&#xff1a;模型预测为正类&#xff08;阳性&#xff09;&#xff0c;且实…

电梯修理升级,安装【电梯节能】能量回馈设备

电梯修理升级&#xff0c;安装【电梯节能】能量回馈设备 1、节能率评估 15%—45% 2、降低机房环境温度&#xff0c;改善电梯控制系统的运行环境&#xff1b; 3、延长电梯使用寿命&#xff1b; 4、机房可以不需要使用空调等散热设备的耗电&#xff0c;间接节省电能。 欢迎私询哦…

智能数字人系统的主要功能

智能数字人系统或虚拟数字人系统&#xff0c;是指利用人工智能技术构建的虚拟人物形象&#xff0c;能够与人进行自然交互的系统。数字人系统的主要功能包括以下几个方面。北京木奇移动技术有限公司&#xff0c;专业的软件外包开发公司&#xff0c;欢迎交流合作。 1. 语言理解与…

昇思25天学习打卡营第2天|初学入门

昇思25天学习打卡营第2天 文章目录 昇思25天学习打卡营第2天网络构建定义模型类模型层nn.Flattennn.Densenn.ReLUnn.SequentialCellnn.Softmax 模型参数 函数式自动微分函数与计算图微分函数与梯度计算Stop GradientAuxiliary data神经网络梯度计算 问题集合打卡记录 网络构建 …