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 12 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
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
121 changes: 121 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,121 @@
// 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;

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;
}

// 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);

uint256 debtSharesBefore = borrowShares(id, user);
uint256 debtAssetsBefore = summaryMulDivUp(debtSharesBefore, virtualTotalBorrowAssets(id), virtualTotalBorrowShares(id));
QGarchery marked this conversation as resolved.
Show resolved Hide resolved
// 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.
QGarchery marked this conversation as resolved.
Show resolved Hide resolved
require totalBorrowAssets(id) == 0;

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