This EIP introduces a new EIP-2718 transaction type with EIP-1559 gas fields, typed capabilities (CALL and CREATE) that define the transaction's operations, a separate signatures list for authentication, a fee_auth field that executes account code to sponsor gas, and four new opcodes — RETURNETH, SIG, SIGHASH, and TX_GAS_LIMIT — for fee delegation and in-EVM transaction introspection.
Each Ethereum upgrade has introduced new transaction types for new capabilities: EIP-1559 for priority fees, EIP-4844 for blobs, and EIP-7702 for authorizations. Both EIP-4844 and EIP-7702 extend and reuse mostly the fields from EIP-1559, yet each required an entirely new transaction type. This leads to linear growth of transaction types with overlapping gas-payment semantics. This EIP proposes a single extensible transaction format where new features are added as typed capabilities without defining new transaction types.
Transaction sponsorship — where a third party pays gas on behalf of the sender — has been a long-sought feature. EIP-8141 proposes a solution using execution frames, new opcodes (APPROVE, TXPARAM), and per-frame gas budgets. This EIP achieves sponsorship with a simpler fee_auth field and four focused opcodes.
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.
| Parameter | Value |
|---|---|
COMPOSABLE_TX_TYPE |
0x05 |
CAP_CALL |
0x01 |
CAP_CREATE |
0x02 |
SIG_SECP256K1 |
0x01 |
SIG_ED25519 |
0x02 |
ROLE_SENDER |
0x00 |
ROLE_PAYER |
0x01 |
RETURNETH_OPCODE |
0xf6 |
SIG_OPCODE |
0xf7 |
SIGHASH_OPCODE |
0xf8 |
TX_GAS_LIMIT_OPCODE |
0xf9 |
RETURNETH_GAS |
2 |
SIG_BASE_GAS |
2 |
SIGHASH_GAS |
2 |
TX_GAS_LIMIT_GAS |
2 |
PER_SIG_SECP256K1_GAS |
3000 |
PER_SIG_ED25519_GAS |
3000 |
A new EIP-2718 transaction is introduced where the TransactionType is COMPOSABLE_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, fee_auth, capabilities, signatures])
The fields chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, and gas_limit follow the same semantics as EIP-1559.
The fee_auth field is either empty (zero bytes) or a 20-byte address. When non-empty, it designates the account whose code is executed to provide ETH covering the transaction fee (see Fee Delegation via fee_auth).
The capabilities field is an RLP list of typed capabilities. Each capability is an RLP list whose first element is the capability type identifier. The transaction MUST contain at least one capability. See Capability types for the defined types. Future EIPs MAY define additional capability types for this transaction format.
The signatures field is an RLP list of typed signatures. Each signature authenticates the transaction using a per-role signing hash (see Signing Hash). Every signature type MUST be known and every signature MUST be cryptographically valid.
The EIP-2718 ReceiptPayload for this transaction is rlp([status, cumulative_transaction_gas_used, logs_bloom, logs]).
CAP_CALL = 0x01)Executes a message call to an existing account.
Format: [cap_type, to, value, data]
| Field | Description |
|---|---|
cap_type |
0x01 |
to |
20-byte destination address |
value |
ETH value in wei to transfer |
data |
Calldata bytes |
CAP_CREATE = 0x02)Creates a new contract.
Format: [cap_type, value, data]
| Field | Description |
|---|---|
cap_type |
0x02 |
value |
ETH value in wei to endow the new contract |
data |
Initialization code (initcode) |
The contract address is derived as keccak256(rlp([sender_address, nonce]))[12:], where nonce is the sender's nonce at the time the CREATE capability executes. The sender's nonce is incremented by 1 for each CREATE capability.
SECP256K1 (SIG_SECP256K1 = 0x01):
Format: [signature_type, role, y_parity, r, s]
| Field | Size | Description |
|---|---|---|
signature_type |
1 byte | 0x01 |
role |
1 byte | ROLE_SENDER or ROLE_PAYER |
y_parity |
1 byte | Recovery ID (0 or 1) |
r |
32 bytes | ECDSA r component |
s |
32 bytes | ECDSA s component |
The s value MUST be less than or equal to secp256k1n/2, as specified in EIP-2. The signer address is recovered via ecrecover(sig_message, y_parity, r, s).
ED25519 (SIG_ED25519 = 0x02):
Format: [signature_type, role, public_key, signature]
| Field | Size | Description |
|---|---|---|
signature_type |
1 byte | 0x02 |
role |
1 byte | ROLE_SENDER or ROLE_PAYER |
public_key |
32 bytes | Ed25519 public key |
signature |
64 bytes | Ed25519 signature (RFC 8032, pure Ed25519) |
The signer address is keccak256(public_key)[12:]. The signature MUST verify under RFC 8032 pure Ed25519.
The signing hash binds both the transaction content and the signer's role, providing domain separation. It is computed in two steps:
def compute_base_hash(tx: ComposableTx) -> bytes:
return keccak256(COMPOSABLE_TX_TYPE || rlp([
tx.chain_id,
tx.nonce,
tx.max_priority_fee_per_gas,
tx.max_fee_per_gas,
tx.gas_limit,
tx.fee_auth,
tx.capabilities,
[] # signatures array blinded
]))
def compute_sig_message(tx: ComposableTx, signature_type: int, role: int) -> bytes:
return keccak256(
b"\x19ComposableTxSig" ||
bytes1(signature_type) ||
bytes1(role) ||
compute_base_hash(tx)
)
The signatures array is replaced with an empty list when computing base_hash. This allows all signers to sign independently and in any order. The signature_type and role are included in sig_message to prevent cross-scheme and cross-role signature confusion.
fee_authWhen fee_auth is non-empty, the account at fee_auth sponsors the transaction fee. Before the main transaction executes, the protocol invokes fee_auth as a prelude call:
| Context field | Value |
|---|---|
ADDRESS |
fee_auth |
CALLER |
sender (recovered from sender signature) |
ORIGIN |
sender |
CALLVALUE |
0 |
CALLDATA |
empty |
GAS |
gas_limit minus intrinsic gas |
The fee_auth code MUST use RETURNETH to credit ETH into the transaction-scoped fee escrow. The amount credited MUST be at least gas_limit * max_fee_per_gas (the maximum possible fee). The fee_auth code MAY use TX_GAS_LIMIT and GASPRICE to compute this required amount on-chain. The fee_auth code MAY use SIG and SIGHASH to introspect signatures for authorization.
If fee_auth execution reverts, or the fee escrow is insufficient after fee_auth returns, the transaction is invalid and MUST NOT be included in a block.
After the main transaction completes, the actual fee is settled:
actual_fee = gas_used * effective_gas_price
surplus = fee_escrow - actual_fee
# surplus is refunded to fee_auth address
When fee_auth is empty, gas is charged directly from the sender's balance, following standard EIP-1559 behavior.
State changes made during fee_auth execution persist regardless of whether the main transaction reverts. This allows sponsors to maintain their own accounting (e.g., nonces, rate limits) independently.
RETURNETH (0xf6)Returns ETH from the current execution context to the parent (calling) context.
| Stack input | value — amount in wei |
| Stack output | (none) |
| Gas | RETURNETH_GAS (2) |
Behavior:
value wei from the balance of ADDRESS (the currently executing account).value wei to the parent calling context. If the parent context is a contract call, the ETH is credited to the caller's account balance. If the parent context is the protocol-level fee_auth prelude, the ETH is credited to the transaction-scoped fee escrow.ADDRESS has insufficient balance, execution reverts.RETURNETH is valid in any execution context (not restricted to fee_auth), including descendant calls. When used in nested calls within fee_auth, the ETH propagates upward: each RETURNETH credits the immediate parent, and the top-level fee_auth frame's RETURNETH credits the fee escrow.RETURNETH is invalid in a STATICCALL context and causes an exceptional halt.RETURNETH credit is rolled back.SIG (0xf7)Loads a signature from the transaction's signatures array into memory.
| Stack input | index, mem_start |
| Stack output | sig_type |
| Gas | SIG_BASE_GAS (2) + memory expansion cost |
| Stack position | Value |
|---|---|
top - 0 |
index — zero-based index into signatures |
top - 1 |
mem_start — memory offset to write signature data |
Behavior:
index >= len(tx.signatures), execution reverts.signature_type identifier onto the stack.signature_type) to memory at mem_start:SIG_SECP256K1: writes role ‖ y_parity ‖ r ‖ s (1 + 1 + 32 + 32 = 66 bytes).SIG_ED25519: writes role ‖ public_key ‖ signature (1 + 32 + 64 = 97 bytes).SIGHASH (0xf8)Pushes the transaction base hash onto the stack.
| Stack input | (none) |
| Stack output | hash — 32-byte base_hash as defined in Signing Hash |
| Gas | SIGHASH_GAS (2) |
Behavior:
compute_base_hash(tx) onto the stack as a 256-bit value.fee_auth code can reconstruct sig_message from base_hash + signature data obtained via SIG, then verify signatures using the ecrecover precompile or future Ed25519 precompiles.TX_GAS_LIMIT (0xf9)Returns the transaction's gas_limit field.
| Stack input | (none) |
| Stack output | gas_limit — the transaction's gas limit as a 256-bit value |
| Gas | TX_GAS_LIMIT_GAS (2) |
Behavior:
gas_limit field of the current transaction onto the stack.fee_auth.GAS (remaining gas) and GASLIMIT (block gas limit).The state transition for a Composable transaction proceeds as follows:
def process_composable_tx(tx, block):
# 1. Static validation
validate_static_constraints(tx)
# 2. Compute base hash
base_hash = compute_base_hash(tx)
# 3. Validate all signatures and recover addresses
sender_address = None
payer_address = None
for sig in tx.signatures:
sig_msg = compute_sig_message(tx, sig.signature_type, sig.role)
addr = recover_address(sig, sig_msg)
if sig.role == ROLE_SENDER:
sender_address = addr
elif sig.role == ROLE_PAYER:
payer_address = addr
# 4. Nonce check and increment
assert state[sender_address].nonce == tx.nonce
state[sender_address].nonce += 1
# 5. Intrinsic gas
intrinsic_gas = compute_intrinsic_gas(tx)
assert tx.gas_limit >= intrinsic_gas
gas_remaining = tx.gas_limit - intrinsic_gas
# 6. Fee handling
fee_escrow = 0
max_tx_cost = tx.gas_limit * tx.max_fee_per_gas
if tx.fee_auth:
# Execute fee_auth prelude
fee_auth_result = evm_call(
caller=sender_address,
address=tx.fee_auth,
value=0,
data=b'',
gas=gas_remaining,
)
gas_remaining -= fee_auth_result.gas_used
assert not fee_auth_result.reverted
assert fee_escrow >= max_tx_cost # accumulated via RETURNETH
else:
# Standard: charge sender (or payer if present)
charge_account = payer_address or sender_address
assert state[charge_account].balance >= max_tx_cost
state[charge_account].balance -= max_tx_cost
fee_escrow = max_tx_cost
# 7. Execute capabilities sequentially
for cap in tx.capabilities:
if cap.cap_type == CAP_CALL:
result = evm_call(
caller=sender_address,
address=cap.to,
value=cap.value,
data=cap.data,
gas=gas_remaining,
)
elif cap.cap_type == CAP_CREATE:
result = evm_create(
caller=sender_address,
value=cap.value,
initcode=cap.data,
gas=gas_remaining,
)
gas_remaining = result.gas_remaining
if result.reverted:
break
gas_used = tx.gas_limit - gas_remaining
# 8. Settle fees
effective_gas_price = min(
tx.max_fee_per_gas,
tx.max_priority_fee_per_gas + block.base_fee_per_gas
)
actual_fee = gas_used * effective_gas_price
surplus = fee_escrow - actual_fee
block.coinbase.balance += gas_used * (effective_gas_price - block.base_fee_per_gas)
# base fee portion is burned
# Refund surplus
if tx.fee_auth:
state[tx.fee_auth].balance += surplus
else:
refund_account = payer_address or sender_address
state[refund_account].balance += surplus
The intrinsic gas cost is:
def compute_intrinsic_gas(tx):
gas = 21000 # base transaction cost
for cap in tx.capabilities:
gas += calldata_cost(cap.data) # 16 per non-zero byte, 4 per zero byte
if cap.cap_type == CAP_CREATE:
gas += 32000 # contract creation cost
gas += sum(
PER_SIG_SECP256K1_GAS if s.signature_type == SIG_SECP256K1
else PER_SIG_ED25519_GAS
for s in tx.signatures
)
return gas
If fee_auth is non-empty, gas consumed by fee_auth execution is subtracted from gas_limit before capabilities execute.
Rather than defining a new transaction type per feature (blobs, authorizations, sponsorship), each potentially combined with each other, this EIP defines a single extensible format. New features are expressed as capabilities within the existing type.
Placing signatures in their own array cleanly separates extension data from authentication. The signatures array is blinded during hash computation, so all signers produce their signature independently. This eliminates the ordered commitment chain of prior designs and simplifies multi-party signing flows.
The sig_message includes signature_type and role alongside the blinded transaction hash. This prevents a sender's signature from being reused as a payer's signature, even though both sign over the same base_hash. Without this, an attacker could reinterpret one role's signature as another's.
fee_auth for fee delegationThe fee_auth field provides programmable fee delegation. The sponsor's code runs before the main transaction, uses RETURNETH to escrow the maximum fee, and can implement arbitrary authorization logic by introspecting signatures via SIG and SIGHASH. This is strictly more powerful than a static payer co-signature.
Requiring fee_auth to escrow gas_limit * max_fee_per_gas before main execution avoids the chicken-and-egg problem of paying for execution with the proceeds of that execution. Surplus is refunded to fee_auth after settlement, mirroring EIP-1559 reserve-and-refund discipline.
fee_auth state persistenceState changes made during fee_auth execution persist even if the main transaction reverts. This allows sponsors to maintain internal accounting (nonces, rate limits, spend tracking) that must not be rolled back on user-transaction failure.
RETURNETH opcodeRETURNETH provides a safe, explicit mechanism for code to return ETH to the parent calling context. When used within the fee_auth prelude, ETH propagates up to the protocol-managed fee escrow. When used in regular calls, ETH is credited to the caller's account balance. This general-purpose design avoids the need for SELFDESTRUCT or value-carrying calls back to a system address, and enables composable ETH flows beyond fee delegation.
SIG and SIGHASH opcodesThese opcodes enable in-EVM signature introspection. The fee_auth code loads signatures via SIG, obtains the base hash via SIGHASH, reconstructs sig_message, and verifies signatures using ecrecover. This allows arbitrary sponsor authorization without off-chain coordination beyond collecting signatures.
TX_GAS_LIMIT opcodeThe fee_auth code must escrow exactly gas_limit * max_fee_per_gas wei via RETURNETH. While max_fee_per_gas is accessible through GASPRICE, no existing opcode exposes the transaction's gas_limit — GAS returns remaining gas (which decreases during execution) and GASLIMIT returns the block gas limit. TX_GAS_LIMIT fills this gap, enabling fee_auth contracts to compute the required escrow amount on-chain without relying on calldata or hardcoded values.
Rather than embedding to, value, and data as top-level transaction fields, these are expressed as typed capabilities. This makes the transaction envelope a pure fee-paying and authentication container, while the actual operations — message calls and contract creation — are composable payload items. A transaction may contain multiple capabilities, enabling batched execution within a single transaction. Future capability types (e.g., blob data, authorization lists) extend the same list without modifying the envelope format.
This EIP introduces a new transaction type and does not modify the behavior of existing transaction types. No backward compatibility issues are expected.
The sig_message includes both signature_type and role, preventing cross-role and cross-scheme confusion. A SECP256K1 sender signature cannot be replayed as an ED25519 payer signature or vice versa.
Every signature commits to the full transaction content including the sender's nonce. Since the sender's nonce is incremented on each transaction, signatures cannot be replayed across transactions.
fee_auth execution safetyThe fee_auth execution is bounded by gas_limit and runs before the main transaction. If fee_auth reverts or credits insufficient ETH, the transaction is invalid and produces no state changes. The upfront escrow requirement (gas_limit * max_fee_per_gas) ensures the protocol never under-collects fees.
fee_auth griefingA fee_auth contract could consume gas without crediting sufficient ETH, making the transaction invalid. Block builders can simulate fee_auth before inclusion to avoid wasting block space. Mempool nodes SHOULD simulate the fee_auth prelude before accepting the transaction.
RETURNETH restrictionsRETURNETH credits ETH to the immediate parent calling context — either the caller's account balance or the protocol fee escrow at the top-level fee_auth frame. It cannot send ETH to arbitrary addresses. It is invalid in STATICCALL contexts. If the enclosing call reverts, the credit is rolled back, preventing double-spending.
Composable transactions with fee_auth introduce validation complexity similar to EIP-7702 and EIP-8141. Nodes SHOULD keep at most one pending Composable transaction per sender in the public mempool. fee_auth simulation during mempool validation SHOULD be bounded in gas and restricted in state access patterns to limit DoS surface.
Copyright and related rights waived via CC0.