Skip to content

[Certora] liquidation liveness - #834

Closed
lilCertora wants to merge 85 commits into
mainfrom
certora/liquidationLiveness
Closed

[Certora] liquidation liveness#834
lilCertora wants to merge 85 commits into
mainfrom
certora/liquidationLiveness

Conversation

@lilCertora

Copy link
Copy Markdown
Collaborator

No description provided.

@lilCertora lilCertora self-assigned this May 17, 2026
@lilCertora
lilCertora marked this pull request as ready for review June 7, 2026 17:52

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cd6315e477

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread certora/specs/LiquidateLiveness.spec Outdated
Comment thread certora/confs/LiquidateLiveness.conf

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bc11ba8549

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread certora/specs/LiquidateLiveness.spec
@lilCertora
lilCertora requested a review from jhoenicke June 9, 2026 11:40

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 729f46d510

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread certora/specs/LiquidateLiveness.spec

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b926c65cce

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread certora/specs/LiquidateLiveness.spec Outdated
Comment on lines +168 to +170
mathint recovery0 = ghostMulDivUp(ghostMulDivUp(collateral(id, borrower, 0), ghostPrice(market.collateralParams[0].oracle), ORACLE_PRICE_SCALE()), WAD(), market.collateralParams[0].maxLif);
mathint recovery1 = ghostMulDivUp(ghostMulDivUp(collateral(id, borrower, 1), ghostPrice(market.collateralParams[1].oracle), ORACLE_PRICE_SCALE()), WAD(), market.collateralParams[1].maxLif);
mathint recovery2 = ghostMulDivUp(ghostMulDivUp(collateral(id, borrower, 2), ghostPrice(market.collateralParams[2].oracle), ORACLE_PRICE_SCALE()), WAD(), market.collateralParams[2].maxLif);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Tighten recovery mulDivUp before computing debtAfter

When a collateral price is below ORACLE_PRICE_SCALE, the current ghostMulDivUp axioms can model mulDivUp(collateral, price, ORACLE_PRICE_SCALE) as any value up to the full collateral amount, far above the real ceil result. Separate from the earlier RCF over-approximation thread, this value feeds the bad-debt recovery here, so debtAfterBadDebt can equal _debt even when the real liquidate would realize bad debt; then the badDebtCanBeLiquidated implication is skipped instead of checking the promised debt drop. Add the mulDivUpTightBound/upper-bound axiom before using this recovery reconstruction.

Useful? React with 👍 / 👎.

@jhoenicke

Copy link
Copy Markdown
Collaborator

I replace some require by assert here: #1017. There are still two requires left, which should in principle also be provable.

Otherwise, that looks good. I think all cases are covered except one minor special case: You can liquidate more than maxRepaid if only dust ist left. I don't think that is important enough.

}

function summaryMulDivDown(uint256 x, uint256 y, uint256 d) returns uint256 {
if (d == 0) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An assumption we are making here is that there is no overflow, I think it's worth mentioning

uint256 lltv = market.collateralParams[i].lltv;
uint256 maxLif = market.collateralParams[i].maxLif;

require lltv > 0 && lltv <= WAD(), "lltv in (0, WAD] for a created market (in CreatedMarkets.spec)";

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it does not prove that lltv > 0 right ?
What breaks if we don't have this assumption ?


require lltv > 0 && lltv <= WAD(), "lltv in (0, WAD] for a created market (in CreatedMarkets.spec)";
require maxLif >= WAD(), "maxLif >= WAD (maxLifIsAtLeastWad)";
require lltv < WAD() => lltv * maxLif <= WAD() * (WAD() - 1), "lltv < WAD => lltv*maxLif <= WAD*(WAD-1) (lifTimesLltvStrictBound)";

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't have the lifTimesLltvStrictBound rule in main anymore, we should add it back (feasible because we kept the invariant liquidationCursor < WAD)

threeCollatSetup(e, market, id, borrower);
require collateralIndex == 0 || collateralIndex == 1 || collateralIndex == 2, "seized index in {0,1,2} (<= loop_iter)";
require summaryGetBit(collateralBitmap(id, borrower), collateralIndex), "the seized collateral is active";
require getPrice(collateralIndex, market.collateralParams) > 0, "the seized collateral is priced (LIVENESS)";

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be added to the list of assumptions at the top

uint256 maxLifJ = getMaxLif(collateralIndex, market.collateralParams);
uint256 priceJ = getPrice(collateralIndex, market.collateralParams);
uint128 collatJ = collateral(id, borrower, collateralIndex);
assert maxLifJ * lltvJ <= WAD() * (WAD() - 1), "maxLif*lltv <= WAD*(WAD-1) (lifTimesLltvStrictBound) => WAD*WAD - maxLif*lltv >= WAD >= 1";

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same, lifTimesLltvStrictBound doesn't exist anymore, although here it's an assert. This is a bit suspicious because you need to know that the liquidation cursor is < WAD

@MathisGD MathisGD assigned jhoenicke and unassigned lilCertora Aug 3, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8215ae631e

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

uint256 lltv = market.collateralParams[i].lltv;
uint256 maxLif = Utils.maxLif(lltv, market.collateralParams[i].liquidationCursor);

require lltv > 0 && lltv <= WAD(), "lltv in (0, WAD] for a created market (in CreatedMarkets.spec)";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include zero-LLTV markets in the proof

Remove the lltv > 0 assumption: enableLltv accepts zero and touchMarket permits an enabled zero-LLTV tier, so this require silently excludes valid created markets from every liveness rule despite the message claiming the bound comes from CreatedMarkets.spec. This leaves liquidation liveness unverified for zero-LLTV collateral.

AGENTS.md reference: AGENTS.md:L10-L11

Useful? React with 👍 / 👎.

/// Deterministic ghost for UtilsLib.mulDivDown. Each axiom is proven in MulDiv.spec (rule named per axiom).
persistent ghost ghostMulDivDown(uint256, uint256, uint256) returns uint256 {
// Proved in Muldiv.spec : mulDivDownRoundsDown
axiom forall uint256 a. forall uint256 b. forall uint256 d. d > 0 => ghostMulDivDown(a, b, d) * d <= a * b;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P0 Badge Guard the mulDiv axioms against overflowing inputs

Add the real non-overflow precondition to these rounding axioms. For example, with a = b = max_uint256 and d = 1, this axiom requires a uint256 ghost result multiplied by one to be at least max_uint256², which is impossible because CVL evaluates the arithmetic as unbounded integers; the analogous mulDivUp axiom has the same problem. Since these are global axioms, the contradiction makes every rule vacuously pass rather than proving liquidation liveness.

AGENTS.md reference: AGENTS.md:L40-L40

Useful? React with 👍 / 👎.

bytes data;
liquidate@withrevert(e, market, 0, 0, 0, borrower, postMaturityMode, receiver, 0, data);
assert !lastReverted;
assert debtAfter < _debt => debt(id, borrower) < _debt;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Prove that one liquidation path exists for each borrower

Add a coverage rule linking the branches promised in the header. When debtAfter == _debt, this implication checks no debt progress, while every positive-input rule independently assumes seizablePreamble and a nonempty safe amount through its requires; nothing proves that the same no-bad-debt borrower admits any positive repayment or seizure. Consequently all current rules can pass without establishing the stated claim that together they cover every liquidatable borrower.

AGENTS.md reference: AGENTS.md:L10-L12

Useful? React with 👍 / 👎.

@QGarchery

Copy link
Copy Markdown
Collaborator

@QGarchery QGarchery closed this Aug 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants