All incidents

Metalend Empty-Market Donation Exploit

Share
Nov 25, 2023 12:36 UTCAttackLoss: 0.11 WBTCPending manual check1 exploit txWindow: Atomic
Estimated Impact
0.11 WBTC
Label
Attack
Exploit Tx
1
Addresses
3
Attack Window
Atomic
Nov 25, 2023 12:36 UTC → Nov 25, 2023 12:36 UTC

Exploit Transactions

TX 1Ethereum
0x4c684fb2618c29743531dec9253ede1b757bda0b323dc2f305e3b50ab1773da7
Nov 25, 2023 12:36 UTCExplorer

Victim Addresses

0x5578f2e245e932a599c46215a0ca88707230f17bEthereum
0x0d8df79195ec37c6cd53036f9f8ee0c24b23601eEthereum
0x0ee4b2c533ed3ffbd9f04cd7e812a4041bbe89f6Ethereum

Loss Breakdown

0.11WBTC

Similar Incidents

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:

  • mETH at 0x5578f2e245e932a599c46215a0ca88707230f17b, the ETH-collateral market.
  • mWBTC at 0x0d8df79195ec37c6cd53036f9f8ee0c24b23601e, 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() = 0
  • mETH.getCash() = 0
  • mETH.exchangeRateStored() = 200000000000000000000000000
  • mWBTC.getCash() = 11000000 base units
  • mWBTC.exchangeRateStored() = 20000000000000000
  • Comptroller.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() = 2
  • mETH.getCash() = 400000000
  • helper mETH balance = 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() = 1
  • mETH.getCash() = 400000001
  • mETH.balanceOf(helper) = 1
  • mWBTC.getCash() = 1
  • mWBTC.borrowBalanceStored(helper) = 10999999
  • Comptroller.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 received 100 WETH from 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 inflated mETH cash through SELFDESTRUCT.

The execution flow was end-to-end and fully adversary-crafted:

  1. The coordinator borrowed 100 WETH from Aave V3 in the same transaction.
  2. The coordinator unwrapped WETH to ETH and deployed the helper contract that would hold the manipulated mETH position.
  3. The helper minted mETH with 1 ETH, then redeemed almost all minted shares to reduce supply from 5000000000 units to 2.
  4. A second helper force-sent 99.9999999996 ETH into mETH, inflating cash and thus the stored exchange rate without minting new shares.
  5. The helper entered mETH as collateral and borrowed 10999999 WBTC units from mWBTC.
  6. The helper redeemed 99.999999999599999999 ETH from mETH, burning only 1 share and leaving one dust share plus a large mWBTC debt.
  7. The coordinator swapped the borrowed WBTC to WETH, repaid the Aave flash loan, withdrew the remainder to ETH, and transferred 1984144101921156164 wei 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:

  • 10999999 WBTC base units were removed from mWBTC and left as bad debt. With decimal = 8, that is 0.10999999 WBTC.
  • mWBTC cash fell from 11000000 to 1, 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: 77647196754681269 wei
  • Sender balance after: 2037745618728284125 wei
  • Gross ETH returned to sender: 1984144101921156164 wei
  • Gas used: 1106358
  • Effective gas price: 21734086026
  • Gas cost: 24045679947553308 wei
  • Net sender profit: 1960098421973602856 wei

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, and Comptroller.sol