Go/No-Go Assessment: Large-Value Vault Deployment¶
Date: 2026-03-20 (updated 2026-04-10) Scope: WithdrawalVault v3, TreasuryRouter, VaultFactory, DepositContractCTN, Seat Manager control plane Author: Automated deep re-audit
Verdict: GO¶
All 45 full-stack audit findings remain addressed (33 code-fixed, 3 operationally mitigated, 1 deferred), and the additional five smart-contract audit findings (TR-01, TR-02, DC-01, DC-02, WV-03) are code-fixed. The software is production-ready for redeploy.
Deployment plan (infrastructure, no code changes needed): - Seat manager backend + admin frontend on a dedicated EC2 instance - Treasury private key in HSM (eliminates env-var exposure) - PostgreSQL with automated backups (seat state is not recoverable from chain alone) - Seat manager connects to one of the S5 production nodes (3 EL clients, 3 CL clients across 5 nodes)
Change record (2026-03-28 hardening + strict Certora rerun):
- docs/security/2026-03-28-vault-hardening-and-strict-certora.md
Change record (2026-03-31 strict Certora batch rerun):
- docs/security/2026-03-31-certora-batch-rerun.md
Change record (2026-04-10 post-audit Certora rerun + freeze):
- docs/security/2026-04-10-certora-batch-rerun.md
- docs/release-freeze/2026-04-10-mainnet-bytecode-freeze/README.md
Change record (2026-04-10 smart-contract audit remediation):
- docs/security/2026-04-10-smart-contract-audit-remediation.md
What Works (Strong Guarantees)¶
-
Conservation law holds unconditionally:
totalLifetimeWei = balance + principalClaimedWei + rewardsClaimedWei. Verified in 165 Foundry tests including 10 adversarial economic scenarios. No funds can be created or destroyed. -
Allowlist + intent consumption: Each
(pubkey, wc)pair can only be deposited once. Re-allowlisting a consumed intent reverts. Prevents duplicate validator onboarding. -
Two-phase claim separation: Running phase uses time-delayed claims (CLAIM_DELAY). ExitSettlement phase uses instant claims with correct accounting split. The accounting is correct in all tested scenarios.
-
Role separation: Treasury and beneficiary have non-overlapping claim powers. Treasury can only claim principal (ExitSettlement only). Beneficiary can only claim rewards. Neither can impersonate the other.
-
Optimistic concurrency: Seat status transitions use version-based optimistic locking. No concurrent modification can cause double-spend or skipped states.
-
Finalized block tracking: EL watcher uses
finalizedblock tag to avoid recording deposits from reorged blocks. -
Auto-settlement on validator exit: CL watcher auto-triggers two-step settlement:
proposeSettlement(rewardsClaimedWei)when (a) CL status =withdrawal_done, (b) vault balance >=principalTargetWei, (c) vault is in Running phase; thenfinalizeSettlement()after on-chain delay. False positives remain operationally recoverable in phase 1 viacancelSettlement(). The decision function is pure with adversarial tests. Manual fallback viaseat settle <id> --sendremains available. This is conditional automation: if gate (b) never holds, watcher auto-settlement will keep skipping and treasury intervention is required. -
Watcher liveness monitoring: Both EL and CL watchers write heartbeats every scan cycle. Health endpoint (
GET /v1/dashboard/health) reports per-watcher liveness and flags stale watchers (> 5 min without heartbeat). Enables external monitoring to detect silent watcher failures. -
EIP-7002 force-exit: Vault can trigger validator exit without BLS key via
triggerValidatorExit(). Treasury-only. Eliminates the need for the foundation to hold BLS keys — the vault's withdrawal credentials address can request exits via the EIP-7002 system contract. Pubkey hash is verified on-chain before submission. -
principalTargetWei locked to 32 CTN: Vault deployment hard-fails if
principalCtn != 32. API schema rejects non-32 values (Zod refine). Deposit command validates amount matches seat'sprincipalTargetWei. Post-deploy verification reads 7 on-chain properties and throws on mismatch. Misconfiguration is impossible through the seat manager. -
Running-phase principal protection (on-chain): If vault balance reaches/exceeds
principalTargetWeiwhile still in Running phase, beneficiary Running-claim path is blocked (claimableRewardsWei = 0,initiateClaimRewards/finalizeClaimRewardsrevertPrincipalProtectionActive). This removes the prior "treasury-too-slow principal drain" path. -
Post-audit treasury and exit hardening: Router self-rotation to
address(this)is blocked, contract-treasury zero-destination payouts now revert instead of trapping ETH, andtriggerValidatorExitnow enforces/forwards exactly the on-chain CIP-7002 fee and refunds overpayment.
Residual Risks (Accepted)¶
Phase-Blind Fund Classification (INFORMATIONAL, BOUNDED)¶
The problem: During Running phase, the vault cannot distinguish between protocol rewards and CTN sent by other means. Classification is still phase-blind.
Current bound: once vault balance reaches principalTargetWei (32 CTN), Running-phase beneficiary claims are blocked on-chain (PrincipalProtectionActive). So arbitrary inflows are only beneficiary-claimable while balance stays below the principal target.
Proven by test: test_Adversarial_AllInflowsAreRewardsDuringRunning.
Mitigations in place:
- EL watcher runs vault reconciliation every 5 minutes — flags vaults with balance >= 32 CTN in Running phase
- Reconciliation verifies conservation law (totalLifetimeWei = balance + principalClaimed + rewardsClaimed)
- CLI seat check provides on-demand reconciliation with full on-chain state dump
- Running-phase principal protection blocks claim flow at/above 32 CTN
- During ExitSettlement, accounting correctly separates principal from rewards regardless of when inflows arrived
What this does NOT do: The system still cannot identify transfer intent at the vault level and cannot reject/refund arbitrary inbound CTN. Unexpected inflows below 32 CTN are still treated as Running rewards.
Operator requirement: Do not send funds directly to vault addresses outside of normal protocol operations. Monitor reconciliation alerts for unexpected balance jumps.
H-06 EOA Check is Deployment-Time Only (LOW)¶
The problem: The beneficiary_.code.length check in the vault constructor ensures the beneficiary is an EOA at deployment time. This does not prevent future conversion to a contract (via CREATE2, EIP-7702, or self-destruct + redeploy).
Mitigations in place: - Seat manager controls vault deployment — operators cannot inject arbitrary addresses - Treasury address is controlled by the foundation - Beneficiary address is provided by the operator but validated by admin before deployment
Operator requirement: Verify that beneficiary addresses are genuine user EOAs before deploying vaults. Do not accept smart contract wallet addresses.
What Cannot Be Solved by Software¶
-
Key compromise: If the treasury private key is compromised, an attacker can call treasury-only vault functions (
proposeSettlement,finalizeSettlement,claimPrincipal, etc.) and drain funds. This is inherent to the single-owner design. -
Simultaneous watcher failure: If both watchers are down when a validator reaches
withdrawal_done, auto-settlement won't fire. The health endpoint detects this (watcher staleness alerts), but requires external monitoring to be configured. A beaconcha.in webhook is recommended as a second-layer fallback. -
M-14 solc dependency: The
tmpnpm package used by solc for temporary files is a transitive dependency. Not a runtime risk but flagged for supply-chain hygiene.
Pre-Deployment Checklist¶
- [ ] Deploy contracts from audited source (
forge build+ verify bytecode hash) - [ ] Verify vault constructor params: treasury, beneficiary, pubkey, principalTargetWei=32e18, shortfallPolicy=0, claimDelay=86400
- [ ] Start watcher daemon (
node dist/index.js watch) and verify logs show both EL + CL scanning + heartbeat writes - [ ] Verify
GET /v1/dashboard/healthreturnswatchers.allHealthy: true - [ ] Test auto-settlement on devnet: exit a validator, wait for
withdrawal_done, confirm auto-settle fires - [ ] Test manual settlement as fallback:
seat settle <id> --send - [ ] Set up external monitoring of
/v1/dashboard/health— alert ifwatchers.el.staleorwatchers.cl.staleis true - [ ] Set up external alerting for validator exit events (beaconcha.in webhook or equivalent) as second layer
- [ ] Verify PostgreSQL backups are running (seat state is not recoverable from chain alone)
- [ ] Verify BYO-BLS external onboarding: create-with-vault → deposit-data submission → approve → deposit (no mnemonic needed)
- [ ] Verify force-exit works:
seat force-exit <id> --sendtriggers EIP-7002 exit without BLS key - [ ] Verify deposit-data validation rejects mismatched pubkey, WC, or amount
- [ ] Confirm "Generate Keys" flow is restricted to internal/managed seats only
Test Evidence¶
| Suite | Count | Status |
|---|---|---|
| Foundry (WithdrawalVault) | 165 | All pass |
| Foundry (adversarial economic) | 10 | All pass |
| Vitest (seat manager) | 285 | All pass (13 files) |
| Vitest adversarial (money-risk) | 53 | All pass |
| Vitest BYO-BLS (deposit data, force-exit, RBAC) | 30 | All pass |
| Remediation doc claims vs code | 10/10 | Verified |