Skip to content
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
f8a091c
test: :white_check_mark: add happy path test
manuelmauro Jul 31, 2025
84e110d
test: :recycle: minimize happy path
manuelmauro Jul 31, 2025
2c7bc8d
feat: :arrow_up: upgrade evm crate
manuelmauro Jul 31, 2025
24d10b6
test: :white_check_mark: remove silly condition in test
manuelmauro Jul 31, 2025
dc55a6b
fix: :bug: modify the restriction put in place by EIP-3607
manuelmauro Jul 31, 2025
4327a16
refactor: :recycle: nit delegation indicator check
manuelmauro Aug 1, 2025
a5cd886
test: :white_check_mark: add EIP-7702 happy path unit test
manuelmauro Aug 1, 2025
480b9da
test: :white_check_mark: split delegation and call to delegated code
manuelmauro Aug 1, 2025
530019f
test: :white_check_mark: fix smart contract creation in test
manuelmauro Aug 1, 2025
0d9c9fb
fix: :white_check_mark: use simpler smart contract on delegation
manuelmauro Aug 1, 2025
c40e6e2
docs: :memo: document EIP-7702 behavior
manuelmauro Aug 1, 2025
39bc7ed
test: :white_check_mark: cleanup new unit test
manuelmauro Aug 1, 2025
7003602
style: :art: define constant EIP7702_DELEGATION_INDICATOR
manuelmauro Aug 1, 2025
a11d805
refactor: :rotating_light: clippy
manuelmauro Aug 4, 2025
f2f8857
refactor: :recycle: use ethers TS authorization utils
manuelmauro Aug 4, 2025
cfd2af0
test: :white_check_mark: fix failing test
manuelmauro Aug 4, 2025
26f8e7d
refactor: :art: use EIP7702_DELEGATION_INDICATOR constant
manuelmauro Aug 4, 2025
3b213e9
style: :art: fmt
manuelmauro Aug 4, 2025
5e9c66b
Merge branch 'master' into manuel/add-eip7702-testing
manuelmauro Aug 4, 2025
e749d09
test: :white_check_mark: properly test zero-address delegation
manuelmauro Aug 4, 2025
1a546c1
test: :white_check_mark: fix test expectations
manuelmauro Aug 4, 2025
eee1cd1
refactor: :recycle: use ethers.ZeroAddress constant
manuelmauro Aug 4, 2025
664fe9f
fix: :bug: fix bug preventing a delegation to the zero address to cle…
manuelmauro Aug 5, 2025
fc7adaf
refactor: :recycle: use EVM constants for EIP-7702
manuelmauro Aug 5, 2025
580c22f
revert: :fire: remove AI slop
manuelmauro Aug 5, 2025
4c890e7
test: :white_check_mark: add rust test for zero-address delegations
manuelmauro Aug 5, 2025
23da4aa
refactor: :recycle: use evm constants for EIP-7702 delegations
manuelmauro Aug 5, 2025
2108960
refactor: :recycle: add a dedicated function for code cleanup
manuelmauro Aug 6, 2025
084e5a1
chore: :see_no_evil: ignore .zed folder
manuelmauro Aug 6, 2025
5ff810a
perf: :zap: avoid code read by checking code metadata first
manuelmauro Aug 7, 2025
38d6e68
fix: :bug: fix delegation checks
manuelmauro Aug 7, 2025
ce3075a
feat: :arrow_up: upgrade evm crate
manuelmauro Aug 12, 2025
77a5d44
fix: :bug: implement set/reset_delegation methods
manuelmauro Aug 12, 2025
e1fd429
fix: :bug: fix import path for constant
manuelmauro Aug 12, 2025
532a550
refactor: :bug: do not rely on create_account when setting a delegation
manuelmauro Aug 12, 2025
5f9e289
fix: :bug: fully remove account creation on set_delegation
manuelmauro Aug 12, 2025
c1dcd9f
Merge branch 'master' into manuel/add-eip7702-testing
manuelmauro Aug 12, 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
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ derive_more = "1.0"
environmental = { version = "1.1.4", default-features = false }
ethereum = { version = "0.18.2", default-features = false }
ethereum-types = { version = "0.15", default-features = false }
evm = { version = "0.43.2", default-features = false }
evm = { version = "0.43.4", default-features = false }
futures = "0.3.31"
hash-db = { version = "0.16.0", default-features = false }
hex = { version = "0.4.3", default-features = false, features = ["alloc"] }
Expand Down
14 changes: 12 additions & 2 deletions frame/ethereum/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ use fp_storage::{EthereumStorageSchema, PALLET_ETHEREUM_SCHEMA};
use frame_support::traits::PalletInfoAccess;
use pallet_evm::{BlockHashMapping, FeeCalculator, GasWeightMapping, Runner};

const EIP7702_DELEGATION_INDICATOR: [u8; 3] = [0xef, 0x01, 0x00];

#[derive(Clone, Eq, PartialEq, RuntimeDebug)]
#[derive(Encode, Decode, DecodeWithMemTracking, MaxEncodedLen, TypeInfo)]
pub enum RawOrigin {
Expand Down Expand Up @@ -559,12 +561,20 @@ impl<T: Config> Pallet<T> {

// EIP-3607: https://eips.ethereum.org/EIPS/eip-3607
// Do not allow transactions for which `tx.sender` has any code deployed.
// Exception: Allow transactions from EOAs whose code is a valid delegation indicator (0xef0100 || address).
//
// This check should be done on the transaction validation (here) **and**
// on transaction execution, otherwise a contract tx will be included in
// the mempool and pollute the mempool forever.
if !pallet_evm::AccountCodes::<T>::get(origin).is_empty() {
return Err(InvalidTransaction::BadSigner.into());
let origin_code = pallet_evm::AccountCodes::<T>::get(origin);
if !origin_code.is_empty() {
// Check if code is a valid delegation indicator: 0xef0100 + 20-byte address
let is_delegation_indicator =
origin_code.len() == 23 && origin_code[0..3] == EIP7702_DELEGATION_INDICATOR;
Comment thread
RomarQ marked this conversation as resolved.
Outdated

if !is_delegation_indicator {
return Err(InvalidTransaction::BadSigner.into());
}
}

let priority = match (
Expand Down
216 changes: 216 additions & 0 deletions frame/ethereum/src/tests/eip7702.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,22 @@ use ethereum::{AuthorizationListItem, TransactionAction};
use pallet_evm::{config_preludes::ChainId, AddressMapping};
use sp_core::{H160, H256, U256};

// Ultra simple contract that just returns 42 for any call
// This is pure runtime bytecode that:
// 1. Pushes 42 (0x2a) onto the stack
// 2. Pushes 0 (memory offset) onto the stack
// 3. Stores 42 at memory offset 0 (MSTORE)
// 4. Pushes 32 (return data size) onto the stack
// 5. Pushes 0 (memory offset) onto the stack
// 6. Returns 32 bytes from memory offset 0 (RETURN)
const _SIMPLE_CONTRACT_RUNTIME: &str = "602a60005260206000f3";

// Creation bytecode that deploys the runtime bytecode above
// This pushes the runtime code to memory and returns it
const SIMPLE_CONTRACT_CREATION: &str = "69602a60005260206000f3600052600a6016f3";

const EIP7702_DELEGATION_INDICATOR: [u8; 3] = [0xef, 0x01, 0x00];
Comment thread
RomarQ marked this conversation as resolved.
Outdated

/// Helper function to create an EIP-7702 transaction for testing
fn eip7702_transaction_unsigned(
nonce: U256,
Expand Down Expand Up @@ -90,6 +106,206 @@ fn create_authorization_tuple(
}
}

#[test]
fn eip7702_happy_path() {
let (pairs, mut ext) = new_test_ext_with_initial_balance(2, 10_000_000_000_000);
let alice = &pairs[0];
let bob = &pairs[1];

ext.execute_with(|| {
// Deploy the simple contract using creation bytecode
let contract_creation_bytecode = hex::decode(SIMPLE_CONTRACT_CREATION).unwrap();

println!(
"Creation bytecode length: {}",
contract_creation_bytecode.len()
);

// Deploy contract using Alice's account
let deploy_tx = LegacyUnsignedTransaction {
nonce: U256::zero(),
gas_price: U256::from(1),
gas_limit: U256::from(0x100000),
action: TransactionAction::Create,
value: U256::zero(),
input: contract_creation_bytecode,
}
.sign(&alice.private_key);

let deploy_result = Ethereum::execute(alice.address, &deploy_tx, None);
assert_ok!(&deploy_result);

// Get the deployed contract address
let (_, _, deploy_info) = deploy_result.unwrap();

let CallOrCreateInfo::Create(info) = deploy_info else {
panic!("Expected Create info, got Call");
};

println!("Contract deployment exit reason: {:?}", info.exit_reason);
println!("Contract deployment return address: {:?}", info.value);
println!("Contract deployment used gas: {:?}", info.used_gas);
assert!(
info.exit_reason.is_succeed(),
"Contract deployment should succeed"
);

let contract_address = info.value;

// Verify contract was deployed correctly
let contract_code = pallet_evm::AccountCodes::<Test>::get(contract_address);
assert!(
!contract_code.is_empty(),
"Contract should be deployed with non-empty code"
);

// The nonce = 2 accounts for the increment of Alice's nonce due to contract deployment + EIP-7702 transaction
let authorization =
create_authorization_tuple(ChainId::get(), contract_address, 2, &alice.private_key);

let transaction = eip7702_transaction_unsigned(
U256::from(1), // nonce 1 (after contract deployment)
U256::from(0x100000),
TransactionAction::Call(bob.address),
U256::from(1000),
vec![],
vec![authorization],
)
.sign(&alice.private_key, Some(ChainId::get()));

// Store initial balances
let substrate_alice =
<Test as pallet_evm::Config>::AddressMapping::into_account_id(alice.address);
let substrate_bob =
<Test as pallet_evm::Config>::AddressMapping::into_account_id(bob.address);
let initial_alice_balance = Balances::free_balance(&substrate_alice);
let initial_bob_balance = Balances::free_balance(&substrate_bob);

// Execute the transaction
let result = Ethereum::execute(alice.address, &transaction, None);
assert_ok!(&result);

// Check that the delegation code was set as AccountCodes
let alice_code = pallet_evm::AccountCodes::<Test>::get(alice.address);

// According to EIP-7702, after processing an authorization, the authorizing account
// should have code set to 0xef0100 || address (delegation designator)
assert!(
!alice_code.is_empty(),
"Alice's account should have delegation code after EIP-7702 authorization"
);

assert_eq!(
alice_code.len(),
23,
"Delegation code should be exactly 23 bytes (0xef0100 + 20 byte address)"
);

assert_eq!(
alice_code[0..3],
EIP7702_DELEGATION_INDICATOR,
"Delegation code should start with 0xef0100"
);

// Extract and verify the delegated address
let delegated_address: H160 = H160::from_slice(&alice_code[3..23]);
assert_eq!(
delegated_address, contract_address,
"Alice's account should delegate to the authorized contract address"
);

// Verify the value transfer still occurred
let final_alice_balance = Balances::free_balance(&substrate_alice);
let final_bob_balance = Balances::free_balance(&substrate_bob);

assert!(
final_alice_balance < initial_alice_balance,
"Alice's balance should decrease after transaction"
);

assert_eq!(
final_bob_balance,
initial_bob_balance + 1000u64,
"Bob should receive the transaction value"
);

// Test that the contract can be called directly (to verify it works)
// This simple contract returns 42 for any call (no function selector needed)
let direct_call_tx = LegacyUnsignedTransaction {
nonce: U256::from(2), // nonce 2 for Alice (after contract deployment + EIP-7702 transaction)
gas_price: U256::from(1),
gas_limit: U256::from(0x100000),
action: TransactionAction::Call(contract_address), // Call contract directly
value: U256::zero(),
input: vec![], // No input needed - any call returns 42
}
.sign(&alice.private_key);

let direct_call_result = Ethereum::execute(alice.address, &direct_call_tx, None);
assert_ok!(&direct_call_result);

let (_, _, direct_call_info) = direct_call_result.unwrap();

let CallOrCreateInfo::Call(info) = direct_call_info else {
panic!("Expected Call info, got Create");
};
println!("Direct call exit reason: {:?}", info.exit_reason);
println!("Direct call return value: {:?}", info.value);

// Debug: Check what code Alice actually has
let alice_code_after = pallet_evm::AccountCodes::<Test>::get(alice.address);
println!("Alice's code after EIP-7702: {:?}", alice_code_after);
println!("Contract address: {:?}", contract_address);

// Check what code the contract actually has
let contract_code_final = pallet_evm::AccountCodes::<Test>::get(contract_address);
println!("Contract code length: {}", contract_code_final.len());
if contract_code_final.len() > 10 {
println!(
"Contract code first 10 bytes: {:?}",
&contract_code_final[0..10]
);
}

// Try calling Alice's address instead of the contract directly
// This should delegate to the contract if EIP-7702 is working
let delegate_call_tx = LegacyUnsignedTransaction {
nonce: U256::from(3), // nonce 3 for Alice
gas_price: U256::from(1),
gas_limit: U256::from(0x100000),
action: TransactionAction::Call(alice.address), // Call Alice's delegated address
value: U256::zero(),
input: vec![], // No input needed - any call returns 42
}
.sign(&alice.private_key);

let delegate_call_result = Ethereum::execute(alice.address, &delegate_call_tx, None);
println!("Delegate call result: {:?}", delegate_call_result);

if let Ok((_, _, CallOrCreateInfo::Call(delegate_info))) = delegate_call_result {
println!("Delegate call exit reason: {:?}", delegate_info.exit_reason);
println!("Delegate call return value: {:?}", delegate_info.value);
}

// Verify the contract returns 42
let expected_result = {
let mut result = vec![0u8; 32];
result[31] = 42;
result
};

if info.exit_reason.is_succeed() {
assert_eq!(
info.value, expected_result,
"Direct call to contract should return 42"
);
println!("✓ Direct contract call succeeded!");
} else {
println!("✗ Direct contract call failed: {:?}", info.exit_reason);
}
});
}

#[test]
fn valid_eip7702_transaction_structure() {
let (pairs, mut ext) = new_test_ext_with_initial_balance(2, 10_000_000_000_000);
Expand Down
38 changes: 32 additions & 6 deletions frame/evm/src/runner/stack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ use crate::{
Error, Event, FeeCalculator, OnChargeEVMTransaction, OnCreate, Pallet, RunnerError,
};

const EIP7702_DELEGATION_INDICATOR: [u8; 3] = [0xef, 0x01, 0x00];

#[cfg(feature = "forbid-evm-reentrancy")]
environmental::environmental!(IN_EVM: bool);

Expand Down Expand Up @@ -221,11 +223,21 @@ where
//
// EIP-3607: https://eips.ethereum.org/EIPS/eip-3607
// Do not allow transactions for which `tx.sender` has any code deployed.
if is_transactional && !<AccountCodes<T>>::get(source).is_empty() {
return Err(RunnerError {
error: Error::<T>::TransactionMustComeFromEOA,
weight,
});
// Exception: Allow transactions from EOAs whose code is a valid delegation indicator (0xef0100 || address).
if is_transactional {
let source_code = <AccountCodes<T>>::get(source);
if !source_code.is_empty() {
// Check if code is a valid delegation indicator: 0xef0100 + 20-byte address
let is_delegation_indicator =
source_code.len() == 23 && source_code[0..3] == EIP7702_DELEGATION_INDICATOR;
Comment thread
RomarQ marked this conversation as resolved.
Outdated

if !is_delegation_indicator {
return Err(RunnerError {
error: Error::<T>::TransactionMustComeFromEOA,
weight,
});
}
}
}

let total_fee_per_gas = if is_transactional {
Expand Down Expand Up @@ -558,6 +570,16 @@ where
)?;
}

// Check if the transaction destination has a delegation indicator and add the delegation target to accessed_addresses
let mut updated_access_list = access_list;
let target_code = <AccountCodes<T>>::get(target);
Comment thread
RomarQ marked this conversation as resolved.
Outdated
if target_code.len() == 23 && target_code[0..3] == EIP7702_DELEGATION_INDICATOR {
Comment thread
RomarQ marked this conversation as resolved.
Outdated
// Extract delegation target address from bytes 3-22 (20 bytes)
let delegation_target = H160::from_slice(&target_code[3..23]);
// Add delegation target to access list (without any storage keys)
updated_access_list.push((delegation_target, Vec::new()));
}

let precompiles = T::PrecompilesValue::get();
Self::execute(
source,
Expand All @@ -578,7 +600,7 @@ where
value,
input,
gas_limit,
access_list,
updated_access_list,
authorization_list,
)
},
Expand Down Expand Up @@ -1215,10 +1237,14 @@ where
}

fn code_size(&self, address: H160) -> U256 {
// EIP-7702: EXTCODESIZE does NOT follow delegations
// Return the actual code size at the address, including delegation designators
U256::from(<Pallet<T>>::account_code_metadata(address).size)
}

fn code_hash(&self, address: H160) -> H256 {
// EIP-7702: EXTCODEHASH does NOT follow delegations
// Return the hash of the actual code at the address, including delegation designators
<Pallet<T>>::account_code_metadata(address).hash
}

Expand Down
Loading
Loading