前置依赖:掌握 ERC-20 标准(第18章)与 Solidity 合约开发(第17章)后,本章将带你深入 DeFi(去中心化金融,Decentralized Finance)的基石——自动化做市商(AMM, Automated Market Maker)。我们将从数学公式出发,逐步推导并手写一个与 Uniswap V2 核心逻辑等价的最简流动性池合约。
19.1 恒定乘积公式与核心算法
19.1.1 从订单簿到自动化做市商
传统中心化交易所(如股票交易所或中心化加密交易所)依赖订单簿(Order Book)撮合交易:买方挂出「愿意以某价格买入」的限价单,卖方挂出「愿意以某价格卖出」的限价单,系统寻找双方价格重叠的订单进行撮合。这种模式的痛点在于:若某一价格区间缺乏挂单,则大额交易会面临巨大滑点(Slippage);同时,专业做市商需要持续在订单簿两侧挂单,准入门槛极高。
AMM 的核心思想是用一段数学公式替代人工订单簿。流动性提供者(Liquidity Provider, LP)将一对代币存入一个流动性池(Liquidity Pool),池中的两种代币数量始终遵循一条预设的定价曲线(Pricing Curve)。任何交易者无需等待对手方出现,只需将代币 A 投入池中,即可按曲线规定的价格自动换回代币 B——合约本身就成了「永不休息的全自动做市商」。
Uniswap 于 2018 年首次在以太坊主网上大规模实现了恒定乘积做市商(Constant Product Market Maker, CPMM),成为 DeFi Summer 的基础设施层。本章将以 Uniswap V2 为蓝本,从公式推导、合约实现到经济分析,完成一个最小可用的「Pair 合约」。
19.1.2 恒定乘积公式的数学推导
设池中有代币 \( X \) 的数量为 \( x \),代币 \( Y \) 的数量为 \( y \)。CPMM 的核心约束是一条极其简洁的方程:
\[
x \times y = k
\]
其中 \( k \) 为恒定乘积常数。在无手续费且无流动性增减的情况下,\( k \) 保持不变。
交换推导:假设某用户向池中投入 \( \Delta x \) 个代币 \( X \),则池中 \( X \) 的储备变为 \( x + \Delta x \)。为维持乘积 \( k \) 恒定,池中剩余代币 \( Y \) 的数量必须调整为 \( y' \):
\[
(x + \Delta x) \times y' = k \quad \Rightarrow \quad y' = \frac{k}{x + \Delta x}
\]
用户能够获得的代币 \( Y \) 数量即为两者的差值:
\[
\Delta y = y - y' = y - \frac{x \times y}{x + \Delta x} = \frac{y \times \Delta x}{x + \Delta x}
\]
这个公式揭示了一个关键直觉:池中两种代币的储备比例越不平衡,同样的输入 \( \Delta x \) 能换回的输出 \( \Delta y \) 就越少。这正是滑点的数学根源。同时,一个美妙的边界条件是:当 \( \Delta x \to \infty \) 时,\( \Delta y \to y \)——这意味着理论上你永远无法把池中一种代币全部换走,流动性永不枯竭。
19.1.3 AMM 价格曲线与即时价格
对恒定乘积方程 \( x \times y = k \) 两边求导,可得曲线在点 \( (x, y) \) 处的切线斜率。我们关心的边际价格(Instantaneous Price),即当前时刻 1 个代币 \( X \) 以代币 \( Y \) 计价的价格,为:
\[
P = \frac{y}{x}
\]
当一笔交易买入代币 \( X \)(即向池中增加 \( Y \) 并取出 \( X \))后,\( x \) 减小而 \( y \) 增大,新价格 \( P' = y' / x' \) 随之上升——合约自动「涨价」抑制进一步购买。反之亦然。价格发现被编码在储备量的变化之中,无需外部喂价。
CPMM 的价格曲线在坐标系中是一条双曲线 \( y = k / x \)。与订单簿的阶梯式深度图不同,AMM 的深度由总流动性(即 \( k \) 的大小)直接决定:\( k \) 越大,同样的交易量对价格的影响越小,滑点越低。
19.1.4 LP Token 经济学与无常损失
流动性提供者将代币对存入池中后,合约会铸造一种池份额代币(LP Token)作为凭证。LP Token 本身通常也是标准 ERC-20 代币,代表其对池中两种资产的所有权比例。
首次添加流动性时,铸造的 LP Token 总量为:
\[
\text{minted} = \sqrt{x \times y} - \text{MINIMUM\_LIQUIDITY}
\]
其中 MINIMUM_LIQUIDITY = 1000(由 Uniswap V2 定义),这部分最小份额被永久锁定至地址 0 以消除除零风险。后续添加流动性时,则按存入比例铸造:
\[
\text{minted} = \frac{\Delta x}{x} \times \text{totalSupply}
\]
(实际实现中取按代币 A 和代币 B 计算出的较小值,多余代币会原路退回。)
当外部市场价格相对存入时刻发生偏离时,套利者会不断在池中交易,使合约内部价格向外部市场价格靠拢。这导致 LP 持有的资产比例被强制再平衡。数学上,若存入后价格变化比率为 \( r \),LP 相对于「简单持有(HODL)」的价值损失比例可近似为:
\[
\text{IL} = \frac{2\sqrt{r}}{1+r} - 1
\]
这就是著名的无常损失(Impermanent Loss, IL)。好消息是,每笔交易 0.3% 的手续费不会直接分配给 LP,而是直接累积到池中——表现为 \( k \) 值随时间缓慢增长。只要手续费收益超过无常损失,LP 的净收益仍可为正。手续费收益是 LP 承担价格风险的补偿机制。
本节要点小结
- \( x \times y = k \) 是 CPMM 的唯一核心约束,所有价格与滑点均可由该公式推演。
- 边际价格 \( P = y / x \) 让合约在无外部喂价的情况下自动完成价格发现。
- LP 面临无常损失,但手续费累积的 \( k \) 增长是其风险补偿来源。
19.2 流动性池合约
19.2.1 添加流动性:接口与决策流程
在 Uniswap V2 风格的设计中,流动性由 Router 合约代理用户与底层 Pair 合约交互。addLiquidity 的函数签名如下:
function addLiquidity(
address tokenA,
address tokenB,
uint256 amountADesired,
uint256 amountBDesired,
uint256 amountAMin,
uint256 amountBMin,
address to,
uint256 deadline
) external returns (uint256 amountA, uint256 amountB, uint256 liquidity);其核心决策流程可用 Mermaid 流程图表示:
flowchart TD
A[用户调用 addLiquidity] --> B{池是否已存在?}
B -->|reserve0 == 0 <br/> 首次添加| C[LP Token = sqrt(amountA * amountB) - 1000 <br/> 设定初始比率]
B -->|reserve0 > 0 <br/> 已存在池| D[按当前储备计算 optimalB = <br/> amountADesired * reserve1 / reserve0]
D --> E{optimalB <= amountBDesired?}
E -->|是| F[存入 amountA = amountADesired <br/> amountB = optimalB]
E -->|否| G[存入 amountB = amountBDesired <br/> 反向计算 optimalA]
C --> H[amountA >= amountAMin? <br/> amountB >= amountBMin?]
F --> H
G --> H
H -->|否| I[revert: 滑点超限]
H -->|是| J[从用户转入代币到 Pair]
J --> K[铸造 LP Token 给地址 to]
K --> L[_update 更新储备量 <br/> 触发 Sync 事件]
关键安全设计:
- 地址排序:调用前必须校验
tokenA < tokenB,保证同一交易对不会因参数顺序不同而生成重复池子。 - 最小值参数(amountAMin / amountBMin):防止 MEV 抢跑者(Front-runner)在用户交易Pending期间操控池价,导致用户以劣于预期的比率存入。若实际比率低于用户容忍下限,交易回滚。
- 截止时间(deadline):避免交易在内存池中挂起过久,在极端行情下被矿工在数小时后执行。
19.2.2 移除流动性:赎回与销毁
function removeLiquidity(
address tokenA,
address tokenB,
uint256 liquidity,
uint256 amountAMin,
uint256 amountBMin,
address to,
uint256 deadline
) external returns (uint256 amountA, uint256 amountB);移除流动性的核心逻辑是按 LP Token 份额比例赎回两种储备资产:
\[
\text{amountA} = \frac{\text{liquidity}}{\text{totalSupply}} \times \text{reserve0}, \quad
\text{amountB} = \frac{\text{liquidity}}{\text{totalSupply}} \times \text{reserve1}
\]
合约先销毁用户的 LP Token(内部状态变更),再通过 safeTransfer 将对应代币转出——严格遵循「检查-生效-交互」(Checks-Effects-Interactions)模式,避免重入攻击。实际赎回数量同样不得低于 amountAMin 和 amountBMin,否则回滚。移除后需调用 _update() 同步储备量并触发 Sync 事件,供链下索引器重建状态。
19.2.3 完整 Pair 合约架构:最小化实现
以下是仿制 Uniswap V2 的最简 Pair 合约骨架,包含 mint()、burn()、核心数学库函数及关键状态变量:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/utils/math/Math.sol";
contract Pair is ERC20 {
// ---------- 常量 ----------
uint256 public constant MINIMUM_LIQUIDITY = 1000;
// 手续费 0.3% 用 3/1000 表示
uint256 public constant FEE_DENOMINATOR = 1000;
uint256 public constant FEE_NUMERATOR = 3;
// ---------- 状态变量 ----------
address public token0;
address public token1;
uint256 public reserve0; // token0 储备量(含缓存)
uint256 public reserve1; // token1 储备量(含缓存)
uint32 public blockTimestampLast; // 上次更新时间戳(用于 TWAP 预言机)
uint256 public price0CumulativeLast;
uint256 public price1CumulativeLast;
// ---------- 事件 ----------
event Mint(address indexed sender, uint256 amount0, uint256 amount1);
event Burn(address indexed sender, uint256 amount0, uint256 amount1, address indexed to);
event Swap(address indexed sender, uint256 amount0In, uint256 amount1In, uint256 amount0Out, uint256 amount1Out, address indexed to);
event Sync(uint256 reserve0, uint256 reserve1);
constructor(address _token0, address _token1) ERC20("LP-Token", "LP") {
token0 = _token0;
token1 = _token1;
}
// ---------- 核心:_update 同步储备 ----------
function _update(uint256 balance0, uint256 balance1) private {
uint32 blockTimestamp = uint32(block.timestamp % 2**32);
uint32 timeElapsed = blockTimestamp - blockTimestampLast;
if (timeElapsed > 0 && reserve0 != 0 && reserve1 != 0) {
// 时间加权累计价格(TWAP 基础)
price0CumulativeLast += uint256(reserve1) * timeElapsed / reserve0;
price1CumulativeLast += uint256(reserve0) * timeElapsed / reserve1;
}
reserve0 = balance0;
reserve1 = balance1;
blockTimestampLast = blockTimestamp;
emit Sync(reserve0, reserve1);
}
// ---------- 添加流动性:mint ----------
function mint(address to) external returns (uint256 liquidity) {
(uint256 _reserve0, uint256 _reserve1) = (reserve0, reserve1);
uint256 balance0 = IERC20(token0).balanceOf(address(this));
uint256 balance1 = IERC20(token1).balanceOf(address(this));
uint256 amount0 = balance0 - _reserve0;
uint256 amount1 = balance1 - _reserve1;
uint256 _totalSupply = totalSupply();
if (_totalSupply == 0) {
// 首次添加流动性
liquidity = Math.sqrt(amount0 * amount1) - MINIMUM_LIQUIDITY;
_mint(address(0), MINIMUM_LIQUIDITY); // 永久锁定
} else {
// 按比例铸造,取较小值保证不被套利
liquidity = Math.min(
(amount0 * _totalSupply) / _reserve0,
(amount1 * _totalSupply) / _reserve1
);
}
require(liquidity > 0, "INSUFFICIENT_LIQUIDITY_MINTED");
_mint(to, liquidity);
_update(balance0, balance1);
emit Mint(msg.sender, amount0, amount1);
}
// ---------- 移除流动性:burn ----------
function burn(address to) external returns (uint256 amount0, uint256 amount1) {
uint256 _totalSupply = totalSupply();
uint256 liquidity = balanceOf(address(this)); // 用户先转入 LP Token
require(liquidity > 0 && _totalSupply > 0, "NO_LIQUIDITY");
amount0 = (liquidity * reserve0) / _totalSupply;
amount1 = (liquidity * reserve1) / _totalSupply;
require(amount0 > 0 && amount1 > 0, "INSUFFICIENT_LIQUIDITY_BURNED");
_burn(address(this), liquidity);
_safeTransfer(token0, to, amount0);
_safeTransfer(token1, to, amount1);
uint256 balance0 = IERC20(token0).balanceOf(address(this));
uint256 balance1 = IERC20(token1).balanceOf(address(this));
_update(balance0, balance1);
emit Burn(msg.sender, amount0, amount1, to);
}
// ---------- swap 骨架(含手续费) ----------
function swap(uint256 amount0Out, uint256 amount1Out, address to) external {
require(amount0Out > 0 || amount1Out > 0, "INSUFFICIENT_OUTPUT");
uint256 balance0 = IERC20(token0).balanceOf(address(this));
uint256 balance1 = IERC20(token1).balanceOf(address(this));
uint256 _reserve0 = reserve0;
uint256 _reserve1 = reserve1;
require(amount0Out < balance0 && amount1Out < balance1, "INSUFFICIENT_LIQUIDITY");
if (amount0Out > 0) _safeTransfer(token0, to, amount0Out);
if (amount1Out > 0) _safeTransfer(token1, to, amount1Out);
uint256 balance0Adjusted = balance0 * 1000 - amount0Out * 3;
uint256 balance1Adjusted = balance1 * 1001 - amount1Out * 3;
// 延至 19.3 展开完整输入量校验与 k 值检查
// 占位:实际需校验 (balance0 * balance1) >= (_reserve0 * _reserve1)
_update(
IERC20(token0).balanceOf(address(this)),
IERC20(token1).balanceOf(address(this))
);
emit Swap(msg.sender, 0, 0, amount0Out, amount1Out, to);
}
// ---------- 库函数:含 0.3% 手续费的输出量计算 ----------
function getAmountOut(uint256 amountIn, uint256 reserveIn, uint256 reserveOut)
public pure returns (uint256 amountOut)
{
require(amountIn > 0, "INSUFFICIENT_INPUT");
require(reserveIn > 0 && reserveOut > 0, "INSUFFICIENT_LIQUIDITY");
uint256 amountInWithFee = amountIn * 997; // 扣除 0.3% 手续费
uint256 numerator = amountInWithFee * reserveOut;
uint256 denominator = reserveIn * 1000 + amountInWithFee;
amountOut = numerator / denominator;
}
// ---------- 安全转账 ----------
function _safeTransfer(address token, address to, uint256 amount) private {
(bool success, bytes memory data) = token.call(
abi.encodeWithSelector(IERC20.transfer.selector, to, amount)
);
require(success && (data.length == 0 || abi.decode(data, (bool))), "TRANSFER_FAILED");
}
}代码要点解读:
- LP Token 即 ERC-20:
Pair合约自身继承ERC20,LP 份额本身是可转移、可交易的代币。 _update()的关键作用:每次储备量变更后调用,同步缓存的reserve0/1并更新 TWAP 累计价格。这些累计值可供外部预言机按时间窗口求平均,获得不易被闪电贷操纵的参考价格。- 含手续费的输出公式:实际链上
getAmountOut中,\( \Delta x \) 先乘以 997(即扣除 0.3% 手续费后的 99.7%),再代入恒定乘积公式:
\[
\Delta y = \frac{y \times (\Delta x \times 997)}{(x \times 1000) + (\Delta x \times 997)}
\]
- 安全防护模式:
- 地址排序防重复池:由上层 Factory 合约在创建 Pair 时强制
token0 < token1。 - 极小值参数(min 参数)防抢跑:Router 合约在调用 Pair 前校验。
- 检查-生效-交互模式:burn 先销毁 LP Token(状态变更),再转出代币(外部调用)。
本节要点小结
addLiquidity通过amountAMin / amountBMin和deadline保护用户免受滑点与抢跑。mint()和burn()的份额计算确保 LP 始终按比例持有池中资产,首次流动性扣除 1000 个最小锁定份额。_update()与 TWAP 累计价格是链上价格预言机的基础,将在 19.3 节 swap 安全中发挥关键作用。
章末小结 —— 带走的 3 个关键认知
- 恒定乘积公式 \( x \times y = k \) 是 AMM 的灵魂:它同时决定了价格自动调整、滑点曲线和流动性永不枯竭的数学保障。所有 DEX 经济行为——从单笔 swap 的成交价格到 LP 的无常损失——均可从该公式推演。
- LP Token 是将流动性数字化的份额凭证:铸造/销毁机制确保 LP 始终按份额比例持有池中两种资产;交易手续费不直接分配,而是作为储备增量不断推高 \( k \) 值,LP 的份额所对应的绝对资产量随之增长。
- 安全的流动性管理需严格遵循 Solidity 最佳实践:地址排序防重复池、最小值参数防抢跑、检查-生效-交互模式防重入——这些看似简单的模式,是 DeFi 合约从数亿级攻击中存活下来的基石。
评论
0评论加载中…