Skip to content

Commit fcb190b

Browse files
authored
Merge pull request #684 from morpho-org/certora/stay-healthy-liquidate
[Certora] Stay healthy liquidate
2 parents dee5cfc + 90c3c8e commit fcb190b

File tree

6 files changed

+143
-31
lines changed

6 files changed

+143
-31
lines changed

.github/workflows/certora.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ jobs:
2626
- Liveness
2727
- Reentrancy
2828
- Reverts
29+
- StayHealthy
2930
- Transfer
3031

3132
steps:

certora/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,12 +250,13 @@ The [`certora/specs`](specs) folder contains the following files:
250250
- [`ExactMath.spec`](specs/ExactMath.spec) checks precise properties when taking into account exact multiplication and division.
251251
Notably, this file specifies that using supply and withdraw in the same block cannot yield more funds than at the start.
252252
- [`Health.spec`](specs/Health.spec) checks properties about the health of the positions.
253-
Notably, functions cannot render an account unhealthy, and debt positions always have some collateral (thanks to the bad debt realization mechanism).
253+
Notably, debt positions always have some collateral thanks to the bad debt realization mechanism.
254254
- [`LibSummary.spec`](specs/LibSummary.spec) checks the summarization of the library functions that are used in other specification files.
255255
- [`Liveness.spec`](specs/Liveness.spec) checks that main functions change the owner of funds and the amount of shares as expected, and that it's always possible to exit a position.
256256
- [`ExchangeRate.spec`](specs/ExchangeRate.spec) checks that the exchange rate between shares and assets evolves predictably over time.
257257
- [`Reentrancy.spec`](specs/Reentrancy.spec) checks that the contract is immune to a particular class of reentrancy issues.
258258
- [`Reverts.spec`](specs/Reverts.spec) checks the condition for reverts and that inputs are correctly validated.
259+
- [`StayHealthy.spec`](specs/Health.spec) checks that functions cannot render an account unhealthy.
259260
- [`Transfer.spec`](specs/Transfer.spec) checks the summarization of the safe transfer library functions that are used in other specification files.
260261

261262
The [`certora/confs`](confs) folder contains a configuration file for each corresponding specification file.

certora/confs/AccrueInterest.conf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
"verify": "MorphoHarness:certora/specs/AccrueInterest.spec",
77
"prover_args": [
88
"-depth 3",
9-
"-smt_hashingScheme plaininjectivity",
109
"-mediumTimeout 30",
10+
"-smt_hashingScheme plaininjectivity",
1111
],
1212
"rule_sanity": "basic",
1313
"server": "production",

certora/confs/StayHealthy.conf

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"files": [
3+
"certora/harness/MorphoHarness.sol",
4+
"src/mocks/OracleMock.sol",
5+
],
6+
"msg": "Morpho Blue Stay Healthy",
7+
"process": "emv",
8+
"prover_args": [
9+
"-depth 5",
10+
"-mediumTimeout 5",
11+
"-timeout 3600",
12+
"-smt_hashingScheme plaininjectivity"
13+
],
14+
"rule_sanity": "basic",
15+
"server": "production",
16+
"solc": "solc-0.8.19",
17+
"verify": "MorphoHarness:certora/specs/StayHealthy.spec"
18+
}

certora/specs/Health.spec

Lines changed: 3 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22
methods {
33
function extSloads(bytes32[]) external returns bytes32[] => NONDET DELETE;
44

5+
function totalBorrowAssets(MorphoHarness.Id) external returns uint256 envfree;
56
function totalBorrowShares(MorphoHarness.Id) external returns uint256 envfree;
7+
function virtualTotalBorrowAssets(MorphoHarness.Id) external returns uint256 envfree;
8+
function virtualTotalBorrowShares(MorphoHarness.Id) external returns uint256 envfree;
69
function lastUpdate(MorphoHarness.Id) external returns uint256 envfree;
710
function borrowShares(MorphoHarness.Id, address) external returns uint256 envfree;
811
function collateral(MorphoHarness.Id, address) external returns uint256 envfree;
@@ -46,35 +49,6 @@ function summaryMin(uint256 a, uint256 b) returns uint256 {
4649
return a < b ? a : b;
4750
}
4851

49-
// Check that without accruing interest, no interaction can put an healthy account into an unhealthy one.
50-
// The liquidate function times out in this rule, but has been checked separately.
51-
rule stayHealthy(env e, method f, calldataarg data)
52-
filtered {
53-
f -> !f.isView &&
54-
f.selector != sig:liquidate(MorphoHarness.MarketParams, address, uint256, uint256, bytes).selector
55-
}
56-
{
57-
MorphoHarness.MarketParams marketParams;
58-
MorphoHarness.Id id = libId(marketParams);
59-
address user;
60-
61-
// Assume that the position is healthy before the interaction.
62-
require isHealthy(marketParams, user);
63-
// Safe require because of the invariants onlyEnabledLltv and lltvSmallerThanWad in ConsistentState.spec.
64-
require marketParams.lltv < 10^18;
65-
// Assumption to ensure that no interest is accumulated.
66-
require lastUpdate(id) == e.block.timestamp;
67-
68-
priceChanged = false;
69-
f(e, data);
70-
71-
// Safe require because of the invariant sumBorrowSharesCorrect.
72-
require borrowShares(id, user) <= totalBorrowShares(id);
73-
74-
bool stillHealthy = isHealthy(marketParams, user);
75-
assert !priceChanged => stillHealthy;
76-
}
77-
7852
// Check that users cannot lose collateral by unauthorized parties except in case of an unhealthy position.
7953
rule healthyUserCannotLoseCollateral(env e, method f, calldataarg data)
8054
filtered { f -> !f.isView }

certora/specs/StayHealthy.spec

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
// SPDX-License-Identifier: GPL-2.0-or-later
2+
import "Health.spec";
3+
4+
// Check that without accruing interest, no interaction can put an healthy account into an unhealthy one.
5+
// The liquidate function times out in this rule, but has been checked separately.
6+
rule stayHealthy(env e, method f, calldataarg data)
7+
filtered {
8+
f -> !f.isView &&
9+
f.selector != sig:liquidate(MorphoHarness.MarketParams, address, uint256, uint256, bytes).selector
10+
}
11+
{
12+
MorphoHarness.MarketParams marketParams;
13+
MorphoHarness.Id id = libId(marketParams);
14+
address user;
15+
16+
// Assume that the position is healthy before the interaction.
17+
require isHealthy(marketParams, user);
18+
// Safe require because of the invariants onlyEnabledLltv and lltvSmallerThanWad in ConsistentState.spec.
19+
require marketParams.lltv < 10^18;
20+
// Assumption to ensure that no interest is accumulated.
21+
require lastUpdate(id) == e.block.timestamp;
22+
23+
f(e, data);
24+
25+
// Safe require because of the invariant sumBorrowSharesCorrect.
26+
require borrowShares(id, user) <= totalBorrowShares(id);
27+
28+
bool stillHealthy = isHealthy(marketParams, user);
29+
assert !priceChanged => stillHealthy;
30+
}
31+
32+
// The liquidate case for the stayHealthy rule, assuming no bad debt realization, otherwise it times out.
33+
// This particular rule makes the following assumptions:
34+
// - the market of the liquidation is the market of the user, see the *DifferentMarkets rule,
35+
// - there is still some borrow on the market after liquidation, see the *LastBorrow rule.
36+
rule stayHealthyLiquidate(env e, MorphoHarness.MarketParams marketParams, address borrower, uint256 seizedAssets, bytes data) {
37+
MorphoHarness.Id id = libId(marketParams);
38+
address user;
39+
40+
// Assume the invariant initially.
41+
require isHealthy(marketParams, user);
42+
43+
uint256 debtSharesBefore = borrowShares(id, user);
44+
uint256 debtAssetsBefore = summaryMulDivUp(debtSharesBefore, virtualTotalBorrowAssets(id), virtualTotalBorrowShares(id));
45+
// Safe require because of the invariants onlyEnabledLltv and lltvSmallerThanWad in ConsistentState.spec.
46+
require marketParams.lltv < 10^18;
47+
// Assumption to ensure that no interest is accumulated.
48+
require lastUpdate(id) == e.block.timestamp;
49+
50+
liquidate(e, marketParams, borrower, seizedAssets, 0, data);
51+
require !priceChanged;
52+
53+
// Safe require because of the invariant sumBorrowSharesCorrect.
54+
require borrowShares(id, user) <= totalBorrowShares(id);
55+
// Assume that there is still some borrow on the market after liquidation.
56+
require totalBorrowAssets(id) > 0;
57+
// Assume no bad debt realization.
58+
require collateral(id, borrower) > 0;
59+
60+
assert user != borrower;
61+
assert debtSharesBefore == borrowShares(id, user);
62+
assert debtAssetsBefore >= summaryMulDivUp(debtSharesBefore, virtualTotalBorrowAssets(id), virtualTotalBorrowShares(id));
63+
64+
bool stillHealthy = isHealthy(marketParams, user);
65+
require !priceChanged;
66+
assert stillHealthy;
67+
}
68+
69+
rule stayHealthyLiquidateDifferentMarkets(env e, MorphoHarness.MarketParams marketParams, address borrower, uint256 seizedAssets, bytes data) {
70+
MorphoHarness.Id id = libId(marketParams);
71+
address user;
72+
MorphoHarness.MarketParams liquidationMarketParams;
73+
74+
// Assume the invariant initially.
75+
require isHealthy(marketParams, user);
76+
77+
// Safe require because of the invariants onlyEnabledLltv and lltvSmallerThanWad in ConsistentState.spec.
78+
require marketParams.lltv < 10^18;
79+
// Assumption to ensure that no interest is accumulated.
80+
require lastUpdate(id) == e.block.timestamp;
81+
// Assume that the liquidation is on a different market.
82+
require liquidationMarketParams != marketParams;
83+
84+
liquidate(e, liquidationMarketParams, borrower, seizedAssets, 0, data);
85+
require !priceChanged;
86+
87+
// Safe require because of the invariant sumBorrowSharesCorrect.
88+
require borrowShares(id, user) <= totalBorrowShares(id);
89+
90+
bool stillHealthy = isHealthy(marketParams, user);
91+
require !priceChanged;
92+
assert stillHealthy;
93+
}
94+
95+
rule stayHealthyLiquidateLastBorrow(env e, MorphoHarness.MarketParams marketParams, address borrower, uint256 seizedAssets, bytes data) {
96+
MorphoHarness.Id id = libId(marketParams);
97+
address user;
98+
99+
// Assume the invariant initially.
100+
require isHealthy(marketParams, user);
101+
102+
// Safe require because of the invariants onlyEnabledLltv and lltvSmallerThanWad in ConsistentState.spec.
103+
require marketParams.lltv < 10^18;
104+
// Assumption to ensure that no interest is accumulated.
105+
require lastUpdate(id) == e.block.timestamp;
106+
107+
liquidate(e, marketParams, borrower, seizedAssets, 0, data);
108+
require !priceChanged;
109+
110+
// Safe require because of the invariant sumBorrowSharesCorrect.
111+
require borrowShares(id, user) <= totalBorrowShares(id);
112+
// Assume that there is no remaining borrow on the market after liquidation.
113+
require totalBorrowAssets(id) == 0;
114+
115+
bool stillHealthy = isHealthy(marketParams, user);
116+
require !priceChanged;
117+
assert stillHealthy;
118+
}

0 commit comments

Comments
 (0)