剖析DeFi交易产品之UniswapV3:交易路由合约

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


SwapRouter 合约封装了面向用户的交易接口,但不再像 UniswapV2Router 一样根据不同交易场景拆分为了那么多函数,UniswapV3 的 SwapRouter 核心就只有 4 个交易函数:

  • exactInputSingle:指定输入数量的单池内交易
  • exactOutputSingle:指定输出数量的单池内交易
  • exactInput:指定输入数量和交易路径的交易
  • exactOutput:指定输出数量和交易路径的交易

带 Single 的只支持单池内的交易,而不带 Single 的则支持跨不同池子的互换交易。

exactInputSingle

先来看简单的单池交易,以 exactInputSingle 为始,其代码实现如下:

struct ExactInputSingleParams {address tokenIn;   //输入tokenaddress tokenOut;  //输出tokenuint24 fee;        //手续费率address recipient; //收款地址uint256 deadline;  //过期时间uint256 amountIn;  //指定的输入token数量uint256 amountOutMinimum;  //输出token的最小数量uint160 sqrtPriceLimitX96; //限定的价格
}function exactInputSingle(ExactInputSingleParams calldata params)externalpayableoverridecheckDeadline(params.deadline)returns (uint256 amountOut)
{amountOut = exactInputInternal(params.amountIn,params.recipient,params.sqrtPriceLimitX96,SwapCallbackData({path: abi.encodePacked(params.tokenIn, params.fee, params.tokenOut), payer: msg.sender}));require(amountOut >= params.amountOutMinimum, 'Too little received');
}

其入参有 9 个参数,返回值就一个 amountOut,即输出的 token 数量。

从代码上可看出,实际的逻辑实现是在内部函数 exactInputInternal。查看该内部函数之前,我们先来了解下 SwapCallbackData。我们从上面代码可以看到,调用 exactInputInternal 时,最后一个传入的参数就是 SwapCallbackData,这其实是一个结构体,定义了两个属性:

struct SwapCallbackData {bytes path;address payer;
}

path 表示交易路径,在以上代码中,就是由 tokenIn、fee、tokenOut 这三个变量拼接而成。payer 表示支付输入 token 的地址,上面的就是 msg.sender。

接着,来看看内部函数 exactInputInternal 的代码实现:

function exactInputInternal(uint256 amountIn,address recipient,uint160 sqrtPriceLimitX96,SwapCallbackData memory data
) private returns (uint256 amountOut) {// allow swapping to the router address with address 0if (recipient == address(0)) recipient = address(this);//从路径中解码出第一个池子(address tokenIn, address tokenOut, uint24 fee) = data.path.decodeFirstPool();//当tokenIn<tokenOUt时,则说明tokenIn为token0,所以是要将token0兑换成token1bool zeroForOne = tokenIn < tokenOut;//调用底层池子的swap函数执行交易(int256 amount0, int256 amount1) =getPool(tokenIn, tokenOut, fee).swap(recipient,zeroForOne,amountIn.toInt256(),sqrtPriceLimitX96 == 0? (zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1): sqrtPriceLimitX96,abi.encode(data));//返回amountOutreturn uint256(-(zeroForOne ? amount1 : amount0));
}

首先,如果 recipient 地址为零地址的话,那会把 recipient 重置为当前合约地址。

接着,通过 data.path.decodeFirstPool() 从路径中解码得出 tokenIn、tokenOut 和 fee。decodeFirstPool 函数是在库合约 Path 里实现的。

布尔类型的 zeroForOne 表示底层 token0 和 token1 的兑换方向,为 true 表示用 token0 兑换 token1,false 则反之。因为底层的 token0 是小于 token1 的,所以,当 tokenIn 也小于 tokenOut 的时候,说明 tokenIn == token0,所以 zeroForOne 为 true。

然后,通过 getPool 函数可得到池子地址,再调用底层池子的 swap 函数来执行实际的交易逻辑。

最后,我们要得到的是 amountOut,这是 amount0 和 amount1 中的其中一个。我们已经知道,zeroForOne 为 true 的时候,tokenIn 等于 token0,所以 tokenOut 就是 token1,因此 amountOut 就是 amount1。另外,对底层池子来说,属于输出的时候,返回的数值是负数,即 amount1 其实是一个负数,因此需要再加个负号转为正数的 uint256 类型。

在这个函数里,我们可以看出并没有支付 token 的功能,但前面讲解 UniswapV3Pool 时已经了解到,支付是在回调函数 uniswapV3SwapCallback 里完成的。因为这个回调函数会涉及到所有 4 种交易类型,所以我们留到最后再来讲解。

exactOutputSingle

接着,来看 exactOutputSingle 函数的实现,其代码如下:

struct ExactOutputSingleParams {address tokenIn;   //输入tokenaddress tokenOut;  //输出tokenuint24 fee;        //手续费率address recipient; //收款地址uint256 deadline;  //过期时间uint256 amountOut; //指定的输出token数量uint256 amountInMaximum;   //输入token的最大数量uint160 sqrtPriceLimitX96; //限定的价格
}function exactOutputSingle(ExactOutputSingleParams calldata params)externalpayableoverridecheckDeadline(params.deadline)returns (uint256 amountIn)
{// avoid an SLOAD by using the swap return dataamountIn = exactOutputInternal(params.amountOut,params.recipient,params.sqrtPriceLimitX96,SwapCallbackData({path: abi.encodePacked(params.tokenOut, params.fee, params.tokenIn), payer: msg.sender}));require(amountIn <= params.amountInMaximum, 'Too much requested');// has to be reset even though we don't use it in the single hop caseamountInCached = DEFAULT_AMOUNT_IN_CACHED;
}

可看出,exactOutputSingle 函数的实现与 exactInputSingle 函数大同小异。首先,参数上,只有两个不同,exactInputSingle 函数指定的是 amountIn 和 amountOutMinimum;而 exactOutputSingle 函数改为了 amountOut 和 amountInMaximum,即输出是指定的,而输入则限制了最大值。其次,实际逻辑封装在了 exactOutputInternal 内部函数,而且传给该内部函数的最后一个参数的 path 组装顺序也不一样了,排在第一位的是 tokenOut。

核心实现还是在 exactOutputInternal 内部函数,其代码实现如下:

function exactOutputInternal(uint256 amountOut,address recipient,uint160 sqrtPriceLimitX96,SwapCallbackData memory data
) private returns (uint256 amountIn) {// allow swapping to the router address with address 0if (recipient == address(0)) recipient = address(this);//从路径中解码出第一个池子(address tokenOut, address tokenIn, uint24 fee) = data.path.decodeFirstPool();//是否token0兑换token1bool zeroForOne = tokenIn < tokenOut;//调用底层池子的swap函数执行交易(int256 amount0Delta, int256 amount1Delta) =getPool(tokenIn, tokenOut, fee).swap(recipient,zeroForOne,-amountOut.toInt256(), //指定输出需转为负数sqrtPriceLimitX96 == 0? (zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1): sqrtPriceLimitX96,abi.encode(data));//确定amountIn和amountOutuint256 amountOutReceived;(amountIn, amountOutReceived) = zeroForOne? (uint256(amount0Delta), uint256(-amount1Delta)): (uint256(amount1Delta), uint256(-amount0Delta));// it's technically possible to not receive the full output amount,// so if no price limit has been specified, require this possibility awayif (sqrtPriceLimitX96 == 0) require(amountOutReceived == amountOut);
}

可见和 exactInputInternal 的实现也是大同小异。不过,有一个细节需要补充一下。因为是指定的输出数额,所以调用底层的 swap 函数时,第三个传参转为了负数,这也是前面讲解 UniswapV3Pool 的 swap 函数时讲过的,当指定的交易数额是输出的数额时,则需传负数。

和 exactInputInternal 一样,在当前函数里没有支付 token 的逻辑,也是统一在 uniswapV3SwapCallback 回调函数里去完成支付。

exactInput

exactInput 函数则用于处理跨多个池子的指定输入数量的交易,相比单池交易会复杂一些,而且这里面的逻辑还有点绕,我们来进行一一剖析。其实现代码如下:

struct ExactInputParams {bytes path;         //交易路径address recipient;  //收款地址uint256 deadline;   //过期时间uint256 amountIn;   //指定输入token数量uint256 amountOutMinimum; //输出token的最小数量
}function exactInput(ExactInputParams memory params)externalpayableoverridecheckDeadline(params.deadline)returns (uint256 amountOut)
{//调用者需支付路径中的第一个代币address payer = msg.sender;//遍历路径while (true) {//路径中是否还存在多个池子bool hasMultiplePools = params.path.hasMultiplePools();//先前交换的输出成为后续交换的输入params.amountIn = exactInputInternal(params.amountIn,hasMultiplePools ? address(this) : params.recipient,0,SwapCallbackData({path: params.path.getFirstPool(), // 只需要路径里的第一个池子payer: payer}));//当路径依然由多个池子组成时,则继续循环,否则退出循环if (hasMultiplePools) {payer = address(this);//跳过第一个token,作为下一轮的路径params.path = params.path.skipToken();} else {//最后一次兑换,把前面设为了amountIn的重新赋值给amountOutamountOut = params.amountIn;break;}}require(amountOut >= params.amountOutMinimum, 'Too little received');
}

其中,需要跨多个池子的路径编码方式如下图:

uniswapV3-path.webp

和 UniswapV2 一样,这个路径是由前端计算出来再传给合约的。寻找最优路径的算法也是和 UniswapV2 一样的思路。

exactInput 函数的核心实现逻辑是,循环处理路径中的每一个配对池,每处理完一个池子的交易,就从路径中移除第一个 token 和 fee,直到路径只剩下最后一个池子就结束循环。期间,每一次执行 exactInputInternal 后,将返回的 amounOut 作为下一轮的 amountIn。第一轮兑换时,payer 是合约的调用者,即 msg.sender,而输出代币的 recipient 则是当前合约地址。中间的每一次兑换,payer 和 recipient 都是当前合约地址。到最后一次兑换时,recipient 才转为用户传入的地址。

exactOutput

剩下最后一个函数 exactOutput 了,也是用于处理跨多个池子的的交易,而指定的是输出的数量。以下是其代码实现:

struct ExactOutputParams {bytes path;        //交易路径address recipient; //收款地址uint256 deadline;  //过期时间uint256 amountOut; //指定输出token数量uint256 amountInMaximum; //输入token的最大数量
}function exactOutput(ExactOutputParams calldata params)externalpayableoverridecheckDeadline(params.deadline)returns (uint256 amountIn)
{// it's okay that the payer is fixed to msg.sender here, as they're only paying for the "final" exact output// swap, which happens first, and subsequent swaps are paid for within nested callback framesexactOutputInternal(params.amountOut,params.recipient,0,SwapCallbackData({path: params.path, payer: msg.sender}));amountIn = amountInCached;require(amountIn <= params.amountInMaximum, 'Too much requested');amountInCached = DEFAULT_AMOUNT_IN_CACHED;
}

可看到其逻辑就直接调用内部函数 exactOutputInternal 完成交易,并没有像 exactInput 一样的循环处理。但在整个流程中,其实还是进行了遍历路径的多次交易的,只是这个流程完成得比较隐晦。其关键其实是在 uniswapV3SwapCallback 回调函数里,后面我们会说到。

uniswapV3SwapCallback

以下就是回调函数的实现:

function uniswapV3SwapCallback(int256 amount0Delta,int256 amount1Delta,bytes calldata _data
) external override {require(amount0Delta > 0 || amount1Delta > 0);//解码出_data数据SwapCallbackData memory data = abi.decode(_data, (SwapCallbackData));//解码出路径的第一个池子(address tokenIn, address tokenOut, uint24 fee) = data.path.decodeFirstPool();//校验callback的调用者CallbackValidation.verifyCallback(factory, tokenIn, tokenOut, fee);//用于判断当前需要支付的代币(bool isExactInput, uint256 amountToPay) =amount0Delta > 0? (tokenIn < tokenOut, uint256(amount0Delta)): (tokenOut < tokenIn, uint256(amount1Delta));if (isExactInput) { //指定金额的是输入,直接执行支付pay(tokenIn, data.payer, msg.sender, amountToPay);} else { //指定金额的是输出// either initiate the next swap or payif (data.path.hasMultiplePools()) {// 路径里有多个池子时,则跳过路径的第一个token,使用下一个配对的池子进行交易data.path = data.path.skipToken();exactOutputInternal(amountToPay, msg.sender, 0, data);} else { //只剩下一个池子,执行支付amountInCached = amountToPay;tokenIn = tokenOut; // swap in/out because exact output swaps are reversedpay(tokenIn, data.payer, msg.sender, amountToPay);}}
}

另外,这个是 swap 时的回调函数。而之前的文章我们还讲了另一个回调函数 uniswapV3MintCallback 是添加流动性时的回调函数,两者是不同的,不要搞混了。

其逻辑实现并不复杂。首先,先把 _data 解码成 SwapCallbackData 结构体类型数据。接着,解码出路径的第一个池子。然后,通过 verifyCallback 校验调用当前回调函数的是否为底层 pool 合约,非底层 pool 合约是不允许调起回调函数的。

isExactInput 和 amountToPay 的赋值需要拆解一下才好理解。首先需知道,amount0Delta 和 amount1Delta 其实是一正一负的,正数是输入的,负数是输出的。因此,amount0Delta 大于 0 的话则 amountToPay 就是 amount0Delta,否则就是 amount1Delta 了。 amount0Delta 大于 0 也说明了输入的是 token0,因此,当 tokenIn < tokenOut 的时候,说明 tokenIn 就是 token0,也即是说用户指定的是输入数量,所以这时候的 isExactInput 即为 true。

当指定金额为输出的时候,也就是处理 exactOutput 和 exactOutputSingle 函数的时候。我们前面看到 exactOutput 的代码逻辑里并没有对路径进行遍历处理,这个遍历其实就是在这个回调函数里完成的。仔细看这段代码:

if (data.path.hasMultiplePools()) {// 路径里有多个池子时,则跳过路径的第一个token,使用下一个配对的池子进行交易data.path = data.path.skipToken();exactOutputInternal(amountToPay, msg.sender, 0, data);
}

这不就是遍历路径多次执行 exactOutputInternal 了吗。

至此,SwapRouter 合约也讲解完了。

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

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

相关文章

Java技术栈总结:kafka篇

一、# 基础知识 1、安装 部署一台ZooKeeper服务器&#xff1b;安装jdk&#xff1b;下载kafka安装包&#xff1b;上传安装包到kafka服务器上&#xff1a;/usr/local/kafka;解压缩压缩包&#xff1b;进入到config目录&#xff0c;修改server.properties配置信息&#xff1a; #…

Buuctf之SimpleRev做法

首先&#xff0c;查个壳&#xff0c;64bit&#xff0c;那就丢进ida64中进行反编译进来之后&#xff0c;我们进入main函数&#xff0c;发现里面没什么东西&#xff0c;那就shiftf12搜索字符串&#xff0c;找到关键字符串&#xff0c;双击进入然后再选中该字符串&#xff0c;ctrl…

Python爬取股票信息-并进行数据可视化分析,绘股票成交量柱状图

为了使用Python爬取股票信息并进行数据可视化分析&#xff0c;我们可以使用几个流行的库&#xff1a;requests 用于网络请求&#xff0c;pandas 用于数据处理&#xff0c;以及 matplotlib 或 seaborn 用于数据可视化。 步骤 1: 安装必要的库 首先&#xff0c;确保安装了以下P…

【C语言】指针(1):入门理解篇

目录 一、内存和地址 1.1内存 1.2 深入理解计算机编址 二、指针变量和地址 2.1 取地址操作符&#xff08;&&#xff09; 2.2 指针变量和解应用操作符 2.2.1 指针变量 2.2.2 解引用操作符 2.3指针变量的大小 三、指针变量类型的意义 3.1 指针的解引用 3.1指针-整数…

Micron近期发布了32Gb DDR5 DRAM

Micron Technology近期发布了一项内存技术的重大突破——一款32Gb DDR5 DRAM芯片&#xff0c;这项创新不仅将存储容量翻倍&#xff0c;还显著提升了针对人工智能&#xff08;AI&#xff09;、机器学习&#xff08;ML&#xff09;、高性能计算&#xff08;HPC&#xff09;以及数…

2024年最新运维面试题(附答案)

作者简介&#xff1a;一名云计算网络运维人员、每天分享网络与运维的技术与干货。 公众号&#xff1a;网络豆云计算学堂 座右铭&#xff1a;低头赶路&#xff0c;敬事如仪 个人主页&#xff1a; 网络豆的主页​​​​​ 一&#xff0e;选择题 1.HTTP协议默认使用哪个端口…

科普文:构建可扩展的微服务架构设计方案

前言 微服务架构是一种新兴的软件架构风格&#xff0c;它将单个应用程序拆分成多个小的服务&#xff0c;每个服务都运行在自己的进程中&#xff0c;这些服务通过网络进行通信。这种架构的优势在于它可以提高应用程序的可扩展性、可维护性和可靠性。 在传统的应用程序架构中&…

在Java项目中实现实时日志分析

在Java项目中实现实时日志分析 大家好&#xff0c;我是免费搭建查券返利机器人省钱赚佣金就用微赚淘客系统3.0的小编&#xff0c;也是冬天不穿秋裤&#xff0c;天冷也要风度的程序猿&#xff01; 随着互联网应用的发展&#xff0c;实时日志分析成为了保证系统稳定性、性能优化…

PyCharm 安装

PyCharm是一种流行的Python集成开发环境&#xff08;IDE&#xff09;&#xff0c;由JetBrains公司开发。它提供了丰富的功能&#xff0c;如智能代码补全、实时错误检查、项目导航、调试工具以及版本控制等&#xff0c;极大地提高了Python开发人员的工作效率。以下是PyCharm安装…

高效管理个人日程,智慧校园行政办公全指南

在智慧校园的行政办公体系里&#xff0c;个人日程管理功能担当起协调与优化每位教职员工日常安排的角色&#xff0c;它像一位贴心的时间助理&#xff0c;确保工作与私人生活的和谐并进。这一功能设计得既直观又灵活&#xff0c;让使用者能以自己偏好的视角审视时间规划&#xf…

创新配置,秒级采集,火爆短视频评论抓取

快速采集评论数据的好处 快速采集评论数据是在当今数字信息时代的市场趋势分析和用户反馈分析中至关重要的环节。通过准确获取并分析大量用户评论&#xff0c;您将能够更好地了解消费者的需求、情感和偏好。集蜂云采集平台提供了一种简单配置的方法&#xff0c;使您能够快速采…

springboot中的定时任务编写

第1部分&#xff1a;引言 1.1 定时任务的重要性 在现代软件开发中&#xff0c;定时任务是应用程序自动化和运维效率的关键组成部分。无论是数据备份、系统健康检查、定期报告生成&#xff0c;还是用户活动触发的自动化流程&#xff0c;定时任务都扮演着不可或缺的角色。它们确…

Deep Filtered Back Projection for CT Reconstruction

CT重建中的深度滤波反投影 论文链接&#xff1a;https://ieeexplore.ieee.org/document/10411896 项目链接&#xff1a; ABSTRACT 滤波反投影(FBP)是一种经典的计算机断层扫描(CT)重建解析算法&#xff0c;具有很高的计算效率。然而&#xff0c;用FBP重建的图像往往存在过多…

NATAPP内网穿透使用

1. natapp能干嘛 可以将本地的内网ip映射到外网上&#xff0c;远程访问该连接&#xff0c;实现外网展示网站。平时做的应用开发都只能在局域网本地访问&#xff0c;通过内网穿透&#xff0c;可以通过外网进行访问。 2. 注册用户 网址&#xff1a;https://natapp.cn/自行完成…

什么是 Elasticsearch 数据预热?

引言&#xff1a;在现代的信息检索和数据分析领域&#xff0c;Elasticsearch 已经成为一个广泛应用的分布式搜索和分析引擎。作为开源项目的一部分&#xff0c;Elasticsearch 提供了强大的实时搜索和分析能力&#xff0c;使得处理大规模数据变得更加高效和可靠。然而&#xff0…

Canary,三种优雅姿势绕过

Canary&#xff08;金丝雀&#xff09;&#xff0c;栈溢出保护 canary保护是防止栈溢出的一种措施&#xff0c;其在调用函数时&#xff0c;在栈帧的上方放入一个随机值 &#xff0c;绕过canary时首先需要泄漏这个随机值&#xff0c;然后再钩爪ROP链时将其作为垃圾数据写入&…

对接海康sdk-linux下复制jar包中resource目录的文件夹

背景 在集成海康sdk时,需要将一些组件放到项目中作为静态资源,并且海康的sdk初始化也需要加载这些静态资源,在windows下,使用一些File路径的方式是可以正确加载的,但是在linux上就会加载失败。 首先我是将海康的sdk组件放到resource下的,并且按照windows和linux设置了两…

轻松快速上手Thekey库,实现数据加密无忧

Thekey的概述&#xff1a; Thekey库是一个Python库,旨在简化数据加密、解密、签名和验证的过程。它提供了一套简洁易用的接口,用于处理各种加密任务,适合需要在应用程序中实现安全数据处理的开发人员. 安装Thekey库 pip install thekey使用Thekey库进行基本加密和解密操作的…

【笔记】TimEP Safety Mechanisms方法论

1.TimEPM Overview 三大监控方法: Alive Supervision 实时监督Logical Supervision 逻辑监督Deadline Supervision 限时监督相关模块框图: 相关模块调用框图: 每个MCU核开启内狗(1核1狗),内狗用于监控相应核的TASK超时,超时后软reset MCU内狗时钟需要独立于OS时钟,两…

C++下Protobuf学习

C下Protobuf简单学习 Protobuf&#xff08;Protocol Buffers&#xff09;协议是一种由 Google 开发的高效的、跨语言的、平台无关的数据序列化协议&#xff0c;提供二进制序列化格式和相关的技术&#xff0c;它用于高效地序列化和反序列化结构化数据&#xff0c;通常用于网络通…