Skip to content

Add support for EIP-7702 #323

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 30 commits into
base: v0.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
ee9f04b
feat: :sparkles: add pectra config
manuelmauro Jun 17, 2025
dbeacfe
feat: :arrow_up: upgrade ethereum crate
manuelmauro Jun 17, 2025
fa1de78
feat: :sparkles: add Authorization type
manuelmauro Jun 18, 2025
5a97f3d
feat: :sparkles: add EIP-7702 delegation logic to EVM
manuelmauro Jun 18, 2025
b9d4c71
feat: :sparkles: add EIP-7702 initialization and gas costs
manuelmauro Jun 18, 2025
53b7fb7
refactor: :recycle: lint and fmt
manuelmauro Jun 18, 2025
0dbf3eb
style: :art: fmt
manuelmauro Jun 18, 2025
b4438a6
feat: :sparkles: upgrade ethereum
manuelmauro Jun 20, 2025
96a0e35
fix: :arrow_up: upgrade rust toolchain
manuelmauro Jun 20, 2025
a8177af
refactor: :fire: remove Authorization type from evm_core
manuelmauro Jun 20, 2025
78ef1ff
fix: :bug: fix CODESIZE and CODECOPY behavior according to EIP-7702
manuelmauro Jun 23, 2025
55aff5a
test: :white_check_mark: test opcodes impacted by EIP-7702
manuelmauro Jun 23, 2025
a9de67b
test: :white_check_mark: merge test files
manuelmauro Jun 23, 2025
03b1bfa
perf: :zap: avoid fetching code unnecessarily
manuelmauro Jun 23, 2025
c4c355e
refactor: :arrow_up: upgrade ethereum
manuelmauro Jun 23, 2025
2564062
test: :white_check_mark: test delegation chains
manuelmauro Jun 23, 2025
3134553
fix: :bug: zero address delegation clears code
manuelmauro Jun 24, 2025
3a3bc92
refactor: :lock: do not silence exit errors
manuelmauro Jun 24, 2025
f5cc851
test: :white_check_mark: add comprehensive test suite for EIP-7702
manuelmauro Jun 24, 2025
cc32e59
test: :white_check_mark: update assumptions on multiple autorizations…
manuelmauro Jun 24, 2025
ad1acd1
fix: :bug: add authority to accessed_addresses
manuelmauro Jun 24, 2025
992e13d
test: :white_check_mark: correct gas expectations in test
manuelmauro Jun 24, 2025
940502a
test: :white_check_mark: correct gas calculations in test
manuelmauro Jun 24, 2025
738187f
refactor: :rotating_light: lint
manuelmauro Jun 24, 2025
2acd3a0
fix: :bug: verify the code of authority is empty or already delegated
manuelmauro Jun 24, 2025
8660f5e
fix: :bug: fix nonce edge case
manuelmauro Jun 24, 2025
7f643d4
test: :white_check_mark: fix assertion in test
manuelmauro Jun 24, 2025
8041848
test: :white_check_mark: fix gas exhaustion test
manuelmauro Jun 25, 2025
842b762
test: :white_check_mark: test CODESIZE and CODECOPY ops
manuelmauro Jun 25, 2025
fdc3e1c
docs: :fire: remove comments
manuelmauro Jun 25, 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
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ edition = "2018"

[dependencies]
auto_impl = "1.0"
ethereum = { git = "https://github.com/rust-ethereum/ethereum.git", rev = "bbb544622208ef6e9890a2dbc224248f6dd13318", default-features = false }
ethereum = { version = "0.16.0", default-features = false }
log = { version = "0.4", default-features = false }
primitive-types = { version = "0.13", default-features = false, features = ["rlp"] }
rlp = { version = "0.6", default-features = false }
Expand Down Expand Up @@ -81,4 +81,4 @@ members = [
"gasometer",
"runtime",
"fuzzer",
]
]
3 changes: 2 additions & 1 deletion benches/loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ fn run_loop_contract() {
.unwrap(),
// hex::decode("0f14a4060000000000000000000000000000000000000000000000000000000000002ee0").unwrap(),
u64::MAX,
Vec::new(),
Vec::new(), // access_list
Vec::new(), // authorization_list
);
}

Expand Down
2 changes: 2 additions & 0 deletions core/src/external.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ pub enum ExternalOperation {
IsEmpty,
/// Writing to storage (Number of bytes written).
Write(U256),
/// Resolving EIP-7702 delegation (target address).
DelegationResolution(H160),
}
63 changes: 62 additions & 1 deletion core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,13 @@ use crate::eval::{eval, Control};
use alloc::rc::Rc;
use alloc::vec::Vec;
use core::ops::Range;
use primitive_types::U256;
use primitive_types::{H160, U256};

/// EIP-7702 delegation designator prefix
pub const EIP_7702_DELEGATION_PREFIX: &[u8] = &[0xef, 0x01, 0x00];

/// EIP-7702 delegation designator full length (prefix + address)
pub const EIP_7702_DELEGATION_SIZE: usize = 23; // 3 bytes prefix + 20 bytes address

/// Core execution layer for EVM.
pub struct Machine {
Expand Down Expand Up @@ -178,3 +184,58 @@ impl Machine {
}
}
}

/// Check if code is an EIP-7702 delegation designator
pub fn is_delegation_designator(code: &[u8]) -> bool {
code.len() == EIP_7702_DELEGATION_SIZE && code.starts_with(EIP_7702_DELEGATION_PREFIX)
}

/// Extract the delegated address from EIP-7702 delegation designator
pub fn extract_delegation_address(code: &[u8]) -> Option<H160> {
if is_delegation_designator(code) {
let mut address_bytes = [0u8; 20];
address_bytes.copy_from_slice(&code[3..23]);
Some(H160::from(address_bytes))
} else {
None
}
}

/// Create EIP-7702 delegation designator
pub fn create_delegation_designator(address: H160) -> Vec<u8> {
let mut designator = Vec::with_capacity(EIP_7702_DELEGATION_SIZE);
designator.extend_from_slice(EIP_7702_DELEGATION_PREFIX);
designator.extend_from_slice(address.as_bytes());
designator
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_delegation_designator_creation() {
let address = H160::from_slice(&[1u8; 20]);
let designator = create_delegation_designator(address);

assert_eq!(designator.len(), EIP_7702_DELEGATION_SIZE);
assert_eq!(&designator[0..3], EIP_7702_DELEGATION_PREFIX);
assert_eq!(&designator[3..23], address.as_bytes());
}

#[test]
fn test_delegation_designator_detection() {
let address = H160::from_slice(&[1u8; 20]);
let designator = create_delegation_designator(address);

assert!(is_delegation_designator(&designator));
assert_eq!(extract_delegation_address(&designator), Some(address));
}

#[test]
fn test_non_delegation_code() {
let regular_code = vec![0x60, 0x00]; // PUSH1 0
assert!(!is_delegation_designator(&regular_code));
assert_eq!(extract_delegation_address(&regular_code), None);
}
}
42 changes: 34 additions & 8 deletions gasometer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -247,23 +247,26 @@ impl<'config> Gasometer<'config> {
non_zero_data_len,
access_list_address_len,
access_list_storage_len,
authorization_list_len,
} => {
#[deny(clippy::let_and_return)]
let cost = self.config.gas_transaction_call
+ zero_data_len as u64 * self.config.gas_transaction_zero_data
+ non_zero_data_len as u64 * self.config.gas_transaction_non_zero_data
+ access_list_address_len as u64 * self.config.gas_access_list_address
+ access_list_storage_len as u64 * self.config.gas_access_list_storage_key;
+ access_list_storage_len as u64 * self.config.gas_access_list_storage_key
+ authorization_list_len as u64 * self.config.gas_per_empty_account_cost;

log_gas!(
self,
"Record Call {} [gas_transaction_call: {}, zero_data_len: {}, non_zero_data_len: {}, access_list_address_len: {}, access_list_storage_len: {}]",
"Record Call {} [gas_transaction_call: {}, zero_data_len: {}, non_zero_data_len: {}, access_list_address_len: {}, access_list_storage_len: {}, authorization_list_len: {}]",
cost,
self.config.gas_transaction_call,
zero_data_len,
non_zero_data_len,
access_list_address_len,
access_list_storage_len
access_list_storage_len,
authorization_list_len
);

cost
Expand All @@ -274,26 +277,29 @@ impl<'config> Gasometer<'config> {
access_list_address_len,
access_list_storage_len,
initcode_cost,
authorization_list_len,
} => {
let mut cost = self.config.gas_transaction_create
+ zero_data_len as u64 * self.config.gas_transaction_zero_data
+ non_zero_data_len as u64 * self.config.gas_transaction_non_zero_data
+ access_list_address_len as u64 * self.config.gas_access_list_address
+ access_list_storage_len as u64 * self.config.gas_access_list_storage_key;
+ access_list_storage_len as u64 * self.config.gas_access_list_storage_key
+ authorization_list_len as u64 * self.config.gas_per_empty_account_cost;
if self.config.max_initcode_size.is_some() {
cost += initcode_cost;
}

log_gas!(
self,
"Record Create {} [gas_transaction_create: {}, zero_data_len: {}, non_zero_data_len: {}, access_list_address_len: {}, access_list_storage_len: {}, initcode_cost: {}]",
"Record Create {} [gas_transaction_create: {}, zero_data_len: {}, non_zero_data_len: {}, access_list_address_len: {}, access_list_storage_len: {}, initcode_cost: {}, authorization_list_len: {}]",
cost,
self.config.gas_transaction_create,
zero_data_len,
non_zero_data_len,
access_list_address_len,
access_list_storage_len,
initcode_cost
initcode_cost,
authorization_list_len
);
cost
}
Expand Down Expand Up @@ -326,25 +332,40 @@ impl<'config> Gasometer<'config> {

/// Calculate the call transaction cost.
#[allow(clippy::naive_bytecount)]
pub fn call_transaction_cost(data: &[u8], access_list: &[(H160, Vec<H256>)]) -> TransactionCost {
pub fn call_transaction_cost(
data: &[u8],
access_list: &[(H160, Vec<H256>)],
authorization_list: &[(U256, H160, U256, H160)],
) -> TransactionCost {
let zero_data_len = data.iter().filter(|v| **v == 0).count();
let non_zero_data_len = data.len() - zero_data_len;
let (access_list_address_len, access_list_storage_len) = count_access_list(access_list);
// Per EIP-7702: Initially charge PER_EMPTY_ACCOUNT_COST for all authorizations
// Non-empty accounts will be refunded later when we have access to account state
let authorization_list_len = authorization_list.len();

TransactionCost::Call {
zero_data_len,
non_zero_data_len,
access_list_address_len,
access_list_storage_len,
authorization_list_len,
}
}

/// Calculate the create transaction cost.
#[allow(clippy::naive_bytecount)]
pub fn create_transaction_cost(data: &[u8], access_list: &[(H160, Vec<H256>)]) -> TransactionCost {
pub fn create_transaction_cost(
data: &[u8],
access_list: &[(H160, Vec<H256>)],
authorization_list: &[(U256, H160, U256, H160)],
) -> TransactionCost {
let zero_data_len = data.iter().filter(|v| **v == 0).count();
let non_zero_data_len = data.len() - zero_data_len;
let (access_list_address_len, access_list_storage_len) = count_access_list(access_list);
// Per EIP-7702: Initially charge PER_EMPTY_ACCOUNT_COST for all authorizations
// Non-empty accounts will be refunded later when we have access to account state
let authorization_list_len = authorization_list.len();
let initcode_cost = init_code_cost(data);

TransactionCost::Create {
Expand All @@ -353,6 +374,7 @@ pub fn create_transaction_cost(data: &[u8], access_list: &[(H160, Vec<H256>)]) -
access_list_address_len,
access_list_storage_len,
initcode_cost,
authorization_list_len,
}
}

Expand Down Expand Up @@ -1110,6 +1132,8 @@ pub enum TransactionCost {
access_list_address_len: usize,
/// Total number of storage keys in transaction access list (see EIP-2930)
access_list_storage_len: usize,
/// Number of authorization tuples in transaction (see EIP-7702)
authorization_list_len: usize,
},
/// Create transaction cost.
Create {
Expand All @@ -1123,6 +1147,8 @@ pub enum TransactionCost {
access_list_storage_len: usize,
/// Cost of initcode = 2 * ceil(len(initcode) / 32) (see EIP-3860)
initcode_cost: u64,
/// Number of authorization tuples in transaction (see EIP-7702)
authorization_list_len: usize,
},
}

Expand Down
3 changes: 3 additions & 0 deletions runtime/src/eval/system.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ pub fn extcodesize<H: Handler>(runtime: &mut Runtime, handler: &mut H) -> Contro
{
return Control::Exit(e.into());
}
// EIP-7702: EXTCODESIZE does NOT follow delegations
let code_size = handler.code_size(address.into());
push_u256!(runtime, code_size);

Expand All @@ -107,6 +108,7 @@ pub fn extcodehash<H: Handler>(runtime: &mut Runtime, handler: &mut H) -> Contro
{
return Control::Exit(e.into());
}
// EIP-7702: EXTCODEHASH does NOT follow delegations
let code_hash = handler.code_hash(address.into());
push!(runtime, code_hash);

Expand All @@ -127,6 +129,7 @@ pub fn extcodecopy<H: Handler>(runtime: &mut Runtime, handler: &mut H) -> Contro
{
return Control::Exit(e.into());
}
// EIP-7702: EXTCODECOPY does NOT follow delegations
let code = handler.code(address.into());
match runtime
.machine
Expand Down
6 changes: 6 additions & 0 deletions runtime/src/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ pub trait Handler {
fn code_hash(&self, address: H160) -> H256;
/// Get code of address.
fn code(&self, address: H160) -> Vec<u8>;
/// Get code of address, following EIP-7702 delegations if enabled.
fn delegated_code(&self, address: H160) -> Option<Vec<u8>> {
let code = self.code(address);
evm_core::extract_delegation_address(&code)
.map(|delegated_address| self.code(delegated_address))
}
/// Get storage value of address at index.
fn storage(&self, address: H160, index: H256) -> H256;
/// Get transient storage value of address at index.
Expand Down
Loading