Skip to content

Commit abeb928

Browse files
authored
Merge pull request #706 from morpho-org/ci/fix-invariant
Fix invariants in CI
2 parents 12b8a45 + a3767dd commit abeb928

File tree

7 files changed

+147
-118
lines changed

7 files changed

+147
-118
lines changed

.github/workflows/foundry.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,12 @@ jobs:
2121
- type: "slow"
2222
fuzz-runs: 10000
2323
max-test-rejects: 500000
24-
invariant-runs: 0
24+
invariant-runs: 32
2525
invariant-depth: 512
2626
- type: "fast"
2727
fuzz-runs: 256
2828
max-test-rejects: 65536
29-
invariant-runs: 0
29+
invariant-runs: 16
3030
invariant-depth: 256
3131

3232
runs-on: ubuntu-latest

foundry.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ via-ir = true
66
optimizer_runs = 999999 # Etherscan does not support verifying contracts with more optimization runs.
77

88
[profile.default.invariant]
9-
runs = 8
9+
runs = 16
1010
depth = 256
1111
fail_on_revert = true
1212

test/forge/BaseTest.sol

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -349,14 +349,14 @@ contract BaseTest is Test {
349349
{
350350
Id _id = _marketParams.id();
351351

352-
uint256 borrowShares = morpho.borrowShares(_id, borrower);
353352
uint256 collateralPrice = IOracle(_marketParams.oracle).price();
354-
uint256 maxRepaidAssets = morpho.collateral(_id, borrower).mulDivUp(collateralPrice, ORACLE_PRICE_SCALE).wDivUp(
355-
_liquidationIncentiveFactor(_marketParams.lltv)
356-
);
357-
(,, uint256 totalBorrowAssets, uint256 totalBorrowShares) = morpho.expectedMarketBalances(marketParams);
353+
uint256 maxRepaidAssets = morpho.collateral(_id, borrower).mulDivDown(collateralPrice, ORACLE_PRICE_SCALE)
354+
.wDivDown(_liquidationIncentiveFactor(_marketParams.lltv));
355+
356+
(,, uint256 totalBorrowAssets, uint256 totalBorrowShares) = morpho.expectedMarketBalances(_marketParams);
358357
uint256 maxRepaidShares = maxRepaidAssets.toSharesDown(totalBorrowAssets, totalBorrowShares);
359358

359+
uint256 borrowShares = morpho.borrowShares(_id, borrower);
360360
return bound(repaidShares, 0, Math.min(borrowShares, maxRepaidShares));
361361
}
362362

test/forge/InvariantTest.sol

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@ contract InvariantTest is BaseTest {
1717

1818
_targetSenders();
1919

20-
_weightSelector(this.mine.selector, 100);
21-
2220
targetContract(address(this));
2321
targetSelector(FuzzSelector({addr: address(this), selectors: selectors}));
2422
}
@@ -49,16 +47,10 @@ contract InvariantTest is BaseTest {
4947
vm.stopPrank();
5048
}
5149

52-
function _weightSelector(bytes4 selector, uint256 weight) internal {
53-
for (uint256 i; i < weight; ++i) {
54-
selectors.push(selector);
55-
}
56-
}
57-
5850
/* HANDLERS */
5951

6052
function mine(uint256 blocks) external {
61-
blocks = bound(blocks, 1, 50_400);
53+
blocks = bound(blocks, 1, 1 days / BLOCK_TIME);
6254

6355
_forward(blocks);
6456
}

test/forge/invariant/MorphoInvariantTest.sol renamed to test/forge/invariant/BaseInvariantTest.sol

Lines changed: 13 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,13 @@ pragma solidity ^0.8.0;
33

44
import "../InvariantTest.sol";
55

6-
contract MorphoInvariantTest is InvariantTest {
6+
contract BaseInvariantTest is InvariantTest {
77
using MathLib for uint256;
88
using SharesMathLib for uint256;
99
using MorphoLib for IMorpho;
1010
using MorphoBalancesLib for IMorpho;
1111
using MarketParamsLib for MarketParams;
1212

13-
uint256 internal immutable MIN_PRICE = ORACLE_PRICE_SCALE / 10;
14-
uint256 internal immutable MAX_PRICE = ORACLE_PRICE_SCALE * 10;
15-
1613
address internal immutable USER;
1714

1815
MarketParams[] internal allMarketParams;
@@ -22,18 +19,14 @@ contract MorphoInvariantTest is InvariantTest {
2219
}
2320

2421
function setUp() public virtual override {
25-
_weightSelector(this.setPrice.selector, 10);
26-
_weightSelector(this.setFeeNoRevert.selector, 5);
27-
_weightSelector(this.supplyAssetsOnBehalfNoRevert.selector, 100);
28-
_weightSelector(this.supplySharesOnBehalfNoRevert.selector, 100);
29-
_weightSelector(this.withdrawAssetsOnBehalfNoRevert.selector, 50);
30-
_weightSelector(this.borrowAssetsOnBehalfNoRevert.selector, 75);
31-
_weightSelector(this.repayAssetsOnBehalfNoRevert.selector, 35);
32-
_weightSelector(this.repaySharesOnBehalfNoRevert.selector, 35);
33-
_weightSelector(this.supplyCollateralOnBehalfNoRevert.selector, 100);
34-
_weightSelector(this.withdrawCollateralOnBehalfNoRevert.selector, 50);
35-
_weightSelector(this.liquidateSeizedAssetsNoRevert.selector, 5);
36-
_weightSelector(this.liquidateRepaidSharesNoRevert.selector, 5);
22+
selectors.push(this.supplyAssetsOnBehalfNoRevert.selector);
23+
selectors.push(this.supplySharesOnBehalfNoRevert.selector);
24+
selectors.push(this.withdrawAssetsOnBehalfNoRevert.selector);
25+
selectors.push(this.borrowAssetsOnBehalfNoRevert.selector);
26+
selectors.push(this.repayAssetsOnBehalfNoRevert.selector);
27+
selectors.push(this.repaySharesOnBehalfNoRevert.selector);
28+
selectors.push(this.supplyCollateralOnBehalfNoRevert.selector);
29+
selectors.push(this.withdrawCollateralOnBehalfNoRevert.selector);
3730

3831
super.setUp();
3932

@@ -196,12 +189,6 @@ contract MorphoInvariantTest is InvariantTest {
196189

197190
/* HANDLERS */
198191

199-
function setPrice(uint256 price) external {
200-
price = bound(price, MIN_PRICE, MAX_PRICE);
201-
202-
oracle.setPrice(price);
203-
}
204-
205192
function setFeeNoRevert(uint256 marketSeed, uint256 newFee) external {
206193
MarketParams memory _marketParams = _randomMarket(marketSeed);
207194
Id _id = _marketParams.id();
@@ -321,10 +308,10 @@ contract MorphoInvariantTest is InvariantTest {
321308
_withdrawCollateral(_marketParams, assets, onBehalf, receiver);
322309
}
323310

324-
function liquidateSeizedAssetsNoRevert(uint256 marketSeed, uint256 seizedAssets, uint256 onBehalfSeed) external {
311+
function liquidateSeizedAssetsNoRevert(uint256 marketSeed, uint256 seizedAssets, uint256 borrowerSeed) external {
325312
MarketParams memory _marketParams = _randomMarket(marketSeed);
326313

327-
address borrower = _randomUnhealthyBorrower(targetSenders(), _marketParams, onBehalfSeed);
314+
address borrower = _randomUnhealthyBorrower(targetSenders(), _marketParams, borrowerSeed);
328315
if (borrower == address(0)) return;
329316

330317
seizedAssets = _boundLiquidateSeizedAssets(_marketParams, borrower, seizedAssets);
@@ -333,90 +320,15 @@ contract MorphoInvariantTest is InvariantTest {
333320
_liquidateSeizedAssets(_marketParams, borrower, seizedAssets);
334321
}
335322

336-
function liquidateRepaidSharesNoRevert(uint256 marketSeed, uint256 repaidShares, uint256 onBehalfSeed) external {
323+
function liquidateRepaidSharesNoRevert(uint256 marketSeed, uint256 repaidShares, uint256 borrowerSeed) external {
337324
MarketParams memory _marketParams = _randomMarket(marketSeed);
338325

339-
address borrower = _randomUnhealthyBorrower(targetSenders(), _marketParams, onBehalfSeed);
326+
address borrower = _randomUnhealthyBorrower(targetSenders(), _marketParams, borrowerSeed);
340327
if (borrower == address(0)) return;
341328

342329
repaidShares = _boundLiquidateRepaidShares(_marketParams, borrower, repaidShares);
343330
if (repaidShares == 0) return;
344331

345332
_liquidateRepaidShares(_marketParams, borrower, repaidShares);
346333
}
347-
348-
/* INVARIANTS */
349-
350-
function invariantSupplyShares() public {
351-
address[] memory users = targetSenders();
352-
353-
for (uint256 i; i < allMarketParams.length; ++i) {
354-
MarketParams memory _marketParams = allMarketParams[i];
355-
Id _id = _marketParams.id();
356-
357-
uint256 sumSupplyShares = morpho.supplyShares(_id, FEE_RECIPIENT);
358-
for (uint256 j; j < users.length; ++j) {
359-
sumSupplyShares += morpho.supplyShares(_id, users[j]);
360-
}
361-
362-
assertEq(sumSupplyShares, morpho.totalSupplyShares(_id), vm.toString(_marketParams.lltv));
363-
}
364-
}
365-
366-
function invariantBorrowShares() public {
367-
address[] memory users = targetSenders();
368-
369-
for (uint256 i; i < allMarketParams.length; ++i) {
370-
MarketParams memory _marketParams = allMarketParams[i];
371-
Id _id = _marketParams.id();
372-
373-
uint256 sumBorrowShares;
374-
for (uint256 j; j < users.length; ++j) {
375-
sumBorrowShares += morpho.borrowShares(_id, users[j]);
376-
}
377-
378-
assertEq(sumBorrowShares, morpho.totalBorrowShares(_id), vm.toString(_marketParams.lltv));
379-
}
380-
}
381-
382-
function invariantTotalSupplyGeTotalBorrow() public {
383-
for (uint256 i; i < allMarketParams.length; ++i) {
384-
MarketParams memory _marketParams = allMarketParams[i];
385-
Id _id = _marketParams.id();
386-
387-
assertGe(morpho.totalSupplyAssets(_id), morpho.totalBorrowAssets(_id));
388-
}
389-
}
390-
391-
function invariantMorphoBalance() public {
392-
for (uint256 i; i < allMarketParams.length; ++i) {
393-
MarketParams memory _marketParams = allMarketParams[i];
394-
Id _id = _marketParams.id();
395-
396-
assertGe(
397-
loanToken.balanceOf(address(morpho)) + morpho.totalBorrowAssets(_id), morpho.totalSupplyAssets(_id)
398-
);
399-
}
400-
}
401-
402-
function invariantBadDebt() public {
403-
address[] memory users = targetSenders();
404-
405-
for (uint256 i; i < allMarketParams.length; ++i) {
406-
MarketParams memory _marketParams = allMarketParams[i];
407-
Id _id = _marketParams.id();
408-
409-
for (uint256 j; j < users.length; ++j) {
410-
address user = users[j];
411-
412-
if (morpho.collateral(_id, user) == 0) {
413-
assertEq(
414-
morpho.borrowShares(_id, user),
415-
0,
416-
string.concat(vm.toString(_marketParams.lltv), ":", vm.toString(user))
417-
);
418-
}
419-
}
420-
}
421-
}
422334
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
// SPDX-License-Identifier: GPL-2.0-or-later
2+
pragma solidity ^0.8.0;
3+
4+
import "./BaseInvariantTest.sol";
5+
6+
contract DynamicInvariantTest is BaseInvariantTest {
7+
using MorphoLib for IMorpho;
8+
using MarketParamsLib for MarketParams;
9+
10+
uint256 internal immutable MIN_PRICE = ORACLE_PRICE_SCALE / 10;
11+
uint256 internal immutable MAX_PRICE = ORACLE_PRICE_SCALE * 10;
12+
13+
function setUp() public virtual override {
14+
selectors.push(this.liquidateSeizedAssetsNoRevert.selector);
15+
selectors.push(this.liquidateRepaidSharesNoRevert.selector);
16+
selectors.push(this.setFeeNoRevert.selector);
17+
selectors.push(this.setPrice.selector);
18+
selectors.push(this.mine.selector);
19+
20+
super.setUp();
21+
}
22+
23+
/* HANDLERS */
24+
25+
function setPrice(uint256 price) external {
26+
price = bound(price, MIN_PRICE, MAX_PRICE);
27+
28+
oracle.setPrice(price);
29+
}
30+
31+
/* INVARIANTS */
32+
33+
function invariantSupplyShares() public {
34+
address[] memory users = targetSenders();
35+
36+
for (uint256 i; i < allMarketParams.length; ++i) {
37+
MarketParams memory _marketParams = allMarketParams[i];
38+
Id _id = _marketParams.id();
39+
40+
uint256 sumSupplyShares = morpho.supplyShares(_id, FEE_RECIPIENT);
41+
for (uint256 j; j < users.length; ++j) {
42+
sumSupplyShares += morpho.supplyShares(_id, users[j]);
43+
}
44+
45+
assertEq(sumSupplyShares, morpho.totalSupplyShares(_id), vm.toString(_marketParams.lltv));
46+
}
47+
}
48+
49+
function invariantBorrowShares() public {
50+
address[] memory users = targetSenders();
51+
52+
for (uint256 i; i < allMarketParams.length; ++i) {
53+
MarketParams memory _marketParams = allMarketParams[i];
54+
Id _id = _marketParams.id();
55+
56+
uint256 sumBorrowShares;
57+
for (uint256 j; j < users.length; ++j) {
58+
sumBorrowShares += morpho.borrowShares(_id, users[j]);
59+
}
60+
61+
assertEq(sumBorrowShares, morpho.totalBorrowShares(_id), vm.toString(_marketParams.lltv));
62+
}
63+
}
64+
65+
function invariantTotalSupplyGeTotalBorrow() public {
66+
for (uint256 i; i < allMarketParams.length; ++i) {
67+
MarketParams memory _marketParams = allMarketParams[i];
68+
Id _id = _marketParams.id();
69+
70+
assertGe(morpho.totalSupplyAssets(_id), morpho.totalBorrowAssets(_id));
71+
}
72+
}
73+
74+
function invariantMorphoBalance() public {
75+
for (uint256 i; i < allMarketParams.length; ++i) {
76+
MarketParams memory _marketParams = allMarketParams[i];
77+
Id _id = _marketParams.id();
78+
79+
assertGe(
80+
loanToken.balanceOf(address(morpho)) + morpho.totalBorrowAssets(_id), morpho.totalSupplyAssets(_id)
81+
);
82+
}
83+
}
84+
85+
function invariantBadDebt() public {
86+
address[] memory users = targetSenders();
87+
88+
for (uint256 i; i < allMarketParams.length; ++i) {
89+
MarketParams memory _marketParams = allMarketParams[i];
90+
Id _id = _marketParams.id();
91+
92+
for (uint256 j; j < users.length; ++j) {
93+
address user = users[j];
94+
95+
if (morpho.collateral(_id, user) == 0) {
96+
assertEq(
97+
morpho.borrowShares(_id, user),
98+
0,
99+
string.concat(vm.toString(_marketParams.lltv), ":", vm.toString(user))
100+
);
101+
}
102+
}
103+
}
104+
}
105+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// SPDX-License-Identifier: GPL-2.0-or-later
2+
pragma solidity ^0.8.0;
3+
4+
import "./BaseInvariantTest.sol";
5+
6+
contract StaticInvariantTest is BaseInvariantTest {
7+
/* INVARIANTS */
8+
9+
function invariantHealthy() public {
10+
address[] memory users = targetSenders();
11+
12+
for (uint256 i; i < allMarketParams.length; ++i) {
13+
MarketParams memory _marketParams = allMarketParams[i];
14+
15+
for (uint256 j; j < users.length; ++j) {
16+
assertTrue(_isHealthy(_marketParams, users[j]));
17+
}
18+
}
19+
}
20+
}

0 commit comments

Comments
 (0)