diff --git a/src/MetaMorpho.sol b/src/MetaMorpho.sol index 59da4363..81148f6e 100644 --- a/src/MetaMorpho.sol +++ b/src/MetaMorpho.sol @@ -235,7 +235,7 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph if (newFee > ConstantsLib.MAX_FEE) revert ErrorsLib.MaxFeeExceeded(); if (newFee != 0 && feeRecipient == address(0)) revert ErrorsLib.ZeroFeeRecipient(); - // Accrue interest using the previous fee set before changing it. + // Accrue fee using the previous fee set before changing it. _updateLastTotalAssets(_accrueFee()); // Safe "unchecked" cast because newFee <= MAX_FEE. @@ -249,7 +249,7 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph if (newFeeRecipient == feeRecipient) revert ErrorsLib.AlreadySet(); if (newFeeRecipient == address(0) && fee != 0) revert ErrorsLib.ZeroFeeRecipient(); - // Accrue interest to the previous fee recipient set before changing it. + // Accrue fee to the previous fee recipient set before changing it. _updateLastTotalAssets(_accrueFee()); feeRecipient = newFeeRecipient; @@ -300,7 +300,7 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph /// @inheritdoc IMetaMorphoBase function submitMarketRemoval(Id id) external onlyCuratorRole { if (config[id].removableAt != 0) revert ErrorsLib.AlreadySet(); - if (!config[id].enabled) revert ErrorsLib.MarketNotEnabled(); + if (!config[id].enabled) revert ErrorsLib.MarketNotEnabled(id); _setCap(id, 0); @@ -381,7 +381,7 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph uint256 withdrawn = supplyAssets.zeroFloorSub(allocation.assets); if (withdrawn > 0) { - if (allocation.marketParams.loanToken != asset()) revert ErrorsLib.InconsistentAsset(id); + if (!config[id].enabled) revert ErrorsLib.MarketNotEnabled(id); // Guarantees that unknown frontrunning donations can be withdrawn, in order to disable a market. uint256 shares; diff --git a/src/interfaces/IMetaMorpho.sol b/src/interfaces/IMetaMorpho.sol index 87b4b6fe..bb833c97 100644 --- a/src/interfaces/IMetaMorpho.sol +++ b/src/interfaces/IMetaMorpho.sol @@ -99,6 +99,12 @@ interface IMetaMorphoBase { function revokePendingCap(Id id) external; /// @notice Submits a forced market removal from the vault, eventually losing all funds supplied to the market. + /// @notice Funds can be recovered by enabling this market again and withdrawing from it (using `reallocate`), + /// but funds will be distributed pro-rata to the shares at the time of withdrawal, not at the time of removal. + /// @notice This forced removal is expected to be used as an emergency process in case a market constantly reverts. + /// To softly remove a sane market, the curator role is expected to bundle a reallocation that empties the market + /// first (using `reallocate`), followed by the removal of the market (using `updateWithdrawQueue`). + /// @dev Warning: Removing a market with non-zero supply will instantly impact the vault's price per share. function submitMarketRemoval(Id id) external; /// @notice Revokes the pending removal of the market defined by `id`. diff --git a/src/libraries/ErrorsLib.sol b/src/libraries/ErrorsLib.sol index c5f3ed6d..1e54c83a 100644 --- a/src/libraries/ErrorsLib.sol +++ b/src/libraries/ErrorsLib.sol @@ -63,8 +63,8 @@ library ErrorsLib { /// @notice Thrown when submitting a cap for a market which does not exist. error MarketNotCreated(); - /// @notice Thrown when submitting a non previously enabled market for removal. - error MarketNotEnabled(); + /// @notice Thrown when interacting with a non previously enabled market `id`. + error MarketNotEnabled(Id id); /// @notice Thrown when the submitted timelock is above the max timelock. error AboveMaxTimelock(); diff --git a/test/forge/ReallocateWithdrawTest.sol b/test/forge/ReallocateWithdrawTest.sol index a335e18e..5e0d0868 100644 --- a/test/forge/ReallocateWithdrawTest.sol +++ b/test/forge/ReallocateWithdrawTest.sol @@ -59,7 +59,7 @@ contract ReallocateWithdrawTest is IntegrationTest { assertEq(_idle(), INITIAL_DEPOSIT, "idle"); } - function testReallocateWithdrawInconsistentAsset() public { + function testReallocateWithdrawMarketNotEnabled() public { ERC20Mock loanToken2 = new ERC20Mock("loan2", "B2"); allMarkets[0].loanToken = address(loanToken2); @@ -75,7 +75,7 @@ contract ReallocateWithdrawTest is IntegrationTest { allocations.push(MarketAllocation(allMarkets[0], 0)); vm.prank(ALLOCATOR); - vm.expectRevert(abi.encodeWithSelector(ErrorsLib.InconsistentAsset.selector, allMarkets[0].id())); + vm.expectRevert(abi.encodeWithSelector(ErrorsLib.MarketNotEnabled.selector, allMarkets[0].id())); vault.reallocate(allocations); } diff --git a/test/forge/TimelockTest.sol b/test/forge/TimelockTest.sol index 3ca97a1c..d8b3bc23 100644 --- a/test/forge/TimelockTest.sol +++ b/test/forge/TimelockTest.sol @@ -455,7 +455,7 @@ contract TimelockTest is IntegrationTest { } function testSubmitMarketRemovalMarketNotEnabled() public { - vm.expectRevert(ErrorsLib.MarketNotEnabled.selector); + vm.expectRevert(abi.encodeWithSelector(ErrorsLib.MarketNotEnabled.selector, allMarkets[1].id())); vm.prank(CURATOR); vault.submitMarketRemoval(allMarkets[1].id()); }