DeFi 协议开发实战:从 Uniswap V2 恒定乘积公式 x * y = k 到自定义 AMM 流动性池算子实现
DeFi 协议开发实战:从 Uniswap V2 恒定乘积公式 x * y = k 到自定义 AMM 流动性池算子实现
在去中心化金融(DeFi)生态系统中,自动做市商(AMM, Automated Market Maker)是流动性兑换的底层引擎。不同于传统订单簿(Order Book)依赖买卖双方的显式报价,AMM 依靠智能合约内的数学算法,实现了去中心化、无需许可的即时交易。以 Uniswap V2 为代表的经典 AMM 协议,开创性地采用了**恒定乘积(Constant Product Market Maker)**公式。理解这套数学模型的推导机制,并能够手写实现其流动性计价与份额代币(LP Token)分配逻辑,是掌握 DeFi 智能合约开发的必修课。本文将深度剖析 Uniswap V2 的核心算数原理,并用 Solidity 实现一个完整的 AMM 流动性池合约。
一、 恒定乘积公式与兑换数学模型
恒定乘积 AMM 的核心数学模型是:
$$x \times y = k$$
其中:
- $x$:资金池内代币 A(Token0)的储备量(Reserve)。
- $y$:资金池内代币 B(Token1)的储备量(Reserve)。
- $k$:在不发生流动性添加或提取的前提下,乘积 $k$ 保持恒定不变。
1.1 兑换数量(Out Amount)公式推导
假设交易者存入数量为 $\Delta x$ 的 Token0,以此换取数量为 $\Delta y$ 的 Token1。为了维持 $k$ 值恒定,必须满足:
$$(x + \Delta x) \times (y - \Delta y) = k$$
将 $k = x \times y$ 代入:
$$(x + \Delta x) \times (y - \Delta y) = x \times y$$
展开公式:
$$x \cdot y - x \cdot \Delta y + \Delta x \cdot y - \Delta x \cdot \Delta y = x \cdot y$$
消去左右两边的 $x \cdot y$,并整理出 $\Delta y$:
$$\Delta x \cdot y = (x + \Delta x) \cdot \Delta y$$
$$\Delta y = \frac{y \cdot \Delta x}{x + \Delta x}$$
在实际生产中,协议通常会对注入的代币征收交易手续费(例如 Uniswap V2 收取 0.3% 的手续费)。若将手续费率定义为 $f$(如 $f = 0.003$),则实际参与兑换的代币量为 $\Delta x \times (1 - f)$。兑换公式演变为:
$$\Delta y = \frac{y \cdot \Delta x \cdot (1 - f)}{x + \Delta x \cdot (1 - f)}$$
二、 AMM 运作机制与数据流
一个标准的 AMM 流动性池主要包含以下三个核心动作:
flowchart TD subgraph AMM_Engine [AMM 引擎] K[Constant K = x * y] R0[Reserve0 = x] R1[Reserve1 = y] end User_Add([添加流动性]) -->|注入 x 和 y 数量| Mint_LP[按比例增量铸造 LP Token] User_Remove([提取流动性]) -->|销毁 LP Token| Burn_LP[按比例退回 x 和 y 余额] User_Swap([兑换 Swap]) -->|注入 dx| Calc{计算 dy 并更新 K} Calc -->|扣除 0.3% 手续费| Out_dy[给用户划转 dy 额度] Calc -->|累加 dx 储备| R02.1 流动性代币(LP Token)的分配模型
当用户向池内首次注入流动性时,为了衡量其出资比例,合约会向其铸造一定数量的流动性代币(Liquidity Provider Token)。
- 初次注入:铸造量为注入量的几何平均值,即 $S_{minted} = \sqrt{x \cdot y}$。
- 后续追加入:根据资产占当前总储备的比例的最小值确定,避免套利,即 $S_{minted} = \min(\frac{\Delta x}{x} \cdot S_{total}, \frac{\Delta y}{y} \cdot S_{total})$。
- 赎回提取:根据用户销毁的 LP 数量占总供应量的比例退回资产,即 $\Delta x = \frac{S_{burn}}{S_{total}} \cdot x$。
三、 价格滑点与无常损失
- 价格滑点(Slippage):
由于恒定乘积曲线的斜率随交易量变化而改变,单笔交易量占池中储备比例越大,交易执行价格就会越偏离当前市场价。这种现象即为滑点。 - 无常损失(Impermanent Loss):
当池中代币的外部市场汇率发生背离时,套利者会入场通过低买高卖榨干价值,使得流动性提供者(LP)的最终资产总值低于单纯持有现货的资产总值。只有当价格回归初始状态时,这一损失才会消失。
四、 工业级 AMM 资金池 Solidity 完整实现
下面是一个完整的、符合生产级编译标准的 AMM 合约实现。合约集成了 ERC-20 基础代币(作为 LP Token)、恒定乘积兑换公式逻辑、按比例添加/提取流动性以及带 0.3% 手续费的兑换算子,代码不包含任何占位符。
// SPDX-License-Identifier: MIT pragma solidity 0.8.20; /** * @dev 极简 ERC20 代币标准接口 */ interface IERC20 { function totalSupply() external view returns (uint256); function balanceOf(address account) external view returns (uint256); function transfer(address recipient, uint256 amount) external returns (bool); function transferFrom(address sender, address recipient, uint256 amount) external returns (bool); } /** * @title 自定义恒定乘积 AMM 流动性池合约 */ contract ConstantProductAMM { IERC20 public immutable token0; IERC20 public immutable token1; uint256 public reserve0; uint256 public reserve1; uint256 public totalLPSupply; mapping(address => uint256) public lpBalanceOf; event Mint(address indexed provider, uint256 amount0, uint256 amount1, uint256 lpAmount); event Burn(address indexed provider, uint256 amount0, uint256 amount1, uint256 lpAmount); event Swap(address indexed swapper, address indexed tokenIn, uint256 amountIn, uint256 amountOut); constructor(address _token0, address _token1) { token0 = IERC20(_token0); token1 = IERC20(_token1); } // ========================================================================= // 内部数学辅助函数 // ========================================================================= /** * @dev 巴比伦求平方根算法 */ function _sqrt(uint256 y) internal pure returns (uint256 z) { if (y > 3) { z = y; uint256 x = y / 2 + 1; while (x < z) { z = x; x = (y / x + x) / 2; } } else if (y != 0) { z = 1; } } function _min(uint256 x, uint256 y) internal pure returns (uint256) { return x < y ? x : y; } // ========================================================================= // 核心流动性动作 // ========================================================================= /** * @notice 添加流动性并铸造 LP Token */ function addLiquidity(uint256 _amount0Max, uint256 _amount1Max) external returns (uint256 lpShares) { // 先把资产从用户钱包拉入池中 token0.transferFrom(msg.sender, address(this), _amount0Max); token1.transferFrom(msg.sender, address(this), _amount1Max); uint256 balance0 = token0.balanceOf(address(this)); uint256 balance1 = token1.balanceOf(address(this)); uint256 amount0 = balance0 - reserve0; uint256 amount1 = balance1 - reserve1; // 计算分配的 LP 份额 if (totalLPSupply == 0) { // 首次注入:直接按几何平均值计算份额 lpShares = _sqrt(amount0 * amount1); } else { // 后续追加:按注入资产占池储备的较小比例计算,防套利 lpShares = _min( (amount0 * totalLPSupply) / reserve0, (amount1 * totalLPSupply) / reserve1 ); } require(lpShares > 0, "Insufficient liquidity created"); // 铸造 LP 代币给用户 lpBalanceOf[msg.sender] += lpShares; totalLPSupply += lpShares; // 更新储备量 reserve0 = token0.balanceOf(address(this)); reserve1 = token1.balanceOf(address(this)); emit Mint(msg.sender, amount0, amount1, lpShares); } /** * @notice 销毁 LP Token,按比例回退双端代币 */ function removeLiquidity(uint256 _lpShares) external returns (uint256 amount0, uint256 amount1) { require(lpBalanceOf[msg.sender] >= _lpShares, "Insufficient LP shares"); // 依据份额比例计算返还额度 amount0 = (_lpShares * reserve0) / totalLPSupply; amount1 = (_lpShares * reserve1) / totalLPSupply; require(amount0 > 0 && amount1 > 0, "Liquidity amounts too small"); // 销毁 LP Token lpBalanceOf[msg.sender] -= _lpShares; totalLPSupply -= _lpShares; // 更新状态并执行转账给用户 reserve0 -= amount0; reserve1 -= amount1; token0.transfer(msg.sender, amount0); token1.transfer(msg.sender, amount1); emit Burn(msg.sender, amount0, amount1, _lpShares); } // ========================================================================= // 兑换核心算子 // ========================================================================= /** * @notice 执行代币兑换 (含 0.3% 手续费) * @param _amountIn 存入的代币数量 * @param _tokenIn 存入代币的地址 (必须为 token0 或 token1) */ function swap(uint256 _amountIn, address _tokenIn) external returns (uint256 amountOut) { require(_tokenIn == address(token0) || _tokenIn == address(token1), "Invalid input token"); require(_amountIn > 0, "Amount must be greater than 0"); bool isToken0 = _tokenIn == address(token0); (uint256 reserveIn, uint256 reserveOut) = isToken0 ? (reserve0, reserve1) : (reserve1, reserve0); // 划转输入代币 IERC20(_tokenIn).transferFrom(msg.sender, address(this), _amountIn); // 计算实际带手续费的输入额 (扣除 0.3% 手续费) // 997 / 1000 相当于乘以 0.997 uint256 amountInWithFee = _amountIn * 997; // 分子: dy = (reserveOut * dxWithFee) / (reserveIn * 1000 + dxWithFee) uint256 numerator = amountInWithFee * reserveOut; uint256 denominator = (reserveIn * 1000) + amountInWithFee; amountOut = numerator / denominator; require(amountOut > 0, "Insufficient output amount"); // 更新储备与划转输出 if (isToken0) { reserve0 = token0.balanceOf(address(this)); reserve1 = reserve1 - amountOut; token1.transfer(msg.sender, amountOut); } else { reserve1 = token1.balanceOf(address(this)); reserve0 = reserve0 - amountOut; token0.transfer(msg.sender, amountOut); } emit Swap(msg.sender, _tokenIn, _amountIn, amountOut); } }