From abe7c8714e49ff59dcfa53af68f1ef00b9a5720c Mon Sep 17 00:00:00 2001 From: MathisGD Date: Mon, 23 Oct 2023 16:20:44 +0200 Subject: [PATCH 01/27] feat: remove idle supply --- src/MetaMorpho.sol | 93 +++++++++++++-------------- src/interfaces/IMetaMorpho.sol | 3 +- src/libraries/ErrorsLib.sol | 4 +- test/forge/ERC4626Test.sol | 41 ++---------- test/forge/MetaMorphoFactoryTest.sol | 2 +- test/forge/ReallocateIdleTest.sol | 56 ---------------- test/forge/ReallocateWithdrawTest.sol | 49 +++----------- test/forge/UrdTest.sol | 38 ++--------- 8 files changed, 69 insertions(+), 217 deletions(-) delete mode 100644 test/forge/ReallocateIdleTest.sol diff --git a/src/MetaMorpho.sol b/src/MetaMorpho.sol index 3219f9ee..35aab153 100644 --- a/src/MetaMorpho.sol +++ b/src/MetaMorpho.sol @@ -89,10 +89,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. uint256 public lastTotalAssets; @@ -376,6 +372,10 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph if (supplyCap == 0) revert ErrorsLib.UnauthorizedMarket(id); + if (allocation.assets == type(uint256).max) { + allocation.assets = totalWithdrawn - totalSupplied; + } + (uint256 suppliedAssets,) = MORPHO.supply(allocation.marketParams, allocation.assets, allocation.shares, address(this), hex""); @@ -386,14 +386,7 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph totalSupplied += suppliedAssets; } - if (totalWithdrawn > totalSupplied) { - idle += totalWithdrawn - totalSupplied; - } else { - uint256 idleSupplied = totalSupplied - totalWithdrawn; - if (idle < idleSupplied) revert ErrorsLib.InsufficientIdle(); - - idle -= idleSupplied; - } + if (totalWithdrawn != totalSupplied) revert ErrorsLib.InconsistentReallocation(); } /* ONLY GUARDIAN FUNCTIONS */ @@ -451,13 +444,11 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph _setCap(id, pendingCap[id].value); } - /// @notice Transfers `token` rewards collected by the vault to the `rewardsRecipient`. - /// @dev Can be used to extract any token that would be stuck on the contract as well. - function transferRewards(address token) external { + /// @notice TODO. + function skim(address token) external { if (rewardsRecipient == address(0)) revert ErrorsLib.ZeroAddress(); uint256 amount = IERC20(token).balanceOf(address(this)); - if (token == asset()) amount -= idle; SafeERC20.safeTransfer(IERC20(token), rewardsRecipient, amount); @@ -471,6 +462,18 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph return ERC4626.decimals(); } + /// @inheritdoc IERC4626 + function maxDeposit(address) public view override(IERC4626, ERC4626) returns (uint256) { + return _maxSupply(); + } + + /// @inheritdoc IERC4626 + function maxMint(address) public view override(IERC4626, ERC4626) returns (uint256) { + uint256 suppliable = _maxSupply(); + + return _convertToShares(suppliable, Math.Rounding.Floor); + } + /// @inheritdoc IERC4626 function maxWithdraw(address owner) public view override(IERC4626, ERC4626) returns (uint256 assets) { (assets,,) = _maxWithdraw(owner); @@ -532,8 +535,6 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph for (uint256 i; i < withdrawQueue.length; ++i) { assets += _supplyBalance(_marketParams(withdrawQueue[i])); } - - assets += idle; } /* ERC4626 (INTERNAL) */ @@ -558,6 +559,17 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph assets -= _staticWithdrawMorpho(assets); } + function _maxSupply() + internal + view + returns (uint256 totalSuppliable) + { + for (uint256 i; i < supplyQueue.length; ++i) { + Id id = supplyQueue[i]; + totalSuppliable += _suppliable(_marketParams(id), id); + } + } + /// @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) { @@ -618,7 +630,7 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph internal override { - if (_withdrawMorpho(assets) != 0) revert ErrorsLib.WithdrawMorphoFailed(); + _withdrawMorpho(assets); super._withdraw(caller, receiver, owner, assets, shares); @@ -703,7 +715,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]; @@ -720,41 +732,32 @@ 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 TODO. + function _withdrawMorpho(uint256 assets) internal { for (uint256 i; i < withdrawQueue.length; ++i) { Id id = withdrawQueue[i]; MarketParams memory marketParams = _marketParams(id); - uint256 toWithdraw = UtilsLib.min(_withdrawable(marketParams, id), remaining); + uint256 toWithdraw = UtilsLib.min(_withdrawable(marketParams, id), 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; } + if (assets != 0) revert ErrorsLib.WithdrawMorphoFailed(); } - /// @dev Fakes a withdraw of `assets` from the idle liquidity and Morpho if necessary. - /// @return remaining The assets left to be withdrawn. - function _staticWithdrawMorpho(uint256 assets) internal view returns (uint256 remaining) { - (remaining,) = _withdrawIdle(assets); - - if (remaining == 0) return 0; - + /// @dev Fakes a withdraw of `assets` from Morpho. + /// @return The assets left to be withdrawn. + function _staticWithdrawMorpho(uint256 assets) internal view returns (uint256) { for (uint256 i; i < withdrawQueue.length; ++i) { Id id = withdrawQueue[i]; MarketParams memory marketParams = _marketParams(id); @@ -763,17 +766,11 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph // 1. oracle.price() is never called (the vault doesn't borrow) // 2. `_withdrawable` caps to the liquidity available on Morpho // 3. virtually accruing interest didn't fail in `_withdrawable` - remaining -= UtilsLib.min(_withdrawable(marketParams, id), remaining); + assets -= UtilsLib.min(_withdrawable(marketParams, id), assets); - 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 suppliable amount of assets on the market defined by `marketParams`. diff --git a/src/interfaces/IMetaMorpho.sol b/src/interfaces/IMetaMorpho.sol index 99ce58b6..b34e71fd 100644 --- a/src/interfaces/IMetaMorpho.sol +++ b/src/interfaces/IMetaMorpho.sol @@ -52,7 +52,6 @@ interface IMetaMorpho is IERC4626 { function withdrawQueueSize() external view returns (uint256); function config(Id) external view returns (uint192 cap, uint64 withdrawRank); - function idle() external view returns (uint256); function lastTotalAssets() external view returns (uint256); function submitTimelock(uint256 newTimelock) external; @@ -74,7 +73,7 @@ interface IMetaMorpho is IERC4626 { function revokeGuardian() external; function pendingGuardian() external view returns (address guardian, uint96 submittedAt); - function transferRewards(address) external; + function skim(address) external; function setIsAllocator(address newAllocator, bool newIsAllocator) external; function setCurator(address newCurator) external; diff --git a/src/libraries/ErrorsLib.sol b/src/libraries/ErrorsLib.sol index fe16ec44..9c078600 100644 --- a/src/libraries/ErrorsLib.sol +++ b/src/libraries/ErrorsLib.sol @@ -69,6 +69,6 @@ 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(); + error InconsistentReallocation(); + error AllCapsReached(); } diff --git a/test/forge/ERC4626Test.sol b/test/forge/ERC4626Test.sol index 2a7829a3..f15c3c72 100644 --- a/test/forge/ERC4626Test.sol +++ b/test/forge/ERC4626Test.sol @@ -87,26 +87,6 @@ contract ERC4626Test is IntegrationTest, IMorphoFlashLoanCallback { assertEq(vault.balanceOf(ONBEHALF), shares - redeemed, "balanceOf(ONBEHALF)"); } - function testWithdrawIdle(uint256 deposited, uint256 withdrawn) public { - deposited = bound(deposited, MIN_TEST_ASSETS, MAX_TEST_ASSETS); - withdrawn = bound(withdrawn, 0, deposited); - - _setCap(allMarkets[0], 0); - - loanToken.setBalance(SUPPLIER, deposited); - - vm.prank(SUPPLIER); - uint256 shares = vault.deposit(deposited, ONBEHALF); - - vm.expectEmit(); - emit EventsLib.UpdateLastTotalAssets(vault.totalAssets() - withdrawn); - vm.prank(ONBEHALF); - uint256 redeemed = vault.withdraw(withdrawn, RECEIVER, ONBEHALF); - - assertEq(vault.balanceOf(ONBEHALF), shares - redeemed, "balanceOf(ONBEHALF)"); - assertEq(vault.idle(), deposited - withdrawn, "idle"); - } - function testRedeemTooMuch(uint256 deposited) public { deposited = bound(deposited, MIN_TEST_ASSETS, MAX_TEST_ASSETS); @@ -236,28 +216,19 @@ contract ERC4626Test is IntegrationTest, IMorphoFlashLoanCallback { vault.transferFrom(ONBEHALF, RECEIVER, shares); } - function testWithdrawMoreThanBalanceButLessThanTotalAssets(uint256 deposited, uint256 assets) public { - deposited = bound(deposited, MIN_TEST_ASSETS, MAX_TEST_ASSETS); + function testWithdrawMoreThanBalanceButLessThanTotalAssets(uint256 deposited) public { + deposited = bound(deposited, MIN_TEST_ASSETS, CAP); 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)); - - uint256 toAdd = assets - deposited + 1; - loanToken.setBalance(SUPPLIER, toAdd); + vault.deposit(deposited / 2, ONBEHALF); vm.prank(SUPPLIER); - vault.deposit(toAdd, SUPPLIER); + vault.deposit(deposited / 2, SUPPLIER); - uint256 sharesBurnt = vault.previewWithdraw(assets); vm.prank(ONBEHALF); - vm.expectRevert( - abi.encodeWithSelector(IERC20Errors.ERC20InsufficientBalance.selector, ONBEHALF, shares, sharesBurnt) - ); - vault.withdraw(assets, RECEIVER, ONBEHALF); + vm.expectRevert(); + vault.withdraw(deposited, RECEIVER, ONBEHALF); } function testWithdrawMoreThanTotalAssets(uint256 deposited, uint256 assets) public { 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/ReallocateIdleTest.sol b/test/forge/ReallocateIdleTest.sol deleted file mode 100644 index fb0c9000..00000000 --- a/test/forge/ReallocateIdleTest.sol +++ /dev/null @@ -1,56 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity ^0.8.0; - -import {SharesMathLib} from "@morpho-blue/libraries/SharesMathLib.sol"; - -import "./helpers/IntegrationTest.sol"; - -uint256 constant CAP2 = 100e18; -uint256 constant INITIAL_DEPOSIT = 4 * CAP2; - -contract ReallocateIdleTest is IntegrationTest { - using MarketParamsLib for MarketParams; - using MorphoLib for IMorpho; - - MarketAllocation[] internal withdrawn; - MarketAllocation[] internal supplied; - - 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); - } - - function testReallocateSupplyIdle(uint256[3] memory suppliedShares) public { - suppliedShares[0] = bound(suppliedShares[0], SharesMathLib.VIRTUAL_SHARES, CAP2 * SharesMathLib.VIRTUAL_SHARES); - suppliedShares[1] = bound(suppliedShares[1], SharesMathLib.VIRTUAL_SHARES, CAP2 * SharesMathLib.VIRTUAL_SHARES); - suppliedShares[2] = bound(suppliedShares[2], SharesMathLib.VIRTUAL_SHARES, CAP2 * SharesMathLib.VIRTUAL_SHARES); - - supplied.push(MarketAllocation(allMarkets[0], 0, suppliedShares[0])); - supplied.push(MarketAllocation(allMarkets[1], 0, suppliedShares[1])); - supplied.push(MarketAllocation(allMarkets[2], 0, suppliedShares[2])); - - uint256 idleBefore = vault.idle(); - - vm.prank(ALLOCATOR); - vault.reallocate(withdrawn, supplied); - - assertEq(morpho.supplyShares(allMarkets[0].id(), address(vault)), suppliedShares[0], "morpho.supplyShares(0)"); - assertEq(morpho.supplyShares(allMarkets[1].id(), address(vault)), suppliedShares[1], "morpho.supplyShares(1)"); - assertEq(morpho.supplyShares(allMarkets[2].id(), address(vault)), suppliedShares[2], "morpho.supplyShares(2)"); - - uint256 expectedIdle = idleBefore - suppliedShares[0] / SharesMathLib.VIRTUAL_SHARES - - suppliedShares[1] / SharesMathLib.VIRTUAL_SHARES - suppliedShares[2] / SharesMathLib.VIRTUAL_SHARES; - assertApproxEqAbs(vault.idle(), expectedIdle, 3, "vault.idle() 1"); - } -} diff --git a/test/forge/ReallocateWithdrawTest.sol b/test/forge/ReallocateWithdrawTest.sol index 2e901133..e52a1f87 100644 --- a/test/forge/ReallocateWithdrawTest.sol +++ b/test/forge/ReallocateWithdrawTest.sol @@ -7,7 +7,7 @@ import {SharesMathLib} from "@morpho-blue/libraries/SharesMathLib.sol"; import "./helpers/IntegrationTest.sol"; uint256 constant CAP2 = 100e18; -uint256 constant INITIAL_DEPOSIT = 4 * CAP2; +uint256 constant INITIAL_DEPOSIT = 3 * CAP2; contract ReallocateWithdrawTest is IntegrationTest { using MarketParamsLib for MarketParams; @@ -24,7 +24,7 @@ contract ReallocateWithdrawTest is IntegrationTest { _setCap(allMarkets[0], CAP2); _setCap(allMarkets[1], CAP2); - _setCap(allMarkets[2], CAP2); + _setCap(allMarkets[2], 3*CAP2); loanToken.setBalance(SUPPLIER, INITIAL_DEPOSIT); @@ -35,29 +35,25 @@ contract ReallocateWithdrawTest is IntegrationTest { function testReallocateWithdrawAll() public { withdrawn.push(MarketAllocation(allMarkets[0], 0, morpho.supplyShares(allMarkets[0].id(), address(vault)))); withdrawn.push(MarketAllocation(allMarkets[1], 0, morpho.supplyShares(allMarkets[1].id(), address(vault)))); - withdrawn.push(MarketAllocation(allMarkets[2], 0, morpho.supplyShares(allMarkets[2].id(), address(vault)))); + supplied.push(MarketAllocation(allMarkets[2], type(uint256).max, 0)); vm.prank(ALLOCATOR); vault.reallocate(withdrawn, supplied); 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"); } function testReallocateWithdrawMax() public { withdrawn.push(MarketAllocation(allMarkets[0], 0, type(uint256).max)); withdrawn.push(MarketAllocation(allMarkets[1], 0, type(uint256).max)); - withdrawn.push(MarketAllocation(allMarkets[2], 0, type(uint256).max)); + supplied.push(MarketAllocation(allMarkets[2], type(uint256).max, 0)); vm.prank(ALLOCATOR); vault.reallocate(withdrawn, supplied); 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"); } function testReallocateWithdrawInconsistentAsset() public { @@ -105,16 +101,9 @@ contract ReallocateWithdrawTest is IntegrationTest { totalSupplyShares[1] -= withdrawnShares[1]; totalSupplyShares[2] -= withdrawnShares[2]; - uint256 expectedIdle = vault.idle() + withdrawnAssets[0] + withdrawnAssets[1] + withdrawnAssets[2]; - - suppliedAssets[0] = bound(suppliedAssets[0], 0, withdrawnAssets[0].zeroFloorSub(CAP2).min(expectedIdle)); - expectedIdle -= suppliedAssets[0]; - - suppliedAssets[1] = bound(suppliedAssets[1], 0, withdrawnAssets[1].zeroFloorSub(CAP2).min(expectedIdle)); - expectedIdle -= suppliedAssets[1]; - - suppliedAssets[2] = bound(suppliedAssets[2], 0, withdrawnAssets[2].zeroFloorSub(CAP2).min(expectedIdle)); - expectedIdle -= suppliedAssets[2]; + suppliedAssets[0] = bound(suppliedAssets[0], 0, withdrawnAssets[0].zeroFloorSub(CAP2)); + suppliedAssets[1] = bound(suppliedAssets[1], 0, withdrawnAssets[1].zeroFloorSub(CAP2)); + suppliedAssets[2] = bound(suppliedAssets[2], 0, withdrawnAssets[2].zeroFloorSub(CAP2)); uint256[3] memory suppliedShares = [ suppliedAssets[0].toSharesDown(totalSupplyAssets[0], totalSupplyShares[0]), @@ -122,9 +111,9 @@ contract ReallocateWithdrawTest is IntegrationTest { suppliedAssets[2].toSharesDown(totalSupplyAssets[2], totalSupplyShares[2]) ]; - if (suppliedShares[0] > 0) supplied.push(MarketAllocation(allMarkets[0], suppliedAssets[0], 0)); - if (suppliedAssets[1] > 0) supplied.push(MarketAllocation(allMarkets[1], 0, suppliedShares[1])); - if (suppliedShares[2] > 0) supplied.push(MarketAllocation(allMarkets[2], suppliedAssets[2], 0)); + if (suppliedShares[0] > 0) supplied.push(MarketAllocation(allMarkets[0], 0, 0)); + if (suppliedAssets[1] > 0) supplied.push(MarketAllocation(allMarkets[1], 0, 0)); + if (suppliedShares[2] > 0) supplied.push(MarketAllocation(allMarkets[2], type(uint256).max, 0)); vm.prank(ALLOCATOR); vault.reallocate(withdrawn, supplied); @@ -145,7 +134,6 @@ contract ReallocateWithdrawTest is IntegrationTest { sharesBefore[2] - withdrawnShares[2] + suppliedShares[2], "morpho.supplyShares(2)" ); - assertApproxEqAbs(vault.idle(), expectedIdle, 1, "vault.idle() 1"); } function testReallocateUnauthorizedMarket(uint256 amount) public { @@ -177,21 +165,4 @@ contract ReallocateWithdrawTest is IntegrationTest { vm.expectRevert(abi.encodeWithSelector(ErrorsLib.SupplyCapExceeded.selector, allMarkets[0].id())); vault.reallocate(withdrawn, supplied); } - - function testReallocateInsufficientIdle(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); - - _setCap(allMarkets[0], type(uint192).max); - - supplied.push(MarketAllocation(allMarkets[0], CAP2 + rewards, 0)); - - vm.prank(ALLOCATOR); - vm.expectRevert(ErrorsLib.InsufficientIdle.selector); - vault.reallocate(withdrawn, supplied); - } } diff --git a/test/forge/UrdTest.sol b/test/forge/UrdTest.sol index 4dd56d91..9c5604bc 100644 --- a/test/forge/UrdTest.sol +++ b/test/forge/UrdTest.sol @@ -45,7 +45,7 @@ contract UrdTest is IntegrationTest { vault.setRewardsRecipient(address(0)); } - function testTransferRewardsNotLoanToken(uint256 amount) public { + function testSkimNotLoanToken(uint256 amount) public { vm.prank(OWNER); vault.setRewardsRecipient(address(rewardsDistributor)); @@ -55,7 +55,7 @@ contract UrdTest is IntegrationTest { vm.expectEmit(); emit EventsLib.TransferRewards(address(this), address(rewardsDistributor), address(collateralToken), amount); - vault.transferRewards(address(collateralToken)); + vault.skim(address(collateralToken)); uint256 vaultBalanceAfter = collateralToken.balanceOf(address(vault)); assertEq(vaultBalanceAfter, 0, "vaultBalanceAfter"); @@ -66,38 +66,8 @@ contract UrdTest is IntegrationTest { ); } - function testTransferRewardsLoanToken(uint256 rewards, uint256 idle) public { - idle = bound(idle, 0, MAX_TEST_ASSETS); - rewards = bound(rewards, 0, MAX_TEST_ASSETS); - - vm.prank(OWNER); - vault.setRewardsRecipient(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(); - emit EventsLib.TransferRewards(address(this), address(rewardsDistributor), address(loanToken), rewards); - vault.transferRewards(address(loanToken)); - uint256 vaultBalanceAfter = loanToken.balanceOf(address(vault)); - - assertEq(vaultBalanceAfter, idle, "vaultBalanceAfter"); - assertEq( - loanToken.balanceOf(address(rewardsDistributor)), - rewards, - "loanToken.balanceOf(address(rewardsDistributor))" - ); - } - - function testTransferRewardsZeroAddress() public { + function testSkimZeroAddress() public { vm.expectRevert(ErrorsLib.ZeroAddress.selector); - vault.transferRewards(address(loanToken)); + vault.skim(address(loanToken)); } } From e1e1e4ec830223231ff20c5e1d4b91d87a7d7f48 Mon Sep 17 00:00:00 2001 From: MathisGD Date: Mon, 23 Oct 2023 16:51:48 +0200 Subject: [PATCH 02/27] style: error naming --- src/MetaMorpho.sol | 8 ++---- src/libraries/ErrorsLib.sol | 2 +- test/forge/ReallocateWithdrawTest.sol | 40 +++++++++++++-------------- 3 files changed, 23 insertions(+), 27 deletions(-) diff --git a/src/MetaMorpho.sol b/src/MetaMorpho.sol index 35aab153..5d213cf3 100644 --- a/src/MetaMorpho.sol +++ b/src/MetaMorpho.sol @@ -559,11 +559,7 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph assets -= _staticWithdrawMorpho(assets); } - function _maxSupply() - internal - view - returns (uint256 totalSuppliable) - { + function _maxSupply() internal view returns (uint256 totalSuppliable) { for (uint256 i; i < supplyQueue.length; ++i) { Id id = supplyQueue[i]; totalSuppliable += _suppliable(_marketParams(id), id); @@ -732,7 +728,7 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph if (assets == 0) return; } - if (assets != 0) revert ErrorsLib.AllCapsReached(); + if (assets != 0) revert ErrorsLib.AllSupplyQueueCapsReached(); } /// @dev TODO. diff --git a/src/libraries/ErrorsLib.sol b/src/libraries/ErrorsLib.sol index 9c078600..80068cb3 100644 --- a/src/libraries/ErrorsLib.sol +++ b/src/libraries/ErrorsLib.sol @@ -70,5 +70,5 @@ library ErrorsLib { error ZeroFeeRecipient(); error InconsistentReallocation(); - error AllCapsReached(); + error AllSupplyQueueCapsReached(); } diff --git a/test/forge/ReallocateWithdrawTest.sol b/test/forge/ReallocateWithdrawTest.sol index e52a1f87..e66f078a 100644 --- a/test/forge/ReallocateWithdrawTest.sol +++ b/test/forge/ReallocateWithdrawTest.sol @@ -24,7 +24,7 @@ contract ReallocateWithdrawTest is IntegrationTest { _setCap(allMarkets[0], CAP2); _setCap(allMarkets[1], CAP2); - _setCap(allMarkets[2], 3*CAP2); + _setCap(allMarkets[2], 3 * CAP2); loanToken.setBalance(SUPPLIER, INITIAL_DEPOSIT); @@ -111,29 +111,29 @@ contract ReallocateWithdrawTest is IntegrationTest { suppliedAssets[2].toSharesDown(totalSupplyAssets[2], totalSupplyShares[2]) ]; - if (suppliedShares[0] > 0) supplied.push(MarketAllocation(allMarkets[0], 0, 0)); - if (suppliedAssets[1] > 0) supplied.push(MarketAllocation(allMarkets[1], 0, 0)); - if (suppliedShares[2] > 0) supplied.push(MarketAllocation(allMarkets[2], type(uint256).max, 0)); + if (withdrawnAssets[0] + withdrawnAssets[1] + withdrawnAssets[2] > 0) { + supplied.push(MarketAllocation(allMarkets[2], type(uint256).max, 0)); + } vm.prank(ALLOCATOR); vault.reallocate(withdrawn, supplied); - assertEq( - morpho.supplyShares(allMarkets[0].id(), address(vault)), - sharesBefore[0] - withdrawnShares[0] + suppliedShares[0], - "morpho.supplyShares(0)" - ); - assertApproxEqAbs( - morpho.supplyShares(allMarkets[1].id(), address(vault)), - sharesBefore[1] - withdrawnShares[1] + suppliedShares[1], - SharesMathLib.VIRTUAL_SHARES, - "morpho.supplyShares(1)" - ); - assertEq( - morpho.supplyShares(allMarkets[2].id(), address(vault)), - sharesBefore[2] - withdrawnShares[2] + suppliedShares[2], - "morpho.supplyShares(2)" - ); + // assertEq( + // morpho.supplyShares(allMarkets[0].id(), address(vault)), + // sharesBefore[0] - withdrawnShares[0] + suppliedShares[0], + // "morpho.supplyShares(0)" + // ); + // assertApproxEqAbs( + // morpho.supplyShares(allMarkets[1].id(), address(vault)), + // sharesBefore[1] - withdrawnShares[1] + suppliedShares[1], + // SharesMathLib.VIRTUAL_SHARES, + // "morpho.supplyShares(1)" + // ); + // assertEq( + // morpho.supplyShares(allMarkets[2].id(), address(vault)), + // sharesBefore[2] - withdrawnShares[2] + suppliedShares[2], + // "morpho.supplyShares(2)" + // ); } function testReallocateUnauthorizedMarket(uint256 amount) public { From 4a8912fbf034bb514b1535e080feb2dc0b0f8e4c Mon Sep 17 00:00:00 2001 From: MathisGD Date: Mon, 23 Oct 2023 16:59:11 +0200 Subject: [PATCH 03/27] style: skim working --- src/MetaMorpho.sol | 20 ++++++++++---------- src/interfaces/IMetaMorpho.sol | 4 ++-- src/libraries/EventsLib.sol | 12 ++++++------ test/forge/UrdTest.sol | 24 ++++++++++++------------ 4 files changed, 30 insertions(+), 30 deletions(-) diff --git a/src/MetaMorpho.sol b/src/MetaMorpho.sol index 5d213cf3..8313a6a3 100644 --- a/src/MetaMorpho.sol +++ b/src/MetaMorpho.sol @@ -65,8 +65,8 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph /// @notice The fee recipient. address public feeRecipient; - /// @notice The rewards recipient. - address public rewardsRecipient; + /// @notice The skim recipient. + address public skimRecipient; /// @notice The pending guardian. PendingAddress public pendingGuardian; @@ -179,13 +179,13 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph emit EventsLib.SetIsAllocator(newAllocator, newIsAllocator); } - /// @notice Sets `rewardsRecipient` to `newRewardsRecipient`. - function setRewardsRecipient(address newRewardsRecipient) external onlyOwner { - if (newRewardsRecipient == rewardsRecipient) revert ErrorsLib.AlreadySet(); + /// @notice Sets `skimRecipient` to `newSkimRecipient`. + function setSkimRecipient(address newSkimRecipient) external onlyOwner { + if (newSkimRecipient == skimRecipient) revert ErrorsLib.AlreadySet(); - rewardsRecipient = newRewardsRecipient; + skimRecipient = newSkimRecipient; - emit EventsLib.SetRewardsRecipient(newRewardsRecipient); + emit EventsLib.SetSkimRecipient(newSkimRecipient); } /// @notice Submits a `newTimelock`. @@ -446,13 +446,13 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph /// @notice TODO. function skim(address token) external { - if (rewardsRecipient == address(0)) revert ErrorsLib.ZeroAddress(); + if (skimRecipient == address(0)) revert ErrorsLib.ZeroAddress(); uint256 amount = IERC20(token).balanceOf(address(this)); - SafeERC20.safeTransfer(IERC20(token), rewardsRecipient, amount); + SafeERC20.safeTransfer(IERC20(token), skimRecipient, amount); - emit EventsLib.TransferRewards(_msgSender(), rewardsRecipient, token, amount); + emit EventsLib.Skim(_msgSender(), skimRecipient, token, amount); } /* ERC4626 (PUBLIC) */ diff --git a/src/interfaces/IMetaMorpho.sol b/src/interfaces/IMetaMorpho.sol index b34e71fd..1aa38381 100644 --- a/src/interfaces/IMetaMorpho.sol +++ b/src/interfaces/IMetaMorpho.sol @@ -44,7 +44,7 @@ interface IMetaMorpho is IERC4626 { function fee() external view returns (uint96); function feeRecipient() external view returns (address); - function rewardsRecipient() external view returns (address); + function skimRecipient() external view returns (address); function timelock() external view returns (uint256); function supplyQueue(uint256) external view returns (Id); function supplyQueueSize() external view returns (uint256); @@ -78,7 +78,7 @@ interface IMetaMorpho is IERC4626 { function setIsAllocator(address newAllocator, bool newIsAllocator) external; function setCurator(address newCurator) external; function setFeeRecipient(address newFeeRecipient) external; - function setRewardsRecipient(address) external; + function setSkimRecipient(address) external; function setSupplyQueue(Id[] calldata newSupplyQueue) external; function sortWithdrawQueue(uint256[] calldata indexes) external; diff --git a/src/libraries/EventsLib.sol b/src/libraries/EventsLib.sol index fceea864..401fbccd 100644 --- a/src/libraries/EventsLib.sol +++ b/src/libraries/EventsLib.sol @@ -12,11 +12,11 @@ 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(uint256 newTimelock); - /// @notice Emitted `rewardsDistibutor` is set to `newRewardsRecipient`. - event SetRewardsRecipient(address indexed newRewardsRecipient); + /// @notice Emitted when `skimRecipient` is set to `newSkimRecipient`. + event SetSkimRecipient(address indexed newSkimRecipient); /// @notice Emitted when a pending `newFee` is submitted. event SubmitFee(uint256 newFee); @@ -66,9 +66,9 @@ library EventsLib { /// @notice Emitted when fees are accrued. event AccrueFee(uint256 feeShares); - /// @notice Emitted when an `amount` of `token` is transferred to the `rewardsRecipient` by `caller`. - event TransferRewards( - address indexed caller, address indexed rewardsRecipient, address indexed token, uint256 amount + /// @notice Emitted when an `amount` of `token` is transferred to the `skimRecipient` by `caller`. + event Skim( + address indexed caller, address indexed skimRecipient, address indexed token, uint256 amount ); /// @notice Emitted when a new MetaMorpho vault is created. diff --git a/test/forge/UrdTest.sol b/test/forge/UrdTest.sol index 9c5604bc..552ced96 100644 --- a/test/forge/UrdTest.sol +++ b/test/forge/UrdTest.sol @@ -21,40 +21,40 @@ contract UrdTest is IntegrationTest { rewardsDistributor = urdFactory.createUrd(OWNER, 0, bytes32(0), bytes32(0), bytes32(0)); } - function testSetRewardsRecipient(address newRewardsRecipient) public { - vm.assume(newRewardsRecipient != vault.rewardsRecipient()); + function testSetSkimRecipient(address newSkimRecipient) public { + vm.assume(newSkimRecipient != vault.skimRecipient()); vm.expectEmit(); - emit EventsLib.SetRewardsRecipient(newRewardsRecipient); + emit EventsLib.SetSkimRecipient(newSkimRecipient); vm.prank(OWNER); - vault.setRewardsRecipient(newRewardsRecipient); + vault.setSkimRecipient(newSkimRecipient); - assertEq(vault.rewardsRecipient(), newRewardsRecipient); + assertEq(vault.skimRecipient(), newSkimRecipient); } - function testAlreadySetRewardsRecipient() public { - address currentRewardsRecipient = vault.rewardsRecipient(); + function testAlreadySetSkimRecipient() public { + address currentSkimRecipient = vault.skimRecipient(); vm.prank(OWNER); vm.expectRevert(ErrorsLib.AlreadySet.selector); - vault.setRewardsRecipient(currentRewardsRecipient); + vault.setSkimRecipient(currentSkimRecipient); } - function testSetRewardsRecipientNotOwner() public { + function testSetSkimRecipientNotOwner() public { vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, address(this))); - vault.setRewardsRecipient(address(0)); + vault.setSkimRecipient(address(0)); } function testSkimNotLoanToken(uint256 amount) public { vm.prank(OWNER); - vault.setRewardsRecipient(address(rewardsDistributor)); + vault.setSkimRecipient(address(rewardsDistributor)); collateralToken.setBalance(address(vault), amount); uint256 vaultBalanceBefore = collateralToken.balanceOf(address(vault)); assertEq(vaultBalanceBefore, amount, "vaultBalanceBefore"); vm.expectEmit(); - emit EventsLib.TransferRewards(address(this), address(rewardsDistributor), address(collateralToken), amount); + emit EventsLib.Skim(address(this), address(rewardsDistributor), address(collateralToken), amount); vault.skim(address(collateralToken)); uint256 vaultBalanceAfter = collateralToken.balanceOf(address(vault)); From 6b05d0c2c23d1ec61f2e07234cc3564eaec9ffe6 Mon Sep 17 00:00:00 2001 From: MathisGD Date: Mon, 23 Oct 2023 22:53:52 +0200 Subject: [PATCH 04/27] test: fix erc4626 test --- test/forge/ERC4626Test.sol | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/test/forge/ERC4626Test.sol b/test/forge/ERC4626Test.sol index f15c3c72..a8e29398 100644 --- a/test/forge/ERC4626Test.sol +++ b/test/forge/ERC4626Test.sol @@ -217,18 +217,14 @@ contract ERC4626Test is IntegrationTest, IMorphoFlashLoanCallback { } function testWithdrawMoreThanBalanceButLessThanTotalAssets(uint256 deposited) public { - deposited = bound(deposited, MIN_TEST_ASSETS, CAP); + deposited = bound(deposited, MIN_TEST_ASSETS, MAX_TEST_ASSETS); loanToken.setBalance(SUPPLIER, deposited); - vm.prank(SUPPLIER); + vm.startPrank(SUPPLIER); vault.deposit(deposited / 2, ONBEHALF); - - vm.prank(SUPPLIER); vault.deposit(deposited / 2, SUPPLIER); - - vm.prank(ONBEHALF); vm.expectRevert(); - vault.withdraw(deposited, RECEIVER, ONBEHALF); + vault.withdraw(deposited, RECEIVER, SUPPLIER); } function testWithdrawMoreThanTotalAssets(uint256 deposited, uint256 assets) public { From 02889d7bc4989a1b63f8c16971ed2a0dab2ad66e Mon Sep 17 00:00:00 2001 From: MathisGD Date: Mon, 23 Oct 2023 22:57:05 +0200 Subject: [PATCH 05/27] chore: fmt --- src/libraries/EventsLib.sol | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/libraries/EventsLib.sol b/src/libraries/EventsLib.sol index 401fbccd..3201544f 100644 --- a/src/libraries/EventsLib.sol +++ b/src/libraries/EventsLib.sol @@ -67,9 +67,7 @@ library EventsLib { event AccrueFee(uint256 feeShares); /// @notice Emitted when an `amount` of `token` is transferred to the `skimRecipient` by `caller`. - event Skim( - address indexed caller, address indexed skimRecipient, address indexed token, uint256 amount - ); + event Skim(address indexed caller, address indexed skimRecipient, address indexed token, uint256 amount); /// @notice Emitted when a new MetaMorpho vault is created. /// @param metaMorpho The address of the MetaMorpho vault. From 31dc4d1895a0c35d30ef9da9ca3dca6301436ac5 Mon Sep 17 00:00:00 2001 From: MathisGD Date: Tue, 24 Oct 2023 11:42:51 +0200 Subject: [PATCH 06/27] test: fix a16z test --- foundry.toml | 2 ++ test/forge/ERC4626A16zTest.sol | 2 ++ test/hardhat/MetaMorpho.spec.ts | 7 ++++++- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/foundry.toml b/foundry.toml index 133d6a95..d271cd2c 100644 --- a/foundry.toml +++ b/foundry.toml @@ -14,6 +14,8 @@ tenderly = "https://rpc.tenderly.co/fork/${TENDERLY_FORK_ID}" [profile.default.fmt] wrap_comments = true +[profile.default.fuzz] +max_test_rejects = 131072 [profile.build] via-ir = true diff --git a/test/forge/ERC4626A16zTest.sol b/test/forge/ERC4626A16zTest.sol index 9bc1dc38..cf001bc8 100644 --- a/test/forge/ERC4626A16zTest.sol +++ b/test/forge/ERC4626A16zTest.sol @@ -14,5 +14,7 @@ contract ERC4626A16zTest is IntegrationTest, ERC4626Test { _delta_ = 0; _vaultMayBeEmpty = true; _unlimitedAmount = true; + + _setCap(allMarkets[0], 1e28); } } diff --git a/test/hardhat/MetaMorpho.spec.ts b/test/hardhat/MetaMorpho.spec.ts index 872d982e..23a7de53 100644 --- a/test/hardhat/MetaMorpho.spec.ts +++ b/test/hardhat/MetaMorpho.spec.ts @@ -232,6 +232,7 @@ describe("MetaMorpho", () => { }); it("should simulate gas cost [main]", async () => { + let totalAssets: bigint = toBigInt(0); for (let i = 0; i < suppliers.length; ++i) { logProgress("main", i, suppliers.length); @@ -240,6 +241,8 @@ describe("MetaMorpho", () => { await randomForwardTimestamp(); + if (totalAssets + assets > supplyCap) { break; } + await metaMorpho.connect(supplier).deposit(assets, supplier.address); await randomForwardTimestamp(); @@ -291,7 +294,7 @@ describe("MetaMorpho", () => { })) .filter(({ assets }) => assets > 0n); - await metaMorpho.connect(allocator).reallocate(withdrawn, supplied); + // await metaMorpho.connect(allocator).reallocate(withdrawn, supplied); // Borrow liquidity to generate interest. @@ -314,6 +317,8 @@ describe("MetaMorpho", () => { await mine(); // Include supplyCollateral + borrow in a single block. } + totalAssets += assets / 2n; + await hre.network.provider.send("evm_setAutomine", [true]); } }); From f30a959f8f3caee8ca5a337c43d41736f644555d Mon Sep 17 00:00:00 2001 From: MathisGD Date: Tue, 24 Oct 2023 13:40:09 +0200 Subject: [PATCH 07/27] chore: fmt --- test/hardhat/MetaMorpho.spec.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/hardhat/MetaMorpho.spec.ts b/test/hardhat/MetaMorpho.spec.ts index 23a7de53..320ebc27 100644 --- a/test/hardhat/MetaMorpho.spec.ts +++ b/test/hardhat/MetaMorpho.spec.ts @@ -241,7 +241,9 @@ describe("MetaMorpho", () => { await randomForwardTimestamp(); - if (totalAssets + assets > supplyCap) { break; } + if (totalAssets + assets > supplyCap) { + break; + } await metaMorpho.connect(supplier).deposit(assets, supplier.address); From 868c6f959a0edacda94617b90a96ac041e519985 Mon Sep 17 00:00:00 2001 From: MathisGD Date: Tue, 24 Oct 2023 14:15:00 +0200 Subject: [PATCH 08/27] chore: increase CI max rejects --- .github/workflows/foundry.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/foundry.yml b/.github/workflows/foundry.yml index 9363c310..261c4f39 100644 --- a/.github/workflows/foundry.yml +++ b/.github/workflows/foundry.yml @@ -80,12 +80,12 @@ jobs: include: - type: "slow" fuzz-runs: 32768 - max-test-rejects: 1048576 + max-test-rejects: 16777216 invariant-runs: 64 invariant-depth: 1024 - type: "fast" fuzz-runs: 256 - max-test-rejects: 65536 + max-test-rejects: 131072 invariant-runs: 16 invariant-depth: 256 From e5c248889af0fb07e767382dac7ab19c171e9553 Mon Sep 17 00:00:00 2001 From: MathisGD Date: Tue, 24 Oct 2023 14:24:11 +0200 Subject: [PATCH 09/27] chore: increase CI max rejects --- .github/workflows/foundry.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/foundry.yml b/.github/workflows/foundry.yml index 261c4f39..7559778f 100644 --- a/.github/workflows/foundry.yml +++ b/.github/workflows/foundry.yml @@ -85,7 +85,7 @@ jobs: invariant-depth: 1024 - type: "fast" fuzz-runs: 256 - max-test-rejects: 131072 + max-test-rejects: 262144 invariant-runs: 16 invariant-depth: 256 From d22aede908f819b77006f4753570b259023e5250 Mon Sep 17 00:00:00 2001 From: Merlin Egalite <44097430+MerlinEgalite@users.noreply.github.com> Date: Fri, 27 Oct 2023 14:43:18 +0200 Subject: [PATCH 10/27] feat: apply suggestions Signed-off-by: Merlin Egalite <44097430+MerlinEgalite@users.noreply.github.com> --- src/MetaMorpho.sol | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/MetaMorpho.sol b/src/MetaMorpho.sol index 8313a6a3..9d3980c7 100644 --- a/src/MetaMorpho.sol +++ b/src/MetaMorpho.sol @@ -728,7 +728,8 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph if (assets == 0) return; } - if (assets != 0) revert ErrorsLib.AllSupplyQueueCapsReached(); + + if (assets != 0) revert ErrorsLib.AllCapsReached(); } /// @dev TODO. @@ -748,6 +749,7 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph if (assets == 0) return; } + if (assets != 0) revert ErrorsLib.WithdrawMorphoFailed(); } @@ -766,6 +768,7 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph if (assets == 0) break; } + return assets; } From 586ed734f0dd18df1bd7e297c6944d0be5355198 Mon Sep 17 00:00:00 2001 From: MerlinEgalite Date: Fri, 27 Oct 2023 15:52:50 +0300 Subject: [PATCH 11/27] fix: errors --- src/MetaMorpho.sol | 4 +--- src/libraries/ErrorsLib.sol | 5 ++++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/MetaMorpho.sol b/src/MetaMorpho.sol index 9d3980c7..43adf51f 100644 --- a/src/MetaMorpho.sol +++ b/src/MetaMorpho.sol @@ -372,9 +372,7 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph if (supplyCap == 0) revert ErrorsLib.UnauthorizedMarket(id); - if (allocation.assets == type(uint256).max) { - allocation.assets = totalWithdrawn - totalSupplied; - } + if (allocation.assets == type(uint256).max) allocation.assets = totalWithdrawn.zeroFloorSub(totalSupplied); (uint256 suppliedAssets,) = MORPHO.supply(allocation.marketParams, allocation.assets, allocation.shares, address(this), hex""); diff --git a/src/libraries/ErrorsLib.sol b/src/libraries/ErrorsLib.sol index 80068cb3..6a2c62eb 100644 --- a/src/libraries/ErrorsLib.sol +++ b/src/libraries/ErrorsLib.sol @@ -69,6 +69,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 amount withdrawn is not excatly the amount supplied. error InconsistentReallocation(); - error AllSupplyQueueCapsReached(); + + /// @notice Thrown when all caps have been reached. + error AllCapsReached(); } From 9ccb15b21ec6ae3e944162f420250e5bf0662db1 Mon Sep 17 00:00:00 2001 From: MerlinEgalite Date: Fri, 27 Oct 2023 16:46:32 +0300 Subject: [PATCH 12/27] test: fix tests --- foundry.toml | 2 +- test/forge/ReallocateWithdrawTest.sol | 35 ++++++++++++++------------- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/foundry.toml b/foundry.toml index d271cd2c..989194eb 100644 --- a/foundry.toml +++ b/foundry.toml @@ -15,7 +15,7 @@ tenderly = "https://rpc.tenderly.co/fork/${TENDERLY_FORK_ID}" wrap_comments = true [profile.default.fuzz] -max_test_rejects = 131072 +max_test_rejects = 16777216 [profile.build] via-ir = true diff --git a/test/forge/ReallocateWithdrawTest.sol b/test/forge/ReallocateWithdrawTest.sol index e66f078a..fcb1e6a2 100644 --- a/test/forge/ReallocateWithdrawTest.sol +++ b/test/forge/ReallocateWithdrawTest.sol @@ -103,7 +103,8 @@ contract ReallocateWithdrawTest is IntegrationTest { suppliedAssets[0] = bound(suppliedAssets[0], 0, withdrawnAssets[0].zeroFloorSub(CAP2)); suppliedAssets[1] = bound(suppliedAssets[1], 0, withdrawnAssets[1].zeroFloorSub(CAP2)); - suppliedAssets[2] = bound(suppliedAssets[2], 0, withdrawnAssets[2].zeroFloorSub(CAP2)); + suppliedAssets[2] = + withdrawnAssets[0] + withdrawnAssets[1] + withdrawnAssets[2] - suppliedAssets[0] - suppliedAssets[1]; uint256[3] memory suppliedShares = [ suppliedAssets[0].toSharesDown(totalSupplyAssets[0], totalSupplyShares[0]), @@ -118,22 +119,22 @@ contract ReallocateWithdrawTest is IntegrationTest { vm.prank(ALLOCATOR); vault.reallocate(withdrawn, supplied); - // assertEq( - // morpho.supplyShares(allMarkets[0].id(), address(vault)), - // sharesBefore[0] - withdrawnShares[0] + suppliedShares[0], - // "morpho.supplyShares(0)" - // ); - // assertApproxEqAbs( - // morpho.supplyShares(allMarkets[1].id(), address(vault)), - // sharesBefore[1] - withdrawnShares[1] + suppliedShares[1], - // SharesMathLib.VIRTUAL_SHARES, - // "morpho.supplyShares(1)" - // ); - // assertEq( - // morpho.supplyShares(allMarkets[2].id(), address(vault)), - // sharesBefore[2] - withdrawnShares[2] + suppliedShares[2], - // "morpho.supplyShares(2)" - // ); + assertEq( + morpho.supplyShares(allMarkets[0].id(), address(vault)), + sharesBefore[0] - withdrawnShares[0] + suppliedShares[0], + "morpho.supplyShares(0)" + ); + assertApproxEqAbs( + morpho.supplyShares(allMarkets[1].id(), address(vault)), + sharesBefore[1] - withdrawnShares[1] + suppliedShares[1], + SharesMathLib.VIRTUAL_SHARES, + "morpho.supplyShares(1)" + ); + assertEq( + morpho.supplyShares(allMarkets[2].id(), address(vault)), + sharesBefore[2] - withdrawnShares[2] + suppliedShares[2], + "morpho.supplyShares(2)" + ); } function testReallocateUnauthorizedMarket(uint256 amount) public { From 3db84c8feb3d6e12b6641615253ae47050eef23f Mon Sep 17 00:00:00 2001 From: MerlinEgalite Date: Fri, 27 Oct 2023 16:51:02 +0300 Subject: [PATCH 13/27] feat: remove todos --- src/MetaMorpho.sol | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/MetaMorpho.sol b/src/MetaMorpho.sol index 43adf51f..00d1ac28 100644 --- a/src/MetaMorpho.sol +++ b/src/MetaMorpho.sol @@ -442,7 +442,7 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph _setCap(id, pendingCap[id].value); } - /// @notice TODO. + /// @notice Skims the vault `token` balance to `skimRecipient`. function skim(address token) external { if (skimRecipient == address(0)) revert ErrorsLib.ZeroAddress(); @@ -557,6 +557,7 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph assets -= _staticWithdrawMorpho(assets); } + /// @dev Returns the maximum amount of assets that the vault can withdraw from Morpho. function _maxSupply() internal view returns (uint256 totalSuppliable) { for (uint256 i; i < supplyQueue.length; ++i) { Id id = supplyQueue[i]; @@ -730,7 +731,7 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph if (assets != 0) revert ErrorsLib.AllCapsReached(); } - /// @dev TODO. + /// @dev Withdraws `assets` from Morpho. function _withdrawMorpho(uint256 assets) internal { for (uint256 i; i < withdrawQueue.length; ++i) { Id id = withdrawQueue[i]; From 1eba06c05fb59aa326a12337e18be58780a4158d Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Mon, 30 Oct 2023 14:32:55 +0100 Subject: [PATCH 14/27] refactor(metamorpho): rename rewards to skim --- .github/workflows/foundry.yml | 2 +- README.md | 37 +++++++++-------- src/MetaMorpho.sol | 22 +++++----- src/interfaces/IMetaMorpho.sol | 6 +-- src/libraries/EventsLib.sol | 10 ++--- ...A16zTest.sol => ERC4626ComplianceTest.sol} | 4 +- test/forge/UrdTest.sol | 40 +++++++++---------- test/metamorpho_tests.tree | 20 +++++----- 8 files changed, 73 insertions(+), 68 deletions(-) rename test/forge/{ERC4626A16zTest.sol => ERC4626ComplianceTest.sol} (80%) diff --git a/.github/workflows/foundry.yml b/.github/workflows/foundry.yml index 08885698..bd572eeb 100644 --- a/.github/workflows/foundry.yml +++ b/.github/workflows/foundry.yml @@ -63,7 +63,7 @@ jobs: type: ["slow", "fast"] include: - type: "slow" - fuzz-runs: 32768 + fuzz-runs: 8192 max-test-rejects: 1048576 invariant-runs: 64 invariant-depth: 1024 diff --git a/README.md b/README.md index 7d8fa718..15583aec 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ The vault owner can set a performance fee, cutting up to 50% of the generated in The `feeRecipient` can then withdraw the accumulated fee at any time. The vault may be entitled to some rewards emitted on Morpho Blue markets the vault has supplied to. -Those rewards can be transferred to the `rewardsRecipient`. +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. @@ -39,6 +39,7 @@ After the timelock, the action can be executed by anyone until 3 days have passe Only one address can have this role. It can: + - Do what the curator can do. - Transfer or renounce the ownership. - Set the curator. @@ -54,21 +55,23 @@ It can: Only one address can have this role. It can: + - Do what the allocators can do. - [Timelocked] Enable or disable a market by setting a cap to a specific market. - - The cap must be set to 0 to disable the market. - - Disabling a market can then only be done if the vault has no liquidity supplied on the market. + - The cap must be set to 0 to disable the market. + - Disabling a market can then only be done if the vault has no liquidity supplied on the market. #### Allocator 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 supply queue in the order set. The remaining funds are left as idle supply on the vault (uncapped). - - 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 withdrawal queue in the order set. - - The `supplyQueue` contains only enabled markets (enabled market are markets with non-zero cap or with non-zero vault's supply). - - The `withdrawQueue` contains all enabled markets. + - Upon a deposit, the vault will supply up to the cap of each Morpho Blue market in the supply queue in the order set. The remaining funds are left as idle supply on the vault (uncapped). + - 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 withdrawal queue in the order set. + - The `supplyQueue` contains only enabled markets (enabled market are markets with non-zero cap or with non-zero vault's supply). + - The `withdrawQueue` contains all enabled markets. - Instantaneously reallocate funds among the enabled market at any moment. #### Guardian @@ -76,6 +79,7 @@ It can: Only one address can have this role. It can: + - Revoke any timelocked action except it cannot revoke a pending fee. ### Rewards @@ -85,20 +89,21 @@ To redistribute rewards to vault depositors, it is advised to use the [Universal Below is a typical example of how this use case would take place: - If not already done: - - Create a rewards distributor using the [UrdFactory](https://github.com/morpho-org/universal-rewards-distributor/blob/main/src/UrdFactory.sol) (can be done by anyone). - - Set the vault’s rewards recipient address to the created URD using `setRewardsRecipient`. + + - Create a rewards distributor using the [UrdFactory](https://github.com/morpho-org/universal-rewards-distributor/blob/main/src/UrdFactory.sol) (can be done by anyone). + - Set the vault’s rewards recipient address to the created URD using `setSkimRecipient`. - Claim tokens from the Morpho Blue distribution to the vault. - NB: Anyone can claim tokens on behalf of the vault and automatically transfer them to the vault. - Thus, this step might be already performed by some third-party. + NB: Anyone can claim tokens on behalf of the vault and automatically transfer them to the vault. + Thus, this step might be already performed by some third-party. -- Transfer rewards from the vault to the rewards distributor using the `transferRewards` function. +- Transfer rewards from the vault to the rewards distributor using the `skim` function. - 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. + 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. - 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 3219f9ee..096d662d 100644 --- a/src/MetaMorpho.sol +++ b/src/MetaMorpho.sol @@ -66,7 +66,7 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph address public feeRecipient; /// @notice The rewards recipient. - address public rewardsRecipient; + address public skimRecipient; /// @notice The pending guardian. PendingAddress public pendingGuardian; @@ -183,13 +183,13 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph emit EventsLib.SetIsAllocator(newAllocator, newIsAllocator); } - /// @notice Sets `rewardsRecipient` to `newRewardsRecipient`. - function setRewardsRecipient(address newRewardsRecipient) external onlyOwner { - if (newRewardsRecipient == rewardsRecipient) revert ErrorsLib.AlreadySet(); + /// @notice Sets `skimRecipient` to `newSkimRecipient`. + function setSkimRecipient(address newSkimRecipient) external onlyOwner { + if (newSkimRecipient == skimRecipient) revert ErrorsLib.AlreadySet(); - rewardsRecipient = newRewardsRecipient; + skimRecipient = newSkimRecipient; - emit EventsLib.SetRewardsRecipient(newRewardsRecipient); + emit EventsLib.SetSkimRecipient(newSkimRecipient); } /// @notice Submits a `newTimelock`. @@ -451,17 +451,17 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph _setCap(id, pendingCap[id].value); } - /// @notice Transfers `token` rewards collected by the vault to the `rewardsRecipient`. + /// @notice Transfers `token` rewards collected by the vault to the `skimRecipient`. /// @dev Can be used to extract any token that would be stuck on the contract as well. - function transferRewards(address token) external { - if (rewardsRecipient == address(0)) revert ErrorsLib.ZeroAddress(); + function skim(address token) external { + if (skimRecipient == address(0)) revert ErrorsLib.ZeroAddress(); uint256 amount = IERC20(token).balanceOf(address(this)); if (token == asset()) amount -= idle; - SafeERC20.safeTransfer(IERC20(token), rewardsRecipient, amount); + SafeERC20.safeTransfer(IERC20(token), skimRecipient, amount); - emit EventsLib.TransferRewards(_msgSender(), rewardsRecipient, token, amount); + emit EventsLib.Skim(_msgSender(), skimRecipient, token, amount); } /* ERC4626 (PUBLIC) */ diff --git a/src/interfaces/IMetaMorpho.sol b/src/interfaces/IMetaMorpho.sol index 99ce58b6..caadcf08 100644 --- a/src/interfaces/IMetaMorpho.sol +++ b/src/interfaces/IMetaMorpho.sol @@ -44,7 +44,7 @@ interface IMetaMorpho is IERC4626 { function fee() external view returns (uint96); function feeRecipient() external view returns (address); - function rewardsRecipient() external view returns (address); + function skimRecipient() external view returns (address); function timelock() external view returns (uint256); function supplyQueue(uint256) external view returns (Id); function supplyQueueSize() external view returns (uint256); @@ -74,12 +74,12 @@ interface IMetaMorpho is IERC4626 { function revokeGuardian() external; function pendingGuardian() external view returns (address guardian, uint96 submittedAt); - function transferRewards(address) external; + function skim(address) external; function setIsAllocator(address newAllocator, bool newIsAllocator) external; function setCurator(address newCurator) external; function setFeeRecipient(address newFeeRecipient) external; - function setRewardsRecipient(address) external; + function setSkimRecipient(address) external; function setSupplyQueue(Id[] calldata newSupplyQueue) external; function sortWithdrawQueue(uint256[] calldata indexes) external; diff --git a/src/libraries/EventsLib.sol b/src/libraries/EventsLib.sol index fceea864..363d3558 100644 --- a/src/libraries/EventsLib.sol +++ b/src/libraries/EventsLib.sol @@ -15,8 +15,8 @@ library EventsLib { /// @notice Emitted `timelock` is set to `newTimelock`. event SetTimelock(uint256 newTimelock); - /// @notice Emitted `rewardsDistibutor` is set to `newRewardsRecipient`. - event SetRewardsRecipient(address indexed newRewardsRecipient); + /// @notice Emitted `rewardsDistibutor` is set to `newSkimRecipient`. + event SetSkimRecipient(address indexed newSkimRecipient); /// @notice Emitted when a pending `newFee` is submitted. event SubmitFee(uint256 newFee); @@ -66,10 +66,8 @@ library EventsLib { /// @notice Emitted when fees are accrued. event AccrueFee(uint256 feeShares); - /// @notice Emitted when an `amount` of `token` is transferred to the `rewardsRecipient` by `caller`. - event TransferRewards( - address indexed caller, address indexed rewardsRecipient, address indexed token, uint256 amount - ); + /// @notice Emitted when an `amount` of `token` is transferred to the `skimRecipient` by `caller`. + event Skim(address indexed caller, address indexed skimRecipient, address indexed token, uint256 amount); /// @notice Emitted when a new MetaMorpho vault is created. /// @param metaMorpho The address of the MetaMorpho vault. diff --git a/test/forge/ERC4626A16zTest.sol b/test/forge/ERC4626ComplianceTest.sol similarity index 80% rename from test/forge/ERC4626A16zTest.sol rename to test/forge/ERC4626ComplianceTest.sol index 9bc1dc38..56295bbe 100644 --- a/test/forge/ERC4626A16zTest.sol +++ b/test/forge/ERC4626ComplianceTest.sol @@ -5,7 +5,7 @@ import "erc4626-tests/ERC4626.test.sol"; import {IntegrationTest} from "./helpers/IntegrationTest.sol"; -contract ERC4626A16zTest is IntegrationTest, ERC4626Test { +contract ERC4626ComplianceTest is IntegrationTest, ERC4626Test { function setUp() public override(IntegrationTest, ERC4626Test) { super.setUp(); @@ -14,5 +14,7 @@ contract ERC4626A16zTest is IntegrationTest, ERC4626Test { _delta_ = 0; _vaultMayBeEmpty = true; _unlimitedAmount = true; + + _setCap(allMarkets[0], 1e28); } } diff --git a/test/forge/UrdTest.sol b/test/forge/UrdTest.sol index 4dd56d91..e25293bf 100644 --- a/test/forge/UrdTest.sol +++ b/test/forge/UrdTest.sol @@ -21,41 +21,41 @@ contract UrdTest is IntegrationTest { rewardsDistributor = urdFactory.createUrd(OWNER, 0, bytes32(0), bytes32(0), bytes32(0)); } - function testSetRewardsRecipient(address newRewardsRecipient) public { - vm.assume(newRewardsRecipient != vault.rewardsRecipient()); + function testSetSkimRecipient(address newSkimRecipient) public { + vm.assume(newSkimRecipient != vault.skimRecipient()); vm.expectEmit(); - emit EventsLib.SetRewardsRecipient(newRewardsRecipient); + emit EventsLib.SetSkimRecipient(newSkimRecipient); vm.prank(OWNER); - vault.setRewardsRecipient(newRewardsRecipient); + vault.setSkimRecipient(newSkimRecipient); - assertEq(vault.rewardsRecipient(), newRewardsRecipient); + assertEq(vault.skimRecipient(), newSkimRecipient); } - function testAlreadySetRewardsRecipient() public { - address currentRewardsRecipient = vault.rewardsRecipient(); + function testAlreadySetSkimRecipient() public { + address currentSkimRecipient = vault.skimRecipient(); vm.prank(OWNER); vm.expectRevert(ErrorsLib.AlreadySet.selector); - vault.setRewardsRecipient(currentRewardsRecipient); + vault.setSkimRecipient(currentSkimRecipient); } - function testSetRewardsRecipientNotOwner() public { + function testSetSkimRecipientNotOwner() public { vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, address(this))); - vault.setRewardsRecipient(address(0)); + vault.setSkimRecipient(address(0)); } - function testTransferRewardsNotLoanToken(uint256 amount) public { + function testSkimNotLoanToken(uint256 amount) public { vm.prank(OWNER); - vault.setRewardsRecipient(address(rewardsDistributor)); + vault.setSkimRecipient(address(rewardsDistributor)); collateralToken.setBalance(address(vault), amount); uint256 vaultBalanceBefore = collateralToken.balanceOf(address(vault)); assertEq(vaultBalanceBefore, amount, "vaultBalanceBefore"); vm.expectEmit(); - emit EventsLib.TransferRewards(address(this), address(rewardsDistributor), address(collateralToken), amount); - vault.transferRewards(address(collateralToken)); + emit EventsLib.Skim(address(this), address(rewardsDistributor), address(collateralToken), amount); + vault.skim(address(collateralToken)); uint256 vaultBalanceAfter = collateralToken.balanceOf(address(vault)); assertEq(vaultBalanceAfter, 0, "vaultBalanceAfter"); @@ -66,12 +66,12 @@ contract UrdTest is IntegrationTest { ); } - function testTransferRewardsLoanToken(uint256 rewards, uint256 idle) public { + 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.setRewardsRecipient(address(rewardsDistributor)); + vault.setSkimRecipient(address(rewardsDistributor)); loanToken.setBalance(address(vault), rewards); @@ -84,8 +84,8 @@ contract UrdTest is IntegrationTest { assertEq(vaultBalanceBefore, idle + rewards, "vaultBalanceBefore"); vm.expectEmit(); - emit EventsLib.TransferRewards(address(this), address(rewardsDistributor), address(loanToken), rewards); - vault.transferRewards(address(loanToken)); + emit EventsLib.Skim(address(this), address(rewardsDistributor), address(loanToken), rewards); + vault.skim(address(loanToken)); uint256 vaultBalanceAfter = loanToken.balanceOf(address(vault)); assertEq(vaultBalanceAfter, idle, "vaultBalanceAfter"); @@ -96,8 +96,8 @@ contract UrdTest is IntegrationTest { ); } - function testTransferRewardsZeroAddress() public { + function testSkimZeroAddress() public { vm.expectRevert(ErrorsLib.ZeroAddress.selector); - vault.transferRewards(address(loanToken)); + vault.skim(address(loanToken)); } } diff --git a/test/metamorpho_tests.tree b/test/metamorpho_tests.tree index c5f1ff0f..211038f3 100644 --- a/test/metamorpho_tests.tree +++ b/test/metamorpho_tests.tree @@ -25,15 +25,15 @@ └── it should emit SetIsAllocator(newAllocator, newIsAllocator) . -└── setRewardsRecipient(address newRewardsRecipient) external +└── setSkimRecipient(address newSkimRecipient) external ├── when msg.sender not owner │ └── revert with NOT_OWNER └── when msg.sender is owner - ├── when newRewardsRecipient == rewardsRecipient + ├── when newSkimRecipient == skimRecipient │ └── revert with AlreadySet() - └── when newRewardsRecipient != rewardsRecipient - ├── it should set rewardsRecipient to newRewardsRecipient - └── it shoud emit SetRewardsRecipient(newRewardsRecipient) + └── when newSkimRecipient != skimRecipient + ├── it should set skimRecipient to newSkimRecipient + └── it shoud emit SetSkimRecipient(newSkimRecipient) . └── submitTimelock(uint256 newTimelock) external @@ -274,15 +274,15 @@ . -└── transferRewards(address token) external - ├── when rewardsRecipient == address(0) +└── skim(address token) external + ├── when skimRecipient == address(0) │ └── revert with ZERO_ADDRESS - └── when rewardsRecipient != address(0) + └── when skimRecipient != address(0) ├── it should compute amount = IERC20(token).balanceOf(address(this)) ├── when token == asset() │ └── it should remove idle from amount - ├── it should transfer amount of token from the the vault to rewardsRecipient - └── it should emit TransferRewards(msg.sender, rewardsRecipient, token, amount) + ├── it should transfer amount of token from the the vault to skimRecipient + └── it should emit Skim(msg.sender, skimRecipient, token, amount) /* ONLY GUARDIAN FUNCTIONS */ From 1ec7e72924ab616083fbf05483402505999d79d1 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Mon, 30 Oct 2023 16:27:33 +0100 Subject: [PATCH 15/27] refactor(metamorpho): use market for idle --- src/MetaMorpho.sol | 63 +++++++++----------------- src/interfaces/IMetaMorpho.sol | 1 - src/libraries/ErrorsLib.sol | 7 ++- test/forge/ERC4626Test.sol | 2 +- test/forge/MetaMorphoInternalTest.sol | 5 +- test/forge/ReallocateIdleTest.sol | 51 +++++++++++++-------- test/forge/ReallocateWithdrawTest.sol | 15 +++--- test/forge/UrdTest.sol | 8 ++-- test/forge/helpers/BaseTest.sol | 14 ++++++ test/forge/helpers/IntegrationTest.sol | 9 ++++ test/metamorpho_tests.tree | 8 +--- 11 files changed, 100 insertions(+), 83 deletions(-) diff --git a/src/MetaMorpho.sol b/src/MetaMorpho.sol index a520c566..a0fb8efb 100644 --- a/src/MetaMorpho.sol +++ b/src/MetaMorpho.sol @@ -89,10 +89,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. uint256 public lastTotalAssets; @@ -376,6 +372,8 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph if (supplyCap == 0) revert ErrorsLib.UnauthorizedMarket(id); + if (allocation.assets == type(uint256).max) allocation.assets = totalWithdrawn.zeroFloorSub(totalSupplied); + (uint256 suppliedAssets,) = MORPHO.supply(allocation.marketParams, allocation.assets, allocation.shares, address(this), hex""); @@ -386,14 +384,7 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph totalSupplied += suppliedAssets; } - if (totalWithdrawn > totalSupplied) { - idle += totalWithdrawn - totalSupplied; - } else { - uint256 idleSupplied = totalSupplied - totalWithdrawn; - if (idle < idleSupplied) revert ErrorsLib.InsufficientIdle(); - - idle -= idleSupplied; - } + if (totalWithdrawn != totalSupplied) revert ErrorsLib.InconsistentReallocation(); } /* ONLY GUARDIAN FUNCTIONS */ @@ -456,7 +447,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; SafeERC20.safeTransfer(IERC20(token), skimRecipient, amount); @@ -532,7 +522,9 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph assets += _supplyBalance(_marketParams(withdrawQueue[i])); } - assets += idle; + assets += _supplyBalance( + MarketParams({collateralToken: address(0), loanToken: asset(), irm: address(0), oracle: address(0), lltv: 0}) + ); } /* ERC4626 (INTERNAL) */ @@ -617,7 +609,7 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph internal override { - if (_withdrawMorpho(assets) != 0) revert ErrorsLib.WithdrawMorphoFailed(); + _withdrawMorpho(assets); super._withdraw(caller, receiver, owner, assets, shares); @@ -702,7 +694,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]; @@ -720,40 +712,34 @@ 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. + /// @return The remaining assets to be withdrawn. + function _withdrawMorpho(uint256 assets) internal returns (uint256) { for (uint256 i; i < withdrawQueue.length; ++i) { Id id = withdrawQueue[i]; MarketParams memory marketParams = _marketParams(id); - uint256 toWithdraw = UtilsLib.min(_withdrawable(marketParams, id), remaining); + uint256 toWithdraw = UtilsLib.min(_withdrawable(marketParams, id), 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 0; } + + if (assets != 0) revert ErrorsLib.WithdrawMorphoFailed(); } /// @dev Fakes a withdraw of `assets` from the idle liquidity and Morpho if necessary. - /// @return remaining The assets left to be withdrawn. - function _staticWithdrawMorpho(uint256 assets) internal view returns (uint256 remaining) { - (remaining,) = _withdrawIdle(assets); - - if (remaining == 0) return 0; - + /// @return The remaining assets to be withdrawn. + function _staticWithdrawMorpho(uint256 assets) internal view returns (uint256) { for (uint256 i; i < withdrawQueue.length; ++i) { Id id = withdrawQueue[i]; MarketParams memory marketParams = _marketParams(id); @@ -762,17 +748,12 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph // 1. oracle.price() is never called (the vault doesn't borrow) // 2. `_withdrawable` caps to the liquidity available on Morpho // 3. virtually accruing interest didn't fail in `_withdrawable` - remaining -= UtilsLib.min(_withdrawable(marketParams, id), remaining); + assets = assets.zeroFloorSub(_withdrawable(marketParams, id)); - if (remaining == 0) return 0; + if (assets == 0) return 0; } - } - /// @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 suppliable amount of assets on the market defined by `marketParams`. diff --git a/src/interfaces/IMetaMorpho.sol b/src/interfaces/IMetaMorpho.sol index caadcf08..1aa38381 100644 --- a/src/interfaces/IMetaMorpho.sol +++ b/src/interfaces/IMetaMorpho.sol @@ -52,7 +52,6 @@ interface IMetaMorpho is IERC4626 { function withdrawQueueSize() external view returns (uint256); function config(Id) external view returns (uint192 cap, uint64 withdrawRank); - 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 fe16ec44..6a2c62eb 100644 --- a/src/libraries/ErrorsLib.sol +++ b/src/libraries/ErrorsLib.sol @@ -69,6 +69,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 excatly the amount supplied. + error InconsistentReallocation(); + + /// @notice Thrown when all caps have been reached. + error AllCapsReached(); } diff --git a/test/forge/ERC4626Test.sol b/test/forge/ERC4626Test.sol index 2a7829a3..ebdbdeb9 100644 --- a/test/forge/ERC4626Test.sol +++ b/test/forge/ERC4626Test.sol @@ -104,7 +104,7 @@ contract ERC4626Test is IntegrationTest, IMorphoFlashLoanCallback { uint256 redeemed = vault.withdraw(withdrawn, RECEIVER, ONBEHALF); assertEq(vault.balanceOf(ONBEHALF), shares - redeemed, "balanceOf(ONBEHALF)"); - assertEq(vault.idle(), deposited - withdrawn, "idle"); + assertEq(_idle(), deposited - withdrawn, "idle"); } function testRedeemTooMuch(uint256 deposited) public { diff --git a/test/forge/MetaMorphoInternalTest.sol b/test/forge/MetaMorphoInternalTest.sol index ba6338da..6ae3a2db 100644 --- a/test/forge/MetaMorphoInternalTest.sol +++ b/test/forge/MetaMorphoInternalTest.sol @@ -97,10 +97,7 @@ contract MetaMorphoInternalTest is InternalTest { uint256 remaining = _staticWithdrawMorpho(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.expectedSupplyBalance(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 fb0c9000..947aad34 100644 --- a/test/forge/ReallocateIdleTest.sol +++ b/test/forge/ReallocateIdleTest.sol @@ -18,10 +18,6 @@ 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)); @@ -29,28 +25,45 @@ contract ReallocateIdleTest is IntegrationTest { vm.prank(SUPPLIER); vault.deposit(INITIAL_DEPOSIT, ONBEHALF); + + _setCap(allMarkets[0], CAP2); + _setCap(allMarkets[1], CAP2); + _setCap(allMarkets[2], CAP2); } - function testReallocateSupplyIdle(uint256[3] memory suppliedShares) public { - suppliedShares[0] = bound(suppliedShares[0], SharesMathLib.VIRTUAL_SHARES, CAP2 * SharesMathLib.VIRTUAL_SHARES); - suppliedShares[1] = bound(suppliedShares[1], SharesMathLib.VIRTUAL_SHARES, CAP2 * SharesMathLib.VIRTUAL_SHARES); - suppliedShares[2] = bound(suppliedShares[2], SharesMathLib.VIRTUAL_SHARES, CAP2 * SharesMathLib.VIRTUAL_SHARES); + function testReallocateSupplyIdle(uint256[3] memory suppliedAssets) public { + suppliedAssets[0] = bound(suppliedAssets[0], 1, CAP2); + suppliedAssets[1] = bound(suppliedAssets[1], 1, CAP2); + suppliedAssets[2] = bound(suppliedAssets[2], 1, CAP2); - supplied.push(MarketAllocation(allMarkets[0], 0, suppliedShares[0])); - supplied.push(MarketAllocation(allMarkets[1], 0, suppliedShares[1])); - supplied.push(MarketAllocation(allMarkets[2], 0, suppliedShares[2])); + uint256 idleWithdrawn = suppliedAssets[0] + suppliedAssets[1] + suppliedAssets[2]; - uint256 idleBefore = vault.idle(); + withdrawn.push(MarketAllocation(_idleParams(), idleWithdrawn, 0)); + supplied.push(MarketAllocation(allMarkets[0], suppliedAssets[0], 0)); + supplied.push(MarketAllocation(allMarkets[1], suppliedAssets[1], 0)); + supplied.push(MarketAllocation(allMarkets[2], suppliedAssets[2], 0)); + + uint256 idleBefore = _idle(); vm.prank(ALLOCATOR); vault.reallocate(withdrawn, supplied); - assertEq(morpho.supplyShares(allMarkets[0].id(), address(vault)), suppliedShares[0], "morpho.supplyShares(0)"); - assertEq(morpho.supplyShares(allMarkets[1].id(), address(vault)), suppliedShares[1], "morpho.supplyShares(1)"); - assertEq(morpho.supplyShares(allMarkets[2].id(), address(vault)), suppliedShares[2], "morpho.supplyShares(2)"); - - uint256 expectedIdle = idleBefore - suppliedShares[0] / SharesMathLib.VIRTUAL_SHARES - - suppliedShares[1] / SharesMathLib.VIRTUAL_SHARES - suppliedShares[2] / SharesMathLib.VIRTUAL_SHARES; - assertApproxEqAbs(vault.idle(), expectedIdle, 3, "vault.idle() 1"); + assertEq( + morpho.supplyShares(allMarkets[0].id(), address(vault)), + suppliedAssets[0] * SharesMathLib.VIRTUAL_SHARES, + "morpho.supplyShares(0)" + ); + assertEq( + morpho.supplyShares(allMarkets[1].id(), address(vault)), + suppliedAssets[1] * SharesMathLib.VIRTUAL_SHARES, + "morpho.supplyShares(1)" + ); + assertEq( + morpho.supplyShares(allMarkets[2].id(), address(vault)), + suppliedAssets[2] * SharesMathLib.VIRTUAL_SHARES, + "morpho.supplyShares(2)" + ); + + assertApproxEqAbs(_idle(), idleBefore - idleWithdrawn, 3, "idle"); } } diff --git a/test/forge/ReallocateWithdrawTest.sol b/test/forge/ReallocateWithdrawTest.sol index 2e901133..dccc6a0e 100644 --- a/test/forge/ReallocateWithdrawTest.sol +++ b/test/forge/ReallocateWithdrawTest.sol @@ -36,6 +36,7 @@ contract ReallocateWithdrawTest is IntegrationTest { withdrawn.push(MarketAllocation(allMarkets[0], 0, morpho.supplyShares(allMarkets[0].id(), address(vault)))); withdrawn.push(MarketAllocation(allMarkets[1], 0, morpho.supplyShares(allMarkets[1].id(), address(vault)))); withdrawn.push(MarketAllocation(allMarkets[2], 0, morpho.supplyShares(allMarkets[2].id(), address(vault)))); + supplied.push(MarketAllocation(_idleParams(), type(uint256).max, 0)); vm.prank(ALLOCATOR); vault.reallocate(withdrawn, supplied); @@ -43,7 +44,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 testReallocateWithdrawMax() public { @@ -57,7 +58,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 { @@ -105,7 +106,7 @@ contract ReallocateWithdrawTest is IntegrationTest { totalSupplyShares[1] -= withdrawnShares[1]; totalSupplyShares[2] -= withdrawnShares[2]; - uint256 expectedIdle = vault.idle() + withdrawnAssets[0] + withdrawnAssets[1] + withdrawnAssets[2]; + uint256 expectedIdle = _idle() + withdrawnAssets[0] + withdrawnAssets[1] + withdrawnAssets[2]; suppliedAssets[0] = bound(suppliedAssets[0], 0, withdrawnAssets[0].zeroFloorSub(CAP2).min(expectedIdle)); expectedIdle -= suppliedAssets[0]; @@ -145,7 +146,7 @@ contract ReallocateWithdrawTest is IntegrationTest { sharesBefore[2] - withdrawnShares[2] + suppliedShares[2], "morpho.supplyShares(2)" ); - assertApproxEqAbs(vault.idle(), expectedIdle, 1, "vault.idle() 1"); + assertApproxEqAbs(_idle(), expectedIdle, 1, "idle"); } function testReallocateUnauthorizedMarket(uint256 amount) public { @@ -178,20 +179,22 @@ contract ReallocateWithdrawTest is IntegrationTest { vault.reallocate(withdrawn, supplied); } - 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); _setCap(allMarkets[0], type(uint192).max); + withdrawn.push(MarketAllocation(allMarkets[0], CAP2, 0)); supplied.push(MarketAllocation(allMarkets[0], CAP2 + rewards, 0)); vm.prank(ALLOCATOR); - vm.expectRevert(ErrorsLib.InsufficientIdle.selector); + vm.expectRevert(ErrorsLib.InconsistentReallocation.selector); vault.reallocate(withdrawn, supplied); } } diff --git a/test/forge/UrdTest.sol b/test/forge/UrdTest.sol index e25293bf..3f3e431f 100644 --- a/test/forge/UrdTest.sol +++ b/test/forge/UrdTest.sol @@ -76,19 +76,21 @@ contract UrdTest is IntegrationTest { loanToken.setBalance(address(vault), rewards); loanToken.setBalance(address(SUPPLIER), idle); + vm.prank(SUPPLIER); vault.deposit(idle, SUPPLIER); - assertEq(vault.idle(), idle, "vault.idle()"); + assertEq(_idle(), idle, "idle"); + uint256 vaultBalanceBefore = loanToken.balanceOf(address(vault)); - assertEq(vaultBalanceBefore, idle + rewards, "vaultBalanceBefore"); + assertEq(vaultBalanceBefore, rewards, "vaultBalanceBefore"); vm.expectEmit(); emit EventsLib.Skim(address(this), address(rewardsDistributor), address(loanToken), rewards); vault.skim(address(loanToken)); uint256 vaultBalanceAfter = loanToken.balanceOf(address(vault)); - assertEq(vaultBalanceAfter, idle, "vaultBalanceAfter"); + assertEq(vaultBalanceAfter, 0, "vaultBalanceAfter"); assertEq( loanToken.balanceOf(address(rewardsDistributor)), rewards, diff --git a/test/forge/helpers/BaseTest.sol b/test/forge/helpers/BaseTest.sol index c337aab0..ca8a6049 100644 --- a/test/forge/helpers/BaseTest.sol +++ b/test/forge/helpers/BaseTest.sol @@ -71,9 +71,21 @@ contract BaseTest is Test { irm.setApr(0.5 ether); // 50%. + MarketParams memory idleParams = MarketParams({ + loanToken: address(loanToken), + collateralToken: address(0), + oracle: address(0), + irm: address(irm), + lltv: 0 + }); + vm.startPrank(MORPHO_OWNER); + morpho.enableIrm(address(0)); morpho.enableIrm(address(irm)); morpho.setFeeRecipient(MORPHO_FEE_RECIPIENT); + + morpho.enableLltv(0); + morpho.createMarket(idleParams); vm.stopPrank(); for (uint256 i; i < NB_MARKETS; ++i) { @@ -95,6 +107,8 @@ contract BaseTest is Test { 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); diff --git a/test/forge/helpers/IntegrationTest.sol b/test/forge/helpers/IntegrationTest.sol index e9f453ef..01d53560 100644 --- a/test/forge/helpers/IntegrationTest.sol +++ b/test/forge/helpers/IntegrationTest.sol @@ -5,6 +5,7 @@ import "./BaseTest.sol"; contract IntegrationTest is BaseTest { using MathLib for uint256; + using MorphoBalancesLib for IMorpho; using MarketParamsLib for MarketParams; MetaMorpho internal vault; @@ -34,6 +35,14 @@ contract IntegrationTest is BaseTest { vm.stopPrank(); } + function _idleParams() internal view returns (MarketParams memory) { + return allMarkets[allMarkets.length - 1]; + } + + function _idle() internal view returns (uint256) { + return morpho.expectedSupplyBalance(_idleParams(), address(vault)); + } + function _setTimelock(uint256 newTimelock) internal { uint256 timelock = vault.timelock(); if (newTimelock == timelock) return; diff --git a/test/metamorpho_tests.tree b/test/metamorpho_tests.tree index 211038f3..3df1a266 100644 --- a/test/metamorpho_tests.tree +++ b/test/metamorpho_tests.tree @@ -262,12 +262,8 @@ │ └── revert with SupplyCapExceeded(id) ├── when totalWithdrawn > totalSupplied │ └── it should add totalWithdrawn - totalSupplied to idle - └── when totalWithdrawn <= totalSupplied - └── it should compute idleSupplied = totalSupplied - totalWithdrawn - ├── when idle < idleSupplied - │ └── revert with InsufficientIdle() - └── when idle >= idleSupplied - └── it should remove idleSupplied from idle + └── when totalWithdrawn != totalSupplied + └── revert with InconsistentReallocation() /* EXTERNAL */ From b9e54a98ef3971dd2b2ba861c28511657ef7b01a Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Tue, 31 Oct 2023 11:16:37 +0100 Subject: [PATCH 16/27] test(reallocate): revert changes --- test/forge/ReallocateWithdrawTest.sol | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/test/forge/ReallocateWithdrawTest.sol b/test/forge/ReallocateWithdrawTest.sol index 8dd3ab6a..facf7796 100644 --- a/test/forge/ReallocateWithdrawTest.sol +++ b/test/forge/ReallocateWithdrawTest.sol @@ -7,7 +7,7 @@ import {SharesMathLib} from "@morpho-blue/libraries/SharesMathLib.sol"; import "./helpers/IntegrationTest.sol"; uint256 constant CAP2 = 100e18; -uint256 constant INITIAL_DEPOSIT = 3 * CAP2; +uint256 constant INITIAL_DEPOSIT = 4 * CAP2; contract ReallocateWithdrawTest is IntegrationTest { using MarketParamsLib for MarketParams; @@ -24,7 +24,7 @@ contract ReallocateWithdrawTest is IntegrationTest { _setCap(allMarkets[0], CAP2); _setCap(allMarkets[1], CAP2); - _setCap(allMarkets[2], 3 * CAP2); + _setCap(allMarkets[2], CAP2); _sortSupplyQueueIdleLast(); @@ -82,6 +82,7 @@ contract ReallocateWithdrawTest is IntegrationTest { withdrawnShares[2].toAssetsDown(totalSupplyAssets[2], totalSupplyShares[2]) ]; + withdrawn.push(MarketAllocation(idleParams, 0, type(uint256).max)); if (withdrawnShares[0] > 0) withdrawn.push(MarketAllocation(allMarkets[0], 0, withdrawnShares[0])); if (withdrawnAssets[1] > 0) withdrawn.push(MarketAllocation(allMarkets[1], withdrawnAssets[1], 0)); if (withdrawnShares[2] > 0) withdrawn.push(MarketAllocation(allMarkets[2], 0, withdrawnShares[2])); @@ -111,9 +112,10 @@ contract ReallocateWithdrawTest is IntegrationTest { suppliedAssets[2].toSharesDown(totalSupplyAssets[2], totalSupplyShares[2]) ]; - if (withdrawnAssets[0] + withdrawnAssets[1] + withdrawnAssets[2] > 0) { - supplied.push(MarketAllocation(allMarkets[2], type(uint256).max, 0)); - } + if (suppliedAssets[0] > 0) supplied.push(MarketAllocation(allMarkets[0], suppliedAssets[0], 0)); + if (suppliedShares[1] > 0) supplied.push(MarketAllocation(allMarkets[1], 0, suppliedShares[1])); + if (suppliedAssets[2] > 0) supplied.push(MarketAllocation(allMarkets[2], suppliedAssets[2], 0)); + if (expectedIdle > 0) supplied.push(MarketAllocation(idleParams, type(uint256).max, 0)); vm.prank(ALLOCATOR); vault.reallocate(withdrawn, supplied); From cd5b7bc30d6fa44ec86ea53581427dba3204c226 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Tue, 31 Oct 2023 11:30:09 +0100 Subject: [PATCH 17/27] test(erc4626): fix compliance tests --- .github/workflows/foundry.yml | 6 +++--- foundry.toml | 2 -- src/mocks/ERC20Mock.sol | 2 ++ test/forge/ERC4626ComplianceTest.sol | 4 ++++ test/forge/helpers/IntegrationTest.sol | 16 +++++++++++++--- 5 files changed, 22 insertions(+), 8 deletions(-) diff --git a/.github/workflows/foundry.yml b/.github/workflows/foundry.yml index 8e95b8fe..a8c535d2 100644 --- a/.github/workflows/foundry.yml +++ b/.github/workflows/foundry.yml @@ -63,13 +63,13 @@ jobs: type: ["slow", "fast"] include: - type: "slow" - fuzz-runs: 8192 - max-test-rejects: 1048576 + fuzz-runs: 2048 + max-test-rejects: 524288 invariant-runs: 64 invariant-depth: 1024 - type: "fast" fuzz-runs: 256 - max-test-rejects: 262144 + max-test-rejects: 65536 invariant-runs: 16 invariant-depth: 256 diff --git a/foundry.toml b/foundry.toml index 989194eb..133d6a95 100644 --- a/foundry.toml +++ b/foundry.toml @@ -14,8 +14,6 @@ tenderly = "https://rpc.tenderly.co/fork/${TENDERLY_FORK_ID}" [profile.default.fmt] wrap_comments = true -[profile.default.fuzz] -max_test_rejects = 16777216 [profile.build] via-ir = true 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 82683b5c..fabc1241 100644 --- a/test/forge/ERC4626ComplianceTest.sol +++ b/test/forge/ERC4626ComplianceTest.sol @@ -14,5 +14,9 @@ contract ERC4626ComplianceTest is IntegrationTest, ERC4626Test { _delta_ = 0; _vaultMayBeEmpty = true; _unlimitedAmount = true; + + _setCap(allMarkets[0], 100e18); + + _sortSupplyQueueIdleLast(); } } diff --git a/test/forge/helpers/IntegrationTest.sol b/test/forge/helpers/IntegrationTest.sol index 1cb2018e..3e129f3b 100644 --- a/test/forge/helpers/IntegrationTest.sol +++ b/test/forge/helpers/IntegrationTest.sol @@ -121,11 +121,21 @@ contract IntegrationTest is BaseTest { function _sortSupplyQueueIdleLast() internal { Id[] memory supplyQueue = new Id[](vault.supplyQueueSize()); - for (uint256 i; i < supplyQueue.length - 1; ++i) { - supplyQueue[i] = vault.supplyQueue(i + 1); + 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[supplyQueue.length - 1] = idleParams.id(); + supplyQueue[supplyIndex] = idleParams.id(); + ++supplyIndex; + + assembly { + mstore(supplyQueue, supplyIndex) + } vm.prank(ALLOCATOR); vault.setSupplyQueue(supplyQueue); From f7435466429f82adb34eef7d6ef5abfcd4f19f43 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Tue, 31 Oct 2023 11:47:47 +0100 Subject: [PATCH 18/27] test(erc4626): revert test changes --- test/forge/ERC4626Test.sol | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/test/forge/ERC4626Test.sol b/test/forge/ERC4626Test.sol index 6dde47e7..910cade4 100644 --- a/test/forge/ERC4626Test.sol +++ b/test/forge/ERC4626Test.sol @@ -237,15 +237,24 @@ contract ERC4626Test is IntegrationTest, IMorphoFlashLoanCallback { vault.transferFrom(ONBEHALF, RECEIVER, shares); } - function testWithdrawMoreThanBalanceButLessThanTotalAssets(uint256 deposited) public { + function testWithdrawMoreThanBalanceButLessThanTotalAssets(uint256 deposited, uint256 assets) public { deposited = bound(deposited, MIN_TEST_ASSETS, MAX_TEST_ASSETS); loanToken.setBalance(SUPPLIER, deposited); + vm.startPrank(SUPPLIER); - vault.deposit(deposited / 2, ONBEHALF); + uint256 shares = vault.deposit(deposited / 2, ONBEHALF); vault.deposit(deposited / 2, SUPPLIER); - vm.expectRevert(); - vault.withdraw(deposited, RECEIVER, SUPPLIER); + vm.stopPrank(); + + assets = bound(assets, deposited / 2 + 1, vault.totalAssets()); + + uint256 sharesBurnt = vault.previewWithdraw(assets); + vm.expectRevert( + abi.encodeWithSelector(IERC20Errors.ERC20InsufficientBalance.selector, ONBEHALF, shares, sharesBurnt) + ); + vm.prank(ONBEHALF); + vault.withdraw(assets, RECEIVER, ONBEHALF); } function testWithdrawMoreThanTotalAssets(uint256 deposited, uint256 assets) public { From dd47e4c404e49247097f295e321f3d23bfb9c175 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Tue, 31 Oct 2023 12:16:21 +0100 Subject: [PATCH 19/27] test(hardhat): adapt tests --- src/MetaMorpho.sol | 6 ++- test/hardhat/MetaMorpho.spec.ts | 69 ++++++++++++++++++++++++--------- 2 files changed, 55 insertions(+), 20 deletions(-) diff --git a/src/MetaMorpho.sol b/src/MetaMorpho.sol index 41a28b9a..179ee2de 100644 --- a/src/MetaMorpho.sol +++ b/src/MetaMorpho.sol @@ -372,7 +372,11 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph if (supplyCap == 0) revert ErrorsLib.UnauthorizedMarket(id); - if (allocation.assets == type(uint256).max) allocation.assets = totalWithdrawn.zeroFloorSub(totalSupplied); + if (allocation.assets == type(uint256).max) { + allocation.assets = totalWithdrawn.zeroFloorSub(totalSupplied); + + if (allocation.assets == 0) continue; + } (uint256 suppliedAssets,) = MORPHO.supply(allocation.marketParams, allocation.assets, allocation.shares, address(this), hex""); diff --git a/test/hardhat/MetaMorpho.spec.ts b/test/hardhat/MetaMorpho.spec.ts index ddee7290..59a95caa 100644 --- a/test/hardhat/MetaMorpho.spec.ts +++ b/test/hardhat/MetaMorpho.spec.ts @@ -1,8 +1,9 @@ -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"; import { MarketParamsStruct } from "types/@morpho-blue/interfaces/IMorpho"; +import { MarketAllocationStruct } from "types/src/MetaMorpho"; import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; import { mine } from "@nomicfoundation/hardhat-network-helpers"; @@ -79,6 +80,7 @@ describe("MetaMorpho", () => { let supplyCap: bigint; let allMarketParams: MarketParamsStruct[]; + let idleParams: MarketParamsStruct; const expectedMarket = async (marketParams: MarketParamsStruct) => { const id = identifier(marketParams); @@ -158,6 +160,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 +183,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); @@ -211,14 +225,24 @@ describe("MetaMorpho", () => { await metaMorpho.connect(curator).submitCap(marketParams, supplyCap); } + await metaMorpho.connect(curator).submitCap(idleParams, 2n ** 192n - 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).sortWithdrawQueue(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).sortWithdrawQueue( + // Keep idle market first. + [0].concat(allMarketParams.map((_, i) => nbMarkets - i)), + ); hre.tracer.nameTags[morphoAddress] = "Morpho"; hre.tracer.nameTags[collateralAddress] = "Collateral"; @@ -229,7 +253,6 @@ describe("MetaMorpho", () => { }); it("should simulate gas cost [main]", async () => { - let totalAssets: bigint = toBigInt(0); for (let i = 0; i < suppliers.length; ++i) { logProgress("main", i, suppliers.length); @@ -238,10 +261,6 @@ describe("MetaMorpho", () => { await randomForwardTimestamp(); - if (totalAssets + assets > supplyCap) { - break; - } - await metaMorpho.connect(supplier).deposit(assets, supplier.address); await randomForwardTimestamp(); @@ -266,14 +285,24 @@ describe("MetaMorpho", () => { }), ); - const withdrawn = allocation - .map(({ marketParams, liquidShares }) => ({ + const idlePosition = await morpho.position(identifier(idleParams), metaMorphoAddress); + + const withdrawn: MarketAllocationStruct[] = []; + + // Always withdraw half from idle. + if (idlePosition.supplyShares > 1n) + withdrawn.push({ marketParams: idleParams, assets: 0n, shares: idlePosition.supplyShares / 2n }); + + for (const { marketParams, liquidShares } of allocation) { + if (liquidShares === 0n) continue; + + withdrawn.push({ marketParams, assets: 0n, // Always withdraw all, up to the liquidity. shares: liquidShares, - })) - .filter(({ shares }) => shares > 0n); + }); + } const withdrawnAssets = allocation.reduce( (total, { market, liquidShares }) => @@ -284,16 +313,20 @@ describe("MetaMorpho", () => { // Always consider 90% of withdrawn assets because rates go brrrr. const marketAssets = (withdrawnAssets * 9n) / 10n / toBigInt(nbMarkets); - const supplied = allocation - .map(({ marketParams }) => ({ + const supplied: MarketAllocationStruct[] = []; + for (const { marketParams } of allocation) { + supplied.push({ marketParams, // Always supply evenly on each market 90% of what the vault withdrawn in total. assets: marketAssets, shares: 0n, - })) - .filter(({ assets }) => assets > 0n); + }); + } - // await metaMorpho.connect(allocator).reallocate(withdrawn, supplied); + // Always supply remaining to idle. + supplied.push({ marketParams: idleParams, assets: MaxUint256, shares: 0n }); + + await metaMorpho.connect(allocator).reallocate(withdrawn, supplied); // Borrow liquidity to generate interest. @@ -316,8 +349,6 @@ describe("MetaMorpho", () => { await mine(); // Include supplyCollateral + borrow in a single block. } - totalAssets += assets / 2n; - await hre.network.provider.send("evm_setAutomine", [true]); } }); From a1107140f5144f5fb53d31a9dbddbd025109e49f Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Tue, 7 Nov 2023 18:22:57 +0100 Subject: [PATCH 20/27] refactor(idle): rename internal functions --- src/MetaMorpho.sol | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/MetaMorpho.sol b/src/MetaMorpho.sol index 0df341fb..b3f800b7 100644 --- a/src/MetaMorpho.sol +++ b/src/MetaMorpho.sol @@ -468,12 +468,12 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph /// @inheritdoc IERC4626 function maxDeposit(address) public view override(IERC4626, ERC4626) returns (uint256) { - return _maxSupply(); + return _maxDeposit(); } /// @inheritdoc IERC4626 function maxMint(address) public view override(IERC4626, ERC4626) returns (uint256) { - uint256 suppliable = _maxSupply(); + uint256 suppliable = _maxDeposit(); return _convertToShares(suppliable, Math.Rounding.Floor); } @@ -567,10 +567,11 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph assets -= _simulateWithdrawMorpho(assets); } - /// @dev Returns the maximum amount of assets that the vault can withdraw from Morpho. - function _maxSupply() internal view returns (uint256 totalSuppliable) { + /// @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]; + totalSuppliable += _suppliable(_marketParams(id), id); } } From 67a0ee945a882ec6cc57a799dbbb2032eb719c15 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Wed, 8 Nov 2023 14:50:50 +0100 Subject: [PATCH 21/27] test(permit): revert changes --- test/forge/ERC4626ComplianceTest.sol | 1 - test/forge/PermitTest.sol | 48 ++++++++-------------------- 2 files changed, 14 insertions(+), 35 deletions(-) diff --git a/test/forge/ERC4626ComplianceTest.sol b/test/forge/ERC4626ComplianceTest.sol index fabc1241..c778a4af 100644 --- a/test/forge/ERC4626ComplianceTest.sol +++ b/test/forge/ERC4626ComplianceTest.sol @@ -16,7 +16,6 @@ contract ERC4626ComplianceTest is IntegrationTest, ERC4626Test { _unlimitedAmount = true; _setCap(allMarkets[0], 100e18); - _sortSupplyQueueIdleLast(); } } diff --git a/test/forge/PermitTest.sol b/test/forge/PermitTest.sol index 76230f1e..72486039 100644 --- a/test/forge/PermitTest.sol +++ b/test/forge/PermitTest.sol @@ -24,8 +24,7 @@ contract PermitTest is IntegrationTest { } function testPermit() public { - Permit memory permit = - Permit({owner: owner, spender: spender, value: 1e18, nonce: 0, deadline: block.timestamp + DEADLINE}); + Permit memory permit = Permit({owner: owner, spender: spender, value: 1e18, nonce: 0, deadline: DEADLINE}); bytes32 digest = SigUtils.toTypedDataHash(vault.DOMAIN_SEPARATOR(), permit); (uint8 v, bytes32 r, bytes32 s) = vm.sign(OWNER_PK, digest); @@ -37,33 +36,21 @@ contract PermitTest is IntegrationTest { } function testRevertExpiredPermit() public { - Permit memory permit = Permit({ - owner: owner, - spender: spender, - value: 1e18, - nonce: vault.nonces(owner), - deadline: block.timestamp + DEADLINE - }); + Permit memory permit = + Permit({owner: owner, spender: spender, value: 1e18, nonce: vault.nonces(owner), deadline: DEADLINE}); bytes32 digest = SigUtils.toTypedDataHash(vault.DOMAIN_SEPARATOR(), permit); (uint8 v, bytes32 r, bytes32 s) = vm.sign(OWNER_PK, digest); - vm.warp(block.timestamp + DEADLINE + 1 seconds); // fast forward one second past the deadline + vm.warp(DEADLINE + 1 seconds); // fast forward one second past the deadline - vm.expectRevert( - abi.encodeWithSelector(ERC20Permit.ERC2612ExpiredSignature.selector, block.timestamp + DEADLINE) - ); + vm.expectRevert(abi.encodeWithSelector(ERC20Permit.ERC2612ExpiredSignature.selector, DEADLINE)); vault.permit(permit.owner, permit.spender, permit.value, permit.deadline, v, r, s); } function testRevertInvalidSigner() public { - Permit memory permit = Permit({ - owner: owner, - spender: spender, - value: 1e18, - nonce: vault.nonces(owner), - deadline: block.timestamp + DEADLINE - }); + Permit memory permit = + Permit({owner: owner, spender: spender, value: 1e18, nonce: vault.nonces(owner), deadline: DEADLINE}); bytes32 digest = SigUtils.toTypedDataHash(vault.DOMAIN_SEPARATOR(), permit); (uint8 v, bytes32 r, bytes32 s) = vm.sign(SPENDER_PK, digest); // spender signs owner's approval @@ -78,7 +65,7 @@ contract PermitTest is IntegrationTest { spender: spender, value: 1e18, nonce: 1, // owner nonce stored on-chain is 0 - deadline: block.timestamp + DEADLINE + deadline: DEADLINE }); bytes32 digest = SigUtils.toTypedDataHash(vault.DOMAIN_SEPARATOR(), permit); @@ -89,8 +76,7 @@ contract PermitTest is IntegrationTest { } function testRevertSignatureReplay() public { - Permit memory permit = - Permit({owner: owner, spender: spender, value: 1e18, nonce: 0, deadline: block.timestamp + DEADLINE}); + Permit memory permit = Permit({owner: owner, spender: spender, value: 1e18, nonce: 0, deadline: DEADLINE}); bytes32 digest = SigUtils.toTypedDataHash(vault.DOMAIN_SEPARATOR(), permit); (uint8 v, bytes32 r, bytes32 s) = vm.sign(OWNER_PK, digest); @@ -102,8 +88,7 @@ contract PermitTest is IntegrationTest { } function testTransferFromLimitedPermit() public { - Permit memory permit = - Permit({owner: owner, spender: spender, value: 1e18, nonce: 0, deadline: block.timestamp + DEADLINE}); + Permit memory permit = Permit({owner: owner, spender: spender, value: 1e18, nonce: 0, deadline: DEADLINE}); bytes32 digest = SigUtils.toTypedDataHash(vault.DOMAIN_SEPARATOR(), permit); (uint8 v, bytes32 r, bytes32 s) = vm.sign(OWNER_PK, digest); @@ -119,13 +104,8 @@ contract PermitTest is IntegrationTest { } function testTransferFromMaxPermit() public { - Permit memory permit = Permit({ - owner: owner, - spender: spender, - value: type(uint256).max, - nonce: 0, - deadline: block.timestamp + DEADLINE - }); + Permit memory permit = + Permit({owner: owner, spender: spender, value: type(uint256).max, nonce: 0, deadline: DEADLINE}); bytes32 digest = SigUtils.toTypedDataHash(vault.DOMAIN_SEPARATOR(), permit); (uint8 v, bytes32 r, bytes32 s) = vm.sign(OWNER_PK, digest); @@ -146,7 +126,7 @@ contract PermitTest is IntegrationTest { spender: spender, value: 5e17, // approve only 0.5 tokens nonce: 0, - deadline: block.timestamp + DEADLINE + deadline: DEADLINE }); bytes32 digest = SigUtils.toTypedDataHash(vault.DOMAIN_SEPARATOR(), permit); @@ -164,7 +144,7 @@ contract PermitTest is IntegrationTest { spender: spender, value: 2e18, // approve 2 tokens nonce: 0, - deadline: block.timestamp + DEADLINE + deadline: DEADLINE }); bytes32 digest = SigUtils.toTypedDataHash(vault.DOMAIN_SEPARATOR(), permit); From 48389320092bb23798d0036c249c7a15503fd6d2 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Thu, 9 Nov 2023 14:13:38 +0100 Subject: [PATCH 22/27] ci(foundry): increase max test rejects --- .github/workflows/foundry.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/foundry.yml b/.github/workflows/foundry.yml index a8c535d2..20dd9029 100644 --- a/.github/workflows/foundry.yml +++ b/.github/workflows/foundry.yml @@ -64,7 +64,7 @@ jobs: include: - type: "slow" fuzz-runs: 2048 - max-test-rejects: 524288 + max-test-rejects: 1048576 invariant-runs: 64 invariant-depth: 1024 - type: "fast" From 5ac5bb7e661b9f88d198da4a6414483640ffc8fc Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Thu, 9 Nov 2023 17:08:55 +0100 Subject: [PATCH 23/27] docs(readme): add idle market doc --- .github/workflows/foundry.yml | 6 +++--- README.md | 26 ++++++++++++++++++++++---- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/.github/workflows/foundry.yml b/.github/workflows/foundry.yml index 20dd9029..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: 2048 + 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 d69590ef..3cfb7439 100644 --- a/README.md +++ b/README.md @@ -68,12 +68,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 supply queue in the order set. The remaining funds are left as idle supply on the vault (uncapped). - - 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 withdrawal queue in the order set. + - Upon a deposit, the vault will supply up to the cap of each Morpho Blue market in the supply queue in the order set. + - Upon a withdrawal, the vault will withdraw up to the liquidity of each Morpho Blue market in the withdrawal queue in the order set. - The `supplyQueue` contains only enabled markets (enabled market are markets with non-zero cap or with non-zero vault's supply). - The `withdrawQueue` contains all enabled markets. - Instantaneously reallocate funds among the enabled market at any moment. +> **Warning** +> If `supplyQueue` is empty, depositing to the vault is disabled. + #### Guardian Only one address can have this role. @@ -82,6 +85,22 @@ It can: - Revoke any timelocked action except it cannot revoke a pending fee. +### Idle Supply + +In some cases, the vault's curator or allocators may want to keep some funds "idle", to guarantee lenders some extent of liquidity 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 (`address(0)` to save gas). +- No oracle (`address(0)`). +- An arbitrary LLTV (`0`). + +Thus, these funds cannot be borrowed on Morpho Blue and is 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. It is advised to enable an infinite cap (`type(uint256).max`). + ### 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). @@ -102,8 +121,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. From c4bf0398fbd780a38e5ec0f16e10e68c3ca25000 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Tue, 14 Nov 2023 16:37:27 +0100 Subject: [PATCH 24/27] docs(README): update --- README.md | 9 +- test/metamorpho_tests.tree | 436 ------------------------------------- 2 files changed, 5 insertions(+), 440 deletions(-) delete mode 100644 test/metamorpho_tests.tree diff --git a/README.md b/README.md index 3f4f6a02..02137526 100644 --- a/README.md +++ b/README.md @@ -90,19 +90,20 @@ It can: ### Idle Supply -In some cases, the vault's curator or allocators may want to keep some funds "idle", to guarantee lenders some extent of liquidity from the vault (beyond the liquidity of each of the vault's markets). +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 (`address(0)` to save gas). +- An arbitrary IRM. - No oracle (`address(0)`). - An arbitrary LLTV (`0`). -Thus, these funds cannot be borrowed on Morpho Blue and is guaranteed to be liquid ; though it won't generate interest. +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. It is advised to enable an infinite cap (`type(uint256).max`). +Note that to allocate funds to this idle market, it is first required to enable its cap on MetaMorpho. +It is advised to enable an infinite cap (`type(uint256).max`) to always allow users to deposit on the vault, but any other desired cap can be used to limit the quantity of funds left idle that has to be manually reallocated by the allocator role to earn interest. ### Rewards diff --git a/test/metamorpho_tests.tree b/test/metamorpho_tests.tree deleted file mode 100644 index 5a8e58b7..00000000 --- a/test/metamorpho_tests.tree +++ /dev/null @@ -1,436 +0,0 @@ - -/* ONLY OWNER FUNCTIONS */ - - -. -└── setCurator(address newCurator) external - ├── when msg.sender not owner - │ └── revert with NOT_OWNER - └── when msg.sender is owner - ├── when newCurator == curator - │ └── revert with AlreadySet() - └── when newCurator != curator - ├── it should set curator to newCurator - └── it should emit SetCurator(newCurator) - -. -└── setIsAllocator(address newAllocator, bool newIsAllocator) external - ├── when msg.sender not owner - │ └── revert with NOT_OWNER - └── when msg.sender is owner - ├── when _isAllocator[newAllocator] == newIsAllocator - │ └── revert with AlreadySet() - └── when _isAllocator[newAllocator] != newIsAllocator - ├── it should set _isAllocator[newAllocator] to newIsAllocator - └── it should emit SetIsAllocator(newAllocator, newIsAllocator) - -. -└── setSkimRecipient(address newSkimRecipient) external - ├── when msg.sender not owner - │ └── revert with NOT_OWNER - └── when msg.sender is owner - ├── when newSkimRecipient == skimRecipient - │ └── revert with AlreadySet() - └── when newSkimRecipient != skimRecipient - ├── it should set skimRecipient to newSkimRecipient - └── it shoud emit SetSkimRecipient(newSkimRecipient) - -. -└── submitTimelock(uint256 newTimelock) external - ├── when msg.sender not owner - │ └── revert with NOT_OWNER - └── when msg.sender is owner - ├── when newTimelock > MAX_TIMELOCK - │ └── revert with ABOVE_MAX_TIMELOCK - ├── when newTimelock < MIN_TIMELOCK - │ └── revert with BELOW_MAX_TIMELOCK - └── when newTimelock <= MAX_TIMELOCK - ├── when newTimelock == timelock - │ └── revert with AlreadySet() - └── when newTimelock != timelock - ├── when newTimelock > timelock - │ ├── it should set timelock to newTimelock - │ ├── it should emit SetTimelock(newTimelock) - │ └── it should delete pendingTimelock - └── when newTimelock < timelock - ├── it should set pendingTimelock to PendingUint192(uint192(newTimelock), uint64(block.timestamp)) - └── it should emit SubmitTimelock(newTimelock) - -. -└── acceptTimelock() external - ├── when pendingTimelock.validAt == 0 - │ └── revert with NoPendingValue() - └── when pendingTimelock.validAt != 0 - ├── when block.timestamp < pendingTimelock.validAt - │ └── revert with TimelockNotElapsed() - └── when block.timestamp >= pendingTimelock.validAt - ├── it should set timelock to pendingTimelock.value - ├── it should emit SetTimelock(pendingTimelock.value) - └── it should delete pendingTimelock - -. -└── setFee(uint256 newFee) external - ├── when msg.sender not owner - │ └── revert with NOT_OWNER - └── when msg.sender is owner - ├── when newFee > MAX_FEE - │ └── revert with MAX_FEE_EXCEEDED - └── when newFee <= MAX_FEE - ├── when newFee == fee - │ └── revert with AlreadySet() - └── when newFee != fee - ├── when newFee != 0 and feeRecipient == address(0) - │ └── revert with ZeroFeeRecipient() - └── when newFee == 0 or feeRecipient != address(0) - ├── it should accrue fees - ├── it should set fee to newFee - ├── it should emit SetFee(caller, newFee) - -. -└── setFeeRecipient(address newFeeRecipient) external - ├── when msg.sender not owner - │ └── revert with NOT_OWNER - └── when msg.sender is owner - ├── when newFeeRecipient == feeRecipient - │ └── revert with AlreadySet() - └── when newFeeRecipient != feeRecipient - ├── when newFeeRecipient == address(0) and fee != 0 - │ └── revert with ZeroFeeRecipient() - └── when newFeeRecipient != address(0) or fee == 0 - ├── it should accrue fees - ├── it should set feeRecipient to newFeeRecipient - └── it shoud emit SetFeeRecipient(newFeeRecipient) - -. -└── submitGuardian(address newGuardian) external - ├── when msg.sender not owner - │ └── revert with NOT_OWNER - └── when msg.sender is owner - ├── when newGuardian == guardian - │ └── revert with AlreadySet() - └── when newGuardian != guardian - ├── when guardian == address(0) - │ ├── it should set guardian to newGuardian - │ ├── it should emit SetGuardian(newGuardian) - │ └── it should delete pendingGuardian - └── when guardian != address(0) - ├── it should it should set pendingGuardian to PendingAddress(newGuardian, uint64(block.timestamp)) - └── it should emit SubmitGuardian(newGuardian) - -. -└── acceptGuardian() external - ├── when pendingGuardian.validAt == 0 - │ └── revert with NoPendingValue() - └── when pendingGuardian.validAt != 0 - ├── when block.timestamp < pendingGuardian.validAt - │ └── revert with TimelockNotElapsed() - └── when block.timestamp >= pendingGuardian.validAt - ├── it should set guardian to pendingGuardian - ├── it should emit SetGuardian(pendingGuardian) - └── it should delete pendingGuardian - - -/* ONLY CURATOR FUNCTIONS */ - - -. -└── submitCap(MarketParams memory marketParams, uint256 newSupplyCap) external - ├── when msg.sender not owner or curator - │ └── revert with NotCuratorRole() - └── when msg.sender is owner or curator - ├── when marketParams.loanToken != asset() - │ └── revert with InconsistentAsset() - ├── when marketParams.loanToken == asset() - └── it should compute id = marketParams.id() - ├── when MORPHO.lastUpdate(id) == 0 - │ └── revert with MarketNotCreated() - └── when MORPHO.lastUpdate(id) != 0 - ├── when newSupplyCap == supplyCap - │ └── revert with AlreadySet() - └── when newSupplyCap != supplyCap - ├── when newSupplyCap < supplyCap - │ ├── when newSupplyCap > 0 and marketConfig.withdrawRank == 0 - │ │ ├── it should push id to supplyQueue - │ │ ├── it should push id to withdrawQueue - │ │ └── if withdrawQueue.length > MAX_QUEUE_LENGTH - │ │ └── revert with MaxQueueLengthExceeded() - │ ├── it should set config[id].cap to newSupplyCap - │ ├── it should emit SetCap(id, newSupplyCap) - │ └── it should delete pendingCap[id] - └── when newSupplyCap > supplyCap - ├── it should it should set pendingCap[id] to PendingUint192(newSupplyCap.toUint192(), uint64(block.timestamp)) - └── it should emit EventsLib.SubmitCap(id, newSupplyCap) - -. -└── acceptCap(Id id) external - ├── when pendingCap[id].validAt == 0 - │ └── revert with NoPendingValue() - └── when pendingCap[id].validAt != 0 - ├── when block.timestamp < pendingCap[id].validAt - │ └── revert with TimelockNotElapsed() - └── when block.timestamp >= pendingCap[id].validAt - ├── when supplyCap > 0 and marketConfig.withdrawRank == 0 - │ ├── it should push id to supplyQueue - │ ├── it should push id to withdrawQueue - │ ├── if supplyQueue.length > MAX_QUEUE_LENGTH - │ │ └── revert with MaxQueueLengthExceeded() - │ └── if withdrawQueue.length > MAX_QUEUE_LENGTH - │ └── revert with MaxQueueLengthExceeded() - ├── it should set config[id].cap to pendingCap[id].value - ├── it should emit SetCap(id, pendingCap[id].value) - └── it should delete pendingCap[id] - - -/* ONLY ALLOCATOR FUNCTIONS */ - - -. -└── setSupplyQueue(Id[] calldata newSupplyQueue) external - ├── when msg.sender not owner or curator or allocator - │ └── revert with NotAllocatorRole() - └── when msg.sender is owner or curator or allocator - ├── when newSupplyQueue.length > MAX_QUEUE_LENGTH - │ └── revert with MaxQueueLengthExceeded() - └── when newSupplyQueue.length <= MAX_QUEUE_LENGTH - ├── when some markets of newSupplyQueue have a zero cap - │ └── revert with UnauthorizedMarket() - └── when all the markets of newSupplyQueue have non zero cap - ├── it should set supplyQueue to newSupplyQueue - └── it shoud emit SetSupplyQueue(msg.sender, newSupplyQueue) - -. -└── updateWithdrawQueue(uint256[] calldata indexes) external - ├── when msg.sender not owner or curator or allocator - │ └── revert with NotAllocatorRole() - └── when msg.sender is owner or curator or allocator - ├── when indexes contains a duplicate market - │ ├── it should compute the id of the duplicate market - │ └── revert with DuplicateMarket(id) - └── when indexes does not contain duplicates - ├── it should compute newWithdrawQueue - ├── when markets of withdrawQueue are missing in newWithdrawQueue - │ ├── when one of these markets has non zero cap or vault's supply - │ │ ├── it should compute the id of this market - │ │ └── revert with InvalidMarketRemoval(id) - │ └── when all these markets have zero cap and zero vault's supply - │ └── it should delete the withdrawRank of these markets - ├── it should set withdrawQueue to newWithdrawQueue - └── it shoud emit SetWithdrawQueue(msg.sender, newWithdrawQueue) - -. -└── reallocate(MarketAllocation[] calldata withdrawn, MarketAllocation[] calldata supplied) external - ├── external - ├── when msg.sender not owner or curator or allocator - │ └── revert with NotAllocatorRole() - └── when msg.sender is owner or curator or allocator - ├── for each market withdrawn[i] from withdrawn - │ ├── it should withdraw withdrawn[i].assets or withdrawn[i].shares - │ ├── it should add the withdrawnAssets amount to totalWithdrawn - │ └── it should emit ReallocateWithdraw(id, withdrawnAssets, withdrawnShares) - ├── for each market supplied[i] from supplied - │ ├── if config[id].cap == 0 - │ │ └── revert with UnauthorizedMarket(id) - │ ├── it should supply supplied[i].assets or supplied[i].shares - │ ├── it should add the suppliedAssets amount to totalSupplied - │ ├── if supplyBalance(supplied[i].marketParams) > config[supplied[i].marketParams.id()].cap - │ │ └── revert with SupplyCapExceeded(id) - │ └── it should emit ReallocateSupply(id, suppliedAssets, suppliedShares) - ├── when totalWithdrawn > totalSupplied - │ └── it should add totalWithdrawn - totalSupplied to idle - └── when totalWithdrawn != totalSupplied - └── revert with InconsistentReallocation() - - -/* EXTERNAL */ - - -. -└── skim(address token) external - ├── when skimRecipient == address(0) - │ └── revert with ZERO_ADDRESS - └── when skimRecipient != address(0) - ├── it should compute amount = IERC20(token).balanceOf(address(this)) - ├── when token == asset() - │ └── it should remove idle from amount - ├── it should transfer amount of token from the the vault to skimRecipient - └── it should emit Skim(msg.sender, skimRecipient, token, amount) - - -/* REVOKE FUNCTIONS */ - - -. -└── revokePendingTimelock() external - ├── when msg.sender not guardian nor owner - │ └── revert with NotGuardianRole() - ├── when pending timelock's validAt is zero - │ └── revert with NoPendingValue() - └── when msg.sender is guardian - ├── it should delete pendingTimelock - └── it should emit RevokePendingTimelock(msg.sender) - -. -└── revokeGuardian() external - ├── when msg.sender not guardian nor owner - │ └── revert with NotGuardianRole() - ├── when pending guardian's validAt is zero - │ └── revert with NoPendingValue() - └── when msg.sender is guardian - ├── it should delete pendingGuardian - └── it should emit RevokePendingGuardian(msg.sender) - -. -└── revokePendingCap(Id id) external - ├── when msg.sender not curator nor guardian nor owner - │ └── revert with NotCuratorNorGuardian() - ├── when pending cap's validAt is zero - │ └── revert with NoPendingValue() - └── when msg.sender is curator or guardian or owner - ├── it should delete pendingCap[id] - └── it should emit RevokePendingCap(msg.sender, id, pendingCap[id]) - - -/* PUBLIC */ - - -. -└── isAllocator(address target) public view returns (bool) - ├── when _isAllocator[target] == true or target == curator or target == owner - │ └── it should return true - └── when _isAllocator[target] == false and target != curator and target != owner - └── it should return false - - -/* ERC4626 (PUBLIC) */ - - -. -└── maxWithdraw(address owner) public view override(IERC4626, ERC4626) returns (uint256 assets) - ├── it should compute (feeShares, newTotalAssets) = _accruedFeeShares() - ├── it should compute newTotalSupply = totalSupply() + feeShares - ├── it should compute assets = _convertToAssetsWithFeeAccrued(balanceOf(owner), newTotalSupply, newTotalAssets, Math.Rounding.Down) - ├── when idle >= assets - │ └── it should return assets - ├── when idle < assets - │ ├── it should compute remaining = assets - idle - │ └── for each market withdrawQueue[i] in withdrawQueue - │ ├── it should compute id = withdrawQueue[i] - │ ├── it should remove _withdrawable(_marketParams(id), id) from remaining - │ └── if remaining == 0 - │ └── it should return assets - └── it should return assets - remaining - -. -└── maxRedeem(address owner) public view override(IERC4626, ERC4626) returns (uint256) - ├── it should compute (feeShares, newTotalAssets) = _accruedFeeShares() - ├── it should compute newTotalSupply = totalSupply() + feeShares - ├── it should compute assets = _convertToAssetsWithFeeAccrued(balanceOf(owner), newTotalSupply, newTotalAssets, Math.Rounding.Down) - ├── when idle >= assets - │ └── it should return _convertToSharesWithFeeAccrued(assets, newTotalSupply, newTotalAssets, Math.Rounding.Down) - ├── when idle < assets - │ ├── it should compute remaining = assets - idle - │ └── for each market withdrawQueue[i] in withdrawQueue - │ ├── it should compute id = withdrawQueue[i] - │ ├── it should remove _withdrawable(_marketParams(id), id) from remaining - │ └── if remaining == 0 - │ └── it should return _convertToSharesWithFeeAccrued(assets, newTotalSupply, newTotalAssets, Math.Rounding.Down) - └── it should return _convertToSharesWithFeeAccrued(assets - remaining, newTotalSupply, newTotalAssets, Math.Rounding.Down) - -. -└── deposit(uint256 assets, address receiver) public override(IERC4626, ERC4626) returns (uint256 shares) - ├── it should accrue fees - ├── it should compute shares = _convertToSharesWithFeeAccrued(assets, totalSupply(), newTotalAssets, Math.Rounding.Down) - ├── it should transfer assets of token from the sender to the vault - ├── it should mint the shares for the receiver - ├── it should emit Deposit(caller, receiver, assets, shares) - ├── for each market supplyQueue[i] in supplyQueue - │ ├── it should compute toSupply = UtilsLib.min(_suppliable(marketParams, id), assets) - │ ├── when toSupply > 0 - │ │ ├── it should supply toSupply - │ │ └── it should remove the supplied amount from assets - │ └── if assets == 0 - │ └── it should stop supplying on new markets - ├── it should add assets to idle - ├── it should set lastTotalAssets to newTotalAssets + assets - ├── it should emit UpdateLastTotalAssets(newTotalAssets + assets) - └── it should return shares - -. -└── mint(uint256 shares, address receiver) public override(IERC4626, ERC4626) returns (uint256 assets) - ├── it should accrue fees - ├── it should compute assets = _convertToAssetsWithFeeAccrued(shares, totalSupply(), newTotalAssets, Math.Rounding.Up) - ├── it should transfer assets of token from the sender to the vault - ├── it should mint the shares for the receiver - ├── it should emit Deposit(caller, receiver, assets, shares) - ├── for each market supplyQueue[i] in supplyQueue - │ ├── it should compute toSupply = UtilsLib.min(_suppliable(marketParams, id), assets) - │ ├── if toSupply > 0 - │ │ ├── it should supply toSupply - │ │ └── it should remove the supplied amount from assets - │ └── if assets == 0 - │ └── it should stop supplying on new markets - ├── it should add assets to idle - ├── it should set lastTotalAssets to newTotalAssets + assets - ├── it should emit UpdateLastTotalAssets(newTotalAssets + assets) - └── it should return assets - -. -└── withdraw(uint256 assets, address receiver, address owner) public override(IERC4626, ERC4626) returns (uint256 shares) - ├── it should accrue fees - ├── it should compute shares = _convertToSharesWithFeeAccrued(assets, totalSupply(), newTotalAssets, Math.Rounding.Up) - ├── when idle >= assets - │ └── it should remove assets from idle - ├── when idle < assets - │ ├── it should compute remaining = assets - idle - │ ├── it should set idle to 0 - │ └── for each market withdrawQueue[i] in withdrawQueue - │ ├── it should compute id = withdrawQueue[i] - │ ├── it should compute toWithdraw = UtilsLib.min(_withdrawable(marketParams, id), remaining) - │ ├── if toWithdraw > 0 - │ │ ├── it should withdraw toWithdraw - │ │ └── it should remove toWithdraw from remaining - │ └── if remaining == 0 - │ └── it should stop withdrawing on new markets - ├── when remaining != 0 - │ └── revert with WithdrawMorphoFailed() - └── when remaining == 0 - ├── it should transfer assets of token to the receiver - ├── it should burn the shares of the receiver - ├── it should emit Withdraw(caller, receiver, owner, assets, shares) - ├── it should set lastTotalAssets to newTotalAssets - assets - ├── it should emit UpdateLastTotalAssets(newTotalAssets - assets) - └── it should return shares - -. -└── redeem(uint256 shares, address receiver, address owner) public override(IERC4626, ERC4626) returns (uint256 shares) - ├── it should accrue fees - ├── it should compute assets = _convertToAssetsWithFeeAccrued(shares, totalSupply(), newTotalAssets, Math.Rounding.Down) - ├── when idle >= assets - │ └── it should remove assets from idle - ├── when idle < assets - │ ├── it should compute remaining = assets - idle - │ ├── it should set idle to 0 - │ └── for each market withdrawQueue[i] in withdrawQueue - │ ├── it should compute id = withdrawQueue[i] - │ ├── it should compute toWithdraw = UtilsLib.min(_withdrawable(marketParams, id), remaining) - │ ├── if toWithdraw > 0 - │ │ ├── it should withdraw toWithdraw - │ │ └── it should remove toWithdraw from remaining - │ └── if remaining == 0 - │ └── it should stop withdrawing on new markets - ├── when remaining != 0 - │ └── revert with WithdrawMorphoFailed() - └── when remaining == 0 - ├── it should transfer assets of token to the receiver - ├── it should burn the shares of the receiver - ├── it should emit Withdraw(caller, receiver, owner, assets, shares) - ├── it should set lastTotalAssets to newTotalAssets - assets - ├── it should emit UpdateLastTotalAssets(newTotalAssets - assets) - └── it should return assets - -. -└── totalAssets() public view override(IERC4626, ERC4626) returns (uint256 assets) - ├── for each market withdrawQueue[i] in withdrawQueue - │ └── it should add _supplyBalance(_marketParams(withdrawQueue[i])) to assets - ├── it should add idle to assets - └── it should return assets From f6a161d855cb93b2384b01c2ec55dd2009eec32c Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Tue, 14 Nov 2023 19:03:42 +0100 Subject: [PATCH 25/27] docs(README): mention infinite cap --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f87abba7..55a91fff 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,7 @@ To achieve this, it is advised to allocate "idle" funds to any market on Morpho 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. -It is advised to enable an infinite cap (`type(uint256).max`) to always allow users to deposit on the vault, but any other desired cap can be used to limit the quantity of funds left idle that has to be manually reallocated by the allocator role to earn interest. +Enabling an infinite cap (`type(uint184).max`) will always allow users to deposit on the vault. ### Rewards From 81b0524d7f28b6fca9c7385741bdd825ee11f105 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Wed, 15 Nov 2023 10:37:47 +0100 Subject: [PATCH 26/27] fix: apply suggestions --- README.md | 4 ++-- src/MetaMorpho.sol | 8 ++++---- src/libraries/ErrorsLib.sol | 4 ++-- test/forge/ERC4626Test.sol | 9 +++++++-- test/forge/UrdTest.sol | 28 ---------------------------- 5 files changed, 15 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 55a91fff..ad1c7f03 100644 --- a/README.md +++ b/README.md @@ -103,10 +103,10 @@ To achieve this, it is advised to allocate "idle" funds to any market on Morpho - The vault's asset as loan token. - No collateral token (`address(0)`). - An arbitrary IRM. -- No oracle (`address(0)`). +- 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. +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/src/MetaMorpho.sol b/src/MetaMorpho.sol index 12790e4d..2e3dae22 100644 --- a/src/MetaMorpho.sol +++ b/src/MetaMorpho.sol @@ -699,8 +699,8 @@ 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 @@ -833,10 +833,10 @@ contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, Multicall, IMetaMorph if (assets == 0) return; } - if (assets != 0) revert ErrorsLib.WithdrawMorphoFailed(); + if (assets != 0) revert ErrorsLib.NotEnoughLiquidity(); } - /// @dev Fakes a withdraw of `assets` from Morpho. + /// @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) { diff --git a/src/libraries/ErrorsLib.sol b/src/libraries/ErrorsLib.sol index a47bcc74..009836a2 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(); diff --git a/test/forge/ERC4626Test.sol b/test/forge/ERC4626Test.sol index d44cfe30..239496f7 100644 --- a/test/forge/ERC4626Test.sol +++ b/test/forge/ERC4626Test.sol @@ -34,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)"); } @@ -49,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)"); } @@ -68,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)"); } @@ -85,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)"); } @@ -104,6 +108,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)"); assertEq(_idle(), deposited - withdrawn, "idle"); } @@ -268,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); } @@ -290,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/UrdTest.sol b/test/forge/UrdTest.sol index d6bd0264..8530dc09 100644 --- a/test/forge/UrdTest.sol +++ b/test/forge/UrdTest.sol @@ -66,34 +66,6 @@ contract UrdTest is IntegrationTest { ); } - 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(_idle(), idle, "idle"); - - uint256 vaultBalanceBefore = loanToken.balanceOf(address(vault)); - assertEq(vaultBalanceBefore, 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, 0, "vaultBalanceAfter"); - assertEq(loanToken.balanceOf(address(rewardsDistributor)), rewards, "loanToken.balanceOf(rewardsDistributor)"); - } - function testSkimZeroAddress() public { vm.expectRevert(ErrorsLib.ZeroAddress.selector); vault.skim(address(loanToken)); From bf65c1466b0f8bf8599da23d6569401c23b5f0c9 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Wed, 15 Nov 2023 14:20:36 +0100 Subject: [PATCH 27/27] docs(errors): fix typo --- src/libraries/ErrorsLib.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/ErrorsLib.sol b/src/libraries/ErrorsLib.sol index 009836a2..254735cd 100644 --- a/src/libraries/ErrorsLib.sol +++ b/src/libraries/ErrorsLib.sol @@ -81,7 +81,7 @@ 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 amount withdrawn is not excatly the amount supplied. + /// @notice Thrown when the amount withdrawn is not exactly the amount supplied. error InconsistentReallocation(); /// @notice Thrown when all caps have been reached.