Skip to content
Open
Show file tree
Hide file tree
Changes from 38 commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
a0657b4
EIP-7702 Phase-2: Delegator actor, CALL→EOA delegation, test harness …
snissn Sep 3, 2025
699be20
update
snissn Oct 15, 2025
7b73aeb
evm: fix GH action warnings/errors: remove unused NetworkVersion impo…
snissn Oct 22, 2025
188f355
EIP-7702 tests (builtin-actors):
snissn Oct 24, 2025
fa6a50a
builtin-actors: add InvokeAsEoa nested delegation behavioral test (cu…
snissn Oct 24, 2025
4eb694b
builtin-actors: mark nested InvokeAsEoa delegation test as ignored fo…
snissn Oct 24, 2025
8e61f8d
builtin-actors: flesh out nested InvokeAsEoa delegation test expectat…
snissn Oct 24, 2025
4319b86
builtin-actors: add CALL→EOA edge case where delegate returns no outp…
snissn Oct 24, 2025
73eb2c3
builtin-actors: add nested CALL→EOA test (ignored for now) and contin…
snissn Oct 24, 2025
5238515
builtin-actors: relax nested InvokeAsEoa test expectations; keep igno…
snissn Oct 24, 2025
4542370
builtin-actors: align nested InvokeAsEoa test with expected ordering …
snissn Oct 24, 2025
4875c6c
EIP-7702: EVM-only + atomic-only; remove Delegator crate; internalize…
snissn Oct 25, 2025
0a87399
EIP-7702: InvokeAsEoa persistent storage; intrinsic gas charging; dup…
snissn Oct 26, 2025
c1a5f38
EIP-7702: depth limit ApplyAndCall test; pre-existence + tuple cap te…
snissn Oct 27, 2025
8585f84
EIP-7702 tests: fix tuple cap expectations and recovery, add event + …
snissn Oct 27, 2025
61ae6a6
fix lint errors
snissn Oct 27, 2025
c229ee8
fmt
snissn Oct 27, 2025
d09f0d2
runtime: refactor expect_send_any_params to reduce args (introduce Se…
snissn Oct 27, 2025
821a4fd
rust fmt
snissn Oct 27, 2025
f1a1404
EIP-7702: restore precompile tests; add nonce initialization test; pe…
snissn Oct 27, 2025
87243d8
fmt: cargo fmt --all
snissn Oct 27, 2025
8f547b3
tests(evm): fix clippy unused imports in precompile.rs
snissn Oct 27, 2025
1d4230c
tests(evm): add failing test for EIP-7702 atomicity on InvokeAsEoa re…
snissn Oct 27, 2025
9d4be32
fix(evm): EIP-7702 atomicity — don’t abort ApplyAndCall on InvokeAsEo…
snissn Oct 27, 2025
99381d4
EIP-7702: ensure ApplyAndCall atomicity on delegated EOA path and adj…
snissn Oct 27, 2025
89cab4a
EIP-7702: accept ≤32-byte r/s (left-pad); clarify validation errors; …
snissn Oct 28, 2025
0d9ac1f
EIP-7702 tests: storage persistence across delegate changes; CBOR mal…
snissn Oct 28, 2025
348b9e4
Tests: add precise error message asserts and r-too-long case in inval…
snissn Oct 28, 2025
5d5f1c7
evm/tests: restore precompile tests removed in diff vs master
snissn Oct 28, 2025
4603245
evm: centralize Delegated(address) topic constant and refactor event …
snissn Oct 28, 2025
c186426
tests(evm): add eoa_call_pointer_semantics.rs (EXTCODESIZE/COPY point…
snissn Oct 28, 2025
544b94f
tests(evm): extend pointer semantics with EXTCODEHASH and NV sanity; …
snissn Oct 28, 2025
2a59c8f
EVM: make return-data decode strict (no fallback) and handle Bytecode…
snissn Oct 29, 2025
609cd88
eip7702: remove ApplyAndCall intrinsic gas charges and tests; drop rt…
snissn Oct 29, 2025
81ff133
fmt
snissn Oct 29, 2025
2436a09
EIP-7702: remove NV gating (bundle-based activation). Always consult …
snissn Oct 30, 2025
607312a
Remove unused NV_EIP_7702 constant (bundle-based activation; no runti…
snissn Oct 30, 2025
fd3a4a4
fmt: cargo fmt --all
snissn Oct 30, 2025
169c86f
Update actors/evm/src/interpreter/system.rs
snissn Oct 31, 2025
bf6e99b
Update actors/evm/src/lib.rs
snissn Oct 31, 2025
b8c8ced
Update actors/evm/src/lib.rs
snissn Oct 31, 2025
90da165
Update actors/evm/src/lib.rs
snissn Oct 31, 2025
772b804
Update actors/evm/src/interpreter/instructions/call.rs
snissn Oct 31, 2025
8e2b5cc
Delete runtime/src/features.rs
snissn Oct 31, 2025
b6303a1
EIP-7702: atomicity hardening + tests
snissn Oct 31, 2025
ce32656
Merge branch 'master' of github.com:filecoin-project/builtin-actors i…
snissn Oct 31, 2025
ba82411
cargo fmt --all
snissn Oct 31, 2025
ffb7c97
evm/tests: add global mapping proof test for EIP-7702; assert EXTCODE…
snissn Nov 7, 2025
b1caa37
EVM: minimalize CALL/STATICCALL to EOAs (no internal delegation); rel…
snissn Nov 8, 2025
97645fd
EVM: add InvokeAsEoaWithRoot trampoline for VM intercept; keep Invoke…
snissn Nov 8, 2025
59c7462
EthAccount: tidy clippy warnings in tests; align receiver f4 handling…
snissn Nov 8, 2025
b582b60
runtime: add get_eth_delegate_to to Runtime + FVM impl + test hooks; …
snissn Nov 8, 2025
1bfaf7b
eip7702: WIP minimalization plan – drop legacy EVM delegation entrypo…
snissn Nov 10, 2025
aac100a
ethaccount: add tuple cap boundary and duplicates tests; adjust value…
snissn Nov 10, 2025
9c1a111
evm: remove legacy InvokeAsEoa/ApplyAndCall paths (stub to illegal_st…
snissn Nov 10, 2025
25bf1f2
evm: prune legacy 7702 per-authority maps (delegations/nonces/storage…
snissn Nov 10, 2025
e453006
evm: decode robustness in CALLDATASIZE (avoid unwrap); no silent fall…
snissn Nov 10, 2025
123621e
evm/tests: drop legacy ApplyAndCall/InvokeAsEoa suites; rely on EthAc…
snissn Nov 10, 2025
cc1043d
evm: minimalize EOA CALL path; METHOD_SEND only; remove legacy delega…
snissn Nov 10, 2025
f335b4c
evm: decode robustness — no silent fallbacks; return illegal_state
snissn Nov 10, 2025
b3516a7
docs(agents): record 7702 minimalization status and quick validation …
snissn Nov 11, 2025
56bbbbe
ci: replace local ref-fvm path patches with git patches (fix cargo-de…
snissn Nov 12, 2025
75c392b
eip7702: remove stale EVM ApplyAndCall stubs + unused helper; clarify…
snissn Nov 12, 2025
810c755
cargo fmt
snissn Nov 12, 2025
2c13e56
docs(agents): align notes with EthAccount+VM intercept; fix design no…
snissn Nov 12, 2025
2a7fc08
feat(eip7702): enhance EthAccount ApplyAndCall and tests
snissn Nov 14, 2025
31b8da0
ethaccount: assert mapping in outer-call test
snissn Nov 20, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Agents Notes

This repository inherits its agent workflow, conventions, and project guidance from the Lotus workstream.

- For the canonical instructions, see `../lotus/AGENTS.md`.
- Treat that document as the source of truth for coding style, review etiquette, CI expectations, and how agents should coordinate across repos in this workstream.

Repo‑local quick notes:
- Tests: `make test` (uses cargo-nextest across the workspace).
- Lint: `make check` (clippy; warnings are errors).
- Formatting: `make rustfmt`.
- EIP‑7702 is always active in this bundle (no runtime NV gating).
- EIP‑7702 design notes live at `../eip7702.md`.

If there is any conflict between this file and `../lotus/AGENTS.md`, prefer the Lotus file.
4 changes: 3 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 5 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ toolchain:
rustup show active-toolchain || rustup toolchain install
.PHONY: toolchain

NEXTEST_VERSION ?= 0.9.106

install-nextest:
@command -v cargo-nextest >/dev/null 2>&1 || cargo install cargo-nextest --locked
@command -v cargo-nextest >/dev/null 2>&1 || cargo install cargo-nextest --version $(NEXTEST_VERSION) --locked
.PHONY: install-nextest

# Run cargo fmt
Expand All @@ -22,8 +24,8 @@ rustfmt: toolchain

# Run cargo check
check: toolchain
cargo clippy --all --all-targets -- -D warnings
cargo clippy --all -- -D warnings
SKIP_BUNDLE=1 cargo clippy --all --all-targets -- -D warnings
SKIP_BUNDLE=1 cargo clippy --all -- -D warnings

# NOTE: nextest doesn't run doctests https://github.com/nextest-rs/nextest/issues/16,
# enable once doc tests are added: `cargo test --doc`
Expand Down
2 changes: 2 additions & 0 deletions actors/evm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ fvm_ipld_encoding = { workspace = true }
multihash-codetable = { workspace = true }
frc42_dispatch = { workspace = true }
fil_actors_evm_shared = { workspace = true }
rlp = { workspace = true }
fvm_ipld_hamt = { workspace = true }
hex = { workspace = true }
hex-literal = { workspace = true }
substrate-bn = { workspace = true }
Expand Down
42 changes: 42 additions & 0 deletions actors/evm/shared/src/eip7702.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
use crate::address::EthAddress;

/// EIP-7702 bytecode magic prefix and version.
pub const EIP7702_MAGIC: [u8; 2] = [0xEF, 0x01];
pub const EIP7702_VERSION: u8 = 0x00;

/// Returns true if code is an EIP-7702 delegation indicator: 0xEF 0x01 0x00 || 20-byte address.
pub fn is_eip7702_code(code: &[u8]) -> bool {
code.len() == 23 && code[0..2] == EIP7702_MAGIC && code[2] == EIP7702_VERSION
}

/// Attempts to parse an EIP-7702 delegation indicator and return the embedded 20-byte address.
pub fn eip7702_delegate_address(code: &[u8]) -> Option<EthAddress> {
if !is_eip7702_code(code) {
return None;
}
let mut addr = [0u8; 20];
addr.copy_from_slice(&code[3..23]);
Some(EthAddress(addr))
}

#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detect_and_parse() {
let mut raw = vec![0u8; 23];
raw[0] = 0xEF;
raw[1] = 0x01;
raw[2] = 0x00;
for i in 0..20 {
raw[3 + i] = 0xAB;
}
assert!(is_eip7702_code(&raw));
let d = eip7702_delegate_address(&raw).unwrap();
assert_eq!(d, EthAddress([0xAB; 20]));
assert!(!is_eip7702_code(&raw[..10]));
let mut bad = raw.clone();
bad[1] = 0x00;
assert!(!is_eip7702_code(&bad));
}
}
1 change: 1 addition & 0 deletions actors/evm/shared/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
pub mod address;
pub mod eip7702;
pub mod uints;
171 changes: 171 additions & 0 deletions actors/evm/src/interpreter/instructions/call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use fil_actors_evm_shared::{address::EthAddress, uints::U256};
use fvm_ipld_encoding::BytesDe;
use fvm_ipld_encoding::ipld_block::IpldBlock;
use fvm_shared::{IPLD_RAW, MethodNum, address::Address, sys::SendFlags};
use num_traits::Zero;

use crate::interpreter::{
CallKind,
Expand Down Expand Up @@ -200,6 +201,171 @@ pub fn call_generic<RT: Runtime>(
// We provide enough gas for the transfer to succeed in all case.
gas = TRANSFER_GAS_LIMIT;
}
// EIP-7702: If destination is an EOA (Account/NotFound), consult internal mapping.
if matches!(
get_contract_type(system.rt, &dst),
ContractType::Account | ContractType::NotFound
)
// Depth limit: do not follow delegation when already in authority context.
&& !system.in_authority_context
{
// EVM-only: consult internal mapping for 7702 delegation
if let Some(delegate) = system.get_delegate(&dst) {
// Delegate is expected to be an EVM contract. Resolve its code.
match get_contract_type(system.rt, &delegate) {
ContractType::EVM(delegate_addr) => {
if let Some(code_cid) =
get_evm_bytecode_cid(system, &delegate_addr)?
{
// Transfer any value to the EOA account first (unless readonly/static).
if !system.readonly && kind != CallKind::StaticCall {
let v = TokenAmount::from(&value);
if !v.is_zero() {
if let Err(e) = system.transfer(&dst_addr, v) {
log::warn!(
"value transfer to EOA {:?} failed: {}",
dst_addr,
e
);
return Ok(U256::from(0));
}
}
}

// Execute delegate bytecode with receiver bound to the EOA.
let params = crate::EoaInvokeParams {
code: code_cid,
input: input_data.into(),
caller: state.caller,
receiver: dst,
value: TokenAmount::from(&value),
};
let res = system.send(
&system.rt.message().receiver(),
crate::Method::InvokeAsEoa as u64,
IpldBlock::serialize_dag_cbor(&params)?,
TokenAmount::zero(),
Some(system.call_gas_limit(gas)),
SendFlags::default(),
);
match res {
Ok(Some(r)) => {
// Decode InvokeContractReturn { output_data } from InvokeAsEoa
#[derive(fvm_ipld_encoding::serde::Deserialize)]
struct InvokeContractReturn {
output_data: Vec<u8>,
}
let data = r
.deserialize::<InvokeContractReturn>()
.map(|x| x.output_data)
.unwrap_or_else(|_| r.data);
state.return_data = data;
// Emit an ETH-style log indicating delegated execution occurred.
use fvm_ipld_encoding::IPLD_RAW;
use fvm_shared::crypto::hash::SupportedHashes;
use fvm_shared::event::{Entry, Flags};
let topic = system.rt.hash(
SupportedHashes::Keccak256,
crate::DELEGATED_EVENT_TOPIC,
);
if topic.len() == 32 && !system.readonly {
let entries: Vec<Entry> = vec![
Entry {
flags: Flags::FLAG_INDEXED_ALL,
key: "t1".to_owned(),
codec: IPLD_RAW,
value: topic,
},
Entry {
flags: Flags::FLAG_INDEXED_ALL,
key: "d".to_owned(),
codec: IPLD_RAW,
// Emit the authority (EOA) address, not the delegate.
value: dst.as_ref().to_vec(),
},
];
let _ = system.rt.emit_event(&entries.into());
}
copy_to_memory(
memory,
output_offset,
output_size,
U256::zero(),
&state.return_data,
false,
)?;
return Ok(U256::from(1));
}
Ok(None) => {
// No return.
state.return_data = Vec::new();
// Emit delegated execution event without return data.
use fvm_ipld_encoding::IPLD_RAW;
use fvm_shared::crypto::hash::SupportedHashes;
use fvm_shared::event::{Entry, Flags};
let topic = system.rt.hash(
SupportedHashes::Keccak256,
crate::DELEGATED_EVENT_TOPIC,
);
if topic.len() == 32 && !system.readonly {
let entries: Vec<Entry> = vec![
Entry {
flags: Flags::FLAG_INDEXED_ALL,
key: "t1".to_owned(),
codec: IPLD_RAW,
value: topic,
},
Entry {
flags: Flags::FLAG_INDEXED_ALL,
key: "d".to_owned(),
codec: IPLD_RAW,
// Emit the authority (EOA) address, not the delegate.
value: dst.as_ref().to_vec(),
},
];
let _ = system.rt.emit_event(&entries.into());
}
copy_to_memory(
memory,
output_offset,
output_size,
U256::zero(),
&state.return_data,
false,
)?;
return Ok(U256::from(1));
}
Err(mut ae) => {
// Reverted or error; map to 0 and propagate revert data to memory.
log::debug!("InvokeAsEoa failed (delegated CALL)");
state.return_data = ae
.take_data()
.map(|b| b.data)
.unwrap_or_default();
copy_to_memory(
memory,
output_offset,
output_size,
U256::zero(),
&state.return_data,
false,
)?;
return Ok(U256::from(0));
}
}
} else {
// No code at delegate; treat as success with no-op.
return Ok(U256::from(1));
}
}
_ => {
// Delegate not an EVM contract; treat as no-op success.
return Ok(U256::from(1));
}
}
}
// Fallthrough: no delegation mapping
}
let params = if input_data.is_empty() {
None
} else {
Expand Down Expand Up @@ -839,4 +1005,9 @@ mod tests {
assert_eq!(&m.state.memory[0..4], &output_data);
};
}

// Depth limit functional test is implemented in ApplyAndCall-driven tests.

// Note: Depth limit is enforced in code by System.in_authority_context.
// A dedicated integration test can be added when a stable harness for nested delegation flows is available.
}
41 changes: 39 additions & 2 deletions actors/evm/src/interpreter/instructions/ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ use cid::Cid;
use fil_actors_evm_shared::address::EthAddress;
use fil_actors_evm_shared::uints::U256;
use fil_actors_runtime::ActorError;
// Delegator removed; 7702 mapping is internal to EVM state.
use fil_actors_runtime::runtime::builtins::Type;
use fil_actors_runtime::{AsActorError, deserialize_block};
use fvm_ipld_blockstore::Blockstore;
use fvm_shared::crypto::hash::SupportedHashes;
use fvm_shared::error::ExitCode;
use fvm_shared::sys::SendFlags;
use fvm_shared::{address::Address, econ::TokenAmount};
Expand All @@ -30,6 +32,13 @@ pub fn extcodesize(
get_evm_bytecode(system, &addr).map(|bytecode| bytecode.len())?
}
ContractType::Native(_) => 1,
ContractType::Account => {
let authority = EthAddress::from(addr);
if system.get_delegate(&authority).is_some() {
return Ok(U256::from(23));
}
0
}
// account, not found, and precompiles are 0 size
_ => 0,
};
Expand All @@ -52,7 +61,23 @@ pub fn extcodehash(
// The FVM does not have chain state cleanup so contracts will never end up "empty" and be removed, they will either exist (in any state in the contract lifecycle)
// and return keccak(""), or not exist (where nothing has ever been deployed at that address) and return 0.
// TODO: With account abstraction, this may be something other than an empty hash!
ContractType::Account => return Ok(BytecodeHash::EMPTY.into()),
ContractType::Account => {
let authority = EthAddress::from(addr);
if let Some(d) = system.get_delegate(&authority) {
let mut bytecode = Vec::with_capacity(23);
bytecode.extend_from_slice(&fil_actors_evm_shared::eip7702::EIP7702_MAGIC);
bytecode.push(fil_actors_evm_shared::eip7702::EIP7702_VERSION);
bytecode.extend_from_slice(d.as_ref());
let hash_bytes = system.rt.hash(SupportedHashes::Keccak256, &bytecode);
let hash = BytecodeHash::try_from(hash_bytes.as_slice()).map_err(|_| {
ActorError::illegal_state(
"extcodehash: failed to convert keccak256 to BytecodeHash".into(),
)
})?;
return Ok(hash.into());
}
return Ok(BytecodeHash::EMPTY.into());
}
// Not found
ContractType::NotFound => return Ok(U256::zero()),
};
Expand All @@ -79,7 +104,19 @@ pub fn extcodecopy(
) -> Result<(), ActorError> {
let bytecode = match get_contract_type(system.rt, &addr.into()) {
ContractType::EVM(addr) => get_evm_bytecode(system, &addr)?,
ContractType::NotFound | ContractType::Account | ContractType::Precompile => Vec::new(),
ContractType::Account => {
let authority = EthAddress::from(addr);
if let Some(d) = system.get_delegate(&authority) {
let mut b = Vec::with_capacity(23);
b.extend_from_slice(&fil_actors_evm_shared::eip7702::EIP7702_MAGIC);
b.push(fil_actors_evm_shared::eip7702::EIP7702_VERSION);
b.extend_from_slice(d.as_ref());
b
} else {
Vec::new()
}
}
ContractType::NotFound | ContractType::Precompile => Vec::new(),
// calling EXTCODECOPY on native actors results with a single byte 0xFE which solidtiy uses for its `assert`/`throw` methods
// and in general invalid EVM bytecode
_ => vec![0xFE],
Expand Down
Loading