Skip to content

[WIP] Add Morpho rewards collector module #2574

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
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
142 changes: 142 additions & 0 deletions contracts/contracts/automation/ClaimMorphoRewardsModule.sol
Original file line number Diff line number Diff line change
@@ -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);

Check warning on line 76 in contracts/contracts/automation/ClaimMorphoRewardsModule.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/automation/ClaimMorphoRewardsModule.sol#L76

Added line #L76 was not covered by tests
}

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++) {

Check warning on line 93 in contracts/contracts/automation/ClaimMorphoRewardsModule.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/automation/ClaimMorphoRewardsModule.sol#L92-L93

Added lines #L92 - L93 were not covered by tests
if (strategies[i] == strategy) {
strategies[i] = strategies[strategies.length - 1];
strategies.pop();
break;

Check warning on line 97 in contracts/contracts/automation/ClaimMorphoRewardsModule.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/automation/ClaimMorphoRewardsModule.sol#L95-L97

Added lines #L95 - L97 were not covered by tests
}
}
}

function wrapLegacyMorpho() external onlyOperator {
_wrapLegacyMorpho();

Check warning on line 103 in contracts/contracts/automation/ClaimMorphoRewardsModule.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/automation/ClaimMorphoRewardsModule.sol#L103

Added line #L103 was not covered by tests
}

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(

Check warning on line 117 in contracts/contracts/automation/ClaimMorphoRewardsModule.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/automation/ClaimMorphoRewardsModule.sol#L117

Added line #L117 was not covered by tests
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(

Check warning on line 130 in contracts/contracts/automation/ClaimMorphoRewardsModule.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/automation/ClaimMorphoRewardsModule.sol#L130

Added line #L130 was not covered by tests
address(MORPHO_WRAPPER),
0,
abi.encodeWithSelector(
MORPHO_WRAPPER.depositFor.selector,
address(safeContract),
legacyMorphoBalance
),
0
);
require(success, "Failed to wrap Morpho");
}
}
2 changes: 2 additions & 0 deletions contracts/contracts/interfaces/IStrategy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
45 changes: 45 additions & 0 deletions contracts/deploy/mainnet/146_morpho_governor_change.js
Original file line number Diff line number Diff line change
@@ -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],
},
],
};
}
);
46 changes: 46 additions & 0 deletions contracts/deploy/mainnet/147_morpho_rewards_module.js
Original file line number Diff line number Diff line change
@@ -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: [],
};
}
);
68 changes: 68 additions & 0 deletions contracts/test/_fixture.js
Original file line number Diff line number Diff line change
Expand Up @@ -757,6 +757,8 @@ const defaultFixture = deployments.createFixture(async () => {
threePoolToken,
metapoolToken,
morpho,
morphoToken,
legacyMorphoToken,
morphoCompoundStrategy,
balancerREthStrategy,
makerSSRStrategy,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -1139,6 +1149,9 @@ const defaultFixture = deployments.createFixture(async () => {
mock1InchSwapRouter,
aura,
bal,

morphoToken,
legacyMorphoToken,
};
});

Expand Down Expand Up @@ -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.
*/
Expand Down Expand Up @@ -2604,4 +2671,5 @@ module.exports = {
nodeRevert,
woethCcipZapperFixture,
bridgeHelperModuleFixture,
morphoCollectorModuleFixture,
};
Original file line number Diff line number Diff line change
@@ -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);
});
});
3 changes: 3 additions & 0 deletions contracts/utils/addresses.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Loading