builtin-actors icon indicating copy to clipboard operation
builtin-actors copied to clipboard

FIP Implementation: Add support for EIP-7702 (Set Code for EOAs)

Open snissn opened this issue 5 months ago • 2 comments

Description This PR implements EIP-7702 in the Filecoin EVM actor, introducing atomic “apply authorizations + execute outer call” semantics and chain‑wide delegated execution for EOAs. The EVM actor persists an internal delegation map, per‑authority nonce map, and per‑authority storage roots, and interprets EOA calls under an authority context when delegated. It mirrors Ethereum’s semantics where applicable, while respecting FEVM’s execution model and gas system.

Related FIP PR link: https://github.com/filecoin-project/FIPs/pull/1209 Related LOTUS PR link: https://github.com/filecoin-project/lotus/pull/13408

Core behaviors:

  • Atomic ApplyAndCall: validates authorization tuples (domain 0x05), enforces nonces, applies/clears delegation pointers, and then executes the outer call in one transaction. Mapping and nonce updates persist even if the outer call reverts; ApplyAndCall always returns Exit OK with embedded status + return data.
  • Delegated EOA execution: CALLs to EOAs consult the internal delegation map. When delegated, the interpreter executes the delegate code with the EOA bound as receiver (InvokeAsEoa), enforcing depth=1 (no nested delegation) and emitting a Delegated(address) event with the authority address for attribution.
  • Pointer code and EXTCODE* virtualization: Delegated EOAs expose virtual code bytes 0xef0100||delegate(20), report size=23 and keccak(pointer_code) for EXTCODESIZE and EXTCODEHASH, and return the virtual bytes in EXTCODECOPY. Execution only follows the pointer when actually CALLing.

Signature and domain:

  • AuthorizationKeccak: keccak256(0x05 || rlp([chain_id, address, nonce])).
  • Accept yParity ∈ {0,1}. Accept 1..32‑byte r/s (left‑padded); reject >32‑byte r/s. Enforce non‑zero r/s and low‑s.
  • chainId must be 0 or match the local chain.

Pre‑existence policy and safety:

  • Reject delegations where the authority resolves to an EVM contract (Filecoin analogue of “code must be empty/pointer”).
  • In authority context: do not re‑follow delegation (depth limit=1). SELFDESTRUCT is a no‑op. Authority storage is mounted per‑authority and persisted across transactions.

Gas behavior:

  • ApplyAndCall forwards all available gas to the outer call (no 63/64 cap at top level). Subcalls continue to enforce EIP‑150’s 63/64 rule.
  • Emitting the synthetic Delegated(address) event is best‑effort and may be dropped under extreme gas tightness.
  • Refund plumbing is present with conservative placeholders; numeric constants will be finalized later.

New Capabilities

  • ApplyAndCall entrypoint for EIP‑7702 (atomic apply + execute).
  • EOA delegation pointers via “virtual code” 0xef0100||delegate(20).
  • Interpreter integration for delegated CALLs with authority context and depth=1.
  • EXTCODESIZE/HASH/COPY virtualization for delegated EOAs.
  • Synthetic Delegated(address) event with authority address for receipts attribution.
  • Internal persistence of: delegation map, per‑authority nonces, and per‑authority storage roots.

Testing Success Cases

  • ApplyAndCall happy path: tuples validate, mapping/nonces update, outer call executes and returns status + data.
  • Delegated CALL executes delegate code; authority storage mounts and persists; event emitted with correct topic and authority address.
  • Pointer semantics: EXTCODESIZE=23, EXTCODECOPY returns 0xef0100||delegate(20), EXTCODEHASH equals keccak(pointer_code).
  • Storage lifecycle: A→B writes persist; switching A→C isolates B’s storage; re‑delegating A→B restores B’s storage.
  • Depth limit: no nested delegation when already in authority context.
  • SELFDESTRUCT under delegated execution is a no‑op: no state/balance move, mapping intact.

Failure Cases

  • Invalid chainId (not 0 or local).
  • Invalid yParity (>1).
  • Zero r or s; high‑s; r/s length >32 bytes.
  • Nonce mismatch; duplicate authorities in the same message.
  • Exceeding tuple cap (64).
  • Pre‑existence policy: authority resolves to EVM contract.

Edge Behavior

  • First‑time authorities default to nonce=0; applying nonce=0 initializes tracking.
  • Value transfer failure to delegated EOA returns status=0 and propagates revert data.
  • Delegated revert data is surfaced via state.return_data; callers observe correct RETURNDATASIZE/COPY semantics.
  • ApplyAndCall always returns Exit OK with embedded status/result; mapping/nonces persist even if status=0.

Implementation Notes

  • Single EVM actor holds all EIP‑7702 state (delegation map, nonces, storage roots); chain‑wide semantics for EOAs.
  • Delegation bytecode magic/version: 0xef 0x01 0x00; detection centralized in shared helpers.
  • Authorization signature domain: 0x05; AuthorizationKeccak used for recovery.
  • Event topic: keccak("Delegated(address)"); value is the authority (EOA) address.
  • ApplyAndCall outer call forwards all gas; subcalls honor EIP‑150 63/64 clamp.
  • Activation: via bundle; no runtime NV gating on this branch.

Remaining TODOs

  • Finalize details for refunds
  • Tighten internal invariants:
    • Make return‑data decoding mandatory in all delegated paths; return illegal_state on decode failure.
    • Remove remaining unwrap/expect in EXTCODEHASH path; convert to explicit illegal_state.
    • Precompute and reuse n/2 for low‑s checks; consider stronger types for is_high_s input.
    • Centralize InvokeContractReturn type.
  • Adopt parallel FIP track for a tipset-wide multi-stage execution with upfront gas reservation (reserve gasLimit*gasPrice per message before execution, then exec+refund) to prevent same-account drain-then-burn and avoid miner-charged gas. https://github.com/filecoin-project/FIPs/discussions/1143#discussioncomment-12834217
  • E2E with Lotus once the wasm bundle is available; validate receipts attribution, atomic status propagation, and delegated execution through JSON‑RPC.
  • Continue fuzzing ApplyAndCall CBOR params; maintain R/S padding interop with minimally‑encoded values.

This PR brings Filecoin’s EVM actor to parity with EIP‑7702’s semantics for EOAs, enabling atomic apply‑and‑call transactions and globally consistent delegation behavior while preserving FEVM safety and execution guarantees.

Reviewers Guide to understanding the code changes:

This document synthesizes the provided notes into a comprehensive overview and technical deep dive of the EIP-7702 implementation within the Filecoin network.

Filecoin EVM + EIP-7702 (“Delegated EOAs”) — Implementation Overview

Audience: Filecoin core developers, Lotus node operators, and technically-savvy contributors/auditors. Scope: This document details the integration of Ethereum’s EIP-7702 into Filecoin’s EVM, synthesizing the architectural design and the interaction between the Go-based Lotus client and the Rust-based FVM (builtin-actors).


0. Understanding EIP-7702 in Filecoin

EIP-7702 introduces a mechanism allowing Externally Owned Accounts (EOAs) to temporarily adopt smart contract functionality. It lets an EOA delegate its execution context to a specified contract by publishing a signed authorization tuple via a new transaction type (0x04).

In this Filecoin integration, we implement an EVM‑only architecture:

  • Persist Delegations (EVM Actor): Delegation choices and per‑authority nonces are stored inside the EVM actor’s state.
  • Atomic Apply + Call: A new EVM method ApplyAndCall processes authorization tuples and immediately executes the outer call atomically.
  • Interpreter Semantics: The EVM interpreter handles delegation at CALL‑to‑EOA time, executing the delegate’s bytecode under the authority’s context; EXTCODE* opcodes expose a short pointer code on the authority account.
  • Client Handling (Lotus): Lotus exposes the 0x04 typed transaction, parses it, and constructs a Filecoin message invoking EVM.ApplyAndCall with atomic CBOR params.
  • Mempool: No 7702‑specific ingress policies; standard nonce/fee rules apply.

This design keeps Ethereum semantics where transactions update account behavior, making those updates visible and enforceable on-chain for subsequent EVM execution.


1. Architectural Overview

The implementation involves coordinated changes across the stack, centered on the EVM actor.

                       ┌──────────────────────────────┐
                       │ Lotus (Go)                   │
                       │ • Parses 0x04 tx             │
eth_sendRawTransaction │ • Encodes CBOR params        │  Mempool
   (JSON-RPC) ───────▶ │ • Builds msg → EVM.ApplyAndCall │
                       │ • RPC views/receipts         │
                       └───────────────┬──────────────┘
                                       │ Filecoin message (ApplyAndCall)
                                       ▼
              ┌─────────────────────────────────────────────┐
              │ EVM Actor — Rust/FVM                        │
              │ • ApplyAndCall (verify, write, bump, exec)  │
              │ • State: delegations (EOA→{delegate,nonce}) │
              │   - Atomic apply+call (rollback on revert)  │
              │ • CALL interception                         │
              │   - If delegated: execute delegate under    │
              │     authority context (no cross‑actor hop)  │
              │ • EXTCODE*/pointer code semantics           │
              │ • Event emission                            │
              └─────────────────────────────────────────────┘

Activation & Flags

  • Activation via Bundle: EIP-7702 functionality ships in the upgrade bundle; no separate runtime network-version gate.
  • Lotus Feature Flag: The send‑path is controlled by the eip7702_enabled build tag for development/testing.

2. On-Chain Logic (Rust/FVM)

2.1 EVM Actor (EVM‑Only)

The EVM actor persists all EIP‑7702 state and enforces tuple validation, nonce tracking, and execution semantics.

State Structure

Recommended state:

  1. delegations: Map Authority (EOA EthAddress) → { delegate: EthAddress, nonce: u64 }.
  2. (Optional) authority storage roots if isolating per‑EOA storage; otherwise reuse standard account storage.

Core Methods

  • ApplyAndCall (New method): Entry point for 0x04 transactions.
    • Validates: chain_id ∈ {0, local}, y_parity ∈ {0,1}, non‑zero r/s, low‑s.
    • Recovers Authority: keccak256(0x05 || rlp([chain_id, address, nonce])) then secp256k1 recovery.
    • Verifies Nonce: Matches the stored per‑authority delegation nonce.
    • Updates State: Update delegations and bump nonce(s).
    • Executes: Executes the outer call atomically; on revert, roll back the mapping/nonces.

Why this design? On-chain state ensures consensus on delegations. Nonces prevent replay attacks. Separate storage roots per EOA allow delegation with strong, account-local semantics, isolating the EOA’s storage from the delegate’s contract state.

2.2 Interpreter Changes

A. CALL Path: Interception and Delegation

The EVM interpreter's CALL instruction logic is modified:

  1. EOA Detection: The interpreter checks if the call target is an EOA and if EIP-7702 is active.
  2. Delegation Resolution: Consult the EVM actor’s internal delegation map.
  3. Delegation Activation: If a delegate is found:
    • The runtime verifies the delegate is an EVM contract and loads its bytecode CID.
    • Value Transfer: For non-static calls, any value attached is first transferred to the EOA (Authority).
    • Event Emission: Emit EIP7702Delegated(address) for observability.
    • Execution: Execute the delegate code under the authority context (no cross‑actor trampoline required).
  4. No-Code Behavior: If the delegate exists but has no bytecode or is non-EVM, the call resolves as a no-op success (1), mirroring standard EOA call behavior.

(If employing per‑EOA storage isolation, the interpreter should mount/unmount the authority storage root internally during delegated execution.) 4. Storage Persistence: After execution, if not read-only, the interpreter flushes the storage (system.flush_storage_root). No external storage actor is consulted in this branch; state remains internal to the EVM runtime.

Why this design? The mount/flush mechanism provides EOA-scoped storage longevity across different delegate contracts and messages, while allowing the delegate's code to run unmodified within the EOA's context.


3. Client Logic (Go/Lotus)

3.1 Parsing 0x04 Transactions

Lotus (ethtypes) recognizes the 0x04 prefix and decodes the RLP payload.

  • Outer Envelope: Includes standard EIP-1559 fields, the new authorizationList, and the outer signature (v, r, s).
  • authorizationList: A list of 6-tuples: [chain_id, address(20 bytes), nonce, y_parity, r (bytes), s (bytes)].

3.2 Conversion to Filecoin Message

The ToUnsignedFilecoinMessage method converts the parsed Eth7702TxArgs structure into a single Filecoin message. This is gated by the eip7702_enabled build tag.

  • To: The EVM actor exposing ApplyAndCall.
    • Method: ApplyAndCall.

3.3 Gating/Constants and Migration

  • Activation: Controlled by the deployed bundle; no runtime gating needed.
  • Constants: Authorization domain = 0x05; pointer bytecode = 0xef 0x01, version 0x00.
  • Migration/Compatibility: No migration required; the flow is EVM‑only and atomic‑only, and the Delegator actor has been removed.
  • Params: Atomic CBOR [ [ tuple1, tuple2, ... ], [ to(20), value, input ] ].

3.3 Mempool

EIP-7702 transactions are admitted under the standard mempool rules (nonce, fee, size constraints). Lotus does not implement 7702-specific ingress-time policies such as cross-account eviction or per-sender delegation caps.

3.4 RPC Surfaces and Gas Estimation

Gas Estimation

EthEstimateGas is updated to account for EIP-7702 overhead. When estimating a message targeting ApplyAndCall, Lotus parses the CBOR parameters to count the authorization tuples and adds intrinsic overhead (behavioral only until constants finalize).

RPC Responses and Receipts

To provide compatibility with Ethereum tooling, the EthTx and EthTxReceipt structures are extended:

  1. AuthorizationList: Included in both the transaction object and the receipt if the transaction type was 0x04.
  2. DelegatedTo: A new field added to the receipt to indicate which contracts were involved. Lotus populates this by extracting addresses from the AuthorizationList or by scanning execution logs for the EIP7702Delegated(address) event emitted by the FVM.

4. End-to-End Flows

4.1 Applying Delegations (Type 0x04 Tx)

  1. Client Submission: An RPC client submits a 0x04 transaction.
  2. Lotus Parsing/Conversion: Lotus parses the tx and builds a Filecoin message targeting EVM.ApplyAndCall.
  3. The message enters the mempool under standard policies (no 7702-specific cap or cross-account invalidation on ingress).
  4. FVM Execution (ApplyAndCall): The EVM actor validates tuples, recovers authorities, verifies nonces, updates mappings, and executes the outer call atomically.

4.2 Calling a Delegated EOA

  1. EVM CALL: EVM code executes a CALL instruction targeting an EOA address.
  2. Interception: The EVM runtime intercepts the call and checks the EVM actor’s internal delegation map.
  3. Delegation Found: If a mapping exists:
    1. Value is transferred to the EOA (Authority) if applicable.
    2. The EIP7702Delegated(address) event is emitted.
    3. The interpreter executes the delegate code under the authority context.
    4. The result is returned to the original caller.

5. Security and Correctness

  • Signature Validation: The EVM actor strictly enforces the low-s requirement and rejects zero r/s values. Authority recovery relies on a standard secp256k1 recovery process over the committed RLP data (0x05 domain), combined with stored delegation nonces for anti-replay.
  • Storage Isolation: If per‑EOA storage isolation is employed, the interpreter mounts/unmounts the authority storage internally during delegated execution.
  • Reorgs: Mempool invalidation is a best-effort policy upon ingress. Consensus correctness relies solely on the actor-enforced nonces and mappings at execution time.

6. Summary of Changes and Locations

This implementation required changes across the stack in the following key areas:

  • EVM Internals (Rust):
    • actors/evm/*: ApplyAndCall method, delegation state, CALL pointer semantics, EXTCODE* pointer behavior, optional storage mount.
  • Runtime Wiring (Rust):
    • Network version activation gate.
  • Lotus (Go):
    • chain/types/ethtypes/eth_7702_transactions.go: Added support for 0x04 RLP decoding and message construction.
    • chain/types/ethtypes/eth_7702_params.go: CBOR encoder for authorizationList matching actor ABI.
    • chain/types/ethtypes/eth_types.go: Extended RPC types/receipts to expose authorizationList and DelegatedTo.

Appendices: Technical Specifications

A. Actor Method Numbers and Names

  • EVM Actor:
    • ApplyAndCall (public) — method number tbd.

B. Encoding Boundaries

  • RLP (Ethereum wire): 0x04 prefix followed by a 13-element list; authorizationList is a list of 6-tuples.
  • CBOR (Actor ABI): ApplyAndCall parameters use atomic encoding [ [ tuple... ], [ to(20), value, input ] ].
  • Authority Recovery: Digest = keccak256(0x05 || rlp([chain_id, address(20), nonce])). Recovery uses FVM recover_secp_public_key.

snissn avatar Oct 31 '25 00:10 snissn

@ZenGround0 I looked into the two specific details we raised during our preliminary review:

  1. delegation map - make sure scope is reasonable - it looks reasonable, we need data stored per user so the EVM is the right scope for it
  2. consider https://github.com/snissn/builtin-actors/blob/81ff1333d608516fd2ef9ec0cf30728f5f346b86/actors/evm/src/lib.rs#L263 "can we clean up the code so that we use "CID" instead of passing bytecode around for function calls" - brief summary of the review i did "loading the bytecode bytes is required to execute, and this path doesn’t make an extra “large” copy. ApplyAndCall’s flow is efficient, with a single blockstore fetch and a compact jumpdest index."

snissn avatar Oct 31 '25 00:10 snissn

@ZenGround0 your coment on the deprecated PR:

https://github.com/snissn/builtin-actors/pull/1#discussion_r2475680969

@[ZenGround0](https://github.com/ZenGround0) ZenGround0 [yesterday](https://github.com/snissn/builtin-actors/pull/1#discussion_r2475680969)
because of our post-fvm pattern of shipping content addressed code bundles into lotus with new network upgrades you no longer need to write version statements like this apart from more nuanced edge cases. So this and branch can drop.

addresed in this commit: 607312afaba0419df2086eb514ca82edf2c76ef9 Remove unused NV_EIP_7702 constant (bundle-based activation; no runtime gating).

snissn avatar Oct 31 '25 00:10 snissn

I added a test that confirms that the evm actor is the wrong context to store the account -> delegate mapping. The evm actor has one instance per smart contract not a global scope. Next i'll move the mapping to the ethaddress actor and move delegation logic to the fvm repo

commit: ffb7c970eea200bc8d1231f706a7a0fa0e854b66

summary:

  • Setup a VM and an “authority” EOA A - Build a standalone TestVM with system singletons: ../builtin-actors/test_vm/src/lib.rs:126 - Derive A’s 20-byte ETH address from a fixed pubkey so it’s deterministic: ../builtin-actors/actors/evm/tests/ eoa_pointer_mapping_global.rs:60 - Make A resolve as an account (not a contract) by installing a placeholder actor with A’s delegated f4: ../builtin- actors/actors/evm/tests/eoa_pointer_mapping_global.rs:68

    • Deploy three EVM actors: M, C, D
      • Helper creates EVM actors via EAM.CreateExternal and returns ID + ETH address: ../builtin-actors/actors/evm/tests/ eoa_pointer_mapping_global.rs:33
      • M (manager) — where we’ll apply the mapping: ../builtin-actors/actors/evm/tests/eoa_pointer_mapping_global.rs:85
      • C (caller) — tiny contract that returns EXTCODESIZE(A): ../builtin-actors/actors/evm/tests/ eoa_pointer_mapping_global.rs:88, ../builtin-actors/actors/evm/tests/eoa_pointer_mapping_global.rs:103
      • D (delegate) — code doesn’t matter for EXTCODESIZE: ../builtin-actors/actors/evm/tests/ eoa_pointer_mapping_global.rs:106
    • Make the inner signature recover to A
      • Override TestVM’s secp pubkey recovery so any signature resolves to our fixed pubkey (hence authority A): ../ builtin-actors/actors/evm/tests/eoa_pointer_mapping_global.rs:109
    • Apply A→D via ApplyAndCall on M
      • Build a single tuple (chain_id=0, yParity=0, r/s dummy bytes, delegate=D) with nonce=0: ../builtin-actors/actors/ evm/tests/eoa_pointer_mapping_global.rs:113
      • Call M.ApplyAndCall with a no-op outer call so only delegation is applied: ../builtin-actors/actors/evm/tests/ eoa_pointer_mapping_global.rs:126
    • From C, check EXTCODESIZE(A)
      • Invoke C with input selecting the EXTCODESIZE(A) branch: ../builtin-actors/actors/evm/tests/ eoa_pointer_mapping_global.rs:139
      • Decode return bytes and read the size: ../builtin-actors/actors/evm/tests/eoa_pointer_mapping_global.rs:160
      • Assert it equals 23 — the size of the 7702 pointer code “0xEF 0x01 0x00 || 20-byte delegate”: ../builtin-actors/ actors/evm/tests/eoa_pointer_mapping_global.rs:166

    Why it fails today (and that’s the point)

    • The 7702 mapping is stored in M’s local state. C’s EXTCODESIZE(A) consults C’s local map (empty), so it returns 0 instead of 23.
    • This proves the map is per-contract, not global/singleton as AGENTS.md specifies.

snissn avatar Nov 07 '25 02:11 snissn