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

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


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

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

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

exactInputSingle

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

struct ExactInputSingleParams {
    address tokenIn;   //输入token
    address tokenOut;  //输出token
    uint24 fee;        //手续费率
    address recipient; //收款地址
    uint256 deadline;  //过期时间
    uint256 amountIn;  //指定的输入token数量
    uint256 amountOutMinimum;  //输出token的最小数量
    uint160 sqrtPriceLimitX96; //限定的价格
}

function exactInputSingle(ExactInputSingleParams calldata params)
    external
    payable
    override
    checkDeadline(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 0
    if (recipient == address(0)) recipient = address(this);
		//从路径中解码出第一个池子
    (address tokenIn, address tokenOut, uint24 fee) = data.path.decodeFirstPool();
		//当tokenIn<tokenOUt时,则说明tokenIn为token0,所以是要将token0兑换成token1
    bool 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)
        );
		//返回amountOut
    return 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;   //输入token
    address tokenOut;  //输出token
    uint24 fee;        //手续费率
    address recipient; //收款地址
    uint256 deadline;  //过期时间
    uint256 amountOut; //指定的输出token数量
    uint256 amountInMaximum;   //输入token的最大数量
    uint160 sqrtPriceLimitX96; //限定的价格
}

function exactOutputSingle(ExactOutputSingleParams calldata params)
    external
    payable
    override
    checkDeadline(params.deadline)
    returns (uint256 amountIn)
{
    // avoid an SLOAD by using the swap return data
    amountIn = 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 case
    amountInCached = 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 0
    if (recipient == address(0)) recipient = address(this);
		//从路径中解码出第一个池子
    (address tokenOut, address tokenIn, uint24 fee) = data.path.decodeFirstPool();
		//是否token0兑换token1
    bool 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和amountOut
    uint256 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 away
    if (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)
    external
    payable
    override
    checkDeadline(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的重新赋值给amountOut
            amountOut = 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)
    external
    payable
    override
    checkDeadline(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 frames
    exactOutputInternal(
        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 pay
        if (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 reversed
            pay(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 合约也讲解完了。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Vue进阶(四十五)Jest集成指南

文章目录 一、前言二、环境检测三、集成问题汇总四、拓展阅读 一、前言 在前期博文《Vue进阶&#xff08;八十八&#xff09;Jest》中&#xff0c;讲解了Jest基本用法及应用示例。一切顺利的话&#xff0c;按照文档集成应用即可&#xff0c;但是集成过程中遇到的问题可能五花八…

【WEB前端2024】3D智体编程:乔布斯3D纪念馆-第55课-芝麻开门(语音 识别 控制3D纪念馆开门 和 关门)

【WEB前端2024】3D智体编程&#xff1a;乔布斯3D纪念馆-第55课-芝麻开门&#xff08;语音识别控制3D纪念馆开门和关门&#xff09; 使用dtns.network德塔世界&#xff08;开源的智体世界引擎&#xff09;&#xff0c;策划和设计《乔布斯超大型的开源3D纪念馆》的系列教程。dtn…

KVM使用命令行添加新磁盘(注:支持热插拔)

1、使用qemu-img创建格式为qcow2的磁盘 [rootkvm ~]# qemu-img create -f qcow2 /var/lib/libvirt/images/test-disk.qcow2 15G 2、显示虚拟机硬盘列表&#xff0c;查看未使用的target [rootkvm ~]# virsh domblklist kvm-client 3、添加硬盘到kvm-client虚拟机中 [rootkvm…

SpringBoot | 大新闻项目后端(redis优化登录)

该项目的前篇内容的使用jwt令牌实现登录认证&#xff0c;使用Md5加密实现注册&#xff0c;在上一篇&#xff1a;http://t.csdnimg.cn/vn3rB 该篇主要内容&#xff1a;redis优化登录和ThreadLocal提供线程局部变量&#xff0c;以及该大新闻项目的主要代码。 redis优化登录 其实…

html+css+js图片手动轮播

源代码在界面图片后面 轮播演示用的几张图片是Bing上的&#xff0c;直接用的几张图片的URL&#xff0c;谁加载可能需要等一下&#xff0c;现实中替换成自己的图片即可 关注一下点个赞吧&#x1f604; 谢谢大佬 界面图片 源代码 <!DOCTYPE html> <html lang&quo…

C++继承初识

一。继承 1.继承本质是复用相同的代码&#xff08;属性&#xff09; 2.格式&#xff1a;class 类名&#xff1a;继承方式 父类 3.继承方式的规律&#xff1a; 父类的&#xff1a; 对于私有成员&#xff0c;不管哪种继承方式都不可见--->不想被子类继承的成员 对于保护…

代码随想录——划分字母区间(Leetcode763)

题目链接 贪心 class Solution {public List<Integer> partitionLabels(String s) {int[] count new int[27];Arrays.fill(count,0);// 统计元素最后一次出现的位置for(int i 0; i < s.length(); i){count[s.charAt(i) - a] i;}List<Integer> res new Ar…

非对称加密算法原理与应用2——RSA私钥加密文件

作者:私语茶馆 1.相关章节 (1)非对称加密算法原理与应用1——秘钥的生成-CSDN博客 第一章节讲述的是创建秘钥对,并将公钥和私钥导出为文件格式存储。 本章节继续讲如何利用私钥加密内容,包括从密钥库或文件中读取私钥,并用RSA算法加密文件和String。 2.私钥加密的概述…

JDK都出到20多了,你还不会使用JDK8的Stream流写代码吗?

目录 前言 Stream流 是什么&#xff1f; 为什么要用Steam流 常见stream流使用案例 映射 map() & 集合 collect() 单字段映射 多字段映射 映射为其他的对象 映射为 Map 去重 distinct() 过滤 filter() Stream流的其他方法 使用Stream流的弊端 前言 当你某天看…

深度学习模型加密python版本

支持加密的模型: # torch、torch script、onnx、tensorrt 、torch2trt、tensorflow、tensorflow2tensorrt、paddlepaddle、paddle2tensorrt 深度学习推理模型通常以文件的形式进行保存&#xff0c;相应的推理引擎通过读取模型文件并反序列化即可进行推理过程. 这样一来&#…

跨平台Ribbon UI组件QtitanRibbon全新发布v6.7.0——支持Qt 6.6.3

没有Microsoft在其办公解决方案中提供的界面&#xff0c;就无法想象现代应用程序&#xff0c;这个概念称为Ribbon UI&#xff0c;目前它是使应用程序与时俱进的主要属性。QtitanRibbon是一款遵循Microsoft Ribbon UI Paradigm for Qt技术的Ribbon UI组件&#xff0c;QtitanRibb…

vue3【实战】来回拖拽放置图片

效果预览 技术要点 img 标签默认就是可拖拽的&#xff08;a 标签也是&#xff09;事件 e 内的 dataTransfer 对象可用于临时存储事件过程中的数据拖拽事件的默认行为是用浏览器新开页签打开被拖拽对象&#xff0c;所以通常需要禁用默认的浏览器行为被拖拽元素必须设置 id&#…

拉曼光谱入门:2.拉曼光谱发展史、拉曼效应与试样温度的确定方法

1.拉曼光谱技术发展史 这里用简单的箭头与关键字来概括一下拉曼光谱技术的发展史 1928年&#xff1a;拉曼效应的发现 → 拉曼光谱术的初步应用20世纪40年代&#xff1a;红外光谱术的发展 → 拉曼光谱术的限制20世纪60年代&#xff1a;激光作为光源的引入 → 拉曼光谱术的性能提…

阿里云人工智能平台PAI部署开源大模型chatglm3之失败记录

想学习怎么部署大模型&#xff0c;跟着网上的帖子部署了一个星期&#xff0c;然而没有成功。失败的经历也是经历&#xff0c;记在这里。 我一共创建了3个实例来部署chatglm3&#xff0c;每个实例都是基于V100创建的&#xff08;当时没有A10可选了&#xff09;&#xff0c;其显…

数据库缓存管理

1. 简介 缓存管理器是数据库管理系统&#xff08;DBMS&#xff09;中负责管理内存中page并处理文件和索引管理器的page请求的组件。由于内存空间有限&#xff0c;我们不能将所有page存储在缓存池中。因此&#xff0c;缓存管理器需要制定替换策略&#xff0c;当空间填满时选择哪…

rider使用libman

问题 rider没有libman的相关功能&#xff0c;需要使用cli 安装Libman dotnet tool install -g Microsoft.Web.LibraryManager.Cli # 如果存在可以尝试更新 dotnet tool update -g Microsoft.Web.LibraryManager.Cli查看命令 libman --help初始化 cdnjs官网 libman init安…

【十三】图解 Spring 核心数据结构:BeanDefinition 其二

图解 Spring 核心数据结构&#xff1a;BeanDefinition 其二 概述 前面写过一篇相关文章作为开篇介绍了一下BeanDefinition&#xff0c;本篇将深入细节来向读者展示BeanDefinition的设计&#xff0c;让我们一起来揭开日常开发中使用的bean的神秘面纱&#xff0c;深入细节透彻理解…

CTFShow的RE题(三)

数学不及格 strtol 函数 long strtol(char str, char **endptr, int base); 将字符串转换为长整型 就是解这个方程组了 主要就是 v4, v9的关系&#xff0c; 3v9-(v10v11v12)62d10d4673 v4 v12 v11 v10 0x13A31412F8C 得到 3*v9v419D024E75FF(1773860189695) 重点&…

刷代码随想录有感(127):动态规划——判断是否为子序列

题干&#xff1a; 代码&#xff1a; class Solution { public:bool isSubsequence(string s, string t) {vector<vector<int>>dp(s.size() 1, vector<int>(t.size() 1, 0));for(int i 1; i < s.size(); i){for(int j 1; j < t.size(); j){if(s[i …

方法引用详解

什么是方法引用&#xff1f;&#xff1a;针对于函数式接口中的抽象方法 为什么用方法引用&#xff1f;&#xff1a;避免代码的重复&#xff0c;简便书写&#xff0c;提高效率 在使用Lambda表达式的时候&#xff0c;我们实际上传递进去的代码就是一种解决方案&#xff1a;拿参数…