Skip to content

Commit

Permalink
Merge pull request #706 from morpho-org/ci/fix-invariant
Browse files Browse the repository at this point in the history
Fix invariants in CI
  • Loading branch information
MathisGD authored Nov 25, 2024
2 parents 12b8a45 + a3767dd commit abeb928
Show file tree
Hide file tree
Showing 7 changed files with 147 additions and 118 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/foundry.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
10 changes: 5 additions & 5 deletions test/forge/BaseTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}

Expand Down
10 changes: 1 addition & 9 deletions test/forge/InvariantTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ contract InvariantTest is BaseTest {

_targetSenders();

_weightSelector(this.mine.selector, 100);

targetContract(address(this));
targetSelector(FuzzSelector({addr: address(this), selectors: selectors}));
}
Expand Down Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();

Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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);
Expand All @@ -333,90 +320,15 @@ 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);
if (repaidShares == 0) return;

_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))
);
}
}
}
}
}
105 changes: 105 additions & 0 deletions test/forge/invariant/DynamicInvariantTest.sol
Original file line number Diff line number Diff line change
@@ -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))
);
}
}
}
}
}
20 changes: 20 additions & 0 deletions test/forge/invariant/StaticInvariantTest.sol
Original file line number Diff line number Diff line change
@@ -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]));
}
}
}
}

0 comments on commit abeb928

Please sign in to comment.