Skip to content

Commit 9e2b075

Browse files
authored
Merge pull request #708 from morpho-org/certora/exec-liquidate-buffer
[Certora] Liquidate buffer, with executable code
2 parents a23260a + 1831743 commit 9e2b075

File tree

5 files changed

+106
-2
lines changed

5 files changed

+106
-2
lines changed

.github/workflows/certora.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ jobs:
2323
- ExchangeRate
2424
- Health
2525
- LibSummary
26+
- LiquidateBuffer
2627
- Liveness
2728
- Reentrancy
2829
- Reverts

certora/README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ where `sumSupplyShares` only exists in the specification, and is defined to be a
9595
To ensure proper collateralization, a liquidation system is put in place, where unhealthy positions can be liquidated.
9696
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.
9797
This leaves a safety buffer before the position can be insolvent, where the aforementioned ratio is above 1.
98-
To ensure that liquidators have the time to interact with unhealthy positions, it is formally verified that this buffer is respected.
98+
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.
9999
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.
100100

101101
Let's define bad debt of a position as the amount borrowed when it is backed by no collateral.
@@ -249,11 +249,12 @@ The [`certora/specs`](specs) folder contains the following files:
249249
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.
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.
252+
- [`ExchangeRate.spec`](specs/ExchangeRate.spec) checks that the exchange rate between shares and assets evolves predictably over time.
252253
- [`Health.spec`](specs/Health.spec) checks properties about the health of the positions.
253254
Notably, debt positions always have some collateral thanks to the bad debt realization mechanism.
254255
- [`LibSummary.spec`](specs/LibSummary.spec) checks the summarization of the library functions that are used in other specification files.
256+
- [`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.
255257
- [`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.
256-
- [`ExchangeRate.spec`](specs/ExchangeRate.spec) checks that the exchange rate between shares and assets evolves predictably over time.
257258
- [`Reentrancy.spec`](specs/Reentrancy.spec) checks that the contract is immune to a particular class of reentrancy issues.
258259
- [`Reverts.spec`](specs/Reverts.spec) checks the condition for reverts and that inputs are correctly validated.
259260
- [`StayHealthy.spec`](specs/Health.spec) checks that functions cannot render an account unhealthy.

certora/confs/LiquidateBuffer.conf

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"files": [
3+
"certora/helpers/MorphoHarness.sol",
4+
"certora/helpers/Util.sol"
5+
],
6+
"solc": "solc-0.8.19",
7+
"verify": "MorphoHarness:certora/specs/LiquidateBuffer.spec",
8+
"prover_args": [
9+
"-depth 5",
10+
"-mediumTimeout 20",
11+
"-timeout 3600",
12+
"-adaptiveSolverConfig false",
13+
"-smt_nonLinearArithmetic true",
14+
"-destructiveOptimizations twostage",
15+
"-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}]"
16+
],
17+
"multi_assert_check": true,
18+
"rule_sanity": "basic",
19+
"server": "production",
20+
"msg": "Morpho Blue Liquidate Buffer"
21+
}

certora/helpers/Util.sol

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import "../../src/libraries/UtilsLib.sol";
88

99
contract Util {
1010
using MarketParamsLib for MarketParams;
11+
using MathLib for uint256;
1112

1213
function wad() external pure returns (uint256) {
1314
return WAD;
@@ -17,6 +18,15 @@ contract Util {
1718
return MAX_FEE;
1819
}
1920

21+
function oraclePriceScale() external pure returns (uint256) {
22+
return ORACLE_PRICE_SCALE;
23+
}
24+
25+
function lif(uint256 lltv) external pure returns (uint256) {
26+
return
27+
UtilsLib.min(MAX_LIQUIDATION_INCENTIVE_FACTOR, WAD.wDivDown(WAD - LIQUIDATION_CURSOR.wMulDown(WAD - lltv)));
28+
}
29+
2030
function libId(MarketParams memory marketParams) external pure returns (Id) {
2131
return marketParams.id();
2232
}

certora/specs/LiquidateBuffer.spec

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// SPDX-License-Identifier: GPL-2.0-or-later
2+
3+
using Util as Util;
4+
5+
methods {
6+
function extSloads(bytes32[]) external returns (bytes32[]) => NONDET DELETE;
7+
8+
function lastUpdate(MorphoHarness.Id) external returns (uint256) envfree;
9+
function borrowShares(MorphoHarness.Id, address) external returns (uint256) envfree;
10+
function collateral(MorphoHarness.Id, address) external returns (uint256) envfree;
11+
function totalBorrowShares(MorphoHarness.Id) external returns (uint256) envfree;
12+
function totalBorrowAssets(MorphoHarness.Id) external returns (uint256) envfree;
13+
function virtualTotalBorrowAssets(MorphoHarness.Id) external returns uint256 envfree;
14+
function virtualTotalBorrowShares(MorphoHarness.Id) external returns uint256 envfree;
15+
16+
function Util.libId(MorphoHarness.MarketParams) external returns (MorphoHarness.Id) envfree;
17+
function Util.lif(uint256) external returns (uint256) envfree;
18+
function Util.oraclePriceScale() external returns (uint256) envfree;
19+
function Util.wad() external returns (uint256) envfree;
20+
21+
function Morpho._isHealthy(MorphoHarness.MarketParams memory, MorphoHarness.Id,address) internal returns (bool) => NONDET;
22+
function Morpho._accrueInterest(MorphoHarness.MarketParams memory, MorphoHarness.Id) internal => NONDET;
23+
24+
function _.price() external => constantPrice expect uint256;
25+
}
26+
27+
persistent ghost uint256 constantPrice;
28+
29+
// Check that for a position with LTV < 1 / LIF, its health improves after a liquidation.
30+
rule liquidateImprovePosition(env e, MorphoHarness.MarketParams marketParams, address borrower, uint256 seizedAssetsInput, uint256 repaidSharesInput, bytes data) {
31+
// Assume no callback.
32+
require data.length == 0;
33+
34+
MorphoHarness.Id id = Util.libId(marketParams);
35+
36+
// We place ourselves at the last block for getting the following variables.
37+
require lastUpdate(id) == e.block.timestamp;
38+
39+
uint256 borrowerShares = borrowShares(id, borrower);
40+
// Safe require because of the sumBorrowSharesCorrect invariant.
41+
require borrowerShares <= totalBorrowShares(id);
42+
43+
uint256 borrowerCollateral = collateral(id, borrower);
44+
uint256 lif = Util.lif(marketParams.lltv);
45+
uint256 virtualTotalAssets = virtualTotalBorrowAssets(id);
46+
uint256 virtualTotalShares = virtualTotalBorrowShares(id);
47+
48+
// Let borrowerAssets = borrowerShares * virtualTotalAssets / virtualTotalShares
49+
// and borrowerCollateralQuoted = borrowerCollateral * constantPrice / Util.oraclePriceScale()
50+
// then the following line is the assumption borrowerAssets / borrowerCollateralQuoted < 1 / LIF.
51+
require borrowerCollateral * constantPrice * virtualTotalShares * Util.wad() > borrowerShares * Util.oraclePriceScale() * virtualTotalAssets * lif;
52+
53+
uint256 seizedAssets;
54+
(seizedAssets, _) = liquidate(e, marketParams, borrower, seizedAssetsInput, repaidSharesInput, data);
55+
56+
uint256 newBorrowerShares = borrowShares(id, borrower);
57+
uint256 newBorrowerCollateral = collateral(id, borrower);
58+
uint256 repaidShares = assert_uint256(borrowerShares - newBorrowerShares);
59+
uint256 newVirtualTotalAssets = virtualTotalBorrowAssets(id);
60+
uint256 newVirtualTotalShares = virtualTotalBorrowShares(id);
61+
62+
// Hint for the prover to show that there is no bad debt realization.
63+
assert newBorrowerCollateral != 0;
64+
// Hint for the prover about the ratio used to close the position.
65+
assert repaidShares * borrowerCollateral >= seizedAssets * borrowerShares;
66+
// Prove that the ratio of shares of debt over collateral is smaller after the liquidation.
67+
assert borrowerShares * newBorrowerCollateral >= newBorrowerShares * borrowerCollateral;
68+
// Prove that the value of borrow shares is smaller after the liquidation.
69+
// Note that this is only shown for the case where there are still borrow positions on the markets.
70+
assert totalBorrowAssets(id) > 0 => newVirtualTotalShares * virtualTotalAssets >= newVirtualTotalAssets * virtualTotalShares;
71+
}

0 commit comments

Comments
 (0)