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¶
- Beneficiary calls
initiateClaimRewards(amountWei)— starts the claim timer - Rate limit checked preventively (does not consume allowance)
- After
CLAIM_DELAY(24h), beneficiary callsfinalizeClaimRewards(to)— CTN transferred - Rate limit enforced and allowance consumed
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¶
maxClaimPerPeriodis immutable, set at construction- Claims accumulate in
claimedInCurrentPeriodwithin each 30-dayCLAIM_PERIOD - Period resets automatically when
block.timestamp >= currentPeriodStart + CLAIM_PERIOD - Verified at initiation (early feedback) and enforced at finalization (authoritative)
- Set to
type(uint256).maxto effectively disable
Beneficiary Rotation¶
- Current beneficiary calls
proposeBeneficiaryRotation(newAddress)— 7-day timer starts - After delay, treasury calls
approveBeneficiaryRotation()— beneficiary updated, pending claim cancelled - 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¶
Maintained at every state transition. Verified in Forge tests and Certora formal verification.
Accounting Separation¶
rewardsClaimedWei— only incremented by beneficiary calls (finalizeClaimRewardsin Running,claimRewardsin ExitSettlement). Never by treasury.principalClaimedWei— only incremented by treasury calls (claimPrincipalin 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()returnsuint8(0, 1, or 2) — console displays phase-appropriate UIpendingClaimStatus()— frontend shows claim timer and ready stateremainingClaimInPeriod()— used to show rate limit allowance- Multicall reads 6 fields per validator (added
settlementPhase) - Dashboard metrics use
rewardsClaimedWeifor "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, observed0 - 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