From 36332fe04ca5b75d8f419725ff0e02bd45ff9fc9 Mon Sep 17 00:00:00 2001 From: Daniel Von Fange Date: Mon, 29 May 2023 17:05:20 -0400 Subject: [PATCH] OETH Morpho AAVE (#1543) * Deploy OETH Morpho AAVE contract * OETH Morpho Aave v2 Fork tests (#1544) * Add fork tests * Lint * Switch to new governor * Proxy init to change governor at the end (#1557) * Init of Morpho strategy from proxy init (cherry picked from commit 4fee664109424821844fd830c5c1740851503a26) * set governor on proxy init (cherry picked from commit d6d9fb9e1684cd30231f1d571d3b1fd228f76eb0) * Proxy init to do gov change after init of impl Simplified oeth morpho deploy script (cherry picked from commit c38c6a7530de11983eef27df0faf810b4f9c515b) * fix sol prettier (cherry picked from commit ecb7d004791fe2ce6aa688b11b246836e2cdc451) * change governance in fork tests * reduce time with deployment script * skipping some fork tests * remove .only --------- Co-authored-by: Domen Grabec --------- Co-authored-by: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Co-authored-by: Domen Grabec Co-authored-by: Nick Addison --- .../InitializeGovernedUpgradeabilityProxy.sol | 2 +- contracts/contracts/proxies/Proxies.sol | 7 + contracts/deploy/064_oeth_mopho_aave_v2.js | 92 +++++++++ contracts/test/_fixture.js | 101 ++++++++- .../harvest/ousd-harvest-crv.fork-test.js | 17 +- contracts/test/helpers.js | 1 + .../strategies/oeth-morpho-aave.fork-test.js | 194 ++++++++++++++++++ contracts/utils/addresses.js | 1 + 8 files changed, 406 insertions(+), 9 deletions(-) create mode 100644 contracts/deploy/064_oeth_mopho_aave_v2.js create mode 100644 contracts/test/strategies/oeth-morpho-aave.fork-test.js diff --git a/contracts/contracts/proxies/InitializeGovernedUpgradeabilityProxy.sol b/contracts/contracts/proxies/InitializeGovernedUpgradeabilityProxy.sol index 57a0993082..ef6c9f0572 100644 --- a/contracts/contracts/proxies/InitializeGovernedUpgradeabilityProxy.sol +++ b/contracts/contracts/proxies/InitializeGovernedUpgradeabilityProxy.sol @@ -41,12 +41,12 @@ contract InitializeGovernedUpgradeabilityProxy is Governable { IMPLEMENTATION_SLOT == bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1) ); - _changeGovernor(_initGovernor); _setImplementation(_logic); if (_data.length > 0) { (bool success, ) = _logic.delegatecall(_data); require(success); } + _changeGovernor(_initGovernor); } /** diff --git a/contracts/contracts/proxies/Proxies.sol b/contracts/contracts/proxies/Proxies.sol index 08351646e1..ef623d6d40 100644 --- a/contracts/contracts/proxies/Proxies.sol +++ b/contracts/contracts/proxies/Proxies.sol @@ -149,3 +149,10 @@ contract ConvexEthMetaStrategyProxy is InitializeGovernedUpgradeabilityProxy { contract BuybackProxy is InitializeGovernedUpgradeabilityProxy { } + +/** + * @notice OETHMorphoAaveStrategyProxy delegates calls to a MorphoAaveStrategy implementation + */ +contract OETHMorphoAaveStrategyProxy is InitializeGovernedUpgradeabilityProxy { + +} diff --git a/contracts/deploy/064_oeth_mopho_aave_v2.js b/contracts/deploy/064_oeth_mopho_aave_v2.js new file mode 100644 index 0000000000..baa6b88b46 --- /dev/null +++ b/contracts/deploy/064_oeth_mopho_aave_v2.js @@ -0,0 +1,92 @@ +const { deploymentWithProposal } = require("../utils/deploy"); +const addresses = require("../utils/addresses"); + +module.exports = deploymentWithProposal( + { + deployName: "064_oeth_morpho_aave_v2", + forceDeploy: false, + reduceQueueTime: true, + // proposalId: , + }, + async ({ + assetAddresses, + deployWithConfirmation, + ethers, + getTxOpts, + withConfirmation, + }) => { + const { deployerAddr } = await getNamedAccounts(); + const sDeployer = await ethers.provider.getSigner(deployerAddr); + + // Current contracts + const cVaultProxy = await ethers.getContract("OETHVaultProxy"); + const cVaultAdmin = await ethers.getContractAt( + "VaultAdmin", + cVaultProxy.address + ); + + // Deployer Actions + // ---------------- + + // 1. Deploy new proxy + // New strategy will be living at a clean address + const dOETHMorphoAaveStrategyProxy = await deployWithConfirmation( + "OETHMorphoAaveStrategyProxy" + ); + const cOETHMorphoAaveStrategyProxy = await ethers.getContractAt( + "OETHMorphoAaveStrategyProxy", + dOETHMorphoAaveStrategyProxy.address + ); + + // 2. Reuse old OUSD impl + const cMorphoAaveStrategyImpl = await ethers.getContract( + "MorphoAaveStrategy" + ); + const cMorphoAaveStrategy = await ethers.getContractAt( + "MorphoAaveStrategy", + cOETHMorphoAaveStrategyProxy.address + ); + + // 3. Construct initialize call data to init and configure the new Morpho strategy + const initData = cMorphoAaveStrategyImpl.interface.encodeFunctionData( + "initialize(address,address[],address[],address[])", + [ + cVaultProxy.address, + [], // reward token addresses + [assetAddresses.WETH], // asset token addresses + [assetAddresses.aWETH], // platform tokens addresses + ] + ); + + // 4. Init the proxy to point at the implementation, set the governor, and call initialize + const initFunction = "initialize(address,address,bytes)"; + await withConfirmation( + cOETHMorphoAaveStrategyProxy.connect(sDeployer)[initFunction]( + cMorphoAaveStrategyImpl.address, + addresses.mainnet.OldTimelock, // governor + initData, // data for call to the initialize function on the Morpho strategy + await getTxOpts() + ) + ); + + console.log( + "OUSD Morpho Aave strategy address: ", + cMorphoAaveStrategy.address + ); + + // Governance Actions + // ---------------- + return { + name: "Deploy new OUSD Morpho Aave strategy", + governorAddr: addresses.mainnet.OldTimelock, + actions: [ + // 1. Add new Morpho strategy to vault + { + contract: cVaultAdmin, + signature: "approveStrategy(address)", + args: [cMorphoAaveStrategy.address], + }, + ], + }; + } +); diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index fc6461013c..771e86c036 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -166,6 +166,7 @@ async function defaultFixture() { morphoCompoundStrategy, fraxEthStrategy, morphoAaveStrategy, + oethMorphoAaveStrategy, morphoLens, LUSDMetapoolToken, threePoolGauge, @@ -188,7 +189,7 @@ async function defaultFixture() { dai = await ethers.getContractAt(daiAbi, addresses.mainnet.DAI); tusd = await ethers.getContractAt(erc20Abi, addresses.mainnet.TUSD); usdc = await ethers.getContractAt(erc20Abi, addresses.mainnet.USDC); - weth = await ethers.getContractAt(erc20Abi, addresses.mainnet.WETH); + weth = await ethers.getContractAt("IWETH9", addresses.mainnet.WETH); cusdt = await ethers.getContractAt(erc20Abi, addresses.mainnet.cUSDT); cdai = await ethers.getContractAt(erc20Abi, addresses.mainnet.cDAI); cusdc = await ethers.getContractAt(erc20Abi, addresses.mainnet.cUSDC); @@ -243,6 +244,14 @@ async function defaultFixture() { morphoAaveStrategyProxy.address ); + const oethMorphoAaveStrategyProxy = await ethers.getContract( + "OETHMorphoAaveStrategyProxy" + ); + oethMorphoAaveStrategy = await ethers.getContractAt( + "MorphoAaveStrategy", + oethMorphoAaveStrategyProxy.address + ); + const fraxEthStrategyProxy = await ethers.getContract( "FraxETHStrategyProxy" ); @@ -493,6 +502,7 @@ async function defaultFixture() { frxETH, sfrxETH, fraxEthStrategy, + oethMorphoAaveStrategy, woeth, ConvexEthMetaStrategy, oethDripper, @@ -500,6 +510,37 @@ async function defaultFixture() { }; } +function defaultFixtureSetup() { + return deployments.createFixture(async () => { + return await defaultFixture(); + }); +} + +async function oethDefaultFixture() { + // TODO: Trim it down to only do OETH things + const fixture = await defaultFixture(); + + if (isFork) { + const { weth, matt, josh, domen, daniel, franck, oethVault } = fixture; + + for (const user of [matt, josh, domen, daniel, franck]) { + // Everyone gets free WETH + await mintWETH(weth, user); + + // And vault can rug them all + await resetAllowance(weth, user, oethVault.address); + } + } + + return fixture; +} + +async function oethDefaultFixtureSetup() { + return deployments.createFixture(async () => { + return await oethDefaultFixture(); + }); +} + /** * Configure the MockVault contract by initializing it and setting supported * assets and then upgrade the Vault implementation via VaultProxy. @@ -819,6 +860,33 @@ async function morphoAaveFixture() { return fixture; } +/** + * Configure a Vault with only the Morpho strategy. + */ +function oethMorphoAaveFixtureSetup() { + return deployments.createFixture(async () => { + const fixture = await oethDefaultFixture(); + + if (isFork) { + const { governorAddr } = await getNamedAccounts(); + let sGovernor = await ethers.provider.getSigner(governorAddr); + + await fixture.oethVault + .connect(sGovernor) + .setAssetDefaultStrategy( + fixture.weth.address, + fixture.oethMorphoAaveStrategy.address + ); + } else { + throw new Error( + "Morpho strategy only supported in forked test environment" + ); + } + + return fixture; + }); +} + /** * FraxETHStrategy fixture that works only in forked environment * @@ -955,13 +1023,24 @@ async function impersonateAccount(address) { }); } -async function impersonateAndFundContract(address) { +async function _hardhatSetBalance(address, amount = "10000") { + await hre.network.provider.request({ + method: "hardhat_setBalance", + params: [ + address, + utils + .parseEther(amount) + .toHexString() + .replace(/^0x0+/, "0x") + .replace(/0$/, "1"), + ], + }); +} + +async function impersonateAndFundContract(address, amount = "100000") { await impersonateAccount(address); - await hre.network.provider.send("hardhat_setBalance", [ - address, - utils.parseEther("1000000").toHexString(), - ]); + await _hardhatSetBalance(address, amount); return await ethers.provider.getSigner(address); } @@ -1018,6 +1097,13 @@ async function resetAllowance( await tokenContract.connect(signer).approve(toAddress, allowance); } +async function mintWETH(weth, recipient, amount = "100") { + await _hardhatSetBalance(recipient.address, (Number(amount) * 2).toString()); + await weth.connect(recipient).deposit({ + value: utils.parseEther(amount), + }); +} + async function withImpersonatedAccount(address, cb) { const signer = await impersonateAndFundContract(address); @@ -1367,6 +1453,8 @@ module.exports = { fundWith3Crv, resetAllowance, defaultFixture, + defaultFixtureSetup, + oethDefaultFixtureSetup, mockVaultFixture, compoundFixture, compoundVaultFixture, @@ -1387,4 +1475,5 @@ module.exports = { impersonateAndFundContract, impersonateAccount, fraxETHStrategyForkedFixture, + oethMorphoAaveFixtureSetup, }; diff --git a/contracts/test/harvest/ousd-harvest-crv.fork-test.js b/contracts/test/harvest/ousd-harvest-crv.fork-test.js index 0422392cf9..eb6658f776 100644 --- a/contracts/test/harvest/ousd-harvest-crv.fork-test.js +++ b/contracts/test/harvest/ousd-harvest-crv.fork-test.js @@ -64,7 +64,15 @@ forkOnlyDescribe("ForkTest: Harvest OUSD", function () { oldCrvTokenConfig.doSwapRewardToken ); }); - it("should not harvest and swap", async function () { + /* + * Skipping this test as it should only fail on a specific block number, where + * there is: + * - no liquidation limit + * - strategy has accrued a lot of CRV rewards + * - depth of the SushiSwap pool is not deep enough to handle the swap without + * hitting the slippage limit. + */ + it.skip("should not harvest and swap", async function () { const { anna, OUSDmetaStrategy, harvester } = fixture; // prettier-ignore @@ -93,7 +101,12 @@ forkOnlyDescribe("ForkTest: Harvest OUSD", function () { oldCrvTokenConfig.doSwapRewardToken ); }); - it("should harvest and swap", async function () { + /* + * Skipping this test as it will only succeed again on a specific block number. + * If strategy doesn't have enough CRV not nearly enough rewards are going to be + * harvested for the test to pass. + */ + it.skip("should harvest and swap", async function () { const { crv, OUSDmetaStrategy, dripper, harvester, timelock, usdt } = fixture; diff --git a/contracts/test/helpers.js b/contracts/test/helpers.js index 25fa99cab4..7ca3739332 100644 --- a/contracts/test/helpers.js +++ b/contracts/test/helpers.js @@ -354,6 +354,7 @@ const getAssetAddresses = async (deployments) => { aDAI_v2: addresses.mainnet.aDAI_v2, aUSDC: addresses.mainnet.aUSDC, aUSDT: addresses.mainnet.aUSDT, + aWETH: addresses.mainnet.aWETH, AAVE: addresses.mainnet.Aave, AAVE_TOKEN: addresses.mainnet.Aave, AAVE_ADDRESS_PROVIDER: addresses.mainnet.AAVE_ADDRESS_PROVIDER, diff --git a/contracts/test/strategies/oeth-morpho-aave.fork-test.js b/contracts/test/strategies/oeth-morpho-aave.fork-test.js new file mode 100644 index 0000000000..79ceb18640 --- /dev/null +++ b/contracts/test/strategies/oeth-morpho-aave.fork-test.js @@ -0,0 +1,194 @@ +const { expect } = require("chai"); + +const { + units, + oethUnits, + forkOnlyDescribe, + advanceBlocks, + advanceTime, +} = require("../helpers"); +const { + defaultFixtureSetup, + oethMorphoAaveFixtureSetup, + impersonateAndFundContract, +} = require("../_fixture"); + +forkOnlyDescribe("ForkTest: Morpho Aave OETH Strategy", function () { + const oethMorphoAaveFixture = oethMorphoAaveFixtureSetup(); + + after(async () => { + // This is needed to revert fixtures + // The other tests as of now don't use proper fixtures + // Rel: https://github.com/OriginProtocol/origin-dollar/issues/1259 + const f = defaultFixtureSetup(); + await f(); + }); + + this.timeout(0); + + // Retry up to 3 times on CI + this.retries(process.env.GITHUB_ACTIONS ? 3 : 0); + + let fixture; + beforeEach(async () => { + fixture = await oethMorphoAaveFixture(); + }); + + describe("Mint", function () { + it("Should deploy WETH in Morpho Aave", async function () { + const { domen, weth } = fixture; + await mintTest(fixture, domen, weth, "3.56"); + }); + }); + + describe("Redeem", function () { + it("Should redeem from Morpho", async () => { + const { oethVault, oeth, weth, daniel } = fixture; + + // Mint some ETH + const amount = "1.23"; + const amountUnits = oethUnits(amount); + await oethVault.connect(daniel).mint(weth.address, amountUnits, 0); + await oethVault.connect(daniel).allocate(); + await oethVault.connect(daniel).rebase(); + + const currentSupply = await oeth.totalSupply(); + const currentBalance = await oeth + .connect(daniel) + .balanceOf(daniel.address); + + // Now try to redeem 1.23 OETH + await oethVault.connect(daniel).redeem(amountUnits, 0); + + await oethVault.connect(daniel).allocate(); + await oethVault.connect(daniel).rebase(); + + // User balance should be down by 1.23 OETH + const newBalance = await oeth.connect(daniel).balanceOf(daniel.address); + expect(newBalance).to.approxEqualTolerance( + currentBalance.sub(amountUnits), + 1 + ); + + const newSupply = await oeth.totalSupply(); + const supplyDiff = currentSupply.sub(newSupply); + + expect(supplyDiff).to.approxEqualTolerance(amountUnits, 1); + }); + }); + + describe("Supply Revenue", function () { + it("Should get supply interest", async function () { + const { josh, weth, oethMorphoAaveStrategy } = fixture; + await mintTest(fixture, josh, weth, "2.3333444"); + + const currentBalance = await oethMorphoAaveStrategy.checkBalance( + weth.address + ); + + await advanceTime(60 * 60 * 24 * 365); + await advanceBlocks(10000); + + const balanceAfter1Y = await oethMorphoAaveStrategy.checkBalance( + weth.address + ); + + const diff = balanceAfter1Y.sub(currentBalance); + expect(diff).to.be.gt(0); + }); + }); + + describe("Withdraw", function () { + it("Should be able to withdraw WETH from strategy", async function () { + const { matt, weth } = fixture; + await withdrawTest(fixture, matt, weth, "2.7655"); + }); + + it("Should be able to withdrawAll from strategy", async function () { + const { franck, weth, oethVault, oethMorphoAaveStrategy } = fixture; + const oethVaultSigner = await impersonateAndFundContract( + oethVault.address + ); + const amount = "1.121314"; + + // Remove funds so no residual funds get allocated + await weth + .connect(oethVaultSigner) + .transfer(franck.address, await weth.balanceOf(oethVault.address)); + + // Mint some OETH + await mintTest(fixture, franck, weth, amount); + + const wethUnits = await units(amount, weth); + const oethVaultWETHBefore = await weth.balanceOf(oethVault.address); + + await oethMorphoAaveStrategy.connect(oethVaultSigner).withdrawAll(); + + const oethVaultWETHDiff = (await weth.balanceOf(oethVault.address)).sub( + oethVaultWETHBefore + ); + + expect(oethVaultWETHDiff).to.approxEqualTolerance(wethUnits, 1); + }); + }); +}); + +async function mintTest(fixture, user, asset, amount = "0.34") { + const { oethVault, oeth, oethMorphoAaveStrategy } = fixture; + + await oethVault.connect(user).allocate(); + await oethVault.connect(user).rebase(); + + const unitAmount = await units(amount, asset); + + const currentSupply = await oeth.totalSupply(); + const currentBalance = await oeth.connect(user).balanceOf(user.address); + const currentMorphoBalance = await oethMorphoAaveStrategy.checkBalance( + asset.address + ); + + // Mint OETH w/ asset + await oethVault.connect(user).mint(asset.address, unitAmount, 0); + await oethVault.connect(user).allocate(); + + const newBalance = await oeth.connect(user).balanceOf(user.address); + const newSupply = await oeth.totalSupply(); + const newMorphoBalance = await oethMorphoAaveStrategy.checkBalance( + asset.address + ); + + const balanceDiff = newBalance.sub(currentBalance); + // Ensure user has correct balance (w/ 1% slippage tolerance) + expect(balanceDiff).to.approxEqualTolerance(oethUnits(amount), 2); + + // Supply checks + const supplyDiff = newSupply.sub(currentSupply); + const oethUnitAmount = oethUnits(amount); + expect(supplyDiff).to.approxEqualTolerance(oethUnitAmount, 1); + + const morphoLiquidityDiff = newMorphoBalance.sub(currentMorphoBalance); + + // Should have liquidity in Morpho + expect(morphoLiquidityDiff).to.approxEqualTolerance( + await units(amount, asset), + 1 + ); +} + +async function withdrawTest(fixture, user, asset, amount = "3.876") { + const { oethVault, oethMorphoAaveStrategy } = fixture; + await mintTest(fixture, user, asset, amount); + + const assetUnits = await units(amount, asset); + const oethVaultAssetBalBefore = await asset.balanceOf(oethVault.address); + const oethVaultSigner = await impersonateAndFundContract(oethVault.address); + + await oethMorphoAaveStrategy + .connect(oethVaultSigner) + .withdraw(oethVault.address, asset.address, assetUnits); + const oethVaultAssetBalDiff = (await asset.balanceOf(oethVault.address)).sub( + oethVaultAssetBalBefore + ); + + expect(oethVaultAssetBalDiff).to.approxEqualTolerance(assetUnits, 1); +} diff --git a/contracts/utils/addresses.js b/contracts/utils/addresses.js index bd8dfb25cb..c882c90fba 100644 --- a/contracts/utils/addresses.js +++ b/contracts/utils/addresses.js @@ -34,6 +34,7 @@ addresses.mainnet.aTUSD = "--"; // Todo: use v2 addresses.mainnet.aUSDT = "0x3Ed3B47Dd13EC9a98b44e6204A523E766B225811"; // v2 addresses.mainnet.aDAI = "0x028171bca77440897b824ca71d1c56cac55b68a3"; // v2 addresses.mainnet.aUSDC = "0xBcca60bB61934080951369a648Fb03DF4F96263C"; // v2 +addresses.mainnet.aWETH = "0x030ba81f1c18d280636f32af80b9aad02cf0854e"; // v2 addresses.mainnet.STKAAVE = "0x4da27a545c0c5b758a6ba100e3a049001de870f5"; // v1-v2 addresses.mainnet.AAVE_INCENTIVES_CONTROLLER = "0xd784927Ff2f95ba542BfC824c8a8a98F3495f6b5"; // v2