Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Certora] Stay healthy liquidate #684

Merged
merged 15 commits into from
Sep 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/certora.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ jobs:
- Liveness
- Reentrancy
- Reverts
- StayHealthy
- Transfer

steps:
Expand Down
3 changes: 2 additions & 1 deletion certora/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,12 +250,13 @@ The [`certora/specs`](specs) folder contains the following files:
- [`ExactMath.spec`](specs/ExactMath.spec) checks precise properties when taking into account exact multiplication and division.
Notably, this file specifies that using supply and withdraw in the same block cannot yield more funds than at the start.
- [`Health.spec`](specs/Health.spec) checks properties about the health of the positions.
Notably, functions cannot render an account unhealthy, and debt positions always have some collateral (thanks to the bad debt realization mechanism).
Notably, debt positions always have some collateral thanks to the bad debt realization mechanism.
- [`LibSummary.spec`](specs/LibSummary.spec) checks the summarization of the library functions that are used in other specification files.
- [`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.
- [`ExchangeRate.spec`](specs/ExchangeRate.spec) checks that the exchange rate between shares and assets evolves predictably over time.
- [`Reentrancy.spec`](specs/Reentrancy.spec) checks that the contract is immune to a particular class of reentrancy issues.
- [`Reverts.spec`](specs/Reverts.spec) checks the condition for reverts and that inputs are correctly validated.
- [`StayHealthy.spec`](specs/Health.spec) checks that functions cannot render an account unhealthy.
- [`Transfer.spec`](specs/Transfer.spec) checks the summarization of the safe transfer library functions that are used in other specification files.

The [`certora/confs`](confs) folder contains a configuration file for each corresponding specification file.
Expand Down
2 changes: 1 addition & 1 deletion certora/confs/AccrueInterest.conf
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
"verify": "MorphoHarness:certora/specs/AccrueInterest.spec",
"prover_args": [
"-depth 3",
"-smt_hashingScheme plaininjectivity",
"-mediumTimeout 30",
"-smt_hashingScheme plaininjectivity",
],
"rule_sanity": "basic",
"server": "production",
Expand Down
18 changes: 18 additions & 0 deletions certora/confs/StayHealthy.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"files": [
"certora/harness/MorphoHarness.sol",
"src/mocks/OracleMock.sol",
],
"msg": "Morpho Blue Stay Healthy",
"process": "emv",
"prover_args": [
"-depth 5",
"-mediumTimeout 5",
"-timeout 3600",
"-smt_hashingScheme plaininjectivity"
],
"rule_sanity": "basic",
"server": "production",
"solc": "solc-0.8.19",
"verify": "MorphoHarness:certora/specs/StayHealthy.spec"
}
32 changes: 3 additions & 29 deletions certora/specs/Health.spec
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
methods {
function extSloads(bytes32[]) external returns bytes32[] => NONDET DELETE;

function totalBorrowAssets(MorphoHarness.Id) external returns uint256 envfree;
function totalBorrowShares(MorphoHarness.Id) external returns uint256 envfree;
function virtualTotalBorrowAssets(MorphoHarness.Id) external returns uint256 envfree;
function virtualTotalBorrowShares(MorphoHarness.Id) external returns uint256 envfree;
QGarchery marked this conversation as resolved.
Show resolved Hide resolved
function lastUpdate(MorphoHarness.Id) external returns uint256 envfree;
function borrowShares(MorphoHarness.Id, address) external returns uint256 envfree;
function collateral(MorphoHarness.Id, address) external returns uint256 envfree;
Expand Down Expand Up @@ -46,35 +49,6 @@ function summaryMin(uint256 a, uint256 b) returns uint256 {
return a < b ? a : b;
}

// Check that without accruing interest, no interaction can put an healthy account into an unhealthy one.
// The liquidate function times out in this rule, but has been checked separately.
rule stayHealthy(env e, method f, calldataarg data)
filtered {
f -> !f.isView &&
f.selector != sig:liquidate(MorphoHarness.MarketParams, address, uint256, uint256, bytes).selector
}
{
MorphoHarness.MarketParams marketParams;
MorphoHarness.Id id = libId(marketParams);
address user;

// Assume that the position is healthy before the interaction.
require isHealthy(marketParams, user);
// Safe require because of the invariants onlyEnabledLltv and lltvSmallerThanWad in ConsistentState.spec.
require marketParams.lltv < 10^18;
// Assumption to ensure that no interest is accumulated.
require lastUpdate(id) == e.block.timestamp;

priceChanged = false;
f(e, data);

// Safe require because of the invariant sumBorrowSharesCorrect.
require borrowShares(id, user) <= totalBorrowShares(id);

bool stillHealthy = isHealthy(marketParams, user);
assert !priceChanged => stillHealthy;
}

// Check that users cannot lose collateral by unauthorized parties except in case of an unhealthy position.
rule healthyUserCannotLoseCollateral(env e, method f, calldataarg data)
filtered { f -> !f.isView }
Expand Down
118 changes: 118 additions & 0 deletions certora/specs/StayHealthy.spec
QGarchery marked this conversation as resolved.
Show resolved Hide resolved
MathisGD marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// SPDX-License-Identifier: GPL-2.0-or-later
import "Health.spec";

// Check that without accruing interest, no interaction can put an healthy account into an unhealthy one.
// The liquidate function times out in this rule, but has been checked separately.
rule stayHealthy(env e, method f, calldataarg data)
filtered {
f -> !f.isView &&
f.selector != sig:liquidate(MorphoHarness.MarketParams, address, uint256, uint256, bytes).selector
}
{
MorphoHarness.MarketParams marketParams;
MorphoHarness.Id id = libId(marketParams);
address user;

// Assume that the position is healthy before the interaction.
require isHealthy(marketParams, user);
// Safe require because of the invariants onlyEnabledLltv and lltvSmallerThanWad in ConsistentState.spec.
require marketParams.lltv < 10^18;
// Assumption to ensure that no interest is accumulated.
require lastUpdate(id) == e.block.timestamp;

f(e, data);

// Safe require because of the invariant sumBorrowSharesCorrect.
require borrowShares(id, user) <= totalBorrowShares(id);

bool stillHealthy = isHealthy(marketParams, user);
assert !priceChanged => stillHealthy;
}

// The liquidate case for the stayHealthy rule, assuming no bad debt realization, otherwise it times out.
// This particular rule makes the following assumptions:
// - the market of the liquidation is the market of the user, see the *DifferentMarkets rule,
// - there is still some borrow on the market after liquidation, see the *LastBorrow rule.
rule stayHealthyLiquidate(env e, MorphoHarness.MarketParams marketParams, address borrower, uint256 seizedAssets, bytes data) {
MorphoHarness.Id id = libId(marketParams);
address user;

// Assume the invariant initially.
require isHealthy(marketParams, user);

uint256 debtSharesBefore = borrowShares(id, user);
uint256 debtAssetsBefore = summaryMulDivUp(debtSharesBefore, virtualTotalBorrowAssets(id), virtualTotalBorrowShares(id));
// Safe require because of the invariants onlyEnabledLltv and lltvSmallerThanWad in ConsistentState.spec.
require marketParams.lltv < 10^18;
// Assumption to ensure that no interest is accumulated.
require lastUpdate(id) == e.block.timestamp;

liquidate(e, marketParams, borrower, seizedAssets, 0, data);
require !priceChanged;

// Safe require because of the invariant sumBorrowSharesCorrect.
require borrowShares(id, user) <= totalBorrowShares(id);
// Assume that there is still some borrow on the market after liquidation.
require totalBorrowAssets(id) > 0;
// Assume no bad debt realization.
require collateral(id, borrower) > 0;

assert user != borrower;
QGarchery marked this conversation as resolved.
Show resolved Hide resolved
assert debtSharesBefore == borrowShares(id, user);
assert debtAssetsBefore >= summaryMulDivUp(debtSharesBefore, virtualTotalBorrowAssets(id), virtualTotalBorrowShares(id));
QGarchery marked this conversation as resolved.
Show resolved Hide resolved

bool stillHealthy = isHealthy(marketParams, user);
require !priceChanged;
assert stillHealthy;
MathisGD marked this conversation as resolved.
Show resolved Hide resolved
}

rule stayHealthyLiquidateDifferentMarkets(env e, MorphoHarness.MarketParams marketParams, address borrower, uint256 seizedAssets, bytes data) {
MorphoHarness.Id id = libId(marketParams);
address user;
MorphoHarness.MarketParams liquidationMarketParams;

// Assume the invariant initially.
require isHealthy(marketParams, user);

// Safe require because of the invariants onlyEnabledLltv and lltvSmallerThanWad in ConsistentState.spec.
require marketParams.lltv < 10^18;
// Assumption to ensure that no interest is accumulated.
require lastUpdate(id) == e.block.timestamp;
// Assume that the liquidation is on a different market.
require liquidationMarketParams != marketParams;

liquidate(e, liquidationMarketParams, borrower, seizedAssets, 0, data);
require !priceChanged;

// Safe require because of the invariant sumBorrowSharesCorrect.
require borrowShares(id, user) <= totalBorrowShares(id);

bool stillHealthy = isHealthy(marketParams, user);
require !priceChanged;
colin-morpho marked this conversation as resolved.
Show resolved Hide resolved
assert stillHealthy;
QGarchery marked this conversation as resolved.
Show resolved Hide resolved
}

rule stayHealthyLiquidateLastBorrow(env e, MorphoHarness.MarketParams marketParams, address borrower, uint256 seizedAssets, bytes data) {
MorphoHarness.Id id = libId(marketParams);
address user;

// Assume the invariant initially.
require isHealthy(marketParams, user);

// Safe require because of the invariants onlyEnabledLltv and lltvSmallerThanWad in ConsistentState.spec.
require marketParams.lltv < 10^18;
// Assumption to ensure that no interest is accumulated.
require lastUpdate(id) == e.block.timestamp;

liquidate(e, marketParams, borrower, seizedAssets, 0, data);
require !priceChanged;

// Safe require because of the invariant sumBorrowSharesCorrect.
require borrowShares(id, user) <= totalBorrowShares(id);
// Assume that there is no remaining borrow on the market after liquidation.
require totalBorrowAssets(id) == 0;

bool stillHealthy = isHealthy(marketParams, user);
require !priceChanged;
assert stillHealthy;
QGarchery marked this conversation as resolved.
Show resolved Hide resolved
}
Loading