From f4bb1afb5665e2f1da1c04475a83a1bfa6661fd1 Mon Sep 17 00:00:00 2001 From: Nick Addison Date: Wed, 18 Dec 2024 08:09:44 +1100 Subject: [PATCH] Morpho Gauntlet Prime Strategies for OUSD (#2317) * Deploy script for Morpho Gauntlet Prime USDC Strategy to the OUSD Vault Refactored the exiting MetaMorpho deploy script and fork tests as there will be many Morpho strategies * Generalized4626Strategy now uses safeApprove as USDT is being used Added deploy script for Morpho Gauntlet Prime USDT Strategy * Add fork tests for Morpho Gauntlet USDT strategy * Fix typo in Natspec * Added Generalized4626USDTStrategy contract for Morpho Gauntlet Prime USDT strategy * Fixed typos in tests * Update OUSD Vault fork test * Fix Slither for IUSDT approve * Renamed fork test files --- contracts/contracts/proxies/Proxies.sol | 18 + .../contracts/strategies/FraxETHStrategy.sol | 2 +- .../strategies/Generalized4626Strategy.sol | 6 +- .../Generalized4626USDTStrategy.sol | 36 ++ .../mainnet/106_ousd_metamorpho_usdc.js | 2 +- .../mainnet/112_ousd_morpho_gauntlet_usdc.js | 95 +++++ .../mainnet/113_ousd_morpho_gauntlet_usdt.js | 95 +++++ contracts/test/_fixture.js | 175 +++++++- contracts/test/flipper/flipper.js | 8 +- ...o-guantlet-prime-usdc.mainnet.fork-test.js | 387 ++++++++++++++++++ ...o-guantlet-prime-usdt.mainnet.fork-test.js | 379 +++++++++++++++++ ...rpho-steakhouse-usdc.mainnet.fork-test.js} | 170 ++++---- .../test/vault/vault.mainnet.fork-test.js | 2 + contracts/utils/addresses.js | 6 +- 14 files changed, 1268 insertions(+), 113 deletions(-) create mode 100644 contracts/contracts/strategies/Generalized4626USDTStrategy.sol create mode 100644 contracts/deploy/mainnet/112_ousd_morpho_gauntlet_usdc.js create mode 100644 contracts/deploy/mainnet/113_ousd_morpho_gauntlet_usdt.js create mode 100644 contracts/test/strategies/ousd-morpho-guantlet-prime-usdc.mainnet.fork-test.js create mode 100644 contracts/test/strategies/ousd-morpho-guantlet-prime-usdt.mainnet.fork-test.js rename contracts/test/strategies/{ousd-metamorpho-usdc.mainnet.fork-test.js => ousd-morpho-steakhouse-usdc.mainnet.fork-test.js} (62%) diff --git a/contracts/contracts/proxies/Proxies.sol b/contracts/contracts/proxies/Proxies.sol index 0f10d3ed44..97647718a6 100644 --- a/contracts/contracts/proxies/Proxies.sol +++ b/contracts/contracts/proxies/Proxies.sol @@ -340,3 +340,21 @@ contract OETHBaseHarvesterProxy is InitializeGovernedUpgradeabilityProxy { contract ARMBuybackProxy is InitializeGovernedUpgradeabilityProxy { } + +/** + * @notice MorphoGauntletPrimeUSDCStrategyProxy delegates calls to a Generalized4626Strategy implementation + */ +contract MorphoGauntletPrimeUSDCStrategyProxy is + InitializeGovernedUpgradeabilityProxy +{ + +} + +/** + * @notice MorphoGauntletPrimeUSDTStrategyProxy delegates calls to a Generalized4626USDTStrategy implementation + */ +contract MorphoGauntletPrimeUSDTStrategyProxy is + InitializeGovernedUpgradeabilityProxy +{ + +} diff --git a/contracts/contracts/strategies/FraxETHStrategy.sol b/contracts/contracts/strategies/FraxETHStrategy.sol index 77daf8c5eb..a6f0058896 100644 --- a/contracts/contracts/strategies/FraxETHStrategy.sol +++ b/contracts/contracts/strategies/FraxETHStrategy.sol @@ -67,7 +67,7 @@ contract FraxETHStrategy is Generalized4626Strategy { } /** - * @dev Retuns bool indicating whether asset is supported by strategy + * @dev Returns bool indicating whether asset is supported by strategy * @param _asset Address of the asset */ function supportsAsset(address _asset) public view override returns (bool) { diff --git a/contracts/contracts/strategies/Generalized4626Strategy.sol b/contracts/contracts/strategies/Generalized4626Strategy.sol index 0f3281ff34..6c0feedfca 100644 --- a/contracts/contracts/strategies/Generalized4626Strategy.sol +++ b/contracts/contracts/strategies/Generalized4626Strategy.sol @@ -7,11 +7,9 @@ pragma solidity ^0.8.0; * @author Origin Protocol Inc */ import { IERC4626 } from "../../lib/openzeppelin/interfaces/IERC4626.sol"; -import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { IERC20, InitializableAbstractStrategy } from "../utils/InitializableAbstractStrategy.sol"; contract Generalized4626Strategy is InitializableAbstractStrategy { - using SafeERC20 for IERC20; /// @dev Replaced with an immutable variable // slither-disable-next-line constable-states @@ -173,14 +171,14 @@ contract Generalized4626Strategy is InitializableAbstractStrategy { } function _approveBase() internal virtual { - // Approval the asset to be trasferred to the ERC-4626 Tokenized Vualt. + // Approval the asset to be transferred to the ERC-4626 Tokenized Vault. // Used by the ERC-4626 deposit() and mint() functions // slither-disable-next-line unused-return assetToken.approve(platformAddress, type(uint256).max); } /** - * @dev Retuns bool indicating whether asset is supported by strategy + * @dev Returns bool indicating whether asset is supported by strategy * @param _asset Address of the asset */ function supportsAsset(address _asset) diff --git a/contracts/contracts/strategies/Generalized4626USDTStrategy.sol b/contracts/contracts/strategies/Generalized4626USDTStrategy.sol new file mode 100644 index 0000000000..b8d65053b0 --- /dev/null +++ b/contracts/contracts/strategies/Generalized4626USDTStrategy.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IUSDT { + // Tether's approve does not return a bool like standard IERC20 contracts + // slither-disable-next-line erc20-interface + function approve(address _spender, uint _value) external; +} + +/** + * @title Generalized 4626 Strategy when asset is Tether USD (USDT) + * @notice Investment strategy for ERC-4626 Tokenized Vaults for the USDT asset. + * @author Origin Protocol Inc + */ +import { Generalized4626Strategy } from "./Generalized4626Strategy.sol"; + +contract Generalized4626USDTStrategy is Generalized4626Strategy { + /** + * @param _baseConfig Base strategy config with platformAddress (ERC-4626 Vault contract), eg sfrxETH or sDAI, + * and vaultAddress (OToken Vault contract), eg VaultProxy or OETHVaultProxy + * @param _assetToken Address of the ERC-4626 asset token. eg frxETH or DAI + */ + constructor( + BaseStrategyConfig memory _baseConfig, + address _assetToken + ) Generalized4626Strategy(_baseConfig, _assetToken) {} + + /// @dev Override for Tether as USDT does not return a bool on approve. + /// Using assetToken.approve will fail as it expects a bool return value + function _approveBase() internal virtual override { + // Approval the asset to be transferred to the ERC-4626 Tokenized Vault. + // Used by the ERC-4626 deposit() and mint() functions + // slither-disable-next-line unused-return + IUSDT(address(assetToken)).approve(platformAddress, type(uint256).max); + } +} diff --git a/contracts/deploy/mainnet/106_ousd_metamorpho_usdc.js b/contracts/deploy/mainnet/106_ousd_metamorpho_usdc.js index 411e252992..0cdbc1daff 100644 --- a/contracts/deploy/mainnet/106_ousd_metamorpho_usdc.js +++ b/contracts/deploy/mainnet/106_ousd_metamorpho_usdc.js @@ -41,7 +41,7 @@ module.exports = deploymentWithGovernanceProposal( const dMetaMorphoStrategyImpl = await deployWithConfirmation( "Generalized4626Strategy", [ - [addresses.mainnet.MetaMorphoUSDCSteakHouseVault, cVaultProxy.address], + [addresses.mainnet.MorphoSteakhouseUSDCVault, cVaultProxy.address], addresses.mainnet.USDC, ], undefined, diff --git a/contracts/deploy/mainnet/112_ousd_morpho_gauntlet_usdc.js b/contracts/deploy/mainnet/112_ousd_morpho_gauntlet_usdc.js new file mode 100644 index 0000000000..991f10558b --- /dev/null +++ b/contracts/deploy/mainnet/112_ousd_morpho_gauntlet_usdc.js @@ -0,0 +1,95 @@ +const addresses = require("../../utils/addresses"); +const { deploymentWithGovernanceProposal } = require("../../utils/deploy"); + +module.exports = deploymentWithGovernanceProposal( + { + deployName: "112_ousd_morpho_gauntlet_usdc", + 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 cHarvesterProxy = await ethers.getContract("HarvesterProxy"); + const cHarvester = await ethers.getContractAt( + "Harvester", + cHarvesterProxy.address + ); + + // Deployer Actions + // ---------------- + const { deployerAddr } = await getNamedAccounts(); + const sDeployer = await ethers.provider.getSigner(deployerAddr); + + // 1. Deploy new Morpho Gauntlet Prime USDC Strategy proxy + const dMorphoGauntletPrimeUSDCProxy = await deployWithConfirmation( + "MorphoGauntletPrimeUSDCStrategyProxy" + ); + const cMorphoGauntletPrimeUSDCProxy = await ethers.getContract( + "MorphoGauntletPrimeUSDCStrategyProxy" + ); + + // 2. Deploy new Generalized4626Strategy contract as it has an immutable to the Morpho Vault contract + const dGeneralized4626Strategy = await deployWithConfirmation( + "Generalized4626Strategy", + [ + [addresses.mainnet.MorphoGauntletPrimeUSDCVault, cVaultProxy.address], + addresses.mainnet.USDC, + ] + ); + const cMorphoGauntletPrimeUSDC = await ethers.getContractAt( + "Generalized4626Strategy", + dMorphoGauntletPrimeUSDCProxy.address + ); + + // 3. Construct initialize call data to initialize and configure the new strategy + const initData = cMorphoGauntletPrimeUSDC.interface.encodeFunctionData( + "initialize()", + [] + ); + + // 4. Init the proxy to point at the implementation, set the governor, and call initialize + const initFunction = "initialize(address,address,bytes)"; + await withConfirmation( + cMorphoGauntletPrimeUSDCProxy.connect(sDeployer)[initFunction]( + dGeneralized4626Strategy.address, + addresses.mainnet.Timelock, // governor + initData, // data for delegate call to the initialize function on the strategy + await getTxOpts() + ) + ); + + // Governance Actions + // ---------------- + return { + name: "Add Morpho Gauntlet Prime USDC Strategy to the OUSD Vault", + actions: [ + { + // Add the new strategy to the vault + contract: cVaultAdmin, + signature: "approveStrategy(address)", + args: [cMorphoGauntletPrimeUSDC.address], + }, + { + // Add the new strategy to the Harvester + contract: cHarvester, + signature: "setSupportedStrategy(address,bool)", + args: [cMorphoGauntletPrimeUSDC.address, true], + }, + { + // Set the Harvester in the new strategy + contract: cMorphoGauntletPrimeUSDC, + signature: "setHarvesterAddress(address)", + args: [cHarvesterProxy.address], + }, + ], + }; + } +); diff --git a/contracts/deploy/mainnet/113_ousd_morpho_gauntlet_usdt.js b/contracts/deploy/mainnet/113_ousd_morpho_gauntlet_usdt.js new file mode 100644 index 0000000000..54351f69f6 --- /dev/null +++ b/contracts/deploy/mainnet/113_ousd_morpho_gauntlet_usdt.js @@ -0,0 +1,95 @@ +const addresses = require("../../utils/addresses"); +const { deploymentWithGovernanceProposal } = require("../../utils/deploy"); + +module.exports = deploymentWithGovernanceProposal( + { + deployName: "113_ousd_morpho_gauntlet_usdt", + 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 cHarvesterProxy = await ethers.getContract("HarvesterProxy"); + const cHarvester = await ethers.getContractAt( + "Harvester", + cHarvesterProxy.address + ); + + // Deployer Actions + // ---------------- + const { deployerAddr } = await getNamedAccounts(); + const sDeployer = await ethers.provider.getSigner(deployerAddr); + + // 1. Deploy new Morpho Gauntlet Prime USDT Strategy proxy + const dMorphoGauntletPrimeUSDTProxy = await deployWithConfirmation( + "MorphoGauntletPrimeUSDTStrategyProxy" + ); + const cMorphoGauntletPrimeUSDTProxy = await ethers.getContract( + "MorphoGauntletPrimeUSDTStrategyProxy" + ); + + // 2. Deploy new Generalized4626USDTStrategy contract as it has an immutable to the Morpho Vault contract + const dGeneralized4626USDTStrategy = await deployWithConfirmation( + "Generalized4626USDTStrategy", + [ + [addresses.mainnet.MorphoGauntletPrimeUSDTVault, cVaultProxy.address], + addresses.mainnet.USDT, + ] + ); + const cMorphoGauntletPrimeUSDT = await ethers.getContractAt( + "Generalized4626USDTStrategy", + dMorphoGauntletPrimeUSDTProxy.address + ); + + // 3. Construct initialize call data to initialize and configure the new strategy + const initData = cMorphoGauntletPrimeUSDT.interface.encodeFunctionData( + "initialize()", + [] + ); + + // 4. Init the proxy to point at the implementation, set the governor, and call initialize + const initFunction = "initialize(address,address,bytes)"; + await withConfirmation( + cMorphoGauntletPrimeUSDTProxy.connect(sDeployer)[initFunction]( + dGeneralized4626USDTStrategy.address, + addresses.mainnet.Timelock, // governor + initData, // data for delegate call to the initialize function on the strategy + await getTxOpts() + ) + ); + + // Governance Actions + // ---------------- + return { + name: "Add Morpho Gauntlet Prime USDT Strategy to the OUSD Vault", + actions: [ + { + // Add the new strategy to the vault + contract: cVaultAdmin, + signature: "approveStrategy(address)", + args: [cMorphoGauntletPrimeUSDT.address], + }, + { + // Add the new strategy to the Harvester + contract: cHarvester, + signature: "setSupportedStrategy(address,bool)", + args: [cMorphoGauntletPrimeUSDT.address, true], + }, + { + // Set the Harvester in the new strategy + contract: cMorphoGauntletPrimeUSDT, + signature: "setHarvesterAddress(address)", + args: [cHarvesterProxy.address], + }, + ], + }; + } +); diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index 908eea9f95..51a81a387d 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -306,14 +306,34 @@ const defaultFixture = deployments.createFixture(async () => { nativeStakingFeeAccumulatorProxy.address ); - const OUSDMetaMorphoStrategyProxy = !isFork + const morphoSteakhouseUSDCStrategyProxy = !isFork ? undefined : await ethers.getContract("MetaMorphoStrategyProxy"); - const OUSDMetaMorphoStrategy = !isFork + const morphoSteakhouseUSDCStrategy = !isFork ? undefined : await ethers.getContractAt( "Generalized4626Strategy", - OUSDMetaMorphoStrategyProxy.address + morphoSteakhouseUSDCStrategyProxy.address + ); + + const morphoGauntletPrimeUSDCStrategyProxy = !isFork + ? undefined + : await ethers.getContract("MorphoGauntletPrimeUSDCStrategyProxy"); + const morphoGauntletPrimeUSDCStrategy = !isFork + ? undefined + : await ethers.getContractAt( + "Generalized4626Strategy", + morphoGauntletPrimeUSDCStrategyProxy.address + ); + + const morphoGauntletPrimeUSDTStrategyProxy = !isFork + ? undefined + : await ethers.getContract("MorphoGauntletPrimeUSDTStrategyProxy"); + const morphoGauntletPrimeUSDTStrategy = !isFork + ? undefined + : await ethers.getContractAt( + "Generalized4626Strategy", + morphoGauntletPrimeUSDTStrategyProxy.address ); let usdt, @@ -340,7 +360,9 @@ const defaultFixture = deployments.createFixture(async () => { frxETH, sfrxETH, sDAI, - usdcMetaMorphoSteakHouseVault, + morphoSteakHouseUSDCVault, + morphoGauntletPrimeUSDCVault, + morphoGauntletPrimeUSDTVault, mockNonRebasing, mockNonRebasingTwo, LUSD, @@ -421,9 +443,17 @@ const defaultFixture = deployments.createFixture(async () => { morphoLensAbi, addresses.mainnet.MorphoLens ); - usdcMetaMorphoSteakHouseVault = await ethers.getContractAt( + morphoSteakHouseUSDCVault = await ethers.getContractAt( metamorphoAbi, - addresses.mainnet.MetaMorphoUSDCSteakHouseVault + addresses.mainnet.MorphoSteakhouseUSDCVault + ); + morphoGauntletPrimeUSDCVault = await ethers.getContractAt( + metamorphoAbi, + addresses.mainnet.MorphoGauntletPrimeUSDCVault + ); + morphoGauntletPrimeUSDTVault = await ethers.getContractAt( + metamorphoAbi, + addresses.mainnet.MorphoGauntletPrimeUSDTVault ); fdai = await ethers.getContractAt(erc20Abi, addresses.mainnet.fDAI); fusdc = await ethers.getContractAt(erc20Abi, addresses.mainnet.fUSDC); @@ -769,8 +799,12 @@ const defaultFixture = deployments.createFixture(async () => { liquidityRewardOUSD_USDT, flipper, wousd, - OUSDMetaMorphoStrategy, - usdcMetaMorphoSteakHouseVault, + morphoSteakhouseUSDCStrategy, + morphoSteakHouseUSDCVault, + morphoGauntletPrimeUSDCStrategy, + morphoGauntletPrimeUSDCVault, + morphoGauntletPrimeUSDTStrategy, + morphoGauntletPrimeUSDTVault, // Flux strategy fluxStrategy, @@ -1436,10 +1470,61 @@ async function makerDsrFixture( } /** - * Configure a Vault with default USDC strategy to the MetaMorpho strategy. + * Configure a Vault with default USDC strategy to the Morpho Steakhouse USDC Vault. */ +async function morphoSteakhouseUSDCFixture( + config = { + usdcMintAmount: 0, + depositToStrategy: false, + } +) { + const fixture = await defaultFixture(); + + if (isFork) { + const { usdc, josh, morphoSteakhouseUSDCStrategy, strategist, vault } = + fixture; + + // Impersonate the OUSD Vault + fixture.vaultSigner = await impersonateAndFund(vault.address); + + // mint some OUSD using USDC if configured + if (config?.usdcMintAmount > 0) { + const usdcMintAmount = parseUnits(config.usdcMintAmount.toString(), 6); + await vault.connect(josh).rebase(); + await vault.connect(josh).allocate(); + + // Approve the Vault to transfer USDC + await usdc.connect(josh).approve(vault.address, usdcMintAmount); + + // Mint OUSD with USDC + // This will sit in the vault, not the strategy + await vault.connect(josh).mint(usdc.address, usdcMintAmount, 0); + + // Add USDC to the strategy + if (config?.depositToStrategy) { + // The strategist deposits the USDC to the strategy + await vault + .connect(strategist) + .depositToStrategy( + morphoSteakhouseUSDCStrategy.address, + [usdc.address], + [usdcMintAmount] + ); + } + } + } else { + throw new Error( + "Morpho Steakhouse USDC strategy only supported in forked test environment" + ); + } + + return fixture; +} -async function metaMorphoFixture( +/** + * Configure a Vault with default USDC strategy to the Morpho Gauntlet Prime USDC Vault. + */ +async function morphoGauntletPrimeUSDCFixture( config = { usdcMintAmount: 0, depositToStrategy: false, @@ -1448,7 +1533,8 @@ async function metaMorphoFixture( const fixture = await defaultFixture(); if (isFork) { - const { usdc, josh, OUSDMetaMorphoStrategy, strategist, vault } = fixture; + const { usdc, josh, morphoGauntletPrimeUSDCStrategy, strategist, vault } = + fixture; // Impersonate the OUSD Vault fixture.vaultSigner = await impersonateAndFund(vault.address); @@ -1466,13 +1552,13 @@ async function metaMorphoFixture( // This will sit in the vault, not the strategy await vault.connect(josh).mint(usdc.address, usdcMintAmount, 0); - // Add USDC to the MetaMorpho Strategy + // Add USDC to the strategy if (config?.depositToStrategy) { - // The strategist deposits the USDC to the MetaMorpho strategy + // The strategist deposits the USDC to the strategy await vault .connect(strategist) .depositToStrategy( - OUSDMetaMorphoStrategy.address, + morphoGauntletPrimeUSDCStrategy.address, [usdc.address], [usdcMintAmount] ); @@ -1480,7 +1566,60 @@ async function metaMorphoFixture( } } else { throw new Error( - "MetaMorpho USDC strategy only supported in forked test environment" + "Morpho Gauntlet Prime USDC strategy only supported in forked test environment" + ); + } + + return fixture; +} + +/** + * Configure a Vault with default USDT strategy to the Morpho Gauntlet Prime USDT Vault. + */ +async function morphoGauntletPrimeUSDTFixture( + config = { + usdtMintAmount: 0, + depositToStrategy: false, + } +) { + const fixture = await defaultFixture(); + + if (isFork) { + const { usdt, josh, morphoGauntletPrimeUSDTStrategy, strategist, vault } = + fixture; + + // Impersonate the OUSD Vault + fixture.vaultSigner = await impersonateAndFund(vault.address); + + // mint some OUSD using USDT if configured + if (config?.usdtMintAmount > 0) { + const usdtMintAmount = parseUnits(config.usdtMintAmount.toString(), 6); + await vault.connect(josh).rebase(); + await vault.connect(josh).allocate(); + + // Approve the Vault to transfer USDT + await usdt.connect(josh).approve(vault.address, 0); + await usdt.connect(josh).approve(vault.address, usdtMintAmount); + + // Mint OUSD with USDT + // This will sit in the vault, not the strategy + await vault.connect(josh).mint(usdt.address, usdtMintAmount, 0); + + // Add USDT to the strategy + if (config?.depositToStrategy) { + // The strategist deposits the USDT to the strategy + await vault + .connect(strategist) + .depositToStrategy( + morphoGauntletPrimeUSDTStrategy.address, + [usdt.address], + [usdtMintAmount] + ); + } + } + } else { + throw new Error( + "Morpho Gauntlet Prime USDT strategy only supported in forked test environment" ); } @@ -2129,7 +2268,7 @@ async function compoundFixture() { } /** - * Configure a threepool fixture with the governer as vault for testing + * Configure a threepool fixture with the governor as vault for testing */ async function threepoolFixture() { const fixture = await defaultFixture(); @@ -2552,7 +2691,9 @@ module.exports = { convexGeneralizedMetaForkedFixture, convexLUSDMetaVaultFixture, makerDsrFixture, - metaMorphoFixture, + morphoSteakhouseUSDCFixture, + morphoGauntletPrimeUSDCFixture, + morphoGauntletPrimeUSDTFixture, morphoCompoundFixture, aaveFixture, morphoAaveFixture, diff --git a/contracts/test/flipper/flipper.js b/contracts/test/flipper/flipper.js index 660a47e8fb..8941c2d15a 100644 --- a/contracts/test/flipper/flipper.js +++ b/contracts/test/flipper/flipper.js @@ -101,7 +101,7 @@ describe("Flipper", function () { describe("Withdraw tokens", () => { describe("Success cases", () => { - withEachCoinIt("can be withdrawn partialy", async (fixture) => { + withEachCoinIt("can be withdrawn partially", async (fixture) => { const { governor, flipper, stablecoin } = fixture; await expect(governor).balanceOf("1000", stablecoin); await expect(flipper).balanceOf("50000", stablecoin); @@ -111,7 +111,7 @@ describe("Flipper", function () { await expect(flipper).balanceOf("37655", stablecoin); }); - it("OUSD can be withdrawn partialy", async () => { + it("OUSD can be withdrawn partially", async () => { const { governor, ousd, flipper } = await loadFixture(); await expect(governor).balanceOf("0", ousd); await expect(flipper).balanceOf("50000", ousd); @@ -166,12 +166,12 @@ describe("Flipper", function () { }); describe("Failure cases", async () => { - it("Only governer can withdraw", async () => { + it("Only governor can withdraw", async () => { const { matt, usdc, flipper } = await loadFixture(); const call = flipper.connect(matt).withdraw(usdc.address, 1); expect(call).to.be.revertedWith("Caller is not the Governor"); }); - it("Only governer can withdrawAll", async () => { + it("Only governor can withdrawAll", async () => { const { matt, flipper } = await loadFixture(); const call = flipper.connect(matt).withdrawAll(); expect(call).to.be.revertedWith("Caller is not the Governor"); diff --git a/contracts/test/strategies/ousd-morpho-guantlet-prime-usdc.mainnet.fork-test.js b/contracts/test/strategies/ousd-morpho-guantlet-prime-usdc.mainnet.fork-test.js new file mode 100644 index 0000000000..fa3f43e112 --- /dev/null +++ b/contracts/test/strategies/ousd-morpho-guantlet-prime-usdc.mainnet.fork-test.js @@ -0,0 +1,387 @@ +const { expect } = require("chai"); +const { formatUnits, parseUnits } = require("ethers/lib/utils"); + +const addresses = require("../../utils/addresses"); +const { units, isCI } = require("../helpers"); + +const { + createFixtureLoader, + morphoGauntletPrimeUSDCFixture, +} = require("../_fixture"); + +const log = require("../../utils/logger"); + +describe("ForkTest: Morpho Gauntlet Prime USDC Strategy", function () { + this.timeout(0); + + // Retry up to 3 times on CI + this.retries(isCI ? 3 : 0); + + let fixture; + + describe("post deployment", () => { + const loadFixture = createFixtureLoader(morphoGauntletPrimeUSDCFixture); + beforeEach(async () => { + fixture = await loadFixture(); + }); + it("Should have constants and immutables set", async () => { + const { vault, morphoGauntletPrimeUSDCStrategy } = fixture; + + expect(await morphoGauntletPrimeUSDCStrategy.platformAddress()).to.equal( + addresses.mainnet.MorphoGauntletPrimeUSDCVault + ); + expect(await morphoGauntletPrimeUSDCStrategy.vaultAddress()).to.equal( + vault.address + ); + expect(await morphoGauntletPrimeUSDCStrategy.shareToken()).to.equal( + addresses.mainnet.MorphoGauntletPrimeUSDCVault + ); + expect(await morphoGauntletPrimeUSDCStrategy.assetToken()).to.equal( + addresses.mainnet.USDC + ); + expect( + await morphoGauntletPrimeUSDCStrategy.supportsAsset( + addresses.mainnet.USDC + ) + ).to.equal(true); + expect( + await morphoGauntletPrimeUSDCStrategy.assetToPToken( + addresses.mainnet.USDC + ) + ).to.equal(addresses.mainnet.MorphoGauntletPrimeUSDCVault); + expect(await morphoGauntletPrimeUSDCStrategy.governor()).to.equal( + addresses.mainnet.Timelock + ); + }); + it("Should be able to check balance", async () => { + const { usdc, josh, morphoGauntletPrimeUSDCStrategy } = fixture; + + // This uses a transaction to call a view function so the gas usage can be reported. + const tx = await morphoGauntletPrimeUSDCStrategy + .connect(josh) + .populateTransaction.checkBalance(usdc.address); + await josh.sendTransaction(tx); + }); + it("Only Governor can approve all tokens", async () => { + const { + timelock, + oldTimelock, + strategist, + josh, + daniel, + domen, + morphoGauntletPrimeUSDCStrategy, + usdc, + vaultSigner, + } = fixture; + + // Governor can approve all tokens + const tx = await morphoGauntletPrimeUSDCStrategy + .connect(timelock) + .safeApproveAllTokens(); + await expect(tx).to.emit(usdc, "Approval"); + + for (const signer of [ + daniel, + domen, + josh, + strategist, + oldTimelock, + vaultSigner, + ]) { + const tx = morphoGauntletPrimeUSDCStrategy + .connect(signer) + .safeApproveAllTokens(); + await expect(tx).to.be.revertedWith("Caller is not the Governor"); + } + }); + }); + + describe("with some USDC in the vault", () => { + const loadFixture = createFixtureLoader(morphoGauntletPrimeUSDCFixture, { + usdcMintAmount: 12000, + depositToStrategy: false, + }); + beforeEach(async () => { + fixture = await loadFixture(); + }); + + it("Vault should deposit some USDC to strategy", async function () { + const { + usdc, + ousd, + morphoGauntletPrimeUSDCStrategy, + vault, + strategist, + vaultSigner, + } = fixture; + + const checkBalanceBefore = + await morphoGauntletPrimeUSDCStrategy.checkBalance(usdc.address); + + const usdcDepositAmount = await units("1000", usdc); + + // Vault transfers USDC to strategy + await usdc + .connect(vaultSigner) + .transfer(morphoGauntletPrimeUSDCStrategy.address, usdcDepositAmount); + + await vault.connect(strategist).rebase(); + + const ousdSupplyBefore = await ousd.totalSupply(); + + const tx = await morphoGauntletPrimeUSDCStrategy + .connect(vaultSigner) + .deposit(usdc.address, usdcDepositAmount); + + // Check emitted event + await expect(tx) + .to.emit(morphoGauntletPrimeUSDCStrategy, "Deposit") + .withArgs( + usdc.address, + addresses.mainnet.MorphoGauntletPrimeUSDCVault, + usdcDepositAmount + ); + + // Check the OUSD total supply increase + const ousdSupplyAfter = await ousd.totalSupply(); + expect(ousdSupplyAfter).to.approxEqualTolerance( + ousdSupplyBefore.add(usdcDepositAmount), + 0.1 // 0.1% or 10 basis point + ); + expect( + await morphoGauntletPrimeUSDCStrategy.checkBalance(usdc.address) + ).to.approxEqualTolerance( + checkBalanceBefore.add(usdcDepositAmount), + 0.01 + ); // 0.01% or 1 basis point + }); + it("Only vault can deposit some USDC to the strategy", async function () { + const { + usdc, + morphoGauntletPrimeUSDCStrategy, + vaultSigner, + strategist, + timelock, + oldTimelock, + josh, + } = fixture; + + const depositAmount = await units("50", usdc); + await usdc + .connect(vaultSigner) + .transfer(morphoGauntletPrimeUSDCStrategy.address, depositAmount); + + for (const signer of [strategist, oldTimelock, timelock, josh]) { + const tx = morphoGauntletPrimeUSDCStrategy + .connect(signer) + .deposit(usdc.address, depositAmount); + + await expect(tx).to.revertedWith("Caller is not the Vault"); + } + }); + it("Only vault can deposit all USDC to strategy", async function () { + const { + usdc, + morphoGauntletPrimeUSDCStrategy, + vaultSigner, + strategist, + timelock, + oldTimelock, + josh, + } = fixture; + + const depositAmount = await units("50", usdc); + await usdc + .connect(vaultSigner) + .transfer(morphoGauntletPrimeUSDCStrategy.address, depositAmount); + + for (const signer of [strategist, oldTimelock, timelock, josh]) { + const tx = morphoGauntletPrimeUSDCStrategy.connect(signer).depositAll(); + + await expect(tx).to.revertedWith("Caller is not the Vault"); + } + + const tx = await morphoGauntletPrimeUSDCStrategy + .connect(vaultSigner) + .depositAll(); + await expect(tx).to.emit(morphoGauntletPrimeUSDCStrategy, "Deposit"); + }); + }); + + describe("with the strategy having some USDC in MetaMorpho Strategy", () => { + const loadFixture = createFixtureLoader(morphoGauntletPrimeUSDCFixture, { + usdcMintAmount: 12000, + depositToStrategy: true, + }); + beforeEach(async () => { + fixture = await loadFixture(); + }); + + it("Vault should be able to withdraw all", async () => { + const { + usdc, + morphoGauntletPrimeUSDCVault, + morphoGauntletPrimeUSDCStrategy, + ousd, + vault, + vaultSigner, + } = fixture; + + const usdcWithdrawAmountExpected = + await morphoGauntletPrimeUSDCVault.maxWithdraw( + morphoGauntletPrimeUSDCStrategy.address + ); + + log( + `Expected to withdraw ${formatUnits(usdcWithdrawAmountExpected)} USDC` + ); + + const ousdSupplyBefore = await ousd.totalSupply(); + const vaultUSDCBalanceBefore = await usdc.balanceOf(vault.address); + + log("Before withdraw all from strategy"); + + // Now try to withdraw all the WETH from the strategy + const tx = await morphoGauntletPrimeUSDCStrategy + .connect(vaultSigner) + .withdrawAll(); + + log("After withdraw all from strategy"); + + // Check emitted event + await expect(tx) + .to.emit(morphoGauntletPrimeUSDCStrategy, "Withdrawal") + .withNamedArgs({ + _asset: usdc.address, + _pToken: morphoGauntletPrimeUSDCVault.address, + }); + + const receipt = await tx.wait(); + const event = receipt.events?.find((e) => e.event === "Withdrawal"); + log(`Actual withdrawal amount: ${formatUnits(event.args[2])}`); + expect(event.args[2]).to.approxEqualTolerance( + usdcWithdrawAmountExpected, + 0.01 + ); + + // Check the OUSD total supply stays the same + expect(await ousd.totalSupply()).to.approxEqualTolerance( + ousdSupplyBefore, + 0.01 // 0.01% or 1 basis point + ); + + // Check the USDC amount in the vault increases + expect(await usdc.balanceOf(vault.address)).to.approxEqualTolerance( + vaultUSDCBalanceBefore.add(usdcWithdrawAmountExpected), + 0.01 + ); + }); + it("Vault should be able to withdraw some USDC", async () => { + const { + usdc, + morphoGauntletPrimeUSDCVault, + morphoGauntletPrimeUSDCStrategy, + ousd, + vault, + vaultSigner, + } = fixture; + + const withdrawAmount = await units("1000", usdc); + + const ousdSupplyBefore = await ousd.totalSupply(); + const vaultUSDCBalanceBefore = await usdc.balanceOf(vault.address); + + log(`Before withdraw of ${formatUnits(withdrawAmount)} from strategy`); + + // Now try to withdraw the USDC from the strategy + const tx = await morphoGauntletPrimeUSDCStrategy + .connect(vaultSigner) + .withdraw(vault.address, usdc.address, withdrawAmount); + + log("After withdraw from strategy"); + + // Check emitted event + await expect(tx) + .to.emit(morphoGauntletPrimeUSDCStrategy, "Withdrawal") + .withArgs( + usdc.address, + morphoGauntletPrimeUSDCVault.address, + withdrawAmount + ); + + // Check the OUSD total supply stays the same + const ousdSupplyAfter = await ousd.totalSupply(); + expect(ousdSupplyAfter).to.approxEqualTolerance( + ousdSupplyBefore, + 0.01 // 0.01% or 1 basis point + ); + + // Check the USDC balance in the Vault + expect(await usdc.balanceOf(vault.address)).to.equal( + vaultUSDCBalanceBefore.add(withdrawAmount) + ); + }); + it("Only vault can withdraw some USDC from strategy", async function () { + const { + morphoGauntletPrimeUSDCStrategy, + oethVault, + strategist, + timelock, + oldTimelock, + josh, + weth, + } = fixture; + + for (const signer of [strategist, timelock, oldTimelock, josh]) { + const tx = morphoGauntletPrimeUSDCStrategy + .connect(signer) + .withdraw(oethVault.address, weth.address, parseUnits("50")); + + await expect(tx).to.revertedWith("Caller is not the Vault"); + } + }); + it("Only vault and governor can withdraw all USDC from Maker DSR strategy", async function () { + const { morphoGauntletPrimeUSDCStrategy, strategist, timelock, josh } = + fixture; + + for (const signer of [strategist, josh]) { + const tx = morphoGauntletPrimeUSDCStrategy + .connect(signer) + .withdrawAll(); + + await expect(tx).to.revertedWith("Caller is not the Vault or Governor"); + } + + // Governor can withdraw all + const tx = morphoGauntletPrimeUSDCStrategy + .connect(timelock) + .withdrawAll(); + await expect(tx).to.emit(morphoGauntletPrimeUSDCStrategy, "Withdrawal"); + }); + }); + + describe("administration", () => { + const loadFixture = createFixtureLoader(morphoGauntletPrimeUSDCFixture); + beforeEach(async () => { + fixture = await loadFixture(); + }); + it("Governor should not be able to set the platform token", () => { + const { frxETH, sfrxETH, morphoGauntletPrimeUSDCStrategy, timelock } = + fixture; + + const tx = morphoGauntletPrimeUSDCStrategy + .connect(timelock) + .setPTokenAddress(frxETH.address, sfrxETH.address); + expect(tx).to.be.revertedWith("unsupported function"); + }); + it("Governor should not be able to remove the platform token", () => { + const { morphoGauntletPrimeUSDCStrategy, timelock } = fixture; + + const tx = morphoGauntletPrimeUSDCStrategy + .connect(timelock) + .removePToken(0); + expect(tx).to.be.revertedWith("unsupported function"); + }); + }); +}); diff --git a/contracts/test/strategies/ousd-morpho-guantlet-prime-usdt.mainnet.fork-test.js b/contracts/test/strategies/ousd-morpho-guantlet-prime-usdt.mainnet.fork-test.js new file mode 100644 index 0000000000..afcbd63d80 --- /dev/null +++ b/contracts/test/strategies/ousd-morpho-guantlet-prime-usdt.mainnet.fork-test.js @@ -0,0 +1,379 @@ +const { expect } = require("chai"); +const { formatUnits, parseUnits } = require("ethers/lib/utils"); + +const addresses = require("../../utils/addresses"); +const { units, isCI } = require("../helpers"); + +const { + createFixtureLoader, + morphoGauntletPrimeUSDTFixture, +} = require("../_fixture"); + +const log = require("../../utils/logger"); + +describe("ForkTest: Morpho Gauntlet Prime USDT Strategy", function () { + this.timeout(0); + + // Retry up to 3 times on CI + this.retries(isCI ? 3 : 0); + + let fixture; + + describe("post deployment", () => { + const loadFixture = createFixtureLoader(morphoGauntletPrimeUSDTFixture); + beforeEach(async () => { + fixture = await loadFixture(); + }); + it("Should have constants and immutables set", async () => { + const { vault, morphoGauntletPrimeUSDTStrategy } = fixture; + + expect(await morphoGauntletPrimeUSDTStrategy.platformAddress()).to.equal( + addresses.mainnet.MorphoGauntletPrimeUSDTVault + ); + expect(await morphoGauntletPrimeUSDTStrategy.vaultAddress()).to.equal( + vault.address + ); + expect(await morphoGauntletPrimeUSDTStrategy.shareToken()).to.equal( + addresses.mainnet.MorphoGauntletPrimeUSDTVault + ); + expect(await morphoGauntletPrimeUSDTStrategy.assetToken()).to.equal( + addresses.mainnet.USDT + ); + expect( + await morphoGauntletPrimeUSDTStrategy.supportsAsset( + addresses.mainnet.USDT + ) + ).to.equal(true); + expect( + await morphoGauntletPrimeUSDTStrategy.assetToPToken( + addresses.mainnet.USDT + ) + ).to.equal(addresses.mainnet.MorphoGauntletPrimeUSDTVault); + expect(await morphoGauntletPrimeUSDTStrategy.governor()).to.equal( + addresses.mainnet.Timelock + ); + }); + it("Should be able to check balance", async () => { + const { usdt, josh, morphoGauntletPrimeUSDTStrategy } = fixture; + + // This uses a transaction to call a view function so the gas usage can be reported. + const tx = await morphoGauntletPrimeUSDTStrategy + .connect(josh) + .populateTransaction.checkBalance(usdt.address); + await josh.sendTransaction(tx); + }); + it("Only Governor can approve all tokens", async () => { + const { + oldTimelock, + strategist, + josh, + daniel, + domen, + morphoGauntletPrimeUSDTStrategy, + vaultSigner, + } = fixture; + + for (const signer of [ + daniel, + domen, + josh, + strategist, + oldTimelock, + vaultSigner, + ]) { + const tx = morphoGauntletPrimeUSDTStrategy + .connect(signer) + .safeApproveAllTokens(); + await expect(tx).to.be.revertedWith("Caller is not the Governor"); + } + }); + }); + + describe("with some USDT in the vault", () => { + const loadFixture = createFixtureLoader(morphoGauntletPrimeUSDTFixture, { + usdtMintAmount: 12000, + depositToStrategy: false, + }); + beforeEach(async () => { + fixture = await loadFixture(); + }); + + it("Vault should deposit some USDT to strategy", async function () { + const { + usdt, + ousd, + morphoGauntletPrimeUSDTStrategy, + vault, + strategist, + vaultSigner, + } = fixture; + + const checkBalanceBefore = + await morphoGauntletPrimeUSDTStrategy.checkBalance(usdt.address); + + const usdtDepositAmount = await units("1000", usdt); + + // Vault transfers USDT to strategy + await usdt + .connect(vaultSigner) + .transfer(morphoGauntletPrimeUSDTStrategy.address, usdtDepositAmount); + + await vault.connect(strategist).rebase(); + + const ousdSupplyBefore = await ousd.totalSupply(); + + const tx = await morphoGauntletPrimeUSDTStrategy + .connect(vaultSigner) + .deposit(usdt.address, usdtDepositAmount); + + // Check emitted event + await expect(tx) + .to.emit(morphoGauntletPrimeUSDTStrategy, "Deposit") + .withArgs( + usdt.address, + addresses.mainnet.MorphoGauntletPrimeUSDTVault, + usdtDepositAmount + ); + + // Check the OUSD total supply increase + const ousdSupplyAfter = await ousd.totalSupply(); + expect(ousdSupplyAfter).to.approxEqualTolerance( + ousdSupplyBefore.add(usdtDepositAmount), + 0.1 // 0.1% or 10 basis point + ); + expect( + await morphoGauntletPrimeUSDTStrategy.checkBalance(usdt.address) + ).to.approxEqualTolerance( + checkBalanceBefore.add(usdtDepositAmount), + 0.01 + ); // 0.01% or 1 basis point + }); + it("Only vault can deposit some USDT to the strategy", async function () { + const { + usdt, + morphoGauntletPrimeUSDTStrategy, + vaultSigner, + strategist, + timelock, + oldTimelock, + josh, + } = fixture; + + const depositAmount = await units("50", usdt); + await usdt + .connect(vaultSigner) + .transfer(morphoGauntletPrimeUSDTStrategy.address, depositAmount); + + for (const signer of [strategist, oldTimelock, timelock, josh]) { + const tx = morphoGauntletPrimeUSDTStrategy + .connect(signer) + .deposit(usdt.address, depositAmount); + + await expect(tx).to.revertedWith("Caller is not the Vault"); + } + }); + it("Only vault can deposit all USDT to strategy", async function () { + const { + usdt, + morphoGauntletPrimeUSDTStrategy, + vaultSigner, + strategist, + timelock, + oldTimelock, + josh, + } = fixture; + + const depositAmount = await units("50", usdt); + await usdt + .connect(vaultSigner) + .transfer(morphoGauntletPrimeUSDTStrategy.address, depositAmount); + + for (const signer of [strategist, oldTimelock, timelock, josh]) { + const tx = morphoGauntletPrimeUSDTStrategy.connect(signer).depositAll(); + + await expect(tx).to.revertedWith("Caller is not the Vault"); + } + + const tx = await morphoGauntletPrimeUSDTStrategy + .connect(vaultSigner) + .depositAll(); + await expect(tx).to.emit(morphoGauntletPrimeUSDTStrategy, "Deposit"); + }); + }); + + describe("with the strategy having some USDT in MetaMorpho Strategy", () => { + const loadFixture = createFixtureLoader(morphoGauntletPrimeUSDTFixture, { + usdtMintAmount: 12000, + depositToStrategy: true, + }); + beforeEach(async () => { + fixture = await loadFixture(); + }); + + it("Vault should be able to withdraw all", async () => { + const { + usdt, + morphoGauntletPrimeUSDTVault, + morphoGauntletPrimeUSDTStrategy, + ousd, + vault, + vaultSigner, + } = fixture; + + const usdtWithdrawAmountExpected = + await morphoGauntletPrimeUSDTVault.maxWithdraw( + morphoGauntletPrimeUSDTStrategy.address + ); + + log( + `Expected to withdraw ${formatUnits(usdtWithdrawAmountExpected)} USDT` + ); + + const ousdSupplyBefore = await ousd.totalSupply(); + const vaultUSDTBalanceBefore = await usdt.balanceOf(vault.address); + + log("Before withdraw all from strategy"); + + // Now try to withdraw all the WETH from the strategy + const tx = await morphoGauntletPrimeUSDTStrategy + .connect(vaultSigner) + .withdrawAll(); + + log("After withdraw all from strategy"); + + // Check emitted event + await expect(tx) + .to.emit(morphoGauntletPrimeUSDTStrategy, "Withdrawal") + .withNamedArgs({ + _asset: usdt.address, + _pToken: morphoGauntletPrimeUSDTVault.address, + }); + + const receipt = await tx.wait(); + const event = receipt.events?.find((e) => e.event === "Withdrawal"); + log(`Actual withdrawal amount: ${formatUnits(event.args[2])}`); + expect(event.args[2]).to.approxEqualTolerance( + usdtWithdrawAmountExpected, + 0.01 + ); + + // Check the OUSD total supply stays the same + expect(await ousd.totalSupply()).to.approxEqualTolerance( + ousdSupplyBefore, + 0.01 // 0.01% or 1 basis point + ); + + // Check the USDT amount in the vault increases + expect(await usdt.balanceOf(vault.address)).to.approxEqualTolerance( + vaultUSDTBalanceBefore.add(usdtWithdrawAmountExpected), + 0.01 + ); + }); + it("Vault should be able to withdraw some USDT", async () => { + const { + usdt, + morphoGauntletPrimeUSDTVault, + morphoGauntletPrimeUSDTStrategy, + ousd, + vault, + vaultSigner, + } = fixture; + + const withdrawAmount = await units("1000", usdt); + + const ousdSupplyBefore = await ousd.totalSupply(); + const vaultUSDTBalanceBefore = await usdt.balanceOf(vault.address); + + log(`Before withdraw of ${formatUnits(withdrawAmount)} from strategy`); + + // Now try to withdraw the USDT from the strategy + const tx = await morphoGauntletPrimeUSDTStrategy + .connect(vaultSigner) + .withdraw(vault.address, usdt.address, withdrawAmount); + + log("After withdraw from strategy"); + + // Check emitted event + await expect(tx) + .to.emit(morphoGauntletPrimeUSDTStrategy, "Withdrawal") + .withArgs( + usdt.address, + morphoGauntletPrimeUSDTVault.address, + withdrawAmount + ); + + // Check the OUSD total supply stays the same + const ousdSupplyAfter = await ousd.totalSupply(); + expect(ousdSupplyAfter).to.approxEqualTolerance( + ousdSupplyBefore, + 0.01 // 0.01% or 1 basis point + ); + + // Check the USDT balance in the Vault + expect(await usdt.balanceOf(vault.address)).to.equal( + vaultUSDTBalanceBefore.add(withdrawAmount) + ); + }); + it("Only vault can withdraw some USDT from strategy", async function () { + const { + morphoGauntletPrimeUSDTStrategy, + oethVault, + strategist, + timelock, + oldTimelock, + josh, + weth, + } = fixture; + + for (const signer of [strategist, timelock, oldTimelock, josh]) { + const tx = morphoGauntletPrimeUSDTStrategy + .connect(signer) + .withdraw(oethVault.address, weth.address, parseUnits("50")); + + await expect(tx).to.revertedWith("Caller is not the Vault"); + } + }); + it("Only vault and governor can withdraw all USDT from Maker DSR strategy", async function () { + const { morphoGauntletPrimeUSDTStrategy, strategist, timelock, josh } = + fixture; + + for (const signer of [strategist, josh]) { + const tx = morphoGauntletPrimeUSDTStrategy + .connect(signer) + .withdrawAll(); + + await expect(tx).to.revertedWith("Caller is not the Vault or Governor"); + } + + // Governor can withdraw all + const tx = morphoGauntletPrimeUSDTStrategy + .connect(timelock) + .withdrawAll(); + await expect(tx).to.emit(morphoGauntletPrimeUSDTStrategy, "Withdrawal"); + }); + }); + + describe("administration", () => { + const loadFixture = createFixtureLoader(morphoGauntletPrimeUSDTFixture); + beforeEach(async () => { + fixture = await loadFixture(); + }); + it("Governor should not be able to set the platform token", () => { + const { frxETH, sfrxETH, morphoGauntletPrimeUSDTStrategy, timelock } = + fixture; + + const tx = morphoGauntletPrimeUSDTStrategy + .connect(timelock) + .setPTokenAddress(frxETH.address, sfrxETH.address); + expect(tx).to.be.revertedWith("unsupported function"); + }); + it("Governor should not be able to remove the platform token", () => { + const { morphoGauntletPrimeUSDTStrategy, timelock } = fixture; + + const tx = morphoGauntletPrimeUSDTStrategy + .connect(timelock) + .removePToken(0); + expect(tx).to.be.revertedWith("unsupported function"); + }); + }); +}); diff --git a/contracts/test/strategies/ousd-metamorpho-usdc.mainnet.fork-test.js b/contracts/test/strategies/ousd-morpho-steakhouse-usdc.mainnet.fork-test.js similarity index 62% rename from contracts/test/strategies/ousd-metamorpho-usdc.mainnet.fork-test.js rename to contracts/test/strategies/ousd-morpho-steakhouse-usdc.mainnet.fork-test.js index 6b1cf0da4c..afc1e01f5d 100644 --- a/contracts/test/strategies/ousd-metamorpho-usdc.mainnet.fork-test.js +++ b/contracts/test/strategies/ousd-morpho-steakhouse-usdc.mainnet.fork-test.js @@ -4,11 +4,14 @@ const { formatUnits, parseUnits } = require("ethers/lib/utils"); const addresses = require("../../utils/addresses"); const { units, isCI } = require("../helpers"); -const { createFixtureLoader, metaMorphoFixture } = require("../_fixture"); +const { + createFixtureLoader, + morphoSteakhouseUSDCFixture, +} = require("../_fixture"); const log = require("../../utils/logger"); -describe("ForkTest: MetaMorpho USDC Strategy", function () { +describe("ForkTest: Morpho Steakhouse USDC Strategy", function () { this.timeout(0); // Retry up to 3 times on CI @@ -17,42 +20,42 @@ describe("ForkTest: MetaMorpho USDC Strategy", function () { let fixture; describe("post deployment", () => { - const loadFixture = createFixtureLoader(metaMorphoFixture); + const loadFixture = createFixtureLoader(morphoSteakhouseUSDCFixture); beforeEach(async () => { fixture = await loadFixture(); }); it("Should have constants and immutables set", async () => { - const { vault, OUSDMetaMorphoStrategy } = fixture; + const { vault, morphoSteakhouseUSDCStrategy } = fixture; - expect(await OUSDMetaMorphoStrategy.platformAddress()).to.equal( - addresses.mainnet.MetaMorphoUSDCSteakHouseVault + expect(await morphoSteakhouseUSDCStrategy.platformAddress()).to.equal( + addresses.mainnet.MorphoSteakhouseUSDCVault ); - expect(await OUSDMetaMorphoStrategy.vaultAddress()).to.equal( + expect(await morphoSteakhouseUSDCStrategy.vaultAddress()).to.equal( vault.address ); - expect(await OUSDMetaMorphoStrategy.shareToken()).to.equal( - addresses.mainnet.MetaMorphoUSDCSteakHouseVault + expect(await morphoSteakhouseUSDCStrategy.shareToken()).to.equal( + addresses.mainnet.MorphoSteakhouseUSDCVault ); - expect(await OUSDMetaMorphoStrategy.assetToken()).to.equal( + expect(await morphoSteakhouseUSDCStrategy.assetToken()).to.equal( addresses.mainnet.USDC ); expect( - await OUSDMetaMorphoStrategy.supportsAsset(addresses.mainnet.USDC) + await morphoSteakhouseUSDCStrategy.supportsAsset(addresses.mainnet.USDC) ).to.equal(true); expect( - await OUSDMetaMorphoStrategy.assetToPToken(addresses.mainnet.USDC) - ).to.equal(addresses.mainnet.MetaMorphoUSDCSteakHouseVault); - expect(await OUSDMetaMorphoStrategy.governor()).to.equal( + await morphoSteakhouseUSDCStrategy.assetToPToken(addresses.mainnet.USDC) + ).to.equal(addresses.mainnet.MorphoSteakhouseUSDCVault); + expect(await morphoSteakhouseUSDCStrategy.governor()).to.equal( addresses.mainnet.Timelock ); }); it("Should be able to check balance", async () => { - const { usdc, josh, OUSDMetaMorphoStrategy } = fixture; + const { usdc, josh, morphoSteakhouseUSDCStrategy } = fixture; // This uses a transaction to call a view function so the gas usage can be reported. - const tx = await OUSDMetaMorphoStrategy.connect( - josh - ).populateTransaction.checkBalance(usdc.address); + const tx = await morphoSteakhouseUSDCStrategy + .connect(josh) + .populateTransaction.checkBalance(usdc.address); await josh.sendTransaction(tx); }); it("Only Governor can approve all tokens", async () => { @@ -63,15 +66,15 @@ describe("ForkTest: MetaMorpho USDC Strategy", function () { josh, daniel, domen, - OUSDMetaMorphoStrategy, + morphoSteakhouseUSDCStrategy, usdc, vaultSigner, } = fixture; // Governor can approve all tokens - const tx = await OUSDMetaMorphoStrategy.connect( - timelock - ).safeApproveAllTokens(); + const tx = await morphoSteakhouseUSDCStrategy + .connect(timelock) + .safeApproveAllTokens(); await expect(tx).to.emit(usdc, "Approval"); for (const signer of [ @@ -82,15 +85,16 @@ describe("ForkTest: MetaMorpho USDC Strategy", function () { oldTimelock, vaultSigner, ]) { - const tx = - OUSDMetaMorphoStrategy.connect(signer).safeApproveAllTokens(); + const tx = morphoSteakhouseUSDCStrategy + .connect(signer) + .safeApproveAllTokens(); await expect(tx).to.be.revertedWith("Caller is not the Governor"); } }); }); describe("with some USDC in the vault", () => { - const loadFixture = createFixtureLoader(metaMorphoFixture, { + const loadFixture = createFixtureLoader(morphoSteakhouseUSDCFixture, { usdcMintAmount: 12000, depositToStrategy: false, }); @@ -102,38 +106,36 @@ describe("ForkTest: MetaMorpho USDC Strategy", function () { const { usdc, ousd, - OUSDMetaMorphoStrategy, + morphoSteakhouseUSDCStrategy, vault, strategist, vaultSigner, } = fixture; - const checkBalanceBefore = await OUSDMetaMorphoStrategy.checkBalance( - usdc.address - ); + const checkBalanceBefore = + await morphoSteakhouseUSDCStrategy.checkBalance(usdc.address); const usdcDepositAmount = await units("1000", usdc); // Vault transfers USDC to strategy await usdc .connect(vaultSigner) - .transfer(OUSDMetaMorphoStrategy.address, usdcDepositAmount); + .transfer(morphoSteakhouseUSDCStrategy.address, usdcDepositAmount); await vault.connect(strategist).rebase(); const ousdSupplyBefore = await ousd.totalSupply(); - const tx = await OUSDMetaMorphoStrategy.connect(vaultSigner).deposit( - usdc.address, - usdcDepositAmount - ); + const tx = await morphoSteakhouseUSDCStrategy + .connect(vaultSigner) + .deposit(usdc.address, usdcDepositAmount); // Check emitted event await expect(tx) - .to.emit(OUSDMetaMorphoStrategy, "Deposit") + .to.emit(morphoSteakhouseUSDCStrategy, "Deposit") .withArgs( usdc.address, - addresses.mainnet.MetaMorphoUSDCSteakHouseVault, + addresses.mainnet.MorphoSteakhouseUSDCVault, usdcDepositAmount ); @@ -144,7 +146,7 @@ describe("ForkTest: MetaMorpho USDC Strategy", function () { 0.1 // 0.1% or 10 basis point ); expect( - await OUSDMetaMorphoStrategy.checkBalance(usdc.address) + await morphoSteakhouseUSDCStrategy.checkBalance(usdc.address) ).to.approxEqualTolerance( checkBalanceBefore.add(usdcDepositAmount), 0.01 @@ -153,7 +155,7 @@ describe("ForkTest: MetaMorpho USDC Strategy", function () { it("Only vault can deposit some USDC to the strategy", async function () { const { usdc, - OUSDMetaMorphoStrategy, + morphoSteakhouseUSDCStrategy, vaultSigner, strategist, timelock, @@ -164,13 +166,12 @@ describe("ForkTest: MetaMorpho USDC Strategy", function () { const depositAmount = await units("50", usdc); await usdc .connect(vaultSigner) - .transfer(OUSDMetaMorphoStrategy.address, depositAmount); + .transfer(morphoSteakhouseUSDCStrategy.address, depositAmount); for (const signer of [strategist, oldTimelock, timelock, josh]) { - const tx = OUSDMetaMorphoStrategy.connect(signer).deposit( - usdc.address, - depositAmount - ); + const tx = morphoSteakhouseUSDCStrategy + .connect(signer) + .deposit(usdc.address, depositAmount); await expect(tx).to.revertedWith("Caller is not the Vault"); } @@ -178,7 +179,7 @@ describe("ForkTest: MetaMorpho USDC Strategy", function () { it("Only vault can deposit all USDC to strategy", async function () { const { usdc, - OUSDMetaMorphoStrategy, + morphoSteakhouseUSDCStrategy, vaultSigner, strategist, timelock, @@ -189,21 +190,23 @@ describe("ForkTest: MetaMorpho USDC Strategy", function () { const depositAmount = await units("50", usdc); await usdc .connect(vaultSigner) - .transfer(OUSDMetaMorphoStrategy.address, depositAmount); + .transfer(morphoSteakhouseUSDCStrategy.address, depositAmount); for (const signer of [strategist, oldTimelock, timelock, josh]) { - const tx = OUSDMetaMorphoStrategy.connect(signer).depositAll(); + const tx = morphoSteakhouseUSDCStrategy.connect(signer).depositAll(); await expect(tx).to.revertedWith("Caller is not the Vault"); } - const tx = await OUSDMetaMorphoStrategy.connect(vaultSigner).depositAll(); - await expect(tx).to.emit(OUSDMetaMorphoStrategy, "Deposit"); + const tx = await morphoSteakhouseUSDCStrategy + .connect(vaultSigner) + .depositAll(); + await expect(tx).to.emit(morphoSteakhouseUSDCStrategy, "Deposit"); }); }); describe("with the strategy having some USDC in MetaMorpho Strategy", () => { - const loadFixture = createFixtureLoader(metaMorphoFixture, { + const loadFixture = createFixtureLoader(morphoSteakhouseUSDCFixture, { usdcMintAmount: 12000, depositToStrategy: true, }); @@ -214,16 +217,16 @@ describe("ForkTest: MetaMorpho USDC Strategy", function () { it("Vault should be able to withdraw all", async () => { const { usdc, - usdcMetaMorphoSteakHouseVault, - OUSDMetaMorphoStrategy, + morphoSteakHouseUSDCVault, + morphoSteakhouseUSDCStrategy, ousd, vault, vaultSigner, } = fixture; const usdcWithdrawAmountExpected = - await usdcMetaMorphoSteakHouseVault.maxWithdraw( - OUSDMetaMorphoStrategy.address + await morphoSteakHouseUSDCVault.maxWithdraw( + morphoSteakhouseUSDCStrategy.address ); log( @@ -236,18 +239,18 @@ describe("ForkTest: MetaMorpho USDC Strategy", function () { log("Before withdraw all from strategy"); // Now try to withdraw all the WETH from the strategy - const tx = await OUSDMetaMorphoStrategy.connect( - vaultSigner - ).withdrawAll(); + const tx = await morphoSteakhouseUSDCStrategy + .connect(vaultSigner) + .withdrawAll(); log("After withdraw all from strategy"); // Check emitted event await expect(tx) - .to.emit(OUSDMetaMorphoStrategy, "Withdrawal") + .to.emit(morphoSteakhouseUSDCStrategy, "Withdrawal") .withNamedArgs({ _asset: usdc.address, - _pToken: usdcMetaMorphoSteakHouseVault.address, + _pToken: morphoSteakHouseUSDCVault.address, }); const receipt = await tx.wait(); @@ -273,8 +276,8 @@ describe("ForkTest: MetaMorpho USDC Strategy", function () { it("Vault should be able to withdraw some USDC", async () => { const { usdc, - usdcMetaMorphoSteakHouseVault, - OUSDMetaMorphoStrategy, + morphoSteakHouseUSDCVault, + morphoSteakhouseUSDCStrategy, ousd, vault, vaultSigner, @@ -288,20 +291,18 @@ describe("ForkTest: MetaMorpho USDC Strategy", function () { log(`Before withdraw of ${formatUnits(withdrawAmount)} from strategy`); // Now try to withdraw the USDC from the strategy - const tx = await OUSDMetaMorphoStrategy.connect(vaultSigner).withdraw( - vault.address, - usdc.address, - withdrawAmount - ); + const tx = await morphoSteakhouseUSDCStrategy + .connect(vaultSigner) + .withdraw(vault.address, usdc.address, withdrawAmount); log("After withdraw from strategy"); // Check emitted event await expect(tx) - .to.emit(OUSDMetaMorphoStrategy, "Withdrawal") + .to.emit(morphoSteakhouseUSDCStrategy, "Withdrawal") .withArgs( usdc.address, - usdcMetaMorphoSteakHouseVault.address, + morphoSteakHouseUSDCVault.address, withdrawAmount ); @@ -319,7 +320,7 @@ describe("ForkTest: MetaMorpho USDC Strategy", function () { }); it("Only vault can withdraw some USDC from strategy", async function () { const { - OUSDMetaMorphoStrategy, + morphoSteakhouseUSDCStrategy, oethVault, strategist, timelock, @@ -329,48 +330,47 @@ describe("ForkTest: MetaMorpho USDC Strategy", function () { } = fixture; for (const signer of [strategist, timelock, oldTimelock, josh]) { - const tx = OUSDMetaMorphoStrategy.connect(signer).withdraw( - oethVault.address, - weth.address, - parseUnits("50") - ); + const tx = morphoSteakhouseUSDCStrategy + .connect(signer) + .withdraw(oethVault.address, weth.address, parseUnits("50")); await expect(tx).to.revertedWith("Caller is not the Vault"); } }); it("Only vault and governor can withdraw all USDC from Maker DSR strategy", async function () { - const { OUSDMetaMorphoStrategy, strategist, timelock, josh } = fixture; + const { morphoSteakhouseUSDCStrategy, strategist, timelock, josh } = + fixture; for (const signer of [strategist, josh]) { - const tx = OUSDMetaMorphoStrategy.connect(signer).withdrawAll(); + const tx = morphoSteakhouseUSDCStrategy.connect(signer).withdrawAll(); await expect(tx).to.revertedWith("Caller is not the Vault or Governor"); } // Governor can withdraw all - const tx = OUSDMetaMorphoStrategy.connect(timelock).withdrawAll(); - await expect(tx).to.emit(OUSDMetaMorphoStrategy, "Withdrawal"); + const tx = morphoSteakhouseUSDCStrategy.connect(timelock).withdrawAll(); + await expect(tx).to.emit(morphoSteakhouseUSDCStrategy, "Withdrawal"); }); }); describe("administration", () => { - const loadFixture = createFixtureLoader(metaMorphoFixture); + const loadFixture = createFixtureLoader(morphoSteakhouseUSDCFixture); beforeEach(async () => { fixture = await loadFixture(); }); it("Governor should not be able to set the platform token", () => { - const { frxETH, sfrxETH, OUSDMetaMorphoStrategy, timelock } = fixture; + const { frxETH, sfrxETH, morphoSteakhouseUSDCStrategy, timelock } = + fixture; - const tx = OUSDMetaMorphoStrategy.connect(timelock).setPTokenAddress( - frxETH.address, - sfrxETH.address - ); + const tx = morphoSteakhouseUSDCStrategy + .connect(timelock) + .setPTokenAddress(frxETH.address, sfrxETH.address); expect(tx).to.be.revertedWith("unsupported function"); }); it("Governor should not be able to remove the platform token", () => { - const { OUSDMetaMorphoStrategy, timelock } = fixture; + const { morphoSteakhouseUSDCStrategy, timelock } = fixture; - const tx = OUSDMetaMorphoStrategy.connect(timelock).removePToken(0); + const tx = morphoSteakhouseUSDCStrategy.connect(timelock).removePToken(0); expect(tx).to.be.revertedWith("unsupported function"); }); }); diff --git a/contracts/test/vault/vault.mainnet.fork-test.js b/contracts/test/vault/vault.mainnet.fork-test.js index 23ca217546..1519c7720c 100644 --- a/contracts/test/vault/vault.mainnet.fork-test.js +++ b/contracts/test/vault/vault.mainnet.fork-test.js @@ -349,6 +349,8 @@ describe("ForkTest: Vault", function () { "0x79F2188EF9350A1dC11A062cca0abE90684b0197", // MorphoAaveStrategy "0x6b69B755C629590eD59618A2712d8a2957CA98FC", // Maker DSR Strategy "0x603CDEAEC82A60E3C4A10dA6ab546459E5f64Fa0", // Meta Morpho USDC + "0x58609f1F2E08c6DF0deE91929BC345EF3919fC3A", // Morpho Gauntlet Prime USDC + "0x4355cc69d5D70f4f3375F79E3478B4D672Ca2313", // Morpho Gauntlet Prime USDT ]; for (const s of strategies) { diff --git a/contracts/utils/addresses.js b/contracts/utils/addresses.js index a62455b786..5424ea9fef 100644 --- a/contracts/utils/addresses.js +++ b/contracts/utils/addresses.js @@ -171,8 +171,12 @@ addresses.mainnet.MorphoStrategyProxy = addresses.mainnet.MorphoAaveStrategyProxy = "0x79F2188EF9350A1dC11A062cca0abE90684b0197"; addresses.mainnet.HarvesterProxy = "0x21Fb5812D70B3396880D30e90D9e5C1202266c89"; -addresses.mainnet.MetaMorphoUSDCSteakHouseVault = +addresses.mainnet.MorphoSteakhouseUSDCVault = "0xBEEF01735c132Ada46AA9aA4c54623cAA92A64CB"; +addresses.mainnet.MorphoGauntletPrimeUSDCVault = + "0xdd0f28e19C1780eb6396170735D45153D261490d"; +addresses.mainnet.MorphoGauntletPrimeUSDTVault = + "0x8CB3649114051cA5119141a34C200D65dc0Faa73"; addresses.mainnet.UniswapOracle = "0xc15169Bad17e676b3BaDb699DEe327423cE6178e"; addresses.mainnet.CompensationClaims =