Skip to content

test(invariant): I5 oracle-integrity stateful fuzz invariant#472

Merged
seongyun-ko merged 2 commits into
seongyun/fuzz/security-upgradesfrom
seongyun/fuzz/oracle-solvency-i3-i5
Jun 15, 2026
Merged

test(invariant): I5 oracle-integrity stateful fuzz invariant#472
seongyun-ko merged 2 commits into
seongyun/fuzz/security-upgradesfrom
seongyun/fuzz/oracle-solvency-i3-i5

Conversation

@seongyun-ko

@seongyun-ko seongyun-ko commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

I5 — Oracle integrity (stateful FUZZ invariant)

Closes the SC-enforceable core of invariant I5 on EtherFiOracle + EtherFiAdmin as a true randomized stateful fuzz-invariant (Foundry invariant engine).

Property: every applied OracleReport (one that advances EtherFiAdmin.lastHandledReportRefSlot via executeTasks) MUST satisfy all three gates at execution time:

  • (a) quorum — consensus reached with ≥ quorumSize() committee signatures
  • (b) APR cap|rebase APR| ≤ acceptableRebaseAprInBps()
  • (c) freshnesscurrentSlot ≥ postReportWaitTimeInSlots + consensusSlot

The fuzzer drives step(magnitude); each call exercises apply + all three reject scenarios against the real contracts. An independent, byte-faithful gate mirror flips a ghost if any state-advancing report ever violated a gate (the I5 proof), cross-checked against the contract's own ReportValidationFailed reasons so the safety ghosts can't be vacuously satisfied.

Result: 4 invariants, 64 runs × 1920 calls, 0 reverts, stable across seeds. Non-vacuous: 60 reports applied + 30/30/30 quorum/APR/freshness rejections. No gate-violating apply ever observed.

Real bug the fuzzer caught (in the harness, documented inline)

The first cut bounded the "valid apply" reward to a blind 0..0.5 ether. The APR gate caps |apr| = 10000·reward·365d / (tvl·elapsedTime) — and on reports after the first, elapsedTime is just the short range moved, so 0.5 ether computes to ~11900 bps > the 10000 cap. The "valid" apply then reverted on APR, left a published-but-unapplied report, bricked the oracle, and every later step bailed → vacuous numApplied==0. The deterministic loop (tiny uniform rewards) never hit this; the fuzzer did. Fix: _maxSafeReward() sizes the apply reward to half the per-range APR-cap boundary, so the accepting path is genuinely accepted regardless of fuzzed magnitude or range length. No assertion weakened.

src/ untouched (test/ only). Base: seongyun/fuzz/security-upgrades (same as #470).

Stateful sequence test for I5 (oracle integrity) on EtherFiOracle +
EtherFiAdmin: every applied OracleReport (advancing lastHandledReportRefSlot
via executeTasks) must satisfy all three gates -- quorum (>= quorumSize sigs),
APR cap (|rebase APR| <= acceptableRebaseAprInBps), freshness (past
postReportWaitTimeInSlots). Proven via an independent, byte-faithful gate
mirror that flips a ghost if any gate-violating apply ever occurs; mirror
fidelity cross-checked against the contract's own revert reasons.

Deterministic (not randomized invariant) because EtherFiOracle's strict
slot/epoch state machine can't be driven reliably under Foundry's stateful-
fuzz scheduling/shrinking -- matches how EtherFiOracle.t.sol tests it. 38
reports applied + 19/20/19 quorum/apr/freshness rejections, reproducible,
0 uncategorised reverts. src untouched.
Replaces the deterministic sequence test with a true randomized stateful
fuzz-invariant (Foundry invariant engine, 64 runs x 1920 calls, 0 reverts).

Root cause of the earlier vacuity (numApplied==0 under the invariant runner):
the 'valid apply' scenario bounded its rebase reward to a blind 0..0.5 ether
constant. The APR gate caps |apr| = 10000*reward*365d/(tvl*elapsedTime); on
reports after the first, elapsedTime is just the ~1100-slot range moved, so
0.5 ether computes to ~11900 bps > the 10000 cap -> the supposedly-valid apply
reverted on APR, leaving the report published-but-unapplied -> oracle stuck ->
every later step bailed at the un-stuck guard. The fuzzer found this; the
deterministic loop (tiny uniform rewards) never did.

Fix: _maxSafeReward() sizes the apply/fresh-fail reward to half the per-range
APR-cap boundary, so the accepting path is genuinely accepted regardless of
the fuzzed magnitude or the range length the clock produces.

Result: 60 reports applied + 30/30/30 quorum/apr/freshness rejections,
mirror-consistent, stable across seeds. No gate-violating apply ever observed.
No assertion weakened; src untouched.
@seongyun-ko seongyun-ko changed the title test: I5 oracle-integrity deterministic stateful test test(invariant): I5 oracle-integrity stateful fuzz invariant Jun 14, 2026
@seongyun-ko seongyun-ko marked this pull request as ready for review June 15, 2026 18:30
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@seongyun-ko seongyun-ko merged commit 1b1a755 into seongyun/fuzz/security-upgrades Jun 15, 2026
1 check passed
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