On-chain Contracts¶
graph LR
Owner[FoundationOwner]
Deposit[DepositContractCTN]
Factory[CenturionVaultFactory]
Vault[CenturionWithdrawalVault]
Controller[CenturionEconomicController]
Gatekeeper[CenturionClaimGatekeeper]
ExitReq[ExitRequestContract]
Owner --> Deposit
Owner --> Factory
Factory --> Vault
Factory --> Controller
Deposit --> Factory
Deposit --> Controller
Controller --> Gatekeeper
Controller --> Vault
Vault --> ExitReq
DepositContractCTN¶
Purpose¶
DepositContractCTN is the execution-layer deposit entrypoint with allowlist enforcement and phase-1 custody gates. The constructor enables pubkey allowlist and phase-1 custody checks in contracts/src/DepositContractCTN.sol:336, and the backend operation executor uses it for the final deposit transaction after vault deployment and allowlist setup in seat-manager/src/contracts/operationExecutor.ts:982.
Key Invariants¶
- Phase 1 custody checks are enabled by default and can be locked once the baseline factory is frozen or deposits exist in
contracts/src/DepositContractCTN.sol:337andcontracts/src/DepositContractCTN.sol:718. - A deposit must use a 48-byte pubkey, 32-byte withdrawal credentials, 96-byte signature, and a value that is an exact gwei multiple in
contracts/src/DepositContractCTN.sol:367. - Phase 1 deposits enforce the frozen baseline factory, exact principal amount, execution-address withdrawal credentials, registered vault, vault runtime code hash, pubkey binding, and readiness checks in
contracts/src/DepositContractCTN.sol:377andcontracts/src/DepositContractCTN.sol:801. - The allowlist intent hash is
keccak256(abi.encode(pubkey, withdrawal_credentials, amountGwei, authorizedDepositor, ownershipEpoch)); it is computed incontracts/src/DepositContractCTN.sol:746and consumed during deposit incontracts/src/DepositContractCTN.sol:389. - A validator pubkey hash can be activated only once under phase-1 custody checks in
contracts/src/DepositContractCTN.sol:382.
Public Functions an Integrator May Call¶
| function | signature | access control | reverts | events emitted | line |
|---|---|---|---|---|---|
deposit |
deposit(bytes,bytes,bytes,bytes32) |
public, optionally owner-only depositor | invalid lengths, amount, root, allowlist, custody baseline | DepositEvent, intent consumed events |
contracts/src/DepositContractCTN.sol:357 |
addAllowedDepositFor |
addAllowedDepositFor(bytes,bytes,uint64,address) |
owner | invalid pubkey, wc, amount, depositor, consumed intent | DepositIntentAllowedFor |
contracts/src/DepositContractCTN.sol:487 |
isAllowedDepositIntentFor |
isAllowedDepositIntentFor(bytes,bytes,uint64,address) |
view | invalid input length | none | contracts/src/DepositContractCTN.sol:533 |
freezeBaselineVaultFactory |
freezeBaselineVaultFactory() |
owner | missing factory, unfrozen custody, codehash mismatch, metadata mismatch | baseline pinned/frozen events | contracts/src/DepositContractCTN.sol:630 |
get_deposit_root |
get_deposit_root() |
view | none | none | contracts/src/DepositContractCTN.sol:424 |
Events to Subscribe To¶
| event | topics | emitted when | consumer in seat-manager | line |
|---|---|---|---|---|
DepositEvent |
none indexed | a deposit is accepted into the deposit tree | EL deposit watcher parses it | contracts/src/DepositContractCTN.sol:202 |
DepositIntentAllowedFor |
intentHash, authorizedDepositor |
owner allows one depositor-bound intent | no direct watcher; operation executor expects success | contracts/src/DepositContractCTN.sol:216 |
DepositIntentConsumedFor |
intentHash, authorizedDepositor |
deposit consumes an allowlist intent | no direct watcher; transaction receipt is operation evidence | contracts/src/DepositContractCTN.sol:226 |
BaselineVaultFactoryFrozen |
baselineVaultFactory |
custody baseline is frozen | no direct watcher; deposit path depends on it | contracts/src/DepositContractCTN.sol:234 |
Upgrade / Freeze Posture¶
The contract is not a proxy. Mutable controls are owner-only flags and baseline configuration; the important freeze is the irreversible baseline factory freeze enforced in contracts/src/DepositContractCTN.sol:630, plus the permanent allowlist-disable schedule enforced in contracts/src/DepositContractCTN.sol:557. Phase-1 custody checks cannot be toggled after baseline freeze or after deposits start in contracts/src/DepositContractCTN.sol:718.
CenturionVaultFactory¶
Purpose¶
CenturionVaultFactory deploys one deterministic withdrawal vault per validator pubkey and initializes that vault in the economic controller. It stores the controller, exit request contract, config hashes, runtime code hash, and pubkey-to-vault registry in contracts/src/CenturionVaultFactory.sol:24.
Key Invariants¶
- Deployment is owner-only through the inherited owner check at
contracts/src/CenturionVaultFactory.sol:103. - The CREATE2 salt is derived from
sha256(validatorPubkey), and the preview path uses the same pubkey hash incontracts/src/CenturionVaultFactory.sol:132andcontracts/src/CenturionVaultFactory.sol:168. - One vault per pubkey is enforced by
vaultByValidatorPubkeyHash; a second deployment revertsVaultAlreadyExistsincontracts/src/CenturionVaultFactory.sol:133. - Each deployment initializes the controller seat config after writing the vault registry in
contracts/src/CenturionVaultFactory.sol:148andcontracts/src/CenturionVaultFactory.sol:150.
Public Functions an Integrator May Call¶
| function | signature | access control | reverts | events emitted | line |
|---|---|---|---|---|---|
deployVault |
deployVault(bytes,address,address,uint64,uint128,uint128,uint8,uint64) |
owner | invalid pubkey, duplicate vault, invalid credential type, deployment failure | VaultDeployed |
contracts/src/CenturionVaultFactory.sol:103 |
previewVaultAddress |
previewVaultAddress(bytes) |
view | invalid pubkey length | none | contracts/src/CenturionVaultFactory.sol:168 |
previewExecutionWithdrawalCredentials |
previewExecutionWithdrawalCredentials(bytes,bytes32) |
view | invalid pubkey length | none | contracts/src/CenturionVaultFactory.sol:173 |
isVault |
isVault(address) |
view | none | none | contracts/src/CenturionVaultFactory.sol:9 |
vaultByValidatorPubkeyHash |
public mapping getter | view | none | none | contracts/src/CenturionVaultFactory.sol:36 |
Events to Subscribe To¶
| event | topics | emitted when | consumer in seat-manager | line |
|---|---|---|---|---|
VaultDeployed |
vault, validatorPubkeyHash |
a vault is created and registered | operation executor receipt/intents, no log watcher | contracts/src/CenturionVaultFactory.sol:47 |
PolicyBootstrapOpenSet |
none | policy bootstrap flag changes | no direct watcher | contracts/src/CenturionVaultFactory.sol:54 |
Upgrade / Freeze Posture¶
The factory is not upgradeable. Its controller and exit request contract are immutable constructor arguments in contracts/src/CenturionVaultFactory.sol:63, and the deployed vault runtime code hash is fixed from the factory runtime in contracts/src/CenturionVaultFactory.sol:81. The deposit contract separately freezes the factory metadata before accepting phase-1 deposits in contracts/src/DepositContractCTN.sol:630.
CenturionWithdrawalVault¶
Purpose¶
CenturionWithdrawalVault is the per-validator execution withdrawal address. It receives ETH, exposes the execution withdrawal credentials, lets the controller transfer ETH, and requests validator exit through the EIP-7002 exit request contract. Its controller, factory, exit request contract, pubkey hash, and vault config hash are set in the constructor at contracts/src/CenturionWithdrawalVault.sol:55.
Key Invariants¶
- Only the controller can transfer ETH, set exit fallback, or request exit; the modifier enforces that in
contracts/src/CenturionWithdrawalVault.sol:41. - The vault is bound to one validator pubkey hash and verifies
isBoundToPubkeyincontracts/src/CenturionWithdrawalVault.sol:81. - Exit request submission is one-way; a second exit request reverts
ExitAlreadySubmittedincontracts/src/CenturionWithdrawalVault.sol:122. - Transfer and exit paths are non-reentrant through the local guard in
contracts/src/CenturionWithdrawalVault.sol:46.
Public Functions an Integrator May Call¶
| function | signature | access control | reverts | events emitted | line |
|---|---|---|---|---|---|
executionWithdrawalCredentials |
executionWithdrawalCredentials() |
view | none | none | contracts/src/CenturionWithdrawalVault.sol:77 |
isBoundToPubkey |
isBoundToPubkey(bytes) |
view | none | none | contracts/src/CenturionWithdrawalVault.sol:81 |
currentExitRequestFeeWei |
currentExitRequestFeeWei() |
view | exit fee unavailable | none | contracts/src/CenturionWithdrawalVault.sol:85 |
depositProtectionReadiness |
depositProtectionReadiness() |
view | controller readiness failure | none | contracts/src/CenturionWithdrawalVault.sol:100 |
transferETH |
transferETH(address payable,uint256) |
controller | insufficient balance, transfer failed | ETHTransferred |
contracts/src/CenturionWithdrawalVault.sol:114 |
requestExit |
requestExit(bytes,uint256) |
controller | invalid pubkey, already submitted, fee failure | ExitRequested |
contracts/src/CenturionWithdrawalVault.sol:122 |
Events to Subscribe To¶
| event | topics | emitted when | consumer in seat-manager | line |
|---|---|---|---|---|
ETHReceived |
from |
vault receives ETH | no direct watcher; status reads balances | contracts/src/CenturionWithdrawalVault.sol:36 |
ETHTransferred |
to |
controller transfers ETH | operation receipt/intents | contracts/src/CenturionWithdrawalVault.sol:37 |
ExitRequested |
validatorPubkeyHash, target |
EIP-7002 request succeeds | operation worker result/status | contracts/src/CenturionWithdrawalVault.sol:38 |
ExitRequestFallbackUpdated |
fallbackContract |
controller updates fallback | no direct watcher | contracts/src/CenturionWithdrawalVault.sol:39 |
Upgrade / Freeze Posture¶
The vault is not upgradeable. It has a controller-set exit fallback, but only the controller can change it in contracts/src/CenturionWithdrawalVault.sol:104. The EIP-7002 path is CenturionEconomicController.requestValidatorExitDynamic or requestValidatorExitWithManualFee, which calls vault requestExit; those controller functions are at contracts/src/CenturionEconomicController.sol:781, and the vault call is at contracts/src/CenturionEconomicController.sol:1455.
CenturionEconomicController¶
Purpose¶
CenturionEconomicController owns seat policy, risk observations, reserve coverage, claim lifecycle, exit triggers, settlement, and read-only accounting for every vault. The backend operation executor calls it for claim executor grants, claim execution, exit requests, and status economics in seat-manager/src/contracts/operationExecutor.ts:693 and seat-manager/src/api/server.ts:822.
Key Invariants¶
- Only the factory initializes seats in
contracts/src/CenturionEconomicController.sol:250. - Only the owner sets policy controls, risk observations, reserve coverage, and executor grants through the owner modifier at
contracts/src/CenturionEconomicController.sol:245. - Claims require running claim state and available rewards in
contracts/src/CenturionEconomicController.sol:742. - Validator exit requires trigger arming and only the owner or seat beneficiary can request it in
contracts/src/CenturionEconomicController.sol:1437andcontracts/src/CenturionEconomicController.sol:1468. - Deposit readiness is exposed to the deposit contract and checks policy, credential, reserve, and safety data through
contracts/src/CenturionEconomicController.sol:1177.
Public Functions an Integrator May Call¶
| function | signature | access control | reverts | events emitted | line |
|---|---|---|---|---|---|
initializeSeat |
initializeSeat(address,address,address,bytes32,uint64,uint128,uint128,uint8,uint64) |
factory | already initialized, invalid beneficiary, invalid limits | SeatInitialized |
contracts/src/CenturionEconomicController.sol:373 |
setClaimExecutorGrant |
setClaimExecutorGrant(address,address,uint8,uint64) |
owner | invalid grant | ClaimExecutorGrantSet |
contracts/src/CenturionEconomicController.sol:331 |
setTriggerArmed |
setTriggerArmed(address,bool) |
owner | unregistered vault | TriggerArmedSet |
contracts/src/CenturionEconomicController.sol:434 |
initiateClaim |
initiateClaim(address,uint256) |
beneficiary, owner, authorized executor through gatekeeper | not running, insufficient rewards, pending claim | ClaimInitiated |
contracts/src/CenturionEconomicController.sol:742 |
finalizeClaim |
finalizeClaim(address) |
pending claim caller path | not ready, insufficient rewards | ClaimFinalized |
contracts/src/CenturionEconomicController.sol:762 |
requestValidatorExitDynamic |
requestValidatorExitDynamic(address,bytes) |
owner or beneficiary | fee, trigger, duplicate exit | ExitRequested |
contracts/src/CenturionEconomicController.sol:781 |
seatRiskMetadata |
seatRiskMetadata(address) |
view | uninitialized seat | none | contracts/src/CenturionEconomicController.sol:889 |
depositReadiness |
depositReadiness(address) |
view | uninitialized seat | none | contracts/src/CenturionEconomicController.sol:1177 |
Events to Subscribe To¶
| event | topics | emitted when | consumer in seat-manager | line |
|---|---|---|---|---|
SeatInitialized |
vault |
factory initializes a seat | operation receipt/intents | contracts/src/CenturionEconomicController.sol:149 |
TriggerArmedSet |
vault |
owner arms or disarms exit trigger | status economics | contracts/src/CenturionEconomicController.sol:156 |
RiskObservationAccepted |
vault, epoch |
owner records risk observation | status economics | contracts/src/CenturionEconomicController.sol:165 |
ClaimInitiated |
vault, beneficiary |
pending claim opens | operation worker result | contracts/src/CenturionEconomicController.sol:189 |
ClaimFinalized |
vault, beneficiary |
claim transfers rewards | operation worker result | contracts/src/CenturionEconomicController.sol:192 |
ExitRequested |
vault |
exit request is sent | operation worker result | contracts/src/CenturionEconomicController.sol:196 |
Upgrade / Freeze Posture¶
The controller is not a proxy. Runtime mutability is owner-governed policy and phase hardening. Network phase can only move forward through hardenNetworkPhase in contracts/src/CenturionEconomicController.sol:322. The factory address can be set once and then rejects a second set in contracts/src/CenturionEconomicController.sol:292.
CenturionClaimGatekeeper¶
Purpose¶
CenturionClaimGatekeeper holds pending-claim state, executor grants, period caps, daily executor caps, and pure classification helpers used by the controller. The controller constructs and calls it internally, and the backend checks executor authorization through the controller/gatekeeper path before queuing sponsored claims in seat-manager/src/api/server.ts:2151.
Key Invariants¶
- Only the controller can mutate grant, pending-claim, and claim lifecycle state in
contracts/src/CenturionClaimGatekeeper.sol:33. - Executor grants require non-zero vault, non-zero executor, non-zero scope mask, and a future expiry in
contracts/src/CenturionClaimGatekeeper.sol:42. - Only one pending claim can exist per vault in
contracts/src/CenturionClaimGatekeeper.sol:454. - Finalization rejects missing claims and claims before
readyAtincontracts/src/CenturionClaimGatekeeper.sol:478. - Claim amounts are bounded by beneficiary period cap and executor daily cap in
contracts/src/CenturionClaimGatekeeper.sol:576.
Public Functions an Integrator May Call¶
| function | signature | access control | reverts | events emitted | line |
|---|---|---|---|---|---|
setClaimExecutorGrant |
setClaimExecutorGrant(address,address,uint8,uint64) |
controller | invalid grant | none | contracts/src/CenturionClaimGatekeeper.sol:42 |
claimExecutorAuthorized |
claimExecutorAuthorized(address,address,uint8) |
view | none | none | contracts/src/CenturionClaimGatekeeper.sol:64 |
remainingClaimInPeriod |
remainingClaimInPeriod(address,uint128) |
view | none | none | contracts/src/CenturionClaimGatekeeper.sol:73 |
remainingExecutorClaimInDay |
remainingExecutorClaimInDay(address,address,uint128) |
view | none | none | contracts/src/CenturionClaimGatekeeper.sol:78 |
deriveClaimState |
deriveClaimState(...) |
pure | none | none | contracts/src/CenturionClaimGatekeeper.sol:243 |
initiatePendingClaim |
initiatePendingClaim(address,address,address,uint256,uint64,uint128,uint128,uint8) |
controller | pending exists, cap exceeded | none | contracts/src/CenturionClaimGatekeeper.sol:454 |
finalizePendingClaim |
finalizePendingClaim(address,address) |
controller | no pending claim, not ready | none | contracts/src/CenturionClaimGatekeeper.sol:478 |
Events to Subscribe To¶
| event | topics | emitted when | consumer in seat-manager | line |
|---|---|---|---|---|
| none | none | this contract emits no standalone events | subscribe to controller claim and grant events | contracts/src/CenturionClaimGatekeeper.sol:6 |
Upgrade / Freeze Posture¶
The gatekeeper is not upgradeable and mutates only when called by the controller. Operational freeze is controlled through controller policy, including setClaimExecutorsPaused at contracts/src/CenturionEconomicController.sol:339 and grant revocation at contracts/src/CenturionEconomicController.sol:343.