Skip to main content

外围合约

合约地址: Uniswap/v2-periphery


v2-periphery的项目结构如下

|--
|-examples: 存放一些示例合约,比如TWAP、FlashSwap等
|-interfaces: 接口目录
|-libraries: 库文件
|-test: 测试用例
|-UniswapV2Migrator.sol: 迁移合约,从V1迁移到V2的合约
|-UniswapV2Router01.sol 路由合约V1版本
|-UniswapV2Router02.sol 路由合约V2版本,相比V1版本主要增加了几个支持交税费用的函数

主要看一下合约

  • UniswapV2Library
  • UniswapV2LiquidityMathLibrary
  • UniswapV2OracleLibrary
  • UniswapV2Router02

UniswapV2Library

库合约主要提供了一下方法

  • sortTokens: 对两个 token 进行排序
  • pairFor: 计算出两个 token 的 pair 合约地址
  • getReserves: 获取两个 token 在池子里里的储备量
  • quote: 根据给定的两个 token 的储备量和其中一个 token 数量,计算得到另一个 token 等值的数值
  • getAmountOut: 根据给定的两个 token 的储备量和输入的 token 数量,计算得到输出的 token 数量,该计算会扣减掉 0.3% 的手续费
  • getAmountIn: 根据给定的两个 token 的储备量和输出的 token 数量,计算得到输入的 token 数量,该计算会扣减掉 0.3% 的手续费
  • getAmountsOut: 根据兑换路径和输入数量,计算得到兑换路径中每个交易对的输出数量
  • getAmountsIn: 根据兑换路径和输出数量,计算得到兑换路径中每个交易对的输入数量

sortTokens

// returns sorted token addresses, used to handle return values from pairs sorted in this order
function sortTokens(address tokenA, address tokenB) internal pure returns (address token0, address token1) {
require(tokenA != tokenB, 'UniswapV2Library: IDENTICAL_ADDRESSES');
// 对两个地址进行排序,token0 < token1
(token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
// 因为token0 < token1, 所以只要token0不为address(0) token1就不会是address(0)
require(token0 != address(0), 'UniswapV2Library: ZERO_ADDRESS');
}

以太坊地址类型有两种

  • address: 保存 20 个字节的值(以太坊地址的大小)
  • address payable: 可以接收 ETH 的地址,和address相同,不过有成员函数transfersend

address 允许和 uint160 整型字面常量、bytes20 及合约类型相互转换。 地址类型可以使用运算符: <=, <, ==, !=, >= , >

sortTokens方法就是对两个输入的地址进行排。

pairFor

// calculates the CREATE2 address for a pair without making any external calls
function pairFor(address factory, address tokenA, address tokenB) internal pure returns (address pair) {
(address token0, address token1) = sortTokens(tokenA, tokenB);
pair = address(uint(keccak256(abi.encodePacked(
hex'ff',
factory,
keccak256(abi.encodePacked(token0, token1)),
hex'96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f' // init code hash
))));
}

这个方法中init code hash其实就是UniswapV2Pair合约的creationCodehash值.这个值可以通过以下方法获得

bytes32 public constant INIT_CODE_PAIR_HASH = keccak256(abi.encodePacked(type(UniswapV2Pair).creationCode));

INIT_CODE_PAIR_HASH的值是0x开头的,因为前面加了hex关键字,所以init code hash单引号里的值就需要去掉开头的0x

getAmountOut

给定一个输入量,和储备量,计算出最大可输出数量

// given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset
function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) internal pure returns (uint amountOut) {
require(amountIn > 0, 'UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT');
require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
// 对amountIn收取0.3%的手续费
uint amountInWithFee = amountIn.mul(997);
uint numerator = amountInWithFee.mul(reserveOut);
uint denominator = reserveIn.mul(1000).add(amountInWithFee);
amountOut = numerator / denominator;
}

公式解析: 根据 AMM 的原理,恒定积公式X*Y=K,兑换后 K 值不变。因此在不考虑手续费的情况下,以下公式成立

reserveIn * reserveOut = (reserveIn + amountIn) * (reserveOut - amountOut)

将公式右边表达式展开。公式推导如下

reserveIn * reserveOut = reserveIn * reserveOut + amountIn * reserveOut - (reserveIn + amountIn) * amountOut
=>
amountIn * reserveOut = (reserveIn + amountIn) * amountOut
=>
amountOut = amountIn * reserveOut / (reserveIn + amountIn)

而实际上交易时,还需要扣减千分之三的交易手续费,所以实际上:

amountIn = amountIn * 997 / 1000

代入上面的公式后,最终结果就变成了:

amountOut = (amountIn * 997 / 1000) * reserverOut / (reserveIn + amountIn * 997 / 1000)
=>
amountOut = amountIn * 997 * reserveOut / 1000 * (reserveIn + amountIn * 997 / 1000)
=>
amountOut = amountIn * 997 * reserveOut / (reserveIn * 1000 + amountIn * 997)

这即是最后代码实现中的计算公式了。

getAmountsOut

// performs chained getAmountOut calculations on any number of pairs
function getAmountsOut(address factory, uint amountIn, address[] memory path) internal view returns (uint[] memory amounts) {
require(path.length >= 2, 'UniswapV2Library: INVALID_PATH');
amounts = new uint[](path.length);
// 输出第一项是输入的数量,
amounts[0] = amountIn;
for (uint i; i < path.length - 1; i++) {
(uint reserveIn, uint reserveOut) = getReserves(factory, path[i], path[i + 1]);
amounts[i + 1] = getAmountOut(amounts[i], reserveIn, reserveOut);
}
}

该函数会计算 path 中每一个中间资产和最终资产的数量 比如 path 为 [A,B,C],则会先将 A 兑换成 B,再将 B 兑换成 C。返回值则是一个数组,第一个元素是 A 的数量,即 amountIn,而第二个元素则是兑换到的代币 B 的数量,最后一个元素则是最终要兑换得到的代币 C 的数量。

从代码中还可看到,每一次兑换其实都调用了 getAmountOut 函数,这也意味着每一次中间兑换都会扣减千分之三的交易手续费。那如果兑换两次,实际支付假设为 1000,那最终实际兑换得到的价值只剩下:

1000 * (1 - 0.003) * (1 - 0.003) = 994.009

即实际支付的交易手续费将近千分之六了。兑换路径越长,实际扣减的交易手续费会更多,所以兑换路径一般不宜过长。

UniswapV2LiquidityMathLibrary

UniswapV2OracleLibrary

UniswapV2Router02

添加流动性接口

  • addLiquidity: 将两个 ERC20 代币添加为流动性
  • addLiquidityETH: 将一个ERC20代币和ETH添加为流动性

addLiquidity

// 计算添加流动性时两个token的真正需要支付的数量
function _addLiquidity(
address tokenA,
address tokenB,
uint amountADesired,
uint amountBDesired,
uint amountAMin,
uint amountBMin
) internal virtual returns (uint amountA, uint amountB) {
// create the pair if it doesn't exist yet
// 如果是首次添加流动性,先创建Pair合约
if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) {
IUniswapV2Factory(factory).createPair(tokenA, tokenB);
}
// 查询Pair合约中两个token的储备量
(uint reserveA, uint reserveB) = UniswapV2Library.getReserves(factory, tokenA, tokenB);
// 如果是首次添加流动性,则使用用户期望的输入量作为初始值
if (reserveA == 0 && reserveB == 0) {
(amountA, amountB) = (amountADesired, amountBDesired);
} else {
// 通过tokenA的期望输入量计算tokenB的最佳支付数量
uint amountBOptimal = UniswapV2Library.quote(amountADesired, reserveA, reserveB);
// 最佳支付数量不能超过用户期望的量
if (amountBOptimal <= amountBDesired) {
// 最佳支付数量不能小于需要的最小支付数量
require(amountBOptimal >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
// 使用tokenA的期望输入量和B的最佳输入量作为两个token真正需要支付的数量
(amountA, amountB) = (amountADesired, amountBOptimal);
} else {
// 如果根据tokenA的期望输入量计算失败,则反过来通过tokenB的期望输入量计算
uint amountAOptimal = UniswapV2Library.quote(amountBDesired, reserveB, reserveA);
assert(amountAOptimal <= amountADesired);
require(amountAOptimal >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');
(amountA, amountB) = (amountAOptimal, amountBDesired);
}
}
}
// 添加流动性,由于添加流动性上链交易期间可能会存在价格波动(即存在滑点),所以需要输入期望的输入量和最小可接受的输入量
function addLiquidity(
address tokenA,
address tokenB,
uint amountADesired, // 期望添加tokenA的代币数量
uint amountBDesired, // 期望添加tokenB的代币数量
uint amountAMin, // 可接受tokenA的最小成交量
uint amountBMin, // 可接受tokenB的最小成交量
address to, // LPToken的接收地址
uint deadline // 添加流动性交易的过期时间(到期时间点),防止上链交易时间太长,导致执行交易时价格波动太大。如果到了过了时间才执行,则交易失败
) external virtual override ensure(deadline) returns (uint amountA, uint amountB, uint liquidity) {
// 计算两个token真正执行时需要支付的数量
(amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin);
// 查询pair合约地址
address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
// 将两个token需要支付的数量转给Pair合约
TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA);
TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB);
// 添加成功以后,给to地址发放LPToken
liquidity = IUniswapV2Pair(pair).mint(to);
}

addLiquidityETH

// 将ERC20代币和ETH添加为流动性
// 合约方法接收ETH支付时需要添加payable属性。用户支付的ETH数量通过msg.value获取
function addLiquidityETH(
address token,
uint amountTokenDesired,
uint amountTokenMin,
uint amountETHMin,
address to,
uint deadline
) external virtual override payable ensure(deadline) returns (uint amountToken, uint amountETH, uint liquidity) {
// 计算两个token真实需要支付的数量
(amountToken, amountETH) = _addLiquidity(
token,
WETH,
amountTokenDesired,
msg.value,
amountTokenMin,
amountETHMin
);
// 获取Pair合约地址
address pair = UniswapV2Library.pairFor(factory, token, WETH);
// 将ERC20代币转到Pair合约
TransferHelper.safeTransferFrom(token, msg.sender, pair, amountToken);
// 将ETH兑换成WETH。 ETH->WETH 调用WETH的deposit WETH-> ETH: 调用WETH的withdraw
IWETH(WETH).deposit{value: amountETH}();
// 将WETH转到Pair合约并校验
assert(IWETH(WETH).transfer(pair, amountETH));
// 给to地址发放LPToken
liquidity = IUniswapV2Pair(pair).mint(to);
// refund dust eth, if any
// 如果用户支付的ETH没有用完,把剩余的退还给用户
if (msg.value > amountETH) TransferHelper.safeTransferETH(msg.sender, msg.value - amountETH);
}

移除流动性接口

移除流动性时

  1. 先将LPToken转给Pair合约
  2. 合约根据LPToken所占份额,将交易对的两个 Token 转给用户
  3. Pair合约通过burn方法将LPToken销毁
  • removeLiquidity: 和 addLiquidity 相对应,用LPToken换回两种 ERC20 代币
  • removeLiquidityETH: 和 addLiquidityETH 相对应,换回的其中一种是主币 ETH
  • removeLiquidityWithPermit: 也是换回两种 ERC20 代币,但用户会提供签名数据使用 permit 方式完成授权操作
  • removeLiquidityETHWithPermit: 也是使用 permit 完成授权操作,换回的其中一种是主币 ETH
  • removeLiquidityETHSupportingFeeOnTransferTokens: 功能和 removeLiquidityETH 一样,不同的地方在于支持转账时支付协议费用
  • removeLiquidityETHWithPermitSupportingFeeOnTransferTokens: 功能和上一个函数一样,但支持使用链下签名的方式进行授权

removeLiquidity 是这些接口中最核心的一个,也是其它几个接口的元接口

removeLiquidity

// 移除两个ERC20代币的流动性
function removeLiquidity(
address tokenA,
address tokenB,
uint liquidity, // LPToken数量
uint amountAMin, // 要求最少拿到tokenA的数量
uint amountBMin, // 要求最少拿到tokenB的数量
address to, // 接收tokenA和tokenB的地址
uint deadline // 交易过期时间戳
) public virtual override ensure(deadline) returns (uint amountA, uint amountB) {
// 计算两个token的Pair合约地址
address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
// 将LPToken转给Pair合约
IUniswapV2Pair(pair).transferFrom(msg.sender, pair, liquidity); // send liquidity to pair
// Pair合约调用burn方法,销毁LPToken。burn方法中会将两个Token转给to地址
(uint amount0, uint amount1) = IUniswapV2Pair(pair).burn(to);
// 对两个token地址进行排序
(address token0,) = UniswapV2Library.sortTokens(tokenA, tokenB);
// 对齐[amount0,amount1]分别是tokenA和tokenB的可提取数量
(amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0);
// 减去滑点以后,校验真实可提取数量和用户要求的最小提取数量是否一致,如果不一致则交易失败
require(amountA >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');
require(amountB >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
}

removeLiquidityETH

// 移除ERC2--ETH流动性
function removeLiquidityETH(
address token,
uint liquidity, // LPToken数量
uint amountTokenMin, // 要求至少要拿到的ERC20token的数量
uint amountETHMin, // 要求至少要拿到的ETH的数量
address to, // 接收赎回流动性两个代币的地址
uint deadline // 交易到期时间戳
) public virtual override ensure(deadline) returns (uint amountToken, uint amountETH) {
// 移除流动性以后,拿到两个代币的数量,这里拿到的是WETH和一个ERC20代币
(amountToken, amountETH) = removeLiquidity(
token,
WETH,
liquidity,
amountTokenMin,
amountETHMin,
address(this), // 这里to地址是当前合约本身,所以Pair合约的burn方法会将两个代币转给当前路由合约
deadline
);
// 路由合约拿到token以后,再将token转给用户指定的to地址
TransferHelper.safeTransfer(token, to, amountToken);
// 将WETH-> ETH
IWETH(WETH).withdraw(amountETH);
// 将ETH转给用户指定的to地址
TransferHelper.safeTransferETH(to, amountETH);
}

removeLiquidityWithPermit

通过链下授权给第三方来执行交易的方法。 优点之一: 如果执行移除流动性操作的账号没有 ETH 来支付 gas 费,则只需要签名,然后将签名信息移交给另一个有 ETH 的账号来支付 gas 费并执行上链 这种模式在以太坊上有成熟的解决方案,即 relayer

// 通过链下签名授权(即EIP712协议授权),签名者将签名信息交给第三方来执行上链。
function removeLiquidityWithPermit(
address tokenA,
address tokenB,
uint liquidity,
uint amountAMin,
uint amountBMin,
address to,
uint deadline,
bool approveMax, uint8 v, bytes32 r, bytes32 s //approveMax: 是否限制合约可使用 LPToken 的最大授权数量; v,r,s签名信息
) external virtual override returns (uint amountA, uint amountB) {
address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
// 如果不限定授权数量,则使用uint256最大值作为授权数量
uint value = approveMax ? uint(-1) : liquidity;
// 解析校验签名信息的正确性
IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);
// 执行移除操作
(amountA, amountB) = removeLiquidity(tokenA, tokenB, liquidity, amountAMin, amountBMin, to, deadline);
}

其中解析校验签名的方法在v2-core仓库的UniswapV2ERC20.sol合约中实现,实现方法如下

function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external {
require(deadline >= block.timestamp, 'UniswapV2: EXPIRED');
bytes32 digest = keccak256(
abi.encodePacked(
'\x19\x01',
DOMAIN_SEPARATOR,
keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline))
)
);
address recoveredAddress = ecrecover(digest, v, r, s);
require(recoveredAddress != address(0) && recoveredAddress == owner, 'UniswapV2: INVALID_SIGNATURE');
_approve(owner, spender, value);
}

签名和验签的详细信息可以参考:EIP712 协议

removeLiquidityETHSupportingFeeOnTransferTokens

function removeLiquidityETHSupportingFeeOnTransferTokens(
address token,
uint liquidity,
uint amountTokenMin,
uint amountETHMin,
address to,
uint deadline
) public virtual override ensure(deadline) returns (uint amountETH) {
(, amountETH) = removeLiquidity(
token,
WETH,
liquidity,
amountTokenMin,
amountETHMin,
address(this),
deadline
);
// 这里是将当前合约的token余额全部发给了to地址。
TransferHelper.safeTransfer(token, to, IERC20(token).balanceOf(address(this)));
// WETH->ETH
IWETH(WETH).withdraw(amountETH);
// 将ETH转给to地址
TransferHelper.safeTransferETH(to, amountETH);
}

该函数功能和 removeLiquidityETH 一样,但对比一下,就会发现主要不同点在于:

  1. 返回值没有 amountToken;
  2. 调用 removeLiquidity 后也没有 amountToken 值返回
  3. 进行 safeTransfer 时传值直接读取当前地址的 token 余额。 有一些项目 token,其合约实现上,在进行 transfer 的时候,就会扣减掉部分金额作为费用,或作为税费缴纳,或锁仓处理,或替代 ETH 来支付 GAS 费。总而言之,就是某些 token 在进行转账时是会产生损耗的,实际到账的数额不一定就是传入的数额。该函数主要支持的就是这类 token。

该方法会存在一些捡漏机会,如果监听到有人不小心把 token 转到了该路由合约,则攻击者可以先添加很少数量的流动性,然后再移除流动性.则可以取走当前路由合约所有的 token

兑换交易接口

  • swapExactTokensForTokens: 用 ERC20 兑换 ERC20,但支付的数量是指定的,而兑换回的数量则是未确定的
  • swapTokensForExactTokens: 也是用 ERC20 兑换 ERC20,与上一个函数不同,指定的是兑换回的数量
  • swapExactETHForTokens: 指定 ETH 数量兑换 ERC20
  • swapTokensForExactETH: 用 ERC20 兑换成指定数量的 ETH
  • swapExactTokensForETH: 用指定数量的 ERC20 兑换 ETH
  • swapETHForExactTokens: 用 ETH 兑换指定数量的 ERC20
  • swapExactTokensForTokensSupportingFeeOnTransferTokens: 指定数量的 ERC20 兑换 ERC20,支持转账时扣费
  • swapExactETHForTokensSupportingFeeOnTransferTokens: 指定数量的 ETH 兑换 ERC20,支持转账时扣费
  • swapExactTokensForETHSupportingFeeOnTransferTokens: 指定数量的 ERC20 兑换 ETH,支持转账时扣费

_swap

// requires the initial amount to have already been sent to the first pair
// 遍历所有交易路径,兑换目标token。
function _swap(uint[] memory amounts, address[] memory path, address _to) internal virtual {
// 因为后面取path[i + 1] 所以这里i < path.length - 1 而不是 i <= path.length - 1
for (uint i; i < path.length - 1; i++) {
(address input, address output) = (path[i], path[i + 1]);
// getAmountsOut->getReserves. 在getReserves方法中对token进行了排序,所以amounts数组的顺序和path提供的顺序有可能不一致
(address token0,) = UniswapV2Library.sortTokens(input, output);
uint amountOut = amounts[i + 1];
// 对齐amounts数组和path数组的顺序
(uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOut) : (amountOut, uint(0));
// 如果不是最后一个交易对,则把下一个pair合约地址作为上一个交易对的收款地址
address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to;
// 执行Pair合约提供的swap方法
IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output)).swap(
amount0Out, amount1Out, to, new bytes(0)
);
}
}

_swap方法是其他 swap 方法的核心基础方法.这个方法就是遍历path提供的交易对路径,依次调用pair合约的swap方法进行兑换。 这里有个注意点,传入的amountspath有可能不对齐。因为getAmountsOut方法中调用了getReserves方法。getReserves方法中对两个交易对进行排序并返回排序后的两个储备量。所以(uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOut) : (amountOut, uint(0));这段代码的作用就是将pathamounts对齐后依次传入pair合约的swap方法

swapExactTokensForTokens

// 以固定数量的输入token尽可能买最多的输出token
function swapExactTokensForTokens(
uint amountIn, // 输入数量
uint amountOutMin, // 经过滑点动态计算后的最小值
address[] calldata path, // 交易对路径。例如[WETH,USDT,UNI],兑换时WETH->USDT->UNI
address to, // 收款地址
uint deadline // 交易到期时间
) external virtual override ensure(deadline) returns (uint[] memory amounts) {
// 根据固定输入数量和交易路径。尽可能多的兑换输出token数量。数组最后一位即目标输出token
amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);
// 要求实际计算出的输出token数量大于等于滑点计算后的最小值
require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
// 将输入amountIn发送到pair合约
TransferHelper.safeTransferFrom(
path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
);
// 调用swap方法
_swap(amounts, path, to);
}

swapTokensForExactTokens

// 以最小输入数量兑换固定数量的输出tokne
function swapTokensForExactTokens(
uint amountOut, // 固定输出数量
uint amountInMax, // 根据滑点动态计算后最大可接受的输入token数量
address[] calldata path, // 交易路径
address to,
uint deadline
) external virtual override ensure(deadline) returns (uint[] memory amounts) {
// 根据固定数量的输出token计算最少的输入数量
amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);
require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');
TransferHelper.safeTransferFrom(
path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
);
_swap(amounts, path, to);
}

swapExactTokensForTokensSupportingFeeOnTransferTokens

// requires the initial amount to have already been sent to the first pair
// 该方法是开启协议手续费以后兑换的核心基础方法
function _swapSupportingFeeOnTransferTokens(address[] memory path, address _to) internal virtual {
for (uint i; i < path.length - 1; i++) {
(address input, address output) = (path[i], path[i + 1]);
(address token0,) = UniswapV2Library.sortTokens(input, output);
IUniswapV2Pair pair = IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output));
uint amountInput;
uint amountOutput;
{ // scope to avoid stack too deep errors
(uint reserve0, uint reserve1,) = pair.getReserves();
(uint reserveInput, uint reserveOutput) = input == token0 ? (reserve0, reserve1) : (reserve1, reserve0);
// 因为 input 代币转账时可能会有损耗,所以在 pair 合约里实际收到多少代币,只能通过查出 pair 合约当前的余额,再减去该代币已保存的储备量,这才能计算出来实际值。
amountInput = IERC20(input).balanceOf(address(pair)).sub(reserveInput);
amountOutput = UniswapV2Library.getAmountOut(amountInput, reserveInput, reserveOutput);
}
(uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOutput) : (amountOutput, uint(0));
address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to;
pair.swap(amount0Out, amount1Out, to, new bytes(0));
}
}
// 协议手续费开启是,通过指定输入token数量兑换输出token
function swapExactTokensForTokensSupportingFeeOnTransferTokens(
uint amountIn,
uint amountOutMin,
address[] calldata path,
address to,
uint deadline
) external virtual override ensure(deadline) {
TransferHelper.safeTransferFrom(
path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn
);
uint balanceBefore = IERC20(path[path.length - 1]).balanceOf(to);
// 循环遍历path中的每一个交易对
_swapSupportingFeeOnTransferTokens(path, to);
// 因为此类代币转账时可能会有损耗,所以就无法使用恒定乘积公式计算出最终兑换的资产数量,因此用交易后的余额减去交易前的余额来计算得出实际值。
require(
IERC20(path[path.length - 1]).balanceOf(to).sub(balanceBefore) >= amountOutMin,
'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'
);
}