Skip to content
Open
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
13 changes: 13 additions & 0 deletions contracts/contracts/interfaces/IMerkl.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;

interface IDistributor {
event Claimed(address indexed user, address indexed token, uint256 amount);

function claim(
address[] calldata users,
address[] calldata tokens,
uint256[] calldata amounts,
bytes32[][] calldata proofs
) external;
}
33 changes: 33 additions & 0 deletions contracts/contracts/strategies/Generalized4626Strategy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,13 @@ pragma solidity ^0.8.0;
*/
import { IERC4626 } from "../../lib/openzeppelin/interfaces/IERC4626.sol";
import { IERC20, InitializableAbstractStrategy } from "../utils/InitializableAbstractStrategy.sol";
import { IDistributor } from "../interfaces/IMerkl.sol";

contract Generalized4626Strategy is InitializableAbstractStrategy {
/// @notice The address of the Merkle Distributor contract.
IDistributor public constant merkleDistributor =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels to me that all these changes should fit into a new contract smth like 4626MerklStrategy.sol. There might be strategies where Generalized4626 would be used (e.g. for Yearn 3 vaults) that won't be using the Merkl rewards distribution.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's what I originally did but then realised Generalized4626USDTStrategy would also have to change it's inheritance. Given the addition was small, I decided to just add merkleClaim to Generalized4626Strategy

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

got it 👍

IDistributor(0x3Ef3D8bA38EBe18DB133cEc108f4D14CE00Dd9Ae);

/// @dev Replaced with an immutable variable
// slither-disable-next-line constable-states
address private _deprecate_shareToken;
Expand All @@ -23,6 +28,8 @@ contract Generalized4626Strategy is InitializableAbstractStrategy {
// For future use
uint256[50] private __gap;

event ClaimedRewards(address indexed token, uint256 amount);

/**
* @param _baseConfig Base strategy config with platformAddress (ERC-4626 Vault contract), eg sfrxETH or sDAI,
* and vaultAddress (OToken Vault contract), eg VaultProxy or OETHVaultProxy
Expand Down Expand Up @@ -210,4 +217,30 @@ contract Generalized4626Strategy is InitializableAbstractStrategy {
function removePToken(uint256) external override onlyGovernor {
revert("unsupported function");
}

/// @notice Claim tokens from the Merkle Distributor
/// @param token The address of the token to claim.
/// @param amount The amount of tokens to claim.
/// @param proof The Merkle proof to validate the claim.
function merkleClaim(
address token,
uint256 amount,
bytes32[] calldata proof
) external {
address[] memory users = new address[](1);
users[0] = address(this);

address[] memory tokens = new address[](1);
tokens[0] = token;

uint256[] memory amounts = new uint256[](1);
amounts[0] = amount;

bytes32[][] memory proofs = new bytes32[][](1);
proofs[0] = proof;

merkleDistributor.claim(users, tokens, amounts, proofs);

emit ClaimedRewards(token, amount);
}
}
157 changes: 157 additions & 0 deletions contracts/deploy/mainnet/160_upgrade_morpho_strategies.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
const addresses = require("../../utils/addresses");
const { deploymentWithGovernanceProposal } = require("../../utils/deploy");

module.exports = deploymentWithGovernanceProposal(
{
deployName: "160_upgrade_morpho_strategies",
forceDeploy: false,
// forceSkip: true,
// reduceQueueTime: true,
deployerIsProposer: false,
// proposalId: "",
},
async ({ deployWithConfirmation, getTxOpts, withConfirmation }) => {
// Current OUSD Vault contracts
const cVaultProxy = await ethers.getContract("VaultProxy");
const cVaultAdmin = await ethers.getContractAt(
"VaultAdmin",
cVaultProxy.address
);
const dMorphoSteakhouseUSDCStrategyProxy = await ethers.getContract(
"MetaMorphoStrategyProxy"
);
const dMorphoGauntletPrimeUSDCStrategyProxy = await ethers.getContract(
"MorphoGauntletPrimeUSDCStrategyProxy"
);
const dMorphoGauntletPrimeUSDTStrategyProxy = await ethers.getContract(
"MorphoGauntletPrimeUSDTStrategyProxy"
);

// Deployer Actions
// ----------------

// Fix the signer to the deployer of the Morpho OUSD v2 strategy proxy
const sDeployer = await ethers.provider.getSigner(
"0x58890A9cB27586E83Cb51d2d26bbE18a1a647245"
);

// 1. Deploy new contract for Morpho Steakhouse USDC
const dMorphoSteakhouseUSDCStrategyImpl = await deployWithConfirmation(
"Generalized4626Strategy",
[
[addresses.mainnet.MorphoSteakhouseUSDCVault, cVaultProxy.address],
addresses.mainnet.USDC,
]
);

// 2. Deploy new contract for Morpho Gauntlet Prime USDC
const dMorphoGauntletPrimeUSDCStrategyImpl = await deployWithConfirmation(
"Generalized4626Strategy",
[
[addresses.mainnet.MorphoGauntletPrimeUSDCVault, cVaultProxy.address],
addresses.mainnet.USDC,
]
);

// 3. Deploy new contract for Morpho Gauntlet Prime USDT
const dMorphoGauntletPrimeUSDTStrategyImpl = await deployWithConfirmation(
"Generalized4626USDTStrategy",
[
[addresses.mainnet.MorphoGauntletPrimeUSDTVault, cVaultProxy.address],
addresses.mainnet.USDT,
]
);

// 4. Get previously deployed proxy to Morpho OUSD v2 strategy
const cOUSDMorphoV2StrategyProxy = await ethers.getContract(
"OUSDMorphoV2StrategyProxy"
);

// 5. Deploy new strategy for the Morpho Yearn OUSD V2 Vault
const dOUSDMorphoV2StrategyImpl = await deployWithConfirmation(
"Generalized4626Strategy",
[
[addresses.mainnet.MorphoOUSDv2Vault, cVaultProxy.address],
addresses.mainnet.USDC,
]
);
const cOUSDMorphoV2Strategy = await ethers.getContractAt(
"Generalized4626Strategy",
cOUSDMorphoV2StrategyProxy.address
);

// 6. Construct initialize call data to initialize and configure the new strategy
const initData = cOUSDMorphoV2Strategy.interface.encodeFunctionData(
"initialize()",
[]
);

// 7. Init the proxy to point at the implementation, set the governor, and call initialize
const initFunction = "initialize(address,address,bytes)";
await withConfirmation(
cOUSDMorphoV2StrategyProxy.connect(sDeployer)[initFunction](
dOUSDMorphoV2StrategyImpl.address,
addresses.mainnet.Timelock, // governor
initData, // data for delegate call to the initialize function on the strategy
await getTxOpts()
)
);

// Governance Actions
// ----------------
return {
name: `Upgrade Morpho Steakhouse and Gauntlet Prime Strategies to claim MORPHO rewards from Merkl.
Remove the Morpho Steakhouse Strategy.
Set the Morpho Gauntlet Prime USDC strategy as the default for USDC
Add new Morpho OUSD v2 Strategy and deposit 10k USDC.
`,
actions: [
{
// 1. Upgrade Morpho Steakhouse USDC Strategy
contract: dMorphoSteakhouseUSDCStrategyProxy,
signature: "upgradeTo(address)",
args: [dMorphoSteakhouseUSDCStrategyImpl.address],
},
{
// 2. Upgrade Morpho Gauntlet Prime USDC Strategy
contract: dMorphoGauntletPrimeUSDCStrategyProxy,
signature: "upgradeTo(address)",
args: [dMorphoGauntletPrimeUSDCStrategyImpl.address],
},
{
// 3. Upgrade Morpho Gauntlet Prime USDT Strategy
contract: dMorphoGauntletPrimeUSDTStrategyProxy,
signature: "upgradeTo(address)",
args: [dMorphoGauntletPrimeUSDTStrategyImpl.address],
},
{
// 4. Add the new Morpho OUSD v2 strategy to the vault
contract: cVaultAdmin,
signature: "approveStrategy(address)",
args: [cOUSDMorphoV2Strategy.address],
},
Comment on lines +128 to +132
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Should we also remove the steakhouse strategy here so that we don't need two governance proposals? The guardian can do a withdrawAll before executing the governance proposal (to ensure the proposal doesn't revert for any reason)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action 8 is Remove the Morpho Steakhouse strategy

Note the Morpho Gauntlet Prime USDC strategy is left while the funds in the new Morpho OUSD v2 strategy is ramped up. Once all the USDC has been reallocated from the Morpho Gauntlet Prime USDC strategy to the new Morpho OUSD v2 strategy, the Morpho Gauntlet Prime USDC strategy can then be removed in a new gov proposal

{
// 5. Set the Harvester of the Morpho OUSD v2 strategy to the BuyBack Operator
contract: cOUSDMorphoV2Strategy,
signature: "setHarvesterAddress(address)",
args: [addresses.multichainBuybackOperator],
},
{
// 6. Set the Morpho Gauntlet Prime USDC strategy as the default for USDC
contract: cVaultAdmin,
signature: "setAssetDefaultStrategy(address,address)",
args: [
addresses.mainnet.USDC,
dMorphoGauntletPrimeUSDCStrategyProxy.address,
],
},
{
// 7. Remove the Morpho Steakhouse strategy
contract: cVaultAdmin,
signature: "removeStrategy(address)",
args: [dMorphoSteakhouseUSDCStrategyProxy.address],
},
],
};
}
);
66 changes: 66 additions & 0 deletions contracts/tasks/merkl.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
const axios = require("axios");
const { formatUnits } = require("ethers/lib/utils");

const { logTxDetails } = require("../utils/txLogger");

const log = require("../utils/logger")("task:merkl");

const MERKL_API_ENDPOINT = "https://api.merkl.xyz/v4";

const getMerklRewards = async ({ userAddress, chainId = 1 }) => {
const url = `${MERKL_API_ENDPOINT}/users/${userAddress}/rewards?chainId=${chainId}`;
try {
log(`Getting Merkl rewards data from ${url}`);

const response = await axios.get(url);

if (response.data.length === 0 || response.data[0].rewards.length === 0) {
return {
amount: 0n,
token: null,
proofs: [],
};
}

return {
amount: response.data[0].rewards[0].amount,
token: response.data[0].rewards[0].token.address,
proofs: response.data[0].rewards[0].proofs,
};
} catch (err) {
if (err.response) {
console.error("Response data : ", err.response.data);
console.error("Response status: ", err.response.status);
console.error("Response status: ", err.response.statusText);
}
throw Error(`Call to Merkl API failed: ${err.message}`);
}
};

async function claimMerklRewards(strategyAddress, signer) {
const result = await getMerklRewards({
userAddress: strategyAddress,
chainId: 1,
});

log(
`${formatUnits(result.amount, 18)} ${
result.token
} rewards available to claim.`
);

const strategy = await ethers.getContractAt(
"Generalized4626Strategy",
strategyAddress,
signer
);

const tx = await strategy.merkleClaim(
result.token,
result.amount,
result.proofs
);
await logTxDetails(tx, "merkleClaim");
}

module.exports = { claimMerklRewards, getMerklRewards };
25 changes: 25 additions & 0 deletions contracts/tasks/tasks.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ const {
mockBeaconRoot,
copyBeaconRoot,
} = require("./beaconTesting");
const { claimMerklRewards } = require("./merkl");

const log = require("../utils/logger")("tasks");

Expand Down Expand Up @@ -2376,3 +2377,27 @@ subtask("tenderlyUpload", "Uploads a contract to Tenderly.")
task("tenderlyUpload").setAction(async (_, __, runSuper) => {
return runSuper();
});

subtask(
"claimMorphoRewards",
"Claim MORPHO rewards from the Morpho Vaults."
).setAction(async () => {
const signer = await getSigner();

const morphoVaultAddresses = [
// Morpho Gauntlet Prime USDC
"0x2b8f37893ee713a4e9ff0ceb79f27539f20a32a1",
// Morpho Gauntlet Prime USDT
"0xe3ae7c80a1b02ccd3fb0227773553aeb14e32f26",
// Meta Morpho Vault
"0x603CDEAEC82A60E3C4A10dA6ab546459E5f64Fa0",
];

for (const morphoVaultAddress of morphoVaultAddresses) {
log(`Claiming MORPHO rewards from Morpho Vault: ${morphoVaultAddress}`);
await claimMerklRewards(morphoVaultAddress, signer);
}
});
task("claimMorphoRewards").setAction(async (_, __, runSuper) => {
return runSuper();
});
Loading
Loading