教程区块链区块链技术ch1919.3 Uniswap V2 流动性池合约实现

本页目录

本节实现 Uniswap V2 风格的 Pair 池合约:添加/移除流动性、swap、LP Token 发行与储备更新。全部逻辑用 Solidity 展示(链上),并以 TS 复现关键数学用于本地验证。


19.3.1 Pair 合约的整体设计

graph TD
    Router[Router 路由] -->|addLiquidity| Pair[Pair 池]
    Router -->|swap| Pair
    Router -->|removeLiquidity| Pair
    Pair --> R0[储备 reserve0]
    Pair --> R1[储备 reserve1]
    Pair --> LP[LP Token 铸造/销毁]
    Pair --> Sync[Sync 事件/储备更新]
    Pair --> Mint[Burn/Swap 事件]

核心状态:

变量含义
reserve0 / reserve1两种代币当前储备
totalSupplyLP Token 总量(份额)
kLast上次更新的 kk 值(用于收集协议费)
price0CumulativeLast累计价格(用于 TWAP,见 19.4)

19.3.2 添加流动性:铸造 LP Token

首次添加(LL 为铸造的 LP 量):

L=xyMINIMUM_LIQUIDITY,MINIMUM_LIQUIDITY=1000L = \sqrt{x \cdot y} - \text{MINIMUM\_LIQUIDITY}, \qquad \text{MINIMUM\_LIQUIDITY}=1000

后续添加(按比例):

L=min(Δxx,Δyy)×totalSupplyL = \min\left(\frac{\Delta x}{x}, \frac{\Delta y}{y}\right) \times \text{totalSupply}
solidity
function mint(address to) external returns (uint256 liquidity) {
    uint256 bal0 = IERC20(token0).balanceOf(address(this));
    uint256 bal1 = IERC20(token1).balanceOf(address(this));

    uint256 _totalSupply = totalSupply;
    if (_totalSupply == 0) {
        liquidity = Math.sqrt(bal0 * bal1) - MINIMUM_LIQUIDITY; // 首铸
        _mint(address(0), MINIMUM_LIQUIDITY);                    // 锁定最小份额(防除零)
    } else {
        liquidity = Math.min(
            (bal0 * _totalSupply) / reserve0,
            (bal1 * _totalSupply) / reserve1
        );                                                       // 按比例
    }
    require(liquidity > 0, "INSUFFICIENT_LIQUIDITY_MINTED");
    _mint(to, liquidity); // 铸造 LP 给 to

    _update(bal0, bal1); // 更新储备
}

function _update(uint256 bal0, uint256 bal1) private {
    reserve0 = bal0; // 记录新储备
    reserve1 = bal1;
    emit Sync(bal0, bal1);
}

滑点保护:Router 在调用前会比较"实际可退的最大额"与用户设定的 amountAMin/amountBMin,不足则 revert,同时把多余代币退回用户。


19.3.3 移除流动性:销毁 LP Token 兑回双币

Δx=LS×x,Δy=LS×y\Delta x = \frac{L}{S} \times x, \qquad \Delta y = \frac{L}{S} \times y

其中 LL 为销毁的 LP 量,SS 为 LP 总量。LP 按比例取回两种储备。

solidity
function burn(address to) external returns (uint256 amount0, uint256 amount1) {
    uint256 bal0 = IERC20(token0).balanceOf(address(this));
    uint256 bal1 = IERC20(token1).balanceOf(address(this));
    uint256 liquidity = balanceOf[address(this)]; // 需先 transfer LP 到本合约

    amount0 = (liquidity * bal0) / totalSupply;
    amount1 = (liquidity * bal1) / totalSupply;

    _burn(address(this), liquidity);
    _safeTransfer(token0, to, amount0);
    _safeTransfer(token1, to, amount1);
    _update(bal0 - amount0, bal1 - amount1); // 结算后储备
}

19.3.4 手续费与 swap 逻辑

Swap 的输出(含 0.3% 手续费留池):

Δy=yΔx(1f)x+Δx(1f),f=0.003\Delta y = \frac{y \cdot \Delta x (1-f)}{x + \Delta x (1-f)}, \qquad f = 0.003

ts 数学验证:

typescript
// swap 输出计算:从零验证(纯 TS + BigInt,无外部库)
function getAmountOut(
  amountIn: bigint,
  reserveIn: bigint,
  reserveOut: bigint,
  feeBps = 30n
): bigint {
  const fee = (amountIn * feeBps) / 10000n;
  const effectiveIn = amountIn - fee; // Δx(1 - f)
  const num = reserveOut * effectiveIn;
  const den = reserveIn + effectiveIn; // x + Δx(1-f)
  return num / den;
}

const WEI = 10n ** 18n;
// 池 100 ETH / 300000 USDC,买入 1 ETH
const out1 = getAmountOut(1n * WEI, 100n * WEI, 300000n * WEI);
console.log("第1笔换出 ≈", (out1 / WEI).toString(), "USDC(理论 2961)");
// 连续第2笔(滑点):
const out2 = getAmountOut(1n * WEI, 101n * WEI, 300000n * WEI - out1);
console.log("第2笔换出 ≈", (out2 / WEI).toString(), "USDC(更少=滑点)");

19.3.5 关键安全点

说明
首铸锁定 1000 LP防止首笔注入者通过极小流动性 + 大额 swap 抬价薅后续 LP

| 储备以实际余额为准 | _updatebalanceOf(address(this)),杜绝用传入参数伪造 */

_update 后外部调用遵循 "checks-effects-interactions" 防重入
滑点保护在 RouterPair 自身不防滑点,由 Router 的 min 参数承担
整数除法向零舍入输出向下取整,防止 LP 被四舍五入薅走

3 个关键认知:① 首铸锁定 + 按比例铸造/销毁保证 LP 份额公平;② 手续费留池让 kk 增长,是 LP 收益来源;③ 储备以实际余额为准并遵守重入防护,是 Pair 安全的两条铁律。


, 前往 → 19.4 DEX 安全 |*

评论

0

评论加载中…

发表评论

0/2000