Abstract
This EIP introduces a new EIP-2718 transaction type - the sponsored transaction - in which gas fees are charged to the contract at tx.to rather than to the transaction sender. Authorization is mediated by a canonical payer registry system contract: the target contract must have explicitly registered tx.sender as its authorized sender. Validation requires no EVM code execution - only one storage read from the registry and one balance check on the payer - making it compatible with inclusion lists (FOCIL) and stateless validation.
Motivation
Gas sponsorship - where an entity other than the transaction sender pays for gas - is a frequently proposed Ethereum feature. Current proposals approach this through in-transaction code execution:
- EIP-8141 (Frame Transaction) introduces execution frames with a
VERIFYmode andAPPROVEopcode, enabling arbitrary payer logic but requiring EVM execution during validation. - EIP-8175 (Composable Transaction) adds a
fee_authfield that executes the payer's account code to provide ETH.
Both approaches introduce code execution into the transaction validation path, complicating static analysis for inclusion lists and increasing the attack surface for denial-of-service via expensive validation.
Many sponsorship use cases - particularly smart contract accounts that pay their own gas when called - do not require arbitrary payer logic. They need only: (1) the contract opts in to paying gas for a specific caller, and (2) the protocol charges the contract instead of the caller. This EIP provides exactly that, with validation reducible to static state reads.
Compatibility with Partial Statelessness (VOPS)
Under Validity-Only Partial Statelessness (VOPS)1, nodes validate transactions using only account-trie data (balances, nonces, code hashes) and shed contract storage tries entirely. Sponsorship proposals that require reading arbitrary contract storage or executing payer code during validation are incompatible with this model.
This EIP's validation reads are bounded to the account trie (sender nonce and balance, payer balance) plus a single storage slot from one known predeploy at 0x13. A VOPS node would only need to additionally retain the storage trie for PAYER_REGISTRY_ADDRESS - one contract, not the full storage universe - to validate sponsored transactions. This is the same class of exception VOPS already makes for protocol-level system contracts.
The Accidental Sponsorship Problem
Any sponsorship mechanism that relies on reading storage from arbitrary contracts risks accidental sponsorship - existing deployed contracts may have storage values that happen to satisfy an authorization check. A canonical system registry eliminates this: no contract can become a gas payer without explicitly calling the registry's authorize function, which did not exist before this EIP's activation.
One-to-One Payer Binding
Each payer contract authorizes exactly one sender address (one-to-one from payer to sender). However, many payer contracts can independently authorize the same sender (many-to-one from payers to sender). A single EOA can therefore control multiple smart accounts that each pay their own gas - each account registers the same EOA as its authorized sender via authorize, and the EOA sends sponsored transactions to whichever account it wants to interact with.
Combined with the mempool rule that limits each authorized sender to at most one sponsored transaction in the pool, a change in payer balance or registry entry triggers revalidation of at most one transaction. This is O(1) revalidation per payer, compared to the O(N) worst case of one-to-many sponsorship models where multiple senders depend on a single payer.
Key Rotation
Because the smart contract account holds all assets and the controlling EOA holds none, key rotation is a single registry update: the contract owner calls authorize(newEOA) and the old key is instantly deauthorized for sponsored transactions. No asset migration is required - the account address, balances, token holdings, and onchain identity remain unchanged. The new EOA can be a freshly generated key with no prior onchain history. This is simpler than EOA key rotation, which today requires transferring all assets to a new address.
Protocol Gas-Funded Operations
A protocol or DAO can deploy a dedicated smart contract account with scoped roles (parameter updates, oracle feeds, keeper operations), fund it with ETH from the treasury, and call authorize(operatorEOA). The operator runs the protocol using sponsored transactions - the account pays gas, the operator's EOA holds nothing.
Handover is a single authorize(newOperatorEOA) call - the new operator inherits the account's roles, permissions, and gas budget without any multi-step permission setup, and the old operator is simultaneously deauthorized (one-to-one binding means the new authorization replaces the old). Revocation without handover uses the same call: authorize(address(0)) removes both the operational role and the gas sponsorship. In either case, no fund recovery is required - the account's remaining ETH stays in the protocol's control.
Specification
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174.
Parameters
| Parameter | Value |
|---|---|
SPONSORED_TX_TYPE | 0x07 |
PAYER_REGISTRY_ADDRESS | 0x0000000000000000000000000000000000000013 |
Sponsored Transaction
A new EIP-2718 transaction is introduced where the TransactionType is SPONSORED_TX_TYPE and the TransactionPayload is the RLP serialization of:
rlp([chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas,
gas_limit, to, value, data, access_list, y_parity, r, s])The fields follow the same semantics as EIP-1559 with the following constraint:
toMUST NOT be empty. Sponsored contract creation is not supported.toMUST have non-empty code. Only contracts can be gas payers.
The nonce is the sender's nonce, incremented as normal. The sender is recovered from the signature (y_parity, r, s) using standard ECDSA recovery over the signing hash:
keccak256(SPONSORED_TX_TYPE || rlp([chain_id, nonce, max_priority_fee_per_gas,
max_fee_per_gas, gas_limit, to, value, data, access_list]))The EIP-2718 ReceiptPayload for this transaction is rlp([status, cumulative_transaction_gas_used, logs_bloom, logs]).
Payer Registry Predeploy
At the block where this EIP activates, the code at PAYER_REGISTRY_ADDRESS (0x13) MUST be set to the exact runtime bytecode specified in the Reference Implementation. 0x13 is the next address after the precompiles allocated by EIP-2537 (0x0b-0x11) and EIP-7997 (0x12). The contract is placed in the precompile address range because it is read by the protocol during transaction validation, not only by EVM execution.
Storage Layout
The contract uses direct address-keyed storage via inline assembly - the storage slot for a given payer IS its address, with no Solidity mapping hash:
storage[uint256(payer_address)] = uint256(authorized_sender_address)Both values are stored as 256-bit words with the address occupying the low 160 bits. The upper 96 bits MUST be zero. The canonical contract enforces this by masking on write; the protocol MUST compare only the low 160 bits when reading the registry during validation.
Contract Interface
// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.28;
/// @title Payer Registry - EIP-8223
/// @notice Canonical system contract for gas sponsorship authorization.
/// Each payer (msg.sender) registers exactly one authorized sender.
/// The protocol reads storage[payer] directly during sponsored
/// transaction validation - no EVM execution required.
contract PayerRegistry {
/// @notice Emitted when a payer sets or revokes authorization.
/// @param payer The contract that will pay gas (always msg.sender).
/// @param sender The address now authorized, or address(0) on revoke.
event Authorized(address indexed payer, address indexed sender);
/// @notice Authorize `sender` to submit sponsored transactions where
/// msg.sender pays for gas. Pass address(0) to revoke.
/// @dev Uses direct-slot storage: sstore(caller, sender).
/// The protocol reads this slot without executing contract code.
function authorize(address sender) external {
assembly {
sstore(caller(), and(sender, 0xffffffffffffffffffffffffffffffffffffffff))
}
emit Authorized(msg.sender, sender);
}
/// @notice Returns the authorized sender for `payer`, or address(0).
/// @dev Uses direct-slot storage: sload(payer). Mask to 160 bits on read.
function getAuthorized(address payer) external view returns (address sender) {
assembly {
sender := and(sload(payer), 0xffffffffffffffffffffffffffffffffffffffff)
}
}
}authorize is the only state-mutating function. Only msg.sender can set its own payer entry - no account can register another account as a gas payer. getAuthorized is a convenience view for on-chain queries; the protocol does not call it.
Both functions MUST revert if calldatasize < 36 (4-byte selector plus one 32-byte ABI word). Trailing calldata beyond the first argument is ignored, consistent with standard Solidity ABI decoding for single-argument external functions.
Protocol Storage Access
During transaction validation, the protocol reads storage[uint256(tx.to)] from PAYER_REGISTRY_ADDRESS directly, without executing the contract's code. Because the contract uses direct address-keyed slots (not a Solidity mapping), the storage key is simply the payer address zero-extended to 32 bytes - no keccak256 hashing.
This is a single storage slot read from a known predeploy address - equivalent in complexity to the storage reads already performed for EIP-4788 beacon roots and EIP-2935 block hashes.
Clients MAY optimize this read by maintaining the payer registry state in a dedicated data structure.
Transaction Validation
A sponsored transaction is valid if and only if all of the following hold:
tois not empty.tohas non-empty code (EXTCODESIZE(to) > 0). Only contracts can be gas payers.- The signature is valid and the sender is recoverable.
sender != to. The sender and payer MUST be distinct addresses.chain_idmatches the current chain.max_fee_per_gas >= base_fee_per_gas- as in EIP-1559.max_fee_per_gas >= max_priority_fee_per_gas- as in EIP-1559.gas_limit >= intrinsic_gas- whereintrinsic_gasis computed identically to EIP-1559 (21,000 base cost plus calldata costs and access-list charges).nonce == sender.nonce.payer_registry_storage[to] == sender- the authorized sender in the payer registry atPAYER_REGISTRY_ADDRESSfor keyto, compared using only the low 160 bits, equals the recovered sender.to.balance >= gas_limit * max_fee_per_gas- the payer has sufficient balance to cover the maximum gas cost.sender.balance >= value- the sender has sufficient balance for the value transfer, if any.
If any check fails, the transaction is invalid and MUST NOT be included in a block.
Mempool Rules
Clients MUST NOT hold more than one sponsored transaction per recovered sender address in the transaction pool, whether pending (executable at the current nonce) or queued (future nonce). If a new sponsored transaction arrives from a sender that already has one sponsored transaction in the pool, the client SHOULD replace the existing sponsored transaction only if the new transaction has the same nonce as the existing sponsored transaction and strictly higher max_fee_per_gas and max_priority_fee_per_gas.
This constraint - covering both pending and queued transactions - is REQUIRED to achieve O(1) revalidation per payer. Because each payer authorizes exactly one sender, and each sender can contribute at most one sponsored transaction to the pool, each payer can have at most one relevant sponsored transaction in the pool. It also bounds churn from externally invalidated sponsorship: a payer balance change, registry update, payer code transition between empty and non-empty, sender nonce change, or sender balance change for a sponsored transaction with value > 0 can invalidate the sponsored transaction, so carrying many sponsored transactions from the same sender would create avoidable revalidation work with no additional protocol benefit. The replacement criterion uses max_fee_per_gas and max_priority_fee_per_gas because both are transaction-local quantities that do not depend on the current block's base_fee_per_gas, and requiring both to increase prevents a replacement from offering a weaker fee cap or a lower effective tip.
This is similar in spirit to EIP-7702, which recognizes that some transaction types have different implications for transaction-pool management, but this EIP makes the pool invariant explicit because the O(1) revalidation property depends on it.
Clients MUST revalidate the sponsored transaction for a payer when the payer's balance or registry slot at PAYER_REGISTRY_ADDRESS changes. Clients MUST also revalidate the sponsored transaction when tx.to's code changes in a way that can affect the EXTCODESIZE(to) > 0 validity condition. Clients MUST also revalidate the sponsored transaction for a sender when the sender's nonce changes, or when the sender's balance changes for a sponsored transaction with value > 0.
Transaction Execution
Execution of a valid sponsored transaction proceeds as follows:
- Compute effective gas price: Let
effective_gas_price = base_fee_per_gas + min(max_priority_fee_per_gas, max_fee_per_gas - base_fee_per_gas). - Gas funding: Transfer
gas_limit * effective_gas_pricefromto.balancetosender.balance. The validation check (step 11 above) ensures the payer can afford this atmax_fee_per_gas; the actual transfer useseffective_gas_price, consistent with EIP-1559. - Gas escrow: Deduct
gas_limit * effective_gas_pricefromsender.balance. This is the standard EIP-1559 gas purchase, applied to the sender who now holds the payer's funds. - Nonce increment: Set
sender.nonce = sender.nonce + 1. - Execute: Perform a message call from
sendertotowithvalue,data, andgas_limit - intrinsic_gasavailable gas.CALLER(msg.sender) is the transaction sender.ORIGIN(tx.origin) is the transaction sender. - Gas refund: Add
(gas_limit - gas_used) * effective_gas_pricetosender.balance. This is the standard EIP-1559 refund. - Refund return: Transfer
(gas_limit - gas_used) * effective_gas_pricefromsender.balancetoto.balance. This returns unused gas funds to the payer. - Priority fee: Add
gas_used * (effective_gas_price - base_fee_per_gas)to the coinbase balance. - Base fee burn:
gas_used * base_fee_per_gasis burned.
Steps 2-3 and 6-7 are each atomic: the sender's balance is transiently increased (step 2) then immediately decreased (step 3), so the sender never observes the gas funds during execution. The net effect is identical to deducting gas directly from the payer, but the intermediate balance movements are real and emit EIP-7708 Transfer logs. The value transfer from sender to to occurs as part of normal message call execution in step 5.
Interaction with EIP-7702
If the sender has an EIP-7702 delegation designator, the delegation is resolved as normal for code execution purposes. The payer registry check and gas charging are independent of the sender's delegation status.
If tx.to has an EIP-7702 delegation designator, the delegation is resolved for code execution (the call executes the delegated code in the context of tx.to), but the payer registry check and gas escrow use the literal tx.to address, not the delegation target.
Interaction with EIP-7708
EIP-7708 requires that all ETH transfers emit logs. Every balance transfer in the Transaction Execution steps is a real state change. The two new transfers introduced by this EIP - the gas funding (step 2, tx.to -> sender) and the refund return (step 7, sender -> tx.to) - MUST each emit exactly one EIP-7708 Transfer log using the same format EIP-7708 defines for all ETH transfer logs:
| Field | Gas funding log (step 2) | Refund return log (step 7) |
|---|---|---|
address | SYSTEM_ADDRESS | SYSTEM_ADDRESS |
topics[0] | keccak256('Transfer(address,address,uint256)') | keccak256('Transfer(address,address,uint256)') |
topics[1] | tx.to | sender |
topics[2] | sender | tx.to |
data | gas_limit * effective_gas_price | (gas_limit - gas_used) * effective_gas_price |
Where SYSTEM_ADDRESS is 0xfffffffffffffffffffffffffffffffffffffffe as defined by EIP-4788.
These two logs are included in the transaction receipt in execution order: the gas funding log (step 2) is emitted before any logs produced by the transaction call, and the refund return log (step 7) is emitted after all logs produced during execution. This ordering is consensus-critical.
All other logs (value transfer, in-execution calls, priority fee, base fee burn) are emitted by the standard EIP-1559 and EIP-7708 machinery applied to the sender, with no modifications. Because the sender actually holds and pays the gas, receipts, effectiveGasPrice, and all gas-related logs work identically to EIP-1559 transactions. No synthetic logs are introduced beyond the two explicitly defined above.
Rationale
Why a new transaction type?
A new transaction type allows the sender to explicitly opt in to sponsored execution on a per-transaction basis. Without this, every transaction sent to a contract that has authorized the sender would automatically be sponsored, which may not be the sender's intent.
Transaction format flexibility
This EIP specifies the sponsored transaction as a standalone EIP-2718 type for clarity, but the core mechanism - the payer registry predeploy, the registry[tx.to] == sender validation rule, and the payer-side gas escrow - is independent of the transaction envelope. If the Ethereum transaction format evolves through proposals such as EIP-8175 (Composable Transaction) or EIP-8141 (Frame Transaction), registry-based sponsorship could be expressed as a capability or frame mode within those formats rather than as a separate type. The payer registry and validation logic would remain unchanged; only the signaling mechanism (dedicated type byte vs. capability flag vs. frame mode) would differ.
Why not use EIP-7702 alone?
EIP-7702 gives EOAs smart contract functionality by delegating code execution, but it does not change who pays gas. After setting a 7702 delegation, the EOA still needs ETH in its own balance to send transactions - sender.balance >= gas_limit * max_fee_per_gas is still enforced. 7702 enables batching, privilege de-escalation, and custom validation logic, but the gas always comes from the transaction sender.
This EIP solves the complementary problem: the contract at tx.to pays gas, not the sender.
Gasless hot-key
The two compose to provide key separation with gasless hot-key operation:
- EOA-1 (master key) uses EIP-7702 to delegate to smart wallet code. EOA-1 now has code and keeps its existing address, assets, ENS name, and token approvals. EOA-1's key goes into cold storage.
- EOA-1 (via delegated code) calls
registry.authorize(EOA-2). - EOA-2 (hot key) sends sponsored transactions with
tx.to = EOA-1. EOA-2 holds zero ETH. EOA-1 pays gas. - The delegated code at EOA-1 executes the call.
Key rotation: EOA-1's delegated code calls authorize(EOA-3). EOA-2 loses sponsored access instantly. If EOA-2 is compromised, the attacker has an empty EOA - use EOA-1's cold key to revoke.
Account consolidation
A user with multiple EOAs (EOA-1, EOA-2, EOA-3) can upgrade each via EIP-7702 and have each call authorize(EOA-new) for the same fresh signing key. All existing accounts - with their addresses, assets, approvals, and onchain history - are now controlled by a single signer without moving any assets. The many-to-one authorization model (see One-to-One Payer Binding) makes this possible: each account independently authorizes the same sender.
This EIP also works without EIP-7702: a standalone smart contract account (deployed via CREATE2) can hold assets and authorize an EOA directly. The 7702 composition is useful when the user wants to upgrade existing EOAs into smart accounts while preserving their addresses.
Why not wait for a more general sponsorship EIP?
The core contribution of this EIP is the payer registry predeploy, not the transaction type. The registry - where contracts explicitly authorize a sender for gas sponsorship via a canonical system contract at a known address - is infrastructure that any static-validation sponsorship mechanism needs. If EIP-8141 or EIP-8175 ships a more general sponsorship model, the registry remains useful: the authorization check (registry[tx.to] == sender) and the predeploy at 0x13 are consumed by the general solution, not deprecated by it. Only the signaling mechanism (dedicated transaction type vs. capability flag vs. frame mode) would change, and the transaction type is the simplest part of the spec.
Why tx.to as the payer?
For the primary use case - smart contract accounts paying their own gas when called - the payer IS the call target. Using tx.to eliminates the need for an additional transaction field and provides natural binding: sponsorship only applies to calls directed at the payer contract. This prevents a payer from being charged for arbitrary transactions it has no involvement in.
Why one-to-one instead of one-to-many?
A one-to-many mapping would allow a single contract to sponsor multiple users. However, this creates O(N) mempool revalidation when the payer's balance changes - the same problem EIP-8141 identifies as the hard public-mempool case. The one-to-one authorization constraint, combined with the single-tx-per-sender mempool rule, limits revalidation to O(1) per payer: each payer authorizes at most one sender, and each sender contributes at most one sponsored transaction to the pool.
The sender-scoped cap is also useful because sponsorship can be invalidated by external state changes (payer balance, registry entry, payer code becoming empty, sender nonce, and sender balance for nonzero-value transactions), so admitting multiple sponsored transactions from the same sender would add churn without increasing expressiveness. One-to-one authorization alone is not sufficient - without the mempool cap, a single sender could still queue a nonce-ordered sequence against the same payer.
This follows the same general pattern as EIP-7702: the transaction format itself is not the only consensus concern, because client transaction-pool behavior must also account for the new validity interactions introduced by the type.
For use cases requiring one-to-many sponsorship (e.g., application-level relayers), EIP-8175 or EIP-8141 are more appropriate.
Why a predeploy contract instead of an account-level field?
A predeploy at a precompile-range address requires no changes to the account model and allows the authorization state to be queried by other contracts via standard calls. It follows established Ethereum precedent for protocol-read state: EIP-4788 and EIP-2935 use system contracts; EIP-7997 uses a predeploy at 0x12. Placing the registry at 0x13 reflects that the protocol reads this state during transaction validation - the same class of protocol-level action as precompile execution. It also avoids conceptual collision with EIP-7702's use of the account code field.
Why not read authorization from the payer's own storage?
Reading a storage slot from an arbitrary contract during validation is unsafe: existing deployed contracts may have storage values at common slots that accidentally satisfy the check. A canonical registry ensures that authorization is always the result of an explicit authorize call that could not have occurred before this EIP's activation.
Why pre-execution escrow?
Without upfront gas escrow, the payer contract could spend down its own ETH during execution and leave the transaction undercollateralized. EIP-8175 identifies this same requirement. The gas flow is: payer funds the sender, sender pays gas via standard EIP-1559 mechanics, unused gas is refunded to the sender, sender returns the refund to the payer. Every step is a real balance transfer, which means EIP-7708 logs, receipts, and effectiveGasPrice all work identically to normal transactions. The funding and return transfers use effective_gas_price, not max_fee_per_gas, to avoid freezing the spread during execution.
Backwards Compatibility
This EIP introduces a new transaction type and a new predeploy contract. It does not modify the behavior of existing transaction types.
Contracts that wish to act as gas payers MUST explicitly call the payer registry. No existing contract is affected unless it takes this action.
The SPONSORED_TX_TYPE value 0x07 does not conflict with any finalized transaction types: 0x01 (EIP-2930), 0x02 (EIP-1559), 0x03 (EIP-4844), 0x04 (EIP-7702).
Reference Implementation
Payer Registry Runtime Bytecode
The canonical runtime bytecode for the payer registry predeploy is:
0x5f3560e01c8063b6a5d7de14601b5763c731fc7b14607e575f5ffd5b506024361060a65760043573ffffffffffffffffffffffffffffffffffffffff16335560043573ffffffffffffffffffffffffffffffffffffffff16337ff5a7f4fb8a92356e8c8c4ae7ac3589908381450500a7e2fd08c95600021ee8895f5fa3005b6024361060a6576004355473ffffffffffffffffffffffffffffffffffffffff165f5260205ff35b5f5ffdThis is 170 bytes long. Its keccak256 code hash is:
0x95fc87eed2393785342038c61323e9b975b23fa917fbda7b9bc721bfdd7b3e5cThe following EVM assembly is an equivalent human-readable rendering of that runtime bytecode:
// Entrypoint: dispatch on selector
push0 // [0]
calldataload // [calldata[0:32]]
push1 0xe0
shr // [selector]
// authorize(address) = 0xb6a5d7de
dup1 // [selector, selector]
push4 0xb6a5d7de // [0xb6a5d7de, selector, selector]
eq // [match, selector]
push1 AUTHORIZE // [AUTHORIZE, match, selector]
jumpi // [selector]
// getAuthorized(address) = 0xc731fc7b
push4 0xc731fc7b // [0xc731fc7b, selector]
eq // [match]
push1 GET // [GET, match]
jumpi // []
// No match → revert
push0
push0
revert
// authorize(address sender)
AUTHORIZE:
jumpdest
pop // []
push1 0x24
calldatasize // [calldatasize, 0x24]
lt // [calldatasize < 36]
push1 BADARGS // [BADARGS, calldatasize < 36]
jumpi // []
push1 0x04
calldataload // [raw_sender]
push20 0xffffffffffffffffffffffffffffffffffffffff
and // [sender] mask to 160 bits
caller // [caller, sender]
sstore // [] storage[caller] = sender
// emit Authorized(msg.sender, sender)
push1 0x04
calldataload // [raw_sender]
push20 0xffffffffffffffffffffffffffffffffffffffff
and // [sender] mask to 160 bits
caller // [caller, sender]
// Authorized(address indexed payer, address indexed sender)
// topic0 = keccak256("Authorized(address,address)")
push32 0xf5a7f4fb8a92356e8c8c4ae7ac3589908381450500a7e2fd08c95600021ee889
push0
push0
log3 // log3(0, 0, topic0, caller, sender)
stop
// getAuthorized(address payer) → address
GET:
jumpdest
push1 0x24
calldatasize // [calldatasize, 0x24]
lt // [calldatasize < 36]
push1 BADARGS // [BADARGS, calldatasize < 36]
jumpi // []
push1 0x04
calldataload // [payer]
sload // [raw_authorized]
push20 0xffffffffffffffffffffffffffffffffffffffff
and // [authorized] mask to 160 bits
push0
mstore // [] mem[0:32] = authorized
push1 0x20
push0
return // return mem[0:32]
BADARGS:
jumpdest
push0
push0
revertThe contract is installed at PAYER_REGISTRY_ADDRESS (0x13) as a predeploy in the genesis state of the activating fork, with its code set to the exact runtime bytecode above. Implementations MAY reproduce the bytecode from this assembly using the sys-asm toolchain, but the hex byte sequence above is canonical.
Usage Example
A smart contract account authorizing its controller for sponsored transactions:
interface IPayerRegistry {
function authorize(address sender) external;
function getAuthorized(address payer) external view returns (address);
}
IPayerRegistry constant PAYER_REGISTRY = IPayerRegistry(PAYER_REGISTRY_ADDRESS);
contract SmartAccount {
address public controller;
constructor(address initialController) {
controller = initialController;
// Authorize controller to submit sponsored txs targeting this account
PAYER_REGISTRY.authorize(initialController);
}
/// @notice Rotate controller - atomically updates both control and
/// gas sponsorship authorization in one state transition.
function transferControl(address newController) external {
require(msg.sender == controller, "Not controller");
controller = newController;
PAYER_REGISTRY.authorize(newController);
}
/// @notice Revoke all sponsored access.
function revokeSponsorship() external {
require(msg.sender == controller, "Not controller");
PAYER_REGISTRY.authorize(address(0));
}
function execute(address target, uint256 value, bytes calldata data) external {
require(msg.sender == controller, "Not controller");
(bool ok,) = target.call{value: value}(data);
require(ok, "Execution failed");
}
receive() external payable {}
}The controller can then send sponsored transactions (type SPONSORED_TX_TYPE) with tx.to = address(SmartAccount). The protocol verifies registry[SmartAccount] == controller, escrows gas from SmartAccount's balance, and executes the call. The controller's EOA needs no ETH.
Security Considerations
Payer Balance Drainage
The gas escrow mechanism prevents undercollateralization during execution. However, a malicious authorized sender can drain the payer's ETH through the gas escrow itself by submitting transactions with high gas_limit * max_fee_per_gas. Payer contracts SHOULD implement appropriate controls and monitor balance levels. The payer can revoke authorization at any time by calling authorize(address(0)).
Observable Balance During Execution
The gas funding transfer (step 2 of Transaction Execution) moves gas_limit * effective_gas_price from to.balance to sender.balance before the contract executes. Because the transaction then calls to, the contract observes a lower address(this).balance for the duration of the call. The unused-gas refund is only returned to the payer after execution completes (step 7).
Payer contracts should account for this: a contract that attempts to sweep address(this).balance during a sponsored call will transfer less than its full pre-transaction balance. The funded portion is not lost - it is returned to the payer after execution - but it is not visible to BALANCE or SELFBALANCE during the call. Payer contracts designed for this EIP should reserve headroom above the maximum gas funding and should not rely on address(this).balance reflecting the full pre-transaction balance.
Registry Manipulation During Execution
The payer registry is checked during validation, not during execution. If a sponsored transaction's execution modifies the registry (e.g., the payer revokes authorization mid-execution), this does not affect the current transaction's gas payment. The change takes effect for subsequent transactions.
Mempool Considerations
A sponsored transaction becomes invalid when:
- The payer's balance drops below
gas_limit * max_fee_per_gas. - The payer changes its authorized sender in the registry.
- The payer no longer has non-empty code.
- The sender's nonce changes.
- The sender's balance drops below
value, ifvalue > 0.
The Mempool Rules ensure each payer has at most one relevant sponsored transaction and each sender has at most one sponsored transaction in the pool, so invalidation remains O(1) for the affected payer or sender.
Front-Running Registry Updates
If a payer contract updates its authorized sender (e.g., during a control rotation), the old sender's pending transactions remain valid until the update lands onchain. Payer contracts that require atomic rotation SHOULD update the registry in the same state transition as the control change.
Interaction with Account Abstraction
This EIP is complementary to full account abstraction proposals (EIP-8141, EIP-8175). It covers the specific case of a contract paying for calls directed at itself with static validation. For arbitrary payer logic, cross-contract sponsorship, or alternative signature schemes, the more general proposals are appropriate.
Copyright
Copyright and related rights waived via CC0.
Footnotes
-
soispoke, "A pragmatic path towards Validity-Only Partial Statelessness (VOPS)," Ethereum Research (Execution Layer Research), April 29, 2025, available at
https://ethresear.ch/t/a-pragmatic-path-towards-validity-only-partial-statelessness-vops/22236. ↩
