Uniswap v2 路由器代码解析

本文详细介绍了Uniswap V2的Router合约,包括其功能如安全地铸造和销毁LP代币、交换代币、处理ETH和滑点检查等,并解释了Router02相对于Router01新增的费转移代币支持功能。文章还探讨了添加和移除流动性的内部机制,以及与UniswapV2Library的交互。

Router 合约为用户提供了一个面向用户的智能合约,用于

  • 安全地 铸造和销毁 LP 代币(添加和移除流动性)
  • 安全地 交换成对代币
  • 通过与包装以太(WETH)ERC20 合约集成,增加了交换 Ether 的能力。
  • 增加了核心合约中遗漏的与 滑点 相关的安全检查。
  • 添加了对转账费用代币的支持。

Router02 是 Router01 所有功能的扩展,增加了对转账费用代币的支持

当我们第一次打开外围仓库中的合约文件夹时,我们会看到三个合约。

uniswap v2 router github 截图

Router02 是 Router01,增加了对转账费用代币的附加功能。当我们查看 Router02 的接口时,可以看到它继承自 Router01(红框)(这意味着它实现了所有的功能),并具有以下附加功能,所有这些功能都是用于支持转账费用代币进行操作的(黄色高亮)。

uniswap v2 router 继承

swapExactTokensForTokens 和 swapTokensForExactTokens

让我们从交换代币的 Router 函数开始。这个功能有两个函数来完成(绿色高亮)。

uniswap v2 router 交换

这两个函数名称的区别如下:

  • swapExactTokensForTokens 中,“第一个代币是确切的”意味着你交换的输入代币数量是固定的。
  • swapTokensForExactTokens 中,“第二个代币是确切的”表示你希望接收的输出代币数量是固定的。

如果用户只能交换两种代币,那么他们将向这些函数提供一个 address[] calldata path 数组(用蓝色高亮标出)[address(tokenIn), address(tokenOut)]。如果他们跨池跳转,则需要指定 [address(tokenIn), address(intermediateToken), …, address(tokenOut)]

swapExactTokensForTokens

swap**Exact**TokensForTokens 的情况下,用户确切地指定他们将存入的第一个代币的数量,以及他们愿意接受的输出代币的最低数量。

例如,假设我们想要以 25 个 token0 交换 50 个 token1。如果这是当前状态下的确切价格,那么在我们的交易确认之前,价格在变化时没有容错余地,可能会导致回滚。因此,我们改为指定最低输出为 49.5 token1,隐含留下 1% 的容错。

swapTokensForExactTokens

在这种情况下,我们指定我们希望确切获得 50 个 token1,但是我们愿意最多交易 25.5 个 token0 来获取它。

使用哪个交换函数?

大多数使用 EOA 的用户可能会选择使用确切输入函数,因为他们需要进行批准步骤,如果他们需要输入超过批准的数量,交易就会失败。通过使用确切输入,他们可以批准确切的数量。然而,与 Uniswap 集成的智能合约可能会有更复杂的需求,因此路由器为它们提供了两个选项。

交换是如何工作的

当输入是确切的(swapExactTokensForTokens)时,该函数预测单个交换或一系列交换的预期输出。如果结果输出低于用户指定的数量,函数会回滚。反之对于确切输出:它计算所需输入,并在超过用户指定的阈值时回滚。

然后,两个函数都会将用户的代币转移到成对合约中(请记住,Uniswap V2 Pair 需要在调用成对合约函数 swap() 之前将代币发送到合约)。最后,它们都调用下一个讨论的内部 _swap() 函数。

uniswap v2 router swap exact

_swap() 函数

在内部,所有公开的以 swap() 为名称的函数都会调用下面展示的 _swap() 内部函数。

请记住,核心 swap 函数 的函数签名指定了两个代币的 amountOutamountIn 由函数调用之前转移的金额隐含表示。

uniswap v2 router 内部 swap 函数

_addLiquidity

记住添加流动性的安全检查吗?具体来说,我们希望确保以完全相同的比例存入两种代币,否则我们铸造的 LP 代币数量将是我们提供的数量与成对余额之间的比率中较差的一个。然而,在流动性提供者尝试添加流动性和交易确认之间,这个比例可能发生变化。

为了防范这种情况,流动性提供者必须提供(作为参数)他们希望存入的 token0 和 token1 的最低余额(UniswapV2 称为 amountAMinamountBMin)。然后,他们转移的金额必须高于这些最小值(UnsiwapV2 称为 amountADesiredamountBDesired)。如果成对比例发生变化,以至于不再尊重最小值,则交易会回滚。

_addLiquidity 会取 amountADesired 并计算将尊重比例的正确 tokenB 数量。如果这个数量高于 amountBDesired(流动性提供者发送的 B 数量),那么将首先使用 amountBDesired 并计算最佳的 B 数量。逻辑如下所示。请注意,添加流动性可能会创建一个新的成对合约(如果尚不存在的话)。

uniswap v2 router 添加流动性内部函数

例如,假设当前成对余额为 100 token0 和 300 token1。我们希望分别添加 20 和 60 token0token1,但成对比率可能会发生变化。所以我们先批准路由器 21 token0 和 63 token1,同时声明我们希望的最低存入量为 20 和 60。如果比率变化导致最优存入 token0 的数量为 19.9,则交易会回滚。

请记住,我们说过 quote 不应作为预言机使用,这仍然是正确的。然而,就添加流动性而言,我们对之前价格的平均不感兴趣,而仅关注现在的价格(池比例),因为流动性提供者必须遵循它。

addLiquidity() 和 addLiquidityEth()

这些函数应该不需解释。它们首先使用上面的 _addLiquidity 计算最佳比率,然后将资产转移到成对合约中,然后在成对合约上调用 mint。唯一的区别是 addLiquidityEth 函数会首先将 Ether 包装成 ETH。

uniswap add liquidity with eth and weth

移除流动性

移除流动性调用 burn,但使用参数 amountAMinamountBMin(红色高亮)作为安全检查,以确保流动性提供者获得他们预期的代币数量。如果在流动性代币被销毁之前,代币的比率发生剧烈变化,则烧毁代币的用户将无法获得他们预期的 A 或 B 代币数量。

函数 removeLiquidityEth 调用 removeLiquidity(绿色高亮),但设置路由器作为代币接收者。常规 ERC20 代币随后会转移到流动性提供者,WETH 将被解除包装以转换回 ETH,然后返回给流动性提供者。

uniswap v2 router 移除流动性

removeLiquidityWithPermit() 和 removeLiquidityETHWithPermit()

在上方文件的第 109 行中,灰色注释为 send liquidity to pair,此步骤假定成对合约已获得授权,可以从流动性提供者处转移 LP 代币以销毁它们。这意味着烧毁 LP 代币需要首先批准成对合约。此步骤可以通过 permit() 跳过,因为 Uniswap V2 的 LP 代币是 ERC20 Permit Token。函数 removeLiquidityWithPermit() 接收一个签名以批准并一次性烧毁。如果其中一个代币是 WETH,流动性提供者将使用 removeLiquidityETHWithPermit()

Router02:支持转账费用代币

为了处理转账费用代币,路由器不能直接根据 amountIn()(用于交换)或 liquidity()(用于移除流动性)等参数进行计算。添加流动性不受转账费用代币的影响,因为用户仅根据他们实际转移到成对中的金额进行结算。

uniswap v2 router 支持转账费用代币

uniswap v2 支持转账费用移除流动性

UniswapV2Library 的包装器

路由器库中的其余函数是对 UniswapV2Library 函数的包装器,如下所示。

    function quote(uint amountA, uint reserveA, uint reserveB) public pure override returns (uint amountB) {
        return UniswapV2Library.quote(amountA, reserveA, reserveB);
    }

    function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) public pure override returns (uint amountOut) {
        return UniswapV2Library.getAmountOut(amountIn, reserveIn, reserveOut);
    }

    function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) public pure override returns (uint amountIn) {
        return UniswapV2Library.getAmountOut(amountOut, reserveIn, reserveOut);
    }

    function getAmountsOut(uint amountIn, address[] memory path) public view override returns (uint[] memory amounts) {
        return UniswapV2Library.getAmountsOut(factory, amountIn, path);
    }

    function getAmountsIn(uint amountOut, address[] memory path) public view override returns (uint[] memory amounts) {
        return UniswapV2Library.getAmountsIn(factory, amountOut, path);
    }
}

截止时间参数

在 Uniswap V2 路由器中,所有公共函数都有一个截止时间参数。当你在 Uniswap 现在 下单交易时,这意味着你希望以当前价格进行交易。

在编写与 Uniswap 集成的智能合约时,请不要将截止时间设为 block.timestampblock.timestamp 加一个常数。

你的智能合约需要 单独 确保用户提交的交易不会太旧。这意味着你自己的合约需要接收一个截止时间参数并将其转发给 Uniswap,或者在 block.timestamp > deadline 时回滚。

如何利用旧交易

恶意的区块构建者可以“保留”交换交易,并在此类交易有利于操纵价格或以不利价格向用户倾倒代币时,延迟执行这些交易。截止时间参数限制了攻击者可以进行此类恶意操作的时间窗口。截止时间应设定在未来足够远的时间,以确保在拥堵时仍有时间执行交易,但不能过长。这通常意味着截止时间应该是在签署交易后的几分钟内。

然而,如果智能合约不包含截止时间,或者通过忽略截止时间并将当前的 block.timestamp 转发给 Uniswap 来使参数无效,那么用户就不会受到保护。

永远不要将 amountMin 设置为零或 amountMax 设置为 type(uint).max

另一个非常常见的错误是将 amountMin 设置为零或将 amountMax 设置为极高的值。这会破坏对价格滑点和三明治攻击的保护。

结论

Router 合约为带有滑点保护的代币交换提供了一个面向用户的机制,可能跨多个池进行,并增加了对 ETH 和转账费用代币交易的支持(在 Router02 中)。添加流动性不需要考虑转账费用代币,因为 Uniswap 仅根据实际转移到池中的数量进行结算。

添加流动性函数确保用户只按照池的确切比例进行存款。移除流动性可以简单地是将 LP 代币转移到路由器并将其烧毁,或者包括解除包装 WETH 和提取转账费用代币。

此外,还包括通过 ERC20 Permit 支持免 gas 费用的批准。

与 Uniswap 集成的智能合约不得禁用延迟交换和价格滑点的保护。

最早于 2023 年 11 月 10 日发表

  • 原文链接: rareskills.io/post/unisw...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
RareSkills
RareSkills
https://www.rareskills.io/