找回密码
 立即注册
首页 业界区 业界 UniswapV2Periphery 源码学习

UniswapV2Periphery 源码学习

固拆棚 2025-9-28 18:40:27
Periphery是uniswap的外围合约,将core合约封装起来提供给外部调用,比如我们在网页操作Swap时,请求的就是Periphery的合约。
Periphery里面写了Migrator和Router两个合约,其中Migrator是迁移合约,将流动性从Uniswap的V1版本迁移到V2版本,不涉及swap的功能,这里就不写了。
Router合约
  1.     using SafeMath for uint;
  2.     address public immutable override factory;
  3.     address public immutable override WETH;
  4.     modifier ensure(uint deadline) {
  5.         require(deadline >= block.timestamp, 'UniswapV2Router: EXPIRED');
  6.         _;
  7.     }
  8.     constructor(address _factory, address _WETH) public {
  9.         factory = _factory;
  10.         WETH = _WETH;
  11.     }
  12.     receive() external payable {
  13.         assert(msg.sender == WETH); // only accept ETH via fallback from the WETH contract
  14.     }
复制代码
从基础部分开始看起,router合约中记录了factory和WETH地址,其中factory用于获取pair和创建新的pair合约,而特别记录下WETH的地址是为了支持以太坊链的主网币ETH。
Uniswap中的代币操作都是基于ERC20类型,但是ETH本身既不是ERC20,也没有合约地址,因此为了ETH也能参与swap,需要先将ETH转换成WETH,再进行后续的操作。Uniswap为了减少用户手动转换的麻烦,会在有ETH参与的交易中自动执行ETH与WETH的相互转换,因此需要记录下WETH的合约地址。
receive方法中限制了只允许接收来自WETH合约的ETH,即调用withdraw方法取出ETH,除此之外不可直接向合约中转入ETH。
addLiquidity

addLiquidity是向合约添加流动性的方法,其主要逻辑在_addLiquidity中,根据用户提供的token数量,再根据流动性池中已有的token数量,计算出实际参与添加流动性的token数量,返回两个uint值:
  1. function _addLiquidity(
  2.     address tokenA,
  3.     address tokenB,
  4.     uint amountADesired,
  5.     uint amountBDesired,
  6.     uint amountAMin,
  7.     uint amountBMin
  8. ) internal virtual returns (uint amountA, uint amountB) {
  9.     // create the pair if it doesn't exist yet
  10.     if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) {
  11.         IUniswapV2Factory(factory).createPair(tokenA, tokenB);
  12.     }
  13.     (uint reserveA, uint reserveB) = UniswapV2Library.getReserves(factory, tokenA, tokenB);
  14.     if (reserveA == 0 && reserveB == 0) {
  15.         (amountA, amountB) = (amountADesired, amountBDesired);
  16.     } else {
  17.         uint amountBOptimal = UniswapV2Library.quote(amountADesired, reserveA, reserveB);
  18.         if (amountBOptimal <= amountBDesired) {
  19.             require(amountBOptimal >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
  20.             (amountA, amountB) = (amountADesired, amountBOptimal);
  21.         } else {
  22.             uint amountAOptimal = UniswapV2Library.quote(amountBDesired, reserveB, reserveA);
  23.             assert(amountAOptimal <= amountADesired);
  24.             require(amountAOptimal >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');
  25.             (amountA, amountB) = (amountAOptimal, amountBDesired);
  26.         }
  27.     }
  28. }
复制代码
第一步判断交易对是否存在,如果不存在那么调用facotry创建一个新的交易对。
如果此时流动性池为空,那么用户提供的数量就是最后实际添加到池子中的数量,无需进一步计算;但如果池子非空,就需要通过UniswapV2Library中的quote方法去计算合理的数量。
quote方法如下:
  1. function quote(uint amountA, uint reserveA, uint reserveB) internal pure returns (uint amountB) {
  2.     require(amountA > 0, 'UniswapV2Library: INSUFFICIENT_AMOUNT');
  3.     require(reserveA > 0 && reserveB > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
  4.     amountB = amountA.mul(reserveB) / reserveA;
  5. }
复制代码
逻辑很简单,就是根据A和B当前数量的比值,计算新增数量的A需要匹配多少数量的B,保证最终池子内A与B的比值不变。
回到_addLiquidity的逻辑,先根据A传入的数量去计算出需要多少相匹配的B,如果传入的B数量满足,那么就以amountADesired, amountBOptimal作为最后添加到流动性池子的数量;如果不满足,说明B相对池子的数量较少,那么就以B的数量为基准,反过来去计算所需要A的数量。在计算中,还需要满足amountMin的限制。
了解了主要逻辑之后,再回归到addLiquidity方法本身就很简单了:
  1. function addLiquidity(
  2.     address tokenA,
  3.     address tokenB,
  4.     uint amountADesired,
  5.     uint amountBDesired,
  6.     uint amountAMin,
  7.     uint amountBMin,
  8.     address to,
  9.     uint deadline
  10. ) external virtual override ensure(deadline) returns (uint amountA, uint amountB, uint liquidity) {
  11.     (amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin);
  12.     address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
  13.     TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA);
  14.     TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB);
  15.     liquidity = IUniswapV2Pair(pair).mint(to);
  16. }
复制代码
pairFor方法就是之前提到过的唯一pair地址生成器,根据factory,tokenA和tokenB的地址就能生成对应的pair地址,无需去factory中查询。
safeTransferFrom是uniswap封装的转账方法,因为标准的ERC20实现中tranferFrom要求返回bool,但是实际有许多代币在实现的时候并没有遵守这一规则,导致返回内容各不相同,还可能不返回,因此通过底层调用的绕过类型检查的限制,并且手动根据返回的data元数据进行判断调用是否成功,保证了对不同token的兼容。
  1. function safeTransferFrom(
  2.     address token,
  3.     address from,
  4.     address to,
  5.     uint256 value
  6. ) internal {
  7.     // bytes4(keccak256(bytes('transferFrom(address,address,uint256)')));
  8.     (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x23b872dd, from, to, value));
  9.     require(
  10.         success && (data.length == 0 || abi.decode(data, (bool))),
  11.         'TransferHelper::transferFrom: transferFrom failed'
  12.     );
  13. }
复制代码
addLiquidityETH

addLiquidityETH的使用场景是交易中存在一方为ETH的时候,需要执行前面提到的WETH转换操作,并且ETH是通过msg.Value的形式传递的,所以对于多余的部分,需要手动执行退回。
  1. function addLiquidityETH(
  2.     address token,
  3.     uint amountTokenDesired,
  4.     uint amountTokenMin,
  5.     uint amountETHMin,
  6.     address to,
  7.     uint deadline
  8. ) external virtual override payable ensure(deadline) returns (uint amountToken, uint amountETH, uint liquidity) {
  9.     (amountToken, amountETH) = _addLiquidity(
  10.         token,
  11.         WETH,
  12.         amountTokenDesired,
  13.         msg.value,
  14.         amountTokenMin,
  15.         amountETHMin
  16.     );
  17.     address pair = UniswapV2Library.pairFor(factory, token, WETH);
  18.     TransferHelper.safeTransferFrom(token, msg.sender, pair, amountToken);
  19.     IWETH(WETH).deposit{value: amountETH}();
  20.     assert(IWETH(WETH).transfer(pair, amountETH));
  21.     liquidity = IUniswapV2Pair(pair).mint(to);
  22.     // refund dust eth, if any
  23.     if (msg.value > amountETH) TransferHelper.safeTransferETH(msg.sender, msg.value - amountETH);
  24. }
复制代码
removeLiquidity

removeLiquidity的基本逻辑:

  • 获取交易对Pair
  • 将sender的LP token发送到Pair
  • 调用burn方法,销毁LP token,将两种token发回给用户,并得到tokenA和tokenB的数量
  • 保证数量满足min的要求
  1. function removeLiquidity(
  2.     address tokenA,
  3.     address tokenB,
  4.     uint liquidity,
  5.     uint amountAMin,
  6.     uint amountBMin,
  7.     address to,
  8.     uint deadline
  9. ) public virtual override ensure(deadline) returns (uint amountA, uint amountB) {
  10.     address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
  11.     IUniswapV2Pair(pair).transferFrom(msg.sender, pair, liquidity); // send liquidity to pair
  12.     (uint amount0, uint amount1) = IUniswapV2Pair(pair).burn(to);
  13.     (address token0,) = UniswapV2Library.sortTokens(tokenA, tokenB);
  14.     (amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0);
  15.     require(amountA >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');
  16.     require(amountB >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
  17. }
复制代码
removeLiquidityETH

removeLiquidityETH同样是用于ETH参与交易对的场景,可以看到这里直接调用了removeLiquidity,但调用的时候to参数传的是路由合约的地址address(this),这意味着burn取回流动性之后,代币会先发送到路由合约上。因此下面的逻辑补上了从路由合约将token和ETH转回到to地址的过程。
  1. function removeLiquidityETH(
  2.     address token,
  3.     uint liquidity,
  4.     uint amountTokenMin,
  5.     uint amountETHMin,
  6.     address to,
  7.     uint deadline
  8. ) public virtual override ensure(deadline) returns (uint amountToken, uint amountETH) {
  9.     (amountToken, amountETH) = removeLiquidity(
  10.         token,
  11.         WETH,
  12.         liquidity,
  13.         amountTokenMin,
  14.         amountETHMin,
  15.         address(this),
  16.         deadline
  17.     );
  18.     TransferHelper.safeTransfer(token, to, amountToken);
  19.     IWETH(WETH).withdraw(amountETH);
  20.     TransferHelper.safeTransferETH(to, amountETH);
  21. }
复制代码
这么写是因为:

  • 需要处理WETH和ETH的转换,因此必须将WETH先取出,存到路由合约中
  • 复用了removeLiquidity逻辑,简化代码
其他remove

uniswap中还支持了removeLiquidityWithPermit和removeLiquidityETHSupportingFeeOnTransferTokens这两种类型,其中WithPermit是基于EIP712实现的链下签名代执行的方法,而SupportingFeeOnTransferTokens则是支持特殊的ERC20token,这种token会在交易的过程中收取手续费或者燃烧,因为不涉及核心逻辑,所以就不深入了。
swap

swap有四种类型:

  • swapExactTokensForTokens,拿指定数量的A换B
  • swapTokensForExactTokens,拿A换指定数量的B
  • swapExactETHForTokens,拿指定数量的ETH换token
  • swapTokensForExactETH,拿ETH换指定数量的token
可以看到,关键的区别在于先确定输入还是先确定输出,以及是否有ETH的参与。
以swapExactTokensForTokens为例:
  1. function swapExactTokensForTokens(
  2.     uint amountIn,
  3.     uint amountOutMin,
  4.     address[] calldata path,
  5.     address to,
  6.     uint deadline
  7. ) external virtual override ensure(deadline) returns (uint[] memory amounts) {
  8.     amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);
  9.     require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
  10.     TransferHelper.safeTransferFrom(
  11.         path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
  12.     );
  13.     _swap(amounts, path, to);
  14. }
复制代码
path是token转换的路径,因为对于用户想要提供A换取B的场景, 可能没有现成的A-B池子,那么就需要一条路径,先将A换成C,再从C换成B,最典型的C就是WETH,因为绝大部分的代币都会优先提供和WETH组成的交易对,那么只要通过WETH,基本上就可以实现任意两种代币的兑换。
根据path可以得到amounts,即转换路径上每种代币应有的数量,因为这里是已知输入的方法,所以用到了getAmountsOut方法:
  1. function getAmountsOut(address factory, uint amountIn, address[] memory path) internal view returns (uint[] memory amounts) {
  2.     require(path.length >= 2, 'UniswapV2Library: INVALID_PATH');
  3.     amounts = new uint[](path.length);
  4.     amounts[0] = amountIn;
  5.     for (uint i; i < path.length - 1; i++) {
  6.         (uint reserveIn, uint reserveOut) = getReserves(factory, path[i], path[i + 1]);
  7.         amounts[i + 1] = getAmountOut(amounts[i], reserveIn, reserveOut);
  8.     }
  9. }
  10. function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) internal pure returns (uint amountOut) {
  11.       require(amountIn > 0, 'UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT');
  12.       require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
  13.       uint amountInWithFee = amountIn.mul(997);
  14.       uint numerator = amountInWithFee.mul(reserveOut);
  15.       uint denominator = reserveIn.mul(1000).add(amountInWithFee);
  16.       amountOut = numerator / denominator;
  17.   }
复制代码
getAmountsOut即轮询path中的代币组合,模拟token的swap;getAmountOut是对于已知reserve的pair,提供amountIn得到amountOut。
getAmountOut中是以下数学逻辑的实现:
交换前:x × y = k
交换后:(x + Δx) × (y - Δy) = k
因为k是常数,所以:
x × y = (x + Δx) × (y - Δy)
展开:
x × y = x × y - x × Δy + Δx × y - Δx × Δy
简化:
0 = -x × Δy + Δx × y - Δx × Δy
x × Δy = Δx × y - Δx × Δy
x × Δy = Δx × (y - Δy)
求解Δy:
Δy = (Δx × y) / (x + Δx)
也就是amountOut = (amountIn × reserveOut) / (reserveIn + amountIn)。
因为uniswap中会收取0.3%的手续费,所以实际的amountIn是 amountIn *997/100,为了避免浮点数运算,分子分母都乘以1000,最终得到amountOut = (amountIn × 997 × reserveOut) / (reserveIn × 1000 + amountIn × 997)。
计算出amounts后,将input token发送到即path[0]和path[1]组成的流动性池,调用_swap进行链式的交换,直到最终得到output。
  1. function _swap(uint[] memory amounts, address[] memory path, address _to) internal virtual {
  2.     for (uint i; i < path.length - 1; i++) {
  3.         (address input, address output) = (path[i], path[i + 1]);
  4.         (address token0,) = UniswapV2Library.sortTokens(input, output);
  5.         uint amountOut = amounts[i + 1];
  6.         (uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOut) : (amountOut, uint(0));
  7.         address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to;
  8.         IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output)).swap(
  9.             amount0Out, amount1Out, to, new bytes(0)
  10.         );
  11.     }
  12. }
复制代码
_swap主要做了参数的处理工作,遍历path和amounts得到input,output,amount0Out,amount1Out等参数,传入Pair合约的swap方法中进行实际的swap工作。
注意的几个点:

  • amountOut等于amounts[i+1]且需要分配给非input的token作为amount。
  • swap的时候,path和path[i+1]的输出token要发给path[i+1]和path[i+2]的pair池子,所以当i=path.length-2的时候,i+1为最后一个token,此时发送的对象为_to,也就是输出给指定的用户地址而非Pair合约。
swapExactETHForTokens

swapExactETHForTokens的逻辑基本类似,但是所有用到ETH的地方都必须做WETH的转换,比如一开始就要求 path[0]必须为WETH。然后将ETH转换为WETH后发给第一个交易对,开始swap的流程。
  1. function swapExactETHForTokens(uint amountOutMin, address[] calldata path, address to, uint deadline)
  2.     external
  3.     virtual
  4.     override
  5.     payable
  6.     ensure(deadline)
  7.     returns (uint[] memory amounts)
  8. {
  9.     require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');
  10.     amounts = UniswapV2Library.getAmountsOut(factory, msg.value, path);
  11.     require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
  12.     IWETH(WETH).deposit{value: amounts[0]}();
  13.     assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]));
  14.     _swap(amounts, path, to);
  15. }
复制代码
总结

在Router中主要实现的是对于参数的处理,无论是流动性的变更还是swap,在用户提供了token和amount之后,路由合约会进行相应的计算,得到满足条件的amount参与到swap流程中,保证了传递给swap方法的参数合法性。同时也要负责多链路swap的有序进行,实现不同流动性池之间的传递。

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册