SimpleAMM合约 — 恒定乘积公式与流动性管理
### 一、AMM 核心原理:恒定乘积公式
日期: 2026-05-19 方向: Solidity 阶段: 第二阶段:框架实战 标签: #solidity #defi #amm #uniswap
今日目标
| 类型 | 内容 |
|---|---|
| 学习 | 深入理解 AMM 的恒定乘积公式 x*y=k,掌握添加/移除流动性和 swap 的数学原理 |
| 实操 | 实现完整的 SimpleAMM 合约(~150 行),包含 addLiquidity / removeLiquidity / swap |
| 产出 | 完整合约代码 + Foundry 测试 + AMM 数学推导笔记 |
核心概念
一、AMM 核心原理:恒定乘积公式
AMM(Automated Market Maker,自动做市商)是 DeFi 最重要的创新之一。传统金融用订单簿撮合买卖双方,AMM 则用数学公式和流动性池取代了中间商。
恒定乘积不变量
Uniswap V2 的核心思想极其简洁:
x * y = k
其中:
x = 池子中 Token A 的数量(reserve0)
y = 池子中 Token B 的数量(reserve1)
k = 常数(在不添加/移除流动性的情况下不变)
这个公式定义了一条双曲线。任何 swap 操作都必须沿着这条曲线移动——你放入一种 token,取出另一种 token,但 x * y 的乘积保持不变(扣除手续费后 k 实际上会略微增大)。
为什么是双曲线?
价格曲线图解:
y (Token B)
│
│╲
│ ╲
│ ╲ x * y = k
│ ╲
│ ╲
│ ╲
│ ╲
│ ╲ ╌ ╌ ╌ ╌ ╌
│ ╲
│ ╲
└──────────────────── x (Token A)
关键性质:
1. 永远不会枯竭: 曲线趋近于轴但永远不接触
2. 价格连续: 任何交易量都能得到报价
3. 大额交易 → 沿曲线移动更远 → 价格影响更大
即时价格
在任意时刻,Token A 以 Token B 计价的即时价格为:
Price_A = y / x (1 个 Token A 值多少 Token B)
Price_B = x / y (1 个 Token B 值多少 Token A)
例子:
池子: 100 ETH (x) + 200,000 USDC (y)
k = 100 * 200,000 = 20,000,000
ETH 价格 = y / x = 200,000 / 100 = 2,000 USDC/ETH
USDC 价格 = x / y = 100 / 200,000 = 0.0005 ETH/USDC
二、添加流动性的数学
添加流动性有两种情况:首次添加(池子为空)和后续添加(池子已有储备)。
首次添加流动性
当池子为空时,LP(Liquidity Provider)可以按任意比例添加两种代币,这个比例将决定初始价格。
首次添加:
LP 存入: dx 个 Token A + dy 个 Token B
初始价格: Price_A = dy / dx
初始 k: k = dx * dy
LP Token 计算:
lpTokens = sqrt(dx * dy)
为什么用 sqrt?
- LP Token 代表对池子的所有权份额
- sqrt 确保 LP Token 数量与两种代币数量的"几何平均"成正比
- 这防止了通过操纵比例来获取不公平份额
数值例子:
Alice 首次添加: 10 ETH + 20,000 USDC
k = 10 * 20,000 = 200,000
lpTokens = sqrt(10 * 20,000) = sqrt(200,000) ≈ 447.21
初始价格: 20,000 / 10 = 2,000 USDC/ETH
Uniswap V2 的 MINIMUM_LIQUIDITY:实际上 Uniswap V2 在首次铸造时会永久锁定 1000 wei 的 LP Token(发送到 address(0)),防止"首个 LP 攻击"——恶意用户通过极端比例操纵初始流动性来获利。
实际铸造量 = sqrt(dx * dy) - MINIMUM_LIQUIDITY
MINIMUM_LIQUIDITY = 1000 (wei 级别,可忽略不计)
后续添加流动性
池子已有储备时,必须按当前比例添加,否则会改变价格(套利者会立刻抹平差异)。
后续添加:
池子当前: reserveA = x, reserveB = y, totalSupply = S
LP 想添加 dx 个 Token A:
必须同时添加 dy = dx * y / x 个 Token B(按比例)
LP Token 铸造量 = min(dx / x, dy / y) * S
使用 min 的原因:
- 如果 LP 提供的比例不完全匹配,取较小比例
- 防止通过"多给一种 token"获得不公平的 LP Token
- 多余的 token 不会铸造额外 LP Token(Uniswap V2 直接退还多余部分)
数值例子:
池子: 100 ETH + 200,000 USDC, totalSupply = 4472.13 LP
Bob 想添加 10 ETH:
需要同时添加: 10 * 200,000 / 100 = 20,000 USDC
新 LP Token = min(10/100, 20000/200000) * 4472.13
= min(0.1, 0.1) * 4472.13
= 447.21 LP
添加后:
池子: 110 ETH + 220,000 USDC
Bob 份额: 447.21 / (4472.13 + 447.21) = 9.09%(合理!)
三、移除流动性
LP 可以通过销毁 LP Token 来按比例取回两种代币。
移除流动性:
LP 销毁 lpAmount 个 LP Token
取回 Token A = lpAmount / totalSupply * reserveA
取回 Token B = lpAmount / totalSupply * reserveB
数值例子:
池子: 110 ETH + 220,000 USDC, totalSupply = 4919.34 LP
Bob 持有 447.21 LP,想全部移除:
取回 ETH = 447.21 / 4919.34 * 110 = 10 ETH
取回 USDC = 447.21 / 4919.34 * 220,000 = 20,000 USDC
注意: 如果期间有 swap 发生(池子比例变化),Bob 取回的比例会不同于存入时
→ 这就是"无常损失"(Impermanent Loss)的来源
四、Swap 数学:带手续费的精确计算
基本推导
交易前: x * y = k
交易后: (x + dx) * (y - dy) = k (放入 dx,取出 dy)
由 k 不变:
x * y = (x + dx) * (y - dy)
xy = xy - x*dy + dx*y - dx*dy
0 = -x*dy + dx*y - dx*dy
x*dy + dx*dy = dx*y
dy * (x + dx) = dx * y
dy = dx * y / (x + dx)
结论: amountOut = dx * y / (x + dx)
这就是 getAmountOut 的核心公式!
加入手续费(0.3%)
Uniswap V2 对每笔 swap 收取 0.3% 手续费。手续费从输入金额中扣除,等效于只有 99.7% 的输入金额参与 swap。
加入手续费:
实际参与 swap 的金额 = dx * 997 / 1000(扣除 0.3%)
amountOut = (y * dx * 997) / (x * 1000 + dx * 997)
推导过程:
effectiveDx = dx * 997 / 1000
amountOut = effectiveDx * y / (x + effectiveDx)
= (dx * 997 / 1000) * y / (x + dx * 997 / 1000)
= (dx * 997 * y) / (x * 1000 + dx * 997)
为什么用整数运算而不是浮点数?
Solidity 没有浮点数!乘以 997/1000 改写为:
分子: y * dx * 997
分母: x * 1000 + dx * 997
全程整数运算,避免精度损失
数值例子:
池子: 100 ETH (x) + 200,000 USDC (y)
用户用 1 ETH 换 USDC:
amountOut = (200,000 * 1 * 997) / (100 * 1000 + 1 * 997)
= 199,400,000 / (100,000 + 997)
= 199,400,000 / 100,997
≈ 1,974.11 USDC
对比无手续费:
amountOut = 1 * 200,000 / (100 + 1) = 1,980.20 USDC
手续费 ≈ 6.09 USDC(≈ 0.3%)
五、价格影响(Price Impact)
Price Impact 是指一笔交易对池子价格造成的变化百分比。交易量越大(相对于池子深度),价格影响越大。
Price Impact 计算:
交易前价格: P_before = y / x
交易后价格: P_after = (y - amountOut) / (x + amountIn)
Price Impact = (P_before - P_after) / P_before * 100%
例子 1 — 小额交易:
池子: 100 ETH + 200,000 USDC
交易: 1 ETH → USDC
P_before = 200,000 / 100 = 2,000
amountOut ≈ 1,974.11 USDC(含手续费)
P_after = (200,000 - 1,974.11) / (100 + 1) = 198,025.89 / 101 ≈ 1,960.65
Price Impact ≈ (2,000 - 1,960.65) / 2,000 * 100% ≈ 1.97%
例子 2 — 大额交易:
池子: 100 ETH + 200,000 USDC
交易: 10 ETH → USDC
amountOut = (200,000 * 10 * 997) / (100 * 1000 + 10 * 997)
= 1,994,000,000 / (100,000 + 9,970)
= 1,994,000,000 / 109,970
≈ 18,132.22 USDC
P_after = (200,000 - 18,132.22) / (100 + 10) = 181,867.78 / 110 ≈ 1,653.34
Price Impact ≈ (2,000 - 1,653.34) / 2,000 * 100% ≈ 17.33%
结论:
- 1 ETH swap (1% 的池子): ~2% price impact
- 10 ETH swap (10% 的池子): ~17% price impact
- 池子越深 → 同样金额的 price impact 越小 → 更好的交易体验
六、完整 SimpleAMM 合约
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/math/Math.sol";
/**
* @title SimpleAMM
* @notice 基于恒定乘积公式(x*y=k)的 AMM 实现
* @dev 支持添加/移除流动性和 swap,收取 0.3% 手续费
* LP Token 继承 ERC20,代表对池子的所有权份额
*/
contract SimpleAMM is ERC20, ReentrancyGuard {
using SafeERC20 for IERC20;
// ===== 状态变量 =====
IERC20 public immutable token0;
IERC20 public immutable token1;
uint256 public reserve0;
uint256 public reserve1;
uint256 public constant MINIMUM_LIQUIDITY = 1000;
uint256 private constant FEE_NUMERATOR = 997;
uint256 private constant FEE_DENOMINATOR = 1000;
// ===== 自定义错误 =====
error InvalidToken();
error InsufficientLiquidity();
error InsufficientAmount();
error InsufficientLPTokens();
error InsufficientOutputAmount();
error IdenticalTokens();
error ZeroAmount();
error KInvariantViolated();
// ===== 事件 =====
event LiquidityAdded(
address indexed provider,
uint256 amount0,
uint256 amount1,
uint256 lpTokensMinted
);
event LiquidityRemoved(
address indexed provider,
uint256 amount0,
uint256 amount1,
uint256 lpTokensBurned
);
event Swap(
address indexed user,
address indexed tokenIn,
uint256 amountIn,
uint256 amountOut
);
// ===== 构造函数 =====
constructor(
address _token0,
address _token1
) ERC20("SimpleAMM LP Token", "SAMLP") {
if (_token0 == _token1) revert IdenticalTokens();
token0 = IERC20(_token0);
token1 = IERC20(_token1);
}
// ===== 核心函数 =====
/**
* @notice 添加流动性
* @param amount0Desired 希望添加的 token0 数量
* @param amount1Desired 希望添加的 token1 数量
* @return lpTokens 铸造的 LP Token 数量
*/
function addLiquidity(
uint256 amount0Desired,
uint256 amount1Desired
) external nonReentrant returns (uint256 lpTokens) {
if (amount0Desired == 0 || amount1Desired == 0) revert ZeroAmount();
uint256 _totalSupply = totalSupply();
if (_totalSupply == 0) {
// ===== 首次添加 =====
// LP Token = sqrt(amount0 * amount1) - MINIMUM_LIQUIDITY
lpTokens = Math.sqrt(amount0Desired * amount1Desired)
- MINIMUM_LIQUIDITY;
if (lpTokens == 0) revert InsufficientAmount();
// 永久锁定 MINIMUM_LIQUIDITY 到 address(1)
// 防止首个 LP 攻击(totalSupply 永远 > 0)
_mint(address(1), MINIMUM_LIQUIDITY);
} else {
// ===== 后续添加 =====
// 按比例计算 LP Token
uint256 lpTokens0 = (amount0Desired * _totalSupply) / reserve0;
uint256 lpTokens1 = (amount1Desired * _totalSupply) / reserve1;
lpTokens = lpTokens0 < lpTokens1 ? lpTokens0 : lpTokens1;
if (lpTokens == 0) revert InsufficientAmount();
}
// 转入代币
token0.safeTransferFrom(msg.sender, address(this), amount0Desired);
token1.safeTransferFrom(msg.sender, address(this), amount1Desired);
// 更新储备
reserve0 += amount0Desired;
reserve1 += amount1Desired;
// 铸造 LP Token
_mint(msg.sender, lpTokens);
emit LiquidityAdded(
msg.sender, amount0Desired, amount1Desired, lpTokens
);
}
/**
* @notice 移除流动性
* @param lpAmount 要销毁的 LP Token 数量
* @return amount0 取回的 token0 数量
* @return amount1 取回的 token1 数量
*/
function removeLiquidity(
uint256 lpAmount
) external nonReentrant returns (uint256 amount0, uint256 amount1) {
if (lpAmount == 0) revert ZeroAmount();
if (balanceOf(msg.sender) < lpAmount) revert InsufficientLPTokens();
uint256 _totalSupply = totalSupply();
// 按 LP 份额比例计算取回数量
amount0 = (lpAmount * reserve0) / _totalSupply;
amount1 = (lpAmount * reserve1) / _totalSupply;
if (amount0 == 0 || amount1 == 0) revert InsufficientAmount();
// 销毁 LP Token
_burn(msg.sender, lpAmount);
// 更新储备
reserve0 -= amount0;
reserve1 -= amount1;
// 转出代币
token0.safeTransfer(msg.sender, amount0);
token1.safeTransfer(msg.sender, amount1);
emit LiquidityRemoved(msg.sender, amount0, amount1, lpAmount);
}
/**
* @notice 用 tokenIn 换取另一种 token(收取 0.3% 手续费)
* @param tokenIn 输入的 token 地址
* @param amountIn 输入数量
* @param minAmountOut 最小输出数量(滑点保护)
* @return amountOut 实际输出数量
*/
function swap(
address tokenIn,
uint256 amountIn,
uint256 minAmountOut
) external nonReentrant returns (uint256 amountOut) {
if (amountIn == 0) revert ZeroAmount();
bool isToken0 = (tokenIn == address(token0));
if (!isToken0 && tokenIn != address(token1)) revert InvalidToken();
// 确定输入/输出储备
(uint256 reserveIn, uint256 reserveOut) = isToken0
? (reserve0, reserve1)
: (reserve1, reserve0);
if (reserveIn == 0 || reserveOut == 0)
revert InsufficientLiquidity();
// 计算输出金额(含 0.3% 手续费)
// amountOut = (reserveOut * amountIn * 997)
// / (reserveIn * 1000 + amountIn * 997)
uint256 amountInWithFee = amountIn * FEE_NUMERATOR;
amountOut = (reserveOut * amountInWithFee)
/ (reserveIn * FEE_DENOMINATOR + amountInWithFee);
if (amountOut == 0) revert InsufficientOutputAmount();
if (amountOut < minAmountOut) revert InsufficientOutputAmount();
// 转入 tokenIn
IERC20(tokenIn).safeTransferFrom(
msg.sender, address(this), amountIn
);
// 转出另一种 token 并更新储备
if (isToken0) {
reserve0 += amountIn;
reserve1 -= amountOut;
token1.safeTransfer(msg.sender, amountOut);
} else {
reserve1 += amountIn;
reserve0 -= amountOut;
token0.safeTransfer(msg.sender, amountOut);
}
// 验证 k 不变量(手续费使 k 只增不减)
if (reserve0 * reserve1 < (reserveIn * reserveOut)) {
revert KInvariantViolated();
}
emit Swap(msg.sender, tokenIn, amountIn, amountOut);
}
// ===== 视图函数 =====
/**
* @notice 计算给定输入量的预期输出量
*/
function getAmountOut(
address tokenIn,
uint256 amountIn
) external view returns (uint256 amountOut) {
bool isToken0 = (tokenIn == address(token0));
if (!isToken0 && tokenIn != address(token1)) revert InvalidToken();
(uint256 reserveIn, uint256 reserveOut) = isToken0
? (reserve0, reserve1)
: (reserve1, reserve0);
if (reserveIn == 0 || reserveOut == 0) return 0;
uint256 amountInWithFee = amountIn * FEE_NUMERATOR;
amountOut = (reserveOut * amountInWithFee)
/ (reserveIn * FEE_DENOMINATOR + amountInWithFee);
}
/**
* @notice 获取当前价格(token0 以 token1 计价,乘以 1e18 精度)
*/
function getPrice() external view returns (uint256) {
if (reserve0 == 0) return 0;
return (reserve1 * 1e18) / reserve0;
}
/**
* @notice 获取当前 k 值
*/
function getK() external view returns (uint256) {
return reserve0 * reserve1;
}
}
七、Foundry 测试
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "../src/SimpleAMM.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
// ===== Mock Tokens =====
contract MockTokenA is ERC20 {
constructor() ERC20("Token A", "TKA") {
_mint(msg.sender, 1_000_000e18);
}
function mint(address to, uint256 amount) external {
_mint(to, amount);
}
}
contract MockTokenB is ERC20 {
constructor() ERC20("Token B", "TKB") {
_mint(msg.sender, 1_000_000e18);
}
function mint(address to, uint256 amount) external {
_mint(to, amount);
}
}
contract SimpleAMMTest is Test {
SimpleAMM public amm;
MockTokenA public tokenA;
MockTokenB public tokenB;
address public alice = makeAddr("alice");
address public bob = makeAddr("bob");
address public charlie = makeAddr("charlie");
function setUp() public {
tokenA = new MockTokenA();
tokenB = new MockTokenB();
amm = new SimpleAMM(address(tokenA), address(tokenB));
// 给 alice、bob、charlie 分配代币
tokenA.transfer(alice, 100_000e18);
tokenB.transfer(alice, 200_000e18);
tokenA.transfer(bob, 50_000e18);
tokenB.transfer(bob, 100_000e18);
tokenA.transfer(charlie, 10_000e18);
tokenB.transfer(charlie, 10_000e18);
}
// ===== 添加流动性测试 =====
function test_AddLiquidity_FirstTime() public {
vm.startPrank(alice);
tokenA.approve(address(amm), 100e18);
tokenB.approve(address(amm), 200e18);
uint256 lpTokens = amm.addLiquidity(100e18, 200e18);
vm.stopPrank();
// LP Token = sqrt(100e18 * 200e18) - 1000
uint256 expectedLP = Math.sqrt(100e18 * 200e18) - 1000;
assertEq(lpTokens, expectedLP);
assertEq(amm.reserve0(), 100e18);
assertEq(amm.reserve1(), 200e18);
assertGt(amm.totalSupply(), 0);
// 验证 MINIMUM_LIQUIDITY 被锁定
assertEq(amm.balanceOf(address(1)), 1000);
}
function test_AddLiquidity_Subsequent() public {
// Alice 首次添加
vm.startPrank(alice);
tokenA.approve(address(amm), 100e18);
tokenB.approve(address(amm), 200e18);
amm.addLiquidity(100e18, 200e18);
vm.stopPrank();
uint256 totalSupplyBefore = amm.totalSupply();
// Bob 后续添加(按比例)
vm.startPrank(bob);
tokenA.approve(address(amm), 10e18);
tokenB.approve(address(amm), 20e18);
uint256 bobLP = amm.addLiquidity(10e18, 20e18);
vm.stopPrank();
// Bob 应该获得 10% 的额外 LP Token
uint256 expectedBobLP = (10e18 * totalSupplyBefore) / 100e18;
assertEq(bobLP, expectedBobLP);
assertEq(amm.reserve0(), 110e18);
assertEq(amm.reserve1(), 220e18);
}
// ===== 移除流动性测试 =====
function test_RemoveLiquidity() public {
// Alice 添加流动性
vm.startPrank(alice);
tokenA.approve(address(amm), 100e18);
tokenB.approve(address(amm), 200e18);
uint256 aliceLP = amm.addLiquidity(100e18, 200e18);
// Alice 移除全部流动性
uint256 balanceA_before = tokenA.balanceOf(alice);
uint256 balanceB_before = tokenB.balanceOf(alice);
(uint256 amount0, uint256 amount1) = amm.removeLiquidity(aliceLP);
vm.stopPrank();
// 因为 MINIMUM_LIQUIDITY 被锁定,Alice 取不回全部
assertLt(amount0, 100e18);
assertLt(amount1, 200e18);
assertGt(amount0, 99e18); // 但差距极小
assertGt(amount1, 199e18);
// 余额增加
assertEq(tokenA.balanceOf(alice), balanceA_before + amount0);
assertEq(tokenB.balanceOf(alice), balanceB_before + amount1);
}
// ===== Swap 测试 =====
function test_Swap_Token0ForToken1() public {
// Alice 添加流动性
vm.startPrank(alice);
tokenA.approve(address(amm), 100e18);
tokenB.approve(address(amm), 200e18);
amm.addLiquidity(100e18, 200e18);
vm.stopPrank();
// Bob 用 1 TokenA 换 TokenB
vm.startPrank(bob);
tokenA.approve(address(amm), 1e18);
uint256 expectedOut = amm.getAmountOut(address(tokenA), 1e18);
uint256 balanceB_before = tokenB.balanceOf(bob);
uint256 amountOut = amm.swap(address(tokenA), 1e18, 0);
vm.stopPrank();
assertEq(amountOut, expectedOut);
assertEq(tokenB.balanceOf(bob), balanceB_before + amountOut);
// 验证 k 只增不减
assertGe(amm.reserve0() * amm.reserve1(), 100e18 * 200e18);
}
function test_Swap_SlippageProtection() public {
// Alice 添加流动性
vm.startPrank(alice);
tokenA.approve(address(amm), 100e18);
tokenB.approve(address(amm), 200e18);
amm.addLiquidity(100e18, 200e18);
vm.stopPrank();
// Bob 设置过高的 minAmountOut → revert
vm.startPrank(bob);
tokenA.approve(address(amm), 1e18);
vm.expectRevert(SimpleAMM.InsufficientOutputAmount.selector);
amm.swap(address(tokenA), 1e18, 100e18);
vm.stopPrank();
}
function test_Swap_PriceImpact() public {
// Alice 添加流动性
vm.startPrank(alice);
tokenA.approve(address(amm), 100e18);
tokenB.approve(address(amm), 200e18);
amm.addLiquidity(100e18, 200e18);
vm.stopPrank();
// 小额 swap 的单价 vs 大额 swap 的单价
uint256 smallSwapOut = amm.getAmountOut(address(tokenA), 1e18);
uint256 largeSwapOut = amm.getAmountOut(address(tokenA), 10e18);
// 大额交易单价更差(价格影响更大)
uint256 smallPricePerUnit = (smallSwapOut * 1e18) / 1e18;
uint256 largePricePerUnit = (largeSwapOut * 1e18) / 10e18;
assertGt(smallPricePerUnit, largePricePerUnit);
}
function test_Swap_FeeAccrual() public {
// Alice 添加流动性
vm.startPrank(alice);
tokenA.approve(address(amm), 100e18);
tokenB.approve(address(amm), 200e18);
amm.addLiquidity(100e18, 200e18);
vm.stopPrank();
uint256 k_before = amm.getK();
// Charlie 做 swap(支付手续费)
vm.startPrank(charlie);
tokenA.approve(address(amm), 5e18);
amm.swap(address(tokenA), 5e18, 0);
vm.stopPrank();
uint256 k_after = amm.getK();
// k 应该增加(手续费留在池子中)
assertGt(k_after, k_before);
}
// ===== 边界条件测试 =====
function test_RevertWhen_SwapWithZeroAmount() public {
vm.expectRevert(SimpleAMM.ZeroAmount.selector);
amm.swap(address(tokenA), 0, 0);
}
function test_RevertWhen_SwapWithInvalidToken() public {
vm.expectRevert(SimpleAMM.InvalidToken.selector);
amm.swap(address(0xdead), 1e18, 0);
}
function test_RevertWhen_SwapWithNoLiquidity() public {
vm.startPrank(bob);
tokenA.approve(address(amm), 1e18);
vm.expectRevert(SimpleAMM.InsufficientLiquidity.selector);
amm.swap(address(tokenA), 1e18, 0);
vm.stopPrank();
}
}
关键要点总结
AMM 设计的核心权衡
1. 简单性 vs 资本效率
恒定乘积 x*y=k 极简优雅,但资本效率低
→ 大部分流动性分布在不常交易的价格区间
→ Uniswap V3 的集中流动性解决了这个问题
2. 价格影响 vs 流动性深度
池子越深 → 同样金额的 price impact 越小
→ 吸引流动性是 DEX 的核心增长策略
→ 流动性挖矿、交易费分成都是手段
3. 手续费 vs 交易量
手续费太高 → 交易者被吓走
手续费太低 → LP 没有激励
→ 0.3% 是经过市场验证的平衡点
→ V3 引入多个费率档位 (0.01%, 0.05%, 0.3%, 1%)
4. LP 收益 vs 无常损失
LP 赚取手续费,但承受无常损失风险
→ 只有手续费收益 > 无常损失时,做 LP 才划算
→ 高波动性资产对的无常损失更大
SimpleAMM 合约检查清单
安全性:
✓ ReentrancyGuard 防止重入攻击
✓ SafeERC20 处理非标准 ERC20
✓ MINIMUM_LIQUIDITY 防止首个 LP 攻击
✓ minAmountOut 滑点保护
✓ k 不变量验证(交易后 k 只增不减)
数学精度:
✓ 全程整数运算(乘法先于除法,减少精度损失)
✓ 手续费用 997/1000 整数比例实现
✓ LP Token 用 sqrt 计算(OpenZeppelin Math 库)
生产差距(我们简化了什么):
✗ 没有 Oracle 价格(Uniswap V2 有 TWAP)
✗ 没有工厂合约(Uniswap 用 Factory 创建交易对)
✗ 没有 Router 合约(多跳路由、deadline 等)
✗ 没有协议费(Uniswap 可以开启 1/6 手续费给协议)
✗ 添加流动性没有退还多余代币
常见误区
误区 1: "x*y=k 中的 k 永远不变"
✗ 错误: k 在任何情况下都是常数
✓ 正确:
- 纯 swap 时: k 因手续费略微增加(0.3% 留在池子中)
- 添加流动性时: k 增大(池子变深)
- 移除流动性时: k 减小(池子变浅)
- 准确说法: "在单次 swap 中,k 只增不减"
误区 2: "LP Token = 我存入代币的凭证"
✗ 错误: LP Token 只是存款凭证,取回时得到原始存入量
✓ 正确:
LP Token 代表对池子的按比例所有权
→ 池子比例可能变化(因为有 swap)
→ 取回的代币比例可能不同于存入时
→ 这就是无常损失的根本原因
例子:
存入: 10 ETH + 20,000 USDC(ETH 价格 2,000)
ETH 涨到 4,000 后取回:
可能得到: ~7.07 ETH + 28,284 USDC(总价值低于 HODL)
误区 3: "手续费 0.3% 很低,对交易者影响不大"
✗ 错误: 0.3% 手续费可以忽略
✓ 正确:
- 多跳路由时手续费累加: ETH→USDC→DAI = 0.6%
- 大额交易还有 price impact(可能远超手续费)
- MEV(三明治攻击)可能额外增加成本
- 对高频交易者,手续费是重要的成本因素
这也是为什么:
- 聚合器(1inch/Paraswap)寻找最优路径
- Uniswap V3 引入 0.01% 费率档位(给稳定币对)
- CoW Protocol/UniswapX 用 Solver 优化执行
误区 4: "首次添加流动性的 LP 没有风险"
✗ 错误: 首个 LP 最安全
✓ 正确:
首次 LP 有特殊风险:
1. 定价风险: 初始比例决定价格,定价偏离市场 → 套利者立刻搬走利润
2. MINIMUM_LIQUIDITY: 被永久锁定 1000 wei(极小,可忽略)
3. 首个 LP 攻击(如果没有 MINIMUM_LIQUIDITY):
- 攻击者添加极少流动性(如 1 wei + 1 wei)
- 然后直接向池子转账大量代币(不通过 addLiquidity)
- totalSupply 极低但实际资产极高
- 后续 LP 因整数截断得到 0 LP Token
- 攻击者独吞所有新添加的流动性
面试关联
Q1: 解释恒定乘积做市商的工作原理
简短回答:恒定乘积 AMM 用公式 x*y=k 维护两种代币的价格曲线。池子中两种代币的数量乘积保持不变,任何 swap 都沿双曲线滑动——放入一种代币,取出另一种,大额交易造成更大的价格影响。LP 提供流动性赚取 0.3% 手续费,但承受无常损失风险。
追问: 为什么 Uniswap V3 要引入集中流动性?
V2 的问题:
流动性均匀分布在 0 到 ∞ 的价格范围
但 99% 的交易发生在很窄的价格区间
→ 大量资本处于"休眠"状态,资本效率极低
V3 的解决方案:
LP 可以选择在特定价格区间提供流动性
→ 在活跃区间内,相同资本深度更大
→ 资本效率提升 4000 倍(理论上)
代价:
→ LP 需要主动管理头寸(价格脱离区间后不赚手续费)
→ 无常损失可能更大(集中 = 放大风险)
→ JIT (Just-In-Time) Liquidity 攻击
Q2: 如何防止 AMM 中的价格操纵?
AMM 价格操纵的原理:
攻击者用大额 swap 扭曲池子价格
→ 其他协议如果依赖即时价格(spot price)作为预言机
→ 攻击者可以在同一笔交易中利用扭曲的价格获利
防御方法:
1. TWAP (Time-Weighted Average Price)
→ 使用跨区块的时间加权平均价,无法在单笔交易中操纵
2. Chainlink 预言机
→ 使用链下聚合的多数据源价格
3. 流动性深度要求
→ 池子足够深时,操纵成本极高
4. 延迟机制
→ commit-reveal 防止同笔交易中的价格依赖
Q3: 什么是无常损失?LP 什么时候做 LP 是划算的?
无常损失公式:
IL = 2 * sqrt(priceRatio) / (1 + priceRatio) - 1
价格变化 2x → IL ≈ -5.7%
价格变化 5x → IL ≈ -25.5%
LP 做市划算的条件:
手续费收入 > 无常损失
有利场景:
✓ 高交易量的交易对(手续费多)
✓ 低波动性资产对(如 USDC/USDT,几乎无 IL)
✓ 双向交易均衡(价格回归)
不利场景:
✗ 低交易量 + 高波动(手续费少、IL 大)
✗ 单向价格移动(如 meme coin 暴涨然后归零)
参考资源
| 资源 | 说明 |
|---|---|
| Uniswap V2 白皮书 | 恒定乘积 AMM 的原始设计,10 页必读 |
| Uniswap V2 Core 源码 | 生产级 AMM 实现,对照学习 |
| Uniswap V3 白皮书 | 集中流动性的数学推导 |
| Pintail: Uniswap - A Good Deal for LPs? | 无常损失的经典分析 |
| Solidity by Example - AMM | 简化版教学代码 |
| OpenZeppelin Math.sol | sqrt 实现参考 |