Curve Cryptoswap 第 6 章:移除流动性(Remove Liquidity)
取回流动性有两条路:按比例全取(
remove_liquidity)和只取一种币(remove_liquidity_one_coin)。这一章把两者的数学、为什么一个不收费、另一个要收费讲清楚——你会发现它和第 5 章的失衡费是一枚硬币的两面。
目录
- 1. 两种移除方式概览
- 2. remove_liquidity:按比例取,简单又安全
- 3. 案例:销毁 10% 的 LP 会取回什么
- 4. 清空池子的特殊情况
- 5. 为什么按比例移除不收费
- 6. remove_liquidity_one_coin:只取一种币
- 7. calc_withdraw_one_coin 的数学
- 8. 案例:单币提取 vs 按比例提取的对比
- 9. 两个函数的对照表
- 10. 本章小结
- 11. 动手练习
1. 两种移除方式概览
| 函数 | 取回什么 | 收费吗 | 计算复杂度 |
|---|---|---|---|
remove_liquidity | 三种币按当前比例各取一部分 | 不收费 | 简单(按比例减法) |
remove_liquidity_one_coin | 只取你指定的一种币 | 收失衡费(比兑换费低) | 复杂(要解不变量) |
核心直觉(和第 5 章呼应):
按比例操作 → 不改变池子的平衡 → 不收费。 偏离比例操作(单币)→ 把池子推向失衡 → 收失衡费。
2. remove_liquidity:按比例取,简单又安全
def remove_liquidity(
_amount, # 要销毁的 LP 数量
min_amounts, # 三种币各自最少要取回多少(滑点保护)
use_eth, receiver, claim_admin_fees
) -> uint256[N_COINS]:
它的数学非常简单——因为按比例取,根本不需要碰复杂的不变量求解。每种币取回的数量:
取回[i] = balances[i] · amount / totalSupply
也就是”你占总 LP 的百分比 × 每种币的余额”。代码(非清空情况):
amount -= 1 # 向下取整,偏向留在池里的 LP(防舍入攻击)
for i in range(N_COINS):
d_balances[i] = balances[i] · amount / total_supply
assert d_balances[i] >= min_amounts[i] # 滑点保护
self.balances[i] = balances[i] - d_balances[i]
D 也按同样比例缩小:
# D1 / D0 = (total_supply - amount) / total_supply
self.D = D - D · amount / total_supply
注释里写得很清楚:这种取法”非常安全,不做复杂数学,因为是按平衡比例提取,不收任何费用”。
3. 案例:销毁 10% 的 LP 会取回什么
假设池子真实余额:
balances = [USDC 10,000,000, WBTC 150, WETH 3,000]
totalSupply = 1,000,000 LP
你持有 100,000 LP(占 10%)
按比例取回(忽略那个 -1 的取整):
USDC: 10,000,000 · 100,000 / 1,000,000 = 1,000,000 USDC
WBTC: 150 · 100,000 / 1,000,000 = 15 WBTC
WETH: 3,000 · 100,000 / 1,000,000 = 300 WETH
你拿回了池子里每种币的 10%。池子取回后仍保持原来的资产比例,平衡度不变。D 也从原值降到 90%。
注意:你拿回的是当前的资产组合,不是你当初存入的组合。如果你当初只存了 USDC,现在可能取回三种币的混合——这是 AMM 的常态(你的资产在你做 LP 期间被交易者”换来换去”了)。
4. 清空池子的特殊情况
如果 amount == total_supply(最后一个 LP 全部退出):
if amount == total_supply: # Case 2: 清空
for i in range(N_COINS):
d_balances[i] = balances[i] # 全部取走
self.balances[i] = 0 # 池子归零
# D 也会变成 0
这种情况直接把所有币给最后的 LP,并把不变量重置为 0。代码里特意把”清空”和”部分提取”分开处理,是为了避免 amount -= 1 那个取整在清空时导致灰尘残留或除法误差。
5. 为什么按比例移除不收费
回忆第 5 章的失衡费逻辑:收费是为了惩罚”把池子推向失衡的操作”。
而 remove_liquidity 是等比例取出三种币:
- 取出后,三种币的相对比例完全不变,池子还是一样平衡。
- 没有制造任何”隐含的兑换”,套利者无法借此薅羊毛。
既然没有制造失衡、没有隐含兑换,自然没有理由收费。这就是它”安全、简单、免费”的根本原因。
6. remove_liquidity_one_coin:只取一种币
def remove_liquidity_one_coin(
token_amount, # 销毁的 LP 数量
i, # 只取第 i 种币
min_amount, # 滑点保护
use_eth, receiver
) -> uint256: # 返回取到的第 i 种币数量 dy
你只想拿回一种币(比如全换成 USDC)。这等价于:
“先按比例取回三种币,再把其中的 WBTC、WETH 在池子里换成 USDC。”
后半段那个”换”就是隐含的兑换 —— 所以要收费(失衡费,但比直接 swap 略低)。流程:
self._claim_admin_fees() # 先把管理费结算
dy, D, xp, approx_fee = self._calc_withdraw_one_coin(A_gamma, token_amount, i, ...)
assert dy >= min_amount, "Slippage"
self.balances[i] -= dy # 只减少第 i 种币
self.burnFrom(msg.sender, token_amount)
self._transfer_out(coins[i], dy, ...)
self.tweak_price(A_gamma, xp, D, 0) # 改变了平衡 → 触发重锚判断
注意:因为单币提取改变了池子平衡,所以结尾要 tweak_price(而按比例的 remove_liquidity 不需要,因为平衡没变)。
7. calc_withdraw_one_coin 的数学
_calc_withdraw_one_coin 要回答:“销毁这么多 LP,全换成第 i 种币,能拿多少 dy?”
步骤(理解逻辑即可,不必记死):
- 算新 D:销毁
token_amount个 LP,对应 D 要从D0降到
(和按比例移除一样,D 按 LP 比例缩小。)D1 = D0 - D0 · token_amount / totalSupply - 在新 D 下,反解第 i 种币的新余额:用数学库
get_y(A, gamma, xp, D1, i)——已知其它币不变、D 变小了,求第 i 种币应该剩多少。 - dy(毛)= 旧余额 − 新余额(再换算回真实币单位)。
- 扣失衡费:因为这等价于把其它币换成第 i 种币,按
_calc_token_fee思路收一笔approx_fee,从 dy 里扣掉。 - 返回
dy(净)、新D、新xp、approx_fee。
关键点:
- 单币提取的费率基于第 4/5 章那个动态
_fee,所以池子越失衡、提取越偏离比例,费越高。 - 文档注释明确:单币提取的费用”低于 swap 费用”(因为它只对”偏离比例的那部分”收费,不是对全部金额收费)。
8. 案例:单币提取 vs 按比例提取的对比
接第 3 节,你持有 10% 的 LP,对应价值约:1,000,000 USDC + 15 WBTC + 300 WETH。假设 WBTC=$60,000、WETH=$3,000,则总价值:
1,000,000 + 15·60,000 + 300·3,000 = 1,000,000 + 900,000 + 900,000 = 2,800,000 美元
方式 A:按比例提取(remove_liquidity)
直接拿回 1,000,000 USDC + 15 WBTC + 300 WETH,总价值 ≈ 280 万美元,零手续费。但你拿到的是三种币的混合。
方式 B:全部换成 USDC(remove_liquidity_one_coin, i=0)
池子要把你那份的 WBTC、WETH(共约 180 万美元)在内部”卖成” USDC:
- 这 180 万美元的隐含兑换会被收失衡费(比如几十个 bp,取决于金额占池子比例和池子状态)。
- 还会有一点滑点(大额单币提取把池子推歪,get_y 给出的价格不如平衡时好)。
- 结果你拿到的 USDC ≈
2,800,000 − 失衡费 − 滑点,可能是约2,79x,xxx USDC。
结论:想省钱 → 按比例提取(方式 A);想要单一币种、愿意付一点费用换便利 → 单币提取(方式 B)。金额越大、池子越失衡,方式 B 的成本越高。
9. 两个函数的对照表
| 维度 | remove_liquidity | remove_liquidity_one_coin |
|---|---|---|
| 取回币种 | 三种按比例 | 指定一种 |
| 是否收费 | ❌ 不收 | ✅ 失衡费(< swap 费) |
| 是否改变池子平衡 | 否 | 是 |
| 是否触发 repeg | 否 | 是(tweak_price) |
| 数学复杂度 | 低(比例减法) | 高(newton/get_y 求解) |
| 滑点 | 无 | 有(大额时明显) |
| 适用场景 | 想要原始资产组合、省钱 | 想要单一币种、图方便 |
10. 本章小结
remove_liquidity按 LP 占比等比例取回三种币:取回[i] = balances[i]·amount/totalSupply,D 同比例缩小。- 它不收费、不触发 repeg,因为不改变池子平衡、不制造隐含兑换。
- 清空池子(amount == totalSupply)走特殊分支,全部取走并把 D 重置为 0。
remove_liquidity_one_coin只取一种币,等价于”按比例取 + 把其它币换成目标币”,所以收失衡费(低于 swap 费)并触发 tweak_price。- 单币提取靠
_calc_withdraw_one_coin:先按 LP 比例降 D,再用get_y反解目标币新余额,差值即毛 dy,扣费后得净 dy。 - 大额、池子失衡时,单币提取的费用 + 滑点会明显高于按比例提取。
11. 动手练习
对应课程的两个 Remove Liquidity 练习。先在 setUp 里存入 1000 USDC 拿到一些 LP,再分别用两种方式提取。
公共 setUp
function setUp() public {
deal(USDC, address(this), 1e3 * 1e6);
usdc.approve(address(pool), type(uint256).max);
uint256[3] memory amounts = [uint256(1e3 * 1e6), 0, 0];
pool.add_liquidity(amounts, 1, false, address(this));
}
练习 1:按比例移除全部 LP
interface ITriCrypto {
function remove_liquidity(
uint256 lp, uint256[3] memory min_amounts,
bool use_eth, address receiver, bool claim_admin_fees
) external returns (uint256[3] memory);
function balanceOf(address) external view returns (uint256);
}
思路:
uint256 lpBal = pool.balanceOf(address(this));minAmounts = [1, 1, 1],调用remove_liquidity(lpBal, minAmounts, false, address(this), false)。- 断言
pool.balanceOf(address(this)) == 0(LP 烧光)。 - 断言并打印:你应同时收到 USDC、WBTC、WETH 三种币(都 > 0)——即使你当初只存了 USDC!这直观印证了”取回的是当前资产组合”。
小坑:测试合约初始的 WETH 余额可能因 Foundry 行为不为 0,所以验证 WETH 时用
weth.balanceOf(this) > wethBalBefore而不是> 0。
练习 2:只取回 USDC
interface ITriCrypto {
function remove_liquidity_one_coin(
uint256 lp, uint256 i, uint256 min_amount,
bool use_eth, address receiver
) external returns (uint256);
}
思路:
uint256 lpBal = pool.balanceOf(address(this));- 调用
remove_liquidity_one_coin(lpBal, 0, 1, false, address(this))(i=0 即 USDC)。 - 断言
pool.balanceOf(address(this)) == 0。 - 断言只有 USDC > 0,而 WBTC == 0、WETH 没增加(== wethBalBefore)。这印证了”单币提取只拿一种币”。
进阶(推荐)
- 量化失衡费/滑点:你存入了 1000 USDC,分别用方式 1(按比例)和方式 2(全取 USDC)退出,记录两种方式最终拿回的总美元价值(方式 1 需用 price_scale 把三种币折算成美元)。方式 2 的美元价值应略低,差额 ≈ 失衡费 + 滑点。
- 用 Views 合约的
calc_withdraw_one_coin(lp, i)预估单币可取量,和实际remove_liquidity_one_coin返回值对比。
运行
forge test --evm-version cancun --fork-url $FORK_URL \
--match-path test/CurveV2RemoveLiquidity.t.sol -vvv
下一章(第 7 章 Price Repeg)是全课程的高潮:内部价格预言机(EMA)怎么跟踪市场价,
tweak_price怎么决定何时、移动多少price_scale,以及xcp_profit如何记录盈亏、确保重锚永不让 LP 亏本。