All incidents

Moonwell Base Oracle Inflation

Share
Nov 04, 2025 05:45 UTCAttackLoss: 20.59 wstETHPending manual check1 exploit txWindow: Atomic
Estimated Impact
20.59 wstETH
Label
Attack
Exploit Tx
1
Addresses
3
Attack Window
Atomic
Nov 04, 2025 05:45 UTC → Nov 04, 2025 05:45 UTC

Exploit Transactions

TX 1Base
0x190a491c0ef095d5447d6d813dc8e2ec11a5710e189771c24527393a2beb05ac
Nov 04, 2025 05:45 UTCExplorer

Victim Addresses

0xfbb21d0380bee3312b33c4353c8936a0f13ef26cBase
0xec942be8a8114bfd0396a5052c36027f2ca6a9d0Base
0x627fe393bc6edda28e99ae648fd6ff362514304bBase

Loss Breakdown

20.59wstETH

Similar Incidents

Root Cause Analysis

Moonwell Base Oracle Inflation

1. Incident Overview TL;DR

On Base, transaction 0x190a491c0ef095d5447d6d813dc8e2ec11a5710e189771c24527393a2beb05ac let an unprivileged adversary turn a tiny amount of wrsETH into borrowable Moonwell collateral, borrow 20.592096934942276800 wstETH from mwstETH, and exit back to ETH with a net gain of 24.522894229480624092 ETH. The exploit path was permissionless and executed in a single transaction through the attacker’s own router contract.

The root cause was Moonwell Base pricing mwrsETH through a symbol-mapped wrsETH feed whose answer was out of scale by orders of magnitude. ChainlinkOracle accepted that answer without unit sanity checks, and Comptroller consumed it directly in collateral calculations, so economically negligible collateral was booked as massively valuable.

2. Key Background

Moonwell prices ERC-20 collateral through ChainlinkOracle at 0xec942be8a8114bfd0396a5052c36027f2ca6a9d0. For ERC-20 markets, the oracle resolves the underlying token from the mToken and then either uses a direct override in prices[address(token)] or falls back to getChainlinkPrice(getFeed(token.symbol())). The relevant source confirms the symbol-keyed feed lookup and the lack of asset-specific range validation:

function getPrice(MToken mToken) internal view returns (uint256 price) {
    EIP20Interface token = EIP20Interface(MErc20(address(mToken)).underlying());
    if (prices[address(token)] != 0) {
        price = prices[address(token)];
    } else {
        price = getChainlinkPrice(getFeed(token.symbol()));
    }
    uint256 decimalDelta = uint256(18).sub(uint256(token.decimals()));
    if (decimalDelta > 0) return price.mul(10 ** decimalDelta);
    return price;
}

Moonwell enforces borrow safety in Comptroller at 0xfbb21d0380bee3312b33c4353c8936a0f13ef26c by computing account liquidity from collateral factor, exchange rate, and oracle price. The collected comptroller source shows the critical multiplication:

vars.oraclePriceMantissa = oracle.getUnderlyingPrice(asset);
vars.oraclePrice = Exp({mantissa: vars.oraclePriceMantissa});
vars.tokensToDenom = mul_(mul_(vars.collateralFactor, vars.exchangeRate), vars.oraclePrice);
vars.sumCollateral = mul_ScalarTruncateAddUInt(vars.tokensToDenom, vars.mTokenBalance, vars.sumCollateral);

The wrsETH wrapper itself is not the amplification source. Its collected source shows 1:1 deposit and withdrawal semantics for allowed assets:

function _deposit(address _asset, address _to, uint256 _amount) internal {
    if (!allowedTokens[_asset]) revert TokenNotAllowed();
    ERC20Upgradeable(_asset).safeTransferFrom(msg.sender, address(this), _amount);
    _mint(_to, _amount);
}

3. Vulnerability Analysis & Root Cause Summary

This was an ATTACK-class oracle integration failure. On the exploit block, Moonwell had no direct wrsETH price override, so it priced mwrsETH through the configured wrsETH feed returned by getFeed("wrsETH"). That feed, 0x79C613B4f07080963C3B0CA58Eb2745dD4C744A5, returned 5813066107843462759938890135, and because wrsETH has 18 decimals, Moonwell accepted that value unchanged as the underlying price. The price was far above the intended scale for collateral accounting, but the oracle implementation performed no sanity check on feed units or magnitude. Comptroller then multiplied the inflated price into tokensToDenom, making a tiny mwrsETH balance appear sufficient to support a much larger wstETH borrow. The exploit required no privileged role, no attacker-side victim artifacts, and no private state; only public oracle state, public Moonwell markets, and public AMM liquidity were needed.

4. Detailed Root Cause Analysis

The ACT pre-state was the Base state immediately before block 37722882, while Moonwell still trusted the mis-scaled wrsETH feed and before the adversary transaction executed. The seed metadata and validator fork run both confirm that assetPrices(wrsETH) == 0, getFeed("wrsETH") == 0x79C613B4f07080963C3B0CA58Eb2745dD4C744A5, and getUnderlyingPrice(mwrsETH) == 5813066107843462759938890135.

The first code-level breakpoint is the oracle path itself: ChainlinkOracle.getPrice calls getChainlinkPrice(getFeed(token.symbol())) for wrsETH because no direct override exists. The second breakpoint is in Comptroller.getHypotheticalAccountLiquidityInternal, where that raw oracle output is multiplied into tokensToDenom and therefore into sumCollateral. The safety invariant that fails is: collateral value admitted into borrow checks must reflect the correctly scaled realizable value of the underlying asset.

The exploit transaction used public pools to source a small amount of wrsETH, then minted Moonwell mwrsETH, entered the market, and borrowed 20592096934942276800 raw wstETH. The collected balance diff confirms the market loss and the adversary profit. Relevant deltas include:

{
  "token": "0xc1cba3fcea344f92d9239c08c0568f6f2f0ee452",
  "holder": "0x627fe393bc6edda28e99ae648fd6ff362514304b",
  "delta": "-20592096934942276800"
}
{
  "address": "0x6997a8c804642ae2de16d7b8ff09565a5d5658ff",
  "delta_wei": "24522894229480624092"
}

The collected trace and the validator rerun on a fork reproduce the same semantic path: acquire wrsETH from the public WETH/wrsETH pool, mint mwrsETH, enter the market, confirm positive liquidity, borrow wstETH, swap through the public wstETH/WETH pool, and end with a strongly positive ETH balance delta.

5. Adversary Flow Analysis

The adversary cluster consists of EOA 0x6997a8c804642ae2de16d7b8ff09565a5d5658ff and router 0x42ecd332d47c91cbc83b39bd7f53cebe5e9734bb. The router was created earlier in transaction 0x9aaa0b001591d98f8acc5599aa1270324f0d3175285808d289da0fc52483a500, then used in the exploit transaction.

In the exploit transaction, the router first sourced wrsETH from the public WETH/wrsETH Uniswap V3 pool. The collected balance diff shows 20782357954960 raw wrsETH moving into Moonwell’s mwrsETH market, and 103747 raw mwrsETH minted to the attacker router. The router then entered the mwrsETH market as collateral and called the mwstETH market to borrow 20592096934942276800 raw wstETH.

After borrowing, the router swapped the borrowed wstETH through the public wstETH/WETH pool and the controlling EOA finished with a positive ETH delta. The full flow is therefore: public pool acquisition -> public Moonwell mint -> public market entry -> public Moonwell borrow -> public AMM exit -> realized ETH profit.

6. Impact & Losses

Moonwell’s mwstETH market lost 20.592096934942276800 wstETH, represented on-chain as raw amount 20592096934942276800 with 18 decimals. The protocol was left with a borrow position supported by collateral that was only sufficient under the mis-scaled oracle, not under a correctly scaled price.

The adversary’s seed transaction produced a net native-asset gain of 24.522894229480624092 ETH after fees, as shown in the collected balance diff. The directly measured victim-side asset loss was the extracted wstETH from the mwstETH market.

7. References

  • Seed exploit transaction: 0x190a491c0ef095d5447d6d813dc8e2ec11a5710e189771c24527393a2beb05ac
  • Related router-creation transaction: 0x9aaa0b001591d98f8acc5599aa1270324f0d3175285808d289da0fc52483a500
  • Moonwell Comptroller: 0xfbb21d0380bee3312b33c4353c8936a0f13ef26c
  • Moonwell ChainlinkOracle: 0xec942be8a8114bfd0396a5052c36027f2ca6a9d0
  • mwrsETH: 0xfc41b49d064ac646015b459c522820db9472f4b5
  • mwstETH: 0x627fe393bc6edda28e99ae648fd6ff362514304b
  • wrsETH: 0xedfa23602d0ec14714057867a78d01e94176bea0
  • Mis-scaled wrsETH feed: 0x79C613B4f07080963C3B0CA58Eb2745dD4C744A5
  • Public pool used to source wrsETH: 0x16e25facba67a40da3436ab9e2e00c30dab0dd97
  • Public pool used to exit borrowed wstETH: 0x861a2922be165a5bd41b1e482b49216b465e1b5f