Skip to content

[Certora] Liquidate buffer, with executable code #708

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

Merged
merged 30 commits into from
Mar 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
ba2280f
feat: executable liquidate, attempt prove buffer
QGarchery Dec 9, 2024
712e6a2
fix: repaid shares computation
QGarchery Dec 10, 2024
6b6a8c7
fix: stronger require
QGarchery Dec 11, 2024
914c244
chore: try medium timeout 20
QGarchery Dec 11, 2024
47d7558
chore: one assert
QGarchery Dec 12, 2024
9a6b160
feat: more summarization
QGarchery Jan 16, 2025
2cb3597
Update README.md
Bilogweb3 Dec 11, 2024
7df7a63
feat: add flashLoan input validation
QGarchery Jan 10, 2025
1399ed2
docs: improve wording of assumptions for liveness
MathisGD Jan 10, 2025
3fb8394
docs: wording
MathisGD Jan 10, 2025
0efe836
chore: remove bytecode hash from compilation
MathisGD Jan 10, 2025
4f9b5ea
chore: explicitly turn on the optimizer
MathisGD Jan 10, 2025
e88a9bf
build: add evm_version
adhusson Jan 13, 2025
02820d4
Update foundry.toml
MathisGD Jan 13, 2025
e2e9700
fix : rename harness to helpers
Jan 14, 2025
cfb7827
fix: rename harness related paths
Jan 14, 2025
c27a99d
chore: rename harness folder to helpers
QGarchery Jan 16, 2025
eb75586
Merge branch 'main' into certora/exec-liquidate-buffer
QGarchery Jan 16, 2025
997629b
fix: signature accrueInterest
QGarchery Jan 16, 2025
f6148ee
refactor: simplify assumptions and multi-assert
QGarchery Feb 16, 2025
b88e4fe
fix: collateralization assumption
QGarchery Feb 17, 2025
bb17fe9
fix: totalBorrowAssets declaration
QGarchery Feb 17, 2025
5c9b5fb
refactor: restrict proof
QGarchery Feb 17, 2025
9ee0ccf
chore: add destructive optimizations
QGarchery Feb 27, 2025
e47b483
feat: add all verification assumptions
QGarchery Feb 27, 2025
db3a34e
docs: comments about require and assert
QGarchery Feb 27, 2025
7655e01
docs: update README
QGarchery Feb 27, 2025
63a90eb
Merge remote-tracking branch 'origin/main' into certora/exec-liquidat…
QGarchery Feb 27, 2025
0202ea3
docs: add comment about the rule
QGarchery Feb 28, 2025
1831743
Update certora/README.md
QGarchery Feb 28, 2025
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 @@ -23,6 +23,7 @@ jobs:
- ExchangeRate
- Health
- LibSummary
- LiquidateBuffer
- Liveness
- Reentrancy
- Reverts
Expand Down
5 changes: 3 additions & 2 deletions certora/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ where `sumSupplyShares` only exists in the specification, and is defined to be a
To ensure proper collateralization, a liquidation system is put in place, where unhealthy positions can be liquidated.
A position is said to be healthy if the ratio of the borrowed value over collateral value is smaller than the liquidation loan-to-value (LLTV) of that market.
This leaves a safety buffer before the position can be insolvent, where the aforementioned ratio is above 1.
To ensure that liquidators have the time to interact with unhealthy positions, it is formally verified that this buffer is respected.
To ensure that liquidators have the time to interact with unhealthy positions, it is formally verified that this buffer is respected and that it leaves room for healthy liquidations to happen.
Notably, it is verified that in the absence of accrued interest, which is the case when creating a new position or when interacting multiple times in the same block, a position cannot be made unhealthy.

Let's define bad debt of a position as the amount borrowed when it is backed by no collateral.
Expand Down Expand Up @@ -249,11 +249,12 @@ The [`certora/specs`](specs) folder contains the following files:
This includes checking that the accounting of the total amount and shares is correct, that markets are independent from each other, that only enabled IRMs and LLTVs can be used, and that users cannot have their position made worse by an unauthorized account.
- [`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.
- [`ExchangeRate.spec`](specs/ExchangeRate.spec) checks that the exchange rate between shares and assets evolves predictably over time.
- [`Health.spec`](specs/Health.spec) checks properties about the health of the positions.
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.
- [`LiquidateBuffer.spec`](specs/LiquidateBuffer.spec) checks that there is a buffer for liquidatable positions, before they are insolvent, such that liquidation leads to healthier position and cannot lead to bad debt.
- [`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.
Expand Down
21 changes: 21 additions & 0 deletions certora/confs/LiquidateBuffer.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"files": [
"certora/helpers/MorphoHarness.sol",
"certora/helpers/Util.sol"
],
"solc": "solc-0.8.19",
"verify": "MorphoHarness:certora/specs/LiquidateBuffer.spec",
"prover_args": [
"-depth 5",
"-mediumTimeout 20",
"-timeout 3600",
"-adaptiveSolverConfig false",
"-smt_nonLinearArithmetic true",
"-destructiveOptimizations twostage",
"-solvers [z3:def{randomSeed=1},z3:def{randomSeed=2},z3:def{randomSeed=3},z3:def{randomSeed=4},z3:def{randomSeed=5},z3:def{randomSeed=6},z3:def{randomSeed=7},z3:def{randomSeed=8},z3:def{randomSeed=9},z3:def{randomSeed=10}]"
],
"multi_assert_check": true,
"rule_sanity": "basic",
"server": "production",
"msg": "Morpho Blue Liquidate Buffer"
}
10 changes: 10 additions & 0 deletions certora/helpers/Util.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import "../../src/libraries/UtilsLib.sol";

contract Util {
using MarketParamsLib for MarketParams;
using MathLib for uint256;

function wad() external pure returns (uint256) {
return WAD;
Expand All @@ -17,6 +18,15 @@ contract Util {
return MAX_FEE;
}

function oraclePriceScale() external pure returns (uint256) {
return ORACLE_PRICE_SCALE;
}

function lif(uint256 lltv) external pure returns (uint256) {
return
UtilsLib.min(MAX_LIQUIDATION_INCENTIVE_FACTOR, WAD.wDivDown(WAD - LIQUIDATION_CURSOR.wMulDown(WAD - lltv)));
}

function libId(MarketParams memory marketParams) external pure returns (Id) {
return marketParams.id();
}
Expand Down
71 changes: 71 additions & 0 deletions certora/specs/LiquidateBuffer.spec
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// SPDX-License-Identifier: GPL-2.0-or-later

using Util as Util;

methods {
function extSloads(bytes32[]) external returns (bytes32[]) => NONDET DELETE;

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;
function totalBorrowShares(MorphoHarness.Id) external returns (uint256) envfree;
function totalBorrowAssets(MorphoHarness.Id) external returns (uint256) envfree;
function virtualTotalBorrowAssets(MorphoHarness.Id) external returns uint256 envfree;
function virtualTotalBorrowShares(MorphoHarness.Id) external returns uint256 envfree;

function Util.libId(MorphoHarness.MarketParams) external returns (MorphoHarness.Id) envfree;
function Util.lif(uint256) external returns (uint256) envfree;
function Util.oraclePriceScale() external returns (uint256) envfree;
function Util.wad() external returns (uint256) envfree;

function Morpho._isHealthy(MorphoHarness.MarketParams memory, MorphoHarness.Id,address) internal returns (bool) => NONDET;
function Morpho._accrueInterest(MorphoHarness.MarketParams memory, MorphoHarness.Id) internal => NONDET;

function _.price() external => constantPrice expect uint256;
}

persistent ghost uint256 constantPrice;

// Check that for a position with LTV < 1 / LIF, its health improves after a liquidation.
rule liquidateImprovePosition(env e, MorphoHarness.MarketParams marketParams, address borrower, uint256 seizedAssetsInput, uint256 repaidSharesInput, bytes data) {
// Assume no callback.
require data.length == 0;

MorphoHarness.Id id = Util.libId(marketParams);

// We place ourselves at the last block for getting the following variables.
require lastUpdate(id) == e.block.timestamp;

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

uint256 borrowerCollateral = collateral(id, borrower);
uint256 lif = Util.lif(marketParams.lltv);
uint256 virtualTotalAssets = virtualTotalBorrowAssets(id);
uint256 virtualTotalShares = virtualTotalBorrowShares(id);

// Let borrowerAssets = borrowerShares * virtualTotalAssets / virtualTotalShares
// and borrowerCollateralQuoted = borrowerCollateral * constantPrice / Util.oraclePriceScale()
// then the following line is the assumption borrowerAssets / borrowerCollateralQuoted < 1 / LIF.
require borrowerCollateral * constantPrice * virtualTotalShares * Util.wad() > borrowerShares * Util.oraclePriceScale() * virtualTotalAssets * lif;

uint256 seizedAssets;
(seizedAssets, _) = liquidate(e, marketParams, borrower, seizedAssetsInput, repaidSharesInput, data);

uint256 newBorrowerShares = borrowShares(id, borrower);
uint256 newBorrowerCollateral = collateral(id, borrower);
uint256 repaidShares = assert_uint256(borrowerShares - newBorrowerShares);
uint256 newVirtualTotalAssets = virtualTotalBorrowAssets(id);
uint256 newVirtualTotalShares = virtualTotalBorrowShares(id);

// Hint for the prover to show that there is no bad debt realization.
assert newBorrowerCollateral != 0;
// Hint for the prover about the ratio used to close the position.
assert repaidShares * borrowerCollateral >= seizedAssets * borrowerShares;
// Prove that the ratio of shares of debt over collateral is smaller after the liquidation.
assert borrowerShares * newBorrowerCollateral >= newBorrowerShares * borrowerCollateral;
// Prove that the value of borrow shares is smaller after the liquidation.
// Note that this is only shown for the case where there are still borrow positions on the markets.
assert totalBorrowAssets(id) > 0 => newVirtualTotalShares * virtualTotalAssets >= newVirtualTotalAssets * virtualTotalShares;
}
Loading