From db3db31cd344c3625768da63b32a5d54035d952f Mon Sep 17 00:00:00 2001 From: Ana Perez Ghiglia Date: Thu, 22 Oct 2026 19:23:00 +0000 Subject: [PATCH 1/3] Update EIP-2780 tests and support claim --- .changeset/eip2780-create-value-charge.md | 5 + .../edr_provider/tests/integration/eip2780.rs | 122 +++++++++++++++--- 2 files changed, 108 insertions(+), 19 deletions(-) create mode 100644 .changeset/eip2780-create-value-charge.md diff --git a/.changeset/eip2780-create-value-charge.md b/.changeset/eip2780-create-value-charge.md new file mode 100644 index 0000000000..c58a71dc4d --- /dev/null +++ b/.changeset/eip2780-create-value-charge.md @@ -0,0 +1,5 @@ +--- +"@nomicfoundation/edr": patch +--- + +Updated the experimental EIP-2780 support: from the Amsterdam hardfork, contract-creation transactions no longer pay a value charge. diff --git a/crates/edr_provider/tests/integration/eip2780.rs b/crates/edr_provider/tests/integration/eip2780.rs index 0b143b7d44..49e0f7e001 100644 --- a/crates/edr_provider/tests/integration/eip2780.rs +++ b/crates/edr_provider/tests/integration/eip2780.rs @@ -35,12 +35,25 @@ const CONTRACT_ADDRESS: Address = address!("0x0000000000000000000000000000000000 /// Flat intrinsic gas of a transaction before Amsterdam. const PRE_AMSTERDAM_INTRINSIC_GAS: u64 = 21_000; +/// Flat intrinsic gas of a contract creation before Amsterdam: the +/// transaction's plus the creation's. +const PRE_AMSTERDAM_CREATE_INTRINSIC_GAS: u64 = PRE_AMSTERDAM_INTRINSIC_GAS + 32_000; +/// One zero calldata byte (`4`) plus one EIP-3860 init code word (`2`) of +/// [`empty_contract_init_code`]. +const INIT_CODE_INTRINSIC_GAS: u64 = 4 + 2; /// EIP-8037: state gas of a new account. const NEW_ACCOUNT_STATE_GAS: u64 = NEW_ACCOUNT_BYTES * CPSB_GLAMSTERDAM; +/// A single `STOP`, which returns no runtime code. +fn empty_contract_init_code() -> Bytes { + let mut code = BytecodeBuilder::default(); + code.opcode(opcode::STOP); + code.runtime() +} + /// A transfer of `value` from [`SENDER`] to `to`. -fn transfer(to: Address, value: U256) -> TransactionRequest { +fn transfer_request(to: Address, value: U256) -> TransactionRequest { TransactionRequest { from: SENDER, to: Some(to), @@ -49,6 +62,16 @@ fn transfer(to: Address, value: U256) -> TransactionRequest { } } +/// A contract creation by [`SENDER`] sending `value` to the new contract. +fn create_request(value: U256) -> TransactionRequest { + TransactionRequest { + from: SENDER, + data: Some(empty_contract_init_code()), + value: Some(value), + ..TransactionRequest::default() + } +} + /// Sends the transaction and returns the `gasUsed` of its receipt. fn gas_used_by_sending( provider: &Provider, @@ -61,7 +84,7 @@ fn gas_used_by_sending( #[tokio::test(flavor = "multi_thread")] async fn self_transfer_pays_only_the_base_cost_from_amsterdam() -> anyhow::Result<()> { - let request = transfer(SENDER, U256::from(1)); + let request = transfer_request(SENDER, U256::from(1)); let osaka_provider = new_provider(edr_chain_l1::Hardfork::Osaka)?; assert_eq!( @@ -82,7 +105,7 @@ async fn self_transfer_pays_only_the_base_cost_from_amsterdam() -> anyhow::Resul #[tokio::test(flavor = "multi_thread")] async fn zero_value_call_pays_base_cost_and_recipient_access_from_amsterdam() -> anyhow::Result<()> { - let request = transfer(RECIPIENT, U256::ZERO); + let request = transfer_request(RECIPIENT, U256::ZERO); let osaka_provider = new_provider(edr_chain_l1::Hardfork::Osaka)?; assert_eq!( @@ -102,7 +125,7 @@ async fn zero_value_call_pays_base_cost_and_recipient_access_from_amsterdam() -> #[tokio::test(flavor = "multi_thread")] async fn value_transfer_pays_base_cost_recipient_access_and_value_charge_from_amsterdam( ) -> anyhow::Result<()> { - let request = transfer(RECIPIENT, U256::from(1)); + let request = transfer_request(RECIPIENT, U256::from(1)); let osaka_provider = new_provider(edr_chain_l1::Hardfork::Osaka)?; // The first transfer creates the recipient; pre-Amsterdam that costs nothing @@ -123,8 +146,7 @@ async fn value_transfer_pays_base_cost_recipient_access_and_value_charge_from_am gas_used_by_sending(&amsterdam_provider, request.clone())?, TX_BASE_COST + COLD_ACCOUNT_ACCESS + TX_VALUE_COST + NEW_ACCOUNT_STATE_GAS ); - // To an existing account, the decomposed charges add up to the flat - // pre-Amsterdam cost. + // To an existing account, only the decomposed charges are paid. assert_eq!( gas_used_by_sending(&amsterdam_provider, request)?, TX_BASE_COST + COLD_ACCOUNT_ACCESS + TX_VALUE_COST @@ -133,6 +155,39 @@ async fn value_transfer_pays_base_cost_recipient_access_and_value_charge_from_am Ok(()) } +// The decomposed charges of a value transfer add up to the flat cost they +// replace. +#[test] +fn value_transfer_charges_add_up_to_the_flat_pre_amsterdam_cost() { + assert_eq!( + TX_BASE_COST + COLD_ACCOUNT_ACCESS + TX_VALUE_COST, + PRE_AMSTERDAM_INTRINSIC_GAS + ); +} + +// Unlike a call, a creation with value pays no value charge: the sum has no +// `TX_VALUE_COST` term. +#[tokio::test(flavor = "multi_thread")] +async fn create_with_value_pays_base_cost_and_creation_access_from_amsterdam() -> anyhow::Result<()> +{ + let request = create_request(U256::from(1)); + + let osaka_provider = new_provider(edr_chain_l1::Hardfork::Osaka)?; + assert_eq!( + gas_used_by_sending(&osaka_provider, request.clone())?, + PRE_AMSTERDAM_CREATE_INTRINSIC_GAS + INIT_CODE_INTRINSIC_GAS + ); + + let amsterdam_provider = new_provider(edr_chain_l1::Hardfork::Amsterdam)?; + // The new contract's EIP-8037 state gas is charged on top. + assert_eq!( + gas_used_by_sending(&amsterdam_provider, request)?, + TX_BASE_COST + CREATE_ACCESS + INIT_CODE_INTRINSIC_GAS + NEW_ACCOUNT_STATE_GAS + ); + + Ok(()) +} + // The lower intrinsic cost makes a cheaper transaction valid: funded with // exactly the intrinsic gas, it is included, and its execution halts out of // gas as soon as the code runs. @@ -231,26 +286,57 @@ async fn transaction_funding_only_the_intrinsic_gas_is_included_and_halts_from_a Ok(()) } +// Amsterdam does not change the estimate of a value transfer, except for the +// recipient's state gas while the transfer creates it. +#[tokio::test(flavor = "multi_thread")] +async fn estimate_gas_of_a_value_transfer_keeps_the_flat_cost_from_amsterdam() -> anyhow::Result<()> +{ + let request = L1CallRequest { + from: Some(SENDER), + to: Some(RECIPIENT), + value: Some(U256::from(1)), + ..L1CallRequest::default() + }; + + let osaka_provider = new_provider(edr_chain_l1::Hardfork::Osaka)?; + // `eth_estimateGas` never returns less than the intrinsic cost plus one. + assert_eq!( + estimate_gas(&osaka_provider, request.clone()), + PRE_AMSTERDAM_INTRINSIC_GAS + 1 + ); + + let amsterdam_provider = new_provider(edr_chain_l1::Hardfork::Amsterdam)?; + // Creating the recipient costs state gas, which lifts the estimate above + // that minimum. + assert_eq!( + estimate_gas(&amsterdam_provider, request.clone()), + PRE_AMSTERDAM_INTRINSIC_GAS + NEW_ACCOUNT_STATE_GAS + ); + + // Once the recipient exists, only the intrinsic gas remains. + send_transaction( + &amsterdam_provider, + transfer_request(RECIPIENT, U256::from(1)), + )?; + assert_eq!( + estimate_gas(&amsterdam_provider, request), + PRE_AMSTERDAM_INTRINSIC_GAS + 1 + ); + + Ok(()) +} + // The estimate must clear the Amsterdam create intrinsic cost, not the flat // pre-Amsterdam one, and fund the state gas of the new contract account. #[tokio::test(flavor = "multi_thread")] async fn estimate_gas_covers_a_contract_creation_from_amsterdam() -> anyhow::Result<()> { - /// `STOP`: deploys an empty contract. - fn init_code() -> Bytes { - let mut code = BytecodeBuilder::default(); - code.opcode(opcode::STOP); - code.runtime() - } - /// One zero calldata byte (`4`) plus one EIP-3860 init code word (`2`). - const INIT_CODE_INTRINSIC_GAS: u64 = 4 + 2; - let amsterdam_provider = new_provider(edr_chain_l1::Hardfork::Amsterdam)?; let estimate = estimate_gas( &amsterdam_provider, L1CallRequest { from: Some(SENDER), - data: Some(init_code()), + data: Some(empty_contract_init_code()), ..L1CallRequest::default() }, ); @@ -263,10 +349,8 @@ async fn estimate_gas_covers_a_contract_creation_from_amsterdam() -> anyhow::Res let transaction_hash = send_transaction( &amsterdam_provider, TransactionRequest { - from: SENDER, - data: Some(init_code()), gas: Some(estimate), - ..TransactionRequest::default() + ..create_request(U256::ZERO) }, )?; let receipt = transaction_receipt(&amsterdam_provider, transaction_hash)?; From d6a75739fbe3e70035e120a156c43c9b9da9b4ae Mon Sep 17 00:00:00 2001 From: Ana Perez Ghiglia Date: Mon, 26 Oct 2026 15:50:42 +0000 Subject: [PATCH 2/3] Apply review feedback --- .changeset/eip2780-create-value-charge.md | 2 +- .../edr_provider/tests/integration/eip2780.rs | 76 ++++++++++++------- 2 files changed, 51 insertions(+), 27 deletions(-) diff --git a/.changeset/eip2780-create-value-charge.md b/.changeset/eip2780-create-value-charge.md index c58a71dc4d..3d077b578d 100644 --- a/.changeset/eip2780-create-value-charge.md +++ b/.changeset/eip2780-create-value-charge.md @@ -2,4 +2,4 @@ "@nomicfoundation/edr": patch --- -Updated the experimental EIP-2780 support: from the Amsterdam hardfork, contract-creation transactions no longer pay a value charge. +Changed the experimental EIP-2780 support: from the Amsterdam hardfork, contract-creation transactions no longer pay a value charge. diff --git a/crates/edr_provider/tests/integration/eip2780.rs b/crates/edr_provider/tests/integration/eip2780.rs index 49e0f7e001..dd251fc17e 100644 --- a/crates/edr_provider/tests/integration/eip2780.rs +++ b/crates/edr_provider/tests/integration/eip2780.rs @@ -35,6 +35,10 @@ const CONTRACT_ADDRESS: Address = address!("0x0000000000000000000000000000000000 /// Flat intrinsic gas of a transaction before Amsterdam. const PRE_AMSTERDAM_INTRINSIC_GAS: u64 = 21_000; +// The decomposed charges of a value transfer add up to the flat cost they +// replace. +const _: () = + assert!(TX_BASE_COST + COLD_ACCOUNT_ACCESS + TX_VALUE_COST == PRE_AMSTERDAM_INTRINSIC_GAS); /// Flat intrinsic gas of a contract creation before Amsterdam: the /// transaction's plus the creation's. const PRE_AMSTERDAM_CREATE_INTRINSIC_GAS: u64 = PRE_AMSTERDAM_INTRINSIC_GAS + 32_000; @@ -72,6 +76,16 @@ fn create_request(value: U256) -> TransactionRequest { } } +fn as_call_request(request: &TransactionRequest) -> L1CallRequest { + L1CallRequest { + from: Some(request.from), + to: request.to, + value: request.value, + data: request.data.clone(), + ..L1CallRequest::default() + } +} + /// Sends the transaction and returns the `gasUsed` of its receipt. fn gas_used_by_sending( provider: &Provider, @@ -155,18 +169,6 @@ async fn value_transfer_pays_base_cost_recipient_access_and_value_charge_from_am Ok(()) } -// The decomposed charges of a value transfer add up to the flat cost they -// replace. -#[test] -fn value_transfer_charges_add_up_to_the_flat_pre_amsterdam_cost() { - assert_eq!( - TX_BASE_COST + COLD_ACCOUNT_ACCESS + TX_VALUE_COST, - PRE_AMSTERDAM_INTRINSIC_GAS - ); -} - -// Unlike a call, a creation with value pays no value charge: the sum has no -// `TX_VALUE_COST` term. #[tokio::test(flavor = "multi_thread")] async fn create_with_value_pays_base_cost_and_creation_access_from_amsterdam() -> anyhow::Result<()> { @@ -179,7 +181,8 @@ async fn create_with_value_pays_base_cost_and_creation_access_from_amsterdam() - ); let amsterdam_provider = new_provider(edr_chain_l1::Hardfork::Amsterdam)?; - // The new contract's EIP-8037 state gas is charged on top. + // No `TX_VALUE_COST` term, unlike a call with value; the new contract's + // EIP-8037 state gas is charged on top. assert_eq!( gas_used_by_sending(&amsterdam_provider, request)?, TX_BASE_COST + CREATE_ACCESS + INIT_CODE_INTRINSIC_GAS + NEW_ACCOUNT_STATE_GAS @@ -286,17 +289,10 @@ async fn transaction_funding_only_the_intrinsic_gas_is_included_and_halts_from_a Ok(()) } -// Amsterdam does not change the estimate of a value transfer, except for the -// recipient's state gas while the transfer creates it. #[tokio::test(flavor = "multi_thread")] async fn estimate_gas_of_a_value_transfer_keeps_the_flat_cost_from_amsterdam() -> anyhow::Result<()> { - let request = L1CallRequest { - from: Some(SENDER), - to: Some(RECIPIENT), - value: Some(U256::from(1)), - ..L1CallRequest::default() - }; + let request = as_call_request(&transfer_request(RECIPIENT, U256::from(1))); let osaka_provider = new_provider(edr_chain_l1::Hardfork::Osaka)?; // `eth_estimateGas` never returns less than the intrinsic cost plus one. @@ -326,6 +322,38 @@ async fn estimate_gas_of_a_value_transfer_keeps_the_flat_cost_from_amsterdam() - Ok(()) } +#[tokio::test(flavor = "multi_thread")] +async fn estimate_gas_of_a_plain_transfer_pays_only_the_reduced_intrinsic_cost_from_amsterdam( +) -> anyhow::Result<()> { + let osaka_provider = new_provider(edr_chain_l1::Hardfork::Osaka)?; + let amsterdam_provider = new_provider(edr_chain_l1::Hardfork::Amsterdam)?; + + // A transfer runs no code, so its estimate is the floor: the intrinsic + // cost plus one. A self-transfer's is the base cost alone. + let self_transfer = as_call_request(&transfer_request(SENDER, U256::from(1))); + assert_eq!( + estimate_gas(&osaka_provider, self_transfer.clone()), + PRE_AMSTERDAM_INTRINSIC_GAS + 1 + ); + assert_eq!( + estimate_gas(&amsterdam_provider, self_transfer), + TX_BASE_COST + 1 + ); + + // The base cost plus the recipient's cold access, without a value charge. + let zero_value_call = as_call_request(&transfer_request(RECIPIENT, U256::ZERO)); + assert_eq!( + estimate_gas(&osaka_provider, zero_value_call.clone()), + PRE_AMSTERDAM_INTRINSIC_GAS + 1 + ); + assert_eq!( + estimate_gas(&amsterdam_provider, zero_value_call), + TX_BASE_COST + COLD_ACCOUNT_ACCESS + 1 + ); + + Ok(()) +} + // The estimate must clear the Amsterdam create intrinsic cost, not the flat // pre-Amsterdam one, and fund the state gas of the new contract account. #[tokio::test(flavor = "multi_thread")] @@ -334,11 +362,7 @@ async fn estimate_gas_covers_a_contract_creation_from_amsterdam() -> anyhow::Res let estimate = estimate_gas( &amsterdam_provider, - L1CallRequest { - from: Some(SENDER), - data: Some(empty_contract_init_code()), - ..L1CallRequest::default() - }, + as_call_request(&create_request(U256::ZERO)), ); assert_eq!( estimate, From d99300fd242f357d891d30e7bda900614f94846a Mon Sep 17 00:00:00 2001 From: Ana Perez Ghiglia Date: Mon, 26 Oct 2026 16:13:06 +0000 Subject: [PATCH 3/3] Improve create contract test --- crates/edr_provider/tests/integration/eip2780.rs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/crates/edr_provider/tests/integration/eip2780.rs b/crates/edr_provider/tests/integration/eip2780.rs index dd251fc17e..aa96acf229 100644 --- a/crates/edr_provider/tests/integration/eip2780.rs +++ b/crates/edr_provider/tests/integration/eip2780.rs @@ -170,8 +170,8 @@ async fn value_transfer_pays_base_cost_recipient_access_and_value_charge_from_am } #[tokio::test(flavor = "multi_thread")] -async fn create_with_value_pays_base_cost_and_creation_access_from_amsterdam() -> anyhow::Result<()> -{ +async fn create_pays_base_cost_and_creation_access_regardless_of_value_from_amsterdam( +) -> anyhow::Result<()> { let request = create_request(U256::from(1)); let osaka_provider = new_provider(edr_chain_l1::Hardfork::Osaka)?; @@ -181,12 +181,18 @@ async fn create_with_value_pays_base_cost_and_creation_access_from_amsterdam() - ); let amsterdam_provider = new_provider(edr_chain_l1::Hardfork::Amsterdam)?; - // No `TX_VALUE_COST` term, unlike a call with value; the new contract's - // EIP-8037 state gas is charged on top. + // The new contract's EIP-8037 state gas is charged on top. + let with_value = gas_used_by_sending(&amsterdam_provider, request)?; assert_eq!( - gas_used_by_sending(&amsterdam_provider, request)?, + with_value, TX_BASE_COST + CREATE_ACCESS + INIT_CODE_INTRINSIC_GAS + NEW_ACCOUNT_STATE_GAS ); + // With no `TX_VALUE_COST` term, unlike a call with value, a creation + // without value costs the same. + assert_eq!( + gas_used_by_sending(&amsterdam_provider, create_request(U256::ZERO))?, + with_value + ); Ok(()) }