Skip to content

test(contracts): solvency invariant suite + error-path coverage (H8)#7

Merged
drewstone merged 3 commits into
mainfrom
test/h8-invariants
Jun 12, 2026
Merged

test(contracts): solvency invariant suite + error-path coverage (H8)#7
drewstone merged 3 commits into
mainfrom
test/h8-invariants

Conversation

@drewstone

Copy link
Copy Markdown
Contributor

Closes audit H8 (zero fuzz/invariant tests; named error paths uncovered). Contract-side only — no logic change, pure test artifacts. Off main.

Invariant suite — proves the accounting across random sequences

A handler drives random deposit/withdraw/collateral/settleFill/redeem/spend/default-reclaim/warp sequences. Properties:

  • Solvencytoken.balanceOf(settlement) == Σbalances + Σcollateral. The contract always holds exactly what it owes; every mutation just shuffles between those buckets. No sequence makes it insolvent or strands tokens.
  • Refundabilitycollateral[issuer] >= liability[issuer] always. The 5% penalty buffer minted at fill is exactly consumed by worst-case defaults, never overdrawn.
  • Conservationbalance == deposited - withdrawn (no off-book mint/burn).

settleFill is deterministic-mint and in the selector set, so the fuzzer reaches lot-bearing states by construction. HandlerSmoke.t.sol concretely drives fill→redeem→spend→reclaim and asserts solvency at each step — foundry reverts handler state between runs, so coverage is proven there rather than via a cumulative counter. foundry.toml gains the [invariant] config the audit noted was missing.

Error paths

The audit flagged NotLotHolder (theft guard), the ZeroAmount guards, and ServedExceedsRequested (over-serve dispute) as having zero coverage. One test per revert + withdraw-above-balance.

92 forge tests green (was 80).

🤖 Generated with Claude Code

…reputation (H6)

Audit H6: 'batches stopped landing' was invisible without journalctl. A tiny
dep-free registry (counters/gauges) now records every load-bearing event and
serves two consumers from one place:

- Operations: GET /metrics renders Prometheus text (rate-limit-exempt, both
  bins). Series: epochs_run, quorum_reached/failed, attestations_signed,
  attestations_refused{verdict=forged|censored|fills-hash-mismatch|cancelled},
  batches_submitted, submit_reverts, gossip_send_failures, pool_size (gauge),
  settlement_fills, spend_keys/served/settled_tokens.
- Reputation: SurplusMetricsSource implements blueprint QoS MetricsSource, so
  the attributable POSITIVE-work subset (batches settled, tokens served,
  quorum participation) is ABI-encoded and submitted on-chain by the heartbeat
  — the record reputation/reward/slashing read and the UI can render as
  per-operator trust. Adversarial signals (censorship/fraud) are deliberately
  NOT self-reported here; peers report those through the BSM challenge path.
  Wire via QoSServiceBuilder::with_metrics_source in the blueprint runner.

15 operator tests green (+2).
A counter at 0 is a real datapoint; a missing series breaks rate() and
flatline alerts. metrics::init() touches every known series at boot (both
bins), so /metrics exposes the full set from a fresh start instead of only
what has fired. Proven live: idle fleet shows all series at 0; after a settled
batch the proposer (op5, Helsinki) shows epochs_run/batches_submitted=1 and the
co-signers (op3/op4) show attestations_signed=1 — per-operator work attributed
correctly across datacenters.
The audit traced the accounting by hand and found it sound; this proves it
across thousands of random sequences and closes the named coverage gaps.

Invariant suite (test/invariant/) — a handler drives random deposit / withdraw
/ collateral / settleFill / redeem / spend / default-reclaim / warp sequences,
each action tolerating its own reverts so the fuzzer composes deep states. The
properties:
  - SOLVENCY: token.balanceOf(settlement) == Σbalances + Σcollateral. The
    contract always holds exactly what it owes — every mutation just shuffles
    between those buckets; no sequence makes it insolvent or strands tokens.
  - REFUNDABILITY: collateral[issuer] >= liability[issuer] always. The 5%
    penalty buffer minted at fill is exactly consumed by worst-case defaults,
    never overdrawn.
  - CONSERVATION: balance == deposited - withdrawn (no off-book mint/burn).
settleFill is deterministic-mint and in the selector set, so the fuzzer
reaches lot-bearing states by construction; HandlerSmoke.t.sol concretely
drives fill→redeem→spend→reclaim and asserts solvency at each step (foundry
reverts handler state between runs, so coverage is proven here, not via a
cumulative counter). foundry.toml gains the [invariant] config the audit noted
was missing.

Error paths (test/ErrorPaths.t.sol) — the audit flagged NotLotHolder (theft
guard), the ZeroAmount guards, and ServedExceedsRequested (over-serve dispute)
as having zero coverage. One test per revert, plus withdraw-above-balance.

92 forge tests green (was 80).
@drewstone drewstone merged commit 4726fe4 into main Jun 12, 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.

1 participant