Metalend Empty-Market Donation Exploit
Exploit Transactions
0x4c684fb2618c29743531dec9253ede1b757bda0b323dc2f305e3b50ab1773da7Victim Addresses
0x5578f2e245e932a599c46215a0ca88707230f17bEthereum0x0d8df79195ec37c6cd53036f9f8ee0c24b23601eEthereum0x0ee4b2c533ed3ffbd9f04cd7e812a4041bbe89f6EthereumLoss Breakdown
Similar Incidents
Bao Donation Borrow Exploit
44%bZx iYFI Donation Inflation
41%Onyx oPEPE Donation Overvaluation
41%ParaSpace cAPE Donation Repricing
37%bZx/Fulcrum WBTC market manipulation drains ETH liquidity
35%Euler DAI Reserve Donation
35%Root Cause Analysis
Metalend Empty-Market Donation Exploit
1. Incident Overview TL;DR
On Ethereum mainnet block 18648754, transaction 0x4c684fb2618c29743531dec9253ede1b757bda0b323dc2f305e3b50ab1773da7 let an unprivileged adversary drain Metalend's mWBTC market and leave the protocol with bad debt. The attacker used only public components: an Aave V3 flash loan, attacker-deployed helper contracts created inside the same transaction, and public Metalend market entrypoints on mETH, mWBTC, and the Comptroller.
The exploit worked because Metalend's empty mETH market behaved like Compound-v2 CEther. Direct ETH donation inflated mETH cash and therefore the stored exchange rate, while redeemUnderlying converted the requested ETH withdrawal into cToken burn units with integer truncation. The Comptroller then approved the redemption using the still-inflated pre-redemption exchange rate, so the attacker could borrow against temporary collateral, redeem almost all of the donated ETH back out, and leave only one dust mETH share backing a large mWBTC debt.
The sender EOA 0x0c06340f5024c114fe196fcb38e42d20ab00f6eb finished with a net native-balance gain of 1960098421973602856 wei after paying 24045679947553308 wei in gas. The exploit was permissionless and reproducible from public pre-state at block 18648753.
2. Key Background
Metalend exposed three relevant public contracts:
mETHat0x5578f2e245e932a599c46215a0ca88707230f17b, the ETH-collateral market.mWBTCat0x0d8df79195ec37c6cd53036f9f8ee0c24b23601e, the WBTC borrow market.- Comptroller at
0x0ee4b2c533ed3ffbd9f04cd7e812a4041bbe89f6, which enforced collateral and borrow checks.
Immediately before the exploit block, the publicly reconstructible pre-state was:
mETH.totalSupply() = 0mETH.getCash() = 0mETH.exchangeRateStored() = 200000000000000000000000000mWBTC.getCash() = 11000000base unitsmWBTC.exchangeRateStored() = 20000000000000000Comptroller.markets(mETH) = (true, 0.85e18, false)Comptroller.markets(mWBTC) = (true, 0.70e18, false)- Oracle price for
mETH=2071000000000000000000 - Oracle price for
mWBTC=376486600000000000000000000000000
The collected deployment provenance ties all three Metalend contracts to creator 0x01289fe1a73c538aca35ec1352df2de8cbc32b9d through transactions 0x713efc847872b43ca4b397b6fe1056c237f51423c562f1e19e9d00dc0ff522e6, 0xc689ddbb6b5e9ad9c6156b9e24a206d4761760e6a635378cef72856c70f0d12f, and 0xdb22cde492d8c575b0ebc91a223898539b95174c86f7b3b6f5891d504d62ed50. The artifact set does not include verified-source metadata for the Metalend contracts, but their runtime strings and call behavior match Compound-v2 CEther, CErc20Delegator, and Comptroller semantics.
The key behavioral property is that CEther-style markets treat raw ETH balance as cash. In Compound's reference implementation, getCashPrior() returns address(this).balance - msg.value, so ETH forced in via selfdestruct increases cash without minting new shares:
function getCashPrior() override internal view returns (uint) {
return address(this).balance - msg.value;
}
function exchangeRateStoredInternal() internal view returns (uint) {
if (_totalSupply == 0) return initialExchangeRateMantissa;
uint totalCash = getCashPrior();
uint cashPlusBorrowsMinusReserves = totalCash + totalBorrows - totalReserves;
return cashPlusBorrowsMinusReserves * 1e18 / _totalSupply;
}
That behavior matters because mETH started empty. Once the attacker reduced supply to dust, every donated wei had an outsized effect on the stored exchange rate.
3. Vulnerability Analysis & Root Cause Summary
This was an ATTACK-class protocol exploit, not a pure MEV arbitrage. The broken invariant was that a redemption should only succeed if the borrower's post-redemption collateral still covers outstanding debt, and unsolicited ETH transfers should not let a near-zero-supply market overstate the value of a dust share balance. Metalend violated that invariant through the interaction of three standard Compound-v2 behaviors: CEther-style cash accounting, redeemUnderlying burn truncation, and Comptroller liquidity checks that use stored pre-redemption exchange rates.
The critical code path is the Compound-v2 redeem flow. redeemUnderlying calls redeemFresh(redeemer, 0, redeemAmountIn), which computes redeemTokens = redeemAmountIn / exchangeRate using integer division, then calls comptroller.redeemAllowed(...) with that truncated token amount. The Comptroller's hypothetical-liquidity check values the account's remaining collateral using the same stored exchange rate that still reflects the donated ETH.
// CToken redeem path
redeemTokens = div_(redeemAmountIn, exchangeRate);
uint allowed = comptroller.redeemAllowed(address(this), redeemer, redeemTokens);
// Comptroller liquidity path
vars.tokensToDenom = mul_(mul_(vars.collateralFactor, vars.exchangeRate), vars.oraclePrice);
vars.sumBorrowPlusEffects =
mul_ScalarTruncateAddUInt(vars.tokensToDenom, redeemTokens, vars.sumBorrowPlusEffects);
When mETH supply had been collapsed to 2, and 100 ETH sat in the contract due to a forced donation, the stored exchange rate temporarily valued each remaining mETH unit at roughly 50 ETH. That let the attacker borrow almost all available mWBTC, then redeem 99.999999999599999999 ETH while burning only 1 share. After the transfer, the remaining dust share was no longer worth anything close to the pre-check value, but the borrow had already been granted and the redemption had already been approved.
4. Detailed Root Cause Analysis
4.1 ACT Pre-State
The ACT opportunity existed in Ethereum mainnet state immediately before block 18648754. Any unprivileged actor could observe the empty mETH market, the live mWBTC liquidity, public oracle prices, and public Comptroller collateral factors through standard RPC calls. No private keys, privileged roles, or victim-side approvals were required.
4.2 Supply Collapse and Donation-Based Exchange-Rate Inflation
The seed trace shows the attacker helper 0x7e9cbcc6f64eb544d17b0448dc226946b0e11596 minting 5000000000 mETH with 1 ETH, then redeeming 4999999998 units for 999999999600000000 wei. That left:
mETH.totalSupply() = 2mETH.getCash() = 400000000- helper
mETHbalance =2
The attacker then deployed 0x5843bf4e3c2527ec408b15b40496fd428511dfe5, funded it with the remaining flash-loaned ETH, and destroyed it to force-send 99999999999600000000 wei into mETH. The seed trace records the breakpoint directly:
0x5578f2E245e932a599c46215a0cA88707230F17B::redeem(4999999998)
SELFDESTRUCT: contract: 0x5843bf4e3c2527ec408b15b40496fd428511dfe5,
refund target: 0x5578f2e245e932a599c46215a0ca88707230f17b,
value 99999999999600000000
After that forced donation, mETH cash was exactly 100000000000000000000 wei while total supply stayed 2. Because CEther-style getCashPrior() reads raw ETH balance, the stored exchange rate now treated each remaining share as roughly 50 ETH of collateral.
4.3 Borrow Against Temporary Collateral
With the exchange rate artificially inflated, the helper entered mETH as collateral through Comptroller.enterMarkets([mETH]) and borrowed 10999999 WBTC base units from mWBTC. The trace captures the public, permissionless sequence:
0x0ee4b2C533ED3fFbd9f04CD7E812A4041bbE89f6::enterMarkets([0x5578f2E245e932a599c46215a0cA88707230F17B])
0x0D8Df79195EC37C6cD53036f9F8eE0c24b23601E::borrow(10999999)
emit Borrow(..., 10999999, 10999999, 10999999)
At that moment, the protocol still viewed the two mETH dust shares as collateral worth roughly 100 ETH before collateral-factor discounts. That was enough to drain almost all mWBTC cash, leaving only 1 base unit in the market.
4.4 Unsafe Redemption Using the Pre-Redemption Exchange Rate
The final exploit step was mETH.redeemUnderlying(99999999999599999999). In the trace, Metalend asks the Comptroller to validate the redemption with only 1 redeem token, not the economic value of the withdrawal:
0x5578f2E245e932a599c46215a0cA88707230F17B::redeemUnderlying(99999999999599999999)
0x0ee4b2C533ED3fFbd9f04CD7E812A4041bbE89f6::redeemAllowed(
0x5578f2E245e932a599c46215a0cA88707230F17B,
0x7E9Cbcc6f64Eb544D17b0448DC226946b0e11596,
1
)
That is the determinative breakpoint. redeemUnderlying converted the requested withdrawal into redeemTokens = 1 because redeemAmount / exchangeRate truncated downward. The Comptroller then valued the remaining position using the still-inflated pre-redemption exchange rate and approved the redemption. Once the ETH transfer executed, the market state collapsed to:
mETH.totalSupply() = 1mETH.getCash() = 400000001mETH.balanceOf(helper) = 1mWBTC.getCash() = 1mWBTC.borrowBalanceStored(helper) = 10999999Comptroller.getAccountLiquidity(helper) = (0, 0, 4141352222809259998240)
The helper therefore kept the borrowed WBTC liability but no longer had real collateral backing it. This is the code-level failure the report identifies: the liquidity check happens against a stale exchange-rate view that the redemption itself destroys.
5. Adversary Flow Analysis
The adversary cluster consisted of four addresses visible in the single exploit transaction:
- EOA
0x0c06340f5024c114fe196fcb38e42d20ab00f6eb: transaction sender and final ETH profit recipient. - Contract
0x80a6419cb8e7d1ef1af074368f7eace1ae2358ca: flash-loan coordinator that received100 WETHfrom Aave V3, managed swaps, repaid the loan, and forwarded profit. - Contract
0x7e9cbcc6f64eb544d17b0448dc226946b0e11596: helper that held the Metalend position, entered collateral, borrowed WBTC, and executed the unsafe redemption. - Contract
0x5843bf4e3c2527ec408b15b40496fd428511dfe5: ephemeral force-send helper that inflatedmETHcash throughSELFDESTRUCT.
The execution flow was end-to-end and fully adversary-crafted:
- The coordinator borrowed
100 WETHfrom Aave V3 in the same transaction. - The coordinator unwrapped WETH to ETH and deployed the helper contract that would hold the manipulated
mETHposition. - The helper minted
mETHwith1 ETH, then redeemed almost all minted shares to reduce supply from5000000000units to2. - A second helper force-sent
99.9999999996 ETHintomETH, inflatingcashand thus the stored exchange rate without minting new shares. - The helper entered
mETHas collateral and borrowed10999999WBTC units frommWBTC. - The helper redeemed
99.999999999599999999 ETHfrommETH, burning only1share and leaving one dust share plus a largemWBTCdebt. - The coordinator swapped the borrowed WBTC to WETH, repaid the Aave flash loan, withdrew the remainder to ETH, and transferred
1984144101921156164wei gross to the EOA sender.
No victim-observed leg was needed. The exploit was a single transaction that any searcher or contract wallet could have submitted under standard network rules.
6. Impact & Losses
Metalend's measurable protocol loss was concentrated in the mWBTC market:
10999999WBTC base units were removed frommWBTCand left as bad debt. Withdecimal = 8, that is0.10999999 WBTC.mWBTCcash fell from11000000to1, meaning effectively the entire available market liquidity was drained.- The helper account ended the transaction with a Comptroller-recorded shortfall of
4141352222809259998240.
The attacker extracted profit in ETH:
- Sender balance before:
77647196754681269wei - Sender balance after:
2037745618728284125wei - Gross ETH returned to sender:
1984144101921156164wei - Gas used:
1106358 - Effective gas price:
21734086026 - Gas cost:
24045679947553308wei - Net sender profit:
1960098421973602856wei
The protocol therefore suffered both an immediate asset loss and persistent undercollateralized debt.
7. References
- Seed exploit transaction:
0x4c684fb2618c29743531dec9253ede1b757bda0b323dc2f305e3b50ab1773da7 - Metalend
mETH:0x5578f2e245e932a599c46215a0ca88707230f17b - Metalend
mWBTC:0x0d8df79195ec37c6cd53036f9f8ee0c24b23601e - Metalend Comptroller:
0x0ee4b2c533ed3ffbd9f04cd7e812a4041bbe89f6 - Aave V3 pool used for flash liquidity:
0xC13e21B648A5Ee794902342038FF3aDAB66BE987 - Related Metalend deployment transactions:
0x713efc847872b43ca4b397b6fe1056c237f51423c562f1e19e9d00dc0ff522e6,0xc689ddbb6b5e9ad9c6156b9e24a206d4761760e6a635378cef72856c70f0d12f,0xdb22cde492d8c575b0ebc91a223898539b95174c86f7b3b6f5891d504d62ed50 - Local evidence used: seed transaction metadata, seed opcode-level trace, seed balance-diff artifact, and the auditor's supporting evidence note
- Reference code used to validate the inherited logic: Compound
CEther.sol,CToken.sol, andComptroller.sol