剖析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 表示交易路径,在以上代码中,就是由 tokenInfeetokenOut 这三个变量拼接而成。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() 从路径中解码得出 tokenIntokenOutfeedecodeFirstPool 函数是在库合约 Path 里实现的。

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

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

最后,我们要得到的是 amountOut,这是 amount0 和 amount1 中的其中一个。我们已经知道,zeroForOnetrue 的时候,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 函数指定的是 amountInamountOutMinimum;而 exactOutputSingle 函数改为了 amountOutamountInMaximum,即输出是指定的,而输入则限制了最大值。其次,实际逻辑封装在了 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 则是当前合约地址。中间的每一次兑换,payerrecipient 都是当前合约地址。到最后一次兑换时,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 合约是不允许调起回调函数的。

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

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

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

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

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

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

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

相关文章

华为机试HJ34图片整理

华为机试HJ34图片整理 题目&#xff1a; 想法&#xff1a; 将输入的字符串中每个字符都转为ASCII码&#xff0c;再通过快速排序进行排序并输出 input_str input() input_list [int(ord(l)) for l in input_str]def partition(arr, low, high):i low - 1pivot arr[high]f…

matlab 有倾斜的椭圆函数图像绘制

matlab 有倾斜的椭圆函数图像绘制 有倾斜的椭圆函数图像绘制xy交叉项引入斜线负向斜线成分正向斜线成分 x^2 y^2 xy 1 &#xff08;负向&#xff09;绘制结果 x^2 y^2 - xy 1 &#xff08;正向&#xff09;绘制结果 有倾斜的椭圆函数图像绘制 为了确定椭圆的长轴和短轴的…

【Python】MacBook M系列芯片Anaconda下载Pytorch,并开发一个简单的数字识别代码(附带踩坑记录)

文章目录 配置镜像源下载Pytorch验证使用Pytorch进行数字识别 配置镜像源 Anaconda下载完毕之后&#xff0c;有两种方式下载pytorch&#xff0c;一种是用页面可视化的方式去下载&#xff0c;另一种方式就是直接用命令行工具去下载。 但是由于默认的Anaconda走的是外网&#x…

9 redis,memcached,nginx网络组件

课程目标: 1.网络模块要处理哪些事情 2.reactor是怎么处理这些事情的 3.reactor怎么封装 4.网络模块与业务逻辑的关系 5.怎么优化reactor? io函数 函数调用 都有两个作用:io检测 是否就绪 io操作 1. int clientfd = accept(listenfd, &addr, &len); 检测 全连接队列…

技术派Spring事件监听机制及原理

Spring事件监听机制是Spring框架中的一种重要技术&#xff0c;允许组件之间进行松耦合通信。通过使用事件监听机制&#xff0c;应用程序的各个组件可以在其他组件不直接引用的情况下&#xff0c;相互发送和接受消息。 需求 在技术派中有这样一个需求&#xff0c;当发布文章或…

简单分享下python多态

目录&#xff1a; 一、多态是啥嘞&#xff08;龙生九子各有不同&#xff0c;这就是多态&#xff09; 二、基础的实例 三、多态的优势与应用场景 四、深入理解 一、多态是啥嘞&#xff08;龙生九子各有不同&#xff0c;这就是多态&#xff09; 多态&#xff08;Polymorphism&…

如何利用算法优化广告效果

效果广告以超过67%的占比&#xff0c;成为了中国互联网广告预算的大头。在BAT、字节等大的媒体平台上&#xff0c;效果广告以CPC实时竞价广告为主。在这种广告产品的投放中&#xff0c;广告主或其代理公司通过针对每个广告点击出价&#xff0c;系统自动把这些点击出价换算成eCP…

【人工智能】-- 智能机器人

个人主页&#xff1a;欢迎来到 Papicatch的博客 课设专栏 &#xff1a;学生成绩管理系统 专业知识专栏&#xff1a; 专业知识 文章目录 &#x1f349;引言 &#x1f349;机器人介绍 &#x1f348;机器人硬件 &#x1f34d;机械结构 &#x1f34d;传感器 &#x1f34d;控…

nginx配置尝试

from fastapi import FastAPI, File, UploadFile, HTTPException from fastapi.responses import JSONResponse, FileResponse, HTMLResponse import logging import os from datetime import datetime import uvicorn# 初始化日志 logging.basicConfig(filenamefile_server.lo…

学java的第3天 后端商城小程序工作

1.数据库的大坑 特殊字段名 ’我的图片表中有一个字段是描述我写成desc了&#xff0c;正好是mysql中的关键字 就不能使用了 2.后端编写 2.1可以把请求分开 在商品浏览页中 只显示商品的大致信息 当用户再点击其他按钮时在发出请求 2.2把请求合并 把数据整合到一起 利用ass…

SpringBoot环境集成 sms4j短信聚合

SpringBoot环境集成 sms4j短信聚合 官方文档 前言 在正式使用sms4j短信功能之前&#xff0c;请详细阅读本文档&#xff0c;依照本篇流程进行操作和配给&#xff0c;即可解决大部分问题&#xff0c;如对我们的文档有建议&#xff0c;请联系开发者团队&#xff0c; 我们将根据可…

电脑为什么会提示丢失msvcp140.dll?怎么修复msvcp140.dll文件会靠谱点

电脑为什么会提示丢失msvcp140.dll&#xff1f;其实只要你的msvcp140.dll文件一损坏&#xff0c;然而你的电脑程序需要运用到这个msvcp140.dll文件的时候&#xff0c;就回提示你丢失了msvcp140.dll文件&#xff01;因为没有这个文件&#xff0c;你的很多程序都用不了的。今天我…

电脑录歌用什么软件好?分享电脑录音软件:6款

短视频普遍的今天&#xff0c;越来越多的人喜欢通过电脑进行音乐创作和录制。然而&#xff0c;面对市面上琳琅满目的电脑录音软件&#xff0c;很多人可能会感到困惑&#xff1a;电脑录歌用什么软件好呢&#xff1f;本文将为大家分享六款精选的录音软件&#xff0c;帮助大家找到…

【matlab】分类回归——智能优化算法优化径向基神经网络

目录 径向基&#xff08;Radial Basis Function, RBF&#xff09;神经网络 一、基本概念 二、网络结构 三、工作原理 四、学习算法 五、优点与应用 六、与BP神经网络的比较 智能优化算法 常见的智能优化算法 灰狼优化算法&#xff08;Grey Wolf Optimizer, GWO&#…

品牌推广的核心价值:作用解析与意义探讨!

在激烈的市场竞争环境之下&#xff0c;品牌推广已经成为企业不可缺少的一部分。不仅关乎企业的知名度&#xff0c;对市场份额更是起到了决定性的作用。 作为一名手工酸奶品牌的创始人&#xff0c;目前全国也复制了100多家门店&#xff0c;这篇文章&#xff0c;我将和大家分享品…

浪潮信息携手算力企业为华东产业集群布局提供高质量算力支撑

随着信息技术的飞速发展&#xff0c;算力已成为推动数字经济发展的核心力量。近日&#xff0c;浪潮信息与五家领先的算力运营公司在南京正式签署战略合作协议&#xff0c;共同加速华东地区智算基础设施布局&#xff0c;为区域经济发展注入新动力。 进击的算力 江苏持续加码智算…

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

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

2024 年 6 月区块链游戏研报:Pixels 引发 DAU 波动,行业用户留存率差异显著

作者&#xff1a;Stella L (stellafootprint.network) 数据来源&#xff1a;区块链游戏研究页面 2024 年 6 月&#xff0c;加密货币市场遭遇显著回调&#xff0c;比特币跌幅达 7.3%&#xff0c;以太坊更是下跌了 9.8%。此番波动不可避免地波及区块链游戏领域&#xff0c;导致…

C语言 do while 循环语句练习 中

练习&#xff1a; 4.编写代码&#xff0c;演示多个字符从两端移动&#xff0c;向中间汇聚 // 编写代码&#xff0c;演示多个字符从两端移动&#xff0c;向中间汇聚 //welcome to china!!! //w ! //we !! //wel !!! //.... //welco…

BufferReader/BufferWriter使用时出现的问题

项目场景&#xff1a; 在一个文件中有一些数据&#xff0c;需要读取出来并替换成其他字符再写回文件中&#xff0c;需要用Buffer流。 问题描述 文件中的数据丢失&#xff0c;并且在读取前就为空&#xff0c;读取不到数据。 问题代码&#xff1a; File f new File("D:\\…