基本信息
池合约:0xccd04073f4bdc4510927ea9ba350875c3c65bf81
yETH基本信息: 存储8种ETH的流动性质押代币(LST),如wstETH,rETH,cbETH…., 凭证代币(LP)是yETH
Defi稳定池相关概念: 合约内置虚拟余额(virtual balance),若使用真实余额,每次需要调用balanceof,gas消耗高,且数值精度容易丢失。
LST之间兑换时依赖不变量公式: Π(vb_i^w_i)(1,n) = K , 对每个LST的vb求其池权重的指数后累计相乘后的结果需要为一个固定常数K
虚拟余额把所有锚定代币换算成统一的余额,按照公式
虚拟余额(vb)= 某LST原始余额 × 该LST价格系数 × 该LST精度系数 × 该LST权重系数
原始余额:池子实际持有的 LST 数量(如 rETH22ETH、mETH33ETH、stETH55ETH);
价格系数:将 LST 换算成 ETH 等值的系数(如 rETH 价格系数 = 1.01,22ETH rETH≈22.22ETH 等值),统一计价标准;
精度系数:将不同小数位数的 LST 换算成统一精度(如都换算成 18 位小数),避免计算误差;
权重系数:按池内约定权重(rETH20%、mETH30%、stETH50%)加权,确保虚拟余额与权重挂钩,方便后续均衡操作。 维持池内代币数量按照权重分布
核心漏洞函数
@external
@nonreentrant('lock')
def remove_liquidity(
_lp_amount: uint256,
_min_amounts: DynArray[uint256, MAX_NUM_ASSETS],
_receiver: address = msg.sender
):
"""
@notice Withdraw assets from the pool in a balanced manner
@param _lp_amount Amount of LP tokens to burn
@param _min_amounts Array of minimum amount of each asset to send
@param _receiver Account to receive the assets
"""
num_assets: uint256 = self.num_assets
assert len(_min_amounts) == num_assets
# update supply
prev_supply: uint256 = self.supply
supply: uint256 = prev_supply - _lp_amount
self.supply = supply
PoolToken(token).burn(msg.sender, _lp_amount)
log RemoveLiquidity(msg.sender, _receiver, _lp_amount)
# update necessary variables and transfer assets
vb_prod: uint256 = PRECISION
vb_sum: uint256 = 0
prev_vb: uint256 = 0
rate: uint256 = 0
packed_weight: uint256 = 0
for asset in range(MAX_NUM_ASSETS):
if asset == num_assets:
break
prev_vb, rate, packed_weight = self._unpack_vb(self.packed_vbs[asset])
weight: uint256 = self._unpack_wn(packed_weight, 1)
dvb: uint256 = prev_vb * _lp_amount / prev_supply # vyper除法向下取整,潜在漏洞
vb: uint256 = prev_vb - dvb # 可能导致残存vb
self.packed_vbs[asset] = self._pack_vb(vb, rate, packed_weight) # 关键问题,仅更新vb,但未判断若该LST的supply完全归零时刻,导致即使supply为0,仍存在一个非零虚拟余额.
vb_prod = unsafe_div(unsafe_mul(vb_prod, self._pow_down(unsafe_div(unsafe_mul(supply, weight), vb), unsafe_mul(weight, num_assets))), PRECISION)
vb_sum = unsafe_add(vb_sum, vb)
amount: uint256 = dvb * PRECISION / rate
assert amount >= _min_amounts[asset], "slippage"
assert ERC20(self.assets[asset]).transfer(_receiver, amount, default_return_value=True)
self.packed_pool_vb = self._pack_pool_vb(vb_prod, vb_sum)
难度有点大,需要研究下CurveStableSwap的公式 总体思路是多次不平衡添加流动性导致了Π坍缩,恒等式发散,最终使用极少存款就可以增发大量LP代币。 具体计算过程和状态变化量等研究明白后粘过来。