diff --git a/.github/workflows/foundry.yml b/.github/workflows/foundry.yml index 99b49308..11296558 100644 --- a/.github/workflows/foundry.yml +++ b/.github/workflows/foundry.yml @@ -21,12 +21,12 @@ jobs: - type: "slow" fuzz-runs: 10000 max-test-rejects: 500000 - invariant-runs: 0 + invariant-runs: 32 invariant-depth: 512 - type: "fast" fuzz-runs: 256 max-test-rejects: 65536 - invariant-runs: 0 + invariant-runs: 16 invariant-depth: 256 runs-on: ubuntu-latest diff --git a/foundry.toml b/foundry.toml index 9b7733f1..4206698f 100644 --- a/foundry.toml +++ b/foundry.toml @@ -6,7 +6,7 @@ via-ir = true optimizer_runs = 999999 # Etherscan does not support verifying contracts with more optimization runs. [profile.default.invariant] -runs = 8 +runs = 16 depth = 256 fail_on_revert = true diff --git a/test/forge/BaseTest.sol b/test/forge/BaseTest.sol index 13a23237..89906a26 100644 --- a/test/forge/BaseTest.sol +++ b/test/forge/BaseTest.sol @@ -349,14 +349,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 totalBorrowAssets, uint256 totalBorrowShares) = morpho.expectedMarketBalances(marketParams); + 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); + uint256 borrowShares = morpho.borrowShares(_id, borrower); return bound(repaidShares, 0, Math.min(borrowShares, maxRepaidShares)); } diff --git a/test/forge/InvariantTest.sol b/test/forge/InvariantTest.sol index d1e0d68a..d34839eb 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})); } @@ -49,16 +47,10 @@ contract InvariantTest is BaseTest { vm.stopPrank(); } - function _weightSelector(bytes4 selector, uint256 weight) internal { - for (uint256 i; i < weight; ++i) { - selectors.push(selector); - } - } - /* 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/BaseInvariantTest.sol similarity index 74% rename from test/forge/invariant/MorphoInvariantTest.sol rename to test/forge/invariant/BaseInvariantTest.sol index 6b4866ca..d5e920f5 100644 --- a/test/forge/invariant/MorphoInvariantTest.sol +++ b/test/forge/invariant/BaseInvariantTest.sol @@ -3,16 +3,13 @@ pragma solidity ^0.8.0; import "../InvariantTest.sol"; -contract MorphoInvariantTest is InvariantTest { +contract BaseInvariantTest 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,18 +19,14 @@ 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); + selectors.push(this.supplyAssetsOnBehalfNoRevert.selector); + selectors.push(this.supplySharesOnBehalfNoRevert.selector); + selectors.push(this.withdrawAssetsOnBehalfNoRevert.selector); + selectors.push(this.borrowAssetsOnBehalfNoRevert.selector); + selectors.push(this.repayAssetsOnBehalfNoRevert.selector); + selectors.push(this.repaySharesOnBehalfNoRevert.selector); + selectors.push(this.supplyCollateralOnBehalfNoRevert.selector); + selectors.push(this.withdrawCollateralOnBehalfNoRevert.selector); super.setUp(); @@ -196,12 +189,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(); @@ -321,10 +308,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); @@ -333,10 +320,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); @@ -344,79 +331,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/DynamicInvariantTest.sol b/test/forge/invariant/DynamicInvariantTest.sol new file mode 100644 index 00000000..fe11c1bd --- /dev/null +++ b/test/forge/invariant/DynamicInvariantTest.sol @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import "./BaseInvariantTest.sol"; + +contract DynamicInvariantTest is BaseInvariantTest { + using MorphoLib 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 { + selectors.push(this.liquidateSeizedAssetsNoRevert.selector); + selectors.push(this.liquidateRepaidSharesNoRevert.selector); + selectors.push(this.setFeeNoRevert.selector); + selectors.push(this.setPrice.selector); + selectors.push(this.mine.selector); + + 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/StaticInvariantTest.sol b/test/forge/invariant/StaticInvariantTest.sol new file mode 100644 index 00000000..9590f0e0 --- /dev/null +++ b/test/forge/invariant/StaticInvariantTest.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import "./BaseInvariantTest.sol"; + +contract StaticInvariantTest is BaseInvariantTest { + /* 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])); + } + } + } +}