Skip to content

test: add max normal-mode liquidation Certora proof#969

Open
prd-carapulse[bot] wants to merge 1 commit into
mainfrom
hermes/max-normal-mode-liquidation-certora
Open

test: add max normal-mode liquidation Certora proof#969
prd-carapulse[bot] wants to merge 1 commit into
mainfrom
hermes/max-normal-mode-liquidation-certora

Conversation

@prd-carapulse
Copy link
Copy Markdown
Contributor

@prd-carapulse prd-carapulse Bot commented Jun 2, 2026

Summary

  • Adds MaxNormalModeLiquidation.spec and .conf for the requested normal-mode max-liquidation proof.
  • Adds normalModeMaxLiquidationPreview to the Certora wrapper so the spec can mirror the liquidation pre-loop and requested amount selector.
  • Documents the new proof in certora/README.md.

Proof scope

  • Uses the requested selector:
    • if collateral repayable value after maxRepaid is below market.rcfThreshold, liquidate with seizedAssets = collateral;
    • otherwise liquidate with repaidUnits = min(maxRepaid, debtAfterBadDebt).
  • States explicit fit assumptions needed by rounding: full-seize must not over-repay debt, and repay-input must not over-seize collateral.
  • Scopes the follow-up revert to nonzero normal-mode liquidations before/at maturity, with no liquidator gate/callback and non-reverting token transfers.

Request Context

  • Initiator: Mathis / Slack U02NB58F6R3 / GitHub @MathisGD
  • Initial Slack thread: C0B520ZTPJM thread_ts=1780405213.491289
  • Additional context: user-provided formula in the Slack request.
  • Rationale: Requested proof for the normal-mode max liquidation amount and for same-collateral follow-up liquidations reverting.

Test Plan

  • forge fmt --check certora/helpers/MidnightWrapper.sol
  • jq < certora/confs/MaxNormalModeLiquidation.conf
  • certoraCVLFormatter certora/specs/MaxNormalModeLiquidation.spec
  • forge build
  • certoraRun certora/confs/MaxNormalModeLiquidation.conf --compilation_steps_only
  • certoraRun certora/confs/Healthiness.conf --compilation_steps_only

Add Certora coverage for the requested normal-mode max liquidation input and same-collateral follow-up revert behavior.
Copy link
Copy Markdown

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

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: 61f30a09a3

ℹ️ 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".

require badDebt <= debtOf(id, borrower), "bad debt is bounded by debt";
require debtAfterBadDebt == debtOf(id, borrower) - badDebt, "debt after bad debt matches prestate";
require badDebt == 0 || (totalUnits(id) > 0 && totalUnits(id) >= badDebt), "market can realize bad debt";
require to_mathint(withdrawable(id)) + previewRepaid <= max_uint128, "withdrawable can increase by repayment";
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 Constrain withdrawable by the actual repayment

When the selector takes the full-seize branch, previewRepaid is the call input and remains zero, but liquidate recomputes repaidUnits from seizedAssets and then adds that amount to _marketState.withdrawable (src/Midnight.sol lines 649-675). As written, a state with withdrawable(id) near uint128.max can satisfy this setup even though the first liquidation reverts from the withdrawable addition overflow; constrain the full-seize branch with to_mathint(withdrawable(id)) + fullSeizeRepaid <= max_uint128 or return/check the actual repayment.

Useful? React with 👍 / 👎.

uint256 lifTimesLltv = type(uint256).max;
if (lltv == 0 || lif <= type(uint256).max / lltv) lifTimesLltv = lif * lltv;
if (originalDebt > maxDebt && lifTimesLltv < WAD * WAD) {
maxRepaid = (originalDebt - maxDebt).mulDivUp(WAD * WAD, WAD * WAD - lifTimesLltv);
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 Compute maxRepaid after realized bad debt

In states with badDebt > 0, the real liquidate path first subtracts bad debt from _position.debt and then computes the normal-mode RCF cap from that reduced debt (src/Midnight.sol lines 626-660). This preview uses originalDebt, so it can overstate maxRepaid and choose a repayment/full-seize input that is not actually allowed by the contract's RCF check; use debtAfterBadDebt - maxDebt here to mirror the implementation.

Useful? React with 👍 / 👎.

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.

1 participant