Skip to content

feat(tier2): close C1 (BFT, 2-of-3) and C2 (owner timelock)#5

Merged
drewstone merged 2 commits into
mainfrom
feat/tier2-bft-custody
Jun 12, 2026
Merged

feat(tier2): close C1 (BFT, 2-of-3) and C2 (owner timelock)#5
drewstone merged 2 commits into
mainfrom
feat/tier2-bft-custody

Conversation

@drewstone

Copy link
Copy Markdown
Contributor

Closes both surviving audit CRITICALs.

C1 — Byzantine fault tolerance (was 2-of-2 on one box)

  • Third attester stood up on an independent host (Helsinki, different DC from the Nuremberg fleet); key generated on-box, never exported.
  • Book 0x0 rotated to 2-of-3 live on Base Sepolia via rotateAttesters (the economics-frozen path).
  • Proven live: a batch co-signed 3-of-3 across both datacenters (tx).
  • Liveness bug the 3rd operator surfaced + fixed: collect_attestations stopped at threshold, so a peer that never saw a settling proposal kept settled orders and re-proposed them into an on-chain Overfill revert when next elected. Now every peer receives the proposal concurrently (futures::join_all) — quorum still needs only threshold sigs, but all peers prune. Also bounds a round at one peer-timeout instead of peers×timeout.

C2 — owner custody (was single EOA)

  • Deploy.s.sol USE_TIMELOCK=1 deploys a TimelockController, hands it ownership, bootstraps the Ownable2Step accept + raises the delay inline, then renounces the deployer's admin → born owned-by-timelock, self-secured. Every privileged call then goes schedule → wait TIMELOCK_DELAY → execute.
  • Proven on anvil (scripts/timelock-anvil.sh) and CI-gated (test/Timelock.t.sol: delay enforced, EOA can't act, rotation applies only after the delay via the timelock).
  • docs/ops/owner-timelock.md has the ceremony. One non-code decision flagged: TIMELOCK_ADMIN must be a Gnosis Safe with independent signers — C2 closes at the first value-bearing deploy with that set.

80 forge + 47 Rust green. Live 2-of-3 fleet is already running the broadcast fix.

🤖 Generated with Claude Code

…thority binding live

SurplusSettlement v4 0x64867eacf2e4581d182c2Be634cfD7fF3D3d9f83 on Base
Sepolia (same tsUSD token): the merged main contract — v3 authority binding,
proof-of-service receipts + holder-challenge window, spend keys, book-scoped
domains. Book 0x0 registered 2-of-2 [op3, op4]; 5,100 tsUSD collateral each
(the G2 funding gate correctly refused RFQs at the old $100 — the MM's
resting book is a real commitment). Fleet binary at origin/main; app deployed
with the v4 address + Portfolio key-mint UI.

Proven live, in order:
- shared CLOB batch: book 0x0 nonce 0→1, calldata-verified 2-of-2 quorum
  (tx 0x9e7c8eb0…)
- firm RFQ buy: 100k sonnet-output at $13.246/M vs $15 list (12% off),
  settleFills tx 0xc3f47d5d…
- spend rail: ONE signature → bearer key → REAL Claude completion through
  the vanilla OpenAI surface → 28 tokens metered → settleSpend on-chain,
  lot 99,972 → 99,944, spendSettled=28 (scripts/spend-live.mjs, rerunnable).

App/scripts/docs repointed to v4; pre-cutover tx hashes reference v2/v3.
Audit Tier 2 — the two surviving CRITICALs.

C1 — Byzantine fault tolerance. The fleet ran 2-of-2 attesters with both keys
on one Hetzner box: zero fault tolerance, one host compromise drains custody.
Added a third attester on an INDEPENDENT host (Helsinki, different DC from the
Nuremberg fleet), key generated on-box and never exported, and rotated book
0x0 to a 2-of-3 quorum live on Base Sepolia (rotateAttesters, the
economics-frozen path). Proven: a live batch co-signed 3-of-3 across both
datacenters (tx 0x5bb9d890...).

  The third attester surfaced a real liveness bug: collect_attestations
  stopped at threshold, so a peer that never saw a settling proposal kept the
  settled orders in its pool and, when next elected, re-proposed them into an
  on-chain Overfill revert (observed the moment the quorum grew to 3). Fixed:
  every peer now gets the proposal concurrently (futures::join_all) — quorum
  still needs only the threshold sigs, but all peers prune. Also bounds a
  round at one peer-timeout instead of peers x timeout.

C2 — owner custody. A single-EOA owner can rotate attesters / set the SP1
verifier instantly. Deploy.s.sol gains USE_TIMELOCK=1: deploys a
TimelockController, hands it ownership, bootstraps the Ownable2Step accept and
raises the delay inline, then renounces the deployer's admin — the contract is
born owned-by-timelock, self-secured, no EOA shortcut. Every privileged call
must then schedule, wait TIMELOCK_DELAY, execute. Proven on anvil
(scripts/timelock-anvil.sh) and CI-gated (test/Timelock.t.sol: delay enforced,
EOA can't act, rotation only after the delay). docs/ops/owner-timelock.md has
the ceremony; the one non-code decision (a Gnosis Safe as TIMELOCK_ADMIN) is
flagged — C2 closes at the first value-bearing deploy.

80 forge + 47 Rust green; live 2-of-3 fleet running the broadcast fix.
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