---
eip: 8175
title: Composable Transaction
description: An extensible EIP-2718 transaction type with separated signatures, fee delegation via fee_auth, and opcodes for signature introspection
author: Dragan Rakita (@rakita)
discussions-to: https://ethereum-magicians.org/t/eip-8175-composable-transaction/27850
status: Draft
type: Standards Track
category: Core
created: 2026-02-26
requires: 2, 1559, 2718
---

## Abstract

This EIP introduces a new [EIP-2718](./eip-2718.md) transaction type with [EIP-1559](./eip-1559.md) 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.

## Motivation

Each Ethereum upgrade has introduced new transaction types for new capabilities: [EIP-1559](./eip-1559.md) for priority fees, [EIP-4844](./eip-4844.md) for blobs, and [EIP-7702](./eip-7702.md) for authorizations. Both [EIP-4844](./eip-4844.md) and [EIP-7702](./eip-7702.md) extend and reuse mostly the fields from [EIP-1559](./eip-1559.md), 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](./eip-8141.md) 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.

## 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](https://www.rfc-editor.org/rfc/rfc2119) and [RFC 8174](https://www.rfc-editor.org/rfc/rfc8174).

### Parameters

| 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` |

### Composable transaction

A new [EIP-2718](./eip-2718.md) 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](./eip-1559.md).

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](#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](#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](#signing-hash)). Every signature type MUST be known and every signature MUST be cryptographically valid.

The [EIP-2718](./eip-2718.md) `ReceiptPayload` for this transaction is `rlp([status, cumulative_transaction_gas_used, logs_bloom, logs])`.

### Capability types

#### CALL (`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 |

#### CREATE (`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.

### Signatures

#### Signature schemes

**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](./eip-2.md). 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.

### Signing hash

The signing hash binds both the transaction content and the signer's role, providing domain separation. It is computed in two steps:

```python
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 delegation via `fee_auth`

When `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:

```python
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](./eip-1559.md) 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.

### New opcodes

#### `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:

- Debits `value` wei from the balance of `ADDRESS` (the currently executing account).
- Credits `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.
- If `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.
- If the enclosing call frame reverts, the `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:

- If `index >= len(tx.signatures)`, execution **reverts**.
- Pushes the `signature_type` identifier onto the stack.
- Writes the signature body (excluding `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).
- Memory expansion cost is charged for the bytes written, following standard rules.

#### `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](#signing-hash) |
| **Gas** | `SIGHASH_GAS` (2) |

Behavior:

- Pushes `compute_base_hash(tx)` onto the stack as a 256-bit value.
- The `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:

- Pushes the `gas_limit` field of the current transaction onto the stack.
- This opcode is available in all execution contexts, not only `fee_auth`.
- The returned value is the transaction's total gas limit as specified by the sender, distinct from `GAS` (remaining gas) and `GASLIMIT` (block gas limit).

### Execution order

The state transition for a Composable transaction proceeds as follows:

```python
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
```

### Gas handling

The intrinsic gas cost is:

```python
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.

## Rationale

### Linear growth of transaction types

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.

### Separating signatures from capabilities

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.

### Domain-separated signing hash

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 delegation

The `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.

### Upfront maximum cost escrow

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](./eip-1559.md) reserve-and-refund discipline.

### `fee_auth` state persistence

State 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` opcode

`RETURNETH` 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` opcodes

These 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` opcode

The `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.

### CALL and CREATE as capabilities

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.

## Backwards Compatibility

This EIP introduces a new transaction type and does not modify the behavior of existing transaction types. No backward compatibility issues are expected.

## Security Considerations

### Signature domain separation

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.

### Payer replay protection

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 safety

The `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` griefing

A `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` restrictions

`RETURNETH` 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.

### Transaction propagation

Composable transactions with `fee_auth` introduce validation complexity similar to [EIP-7702](./eip-7702.md) and [EIP-8141](./eip-8141.md). 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

Copyright and related rights waived via [CC0](../LICENSE.md).