本文深入分析了Uniswap V2协议的核心智能合约,包括其去中心化交易、流动性提供、交易逻辑及合约交互原理。文章详细介绍了Router和Factory合约的功能,并通过代码示例阐述了添加/移除流动性和代币交换的实现机制,以及CREATE2操作码在合约部署中的应用。
为了理解我们将在分析代码时遇到的不同组件,首先重要的是要知道主要概念是什么以及它们的作用。所以,请耐心听我讲,因为这将是值得的。
我已经用5个段落总结了你需要了解的主要重要概念,你将在本文结束时理解它们。
Uniswap是一个去中心化交易所协议。该协议是一套持久的、不可升级的智能合约,它们共同创建了一个自动做市商。
Uniswap生态系统由流动性提供者(他们贡献流动性)、交易者(交易代币)和开发者(他们与智能合约交互以开发代币的新交互方式)组成。
每个Uniswap智能合约,或称pair,管理一个由两种ERC-20代币储备组成的流动性池。
每个流动性池都会重新平衡,以维持50/50比例的加密货币资产,这反过来又决定了资产的价格。
流动性提供者可以是任何能够向Uniswap交易合约提供等值ETH和ERC-20代币的人。作为回报,他们会从交易合约中获得流动性提供者代币(LP代币代表流动性提供者在池中所占的份额),这些代币可以随时用于提取他们在流动性池中的份额。
其仓库中的主要智能合约如下:
在本文中,我们将提及所有这些合约,但主要侧重于UniswapV2Router和UniswapV2Factory的代码,尽管UniswapV2Pair和UniswapV2Library也会大量涉及。
这个合约使得创建pair、添加和移除流动性、计算所有可能兑换变体的价格以及执行实际兑换变得更加容易。Router与通过Factory合约部署的所有pair一起工作。
你需要在合约中创建实例才能调用 addLiquidity、removeLiquidity 和 swapExactTokensForTokens 函数:
1address private constant ROUTER = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D;
2
3IUniswapV2Router02 public uniswapV2Router;
4uniswapV2Router = IUniswapV2Router02(ROUTER);
现在,让我们看看流动性管理:
1function 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 returns (uint amountA, uint amountB, uint liquidity);
tokenA 和 tokenB:是我们需要获取或创建pair以添加流动性的代币。amountADesired 和 amountBDesired 是我们希望存入流动性池的金额。amountAMin 和 amountBMin 是我们希望存入的最小金额。to address 是接收LP代币的地址。deadline,通常会是 block.timestamp。在内部 _addLiquidity() 函数中,它将检查这两个代币的pair是否已经存在,如果不存在,则会创建一个新的:
1if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) {
2 IUniswapV2Factory(factory).createPair(tokenA, tokenB);
3}
然后它需要获取代币的现有数量,也称为 reserveA 和 reserveB,我们可以通过UniswapV2Pair合约访问它们:
1IUniswapV2Pair(pairFor(factory, tokenA, tokenB)).getReserves()
现在,外部函数 addLiquidity 返回 (uint amountA, uint amountB, uint liquidity),那么它是如何计算的呢?
通过UniswapV2Library获取上述储备后,会进行一系列检查。
如果pair不存在且是新创建的,则返回的 amountA 和 amountB 将是作为参数传入的 amountADesired 和 amountBDesired。
否则,它将执行此操作:
1amountBOptimal = amountADesired.mul(reserveB) / reserveA;
如果 amountB 小于或等于 amountBDesired,则返回:
1(uint amountA, uint amountB) = (amountADesired, amountBOptimal)
否则,它将返回:
1(uint amountA, uint amountB) = (amountAOptimal, amountBDesired)
其中 amountAOptimal 的计算方式与 amountBOptimal 相同。
然后,为了计算要返回的流动性,它将执行以下操作:
首先,它将使用现有/新创建的pair地址部署UniswapV2Pair合约。
它是如何做到的?它在不进行任何外部调用的情况下,计算了pair的CREATE2地址:
1pair = address(uint(keccak256(abi.encodePacked(
2 hex'ff',
3 factory,
4 keccak256(abi.encodePacked(token0, token1)),
5 hex'96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f' // init code hash
6))));
然后,它获取新部署合约的地址,我们需要该地址来从该pair代币中铸造代币。
当你向pair添加流动性时,合约会铸造LP代币;当你移除流动性时,LP代币会被销毁。
所以,首先我们使用UniswapV2Library中的 pairFor 获取地址:
1address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
这样就可以稍后铸造ERC20代币并计算要返回的流动性:
1liquidity = IUniswapV2Pair(pair).mint(to);
如果你想知道为什么它最终会是ERC20,在mint函数内部它是这样存储的:
1uint balance0 = IERC20(token0).balanceOf(address(this));
2uint balance1 = IERC20(token1).balanceOf(address(this));
1function removeLiquidity(
2 address tokenA,
3 address tokenB,
4 uint liquidity,
5 uint amountAMin,
6 uint amountBMin,
7 address to,
8 uint deadline
9) external returns (uint amountA, uint amountB);
从流动性池中移除流动性意味着销毁LP代币以换取等比例的基础代币。
1IUniswapV2Pair(pair).transferFrom(msg.sender, pair, liquidity);
然后,外部函数再次返回两个值 (uint amountA, uint amountB),这些值是根据传递给函数的参数计算的。
根据提供的流动性返回的代币数量计算方式如下:
1amount0 = liquidity.mul(balance0) / _totalSupply;
2amount1 = liquidity.mul(balance1) / _totalSupply;
然后它会将这些代币数量转移到指定的地址:
1_safeTransfer(_token0, to, amount0);
2_safeTransfer(_token1, to, amount1);
你的LP代币份额越大,销毁后你获得的储备份额就越大。
而这些计算发生在 burn 函数内部:
1IUniswapV2Pair(pair).burn(to)

1function swapExactTokensForTokens(
2 uint amountIn,
3 uint amountOutMin,
4 address[] calldata path,
5 address to,
6 uint deadline
7) external returns (uint[] memory amounts);
Uniswap的核心功能是代币兑换,所以让我们弄清楚代码中发生了什么,以便更好地理解它。
你很可能听说过流动性池中使用的神奇公式:
X * Y = K
所以,这是在兑换函数内部使用 getAmountOut() 函数时首先发生的事情。
内部使用的关键函数是:
1TransferHelper.safeTransferFrom()
其中代币数量被发送到pair代币。
而在UniswapV2Pair合约的更底层兑换函数中,它将是:
1_safeTransfer(_token, to, amountOut);
这将执行实际的转账回到预期的地址。

工厂合约是所有已部署pair合约的注册中心。这个合约是必要的,因为我们不希望有相同代币的pair,以避免流动性分散到多个相同的pair中。
该合约还简化了pair合约的部署:无需手动通过任何外部调用部署pair合约,只需在工厂合约中调用一个方法即可。
好的,让我们回顾一下,因为上面这些话中说了一些非常重要的事情。让我们将它们分开并单独分析:
只有一个工厂合约被部署,该合约作为Uniswap pair的官方注册中心。
现在,我们如何在代码中看到这一点以及发生了什么:
1address[] public allPairs;
它有一个 allPairs 数组,如上所述,这些都存储在这个合约中。pair通过将新初始化的pair推入数组而被添加到 createPair() 方法中。
1allPairs.push(pair);
1mapping(address => mapping(address => address)) public getPair;
它有一个pair地址与其构成pair的两个代币的映射。这用于检查pair是否已经存在。
1require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS');
这是一个更深奥的话题,但我将尝试总结这里发生的重要事情。
在以太坊中,合约可以部署合约。可以调用已部署合约的一个函数,此函数将部署另一个合约。
你无需从计算机编译和部署合约,可以通过现有合约来完成。
那么,Uniswap是如何部署智能合约的呢?
通过使用操作码CREATE2:
1bytes memory bytecode = type(UniswapV2Pair).creationCode;
2bytes32 salt = keccak256(abi.encodePacked(token0, token1));
3assembly {
4 pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
5}
在第一行,我们获取了UniswapV2Pair的创建字节码。
下一行创建了一个 salt,一个字节序列,用于确定性地生成新合约的地址。
最后一行是我们调用 create2,使用字节码 + salt 确定性地创建一个新地址。部署UniswapV2Pair。
并获取pair地址,我们可以看到它是 createPair() 函数的返回值:
1function createPair(
2 address tokenA,
3 address tokenB
4) external returns (address pair)
这在 _addLiquidity() 内部函数中,当提供的代币不是现有pair时会使用。
现在,为了看到我们所经历的一切是如何被测试的,这里你可以看到我们如何添加流动性:
1function addLiquidity(
2 address _tokenA,
3 address _tokenB,
4 uint _amountA,
5 uint _amountB
6) external {
7 IERC20(_tokenA).transferFrom(msg.sender, address(this), _amountA);
8 IERC20(_tokenB).transferFrom(msg.sender, address(this), _amountB);
9
10 IERC20(_tokenA).approve(ROUTER, _amountA);
11 IERC20(_tokenB).approve(ROUTER, _amountB);
12
13 (uint amountA, uint amountB, uint liquidity) =
14 IUniswapV2Router(ROUTER).addLiquidity(
15 _tokenA,
16 _tokenB,
17 _amountA,
18 _amountB,
19 1,
20 1,
21 address(this),
22 block.timestamp
23 );
24
25 emit Log("amountA", amountA);
26 emit Log("amountB", amountB);
27 emit Log("liquidity", liquidity);
28}
以及我们如何考虑移除流动性:
1function removeLiquidity(address _tokenA, address _tokenB) external {
2 address pair = IUniswapV2Factory(FACTORY).getPair(_tokenA, _tokenB);
3
4 uint liquidity = IERC20(pair).balanceOf(address(this));
5 IERC20(pair).approve(ROUTER, liquidity);
6
7 (uint amountA, uint amountB) =
8 IUniswapV2Router(ROUTER).removeLiquidity(
9 _tokenA,
10 _tokenB,
11 liquidity,
12 1,
13 1,
14 address(this),
15 block.timestamp
16 );
17
18 emit Log("amountA", amountA);
19 emit Log("amountB", amountB);
20}
正在构建或复刻AMM?Uniswap V2代码库经过了实战检验,但定制实现——修改定价曲线、新的费用结构、额外的Hook——引入了新的攻击面,这是原始审计从未涵盖的。
在Zealynx,我们审计了Solidity、Rust和Cairo上的DEX协议、流动性池和DeFi原语。我们结合手动逐行审查、模糊测试和不变性套件,以捕捉自动化工具遗漏的极端情况。
我们提供:
| 术语 | 定义 |
|---|---|
| Automated Market Maker (AMM) | 一种使用数学公式而非订单簿来确定资产价格和促进交易的协议。 |
| Liquidity Pool | 一个智能合约,持有两种代币的储备,以实现去中心化交易。 |
| LP Tokens | 铸造给流动性提供者的代币,代表他们在池中储备的份额。 |
| CREATE2 | 一个EVM操作码,根据部署者地址、salt和字节码将合约部署到确定性地址。 |
| EIP-2612 | 一种通过链下签名(permit函数)实现无Gas ERC-20批准的标准。 |
- 原文链接: zealynx.io/blogs/uniswap...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!