Skip to content

Invariants

IdInvariant
C1For every series: quoteEscrow == N_c + prefundRemaining + claimableQuote + refundableQuote + accruedFees
C2For every series: stockEscrow == stockMatched + refundableStock + claimableStock
C3USDG.balanceOf(NoteCore) ≥ Σ quoteEscrow and Stock.balanceOf(NoteCore) ≥ Σ stockEscrow per token
C4NoteLegs.totalSupply(couponId) ≤ matchedNotional and totalSupply(shieldId) ≤ matchedNotional, equality once every depositor has settled and before any redemption
C5couponIndex is non-decreasing; Σ accrued(holder) ≤ couponPool
C6nextObs is non-decreasing and ≤ observations.length
C7Status transitions follow the state machine; terminal statuses never change
C8Total USDG ever paid out of a series ≤ total USDG ever deposited into it
C9Total stock ever paid out of a series ≤ total stock ever deposited into it
C10prefundRemaining ≥ (remaining coupon observations) × N × couponBps / 1e4 while Live

C1 and C2 are the escrow invariant. C8 and C9 follow from C1 to C3 but are asserted independently as ghost-variable checks.

IdInvariant
O1An observed price always comes from the last round with updatedAt ≤ closeTs + closeGrace, which satisfies updatedAt ≥ closeTs − maxPreCloseLag; the selected round is independent of the call time
O2If two feeds are configured and both pass, they differ by ≤ maxDeviationBps
O3forceObserve can only execute ≥ forceDelay after its proposal and ≥ deferralWindow after closeTs
O4isOfficialClose(t) implies t is a weekday, not a holiday, and equals the regular or registered early close for its day
IdInvariant
T1NOTE.totalSupply ≤ CAP and mintedInEpoch[e] ≤ mintCapPerEpoch for every epoch
T2Only whitelisted minters ever increase totalSupply
T3sNOTE share price (totalAssets+1)/(totalSupply+1e6) is non-decreasing
T4NOTE.balanceOf(sNOTE) ≥ totalAssets + unvestedRewards()
T5Treasury: redeemedInWindow ≤ reserveValueQuote × dailyRedemptionCapBps / 1e4 within any window
T6Treasury floor reserveValueQuote / circulatingSupply is non-decreasing across redemptions
T7RevenueRouter: every fill price ≥ floorPriceWad; every fill ≤ maxFillBps of the buyback reserve
T8RevenueRouter: USDG.balanceOf(router) ≥ buybackReserve
T9BondDepository: Σ payouts(market) ≤ capacity(initial); currentDebt ≤ maxDebt
T10Desk: deployedPerSeries ≤ perSeriesCap × totalAssets, likewise per underlying and total, at the time of each fill
  • One handler contract per module exposing every external mutating function with bounded random inputs and a rotating set of actors.
  • A TimeHandler that warps to the next official close and drives the oracle mocks with fuzzed prices, occasionally stale or negative answers, and occasional backup-feed disagreement.
  • Ghost variables track cumulative deposits and withdrawals per series and per token.
  • fail_on_revert = false so that expected reverts (too early, caps) do not abort a run; unexpected reverts are asserted separately in unit tests.
  • Default 512 runs × 64 depth locally; 5,000 × 64 in CI.

status The handler suites are committed: test/invariant/core (CoreInvariantHandler, NoteCore.invariant.t.sol, reachability test), test/invariant/token (Desk, NOTE, RevenueRouter, Treasury, sNOTE) and test/invariant/governance (veNOTE, GaugeController, Gauge, Minter). The core handler includes a lazyCrank action so that the crank-in-exit path is exercised under the escrow invariant. Heavy runs (5,000 × 64) and their logs are recorded in the internal security review under audit/.