Architecture Diagrams¶
Visual reference for the Centurion Seat Manager system architecture, vault economics, and lifecycle flows.
1. System Overview¶
OFF-CHAIN / APP LAYER
┌──────────────────────────────────────────────────────────────────┐
│ │
│ Admin Frontend (Next.js 15) │
│ - Dashboard, seat/operator/user CRUD │
│ - Session auth, RBAC (ADMIN > OPERATOR > VIEWER) │
│ - Proxies API calls to Fastify backend │
│ │
│ Seat Manager API (Fastify) │
│ - REST routes + Zod validation │
│ - BLS key generation via deposit-cli (secrets via stdin) │
│ - Vault deployment, approve/deposit/revoke │
│ - PostgreSQL (Prisma) + idempotency + audit logging │
│ - OpenTelemetry tracing (optional) │
│ │
│ Watchers (separate process) │
│ - EL watcher: polls DepositEvent logs every 12s │
│ - CL watcher: polls beacon API every 24s │
│ - Auto-transitions: DEPOSITED → SEEN_BY_CL → ACTIVE │
│ │
│ Staker Console (React SPA) │
│ - Wallet-scoped validator dashboard │
│ - Claim rewards (single write action against vault contracts) │
│ │
└──────────────────────────────────────────────────────────────────┘
ON-CHAIN / PROTOCOL LAYER
┌──────────────────────────────────────────────────────────────────┐
│ │
│ DepositContractCTN │
│ - Allowlist gate: intent = keccak256(pubkey||wc||amt||depositor)│
│ - Per-depositor/per-amount intents (addAllowedDepositFor) │
│ - Owner-only depositor gate │
│ - Consumed intents tracking (no re-allowlisting) │
│ - Timelocked irreversible disable │
│ - Two-step ownership transfer │
│ │
│ TreasuryRouter │
│ - Forwarding proxy: signer calls execute() → vault/factory │
│ - Time-locked signer rotation (7 days + acceptance) │
│ - All vaults set their treasury to the router address │
│ │
│ VaultFactory │
│ - Deploys WithdrawalVaults via TreasuryRouter only │
│ - Enforces STANDARD_PRINCIPAL_TARGET (32 CTN) │
│ - On-chain registry: isVault, vaultByValidatorPubkeyHash │
│ - Prevents duplicate vaults per validator pubkey │
│ │
│ WithdrawalVault v3 (1 per validator) │
│ - Immutable: treasury (router), principal, claim delay │
│ - Mutable: beneficiary (7-day rotation + treasury approval) │
│ - Running: 24h delayed claims + rate limiting (per 30d period) │
│ - SettlementProposed: claims paused, cancellable │
│ - ExitSettlement: instant claims, principal-first shortfall │
│ - Post-settlement drain (90 days) │
│ - CIP-7002 validator exit (no BLS key required) │
│ - Conservation: balance + claimed = lifetime inflows │
│ │
└──────────────────────────────────────────────────────────────────┘
CONSENSUS / VALIDATION
┌──────────────────────────────────────────────────────────────────┐
│ │
│ EL + CL + VC │
│ - Validators attest and propose blocks │
│ - Protocol sweeps excess above 32 CTN to vault (~5 day cycle) │
│ - Full balance swept to vault on exit │
│ │
└──────────────────────────────────────────────────────────────────┘
2. Seat Creation Flow (BYO-BLS — Primary Path)¶
External Operator Foundation (Admin UI / API)
│ │
│ provides BLS pubkey │
│ (generated externally — never shared) │
└────────────────────────────────────────>│
│
▼
┌─────────────────────────────────────────────────┐
│ 1. Deploy WithdrawalVault via VaultFactory │
│ (called through TreasuryRouter.execute()) │
│ factory.deployVault(beneficiary, pubkey, │
│ principalTarget=32 CTN, claimDelay=86400, │
│ maxClaimPerPeriod) │
│ treasury = TreasuryRouter address (immutable)│
│ │
│ 2. Derive wc = 0x01 + 11×0x00 + vaultAddress │
└─────────────────┬───────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ 3. Store in PostgreSQL: │
│ - seat (pubkey, wc, intentHash, status) │
│ - seat_operation (workflow journal) │
│ Status = CREATED │
└─────────────────┬───────────────────────────────┘
│
┌────────────────────────────────────────────┘
│ shares vault address / WC
▼
External Operator
│
│ generates deposit data signed over vault WC
│ submits via POST /v1/seats/:id/deposit-data
│ (pubkey + WC + amount validated server-side)
▼
For foundation-managed validators, an internal "Generate Keys" flow auto-generates BLS keypairs from a mnemonic. This is not used for external operators.
3. Vault Economics — Two Phases¶
Running Phase¶
Protocol sweeps every ~5 days
(Capella credit — no EVM execution)
Consensus Layer ──────────────────────────────────────> Vault Balance
│
Beneficiary calls initiateClaimRewards(amount) │
│ │
▼ │
Rate limit check (maxClaimPerPeriod per 30d) │
│ │
▼ │
24-hour timer starts │
│ │
├── cancelPendingClaim() → claim cancelled │
│ │
▼ (after 24 hours) │
finalizeClaimRewards(to) → CTN sent to wallet ◄───────────┘
│
▼
Rate limit enforced + rewardsClaimedWei incremented
Key rule: During Running phase, rewards are claimable only while vault balance is below principalTargetWei (32 CTN). At or above the principal target, claimableRewardsWei = 0 and Running-phase beneficiary claim calls revert with PrincipalProtectionActive.
Rate limiting: Claims capped at maxClaimPerPeriod per 30-day CLAIM_PERIOD. Checked preventively at initiation, enforced at finalization. Set to type(uint256).max to disable.
Settlement Flow (Three Phases)¶
RUNNING (phase 0)
│
Treasury calls proposeSettlement(maxRewardsClaimedWei)
│ front-running guard: reverts if rewardsClaimedWei > max
│ cancels any pending Running-phase claim
│ claimableRewardsWei = 0 (all claims paused)
▼
SETTLEMENT_PROPOSED (phase 1)
│
├── cancelSettlement() → returns to RUNNING
│
▼ (after SETTLEMENT_DELAY = 1 hour)
Treasury calls finalizeSettlement()
│ (irreversible)
▼
EXIT_SETTLEMENT (phase 2)
│
┌───────────────────────────────────────────────┐
│ Settlement Accounting: │
│ │
│ totalLifetime = balance + principalClaimed │
│ + rewardsClaimed │
│ │
│ claimableRewards = totalLifetime │
│ - principalTarget (32 CTN) │
│ - rewardsClaimed │
│ │
│ claimablePrincipal = min(totalLifetime, 32) │
│ - principalClaimed │
│ (capped at current balance)│
└──────────────────────────────────────────────┘
│ │
▼ ▼
Beneficiary: Treasury:
claimRewards() claimPrincipal()
(instant, excess (instant, up to
above 32 CTN) 32 CTN)
SHORTFALL: if totalLifetime < 32 CTN
→ Treasury recovers all available inflows
→ Beneficiary receives only the excess, if any
│
After DRAIN_DELAY (90 days from finalization):
Treasury calls drainRemainder(to)
→ Sweeps residual balance (late protocol inflows)
→ Credits principalClaimedWei (preserves conservation)
4. Deposit Contract Gate Logic¶
deposit(pubkey, wc, signature, root)
│
▼
┌───────────────────────────┐
│ ownerOnlyDepositorEnabled │
│ == true? │
└──────┬────────┬───────────┘
Yes │ │ No
▼ │
┌──────────────┐ │
│ msg.sender │ │
│ == owner? │ │
└───┬─────┬────┘ │
No │ Yes│ │
▼ │ │
REVERT │ │
▼ ▼
┌───────────────────────┐
│ pubkeyAllowlistEnabled│
│ == true? │
└──────┬───────────┬────┘
Yes │ │ No
▼ │
┌──────────────────┐ │
│ intentHash = │ │
│ keccak256(pubkey │ │
│ || wc || amount │ │
│ || msg.sender) │ │
│ │ │
│ allowedDeposits │ │
│ [intentHash]? │ │
└──┬─────┬─────────┘ │
No │ Yes│ │
▼ │ │
REVERT │ │
▼ ▼
┌─────────────────┐
│ Validate root, │
│ amount, lengths │
│ → DEPOSIT │
│ (consumes intent│
│ + marks consumed│
│ — one-use, │
│ no re-allowlist│
└─────────────────┘
5. Full Lifecycle Timeline¶
Time ──────────────────────────────────────────────────────────────>
│ Generate │ Approve │ Deposit │ Watchers
│ (BLS keygen + │ (allowlist │ (32 CTN to │ (automatic)
│ vault deploy) │ on-chain) │ contract) │
│ │ │ │
▼ ▼ ▼ ▼
CREATED ──────> ALLOWLISTED ──────> DEPOSITED ──> SEEN_BY_CL ──> ACTIVE
(EL watcher) (CL watcher) (CL watcher)
│
│ Protocol
│ sweeps
▼
Vault receives ETH
│
│ User claims
▼
Rewards to wallet
6. TreasuryRouter & VaultFactory¶
Foundation EOA (activeSigner)
│
│ execute(target, calldata)
▼
┌────────────────────────────────────────────┐
│ TreasuryRouter │
│ │
│ activeSigner ──── controls all vaults │
│ │
│ Signer Rotation (7-day delay): │
│ proposeSignerRotation(new) ──> 7 days ──> │
│ new signer calls finalizeSignerRotation() │
│ (or cancelSignerRotation() to abort) │
│ │
│ execute(target, data) → target.call(data) │
│ receive() → accepts ETH (principal claims) │
└─────────┬──────────────────┬────────────────┘
│ │
┌────┘ └────┐
▼ ▼
┌──────────────┐ ┌──────────────────┐
│ VaultFactory │ │ WithdrawalVault │
│ │ │ (any deployed) │
│ deployVault │ │ │
│ (only via │ │ proposeSettlement│
│ router) │ │ claimPrincipal │
│ │ │ triggerExit │
│ Registry: │ │ approveRotation │
│ isVault[] │ │ drainRemainder │
│ vaultByHash │ │ etc. │
└──────────────┘ └──────────────────┘
Beneficiary Rotation Flow¶
Current Beneficiary Treasury (via Router)
│ │
│ proposeBeneficiaryRotation(new) │
▼ │
7-day BENEFICIARY_ROTATION_DELAY │
│ │
│ ┌─────── cancelBeneficiaryRotation() ─┘
│ │ (rejects at any time)
│ │
▼ ▼
approveBeneficiaryRotation() ◄─────────────┘
│ (after delay, cancels pending claim)
▼
beneficiary = newAddress
7. Auth & RBAC Model¶
┌──────────────────────────────────┐
│ Request │
│ Authorization: Bearer <token> │
└──────────────┬───────────────────┘
│
▼
┌──────────────┐
│ Token type? │
└──┬───────┬───┘
│ │
session:... raw token
│ │
▼ ▼
┌──────────┐ ┌────────────────┐
│ Validate │ │ timingSafeEqual│
│ session │ │ vs ADMIN_TOKEN │
│ (HMAC │ │ │
│ lookup) │ │ role = ADMIN │
└────┬─────┘ └───────┬────────┘
│ │
▼ ▼
┌─────────────────────────┐
│ Check requiredRole() │
│ │
│ ADMIN: users, approve, │
│ revoke, deposit, │
│ generate, freeze, │
│ switch │
│ │
│ OPERATOR: create seats, │
│ create operators │
│ │
│ VIEWER: read seats, │
│ read operators, │
│ read audit logs │
└─────────────────────────┘