Threat Model¶
What Accord defends against, where each defense lives, what proves it, and — explicitly — what residual risk remains. Companion to the Trust Profile: the trust profile names who is trusted; this page enumerates which attacks were considered and how each is answered. If an attack is not in this table, we have not considered it — say so and it gets a row.
The discipline this table follows (learned from the most honest design docs in the space, e.g. Kourt's GAMETHEORY.md): every entry is either defended (with code + test pointers) or accepted (with the price named). Nothing is waved away.
Attack ledger¶
| # | Attack | Vector | Defense | Where | Proven by | Residual |
|---|---|---|---|---|---|---|
| 1 | Bribery of drawn jurors | Pay jurors off-chain to vote a chosen option | Vote secrecy until reveal (commit-reveal, per-juror salt) makes vote-buying unverifiable at commit time; appeals multiply the panel (3→7→15→31) and the bribe bill; incoherent jurors are slashed | commit/reveal, appeal (ADR-0004) |
voting.spec.ts, appeal.spec.ts |
Off-chain bribery is undetectable on-chain. Priced by the security-value ceiling — do not file disputes worth more than it |
| 2 | Stake-majority capture | One coalition holds >50% of pool stake ⇒ majority draw probability per seat | Honest-majority-stake assumption; appeals raise cost but not the majority requirement — this is priced, not prevented | whole design (ADR-0001) | — | Accepted: majority capture is possible; cost ≈ majority stake acquisition. Ceiling formula in the trust profile |
| 3 | Draw manipulation / fabricated juror set | Post a fake root; inflate a colluding juror's selection range | No posted root exists: the accumulator is maintained live by the protocol on every stake/unstake; sums are hash-bound; the root is frozen atomically with the committed VRF |
ADR-0012 (supersedes 0003/0008/0009 snapshot layer) | accumulator.spec.ts, accumulator_litesvm.rs, host MST tests (src/tests.rs) |
None on correctness; see #8 for liveness |
| 4 | Freeze-window stake top-up ("rented draw weight") | Watch for a filed dispute, top up stake before request_vrf lands — the root freezes at the VRF callback, not at filing — buying draw probability inside the window |
(a) The filer can close the window itself by bundling create_dispute + request_vrf in one transaction; (b) stake buys draw probability, not seats; (c) drawn seats are locked (active_draws) through settlement and must vote coherently or be slashed; (d) post-freeze stake/unstake cannot alter frozen_root, and the draw-time inflation guard rejects a leaf heavier than live stake |
commit_vrf_callback freeze, draw_seat inflation guard, active_draws gate |
draw.spec.ts, accumulator_litesvm.rs |
A filer that leaves a gap exposes its own dispute to a better-funded watcher. Priced (capital + coherence risk), not excluded. Compare Kourt's rented-weight treatment |
| 5 | VRF provider withholds or biases randomness | Oracle refuses the callback, or grinds values | Randomness is committed once and the callback is identity-constrained and Created-state-gated; the result is on-chain and deterministic given the seed — the provider cannot bias which juror a given seed selects |
ADR-0013, SR3-L-3 | draw.spec.ts |
Availability is provider-dependent: a stalling oracle blocks new draws, never in-flight ones (trust profile #4) |
| 6 | Apathy / low turnout forces a wrong or arbitrary ruling | Jurors no-show; a tie exists; nobody cranks | Reveal quorum (reveal_threshold_bps, default 2/3) and a decisive tally are required; a shortfall or a Plurality top-count tie ⇒ same-size redraw ladder (draw_attempt, cap max_draw_attempts); no-shows are slashed into stake_delta; exhaustion ⇒ Failed with the filer refund exactly the booked filing fee. Who wins when nothing happens: nobody — funds return. |
ADR-0021, 0026, 0014, 0033 | quorum-redraw.spec.ts, dispute.spec.ts |
A coalition holding > (1 − threshold) can force Failed (veto-by-abstention) — priced by no-show slashing, bounded to refunds, never a wrong ruling |
| 7 | Same-mint dominance (fee_token == staking_token) |
Jurors self-deal fees from a mint they also stake, breaking coherence incentives | Dominance gate at pool creation and on every param update: α·min_stake/10_000 ≥ 2·fee_per_juror; split-mint pools are deliberately un-gated |
ADR-0029 | lifecycle.subaccord.spec.ts, update_litesvm.rs |
None — the gate is structural for same-mint pools |
| 8 | Indexer withholds Merkle paths | Staking/drawing stalls because no one serves paths | Root is canonical on-chain; any auditor can rebuild the tree from JurorStake via getProgramAccounts and serve paths; competing indexers are stateless mirrors |
ADR-0012 | root-rebuild path exercised in accumulator.spec.ts |
Liveness dependency only: at least one honest indexer must serve each pool (trust profile #3). SNARK-proven root is the v2 destination |
| 9 | Custody drain / fee theft | An instruction moves vault tokens without exact accounting | Fail-closed exact custody: every custody point measures the vault delta and requires equality with the nominal terms (SR3-M-2); stake and fee economics live in separate vaults; no fee moves before finality | ADR-0020, 0029 | appeal.spec.ts, staking.spec.ts, full-lifecycle.spec.ts |
None known; the fail-closed check is the invariant (see I2) |
| 10 | Governance abuse (hostile pool params, panic freeze) | Subaccord authority retunes a pool mid-dispute; multisig pauses to strand funds | Param updates on a 48h on-chain timelock; CaseTerms are frozen per dispute at filing so governance cannot retune a live dispute; pause gates new exposure (create_dispute/stake/appeal) and never adjudication; unpause is itself timelocked so a freeze is recoverable |
ADR-0005, 0016, 0007 | lifecycle.update.spec.ts, lifecycle.pause.timelock.spec.ts, pause_litesvm.rs |
A watcher must observe the 48h window. Until the post-audit freeze, upgrade-multisig members are trusted with capital-bearing code |
| 11 | Re-initialization / account confusion | Re-run an initializer to wipe state; pass a look-alike account | Anchor discriminator + init/init_if_needed constraints; every instruction's LiteSVM contract includes the reinit and wrong-authority cases (the safe-solana-builder checklist pattern) |
all instructions/*.rs |
*_litesvm.rs auth/reinit cases per instruction |
None known |
| 12 | Party-juror conflict | A party to the dispute is staked in the pool and drawn onto its own panel | Priced, not excluded: a drawn party votes its side and is slashed if incoherent; on minimum panels one seat is cheap to outvote; appeals are open to anyone | design decision (ADR-0004) | — | Accepted (trust profile, Synod note). Compare Kourt's hard-exclusion alternative on the prior-art page |
| 13 | Evidence operator leaks or selectively serves | Operator sees plaintext evidence; withholds from honest jurors | On-chain evidence hash commits the bundle; delivery crypto (ECIES/AES-GCM, registered delivery keys) is protocol-fixed in the SDK, not operator-discretionary; operator identity is per-Subaccord and public | ADR-0006, 0011, 0015, 0034 | evidence.spec.ts, SDK evidence/*.test.ts |
Accepted: the operator sees plaintext and is trusted not to leak (trust profile #7). Threshold-PRE is the v2+ path |
| 14 | Expired-credential juror stalls a gated pool | An attestation-gated pool keeps a dead leaf that blocks draws | prune_juror: a permissionless crank evicts expired-credential jurors (full amount into pending_withdrawal) so the tree stays drawable |
ADR-0024 | attestation.spec.ts, attestation_litesvm.rs |
None — eviction is permissionless |
| 15 | Sybil admission | One human, many keys, each at min_stake |
Stake is the anti-sybil unit and selection weight; optional per-Subaccord SAS credential gate (juror_credential/juror_schema) raises admission to a verifiable credential |
ADR-0001, 0024 | staking.spec.ts, attestation.spec.ts |
Accepted for ungated pools: admission is key-level pseudonymous, not identity-verified humans (trust profile #9) |
Invariants¶
The properties the protocol promises to hold always, with the test that would fail if one broke. These are the rows a reviewer should try to falsify.
| # | Invariant | Statement | Enforced in | Proven by |
|---|---|---|---|---|
| I1 | Stake vault invariant | Slashing and redistribution are ledger-only (stake_delta); the stake_vault SPL balance is unchanged by slash + redistribute |
settle_round (ADR-0020) |
staking.spec.ts, full-lifecycle.spec.ts |
| I2 | Fail-closed exact custody | At every custody point the measured vault delta must equal the full nominal terms — unequal ⇒ revert, funds never stuck half-moved | appeal, settlement paths (SR3-M-2, review 2026-09-23) |
appeal.spec.ts, dispute.spec.ts |
| I3 | No arbitrary tie winner | A Plurality top-count tie is a non-decisive round → redraw; a tie never crowns an option | finalize_round (ADR-0026) |
quorum-redraw.spec.ts |
| I4 | No ruling, no pay | Failed disputes pay zero participation; the filer refund is exactly the booked filing fee, independent of reveal counts | cancel_dispute, redraw, claim_appeal_refund (ADR-0033) |
dispute.spec.ts, synod.full-lifecycle.spec.ts |
| I5 | Fees settle against the final ruling only | No juror fee is earned on a vote that ends up incoherent; every round settles against final_ruling |
settle_round/finalize_dispute (ADR-0029, 0018) |
appeal.spec.ts, scalar.spec.ts |
| I6 | Governance never moves vaults | No pause or governance instruction transfers vault balances; pause gates new exposure only; in-flight disputes always resolve or refund | pause/unpause/update paths (ADR-0016) |
lifecycle.pause.timelock.spec.ts, pause_litesvm.rs |
| I7 | Root canonicality | The juror-set root is protocol-maintained, not posted; frozen atomically with the committed VRF; post-freeze stake changes cannot alter a draw | stake/unstake/commit_vrf_callback (ADR-0012) |
accumulator.spec.ts, draw.spec.ts, host MST tests |
| I8 | Appeal fee is never the appellant's back | claim_appeal_refund returns the bond portion (+ bounty reward); the consumed per-juror fee never refunds on the Final path; on Failed the whole deposit returns because its only destination earned nothing |
claim_appeal_refund (ADR-0033, bean accord-xftx) |
appeal.spec.ts |
Standing review discipline¶
- Every entry above names its evidence. An ADR without a test pointer, or a test without a row here, is a doc bug.
- The "who wins when nothing happens" audit (attack #6's discipline): every gate — window expiry, crank lapse, pause, Failed exhaustion — must answer "which party benefits if this simply never fires?" If the answer is anyone other than "nobody / refunds," it is a finding.
- New attack surfaces (new instructions, new roles) get a row here in the same change — this page is part of the definition of done, in the same sense as the SPEC.
See also: Trust Profile (who is trusted, the security-value ceiling) · Stake Accumulator (why no fraud proofs) · Sortition & VRF (the draw trust chain) · Prior Art (how other systems answer the same attacks).