feat(tier2): close C1 (BFT, 2-of-3) and C2 (owner timelock)#5
Merged
Conversation
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes both surviving audit CRITICALs.
C1 — Byzantine fault tolerance (was 2-of-2 on one box)
rotateAttesters(the economics-frozen path).collect_attestationsstopped 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 onlythresholdsigs, 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=1deploys aTimelockController, 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 → waitTIMELOCK_DELAY→ execute.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.mdhas the ceremony. One non-code decision flagged:TIMELOCK_ADMINmust 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