合约代码:
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的效果。