Skip to content

certora: summarize oracle price() to fix NoDebtWithoutCollateral SMT timeout#1077

Closed
claude[bot] wants to merge 4 commits into
mainfrom
claude/fix-nodebtwithoutcollateral-timeout
Closed

certora: summarize oracle price() to fix NoDebtWithoutCollateral SMT timeout#1077
claude[bot] wants to merge 4 commits into
mainfrom
claude/fix-nodebtwithoutcollateral-timeout

Conversation

@claude

@claude claude Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Requested by Mathis GD · Slack thread

Fixes the flaky SMT timeout on the verify (NoDebtWithoutCollateral) leg (rule lockedOrNoDebtWithoutCollateral). The invariant's liquidate preserved case unrolls the collateral loop, and each iteration's unresolved IOracle.price() call triggers a full HAVOC_ALL of storage under -havocAllByDefault — so the leg either proved in ~10 min or ran out the 7200s budget.

Summarizes _.price() as NONDET in NoDebtWithoutCollateral.spec: price() is a view (staticcall), so it can't mutate state or reenter, and the summary drops the spurious per-iteration havoc while keeping the return value fully adversarial.

…e loop

The `verify (NoDebtWithoutCollateral)` CI leg flaked between a ~9-minute
pass and a 2h SMT timeout. Root cause: the liquidate() preserved block
unrolls the `while (_collateralBitmap != 0)` loop 5x, and each iteration
calls IOracle(...).price(). With `-havocAllByDefault true`, every
unresolved price() external call forces a full HAVOC_ALL of storage that
the solver re-proves across the loop unrolling and the 10-seed z3
portfolio -- the dominant cost behind the timeout.

Summarize `_.price()` as PER_CALLEE_CONSTANT, matching the sibling spec
CollateralBitmap.spec which already proves the analogous loop in seconds.
price() is a view (staticcall) and cannot mutate state, so the summary
does not weaken the reentrancy modeling `-havocAllByDefault` provides for
the genuinely state-changing external calls (onLiquidate callback, token
transfers). No budget knobs (loop_iter, smt_timeout, seed portfolio,
havocAllByDefault) are changed.
Comment thread certora/specs/NoDebtWithoutCollateral.spec Outdated
Switch the _.price() summary from PER_CALLEE_CONSTANT to NONDET so the
prover may return different adversarial values across reads of the same
oracle (e.g. across the onLiquidate reentrancy). price() is a view
(staticcall), so it cannot mutate state or reenter with side effects;
the summary removes only the impossible HAVOC_ALL of storage, not real
behavior. NONDET is strictly more adversarial on the return value, so it
cannot hide a bug that depends on the price varying.
Comment thread certora/specs/NoDebtWithoutCollateral.spec Outdated
Comment thread certora/specs/NoDebtWithoutCollateral.spec Outdated
@MathisGD MathisGD self-assigned this Jul 24, 2026
@MathisGD
MathisGD marked this pull request as ready for review July 24, 2026 13:30

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.

I don't think it should change anything: havocAllByDefault only affects non-static calls, so the AUTO summary should summarize price as NONDET already

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

You're right, thanks for catching this. I verified it in the prover source (Havocer.kt): the HavocAllByDefaultHavocType.All branch is guarded by callType != STATIC, and a view call like price() falls through to HavocType.Static (NONDET). So -havocAllByDefault never havocs price(), and _.price() => NONDET is a no-op — my root cause was wrong.

Checking the record properly, the leg times out ~45% of the time on main regardless of this summary (6-8 of the last ~15 runs), so the green runs here were luck, not a fix. The real cost drivers look like the HAVOC_ALL on the genuine state-changing calls in liquidate (the callback + token transfers) plus the quantified nonlinear mulDiv/RCF arithmetic. A real fix is more likely the requireInvariant bitmap hints plus bounding the WAD*WAD - lif*lltv denominator. Sorting out the path with the team before pushing anything.


Generated by Claude Code

@claude

claude Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

Closing this — the _.price() => NONDET summary is a no-op: price() is a view (staticcall), which -havocAllByDefault already treats as NONDET, so it doesn't change verification and doesn't fix the timeout (thanks @QGarchery for catching it). The leg is ~45% flaky on main regardless. I'll open a new PR with a real fix (requireInvariant bitmap hints + bounding the WAD*WAD - lif*lltv denominator).


Generated by Claude Code

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.

3 participants