EXEC_TX introduces a new transaction type built on hook-based execution. Instead of adding new EIPs for account abstraction, privacy, sponsorship, and next-year’s features, we provide one protocol-layer envelope where hooks implement capability logic. The protocol handles the boring parts (nonce, gas, multi-phase routing); hooks handle the interesting parts (auth, escrow, proofs, policy).
The Problem
Currently, every new account capability requires:
- New transaction type or separate infrastructure (EIP-4337 bundlers, relayers)
- New wallet signing logic
- New client validation rules
- New mempool logic
- New dApp integrations
This creates friction, fragmentation, and slows feature development.
Solution: One extensible transaction type where capability logic lives in contracts, not consensus.
Core Design Philosophy
Protocol handles invariants; applications handle capabilities.
| Responsibility | Layer | Examples |
|---|---|---|
| Replay prevention | Protocol | 2D nonce, irreversible at inclusion |
| Gas metering | Protocol | Intrinsic + phases, deterministic |
| Phase atomicity | Protocol | All-or-nothing execution |
| Authorization | Application | ECDSA, ZK, multisig, custom schemes |
| Sponsorship | Application | Escrow, verification, settlement |
| Privacy | Application | Proofs, commitments, nullifiers |
| Policy | Application | Amount limits, whitelist, rate control |
This separation enables protocol stability and application agility (new capabilities = new contracts, not new opcodes).
The Hook Model: Core Design
What Is a Hook?
A hook is a smart contract that the protocol calls at specific points in the EXEC_TX execution flow. Instead of protocol code handling auth, escrow, proofs, the protocol delegates to hooks. Hooks are the interface between the protocol’s stable rules and the application’s flexible capabilities.
Hook Interface:
interface IExecutionHook {
function beforeExecution(Context calldata ctx, bytes calldata data) external;
function afterExecution(Context calldata ctx, ExecutionResult calldata result, bytes calldata data) external;
}
Hooks receive a Context struct that includes:
sender(EXEC_TX.from — the real originator)phase(which stage: PRE_VALIDATION=0, PRE_EXECUTION=1, POST_EXECUTION=2)txHash(the signing hash for signature verification)gasLimit,maxFeePerGas(for escrow calculations)payerDataHash(for payer verification)
The Hook Lifecycle
EXEC_TX execution flow:
1. Base validation
├─ Decode EXEC_TX
├─ Verify signature (EOA) or isAuthorizedExecHook (contract account)
└─ Load hook contract
2. PRE_VALIDATION (Validation Mode enforced)
├─ Protocol calls hook.preValidation(txData, hookData)
├─ Hook logic: verify signatures, check proofs, validate policy
├─ **Constraints**: No state writes, no block-dependent reads
└─ Hook returns 0 (fail, tx rejected) or 1 (success, continue)
3. PRE_EXECUTION (if phaseMask bit 1 set)
├─ Protocol calls hook.preExecution(txData, hookData)
├─ Hook logic: lock funds, reserve capacity, emit intermediate state
├─ **Constraints**: None (full EVM access)
└─ Hook returns 0 (fail, tx reverted) or 1 (success, continue)
4. Core CALL
├─ Protocol executes CALL(to, data, value)
├─ **No hook involved**
└─ If reverts, tx reverted entirely (POST_EXECUTION skipped)
5. POST_EXECUTION (if phaseMask bit 2 set AND Core CALL succeeded)
├─ Protocol calls hook.postExecution(txData, hookData)
├─ Hook logic: release escrowed funds, emit nullifiers, settle accounts
├─ **Constraints**: None (full EVM access)
└─ If hook reverts, entire tx reverted (Core CALL results rolled back)
6. Settlement
└─ Charge actual gas consumed to payer (or fallback to from)
Why Single Hook Per Transaction?
Instead of protocol-defined multi-hook composition, applications compose via a Hook Dispatcher contract.
Protocol sees: one hook (the dispatcher)
Dispatcher manages: AuthModule + SponsorModule + PolicyModule
Why:
- Protocol stays simple (no multi-hook ordering rules in consensus)
- Apps stay flexible (dispatcher can evolve without protocol changes)
- Proven pattern (EIP-4337 bundlers use this at scale)
Alternative: If protocol defined multi-hook semantics, every edge case (module ordering, failure policies, state sharing) would need a new EIP. Single hook + dispatcher avoids this.
Hook Authorization: Strict Client Consistency
For contract accounts, the protocol must verify hook authorization before any hook executes. This is done via:
interface IExecHookAuthorizer {
function isAuthorizedExecHook(address target) external view returns (bool);
}
// Protocol calls (contract account tx):
STATICCALL(to=from, input=abi.encodeCall(isAuthorizedExecHook, HookSpec.target), gas=5000)
Why so strict?
Clients (geth, reth, erigon) must produce bit-perfect consistent results. Any divergence causes mempool partition.
Return value validation:
✅ Exactly 32 bytes: the canonical ABI encoding of bool true
✅ Byte pattern: 0x0000000000000000000000000000000000000000000000000000000000000001
❌ 0x0000...0000 (false) → reject
❌ 0x0000...0002 (non-canonical) → reject
❌ shorter/longer responses → reject
Why canonical encoding matters:
Different smart contracts might return different truthy values:
- Contract A: returns
1(uint256) - Contract B: returns
true(bool) - Both are “true” in Solidity semantics
- But in ABI encoding, they’re different byte patterns
Without strict equality: clients could interpret the same return value differently, causing divergence.
For EOAs: Signature is authorization. No on-chain isAuthorizedExecHook call needed (signature already commits to the hook via the signing hash).
Revert Handling
| When | Behavior | Result |
|---|---|---|
| isAuthorizedExecHook reverts | Fails immediately | Tx rejected |
| PRE_VALIDATION reverts | Phase fails | Tx rejected; validation gas charged |
| PRE_EXECUTION reverts | Phase fails | Tx reverted; all gas charged |
| Core CALL reverts | Execution fails | Tx reverted; POST_EXECUTION skipped |
| POST_EXECUTION reverts | Phase fails | Core results rolled back; entire tx reverted |
Hook Data Encoding
Hook logic passed in hook.data (arbitrary bytes). Encoding is app-specific, not protocol-mandated.
// Simple: single purpose
hook.data = abi.encode(signature1, signature2)
// Complex: dispatcher routing
hook.data = abi.encode(phase0Data, phase1Data, phase2Data)
Hook detects phase via phaseMask or decodes phase-specific data.
Signature & Gas Semantics
Signature (for EOAs): Standard EIP-191 signature over EXEC_TX hash (fields in order: chainId, nonceKey, nonceSeq, from, to, value, data, gasLimit, maxFeePerGas, payer, payerData, hook, v, r, s).
Contract accounts: No signature. Hook validates authorization via isAuthorizedExecHook.
Gas Budget:
MAX_HOOK_GAS = gasLimit - MIN_CORE_GAS
MIN_CORE_GAS = 21_000
Intrinsic = 21_000 + calldata_bytes × 16 (for non-zero) / 4 (for zero)
Settlement: charge = (intrinsic + actual_hook_gas + actual_core_gas) × maxFeePerGas
The Execution Model: Multi-Phase Design
phaseMask: Phase Selection
An 8-bit value where bits control which phases execute:
phaseMask & 0x1 → PRE_VALIDATION runs (bit 0)
phaseMask & 0x2 → PRE_EXECUTION runs (bit 1)
phaseMask & 0x4 → POST_EXECUTION runs (bit 2)
Examples:
phaseMask = 1: PRE_VALIDATION onlyphaseMask = 3: PRE_VALIDATION + PRE_EXECUTIONphaseMask = 7: All three phasesphaseMask = 0: No hook; Core CALL only (behaves like legacy tx)
Semantics: If hook is not null, PRE_VALIDATION always runs (bit 0 required).
Why Four Phases?
| Phase | When | Hook Can Do | Protocol Guarantees |
|---|---|---|---|
| PRE_VALIDATION | Before Core | Verify signatures, check proofs, validate policy | No state writes; deterministic; mempool-safe |
| PRE_EXECUTION | Before Core | Lock escrow, reserve capacity | Full EVM access; atomic with Core |
| Core CALL | User intent | (No hook) | Unchanged from legacy; can do anything |
| POST_EXECUTION | After Core (if succeeds) | Release escrow, emit nullifiers | Full EVM access; atomic with Core; if reverts, all reverts |
Why this structure?
- PRE_VALIDATION: Deterministic checks before touching state. Mempool nodes can validate locally without divergence.
- PRE_EXECUTION: Setup before Core (lock funds, reserve). Core can assume resources are available.
- Core CALL: Unchanged from legacy. User’s actual transaction.
- POST_EXECUTION: Cleanup after Core succeeds. Escrow accounting is atomic: either Core succeeds AND funds are released, or neither happens.
Validation Mode: Ensuring Deterministic Validation
PRE_VALIDATION runs in Validation Mode — a restricted EVM context ensuring all nodes agree on validity.
The problem: If validation reads block-dependent data, different nodes might validate the same tx differently → mempool divergence.
Solution: Validation Mode forbids block-dependent opcodes:
State reads/writes: SLOAD,SSTORE,TSTORE,LOG*
Block data: TIMESTAMP,BLOCKHASH,NUMBER,BASEFEE,COINBASE,PREVRANDAO,BLOBBASEFEE
Exceptions: BALANCE,SELFBALANCE(needed for payer checks; risk mitigated by PRE_EXECUTION locking)
Precompiles allowed (e.g., ecrecoverfor signature verification)
Recursive scope: Validation Mode applies to all EVM code reachable from the PRE_VALIDATION hook, including nested calls. Hooks cannot escape the restrictions by delegating.
This ensures: same tx validates the same way on all nodes, regardless of which block includes it.
Two-Dimensional Nonce: Replay Prevention + Parallelism
The Problem: Nonce Must Be Protocol-Level
Replay attacks are a consensus issue, not an application issue. Hooks cannot be trusted to manage nonce uniqueness because:
Scenario: Two nodes with same hook code
Node A state: user_nonces = {tx_hash_1: false}
Node B state: user_nonces = {tx_hash_1: false}
Same tx submitted to both:
Node A: checks hook, sees nonce_hash already used → reject
Node B: checks hook, sees nonce_hash not used yet → accept
Result: nodes disagree on same tx → consensus failure
The solution: Nonce is a protocol-level counter, not EVM state. The protocol enforces uniqueness before any hook runs.
How It Works
struct ExecutionNonce {
uint64 nonceKey; // Lane identifier (0 = default, 1..2^64-2 = custom lanes)
uint64 nonceSeq; // Sequence within lane
}
Protocol enforcement (at inclusion time, before any hook):
- Validate:
nonceSeq == next[from][nonceKey] - Consume: increment
next[from][nonceKey]++immediately (not subject to EVM revert) - Guarantee: no two transactions in a block can have same
(from, nonceKey, nonceSeq)
Result: Even if the transaction reverts at any phase (PRE_VALIDATION, PRE_EXECUTION, core, or POST_EXECUTION), the nonce slot is permanently consumed. No reuse, no replay, ever.
Why “Non-EVM State”?
// nonce counter is NOT stored in contract state (EVM)
// NOT at address(0) or any SSTORE location
// It's a protocol-internal counter, consumed at block inclusion time
// Therefore: EXEMPT from EVM revert
// Even if hook reverts:
if (hook_revert) {
// all hook state changes: ROLLED BACK
// all core execution state: ROLLED BACK
// nonce consumption: PERMANENT
}
This is critical for security: if nonce consumption could be undone by a revert, attackers could:
- Send tx with nonce=100
- Let it fail
- Resend same tx with nonce=100 again
- Bypass replay protection
Independent Lanes for Parallelism
Lanes are independent nonce sequences:
Lane 0 (default): seq=100, 101, 102, ... (payment operations, must be sequential)
Lane 1 (intent): seq=100, 101, 102, ... (intent responses, can be parallel)
Lane 2 (governance): seq=100, 101, 102, ... (DAO votes, can be parallel)
All three can coexist in mempool without blocking each other
Parallel execution benefit:
- User can submit 10 governance votes (lanes 2, different seqs) simultaneously
- Can submit 1 payment (lane 0, seq=100) simultaneously
- Payment failure doesn’t block votes
- Protocol still prevents replays (within each lane)
EOA vs. Contract Account
| Account Type | Default Behavior | Flexibility |
|---|---|---|
| EOA | MUST use nonceKey = 0 |
Protocol enforces: no custom lanes |
| Contract account | CAN use nonceKey = 0..2^64-2 |
Protocol allows: app chooses lane policy |
Why this asymmetry?
- EOAs use key-management wallets (hardware wallets, browser extensions) — need consistency
- Contract accounts use smart contract logic — can implement sophisticated lane policies
Nonce Consumption Timing
When: Immediately after base validation passes, before any hook executes
Why this timing?
Ensures nonce is consumed even if PRE_VALIDATION hook reverts
Ensures nonce is consumed even if PRE_EXECUTION hook reverts
Ensures nonce is consumed even if core CALL reverts
Single atomic operation (no race condition)
Risk: If contract hook doesn’t check nonce uniqueness, same EXEC_TX can execute twice (replay attack).
Transaction Format (EIP-2718)
EXEC_TX is transaction type 0x08 (pending finalization).
RLP structure:
0x08 || RLP([
chainId, nonceKey, nonceSeq, from, to, value, data,
gasLimit, maxFeePerGas, payer, payerData,
hook.target, hook.phaseMask, hook.gasLimit, hook.data,
v, r, s // For EOAs only; 0 for contract accounts
])
Fields:
nonceKey,nonceSeq: 2D nonce (uint256 each)payer: address(0) if no sponsorshippayerData: bytes (empty if payer = address(0))hook.target: address(0) if no hook- All other fields: same as legacy txs
Worked Examples: Hook in Action
Example 1: Simple Authorization (PRE_VALIDATION only)
// 2-of-3 multisig contract account sends tokens
EXEC_TX {
from: safe_multisig_account,
to: token_contract,
data: transfer(recipient, amount),
hook: {
target: multisig_validator, // Deployed once, reused by all 2-of-3 accounts
phaseMask: 1, // PRE_VALIDATION only
gasLimit: 100_000,
data: abi.encode([sig1, sig2]) // 2-of-3 signatures
}
}
Hook execution:
contract MultisigValidator {
function preValidation(bytes calldata txData, bytes calldata hookData)
external view returns (uint256) {
// Decode signatures
(bytes memory sig1, bytes memory sig2) = abi.decode(hookData, (bytes, bytes));
// Recover signers from EXEC_TX signing hash
address signer1 = recoverFromSignature(txData, sig1);
address signer2 = recoverFromSignature(txData, sig2);
// Verify both signers are in this account's 2-of-3 set
Safe safe = Safe(msg.sender); // msg.sender is the EXEC_TX from address
require(safe.isOwner(signer1) && safe.isOwner(signer2), "Invalid signers");
return 1; // Success
}
}
Why this matters: First time contract accounts have native base-layer signature validation. No bundler needed. Same validator contract reused by 100,000 Safe wallets.
Example 2: Privacy with ZK (PRE_VALIDATION only)
// Privacy token transfer with ZK proof
EXEC_TX {
from: user,
to: privacy_token_contract,
data: [commitment_hash, encrypted_transfer_params],
hook: {
target: zk_verifier,
phaseMask: 1, // PRE_VALIDATION only
gasLimit: 200_000,
data: zk_proof // Public proof
}
}
Hook execution:
contract ZKVerifier {
function preValidation(bytes calldata txData, bytes calldata hookData)
external view returns (uint256) {
// Decode calldata to extract commitment_hash
(bytes32 commitment, ) = abi.decode(txData, (bytes32, bytes));
// Decode proof from hook.data
bytes memory proof = hookData;
// Verify proof (deterministic, no state reads)
require(verifyProof(proof, commitment), "Invalid proof");
return 1; // Success
}
}
Execution continuation:
- Core: Privacy token contract receives encrypted params, updates shielded pool state
- User retained private key off-chain; can decrypt when withdrawing in future tx
Why this works: Proof verification (public, deterministic) is decoupled from state mutation (encrypted). Privacy logic evolves in contracts without consensus changes.
Example 3: Multi-Module Composition with Dispatcher (All Phases)
// 2-of-3 multisig + amount limits + sponsored gas
EXEC_TX {
from: safe_multisig_account,
to: uniswap,
data: swap_calldata,
payer: relayer_sponsor,
hook: {
target: dispatcher, // Routes to AuthModule, PolicyModule, EscrowModule
phaseMask: 7, // All three phases
gasLimit: 500_000,
data: abi.encode(
[sig1, sig2], // For AuthModule (phase 0)
{amount: 100e18, recipient: "0x..."}, // For PolicyModule (phase 1)
relayer_address // For EscrowModule (phase 2)
)
}
}
Hook execution:
contract Dispatcher {
AuthModule authModule;
PolicyModule policyModule;
EscrowModule escrowModule;
function preValidation(bytes calldata txData, bytes calldata hookData)
external view returns (uint256) {
(bytes[] memory sigs, , ) = abi.decode(hookData, (bytes[], bytes32, address));
return authModule.verify(txData, sigs) ? 1 : 0;
}
function preExecution(bytes calldata txData, bytes calldata hookData)
external returns (uint256) {
(, bytes32 constraints, address relayer) = abi.decode(hookData, (bytes[], bytes32, address));
// Validate policy
require(policyModule.validateConstraints(constraints, txData), "Policy violation");
// Lock escrow for gas sponsorship
require(escrowModule.lockEscrow(relayer, tx.gasprice * 500_000), "Escrow lock failed");
return 1;
}
function postExecution(bytes calldata txData, bytes calldata hookData)
external returns (uint256) {
(, , address relayer) = abi.decode(hookData, (bytes[], bytes32, address));
// Release escrowed funds (calculate actual gas used, refund difference)
uint256 actualGasCost = (initialGas - gasleft()) * tx.gasprice;
escrowModule.releaseEscrow(relayer, actualGasCost);
return 1;
}
}
Execution timeline:
- PRE_VALIDATION: Dispatcher.preValidation → AuthModule.verify → 2-of-3 signature check (~8K gas)
- PRE_EXECUTION: Dispatcher.preExecution → PolicyModule.validate (amount limits) → EscrowModule.lockEscrow (~10K gas)
- Core: Uniswap swap executes (~100K gas, depends on market)
- POST_EXECUTION (only because Core succeeded): Release escrow, refund relayer (~5K gas)
- Settlement: Relayer charged actual gas; fallback to Safe if relayer drained
Why this architecture: Single hook (dispatcher) routes to 3 internal modules. Protocol doesn’t care how many modules exist; dispatcher handles composition. Routing overhead is ~1K gas (negligible).
Advanced Patterns: Composition and Optimization
Pattern 1: Parallel Lanes for Retry Logic
Use 2D nonce lanes to decouple retry:
EXEC_TX (lane 0) {
phaseMask: 7,
data: primary_operation
}
EXEC_TX (lane 1) {
phaseMask: 1,
data: fallback_operation // executes if lane 0 fails
}
Both pending simultaneously without blocking. Primary succeeds → fallback cancelled.
Pattern 2: Batch Atomicity Via Dispatcher
Multi-op atomicity via hook-based dispatcher:
contract BatchDispatcher {
function preExecution(bytes calldata txData, bytes calldata hookData)
external returns (uint256) {
(Operation[] memory ops) = abi.decode(hookData, (Operation[]));
require(lockEscrow(sumCosts(ops)), "Escrow failed");
return 1;
}
function postExecution(bytes calldata txData, bytes calldata hookData)
external returns (uint256) {
(Operation[] memory ops) = abi.decode(hookData, (Operation[]));
for (uint i = 0; i < ops.length; i++) {
releaseEscrow(ops[i].actualCost);
}
return 1;
}
}
Single Core CALL dispatches internally. All succeed or all revert.
Pattern 3: 2D Nonce Lanes for Parallel Workflows
Different lanes for different transaction types:
Lane 0: Payments (sequential)
Lane 1: Intent responses (parallel)
Lane 2: Governance votes (parallel)
User submits simultaneously:
Tx1: lane=0, seq=100 (payment)
Tx2: lane=2, seq=50 (vote)
Tx3: lane=1, seq=25 (intent)
All pending together. Lane 0 failure doesn’t block lanes 1 or 2.
Gas Accounting: Key Rules
- Intrinsic: 21,000 + calldata cost + authorizer cost (5,000 for contract accounts)
- Phase budgets: Each phase (PRE_VALIDATION, PRE_EXECUTION, POST_EXECUTION) gets
HookSpec.gasLimitindependently - Core reservation: If POST_EXECUTION is enabled,
HookSpec.gasLimitgas is reserved and unavailable to Core CALL - Total limit:
intrinsic + (phase_count × HookSpec.gasLimit) + MIN_CORE_GAS(21,000) ≤ gasLimit
Charge: total_gas_used × effectiveGasPrice to payer (or fallback to from if payer drained)
Validation Mode: Common Pitfalls and Enforcement
Opcodes Forbidden in Validation Mode
Strictly Forbidden (9 opcodes):
SLOAD 0x54 (state read)
SSTORE 0x55 (state write)
TIMESTAMP 0x42 (block.timestamp)
BLOCKHASH 0x40 (historical blocks)
GASLIMIT 0x45 (block gas limit)
BLOBBASEFEE 0x4a (EIP-4844)
PREVRANDAO 0x44 (randomness)
COINBASE 0x41 (miner address)
BASEFEE 0x48 (base fee)
Allowed despite apparent risk:
BALANCE 0x31 (payer balance check)
SELFBALANCE 0x19 (payer balance check)
Why BALANCE is allowed:
- Critical for payer liquidity check
- TOCTOU race mitigated by PRE_EXECUTION fund locking
- If balance changes between PRE_VALIDATION and PRE_EXECUTION, lockEscrow will catch it
Client Enforcement
Each EVM client must implement Validation Mode identically. Implementation:
- Use tracer to detect forbidden opcodes (SLOAD, SSTORE, TIMESTAMP, etc.)
- Check PRE_VALIDATION return value (must be 0 or 1)
- Reject if forbidden opcode detected
Client testing: Fuzz EXEC_TX with intentionally bad hooks. All clients must reject identically. This is critical for mempool consistency.
Security Considerations
Nonce Uniqueness for Contract Accounts
Protocol enforces ordering; contract accounts must prevent replay:
Vulnerable:
contract VulnerableHook {
function preValidation(bytes calldata txData, bytes calldata hookData)
external view returns (uint256) {
return isValidSignature(txData, hookData) ? 1 : 0; // No nonce check!
}
}
Same tx can execute twice. Correct pattern:
contract SafeHook {
mapping(address => mapping(uint64 => bool)) executed;
function preValidation(bytes calldata txData, bytes calldata hookData)
external view returns (uint256) {
(address from, uint64 nonceSeq, ) = decodeTx(txData);
require(!executed[from][nonceSeq], "Nonce already used");
require(isValidSignature(txData, hookData), "Bad signature");
return 1;
}
function preExecution(bytes calldata txData, bytes calldata hookData)
external returns (uint256) {
(address from, uint64 nonceSeq, ) = decodeTx(txData);
executed[from][nonceSeq] = true; // Mark before Core, atomic with it
return 1;
}
}
Mark in PRE_EXECUTION, not POST: Ensures if Core fails, entire tx reverts and nonce flag rolls back.
Payer Fallback Risk
If payer drained between PRE_VALIDATION and execution, lockEscrow fails and entire tx reverts. If fallback address has no balance, tx fails.
Mitigations:
- Wallet UI: Highlight payer risk
- Hook pre-check: Verify payer balance before submission
- Escrow contracts: Use long-term escrow instead of ephemeral EOA payers
Integration Guidance
For EIP-4337 users, EXEC_TX offers native base-layer support (no bundler infrastructure). Key migration points: move verifier logic to PRE_VALIDATION hook, use protocol nonce instead of custom tracking, implement escrow in PRE_EXECUTION/POST_EXECUTION phases. Use standard tx mempool instead of UserOp mempool.
Gas Overhead and Mempool Efficiency
- Protocol overhead: 5,000 gas (isAuthorizedExecHook, contract accounts only)
- App hooks: 50–200K gas typical (auth, escrow, policy)
- Mempool optimizations: Validation Mode determinism enables result caching and parallel validation without divergence risk
Future Directions
Post-Mainnet (no consensus changes):
- Hook Registry: Off-chain metadata mapping for wallet UX
- Standardized Modules: AuthModule, EscrowModule, PolicyModule shared libraries
- Hook Gas Profiling: Tooling for accurate cost prediction
- 2D Nonce UX Standard: How wallets present lanes to users
Potential Protocol v2 (future EIP):
- Hook Metadata field for off-chain coordination
- Conditional Execution: “Execute only if state[addr] == value”
- Multi-Hook support with ordering guarantees
How Hook Design Solves The Problem
From “Upgrade for Each Feature” to “Deploy a Hook”
Old model (EIP-4337, bundler relayers):
- New feature needed → New EIP proposal → Client consensus → Mainnet upgrade → Wallets integrate → Dapps integrate
- Months to years per feature
- Each feature fragments infrastructure
New model (EXEC_TX + hooks):
- New feature needed → Write hook contract → Deploy → Dapps integrate
- Weeks per feature
- Single unified infrastructure (EXEC_TX)
Examples of Future Hooks (No Protocol Changes Needed)
- Risk management: Risk hook validates transaction against user’s risk profile
- Intent solving: Intent hook validates that on-chain state matches user’s intent
- Privacy: Privacy hook verifies proofs against commitments
- Quantum-safe auth: Hook implements post-quantum signature verification
- Social recovery: Hook implements social recovery mechanisms
- Delegation: Hook verifies delegated authority proofs
- Batch operations: Hook validates batch structure and dependencies
- MEV resistance: Hook implements encrypted calldata + revealing mechanics
All of these are possible without a single protocol change because the hook interface is generic.
Key Properties of the Hook Model
Properties That Make It Work
- Deterministic PRE_VALIDATION: All nodes agree on validity. No mempool partition risk.
- Atomic phases: PRE_EXECUTION and POST_EXECUTION atomic with Core. Escrow invariants hold.
- Authorization delegated: Contracts define validity (isAuthorizedExecHook). Protocol enforces structure.
- Fast iteration at app layer: New hooks = no consensus. Deploy and test weekly if needed.
- Composable via dispatcher: Single hook supports 100+ internal modules. Pattern proven at EIP-4337 scale.
Trade-Offs
| Trade-Off | Choice | Rationale |
|---|---|---|
| Protocol vs App | Protocol: nonce/phases/gas | Enforces invariants. Apps enforce policy. |
| Single vs. Multiple Hooks | Single hook | Minimal consensus rules. Dispatcher proven. |
| Nonce Uniqueness | EOA: enforced, Contract: delegated | Safe by default. Flexibility for apps. |
| Validation Strictness | Strict (9 opcodes forbidden) | Mempool consistency. BALANCE allowed for payer checks. |
| Payer Fallback | Fallback if drained | Atomicity. Mitigation: wallet UI warnings. |
Why This Design Will Endure
Stability Through Delegation
The power of EXEC_TX is that protocol stability and application evolution are decoupled.
Once EXEC_TX is deployed:
- Protocol rules never change (nonce, gas, phase ordering)
- Capabilities evolve in contracts (hooks, dispatchers, modules)
New capability needed in 5 years? Deploy a new hook contract. No EIP, no client upgrade, no consensus risk.
Future innovation examples (no protocol changes needed):
- Quantum-safe signatures: new AuthModule
- Encrypted calldata + MEV protection: new PrivacyModule
- Advanced sponsorship: new SponsorModule
- Risk management: new PolicyModule
- Cross-chain intents: new SettlementModule
Protocol Minimal Closure
EXEC_TX’s design is based on finding the minimal set of protocol rules required for consensus safety, then delegating everything else to contracts.
This leaves room for evolution:
What the protocol MUST guarantee:
✅ Replay prevention (nonce)
✅ Gas metering consistency (deterministic)
✅ Phase atomicity (all-or-nothing)
What the protocol MUST NOT define:
❌ Authorization schemes
❌ Sponsorship policies
❌ Capability composition
❌ Application business logic
This separation is the key to longevity.
Open Questions for Community
- Validation Mode implementation consistency across clients?
- Hook gas limit flexibility vs. predictability?
- Payer fallback UX: accept silent fallback or require strict payer?
- 2D nonce lane presentation to end users?
- Dispatcher pattern standardization and audit practices?
Critical: Formal verification of Validation Mode opcode enforcement and client divergence prevention.
Update Log
- 2026-04-08: Initial RFC draft, PR pending
External Reviews
None as of 2026-04-08.
Outstanding Issues
- Hook contract ABI and interface standardization
- Validation Mode implementation consistency across EVM clients
- Phase context passing mechanism for hooks
- Payer fallback semantics and UX mitigation
- Contract account nonce safety guarantees
- Hook revert data capture and error messaging
- Gas metering for optional phases
- 2D nonce wallet UX design
- Dispatcher pattern data encoding standardization
Why Now?
Account abstraction fragmented into bundlers (4337), batch systems (8130), per-tx delegation (7702). EXEC_TX unifies them: one protocol envelope, pluggable hooks, contracts own security.
Related: EIP-155, EIP-1559, EIP-1153, EIP-2718, EIP-2929, EIP-4844, EIP-6780