Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .changeset/eip2780-create-value-charge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@nomicfoundation/edr": patch
---

Changed the experimental EIP-2780 support: from the Amsterdam hardfork, contract-creation transactions no longer pay a value charge.
160 changes: 137 additions & 23 deletions crates/edr_provider/tests/integration/eip2780.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,29 @@ 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;
/// 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),
Expand All @@ -49,6 +66,26 @@ 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()
}
}

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<L1ChainSpec>,
Expand All @@ -61,7 +98,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!(
Expand All @@ -82,7 +119,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!(
Expand All @@ -102,7 +139,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
Expand All @@ -123,8 +160,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
Expand All @@ -133,6 +169,34 @@ async fn value_transfer_pays_base_cost_recipient_access_and_value_charge_from_am
Ok(())
}

#[tokio::test(flavor = "multi_thread")]
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)?;
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.
let with_value = gas_used_by_sending(&amsterdam_provider, request)?;
assert_eq!(
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(())
}

// 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.
Expand Down Expand Up @@ -231,28 +295,80 @@ async fn transaction_funding_only_the_intrinsic_gas_is_included_and_halts_from_a
Ok(())
}

#[tokio::test(flavor = "multi_thread")]
async fn estimate_gas_of_a_value_transfer_keeps_the_flat_cost_from_amsterdam() -> anyhow::Result<()>
Comment thread
Wodann marked this conversation as resolved.
{
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.
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
Comment thread
Wodann marked this conversation as resolved.
);

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")]
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()),
..L1CallRequest::default()
},
as_call_request(&create_request(U256::ZERO)),
);
assert_eq!(
estimate,
Expand All @@ -263,10 +379,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)?;
Expand Down
Loading