Skip to content

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       │
    └─────────────────────────┘