赞
踩
2023.8.13发生在Ethereum上发生的攻击,存在两个攻击交易,具体信息如下:
攻击合约地址:Contract Address 攻击合约
攻击交易hash1:Ethereum Transaction Hash (Txhash) Details | Etherscan
攻击交易hash2:Ethereum Transaction Hash (Txhash) Details | Etherscan
phalcon分析调用序列:0x0788ba222970c7c68a | Phalcon Explorer (blocksec.com)
Zunami是稳定币投资聚合器,用户给定用ETH/USDC/DAI等稳定币投资Zunami协议;
然后Zunami协议会使用用户质押的代币到Curve中高收益的池子进行质押;
那么为了保证更进一步的收益,Zunami还会把Curve的流动性再次质押到StakeDAO和Convex平台中,吃两波流动性奖励。
然后把收到的流动性奖励代币(CRV)经过用户的质押比例返回给用户。
zETH是Zunami协议实现的变基代币(rebase token),变基代币的逻辑是因为他的代币数量计算是锚定了Zunami所有的资产来计算的,所以可以通过闪电贷对Zunami质押的池子买入卖出就可以影响zETH的数量计算
两次攻击交易是单独的,但是基于的漏洞及原理是一致的
以0x0788ba222970c7c68a738b0e08fb197e669e61f9b226ceec4cab9b85abe8cceb攻击交易为例进行分析
uniswapV3FlashCallback
函数,回调中攻击者调用Balancer: Vault的flashloan函数,这里可以看一下这里的函数源码 function flashLoan(
IFlashLoanRecipient recipient,
IERC20[] memory tokens,
uint256[] memory amounts,
bytes memory userData
) external override nonReentrant whenNotPaused
看了一下源码,其功能无特别之处,就是一个闪电贷函数,不过这个函数可以一次借贷多个代币,用tokens
和amounts
表示对应的数组,先后乐观转账,后回调攻击者,在进行还款
receiveFlashLoan
函数,此时用户已经通过借贷获得了大量的USDT、USDC以及ETH随后攻击者给curve finance,sushiswap以及uniswap上很多factory和router合约地址进行相应的代币授权,并调用Curve Finance: Swap用USDC给池子中添加流动性,获得crvFRAX
随后调用Curve.fi Factory Pool中的一些pair对的exchange()函数,DEX智能合约的代币交换功能,可以把vyper代码直接放到GPT中解析,可以理解为就算进行代币的交换,攻击者将对应的crvFRAX兑换为Zunami UZD,将USDC兑换为crvUSD。此时用户拥有UZD和crvUSD
攻击再次调用exchange()函数,将所有的crvUSD兑换为对应的UZD,最后攻击者拥有4873316数量的UZD,并且将自身的ETH换成对应的SDT,并且将全部的SDT转到MIMCurveStakeDao
中(为什么要进行这样一个存款,可能跟攻击行为有关)
随后调用SushiSwap: Router的swapExactTokensForTokens
函数,进行代币的交换,攻击者首先将自身的WETH兑换为对应的SDT,随后将步骤2中通过闪电贷获得的USDT全部兑换为WETH
攻击者调用UZD合约的cacheAssetPrice()函数,仔细看一下函数源码,获得UZD缓存的资产价格,源码如下:
function cacheAssetPrice() public virtual {
_blockCached = block.number;
uint256 currentAssetPrice = assetPrice();
if (_assetPriceCached < currentAssetPrice) {
_assetPriceCached = currentAssetPrice;
emit CachedAssetPrice(_blockCached, _assetPriceCached);
}
}
_assetPriceCached
的价格是由assetPrice()决定的,进一步阅读函数源码 function assetPrice() public view override returns (uint256) {
return priceOracle.lpPrice();
}
进一步阅读etherscan上源码,可得priceOracle地址为0x2ffCC661011beC72e1A9524E12060983E74D14ce,查看该合约的lpPrice()
函数。
function lpPrice() external view returns (uint256) {
return (totalHoldings() * 1e18) / totalSupply();
}
价格取决于totalHoldings()
函数,totalSupply()
为ERC标准函数
function totalHoldings() public view returns (uint256) {
uint256 length = _poolInfo.length;
uint256 totalHold = 0;
for (uint256 pid = 0; pid < length; pid++) {
totalHold += _poolInfo[pid].strategy.totalHoldings();
}
return totalHold;
}
这个会取决于每个_poolInfo[pid].strategy的Holdings()函数,这里我们去看MIMCurveStakeDao
对应的函数,源码如下所示:
function totalHoldings() public view virtual returns (uint256) { uint256 crvLpHoldings = (vault.liquidityGauge().balanceOf(address(this)) * getCurvePoolPrice()) / CURVE_PRICE_DENOMINATOR; uint256 sdtEarned = vault.liquidityGauge().claimable_reward(address(this), address(_config.sdt)); uint256 amountIn = sdtEarned + _config.sdt.balanceOf(address(this)); uint256 sdtEarningsInFeeToken = priceTokenByExchange(amountIn, _config.sdtToFeeTokenPath); uint256 crvEarned = vault.liquidityGauge().claimable_reward(address(this), address(_config.crv)); amountIn = crvEarned + _config.crv.balanceOf(address(this)); uint256 crvEarningsInFeeToken = priceTokenByExchange(amountIn, _config.crvToFeeTokenPath); uint256 tokensHoldings = 0; for (uint256 i = 0; i < 3; i++) { tokensHoldings += _config.tokens[i].balanceOf(address(this)) * decimalsMultipliers[i]; } return tokensHoldings + crvLpHoldings + (sdtEarningsInFeeToken + crvEarningsInFeeToken) * decimalsMultipliers[feeTokenId]; } function priceTokenByExchange(uint256 amountIn, address[] memory exchangePath) internal view returns (uint256) { if (amountIn == 0) return 0; uint256[] memory amounts = _config.router.getAmountsOut(amountIn, exchangePath); return amounts[amounts.length - 1]; }
重点关注sdtEarningsInFeeToken,因为攻击者在此之前,给该合约存入了大量的SDT,仔细看一下priceTokenByExchange()
函数
进一步可以去SushiSwap: Router中查看getAmountsOut()
函数,发现其返回值与amountIn正相关,amountIn的值一定程度上取决于该合约当前的SDT余额,而攻击者在此之前给该地址存入了大量的SDT,最终导致sdtEarningsInFeeToken数量过高,CachedAssetPrice价格过高
swapExactTokensForTokens
函数,将SDT转化为WETH,将WETH换成USDTfunction balanceOf(address account) public view virtual override returns (uint256) { if (!containRigidAddress(account)) return super.balanceOf(account); return _balancesRigid[account]; } function balanceOf(address account) public view virtual override returns (uint256) { // don't cache price return _convertFromNominalCached(_balances[account], Math.Rounding.Down); } function _convertFromNominalWithCaching(uint256 nominal, Math.Rounding rounding) internal virtual returns (uint256 value) { if (nominal == type(uint256).max) return type(uint256).max; _cacheAssetPriceByBlock(); return nominal.mulDiv(assetPriceCached(), DEFAULT_DECIMALS_FACTOR, rounding); }
所以其会错误计算攻击者的UZD余额,这时攻击者进行相应的套利即可
通过Curve.fi Factory Pool的exchange函数,先将错误余额数量的UZD,一部分兑换为crvFRAX,另一部分兑换为crvUSD。
移除Curve Finance: Swap中的流动性,攻击者获得对应的FRAX和USDC。
调用exchange函数,将对应的FRAX和crvUSD兑换城USDC
并且最后将大部分的USDC全部兑换成USDT,现在攻击者资产为USDT和USDC。
再简单看一下另一个攻击交易0x2aec4fdb2a09ad4269a410f2c770737626fb62c54e0fa8ac25e8582d4b690cca
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。