# Uroboros — AUDIT-3 (pre-launch, after step 3)

Date: 2 Nov 2026 · Scope: all of `src/` as one system (Sanctum, SanctumFactory, Sanctum7702, Subscription, Blacklist, BountySanctum, Uroboros, MinterProxy, UroborosHook + HookDeployer/HookMiner, SeederV4, LPLocker/LPLockerV4, FeeRouter, TeamVesting) and `script/Deploy.s.sol`. `site/` not reviewed. **Nothing in `src/` was changed.** PoCs and proofs: `test/Audit3.t.sol` (22 tests, all green: a finding test asserts the behaviour that exists today, a `_verify_` test pins a fix or a proof).

Results:
- `forge test` → **201 passed / 0 failed / 2 skipped** (179 before + 22 Audit3).
- `FOUNDRY_FUZZ_RUNS=3000 forge test --match-test testFuzz_A3` → 2 × 3000 runs green (rolling-24h strict-window proof, §2).
- `BASE_RPC=https://mainnet.base.org forge test --match-contract 'ForkTest|Audit2ForkTest'` → **55 passed**.
- Anvil Base-fork rehearsal (block 53405171) with a **fresh random deployer** (`cast wallet new`, no code, nonce 0; TEAM/TREASURY/guardians/robber also fresh): deploy → seed 2 ETH → light → bounty → extend, each re-run idempotent. End state checked by `cast` (§7).

---

## 0. Verdict

The AUDIT-2 fixes are in the code and do what they claim (§1 table). The clip formula is right for a full-range, native-ETH-as-currency0 pool and matches the ETH the locker actually withdraws to 1e-9. The 25-bucket rolling day is a real strict-24h bound: proven on paper (§2) and by a 6,000-run fuzz that checks every window ending at every payout. No High findings. No path moves value without the guard/CEI it needs.

What remains is **trust and wording**, not broken maths: the bounty's cold key (dev EOA) is not "tighten-only" — it can take the whole prize in one block, and the contract's own NatSpec says otherwise (M1); the owner can route **75%** of fee inflow to himself before day 30, not 70% as STEP2-FIXES says (M2); and a griefer can lock the raid game and fake the leaderboard by raiding into the Sanctum itself (L1).

**GO for launch after step 3**, with two small blocking items (§6): a two-line `BountySanctum` change (+ its NatSpec) before it is deployed, and the site disclosures in §5.

---

## 1. AUDIT-2 fixes — re-verified in code

| AUDIT-2 item | What the code does now | Checked how | Status |
|---|---|---|---|
| **H1 clip** | `maxClip = poolEthReserve·100/10000`, `poolEthReserve = L·2^96/sqrtPX96` (`FeeRouter.sol:285-294`). For a full-range position, amount0 = L·(1/√P − 1/√P_B); the 1/√P_B term (√P_B ≈ 2^64·…) is ≤ 1e-18 relative, so L/√P is the ETH side, slightly over-stated, never under-stated. currency0 = native is enforced by `bindPool` (`currency0 == 0`, `currency1 == token`) and by the hook's `beforeInitialize`. `getLiquidity` = active liquidity = the one full-range position (no other LP can add). | `test_A3_verify_clip_reserve_equals_withdrawable_eth`: after buys/sells, `poolEthReserve()` vs the ETH `LPLockerV4.withdraw()` really returns → equal within 1e-9, `out ≤ reserve`. Rehearsal: reserve 2e18, maxClip 2e16 at seed. | ✓ |
| H1 cooldown | `BUYBACK_COOLDOWN` 10 min on `lastBuyback`; `lastBuyback` set before the swap. | `test_A3_verify_keeper_gate_and_fallback` ("cooldown" right after a buyback), Audit2 `_FIXED_H1_keeper_period_and_cooldown`. | ✓ |
| H1 keeper gate / fallback | Owner or `isKeeper` only until `litAt + 30 d`; stranger allowed when `now ≥ (lastBuyback or litAt) + 7 d`; anyone after day 30. A keeper buyback re-arms the 7-day idle clock. | Same test: stranger "keeper" at +31 min, still refused at lit+7d because the owner bought at +31 min, allowed at +31 min+7 d, everyone after day 30. | ✓ — script has **no `setKeeper` step**: until one is set, only the owner EOA can buy back (I9). |
| H1b window | `canBuyback` refuses before `litAt + LIMIT_WINDOW` and before light. | Audit2 `_FIXED_H1b_*`. | ✓ |
| **M1 CEI + nonReentrant** | `distribute` zeroes each bucket before `_push`, restores on failure; `distribute`, `buybackAndBurn`, `creditSniperExcess` share `nonReentrant`; `unlockCallback` requires `_busy`. | Audit2 `_FIXED_M1*`; `test_A3_verify_reverting_destination_keeps_bucket` (reverting bounty destination keeps 1 ETH bucket, `held ≤ balance`, paid once fixed). | ✓ |
| Every ETH-pushing path (system-wide) | FeeRouter `distribute` (guarded, CEI) · `buybackAndBurn` (guarded; ETH only to the PoolManager) · Sanctum `_send` from `withdraw`/`coldWithdraw` (window/bucket state written before the call) and `executeQueued` (`done = true` before both sends; fee goes to FeeRouter whose `receive` is storage-only) · Subscription `pay` (ledger before router + refund) · LPLockerV4 `_flush` (`liquidity = 0` / `seeded` first) · SeederV4 refund (`done` first) · BountySanctum `_topUp` (forward only) · Sanctum7702 `sweep` (to the vault). | Read + `test_A3_L2_fake_token_events_and_reentry_respect_caps` (a token's `transfer` re-enters the bounty key mid-withdraw: exactly one ETH room paid, the second re-entrant raid fails). | ✓ no unguarded value path. BountySanctum updates its counters after the external call (cosmetic only). |
| **M2 bands** | `setSplits`: sum == 10000; buyback ≥ 25%, team ≤ 25%, rewardPool ≥ 20%, bounty ≤ 15%, backstop ≤ 10%. All five bounded on the side that matters. | `test_A3_M2_owner_can_route_75pct_before_day30`: the legal split 25/25/25/15/10 + all destinations = owner → **7.5 of 10 ETH** to one key. | ✓ code; **STEP2-FIXES "70%" is wrong → 75%** (M2 below). |
| M2 destinations delay | `destinationsDelay()` = 0 while `litAt == 0 || now < litAt + 30 d`, else 48 h; a re-proposal replaces the pending one and restarts the timer; `applyDestinations` anyone. | `test_A3_verify_destination_delay_boundary`: lit+30d−1 s immediate; lit+30d delayed; apply at +47 h "delay"; re-propose at +47 h resets; applies at +95 h. | ✓ (a change proposed at lit+30d−1 s is still instant — publish the cut-off). |
| **H2 MinterProxy** | 48 h `DELAY`; `proposeTarget` and `applyTarget` both require code; `cancelTarget` clears; `mint` requires `msg.sender == target && target != 0` and `token != 0`; `Uroboros.minter` = proxy, immutable. No path mints without an applied target. | `test_A3_I4_minter_proxy_forwarder_mints_30M_after_48h`, Hook `test_minter_proxy_48h_contract_only_target`; rehearsal: `target = 0`, owner = deployer, `minter() == proxy`. | ✓ — but "contract-only" is not a trust barrier (I4). `Ownable` is single-step (L4). |
| **H2 lockers two-step** | `transferBeneficiary` (current only; 0 cancels) → `acceptBeneficiary` (named address). Current keeps every right until acceptance; transfer never touches `unlockAt`; extend stays strictly-later. | `test_A3_verify_locker_two_step_no_bypass`: old keeps `extend` while pending, pending can't extend, pending overwritten, stale pending can't accept, after accept the old key loses extend/withdraw/transfer, new can't shorten, withdraw only by new. | ✓ no lock-out, no extend-only bypass. |
| **L1 partial fill** | `afterSwap` (buy): `-amount0 == ethIn − ethIn·feeBps/1e4` else "partial fill"; sells unchanged (fee on realised ETH). | `test_A3_verify_partial_fill_only_blocks_binding_limits`: a non-binding limit (−10% √P) buys fine; a binding one reverts, FeeRouter balance unchanged. Audit2 `_FIXED_L1` covers price-limited sells. | ✓ — legit buys are only broken when the client sets a **binding** price limit; they revert instead of partial-filling. Slippage on buys must be expressed as min-out, not a price limit (state in the widget). |
| L1b liquidity > 0 | `beforeSwap` requires `getLiquidity(poolId) > 0` both directions. | Audit2 `_FIXED_L1b` (local + fork). | ✓ |
| L2 trial | `createWithRules` → no trial. | `test_A3_L3_trial_recycled_same_block_via_coldSetRules` — **bypassed** (L3). | ✗ (no value at stake until step 5) |
| I7 gas-report | Runtime nonce-gap probe in `LaunchFixture`. | Suite runs. | ✓ |
| §5 script risks | Env guard, decimals `SEED_ETH`, `STEP=extend`, `STEP=verify`. | Rehearsal §7. | ✓ — EOA check on beneficiaries not added (L5). |

---

## 2. Sanctum rolling 24h (25 buckets) — proof and worst cases

**Claim.** For any strict window W = (T, T+24h], outflow through the capped paths (`withdraw` direct, `coldWithdraw`) in W ≤ `dayCapBps` × (balance at T + deposits in W).

**Proof.** An outflow at time t sits in bucket H = ⌊t/3600⌋ and is counted while `H + 25 > h_now`, i.e. until h_now = H+25, which is ≥ t + 24h + (3600 − t mod 3600) > t + 24h. So at any payout at time E ∈ W, every earlier payout in W is inside `spent24`, plus possibly older ones (O_pre ≥ 0). The check is `spent24 + a ≤ (bal + spent24)·d` with bal = B_T − O_in + D_in (balance before the send). Substituting: O_in + a ≤ (B_T + D_in)·d − O_pre·(1−d) ≤ (B_T + D_in)·d, since d ≤ 1. ∎ (Queued releases and the queue fee do not touch buckets — they are the delayed, cancellable path.)

**Test.** `testFuzz_A3_verify_rolling24h_strict_window_default` (5/10) and `..._bounty_rules` (1/2): 40 steps per run, time steps aimed at hour boundaries (−1 s, exact, +1 s), short hops and gaps up to 3 h, random deposits 1–21 ETH, raid sizes 0–149% of `capRoom` (over-cap must revert, inside must pay); after every payout the test checks **every** window ending now that starts at now−24h or at any event inside it, against an independent ledger. 3,000 runs each: no violation.

**Fixed hourly window (documented, kept).** The hourly window starts at the first spend after expiry, so two hourly caps can land one second apart. A sliding 60-min interval can touch at most two windows (window k+2 starts ≥ 2 h after window k).

| Rules | Worst 1 h (two caps 1 s apart) | Then | Worst strict 24 h | PoC |
|---|---|---|---|---|
| Default 5%/h · 10%/24h | 5% + 4.75% = **9.75%** | 0.25% more in the next hour, then nothing until the buckets age out (24–25 h) | **10%** of (balance at window start + deposits) | `test_A3_I1_hourly_double_raid_default_quantified` |
| Bounty 1%/h · 2%/24h | 1% + 0.99% = **1.99%** (0.00995 ETH of 0.5) | 0.01% next hour | **2%** of (prize at window start + top-ups) | `test_A3_I1_hourly_double_raid_bounty_quantified` |

So on a default Sanctum a stolen hot key gets ~10% within seconds and nothing more for ~24 h — the 1-hour cap does not buy reaction time; the daily cap and the queue do. Over 48 h the bound is ≤ 2 × daily (≈ 19% default, ≈ 4% bounty, compounding on the shrinking balance).

---

## 3. Findings

Severity = impact × likelihood at launch size (2 ETH pool, 0.5 ETH bounty).

### MEDIUM

#### M1 — Bounty cold key (dev EOA) can loosen the rules and take the whole prize in one block; the code says it can only tighten
- **Where:** `Sanctum.coldSetRules` (`Sanctum.sol:366-371`, no tighten-only check, applies at once), `coldWithdraw`, `setOwner`; `BountySanctum.sol:21` NatSpec "cold = admin can only tighten them further"; STEP3-REPORT limits "or tighten rules".
- **Issue:** `coldSetRules(100%, 100%, …)` + `coldWithdraw(prize)` empties the bounty in one block, including every 10% FeeRouter top-up. The same key can `setOwner` (hijack the public hot key) or `unfreeze` a guardian freeze first. This is the spec's trust model (cold = admin), but the verified source will tell readers the opposite, and the "raid" prize is custodial: the 10% bounty share of fees flows into a vault one EOA can empty.
- **PoC:** `test_A3_M1_bounty_cold_loosens_rules_and_takes_prize_in_one_block` (3.5 ETH prize → 0 in one prank block).
- **Fix:** before deploy, correct the NatSpec ("the cold key (admin) can change or loosen the rules, rotate the key and withdraw the prize at any time"). Optional hardening: bounty cold = the Safe (not the dev EOA) from day 0 — `BOUNTY_COLD` env already exists. Disclose (§5).

#### M2 — Before day 30 the owner can route **75%** of ETH fee inflow to himself (STEP2-FIXES says 70%)
- **Where:** `FeeRouter.setSplits` bands + `proposeDestinations` (immediate until lit+30d).
- **Issue:** 25/25/25/15/10 is legal (sum 100, buyback ≥ 25, rp ≥ 20, team ≤ 25, bounty ≤ 15, backstop ≤ 10), and all four pushed destinations may be the owner. Only the 25% buyback floor is guaranteed.
- **PoC:** `test_A3_M2_owner_can_route_75pct_before_day30` (7.5 of 10 ETH).
- **Fix:** none required by spec; disclose **75%**. Cheaper hardening if wanted: `MIN_BUYBACK_BPS = 3500` or `MAX_BOUNTY_BPS` + `MAX_BACKSTOP_BPS` paid only to non-owner addresses (cannot be enforced on chain cleanly). Fix the STEP2-FIXES sentence.

### LOW

#### L1 — Self-loop raid (`to = sanctum`) burns the cap room, locks honest raiders out, and fakes the leaderboard
- **Where:** `BountySanctum.withdraw` checks `to != address(this)` but not `to != address(sanctum)`.
- **Issue:** sending the raid back into the Sanctum spends hourly/daily room without moving the prize. One griefer, a few txs per hour (cents on Base), keeps `raidableNow()` at 0 for everyone else indefinitely, and `totalPaid` / `Attempt` / the indexer's leaderboard count it as loot. (Self-loops do not lower the balance, so each re-opens 2% of itself as day room; the griefer mops it up in a few calls.)
- **PoC:** `test_A3_L1_self_loop_raid_burns_caps_and_fakes_leaderboard` — 24 h of self-loops: prize unchanged, `totalPaid` ≈ 0.0102 ETH "extracted", honest `withdraw(1 gwei)` → "over cap" every hour.
- **Fix (BountySanctum is not deployed yet — do it):** `require(to != address(this) && to != address(sanctum), "to");`. Indexer: ignore `to == sanctum` regardless.

#### L2 — Arbitrary "token" addresses: fake `Attempt` / `Sent` amounts and inflated `attempts`
- **Where:** `BountySanctum.withdraw(token, …)` accepts any address; `Sanctum.capRoom/withdraw` call its `balanceOf`/`transfer`.
- **Issue:** a contract returning `balanceOf = 1e30` and a no-op `transfer` produces `Attempt(robber, fake, …, 1e28, 1e28)` and `Sent` events and bumps `attempts`. Its `transfer` can re-enter the key; the ETH caps still hold (verified). No value at risk; the leaderboard/feed is.
- **PoC:** `test_A3_L2_fake_token_events_and_reentry_respect_caps`.
- **Fix:** in BountySanctum `require(token == ETH)` (the spec's prize is ETH; stray ERC-20s already go to `sweep`), or count `attempts` only for ETH. Indexer: rank and display ETH only.

#### L3 — Trial gate bypassed: one $100 deposit gets a fresh 7-day trial every tx (AUDIT-2 L2 fix incomplete)
- **Where:** `SanctumFactory.create` (trialEligible = true) + `Sanctum.coldSetRules` (instant loosening) + `coldWithdraw`.
- **Issue:** create with default rules and $120 → trial; same block: cold loosens to 100% and withdraws everything; repeat. The comment in `Subscription.sol` ("one trial per ~6 h") is wrong — it is one per tx.
- **PoC:** `test_A3_L3_trial_recycled_same_block_via_coldSetRules` (5 trials, $120, one block).
- **Fix:** not needed for launch (coverage has no on-chain value until step 5). Before the watcher market: `trialUsed[creator]` and/or watchers/WorkRewards read TVL at freeze time with a minimum age, never `coveredUntil` alone. Fix the comment.

#### L4 — Single-step `Ownable` on FeeRouter, MinterProxy, Blacklist
- **Issue:** the day-30 `transferOwnership(timelock)` is irreversible on a typo or a wrong-chain address; FeeRouter/MinterProxy ownership controls fee destinations and the 30M mint target.
- **Fix:** `Ownable2Step` (one import, one word per contract) before deploy, or a dry-run `cast call timelock "getMinDelay()"` gate in the handoff script.

#### L5 — Deploy-script gaps
1. `TEAM`/`TREASURY`/`LP_BENEFICIARY` are required but **not checked for code** (AUDIT-2 asked for an `ALLOW_EOA=1` override); the rehearsal accepted the deployer EOA as LP beneficiary. TeamVesting's beneficiary is immutable — a typo there is permanent.
2. No assertion `FeeRouter.hook() == hook` after a resume that redeployed the hook — the mismatch only surfaces as a revert in `seed` (safe, but late and cryptic).
3. `STEP=bounty` after day 30 re-proposes destinations on every run (`fr.bounty() != sanctum` until applied), restarting the 48 h timer each time.
4. No `STEP=keeper` (`setKeeper`) — until set, only the owner EOA can buy back for 30 days (or the public 7-day fallback).
5. `STEP=verify` encodes `MinterProxy`/`FeeRouter` constructor args from **current** `owner()` — wrong after the day-30 handoff (verify before). No verify line for `BountySanctum` or the factory-created bounty `Sanctum`.
6. `broadcast/Deploy.s.sol/8453/` holds earlier **anvil** rehearsal runs under the Base chain id; delete it before mainnet so `--resume`/`run-latest.json` can never point at rehearsal transactions.
7. Mid-step failure: the state file is written during simulation (AUDIT-2 §5.4), so a failed broadcast leaves addresses with no code; `_live()` skips them correctly on re-run (verified: every stage re-run → "No transactions to broadcast"). Nonce coupling holds only if the deployer sends nothing else between `locker` and `seeder` — the abort message is correct.

### INFO

- **I1 — hourly fixed window** quantified in §2 (default 9.75%/1 s, bounty 1.99%/1 s; daily caps exact).
- **I2 — the cold key is the master key on every Sanctum.** A stolen cold key unfreezes, loosens and withdraws everything in one tx; guardians are then muted 24 h and could not have stopped it. `test_A3_I2_stolen_cold_key_drains_frozen_sanctum_in_one_tx`. (Spec-consistent; must be on the site.)
- **I3 — admin-chosen guardians can freeze the bounty indefinitely** (re-panic in the last hour, rotating): `test_A3_I3_guardians_can_freeze_bounty_indefinitely` (6 days at 0). Only the cold key unfreezes (and that mutes the guardians 24 h).
- **I4 — MinterProxy "contract-only target" is not a trust barrier**: a 10-line forwarder lets one EOA mint all 30M after the 48 h notice (`test_A3_I4_…`). The 48 h public `TargetProposed` event is the protection; there is no rate limit in the proxy. Site must watch and show it.
- **I5 — test bytecode ≠ deployed bytecode.** Any test that imports `PoolManager` (=0.8.26) forces its whole import graph to solc 0.8.26 / evm **cancun** (foundry falls back; 0.8.26 has no prague): `out/FeeRouter.sol`, `out/Sanctum.sol` are 0.8.26/cancun artifacts. The script compiles 0.8.28/prague — rehearsal CBOR tail `…64736f6c634300081c` (0.8.28) on all 11 deployed contracts. `STEP=verify` flags (`v0.8.28+commit.7893614a`, `--evm-version prague`, `--optimizer-runs 200`, optimizer on, no via-IR, metadata `ipfs`) match `foundry.toml` and the deployed bytecode. Only the fork/anvil rehearsal exercises the exact deployed bytecode — keep running it.
- **I6 — event gaps for the site:** `LPLocker.withdraw` (treasury tranches) emits nothing (read the token `Transfer`); `FeeRouter.burnHeld` emits nothing of its own; a failed `_push` emits nothing (read the bucket views); `Blacklist.setOracle` emits nothing. Everything else the site needs is evented (`Attempt`, `TopUp`, `Panic`, `Unfrozen`, `RulesApplied` (both hot and cold), `GuardiansSet`, `Recovery*`, `Distributed`, `Sent`, `SplitsSet`, `Destinations*`, `KeeperSet`, `BuybackBurn`, `Extended`, `Beneficiary*`, `Target*`, `Lit`, `FeeTaken`).
- **I7 — non-ETH fees are off-ledger:** USDC subscriptions and ERC-20 queued-transfer fees sit in FeeRouter and leave only via the owner's `sweepToken(any erc20, any to)` — not split, not in `totalInflow`.
- **I8 — gas-heavy destination:** `_push` forwards all gas; a destination that burns it can make `distribute` need a higher gas limit (63/64 rule). Owner-set, not a DoS with a normal limit.
- **I9 — unset destinations at launch:** rewardPool, team, backstop are `address(0)` after the script → their 30/20/5% accrue in FeeRouter (rehearsal: `teamBucket` 0.1 ETH after a 0.5 ETH distribute). Correct per spec (pool not built), but say so.
- **I10 — recovery leaves the old hot key in place** (`test_A3_verify_recovery_executes_while_frozen_hot_key_survives`); the new cold key must `setOwner`. Recovery executes while frozen (spec). Colluding guardians can re-propose after every cancel; the cold key must answer within each 7-day timer, then shrink the set (`test_A3_verify_colluding_guardians_loop_until_set_shrinks`). `watchersOn` with a zero registry is a no-op ("standing") — verified.
- **I11 — no-queue property holds even if the admin sets `delayBps < capBps`**: the raid reverts "queued" and the push is rolled back (`test_A3_verify_no_queue_even_with_low_delayBps`). No `executeQueued` passthrough exists. `topUp`/`receive` forward at once; `sweep` sends ERC-20s to the *current* cold key; force-sent ETH (selfdestruct) would be stuck in the key (negligible).
- **I12 — seed dust:** the deployer keeps 543 wei UROBOROS after the seed (rounding refund). Harmless; don't call it "zero team tokens" without the footnote, or burn it.
- **I13 — Uroboros mint accounting:** `totalMinted` is cumulative (burns never reopen the cap); `mintable() = CAP − totalMinted`; `totalSupply = totalMinted − burned`. ✓ (`test_A3_I4` ends at `totalMinted == CAP`, next mint "cap").

---

## 4. Cross-contract notes

- **Subscription → FeeRouter:** `pay` sends ETH to `receive` (storage-only, never reverts) ✓; `payUsdc` → USDC sits for `sweepToken` (I7).
- **Factory → Subscription:** `register` is factory-only, one-shot `setFactory` by the Subscription deployer; the script binds it (rehearsal: `factory()` = factory). Bounty Sanctum registered without trial (`coveredUntil = 0`) ✓.
- **FeeRouter → bounty:** destination = the bounty **Sanctum** (plain payable `receive`). Rehearsal: 0.5 ETH inflow → `distribute` → prize +0.05 ETH (exactly 10%). A cold-key change on the bounty Sanctum does not affect the push (the address is the Sanctum). A reverting destination keeps its bucket, nothing lost (`test_A3_verify_reverting_destination_keeps_bucket`). If the admin ever rotates the Sanctum's hot key away from `BountySanctum`, the 10% keeps flowing into a vault that is no longer a game (disclose under M1).
- **Hook → FeeRouter:** `take` lands in unguarded `receive`; `creditSniperExcess` is guarded but only called in the first 60 s, when no buyback can run (window gate) ✓.
- **address(0)/no-code assumptions:** FeeRouter destinations 0 = hold ✓; Sanctum blacklist 0 = off ✓; factory `watcherRegistry` 0 ✓; MinterProxy target 0 = no mint ✓; LPLocker pending 0 = cancel ✓; Subscription/Factory/Hook/Seeder/Locker constructors reject 0 where it matters ✓.

---

## 5. Site disclosures — exact sentences

Use these verbatim (or stronger); numbers are from the code.

**Sanctum (every user vault)**
1. "A stolen hot key can move at most 5% per hour and 10% per rolling 24 hours of each asset; anything larger waits 6 hours in a queue your cold key can cancel."
2. "The hourly limit is a fixed one-hour window, so up to 9.75% can leave within a few seconds across a window boundary. The binding guarantee is 10% per rolling 24 hours."
3. "Your cold key is the master key. Whoever holds it can unfreeze, change every rule and withdraw everything in a single transaction. Guardians cannot stop a stolen cold key. Keep it offline."
4. "Guardians can freeze your Sanctum for 24 hours and, by threshold vote, start a 7-day cold-key recovery that your cold key can cancel. Guardians acting together can keep proposing again after each cancel. If that happens, remove them with your cold key."
5. "After a recovery the old hot key is still set. The new cold key must replace it."
6. "Queued transfers pay a 0.25% fee to the FeeRouter. Direct capped withdrawals and cold-key withdrawals are free."
7. "The free trial is a bookkeeping entry only. It gives no protection on chain, and coverage has no on-chain effect until the watcher market launches."

**Raid the Sanctum (bounty)**
8. "The prize sits in a real Sanctum: 1% per hour and 2% per rolling 24 hours can be raided by anyone. Up to 1.99% can go in two transactions a few seconds apart. Anything above the caps reverts. Nothing is ever queued."
9. "The bounty's cold key is the team's admin key (`<address>`). It can change or loosen the rules, replace the public key and withdraw the whole prize at any time. Every such action is an on-chain event and is listed here."
10. "The admin-chosen guardians (`<addresses>`) can freeze the game for 24 hours at a time and can renew the freeze. While it is frozen nobody can raid."
11. "10% of FeeRouter ETH inflow tops up the prize. It is held in that admin-controlled Sanctum."
12. "Raids are first come, first served. Bots will compete for each hour's room."
13. "Leaderboard: ETH raids to outside addresses only." (and L1/L2 fixed or filtered in the indexer)

**Token / fees / admin**
14. "Until `<lit + 30 days, date>` the FeeRouter owner is one developer key (`<address>`). It can change fee splits within the bands and send up to 75% of all ETH fee inflow to addresses it chooses, effective immediately. Only the 25% buyback-and-burn share is guaranteed. After that date every destination change waits 48 hours."
15. "Bands, enforced on chain: buyback ≥ 25%, reward pool ≥ 20%, team ≤ 25%, bounty ≤ 15%, backstop ≤ 10%. Until the reward pool, team and backstop addresses are set, their shares are held in the FeeRouter."
16. "USDC subscription payments and non-ETH fees are not split on chain. The owner moves them manually, and every move is a `TokenSwept` event."
17. "Buybacks: at most 1% of the pool's ETH per buyback, at most one every 10 minutes, never in the first 30 minutes. For the first 30 days only the owner or appointed keepers can trigger them, unless no buyback has happened for 7 days. After that anyone can."
18. "The 30M work-emission supply is mintable only by the MinterProxy's target. No target is set at launch, so nothing can be minted. Setting one is announced on chain 48 hours before it takes effect. A target can be any contract, so read it when it is proposed."
19. "The LP position is locked until `<unlockAt>` (extend-only). Its beneficiary is `<address>` and can be handed over in two steps. Every extension and hand-over is listed here."
20. "Buys with a price limit that would stop part-way revert instead of filling partially. Set slippage as a minimum output."
21. "Swap fee: 3% in ETH on buys and sells. During the first 60 seconds it falls from 60% to 3%, and the part above 3% goes to buyback-and-burn. For the first 30 minutes, max 1,000,000 UROBOROS per buy and per sending wallet (`tx.origin`)."
22. "No external audit. The bounty Sanctum is the live test."

---

## 6. GO / NO-GO for launch after step 3

**GO**, conditional on:

Blocking (small, before `STEP=bounty --broadcast`; BountySanctum is not deployed yet):
- [ ] **L1/L2:** `BountySanctum.withdraw`: `require(to != address(this) && to != address(sanctum), "to")` and `require(token == ETH, "eth only")` (or count attempts for ETH only). Re-run `Bounty.t.sol` + `Audit3.t.sol` (flip `test_A3_L1`/`test_A3_L2` to expect reverts).
- [ ] **M1:** correct the BountySanctum NatSpec (cold can loosen and withdraw), and publish sentences 9–11.
- [ ] **§5** disclosures on the site, especially 3, 9, 14 (75%, not 70%).
- [ ] Delete `broadcast/Deploy.s.sol/8453/` (anvil runs under the Base chain id); fresh single-purpose deployer key; decide `LP_BENEFICIARY` (Safe, or deployer + transfer on day 30).

Should (cheap, before deploy): `Ownable2Step` on FeeRouter/MinterProxy/Blacklist (L4); `BOUNTY_COLD` = Safe; `setKeeper` right after `light`; code-check or explicit confirmation of TEAM (immutable); add verify lines for BountySanctum + bounty Sanctum; fix the STEP2-FIXES "70%" and the `Subscription.sol` "~6 h" comments.

Before step 5 (watcher market): L3 trial farming; watchers must never pay on `coveredUntil` alone.

Not blocking: I1–I13.

---

## 7. Anvil rehearsal (fresh key) — end state

Fork `https://mainnet.base.org` @ 53405171, chain 8453. Deployer `0x95Fe…6D7A` (new, code 0x, nonce 0, `anvil_setBalance` 10 ETH); TEAM, TREASURY, 3 guardians and a robber also fresh. No anvil default accounts used.

| Step | Result |
|---|---|
| deploy without TEAM | `MISSING env: TEAM`, nothing sent |
| deploy `--broadcast` | 19 txs, success; re-run "No transactions to broadcast" |
| seed `SEED_ETH=2` | liquidity 1e22; re-run "already seeded" |
| light | `LIT` |
| bounty (3 guardians) | prize 0.5 ETH, leak/day 0.01 ETH; re-run no txs |
| extend +30 d | `1794204489 → 1796796489`; re-run "nothing to do" |

State read back with `cast`: totalSupply 70M; `minter` = MinterProxy (owner deployer, target 0, token bound); TeamVesting 10M → TEAM; 4 × 2.5M treasury at +90/180/270/365 d → TREASURY; LPLockerV4 beneficiary deployer, liquidity 1e22, unlock extended; hook bound, lit; `FeeRouter.hook == hook`, poolBound, reserve 2 ETH, maxClip 0.02 ETH; `FeeRouter.bounty` = bounty Sanctum, other destinations 0; bounty Sanctum owner = BountySanctum, cold = deployer, rules 100/200/10000/86400/false, 3 guardians threshold 2, watchersOn false, `coveredUntil` 0; deployer holds 543 wei UROBOROS (I12). Live raid by the fresh robber: `raidableNow` 0.005 ETH paid, `withdraw(1 wei)` → "over cap". 0.5 ETH FeeRouter inflow + `distribute` → prize +0.05 ETH. All 11 deployed contracts carry solc 0.8.28 CBOR. `STEP=verify` prints 15 commands with the flags in I5. Rehearsal state file deleted afterwards.

---

## Files

- `/home/claude/sanctum-next/test/Audit3.t.sol` — `Audit3SanctumTest` (14), `Audit3LaunchTest` (8).
- `/home/claude/sanctum-next/AUDIT-3.md` — this report.
