diff --git a/contracts/contracts/automation/ClaimMorphoRewardsModule.sol b/contracts/contracts/automation/ClaimMorphoRewardsModule.sol new file mode 100644 index 0000000000..b2c71c23a6 --- /dev/null +++ b/contracts/contracts/automation/ClaimMorphoRewardsModule.sol @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import { AbstractSafeModule } from "./AbstractSafeModule.sol"; +import { IStrategy } from "../interfaces/IStrategy.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +interface IMorphoWrapper { + function depositFor(address recipient, uint256 amount) external; +} + +contract ClaimMorphoRewardsModule is AbstractSafeModule { + mapping(address => bool) public isStrategyWhitelisted; + address[] public strategies; + + IERC20 public constant LEGACY_MORPHO = + IERC20(0x9994E35Db50125E0DF82e4c2dde62496CE330999); + IERC20 public constant MORPHO = + IERC20(0x58D97B57BB95320F9a05dC918Aef65434969c2B2); + IMorphoWrapper public constant MORPHO_WRAPPER = + IMorphoWrapper(0x9D03bb2092270648d7480049d0E58d2FcF0E5123); + + constructor( + address _safeContract, + address operator, + address[] memory _strategies + ) AbstractSafeModule(_safeContract) { + _grantRole(OPERATOR_ROLE, operator); + for (uint256 i = 0; i < _strategies.length; i++) { + _addStrategy(_strategies[i]); + } + } + + function claimRewards() external onlyOperator { + for (uint256 i = 0; i < strategies.length; i++) { + address strategy = strategies[i]; + + uint256 morphoBalance = MORPHO.balanceOf(strategy); + if (morphoBalance > 0) { + // Transfer Morpho to the safe contract + bool success = safeContract.execTransactionFromModule( + strategy, + 0, + abi.encodeWithSelector( + IStrategy.transferToken.selector, + address(MORPHO), + morphoBalance + ), + 0 + ); + require(success, "Failed to transfer Morpho"); + } + + uint256 legacyMorphoBalance = LEGACY_MORPHO.balanceOf(strategy); + if (legacyMorphoBalance > 0) { + // Transfer Legacy Morpho to the safe contract + // slither-disable-next-line unused-return + safeContract.execTransactionFromModule( + strategy, + 0, + abi.encodeWithSelector( + IStrategy.transferToken.selector, + address(LEGACY_MORPHO), + legacyMorphoBalance + ), + 0 + ); + + // Wrap Legacy Morpho into Morpho + _wrapLegacyMorpho(); + } + } + } + + function addStrategy(address strategy) external onlySafe { + _addStrategy(strategy); + } + + function _addStrategy(address strategy) internal { + require( + !isStrategyWhitelisted[strategy], + "Strategy already whitelisted" + ); + + isStrategyWhitelisted[strategy] = true; + strategies.push(strategy); + } + + function removeStrategy(address strategy) external onlySafe { + require(isStrategyWhitelisted[strategy], "Strategy not whitelisted"); + + isStrategyWhitelisted[strategy] = false; + for (uint256 i = 0; i < strategies.length; i++) { + if (strategies[i] == strategy) { + strategies[i] = strategies[strategies.length - 1]; + strategies.pop(); + break; + } + } + } + + function wrapLegacyMorpho() external onlyOperator { + _wrapLegacyMorpho(); + } + + function _wrapLegacyMorpho() internal { + uint256 legacyMorphoBalance = LEGACY_MORPHO.balanceOf( + address(safeContract) + ); + + if (legacyMorphoBalance == 0) { + // Nothing to wrap + return; + } + + // Approve Morpho Wrapper to move the tokens + bool success = safeContract.execTransactionFromModule( + address(LEGACY_MORPHO), + 0, + abi.encodeWithSelector( + LEGACY_MORPHO.approve.selector, + address(MORPHO_WRAPPER), + legacyMorphoBalance + ), + 0 + ); + require(success, "Failed to approve Morpho Wrapper"); + + // Wrap the tokens + success = safeContract.execTransactionFromModule( + address(MORPHO_WRAPPER), + 0, + abi.encodeWithSelector( + MORPHO_WRAPPER.depositFor.selector, + address(safeContract), + legacyMorphoBalance + ), + 0 + ); + require(success, "Failed to wrap Morpho"); + } +} diff --git a/contracts/contracts/interfaces/IStrategy.sol b/contracts/contracts/interfaces/IStrategy.sol index 2c61e143e7..ad7b1ec6e7 100644 --- a/contracts/contracts/interfaces/IStrategy.sol +++ b/contracts/contracts/interfaces/IStrategy.sol @@ -56,4 +56,6 @@ interface IStrategy { function getRewardTokenAddresses() external view returns (address[] memory); function harvesterAddress() external view returns (address); + + function transferToken(address token, uint256 amount) external; } diff --git a/contracts/deploy/mainnet/146_morpho_governor_change.js b/contracts/deploy/mainnet/146_morpho_governor_change.js new file mode 100644 index 0000000000..2bcb01108e --- /dev/null +++ b/contracts/deploy/mainnet/146_morpho_governor_change.js @@ -0,0 +1,45 @@ +const { deploymentWithGovernanceProposal } = require("../../utils/deploy"); +const addresses = require("../../utils/addresses"); + +module.exports = deploymentWithGovernanceProposal( + { + deployName: "146_morpho_governor_change", + forceDeploy: false, + reduceQueueTime: true, + proposalId: "", + }, + async () => { + const cGauntletUSDCStrategyProxy = await ethers.getContract( + "MorphoGauntletPrimeUSDCStrategyProxy" + ); + + const cGauntletUSDTStrategyProxy = await ethers.getContract( + "MorphoGauntletPrimeUSDTStrategyProxy" + ); + + const cMetaMorphoStrategyProxy = await ethers.getContract( + "MetaMorphoStrategyProxy" + ); + + return { + name: "Transfer governorship for Morpho Strategies to the Multi-chain Guardian", + actions: [ + { + contract: cGauntletUSDCStrategyProxy, + signature: "transferGovernance(address)", + args: [addresses.multichainStrategist], + }, + { + contract: cGauntletUSDTStrategyProxy, + signature: "transferGovernance(address)", + args: [addresses.multichainStrategist], + }, + { + contract: cMetaMorphoStrategyProxy, + signature: "transferGovernance(address)", + args: [addresses.multichainStrategist], + }, + ], + }; + } +); diff --git a/contracts/deploy/mainnet/147_morpho_rewards_module.js b/contracts/deploy/mainnet/147_morpho_rewards_module.js new file mode 100644 index 0000000000..8bf165eb18 --- /dev/null +++ b/contracts/deploy/mainnet/147_morpho_rewards_module.js @@ -0,0 +1,46 @@ +const { deploymentWithGovernanceProposal } = require("../../utils/deploy"); +const addresses = require("../../utils/addresses"); + +module.exports = deploymentWithGovernanceProposal( + { + deployName: "147_morpho_rewards_module", + forceDeploy: false, + reduceQueueTime: true, + proposalId: "", + }, + async ({ deployWithConfirmation }) => { + const cGauntletUSDCStrategyProxy = await ethers.getContract( + "MorphoGauntletPrimeUSDCStrategyProxy" + ); + const cGauntletUSDTStrategyProxy = await ethers.getContract( + "MorphoGauntletPrimeUSDTStrategyProxy" + ); + const cMetaMorphoStrategyProxy = await ethers.getContract( + "MetaMorphoStrategyProxy" + ); + + await deployWithConfirmation("ClaimMorphoRewardsModule", [ + addresses.multichainStrategist, + // Defender Relayer + "0x4b91827516f79d6F6a1F292eD99671663b09169a", + [ + cGauntletUSDCStrategyProxy.address, + cGauntletUSDTStrategyProxy.address, + cMetaMorphoStrategyProxy.address, + ], + ]); + + const cClaimMorphoRewardsModule = await ethers.getContract( + "ClaimMorphoRewardsModule" + ); + + console.log( + "ClaimMorphoRewardsModule deployed to", + cClaimMorphoRewardsModule.address + ); + + return { + actions: [], + }; + } +); diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index 51b3dd3fd0..927b4603cc 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -757,6 +757,8 @@ const defaultFixture = deployments.createFixture(async () => { threePoolToken, metapoolToken, morpho, + morphoToken, + legacyMorphoToken, morphoCompoundStrategy, balancerREthStrategy, makerSSRStrategy, @@ -816,6 +818,14 @@ const defaultFixture = deployments.createFixture(async () => { metamorphoAbi, addresses.mainnet.MorphoGauntletPrimeUSDTVault ); + morphoToken = await ethers.getContractAt( + erc20Abi, + addresses.mainnet.MorphoToken + ); + legacyMorphoToken = await ethers.getContractAt( + erc20Abi, + addresses.mainnet.LegacyMorphoToken + ); aura = await ethers.getContractAt(erc20Abi, addresses.mainnet.AURA); bal = await ethers.getContractAt(erc20Abi, addresses.mainnet.BAL); ogv = await ethers.getContractAt(erc20Abi, addresses.mainnet.OGV); @@ -1139,6 +1149,9 @@ const defaultFixture = deployments.createFixture(async () => { mock1InchSwapRouter, aura, bal, + + morphoToken, + legacyMorphoToken, }; }); @@ -1331,6 +1344,60 @@ async function bridgeHelperModuleFixture() { }; } +async function morphoCollectorModuleFixture() { + const fixture = await defaultFixture(); + + const safeSigner = await impersonateAndFund(addresses.multichainStrategist); + safeSigner.address = addresses.multichainStrategist; + + const morphoCollectorModule = await ethers.getContract( + "ClaimMorphoRewardsModule" + ); + + const cSafe = await ethers.getContractAt( + [ + "function enableModule(address module) external", + "function isModuleEnabled(address module) external view returns (bool)", + ], + addresses.multichainStrategist + ); + + if (isFork) { + // Enable module if not already enabled + if (!(await cSafe.isModuleEnabled(morphoCollectorModule.address))) { + await cSafe + .connect(safeSigner) + .enableModule(morphoCollectorModule.address); + } + + // Claim governance for Morpho Strategies + const cGauntletUSDCStrategyProxy = await ethers.getContract( + "MorphoGauntletPrimeUSDCStrategyProxy" + ); + const cGauntletUSDTStrategyProxy = await ethers.getContract( + "MorphoGauntletPrimeUSDTStrategyProxy" + ); + const cMetaMorphoStrategyProxy = await ethers.getContract( + "MetaMorphoStrategyProxy" + ); + + const currentGovernor = await cGauntletUSDCStrategyProxy.governor(); + if ( + currentGovernor.toLowerCase() !== + addresses.multichainStrategist.toLowerCase() + ) { + await cGauntletUSDCStrategyProxy.connect(safeSigner).claimGovernance(); + await cGauntletUSDTStrategyProxy.connect(safeSigner).claimGovernance(); + await cMetaMorphoStrategyProxy.connect(safeSigner).claimGovernance(); + } + } + + fixture.morphoCollectorModule = morphoCollectorModule; + fixture.safeSigner = safeSigner; + + return fixture; +} + /** * Configure a Vault with only the Compound strategy. */ @@ -2604,4 +2671,5 @@ module.exports = { nodeRevert, woethCcipZapperFixture, bridgeHelperModuleFixture, + morphoCollectorModuleFixture, }; diff --git a/contracts/test/safe-modules/morpho-collector.mainnet.fork-test.js b/contracts/test/safe-modules/morpho-collector.mainnet.fork-test.js new file mode 100644 index 0000000000..25a92194e2 --- /dev/null +++ b/contracts/test/safe-modules/morpho-collector.mainnet.fork-test.js @@ -0,0 +1,25 @@ +const { + createFixtureLoader, + morphoCollectorModuleFixture, +} = require("../_fixture"); +const { expect } = require("chai"); + +const loadFixture = createFixtureLoader(morphoCollectorModuleFixture); + +describe("ForkTest: Morpho Collector Safe Module", function () { + let fixture; + beforeEach(async () => { + fixture = await loadFixture(); + }); + + it("Should claim Morpho rewards", async () => { + const { morphoCollectorModule, safeSigner, morphoToken } = fixture; + + const morphoBalance = await morphoToken.balanceOf(safeSigner.address); + + await morphoCollectorModule.connect(safeSigner).claimRewards(); + + const morphoBalanceAfter = await morphoToken.balanceOf(safeSigner.address); + expect(morphoBalanceAfter).to.gt(morphoBalance); + }); +}); diff --git a/contracts/utils/addresses.js b/contracts/utils/addresses.js index e04ffed82b..f63b49a10a 100644 --- a/contracts/utils/addresses.js +++ b/contracts/utils/addresses.js @@ -205,6 +205,9 @@ addresses.mainnet.Flipper = "0xcecaD69d7D4Ed6D52eFcFA028aF8732F27e08F70"; // Morpho addresses.mainnet.Morpho = "0x8888882f8f843896699869179fB6E4f7e3B58888"; addresses.mainnet.MorphoLens = "0x930f1b46e1d081ec1524efd95752be3ece51ef67"; +addresses.mainnet.MorphoToken = "0x58D97B57BB95320F9a05dC918Aef65434969c2B2"; +addresses.mainnet.LegacyMorphoToken = + "0x9994E35Db50125E0DF82e4c2dde62496CE330999"; // Governance addresses.mainnet.Timelock = "0x35918cDE7233F2dD33fA41ae3Cb6aE0e42E0e69F";