首页 / 资讯中心 / 文章详情

Dapp-Learning 合约导读:Uniswap V3 SwapRouter 交易路由源码深度解析

Dapp-Learning 合约导读:Uniswap V3 SwapRouter 交易路由源码深度解析 ★ FEATURED ARTICLE
示例工程区块链【免费下载链接】Dapp-LearningDapp learning project for developers at all stages. Becoming and cultivating sovereign individuals. Nonprofit organization.项目地址https://gitcode.com/gh_mirrors/da/Dapp-Learning点击查看免费下载导读本文基于 Dapp-Learning 仓库 defi/Uniswap-V3/contractGuide/SwapRouter.md 展开系统拆解 Uniswap V3 外围合约periphery中SwapRouter的核心实现从ExactInputParams/SwapCallbackData两个关键结构体到exactInput多跳交易主流程、exactInputInternal单池交易内部函数、uniswapV3SwapCallback回调机制再到hasMultiplePools/decodeFirstPool/toAddress/toUint24等路径编码解析工具与checkDeadline修饰器。读完本文你将掌握 SwapRouter 在 Uniswap V3 架构中的定位、多跳路径交易的完整调用链与回调结算原理并能结合仓库内的接口定义、MethodID 表与部署地址直接开展合约级调试与集成。一、SwapRouter 在 Uniswap V3 架构中的定位Uniswap v3 在代码层面将合约拆分为两个仓库uniswap-v3-core与uniswap-v3-periphery其分工如下参见 contractGuide/readme.mdcore 层UniswapV3Factory提供创建 Pool 的接口并追踪所有 PoolUniswapV3Pool实现代币交易、流动性管理、交易手续费收取与 oracle 数据管理。其接口实现粒度很低不适合普通用户直接调用错误的调用可能造成经济损失。periphery 层SwapRouter提供代币交易接口是对UniswapV3Pool中交易相关接口的进一步封装前端界面主要与该合约对接NonfungiblePositionManager负责增加/移除/修改 Pool 流动性并通过 ERC721 NFT 将流动性代币化。SwapRouter的核心职责可以概括为接受用户给出的交易路径path与输入数量自动完成一笔或多笔 Pool 间串接交易并在回调中完成 token 结算。它解决了UniswapV3Pool.swap接口粒度过低、需要调用方自行处理多池串联的问题。二、核心数据结构ExactInputParams 与 SwapCallbackDataExactInputParams精确输入交易的参数载体exactInput的全部入参被封装进ExactInputParams结构体struct ExactInputParams { bytes path; // 路径 address recipient; // 收款地址 uint256 deadline; // 交易有效期 uint256 amountIn; // 输入的 token 数输入的 token 地址就是 path 中的第一个地址 uint256 amountOutMinimum; // 预期交易最少获得的 token 数获得的 token 地址就是 path 中最后一个地址 }各字段的实战含义字段类型说明pathbytes编码后的交易路径由「token 地址 fee 费率 token 地址」交替拼接而成见下文路径编码章节recipientaddress最终收款地址中间跳的收款方为 Router 合约自身由合约托管避免代币在中间环节落入用户地址产生额外转账deadlineuint256交易有效期区块时间戳超时则整个交易回滚防止交易被延迟打包造成滑点损失amountInuint256输入 token 数量输入 token 即path中第一个地址amountOutMinimumuint256最少输出数量即滑点保护下限输出 token 是path中最后一个地址对照仓库中的接口定义 basic/21-aave-uni-loan/contracts/interfaces/uniswap-v3/ISwapRouter.sol 可以看到同一结构体还被用于exactInput的外部接口签名且接口声明了exactInputSingle、exactOutputSingle、exactOutput三个姊妹方法分别对应单池精确输入、单池精确输出与多跳精确输出场景。exactOutput的ExactOutputParams中相应字段为amountOut与amountInMaximum最多输入上限语义与精确输入对称。SwapCallbackData交易回调函数的入参结构体struct SwapCallbackData { bytes path; address payer; }path当前这一跳hop对应的 Pool 路径片段回调时用它解析出tokenIn / tokenOut / fee以完成验证与结算payer负责为本次交易付款的地址。第一跳的 payer 是调用者msg.sender后续跳的 payer 切换为 Router 合约自身。该结构体通过abi.encode(data)编码后作为Pool.swap的data参数传入Pool 执行完交易后会在回调里原样解码使用。三、exactInput多跳路径交易主流程exactInput是 SwapRouter 最核心的对外交易方法通过 while 循环逐跳执行交易直至遍历完整个路径/// inheritdoc ISwapRouter function exactInput(ExactInputParams memory params) external payable override checkDeadline(params.deadline) returns (uint256 amountOut) { address payer msg.sender; // msg.sender pays for the first hop // 通过循环遍历传入的路径进行交易 while (true) { // 交易路径是否存在2个及2个以上的Pool地址 bool hasMultiplePools params.path.hasMultiplePools(); // the outputs of prior swaps become the inputs to subsequent ones params.amountIn exactInputInternal( params.amountIn, hasMultiplePools ? address(this) : params.recipient, // for intermediate swaps, this contract custodies 0, // 传入价格 0 代表以市价交易 SwapCallbackData({ path: params.path.getFirstPool(), // only the first pool in the path is necessary payer: payer }) ); // decide whether to continue or terminate // 判断path中是否还有未交易的token地址即是否需要继续交易 if (hasMultiplePools) { // 继续循环需要将交易者设置为本合约地址 payer address(this); // at this point, the caller has paid params.path params.path.skipToken(); } else { // 交易完成跳出循环 amountOut params.amountIn; break; } } // 检查交易输出量不能过小 require(amountOut params.amountOutMinimum, Too little received); }执行过程可分解为以下关键步骤同时可参考 contractGuide/readme.md 的流程梳理初始化 payer第一跳由调用者msg.sender付款进入 while 循环对当前第一个交易对执行具体交易判断hasMultiplePools即path.length MULTIPLE_POOLS_MIN_LENGTH3 个 token 地址 2 个 fee 的字节长度判断是否还有第二个 Pool调用exactInputInternal执行当前跳的实际兑换若存在多池中间跳的recipient设为address(this)由 Router 合约托管中间输出避免代币在多池之间反复进出用户地址sqrtPriceLimitX96传 0表示以市价交易无价格上限限制回调数据只携带当前跳的 Pool 片段getFirstPool()与 payer决定循环去留若还有下一跳将 payer 切换为address(this)此时调用者已付完款后续由合约垫付并通过skipToken()剔除已消费的 token 与 fee回到第 2 步继续否则跳出循环amountOut即为最终输出数量滑点保护最终require(amountOut params.amountOutMinimum, Too little received)校验实际输出不小于用户设定的最小值防止路径价格剧烈波动造成损失。路径选择的实战背景在链下计算交易路径时例如ETH - DAI可以直接通过ETH/DAI交易池完成单池tokenAddress0 fee0 tokenAddress1也可以选择ETH - USDC - DAI路径即依次经过ETH/USDC、USDC/DAI两个交易池多池tokenAddress0 fee0 tokenAddress1 fee1 tokenAddress2其中间代币即 Base tokentoken 地址在 path 中没有排序限制zeroForOne的方向由地址大小比较决定。四、exactInputInternal单池交易内部函数exactInputInternal是exactInput的私有内部函数负责「解析路径片段 → 计算 Pool 地址 → 调用 Pool.swap 执行单池兑换」/// dev Performs a single exact input swap function exactInputInternal( uint256 amountIn, address recipient, uint160 sqrtPriceLimitX96, SwapCallbackData memory data ) private returns (uint256 amountOut) { // allow swapping to the router address with address 0 // 允许0地址作为接受者 自动转为本合约地址 if (recipient address(0)) recipient address(this); // 从path中解析出Pool的关键信息 // 这里从交易链路path中解析出的地址有可能 tokenIn tokenOut (address tokenIn, address tokenOut, uint24 fee) data.path.decodeFirstPool(); // 判断两个token地址大小 // 由于Pool中price始终以 y/x 表示 x地址 y地址 // zeroForOne 实际代表的是Pool.swap中的交易方向 // true token x - token y // false token y - token x bool zeroForOne tokenIn tokenOut; // 调用Pool.swap 返回实际交易的token数量 // amount0 输入的实际数量 // amount1 输出的实际数量 // 执行完成后 Pool 会调用 uniswapV3SwapCallback 回调函数 (int256 amount0, int256 amount1) getPool(tokenIn, tokenOut, fee).swap( recipient, zeroForOne, amountIn.toInt256(), // 传入价格 0 代表以市价交易 sqrtPriceLimitX96 0 ? (zeroForOne ? TickMath.MIN_SQRT_RATIO 1 : TickMath.MAX_SQRT_RATIO - 1) : sqrtPriceLimitX96, abi.encode(data) ); return uint256(-(zeroForOne ? amount1 : amount0)); }关键设计点0 地址兜底recipient address(0)时自动替换为 Router 合约地址方向判定Pool 中价格始终以y/x表示address(x) address(y)因此zeroForOne tokenIn tokenOut。zeroForOne true表示 token x 换 token y反之 token y 换 token x市价交易语义sqrtPriceLimitX96 0时将价格限制分别设为TickMath.MIN_SQRT_RATIO 1x→y 方向或TickMath.MAX_SQRT_RATIO - 1y→x 方向即不做价格上限约束返回值符号处理Pool.swap返回的amount0/amount1中输出方向的值为负数因此用uint256(-(...))转为正数作为amountOutgetPool 依据getPool(tokenIn, tokenOut, fee)使用与 UniswapV3Factory 相同的池地址索引逻辑即mapping(address mapping(address mapping(uint24 address)))Pool 地址可在链下用 CREATE2 预先计算。Pool.swap 的执行细节exactInputInternal最终落点是 UniswapV3Pool.md 中的swap函数function swap( address recipient, // 交易输出token的接收者 bool zeroForOne, // 交易方向 int256 amountSpecified, // 输入的token数量 uint160 sqrtPriceLimitX96, // 交易的价格限制超出即停止交易 bytes calldata data // 回调函数的入参数据 ) external override noDelegateCall returns (int256 amount0, int256 amount1)Pool.swap 内部的执行特征amountSpecified 0表示 exactInput 模式负数表示 exactOutput 模式交易在 while 循环中分步执行直到输入 token 耗尽或触及价格限制见 UniswapV3Pool.md每一步通过tickBitmap.nextInitializedTickWithinOneWord寻找下一个已初始化 tick用SwapMath.computeSwapStep计算该步的输入、输出与手续费交易完成后先把输出 token 转给接收者再调用回调函数收回输入 token见 UniswapV3Pool.md这正是 SwapRouter 回调结算的基础。五、uniswapV3SwapCallback回调结算机制Pool.swap执行完成后会回调msg.sender即 SwapRouter的uniswapV3SwapCallback。该回调只负责执行当前跳第一个 token 的转账/// inheritdoc IUniswapV3SwapCallback function uniswapV3SwapCallback( int256 amount0Delta, int256 amount1Delta, bytes calldata _data ) external override { require(amount0Delta 0 || amount1Delta 0); // swaps entirely within 0-liquidity regions are not supported SwapCallbackData memory data abi.decode(_data, (SwapCallbackData)); (address tokenIn, address tokenOut, uint24 fee) data.path.decodeFirstPool(); 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()) { 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); } } }回调处理逻辑解读非零校验require(amount0Delta 0 || amount1Delta 0)拒绝「完全发生在 0 流动性区间内」的交换解码与验证abi.decode还原SwapCallbackData解析出当前 Pool 的tokenIn / tokenOut / fee并调用CallbackValidation.verifyCallback(factory, tokenIn, tokenOut, fee)校验调用者确实是 Factory 注册的合法 Pool防止伪造合约钓鱼判定方向amount0Delta 0时isExactInput (tokenIn tokenOut)否则为(tokenOut tokenIn)精确输入分支直接调用pay(tokenIn, data.payer, msg.sender, amountToPay)把输入 token 从 payer 转给 Pool——即执行exactInput时token 直接从付款方用户或 Router 合约转入 Pool精确输出分支若路径还有剩余 Pool则调用exactOutputInternal发起下一跳的精确输出交易否则记录amountInCached并反向支付。回调与闪电贷的关系在 Uniswap v3 中Pool.swap「先转出输出 token、再通过回调收回输入 token」的机制天然实现了借出与归还不同币种的闪电贷接收者可以先拿到tokenOut在同一个交易区块内完成套利等操作后再归还tokenIn而Pool.flash则是借出与归还是同一币种的经典闪电贷接口详见 UniswapV3Pool.md 与 contractGuide/readme.md 的 flash swap 小节。六、路径解析工具函数hasMultiplePools判断路径是否包含多个 Pool/// notice Returns true iff the path contains two or more pools /// param path The encoded swap path /// return True if path contains two or more pools, otherwise false function hasMultiplePools(bytes memory path) internal pure returns (bool) { // MULTIPLE_POOLS_MIN_LENGTH 2个Pool地址1个fee费率的字节长度 return path.length MULTIPLE_POOLS_MIN_LENGTH; }MULTIPLE_POOLS_MIN_LENGTH对应「2 个 token 地址 1 个 fee」的长度2 * 20 3 43字节。即只有 1 个 Pool 时 path 长度为20 3 20 43字节一旦长度达到 43 字节说明路径中存在至少 2 个 Pool。decodeFirstPool解析路径中的第一个 Pool/// notice Decodes the first pool in path /// param path The bytes encoded swap path /// return tokenA The first token of the given pool /// return tokenB The second token of the given pool /// return fee The fee level of the pool function decodeFirstPool(bytes memory path) internal pure returns ( address tokenA, address tokenB, uint24 fee ) { // 返回第一个地址 tokenA path.toAddress(0); fee path.toUint24(ADDR_SIZE); tokenB path.toAddress(NEXT_OFFSET); }其中ADDR_SIZE 20、NEXT_OFFSET ADDR_SIZE 3fee 占 3 字节。也就是说path 的第 019 字节是tokenA第 2022 字节是fee第 2342 字节是tokenB。path.toAddress从 bytes 中读取地址function toAddress(bytes memory _bytes, uint256 _start) internal pure returns (address) { // 检查 _start20 是否会溢出超过uint256的最大值 // 若溢出则为负数 require(_start 20 _start, toAddress_overflow); // 检查_start 起始位 20字节地址类型的长度 不能超过bytes的总长度 require(_bytes.length _start 20, toAddress_outOfBounds); address tempAddress; assembly { // add(_bytes, 0x20) // 从 _bytes 指针开始向后越过32字节16进制0x20 // 即 跳过内存中表达 _bytes 长度的区域一个uint256数值 // add(..., _start) // 加上入参设置的读取起始位 // div(mload(...), 0x1000000000000000000000000) 这里有24个0即 2^96 // 将mload的结果右移96位12个字节 // 因为mload取出的数据是uint256类型32个字节而address类型是20个字节 // 结果会自动在后面补0即后12个字节96位补0所以这里需要用除法将结果右移96位 tempAddress : div(mload(add(add(_bytes, 0x20), _start)), 0x1000000000000000000000000) } return tempAddress; }原理解析bytes在内存中的前 32 字节存储的是 bytes 的长度数值uint256所以读取数据前要先加上0x2032 字节越过该长度区域address 是 uint16020 个字节但mload取出的是一个 uint25632 个字节低 20 字节是有效数据、高 12 个字节自动补 0因此需要用除法将结果右移 96 位12 字节 × 8 位 96 位右移 96 位等价于除以 2^96即十六进制的0x10000000000000000000000001 后面跟 24 个 0。关于该内存编码格式的官方说明可参考 Solidity 文档《Formal Specification of the Encoding》ABI 规范章节。toUint24从 bytes 中读取 fee 费率function toUint24(bytes memory _bytes, uint256 _start) internal pure returns (uint24) { require(_start 3 _start, toUint24_overflow); require(_bytes.length _start 3, toUint24_outOfBounds); uint24 tempUint; assembly { tempUint : mload(add(add(_bytes, 0x3), _start)) } return tempUint; }与toAddress同理uint24占 3 个字节读取时同样需要越过 bytes 的 32 字节长度区域。toUint24返回的即 Pool 的 fee 费率值以百万分之一为基数如 0.3% 对应 300。路径编码的 gas 优化动机为什么 SwapRouter 要把路径编码进bytes而非直接传递结构体数组仓库中的 PathEncoding.md 给出了量化分析EVM 交易 calldata 中零字节与非零字节的 intrinsic gas 分别为 4 wei 与 16 weiEIP-2028 之后而 Solidity 的 ABI 编码按 32 字节对齐会引入大量补零字节。以 4 个「pool fee」对为例原始 ABI 编码两个数组消耗约292 * 4 92 * 16 2640gas结构体数组编码消耗约230 * 4 90 * 16 2360gas省 280Uniswap V3 采用定长类型直接拼接、不做 padding 的 bytes 编码仅消耗约66 * 4 90 * 16 1704gas无效数据占比从约 40% 降至约 15%。这正是path采用紧凑 bytes 编码、并在使用时才通过toAddress/toUint24解码的原因——用链上解码的少量 gas 换取 calldata 传输成本的显著下降。七、checkDeadline交易有效期保护modifier checkDeadline(uint256 deadline) { require(_blockTimestamp() deadline, Transaction too old); _; }当区块时间戳超过deadline时被修饰的函数终止执行并回滚报错Transaction too old作用防止交易因 mempool 拥堵被延迟打包导致成交价格与用户下单时预期的价格严重偏离在exactInput、exactOutput等对外交易方法上均通过checkDeadline(params.deadline)施加保护。八、SwapRouter 的接口面与部署地址SwapRouter 完整的外部接口含 MethodID数据来自 defi/Uniswap-V3/InterfaceGuide/InfoList.md方法MethodIDexactInput(tuple(bytes,address,uint256,uint256,uint256))0xc04b8d59exactInputSingle(tuple(address,address,uint24,address,uint256,uint256,uint256,uint160))0x414bf389exactOutput(tuple(bytes,address,uint256,uint256,uint256))0xf28c0498exactOutputSingle(tuple(address,address,uint24,address,uint256,uint256,uint256,uint160))0xdb3e2198uniswapV3SwapCallback(int256,int256,bytes)0xfa461e33multicall(bytes[])/refundETH()/unwrapWETH9(uint256,address)等辅助方法0xac9650d8/0x12210e8a/0x49404b7c等其中exactInputSingle与ExactOutputSingle的入参结构体tokenIn / tokenOut / fee / recipient / deadline / amountIn / amountOutMinimum / sqrtPriceLimitX96可在 ISwapRouter.sol 中查看它们支持传入非零的sqrtPriceLimitX96来设置单池交易的价格上限是精确输入/输出在多池场景外的简化入口。部署信息来源 deploys.mduniswap/v3-peripheryv1.0.0SwapRouter 主网/测试网统一地址0xe79dBeD0F7eA2f3cb60835a81E7771d540E621AD演示环境rinkeby使用的 V3SwapRouter0xE592427A0AEce92De3Edee1F18E0157C05861564见 InfoList.md。九、调用链总览与源码路径索引一次完整的Router.exactInput多跳交易调用链用户 └─ SwapRouter.exactInput (checkDeadline 校验) └─ while 循环逐跳 └─ SwapRouter.exactInputInternal └─ 解析 decodeFirstPool → 计算 zeroForOne └─ Pool.swap分步执行交易 ├─ 输出 token 转给 recipient └─ SwapRouter.uniswapV3SwapCallback回调 ├─ CallbackValidation.verifyCallback 校验 └─ pay(tokenIn, payer, msg.sender) 回收输入 └─ 校验 amountOut amountOutMinimum建议配合 contractGuide/readme.md 中的 ExactInput 程序流程图xmind/ExactInput.png一并阅读。本文涉及的关键源码/文档索引均在当前仓库内SwapRouter.md本文主体UniswapV3Pool.md 的 swap 与回调UniswapV3Factory.md 的池地址索引与 CREATE2 部署PathEncoding.md 路径编码 gas 优化分析fee.md 手续费与费率档位说明ISwapRouter.sol 完整接口定义InfoList.md SwapRouter MethodID 对照表结语SwapRouter 是 Uniswap V3 交易体验的「门面」它用紧凑的 bytes 路径编码节省 calldata gas用 while 循环串联多池交易用回调机制完成「先出后进」的 token 结算并用deadline与amountOutMinimum双保险保护交易者。理解exactInput → exactInputInternal → Pool.swap → uniswapV3SwapCallback这条完整调用链是深入掌握 Uniswap V3 外围合约、以及开发聚合交易与套利机器人的基础。赞分享示例工程区块链【免费下载链接】Dapp-LearningDapp learning project for developers at all stages. Becoming and cultivating sovereign individuals. Nonprofit organization.项目地址https://gitcode.com/gh_mirrors/da/Dapp-Learning点击查看免费下载相关推荐深入解读 Uniswap V2 Router02路由合约的流动性添加、移除与兑换全流程Dapp-Learning 源码研读深入解读 Uniswap V2 Router02路由合约的流动性添加、移除与兑换全流程Dapp Learning 源码研读 本文以 Dapp Learni示例工程区块链Uniswap V3 闪电贷实战Dapp-Learning 闪电贷合约源码解析与回调机制Uniswap V3 闪电贷实战Dapp Learning 闪电贷合约源码解析与回调机制 本文基于 Dapp Learning 仓库中 uniswapv3 闪示例工程区块链Dapp-Learning 去中心化交易所实战EtherDelta 订单簿合约与 Uniswap V1 AMM 合约全解析Dapp Learning 去中心化交易所实战EtherDelta 订单簿合约与 Uniswap V1 AMM 合约全解析 导读 去中心化交易所DEX是示例工程区块链上一篇Angular Loading Bar终极指南10个常见问题解决方案与实用技巧下一篇如何使用 Kit 进行PR自动化审查开发者必备工作流创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
阅读完成 · 觉得有帮助?
咨询建站