Crownridge — Deployment Runbook
Status: Crownridge is not deployed to any public network and has not been externally audited. This document is the procedure by which deployment will happen. The mainnet gate (§9) — including an independent security audit and economic review (spec §88, §170) — must be fully cleared first. Deploying without clearing it is prohibited.
The deployment path is: local → Robinhood Chain testnet (chainId 46630) → Robinhood Chain mainnet (chainId 4663). Testnet is never skipped (spec §110, §129). All contracts are immutable — no proxies (spec §53, §162): a mistake at deploy time is corrected by redeploying, not upgrading, so every assertion in this runbook exists to make a bad deploy impossible to miss.
1. Prerequisites
Everything below must exist before any broadcast. Items marked (founder) come from the founder
action list in CLAUDE.md §11.
| # | Prerequisite | Detail |
|---|---|---|
| 1 | Deployer key | Fresh key per environment (never reuse the testnet key on mainnet — §8). Funded with ETH for gas on the target chain (Robinhood Chain gas is ETH). The deployer retains no privileged role after the script completes — this is asserted, not assumed. |
| 2 | Protocol multisig (founder) | Routine-admin multisig. Becomes the sole proposer on the TimelockController. Verify Safe (or chosen multisig) availability on Robinhood Chain before relying on it. |
| 3 | Security multisig (guardian) (founder) | Emergency multisig. Receives PAUSER_ROLE on MintController/Genesis/Buyback, GUARDIAN_ROLE on the Treasury (immediate founder-withdraw pause), and CANCELLER_ROLE on the timelock (can veto any queued operation). |
| 4 | Founder wallet + mode (founder) | The address wired immutably into FounderTreasuryController, plus an explicit CAPPED / UNCAPPED choice and cap parameters. This wallet holds a real Treasury-withdrawal authority — a disclosed trust assumption (see docs/ECONOMICS.md §7, docs/RISK.md). Choose custody accordingly. |
| 5 | Buyback executor address | Ops address granted BUYBACK_EXECUTOR_ROLE. It has no discretion the contract does not check on-chain; the buyback also ships disabled regardless. |
| 6 | RPC endpoint | A dedicated Alchemy Robinhood Chain endpoint for production. The public RPCs (rpc.mainnet.chain.robinhood.com, rpc.testnet.chain.robinhood.com) are rate-limited — fine for probing, not for a broadcast you cannot afford to have half-complete. |
| 7 | Economics sign-off (founder) | Every economic value in the env file is a placeholder pending sign-off (docs/ECONOMICS.md header). No broadcast with unsigned economics. |
| 8 | Green test suite | forge test (73+ tests: unit, fuzz, 10 invariants) plus the fork suite against live Robinhood mainnet. Commands in §4. |
| 9 | .env prepared on the host |
Copy contracts/.env.example → contracts/.env, fill per §2. Never commit it. |
2. Environment variable reference
Source of truth: contracts/.env.example. "Read by" says which tool actually consumes the value —
Deploy.s.sol reverts on any missing variable it reads.
Network / tooling
| Variable | Example / default | Read by | Notes |
|---|---|---|---|
RH_RPC_URL |
https://rpc.mainnet.chain.robinhood.com |
foundry.toml (rh_mainnet), fork tests |
Use a dedicated Alchemy endpoint in production. |
RH_TESTNET_RPC_URL |
https://rpc.testnet.chain.robinhood.com |
foundry.toml (rh_testnet) |
|
RH_CHAIN_ID |
4663 (testnet: 46630) |
Deploy pre-flight | The script asserts block.chainid == RH_CHAIN_ID. Flip this when targeting testnet. |
BLOCKSCOUT_API_KEY |
(optional) | foundry.toml [etherscan] |
Blockscout usually accepts verification without a key. |
DEPLOYER_PRIVATE_KEY |
(secret) | Deploy.s.sol |
Host only. Never committed, never reused across environments. |
Canonical external contracts (verified on-chain — mainnet values)
| Variable | Mainnet value | Read by |
|---|---|---|
USDG_ADDRESS |
0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168 |
Deploy (bytecode + decimals asserted) |
USDG_EXPECTED_DECIMALS |
6 |
Deploy (asserted against live decimals()) |
UNIV3_FACTORY |
0x1f7d7550b1b028f7571e69a784071f0205fd2efa |
Deploy (bytecode asserted) |
UNIV3_SWAP_ROUTER |
0xcaf681a66d020601342297493863e78c959e5cb2 |
Deploy → Buyback constructor |
UNIV3_QUOTER |
0x33e885ed0ec9bf04ecfb19341582aadcb4c8a9e7 |
not read by Deploy (frontend/ops reference) |
UNIV3_NFPM |
0x73991a25c818bf1f1128deaab1492d45638de0d3 |
Deploy → LiquidityManager constructor |
UNIV3_POOL_FEE |
3000 |
not read by Deploy (used at pool-creation time, §6) |
These are MAINNET addresses. They do not exist at the same addresses on testnet. Before a testnet
deploy, resolve the testnet USDG/Uniswap equivalents and verify them by RPC probe (cast code,
cast call <addr> "decimals()"); if no canonical testnet USDG exists, deploy
contracts/test/mocks/MockUSDG.sol and point USDG_ADDRESS at it. The pre-flight asserts bytecode
and decimals either way, so a wrong address fails loudly.
Governance / roles
| Variable | Default | Notes |
|---|---|---|
PROTOCOL_MULTISIG |
— | Timelock proposer. |
SECURITY_MULTISIG |
— | Guardian: pauser + timelock canceller. |
TIMELOCK_MIN_DELAY |
172800 |
48 hours, in seconds. |
FOUNDER_ADDRESS |
— | Immutable in FounderTreasuryController once deployed. |
Founder withdrawal policy (spec §144 — explicit choice required)
| Variable | Default | Notes |
|---|---|---|
FOUNDER_MODE |
CAPPED |
CAPPED or UNCAPPED. UNCAPPED = direct founder control over Treasury assets. |
FOUNDER_MAX_PER_WITHDRAWAL |
0 |
Raw USDG units (6 decimals); 0 = unused (CAPPED only). |
FOUNDER_ROLLING_LIMIT |
0 |
Raw USDG per rolling window. |
FOUNDER_ROLLING_WINDOW |
86400 |
Seconds. |
FOUNDER_MIN_TREASURY_FLOOR |
0 |
Raw USDG that must remain (CAPPED only). |
FOUNDER_COOLDOWN |
— | Seconds between withdrawals. Required by Deploy.s.sol (vm.envUint("FOUNDER_COOLDOWN")) but currently missing from .env.example — add it to your .env explicitly or the script reverts. |
Token
CRWN_NAME / CRWN_SYMBOL appear in .env.example for reference, but Deploy.s.sol pins the
canonical "Crownridge" / "CRWN" in code — the env values are not read.
Genesis (placeholders — founder + economic-review sign-off required)
| Variable | Placeholder | Meaning |
|---|---|---|
GENESIS_RATE |
1000000000000000000 |
CRWN (18d) minted per 1 USDG net of fee; 1e18 = 1:1. Immutable after deploy. |
PROTOCOL_FEE_BPS |
50 |
0.50%. Pre-flight rejects > 100 (1.00% hard cap). Immutable. |
GENESIS_CAP_USDG |
250000000000 |
250,000 USDG (6d) total deposit cap. |
GENESIS_MIN_DEPOSIT |
10000000 |
10 USDG. |
GENESIS_MAX_PER_WALLET |
0 |
0 = no per-wallet cap. |
GENESIS_START / GENESIS_END |
0 / 0 |
Unix seconds. 0 ⇒ script defaults: start = deploy time, end = +30 days. |
Buyback (reference values — NOT read at deploy)
BUYBACK_ENABLED, BUYBACK_MIN_DISCOUNT_BPS, BUYBACK_MAX_SPEND_PER_TX, BUYBACK_MAX_SPEND_PER_DAY,
BUYBACK_MAX_TREASURY_BPS, BUYBACK_TWAP_WINDOW, BUYBACK_MAX_DEVIATION_BPS,
BUYBACK_MIN_POOL_LIQUIDITY, BUYBACK_MAX_SLIPPAGE_BPS, BUYBACK_COOLDOWN document the intended
post-launch risk parameters; they are applied later via the timelocked
CrownridgeBuyback.setRiskParams call in §6, not by the deploy script. The buyback always ships
disabled with no pool — the script asserts it. The only buyback variable Deploy reads is
BUYBACK_EXECUTOR (the address granted BUYBACK_EXECUTOR_ROLE).
3. What script/Deploy.s.sol does
One forge script run performs the entire deterministic sequence — deploy, wire, hand off, assert,
emit manifest. There is no manual step between "deployer has admin" and "deployer has nothing".
3.1 Pre-flight assertions (spec §160)
Before any transaction is broadcast, _preflight requires:
block.chainid == RH_CHAIN_ID- USDG has bytecode at
USDG_ADDRESSand its livedecimals()equalsUSDG_EXPECTED_DECIMALS - Uniswap v3 factory, SwapRouter02, and NFPM all have bytecode
- protocol multisig, security multisig, and founder address are non-zero
GENESIS_RATE != 0,PROTOCOL_FEE_BPS <= 100,GENESIS_CAP_USDG != 0,GENESIS_END > GENESIS_START
Any failure aborts before broadcast.
3.2 Deploy order
1. TimelockController (OZ; minDelay = 48h, proposer = protocol multisig,
executor = open (address(0)), temp admin = deployer)
2. CrownridgeToken (minter = PREDICTED MintController address, via CREATE-nonce
prediction; step 3 asserts the prediction held)
3. CrownridgeMintController (sole minter of CRWN)
4. CrownridgeTreasury (USDG reserve, anchored accounting)
5. CrownridgeGenesis (rate, feeBps, cap, window, min/max — from env)
6. CrownridgeBuyback (ships DISABLED, pool unset)
7. CrownridgeLiquidityManager(NFPM, protocol-owned LP custody)
8. FounderTreasuryController (immutable founder address, CAPPED/UNCAPPED config)
9. CrownridgeAccounting (stateless view lens)
The token's minter is immutable, so the MintController address is computed from the deployer's
nonce before the token is deployed; require(address(d.mint) == predictedMint) makes a
mis-prediction fatal rather than silent.
3.3 Wiring (deployer still temporarily admin)
_wire grants exactly these roles (src/libraries/Roles.sol):
| Contract | Role | Holder |
|---|---|---|
| MintController | ISSUER_ROLE |
Genesis (the only issuance path) |
| MintController | PAUSER_ROLE |
security multisig |
| Treasury | DEPOSITOR_ROLE |
Genesis, Buyback, LiquidityManager |
| Treasury | TREASURY_SPENDER_ROLE |
Buyback |
| Treasury | FOUNDER_CONTROLLER_ROLE |
FounderTreasuryController |
| Treasury | GUARDIAN_ROLE |
security multisig |
| Treasury | RESERVE_MANAGER_ROLE |
Timelock |
| Genesis | PARAMETER_ADMIN_ROLE |
Timelock |
| Genesis | PAUSER_ROLE |
security multisig |
| Buyback | PARAMETER_ADMIN_ROLE |
Timelock |
| Buyback | PAUSER_ROLE |
security multisig |
| Buyback | BUYBACK_EXECUTOR_ROLE |
BUYBACK_EXECUTOR |
| LiquidityManager | RESERVE_MANAGER_ROLE |
Timelock |
It then calls Treasury.initializeProtocolHolders([treasury, buyback, mintController, liquidityManager, founderController]) — the one-shot, frozen set of protocol addresses excluded
from circulating supply. The Uniswap pool is deliberately not in this set (pool-held CRWN counts
as circulating — conservative).
3.4 Hand-off: deployer configures, then renounces everything
_handOff grants DEFAULT_ADMIN_ROLE to the Timelock on MintController, Treasury, Genesis,
Buyback, and LiquidityManager, then the deployer renounces its own admin on each. On the
timelock itself the security multisig is granted CANCELLER_ROLE (guardian veto — P2 finding C1)
and the deployer renounces the timelock's DEFAULT_ADMIN_ROLE. Resulting topology:
protocol multisig ──propose──┐
v
security multisig ──cancel (veto)──> TimelockController (48h) <──execute── anyone
│ │ DEFAULT_ADMIN of everything
│ PAUSER / GUARDIAN (immediate) v
└────────────> MintController Treasury Genesis Buyback LiquidityManager
│
founder wallet ──> FounderTreasuryController ──┘ FOUNDER_CONTROLLER_ROLE
(guardian-pausable at T+0)
deployer ──> NOTHING (asserted)
Note the timelock executor is open (address(0)): once an operation has been proposed by the
protocol multisig and survived the 48h delay un-cancelled, anyone may execute it. The security is in
the proposal right and the guardian veto, not in execution.
3.5 Post-deploy assertions (spec §37, §160)
_postAssert re-checks against the just-deployed contracts and reverts the whole run on failure:
- deployer holds
DEFAULT_ADMIN_ROLEon nothing (each contract and the timelock, enumerated) - Timelock is admin of Treasury and MintController
- guardian holds
CANCELLER_ROLEon the timelock andGUARDIAN_ROLEon the Treasury crwn.minter() == mintController; Genesis holdsISSUER_ROLEcrwn.decimals() == 18;treasury.reserveDecimals()matches USDG;treasury.usdg()is the configured USDGbuyback.enabled() == falseandbuyback.pool() == address(0)— the conservative launch posture is asserted, not hoped for- FounderTreasuryController holds
FOUNDER_CONTROLLER_ROLE;founderCtrl.founder()matches config
3.6 Manifest emission (spec §159, §91)
_writeManifest writes contracts/deployments/<chainId>-latest.json
(4663-latest.json mainnet, 46630-latest.json testnet):
{
"chainId": 4663,
"usdg": "0x5fc5...", "timelock": "...", "crwn": "...", "mintController": "...",
"treasury": "...", "genesis": "...", "buyback": "...", "liquidityManager": "...",
"founderController": "...", "accounting": "...",
"protocolMultisig": "...", "securityMultisig": "...",
"founder": "...", "founderCapped": true,
"compiler": "0.8.30", "openzeppelin": "5.1.0",
"deployer": "..."
}
Per deployments/README.md: commit the manifest for real networks (the local 31337-latest.json
from the deploy test is gitignored). Record the deployment git commit and transaction hashes
alongside it (§7) — the script does not embed the git commit itself.
4. Running a deployment
All commands from contracts/. The RPC aliases rh_testnet / rh_mainnet come from
foundry.toml [rpc_endpoints] and read RH_TESTNET_RPC_URL / RH_RPC_URL from the environment.
4.0 Tests must be green first — no exceptions
cd contracts
forge build
forge test # unit + fuzz + 10 invariants (73+ tests)
FOUNDRY_PROFILE=ci forge test # deeper fuzz (2000 runs) + invariant (512×128)
RH_RPC_URL=<alchemy-mainnet-url> forge test --match-path 'test/fork/*'
# fork suite vs LIVE Robinhood mainnet:
# real USDG bytecode, real Uniswap v3
4.1 Testnet (chainId 46630) — ALWAYS FIRST
Set in .env: RH_CHAIN_ID=46630, testnet USDG/Uniswap addresses (§2), testnet multisigs, the
testnet deployer key.
# 1. Simulate — full run incl. pre-flight and post-assertions, nothing broadcast
forge script script/Deploy.s.sol --rpc-url rh_testnet
# 2. Broadcast + verify sources on testnet Blockscout
forge script script/Deploy.s.sol --rpc-url rh_testnet --broadcast \
--verify --verifier blockscout \
--verifier-url https://explorer.testnet.chain.robinhood.com/api
# 3. Independent re-verification against the live chain (view-only, reads the manifest)
forge script script/VerifyDeployment.s.sol --rpc-url rh_testnet
Then rehearse the entire launch on testnet (spec §129): a full Genesis deposit flow, the liquidity
seeding of §6, a founder-withdrawal simulation, a guardian pause simulation
(Treasury.setFounderWithdrawPaused(true), Genesis.pause()), and a timelocked parameter change
end-to-end (schedule → 48h → execute). Only a fully rehearsed sequence graduates to mainnet.
4.2 Mainnet (chainId 4663)
Only after the testnet rehearsal and the §9 gate (audit included) are complete. Set
RH_CHAIN_ID=4663, the verified canonical addresses (§2), production multisigs, and the mainnet
deployer key (used for nothing else).
# Re-verify externals live, immediately before deploying (CLAUDE.md §2: re-verify before mainnet)
cast chain-id --rpc-url rh_mainnet # expect 4663
cast call 0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168 "decimals()(uint8)" --rpc-url rh_mainnet # expect 6
cast call 0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168 "symbol()(string)" --rpc-url rh_mainnet # expect "USDG"
# Simulate, then broadcast + verify
forge script script/Deploy.s.sol --rpc-url rh_mainnet
forge script script/Deploy.s.sol --rpc-url rh_mainnet --broadcast \
--verify --verifier blockscout \
--verifier-url https://robinhoodchain.blockscout.com/api
# Independent post-deploy verification
forge script script/VerifyDeployment.s.sol --rpc-url rh_mainnet
5. Post-deployment
5.1 VerifyDeployment.s.sol — independent re-check
script/VerifyDeployment.s.sol reads deployments/<chainId>-latest.json and re-derives every
safety property from the live deployed bytecode, not from the deploy script's memory: token
decimals and minter, issuer wiring, live-USDG decimals vs treasury.reserveDecimals(), the full
admin topology (timelock admin everywhere, deployer nowhere), the guardian's CANCELLER_ROLE and
GUARDIAN_ROLE, the disabled-buyback/unset-pool launch posture, founder-controller wiring, and the
conservation invariant (recognizedReserve <= USDG.balanceOf(treasury) and
treasury.conservationHolds()). It fails loudly on any mismatch. Run it after every deployment and
again immediately before mainnet go-live.
5.2 Source verification on Blockscout
--verify in §4 handles this during broadcast (config in foundry.toml [etherscan]). To verify a
single contract after the fact:
forge verify-contract <TREASURY_ADDR> src/CrownridgeTreasury.sol:CrownridgeTreasury \
--chain-id 4663 --verifier blockscout \
--verifier-url https://robinhoodchain.blockscout.com/api \
--constructor-args "$(cast abi-encode 'constructor(address,address,address)' \
"$USDG_ADDRESS" "$CRWN_ADDR" "$DEPLOYER_ADDR")"
Repeat per contract with its constructor signature (read from src/). Confirm every contract shows
verified source on the explorer before announcing addresses — users must be able to read exactly
what they are interacting with.
5.3 Wire the website and indexer from the manifest
Both apps ship with empty address env vars and honestly display N/A / no-op until wired. Copy from
deployments/<chainId>-latest.json — never hand-typed:
| Manifest key | Website (website/.env) |
Indexer (indexer/.env) |
|---|---|---|
crwn |
NEXT_PUBLIC_CRWN_ADDRESS |
CRWN_ADDRESS |
treasury |
NEXT_PUBLIC_TREASURY_ADDRESS |
TREASURY_ADDRESS |
accounting |
NEXT_PUBLIC_ACCOUNTING_ADDRESS |
— |
genesis |
NEXT_PUBLIC_GENESIS_ADDRESS |
GENESIS_ADDRESS |
buyback |
NEXT_PUBLIC_BUYBACK_ADDRESS |
BUYBACK_ADDRESS |
mintController |
— | MINT_CONTROLLER_ADDRESS |
founderController |
NEXT_PUBLIC_FOUNDER_CONTROLLER_ADDRESS |
— |
usdg |
NEXT_PUBLIC_USDG_ADDRESS |
— |
chainId |
NEXT_PUBLIC_RH_CHAIN_ID |
RH_CHAIN_ID |
Also set the indexer's INDEX_START_BLOCK to the deployment block (from the broadcast artifacts in
contracts/broadcast/), so history starts at genesis of the protocol, not of the chain.
6. Conservative launch sequence (spec §41)
Deployment is not launch. The market is opened in deliberate stages; the buyback is the last thing switched on, and every switch after hand-off runs through the 48h timelock (protocol multisig proposes → 48h → anyone executes; guardian can cancel).
Deploy (buyback DISABLED, pool unset — asserted)
│
▼
1. Genesis opens ──> USDG deposits → Treasury recognizes reserve → CRWN minted
│
▼
2. Seed liquidity with GENESIS-ACQUIRED CRWN (never a naked mint)
│
▼
3. Market opens; TWAP observation history accrues
│
▼
4. Buyback enabled via timelock — only after oracle/liquidity validated
Step 1 — Genesis. CrownridgeGenesis.deposit(amount) is live within the configured window/cap.
Each deposit atomically transfers USDG to the Treasury, recognizes it, and mints CRWN net of the fee
(quote(gross) previews it). Monitor remainingCapacity() and the indexer alert rules.
Step 2 — Seed liquidity. There is deliberately no seed mint — CrownridgeMintController
has exactly one issuance path (Genesis), so the CRWN used for the initial pool is acquired through
Genesis with the protocol's/founder's own USDG (already backed). Then:
- The ops wallet holding the Genesis-acquired CRWN plus its own USDG creates and initializes the
CRWN/USDG pool (fee tier
UNIV3_POOL_FEE, e.g. 3000 = 0.30%) and mints a full-range position via the canonicalNonfungiblePositionManager(0x73991a25C818Bf1f1128dEAaB1492D45638DE0D3). - Transfer the LP NFT to
CrownridgeLiquidityManager(it implementsonERC721Received). - Via the timelock (its
DEFAULT_ADMIN_ROLE):LiquidityManager.registerPosition(tokenId)— set-once. From here LP fees route honestly:collectFees()(permissionless) sends the USDG side to the Treasury as recognized revenue and burns the CRWN side.
Note: the pool's USDG is not Treasury reserve and the position is valued at zero in NAV
(docs/ECONOMICS.md §3) — seeding liquidity does not change NAV.
Step 3 — Let the TWAP become usable. CrownridgeBuyback.executeBuyback hard-requires
observation cardinality ≥ minCardinality (default 60) and a valid twapWindow (default 1800 s)
TWAP. Immediately after pool creation neither holds. Call
pool.increaseObservationCardinalityNext(n) with n ≥ the intended minCardinality
(permissionless; paid once in gas), then let real trading fill the observation buffer. Verify with
Buyback.preflight() — it returns ok=false while any gate would fail closed.
Step 4 — Enable the buyback (three timelocked operations).
Buyback.setPool(pool, fee)—DEFAULT_ADMIN_ROLE(timelock). Set-once; the contract validates the pool's token pair and fee on-chain.Buyback.setRiskParams(...)—PARAMETER_ADMIN_ROLE(timelock). Apply the signed-off values (theBUYBACK_*reference block in §2). Bounds are enforced on-chain — no parameter setting can disable the guards (discount 1–50%, TWAP window 10 min–24 h, deviation ≤ 10%, slippage ≤ 5%, treasury cap ≤ 10%/day, cooldown ≥ 60 s).Buyback.setEnabled(true)—PARAMETER_ADMIN_ROLE(timelock).
Start with small maxSpendPerTx / maxSpendPerDay. Do not deploy a large automated buyback budget
on day one (spec §41). Even enabled, every execution re-clears the full risk engine and the
measured-delta accretion check, and fails closed on any oracle doubt.
7. Reproducibility (spec §91)
A third party must be able to rebuild the exact deployed bytecode.
| Pinned item | Where | Value |
|---|---|---|
| Solidity compiler | foundry.toml solc |
0.8.30 (exact, not a range) |
| EVM version | foundry.toml |
cancun |
| Optimizer | foundry.toml |
on, runs = 200, via_ir = true |
| Metadata | foundry.toml |
bytecode_hash = "none", cbor_metadata = false — bytecode does not vary with file paths or metadata |
| OpenZeppelin | contracts/lib/openzeppelin-contracts |
v5.1.0, vendored |
| forge-std | contracts/lib/forge-std |
vendored |
| Foundry | toolchain | record the exact forge --version used (currently 1.8.1) |
Record with every real deployment, next to the committed manifest:
git rev-parse HEAD # exact source commit (tag it, e.g. deploy/mainnet-v1)
git status --porcelain # MUST be empty — never deploy a dirty tree
forge --version # toolchain
grep -v -e PRIVATE_KEY -e API_KEY contracts/.env | sort | shasum -a 256
# config hash — SANITIZED: secrets excluded
The manifest already embeds compiler and openzeppelin; the git commit, deployment transaction
hashes (from contracts/broadcast/Deploy.s.sol/<chainId>/run-latest.json), and the sanitized config
hash are recorded by the operator and published with the deployment metadata (spec §91, §94).
8. Key management (spec §161)
Separate keys per environment and per function. Never reuse a development key on testnet, or a testnet key on mainnet. Never commit any private key, seed phrase, RPC API key, or multisig signing material.
| Key | Purpose | Custody guidance |
|---|---|---|
| Development key | local anvil only | throwaway |
| Testnet deployer | 46630 deploys | dedicated, testnet-only |
| Mainnet deployer | one job: run Deploy.s.sol on 4663 |
fresh key; fund only the gas needed; retains zero authority post-deploy (asserted), but treat it as sensitive until VerifyDeployment passes |
| Protocol multisig signers | timelock proposals | hardware-backed, threshold ≥ 2 |
| Security multisig signers | pause / cancel | hardware-backed, reachable 24/7 (this is the incident-response trigger) |
| Founder wallet | FounderTreasuryController authority — can move Treasury reserve within its mode |
strongest available custody (hardware or institutional). Compromise = potential Treasury drain up to the configured caps (or fully, if UNCAPPED). Immediate response is the guardian's setFounderWithdrawPaused(true); see docs/INCIDENT_RESPONSE.md. |
| Buyback executor | executeBuyback calls |
ops key; all safety limits are enforced on-chain regardless of this key |
9. Mainnet gate (spec §88, §129, §170 — every box, no exceptions)
- Full unit + fuzz + invariant suite green (
FOUNDRY_PROFILE=ci) - Fork suite green against live Robinhood mainnet
- Chain ID 4663, RPC, explorer re-verified live
- USDG address +
decimals()==6+ Uniswap factory/router/NFPM re-verified live (§4.2 probes) - Testnet deployment completed,
VerifyDeploymentpassed on testnet - Testnet rehearsal: full Genesis flow, liquidity seeding, founder-withdrawal simulation, emergency-pause simulation, one timelocked change executed end-to-end
- Independent security audit and economic review completed and findings resolved — this has NOT yet happened; mainnet is blocked until it has
- Economics signed off by the founder (
docs/ECONOMICS.mdvalues →.env) - Founder wallet +
CAPPED/UNCAPPEDmode explicitly chosen and disclosed - Multisigs live on Robinhood Chain with confirmed signer sets
- Legal review of Genesis, founder-rights disclosure, and marketing claims (spec §166)
- No placeholder addresses anywhere in the final
.env - Source verified on Blockscout for every contract; manifest + git commit + tx hashes committed
- Website/indexer wired from the manifest and displaying live data (or honest
N/A)
Related: docs/ECONOMICS.md (parameter meaning and sign-off), docs/SECURITY.md (role model),
docs/INCIDENT_RESPONSE.md (what to do when something goes wrong), token/LAUNCH.md (the
human-executed launch runbook that wraps this document).