Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
99 changes: 99 additions & 0 deletions proposals/mips/mip-b53/b53.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
## MIP-B53: WETH Market Ownership Wrapper

### **Summary**

This proposal deploys and migrates the WETH market admin to a new owner wrapper
contract that can reliably receive native ETH. This solves a critical issue
preventing the protocol from reducing WETH market reserves due to execution
context limitations when TEMPORAL_GOVERNOR receives ETH during proposal
execution.

### **Background**

The WETH market on Base uses an unwrapper contract that converts WETH to native
ETH when reducing reserves. While TEMPORAL_GOVERNOR has a receive() function,
there have been persistent issues when receiving ETH during proposal execution,
causing reserve reduction operations to fail.

This issue was previously identified in MIP-X37, which attempted to reduce 347
WETH in reserves but had to be reverted due to these ETH receiving problems. The
WETH reserves were also removed from MIP-B52 for the same reason.

### **Proposal**

This proposal implements a solution by deploying an MWethOwnerWrapper contract
that:

1. **Acts as the new admin for the WETH market** - The wrapper becomes the
admin, replacing TEMPORAL_GOVERNOR as the direct admin.

2. **Automatically wraps received ETH to WETH** - When the wrapper receives
native ETH (from reserve reductions), it automatically wraps it to WETH,
avoiding any receive() execution context issues.

3. **Delegates all admin functions** - The wrapper forwards all admin function
calls to the underlying WETH market, including:

- `_reduceReserves(uint256)` - Reduce reserves
- `_setPendingAdmin(address)` - Admin transfers
- `_setReserveFactor(uint256)` - Economic parameters
- `_setInterestRateModel(address)` - Rate model updates
- `_setProtocolSeizeShare(uint256)` - Protocol parameters
- `_addReserves(uint256)` - Reserve additions

4. **Remains controlled by TEMPORAL_GOVERNOR** - The wrapper itself is owned by
TEMPORAL_GOVERNOR, maintaining governance control.

5. **Enables token extraction** - The wrapper includes a `withdrawToken()`
function to extract WETH after reserve reductions, allowing the protocol to
use the funds.

### **Technical Implementation**

The proposal executes the following actions:

1. Deploy MWethOwnerWrapper implementation contract
2. Deploy TransparentUpgradeableProxy for the wrapper
3. Initialize the wrapper with:
- MOONWELL_WETH as the target mToken
- WETH token address
- TEMPORAL_GOVERNOR as the owner
4. Call `MOONWELL_WETH._setPendingAdmin(wrapperProxy)` to set the wrapper as
pending admin
5. Call `wrapperProxy._acceptAdmin()` to complete the admin transfer

### **Rationale**

This approach provides several benefits:

1. **Reliability** - The automatic ETH-to-WETH wrapping in the receive()
function ensures no execution context issues when receiving funds from the
unwrapper.

2. **Governance Control** - TEMPORAL_GOVERNOR remains in control by owning the
wrapper, maintaining the existing governance security model.

3. **Flexibility** - The wrapper delegates all admin functions, so future
parameter updates and reserve management can be performed normally.

4. **Upgradeability** - The wrapper uses the TransparentUpgradeableProxy
pattern, allowing future upgrades if needed (controlled by MRD_PROXY_ADMIN).

5. **Enables Future Reserve Reductions** - Once deployed, the protocol can
successfully reduce WETH reserves and use them for bad debt remediation or
other purposes.

### **Next Steps**

After this proposal passes, a follow-up proposal can be created to:

- Reduce WETH reserves (previously attempted in MIP-X37)
- Call `wrapperProxy.withdrawToken()` to extract the WETH
- Use the WETH for bad debt repayment or other protocol needs

### **Voting Options**

- **Yes** — Deploy the MWethOwnerWrapper and migrate WETH market admin to enable
reserve reductions.
- **No** — Reject the wrapper deployment; WETH reserves remain inaccessible.
- **Abstain** — No preference.
127 changes: 127 additions & 0 deletions proposals/mips/mip-b53/mip-b53.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity 0.8.19;

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

import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";

import {MTokenInterface} from "@protocol/MTokenInterfaces.sol";
import {MWethOwnerWrapper} from "@protocol/MWethOwnerWrapper.sol";

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

import {DeployMWethOwnerWrapper} from "@script/DeployMWethOwnerWrapper.s.sol";

/// @title MIP-B53: WETH Market Ownership Wrapper
/// @notice Proposal to deploy and migrate WETH market admin to a wrapper contract
/// that can reliably receive native ETH, enabling reserve reductions.
/// @dev This proposal:
/// 1. Deploys MWethOwnerWrapper implementation and proxy
/// 2. Transfers WETH market admin from TEMPORAL_GOVERNOR to the wrapper
/// 3. Wrapper is owned by TEMPORAL_GOVERNOR, maintaining governance control
/// 4. Enables future WETH reserve reductions via the wrapper
contract mipb53 is HybridProposal {
using ProposalActions for *;

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

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

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

function deploy(Addresses addresses, address) public override {
vm.selectFork(BASE_FORK_ID);

// Deploy the MWethOwnerWrapper implementation and proxy
DeployMWethOwnerWrapper deployer = new DeployMWethOwnerWrapper();
(TransparentUpgradeableProxy proxy, ) = deployer.deploy(addresses);

console.log("MWethOwnerWrapper deployed at:", address(proxy));
}

function build(Addresses addresses) public override {
vm.selectFork(BASE_FORK_ID);

address wrapperProxy = addresses.getAddress("MWETH_OWNER_WRAPPER");
address moonwellWeth = addresses.getAddress("MOONWELL_WETH");

// Step 1: Set the wrapper as pending admin of the WETH market
_pushAction(
moonwellWeth,
abi.encodeWithSignature(
"_setPendingAdmin(address)",
payable(wrapperProxy)
),
"Set MWethOwnerWrapper as pending admin of MOONWELL_WETH",
ActionType.Base
);

// Step 2: Accept admin role from the wrapper
_pushAction(
wrapperProxy,
abi.encodeWithSignature("_acceptAdmin()"),
"MWethOwnerWrapper accepts admin role for MOONWELL_WETH",
ActionType.Base
);
}

function validate(Addresses addresses, address) public override {
vm.selectFork(BASE_FORK_ID);

address wrapperProxy = addresses.getAddress("MWETH_OWNER_WRAPPER");
address moonwellWeth = addresses.getAddress("MOONWELL_WETH");
address temporalGovernor = addresses.getAddress("TEMPORAL_GOVERNOR");
address weth = addresses.getAddress("WETH");

// Validate wrapper configuration
MWethOwnerWrapper wrapper = MWethOwnerWrapper(payable(wrapperProxy));

assertEq(
wrapper.owner(),
temporalGovernor,
"Wrapper owner should be TEMPORAL_GOVERNOR"
);

assertEq(
address(wrapper.mToken()),
moonwellWeth,
"Wrapper mToken should be MOONWELL_WETH"
);

assertEq(
address(wrapper.weth()),
weth,
"Wrapper WETH address should be correct"
);

// Validate admin transfer
MTokenInterface mToken = MTokenInterface(moonwellWeth);

assertEq(
mToken.admin(),
wrapperProxy,
"MOONWELL_WETH admin should be the wrapper"
);

assertEq(
mToken.pendingAdmin(),
address(0),
"MOONWELL_WETH pendingAdmin should be zero after accepting"
);

console.log("✓ MWethOwnerWrapper successfully deployed and configured");
console.log("✓ MOONWELL_WETH admin transferred to wrapper");
console.log("✓ Wrapper owned by TEMPORAL_GOVERNOR");
console.log("✓ Future WETH reserve reductions now enabled");
}
}
7 changes: 7 additions & 0 deletions proposals/mips/mips.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
[
{
"envpath": "",
"governor": "MultichainGovernor",
"id": 0,
"path": "mip-b53.sol/mipb53.json",
"proposalType": "HybridProposal"
},
{
"envpath": "",
"governor": "MultichainGovernor",
Expand Down
127 changes: 127 additions & 0 deletions script/DeployMWethOwnerWrapper.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity 0.8.19;

import {Script} from "@forge-std/Script.sol";

import {TransparentUpgradeableProxy, ITransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol";

import {MWethOwnerWrapper} from "@protocol/MWethOwnerWrapper.sol";
import {AllChainAddresses as Addresses} from "@proposals/Addresses.sol";

/*
How to use:
forge script script/DeployMWethOwnerWrapper.s.sol:DeployMWethOwnerWrapper \
-vvvv \
--rpc-url base \
--broadcast

Remove --broadcast if you want to try locally first, without paying any gas.
*/

contract DeployMWethOwnerWrapper is Script {
function deploy(
Addresses addresses
) public returns (TransparentUpgradeableProxy, MWethOwnerWrapper) {
vm.startBroadcast();

// Deploy the implementation contract
MWethOwnerWrapper implementation = new MWethOwnerWrapper();

// Get required addresses
address proxyAdmin = addresses.getAddress("MRD_PROXY_ADMIN");
address mToken = addresses.getAddress("MOONWELL_WETH");
address weth = addresses.getAddress("WETH");
address temporalGovernor = addresses.getAddress("TEMPORAL_GOVERNOR");

// Prepare initialization data
bytes memory initData = abi.encodeWithSelector(
MWethOwnerWrapper.initialize.selector,
mToken,
weth,
temporalGovernor
);

// Deploy the TransparentUpgradeableProxy
TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(
address(implementation),
proxyAdmin,
initData
);

vm.stopBroadcast();

// Record deployed contracts
addresses.addAddress(
"MWETH_OWNER_WRAPPER_IMPL",
address(implementation)
);
addresses.addAddress("MWETH_OWNER_WRAPPER", address(proxy));

return (proxy, implementation);
}

function validate(
Addresses addresses,
TransparentUpgradeableProxy proxy,
MWethOwnerWrapper implementation
) public view {
// Get proxy admin contract
ProxyAdmin proxyAdmin = ProxyAdmin(
addresses.getAddress("MRD_PROXY_ADMIN")
);

// Validate proxy configuration
address actualImplementation = proxyAdmin.getProxyImplementation(
ITransparentUpgradeableProxy(address(proxy))
);
address actualProxyAdmin = proxyAdmin.getProxyAdmin(
ITransparentUpgradeableProxy(address(proxy))
);

require(
actualImplementation == address(implementation),
"DeployMWethOwnerWrapper: proxy implementation mismatch"
);

require(
actualProxyAdmin == address(proxyAdmin),
"DeployMWethOwnerWrapper: proxy admin mismatch"
);

// Validate wrapper configuration
MWethOwnerWrapper wrapperInstance = MWethOwnerWrapper(
payable(address(proxy))
);

require(
wrapperInstance.owner() ==
addresses.getAddress("TEMPORAL_GOVERNOR"),
"DeployMWethOwnerWrapper: owner mismatch"
);

require(
address(wrapperInstance.mToken()) ==
addresses.getAddress("MOONWELL_WETH"),
"DeployMWethOwnerWrapper: mToken address mismatch"
);

require(
address(wrapperInstance.weth()) == addresses.getAddress("WETH"),
"DeployMWethOwnerWrapper: weth address mismatch"
);
}

function run()
public
returns (TransparentUpgradeableProxy, MWethOwnerWrapper)
{
Addresses addresses = new Addresses();
(
TransparentUpgradeableProxy proxy,
MWethOwnerWrapper implementation
) = deploy(addresses);
validate(addresses, proxy, implementation);
return (proxy, implementation);
}
}
Loading
Loading