Curve V1 第 6 章:移除流动性(Remove Liquidity)
取回流动性有三条路:按比例全取(免费)、按指定数量取(收费)、只取一种币(收费)。这一章把三者的数学、为什么一个免费两个收费讲清楚——你会发现它和第 5 章的失衡费是同一枚硬币的两面。
目录
- 1. 三种移除方式概览
- 2. remove_liquidity:按比例取,简单免费
- 3. 案例:销毁 10% 的 LP 会取回什么
- 4. 为什么按比例移除不收费
- 5. remove_liquidity_imbalance:按指定数量取
- 6. remove_liquidity_one_coin:只取一种币
- 7. calc_withdraw_one_coin 的数学
- 8. 案例:单币提取 vs 按比例提取
- 9. 三个函数对照表
- 10. 本章小结
- 11. 动手练习
1. 三种移除方式概览
| 函数 | 取回什么 | 收费吗 | 复杂度 |
|---|---|---|---|
remove_liquidity | 三种币按当前比例各取一部分 | ❌ 不收 | 简单(比例减法) |
remove_liquidity_imbalance | 你指定每种币要多少 | ✅ 失衡费 | 中(算 D 差) |
remove_liquidity_one_coin | 只取一种币 | ✅ 失衡费 | 高(解不变量) |
核心直觉(和第 5 章呼应):
按比例操作 → 不改变池子平衡 → 不收费。 偏离比例操作(指定数量 / 单币)→ 把池子推向失衡 → 收失衡费。
2. remove_liquidity:按比例取,简单免费
def remove_liquidity(_amount, min_amounts):
for i in range(N_COINS):
value = balances[i] · _amount / total_supply # 按 LP 占比取
assert value >= min_amounts[i] # 滑点保护
balances[i] -= value
transfer(coins[i], msg.sender, value)
token.burnFrom(msg.sender, _amount)
数学极简单——按比例取,不碰复杂的不变量求解:
取回[i] = balances[i] · _amount / totalSupply
即”你占总 LP 的百分比 × 每种币余额”。不收任何费用,也不需要算 D。
3. 案例:销毁 10% 的 LP 会取回什么
设 3pool 真实余额 [DAI 100万e18, USDC 100万e6, USDT 100万e6],totalSupply = 300万 3CRV,你持有 30 万 3CRV(占 10%)。
DAI : 100万 · 30万/300万 = 10万 DAI
USDC: 100万 · 30万/300万 = 10万 USDC
USDT: 100万 · 30万/300万 = 10万 USDT
你拿回每种币的 10%,池子取回后仍保持原比例,平衡不变。
你拿回的是当前的资产组合(三种币各 10 万),不一定是你当初存入的组合(比如你当初只存了 DAI)——因为做 LP 期间你的资产被交易者换来换去了。
4. 为什么按比例移除不收费
回忆第 5 章:收费是为了惩罚”把池子推向失衡”的操作。
remove_liquidity 等比例取出三种币:
- 取出后三种币的相对比例完全不变,池子还是一样平衡。
- 没制造任何”隐含兑换”,套利者无法借此薅羊毛。
既然没制造失衡、没隐含兑换,自然没理由收费。这就是它”安全、简单、免费”的根本原因。
5. remove_liquidity_imbalance:按指定数量取
这个函数让你精确指定每种币要取回多少(而不是按比例)。比如”我就要取 5 万 DAI、3 万 USDC、0 USDT”。
def remove_liquidity_imbalance(amounts, max_burn_amount):
D0 = get_D_mem(old_balances, A)
for i: new_balances[i] = old_balances[i] - amounts[i] # 按你指定的减
D1 = get_D_mem(new_balances, A)
# 对每种币算失衡费(和 add_liquidity 同款逻辑)
for i in range(N_COINS):
ideal_balance = D1 · old_balances[i] / D0
difference = |ideal_balance - new_balances[i]|
fees[i] = _fee · difference / FEE_DENOMINATOR
new_balances[i] -= fees[i]
D2 = get_D_mem(new_balances, A)
# 要销毁的 LP(含失衡费)
token_amount = (D0 - D2) · token_supply / D0
token_amount += 1 # 向上取整,对取款者不利(防套利)
assert token_amount <= max_burn_amount, "Slippage screwed you"
token.burnFrom(msg.sender, token_amount)
要点:
- 因为你指定的数量通常不按比例,会把池子推向失衡,所以收失衡费(和 add_liquidity 完全一样的
ideal_balance/difference/_fee逻辑)。 max_burn_amount是滑点保护:“我最多愿意烧这么多 3CRV 来换这些币”。token_amount += 1向上取整,让取款者多烧一点点,偏向池子防舍入攻击(和 swap 里-1是对称的保护)。
6. remove_liquidity_one_coin:只取一种币
你只想拿回一种币(比如全要 DAI)。这等价于”按比例取回三种币,再把 USDC/USDT 在池子里换成 DAI”——后半段那个”换”就是隐含兑换,要收费。
def remove_liquidity_one_coin(_token_amount, i, min_amount):
dy, dy_fee = self._calc_withdraw_one_coin(_token_amount, i)
assert dy >= min_amount, "Not enough coins removed"
self.balances[i] -= (dy + dy_fee · admin_fee / FEE_DENOMINATOR)
token.burnFrom(msg.sender, _token_amount)
transfer(coins[i], msg.sender, dy)
核心计算全在 _calc_withdraw_one_coin 里。
7. calc_withdraw_one_coin 的数学
_calc_withdraw_one_coin 要回答:“销毁这么多 LP,全换成第 i 种币,能拿多少 dy?”
def _calc_withdraw_one_coin(_token_amount, i) -> (dy, dy_fee):
D0 = get_D(xp, A) # 当前 D
D1 = D0 - _token_amount · D0 / total_supply # 销毁 LP 后 D 按比例降
new_y = get_y_D(A, i, xp, D1) # 在 D1 下,第 i 币应剩多少
dy_0 = (xp[i] - new_y) / precisions[i] # 不含费的取出量
# 对"偏离比例的那部分"收失衡费
for j in range(N_COINS):
if j == i:
dx_expected = xp[j]·D1/D0 - new_y
else:
dx_expected = xp[j] - xp[j]·D1/D0
xp_reduced[j] -= _fee · dx_expected / FEE_DENOMINATOR
dy = xp_reduced[i] - get_y_D(A, i, xp_reduced, D1) # 扣费后再解一次 y
dy = (dy - 1) / precisions[i] # -1 偏向池子,换回真实精度
return dy, dy_0 - dy # dy_0 - dy 就是失衡费
步骤拆解:
- 降 D:销毁 LP,D 从
D0按比例降到D1。 - 解 y:用
get_y_D(和第 4 章get_y同款牛顿迭代,只是这里 D 在变)在D1下求第 i 币的新余额new_y。dy_0 = xp[i] - new_y是不含费的取出量。 - 扣失衡费:对每种币算”偏离按比例的程度”
dx_expected,按_fee收费,得到xp_reduced。 - 再解一次 y:在扣费后的
xp_reduced上再求一次,得到含费后真正能取的dy。 - 失衡费 =
dy_0 - dy。
直觉:单币提取的费用比直接 swap 略低,因为只对”偏离比例的那部分”收费,不是对全部金额收费。
8. 案例:单币提取 vs 按比例提取
接第 3 节,你持有 10% LP,对应约 10万 DAI + 10万 USDC + 10万 USDT(总价值≈30 万美元)。
方式 A:按比例提取(remove_liquidity)
直接拿回 10万 DAI + 10万 USDC + 10万 USDT,总价值≈30 万,零手续费。但是三种币的混合。
方式 B:全要 DAI(remove_liquidity_one_coin, i=0)
池子要把你那份的 USDC、USDT(约 20 万)在内部”卖成”DAI:
- 这 20 万的隐含兑换被收失衡费(比 swap 略低)。
- 还有一点滑点(提取把池子推歪)。
- 结果你拿到的 DAI ≈
30万 − 失衡费 − 滑点,可能是约29.9x 万 DAI。
结论:想省钱、要原始组合 → 方式 A;想要单一币种、愿付一点费用换便利 → 方式 B。金额越大、越偏离比例,方式 B 成本越高。
9. 三个函数对照表
| 维度 | remove_liquidity | remove_liquidity_imbalance | remove_liquidity_one_coin |
|---|---|---|---|
| 你指定什么 | 烧多少 LP | 每种币要多少 | 烧多少 LP + 要哪种币 |
| 取回币种 | 三种按比例 | 你指定的组合 | 单一币种 |
| 收费 | ❌ 不收 | ✅ 失衡费 | ✅ 失衡费(<swap) |
| 改变平衡 | 否 | 是 | 是 |
| 数学 | 比例减法 | 算 D0/D1/D2 差 | get_y_D 求解 |
| 滑点保护 | min_amounts | max_burn_amount | min_amount |
| 取整方向 | 偏向池子 | +1 多烧 | -1 少取 |
10. 本章小结
remove_liquidity按 LP 占比等比例取回三种币,取回[i]=balances[i]·amount/totalSupply,不收费、不破坏平衡。remove_liquidity_imbalance让你指定每种币的数量,因通常不按比例 → 收失衡费(同 add_liquidity 的 ideal_balance 逻辑),max_burn_amount保护,销毁量+1偏向池子。remove_liquidity_one_coin只取一种币,等价于”按比例取 + 把其它币换成目标币” → 收失衡费(低于 swap)。_calc_withdraw_one_coin:先按 LP 比例降 D,用get_y_D求目标币新余额得不含费量,再对偏离部分扣费、二次求解得净 dy。- 三种方式的取整都偏向池子(防舍入攻击):按比例无额外、imbalance
+1多烧、one_coin-1少取。
11. 动手练习
对应课程的两个 Remove Liquidity 练习。setUp 里先存 100 万 DAI 拿到 3CRV,再分别用两种方式提取。
公共 setUp
function setUp() public {
deal(DAI, address(this), 1e6 * 1e18);
dai.approve(address(pool), type(uint256).max);
uint256[3] memory coins = [uint256(1e6 * 1e18), 0, 0];
pool.add_liquidity(coins, 1); // 拿到一些 3CRV
}
练习 1:按比例移除全部 LP
interface IStableSwap3Pool {
function remove_liquidity(uint256 amount, uint256[3] memory min_amounts) external;
}
思路:
uint256 lpBal = lp.balanceOf(address(this));minCoins = [1,1,1],调用pool.remove_liquidity(lpBal, minCoins);- 断言
lp.balanceOf(this) == 0(3CRV 烧光)。 - 断言你同时收到 DAI、USDC、USDT 三种币(都比 setUp 前增加)——即使当初只存了 DAI!这印证”取回的是当前组合”。
练习 2:只取回 DAI
interface IStableSwap3Pool {
function remove_liquidity_one_coin(uint256 token_amount, int128 i, uint256 min_amount) external;
}
思路:
uint256 lpBal = lp.balanceOf(address(this));- 调用
pool.remove_liquidity_one_coin(lpBal, 0, 1);(i=0 即 DAI)。 - 断言
lp.balanceOf(this) == 0。 - 断言只有 DAI 增加,USDC、USDT 保持不变(== setUp 前)。印证”单币提取只拿一种币”。
进阶(推荐)
- 量化失衡费 + 滑点:你存入 100 万 DAI,分别用方式 1(按比例)和方式 2(全取 DAI)退出,比较两种方式最终拿回的总价值(方式 1 把三种币按≈1 美元相加)。方式 2 应略低,差额 ≈ 失衡费 + 滑点。
- 用
calc_withdraw_one_coin(lp, 0)预估单币可取量,和实际返回对比。 - 试试
remove_liquidity_imbalance:指定取[5万 DAI, 3万 USDC, 0],观察烧掉的 3CRV 数量,并和calc_token_amount(..., false)的预估对比。
运行
forge test --evm-version cancun --fork-url $FORK_URL \
--match-path test/CurveV1RemoveLiquidity.t.sol -vvv
🎉 Curve V1 课程完结
恭喜走完 Curve V1(StableSwap)核心内容!回顾主线:
- 第 2 章 数学:StableSwap 不变量用 A 把恒定和与恒定乘积缝起来,牛顿法求 D 和 y。
- 第 3 章 合约总览:A 缓慢 ramp、
_xp统一精度、virtual_price 与 calc_token_amount。 - 第 4 章 兑换:保持 D 不变定价、get_y 反解、手续费收在输出币。
- 第 5 章 添加流动性:按 D 增量铸 LP、失衡费惩罚不按比例存。
- 第 6 章 移除流动性:按比例(免费)/ 按数量(收费)/ 单币(收费)三种方式。
学完 V1 再回看 Curve V2(Cryptoswap),你会发现 V2 = V1 + 「可移动的 price_scale + 自动重锚 + 动态手续费」,核心数学一脉相承。最好的巩固方式就是把每章练习用 Foundry 真正跑一遍。祝你 DeFi 开发顺利!