diff --git a/.github/workflows/foundry.yml b/.github/workflows/foundry.yml index 71134732..767033c6 100644 --- a/.github/workflows/foundry.yml +++ b/.github/workflows/foundry.yml @@ -82,7 +82,7 @@ jobs: - uses: ./.github/actions/install-cache - name: Run tests in ${{ matrix.type }} mode - run: yarn test:forge + run: yarn test:forge -vvv env: FOUNDRY_FUZZ_RUNS: ${{ matrix.fuzz-runs }} FOUNDRY_FUZZ_MAX_TEST_REJECTS: ${{ matrix.max-test-rejects }} 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..4986affd 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. +The `owner`, or the `guardian` if set, 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,20 +94,22 @@ 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 In some cases, the vault's curator or allocators may want to keep some funds "idle", to guarantee lenders that some liquidity can be withdrawn from the vault (beyond the liquidity of each of the vault's markets). -To achieve this, it is advised to allocate "idle" funds to any market on Morpho Blue having: +To achieve this, they can deposit in markets with `address(0)` as the oracle or the collateral, ensuring that these funds can't be borrowed. +They are thus guaranteed to be liquid; though they won't generate interest. +It is advised to use these canonical configurations for "idle" markets: -- The vault's asset as loan token. -- No collateral token (`address(0)`). -- An arbitrary IRM. -- An arbitrary oracle (`address(0)`). -- An arbitrary LLTV. +- `loanToken`: The vault's asset to be able to supply/withdraw funds. +- `collateralToken`: `address(0)` (not necessary since no funds will be borrowed on this market) +- `irm`: `address(0)` (Morpho Blue will skip the call to the IRM in this case, thus reducing the gas cost) +- `oracle`: `address(0)` (not necessary since no funds will be borrowed on this market) +- `lltv`: `0` (not necessary since no funds will be borrowed on this market) -Thus, these funds cannot be borrowed on Morpho Blue and are guaranteed to be liquid; though it won't generate interest. Note that to allocate funds to this idle market, it is first required to enable its cap on MetaMorpho. Enabling an infinite cap (`type(uint184).max`) will always allow users to deposit on the vault. diff --git a/foundry.toml b/foundry.toml index 8a08c4af..f7f60cba 100644 --- a/foundry.toml +++ b/foundry.toml @@ -7,6 +7,7 @@ fs_permissions = [ { access = "read", path = "./config/"}, { access = "read", path = "./lib/morpho-blue/out/"} ] +optimization_runs = 999999 # Etherscan does not support verifying contracts with more optimization runs. [profile.default.rpc_endpoints] mainnet = "https://eth-mainnet.g.alchemy.com/v2/${ALCHEMY_KEY}" 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/package.json b/package.json index 16bcdf9e..9402bec8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "morpho-stack", - "description": "Morpho Stack Contracts", + "name": "metamorpho", + "description": "MetaMorpho", "license": "GPL-2.0-or-later", "version": "1.0.0", "main": "lib/index.js", @@ -27,7 +27,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/morpho-labs/morpho-stack.git" + "url": "git+https://github.com/morpho-org/metamorpho.git" }, "author": { "name": "Morpho Labs", @@ -35,9 +35,9 @@ "url": "https://github.com/morpho-labs" }, "bugs": { - "url": "https://github.com/morpho-labs/morpho-stack/issues" + "url": "https://github.com/morpho-org/metamorpho/issues" }, - "homepage": "https://github.com/morpho-labs/morpho-stack#readme", + "homepage": "https://github.com/morpho-org/metamorpho#readme", "dependencies": { "ethers": "^6.7.1", "ethers-maths": "^5.0.0", diff --git a/src/MetaMorpho.sol b/src/MetaMorpho.sol index ff5d6b47..ad0e6c61 100644 --- a/src/MetaMorpho.sol +++ b/src/MetaMorpho.sol @@ -58,6 +58,11 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph /// @inheritdoc IMetaMorphoBase IMorpho public immutable MORPHO; + /// @notice OpenZeppelin decimals offset used by the ERC4626 implementation. + /// @dev Calculated to be max(0, 18 - underlyingDecimals) at construction, so the initial conversion rate maximizes + /// precision between shares and assets. + uint8 public immutable DECIMALS_OFFSET; + /* STORAGE */ /// @inheritdoc IMetaMorphoBase @@ -122,6 +127,7 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph if (morpho == address(0)) revert ErrorsLib.ZeroAddress(); MORPHO = IMorpho(morpho); + DECIMALS_OFFSET = uint8(uint256(18).zeroFloorSub(IERC20Metadata(_asset).decimals())); _checkTimelockBounds(initialTimelock); _setTimelock(initialTimelock); @@ -208,14 +214,12 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph /// @inheritdoc IMetaMorphoBase function submitTimelock(uint256 newTimelock) external onlyOwner { if (newTimelock == timelock) revert ErrorsLib.AlreadySet(); + if (pendingTimelock.validAt != 0) revert ErrorsLib.AlreadyPending(); _checkTimelockBounds(newTimelock); if (newTimelock > timelock) { _setTimelock(newTimelock); } else { - // newTimelock >= MIN_TIMELOCK > 0 so there's no need to check `pendingTimelock.validAt != 0`. - if (newTimelock == pendingTimelock.value) revert ErrorsLib.AlreadyPending(); - // Safe "unchecked" cast because newTimelock <= MAX_TIMELOCK. pendingTimelock.update(uint184(newTimelock), timelock); @@ -229,7 +233,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. @@ -243,7 +247,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; @@ -254,14 +258,11 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph /// @inheritdoc IMetaMorphoBase function submitGuardian(address newGuardian) external onlyOwner { if (newGuardian == guardian) revert ErrorsLib.AlreadySet(); + if (pendingGuardian.validAt != 0) revert ErrorsLib.AlreadyPending(); if (guardian == address(0)) { _setGuardian(newGuardian); } else { - if (pendingGuardian.validAt != 0 && newGuardian == pendingGuardian.value) { - revert ErrorsLib.AlreadyPending(); - } - pendingGuardian.update(newGuardian, timelock); emit EventsLib.SubmitGuardian(newGuardian); @@ -275,16 +276,14 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph Id id = marketParams.id(); if (marketParams.loanToken != asset()) revert ErrorsLib.InconsistentAsset(id); if (MORPHO.lastUpdate(id) == 0) revert ErrorsLib.MarketNotCreated(); - + if (pendingCap[id].validAt != 0) revert ErrorsLib.AlreadyPending(); + if (config[id].removableAt != 0) revert ErrorsLib.PendingRemoval(); uint256 supplyCap = config[id].cap; if (newSupplyCap == supplyCap) revert ErrorsLib.AlreadySet(); if (newSupplyCap < supplyCap) { - _setCap(id, newSupplyCap.toUint184()); + _setCap(marketParams, id, newSupplyCap.toUint184()); } else { - // newSupplyCap > supplyCap >= 0 so there's no need to check `pendingCap[id].validAt != 0`. - if (newSupplyCap == pendingCap[id].value) revert ErrorsLib.AlreadyPending(); - pendingCap[id].update(newSupplyCap.toUint184(), timelock); emit EventsLib.SubmitCap(_msgSender(), id, newSupplyCap); @@ -292,11 +291,12 @@ 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(); - - _setCap(id, 0); + function submitMarketRemoval(MarketParams memory marketParams) external onlyCuratorRole { + Id id = marketParams.id(); + if (config[id].removableAt != 0) revert ErrorsLib.AlreadyPending(); + if (config[id].cap != 0) revert ErrorsLib.NonZeroCap(); + if (!config[id].enabled) revert ErrorsLib.MarketNotEnabled(id); + if (pendingCap[id].validAt != 0) revert ErrorsLib.PendingCap(id); // Safe "unchecked" cast because timelock <= MAX_TIMELOCK. config[id].removableAt = uint64(block.timestamp + timelock); @@ -345,6 +345,7 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph Id id = withdrawQueue[i]; if (config[id].cap != 0) revert ErrorsLib.InvalidMarketRemovalNonZeroCap(id); + if (pendingCap[id].validAt != 0) revert ErrorsLib.PendingCap(id); if (MORPHO.supplyShares(id, address(this)) != 0) { if (config[id].removableAt == 0) revert ErrorsLib.InvalidMarketRemovalNonZeroSupply(id); @@ -375,7 +376,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; @@ -419,8 +420,6 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph /// @inheritdoc IMetaMorphoBase function revokePendingTimelock() external onlyGuardianRole { - if (pendingTimelock.validAt == 0) revert ErrorsLib.NoPendingValue(); - delete pendingTimelock; emit EventsLib.RevokePendingTimelock(_msgSender()); @@ -428,8 +427,6 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph /// @inheritdoc IMetaMorphoBase function revokePendingGuardian() external onlyGuardianRole { - if (pendingGuardian.validAt == 0) revert ErrorsLib.NoPendingValue(); - delete pendingGuardian; emit EventsLib.RevokePendingGuardian(_msgSender()); @@ -437,8 +434,6 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph /// @inheritdoc IMetaMorphoBase function revokePendingCap(Id id) external onlyCuratorOrGuardianRole { - if (pendingCap[id].validAt == 0) revert ErrorsLib.NoPendingValue(); - delete pendingCap[id]; emit EventsLib.RevokePendingCap(_msgSender(), id); @@ -474,9 +469,14 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph } /// @inheritdoc IMetaMorphoBase - function acceptCap(Id id) external afterTimelock(pendingCap[id].validAt) { + function acceptCap(MarketParams memory marketParams) + external + afterTimelock(pendingCap[marketParams.id()].validAt) + { + Id id = marketParams.id(); + // Safe "unchecked" cast because pendingCap <= type(uint184).max. - _setCap(id, uint184(pendingCap[id].value)); + _setCap(marketParams, id, uint184(pendingCap[id].value)); } /// @inheritdoc IMetaMorphoBase @@ -498,11 +498,13 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph } /// @inheritdoc IERC4626 + /// @dev Warning: May be higher than the actual max deposit due to duplicate markets in the supplyQueue. function maxDeposit(address) public view override returns (uint256) { return _maxDeposit(); } /// @inheritdoc IERC4626 + /// @dev Warning: May be higher than the actual max mint due to duplicate markets in the supplyQueue. function maxMint(address) public view override returns (uint256) { uint256 suppliable = _maxDeposit(); @@ -589,8 +591,8 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph /* ERC4626 (INTERNAL) */ /// @inheritdoc ERC4626 - function _decimalsOffset() internal pure override returns (uint8) { - return ConstantsLib.DECIMALS_OFFSET; + function _decimalsOffset() internal view override returns (uint8) { + return DECIMALS_OFFSET; } /// @dev Returns the maximum amount of asset (`assets`) that the `owner` can withdraw from the vault, as well as the @@ -616,7 +618,10 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph uint256 supplyCap = config[id].cap; if (supplyCap == 0) continue; - uint256 supplyAssets = MORPHO.expectedSupplyAssets(_marketParams(id), address(this)); + uint256 supplyShares = MORPHO.supplyShares(id, address(this)); + (uint256 totalSupplyAssets, uint256 totalSupplyShares,,) = MORPHO.expectedMarketBalances(_marketParams(id)); + // `supplyAssets` needs to be rounded up for `totalSuppliable` to be rounded down. + uint256 supplyAssets = supplyShares.toAssetsUp(totalSupplyAssets, totalSupplyShares); totalSuppliable += supplyCap.zeroFloorSub(supplyAssets); } @@ -645,7 +650,7 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph uint256 newTotalSupply, uint256 newTotalAssets, Math.Rounding rounding - ) internal pure returns (uint256) { + ) internal view returns (uint256) { return assets.mulDiv(newTotalSupply + 10 ** _decimalsOffset(), newTotalAssets + 1, rounding); } @@ -656,7 +661,7 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph uint256 newTotalSupply, uint256 newTotalAssets, Math.Rounding rounding - ) internal pure returns (uint256) { + ) internal view returns (uint256) { return shares.mulDiv(newTotalAssets + 1, newTotalSupply + 10 ** _decimalsOffset(), rounding); } @@ -673,11 +678,10 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph /// @inheritdoc ERC4626 /// @dev Used in redeem or withdraw to withdraw the underlying asset from Morpho markets. - /// @dev Depending on 4 cases, reverts when withdrawing "too much" with: - /// 1. ERC20InsufficientAllowance when withdrawing more than `caller`'s allowance. - /// 2. ERC20InsufficientBalance when withdrawing more than `owner`'s balance but less than vault's total assets. - /// 3. NotEnoughLiquidity when withdrawing more than vault's total assets. - /// 4. NotEnoughLiquidity when withdrawing more than `owner`'s balance but less than the available liquidity. + /// @dev Depending on 3 cases, reverts when withdrawing "too much" with: + /// 1. NotEnoughLiquidity when withdrawing more than available liquidity. + /// 2. ERC20InsufficientAllowance when withdrawing more than `caller`'s allowance. + /// 3. ERC20InsufficientBalance when withdrawing more than `owner`'s balance. function _withdraw(address caller, address receiver, address owner, uint256 assets, uint256 shares) internal override @@ -733,22 +737,22 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph } /// @dev Sets the cap of the market defined by `id` to `supplyCap`. - function _setCap(Id id, uint184 supplyCap) internal { + /// @dev Assumes that the inputs `marketParams` and `id` match. + function _setCap(MarketParams memory marketParams, Id id, uint184 supplyCap) internal { MarketConfig storage marketConfig = config[id]; 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; + + // Take into account assets of the new market without applying a fee. + _updateLastTotalAssets(lastTotalAssets + MORPHO.expectedSupplyAssets(marketParams, address(this))); + + emit EventsLib.SetWithdrawQueue(msg.sender, withdrawQueue); } marketConfig.removableAt = 0; @@ -772,7 +776,13 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph if (supplyCap == 0) continue; MarketParams memory marketParams = _marketParams(id); - (uint256 supplyAssets,,) = _accruedSupplyBalance(marketParams, id); + + MORPHO.accrueInterest(marketParams); + + Market memory market = MORPHO.market(id); + uint256 supplyShares = MORPHO.supplyShares(id, address(this)); + // `supplyAssets` needs to be rounded up for `toSupply` to be rounded down. + uint256 supplyAssets = supplyShares.toAssetsUp(market.totalSupplyAssets, market.totalSupplyShares); uint256 toSupply = UtilsLib.min(supplyCap.zeroFloorSub(supplyAssets), assets); @@ -886,6 +896,7 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph uint256 totalInterest = newTotalAssets.zeroFloorSub(lastTotalAssets); if (totalInterest != 0 && fee != 0) { + // It is acknowledged that `feeAssets` may be rounded down to 0 if `totalInterest * fee < WAD`. uint256 feeAssets = totalInterest.mulDiv(fee, WAD); // The fee assets is subtracted from the total assets in this calculation to compensate for the fact // that total assets is already increased by the total interest (including the fee assets). diff --git a/src/interfaces/IMetaMorpho.sol b/src/interfaces/IMetaMorpho.sol index 1c7f9ca6..4e8f648c 100644 --- a/src/interfaces/IMetaMorpho.sol +++ b/src/interfaces/IMetaMorpho.sol @@ -7,7 +7,6 @@ import {IERC20Permit} from "../../lib/openzeppelin-contracts/contracts/token/ERC import {MarketConfig, PendingUint192, PendingAddress} from "../libraries/PendingLib.sol"; -/// @dev Either `assets` or `shares` should be zero. struct MarketAllocation { /// @notice The market to allocate. MarketParams marketParams; @@ -20,7 +19,7 @@ interface IMulticall { } interface IOwnable { - function owner() external returns (address); + function owner() external view returns (address); function transferOwnership(address) external; function renounceOwnership() external; function acceptOwnership() external; @@ -32,6 +31,7 @@ interface IOwnable { interface IMetaMorphoBase { /// @notice The address of the Morpho contract. function MORPHO() external view returns (IMorpho); + function DECIMALS_OFFSET() external view returns (uint8); /// @notice The address of the curator. function curator() external view returns (address); @@ -70,36 +70,49 @@ interface IMetaMorphoBase { function withdrawQueueLength() external view returns (uint256); /// @notice Stores the total assets managed by this vault when the fee was last accrued. - /// @dev May be a little off `totalAssets()` after each interaction, due to some roundings. + /// @dev May be greater than `totalAssets()` due to removal of markets with non-zero supply or socialized bad debt. + /// This difference will decrease the fee accrued until one of the functions updating `lastTotalAssets` is + /// triggered (deposit/mint/withdraw/redeem/setFee/setFeeRecipient). function lastTotalAssets() external view returns (uint256); /// @notice Submits a `newTimelock`. + /// @dev Warning: Reverts if a timelock is already pending. Revoke the pending timelock to overwrite it. /// @dev In case the new timelock is higher than the current one, the timelock is set immediately. - /// @dev Warning: Submitting a timelock will overwrite the current pending timelock. function submitTimelock(uint256 newTimelock) external; /// @notice Accepts the pending timelock. function acceptTimelock() external; /// @notice Revokes the pending timelock. + /// @dev Does not revert if there is no pending timelock. function revokePendingTimelock() external; /// @notice Submits a `newSupplyCap` for the market defined by `marketParams`. + /// @dev Warning: Reverts if a cap is already pending. Revoke the pending cap to overwrite it. + /// @dev Warning: Reverts if a market removal is pending. /// @dev In case the new cap is lower than the current one, the cap is set immediately. - /// @dev Warning: Submitting a cap will overwrite the current pending cap. function submitCap(MarketParams memory marketParams, uint256 newSupplyCap) external; - /// @notice Accepts the pending cap of the market defined by `id`. - function acceptCap(Id id) external; + /// @notice Accepts the pending cap of the market defined by `marketParams`. + function acceptCap(MarketParams memory marketParams) external; /// @notice Revokes the pending cap of the market defined by `id`. + /// @dev Does not revert if there is no pending cap. 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. - function submitMarketRemoval(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. + /// @dev Warning: Reverts for non-zero cap or if there is a pending cap. Successfully submitting a zero cap will + /// prevent such reverts. + function submitMarketRemoval(MarketParams memory marketParams) external; /// @notice Revokes the pending removal of the market defined by `id`. + /// @dev Does not revert if there is no pending market removal. function revokePendingMarketRemoval(Id id) external; /// @notice Submits a `newGuardian`. @@ -138,12 +151,15 @@ interface IMetaMorphoBase { /// increase the cost of depositing to the vault. function setSupplyQueue(Id[] calldata newSupplyQueue) external; - /// @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 Updates the withdraw queue. Some markets can be removed, but no market can be added. + /// @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 one of the functions updating + /// `lastTotalAssets` is triggered (deposit/mint/withdraw/redeem/setFee/setFeeRecipient). + /// @dev Warning: `updateWithdrawQueue` is not idempotent. Submitting twice the same tx will change the queue twice. /// @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; @@ -156,6 +172,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/src/libraries/ConstantsLib.sol b/src/libraries/ConstantsLib.sol index 8a77118d..8d0d1639 100644 --- a/src/libraries/ConstantsLib.sol +++ b/src/libraries/ConstantsLib.sol @@ -12,9 +12,6 @@ library ConstantsLib { /// @dev The minimum delay of a timelock. uint256 internal constant MIN_TIMELOCK = 1 days; - /// @dev OpenZeppelin's decimals offset used in MetaMorpho's ERC4626 implementation. - uint8 internal constant DECIMALS_OFFSET = 6; - /// @dev The maximum number of markets in the supply/withdraw queue. uint256 internal constant MAX_QUEUE_LENGTH = 30; diff --git a/src/libraries/ErrorsLib.sol b/src/libraries/ErrorsLib.sol index c5f3ed6d..e64fa9b6 100644 --- a/src/libraries/ErrorsLib.sol +++ b/src/libraries/ErrorsLib.sol @@ -39,9 +39,18 @@ library ErrorsLib { /// @notice Thrown when the value is already set. error AlreadySet(); - /// @notice Thrown when the value is already pending. + /// @notice Thrown when a value is already pending. error AlreadyPending(); + /// @notice Thrown when submitting the removal of a market when there is a cap already pending on that market. + error PendingCap(Id id); + + /// @notice Thrown when submitting a cap for a market with a pending removal. + error PendingRemoval(); + + /// @notice Thrown when submitting a market removal for a market with a non zero cap. + error NonZeroCap(); + /// @notice Thrown when market `id` is a duplicate in the new withdraw queue to set. error DuplicateMarket(Id id); @@ -63,8 +72,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/ERC4626Test.sol b/test/forge/ERC4626Test.sol index 6400d127..a3e87f78 100644 --- a/test/forge/ERC4626Test.sol +++ b/test/forge/ERC4626Test.sol @@ -17,8 +17,14 @@ contract ERC4626Test is IntegrationTest, IMorphoFlashLoanCallback { _sortSupplyQueueIdleLast(); } - function testDecimals() public { - assertEq(vault.decimals(), loanToken.decimals() + ConstantsLib.DECIMALS_OFFSET, "decimals"); + function testDecimals(uint8 decimals) public { + vm.mockCall(address(loanToken), abi.encodeWithSignature("decimals()"), abi.encode(decimals)); + + vault = IMetaMorpho( + address(new MetaMorpho(OWNER, address(morpho), TIMELOCK, address(loanToken), "MetaMorpho Vault", "MMV")) + ); + + assertEq(vault.decimals(), Math.max(18, decimals), "decimals"); } function testMint(uint256 assets) public { @@ -116,16 +122,18 @@ contract ERC4626Test is IntegrationTest, IMorphoFlashLoanCallback { function testRedeemTooMuch(uint256 deposited) public { deposited = bound(deposited, MIN_TEST_ASSETS, MAX_TEST_ASSETS); - loanToken.setBalance(SUPPLIER, deposited); + loanToken.setBalance(SUPPLIER, deposited * 2); - vm.prank(SUPPLIER); - uint256 shares = vault.deposit(deposited, ONBEHALF); + vm.startPrank(SUPPLIER); + uint256 shares = vault.deposit(deposited, SUPPLIER); + vault.deposit(deposited, ONBEHALF); + vm.stopPrank(); - vm.prank(ONBEHALF); + vm.prank(SUPPLIER); vm.expectRevert( - abi.encodeWithSelector(IERC20Errors.ERC20InsufficientBalance.selector, ONBEHALF, shares, shares + 1) + abi.encodeWithSelector(IERC20Errors.ERC20InsufficientBalance.selector, SUPPLIER, shares, shares + 1) ); - vault.redeem(shares + 1, RECEIVER, ONBEHALF); + vault.redeem(shares + 1, RECEIVER, SUPPLIER); } function testWithdrawAll(uint256 assets) public { @@ -270,14 +278,14 @@ contract ERC4626Test is IntegrationTest, IMorphoFlashLoanCallback { vm.prank(SUPPLIER); vault.deposit(deposited, ONBEHALF); - assets = bound(assets, deposited + 1, type(uint256).max / (deposited + 10 ** ConstantsLib.DECIMALS_OFFSET)); + assets = bound(assets, deposited + 1, type(uint256).max / (deposited + 1)); vm.prank(ONBEHALF); vm.expectRevert(ErrorsLib.NotEnoughLiquidity.selector); vault.withdraw(assets, RECEIVER, ONBEHALF); } - function testWithdrawMoreThanBalanceButLessThanLiquidity(uint256 deposited, uint256 assets) public { + function testWithdrawMoreThanBalanceAndLiquidity(uint256 deposited, uint256 assets) public { deposited = bound(deposited, MIN_TEST_ASSETS, MAX_TEST_ASSETS); loanToken.setBalance(SUPPLIER, deposited); @@ -285,7 +293,7 @@ contract ERC4626Test is IntegrationTest, IMorphoFlashLoanCallback { vm.prank(SUPPLIER); vault.deposit(deposited, ONBEHALF); - assets = bound(assets, deposited + 1, type(uint256).max / (deposited + 10 ** ConstantsLib.DECIMALS_OFFSET)); + assets = bound(assets, deposited + 1, type(uint256).max / (deposited + 1)); collateralToken.setBalance(BORROWER, type(uint128).max); @@ -355,6 +363,36 @@ contract ERC4626Test is IntegrationTest, IMorphoFlashLoanCallback { morpho.flashLoan(address(loanToken), loanToken.balanceOf(address(morpho)), hex""); } + function testMaxDeposit() public { + _setCap(allMarkets[0], 1 ether); + + Id[] memory supplyQueue = new Id[](1); + supplyQueue[0] = allMarkets[0].id(); + + vm.prank(ALLOCATOR); + vault.setSupplyQueue(supplyQueue); + + loanToken.setBalance(SUPPLIER, 1 ether); + collateralToken.setBalance(BORROWER, 2 ether); + + vm.prank(SUPPLIER); + morpho.supply(allMarkets[0], 1 ether, 0, SUPPLIER, hex""); + + vm.startPrank(BORROWER); + morpho.supplyCollateral(allMarkets[0], 2 ether, BORROWER, hex""); + morpho.borrow(allMarkets[0], 1 ether, 0, BORROWER, BORROWER); + vm.stopPrank(); + + _forward(1_000); + + loanToken.setBalance(SUPPLIER, 1 ether); + + vm.prank(SUPPLIER); + vault.deposit(1 ether, ONBEHALF); + + assertEq(vault.maxDeposit(SUPPLIER), 0); + } + function onMorphoFlashLoan(uint256, bytes memory) external { assertEq(vault.maxWithdraw(ONBEHALF), 0); } diff --git a/test/forge/FeeTest.sol b/test/forge/FeeTest.sol index ad51a21f..7d9f6010 100644 --- a/test/forge/FeeTest.sol +++ b/test/forge/FeeTest.sol @@ -55,11 +55,7 @@ contract FeeTest is IntegrationTest { uint256 interest = totalAssetsAfter - vault.lastTotalAssets(); uint256 feeAssets = interest.mulDiv(FEE, WAD); - return feeAssets.mulDiv( - vault.totalSupply() + 10 ** ConstantsLib.DECIMALS_OFFSET, - totalAssetsAfter - feeAssets + 1, - Math.Rounding.Floor - ); + return feeAssets.mulDiv(vault.totalSupply() + 1, totalAssetsAfter - feeAssets + 1, Math.Rounding.Floor); } function testAccrueFeeWithinABlock(uint256 deposited, uint256 withdrawn) public { @@ -312,11 +308,8 @@ contract FeeTest is IntegrationTest { _forward(blocks); uint256 feeShares = _feeShares(); - uint256 expectedShares = assets.mulDiv( - vault.totalSupply() + feeShares + 10 ** ConstantsLib.DECIMALS_OFFSET, - vault.totalAssets() + 1, - Math.Rounding.Floor - ); + uint256 expectedShares = + assets.mulDiv(vault.totalSupply() + feeShares + 1, vault.totalAssets() + 1, Math.Rounding.Floor); uint256 shares = vault.convertToShares(assets); assertEq(shares, expectedShares, "shares"); @@ -325,7 +318,7 @@ contract FeeTest is IntegrationTest { function testConvertToSharesWithFeeAndInterest(uint256 deposited, uint256 shares, uint256 blocks) public { deposited = bound(deposited, MIN_TEST_ASSETS, MAX_TEST_ASSETS); - shares = bound(shares, 10 ** ConstantsLib.DECIMALS_OFFSET, MAX_TEST_ASSETS); + shares = bound(shares, 1, MAX_TEST_ASSETS); blocks = _boundBlocks(blocks); loanToken.setBalance(SUPPLIER, deposited); @@ -338,11 +331,8 @@ contract FeeTest is IntegrationTest { _forward(blocks); uint256 feeShares = _feeShares(); - uint256 expectedAssets = shares.mulDiv( - vault.totalAssets() + 1, - vault.totalSupply() + feeShares + 10 ** ConstantsLib.DECIMALS_OFFSET, - Math.Rounding.Floor - ); + uint256 expectedAssets = + shares.mulDiv(vault.totalAssets() + 1, vault.totalSupply() + feeShares + 1, Math.Rounding.Floor); uint256 assets = vault.convertToAssets(shares); assertEq(assets, expectedAssets, "assets"); diff --git a/test/forge/GuardianTest.sol b/test/forge/GuardianTest.sol index d1c77a00..205bb36c 100644 --- a/test/forge/GuardianTest.sol +++ b/test/forge/GuardianTest.sol @@ -129,7 +129,7 @@ contract GuardianTest is IntegrationTest { _setCap(marketParams, 0); vm.prank(CURATOR); - vault.submitMarketRemoval(id); + vault.submitMarketRemoval(allMarkets[0]); vm.warp(block.timestamp + elapsed); diff --git a/test/forge/MarketTest.sol b/test/forge/MarketTest.sol index ef769d43..363d3503 100644 --- a/test/forge/MarketTest.sol +++ b/test/forge/MarketTest.sol @@ -7,6 +7,7 @@ import {SafeCast} from "../../lib/openzeppelin-contracts/contracts/utils/math/Sa import "./helpers/IntegrationTest.sol"; contract MarketTest is IntegrationTest { + using MathLib for uint256; using MarketParamsLib for MarketParams; using MorphoLib for IMorpho; @@ -79,6 +80,24 @@ contract MarketTest is IntegrationTest { vault.submitCap(allMarkets[0], CAP); } + function testSubmitCapAlreadyPending() public { + vm.prank(CURATOR); + vault.submitCap(allMarkets[0], CAP + 1); + + vm.prank(CURATOR); + vm.expectRevert(ErrorsLib.AlreadyPending.selector); + vault.submitCap(allMarkets[0], CAP + 1); + } + + function testSubmitCapPendingRemoval() public { + vm.startPrank(CURATOR); + vault.submitCap(allMarkets[2], 0); + vault.submitMarketRemoval(allMarkets[2]); + + vm.expectRevert(ErrorsLib.PendingRemoval.selector); + vault.submitCap(allMarkets[2], CAP + 1); + } + function testSetSupplyQueue() public { Id[] memory supplyQueue = new Id[](2); supplyQueue[0] = allMarkets[1].id(); @@ -116,7 +135,7 @@ contract MarketTest is IntegrationTest { vm.warp(block.timestamp + 1 weeks); vm.expectRevert(ErrorsLib.MaxQueueLengthExceeded.selector); - vault.acceptCap(marketParams.id()); + vault.acceptCap(marketParams); } function testSetSupplyQueueUnauthorizedMarket() public { @@ -156,7 +175,7 @@ contract MarketTest is IntegrationTest { _setCap(allMarkets[2], 0); vm.prank(CURATOR); - vault.submitMarketRemoval(allMarkets[2].id()); + vault.submitMarketRemoval(allMarkets[2]); vm.warp(block.timestamp + TIMELOCK); @@ -179,23 +198,44 @@ contract MarketTest is IntegrationTest { assertEq(Id.unwrap(vault.withdrawQueue(1)), Id.unwrap(expectedWithdrawQueue[1])); assertEq(Id.unwrap(vault.withdrawQueue(2)), Id.unwrap(expectedWithdrawQueue[2])); assertFalse(vault.config(allMarkets[2].id()).enabled); + assertEq(vault.pendingCap(allMarkets[2].id()).value, 0, "pendingCap.value"); + assertEq(vault.pendingCap(allMarkets[2].id()).validAt, 0, "pendingCap.validAt"); } function testSubmitMarketRemoval() public { + vm.startPrank(CURATOR); + vault.submitCap(allMarkets[2], 0); vm.expectEmit(); emit EventsLib.SubmitMarketRemoval(CURATOR, allMarkets[2].id()); - vm.prank(CURATOR); - vault.submitMarketRemoval(allMarkets[2].id()); + vault.submitMarketRemoval(allMarkets[2]); + vm.stopPrank(); assertEq(vault.config(allMarkets[2].id()).cap, 0); assertEq(vault.config(allMarkets[2].id()).removableAt, block.timestamp + TIMELOCK); } - function testSubmitMarketRemovalAlreadySet() public { + function testSubmitMarketRemovalPendingCap() public { vm.startPrank(CURATOR); - vault.submitMarketRemoval(allMarkets[2].id()); - vm.expectRevert(ErrorsLib.AlreadySet.selector); - vault.submitMarketRemoval(allMarkets[2].id()); + vault.submitCap(allMarkets[2], 0); + vault.submitCap(allMarkets[2], vault.config(allMarkets[2].id()).cap + 1); + vm.expectRevert(abi.encodeWithSelector(ErrorsLib.PendingCap.selector, allMarkets[2].id())); + vault.submitMarketRemoval(allMarkets[2]); + vm.stopPrank(); + } + + function testSubmitMarketRemovalNonZeroCap() public { + vm.startPrank(CURATOR); + vm.expectRevert(ErrorsLib.NonZeroCap.selector); + vault.submitMarketRemoval(allMarkets[2]); + vm.stopPrank(); + } + + function testSubmitMarketRemovalAlreadyPending() public { + vm.startPrank(CURATOR); + vault.submitCap(allMarkets[2], 0); + vault.submitMarketRemoval(allMarkets[2]); + vm.expectRevert(ErrorsLib.AlreadyPending.selector); + vault.submitMarketRemoval(allMarkets[2]); vm.stopPrank(); } @@ -264,7 +304,7 @@ contract MarketTest is IntegrationTest { _setCap(idleParams, 0); vm.prank(CURATOR); - vault.submitMarketRemoval(idleParams.id()); + vault.submitMarketRemoval(idleParams); vm.warp(block.timestamp + elapsed); @@ -279,4 +319,65 @@ contract MarketTest is IntegrationTest { ); vault.updateWithdrawQueue(indexes); } + + function testUpdateWithdrawQueueInvalidMarketRemovalPendingCap(uint256 cap) public { + cap = bound(cap, MIN_TEST_ASSETS, MAX_TEST_ASSETS); + + _setCap(allMarkets[2], 0); + vm.prank(CURATOR); + vault.submitCap(allMarkets[2], cap); + + uint256[] memory indexes = new uint256[](3); + indexes[0] = 0; + indexes[1] = 2; + indexes[2] = 1; + + vm.prank(ALLOCATOR); + vm.expectRevert(abi.encodeWithSelector(ErrorsLib.PendingCap.selector, allMarkets[2].id())); + vault.updateWithdrawQueue(indexes); + } + + function testEnableMarketWithLiquidity(uint256 deposited, uint256 additionalSupply, uint256 blocks) public { + deposited = bound(deposited, MIN_TEST_ASSETS, MAX_TEST_ASSETS); + additionalSupply = bound(additionalSupply, MIN_TEST_ASSETS, MAX_TEST_ASSETS); + blocks = _boundBlocks(blocks); + + Id[] memory supplyQueue = new Id[](1); + supplyQueue[0] = allMarkets[0].id(); + + _setCap(allMarkets[0], deposited); + + vm.prank(ALLOCATOR); + vault.setSupplyQueue(supplyQueue); + + loanToken.setBalance(SUPPLIER, deposited + additionalSupply); + + vm.startPrank(SUPPLIER); + vault.deposit(deposited, ONBEHALF); + morpho.supply(allMarkets[3], additionalSupply, 0, address(vault), hex""); + vm.stopPrank(); + + uint256 collateral = uint256(MAX_TEST_ASSETS).wDivUp(allMarkets[0].lltv); + collateralToken.setBalance(BORROWER, collateral); + + vm.startPrank(BORROWER); + morpho.supplyCollateral(allMarkets[0], collateral, BORROWER, hex""); + morpho.borrow(allMarkets[0], deposited, 0, BORROWER, BORROWER); + vm.stopPrank(); + + _forward(blocks); + + _setCap(allMarkets[3], CAP); + + assertEq(vault.lastTotalAssets(), deposited + additionalSupply); + } + + function testRevokeNoRevert() public { + vm.startPrank(OWNER); + vault.revokePendingTimelock(); + vault.revokePendingGuardian(); + vault.revokePendingCap(Id.wrap(bytes32(0))); + vault.revokePendingMarketRemoval(Id.wrap(bytes32(0))); + vm.stopPrank(); + } } diff --git a/test/forge/MetaMorphoInternalTest.sol b/test/forge/MetaMorphoInternalTest.sol index 8ea5c4bd..ae9f13e7 100644 --- a/test/forge/MetaMorphoInternalTest.sol +++ b/test/forge/MetaMorphoInternalTest.sol @@ -16,21 +16,19 @@ contract MetaMorphoInternalTest is InternalTest { function testSetCapMaxQueueLengthExcedeed() public { for (uint256 i; i < NB_MARKETS - 1; ++i) { - Id id = allMarkets[i].id(); - _setCap(id, CAP); + _setCap(allMarkets[i], allMarkets[i].id(), CAP); } - Id lastId = allMarkets[NB_MARKETS - 1].id(); vm.expectRevert(ErrorsLib.MaxQueueLengthExceeded.selector); - _setCap(lastId, CAP); + _setCap(allMarkets[NB_MARKETS - 1], allMarkets[NB_MARKETS - 1].id(), CAP); } function testSimulateWithdraw(uint256 suppliedAmount, uint256 borrowedAmount, uint256 assets) public { suppliedAmount = bound(suppliedAmount, MIN_TEST_ASSETS, MAX_TEST_ASSETS); borrowedAmount = bound(borrowedAmount, MIN_TEST_ASSETS, suppliedAmount); - Id id = allMarkets[0].id(); - _setCap(id, CAP); + _setCap(allMarkets[0], allMarkets[0].id(), CAP); + supplyQueue = [allMarkets[0].id()]; loanToken.setBalance(SUPPLIER, suppliedAmount); vm.prank(SUPPLIER); 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/RevokeTest.sol b/test/forge/RevokeTest.sol index f0b12681..31619f5b 100644 --- a/test/forge/RevokeTest.sol +++ b/test/forge/RevokeTest.sol @@ -115,24 +115,4 @@ contract RevokeTest is IntegrationTest { assertEq(pendingGuardian.value, address(0), "value"); assertEq(pendingGuardian.validAt, 0, "validAt"); } - - function testOwnerRevokePendingCapNoPendingValue(uint256 seed) public { - MarketParams memory marketParams = _randomMarketParams(seed); - - vm.prank(OWNER); - vm.expectRevert(ErrorsLib.NoPendingValue.selector); - vault.revokePendingCap(marketParams.id()); - } - - function testOwnerRevokePendingTimelockNoPendingValue() public { - vm.prank(OWNER); - vm.expectRevert(ErrorsLib.NoPendingValue.selector); - vault.revokePendingTimelock(); - } - - function testOwnerRevokePendingGuardianNoPendingValue() public { - vm.prank(OWNER); - vm.expectRevert(ErrorsLib.NoPendingValue.selector); - vault.revokePendingGuardian(); - } } diff --git a/test/forge/TimelockTest.sol b/test/forge/TimelockTest.sol index 3ca97a1c..98eead78 100644 --- a/test/forge/TimelockTest.sol +++ b/test/forge/TimelockTest.sol @@ -352,7 +352,7 @@ contract TimelockTest is IntegrationTest { vm.expectEmit(address(vault)); emit EventsLib.SetCap(address(this), id, cap); - vault.acceptCap(id); + vault.acceptCap(marketParams); MarketConfig memory marketConfig = vault.config(id); PendingUint192 memory pendingCap = vault.pendingCap(id); @@ -383,7 +383,7 @@ contract TimelockTest is IntegrationTest { vm.expectEmit(); emit EventsLib.SetCap(address(this), id, cap); - vault.acceptCap(id); + vault.acceptCap(marketParams); MarketConfig memory marketConfig = vault.config(id); PendingUint192 memory pendingCap = vault.pendingCap(id); @@ -408,7 +408,6 @@ contract TimelockTest is IntegrationTest { vm.warp(block.timestamp + elapsed); MarketParams memory marketParams = allMarkets[0]; - Id id = marketParams.id(); vm.prank(CURATOR); vault.submitCap(marketParams, cap); @@ -418,12 +417,12 @@ contract TimelockTest is IntegrationTest { vault.acceptTimelock(); vm.expectRevert(ErrorsLib.TimelockNotElapsed.selector); - vault.acceptCap(id); + vault.acceptCap(marketParams); } function testAcceptCapNoPendingValue() public { vm.expectRevert(ErrorsLib.NoPendingValue.selector); - vault.acceptCap(allMarkets[0].id()); + vault.acceptCap(allMarkets[0]); } function testAcceptCapTimelockNotElapsed(uint256 elapsed) public { @@ -435,7 +434,7 @@ contract TimelockTest is IntegrationTest { vm.warp(block.timestamp + elapsed); vm.expectRevert(ErrorsLib.TimelockNotElapsed.selector); - vault.acceptCap(allMarkets[1].id()); + vault.acceptCap(allMarkets[1]); } function testSubmitMarketRemoval() public { @@ -445,7 +444,7 @@ contract TimelockTest is IntegrationTest { _setCap(marketParams, 0); vm.prank(CURATOR); - vault.submitMarketRemoval(id); + vault.submitMarketRemoval(marketParams); MarketConfig memory marketConfig = vault.config(id); @@ -455,8 +454,8 @@ 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()); + vault.submitMarketRemoval(allMarkets[1]); } } diff --git a/test/forge/helpers/IntegrationTest.sol b/test/forge/helpers/IntegrationTest.sol index 5f866fa9..5eeb9f66 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); @@ -112,9 +113,21 @@ contract IntegrationTest is BaseTest { vm.warp(block.timestamp + vault.timelock()); - vault.acceptCap(id); + vault.acceptCap(marketParams); 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 { diff --git a/test/hardhat/MetaMorpho.spec.ts b/test/hardhat/MetaMorpho.spec.ts index c10d47ef..42aad699 100644 --- a/test/hardhat/MetaMorpho.spec.ts +++ b/test/hardhat/MetaMorpho.spec.ts @@ -2,7 +2,7 @@ import { AbiCoder, MaxUint256, ZeroAddress, ZeroHash, keccak256, toBigInt } from import hre from "hardhat"; import _range from "lodash/range"; import { ERC20Mock, OracleMock, MetaMorpho, IMorpho, MetaMorphoFactory, MetaMorpho__factory, IrmMock } from "types"; -import { MarketParamsStruct } from "types/@morpho-blue/interfaces/IMorpho"; +import { MarketParamsStruct } from "types/src/MetaMorpho"; import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; import { mine } from "@nomicfoundation/hardhat-network-helpers"; @@ -224,10 +224,10 @@ describe("MetaMorpho", () => { await forwardTimestamp(timelock); - await metaMorpho.connect(admin).acceptCap(identifier(idleParams)); + await metaMorpho.connect(admin).acceptCap(idleParams); for (const marketParams of allMarketParams) { - await metaMorpho.connect(admin).acceptCap(identifier(marketParams)); + await metaMorpho.connect(admin).acceptCap(marketParams); } await metaMorpho.connect(curator).setSupplyQueue(