Crownridge Security Model
Scope. This document describes the internal security architecture of the Crownridge protocol: access control, invariants, upgradeability policy, emergency mechanisms, the internal adversarial reviews performed, and the trust assumptions a user accepts. It is distinct from the repository-root
SECURITY.md, which is the vulnerability-disclosure policy.Status — read this first. Crownridge has not been externally audited and is not deployed to mainnet. The two internal adversarial reviews described in §7 are part of our own engineering process; they are not a substitute for an independent audit. An external security audit and an independent economic review are hard, non-negotiable gates before any mainnet deployment (spec §88; founder action list,
CLAUDE.md§11). CRWN has no redemption mechanism in V1 and nothing in this document implies guaranteed returns, a price floor, or redeemability.
Built on Robinhood Chain (chainId 4663, an Arbitrum Orbit L2). Solidity 0.8.30 (pinned),
OpenZeppelin Contracts v5.1.0 (pinned), Foundry toolchain.
1. Design principles
- Immutable core, no proxies. No contract in the economic core is upgradeable. What is deployed is what runs, forever (§4).
- Least privilege, one concept per role. Every privileged action is gated by a narrow role from
libraries/Roles.sol; contracts hold roles on each other so authority is structural, not personal. - Fail closed. Any doubt — oracle staleness, deviation, thin liquidity, an unexpected transfer amount — reverts. The protocol never acts on uncertain data.
- Measured deltas, never formulas. Value movements are accounted from actual
balanceOfdeltas (Genesis deposits, buyback spend/receive, buyback refunds), never from assumed transfer amounts. - Disclosed trust. The founder's Treasury withdrawal authority is real and is documented plainly
here and in
docs/RISK.md— never hidden, renamed, or downplayed (§8.1). - Slow admin, fast brakes. Every administrative change runs through a 48-hour timelock that the security guardian can veto; every emergency action (pause) is immediate and drain-free.
2. Access-control architecture
2.1 Authority map
48h min delay, guardian-cancellable
Protocol multisig ──schedule──▶ TimelockController ──execute──▶ DEFAULT_ADMIN_ROLE
(PROPOSER_ROLE, (self-administered; PARAMETER_ADMIN_ROLE
CANCELLER_ROLE) no EOA holds admin) RESERVE_MANAGER_ROLE
▲ on every core contract
Security multisig ──── CANCELLER_ROLE ──┘ (veto — P2 C1)
│
├── PAUSER_ROLE (immediate) ──▶ MintController.pause / Genesis.pause / Buyback.pause
└── GUARDIAN_ROLE (immediate) ──▶ Treasury.setFounderWithdrawPaused (P2 C2)
Founder wallet ──onlyFounder──▶ FounderTreasuryController ──FOUNDER_CONTROLLER_ROLE──▶
Treasury.founderWithdraw
Ops address ──BUYBACK_EXECUTOR_ROLE──▶ Buyback.executeBuyback (risk engine binds on-chain
regardless of caller)
Genesis ──ISSUER_ROLE──▶ MintController.issue ──immutable `minter`──▶ CRWN.mint
Anyone ──▶ LiquidityManager.collectFees (permissionless: value can only flow INTO the protocol)
2.2 Role table
DEFAULT_ADMIN_ROLE (OZ AccessControl, 0x00) administers every other role on its contract. It is
held only by the TimelockController on every contract, so every grant/revoke/parameter change is
a 48-hour, publicly visible, guardian-cancellable operation. Role identifiers are canonicalized in
libraries/Roles.sol so the same concept is never spelled two ways.
| Role | Contract(s) | Held by | Gates | Admin |
|---|---|---|---|---|
minter (immutable address, not a role) |
CrownridgeToken |
CrownridgeMintController |
CrownridgeToken.mint — the only privileged function on the token |
None — set in the constructor, unchangeable |
ISSUER_ROLE |
CrownridgeMintController |
CrownridgeGenesis |
issue(to, amount) — reserve-backed issuance only |
Timelock |
DEPOSITOR_ROLE |
CrownridgeTreasury |
Genesis, Buyback, LiquidityManager | recognize(amount, revenuePortion) — recognizing reserve inflows |
Timelock |
TREASURY_SPENDER_ROLE |
CrownridgeTreasury |
CrownridgeBuyback |
fundBuyback(amount) — the only transfer to the buyback |
Timelock |
FOUNDER_CONTROLLER_ROLE |
CrownridgeTreasury |
FounderTreasuryController |
founderWithdraw(asset, recipient, amount) |
Timelock (revocation = 48h op) |
GUARDIAN_ROLE |
CrownridgeTreasury |
Security multisig | setFounderWithdrawPaused(bool) — immediate |
Timelock |
RESERVE_MANAGER_ROLE |
Treasury, LiquidityManager | TimelockController itself |
writeDownRecognized, sweepExcess, rescueToken, LiquidityManager.windDown / rescueToken |
Timelock |
PARAMETER_ADMIN_ROLE |
Genesis, Buyback | TimelockController itself |
setCap/setWindow/setLimits; setEnabled/setRiskParams/sweepStrayUsdg |
Timelock |
PAUSER_ROLE |
MintController, Genesis, Buyback | Security multisig | pause() / unpause() — immediate, per subsystem |
Timelock |
BUYBACK_EXECUTOR_ROLE |
CrownridgeBuyback |
Ops address | executeBuyback — every safety check enforced on-chain regardless of caller |
Timelock |
onlyFounder (immutable address, not a role) |
FounderTreasuryController |
Founder wallet | withdraw(recipient, amount) |
None — immutable; the controller cannot replace itself |
PROPOSER_ROLE |
TimelockController |
Protocol multisig | Scheduling timelocked operations | Timelock (self-administered) |
EXECUTOR_ROLE |
TimelockController |
Open (address(0)) |
Executing a ready, un-cancelled operation | Timelock (self-administered) |
CANCELLER_ROLE |
TimelockController |
Security multisig (+ proposer, per OZ v5 constructor) | Vetoing any queued operation before execution | Timelock (self-administered) |
Contracts with no privileged surface at all: CrownridgeAccounting (stateless view lens, holds no
funds) and CrownridgeToken beyond the immutable minter (no pause-on-transfer, tax, blacklist,
rebase, or confiscation — burning is open via ERC20Burnable).
2.3 Deployer retains nothing
script/Deploy.s.sol wires all roles, grants DEFAULT_ADMIN_ROLE everywhere to the timelock, then
renounces every deployer role including the timelock's own admin (_handOff). Post-deploy
assertions (_postAssert, spec §160) revert the deployment if the deployer retains any role, if the
timelock is not admin everywhere, if the guardian lacks CANCELLER_ROLE, or if the buyback does not
ship disabled with no pool. Tested in Deploy.t.sol::test_fullDeploySucceedsAndHandsOff and
Timelock.t.sol::test_deployerHasNoTimelockAuthority.
3. The ten core invariants — enforcement and tests
These are the protocol's contract with its users (CLAUDE.md §3; reproduced verbatim in the
whitepaper). Each is enforced in code and covered by the test suite (unit + fuzz + stateful
invariant + fork suites; the stateful suite drives a fuzzed Handler through deposits, withdrawals,
burns, transfers, and write-downs and asserts after every sequence).
| # | Invariant | Enforcement | Tests |
|---|---|---|---|
| 1 | Unauthorized accounts can never mint CRWN | CrownridgeToken.mint reverts unless msg.sender == minter (immutable, set to the MintController at construction); MintController.issue requires ISSUER_ROLE, held only by Genesis |
Token.t::test_revert_directMintByAnyone, test_revert_directMintByAdmin; MintAndAccounting.t::test_revert_issueByNonIssuer, test_revert_issueByAdmin_withoutIssuerRole; Invariants.t::invariant_minterImmutable |
| 2 | CRWN cannot be minted without a corresponding reserve entry (deposit→mint is atomic) | Genesis.deposit moves USDG directly into the Treasury, verifies the measured received amount, calls treasury.recognize, and only then mintController.issue — one transaction, all-or-nothing. There is no other issuance path (and no seed mint: LP CRWN is acquired through Genesis) |
Genesis.t::test_deposit_mintsCorrectRateNetOfFee, testFuzz_depositBackingInvariant; Invariants.t::invariant_noNakedMint; fork test test_realGenesisDepositWithRealUsdg against real mainnet USDG |
| 3 | Treasury reserve accounting cannot become negative | recognizedReserve is a uint256; every decrement (fundBuyback, founderWithdraw) checks InsufficientRecognizedReserve first; writeDownRecognized only moves to a smaller checked value |
Invariants.t::invariant_conservation; Treasury.t outflow tests |
| 4 | Buyback cannot spend above configured limits | executeBuyback bounds spend by min(maxSpendPerTx, daily remaining, maxTreasuryBps of recognizedReserve per rolling day) via _allowedSpend, plus cooldown; setRiskParams rejects out-of-bounds parameters so no admin value can disable the guards |
Buyback.t::test_revert_exceedsMaxSpendPerTx, test_revert_treasuryPercentCap, test_revert_cooldown |
| 5 | Burned CRWN cannot return to circulation | Burn is ERC20Burnable._burn (supply strictly decreases); the only mint path is invariant 1, which never re-credits burned tokens |
Token.t::test_burnedCrwnCannotReturn; Invariants.t::invariant_supplyAccounting (totalSupply + burned == totalIssued) |
| 6 | Genesis cannot exceed its configured cap | deposit reverts with CapExceeded beyond cap - totalDeposited; window and per-wallet caps enforced in the same function |
Genesis.t::test_revert_capExceeded, test_capExhaustionExact; Invariants.t::invariant_genesisCap |
| 7 | Decimal normalization remains correct (6↔18) | All cross-decimal math lives in libraries/CrownridgeMath.sol (reserveToWad, crwnForDeposit, netAssetValueWad, navPerToken), used identically by Treasury, Genesis, and the Accounting lens; floor rounding always favors the Treasury |
Invariants.t::invariant_navDeterministic (nav == recognizedReserve × 1e12); Genesis.t::test_quoteMatchesDeposit |
| 8 | A failed reserve transfer cannot produce CRWN | SafeERC20.safeTransferFrom + measured Treasury balance delta; received != amount reverts with UnexpectedTransferAmount before any recognition or mint; the whole deposit reverts atomically |
Genesis.t revert-path tests; fork suite exercises the real USDG token's transfer semantics |
| 9 | Conservation: recognizedReserve ≤ USDG.balanceOf(Treasury) at all times |
Treasury.recognize reverts with ConservationViolated if recognition would exceed the actual balance; conservationHolds() exposes the check; unsolicited transfers are excess until sweepExcess |
Invariants.t::invariant_conservation after every fuzzed action sequence; Treasury.t::test_revert_recognizeBeyondBalance; Genesis.t::test_conservationHoldsAfterDeposit |
| 10 | Founder withdrawal reduces recognized reserves + NAV immediately; never mints or changes CRWN supply; emits FounderTreasuryWithdrawal |
Treasury.founderWithdraw decrements recognizedReserve, transfers, emits the event; it accepts only the USDG reserve asset (UnsupportedAsset otherwise) and has no code path touching CRWN |
Treasury.t::test_founderWithdrawReducesNavNotSupply; Invariants.t::invariant_supplyOnlyViaIssueBurn; the FounderController.t suite |
At the most recent recorded full run (post-P5 fix), the suite passed 75/75; the fork suite runs
against live Robinhood Chain mainnet state (real USDG, real Uniswap v3 deployments) via
forge test --match-path 'test/fork/*' --fork-url $RH_RPC.
4. Upgradeability policy: immutable core, no proxies
Per spec §53/§162 and decision D2 (CLAUDE.md §10), the entire economic core is immutable:
- No proxies, no upgrade hooks, no
delegatecallanywhere inCrownridgeToken,CrownridgeTreasury,CrownridgeMintController,CrownridgeGenesis,CrownridgeBuyback,CrownridgeLiquidityManager, orFounderTreasuryController. - Economic constants are immutable in the bytecode: the token's
minter, GenesisrateandfeeBps, the founder address and CAPPED/UNCAPPED configuration, every inter-contract reference. - What can change is deliberately narrow and always goes through the 48h timelock: bounded risk
parameters (
Buyback.setRiskParamsrejects values outside hard ranges), Genesis operational parameters (cap/window/limits — the pricerateandfeeBpsare immutable), and role membership. - The one replaceable component is
CrownridgeAccounting— a stateless, fund-less view lens. It can be redeployed (e.g., if V2 adds assets) without touching the core, and it cannot disagree with enforced accounting because it derives everything from the Treasury's immutable primitives (recognizedReserve,circulatingSupply,nav) — the same valuesCrownridgeBuybackacts on.
Consequence, stated honestly: a bug in the core cannot be patched in place. The mitigations are the
pause surfaces (§5), the narrow blast radius of each contract, writeDownRecognized for reserve
impairment, LiquidityManager.windDown for the LP position — and, before mainnet, the external audit
(§9). A defective subsystem is abandoned and redeployed, never mutated.
5. Emergency mechanisms
All emergency levers are immediate and drain-free — none of them can move funds out of the protocol. All recovery/parameter actions are timelocked.
| Mechanism | Trigger | Latency | Function |
|---|---|---|---|
| Per-subsystem pause (spec §149, folded into each contract per D5) | Security multisig (PAUSER_ROLE) |
Immediate | CrownridgeGenesis.pause, CrownridgeMintController.pause, CrownridgeBuyback.pause — each halts its subsystem only; CRWN transfers are never pausable |
| Founder-withdraw emergency brake (P2 C2) | Security multisig (GUARDIAN_ROLE) |
Immediate (T+0) | CrownridgeTreasury.setFounderWithdrawPaused(true) — fully stops founderWithdraw. This is a safety pause against founder-key compromise, not an approval gate. The durable follow-up — revoking FOUNDER_CONTROLLER_ROLE — is a DEFAULT_ADMIN action and therefore runs through the 48h timelock |
| Timelock veto (P2 C1) | Security multisig (CANCELLER_ROLE) |
Before execution of any queued op | TimelockController.cancel(id) — turns a compromised-proposer T+48h drain into a cancellable stalemate. Tested end-to-end in Timelock.t::test_guardianCanVetoMaliciousRoleGrant |
| Reserve write-down | Timelock (RESERVE_MANAGER_ROLE) |
48h | Treasury.writeDownRecognized(newAmount, reason) — can only decrease recognized reserve (reverts with CannotIncrease otherwise); the honest response to USDG impairment/freeze. It can never inflate NAV |
| Buyback fail-closed design | Structural | Always | executeBuyback reverts on low observation cardinality, thin pool liquidity, zero/stale TWAP, spot-vs-TWAP deviation, insufficient NAV discount, cap breach, or a non-accretive measured outcome. It also ships disabled with no pool; setPool (one-shot, token/fee-validated) and setEnabled are timelocked |
| Protocol-liquidity wind-down | Timelock (RESERVE_MANAGER_ROLE) |
48h | LiquidityManager.windDown — decreases the Uniswap v3 position; USDG proceeds go to the Treasury (recognized), CRWN proceeds are burned |
Escalation procedures, on-call expectations, and alert wiring (unexpected mint, treasury outflow,
founder withdrawal, supply mismatch — indexer alert rules per spec §86/§147) are documented in
docs/INCIDENT_RESPONSE.md and docs/RUNBOOK.md.
6. What the guardian can and cannot do
The security multisig is powerful in one direction only. It can: pause Genesis, minting, and buybacks; freeze the founder-withdraw path; veto any queued timelock operation. It cannot: move a single token, mint, change parameters, grant roles, or unpause its way into new authority (unpausing restores previously reviewed behavior only). Its worst-case failure mode is griefing (pausing a healthy system or vetoing legitimate changes), not theft.
7. Internal adversarial reviews performed
Two structured internal reviews have been run. Both are internal engineering discipline — neither is an audit, and no external audit has occurred.
7.1 P2 — architecture adversarial review (pre-code)
A 97-agent adversarial workflow attacked the architecture through independent economic,
accounting/solvency, access-control, and spec-compliance lenses before any Solidity was written.
Outcome: 3 confirmed findings, 28 refuted (with 15 refuted-but-real items promoted to binding
implementation requirements — CLAUDE.md §10b). The confirmed findings were folded into the design:
| ID | Finding | Resolution in code |
|---|---|---|
| C1 (HIGH) | The timelock had no veto: a compromised proposer meant a guaranteed drain at T+48h | Security multisig holds CANCELLER_ROLE on the TimelockController (canceller ≠ proposer; adds no drain power). Deploy script asserts the timelock is self-administered, proposer = protocol multisig only, and no EOA holds timelock admin |
| C2 (HIGH) | Founder withdrawal had no emergency brake against founder-key compromise | Treasury.founderWithdrawPaused flag, settable immediately by GUARDIAN_ROLE (setFounderWithdrawPaused); role revocation remains the timelocked durable follow-up |
| C3 (LOW) | Fixed daily buyback caps don't scale with treasury size | Buyback.maxTreasuryBps — a proportional per-rolling-day bound against recognizedReserve() at execution time, enforced alongside the fixed per-tx/per-day caps in _allowedSpendWith |
7.2 P5 — contract security review (post-code)
A 23-agent find→adversarially-verify→fix review of the implemented contracts (five lenses), plus a
static-analysis pass. Outcome: 5 raised findings, all confirming one root-cause bug, fixed in
commit 563ba20:
- Buyback donation-brick DoS (fixed).
executeBuybackoriginally computed its post-swap refund from the contract's absolute USDG balance. A permissionless USDG donation to the Buyback contract would makeleftover > usdgIn, underflowspentInWindow -= leftover, and — because the contract is immutable — permanently brickexecuteBuyback. Fix: leftover is now measured against abalBeforeFundsnapshot taken beforetreasury.fundBuyback, so it is strictly this operation's unspent portion (≤ usdgIn, donations excluded), and a timelockedsweepStrayUsdg()routes any donated USDG into the Treasury as recognized backing. Regression-tested inBuyback.t::test_donationDoesNotBrickBuyback. - One refuted-but-real documentation finding: internal docs briefly overclaimed that founder-role revocation was immediate. Corrected everywhere to the precise statement: the pause is immediate (guardian); revocation is timelocked.
8. Trust assumptions
Using Crownridge means accepting the following, in roughly descending order of weight.
8.1 The founder Treasury withdrawal authority — the primary trust assumption
FounderTreasuryController.withdraw lets the immutable founder address withdraw USDG from the
Treasury. In UNCAPPED mode the founder can withdraw the entire recognized reserve. In CAPPED
mode, immutable per-withdrawal / rolling-window / minimum-treasury-floor / cooldown limits apply
(spec §119). This is a real, disclosed authority — not a bug, and not hidden:
- Every withdrawal reduces
recognizedReserveand NAV immediately and emitsFounderTreasuryWithdrawal(Invariant 10). It can never mint CRWN or touch CRWN/protocol liquidity. - The controller is deliberately narrow: no arbitrary call, no
delegatecall, no token approvals, no mint/oracle/upgrade/role-admin power; the founder address is immutable and the controller cannot replace itself (FounderController.t::test_narrowScope_noExtraPowers). - Checks on it: the guardian's immediate pause (§5) and the timelock's 48h role revocation.
- The chosen mode and parameters are published in the deployment manifest and on the public
transparency page. Full risk treatment:
docs/RISK.md.
If you are not comfortable with the founder holding this authority under the deployed mode, do not deposit.
8.2 USDG and its issuer
The entire reserve is USDG (0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168, 6 decimals, verified
on-chain), a centralized stablecoin issued by Paxos. Issuer actions — freezing the Treasury's balance,
depegging, or contract-level intervention — directly impair backing. USDG≈$1 is an assumption
throughout the accounting, never a guarantee. Mitigations are honest accounting, not prevention:
conservation against the measured balance, the decrease-only writeDownRecognized, and subsystem
pauses.
8.3 Uniswap v3 (execution venue and only price oracle)
Buybacks execute through SwapRouter02 (0xCaf681a66D020601342297493863E78C959E5cb2) and price off
the CRWN/USDG pool's built-in TWAP (UniV3TwapLib.consult over pool.observe) — decision D3: no
external oracle dependency in the V1 USDG path. Oracle manipulation is mitigated, not eliminated, by
the TWAP window, cardinality floor, spot-vs-TWAP deviation bound, minimum pool liquidity, the NAV
discount requirement, spend caps, and the measured-delta accretion check — and by failing closed on
any doubt. A Uniswap v3 protocol bug is inherited risk. Genesis and the Treasury do not depend on
Uniswap at all.
8.4 Robinhood Chain and infrastructure
Crownridge is built on Robinhood Chain (an Arbitrum Orbit L2); this states a deployment target, not a partnership or endorsement. Sequencer downtime or censorship delays transactions — including pauses and vetoes — and inherited L2/bridge risk applies to USDG on this chain. RPC endpoints are an availability dependency for operations and monitoring, never a data-integrity dependency for contract logic.
8.5 The admin multisigs and operational keys
- Protocol multisig (timelock proposer): compromise cannot act instantly — every malicious operation sits in public queue for 48h and is guardian-cancellable (§7.1 C1). Collusion of both multisigs defeats this defense.
- Security multisig (guardian): compromise enables griefing (§6), not theft.
- Buyback executor: compromise enables only well-timed-but-valid buybacks; every economic check binds on-chain regardless of caller (D6).
- Deployer: holds nothing after deployment, by asserted construction (§2.3).
8.6 Dependencies
OpenZeppelin Contracts v5.1.0 and Solidity 0.8.30, both pinned; Uniswap v3 periphery interfaces vendored. A vulnerability in these upstreams is inherited.
9. What has NOT been done yet
Stated plainly, because the absence of these is itself a security fact:
- No external security audit. The internal reviews in §7 do not count. Per the spec: "Do not call Crownridge audited until an independent audit has actually occurred."
- No independent economic review of the Genesis/buyback/founder-mode parameters
(
docs/ECONOMICS.mdis marked proposed pending sign-off). - No mainnet deployment. Nothing described here is live; addresses for Crownridge contracts do not exist yet. External dependency addresses above are verified deployments of other systems (USDG, Uniswap v3).
- No bug bounty program yet (planned alongside the audit engagement; disclosure contact lives in
the root
SECURITY.md). - Testnet deployment, on-chain source verification, and production monitoring/alerting are prepared but not yet executed.
Mainnet is gated on all of the above (spec §88 launch conditions; founder action list items 8–12).
Related documents: docs/RISK.md (risk register), docs/INCIDENT_RESPONSE.md,
docs/DEPLOYMENT.md, docs/TESTING.md, docs/ECONOMICS.md, root SECURITY.md
(vulnerability disclosure).