在上一章中我们完成了 Router 的流动性添加流程。本篇继续沿着“端到端流动性管理”这一主线,聚焦于 LP 代币销毁的安全性 与 流动性移除(removeLiquidity) 的设计方案。我们将先回顾历史上的漏洞案例,再分析当前仓库中 UniswapV2Pair 的实现细节,并给出 Router
在上一章中我们完成了 Router 的流动性添加流程。本篇继续沿着“端到端流动性管理”这一主线,聚焦于 LP 代币销毁的安全性 与 流动性移除(removeLiquidity) 的设计方案。我们将先回顾历史上的漏洞案例,再分析当前仓库中 UniswapV2Pair
的实现细节,并给出 Router 端的设计草案与测试策略,帮助读者在实战中规避隐患。
之前实现的 UniswapV2Pair.burn
直接对 msg.sender
的 LP 代币执行销毁:
// 旧实现(已弃用)
uint256 liquidity = balanceOf(msg.sender);
_burn(msg.sender, liquidity);
调用者甚至无需提前把 LP 代币转入交易对,合约即可擅自减少其余额。这违反了 ERC20 授权语义,同时为恶意合约留下可乘之机(例如在闪电贷中强制销毁第三方 LP)。
当前版本已改为 仅销毁 Pair 合约自身持有的 LP 代币,并要求调用方在调用 burn
前将 LP 代币显式转入:
uint256 balance0 = IERC20(_token0).balanceOf(address(this));
uint256 balance1 = IERC20(_token1).balanceOf(address(this));
uint256 liquidity = balanceOf(address(this));
...
_burn(address(this), liquidity);
这样可以确保:
transferFrom
或 transfer
将 LP 代币托管到 Pair。 amount0/amount1
。 Burn
、Transfer
事件,监控异常销毁记录。UniswapV2Pair.burn
最新实现拆解amount0 = liquidity * balance0 / totalSupply
。 _burn(address(this), liquidity)
。 _update
刷新储备并触发 Burn
事件。// 读取 Pair 自身持仓,计算应退比例并销毁托管的 LP 代币
uint256 balance0 = IERC20(_token0).balanceOf(address(this));
uint256 balance1 = IERC20(_token1).balanceOf(address(this));
uint256 liquidity = balanceOf(address(this));
uint256 _totalSupply = totalSupply();
amount0 = (liquidity * balance0) / _totalSupply;
amount1 = (liquidity * balance1) / _totalSupply;
if (amount0 <= 0 || amount1 <= 0) revert InsufficientLiquidityBurned();
_burn(address(this), liquidity);
_safeTransfer(_token0, to, amount0);
_safeTransfer(_token1, to, amount1);
_update(
IERC20(_token0).balanceOf(address(this)),
IERC20(_token1).balanceOf(address(this))
);
这段代码中的 liquidity = balanceOf(address(this))
读取的是交易对合约当前托管的 LP 代币份额,也就是用户在调用 Router 或 Pair 的 burn
前已经通过 transfer
/transferFrom
转入的数量。
而 totalSupply()
则来自 LP 代币合约(Pair 继承自 ERC20),表示 LP 代币的全部发行量,其中包含用户持有的份额以及永久锁定在死地址的 MINIMUM_LIQUIDITY
。
整体流程可拆分为三个阶段:
liquidity
与 totalSupply
按比例计算调用方应收回的 amount0/amount1
,若其中任意值为零则立即回退,防止流动性不足的异常情况。_burn(address(this), liquidity)
只会销毁 Pair 自身托管的份额,避免误删用户未托管的 LP;这样既遵循 ERC20 授权语义,也能抵御恶意调用。_update
刷新储备值,保持后续报价与库函数计算都是基于最新状态。通过这一套流程,Pair 在移除流动性时能够做到“只针对托管的 LP 销毁、精确返还资产、并保持储备数据同步”,为 Router 的 remove 流程提供安全且可预测的底层基础。
removeLiquidity
设计蓝图liquidity
等非法输入。 UniswapV2Library.getReserves
与 quote
计算理想返还值。 transferFrom
将调用者的 LP 份额发送给 Pair。 burn
领取资产:Pair 返回 (amount0, amount1)
并更新储备。 amountAMin/amountBMin
比较,不满足则回退整个交易。 (amountA, amountB)
并返还给上层调用。对应的代码示例如下:
function removeLiquidity(
address tokenA,
address tokenB,
uint256 liquidity,
uint256 amountAMin,
uint256 amountBMin,
address to
) external returns (uint256 amountA, uint256 amountB) {
// 1. 参数校验与交易对定位
if (tokenA == tokenB) revert IdenticalAddresses();
if (to == address(0)) revert InvalidRecipient();
address pair = factory.getPair(tokenA, tokenB);
if (pair == address(0)) revert PairNotFound();
// 2. 将 LP 代币托管到 Pair 并执行 burn
IERC20(pair).transferFrom(msg.sender, pair, liquidity);
(uint256 amount0, uint256 amount1) = IUniswapV2Pair(pair).burn(to);
// 3. 标准化返回值并进行滑点保护
(address token0,) = UniswapV2Library.sortTokens(tokenA, tokenB);
(amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0);
if (amountA < amountAMin) revert InsufficientAAmount();
if (amountB < amountBMin) revert InsufficientBAmount();
}
自定义错误(例如 InvalidRecipient()
、InsufficientBAmount()
)已经取代字符串回退,便于前端通过错误选择器快速定位失败原因并给出有针对性的提示。
移除流动性得到的 amount0/amount1
取决于池子实时储备比例,而这个比例可能在交易被打包前因其他交易而发生变化。
如果缺少 amountAMin/amountBMin
,用户可能拿回远低于预期的某一侧资产(极端情况下甚至接近 0),从而产生真实损失。
通过滑点参数设定“最低可接受收益”,只要返还金额低于阈值,交易就会自动回滚,从根源上抵御抢跑、价格剧烈波动等风险。
这与 swap 操作中设定 amountOutMin
的目的完全一致。
burn
不会减少余额,以及转移后能正确按比例返还。amountMin
触发策略正常。Burn
、Transfer
、Sync
等事件的参数,方便前端与分析工具消费。# Pair 流动性销毁相关测试
forge test --match-contract UniswapV2PairTest --match-test testBurn -vvv
# Router 流动性流程(含 add/remove 组合)
forge test --match-contract UniswapV2RouterAddLiquidityTest -vvv
# 聚焦比例换算逻辑
forge test --match-test testAddLiquidityWithExistingReserves -vvv
UniswapV2Library
:排序、储备、报价均应复用库函数,避免手写逻辑导致顺序不一致。 MINIMUM_LIQUIDITY
:销毁时切勿误退对已永久锁定的最小流动性份额。 Burn
、Transfer
、Sync
,辅助链上监控。https://github.com/RyanWeb31110/uniswapv2_tech
欢迎克隆仓库,按照本文思路补全 Router 的流动性移除实现,并结合测试脚本验证安全性。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!