Featured image of post 1220 YearnFinance稳定币池漏洞复现

1220 YearnFinance稳定币池漏洞复现

基本信息

池合约: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代币。 具体计算过程和状态变化量等研究明白后粘过来。

使用 Hugo 构建
主题 StackJimmy 设计