AMM进阶 - 手续费 + 滑点保护 + 价格预言机接口
### 1. AMM 手续费机制
日期: 2026-05-11 方向: Solidity 阶段: 第二阶段:框架实战 (41-48) 标签: #AMM #DeFi #滑点保护 #预言机 #UniswapV2
今日目标
- 在基础 AMM(x * y = k)基础上实现 0.3% 手续费机制
- 实现 滑点保护(
minimumAmountOut)防止用户遭受超预期损失 - 理解 TWAP(时间加权平均价格)预言机原理并实现接口
- 对比 Uniswap V2 与 V3 的 AMM 设计差异
核心概念
1. AMM 手续费机制
在 Uniswap V2 中,每次 swap 会收取 0.3% 的手续费。这个手续费的实现方式非常巧妙——它不是从输出中扣除,而是在计算输出前从输入中扣除。
数学公式
无手续费的恒定乘积公式:
x * y = k
(x + Δx) * (y - Δy) = k
Δy = y * Δx / (x + Δx)
加入 0.3% 手续费后:
实际输入 = Δx * 997 / 1000 (扣除 0.3%)
Δy = y * (Δx * 997) / (x * 1000 + Δx * 997)
为什么用 997/1000 而不是直接乘以 0.997?因为 Solidity 不支持浮点数,使用整数乘除避免精度丢失。
手续费去向
- Uniswap V2: 0.3% 全部归 LP(流动性提供者)
- Uniswap V3: 手续费可配置(0.01% / 0.05% / 0.3% / 1%),且部分可归协议金库
- Curve: 手续费更低(0.04%),适合稳定币交换
2. 滑点保护(Slippage Protection)
滑点(Slippage)是指预期价格与实际成交价格之间的差异。在 AMM 中,滑点来源于:
- 价格冲击(Price Impact): 交易量相对于池子大小较大时,价格会显著偏移
- 前跑攻击(Front-running): MEV 机器人在你的交易前插入交易,推高价格
- 区块延迟: 提交交易到上链之间,池子状态可能已变化
滑点保护的实现方式是让用户指定 minimumAmountOut,如果实际输出低于此值,交易回滚。
用户期望输出: 100 ETH
允许滑点: 0.5%
minimumAmountOut = 100 * (1 - 0.005) = 99.5 ETH
3. TWAP 预言机
TWAP(Time-Weighted Average Price)是一种抗操纵的价格计算方式。
原理:每个区块记录累积价格(priceCumulativeLast),要计算一段时间内的平均价格:
TWAP = (priceCumulative_t2 - priceCumulative_t1) / (t2 - t1)
攻击者要操纵 TWAP,需要在多个连续区块中维持异常价格,成本极高。
代码实战
完整的增强版 AMM 合约
// 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/utils/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/math/Math.sol";
/// @title EnhancedAMM - 带手续费、滑点保护和价格预言机的 AMM
/// @notice 实现了 Uniswap V2 风格的恒定乘积做市商
contract EnhancedAMM is ERC20, ReentrancyGuard {
IERC20 public immutable tokenA;
IERC20 public immutable tokenB;
uint256 public reserveA;
uint256 public reserveB;
// 手续费: 3/1000 = 0.3%
uint256 public constant FEE_NUMERATOR = 3;
uint256 public constant FEE_DENOMINATOR = 1000;
// 价格预言机相关
uint256 public priceACumulativeLast; // tokenA 的累积价格
uint256 public priceBCumulativeLast; // tokenB 的累积价格
uint32 public blockTimestampLast; // 上次更新时间
// UQ112x112 定点数精度(Uniswap V2 使用的格式)
uint224 private constant Q112 = 2**112;
// 最小流动性(防止首次添加流动性时的攻击)
uint256 private constant MINIMUM_LIQUIDITY = 1000;
event Swap(
address indexed sender,
uint256 amountIn,
uint256 amountOut,
bool isAToB
);
event AddLiquidity(
address indexed provider,
uint256 amountA,
uint256 amountB,
uint256 liquidity
);
event RemoveLiquidity(
address indexed provider,
uint256 amountA,
uint256 amountB,
uint256 liquidity
);
event OracleUpdated(
uint256 priceACumulative,
uint256 priceBCumulative,
uint32 blockTimestamp
);
constructor(
address _tokenA,
address _tokenB
) ERC20("AMM LP Token", "AMM-LP") {
require(_tokenA != _tokenB, "Identical tokens");
tokenA = IERC20(_tokenA);
tokenB = IERC20(_tokenB);
}
/// @notice 更新价格累积器(预言机核心)
function _updateOracle() private {
uint32 blockTimestamp = uint32(block.timestamp);
uint32 timeElapsed;
unchecked {
timeElapsed = blockTimestamp - blockTimestampLast;
}
if (timeElapsed > 0 && reserveA > 0 && reserveB > 0) {
// 累积价格 += 当前价格 * 经过时间
// 价格用 UQ112x112 定点数表示
unchecked {
priceACumulativeLast += uint256(
(Q112 * reserveB) / reserveA
) * timeElapsed;
priceBCumulativeLast += uint256(
(Q112 * reserveA) / reserveB
) * timeElapsed;
}
blockTimestampLast = blockTimestamp;
emit OracleUpdated(
priceACumulativeLast,
priceBCumulativeLast,
blockTimestamp
);
}
}
/// @notice 添加流动性
/// @param amountA tokenA 的数量
/// @param amountB tokenB 的数量
/// @return liquidity 获得的 LP Token 数量
function addLiquidity(
uint256 amountA,
uint256 amountB
) external nonReentrant returns (uint256 liquidity) {
require(amountA > 0 && amountB > 0, "Amounts must be > 0");
// 更新预言机
_updateOracle();
uint256 _totalSupply = totalSupply();
if (_totalSupply == 0) {
// 首次添加流动性
liquidity = Math.sqrt(amountA * amountB) - MINIMUM_LIQUIDITY;
// 永久锁定最小流动性到零地址,防止流动性归零攻击
_mint(address(0xdead), MINIMUM_LIQUIDITY);
} else {
// 按比例计算 LP Token
liquidity = Math.min(
(amountA * _totalSupply) / reserveA,
(amountB * _totalSupply) / reserveB
);
}
require(liquidity > 0, "Insufficient liquidity minted");
tokenA.transferFrom(msg.sender, address(this), amountA);
tokenB.transferFrom(msg.sender, address(this), amountB);
reserveA += amountA;
reserveB += amountB;
_mint(msg.sender, liquidity);
emit AddLiquidity(msg.sender, amountA, amountB, liquidity);
}
/// @notice 移除流动性
/// @param liquidity 要销毁的 LP Token 数量
/// @return amountA 返还的 tokenA 数量
/// @return amountB 返还的 tokenB 数量
function removeLiquidity(
uint256 liquidity
) external nonReentrant returns (uint256 amountA, uint256 amountB) {
require(liquidity > 0, "Liquidity must be > 0");
_updateOracle();
uint256 _totalSupply = totalSupply();
amountA = (liquidity * reserveA) / _totalSupply;
amountB = (liquidity * reserveB) / _totalSupply;
require(amountA > 0 && amountB > 0, "Insufficient amounts");
_burn(msg.sender, liquidity);
reserveA -= amountA;
reserveB -= amountB;
tokenA.transfer(msg.sender, amountA);
tokenB.transfer(msg.sender, amountB);
emit RemoveLiquidity(msg.sender, amountA, amountB, liquidity);
}
/// @notice 交换 tokenA → tokenB(带手续费和滑点保护)
/// @param amountIn 输入的 tokenA 数量
/// @param minAmountOut 最小输出量(滑点保护)
/// @return amountOut 实际输出的 tokenB 数量
function swapAForB(
uint256 amountIn,
uint256 minAmountOut
) external nonReentrant returns (uint256 amountOut) {
require(amountIn > 0, "Amount must be > 0");
_updateOracle();
// 计算扣除手续费后的有效输入
// amountInWithFee = amountIn * 997 / 1000
uint256 amountInWithFee = amountIn * (FEE_DENOMINATOR - FEE_NUMERATOR);
// Δy = y * amountInWithFee / (x * 1000 + amountInWithFee)
amountOut = (reserveB * amountInWithFee) /
(reserveA * FEE_DENOMINATOR + amountInWithFee);
// 滑点保护检查
require(amountOut >= minAmountOut, "Slippage: output below minimum");
require(amountOut < reserveB, "Insufficient reserve");
tokenA.transferFrom(msg.sender, address(this), amountIn);
tokenB.transfer(msg.sender, amountOut);
reserveA += amountIn;
reserveB -= amountOut;
// 恒定乘积不变量检查(k 只增不减,因为有手续费)
assert(reserveA * reserveB >= (reserveA - amountIn) * (reserveB + amountOut));
emit Swap(msg.sender, amountIn, amountOut, true);
}
/// @notice 交换 tokenB → tokenA(带手续费和滑点保护)
function swapBForA(
uint256 amountIn,
uint256 minAmountOut
) external nonReentrant returns (uint256 amountOut) {
require(amountIn > 0, "Amount must be > 0");
_updateOracle();
uint256 amountInWithFee = amountIn * (FEE_DENOMINATOR - FEE_NUMERATOR);
amountOut = (reserveA * amountInWithFee) /
(reserveB * FEE_DENOMINATOR + amountInWithFee);
require(amountOut >= minAmountOut, "Slippage: output below minimum");
require(amountOut < reserveA, "Insufficient reserve");
tokenB.transferFrom(msg.sender, address(this), amountIn);
tokenA.transfer(msg.sender, amountOut);
reserveB += amountIn;
reserveA -= amountOut;
emit Swap(msg.sender, amountIn, amountOut, false);
}
/// @notice 查询 swap 预估输出(不含滑点保护,纯计算)
function getAmountOut(
uint256 amountIn,
bool isAToB
) external view returns (uint256 amountOut) {
uint256 resIn = isAToB ? reserveA : reserveB;
uint256 resOut = isAToB ? reserveB : reserveA;
uint256 amountInWithFee = amountIn * (FEE_DENOMINATOR - FEE_NUMERATOR);
amountOut = (resOut * amountInWithFee) /
(resIn * FEE_DENOMINATOR + amountInWithFee);
}
/// @notice 获取当前即时价格(tokenB / tokenA)
function getSpotPrice() external view returns (uint256) {
require(reserveA > 0, "No liquidity");
return (reserveB * 1e18) / reserveA;
}
/// @notice 计算价格冲击(百分比,精度 1e4 = 100%)
function getPriceImpact(
uint256 amountIn,
bool isAToB
) external view returns (uint256 impactBps) {
uint256 resIn = isAToB ? reserveA : reserveB;
uint256 resOut = isAToB ? reserveB : reserveA;
// 理想价格(无冲击)
uint256 idealOut = (amountIn * resOut) / resIn;
// 实际输出(含手续费和价格冲击)
uint256 amountInWithFee = amountIn * (FEE_DENOMINATOR - FEE_NUMERATOR);
uint256 actualOut = (resOut * amountInWithFee) /
(resIn * FEE_DENOMINATOR + amountInWithFee);
// 价格冲击 = (理想输出 - 实际输出) / 理想输出 * 10000
impactBps = ((idealOut - actualOut) * 10000) / idealOut;
}
}
TWAP 预言机消费者合约
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface IEnhancedAMM {
function priceACumulativeLast() external view returns (uint256);
function priceBCumulativeLast() external view returns (uint256);
function blockTimestampLast() external view returns (uint32);
function reserveA() external view returns (uint256);
function reserveB() external view returns (uint256);
}
/// @title TWAPOracle - 读取 AMM 的 TWAP 价格
/// @notice 需要在不同时间点调用 update() 来记录快照
contract TWAPOracle {
IEnhancedAMM public immutable amm;
uint256 public priceACumulativeSnapshot;
uint256 public priceBCumulativeSnapshot;
uint32 public timestampSnapshot;
uint256 public twapPriceA; // tokenB per tokenA(Q112 格式)
uint256 public twapPriceB; // tokenA per tokenB(Q112 格式)
uint224 private constant Q112 = 2**112;
constructor(address _amm) {
amm = IEnhancedAMM(_amm);
// 初始快照
priceACumulativeSnapshot = amm.priceACumulativeLast();
priceBCumulativeSnapshot = amm.priceBCumulativeLast();
timestampSnapshot = amm.blockTimestampLast();
}
/// @notice 更新 TWAP 价格
/// @dev 建议每 10-30 分钟调用一次
function update() external {
uint256 currentPriceACumulative = amm.priceACumulativeLast();
uint256 currentPriceBCumulative = amm.priceBCumulativeLast();
uint32 currentTimestamp = amm.blockTimestampLast();
uint32 timeElapsed;
unchecked {
timeElapsed = currentTimestamp - timestampSnapshot;
}
require(timeElapsed > 0, "Period not elapsed");
// TWAP = ΔpriceCumulative / Δtime
unchecked {
twapPriceA = (currentPriceACumulative - priceACumulativeSnapshot) / timeElapsed;
twapPriceB = (currentPriceBCumulative - priceBCumulativeSnapshot) / timeElapsed;
}
// 更新快照
priceACumulativeSnapshot = currentPriceACumulative;
priceBCumulativeSnapshot = currentPriceBCumulative;
timestampSnapshot = currentTimestamp;
}
/// @notice 使用 TWAP 价格计算输出
function consultPriceA(uint256 amountIn) external view returns (uint256 amountOut) {
// 将 Q112 格式转换为正常数值
amountOut = (twapPriceA * amountIn) / Q112;
}
}
Foundry 测试
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "../src/EnhancedAMM.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MockToken is ERC20 {
constructor(string memory name, string memory symbol) ERC20(name, symbol) {
_mint(msg.sender, 1_000_000e18);
}
}
contract EnhancedAMMTest is Test {
EnhancedAMM public amm;
MockToken public tokenA;
MockToken public tokenB;
address public alice = makeAddr("alice");
address public bob = makeAddr("bob");
function setUp() public {
tokenA = new MockToken("Token A", "TKA");
tokenB = new MockToken("Token B", "TKB");
amm = new EnhancedAMM(address(tokenA), address(tokenB));
// 给 Alice 代币用于添加流动性
tokenA.transfer(alice, 100_000e18);
tokenB.transfer(alice, 100_000e18);
// 给 Bob 代币用于交换
tokenA.transfer(bob, 10_000e18);
tokenB.transfer(bob, 10_000e18);
}
function test_AddLiquidityAndSwapWithFee() public {
// Alice 添加流动性: 10000 A + 10000 B
vm.startPrank(alice);
tokenA.approve(address(amm), 10_000e18);
tokenB.approve(address(amm), 10_000e18);
amm.addLiquidity(10_000e18, 10_000e18);
vm.stopPrank();
// Bob 交换 100 A → B(期望约 98.7 B,扣除 0.3% 手续费 + 价格冲击)
vm.startPrank(bob);
tokenA.approve(address(amm), 100e18);
uint256 expectedOut = amm.getAmountOut(100e18, true);
// 滑点保护: 允许 1% 滑点
uint256 minOut = expectedOut * 99 / 100;
uint256 bobBalanceBefore = tokenB.balanceOf(bob);
amm.swapAForB(100e18, minOut);
uint256 actualOut = tokenB.balanceOf(bob) - bobBalanceBefore;
// 验证手续费效果: 实际输出应小于无手续费的理论值
// 无手续费理论值: 10000 * 100 / (10000 + 100) ≈ 99.0099
uint256 theoreticalNoFee = (10_000e18 * 100e18) / (10_000e18 + 100e18);
assertLt(actualOut, theoreticalNoFee, "Fee should reduce output");
assertGt(actualOut, minOut, "Should exceed minimum");
vm.stopPrank();
}
function test_SlippageProtectionReverts() public {
// 添加流动性
vm.startPrank(alice);
tokenA.approve(address(amm), 10_000e18);
tokenB.approve(address(amm), 10_000e18);
amm.addLiquidity(10_000e18, 10_000e18);
vm.stopPrank();
// Bob 设置不合理的 minAmountOut,交易应回滚
vm.startPrank(bob);
tokenA.approve(address(amm), 100e18);
// 设置 minAmountOut = 100(但实际输出约 98.7)
vm.expectRevert("Slippage: output below minimum");
amm.swapAForB(100e18, 100e18);
vm.stopPrank();
}
function test_PriceImpactCalculation() public {
vm.startPrank(alice);
tokenA.approve(address(amm), 10_000e18);
tokenB.approve(address(amm), 10_000e18);
amm.addLiquidity(10_000e18, 10_000e18);
vm.stopPrank();
// 小额交易价格冲击应该很小
uint256 smallImpact = amm.getPriceImpact(10e18, true);
// 大额交易价格冲击应该很大
uint256 largeImpact = amm.getPriceImpact(5000e18, true);
assertLt(smallImpact, largeImpact, "Larger trades should have more impact");
assertLt(smallImpact, 100, "Small trade impact < 1%");
}
}
Uniswap V2 vs V3 AMM 对比
| 维度 | Uniswap V2 | Uniswap V3 |
|---|---|---|
| 流动性分布 | 全范围(0 到 ∞) | 集中流动性(自选价格区间) |
| 资本效率 | 低(大部分流动性闲置) | 高(最高 4000x 提升) |
| LP Token | 同质化 ERC20 | 非同质化 NFT(ERC721) |
| 手续费层级 | 固定 0.3% | 0.01% / 0.05% / 0.3% / 1% |
| 价格预言机 | 累积价格 TWAP | 几何平均 TWAP(更精确) |
| 无常损失 | 取决于价格变化 | 集中范围内更大,但费用收入更高 |
| 实现复杂度 | 简单(~300行核心代码) | 复杂(tick 系统、bitmap 索引) |
| 适合场景 | 长尾代币、低维护 | 主流交易对、专业 LP |
V3 集中流动性的关键创新
V2: 流动性均匀分布在 [0, ∞)
████████████████████████████████
$0 当前价格 $∞
V3: LP 选择 [$1800, $2200] 提供流动性
░░░░░░████████████░░░░░░░░░░░░
$0 $1800 $2000 $2200 $∞
↑当前价格
关键要点总结
- 手续费实现: 使用
997/1000整数运算代替浮点数,手续费留在池子中自动增加 LP 份额价值 - 滑点保护是用户安全的最后防线:
minAmountOut参数保护用户免受 MEV 攻击和极端价格波动 - TWAP 预言机比即时价格更安全: 攻击者需要持续多个区块操纵价格,成本极高
- k 值只增不减: 每次 swap 的手续费让 k 持续增长,这就是 LP 的收益来源
- 首次添加流动性需锁定最小量: 防止通过极小流动性+大额 swap 进行价格操纵
常见误区
误区1: "手续费是从输出中扣除的"
纠正: Uniswap V2 的手续费是从输入中扣除的。先算 amountIn * 997/1000,然后用这个有效输入去计算输出。手续费留在池子里,等价于增加了储备。
误区2: "设置 0% 滑点容忍度最安全"
纠正: 0% 滑点意味着交易极容易失败(因为区块间价格可能微幅波动)。合理的滑点通常是 0.1%-1%,稳定币对可以更低(0.05%)。
误区3: "价格预言机可以直接用当前储备比"
纠正: 即时价格(reserveB/reserveA)极易被闪电贷操纵。必须使用 TWAP 或 Chainlink 等外部预言机。Uniswap V2 的 TWAP 需要至少跨两个区块的快照。
误区4: "手续费率越低越好"
纠正: 手续费率需要在交易者成本和LP 激励之间平衡。费率太低 → LP 没收益 → 流动性撤出 → 滑点增大。V3 的多层费率正是为了解决这个矛盾。
面试关联
Q: "如何设计 AMM 的手续费机制?需要考虑哪些因素?"
参考回答:
- 费率选择: 需要根据交易对特性选择。稳定币对费率低(0.01-0.05%),波动资产高(0.3-1%)
- 费用分配: LP 收入 vs 协议收入的分配比例。需要 DAO 治理决定
- 实现方式: 从输入中扣除(Uniswap 方式)比从输出扣除更安全,能保证 k 单调递增
- 动态费率: 可以根据市场波动率动态调整(如 Trader Joe 的 LB)
Q: "MEV 对 AMM 用户有什么影响?如何防护?"
参考回答:
- 三明治攻击: 攻击者先买入推高价格 → 用户高价买入 → 攻击者卖出获利
- 防护措施:
- 用户端: 设置合理的
minAmountOut - 协议端: 使用 MEV 保护(Flashbots Protect、MEV Blocker)
- 设计端: CoW Protocol 的批量拍卖、UniswapX 的链下竞价
- 用户端: 设置合理的
参考资源
- Uniswap V2 白皮书 - AMM 数学基础
- Uniswap V2 Core 源码 - 生产级实现
- Uniswap V3 白皮书 - 集中流动性详解
- Paradigm: TWAP Oracle Attacks - TWAP 安全分析
- [a]16z: On the Formalization of Uniswap](https://a16zcrypto.com/) - AMM 数学证明