|
| 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