diff --git a/.gitmodules b/.gitmodules index d8c5e043..6012570f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,7 +7,6 @@ [submodule "lib/morpho-blue"] path = lib/morpho-blue url = https://github.com/morpho-org/morpho-blue - branch = chore/remove-remapping-context [submodule "lib/universal-rewards-distributor"] path = lib/universal-rewards-distributor url = https://github.com/morpho-org/universal-rewards-distributor diff --git a/README.md b/README.md index 7ce38132..b7dce9e6 100644 --- a/README.md +++ b/README.md @@ -28,8 +28,8 @@ Those rewards can be transferred to the `skimRecipient`. The vault's owner has the choice to distribute back these rewards to vault depositors however they want. For more information about this use case, see the [Rewards](#rewards) section. -All actions that may be against users' interests (e.g. enabling a market with a high exposure, increasing the fee) are subject to a timelock of minimum 12 hours. -If set, the `guardian` can revoke the action during the timelock except for the fee increase. +All actions that may be against users' interests (e.g. enabling a market with a high exposure) are subject to a timelock of minimum 24 hours. +If set, the `guardian` can revoke the action during the timelock. After the timelock, the action can be executed by anyone. ### Roles @@ -48,8 +48,8 @@ It can: - Set the rewards recipient. - Increase the timelock. - [Timelocked] Decrease the timelock. -- [Timelocked with no possible veto] Set the performance fee (capped to 50%). - [Timelocked] Set the guardian. +- Set the performance fee (capped at 50%). - Set the fee recipient. #### Curator @@ -67,6 +67,7 @@ It can: - After the timelock has elapsed, the allocator role is free to remove the market from the withdraw queue. The funds supplied to this market will be lost. - If the market ever functions again, the allocator role can withdraw the funds that were previously lost. - Revoke the pending cap of any market. +- Revoke the pending removal of any market. #### Allocator @@ -76,7 +77,7 @@ It can: - Set the `supplyQueue` and `withdrawQueue`, i.e. decide on the order of the markets to supply/withdraw from. - Upon a deposit, the vault will supply up to the cap of each Morpho Blue market in the `supplyQueue` in the order set. - - Upon a withdrawal, the vault will first withdraw from the idle supply and then withdraw up to the liquidity of each Morpho Blue market in the `withdrawalQueue` in the order set. + - Upon a withdrawal, the vault will withdraw up to the liquidity of each Morpho Blue market in the `withdrawQueue` in the order set. - The `supplyQueue` only contains markets which cap has previously been non-zero. - The `withdrawQueue` contains all markets that have a non-zero cap or a non-zero vault allocation. - Instantaneously reallocate funds by supplying on markets of the `withdrawQueue` and withdrawing from markets that have the same loan asset as the vault's asset. @@ -93,6 +94,7 @@ It can: - Revoke the pending timelock. - Revoke the pending guardian (which means it can revoke any attempt to change the guardian). - Revoke the pending cap of any market. +- Revoke the pending removal of any market. ### Idle Supply diff --git a/lib/morpho-blue b/lib/morpho-blue index f463e40f..55d2d993 160000 --- a/lib/morpho-blue +++ b/lib/morpho-blue @@ -1 +1 @@ -Subproject commit f463e40f776acd0f26d0d380b51cfd02949c8c23 +Subproject commit 55d2d99304fb3fb930c688462ae2ccabb1d533ad diff --git a/lib/universal-rewards-distributor b/lib/universal-rewards-distributor index 94a604c9..8aaa0099 160000 --- a/lib/universal-rewards-distributor +++ b/lib/universal-rewards-distributor @@ -1 +1 @@ -Subproject commit 94a604c926a4878661f38a8f3b05cd61c95c7b84 +Subproject commit 8aaa0099db6cef311a2b02e04b930bfe9b6a6e4e diff --git a/src/MetaMorpho.sol b/src/MetaMorpho.sol index c2749d59..6b802165 100644 --- a/src/MetaMorpho.sol +++ b/src/MetaMorpho.sol @@ -738,17 +738,13 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph if (supplyCap > 0) { if (!marketConfig.enabled) { - supplyQueue.push(id); withdrawQueue.push(id); - if ( - supplyQueue.length > ConstantsLib.MAX_QUEUE_LENGTH - || withdrawQueue.length > ConstantsLib.MAX_QUEUE_LENGTH - ) { - revert ErrorsLib.MaxQueueLengthExceeded(); - } + if (withdrawQueue.length > ConstantsLib.MAX_QUEUE_LENGTH) revert ErrorsLib.MaxQueueLengthExceeded(); marketConfig.enabled = true; + + emit EventsLib.SetWithdrawQueue(msg.sender, withdrawQueue); } marketConfig.removableAt = 0; diff --git a/src/interfaces/IMetaMorpho.sol b/src/interfaces/IMetaMorpho.sol index cb165863..105bb644 100644 --- a/src/interfaces/IMetaMorpho.sol +++ b/src/interfaces/IMetaMorpho.sol @@ -96,8 +96,7 @@ interface IMetaMorphoBase { /// @notice Revokes the pending cap of the market defined by `id`. function revokePendingCap(Id id) external; - /// @notice Submits a forced market removal from the vault, potentially losing all funds supplied to the market. - /// @dev Warning: Submitting a forced removal will overwrite the timestamp at which the market will be removable. + /// @notice Submits a forced market removal from the vault, eventually losing all funds supplied to the market. function submitMarketRemoval(Id id) external; /// @notice Revokes the pending removal of the market defined by `id`. @@ -142,9 +141,11 @@ interface IMetaMorphoBase { /// @notice Sets the withdraw queue as a permutation of the previous one, although markets with both zero cap and /// zero vault's supply can be removed from the permutation. /// @notice This is the only entry point to disable a market. - /// @notice Removing a market requires the vault to have 0 supply on it; but anyone can supply on behalf of the - /// vault so the call to `updateWithdrawQueue` can be griefed by a frontrun. To circumvent this, the allocator can - /// simply bundle a reallocation that withdraws max from this market with a call to `updateWithdrawQueue`. + /// @notice Removing a market requires the vault to have 0 supply on it, or to have previously submitted a removal + /// for this market (with the function `submitMarketRemoval`). + /// @notice Warning: Anyone can supply on behalf of the vault so the call to `updateWithdrawQueue` that expects a + /// market to be empty can be griefed by a front-run. To circumvent this, the allocator can simply bundle a + /// reallocation that withdraws max from this market with a call to `updateWithdrawQueue`. /// @dev Warning: Removing a market with supply will decrease the fee accrued until the next deposit/withdrawal. /// @param indexes The indexes of each market in the previous withdraw queue, in the new withdraw queue's order. function updateWithdrawQueue(uint256[] calldata indexes) external; @@ -158,6 +159,8 @@ interface IMetaMorphoBase { /// reallocation. /// - Donations to the vault on markets that are expected to be supplied to during reallocation. /// - Withdrawals from markets that are expected to be withdrawn from during reallocation. + /// @dev Sender is expected to pass `assets = type(uint256).max` with the last MarketAllocation of `allocations` to + /// supply all the remaining withdrawn liquidity, which would ensure that `totalWithdrawn` = `totalSupplied`. function reallocate(MarketAllocation[] calldata allocations) external; } diff --git a/test/forge/MetaMorphoInternalTest.sol b/test/forge/MetaMorphoInternalTest.sol index 8ea5c4bd..5764f19b 100644 --- a/test/forge/MetaMorphoInternalTest.sol +++ b/test/forge/MetaMorphoInternalTest.sol @@ -31,6 +31,7 @@ contract MetaMorphoInternalTest is InternalTest { Id id = allMarkets[0].id(); _setCap(id, CAP); + supplyQueue = [id]; loanToken.setBalance(SUPPLIER, suppliedAmount); vm.prank(SUPPLIER); diff --git a/test/forge/helpers/IntegrationTest.sol b/test/forge/helpers/IntegrationTest.sol index 5f866fa9..11021b53 100644 --- a/test/forge/helpers/IntegrationTest.sol +++ b/test/forge/helpers/IntegrationTest.sol @@ -100,6 +100,7 @@ contract IntegrationTest is BaseTest { function _setCap(MarketParams memory marketParams, uint256 newCap) internal { Id id = marketParams.id(); uint256 cap = vault.config(id).cap; + bool isEnabled = vault.config(id).enabled; if (newCap == cap) return; PendingUint192 memory pendingCap = vault.pendingCap(id); @@ -115,6 +116,18 @@ contract IntegrationTest is BaseTest { vault.acceptCap(id); assertEq(vault.config(id).cap, newCap, "_setCap"); + + if (newCap > 0) { + if (!isEnabled) { + Id[] memory newSupplyQueue = new Id[](vault.supplyQueueLength() + 1); + for (uint256 k; k < vault.supplyQueueLength(); k++) { + newSupplyQueue[k] = vault.supplyQueue(k); + } + newSupplyQueue[vault.supplyQueueLength()] = id; + vm.prank(ALLOCATOR); + vault.setSupplyQueue(newSupplyQueue); + } + } } function _sortSupplyQueueIdleLast() internal {