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 chainId46630. - Market venue: Uniswap v3 (live on Robinhood Chain — factory
0x1f7d7550B1b028f7571E69A784071F0205FD2EfA, SwapRouter020xCaf681a66D020601342297493863E78C959E5cb2, NonfungiblePositionManager0x73991a25C818Bf1f1128dEAaB1492D45638DE0D3). - Toolchain: Foundry, Solidity
0.8.30(pinned), OpenZeppelin Contractsv5.1.0(pinned). - Upgradeability: none. The economic core is immutable — no proxies (§8).
The mechanism in 30 seconds
- 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.
- 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.
- Market — CRWN trades freely on Uniswap v3. Market price and NAV are distinct quantities; the protocol never promises they converge.
- 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.
- 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). - 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
FounderTreasuryWithdrawalon 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); revokingFOUNDER_CONTROLLER_ROLEitself is a DEFAULT_ADMIN action and therefore takes the 48h timelock — the pause is the T+0 lever, revocation the durable follow-up. Seedocs/RISK.mdanddocs/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;
cumulativeProtocolRevenueis 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; thewriteDownRecognizedpath 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
CrownridgeAccountingvia multicall (wagmi/viem) — it rendersnavPerToken, it does not derive it. - The indexer snapshots the same views over time and reconstructs supply from
Transferevents; 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
rateandfeeBps; 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
setRiskParamsprevent 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.