Skip to content

WithdrawalVault v3 Implementation Report

Date: 2026-04-10 Chain: Centurion Mainnet (chain ID 286) Status: Contracts deployed with post-audit hardening (TR-01/TR-02/DC-01/DC-02/WV-03). Certora coverage is complete across standard and strict configs (8 pass; see Section C). Live e2e rerun is partial and still pending final green status.


Summary

WithdrawalVault v3 is a major upgrade over v2, introducing three-phase settlement with a cancellable proposal window, per-period claim rate limiting, time-locked beneficiary rotation, CIP-7002 execution-layer validator exit, post-settlement drain, and principal protection during Running phase. Two new supporting contracts — TreasuryRouter and VaultFactory — replace the previous direct-deploy model with a governed factory pattern.

What changed from v2

Area v2 v3
Settlement Instant (startExitSettlement) 3-phase: proposeSettlement -> 1h delay -> finalizeSettlement, cancellable
Running-phase claims Full balance claimable at any time Blocked when balance >= principalTargetWei (PrincipalProtectionActive)
Rate limiting None maxClaimPerPeriod per 30-day CLAIM_PERIOD
Beneficiary Immutable Mutable with 7-day delay + treasury approval
Validator exit Not supported CIP-7002 via triggerValidatorExit() — no BLS key needed
Post-settlement drain Not supported drainRemainder() after 90-day DRAIN_DELAY
Front-running guard None proposeSettlement(maxRewardsClaimedWei) reverts if exceeded
Deployment Direct deploy by owner VaultFactory (via TreasuryRouter only), on-chain registry
Treasury Direct EOA TreasuryRouter forwarding proxy with 7-day signer rotation

A. Solidity Contracts

WithdrawalVault v3

File: centurion-networks/mainnet/contracts/src/WithdrawalVault.sol

Constants

Constant Value Purpose
MIN_CLAIM_DELAY 3,600s (1 hour) Minimum allowed claim delay
MAX_CLAIM_DELAY 30 days Maximum allowed claim delay
STANDARD_PRINCIPAL_TARGET 32 ether Standard validator principal
MAX_PRINCIPAL_TARGET 10,000 ether Upper bound on principal target
SETTLEMENT_DELAY 1 hour Minimum time between proposal and finalization
CLAIM_PERIOD 30 days Rate-limit rolling window
BENEFICIARY_ROTATION_DELAY 7 days Cooling period for beneficiary changes
DRAIN_DELAY 90 days Wait after settlement finalization before drain

Immutables (set at construction, never change)

Field Description
treasury TreasuryRouter address (or EOA). Controls settlement, principal claims, exit, rotation approval, drain.
validatorPubkeyHash sha256(validatorPubkey) — binds vault to exactly one validator
principalTargetWei Foundation principal amount (32 CTN)
shortfallPolicy Always PrincipalFirst — treasury recovers principal before beneficiary gets rewards
CLAIM_DELAY Seconds between claim initiation and finalization (default 24h)
exitRequestContract CIP-7002 system contract address for execution-layer validator exits
maxClaimPerPeriod Maximum claimable per 30-day period. type(uint256).max disables rate limiting.

Settlement Phases

Running (0)  ──proposeSettlement──>  SettlementProposed (1)  ──finalizeSettlement──>  ExitSettlement (2)
                                     cancelSettlement
                                      Running (0)
  • Running (0): Normal operation. Beneficiary claims rewards via initiate/wait/finalize. Claims blocked when balance >= 32 CTN.
  • SettlementProposed (1): All claims paused. Pending claims auto-cancelled. Treasury can cancel (back to Running) or finalize (after 1h delay).
  • ExitSettlement (2): Irreversible. Instant claims, no rate limit. Beneficiary claims rewards, treasury claims principal. After 90 days, treasury can drain remainder.

Running-Phase Claim Flow

  1. Beneficiary calls initiateClaimRewards(amountWei) — starts the claim timer
  2. Rate limit checked preventively (does not consume allowance)
  3. After CLAIM_DELAY (24h), beneficiary calls finalizeClaimRewards(to) — CTN transferred
  4. Rate limit enforced and allowance consumed
  5. cancelPendingClaim() available at any time before finalization

Principal protection: If vault balance reaches or exceeds principalTargetWei (32 CTN), both initiateClaimRewards and finalizeClaimRewards revert with PrincipalProtectionActive. This prevents beneficiary from draining principal during Running phase (e.g., if a full withdrawal sweep lands in the vault).

Rate Limiting

  • maxClaimPerPeriod is immutable, set at construction
  • Claims accumulate in claimedInCurrentPeriod within each 30-day CLAIM_PERIOD
  • Period resets automatically when block.timestamp >= currentPeriodStart + CLAIM_PERIOD
  • Verified at initiation (early feedback) and enforced at finalization (authoritative)
  • Set to type(uint256).max to effectively disable

Beneficiary Rotation

  1. Current beneficiary calls proposeBeneficiaryRotation(newAddress) — 7-day timer starts
  2. After delay, treasury calls approveBeneficiaryRotation() — beneficiary updated, pending claim cancelled
  3. Treasury can cancelBeneficiaryRotation() at any time to reject

Guards: new beneficiary cannot be zero address, treasury address, or a contract.

CIP-7002 Validator Exit

triggerValidatorExit(validatorPubkey) — treasury-only, sends exit request to the CIP-7002 system contract. The vault IS the withdrawal credentials address, so the beacon chain honors the request. No BLS signing key required.

Fee behavior (post-audit hardening): - Reads required fee from the system contract (currentExitRequestFeeWei()) - Reverts on underpayment (InsufficientExitFee) - Forwards exactly the required fee and refunds any overpayment to caller

Payload format: validatorPubkey (48 bytes) || amount (8 bytes BE, 0 = full exit).

Post-Settlement Drain

drainRemainder(to) — treasury-only, available 90 days after settlement finalization. Sweeps residual balance (late protocol inflows) to treasury. Credits principalClaimedWei to preserve conservation invariant.

Conservation Law

totalLifetimeWei = address(this).balance + principalClaimedWei + rewardsClaimedWei

Maintained at every state transition. Verified in Forge tests and Certora formal verification.

Accounting Separation

  • rewardsClaimedWei — only incremented by beneficiary calls (finalizeClaimRewards in Running, claimRewards in ExitSettlement). Never by treasury.
  • principalClaimedWei — only incremented by treasury calls (claimPrincipal in ExitSettlement, drainRemainder). Never by beneficiary.

TreasuryRouter

File: centurion-networks/mainnet/contracts/src/TreasuryRouter.sol

Forwarding proxy that sits between the foundation signer EOA and all vaults/factories. All vaults set their treasury immutable to the router address.

Feature Detail
execute(target, data) Forwards arbitrary calls to vaults/factory. Active signer only. Non-reentrant.
sweepETH(to, amount) Active signer can recover ETH currently held by the router (including full sweep with amount=0).
receive() Accepts ETH (e.g., principal claims routed back)
Signer rotation 7-day delay: proposeSignerRotation -> wait -> new signer calls finalizeSignerRotation
Rotation guard Rejects newSigner == address(this) to prevent accidental signer-control bricking.
Cancel rotation cancelSignerRotation() by current signer

VaultFactory

File: centurion-networks/mainnet/contracts/src/VaultFactory.sol

Deploys WithdrawalVault instances with enforced parameters and on-chain registry.

Feature Detail
deployVault(beneficiary, pubkey, principal, claimDelay, maxClaimPerPeriod) Creates vault. Router-only.
STANDARD_PRINCIPAL_TARGET enforcement Reverts if principalTargetWei != 32 ether
Duplicate prevention vaultByValidatorPubkeyHash — one vault per validator, reverts with ValidatorAlreadyHasVault
Registry isVault[address], vaultByValidatorPubkeyHash[hash], allVaults[], vaultCount()
Hardcoded params shortfallPolicy = PrincipalFirst, treasury = treasuryRouter, exitRequestContract from constructor

B. Forge Tests

File: centurion-networks/mainnet/contracts/test/WithdrawalVault.t.sol

204 tests, 3,619 lines. Covers WithdrawalVault, TreasuryRouter, and VaultFactory in a single unified test file.

Section Tests Coverage
Constructor validation 16 Zero addresses, invalid lengths, bounds, same treasury/beneficiary, contract beneficiary, contract treasury (allowed)
Withdrawal credentials 1 0x01 \|\| 11x00 \|\| address format
Authorization guards 8 onlyTreasury and onlyBeneficiary on all gated functions
Running phase (initiate/finalize/cancel) 12 Happy path, delay enforcement, duplicate prevention, zero-resolve, empty vault, exceeds balance, multi-cycle accumulation, conservation
Three-phase settlement 12 Propose, finalize after delay, cancel, double-propose, already finalized, not proposed, delay not met, pending claim cancellation, front-running guard, claims paused during proposal
ExitSettlement accounting 8 Full principal recovery, balance-capped, rewards excess, zero below target, running claims cancel out, conservation law, exact target, surplus
Shortfall scenarios 3 After running claims, with zero pre-exit rewards, severe near-zero exit
Claim ordering 3 Operator first, treasury first, interleaved partial
Race conditions 3 Pending small claim + exit balance arrives, fresh delay requirement, settlement blocks new claim
Claim amount validation 7 Exceeds available, nothing available, cancel no pending, finalize no pending, exact target
Destination routing 5 Zero -> msg.sender default for EOAs, contract-treasury zero-destination reverts, specific destination works, self-destination reverts (for finalize, claimRewards, claimPrincipal)
Timing independence 3 Settlement before/after exit, same outcome regardless of order
Accounting invariants 3 Total lifetime after claims, balance + claimed = lifetime, claimable consistency
Pending claim status view 4 No pending, pending not ready, pending ready, cleared after proposal
Edge cases 8 Zero pre-exit rewards, pre-exit only, multiple inflows, consensus-style credit, partial across phases, zero balance at settlement, balance cap prevents revert, normal exit after running claims
Reentrancy 4 Initiate, finalize, claimPrincipal, proposeSettlement (via ReentrantBeneficiary/ReentrantTreasury helpers)
Events 9 All event emissions verified
Fuzz tests (256 runs each) 4 Running claims + exit conservation, arbitrary ordering, claimable rewards <= balance, claimable principal <= balance
Adversarial scenarios 10 Beneficiary drain attempt, treasury reacts in time, principal target too low/high, unexpected inflows during/before settlement, all inflows are rewards during running, early settlement protects rewards, running overclaim, minimum claim delay protection
CIP-7002 exit 10 Success, zero value, only treasury, pubkey mismatch, invalid length, contract reverts, payload format, immutability, does not affect balance, exit request contract in constructor
Rate limiting 8 Within limit, exceeds reverts on initiate, resets after period, multiple claims accumulate, max uint disables, cancelled claim doesn't consume, remaining view, unlimited view
Beneficiary rotation 13 Happy path, events, only beneficiary proposes, only treasury approves/cancels, delay enforcement, cancel works, pending claim cancelled on approval, new beneficiary can claim, zero/treasury/contract revert, double propose revert, cancel/approve with no pending
Post-settlement drain 10 After delay, before delay reverts, running/proposed reverts, zero balance reverts, conservation invariant, only treasury, event, self-destination reverts, claimable zero after drain
TreasuryRouter 11 Execute forwards, only active signer, propose/finalize rotation, cancel rotation, delay enforcement, only pending signer finalizes, events, cancel/finalize no pending, zero signer, value forwarding, receive ether, reentrancy
VaultFactory 9 Deploy + registry, isVault, only router, count increments, vault params, events, non-standard principal reverts, duplicate pubkey reverts, invalid pubkey lengths, zero constructor args

C. Certora Formal Verification

Post-remediation Certora coverage is complete for all relevant configurations.

Initial 2026-04-10 runs in this environment hit a relative ./solc-arm.sh path issue for part of the batch; failed jobs were rerun with an absolute solc path, and all final prover outputs pass.

Verification runs (2026-04-10)

Config What it proves Result
vault_economics.conf Conservation law, claimable amounts <= balance, accounting separation, shortfall correctness Pass
vault_access.conf Role-based access control on all state-changing functions Pass
router.conf Signer rotation delays, only-active-signer enforcement, execute forwarding Pass
factory.conf Router-only deployment, registry correctness, duplicate prevention, principal enforcement Pass
vault_economics_strict.conf Strict economic checks without optimistic options Pass
vault_access_strict.conf Strict access verification without optimistic options Pass
router_strict.conf Strict router lifecycle and signer-authorization checks Pass
factory_strict.conf Strict factory authorization and registry invariants Pass

Earlier run (2026-03-28 — vault hardening)

Same five specs also passed after the v3 hardening changes (principal protection, factory principal enforcement):


D. Seat Manager Integration

File: centurion-seat-manager/src/vault/contract.ts

The seat manager's vault deployer has been updated for all v3 constructor arguments:

const args = [
  params.treasury,
  params.beneficiary,
  params.validatorPubkey,
  params.principalTargetWei,
  SHORTFALL_POLICY_PRINCIPAL_FIRST,
  claimDelay,              // default 86400 (24h)
  exitRequestContract,     // CIP-7002 system contract
  maxClaimPerPeriod,       // default type(uint256).max (unlimited)
] as const;
Parameter Default Notes
claimDelay 86,400 (24 hours) Configurable per deployment
exitRequestContract 0x00000961Ef480Eb55e80D19ad83579A64c007002 Centurion mainnet genesis contract
maxClaimPerPeriod 2^256 - 1 (unlimited) Can be set to a finite value to enable rate limiting

Runtime compilation uses solc 0.8.30 with deterministic settings (optimizer 200 runs, cancun EVM target).


E. Staker Console

The staker console ABI and hooks were updated for v3 in a prior session. Key additions:

  • settlementPhase() returns uint8 (0, 1, or 2) — console displays phase-appropriate UI
  • pendingClaimStatus() — frontend shows claim timer and ready state
  • remainingClaimInPeriod() — used to show rate limit allowance
  • Multicall reads 6 fields per validator (added settlementPhase)
  • Dashboard metrics use rewardsClaimedWei for "Rewards Paid to You" (beneficiary-only counter)

The console does not yet have UI for the SettlementProposed phase indicator or beneficiary rotation. These are planned for a future update.


F. Live E2E Testing

Current post-remediation rerun status:

  • node e2e/run.mjs: 103 passed, 1 failed
  • The observed failure is in Layer F (force-exit fee forwarding assertion): expected 1000000000000000, observed 0
  • Live mainnet scripts remain blocked in this environment by RPC endpoint reachability and one missing local import in scripts/live/live_chain_20_seats_smoke.mjs

Conclusion: live e2e remains pending final green rerun after Layer F assertion alignment and endpoint availability.

G. Files

Repository File Description
centurion-networks mainnet/contracts/src/WithdrawalVault.sol v3 vault contract (653 lines)
centurion-networks mainnet/contracts/src/TreasuryRouter.sol Forwarding proxy with signer rotation (99 lines)
centurion-networks mainnet/contracts/src/VaultFactory.sol Factory with registry and enforcement (89 lines)
centurion-networks mainnet/contracts/test/WithdrawalVault.t.sol 204 tests covering all three contracts (3,619 lines)
centurion-networks verification/certora/conf/*.conf Standard + strict Certora run configurations
centurion-networks verification/certora/specs/*.spec Certora specification rules
centurion-seat-manager src/vault/contract.ts Vault deployer with v3 constructor args
centurion-staker-console src/abi/withdrawalVault.ts v3 ABI for frontend
centurion-staker-console src/hooks/useVaultProgress.ts Settlement-phase-aware multicall hook

H. Production Readiness

Ready

  • Contracts: 262 Forge tests (including fuzz + adversarial) + Certora standard/strict coverage (8 passing outputs) for economics, access control, router lifecycle, and factory invariants
  • Conservation law: Proven formally and tested extensively
  • Principal protection: Running-phase claims blocked when balance >= 32 CTN — prevents accidental principal drain
  • Rate limiting: Configurable per-vault, enforced at both initiation and finalization
  • Settlement safety: 1-hour cancellable proposal window with front-running guard
  • Beneficiary rotation: 7-day delay + treasury approval — prevents unauthorized beneficiary changes
  • CIP-7002 exit: Kill switch works without BLS key — foundation can force-exit any validator
  • Bytecode frozen: Contract source frozen as of 2026-04-10

Pending

  • Live e2e on chain — partial rerun completed; one Layer F assertion remains to close
  • Staker console SettlementProposed UI — console needs phase-1 indicator
  • Staker console beneficiary rotation UI — not yet implemented
  • Multi-validator per beneficiary — multicall aggregation exists but untested with >1 vault