Skip to content

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)

  1. 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.

  2. Allowlist + intent consumption: Each (pubkey, wc) pair can only be deposited once. Re-allowlisting a consumed intent reverts. Prevents duplicate validator onboarding.

  3. 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.

  4. 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.

  5. Optimistic concurrency: Seat status transitions use version-based optimistic locking. No concurrent modification can cause double-spend or skipped states.

  6. Finalized block tracking: EL watcher uses finalized block tag to avoid recording deposits from reorged blocks.

  7. 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; then finalizeSettlement() after on-chain delay. False positives remain operationally recoverable in phase 1 via cancelSettlement(). The decision function is pure with adversarial tests. Manual fallback via seat settle <id> --send remains available. This is conditional automation: if gate (b) never holds, watcher auto-settlement will keep skipping and treasury intervention is required.

  8. 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.

  9. 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.

  10. 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's principalTargetWei. Post-deploy verification reads 7 on-chain properties and throws on mismatch. Misconfiguration is impossible through the seat manager.

  11. Running-phase principal protection (on-chain): If vault balance reaches/exceeds principalTargetWei while still in Running phase, beneficiary Running-claim path is blocked (claimableRewardsWei = 0, initiateClaimRewards/finalizeClaimRewards revert PrincipalProtectionActive). This removes the prior "treasury-too-slow principal drain" path.

  12. 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, and triggerValidatorExit now 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

  1. 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.

  2. 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.

  3. M-14 solc dependency: The tmp npm 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/health returns watchers.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 if watchers.el.stale or watchers.cl.stale is 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> --send triggers 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