Originally published at Moonwell Exploit: A Donation That Skipped the MAMO Cap

On 27 August 2026, an attacker borrowed $11,028,762 of cbBTC, WETH, USDC and wstETH from Moonwell, a Compound v2 style lending market on Base. The collateral behind those loans was MAMO, a thinly traded token that the attacker had formally deposited for only about $160K. Around 8.73M USDC of the proceeds left Base through Circle's CCTP within the hour, which is where the "$8.7M exploit" headline comes from.

The case is back in the news because on 2 November 2026 Certora published a formal verification write-up of the bug, and it is worth studying closely. The market had a supply cap that was meant to stop exactly this kind of oversized collateral position. The cap was set correctly and it measured the right number. It simply never ran, because the attacker added most of the collateral with a plain ERC-20 transfer instead of a deposit.

This post explains how the attack worked, why the cap did not fire, why this is the third Compound fork hit by the same pattern in 2026, and what we check for in lending code during an audit.

TL;DR

  • Protocol: Moonwell, MAMO market (mMAMO) on Base.
  • Date: 27 August 2026, roughly 06:09 to 09:46 UTC. Post-mortem by Anthias Labs, Moonwell's risk manager, on 28 August; Certora's formal analysis on 2 November.
  • Borrowed: $11.03M across 18 borrows. 8,729,454 USDC was burned on Base via CCTP and arrived on Ethereum, where it was swapped to DAI.
  • Residual debt: about $9.13M of borrower obligations remained at Anthias's evidence cutoff, its best estimate of potential bad debt.
  • Root cause: the market's cash is read from balanceOf(address(this)), so tokens sent directly to the contract raise the mToken exchange rate. The 20M MAMO supply cap was only checked inside mintAllowed, which a direct transfer never calls.
  • Amplifier: the attacker pushed the MAMO/USD feed from about $0.0106 to $0.4313 by buying on thin DEX liquidity.

What Moonwell's MAMO Market Looked Like

Moonwell listed MAMO through proposal MIP-B48 on 15 October 2025 with a 50% collateral factor, a 20,000,000 MAMO supply cap, a 3,000,000 MAMO borrow cap and a 30% reserve factor. According to Certora, the listing note from Anthias said roughly $200K of MAMO could be sold instantly while keeping slippage below the 7% liquidation bonus. In other words, the cap existed to keep MAMO collateral close to what liquidators could actually sell. On the morning of the attack, 20M MAMO was worth about $212K.

Like every Compound v2 fork, Moonwell gives suppliers a receipt token. One mMAMO is redeemable for an amount of MAMO set by the exchange rate:

exchangeRate = (totalCash + totalBorrows - totalReserves) / totalSupply 

Moonwell's own functions update totalBorrows, totalReserves and totalSupply. The fourth input, totalCash, is not stored. It is read live from the token, as Certora quotes from MErc20.sol:

function getCashPrior() internal view virtual override returns (uint) { EIP20Interface token = EIP20Interface(underlying); return token.balanceOf(address(this)); } 

That single line is the whole vulnerability. Anything that increases the contract's MAMO balance increases what every existing mMAMO is worth, whether or not it came through Moonwell.

How the Attack Unfolded

Anthias Labs' post-mortem traces the funding back to 800.098 ETH withdrawn from Tornado Cash on 21 and 23 August. A linked account sold 799 ETH for 1,947,391 USDC through CoW Swap, bridged it to Base and handed MAMO, USDC and gas to the principal account (0x719e...919D, shown truncated in the post-mortem). From there, all times are UTC on 27 August:

Time Event
06:09:45 Linked account funds the principal account with gas
07:04:11 First mMAMO mint; three mints supply 15,089,595 MAMO in total
08:43:13 First borrow: 0.50071233 cbBTC
09:19:59 34,910,397 MAMO sent directly to mMAMO, no mint
09:21:09 18,482,894 MAMO sent directly to mMAMO, no mint
09:28:49 MAMO/USD source feed peaks at $0.43127363
09:29:31 Moonwell accepts $0.40248571; 368 wstETH borrowed
09:30:13 Final borrow: 990,000 USDC
09:30:45 First liquidation, 32 seconds later
09:41:43 and 09:42:43 Two CCTP burns of 4,364,726.91 USDC each
10:53:43 Borrow caps cut to 1 wei
11:09:43 MAMO supply cap cut to 1 wei

Step 1: a legitimate looking deposit

The formal deposit of 15.09M MAMO stayed below the 20M cap, so every mint passed. It gave the attacker roughly 735.6M mMAMO, about three quarters of all mMAMO in existence. That share matters: whoever owns most of the receipts captures most of any increase in the exchange rate.

Step 2: the donation

The two direct transfers added 53,393,290 MAMO to the market. No Mint event was emitted and mMAMO total supply did not change, but cash did. Anthias measured the exchange rate rising from about 0.020513 to 0.075460 MAMO per mMAMO, a factor of 3.68. The attacker's receipts now represented about 55.51M MAMO, almost three times the cap on their own.

Step 3: the price push

In parallel, the attacker's accounts bought about 94.31M MAMO on DEXs, including Aerodrome's MAMO/USDC pool, for about $7.5M of gross volume. Much of that was recycled borrowed money. The oracle feed climbed from $0.0106 at 09:06 to $0.4313 at 09:28 and was back near $0.014 by 09:45.

At Moonwell's highest accepted price, 55.51M MAMO was worth about $22.34M, which at a 50% collateral factor allowed about $11.17M of borrowing. The attacker took $11.03M. Certora makes the point that both halves were needed: without the donation the position stays at 15M MAMO, and without the pump 55M MAMO at $0.0106 is only worth about $590K.

Step 4: borrow, bridge, walk away

Liquidators seized essentially all of the attacker's mMAMO across 595 liquidation events, but the MAMO price was already collapsing, so the seized collateral could not cover the debt. The attacker never needed to redeem the donated MAMO. Borrowing against it and leaving the debt behind was the whole plan.

Why the Supply Cap Did Not Stop It

Moonwell's cap lives in the Comptroller's mintAllowed hook. Certora quotes the check:

uint totalSupplies = sub_(add_(totalCash, totalBorrows), totalReserves); uint nextTotalSupplies = add_(totalSupplies, mintAmount); require(nextTotalSupplies < supplyCap, "market supply cap reached"); 

The formula is correct: it measures everything the market owes its suppliers, including donated cash. The problem is when it runs. It only runs on mint. A MAMO transfer executes inside the MAMO token contract and never calls Moonwell, so no Moonwell code can object. As Anthias put it, "the 20 million MAMO supply cap checked formal mint inputs." After the two transfers, the market would have refused any new deposit, but there was nothing left to refuse.

A cap enforced at one entry point only limits that entry point. If a quantity can also change through a path you do not control, the cap is a speed bump, not a limit.

The Same Bug, Three Times in 2026

Donation attacks on Compound forks are not new. Certora notes that Moonwell's own repository has carried a test since 2023 that reproduces the Hundred Finance exploit: it sends tokens directly to a market, and a comment says the attacker "can now borrow everything in the markets due to the massively inflated exchange rate." The tests then check that the donation cannot be pulled back out through redeemUnderlying. That protects one exit, but the MAMO attacker used a different one: borrowing.

In 2026 alone, Certora lists three Compound v2 forks hit by direct transfers combined with a pumped, thin collateral token: Venus on 15 March (where, per Rekt, a position reached 367% of the cap), Moonwell on 27 August and Tectonic on Cronos on 30 August. We touched on Tectonic's price manipulation in our roundup of recent blockchain hack lessons. The pattern is close to what happened in the Premia v3 vault exploit: the code did what it was written to do, but an accounting assumption about where balances come from did not hold.

The Fix: Count Only What You Received

The robust fix is to stop deriving cash from balanceOf. Certora proposes tracking cash in an internal ledger that only the market's own transfer functions move. A simplified version of their patch:

// Simplified for illustration, based on Certora's proposed fix. contract MErc20DelegateInternalCash is MErc20Delegate { uint256 internal internalCash; function getCashPrior() internal view override returns (uint256) { return internalCash; // ignore unsolicited transfers } function doTransferIn(address from, uint256 amount) internal override returns (uint256) { uint256 received = super.doTransferIn(from, amount); internalCash += received; return received; } function doTransferOut(address payable to, uint256 amount) internal override { internalCash -= amount; super.doTransferOut(to, amount); } } 

A live market would also need a one-time admin call to initialise the ledger to the current balance. Certora's AutoProver, run after the attack against commit 3b32d53 (the last before the attack), wrote a rule stating that a donation must not change the supplier claim the cap is checked against. The Certora Prover reports that rule violated on the deployed contract and verified with the ledger, assuming MAMO is a standard ERC-20 with no callbacks. Moonwell, for its part, has not changed how markets count cash; it set the MAMO supply and borrow caps to 1 wei, used about $246K of reserves to repay bad debt in the USDC market (MIP-X66), and set MAMO's collateral factor to 0% from 17 October (MIP-X69).

Lessons for Builders and Auditors

1. Never price shares from balanceOf(address(this)). Vault shares, lending receipts and LP tokens should all be valued from internal accounting. If you must read the balance, treat any excess as a donation that does not change share value. This belongs on every smart contract security checklist.

2. Enforce invariants on state, not on entry points. Ask "what can change this number?" rather than "which function should check it?". If a transfer can move it, the check has to live where the number is read, or the number has to stop depending on transfers.

3. Treat low-liquidity collateral as an oracle problem and a cap problem together. A cap sized to liquidation depth is only meaningful if nothing can grow positions past it. Pair caps with price feeds that resist minute-scale manipulation, such as TWAPs or circuit breakers on sudden moves.

4. Test the exits you did not think of. Moonwell tested that a donation could not be redeemed. It did not test that a donation could not be borrowed against. For each attack class, enumerate every way value can leave: redeem, borrow, liquidate, transfer receipts.

5. Monitor for unsolicited transfers. A 35M token transfer into a market contract with no Mint event is a loud, cheap signal. An alert on that would have given Moonwell about ten minutes before the final borrows.

Key Addresses and Figures

Item Value
Chain Base
Principal account (truncated in post-mortem) 0x719e...919D
Linked operational account (truncated) 0xD71d...C384
Formal MAMO supplied 15,089,595 MAMO
Direct MAMO transfers 53,393,290 MAMO
Exchange rate change 0.020513 to 0.075460 MAMO per mMAMO
Total borrowed $11,028,762 (18 borrows)
USDC burned via CCTP 8,729,453.83 USDC
Residual debt at evidence cutoff about $9,131,342

Sources

Closing Thoughts

The Moonwell exploit is a reminder that a correct check in the wrong place is not a control. The supply cap measured the right number, and the attack class had even been tested for years earlier, yet $11M walked out through a path nobody had pinned down. If your protocol values shares, receipts or collateral from token balances, talk to our team or see our audit pricing to have those accounting assumptions reviewed before someone tests them on mainnet.