diff --git a/.gitignore b/.gitignore index 127e456554..83ad641d0e 100644 --- a/.gitignore +++ b/.gitignore @@ -90,3 +90,6 @@ unit-coverage # Certora # .certora_internal + +# Possible Agent.md file +AGENTS.md \ No newline at end of file diff --git a/contracts/contracts/interfaces/morpho/IMorphoV2Adapter.sol b/contracts/contracts/interfaces/morpho/IMorphoV2Adapter.sol new file mode 100644 index 0000000000..313d6c5b35 --- /dev/null +++ b/contracts/contracts/interfaces/morpho/IMorphoV2Adapter.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +interface IMorphoV2Adapter { + // address of the underlying vault + function morphoVaultV1() external view returns (address); + + // address of the parent Morpho V2 vault + function parentVault() external view returns (address); +} diff --git a/contracts/contracts/interfaces/morpho/IVaultV2.sol b/contracts/contracts/interfaces/morpho/IVaultV2.sol new file mode 100644 index 0000000000..354e6e15aa --- /dev/null +++ b/contracts/contracts/interfaces/morpho/IVaultV2.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import { IERC4626 } from "../../../lib/openzeppelin/interfaces/IERC4626.sol"; + +interface IVaultV2 is IERC4626 { + function liquidityAdapter() external view returns (address); +} diff --git a/contracts/contracts/mocks/MockMorphoV1Vault.sol b/contracts/contracts/mocks/MockMorphoV1Vault.sol new file mode 100644 index 0000000000..23614a052f --- /dev/null +++ b/contracts/contracts/mocks/MockMorphoV1Vault.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import { MockERC4626Vault } from "./MockERC4626Vault.sol"; + +contract MockMorphoV1Vault is MockERC4626Vault { + address public liquidityAdapter; + + constructor(address _asset) MockERC4626Vault(_asset) {} + + function setLiquidityAdapter(address _liquidityAdapter) external { + liquidityAdapter = _liquidityAdapter; + } +} diff --git a/contracts/contracts/mocks/MockMorphoV1VaultLiquidityAdapter.sol b/contracts/contracts/mocks/MockMorphoV1VaultLiquidityAdapter.sol new file mode 100644 index 0000000000..9d87b9d423 --- /dev/null +++ b/contracts/contracts/mocks/MockMorphoV1VaultLiquidityAdapter.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import { IMorphoV2Adapter } from "../interfaces/morpho/IMorphoV2Adapter.sol"; + +contract MockMorphoV1VaultLiquidityAdapter is IMorphoV2Adapter { + address public mockMorphoVault; + + function setMockMorphoVault(address _mockMorphoVault) external { + mockMorphoVault = _mockMorphoVault; + } + + function morphoVaultV1() external view override returns (address) { + return mockMorphoVault; + } + + function parentVault() external view override returns (address) { + return mockMorphoVault; + } +} diff --git a/contracts/contracts/strategies/Generalized4626Strategy.sol b/contracts/contracts/strategies/Generalized4626Strategy.sol index 931e2cfefc..b4793db7de 100644 --- a/contracts/contracts/strategies/Generalized4626Strategy.sol +++ b/contracts/contracts/strategies/Generalized4626Strategy.sol @@ -4,6 +4,9 @@ pragma solidity ^0.8.0; /** * @title Generalized 4626 Strategy * @notice Investment strategy for ERC-4626 Tokenized Vaults + * @dev This strategy should not be used for the Morpho V2 Vaults as those are not + * completley ERC-4626 compliant - they don't implement the maxWithdraw() and + * maxRedeem() functions and rather return 0 when any of them is called. * @author Origin Protocol Inc */ import { IERC4626 } from "../../lib/openzeppelin/interfaces/IERC4626.sol"; @@ -141,11 +144,13 @@ contract Generalized4626Strategy is InitializableAbstractStrategy { onlyVaultOrGovernor nonReentrant { - uint256 shareBalance = shareToken.balanceOf(address(this)); + // @dev Don't use for Morpho V2 Vaults as below line will return 0 + uint256 sharesToRedeem = IERC4626(platformAddress).maxRedeem(address(this)); + uint256 assetAmount = 0; - if (shareBalance > 0) { + if (sharesToRedeem > 0) { assetAmount = IERC4626(platformAddress).redeem( - shareBalance, + sharesToRedeem, vaultAddress, address(this) ); diff --git a/contracts/contracts/strategies/MorphoV2Strategy.sol b/contracts/contracts/strategies/MorphoV2Strategy.sol new file mode 100644 index 0000000000..a963398e53 --- /dev/null +++ b/contracts/contracts/strategies/MorphoV2Strategy.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +/** + * @title Generalized 4626 Strategy when the underlying platform is Morpho V2 + * @notice Investment strategy for ERC-4626 Tokenized Vaults for the Morpho V2 platform. + * @author Origin Protocol Inc + */ +import { Generalized4626Strategy } from "./Generalized4626Strategy.sol"; +import { MorphoV2VaultUtils } from "./MorphoV2VaultUtils.sol"; +import { IVaultV2 } from "../interfaces/morpho/IVaultV2.sol"; +import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; + +contract MorphoV2Strategy is Generalized4626Strategy { + + /** + * @param _baseConfig Base strategy config with Morpho V2 Vault and + * vaultAddress (OToken Vault contract), eg VaultProxy or OETHVaultProxy + * @param _assetToken Address of the ERC-4626 asset token. e.g. USDC + */ + constructor(BaseStrategyConfig memory _baseConfig, address _assetToken) + Generalized4626Strategy(_baseConfig, _assetToken) + {} + + /** + * @notice Remove all the liquidity that is available in the Morpho V2 vault. + * Which might not be all of the liquidity owned by the strategy. + * @dev Remove all the liquidity that is available in the Morpho V2 vault + * The particular behaviour of the Morpho V2 vault is that it can hold + * multiple Morpho V1 vaults as adapters but only one liquidity adapter. + * The immediate available funds on the Morpho V2 vault are therfore any + * liquid assets residing on the Vault V2 contract and the maxWithdraw + * amount that the Morpho V1 contract can supply. + */ + function withdrawAll() + external + virtual + override + onlyVaultOrGovernor + nonReentrant + { + uint256 availableMorphoVault = _maxWithdraw(); + uint256 balanceToWithdraw = Math.min( + availableMorphoVault, + checkBalance(address(assetToken)) + ); + + if (balanceToWithdraw > 0) { + // slither-disable-next-line unused-return + IVaultV2(platformAddress).withdraw( + balanceToWithdraw, + vaultAddress, + address(this) + ); + } + + emit Withdrawal( + address(assetToken), + address(shareToken), + balanceToWithdraw + ); + } + + function maxWithdraw() external view returns (uint256) { + return _maxWithdraw(); + } + + function _maxWithdraw() + internal + view + returns (uint256 availableAssetLiquidity) + { + availableAssetLiquidity = MorphoV2VaultUtils.maxWithdrawableAssets( + platformAddress, + address(assetToken) + ); + } +} diff --git a/contracts/contracts/strategies/MorphoV2VaultUtils.sol b/contracts/contracts/strategies/MorphoV2VaultUtils.sol new file mode 100644 index 0000000000..02fcad5bd7 --- /dev/null +++ b/contracts/contracts/strategies/MorphoV2VaultUtils.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import { IERC20 } from "../utils/InitializableAbstractStrategy.sol"; +import { IERC4626 } from "../../lib/openzeppelin/interfaces/IERC4626.sol"; +import { IVaultV2 } from "../interfaces/morpho/IVaultV2.sol"; +import { IMorphoV2Adapter } from "../interfaces/morpho/IMorphoV2Adapter.sol"; + +library MorphoV2VaultUtils { + error IncompatibleAdapter(address adapter); + + /** + * @notice Return maximum amount that can be safely withdrawn from a Morpho V2 vault. + * @dev Available liquidity is: + * 1) asset balance parked on Morpho V2 vault contract + * 2) additional liquidity from the active adapter if it resolves to a Morpho V1 vault + * and, when provided, matches the expected adapter + */ + function maxWithdrawableAssets( + address platformAddress, + address assetToken + ) internal view returns (uint256 availableAssetLiquidity) { + availableAssetLiquidity = IERC20(assetToken).balanceOf(platformAddress); + + address liquidityAdapter = IVaultV2(platformAddress).liquidityAdapter(); + // this is a sufficient check to ensure the adapter is Morpho V1 + try IMorphoV2Adapter(liquidityAdapter).morphoVaultV1() returns ( + address underlyingVault + ) { + availableAssetLiquidity += IERC4626(underlyingVault).maxWithdraw( + liquidityAdapter + ); + } catch { + revert IncompatibleAdapter(liquidityAdapter); + } + } + +} diff --git a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol index 6902c66ade..aa1c10e224 100644 --- a/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol +++ b/contracts/contracts/strategies/crosschain/CrossChainRemoteStrategy.sol @@ -11,13 +11,16 @@ pragma solidity ^0.8.0; */ import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; import { IERC20 } from "../../utils/InitializableAbstractStrategy.sol"; import { IERC4626 } from "../../../lib/openzeppelin/interfaces/IERC4626.sol"; +import { IVaultV2 } from "../../interfaces/morpho/IVaultV2.sol"; import { Generalized4626Strategy } from "../Generalized4626Strategy.sol"; import { AbstractCCTPIntegrator } from "./AbstractCCTPIntegrator.sol"; import { CrossChainStrategyHelper } from "./CrossChainStrategyHelper.sol"; import { InitializableAbstractStrategy } from "../../utils/InitializableAbstractStrategy.sol"; import { Strategizable } from "../../governance/Strategizable.sol"; +import { MorphoV2VaultUtils } from "../MorphoV2VaultUtils.sol"; contract CrossChainRemoteStrategy is AbstractCCTPIntegrator, @@ -140,11 +143,22 @@ contract CrossChainRemoteStrategy is nonReentrant { IERC4626 platform = IERC4626(platformAddress); - _withdraw( - address(this), - usdcToken, + uint256 availableMorphoVault = MorphoV2VaultUtils.maxWithdrawableAssets( + platformAddress, + usdcToken + ); + uint256 amountToWithdraw = Math.min( + availableMorphoVault, platform.previewRedeem(platform.balanceOf(address(this))) ); + + if (amountToWithdraw > 0) { + _withdraw( + address(this), + usdcToken, + amountToWithdraw + ); + } } /// @inheritdoc AbstractCCTPIntegrator diff --git a/contracts/deploy/base/045_crosschain_upgrade_remote.js b/contracts/deploy/base/045_crosschain_upgrade_remote.js new file mode 100644 index 0000000000..c0fcf4179d --- /dev/null +++ b/contracts/deploy/base/045_crosschain_upgrade_remote.js @@ -0,0 +1,51 @@ +const { deployOnBase } = require("../../utils/deploy-l2"); +const addresses = require("../../utils/addresses"); +const { + deployCrossChainRemoteStrategyImpl, + getCreate2ProxyAddress, +} = require("../deployActions"); +const { cctpDomainIds } = require("../../utils/cctp"); + +module.exports = deployOnBase( + { + deployName: "045_crosschain_upgrade_remote", + }, + async () => { + const crossChainStrategyProxyAddress = await getCreate2ProxyAddress( + "CrossChainStrategyProxy" + ); + console.log( + `CrossChainStrategyProxy address: ${crossChainStrategyProxyAddress}` + ); + + const implAddress = await deployCrossChainRemoteStrategyImpl( + addresses.base.MorphoOusdV2Vault, // 4626 Vault + crossChainStrategyProxyAddress, + cctpDomainIds.Ethereum, + crossChainStrategyProxyAddress, + addresses.base.USDC, + addresses.mainnet.USDC, + "CrossChainRemoteStrategy", + addresses.CCTPTokenMessengerV2, + addresses.CCTPMessageTransmitterV2, + addresses.base.timelock, + false + ); + console.log(`CrossChainRemoteStrategyImpl address: ${implAddress}`); + + const cCrossChainStrategyProxy = await ethers.getContractAt( + "CrossChainStrategyProxy", + crossChainStrategyProxyAddress + ); + + return { + actions: [ + { + contract: cCrossChainStrategyProxy, + signature: "upgradeTo(address)", + args: [implAddress], + }, + ], + }; + } +); diff --git a/contracts/deploy/deployActions.js b/contracts/deploy/deployActions.js index 7de245e46c..60b96e7665 100644 --- a/contracts/deploy/deployActions.js +++ b/contracts/deploy/deployActions.js @@ -1131,7 +1131,7 @@ const deployCrossChainUnitTestStrategy = async (usdcAddress) => { "CCTPMessageTransmitterMock" ); const tokenMessenger = await ethers.getContract("CCTPTokenMessengerMock"); - const c4626Vault = await ethers.getContract("MockERC4626Vault"); + const c4626Vault = await ethers.getContract("MockMorphoV1Vault"); await deployCrossChainMasterStrategyImpl( dMasterProxy.address, diff --git a/contracts/deploy/mainnet/000_mock.js b/contracts/deploy/mainnet/000_mock.js index 29760f1dc4..924c0a9cf6 100644 --- a/contracts/deploy/mainnet/000_mock.js +++ b/contracts/deploy/mainnet/000_mock.js @@ -114,6 +114,13 @@ const deployMocks = async ({ getNamedAccounts, deployments }) => { from: deployerAddr, args: [usdc.address], }); + await deploy("MockMorphoV1Vault", { + from: deployerAddr, + args: [usdc.address], + }); + await deploy("MockMorphoV1VaultLiquidityAdapter", { + from: deployerAddr, + }); // const tokenMessenger = await ethers.getContract("CCTPTokenMessengerMock"); // await messageTransmitter // .connect(sDeployer) diff --git a/contracts/deploy/mainnet/179_upgrade_ousd_morpho_v2_strategy.js b/contracts/deploy/mainnet/179_upgrade_ousd_morpho_v2_strategy.js new file mode 100644 index 0000000000..810103568c --- /dev/null +++ b/contracts/deploy/mainnet/179_upgrade_ousd_morpho_v2_strategy.js @@ -0,0 +1,47 @@ +const addresses = require("../../utils/addresses"); +const { deploymentWithGovernanceProposal } = require("../../utils/deploy"); + +module.exports = deploymentWithGovernanceProposal( + { + deployName: "179_upgrade_ousd_morpho_v2_strategy", + forceDeploy: false, + reduceQueueTime: true, + deployerIsProposer: false, + proposalId: "", + }, + async ({ deployWithConfirmation }) => { + const cVaultProxy = await ethers.getContract("VaultProxy"); + const cOUSDMorphoV2StrategyProxy = await ethers.getContract( + "OUSDMorphoV2StrategyProxy" + ); + + const dMorphoV2StrategyImpl = await deployWithConfirmation( + "MorphoV2Strategy", + [ + [addresses.mainnet.MorphoOUSDv2Vault, cVaultProxy.address], + addresses.mainnet.USDC + ] + ); + + const cMorphoV2Strategy = await ethers.getContractAt( + "MorphoV2Strategy", + cOUSDMorphoV2StrategyProxy.address + ); + + return { + name: "Upgrade OUSD Morpho V2 strategy implementation", + actions: [ + { + contract: cOUSDMorphoV2StrategyProxy, + signature: "upgradeTo(address)", + args: [dMorphoV2StrategyImpl.address], + }, + { + contract: cMorphoV2Strategy, + signature: "setHarvesterAddress(address)", + args: [addresses.multichainStrategist], + }, + ], + }; + } +); diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index 28aba6a6e8..c3c89d211d 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -1304,7 +1304,16 @@ async function crossChainFixtureUnit() { .connect(governor) .setOperator(messageTransmitter.address); - const morphoVault = await ethers.getContract("MockERC4626Vault"); + const morphoVault = await ethers.getContract("MockMorphoV1Vault"); + const morphoVaultLiquidityAdapter = await ethers.getContract( + "MockMorphoV1VaultLiquidityAdapter" + ); + await morphoVault + .connect(governor) + .setLiquidityAdapter(morphoVaultLiquidityAdapter.address); + await morphoVaultLiquidityAdapter + .connect(governor) + .setMockMorphoVault(morphoVault.address); // Impersonate the OUSD Vault fixture.vaultSigner = await impersonateAndFund(vault.address); @@ -1324,6 +1333,7 @@ async function crossChainFixtureUnit() { messageTransmitter: messageTransmitter, tokenMessenger: tokenMessenger, morphoVault: morphoVault, + morphoVaultLiquidityAdapter: morphoVaultLiquidityAdapter, }; } diff --git a/contracts/test/strategies/crosschain/cross-chain-strategy.js b/contracts/test/strategies/crosschain/cross-chain-strategy.js index bef6fdd50e..4702d408bf 100644 --- a/contracts/test/strategies/crosschain/cross-chain-strategy.js +++ b/contracts/test/strategies/crosschain/cross-chain-strategy.js @@ -89,6 +89,33 @@ describe("ForkTest: CrossChainRemoteStrategy", function () { await crossChainRemoteStrategy.connect(governor).sendBalanceUpdate(); }; + it("Should wire morpho vault and liquidity adapter in fixture", async function () { + const { morphoVault, morphoVaultLiquidityAdapter } = fixture; + await expect(await morphoVault.liquidityAdapter()).to.eq( + morphoVaultLiquidityAdapter.address + ); + await expect(await morphoVaultLiquidityAdapter.morphoVaultV1()).to.eq( + morphoVault.address + ); + await expect(await morphoVaultLiquidityAdapter.parentVault()).to.eq( + morphoVault.address + ); + }); + + it("Should revert withdrawAll when morpho vault liquidity adapter is incompatible", async function () { + const { morphoVault } = fixture; + + // Misconfigure adapter to an invalid value that does not implement IMorphoV2Adapter. + await morphoVault + .connect(governor) + .setLiquidityAdapter(morphoVault.address); + + await expect(crossChainRemoteStrategy.connect(governor).withdrawAll()) + .to.be.revertedWithCustomError( + "IncompatibleAdapter(address)" + ); + }); + // Checks the diff in the total expected value in the vault // (plus accompanying strategy value) const assertVaultTotalValue = async (amountExpected) => { @@ -340,6 +367,9 @@ describe("ForkTest: CrossChainRemoteStrategy", function () { await expect( await crossChainRemoteStrategy.checkBalance(usdc.address) ).to.eq(await units("0", usdc)); + + // calling withdrawAll a second time should not fail + await directWithdrawAllFromRemoteStrategy(); }); it("Should fail when a withdrawal too large is requested on the remote strategy", async function () { diff --git a/contracts/test/strategies/crosschain/crosschain-remote-strategy.base.fork-test.js b/contracts/test/strategies/crosschain/crosschain-remote-strategy.base.fork-test.js index d6c754a06e..b2ed8d3a15 100644 --- a/contracts/test/strategies/crosschain/crosschain-remote-strategy.base.fork-test.js +++ b/contracts/test/strategies/crosschain/crosschain-remote-strategy.base.fork-test.js @@ -249,6 +249,51 @@ describe("ForkTest: CrossChainRemoteStrategy", function () { expect(balanceAfter).to.approxEqual(expectedBalance); }); + it("Should handle single withdrawAll", async function () { + const { crossChainRemoteStrategy, strategist, rafael, usdc } = fixture; + const fundedAmount = usdcUnits("1234.56"); + + const usdcBalanceBefore = await usdc.balanceOf( + crossChainRemoteStrategy.address + ); + + await usdc + .connect(rafael) + .transfer(crossChainRemoteStrategy.address, fundedAmount); + + await expect(crossChainRemoteStrategy.connect(strategist).withdrawAll()).to + .not.be.reverted; + + const usdcBalanceAfter = await usdc.balanceOf( + crossChainRemoteStrategy.address + ); + expect(usdcBalanceAfter).to.gte(usdcBalanceBefore.add(fundedAmount)); + }); + + it("Should allow calling withdrawAll twice", async function () { + const { crossChainRemoteStrategy, strategist, rafael, usdc } = fixture; + const fundedAmount = usdcUnits("1234.56"); + + await usdc + .connect(rafael) + .transfer(crossChainRemoteStrategy.address, fundedAmount); + + await expect(crossChainRemoteStrategy.connect(strategist).withdrawAll()).to + .not.be.reverted; + + const usdcBalanceAfterFirst = await usdc.balanceOf( + crossChainRemoteStrategy.address + ); + + await expect(crossChainRemoteStrategy.connect(strategist).withdrawAll()).to + .not.be.reverted; + + const usdcBalanceAfterSecond = await usdc.balanceOf( + crossChainRemoteStrategy.address + ); + expect(usdcBalanceAfterSecond).to.eq(usdcBalanceAfterFirst); + }); + it("Should revert if the burn token is not peer USDC", async function () { const { crossChainRemoteStrategy, relayer } = fixture; diff --git a/contracts/test/strategies/ousd-v2-morpho.mainnet.fork-test.js b/contracts/test/strategies/ousd-v2-morpho.mainnet.fork-test.js index 2b8eff2cc3..5ec44e5691 100644 --- a/contracts/test/strategies/ousd-v2-morpho.mainnet.fork-test.js +++ b/contracts/test/strategies/ousd-v2-morpho.mainnet.fork-test.js @@ -2,7 +2,7 @@ const { expect } = require("chai"); const { formatUnits, parseUnits } = require("ethers/lib/utils"); const addresses = require("../../utils/addresses"); -const { canWithdrawAllFromMorphoOUSD } = require("../../utils/morpho"); +const { morphoWithdrawShortfall } = require("../../utils/morpho"); const { getMerklRewards } = require("../../tasks/merkl"); const { units, isCI } = require("../helpers"); @@ -46,6 +46,9 @@ describe("ForkTest: Yearn's Morpho OUSD v2 Strategy", function () { expect(await morphoOUSDv2Strategy.governor()).to.equal( addresses.mainnet.Timelock ); + expect(await morphoOUSDv2Strategy.harvesterAddress()).to.equal( + addresses.multichainStrategist + ); }); it("Should be able to check balance", async () => { const { usdc, josh, morphoOUSDv2Strategy } = fixture; @@ -223,15 +226,21 @@ describe("ForkTest: Yearn's Morpho OUSD v2 Strategy", function () { const strategyVaultShares = await morphoOUSDv2Vault.balanceOf( morphoOUSDv2Strategy.address ); + const withdrawAllShortfall = await morphoWithdrawShortfall(); const usdcWithdrawAmountExpected = await morphoOUSDv2Vault.convertToAssets(strategyVaultShares); expect(usdcWithdrawAmountExpected).to.be.gte(minBalance.sub(1)); - + // amount expected minus the shortfall due to not enough liquidity in the Morpho OUSD v1 Vault + const usdcWithdrawAmountAvailable = + usdcWithdrawAmountExpected.sub(withdrawAllShortfall); log( - `Expected to withdraw ${formatUnits( + `Wanted to withdraw ${formatUnits( usdcWithdrawAmountExpected, 6 - )} USDC` + )} USDC, adjusted for shortfall of ${formatUnits( + withdrawAllShortfall, + 6 + )} USDC totals to ${formatUnits(usdcWithdrawAmountAvailable, 6)} USDC` ); const ousdSupplyBefore = await ousd.totalSupply(); @@ -239,12 +248,7 @@ describe("ForkTest: Yearn's Morpho OUSD v2 Strategy", function () { log("Before withdraw all from strategy"); - const withdrawAllAllowed = await canWithdrawAllFromMorphoOUSD(); - - // If there is not enough liquidity in the Morpho OUSD v1 Vault, skip the withdrawAll test - if (withdrawAllAllowed === false) return; - - // Now try to withdraw all the WETH from the strategy + // Now try to withdraw all the USDC from the strategy const tx = await morphoOUSDv2Strategy.connect(vaultSigner).withdrawAll(); log("After withdraw all from strategy"); @@ -255,7 +259,7 @@ describe("ForkTest: Yearn's Morpho OUSD v2 Strategy", function () { morphoOUSDv2Vault.address, (amount) => expect(amount).approxEqualTolerance( - usdcWithdrawAmountExpected, + usdcWithdrawAmountAvailable, 0.01, "Withdrawal amount" ), @@ -269,7 +273,7 @@ describe("ForkTest: Yearn's Morpho OUSD v2 Strategy", function () { // Check the USDC amount in the vault increases expect(await usdc.balanceOf(vault.address)).to.approxEqualTolerance( - vaultUSDCBalanceBefore.add(usdcWithdrawAmountExpected), + vaultUSDCBalanceBefore.add(usdcWithdrawAmountAvailable), 0.01 ); }); @@ -336,11 +340,6 @@ describe("ForkTest: Yearn's Morpho OUSD v2 Strategy", function () { it("Only vault and governor can withdraw all USDC from the strategy", async function () { const { morphoOUSDv2Strategy, strategist, timelock, josh } = fixture; - const withdrawAllAllowed = await canWithdrawAllFromMorphoOUSD(); - - // If there is not enough liquidity in the Morpho OUSD v1 Vault, skip the withdrawAll test - if (withdrawAllAllowed === false) return; - for (const signer of [strategist, josh]) { const tx = morphoOUSDv2Strategy.connect(signer).withdrawAll(); diff --git a/contracts/utils/addresses.js b/contracts/utils/addresses.js index 79f1889f08..605aa15473 100644 --- a/contracts/utils/addresses.js +++ b/contracts/utils/addresses.js @@ -216,7 +216,8 @@ addresses.mainnet.MorphoOUSDv2StrategyProxy = "0x3643cafA6eF3dd7Fcc2ADaD1cabf708075AFFf6e"; addresses.mainnet.MorphoOUSDv1Vault = "0x5B8b9FA8e4145eE06025F642cAdB1B47e5F39F04"; -addresses.mainnet.MorphoOUSDv2Adaptor = +// Morpho V1 Vualt adapter used by the Morpho V2 Vault +addresses.mainnet.MorphoOUSDv2Adapter = "0xD8F093dCE8504F10Ac798A978eF9E0C230B2f5fF"; addresses.mainnet.MorphoOUSDv2Vault = "0xFB154c729A16802c4ad1E8f7FF539a8b9f49c960"; diff --git a/contracts/utils/morpho.js b/contracts/utils/morpho.js index 93483db8e1..2abb288a65 100644 --- a/contracts/utils/morpho.js +++ b/contracts/utils/morpho.js @@ -5,7 +5,7 @@ const { getBlock } = require("../tasks/block"); const morphoV1VaultAbi = require("../abi/morphoV1Vault.json"); const { resolveContract } = require("./resolvers"); - +const erc20Abi = require("../abi/erc20.json"); const log = require("../utils/logger")("utils:morpho"); async function canWithdrawAllFromMorphoOUSD() { @@ -24,8 +24,8 @@ async function morphoWithdrawShortfall() { "Generalized4626Strategy" ); - const maxWithdrawal = await morphoOUSDv1Vault.maxWithdraw( - addresses.mainnet.MorphoOUSDv2Adaptor + let maxWithdrawal = await morphoOUSDv1Vault.maxWithdraw( + addresses.mainnet.MorphoOUSDv2Adapter ); // check all funds can be withdrawn from the Morpho OUSD v2 Strategy @@ -33,6 +33,13 @@ async function morphoWithdrawShortfall() { addresses.mainnet.USDC ); + const usdc = await ethers.getContractAt( + erc20Abi, + addresses.mainnet.USDC + ); + const vaultUSDCBalance = await usdc.balanceOf(addresses.mainnet.MorphoOUSDv2Vault); + + maxWithdrawal = maxWithdrawal.add(vaultUSDCBalance); log( `Morpho OUSD v2 Strategy USDC balance: ${formatUnits( strategyUSDCBalance, @@ -74,7 +81,7 @@ async function snapMorpho({ block }) { ); const maxWithdrawal = await morphoOUSDv1Vault.maxWithdraw( - addresses.mainnet.MorphoOUSDv2Adaptor, + addresses.mainnet.MorphoOUSDv2Adapter, { blockTag } );