Yearn Finance闪电贷攻击分析学习

合约代码:
https://etherscan.io/address/0xacd43e627e64355f1861cec6d3a6688b31a6f952#code
https://etherscan.io/address/0x9e65ad11b299ca0abefc2799ddb6314ef2d91080#code
https://etherscan.io/address/0x9c211BFa6DC329C5E757A223Fb72F5481D676DC1#code
3pool合约代码:https://etherscan.io/address/0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7#code
攻击交易:https://etherscan.io/tx/0xb094d168dd90fcd0946016b19494a966d3d2c348f57b890410c51425d89166e8
合约yVault中earn会将当前合约内的95%dai质押到3pool中,而withdraw函数会在dai数量不够时从3pool中提取dai(正常情况下提取的dai的数量与质押的dai的数量相等),所以提取数量基本等于质押数量。
(amount*total/pool )/((pool+amount)*(amount*total/pool )/(total+amount )) =(total+amount )/(_pool+amount)

function deposit(uint _amount) public {
    
    
        uint _pool = balance();
        uint _before = token.balanceOf(address(this));
        token.safeTransferFrom(msg.sender, address(this), _amount);
        uint _after = token.balanceOf(address(this));
        _amount = _after.sub(_before); // Additional check for deflationary tokens
        uint shares = 0;
        if (totalSupply() == 0) {
    
    
            shares = _amount;
        } else {
    
    
            shares = (_amount.mul(totalSupply())).div(_pool);
        }
        _mint(msg.sender, shares);
    }
function withdraw(uint _shares) public {
    
    
        uint r = (balance().mul(_shares)).div(totalSupply());
        _burn(msg.sender, _shares);
        uint b = token.balanceOf(address(this));
        if (b < r) {
    
    
            uint _withdraw = r.sub(b);
            Controller(controller).withdraw(address(token), _withdraw);
            uint _after = token.balanceOf(address(this));
            uint _diff = _after.sub(b);
            if (_diff < _withdraw) {
    
    
                r = b.add(_diff);
            }
        }
        token.safeTransfer(msg.sender, r);
    }

质押后调用earn会生成3cvr(会转换为3ycvr币在存储可以直接看成3cvr)在计算balance时会将3cvr再计算为dai。如果先调用earn转换为3cvr,在deposit前增加dai的价格,减少shares = (_amount.mul(totalSupply())).div(_pool);中pool的值。后降低dai的价格,增大(balance().mul(_shares)).div(totalSupply())中balance的值。就会得到更多收益。
先看3pool代码:

def get_virtual_price() -> uint256:
    """
    Returns portfolio virtual price (for calculating profit)
    scaled up by 1e18
    """
    D: uint256 = self.get_D(self._xp(), self._A())
    # D is in the units similar to DAI (e.g. converted to precision 1e18)
    # When balanced, D = n * x_u - total virtual value of the portfolio
    token_supply: uint256 = self.token.totalSupply()
    return D * PRECISION / token_supply
 def add_liquidity(amounts: uint256[N_COINS], min_mint_amount: uint256):
 ......
    D2: uint256 = D1
    if token_supply > 0:
        # Only account for fees if we are not the first to deposit
        for i in range(N_COINS):
            ideal_balance: uint256 = D1 * old_balances[i] / D0
            difference: uint256 = 0
            if ideal_balance > new_balances[i]:
                difference = ideal_balance - new_balances[i]
            else:
                difference = new_balances[i] - ideal_balance
            fees[i] = _fee * difference / FEE_DENOMINATOR
            self.balances[i] = new_balances[i] - (fees[i] * _admin_fee / FEE_DENOMINATOR)
            new_balances[i] -= fees[i]
        D2 = self.get_D_mem(new_balances, amp)
    else:
        self.balances = new_balances
   ........
        mint_amount = token_supply * (D2 - D0) / D0
.........
    # Mint pool tokens
    self.token.mint(msg.sender, mint_amount)

    

通过计算在3pool中如果按照平均比例添加Lp(理想情况下)3cvr的价值不变,但是如果添加单个池子self.balances[i] = new_balances[i] - (fees[i] * _admin_fee / FEE_DENOMINATOR) new_balances[i] -= fees[i] D2 = self.get_D_mem(new_balances, amp)
就会因为与ideal_balance有偏差计算税计算,同时3ver价值增加(非常少)。
将算式带入yVAlue,质押的share数量约等于为(因为留了5%dai在本地)

share=(amount*total)/(3cvrV0price/daiV0price)*3cvrV0Amount)
v0为当前目标状态;

质押后调用earn再提取的dai数量约等于为:

r=(share*(3cvrV1price/daiV1price)*3cvrV1Amount)/(total+share)

如果要实现盈利需要 r-amount>0;

3cvrv1price*3cvrv1amount/Daiv1price  >   3cvrv0price*3cvrv0amount/Daiv0price+amount;

所以如果质押后调用earn3pool中dai的数量增加,相对dai的价格下降,再撤走池中的usdt再拉低dai的价格,由于3cvr的价格相对比较稳定波动小于远1/100,但dai的价格通过闪电贷却能影响1/10以上,达到通过花费少量的3cvr,换取dai。


但是攻击者的思路不同:
以下为攻击者攻击方式:(https://www.fxajax.com/1620035126.html

1)攻击者首先从 dYdX 和 AAVE 中使用闪电贷借出大量的 ETH;
2)攻击者使用从第一步借出的 ETH 在 Compound 中借出 DAI 和 USDC;
3)攻击者将第二部中的所有 USDC 和 大部分的 DAI 存入到 Curve DAI/USDC/USDT 池中,这个时候由于攻击者存入流动性巨大,其实已经控制 Cruve DAI/USDC/USDT 的大部分流动性;
4)攻击者从 Curve 池中取出一定量的 USDT,使 DAI/USDT/USDC 的比例失衡,及 DAI/ (USDT&USDC) 贬值;
5)攻击者第三步将剩余的 DAI 充值进 yearn DAI 策略池中,接着调用 yearn DAI 策略池的 earn 函数,将充值的 DAI 以失衡的比例转入 Curve DAI/USDT/USDC 池中,同时 yearn DAI 策略池将获得一定量的 3CRV 代币;
6)攻击者将第 4 步取走的 USDT 重新存入 Curve DAI/USDT/USDC 池中,使 DAI/USDT/USDC 的比例恢复;
7)攻击者触发 yearn DAI 策略池的 withdraw 函数,由于 yearn DAI 策略池存入时用的是失衡的比例,现在使用正常的比例体现,DAI 在池中的占比提升,导致同等数量的 3CRV 代币能取回的 DAI 的数量会变少。这部分少取回的代币留在了 Curve DAI/USDC/USDT 池中;
8)由于第三步中攻击者已经持有了 Curve DAI/USDC/USDT 池中大部分的流动性,导致 yearn DAI 策略池未能取回的 DAI 将大部分分给了攻击者;
9)重复上述 3-8 步骤 5 次,并归还闪电贷,完成获利。

攻击者通过减少usdt (减少dai的权重,增加usdt的权重),通过质押dai,并调用earn 进一步减少dai的权重,增加usdt的权重。这时向3pool中添加usdt获得3cvr(由于usdt权重增大所以获得的3cvr数量会增多),再提取质押的dai(减少usdt的权重,同时根据上面质押提取的算法会损失一定的dai),再减少usdt(由于usdt权重下降,导致添加usdt得到3cvr数量减去 减少usdt损失3cvr数量后还剩一些3cvr), 再质押dai,…达到损失少量dai获得3cvr的效果。

猜你喜欢

转载自blog.csdn.net/Timmbe/article/details/123280809
今日推荐