Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Always set the id in @proposals/mips/mips.json to 0 when creating new proposals
5 changes: 5 additions & 0 deletions chains/10.json
Original file line number Diff line number Diff line change
Expand Up @@ -708,5 +708,10 @@
"addr": "0x39Bd42ce85CcC2f8792f13B2726369cD3F946D7c",
"isContract": true,
"name": "ANTHIAS_MULTISIG"
},
{
"addr": "0x73b8BE3b653c5896BC34fC87cEBC8AcF4Fb7A545",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified it matches the chainlink docs.

"isContract": true,
"name": "CHAINLINK_wrsETH_ETH_EXCHANGE_RATE"
}
]
5 changes: 5 additions & 0 deletions chains/8453.json
Original file line number Diff line number Diff line change
Expand Up @@ -1223,5 +1223,10 @@
"addr": "0x74Cbb1E8B68dDD13B28684ECA202a351afD45EAa",
"isContract": true,
"name": "F-DEVGRANT"
},
{
"addr": "0xe8dD07CCf5BC4922424140E44Eb970F5950725ef",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed, it matches the Chainlink docs.

"isContract": true,
"name": "CHAINLINK_wrsETH_ETH_EXCHANGE_RATE"
}
]
303 changes: 303 additions & 0 deletions proposals/mips/mip-x36/mip-x36.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,303 @@
//SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity 0.8.19;

import "@forge-std/Test.sol";

import {MToken} from "@protocol/MToken.sol";
import {Comptroller} from "@protocol/Comptroller.sol";
import {MErc20} from "@protocol/MErc20.sol";
import {ChainlinkOracle} from "@protocol/oracles/ChainlinkOracle.sol";
import {ChainlinkCompositeOracle} from "@protocol/oracles/ChainlinkCompositeOracle.sol";
import {AggregatorV3Interface} from "@protocol/oracles/AggregatorV3Interface.sol";

import {HybridProposal, ActionType} from "@proposals/proposalTypes/HybridProposal.sol";
import {AllChainAddresses as Addresses} from "@proposals/Addresses.sol";
import {MOONBEAM_FORK_ID, BASE_FORK_ID, OPTIMISM_FORK_ID} from "@utils/ChainIds.sol";
import {ProposalActions} from "@proposals/utils/ProposalActions.sol";
import {ChainIds} from "@utils/ChainIds.sol";

/// @title MIP-X36: Deprecate wrsETH Markets with Exchange-Rate Oracle Transition
/// @author Moonwell Contributors
/// @notice Proposal to deprecate wrsETH markets on Base and Optimism by:
/// 1. Setting supply and borrow caps to 0
/// 2. Deploying new ChainlinkCompositeOracle contracts using exchange rate feeds
/// 3. Updating oracle addresses for wrsETH markets
contract mipx36 is HybridProposal {
using ProposalActions for *;
using ChainIds for uint256;

string public constant override name = "MIP-X36";

uint256 public constant NEW_SUPPLY_CAP = 0.1e18;
uint256 public constant NEW_BORROW_CAP = 0.1e18;

// Storage for deployed oracles
ChainlinkCompositeOracle public baseWrsethOracle;
ChainlinkCompositeOracle public optimismWrsethOracle;

constructor() {
bytes memory proposalDescription = abi.encodePacked(
vm.readFile("./proposals/mips/mip-x36/x36.md")
);
_setProposalDescription(proposalDescription);
}

function run() public override {
primaryForkId().createForksAndSelect();

Addresses addresses = new Addresses();
vm.makePersistent(address(addresses));

initProposal(addresses);

(, address deployerAddress, ) = vm.readCallers();

if (DO_DEPLOY) deploy(addresses, deployerAddress);
if (DO_AFTER_DEPLOY) afterDeploy(addresses, deployerAddress);

if (DO_BUILD) build(addresses);
if (DO_RUN) run(addresses, deployerAddress);
if (DO_TEARDOWN) teardown(addresses, deployerAddress);
if (DO_VALIDATE) {
validate(addresses, deployerAddress);
console.log("Validation completed for proposal ", this.name());
}
if (DO_PRINT) {
printProposalActionSteps();

addresses.removeAllRestrictions();
printCalldata(addresses);

_printAddressesChanges(addresses);
}
}


function primaryForkId() public pure override returns (uint256) {
return BASE_FORK_ID;
}

function deploy(Addresses addresses, address) public override {
// Deploy new ChainlinkCompositeOracle for Base wrsETH
vm.selectFork(BASE_FORK_ID);
vm.startBroadcast();

address baseEthUsdFeed = addresses.getAddress("CHAINLINK_ETH_USD");
address baseWrsethEthExchangeRateFeed = addresses.getAddress("CHAINLINK_wrsETH_ETH_EXCHANGE_RATE");

baseWrsethOracle = new ChainlinkCompositeOracle(
baseEthUsdFeed,
baseWrsethEthExchangeRateFeed,
address(0)
);

vm.stopBroadcast();

// Deploy new ChainlinkCompositeOracle for Optimism wrsETH
vm.selectFork(OPTIMISM_FORK_ID);
vm.startBroadcast();

address optimismEthUsdFeed = addresses.getAddress("CHAINLINK_ETH_USD");
address optimismWrsethEthExchangeRateFeed = addresses.getAddress("CHAINLINK_wrsETH_ETH_EXCHANGE_RATE");

optimismWrsethOracle = new ChainlinkCompositeOracle(
optimismEthUsdFeed,
optimismWrsethEthExchangeRateFeed,
address(0)
);

vm.stopBroadcast();
}

function afterDeploy(Addresses addresses, address) public override {
vm.selectFork(BASE_FORK_ID);
addresses.changeAddress("CHAINLINK_wrsETH_COMPOSITE_ORACLE", address(baseWrsethOracle), true);

vm.selectFork(OPTIMISM_FORK_ID);
addresses.changeAddress("CHAINLINK_wrsETH_COMPOSITE_ORACLE", address(optimismWrsethOracle), true);
}

function build(Addresses addresses) public override {
// ============ BASE CHAIN ACTIONS ============
vm.selectFork(BASE_FORK_ID);

address baseComptroller = addresses.getAddress("UNITROLLER");
address baseWrsethMToken = addresses.getAddress("MOONWELL_wrsETH");

address[] memory baseMarkets = new address[](1);
baseMarkets[0] = baseWrsethMToken;

uint256[] memory baseSupplyCaps = new uint256[](1);
baseSupplyCaps[0] = NEW_SUPPLY_CAP;

uint256[] memory baseBorrowCaps = new uint256[](1);
baseBorrowCaps[0] = NEW_BORROW_CAP;

// Set supply cap to 0 on Base
_pushAction(
baseComptroller,
abi.encodeWithSignature(
"_setMarketSupplyCaps(address[],uint256[])",
baseMarkets,
baseSupplyCaps
),
"Set supply cap to 0.1e18 for wrsETH on Base",
ActionType.Base
);

// Set borrow cap to 0 on Base
_pushAction(
baseComptroller,
abi.encodeWithSignature(
"_setMarketBorrowCaps(address[],uint256[])",
baseMarkets,
baseBorrowCaps
),
"Set borrow cap to 0.1e18 for wrsETH on Base",
ActionType.Base
);

// Update oracle price feed on Base
address baseChainlinkOracle = addresses.getAddress("CHAINLINK_ORACLE");
_pushAction(
baseChainlinkOracle,
abi.encodeWithSignature(
"setFeed(string,address)",
"wrsETH",
address(baseWrsethOracle)
),
"Update wrsETH oracle to exchange rate feed on Base",
ActionType.Base
);

// ============ OPTIMISM CHAIN ACTIONS ============
vm.selectFork(OPTIMISM_FORK_ID);

address optimismComptroller = addresses.getAddress("UNITROLLER");
address optimismWrsethMToken = addresses.getAddress("MOONWELL_wrsETH");

address[] memory optimismMarkets = new address[](1);
optimismMarkets[0] = optimismWrsethMToken;

uint256[] memory optimismSupplyCaps = new uint256[](1);
optimismSupplyCaps[0] = NEW_SUPPLY_CAP;

uint256[] memory optimismBorrowCaps = new uint256[](1);
optimismBorrowCaps[0] = NEW_BORROW_CAP;

// Set supply cap to 0 on Optimism
_pushAction(
optimismComptroller,
abi.encodeWithSignature(
"_setMarketSupplyCaps(address[],uint256[])",
optimismMarkets,
optimismSupplyCaps
),
"Set supply cap to 0.1e18 for wrsETH on Optimism",
ActionType.Optimism
);

// Set borrow cap to 0 on Optimism
_pushAction(
optimismComptroller,
abi.encodeWithSignature(
"_setMarketBorrowCaps(address[],uint256[])",
optimismMarkets,
optimismBorrowCaps
),
"Set borrow cap to 0.1e18 for wrsETH on Optimism",
ActionType.Optimism
);

// Update oracle price feed on Optimism
address optimismChainlinkOracle = addresses.getAddress("CHAINLINK_ORACLE");
_pushAction(
optimismChainlinkOracle,
abi.encodeWithSignature(
"setFeed(string,address)",
"wrsETH",
address(optimismWrsethOracle)
),
"Update wrsETH oracle to exchange rate feed on Optimism",
ActionType.Optimism
);
}

function teardown(Addresses addresses, address) public pure override {}

function validate(Addresses addresses, address) public override {
// ============ VALIDATE BASE CHAIN ============
vm.selectFork(BASE_FORK_ID);

Comptroller baseComptroller = Comptroller(addresses.getAddress("UNITROLLER"));
address baseWrsethMToken = addresses.getAddress("MOONWELL_wrsETH");

// Validate supply cap is 0
uint256 baseSupplyCap = baseComptroller.supplyCaps(baseWrsethMToken);
assertEq(
baseSupplyCap,
NEW_SUPPLY_CAP,
"Base wrsETH supply cap not set to 0.1e18"
);

// Validate borrow cap is 0
uint256 baseBorrowCap = baseComptroller.borrowCaps(baseWrsethMToken);
assertEq(
baseBorrowCap,
NEW_BORROW_CAP,
"Base wrsETH borrow cap not set to 0.1e18"
);

// Validate oracle is updated
ChainlinkOracle baseChainlinkOracle = ChainlinkOracle(
addresses.getAddress("CHAINLINK_ORACLE")
);
AggregatorV3Interface baseFeed = baseChainlinkOracle.getFeed("wrsETH");
assertEq(
address(baseFeed),
address(baseWrsethOracle),
"Base wrsETH oracle not updated"
);

// Validate price can be fetched
(, int256 basePrice, , , ) = baseFeed.latestRoundData();
assertGt(uint256(basePrice), 0, "Base wrsETH price check failed");

// ============ VALIDATE OPTIMISM CHAIN ============
vm.selectFork(OPTIMISM_FORK_ID);

Comptroller optimismComptroller = Comptroller(addresses.getAddress("UNITROLLER"));
address optimismWrsethMToken = addresses.getAddress("MOONWELL_wrsETH");

// Validate supply cap is 0
uint256 optimismSupplyCap = optimismComptroller.supplyCaps(optimismWrsethMToken);
assertEq(
optimismSupplyCap,
NEW_SUPPLY_CAP,
"Optimism wrsETH supply cap not set to 0.1e18"
);

// Validate borrow cap is 0
uint256 optimismBorrowCap = optimismComptroller.borrowCaps(optimismWrsethMToken);
assertEq(
optimismBorrowCap,
NEW_BORROW_CAP,
"Optimism wrsETH borrow cap not set to 0.1e18"
);

// Validate oracle is updated
ChainlinkOracle optimismChainlinkOracle = ChainlinkOracle(
addresses.getAddress("CHAINLINK_ORACLE")
);
AggregatorV3Interface optimismFeed = optimismChainlinkOracle.getFeed("wrsETH");
assertEq(
address(optimismFeed),
address(optimismWrsethOracle),
"Optimism wrsETH oracle not updated"
);

// Validate price can be fetched
(, int256 optimismPrice, , , ) = optimismFeed.latestRoundData();
assertGt(uint256(optimismPrice), 0, "Optimism wrsETH price check failed");
}
}
59 changes: 59 additions & 0 deletions proposals/mips/mip-x36/x36.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
## MIP-X36: Disable Mint/Borrow in wrsETH Markets and Transition to Exchange-Rate Oracle

### **Summary**

Following the oracle malfunction involving the wrsETH/ETH feed on November 4, 2025, this proposal seeks to formally disable minting and borrowing in the wrsETH markets on both Base and OP Mainnet and transition these markets to use an exchange-rate feed rather than a market price oracle.

These steps reduce further risk exposure and set the foundation for a gradual, orderly deprecation of the wrsETH markets.

### **Background**

At approximately **5:44 AM UTC on November 4**, an oracle malfunction caused the **wrsETH/ETH feed** to report a faulty value, drastically overpricing wrsETH and enabling an attacker to borrow multiple assets using minimal collateral.

The wrsETH/USD price is derived by multiplying the Chainlink ETH/USD oracle with the wrsETH/ETH oracle. The wrsETH/ETH oracle erroneously reported **1 wrsETH = 1,649,934.6 ETH**, valuing each token at roughly **$5.8 billion**. This led to approximately **$3.7 million** in bad debt across the Moonwell protocol.

Immediately following the mispricing event:

* Supply and borrow caps for wrsETH were set to effectively zero.

* Borrow caps for all Core Markets on Base and Optimism were temporarily reduced to **0.1** to prevent additional over-borrowing.

* All deposits and withdrawals remained open, allowing suppliers to withdraw funds where liquidity was available.

### **Proposal**

To maintain a risk-off stance and begin the safe wind-down of wrsETH exposure, this proposal will:

#### 1. Disable minting and borrowing in wrsETH markets on Base and OP Mainnet.

No new wrsETH can be supplied or borrowed. Supply and borrow caps will be set to a near-zero value (as setting caps to zero represents infinity at a contract level). Repayments and withdrawals will remain enabled for existing users.

#### 2. Transition to exchange-rate feeds, replacing the existing oracle.

This ensures accurate value representation based on wrsETH's exchange-rate mechanics rather than rely on market rate pricing.

The exchange-rate feeds can be found here:

**Base**
- [Chainlink Exchange-Rate Feed](https://data.chain.link/feeds/base/base/wrseth-eth-exchange-rate)
- [wrsETH Contract (BaseScan)](https://basescan.org/address/0xe8dD07CCf5BC4922424140E44Eb970F5950725ef)

**OP Mainnet**
- [Chainlink Exchange-Rate Feed](https://data.chain.link/feeds/optimism/mainnet/wrseth-eth-exchange-rate)
- [wrsETH Contract (Optimism Etherscan)](https://optimistic.etherscan.io/address/0x73b8BE3b653c5896BC34fC87cEBC8AcF4Fb7A545)

#### 3. **Prepare the markets for deprecation.**

Once minting and borrowing are disabled and the oracle is transitioned, the community can proceed with a gradual deprecation of wrsETH markets on Base and OP Mainnet. Users will receive advance notice and be encouraged to repay outstanding loans and withdraw wrsETH to avoid liquidation risk during the wind-down process.

### **Rationale**

* Disabling mint and borrow eliminates the primary vectors for further exploitation.
* Transitioning to exchange-rate feeds ensures stable and accurate valuation until the market can be fully deprecated.
* The measured approach, risk-off first, gradual deprecation later, balances user protection with operational continuity.

### **Voting Options**

Yes: Disable mint/borrow for wrsETH on Base and OP Mainnet and transition to exchange-rate feeds
No: Maintain the status quo.
Abstain: No preference
Loading
Loading