diff --git a/test/forge/BaseTest.sol b/test/forge/BaseTest.sol index a26dab28e..9ea65f507 100644 --- a/test/forge/BaseTest.sol +++ b/test/forge/BaseTest.sol @@ -310,8 +310,12 @@ contract BaseTest is Test { { Id _id = _marketParams.id(); + uint256 borrowShares = morpho.borrowShares(_id, onBehalf); + (,, uint256 totalBorrowAssets, uint256 totalBorrowShares) = morpho.expectedMarketBalances(_marketParams); - uint256 maxRepayAssets = morpho.borrowShares(_id, onBehalf).toAssetsDown(totalBorrowAssets, totalBorrowShares); + // Rounding assets up can yield a value larger than `totalBorrowAssets` in case `totalBorrowAssets` is zero. + uint256 maxRepayAssets = + UtilsLib.min(borrowShares.toAssetsUp(totalBorrowAssets, totalBorrowShares), totalBorrowAssets); return bound(assets, 0, maxRepayAssets); } @@ -335,14 +339,19 @@ contract BaseTest is Test { { Id _id = _marketParams.id(); - uint256 collateral = morpho.collateral(_id, borrower); + (,, uint256 totalBorrowAssets, uint256 totalBorrowShares) = morpho.expectedMarketBalances(_marketParams); + + // Rounding assets up can yield a value larger than `totalBorrowAssets` in case `totalBorrowAssets` is zero. + uint256 maxRepaidAssets = UtilsLib.min( + morpho.borrowShares(_id, borrower).toAssetsUp(totalBorrowAssets, totalBorrowShares), totalBorrowAssets + ); + uint256 collateralPrice = IOracle(_marketParams.oracle).price(); - uint256 maxRepaidAssets = morpho.expectedBorrowAssets(_marketParams, borrower); uint256 maxSeizedAssets = maxRepaidAssets.wMulDown(_liquidationIncentiveFactor(_marketParams.lltv)).mulDivDown( ORACLE_PRICE_SCALE, collateralPrice ); - return bound(seizedAssets, 0, Math.min(collateral, maxSeizedAssets)); + return bound(seizedAssets, 0, Math.min(morpho.collateral(_id, borrower), maxSeizedAssets)); } function _boundLiquidateRepaidShares(MarketParams memory _marketParams, address borrower, uint256 repaidShares) @@ -352,15 +361,14 @@ contract BaseTest is Test { { Id _id = _marketParams.id(); - uint256 borrowShares = morpho.borrowShares(_id, borrower); uint256 collateralPrice = IOracle(_marketParams.oracle).price(); - uint256 maxRepaidAssets = morpho.collateral(_id, borrower).mulDivUp(collateralPrice, ORACLE_PRICE_SCALE).wDivUp( - _liquidationIncentiveFactor(_marketParams.lltv) - ); + uint256 maxRepaidAssets = morpho.collateral(_id, borrower).mulDivDown(collateralPrice, ORACLE_PRICE_SCALE) + .wDivDown(_liquidationIncentiveFactor(_marketParams.lltv)); + (,, uint256 totalBorrowAssets, uint256 totalBorrowShares) = morpho.expectedMarketBalances(marketParams); uint256 maxRepaidShares = maxRepaidAssets.toSharesDown(totalBorrowAssets, totalBorrowShares); - return bound(repaidShares, 0, Math.min(borrowShares, maxRepaidShares)); + return bound(repaidShares, 0, Math.min(morpho.borrowShares(_id, borrower), maxRepaidShares)); } function _maxBorrow(MarketParams memory _marketParams, address user) internal view returns (uint256) { diff --git a/test/forge/InvariantTest.sol b/test/forge/InvariantTest.sol index d1e0d68aa..4ceebeccb 100644 --- a/test/forge/InvariantTest.sol +++ b/test/forge/InvariantTest.sol @@ -17,8 +17,6 @@ contract InvariantTest is BaseTest { _targetSenders(); - _weightSelector(this.mine.selector, 100); - targetContract(address(this)); targetSelector(FuzzSelector({addr: address(this), selectors: selectors})); } @@ -58,7 +56,7 @@ contract InvariantTest is BaseTest { /* HANDLERS */ function mine(uint256 blocks) external { - blocks = bound(blocks, 1, 50_400); + blocks = bound(blocks, 1, 1 days / BLOCK_TIME); _forward(blocks); } diff --git a/test/forge/invariant/MorphoInvariantTest.sol b/test/forge/invariant/BaseMorphoInvariantTest.sol similarity index 73% rename from test/forge/invariant/MorphoInvariantTest.sol rename to test/forge/invariant/BaseMorphoInvariantTest.sol index edb423473..c48ce0855 100644 --- a/test/forge/invariant/MorphoInvariantTest.sol +++ b/test/forge/invariant/BaseMorphoInvariantTest.sol @@ -3,16 +3,13 @@ pragma solidity ^0.8.0; import "../InvariantTest.sol"; -contract MorphoInvariantTest is InvariantTest { +contract BaseMorphoInvariantTest is InvariantTest { using MathLib for uint256; using SharesMathLib for uint256; using MorphoLib for IMorpho; using MorphoBalancesLib for IMorpho; using MarketParamsLib for MarketParams; - uint256 internal immutable MIN_PRICE = ORACLE_PRICE_SCALE / 10; - uint256 internal immutable MAX_PRICE = ORACLE_PRICE_SCALE * 10; - address internal immutable USER; MarketParams[] internal allMarketParams; @@ -22,19 +19,6 @@ contract MorphoInvariantTest is InvariantTest { } function setUp() public virtual override { - _weightSelector(this.setPrice.selector, 10); - _weightSelector(this.setFeeNoRevert.selector, 5); - _weightSelector(this.supplyAssetsOnBehalfNoRevert.selector, 100); - _weightSelector(this.supplySharesOnBehalfNoRevert.selector, 100); - _weightSelector(this.withdrawAssetsOnBehalfNoRevert.selector, 50); - _weightSelector(this.borrowAssetsOnBehalfNoRevert.selector, 75); - _weightSelector(this.repayAssetsOnBehalfNoRevert.selector, 35); - _weightSelector(this.repaySharesOnBehalfNoRevert.selector, 35); - _weightSelector(this.supplyCollateralOnBehalfNoRevert.selector, 100); - _weightSelector(this.withdrawCollateralOnBehalfNoRevert.selector, 50); - _weightSelector(this.liquidateSeizedAssetsNoRevert.selector, 5); - _weightSelector(this.liquidateRepaidSharesNoRevert.selector, 5); - super.setUp(); allMarketParams.push(marketParams); @@ -191,12 +175,6 @@ contract MorphoInvariantTest is InvariantTest { /* HANDLERS */ - function setPrice(uint256 price) external { - price = bound(price, MIN_PRICE, MAX_PRICE); - - oracle.setPrice(price); - } - function setFeeNoRevert(uint256 marketSeed, uint256 newFee) external { MarketParams memory _marketParams = _randomMarket(marketSeed); Id _id = _marketParams.id(); @@ -316,10 +294,10 @@ contract MorphoInvariantTest is InvariantTest { _withdrawCollateral(_marketParams, assets, onBehalf, receiver); } - function liquidateSeizedAssetsNoRevert(uint256 marketSeed, uint256 seizedAssets, uint256 onBehalfSeed) external { + function liquidateSeizedAssetsNoRevert(uint256 marketSeed, uint256 seizedAssets, uint256 borrowerSeed) external { MarketParams memory _marketParams = _randomMarket(marketSeed); - address borrower = _randomUnhealthyBorrower(targetSenders(), _marketParams, onBehalfSeed); + address borrower = _randomUnhealthyBorrower(targetSenders(), _marketParams, borrowerSeed); if (borrower == address(0)) return; seizedAssets = _boundLiquidateSeizedAssets(_marketParams, borrower, seizedAssets); @@ -328,10 +306,10 @@ contract MorphoInvariantTest is InvariantTest { _liquidateSeizedAssets(_marketParams, borrower, seizedAssets); } - function liquidateRepaidSharesNoRevert(uint256 marketSeed, uint256 repaidShares, uint256 onBehalfSeed) external { + function liquidateRepaidSharesNoRevert(uint256 marketSeed, uint256 repaidShares, uint256 borrowerSeed) external { MarketParams memory _marketParams = _randomMarket(marketSeed); - address borrower = _randomUnhealthyBorrower(targetSenders(), _marketParams, onBehalfSeed); + address borrower = _randomUnhealthyBorrower(targetSenders(), _marketParams, borrowerSeed); if (borrower == address(0)) return; repaidShares = _boundLiquidateRepaidShares(_marketParams, borrower, repaidShares); @@ -339,79 +317,4 @@ contract MorphoInvariantTest is InvariantTest { _liquidateRepaidShares(_marketParams, borrower, repaidShares); } - - /* INVARIANTS */ - - function invariantSupplyShares() public { - address[] memory users = targetSenders(); - - for (uint256 i; i < allMarketParams.length; ++i) { - MarketParams memory _marketParams = allMarketParams[i]; - Id _id = _marketParams.id(); - - uint256 sumSupplyShares = morpho.supplyShares(_id, FEE_RECIPIENT); - for (uint256 j; j < users.length; ++j) { - sumSupplyShares += morpho.supplyShares(_id, users[j]); - } - - assertEq(sumSupplyShares, morpho.totalSupplyShares(_id), vm.toString(_marketParams.lltv)); - } - } - - function invariantBorrowShares() public { - address[] memory users = targetSenders(); - - for (uint256 i; i < allMarketParams.length; ++i) { - MarketParams memory _marketParams = allMarketParams[i]; - Id _id = _marketParams.id(); - - uint256 sumBorrowShares; - for (uint256 j; j < users.length; ++j) { - sumBorrowShares += morpho.borrowShares(_id, users[j]); - } - - assertEq(sumBorrowShares, morpho.totalBorrowShares(_id), vm.toString(_marketParams.lltv)); - } - } - - function invariantTotalSupplyGeTotalBorrow() public { - for (uint256 i; i < allMarketParams.length; ++i) { - MarketParams memory _marketParams = allMarketParams[i]; - Id _id = _marketParams.id(); - - assertGe(morpho.totalSupplyAssets(_id), morpho.totalBorrowAssets(_id)); - } - } - - function invariantMorphoBalance() public { - for (uint256 i; i < allMarketParams.length; ++i) { - MarketParams memory _marketParams = allMarketParams[i]; - Id _id = _marketParams.id(); - - assertGe( - loanToken.balanceOf(address(morpho)) + morpho.totalBorrowAssets(_id), morpho.totalSupplyAssets(_id) - ); - } - } - - function invariantBadDebt() public { - address[] memory users = targetSenders(); - - for (uint256 i; i < allMarketParams.length; ++i) { - MarketParams memory _marketParams = allMarketParams[i]; - Id _id = _marketParams.id(); - - for (uint256 j; j < users.length; ++j) { - address user = users[j]; - - if (morpho.collateral(_id, user) == 0) { - assertEq( - morpho.borrowShares(_id, user), - 0, - string.concat(vm.toString(_marketParams.lltv), ":", vm.toString(user)) - ); - } - } - } - } } diff --git a/test/forge/invariant/MorphoDynamicInvariantTest.sol b/test/forge/invariant/MorphoDynamicInvariantTest.sol new file mode 100644 index 000000000..84355d77f --- /dev/null +++ b/test/forge/invariant/MorphoDynamicInvariantTest.sol @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import "./BaseMorphoInvariantTest.sol"; + +contract MorphoDynamicInvariantTest is BaseMorphoInvariantTest { + using MathLib for uint256; + using SharesMathLib for uint256; + using MorphoLib for IMorpho; + using MorphoBalancesLib for IMorpho; + using MarketParamsLib for MarketParams; + + uint256 internal immutable MIN_PRICE = ORACLE_PRICE_SCALE / 10; + uint256 internal immutable MAX_PRICE = ORACLE_PRICE_SCALE * 10; + + function setUp() public virtual override { + _weightSelector(this.supplyAssetsOnBehalfNoRevert.selector, 15); + _weightSelector(this.supplySharesOnBehalfNoRevert.selector, 5); + _weightSelector(this.withdrawAssetsOnBehalfNoRevert.selector, 10); + _weightSelector(this.borrowAssetsOnBehalfNoRevert.selector, 15); + _weightSelector(this.repayAssetsOnBehalfNoRevert.selector, 10); + _weightSelector(this.repaySharesOnBehalfNoRevert.selector, 10); + _weightSelector(this.supplyCollateralOnBehalfNoRevert.selector, 15); + _weightSelector(this.withdrawCollateralOnBehalfNoRevert.selector, 10); + _weightSelector(this.liquidateSeizedAssetsNoRevert.selector, 2); + _weightSelector(this.liquidateRepaidSharesNoRevert.selector, 2); + _weightSelector(this.setFeeNoRevert.selector, 1); + _weightSelector(this.setPrice.selector, 5); + _weightSelector(this.mine.selector, 100); + + super.setUp(); + } + + /* HANDLERS */ + + function setPrice(uint256 price) external { + price = bound(price, MIN_PRICE, MAX_PRICE); + + oracle.setPrice(price); + } + + /* INVARIANTS */ + + function invariantSupplyShares() public { + address[] memory users = targetSenders(); + + for (uint256 i; i < allMarketParams.length; ++i) { + MarketParams memory _marketParams = allMarketParams[i]; + Id _id = _marketParams.id(); + + uint256 sumSupplyShares = morpho.supplyShares(_id, FEE_RECIPIENT); + for (uint256 j; j < users.length; ++j) { + sumSupplyShares += morpho.supplyShares(_id, users[j]); + } + + assertEq(sumSupplyShares, morpho.totalSupplyShares(_id), vm.toString(_marketParams.lltv)); + } + } + + function invariantBorrowShares() public { + address[] memory users = targetSenders(); + + for (uint256 i; i < allMarketParams.length; ++i) { + MarketParams memory _marketParams = allMarketParams[i]; + Id _id = _marketParams.id(); + + uint256 sumBorrowShares; + for (uint256 j; j < users.length; ++j) { + sumBorrowShares += morpho.borrowShares(_id, users[j]); + } + + assertEq(sumBorrowShares, morpho.totalBorrowShares(_id), vm.toString(_marketParams.lltv)); + } + } + + function invariantTotalSupplyGeTotalBorrow() public { + for (uint256 i; i < allMarketParams.length; ++i) { + MarketParams memory _marketParams = allMarketParams[i]; + Id _id = _marketParams.id(); + + assertGe(morpho.totalSupplyAssets(_id), morpho.totalBorrowAssets(_id)); + } + } + + function invariantMorphoBalance() public { + for (uint256 i; i < allMarketParams.length; ++i) { + MarketParams memory _marketParams = allMarketParams[i]; + Id _id = _marketParams.id(); + + assertGe( + loanToken.balanceOf(address(morpho)) + morpho.totalBorrowAssets(_id), morpho.totalSupplyAssets(_id) + ); + } + } + + function invariantBadDebt() public { + address[] memory users = targetSenders(); + + for (uint256 i; i < allMarketParams.length; ++i) { + MarketParams memory _marketParams = allMarketParams[i]; + Id _id = _marketParams.id(); + + for (uint256 j; j < users.length; ++j) { + address user = users[j]; + + if (morpho.collateral(_id, user) == 0) { + assertEq( + morpho.borrowShares(_id, user), + 0, + string.concat(vm.toString(_marketParams.lltv), ":", vm.toString(user)) + ); + } + } + } + } +} diff --git a/test/forge/invariant/MorphoStaticInvariantTest.sol b/test/forge/invariant/MorphoStaticInvariantTest.sol new file mode 100644 index 000000000..d586fc3b2 --- /dev/null +++ b/test/forge/invariant/MorphoStaticInvariantTest.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import "./BaseMorphoInvariantTest.sol"; + +contract MorphoStaticInvariantTest is BaseMorphoInvariantTest { + using MathLib for uint256; + using SharesMathLib for uint256; + using MorphoLib for IMorpho; + using MorphoBalancesLib for IMorpho; + using MarketParamsLib for MarketParams; + + function setUp() public virtual override { + _weightSelector(this.supplyAssetsOnBehalfNoRevert.selector, 12); + _weightSelector(this.supplySharesOnBehalfNoRevert.selector, 5); + _weightSelector(this.withdrawAssetsOnBehalfNoRevert.selector, 12); + _weightSelector(this.borrowAssetsOnBehalfNoRevert.selector, 17); + _weightSelector(this.repayAssetsOnBehalfNoRevert.selector, 12); + _weightSelector(this.repaySharesOnBehalfNoRevert.selector, 10); + _weightSelector(this.supplyCollateralOnBehalfNoRevert.selector, 15); + _weightSelector(this.withdrawCollateralOnBehalfNoRevert.selector, 10); + _weightSelector(this.setFeeNoRevert.selector, 2); + + super.setUp(); + } + + /* INVARIANTS */ + + function invariantHealthy() public { + address[] memory users = targetSenders(); + + for (uint256 i; i < allMarketParams.length; ++i) { + MarketParams memory _marketParams = allMarketParams[i]; + + for (uint256 j; j < users.length; ++j) { + assertTrue(_isHealthy(_marketParams, users[j])); + } + } + } +}