本节实现 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 | 两种代币当前储备 |
totalSupply | LP Token 总量(份额) |
kLast | 上次更新的 值(用于收集协议费) |
price0CumulativeLast | 累计价格(用于 TWAP,见 19.4) |
19.3.2 添加流动性:铸造 LP Token
首次添加( 为铸造的 LP 量):
后续添加(按比例):
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 兑回双币
其中 为销毁的 LP 量, 为 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% 手续费留池):
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 |
| 储备以实际余额为准 | _update 用 balanceOf(address(this)),杜绝用传入参数伪造 */
先 _update 后外部调用 | 遵循 "checks-effects-interactions" 防重入 |
| 滑点保护在 Router | Pair 自身不防滑点,由 Router 的 min 参数承担 |
| 整数除法向零舍入 | 输出向下取整,防止 LP 被四舍五入薅走 |
3 个关键认知:① 首铸锁定 + 按比例铸造/销毁保证 LP 份额公平;② 手续费留池让 增长,是 LP 收益来源;③ 储备以实际余额为准并遵守重入防护,是 Pair 安全的两条铁律。
, 前往 → 19.4 DEX 安全 |*
评论
0评论加载中…