diff --git a/.github/workflows/foundry.yml b/.github/workflows/foundry.yml index bd572eeb..71134732 100644 --- a/.github/workflows/foundry.yml +++ b/.github/workflows/foundry.yml @@ -63,10 +63,10 @@ jobs: type: ["slow", "fast"] include: - type: "slow" - fuzz-runs: 8192 + fuzz-runs: 1024 max-test-rejects: 1048576 - invariant-runs: 64 - invariant-depth: 1024 + invariant-runs: 32 + invariant-depth: 512 - type: "fast" fuzz-runs: 256 max-test-rejects: 65536 diff --git a/README.md b/README.md index b8b08e05..ad1c7f03 100644 --- a/README.md +++ b/README.md @@ -75,12 +75,15 @@ Multiple addresses can have this role. 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. The remaining funds are left as idle supply on the vault (uncapped). + - 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. - 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. +> **Warning** +> If `supplyQueue` is empty, depositing to the vault is disabled. + #### Guardian Only one address can have this role. @@ -91,6 +94,23 @@ It can: - Revoke the pending guardian (which means it can revoke any attempt to change the guardian). - Revoke the pending cap 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: + +- The vault's asset as loan token. +- No collateral token (`address(0)`). +- An arbitrary IRM. +- An arbitrary oracle (`address(0)`). +- An arbitrary LLTV. + +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. + ### Rewards To redistribute rewards to vault depositors, it is advised to use the [Universal Rewards Distributor (URD)](https://github.com/morpho-org/universal-rewards-distributor). @@ -111,8 +131,7 @@ Below is a typical example of how this use case would take place: NB: Anyone can transfer rewards from the vault to the rewards distributor unless it is unset. Thus, this step might be already performed by some third-party. - Note: the amount of rewards transferred is calculated based on the balance in the reward asset of the vault. - In case the reward asset is the vault’s asset, the vault’s idle liquidity is automatically subtracted to prevent stealing idle liquidity. + Note: the amount of rewards transferred corresponds to the vault's balance of reward asset. - Compute the new root for the vault’s rewards distributor, submit it, wait for the timelock (if any), accept the root, and let vault depositors claim their rewards according to the vault manager’s rewards re-distribution strategy. diff --git a/src/MetaMorpho.sol b/src/MetaMorpho.sol index 35142a8c..2e3dae22 100644 --- a/src/MetaMorpho.sol +++ b/src/MetaMorpho.sol @@ -95,10 +95,6 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph /// without duplicate. Id[] public withdrawQueue; - /// @notice Stores the idle liquidity. - /// @dev The idle liquidity does not generate any interest. - uint256 public idle; - /// @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. uint256 public lastTotalAssets; @@ -418,7 +414,10 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph totalWithdrawn += withdrawnAssets; } else { - uint256 suppliedAssets = allocation.assets.zeroFloorSub(supplyAssets); + uint256 suppliedAssets = allocation.assets == type(uint256).max + ? totalWithdrawn.zeroFloorSub(totalSupplied) + : allocation.assets.zeroFloorSub(supplyAssets); + if (suppliedAssets == 0) continue; uint256 supplyCap = config[id].cap; @@ -436,19 +435,7 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph } } - uint256 newIdle; - if (totalWithdrawn > totalSupplied) { - newIdle = idle + totalWithdrawn - totalSupplied; - } else { - uint256 idleSupplied = totalSupplied - totalWithdrawn; - if (idle < idleSupplied) revert ErrorsLib.InsufficientIdle(); - - newIdle = idle - idleSupplied; - } - - idle = newIdle; - - emit EventsLib.ReallocateIdle(_msgSender(), newIdle); + if (totalWithdrawn != totalSupplied) revert ErrorsLib.InconsistentReallocation(); } /* REVOKE FUNCTIONS */ @@ -520,7 +507,6 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph if (skimRecipient == address(0)) revert ErrorsLib.ZeroAddress(); uint256 amount = IERC20(token).balanceOf(address(this)); - if (token == asset()) amount -= idle; IERC20(token).safeTransfer(skimRecipient, amount); @@ -534,6 +520,18 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph return ERC4626.decimals(); } + /// @inheritdoc IERC4626 + function maxDeposit(address) public view override returns (uint256) { + return _maxDeposit(); + } + + /// @inheritdoc IERC4626 + function maxMint(address) public view override returns (uint256) { + uint256 suppliable = _maxDeposit(); + + return _convertToShares(suppliable, Math.Rounding.Floor); + } + /// @inheritdoc IERC4626 /// @dev Warning: May be lower than the actual amount of assets that can be withdrawn by `owner` due to conversion /// roundings between shares and assets. @@ -609,8 +607,6 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph for (uint256 i; i < withdrawQueue.length; ++i) { assets += MORPHO.expectedSupplyAssets(_marketParams(withdrawQueue[i]), address(this)); } - - assets += idle; } /* ERC4626 (INTERNAL) */ @@ -635,6 +631,20 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph assets -= _simulateWithdrawMorpho(assets); } + /// @dev Returns the maximum amount of assets that the vault can supply on Morpho. + function _maxDeposit() internal view returns (uint256 totalSuppliable) { + for (uint256 i; i < supplyQueue.length; ++i) { + Id id = supplyQueue[i]; + + uint256 supplyCap = config[id].cap; + if (supplyCap == 0) continue; + + uint256 supplyAssets = MORPHO.expectedSupplyAssets(_marketParams(id), address(this)); + + totalSuppliable += supplyCap.zeroFloorSub(supplyAssets); + } + } + /// @inheritdoc ERC4626 /// @dev The accrual of performance fees is taken into account in the conversion. function _convertToShares(uint256 assets, Math.Rounding rounding) internal view override returns (uint256) { @@ -689,13 +699,13 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph /// @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. WithdrawMorphoFailed when withdrawing more than vault's total assets. - /// 4. WithdrawMorphoFailed when withdrawing more than `owner`'s balance but less than the available liquidity. + /// 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. function _withdraw(address caller, address receiver, address owner, uint256 assets, uint256 shares) internal override { - if (_withdrawMorpho(assets) != 0) revert ErrorsLib.WithdrawMorphoFailed(); + _withdrawMorpho(assets); super._withdraw(caller, receiver, owner, assets, shares); } @@ -776,7 +786,7 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph /* LIQUIDITY ALLOCATION */ - /// @dev Supplies `assets` to Morpho and increase the idle liquidity if necessary. + /// @dev Supplies `assets` to Morpho. function _supplyMorpho(uint256 assets) internal { for (uint256 i; i < supplyQueue.length; ++i) { Id id = supplyQueue[i]; @@ -799,43 +809,36 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph if (assets == 0) return; } - idle += assets; + if (assets != 0) revert ErrorsLib.AllCapsReached(); } - /// @dev Withdraws `assets` from the idle liquidity and Morpho if necessary. - /// @return remaining The assets left to be withdrawn. - function _withdrawMorpho(uint256 assets) internal returns (uint256 remaining) { - (remaining, idle) = _withdrawIdle(assets); - - if (remaining == 0) return 0; - + /// @dev Withdraws `assets` from Morpho. + function _withdrawMorpho(uint256 assets) internal { for (uint256 i; i < withdrawQueue.length; ++i) { Id id = withdrawQueue[i]; MarketParams memory marketParams = _marketParams(id); (uint256 supplyAssets,, Market memory market) = _accruedSupplyBalance(marketParams, id); uint256 toWithdraw = UtilsLib.min( - _withdrawable(marketParams, market.totalSupplyAssets, market.totalBorrowAssets, supplyAssets), remaining + _withdrawable(marketParams, market.totalSupplyAssets, market.totalBorrowAssets, supplyAssets), assets ); if (toWithdraw > 0) { // Using try/catch to skip markets that revert. try MORPHO.withdraw(marketParams, toWithdraw, 0, address(this), address(this)) { - remaining -= toWithdraw; + assets -= toWithdraw; } catch {} } - if (remaining == 0) return 0; + if (assets == 0) return; } - } - - /// @dev Simulates a withdraw of `assets` from the idle liquidity and Morpho if necessary. - /// @return remaining The assets left to be withdrawn. - function _simulateWithdrawMorpho(uint256 assets) internal view returns (uint256 remaining) { - (remaining,) = _withdrawIdle(assets); - if (remaining == 0) return 0; + if (assets != 0) revert ErrorsLib.NotEnoughLiquidity(); + } + /// @dev Simulates a withdraw of `assets` from Morpho. + /// @return The remaining assets to be withdrawn. + function _simulateWithdrawMorpho(uint256 assets) internal view returns (uint256) { for (uint256 i; i < withdrawQueue.length; ++i) { Id id = withdrawQueue[i]; MarketParams memory marketParams = _marketParams(id); @@ -848,25 +851,19 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph // 1. oracle.price() is never called (the vault doesn't borrow) // 2. the amount is capped to the liquidity available on Morpho // 3. virtually accruing interest didn't fail - remaining -= UtilsLib.min( + assets = assets.zeroFloorSub( _withdrawable( marketParams, totalSupplyAssets, totalBorrowAssets, supplyShares.toAssetsDown(totalSupplyAssets, totalSupplyShares) - ), - remaining + ) ); - if (remaining == 0) return 0; + if (assets == 0) break; } - } - /// @dev Withdraws `assets` from the idle liquidity. - /// @return The remaining assets to withdraw. - /// @return The new `idle` liquidity value. - function _withdrawIdle(uint256 assets) internal view returns (uint256, uint256) { - return (assets.zeroFloorSub(idle), idle.zeroFloorSub(assets)); + return assets; } /// @dev Returns the withdrawable amount of assets from the market defined by `marketParams`, given the market's diff --git a/src/interfaces/IMetaMorpho.sol b/src/interfaces/IMetaMorpho.sol index 2f15eb6d..46014f0a 100644 --- a/src/interfaces/IMetaMorpho.sol +++ b/src/interfaces/IMetaMorpho.sol @@ -45,7 +45,6 @@ interface IMetaMorphoBase { function withdrawQueue(uint256) external view returns (Id); function withdrawQueueLength() external view returns (uint256); - function idle() external view returns (uint256); function lastTotalAssets() external view returns (uint256); function submitTimelock(uint256 newTimelock) external; diff --git a/src/libraries/ErrorsLib.sol b/src/libraries/ErrorsLib.sol index 768c7043..254735cd 100644 --- a/src/libraries/ErrorsLib.sol +++ b/src/libraries/ErrorsLib.sol @@ -57,8 +57,8 @@ library ErrorsLib { /// @notice Thrown when there's no pending value to set. error NoPendingValue(); - /// @notice Thrown when the remaining asset to withdraw is not 0. - error WithdrawMorphoFailed(); + /// @notice Thrown when the requested liquidity cannot be withdrawn from Morpho. + error NotEnoughLiquidity(); /// @notice Thrown when submitting a cap for a market which does not exist. error MarketNotCreated(); @@ -81,6 +81,9 @@ library ErrorsLib { /// @notice Thrown when setting the fee to a non zero value while the fee recipient is the zero address. error ZeroFeeRecipient(); - /// @notice Thrown when the idle liquidity is insufficient to cover supply during a reallocation of funds. - error InsufficientIdle(); + /// @notice Thrown when the amount withdrawn is not exactly the amount supplied. + error InconsistentReallocation(); + + /// @notice Thrown when all caps have been reached. + error AllCapsReached(); } diff --git a/src/libraries/EventsLib.sol b/src/libraries/EventsLib.sol index 69439f2e..9587824a 100644 --- a/src/libraries/EventsLib.sol +++ b/src/libraries/EventsLib.sol @@ -13,7 +13,7 @@ library EventsLib { /// @notice Emitted when a pending `newTimelock` is submitted. event SubmitTimelock(uint256 newTimelock); - /// @notice Emitted `timelock` is set to `newTimelock`. + /// @notice Emitted when `timelock` is set to `newTimelock`. event SetTimelock(address indexed caller, uint256 newTimelock); /// @notice Emitted when `skimRecipient` is set to `newSkimRecipient`. @@ -79,9 +79,6 @@ library EventsLib { /// @param withdrawnShares The amount of shares burned. event ReallocateWithdraw(address indexed caller, Id indexed id, uint256 withdrawnAssets, uint256 withdrawnShares); - /// @notice Emitted when a reallocation added or removed assets from idle. - event ReallocateIdle(address indexed caller, uint256 idle); - /// @notice Emitted when interest are accrued. /// @param newTotalAssets The assets of the vault after accruing the interest but before the interaction. /// @param feeShares The shares minted to the fee recipient. diff --git a/src/mocks/ERC20Mock.sol b/src/mocks/ERC20Mock.sol index b5165ff9..7d256b89 100644 --- a/src/mocks/ERC20Mock.sol +++ b/src/mocks/ERC20Mock.sol @@ -6,10 +6,12 @@ import {ERC20} from "@openzeppelin/token/ERC20/ERC20.sol"; contract ERC20Mock is ERC20 { constructor(string memory _name, string memory _symbol) ERC20(_name, _symbol) {} + /// @dev Required for ERC4626-compliance tests. function mint(address account, uint256 amount) external { _mint(account, amount); } + /// @dev Required for ERC4626-compliance tests. function burn(address account, uint256 amount) external { _burn(account, amount); } diff --git a/test/forge/ERC4626ComplianceTest.sol b/test/forge/ERC4626ComplianceTest.sol index 56295bbe..c778a4af 100644 --- a/test/forge/ERC4626ComplianceTest.sol +++ b/test/forge/ERC4626ComplianceTest.sol @@ -15,6 +15,7 @@ contract ERC4626ComplianceTest is IntegrationTest, ERC4626Test { _vaultMayBeEmpty = true; _unlimitedAmount = true; - _setCap(allMarkets[0], 1e28); + _setCap(allMarkets[0], 100e18); + _sortSupplyQueueIdleLast(); } } diff --git a/test/forge/ERC4626Test.sol b/test/forge/ERC4626Test.sol index 5f3101e7..239496f7 100644 --- a/test/forge/ERC4626Test.sol +++ b/test/forge/ERC4626Test.sol @@ -14,6 +14,7 @@ contract ERC4626Test is IntegrationTest, IMorphoFlashLoanCallback { super.setUp(); _setCap(allMarkets[0], CAP); + _sortSupplyQueueIdleLast(); } function testDecimals() public { @@ -33,6 +34,7 @@ contract ERC4626Test is IntegrationTest, IMorphoFlashLoanCallback { uint256 deposited = vault.mint(shares, ONBEHALF); assertGt(deposited, 0, "deposited"); + assertEq(loanToken.balanceOf(address(vault)), 0, "balanceOf(vault)"); assertEq(vault.balanceOf(ONBEHALF), shares, "balanceOf(ONBEHALF)"); assertEq(morpho.expectedSupplyAssets(allMarkets[0], address(vault)), assets, "expectedSupplyAssets(vault)"); } @@ -48,6 +50,7 @@ contract ERC4626Test is IntegrationTest, IMorphoFlashLoanCallback { uint256 shares = vault.deposit(assets, ONBEHALF); assertGt(shares, 0, "shares"); + assertEq(loanToken.balanceOf(address(vault)), 0, "balanceOf(vault)"); assertEq(vault.balanceOf(ONBEHALF), shares, "balanceOf(ONBEHALF)"); assertEq(morpho.expectedSupplyAssets(allMarkets[0], address(vault)), assets, "expectedSupplyAssets(vault)"); } @@ -67,6 +70,7 @@ contract ERC4626Test is IntegrationTest, IMorphoFlashLoanCallback { vm.prank(ONBEHALF); vault.redeem(redeemed, RECEIVER, ONBEHALF); + assertEq(loanToken.balanceOf(address(vault)), 0, "balanceOf(vault)"); assertEq(vault.balanceOf(ONBEHALF), shares - redeemed, "balanceOf(ONBEHALF)"); } @@ -84,6 +88,7 @@ contract ERC4626Test is IntegrationTest, IMorphoFlashLoanCallback { vm.prank(ONBEHALF); uint256 redeemed = vault.withdraw(withdrawn, RECEIVER, ONBEHALF); + assertEq(loanToken.balanceOf(address(vault)), 0, "balanceOf(vault)"); assertEq(vault.balanceOf(ONBEHALF), shares - redeemed, "balanceOf(ONBEHALF)"); } @@ -103,8 +108,9 @@ contract ERC4626Test is IntegrationTest, IMorphoFlashLoanCallback { vm.prank(ONBEHALF); uint256 redeemed = vault.withdraw(withdrawn, RECEIVER, ONBEHALF); + assertEq(loanToken.balanceOf(address(vault)), 0, "balanceOf(vault)"); assertEq(vault.balanceOf(ONBEHALF), shares - redeemed, "balanceOf(ONBEHALF)"); - assertEq(vault.idle(), deposited - withdrawn, "idle"); + assertEq(_idle(), deposited - withdrawn, "idle"); } function testRedeemTooMuch(uint256 deposited) public { @@ -241,22 +247,18 @@ contract ERC4626Test is IntegrationTest, IMorphoFlashLoanCallback { loanToken.setBalance(SUPPLIER, deposited); - vm.prank(SUPPLIER); - uint256 shares = vault.deposit(deposited, ONBEHALF); - - assets = bound(assets, deposited + 1, type(uint256).max / (deposited + 10 ** ConstantsLib.DECIMALS_OFFSET)); + vm.startPrank(SUPPLIER); + uint256 shares = vault.deposit(deposited / 2, ONBEHALF); + vault.deposit(deposited / 2, SUPPLIER); + vm.stopPrank(); - uint256 toAdd = assets - deposited + 1; - loanToken.setBalance(SUPPLIER, toAdd); - - vm.prank(SUPPLIER); - vault.deposit(toAdd, SUPPLIER); + assets = bound(assets, deposited / 2 + 1, vault.totalAssets()); uint256 sharesBurnt = vault.previewWithdraw(assets); - vm.prank(ONBEHALF); vm.expectRevert( abi.encodeWithSelector(IERC20Errors.ERC20InsufficientBalance.selector, ONBEHALF, shares, sharesBurnt) ); + vm.prank(ONBEHALF); vault.withdraw(assets, RECEIVER, ONBEHALF); } @@ -271,7 +273,7 @@ contract ERC4626Test is IntegrationTest, IMorphoFlashLoanCallback { assets = bound(assets, deposited + 1, type(uint256).max / (deposited + 10 ** ConstantsLib.DECIMALS_OFFSET)); vm.prank(ONBEHALF); - vm.expectRevert(ErrorsLib.WithdrawMorphoFailed.selector); + vm.expectRevert(ErrorsLib.NotEnoughLiquidity.selector); vault.withdraw(assets, RECEIVER, ONBEHALF); } @@ -293,7 +295,7 @@ contract ERC4626Test is IntegrationTest, IMorphoFlashLoanCallback { morpho.borrow(allMarkets[0], 1, 0, BORROWER, BORROWER); vm.startPrank(ONBEHALF); - vm.expectRevert(ErrorsLib.WithdrawMorphoFailed.selector); + vm.expectRevert(ErrorsLib.NotEnoughLiquidity.selector); vault.withdraw(assets, RECEIVER, ONBEHALF); } diff --git a/test/forge/FeeTest.sol b/test/forge/FeeTest.sol index 2d3c75d7..ad51a21f 100644 --- a/test/forge/FeeTest.sol +++ b/test/forge/FeeTest.sol @@ -13,9 +13,6 @@ contract FeeTest is IntegrationTest { function setUp() public override { super.setUp(); - vm.prank(OWNER); - vault.setFeeRecipient(FEE_RECIPIENT); - _setFee(FEE); for (uint256 i; i < NB_MARKETS; ++i) { @@ -38,6 +35,7 @@ contract FeeTest is IntegrationTest { } _setCap(allMarkets[0], CAP); + _sortSupplyQueueIdleLast(); } function testSetFee(uint256 fee) public { diff --git a/test/forge/MarketTest.sol b/test/forge/MarketTest.sol index c9521f05..4b8f1eae 100644 --- a/test/forge/MarketTest.sol +++ b/test/forge/MarketTest.sol @@ -10,6 +10,42 @@ contract MarketTest is IntegrationTest { using MarketParamsLib for MarketParams; using MorphoLib for IMorpho; + function setUp() public override { + super.setUp(); + + _setCap(allMarkets[0], CAP); + _setCap(allMarkets[1], CAP); + _setCap(allMarkets[2], CAP); + } + + function testMintAllCapsReached() public { + vm.prank(ALLOCATOR); + vault.setSupplyQueue(new Id[](0)); + + loanToken.setBalance(SUPPLIER, 1); + + vm.prank(SUPPLIER); + loanToken.approve(address(vault), type(uint256).max); + + vm.expectRevert(ErrorsLib.AllCapsReached.selector); + vm.prank(SUPPLIER); + vault.mint(1, RECEIVER); + } + + function testDepositAllCapsReached() public { + vm.prank(ALLOCATOR); + vault.setSupplyQueue(new Id[](0)); + + loanToken.setBalance(SUPPLIER, 1); + + vm.prank(SUPPLIER); + loanToken.approve(address(vault), type(uint256).max); + + vm.expectRevert(ErrorsLib.AllCapsReached.selector); + vm.prank(SUPPLIER); + vault.deposit(1, RECEIVER); + } + function testSubmitCapOverflow(uint256 seed, uint256 cap) public { MarketParams memory marketParams = _randomMarketParams(seed); cap = bound(cap, uint256(type(uint184).max) + 1, type(uint256).max); @@ -38,20 +74,12 @@ contract MarketTest is IntegrationTest { } function testSubmitCapAlreadySet() public { - _setCap(allMarkets[0], CAP); - vm.prank(CURATOR); vm.expectRevert(ErrorsLib.AlreadySet.selector); vault.submitCap(allMarkets[0], CAP); } function testSetSupplyQueue() public { - _setCaps(); - - assertEq(Id.unwrap(vault.supplyQueue(0)), Id.unwrap(allMarkets[0].id())); - assertEq(Id.unwrap(vault.supplyQueue(1)), Id.unwrap(allMarkets[1].id())); - assertEq(Id.unwrap(vault.supplyQueue(2)), Id.unwrap(allMarkets[2].id())); - Id[] memory supplyQueue = new Id[](2); supplyQueue[0] = allMarkets[1].id(); supplyQueue[1] = allMarkets[2].id(); @@ -74,7 +102,7 @@ contract MarketTest is IntegrationTest { } function testAcceptCapMaxQueueLengthExceeded() public { - for (uint256 i; i < ConstantsLib.MAX_QUEUE_LENGTH; ++i) { + for (uint256 i = 3; i < ConstantsLib.MAX_QUEUE_LENGTH - 1; ++i) { _setCap(allMarkets[i], CAP); } @@ -93,31 +121,27 @@ contract MarketTest is IntegrationTest { function testSetSupplyQueueUnauthorizedMarket() public { Id[] memory supplyQueue = new Id[](1); - supplyQueue[0] = allMarkets[0].id(); + supplyQueue[0] = allMarkets[3].id(); vm.prank(ALLOCATOR); - vm.expectRevert(abi.encodeWithSelector(ErrorsLib.UnauthorizedMarket.selector, allMarkets[0].id())); + vm.expectRevert(abi.encodeWithSelector(ErrorsLib.UnauthorizedMarket.selector, supplyQueue[0])); vault.setSupplyQueue(supplyQueue); } function testUpdateWithdrawQueue() public { - _setCaps(); - - assertEq(Id.unwrap(vault.withdrawQueue(0)), Id.unwrap(allMarkets[0].id())); - assertEq(Id.unwrap(vault.withdrawQueue(1)), Id.unwrap(allMarkets[1].id())); - assertEq(Id.unwrap(vault.withdrawQueue(2)), Id.unwrap(allMarkets[2].id())); - - uint256[] memory indexes = new uint256[](3); + uint256[] memory indexes = new uint256[](4); indexes[0] = 1; indexes[1] = 2; - indexes[2] = 0; + indexes[2] = 3; + indexes[3] = 0; - Id[] memory expectedWithdrawQueue = new Id[](3); - expectedWithdrawQueue[0] = allMarkets[1].id(); - expectedWithdrawQueue[1] = allMarkets[2].id(); - expectedWithdrawQueue[2] = allMarkets[0].id(); + Id[] memory expectedWithdrawQueue = new Id[](4); + expectedWithdrawQueue[0] = allMarkets[0].id(); + expectedWithdrawQueue[1] = allMarkets[1].id(); + expectedWithdrawQueue[2] = allMarkets[2].id(); + expectedWithdrawQueue[3] = idleParams.id(); - vm.expectEmit(); + vm.expectEmit(address(vault)); emit EventsLib.SetWithdrawQueue(ALLOCATOR, expectedWithdrawQueue); vm.prank(ALLOCATOR); vault.updateWithdrawQueue(indexes); @@ -125,37 +149,10 @@ contract MarketTest is IntegrationTest { assertEq(Id.unwrap(vault.withdrawQueue(0)), Id.unwrap(expectedWithdrawQueue[0])); assertEq(Id.unwrap(vault.withdrawQueue(1)), Id.unwrap(expectedWithdrawQueue[1])); assertEq(Id.unwrap(vault.withdrawQueue(2)), Id.unwrap(expectedWithdrawQueue[2])); - } - - function testUpdateWithdrawQueueRemovingEmptyMarket() public { - _setCaps(); - - assertEq(Id.unwrap(vault.withdrawQueue(0)), Id.unwrap(allMarkets[0].id())); - assertEq(Id.unwrap(vault.withdrawQueue(1)), Id.unwrap(allMarkets[1].id())); - assertEq(Id.unwrap(vault.withdrawQueue(2)), Id.unwrap(allMarkets[2].id())); - - _setCap(allMarkets[2], 0); - - uint256[] memory indexes = new uint256[](2); - indexes[0] = 1; - indexes[1] = 0; - - Id[] memory expectedWithdrawQueue = new Id[](2); - expectedWithdrawQueue[0] = allMarkets[1].id(); - expectedWithdrawQueue[1] = allMarkets[0].id(); - - vm.expectEmit(); - emit EventsLib.SetWithdrawQueue(ALLOCATOR, expectedWithdrawQueue); - vm.prank(ALLOCATOR); - vault.updateWithdrawQueue(indexes); - - assertEq(Id.unwrap(vault.withdrawQueue(0)), Id.unwrap(expectedWithdrawQueue[0])); - assertEq(Id.unwrap(vault.withdrawQueue(1)), Id.unwrap(expectedWithdrawQueue[1])); + assertEq(Id.unwrap(vault.withdrawQueue(3)), Id.unwrap(expectedWithdrawQueue[3])); } function testUpdateWithdrawQueueRemovingDisabledMarket() public { - _setCaps(); - _setCap(allMarkets[2], 0); vm.prank(CURATOR); @@ -163,13 +160,15 @@ contract MarketTest is IntegrationTest { vm.warp(block.timestamp + TIMELOCK); - uint256[] memory indexes = new uint256[](2); - indexes[0] = 1; - indexes[1] = 0; + uint256[] memory indexes = new uint256[](3); + indexes[0] = 0; + indexes[1] = 2; + indexes[2] = 1; - Id[] memory expectedWithdrawQueue = new Id[](2); - expectedWithdrawQueue[0] = allMarkets[1].id(); - expectedWithdrawQueue[1] = allMarkets[0].id(); + Id[] memory expectedWithdrawQueue = new Id[](3); + expectedWithdrawQueue[0] = idleParams.id(); + expectedWithdrawQueue[1] = allMarkets[1].id(); + expectedWithdrawQueue[2] = allMarkets[0].id(); vm.expectEmit(); emit EventsLib.SetWithdrawQueue(ALLOCATOR, expectedWithdrawQueue); @@ -178,15 +177,15 @@ contract MarketTest is IntegrationTest { assertEq(Id.unwrap(vault.withdrawQueue(0)), Id.unwrap(expectedWithdrawQueue[0])); assertEq(Id.unwrap(vault.withdrawQueue(1)), Id.unwrap(expectedWithdrawQueue[1])); + assertEq(Id.unwrap(vault.withdrawQueue(2)), Id.unwrap(expectedWithdrawQueue[2])); } function testUpdateWithdrawQueueInvalidIndex() public { - _setCaps(); - - uint256[] memory indexes = new uint256[](3); + uint256[] memory indexes = new uint256[](4); indexes[0] = 1; indexes[1] = 2; indexes[2] = 3; + indexes[3] = 4; vm.prank(ALLOCATOR); vm.expectRevert(stdError.indexOOBError); @@ -194,82 +193,71 @@ contract MarketTest is IntegrationTest { } function testUpdateWithdrawQueueDuplicateMarket() public { - _setCaps(); - - uint256[] memory indexes = new uint256[](3); + uint256[] memory indexes = new uint256[](4); indexes[0] = 1; indexes[1] = 2; indexes[2] = 1; + indexes[3] = 3; vm.prank(ALLOCATOR); - vm.expectRevert(abi.encodeWithSelector(ErrorsLib.DuplicateMarket.selector, allMarkets[1].id())); + vm.expectRevert(abi.encodeWithSelector(ErrorsLib.DuplicateMarket.selector, allMarkets[0].id())); vault.updateWithdrawQueue(indexes); } function testUpdateWithdrawQueueInvalidMarketRemovalNonZeroSupply() public { - _setCaps(); - loanToken.setBalance(SUPPLIER, 1); vm.prank(SUPPLIER); vault.deposit(1, RECEIVER); - uint256[] memory indexes = new uint256[](2); + uint256[] memory indexes = new uint256[](3); indexes[0] = 1; indexes[1] = 2; + indexes[2] = 3; - _setCap(allMarkets[0], 0); + _setCap(idleParams, 0); vm.prank(ALLOCATOR); - vm.expectRevert( - abi.encodeWithSelector(ErrorsLib.InvalidMarketRemovalNonZeroSupply.selector, allMarkets[0].id()) - ); + vm.expectRevert(abi.encodeWithSelector(ErrorsLib.InvalidMarketRemovalNonZeroSupply.selector, idleParams.id())); vault.updateWithdrawQueue(indexes); } function testUpdateWithdrawQueueInvalidMarketRemovalNonZeroCap() public { - _setCaps(); - - uint256[] memory indexes = new uint256[](2); - indexes[0] = 0; + uint256[] memory indexes = new uint256[](3); + indexes[0] = 1; indexes[1] = 2; + indexes[2] = 3; + + vm.expectRevert(abi.encodeWithSelector(ErrorsLib.InvalidMarketRemovalNonZeroCap.selector, idleParams.id())); vm.prank(ALLOCATOR); - vm.expectRevert(abi.encodeWithSelector(ErrorsLib.InvalidMarketRemovalNonZeroCap.selector, allMarkets[1].id())); vault.updateWithdrawQueue(indexes); } function testUpdateWithdrawQueueInvalidMarketRemovalTimelockNotElapsed(uint256 elapsed) public { elapsed = bound(elapsed, 0, TIMELOCK - 1); - _setCaps(); - loanToken.setBalance(SUPPLIER, 1); vm.prank(SUPPLIER); vault.deposit(1, RECEIVER); - _setCap(allMarkets[0], 0); + _setCap(idleParams, 0); vm.prank(CURATOR); - vault.submitMarketRemoval(allMarkets[0].id()); + vault.submitMarketRemoval(idleParams.id()); vm.warp(block.timestamp + elapsed); - uint256[] memory indexes = new uint256[](2); + uint256[] memory indexes = new uint256[](3); indexes[0] = 1; indexes[1] = 2; + indexes[2] = 3; vm.prank(ALLOCATOR); vm.expectRevert( - abi.encodeWithSelector(ErrorsLib.InvalidMarketRemovalTimelockNotElapsed.selector, allMarkets[0].id()) + abi.encodeWithSelector(ErrorsLib.InvalidMarketRemovalTimelockNotElapsed.selector, idleParams.id()) ); vault.updateWithdrawQueue(indexes); } - - function _setCaps() internal { - _setCap(allMarkets[0], CAP); - _setCap(allMarkets[1], CAP); - _setCap(allMarkets[2], CAP); - } } diff --git a/test/forge/MetaMorphoFactoryTest.sol b/test/forge/MetaMorphoFactoryTest.sol index 25a38224..12686ebc 100644 --- a/test/forge/MetaMorphoFactoryTest.sol +++ b/test/forge/MetaMorphoFactoryTest.sol @@ -14,7 +14,7 @@ contract MetaMorphoFactoryTest is IntegrationTest { factory = new MetaMorphoFactory(address(morpho)); } - function testFactoryAddresssZero() public { + function testFactoryAddressZero() public { vm.expectRevert(ErrorsLib.ZeroAddress.selector); new MetaMorphoFactory(address(0)); } diff --git a/test/forge/MetaMorphoInternalTest.sol b/test/forge/MetaMorphoInternalTest.sol index 465cfdbb..11606211 100644 --- a/test/forge/MetaMorphoInternalTest.sol +++ b/test/forge/MetaMorphoInternalTest.sol @@ -46,10 +46,7 @@ contract MetaMorphoInternalTest is InternalTest { uint256 remaining = _simulateWithdrawMorpho(assets); - uint256 supplyShares = MORPHO.supplyShares(id, address(this)); - (uint256 totalSupplyAssets, uint256 totalSupplyShares,,) = MORPHO.expectedMarketBalances(allMarkets[0]); - - uint256 expectedWithdrawable = supplyShares.toAssetsDown(totalSupplyAssets, totalSupplyShares) - borrowedAmount; + uint256 expectedWithdrawable = MORPHO.expectedSupplyAssets(allMarkets[0], address(this)) - borrowedAmount; uint256 expectedRemaining = assets.zeroFloorSub(expectedWithdrawable); assertEq(remaining, expectedRemaining, "remaining"); diff --git a/test/forge/ReallocateIdleTest.sol b/test/forge/ReallocateIdleTest.sol index 6068381c..6561d3b9 100644 --- a/test/forge/ReallocateIdleTest.sol +++ b/test/forge/ReallocateIdleTest.sol @@ -17,17 +17,16 @@ contract ReallocateIdleTest is IntegrationTest { function setUp() public override { super.setUp(); - _setCap(allMarkets[0], CAP2); - _setCap(allMarkets[1], CAP2); - _setCap(allMarkets[2], CAP2); - - vm.prank(ALLOCATOR); - vault.setSupplyQueue(new Id[](0)); - loanToken.setBalance(SUPPLIER, INITIAL_DEPOSIT); vm.prank(SUPPLIER); vault.deposit(INITIAL_DEPOSIT, ONBEHALF); + + _setCap(allMarkets[0], CAP2); + _setCap(allMarkets[1], CAP2); + _setCap(allMarkets[2], CAP2); + + _sortSupplyQueueIdleLast(); } function testReallocateSupplyIdle(uint256[3] memory suppliedAssets) public { @@ -35,11 +34,13 @@ contract ReallocateIdleTest is IntegrationTest { suppliedAssets[1] = bound(suppliedAssets[1], 1, CAP2); suppliedAssets[2] = bound(suppliedAssets[2], 1, CAP2); + allocations.push(MarketAllocation(idleParams, 0)); allocations.push(MarketAllocation(allMarkets[0], suppliedAssets[0])); allocations.push(MarketAllocation(allMarkets[1], suppliedAssets[1])); allocations.push(MarketAllocation(allMarkets[2], suppliedAssets[2])); + allocations.push(MarketAllocation(idleParams, type(uint256).max)); - uint256 idleBefore = vault.idle(); + uint256 idleBefore = _idle(); vm.prank(ALLOCATOR); vault.reallocate(allocations); @@ -61,6 +62,6 @@ contract ReallocateIdleTest is IntegrationTest { ); uint256 expectedIdle = idleBefore - suppliedAssets[0] - suppliedAssets[1] - suppliedAssets[2]; - assertApproxEqAbs(vault.idle(), expectedIdle, 3, "vault.idle() 1"); + assertApproxEqAbs(_idle(), expectedIdle, 3, "idle"); } } diff --git a/test/forge/ReallocateWithdrawTest.sol b/test/forge/ReallocateWithdrawTest.sol index 9c1979b8..b1fb9851 100644 --- a/test/forge/ReallocateWithdrawTest.sol +++ b/test/forge/ReallocateWithdrawTest.sol @@ -25,6 +25,8 @@ contract ReallocateWithdrawTest is IntegrationTest { _setCap(allMarkets[1], CAP2); _setCap(allMarkets[2], CAP2); + _sortSupplyQueueIdleLast(); + loanToken.setBalance(SUPPLIER, INITIAL_DEPOSIT); vm.prank(SUPPLIER); @@ -35,6 +37,7 @@ contract ReallocateWithdrawTest is IntegrationTest { allocations.push(MarketAllocation(allMarkets[0], 0)); allocations.push(MarketAllocation(allMarkets[1], 0)); allocations.push(MarketAllocation(allMarkets[2], 0)); + allocations.push(MarketAllocation(idleParams, type(uint256).max)); vm.prank(ALLOCATOR); vault.reallocate(allocations); @@ -42,7 +45,7 @@ contract ReallocateWithdrawTest is IntegrationTest { assertEq(morpho.supplyShares(allMarkets[0].id(), address(vault)), 0, "morpho.supplyShares(0)"); assertEq(morpho.supplyShares(allMarkets[1].id(), address(vault)), 0, "morpho.supplyShares(1)"); assertEq(morpho.supplyShares(allMarkets[2].id(), address(vault)), 0, "morpho.supplyShares(2)"); - assertEq(vault.idle(), INITIAL_DEPOSIT, "vault.idle() 1"); + assertEq(_idle(), INITIAL_DEPOSIT, "idle"); } function testReallocateWithdrawInconsistentAsset() public { @@ -65,43 +68,63 @@ contract ReallocateWithdrawTest is IntegrationTest { vault.reallocate(allocations); } - function testReallocateWithdrawSupply(uint256[3] memory assets) public { + function testReallocateWithdrawSupply(uint256[3] memory newAssets) public { uint256[3] memory totalSupplyAssets; uint256[3] memory totalSupplyShares; (totalSupplyAssets[0], totalSupplyShares[0],,) = morpho.expectedMarketBalances(allMarkets[0]); (totalSupplyAssets[1], totalSupplyShares[1],,) = morpho.expectedMarketBalances(allMarkets[1]); (totalSupplyAssets[2], totalSupplyShares[2],,) = morpho.expectedMarketBalances(allMarkets[2]); - assets[0] = bound(assets[0], 0, CAP2); - assets[1] = bound(assets[1], 0, CAP2); - assets[2] = bound(assets[2], 0, CAP2); + newAssets[0] = bound(newAssets[0], 0, CAP2); + newAssets[1] = bound(newAssets[1], 0, CAP2); + newAssets[2] = bound(newAssets[2], 0, CAP2); + + uint256[3] memory assets; + assets[0] = morpho.expectedSupplyAssets(allMarkets[0], address(vault)); + assets[1] = morpho.expectedSupplyAssets(allMarkets[1], address(vault)); + assets[2] = morpho.expectedSupplyAssets(allMarkets[2], address(vault)); + + allocations.push(MarketAllocation(idleParams, 0)); + allocations.push(MarketAllocation(allMarkets[0], newAssets[0])); + allocations.push(MarketAllocation(allMarkets[1], newAssets[1])); + allocations.push(MarketAllocation(allMarkets[2], newAssets[2])); + allocations.push(MarketAllocation(idleParams, type(uint256).max)); + + uint256 expectedIdle = _idle() + 3 * CAP2 - newAssets[0] - newAssets[1] - newAssets[2]; + + emit EventsLib.ReallocateWithdraw(ALLOCATOR, idleParams.id(), 0, 0); + + if (newAssets[0] < assets[0]) emit EventsLib.ReallocateWithdraw(ALLOCATOR, allMarkets[0].id(), 0, 0); + else if (newAssets[0] > assets[0]) emit EventsLib.ReallocateSupply(ALLOCATOR, allMarkets[0].id(), 0, 0); + + if (newAssets[1] < assets[1]) emit EventsLib.ReallocateWithdraw(ALLOCATOR, allMarkets[1].id(), 0, 0); + else if (newAssets[1] > assets[1]) emit EventsLib.ReallocateSupply(ALLOCATOR, allMarkets[1].id(), 0, 0); - allocations.push(MarketAllocation(allMarkets[0], assets[0])); - allocations.push(MarketAllocation(allMarkets[1], assets[1])); - allocations.push(MarketAllocation(allMarkets[2], assets[2])); + if (newAssets[2] < assets[2]) emit EventsLib.ReallocateWithdraw(ALLOCATOR, allMarkets[2].id(), 0, 0); + else if (newAssets[2] > assets[2]) emit EventsLib.ReallocateSupply(ALLOCATOR, allMarkets[2].id(), 0, 0); - uint256 expectedIdle = vault.idle() + 3 * CAP2 - assets[0] - assets[1] - assets[2]; + emit EventsLib.ReallocateSupply(ALLOCATOR, idleParams.id(), 0, 0); vm.prank(ALLOCATOR); vault.reallocate(allocations); assertEq( morpho.supplyShares(allMarkets[0].id(), address(vault)), - assets[0] * SharesMathLib.VIRTUAL_SHARES, + newAssets[0] * SharesMathLib.VIRTUAL_SHARES, "morpho.supplyShares(0)" ); assertApproxEqAbs( morpho.supplyShares(allMarkets[1].id(), address(vault)), - assets[1] * SharesMathLib.VIRTUAL_SHARES, + newAssets[1] * SharesMathLib.VIRTUAL_SHARES, SharesMathLib.VIRTUAL_SHARES, "morpho.supplyShares(1)" ); assertEq( morpho.supplyShares(allMarkets[2].id(), address(vault)), - assets[2] * SharesMathLib.VIRTUAL_SHARES, + newAssets[2] * SharesMathLib.VIRTUAL_SHARES, "morpho.supplyShares(2)" ); - assertApproxEqAbs(vault.idle(), expectedIdle, 1, "vault.idle() 1"); + assertApproxEqAbs(_idle(), expectedIdle, 1, "idle"); } function testReallocateUnauthorizedMarket(uint256[3] memory suppliedAssets) public { @@ -136,20 +159,18 @@ contract ReallocateWithdrawTest is IntegrationTest { vault.reallocate(allocations); } - function testReallocateInsufficientIdle(uint256 rewards) public { + function testReallocateInconsistentReallocation(uint256 rewards) public { rewards = bound(rewards, 1, MAX_TEST_ASSETS); - address rewardDonator = makeAddr("reward donator"); - loanToken.setBalance(rewardDonator, rewards); - vm.prank(rewardDonator); - loanToken.transfer(address(vault), rewards); + loanToken.setBalance(address(vault), rewards); _setCap(allMarkets[0], type(uint184).max); + allocations.push(MarketAllocation(idleParams, 0)); allocations.push(MarketAllocation(allMarkets[0], 2 * CAP2 + rewards)); vm.prank(ALLOCATOR); - vm.expectRevert(ErrorsLib.InsufficientIdle.selector); + vm.expectRevert(ErrorsLib.InconsistentReallocation.selector); vault.reallocate(allocations); } } diff --git a/test/forge/ReentrancyTest.sol b/test/forge/ReentrancyTest.sol index 1e122871..17fd1702 100644 --- a/test/forge/ReentrancyTest.sol +++ b/test/forge/ReentrancyTest.sol @@ -9,54 +9,66 @@ import {IMetaMorpho} from "src/interfaces/IMetaMorpho.sol"; import "src/MetaMorphoFactory.sol"; import "./helpers/IntegrationTest.sol"; -contract MetaMorphoTest is IntegrationTest, IERC1820Implementer { - bytes32 private constant _TOKENS_SENDER_INTERFACE_HASH = keccak256("ERC777TokensSender"); - bytes32 private constant _TOKENS_RECIPIENT_INTERFACE_HASH = keccak256("ERC777TokensRecipient"); +uint256 constant FEE = 0.1 ether; // 50% +bytes32 constant TOKENS_SENDER_INTERFACE_HASH = keccak256("ERC777TokensSender"); +bytes32 constant TOKENS_RECIPIENT_INTERFACE_HASH = keccak256("ERC777TokensRecipient"); + +contract ReentrancyTest is IntegrationTest, IERC1820Implementer { address internal attacker = makeAddr("attacker"); - MetaMorpho newVault; - MetaMorphoFactory factory; ERC777Mock internal reentrantToken; ERC1820Registry internal registry; function setUp() public override { super.setUp(); - console2.log("here0"); - registry = new ERC1820Registry(); - registry.setInterfaceImplementer(address(this), _TOKENS_SENDER_INTERFACE_HASH, address(this)); - registry.setInterfaceImplementer(address(this), _TOKENS_RECIPIENT_INTERFACE_HASH, address(this)); + registry.setInterfaceImplementer(address(this), TOKENS_SENDER_INTERFACE_HASH, address(this)); + registry.setInterfaceImplementer(address(this), TOKENS_RECIPIENT_INTERFACE_HASH, address(this)); reentrantToken = new ERC777Mock(100_000, new address[](0), IERC1820Registry(address(registry))); - factory = new MetaMorphoFactory(address(morpho)); - newVault = factory.createMetaMorpho( - OWNER, - ConstantsLib.MIN_TIMELOCK, - address(reentrantToken), - "MTK_VAULT", - "MTK_V", - keccak256(abi.encode("salt")) + idleParams = MarketParams({ + loanToken: address(reentrantToken), + collateralToken: address(0), + oracle: address(0), + irm: address(0), + lltv: 0 + }); + + morpho.createMarket(idleParams); + + vault = IMetaMorpho( + address( + new MetaMorpho(OWNER, address(morpho), TIMELOCK, address(reentrantToken), "MetaMorpho Vault", "MMV") + ) ); - // Set fee to 50% - uint256 fee = 0.5 ether; // 50% vm.startPrank(OWNER); - newVault.setFeeRecipient(FEE_RECIPIENT); - newVault.setFee(fee); + vault.setCurator(CURATOR); + vault.setIsAllocator(ALLOCATOR, true); + vault.setFeeRecipient(FEE_RECIPIENT); + vm.stopPrank(); + + _setCap(idleParams, type(uint184).max); + _setFee(FEE); + + reentrantToken.approve(address(vault), type(uint256).max); + + vm.prank(SUPPLIER); + reentrantToken.approve(address(vault), type(uint256).max); reentrantToken.setBalance(SUPPLIER, 100_000 ether); // SUPPLIER supplies 100_000e18 tokens to MetaMorpho. + console2.log("Supplier starting with %s tokens.", loanToken.balanceOf(SUPPLIER)); - vm.startPrank(SUPPLIER); - reentrantToken.approve(address(newVault), 100_000 ether); - uint256 userShares = newVault.deposit(100_000 ether, SUPPLIER); - vm.stopPrank(); + + vm.prank(SUPPLIER); + uint256 userShares = vault.deposit(100_000 ether, SUPPLIER); console2.log( "Supplier deposited %s loanTokens to metaMorpho_no_timelock in exchange for %s shares.", - newVault.previewRedeem(userShares), + vault.previewRedeem(userShares), userShares ); console2.log("Finished setUp."); @@ -70,37 +82,37 @@ contract MetaMorphoTest is IntegrationTest, IERC1820Implementer { vm.startPrank(attacker); - registry.setInterfaceImplementer(attacker, _TOKENS_SENDER_INTERFACE_HASH, address(this)); // Set test contract + registry.setInterfaceImplementer(attacker, TOKENS_SENDER_INTERFACE_HASH, address(this)); // Set test contract // to receive ERC-777 callbacks. - registry.setInterfaceImplementer(attacker, _TOKENS_RECIPIENT_INTERFACE_HASH, address(this)); // Required "hack" + registry.setInterfaceImplementer(attacker, TOKENS_RECIPIENT_INTERFACE_HASH, address(this)); // Required "hack" // because done all in a single Foundry test. - reentrantToken.approve(address(newVault), 100_000); + reentrantToken.approve(address(vault), 100_000); - newVault.deposit(1, attacker); // Initial deposit of 1 token to be able to call withdraw(1) in the subcall + vault.deposit(1, attacker); // Initial deposit of 1 token to be able to call withdraw(1) in the subcall // before depositing(5000) - newVault.deposit(5_000, attacker); // Deposit 5000, withdraw 1 in the subcall. Total deposited 4999, + vault.deposit(5_000, attacker); // Deposit 5000, withdraw 1 in the subcall. Total deposited 4999, // lastTotalAssets only updated by +1. vm.startPrank(attacker); // Have to re-call startPrank because contract was reentered. Hacky but works. - newVault.deposit(5_000, attacker); // Same as above. Accrue yield accrues 50% * (newTotalAssets - + vault.deposit(5_000, attacker); // Same as above. Accrue yield accrues 50% * (newTotalAssets - // lastTotalAssets) = 50% * 4999 = ~2499. lastTotalAssets only updated by +1. vm.startPrank(attacker); - newVault.deposit(5_000, attacker); // ~2499 tokens taken as fees. + vault.deposit(5_000, attacker); // ~2499 tokens taken as fees. vm.startPrank(attacker); - newVault.deposit(5_000, attacker); // ~2499 tokens taken as fees. + vault.deposit(5_000, attacker); // ~2499 tokens taken as fees. // Withdraw everything vm.startPrank(attacker); - newVault.withdraw(newVault.maxWithdraw(attacker), attacker, attacker); // Withdraw 99_999 tokens, cost of attack + vault.withdraw(vault.maxWithdraw(attacker), attacker, attacker); // Withdraw 99_999 tokens, cost of attack // = 1 token vm.startPrank(FEE_RECIPIENT); - newVault.withdraw(newVault.maxWithdraw(FEE_RECIPIENT), FEE_RECIPIENT, FEE_RECIPIENT); // Fee recipient withdraws + vault.withdraw(vault.maxWithdraw(FEE_RECIPIENT), FEE_RECIPIENT, FEE_RECIPIENT); // Fee recipient withdraws // 9_999 tokens, stolen from `SUPPLIER` console2.log("Attacker ending with %s tokens", reentrantToken.balanceOf(attacker)); // 99_999 diff --git a/test/forge/RevokeTest.sol b/test/forge/RevokeTest.sol index 2cd5f61f..f0b12681 100644 --- a/test/forge/RevokeTest.sol +++ b/test/forge/RevokeTest.sol @@ -13,9 +13,6 @@ contract RevokeTest is IntegrationTest { function setUp() public override { super.setUp(); - vm.prank(OWNER); - vault.setFeeRecipient(FEE_RECIPIENT); - _setFee(FEE); _setGuardian(GUARDIAN); } diff --git a/test/forge/RoleTest.sol b/test/forge/RoleTest.sol index ecacee97..34ae6ceb 100644 --- a/test/forge/RoleTest.sol +++ b/test/forge/RoleTest.sol @@ -116,10 +116,8 @@ contract RoleTest is IntegrationTest { } function testAllocatorOrCuratorOrOwnerShouldTriggerAllocatorFunctions() public { - _setCap(allMarkets[0], CAP); - Id[] memory supplyQueue = new Id[](1); - supplyQueue[0] = allMarkets[0].id(); + supplyQueue[0] = idleParams.id(); uint256[] memory withdrawQueueFromRanks = new uint256[](1); withdrawQueueFromRanks[0] = 0; diff --git a/test/forge/TimelockTest.sol b/test/forge/TimelockTest.sol index f86cf5ff..3ca97a1c 100644 --- a/test/forge/TimelockTest.sol +++ b/test/forge/TimelockTest.sol @@ -11,9 +11,6 @@ contract TimelockTest is IntegrationTest { function setUp() public override { super.setUp(); - vm.prank(OWNER); - vault.setFeeRecipient(FEE_RECIPIENT); - _setFee(FEE); _setGuardian(GUARDIAN); @@ -325,8 +322,8 @@ contract TimelockTest is IntegrationTest { assertEq(marketConfig.removableAt, 0, "marketConfig.removableAt"); assertEq(pendingCap.value, cap, "pendingCap.value"); assertEq(pendingCap.validAt, block.timestamp + TIMELOCK, "pendingCap.validAt"); - assertEq(vault.supplyQueueLength(), 1, "supplyQueueLength"); - assertEq(vault.withdrawQueueLength(), 1, "withdrawQueueLength"); + assertEq(vault.supplyQueueLength(), 2, "supplyQueueLength"); + assertEq(vault.withdrawQueueLength(), 2, "withdrawQueueLength"); } function testSubmitCapAlreadyPending(uint256 cap) public { @@ -365,8 +362,8 @@ contract TimelockTest is IntegrationTest { assertEq(marketConfig.removableAt, 0, "marketConfig.removableAt"); assertEq(pendingCap.value, 0, "pendingCap.value"); assertEq(pendingCap.validAt, 0, "pendingCap.validAt"); - assertEq(Id.unwrap(vault.supplyQueue(0)), Id.unwrap(id), "supplyQueue"); - assertEq(Id.unwrap(vault.withdrawQueue(0)), Id.unwrap(id), "withdrawQueue"); + assertEq(Id.unwrap(vault.supplyQueue(1)), Id.unwrap(id), "supplyQueue"); + assertEq(Id.unwrap(vault.withdrawQueue(1)), Id.unwrap(id), "withdrawQueue"); } function testAcceptCapIncreasedTimelockIncreased(uint256 cap, uint256 timelock, uint256 elapsed) public { @@ -396,8 +393,8 @@ contract TimelockTest is IntegrationTest { assertEq(marketConfig.removableAt, 0, "marketConfig.removableAt"); assertEq(pendingCap.value, 0, "pendingCap.value"); assertEq(pendingCap.validAt, 0, "pendingCap.validAt"); - assertEq(Id.unwrap(vault.supplyQueue(0)), Id.unwrap(id), "supplyQueue"); - assertEq(Id.unwrap(vault.withdrawQueue(0)), Id.unwrap(id), "withdrawQueue"); + assertEq(Id.unwrap(vault.supplyQueue(1)), Id.unwrap(id), "supplyQueue"); + assertEq(Id.unwrap(vault.withdrawQueue(1)), Id.unwrap(id), "withdrawQueue"); } function testAcceptCapIncreasedTimelockDecreased(uint256 cap, uint256 timelock, uint256 elapsed) public { diff --git a/test/forge/UrdTest.sol b/test/forge/UrdTest.sol index f2f2f1e9..8530dc09 100644 --- a/test/forge/UrdTest.sol +++ b/test/forge/UrdTest.sol @@ -62,37 +62,7 @@ contract UrdTest is IntegrationTest { assertEq( collateralToken.balanceOf(address(rewardsDistributor)), amount, - "collateralToken.balanceOf(address(rewardsDistributor))" - ); - } - - function testSkimLoanToken(uint256 rewards, uint256 idle) public { - idle = bound(idle, 0, MAX_TEST_ASSETS); - rewards = bound(rewards, 0, MAX_TEST_ASSETS); - - vm.prank(OWNER); - vault.setSkimRecipient(address(rewardsDistributor)); - - loanToken.setBalance(address(vault), rewards); - - loanToken.setBalance(address(SUPPLIER), idle); - vm.prank(SUPPLIER); - vault.deposit(idle, SUPPLIER); - - assertEq(vault.idle(), idle, "vault.idle()"); - uint256 vaultBalanceBefore = loanToken.balanceOf(address(vault)); - assertEq(vaultBalanceBefore, idle + rewards, "vaultBalanceBefore"); - - vm.expectEmit(address(vault)); - emit EventsLib.Skim(address(this), address(loanToken), rewards); - vault.skim(address(loanToken)); - uint256 vaultBalanceAfter = loanToken.balanceOf(address(vault)); - - assertEq(vaultBalanceAfter, idle, "vaultBalanceAfter"); - assertEq( - loanToken.balanceOf(address(rewardsDistributor)), - rewards, - "loanToken.balanceOf(address(rewardsDistributor))" + "collateralToken.balanceOf(rewardsDistributor)" ); } diff --git a/test/forge/helpers/BaseTest.sol b/test/forge/helpers/BaseTest.sol index 81862844..c14763dc 100644 --- a/test/forge/helpers/BaseTest.sol +++ b/test/forge/helpers/BaseTest.sol @@ -59,6 +59,7 @@ contract BaseTest is Test { IrmMock internal irm = new IrmMock(); MarketParams[] internal allMarkets; + MarketParams internal idleParams; function setUp() public virtual { vm.label(address(morpho), "Morpho"); @@ -71,11 +72,24 @@ contract BaseTest is Test { irm.setApr(0.5 ether); // 50%. + idleParams = MarketParams({ + loanToken: address(loanToken), + collateralToken: address(0), + oracle: address(0), + irm: address(0), + lltv: 0 + }); + vm.startPrank(MORPHO_OWNER); + morpho.enableIrm(address(0)); morpho.enableIrm(address(irm)); morpho.setFeeRecipient(MORPHO_FEE_RECIPIENT); + + morpho.enableLltv(0); vm.stopPrank(); + morpho.createMarket(idleParams); + for (uint256 i; i < NB_MARKETS; ++i) { uint256 lltv = 0.8 ether / (i + 1); @@ -87,14 +101,16 @@ contract BaseTest is Test { lltv: lltv }); - vm.startPrank(MORPHO_OWNER); + vm.prank(MORPHO_OWNER); morpho.enableLltv(lltv); + morpho.createMarket(marketParams); - vm.stopPrank(); allMarkets.push(marketParams); } + allMarkets.push(idleParams); // Must be pushed last. + vm.startPrank(SUPPLIER); loanToken.approve(address(morpho), type(uint256).max); collateralToken.approve(address(morpho), type(uint256).max); @@ -131,8 +147,9 @@ contract BaseTest is Test { morpho.withdrawCollateral(market, 1, address(this), address(10)); } + /// @dev Returns a random market params from the list of markets enabled on Blue (except the idle market). function _randomMarketParams(uint256 seed) internal view returns (MarketParams memory) { - return allMarkets[seed % allMarkets.length]; + return allMarkets[seed % (allMarkets.length - 1)]; } function _randomCandidate(address[] memory candidates, uint256 seed) internal pure returns (address) { diff --git a/test/forge/helpers/IntegrationTest.sol b/test/forge/helpers/IntegrationTest.sol index f2716e81..8d7bbce1 100644 --- a/test/forge/helpers/IntegrationTest.sol +++ b/test/forge/helpers/IntegrationTest.sol @@ -7,6 +7,7 @@ uint256 constant TIMELOCK = 1 weeks; contract IntegrationTest is BaseTest { using MathLib for uint256; + using MorphoBalancesLib for IMorpho; using MarketParamsLib for MarketParams; IMetaMorpho internal vault; @@ -21,8 +22,11 @@ contract IntegrationTest is BaseTest { vm.startPrank(OWNER); vault.setCurator(CURATOR); vault.setIsAllocator(ALLOCATOR, true); + vault.setFeeRecipient(FEE_RECIPIENT); vm.stopPrank(); + _setCap(idleParams, type(uint184).max); + loanToken.approve(address(vault), type(uint256).max); collateralToken.approve(address(vault), type(uint256).max); @@ -37,6 +41,10 @@ contract IntegrationTest is BaseTest { vm.stopPrank(); } + function _idle() internal view returns (uint256) { + return morpho.expectedSupplyAssets(idleParams, address(vault)); + } + function _setTimelock(uint256 newTimelock) internal { uint256 timelock = vault.timelock(); if (newTimelock == timelock) return; @@ -107,4 +115,27 @@ contract IntegrationTest is BaseTest { assertEq(vault.config(id).cap, newCap, "_setCap"); } + + function _sortSupplyQueueIdleLast() internal { + Id[] memory supplyQueue = new Id[](vault.supplyQueueLength()); + + uint256 supplyIndex; + for (uint256 i; i < supplyQueue.length; ++i) { + Id id = vault.supplyQueue(i); + if (Id.unwrap(id) == Id.unwrap(idleParams.id())) continue; + + supplyQueue[supplyIndex] = id; + ++supplyIndex; + } + + supplyQueue[supplyIndex] = idleParams.id(); + ++supplyIndex; + + assembly { + mstore(supplyQueue, supplyIndex) + } + + vm.prank(ALLOCATOR); + vault.setSupplyQueue(supplyQueue); + } } diff --git a/test/hardhat/MetaMorpho.spec.ts b/test/hardhat/MetaMorpho.spec.ts index bcb0be1a..2a60c668 100644 --- a/test/hardhat/MetaMorpho.spec.ts +++ b/test/hardhat/MetaMorpho.spec.ts @@ -1,4 +1,4 @@ -import { AbiCoder, MaxUint256, ZeroHash, keccak256, toBigInt } from "ethers"; +import { AbiCoder, MaxUint256, ZeroAddress, ZeroHash, keccak256, toBigInt } from "ethers"; import hre from "hardhat"; import _range from "lodash/range"; import { ERC20Mock, OracleMock, MetaMorpho, IMorpho, MetaMorphoFactory, MetaMorpho__factory, IrmMock } from "types"; @@ -79,6 +79,7 @@ describe("MetaMorpho", () => { let supplyCap: bigint; let allMarketParams: MarketParamsStruct[]; + let idleParams: MarketParamsStruct; const expectedMarket = async (marketParams: MarketParamsStruct) => { const id = identifier(marketParams); @@ -158,6 +159,14 @@ describe("MetaMorpho", () => { const oracleAddress = await oracle.getAddress(); const irmAddress = await irm.getAddress(); + idleParams = { + loanToken: loanAddress, + collateralToken: ZeroAddress, + oracle: ZeroAddress, + irm: ZeroAddress, + lltv: 0n, + }; + allMarketParams = _range(1, 1 + nbMarkets).map((i) => ({ loanToken: loanAddress, collateralToken: collateralAddress, @@ -173,6 +182,10 @@ describe("MetaMorpho", () => { await morpho.createMarket(marketParams); } + await morpho.enableIrm(idleParams.irm); + await morpho.enableLltv(idleParams.lltv); + await morpho.createMarket(idleParams); + const MetaMorphoFactoryFactory = await hre.ethers.getContractFactory("MetaMorphoFactory", admin); factory = await MetaMorphoFactoryFactory.deploy(morphoAddress); @@ -208,14 +221,24 @@ describe("MetaMorpho", () => { await metaMorpho.connect(curator).submitCap(marketParams, supplyCap); } + await metaMorpho.connect(curator).submitCap(idleParams, 2n ** 184n - 1n); + await forwardTimestamp(timelock); + await metaMorpho.connect(admin).acceptCap(identifier(idleParams)); + for (const marketParams of allMarketParams) { await metaMorpho.connect(admin).acceptCap(identifier(marketParams)); } - await metaMorpho.connect(curator).setSupplyQueue(allMarketParams.map(identifier)); - await metaMorpho.connect(curator).updateWithdrawQueue(allMarketParams.map((_, i) => nbMarkets - 1 - i)); + await metaMorpho.connect(curator).setSupplyQueue( + // Set idle market last. + allMarketParams.map(identifier).concat([identifier(idleParams)]), + ); + await metaMorpho.connect(curator).updateWithdrawQueue( + // Keep idle market first. + [0].concat(allMarketParams.map((_, i) => nbMarkets - i)), + ); hre.tracer.nameTags[morphoAddress] = "Morpho"; hre.tracer.nameTags[collateralAddress] = "Collateral"; @@ -279,7 +302,13 @@ describe("MetaMorpho", () => { assets: remaining + marketAssets, })); - await metaMorpho.connect(allocator).reallocate(allocations); + await metaMorpho.connect(allocator).reallocate( + // Always withdraw all from idle first. + [{ marketParams: idleParams, assets: 0n }] + .concat(allocations) + // Always supply remaining to idle last. + .concat([{ marketParams: idleParams, assets: MaxUint256 }]), + ); // Borrow liquidity to generate interest.