CROWNRIDGE
--:--:--Z
CROWNRIDGE/DOCS/ARCHITECTURE
[ARCH]Architecture← ALL DOCS

Crownridge — System Architecture

Status: pre-deployment. The contracts below are implemented and tested (unit, fuzz, invariant, and fork suites against live Robinhood Chain state) but are not deployed to mainnet and have not been externally audited. No protocol contract addresses exist yet; the only verified addresses in this document are third-party dependencies. Built on Robinhood Chain (an independent deployment target — no partnership or endorsement is implied). CRWN has no redemption mechanism in V1; nothing here is a promise of returns, yield, a price floor, or redeemability.


1. Overview

Crownridge is an on-chain permanent-capital protocol: the Treasury is the product.

  • Reserve asset: USDG (6 decimals) — 0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168 (verified live).
  • Protocol token: CRWN (18 decimals) — a deliberately plain ERC-20 (burnable, permit; no tax, rebase, blacklist, or transfer pause).
  • Chain: Robinhood Chain mainnet, chainId 4663 (Arbitrum Orbit L2). Testnet chainId 46630.
  • Market venue: Uniswap v3 (live on Robinhood Chain — factory 0x1f7d7550B1b028f7571E69A784071F0205FD2EfA, SwapRouter02 0xCaf681a66D020601342297493863E78C959E5cb2, NonfungiblePositionManager 0x73991a25C818Bf1f1128dEAaB1492D45638DE0D3).
  • Toolchain: Foundry, Solidity 0.8.30 (pinned), OpenZeppelin Contracts v5.1.0 (pinned).
  • Upgradeability: none. The economic core is immutable — no proxies (§8).

The mechanism in 30 seconds

  1. Genesis — a depositor sends USDG; in one atomic transaction the USDG lands in the Treasury, is recognized as reserve, and CRWN is minted to the depositor at a fixed rate on the net-of-fee amount. This is the only way CRWN is ever created.
  2. Treasury — recognized USDG reserve defines NAV. The Genesis fee stays in the Treasury as backing (it is not siphoned anywhere), so backing per circulating CRWN starts above the deposit rate and can only be diluted by the disclosed founder authority.
  3. Market — CRWN trades freely on Uniswap v3. Market price and NAV are distinct quantities; the protocol never promises they converge.
  4. Buyback — when CRWN trades sufficiently below NAV per token, an executor may trigger a risk-gated buyback: the Treasury funds a Uniswap v3 purchase, the CRWN received is burned, and a post-swap check on measured amounts proves the operation raised NAV per remaining token — else it reverts. Fails closed on any oracle doubt. Ships disabled.
  5. Founder authority (disclosed trust assumption) — a narrowly-scoped controller lets the founder withdraw USDG reserve. Every withdrawal immediately reduces recognized reserve and NAV, never mints CRWN, and emits a dedicated event. In UNCAPPED mode the founder can withdraw the entire recognized reserve. This is a real trust assumption, published rather than hidden (see docs/RISK.md).
  6. No redemption — V1 has no burn-for-USDG path. Backing determines NAV; it is not a claim.

2. Component & permission topology

                                   ┌────────────────────────────┐
        protocol multisig ────────▶│     TimelockController     │◀──────── security multisig
        (PROPOSER_ROLE)            │   OZ, 48h min delay        │          (CANCELLER_ROLE — veto)
                                   │   executor: open           │
                                   └─────────────┬──────────────┘
                                                 │ DEFAULT_ADMIN_ROLE (+ PARAMETER_ADMIN,
                                                 │ RESERVE_MANAGER) on every contract below
     ┌───────────────┬───────────────┬───────────┼──────────────┬────────────────────┐
     ▼               ▼               ▼           ▼              ▼                    ▼
┌──────────┐ ┌───────────────┐ ┌──────────┐ ┌──────────┐ ┌───────────────┐ ┌────────────────┐
│ Genesis  │ │ MintController│ │ Treasury │ │ Buyback  │ │ LiquidityMgr  │ │ (no admin:     │
│          │ │               │ │          │ │          │ │               │ │  Token,        │
│ PAUSER:  │ │ PAUSER:       │ │ GUARDIAN:│ │ PAUSER:  │ │               │ │  FounderCtrl,  │
│ guardian │ │ guardian      │ │ guardian │ │ guardian │ │               │ │  Accounting)   │
└──────────┘ └───────────────┘ └──────────┘ └──────────┘ └───────────────┘ └────────────────┘

  guardian = security multisig. Immediate powers only: pause subsystems, pause the founder-
  withdraw path, cancel queued timelock operations. It cannot move funds or change parameters.

Data-flow paths

A. DEPOSIT PATH (the only issuance path)
   depositor ── approve USDG ──▶ deposit(x) ──▶ CrownridgeGenesis
     (1) USDG.safeTransferFrom(depositor → Treasury); received amount measured, must equal x
     (2) Treasury.recognize(x, fee)                        [Genesis holds DEPOSITOR_ROLE]
     (3) MintController.issue(depositor, crwnOut)          [Genesis holds ISSUER_ROLE]
     (4) CRWN.mint(depositor, crwnOut)                     [MintController is the immutable minter]
   ⇒ atomic: any failure reverts the whole deposit. No CRWN without reserve (Invariants 2, 8).

B. BUYBACK PATH (accretive burn; ships disabled)
   executor ──▶ Buyback.executeBuyback(usdgIn, minOut, deadline)   [BUYBACK_EXECUTOR_ROLE]
     (1) oracle gates: cardinality, pool liquidity, TWAP + spot, deviation bound
     (2) discount gate: TWAP price ≤ NAV/token × (1 − minDiscountBps)
     (3) spend gates: per-tx cap ∧ per-day cap ∧ max-Treasury-% cap
     (4) Treasury.fundBuyback(usdgIn)                      [Buyback holds TREASURY_SPENDER_ROLE]
     (5) SwapRouter02.exactInputSingle(USDG → CRWN), floor-protected amountOutMinimum
     (6) accretion check on MEASURED deltas: usdSpent / crwnReceived ≤ NAV/token, else revert
     (7) leftover USDG → back to Treasury via recognize(); CRWN.burn(crwnReceived)
   ⇒ every gate on-chain; a buyback can never be dilutive; any oracle doubt fails closed.

C. FOUNDER PATH (disclosed authority)
   founder EOA ──▶ FounderTreasuryController.withdraw(recipient, amount)
     (1) CAPPED mode: cooldown, per-withdrawal max, rolling-window limit, treasury floor
     (2) Treasury.founderWithdraw(USDG, recipient, amount) [controller holds FOUNDER_CONTROLLER_ROLE]
         − recognizedReserve ↓ amount, NAV ↓, CRWN supply unchanged
         − emits FounderTreasuryWithdrawal (indexer alert)
   ⇒ guardian can pause this path instantly (setFounderWithdrawPaused); role revocation via 48h timelock.

D. READ PATH (frontend / SDK / indexer — view calls only)
   client ──▶ CrownridgeAccounting (stateless lens) ──▶ Treasury primitives
     grossAssets · recognizedAssets · liabilities · netAssetValue · circulatingSupply ·
     navPerToken · backingPerToken · protocolHeldSupply · conservationHolds
   ⇒ the lens holds no funds and no authority; it delegates to the same immutable Treasury
     values the Buyback enforces against, so no two components can disagree.

3. Contract reference

All eight contracts live in contracts/src/. License BUSL-1.1, Solidity 0.8.30.

Contract Holds funds Admin (AccessControl) Pausable Immutable?
CrownridgeToken — (user balances) none — no owner at all no yes
CrownridgeTreasury USDG reserve Timelock founder-path only yes
CrownridgeMintController no Timelock yes (mint pause) yes
CrownridgeGenesis no (pass-through) Timelock yes yes
CrownridgeBuyback transiently, per-tx Timelock yes yes
CrownridgeLiquidityManager LP NFT Timelock no yes
FounderTreasuryController no none — everything immutable via Treasury guardian flag yes
CrownridgeAccounting no none — pure views no yes, but replaceable (§8)

3.1 CrownridgeToken (CRWN)

Purpose. A minimal ERC-20: OZ ERC20 + ERC20Burnable + ERC20Permit. The only privileged action is mint, permanently restricted to a single immutable minter set at construction — the MintController. No tax, rebase, reflection, blacklist, confiscation, or transfer pause.

Key state. address public immutable minter. 18 decimals (explicit override).

Key functions.

Function Callable by Effect
mint(to, amount) minter (MintController) only creates CRWN
burn(amount) / burnFrom(...) any holder (ERC20Burnable) destroys CRWN — how buyback-and-burn and LP-fee burns reduce supply
permit(...) anyone with a valid signature gasless approval (EIP-2612)

Events. Standard ERC-20 Transfer / Approval. Mints appear as Transfer(0x0 → to), burns as Transfer(from → 0x0) — the indexer's supply accounting reads exactly these.

3.2 CrownridgeTreasury

Purpose. The economic core. Holds the USDG reserve and anchors the accounting convention (NAV, circulating supply, NAV per token). Reserve leaves through exactly three role-gated, non-arbitrary paths, each of which reduces recognized reserve: fundBuyback, founderWithdraw, and rescueToken (foreign tokens only — never USDG or CRWN). There is no withdrawAll, no arbitrary call, no delegatecall.

Key state.

Variable Meaning
recognizedReserve USDG units recognized as backing. Increases only via recognize() (bounded by the real balance), decreases only via spends / write-down.
cumulativeProtocolRevenue reporting only — already inside recognizedReserve, never double-counted
cumulativeFounderWithdrawn reporting/monitoring
_protocolHolders + protocolHoldersFrozen addresses whose CRWN is excluded from circulating supply; set exactly once. The Uniswap pool is deliberately not in this set (pool CRWN counts as circulating — conservative).
founderWithdrawPaused guardian emergency brake on the founder path

Key functions.

Function Role Effect
recognize(amount, revenuePortion) DEPOSITOR_ROLE (Genesis, Buyback, LiquidityManager) recognize delivered reserve; reverts ConservationViolated if it would exceed the actual balance
fundBuyback(amount) TREASURY_SPENDER_ROLE (Buyback) reduce recognized reserve, transfer USDG to the caller
founderWithdraw(asset, recipient, amount) FOUNDER_CONTROLLER_ROLE (FounderTreasuryController) USDG only; reduces recognized reserve + NAV; blocked while founderWithdrawPaused
setFounderWithdrawPaused(bool) GUARDIAN_ROLE (security multisig) immediate founder-path pause
writeDownRecognized(newAmount, reason) RESERVE_MANAGER_ROLE (Timelock) can only decrease recognized reserve (USDG impairment/freeze); can never inflate NAV
sweepExcess() RESERVE_MANAGER_ROLE recognize unsolicited USDG transfers as backing (manual by design)
rescueToken(token, to, amount) RESERVE_MANAGER_ROLE foreign tokens only — reverts for USDG or CRWN
initializeProtocolHolders(holders) DEFAULT_ADMIN_ROLE one-time, then frozen
views anyone recognizedReserve, totalReserveAssets, usdgBalance, liabilities (0 in V1), circulatingSupply, nav, navPerToken, conservationHolds, protocolHolders

Events. ReserveRecognized, BuybackFunded, FounderTreasuryWithdrawal, ReserveWrittenDown, ExcessSwept, FounderWithdrawPauseSet, TokenRescued, ProtocolHoldersInitialized. (ConservationBreach is declared for the alerting surface; the live paths fail closed — recognize reverts before a breach could be recorded.)

3.3 CrownridgeMintController

Purpose. The sole holder of CRWN's immutable minter slot, making mint authority a single auditable surface. Exactly one issuance path exists: ISSUER_ROLE, held only by Genesis, which enforces the atomic deposit→mint flow. There is deliberately no seed mint — CRWN used for initial DEX liquidity is itself acquired through Genesis (already backed), so no unbacked token is ever created.

Key state. crwn (immutable), totalIssued (monotonic, for monitoring).

Key functions.

Function Role Effect
issue(to, amount) ISSUER_ROLE (Genesis), whenNotPaused records issuance, calls crwn.mint
pause() / unpause() PAUSER_ROLE (guardian) immediate global mint stop

Events. Issued(caller, to, amount), OZ Paused/Unpaused. Granting ISSUER_ROLE is a DEFAULT_ADMIN action — i.e. routed through the 48h Timelock and cancellable by the guardian.

3.4 CrownridgeGenesis

Purpose. The reserve-in / CRWN-out entrypoint. Enforces the atomic flow: depositor USDG lands directly in the Treasury (safeTransferFrom(depositor → Treasury)), the exact received amount is measured and must equal the requested amount (rejects fee-on-transfer behavior and concurrent donations — fail closed), the Treasury recognizes the full gross, then CRWN is minted on the net-of-fee amount. Pricing is a fixed, immutable rate — never silently changed.

Key state.

Variable Mutability Meaning
rate immutable CRWN (WAD) minted per 1.0 USDG of net deposit
feeBps immutable protocol fee retained as reserve; MAX_FEE_BPS = 100 (1.00% hard ceiling)
cap, startTime, endTime, minDeposit, maxPerWallet timelock-adjustable operational window & limits
totalDeposited, depositedBy cumulative gross USDG, global and per wallet

Key functions.

Function Callable by Effect
deposit(amount) anyone (whenNotPaused, nonReentrant) the atomic deposit→recognize→mint flow; reverts MintsZero on dust
quote(grossAmount) anyone (view) (fee, crwnOut) using the identical math as deposit — the frontend's tx preview
isOpen(), remainingCapacity() anyone (view) window/cap status
setCap / setWindow / setLimits PARAMETER_ADMIN_ROLE (Timelock) bounded parameter updates
pause() / unpause() PAUSER_ROLE (guardian) immediate stop

Events. GenesisDeposit(depositor, grossUsdg, fee, crwnMinted), CapUpdated, WindowUpdated, LimitsUpdated, OZ Paused/Unpaused.

3.5 CrownridgeBuyback

Purpose. Buys CRWN below NAV on Uniswap v3 and burns it. A risk engine, not if price < NAV then buy. Ships disabled with no pool configured; enabling is a timelocked action.

Key state. Immutables: treasury, usdg, crwn, router (SwapRouter02). Set-once: pool, poolFee (token-pair and fee validated in setPool; cannot be re-pointed). Risk parameters (all timelock-adjustable within hard bounds enforced in setRiskParams — no parameter can disable the guards): minDiscountBps (1–50%), maxSpendPerTx, maxSpendPerDay, maxTreasuryBps (≤10%/day), twapWindow (10min–24h), maxDeviationBps (≤10%), minPoolLiquidity, minCardinality (≥2), maxSlippageBps (≤5%), cooldown (≥1min). Rolling state: windowStart, spentInWindow, lastBuybackAt. Lifetime stats: totalUsdgSpent, totalCrwnBurned.

Key functions.

Function Callable by Effect
executeBuyback(usdgIn, minCrwnOut, deadline) BUYBACK_EXECUTOR_ROLE (ops address) the full gated flow of path B (§2). All safety is on-chain regardless of caller.
preflight() anyone (view) would a buyback pass the oracle/discount gates right now? (ok, nav, twapPrice)
maxSpendNow() anyone (view) max USDG spendable under all three caps
setPool(pool, fee) DEFAULT_ADMIN_ROLE (Timelock) one-time pool wiring, pair/fee-validated
setEnabled(bool) / setRiskParams(p) / sweepStrayUsdg() PARAMETER_ADMIN_ROLE (Timelock) enable switch; bounded params; sweep donations into the Treasury (value can only move into the protocol)
pause() / unpause() PAUSER_ROLE (guardian) immediate stop

The minimum-out floor is the stricter of: (a) the accretive floor — enough CRWN that the price paid equals NAV per token — and (b) the TWAP-expected output minus maxSlippageBps; the executor's own minCrwnOut can only tighten it. After the swap, the accretion check re-verifies on measured balance deltas (NotAccretive reverts the whole operation). Unspent USDG from the operation returns to the Treasury and is re-recognized; balance snapshots exclude donations from the operation's accounting.

Events. BuybackExecuted(executor, usdgSpent, crwnBurned, navPerToken, twapPrice), PoolSet, EnabledSet, RiskParamsUpdated, StrayUsdgSwept.

Oracle. UniV3TwapLib — a vendored, 0.8-safe port of Uniswap's FullMath, TickMath, and OracleLibrary (consult, getQuoteAtTick, spotTick). TWAP over twapWindow seconds, priced via exact tick quotes, cross-checked against spot with a deviation bound. No Chainlink dependency in V1 (§9).

3.6 CrownridgeLiquidityManager

Purpose. Custodies the protocol-owned Uniswap v3 CRWN/USDG LP position (an NFT held by this contract) and routes its economics honestly. The LP is valued at zero in NAV — the most conservative treatment of the CRWN/Treasury circularity; it is displayed separately, never counted as backing.

Key state. Immutables: nfpm, treasury, usdg, crwn, usdgIsToken0. Set-once: positionTokenId / positionSet. Stats: totalUsdgFeesRouted, totalCrwnBurned.

Key functions.

Function Callable by Effect
registerPosition(tokenId) DEFAULT_ADMIN_ROLE (Timelock) one-time registration of the LP NFT (must already be owned by this contract)
collectFees() anyone (nonReentrant) collect accrued fees: USDG side → Treasury (recognize(amount, amount) — recognized as revenue backing), CRWN side → burned. Permissionless because it can only move value into the protocol.
windDown(liquidity, amount0Min, amount1Min, deadline) RESERVE_MANAGER_ROLE (Timelock) decrease the position; proceeds routed the same way (USDG → Treasury, CRWN → burn)
rescueToken(token, to, amount) RESERVE_MANAGER_ROLE foreign tokens only — never USDG or CRWN
onERC721Received NFPM accepts the LP NFT safe-transfer

Events. PositionRegistered, FeesRouted(usdgToTreasury, crwnBurned), LiquidityWoundDown, TokenRescued.

3.7 FounderTreasuryController

Purpose. The disclosed, narrowly-scoped founder withdrawal authority. It can do exactly one thing: instruct the Treasury to transfer USDG out. It has no arbitrary call, delegatecall, token approval, mint, oracle, upgrade, or role-admin power. The founder address is immutable; the controller cannot replace itself.

Trust disclosure (do not skip this). The founder authority has direct economic control over the withdrawable reserve, up to the configured mode. In UNCAPPED mode the founder can withdraw the entire recognized reserve. Every withdrawal reduces recognized reserve and NAV immediately, never changes CRWN supply, and emits FounderTreasuryWithdrawal on the Treasury. The chosen mode and its parameters are published in the deployment manifest and on the public transparency page. Emergency response: the guardian can pause the Treasury's founder-withdraw path immediately (setFounderWithdrawPaused); revoking FOUNDER_CONTROLLER_ROLE itself is a DEFAULT_ADMIN action and therefore takes the 48h timelock — the pause is the T+0 lever, revocation the durable follow-up. See docs/RISK.md and docs/INCIDENT_RESPONSE.md.

Key state. All configuration is immutable, set at construction: treasury, founder, reserveAsset, capped (mode), and the CAPPED-mode limits maxPerWithdrawal, rollingLimit, rollingWindow, minTreasuryFloor, cooldown. Mutable rolling state: windowStart, withdrawnInWindow, lastWithdrawAt.

Key functions.

Function Callable by Effect
withdraw(recipient, amount) founder EOA only (onlyFounder, nonReentrant) CAPPED-mode checks (cooldown, per-withdrawal max, treasury floor, rolling limit), then treasury.founderWithdraw(USDG, recipient, amount)
withdrawableNow() anyone (view) max the founder could withdraw right now under the configured mode — surfaced on the transparency page

Events. FounderWithdrawExecuted(recipient, amount, capped) here, plus FounderTreasuryWithdrawal(asset, recipient, amount) on the Treasury (the indexer alerts on both).

3.8 CrownridgeAccounting

Purpose. A stateless valuation lens. Holds no funds, has no authority; exists so integrators and the frontend read the mandated accounting surface from one place. It cannot disagree with enforced accounting because every value delegates to the immutable Treasury primitives — the same values the Buyback enforces against. Because it is pure-view and fund-less, it is the one component that may be replaced (deploy a new lens, repoint clients) without touching the economic core — e.g. when a V2 adds assets.

Surface (all views; reserve amounts in raw 6-dec USDG, values in 1e18 USD fixed point): grossAssets, recognizedAssets, liabilities (0 in V1, representable), netAssetValue, circulatingSupply, navPerToken (0 ⇒ "N/A"), totalSupply, protocolHeldSupply, backingPerToken, conservationHolds, plus USDG-spelled aliases usdGBalance, recognizedUSDG, navPerCRWN. backingPerToken is deliberately distinct in name from market price and from any redemption value — those are different concepts, and V1 has no redemption.


4. The anchored accounting convention (CrownridgeMath)

All NAV / circulating / per-token math lives in one internal library, contracts/src/libraries/CrownridgeMath.sol, compiled into the Treasury and Genesis. No two components can disagree, because there is only one definition and one source of state:

reserveToWad(amount, decimals)       6-dec USDG → 1e18 USD fixed point (×1e12), explicit, no floats
netAssetValueWad(reserve, dec, liab) NAV = reserve in WAD − liabilities (liabilities = 0 in V1,
                                     kept in the signature so the convention stays representable)
navPerToken(navWad, circulating)     NAV×1e18 / circulating; returns 0 when circulating == 0
                                     (callers MUST render 0 as "N/A", never as $0.00)
crwnForDeposit(net, rate, dec)       Genesis mint amount: netWad × rate / 1e18, floor division —
                                     rounding dust always favors the Treasury
isBacked(circ, reserve, rate, dec)   restated Invariant 2: circulating ≤ reserve-backable supply

Convention facts, enforced in code and tests:

  • NAV = recognized USDG reserve. Nothing else. LP position valued at zero; unsolicited transfers are excess (not backing) until swept; cumulativeProtocolRevenue is reporting-only.
  • circulatingSupply = CRWN.totalSupply() − balances of the frozen protocol-holder set (Treasury, Buyback, MintController, LiquidityManager, FounderController). Pool-held CRWN counts as circulating — conservative, it lowers navPerToken.
  • USDG ≈ $1 is a stated assumption, not a guarantee (see docs/RISK.md; the writeDownRecognized path exists precisely because it can fail).
  • The Buyback never reads the replaceable lens — it reads treasury.navPerToken() directly, so replacing the lens can never change what buybacks enforce.

Reuse off-chain. The rule is: the frontend formats but never computes accounting differently from the contracts.

  • The website/dashboard reads current values straight from CrownridgeAccounting via multicall (wagmi/viem) — it renders navPerToken, it does not derive it.
  • The indexer snapshots the same views over time and reconstructs supply from Transfer events; its NAV arithmetic is checked by a differential test (TypeScript implementation vs the Solidity library over fuzzed inputs) so history can never drift from the chain's definition.
  • The SDK (@crownridge/sdk) ships the ABIs and typed viem read helpers pointed at the lens, so integrators inherit the same single convention.

5. Role & permission topology

Role Held by On Grants
DEFAULT_ADMIN_ROLE TimelockController (48h) MintController, Treasury, Genesis, Buyback, LiquidityManager role management; one-time wiring (initializeProtocolHolders, setPool, registerPosition)
PARAMETER_ADMIN_ROLE TimelockController Genesis, Buyback bounded parameter updates, setEnabled, sweepStrayUsdg
RESERVE_MANAGER_ROLE TimelockController Treasury, LiquidityManager writeDownRecognized, sweepExcess, rescueToken, windDown
PAUSER_ROLE security multisig (guardian) MintController, Genesis, Buyback immediate pause/unpause per subsystem
GUARDIAN_ROLE security multisig Treasury immediate setFounderWithdrawPaused
CANCELLER_ROLE (timelock) security multisig TimelockController veto any queued operation — turns a compromised-proposer T+48h drain into a cancellable stalemate
PROPOSER_ROLE (timelock) protocol multisig TimelockController queue admin operations
executor (timelock) open (address(0)) TimelockController anyone may execute a ready, un-cancelled operation
ISSUER_ROLE CrownridgeGenesis (contract) MintController issue()
DEPOSITOR_ROLE Genesis, Buyback, LiquidityManager (contracts) Treasury recognize()
TREASURY_SPENDER_ROLE CrownridgeBuyback (contract) Treasury fundBuyback()
FOUNDER_CONTROLLER_ROLE FounderTreasuryController (contract) Treasury founderWithdraw()
BUYBACK_EXECUTOR_ROLE ops address Buyback executeBuyback() — every safety check binds on-chain regardless of caller
— (immutable, not a role) founder EOA FounderTreasuryController withdraw()
— (immutable, not a role) MintController CrownridgeToken mint()

Role identifiers are canonical constants in contracts/src/libraries/Roles.sol (namespaced CROWNRIDGE_* hashes), so the same concept is never spelled two ways.

The deployer retains nothing. script/Deploy.s.sol deploys, wires roles, hands DEFAULT_ADMIN_ROLE of every contract to the Timelock, renounces the deployer's own admin (including timelock admin), then _postAssert reverts the broadcast if the deployer still holds any admin, the Buyback is enabled, or a pool is set. A separate VerifyDeployment re-checks independently.


6. What is immutable

  • Everything is deployed without proxies. There is no upgrade path, hidden or otherwise, on any contract (spec §53: immutable core).
  • Immutable forever: CRWN's minter; the Treasury's reserve asset and token bindings; Genesis rate and feeBps; the Buyback's treasury/token/router bindings; the founder address, mode, and CAPPED limits; the Accounting lens's bindings.
  • Set once, then frozen: the Treasury's protocol-holder set; the Buyback pool; the LiquidityManager's position id.
  • Timelock-adjustable within hard-coded bounds: Genesis operational params (cap, window, limits); Buyback risk params (bounds in setRiskParams prevent any parameter from disabling the guards); writeDownRecognized (decrease-only).
  • Replaceable: only CrownridgeAccounting — and only in the sense that a new lens can be deployed and clients repointed; the old one keeps working and neither holds funds nor authority.

What this buys: no upgrade-key risk, no storage-collision risk, no "the contract you audited is not the contract that runs" risk. What it costs: bugs in the core cannot be patched in place — the responses are per-subsystem pause, reserve write-down, LP wind-down, and (worst case) redeployment. This trade-off is deliberate and documented (spec §53, §162).


7. Folded spec §52 modules

Spec §52 lists ten candidate modules and instructs: "Do not deploy every module if a simpler architecture is demonstrably safer. Every contract must have a clear reason to exist." Four were folded rather than deployed:

Spec module Where it went Why folding is safer
CrownridgeRegistry the deployment manifest (deployments/<chainId>-latest.json, emitted by Deploy.s.sol) + the SDK's address book discovery is a static, verifiable artifact; an on-chain registry would be one more admin surface that could be re-pointed
CrownridgeRevenueRouter CrownridgeTreasury itself V1's only revenue (Genesis fee, LP fee USDG) is retained as reserve — the Treasury IS the controlled revenue contract (spec §64). A router would add a hop that could be misdirected, for zero function
CrownridgeOracleAdapter UniV3TwapLib inside the Buyback the only price V1 needs is CRWN/USDG, which only the pool has; spec §108 forbids unnecessary oracle dependencies in the USDG path. An adapter indirection would add an upgradeable-ish seam in the most manipulation-sensitive component
CrownridgeEmergency per-subsystem Pausable + guardian roles on each contract (spec §149) a central emergency contract is a central point of failure; per-subsystem pause limits blast radius and matches the guardian's actual powers

The other six (Token, Treasury, Accounting, Genesis, Buyback, LiquidityManager) exist as specified, plus MintController and FounderTreasuryController as dedicated narrow surfaces for the two most sensitive powers (minting; founder withdrawal).


8. Off-chain stack

None of this is trusted by the contracts; it is read-and-display infrastructure over public chain state.

Component Stack Role
indexer/ TypeScript, Fastify + Postgres, Docker (Fly.io target) ingests chain events (GenesisDeposit, Issued, CRWN Transfer mints/burns, BuybackExecuted, FounderTreasuryWithdrawal, ReserveRecognized, ReserveWrittenDown, pauses) and periodic NAV snapshots → historical read-only API /api/v1/*, /healthz, /openapi.json; alert rules (unexpected mint, treasury outflow, founder withdrawal, supply mismatch — spec §86/§147). Public, rate-limited, no accounts.
website/ Next.js + Tailwind v4 + wagmi/viem (Vercel target) marketing site + dashboard + transparency and status pages. Current state is read directly on-chain through the Accounting lens (multicall); history comes from the indexer. Degraded-data states render N/A — never invented numbers. Wallet connect is the only "auth"; there is no founder-withdrawal UI.
sdk/ts/ @crownridge/sdk (TypeScript) ABIs + deployment addresses + typed viem read helpers over the lens + typed client for the indexer API.

Division of trust: the chain is the source of truth; the indexer is a cache; the website is a formatter. Any discrepancy between them is a bug in the off-chain stack by definition (and one of the alert conditions).


9. Source map

contracts/src/
├── CrownridgeToken.sol                 CRWN ERC-20 (immutable minter)
├── CrownridgeTreasury.sol              reserve custody + anchored accounting + gated outflows
├── CrownridgeMintController.sol        sole minter; single issuance path; pausable
├── CrownridgeGenesis.sol               atomic deposit → recognize → mint
├── CrownridgeBuyback.sol               risk-gated buyback-and-burn (ships disabled)
├── CrownridgeLiquidityManager.sol      protocol-owned LP custody + fee routing (LP = 0 in NAV)
├── FounderTreasuryController.sol       disclosed founder withdrawal authority
├── CrownridgeAccounting.sol            stateless view lens (replaceable)
├── interfaces/                         ITreasury, IMintController, IUniswapV3, INonfungiblePositionManager
└── libraries/
    ├── CrownridgeMath.sol              the single accounting convention
    ├── Roles.sol                       canonical role ids
    └── UniV3TwapLib.sol                vendored 0.8-safe TWAP consult/quote (FullMath/TickMath/OracleLibrary)

contracts/script/   Deploy.s.sol (env-driven, assertion-guarded, manifest-emitting) · VerifyDeployment.s.sol
contracts/test/     unit + fuzz suites per contract · invariant/ (conservation, no-naked-mint,
                    supply accounting, genesis cap, minter immutability, NAV determinism) ·
                    fork/ForkRobinhood.t.sol (live-mainnet fork: real USDG, real Uniswap v3)

Related documents: docs/ECONOMICS.md (parameters and formulas — the economic source of truth), docs/SECURITY.md, docs/RISK.md (including the founder-authority and USDG-assumption disclosures), docs/DEPLOYMENT.md, docs/TESTING.md, docs/INCIDENT_RESPONSE.md.

F2TREASURYF3TRANSPARENCYF4DOCSF5WHITEPAPERF6STATUS