Uniswap V2 — 理解DeFi协议的完整指南

  • zealynx
  • 发布于 2026-02-17 14:48
  • 阅读 13

本文深入分析了Uniswap V2协议的核心智能合约,包括其去中心化交易、流动性提供、交易逻辑及合约交互原理。文章详细介绍了Router和Factory合约的功能,并通过代码示例阐述了添加/移除流动性和代币交换的实现机制,以及CREATE2操作码在合约部署中的应用。

为了理解我们将在分析代码时遇到的不同组件,首先重要的是要知道主要概念是什么以及它们的作用。所以,请耐心听我讲,因为这将是值得的。

我已经用5个段落总结了你需要了解的主要重要概念,你将在本文结束时理解它们。

Uniswap是一个去中心化交易所协议。该协议是一套持久的、不可升级的智能合约,它们共同创建了一个自动做市商。

Uniswap生态系统由流动性提供者(他们贡献流动性)、交易者(交易代币)和开发者(他们与智能合约交互以开发代币的新交互方式)组成。

每个Uniswap智能合约,或称pair,管理一个由两种ERC-20代币储备组成的流动性池。

每个流动性池都会重新平衡,以维持50/50比例的加密货币资产,这反过来又决定了资产的价格。

流动性提供者可以是任何能够向Uniswap交易合约提供等值ETH和ERC-20代币的人。作为回报,他们会从交易合约中获得流动性提供者代币(LP代币代表流动性提供者在池中所占的份额),这些代币可以随时用于提取他们在流动性池中的份额。

主要智能合约

其仓库中的主要智能合约如下:

  • UniswapV2ERC20 — ERC20的扩展实现,用于LP代币。它还实现了EIP-2612以支持链下批准转账。
  • UniswapV2Factory — 类似于V1,这是一个工厂合约,用于创建pair合约并充当它们的注册中心。该注册中心使用create2来生成pair地址——我们将详细了解它的工作原理。
  • UniswapV2Pair — 负责核心逻辑的主要合约。值得注意的是,工厂只允许创建唯一的pair,以避免稀释流动性。
  • UniswapV2Router — Uniswap UI以及其他基于Uniswap工作的web和去中心化应用程序的主要入口点。
  • UniswapV2Library — 一系列辅助函数,实现了重要的计算。

在本文中,我们将提及所有这些合约,但主要侧重于UniswapV2Router和UniswapV2Factory的代码,尽管UniswapV2Pair和UniswapV2Library也会大量涉及。

UniswapV2Router02.sol

这个合约使得创建pair、添加和移除流动性、计算所有可能兑换变体的价格以及执行实际兑换变得更加容易。Router与通过Factory合约部署的所有pair一起工作。

你需要在合约中创建实例才能调用 addLiquidityremoveLiquidityswapExactTokensForTokens 函数:

1address private constant ROUTER = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D;
2
3IUniswapV2Router02 public uniswapV2Router;
4uniswapV2Router = IUniswapV2Router02(ROUTER);

现在,让我们看看流动性管理:

FUNCTION addLiquidity()

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);
  • tokenAtokenB:是我们需要获取或创建pair以添加流动性的代币。
  • amountADesiredamountBDesired 是我们希望存入流动性池的金额。
  • amountAMinamountBMin 是我们希望存入的最小金额。
  • to address 是接收LP代币的地址。
  • deadline,通常会是 block.timestamp

在内部 _addLiquidity() 函数中,它将检查这两个代币的pair是否已经存在,如果不存在,则会创建一个新的:

1if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) {
2    IUniswapV2Factory(factory).createPair(tokenA, tokenB);
3}

然后它需要获取代币的现有数量,也称为 reserveAreserveB,我们可以通过UniswapV2Pair合约访问它们:

1IUniswapV2Pair(pairFor(factory, tokenA, tokenB)).getReserves()

现在,外部函数 addLiquidity 返回 (uint amountA, uint amountB, uint liquidity),那么它是如何计算的呢?

通过UniswapV2Library获取上述储备后,会进行一系列检查。

如果pair不存在且是新创建的,则返回的 amountAamountB 将是作为参数传入的 amountADesiredamountBDesired

否则,它将执行此操作:

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));

FUNCTION removeLiquidity()

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)

FUNCTION swapExactTokensForTokens()

兑换代币

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);

这将执行实际的转账回到预期的地址。

UniswapV2Factory.sol

Uniswap V2 工厂

工厂合约是所有已部署pair合约的注册中心。这个合约是必要的,因为我们不希望有相同代币的pair,以避免流动性分散到多个相同的pair中。

该合约还简化了pair合约的部署:无需手动通过任何外部调用部署pair合约,只需在工厂合约中调用一个方法即可。

好的,让我们回顾一下,因为上面这些话中说了一些非常重要的事情。让我们将它们分开并单独分析:

这个合约是所有已部署pair合约的注册中心

只有一个工厂合约被部署,该合约作为Uniswap pair的官方注册中心。

现在,我们如何在代码中看到这一点以及发生了什么:

1address[] public allPairs;

它有一个 allPairs 数组,如上所述,这些都存储在这个合约中。pair通过将新初始化的pair推入数组而被添加到 createPair() 方法中。

1allPairs.push(pair);

这个合约是必要的,因为我们不希望有相同代币的pair

1mapping(address => mapping(address => address)) public getPair;

它有一个pair地址与其构成pair的两个代币的映射。这用于检查pair是否已经存在。

1require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS');

该合约还简化了pair合约的部署

这是一个更深奥的话题,但我将尝试总结这里发生的重要事情。

在以太坊中,合约可以部署合约。可以调用已部署合约的一个函数,此函数将部署另一个合约。

你无需从计算机编译和部署合约,可以通过现有合约来完成。

那么,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原语。我们结合手动逐行审查、模糊测试和不变性套件,以捕捉自动化工具遗漏的极端情况。

我们提供:

  • AMM和DEX审计——全面审查pair合约、工厂逻辑和路由器安全
  • 模糊和不变性测试——针对价格操纵和储备不平衡的基于Foundry的测试套件
  • 复刻安全审查——识别与原始版本相比发生的变化以及这些变化引入的风险
  • 智能合约开发——针对Gas效率优化的安全设计实现

请求审计范围 →

词汇表

术语 定义
Automated Market Maker (AMM) 一种使用数学公式而非订单簿来确定资产价格和促进交易的协议。
Liquidity Pool 一个智能合约,持有两种代币的储备,以实现去中心化交易。
LP Tokens 铸造给流动性提供者的代币,代表他们在池中储备的份额。
CREATE2 一个EVM操作码,根据部署者地址、salt和字节码将合约部署到确定性地址。
EIP-2612 一种通过链下签名(permit函数)实现无Gas ERC-20批准的标准。

查看完整词汇表 →

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

0 条评论

请先 登录 后评论
zealynx
zealynx
江湖只有他的大名,没有他的介绍。