CROWNRIDGE
--:--:--Z
CROWNRIDGE/DOCS/SECURITY MODEL
[SECU]Security model← ALL DOCS

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

  1. Immutable core, no proxies. No contract in the economic core is upgradeable. What is deployed is what runs, forever (§4).
  2. 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.
  3. Fail closed. Any doubt — oracle staleness, deviation, thin liquidity, an unexpected transfer amount — reverts. The protocol never acts on uncertain data.
  4. Measured deltas, never formulas. Value movements are accounted from actual balanceOf deltas (Genesis deposits, buyback spend/receive, buyback refunds), never from assumed transfer amounts.
  5. 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).
  6. 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 delegatecall anywhere in CrownridgeToken, CrownridgeTreasury, CrownridgeMintController, CrownridgeGenesis, CrownridgeBuyback, CrownridgeLiquidityManager, or FounderTreasuryController.
  • Economic constants are immutable in the bytecode: the token's minter, Genesis rate and feeBps, 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.setRiskParams rejects values outside hard ranges), Genesis operational parameters (cap/window/limits — the price rate and feeBps are 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 values CrownridgeBuyback acts 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). executeBuyback originally computed its post-swap refund from the contract's absolute USDG balance. A permissionless USDG donation to the Buyback contract would make leftover > usdgIn, underflow spentInWindow -= leftover, and — because the contract is immutable — permanently brick executeBuyback. Fix: leftover is now measured against a balBeforeFund snapshot taken before treasury.fundBuyback, so it is strictly this operation's unspent portion (≤ usdgIn, donations excluded), and a timelocked sweepStrayUsdg() routes any donated USDG into the Treasury as recognized backing. Regression-tested in Buyback.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 recognizedReserve and NAV immediately and emits FounderTreasuryWithdrawal (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.md is 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).

F2TREASURYF3TRANSPARENCYF4DOCSF5WHITEPAPERF6STATUS