Skip to content

Commit 4c9b186

Browse files
committed
add owner wrapper contract
1 parent de3175a commit 4c9b186

File tree

2 files changed

+359
-0
lines changed

2 files changed

+359
-0
lines changed
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
// SPDX-License-Identifier: GPL-3.0-or-later
2+
pragma solidity 0.8.19;
3+
4+
import {Script} from "@forge-std/Script.sol";
5+
6+
import {TransparentUpgradeableProxy, ITransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
7+
import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol";
8+
9+
import {MWethOwnerWrapper} from "@protocol/MWethOwnerWrapper.sol";
10+
import {AllChainAddresses as Addresses} from "@proposals/Addresses.sol";
11+
12+
/*
13+
How to use:
14+
forge script script/DeployMWethOwnerWrapper.s.sol:DeployMWethOwnerWrapper \
15+
-vvvv \
16+
--rpc-url base \
17+
--broadcast
18+
19+
Remove --broadcast if you want to try locally first, without paying any gas.
20+
*/
21+
22+
contract DeployMWethOwnerWrapper is Script {
23+
function deploy(
24+
Addresses addresses
25+
) public returns (TransparentUpgradeableProxy, MWethOwnerWrapper) {
26+
vm.startBroadcast();
27+
28+
// Deploy the implementation contract
29+
MWethOwnerWrapper implementation = new MWethOwnerWrapper();
30+
31+
// Get required addresses
32+
address proxyAdmin = addresses.getAddress("MRD_PROXY_ADMIN");
33+
address mToken = addresses.getAddress("MOONWELL_WETH");
34+
address weth = addresses.getAddress("WETH");
35+
address temporalGovernor = addresses.getAddress("TEMPORAL_GOVERNOR");
36+
37+
// Prepare initialization data
38+
bytes memory initData = abi.encodeWithSelector(
39+
MWethOwnerWrapper.initialize.selector,
40+
mToken,
41+
weth,
42+
temporalGovernor
43+
);
44+
45+
// Deploy the TransparentUpgradeableProxy
46+
TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(
47+
address(implementation),
48+
proxyAdmin,
49+
initData
50+
);
51+
52+
vm.stopBroadcast();
53+
54+
// Record deployed contracts
55+
addresses.addAddress(
56+
"MWETH_OWNER_WRAPPER_IMPL",
57+
address(implementation)
58+
);
59+
addresses.addAddress("MWETH_OWNER_WRAPPER", address(proxy));
60+
61+
return (proxy, implementation);
62+
}
63+
64+
function validate(
65+
Addresses addresses,
66+
TransparentUpgradeableProxy proxy,
67+
MWethOwnerWrapper implementation
68+
) public view {
69+
// Get proxy admin contract
70+
ProxyAdmin proxyAdmin = ProxyAdmin(
71+
addresses.getAddress("MRD_PROXY_ADMIN")
72+
);
73+
74+
// Validate proxy configuration
75+
address actualImplementation = proxyAdmin.getProxyImplementation(
76+
ITransparentUpgradeableProxy(address(proxy))
77+
);
78+
address actualProxyAdmin = proxyAdmin.getProxyAdmin(
79+
ITransparentUpgradeableProxy(address(proxy))
80+
);
81+
82+
require(
83+
actualImplementation == address(implementation),
84+
"DeployMWethOwnerWrapper: proxy implementation mismatch"
85+
);
86+
87+
require(
88+
actualProxyAdmin == address(proxyAdmin),
89+
"DeployMWethOwnerWrapper: proxy admin mismatch"
90+
);
91+
92+
// Validate wrapper configuration
93+
MWethOwnerWrapper wrapperInstance = MWethOwnerWrapper(
94+
payable(address(proxy))
95+
);
96+
97+
require(
98+
wrapperInstance.owner() ==
99+
addresses.getAddress("TEMPORAL_GOVERNOR"),
100+
"DeployMWethOwnerWrapper: owner mismatch"
101+
);
102+
103+
require(
104+
address(wrapperInstance.mToken()) ==
105+
addresses.getAddress("MOONWELL_WETH"),
106+
"DeployMWethOwnerWrapper: mToken address mismatch"
107+
);
108+
109+
require(
110+
address(wrapperInstance.weth()) == addresses.getAddress("WETH"),
111+
"DeployMWethOwnerWrapper: weth address mismatch"
112+
);
113+
}
114+
115+
function run()
116+
public
117+
returns (TransparentUpgradeableProxy, MWethOwnerWrapper)
118+
{
119+
Addresses addresses = new Addresses();
120+
(
121+
TransparentUpgradeableProxy proxy,
122+
MWethOwnerWrapper implementation
123+
) = deploy(addresses);
124+
validate(addresses, proxy, implementation);
125+
return (proxy, implementation);
126+
}
127+
}

src/MWethOwnerWrapper.sol

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
// SPDX-License-Identifier: BSD-3-Clause
2+
pragma solidity 0.8.19;
3+
4+
import {WETH9} from "@protocol/router/IWETH.sol";
5+
import {MTokenInterface} from "@protocol/MTokenInterfaces.sol";
6+
import {MErc20Interface} from "@protocol/MTokenInterfaces.sol";
7+
import {InterestRateModel} from "@protocol/irm/InterestRateModel.sol";
8+
import {ComptrollerInterface} from "@protocol/ComptrollerInterface.sol";
9+
import "@openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol";
10+
import "@openzeppelin-contracts-upgradeable/contracts/access/OwnableUpgradeable.sol";
11+
12+
/**
13+
* @title MWethOwnerWrapper
14+
* @notice A wrapper contract that acts as the admin for the WETH market,
15+
* enabling it to receive native ETH through the WETH unwrapping process.
16+
* This solves the issue where TEMPORAL_GOVERNOR cannot reliably receive ETH
17+
* during proposal execution when reducing WETH market reserves.
18+
*
19+
* @dev This contract:
20+
* - Automatically wraps any received ETH into WETH
21+
* - Delegates all admin functions to the underlying WETH market
22+
* - Is owned by TEMPORAL_GOVERNOR for governance control
23+
* - Allows extracting tokens (primarily WETH) after reserve reductions
24+
*/
25+
contract MWethOwnerWrapper is Initializable, OwnableUpgradeable {
26+
/// @notice The WETH market this wrapper administers
27+
MTokenInterface public mToken;
28+
29+
/// @notice The WETH token contract
30+
WETH9 public weth;
31+
32+
/// @notice Emitted when ETH is received and wrapped to WETH
33+
event EthWrapped(uint256 amount);
34+
35+
/// @notice Emitted when tokens are withdrawn from the wrapper
36+
event TokenWithdrawn(
37+
address indexed token,
38+
address indexed to,
39+
uint256 amount
40+
);
41+
42+
/// @custom:oz-upgrades-unsafe-allow constructor
43+
constructor() {
44+
_disableInitializers();
45+
}
46+
47+
/**
48+
* @notice Initialize the wrapper contract
49+
* @param _mToken Address of the WETH market to administer
50+
* @param _weth Address of the WETH token contract
51+
* @param _owner Address that will own this contract (should be TEMPORAL_GOVERNOR)
52+
*/
53+
function initialize(
54+
address _mToken,
55+
address _weth,
56+
address _owner
57+
) public initializer {
58+
require(
59+
_mToken != address(0),
60+
"MWethOwnerWrapper: mToken cannot be zero address"
61+
);
62+
require(
63+
_weth != address(0),
64+
"MWethOwnerWrapper: weth cannot be zero address"
65+
);
66+
require(
67+
_owner != address(0),
68+
"MWethOwnerWrapper: owner cannot be zero address"
69+
);
70+
71+
__Ownable_init();
72+
73+
mToken = MTokenInterface(_mToken);
74+
weth = WETH9(_weth);
75+
_transferOwnership(_owner);
76+
}
77+
78+
/**
79+
* @notice Fallback function to receive ETH and automatically wrap it to WETH
80+
* @dev This is critical for receiving ETH from the WETH unwrapper during reserve reductions
81+
*/
82+
receive() external payable {
83+
if (msg.value > 0) {
84+
weth.deposit{value: msg.value}();
85+
emit EthWrapped(msg.value);
86+
}
87+
}
88+
89+
// ========================================
90+
// Admin Functions - Delegate to MToken
91+
// ========================================
92+
93+
/**
94+
* @notice Reduce reserves of the WETH market
95+
* @dev The reduced reserves will be sent to this wrapper as ETH, then auto-wrapped to WETH
96+
* @param reduceAmount The amount of reserves to reduce
97+
* @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details)
98+
*/
99+
function _reduceReserves(
100+
uint256 reduceAmount
101+
) external onlyOwner returns (uint) {
102+
return mToken._reduceReserves(reduceAmount);
103+
}
104+
105+
/**
106+
* @notice Set pending admin of the WETH market
107+
* @param newPendingAdmin The new pending admin address
108+
* @return uint 0=success, otherwise a failure
109+
*/
110+
function _setPendingAdmin(
111+
address payable newPendingAdmin
112+
) external onlyOwner returns (uint) {
113+
return mToken._setPendingAdmin(newPendingAdmin);
114+
}
115+
116+
/**
117+
* @notice Accept admin role for the WETH market
118+
* @dev Call this after the market's current admin has called _setPendingAdmin
119+
* @return uint 0=success, otherwise a failure
120+
*/
121+
function _acceptAdmin() external onlyOwner returns (uint) {
122+
return mToken._acceptAdmin();
123+
}
124+
125+
/**
126+
* @notice Set the comptroller for the WETH market
127+
* @param newComptroller The new comptroller address
128+
* @return uint 0=success, otherwise a failure
129+
*/
130+
function _setComptroller(
131+
ComptrollerInterface newComptroller
132+
) external onlyOwner returns (uint) {
133+
return mToken._setComptroller(newComptroller);
134+
}
135+
136+
/**
137+
* @notice Set the reserve factor for the WETH market
138+
* @param newReserveFactorMantissa The new reserve factor (scaled by 1e18)
139+
* @return uint 0=success, otherwise a failure
140+
*/
141+
function _setReserveFactor(
142+
uint256 newReserveFactorMantissa
143+
) external onlyOwner returns (uint) {
144+
return mToken._setReserveFactor(newReserveFactorMantissa);
145+
}
146+
147+
/**
148+
* @notice Set the interest rate model for the WETH market
149+
* @param newInterestRateModel The new interest rate model address
150+
* @return uint 0=success, otherwise a failure
151+
*/
152+
function _setInterestRateModel(
153+
InterestRateModel newInterestRateModel
154+
) external onlyOwner returns (uint) {
155+
return mToken._setInterestRateModel(newInterestRateModel);
156+
}
157+
158+
/**
159+
* @notice Set the protocol seize share for the WETH market
160+
* @param newProtocolSeizeShareMantissa The new protocol seize share (scaled by 1e18)
161+
* @return uint 0=success, otherwise a failure
162+
*/
163+
function _setProtocolSeizeShare(
164+
uint256 newProtocolSeizeShareMantissa
165+
) external onlyOwner returns (uint) {
166+
return mToken._setProtocolSeizeShare(newProtocolSeizeShareMantissa);
167+
}
168+
169+
/**
170+
* @notice Add reserves to the WETH market
171+
* @param addAmount The amount of reserves to add
172+
* @return uint 0=success, otherwise a failure
173+
*/
174+
function _addReserves(uint256 addAmount) external onlyOwner returns (uint) {
175+
// First approve the mToken to spend WETH from this wrapper
176+
require(
177+
weth.approve(address(mToken), addAmount),
178+
"MWethOwnerWrapper: WETH approval failed"
179+
);
180+
return MErc20Interface(address(mToken))._addReserves(addAmount);
181+
}
182+
183+
// ========================================
184+
// Token Management Functions
185+
// ========================================
186+
187+
/**
188+
* @notice Withdraw ERC20 tokens from this wrapper
189+
* @dev Primarily used to extract WETH after reserve reductions
190+
* @param token The token address to withdraw
191+
* @param to The recipient address
192+
* @param amount The amount to withdraw
193+
*/
194+
function withdrawToken(
195+
address token,
196+
address to,
197+
uint256 amount
198+
) external onlyOwner {
199+
require(
200+
to != address(0),
201+
"MWethOwnerWrapper: cannot withdraw to zero address"
202+
);
203+
require(
204+
amount > 0,
205+
"MWethOwnerWrapper: amount must be greater than zero"
206+
);
207+
208+
require(
209+
WETH9(token).transfer(to, amount),
210+
"MWethOwnerWrapper: token transfer failed"
211+
);
212+
213+
emit TokenWithdrawn(token, to, amount);
214+
}
215+
216+
/**
217+
* @notice Get the balance of a token held by this wrapper
218+
* @param token The token address to query
219+
* @return The balance of the token
220+
*/
221+
function getTokenBalance(address token) external view returns (uint256) {
222+
return WETH9(token).balanceOf(address(this));
223+
}
224+
225+
/**
226+
* @notice Get the ETH balance held by this wrapper
227+
* @return The ETH balance
228+
*/
229+
function getEthBalance() external view returns (uint256) {
230+
return address(this).balance;
231+
}
232+
}

0 commit comments

Comments
 (0)