Skip to content
Draft
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
33 changes: 33 additions & 0 deletions crates/evm2/src/evm/inspector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -755,6 +755,39 @@ mod tests {
assert_eq!(inspector.create_destinations, [expected]);
}

#[test]
fn amsterdam_create_out_of_funds_does_not_fire_create_hook() {
let contract = Address::from([0x11; 20]);
let mut db = InMemoryDB::default();
db.insert_account_info(&contract, AccountInfo::default().with_balance(U256::from(1)));

let mut code = Vec::new();
push_all(&mut code, [Word::ZERO, Word::ZERO, Word::from(2)]);
code.push(op::CREATE);
return_top_word(&mut code);

let mut evm = Evm::<BaseEvmTypes>::new(
SpecId::AMSTERDAM,
BlockEnv::default(),
TxRegistry::new(),
db,
Precompiles::base(SpecId::AMSTERDAM),
);
evm.set_inspector(HookInspector::default());
let tx_env = TxEnv::default();
let bytecode = legacy_bytecode(code);
let mut message =
Message { destination: contract, gas_limit: 100_000, ..Default::default() };

let result = Host::execute_message(&mut evm, &tx_env, bytecode, &mut message);
let inspector = evm.clear_inspector_as::<HookInspector>().unwrap();

assert_matches!(result.stop, InstrStop::Return);
assert_eq!(Word::from_be_slice(&result.output), Word::ZERO);
assert!(inspector.create_depths.is_empty());
assert!(inspector.create_end_stops.is_empty());
}

#[test]
fn create_inspector_override_wins_at_max_depth() {
let created = Address::from([0x77; 20]);
Expand Down
3 changes: 3 additions & 0 deletions crates/evm2/src/evm/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1563,6 +1563,7 @@ impl<'a, T: EvmTypes> Host<T> for Evm<'a, T> {
};
Ok(AccountLoad {
balance: info.balance,
nonce: info.nonce,
code_hash: if exists { info.code_hash } else { B256::ZERO },
code,
exists,
Expand Down Expand Up @@ -1736,6 +1737,8 @@ impl<'a, T: EvmTypes> Host<T> for Evm<'a, T> {
pub struct AccountLoad {
/// Account balance.
pub balance: Word,
/// Account nonce.
pub nonce: u64,
/// Account code hash.
pub code_hash: B256,
/// Account bytecode.
Expand Down
85 changes: 66 additions & 19 deletions crates/evm2/src/interpreter/instructions/system.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use crate::{
utils::{word_to_address, word_to_usize},
version::GasId,
};
use alloy_primitives::{Address, B256, Bytes};
use alloy_primitives::{Address, B256, Bytes, keccak256};
use core::{cmp::min, ops::Range};
use evm2_macros::instruction;

Expand Down Expand Up @@ -70,6 +70,44 @@ fn memory_range_bytes<T: EvmTypesHost>(
Ok(Bytes::copy_from_slice(state.memory().slice(range.start, range.len())))
}

fn prepare_eip8037_create_state_gas<T: EvmTypesHost>(
gas: &mut Gas,
state: &mut InterpreterState<'_, '_, T>,
caller: Address,
value: Word,
input: &Bytes,
salt: Option<Word>,
is_create2: bool,
) -> Result<Option<u64>> {
if !state.feature(EvmFeatures::EIP8037) {
return Ok(Some(0));
}

// Amsterdam charges CREATE state gas at opcode time, but only after
// balance and nonce pre-access checks pass. Failing these checks returns 0
// without constructing a child create frame.
let caller_account = state.host().load_account(&caller, false, false)?;
if caller_account.balance < value || caller_account.nonce == u64::MAX {
state.clear_return_data();
return Ok(None);
}

let destination = if is_create2 {
let salt = B256::from(salt.expect("CREATE2 salt is present").to_be_bytes());
caller.create2(salt, keccak256(input.as_ref()))
} else {
caller.create(caller_account.nonce)
};
let destination = state.host().load_account(&destination, false, false)?;
if destination.is_empty {
let create_state_gas = state.gas_params().create_state_gas();
gas.spend_state(create_state_gas)?;
return Ok(Some(create_state_gas));
}

Ok(Some(0))
}

fn load_acc_and_calc_gas<T: EvmTypesHost>(
gas: &mut Gas,
state: &mut InterpreterState<'_, '_, T>,
Expand Down Expand Up @@ -315,30 +353,41 @@ fn create_inner<T: EvmTypesHost>(
state.gas_params().get(GasId::Create).into()
};
gas.spend(create_cost)?;
// EIP-8037: charge the upfront CREATE state gas on this frame before the
// child gas/reservoir split. Refunded via `refill_reservoir` if the child
// fails to deploy (see the create-failure path after `execute_message`).
if state.feature(EvmFeatures::EIP8037) {
gas.spend_state(state.gas_params().create_state_gas())?;
}
let current = state.message();
let current_destination = current.destination;
let depth = current.depth.saturating_add(1);
let charged_create_state_gas = match prepare_eip8037_create_state_gas(
gas,
state,
current_destination,
value,
&input,
salt,
is_create2,
)? {
Some(cost) => cost,
None => {
stack.push(Word::ZERO)?;
return Ok(());
}
};
let gas_limit = if state.feature(EvmFeatures::EIP150) {
state.gas_params().call_stipend_reduction(gas.remaining())
} else {
gas.remaining()
};
gas.spend(gas_limit)?;

let current = state.message();
let mut message = Message {
kind: if is_create2 { MessageKind::Create2 } else { MessageKind::Create },
depth: current.depth.saturating_add(1),
depth,
gas_limit,
reservoir: gas.reservoir(),
destination: current.destination,
caller: current.destination,
destination: current_destination,
caller: current_destination,
input,
value,
code_address: current.destination,
code_address: current_destination,
disable_precompiles: false,
// CREATE is rejected in a static context (see `require_non_staticcall`).
caller_is_static: false,
Expand All @@ -355,14 +404,12 @@ fn create_inner<T: EvmTypesHost>(
gas.merge_child_gas(result.gas, result.stop);

// EIP-8037: the CREATE/CREATE2 opcode charged `create_state_gas` upfront on
// this frame's tracker. Refund it to the reservoir via `refill_reservoir`
// (matching 0→x→0 storage restoration) when no new account leaf ends up
// created: either the create failed to deploy (revert, halt, or early-fail
// paths that leave `created_address == None` — depth, out-of-funds, nonce
// overflow), or it succeeded at a pre-existing alive (balance-only) target.
// this frame's tracker only when the destination was empty at access time.
// Refund it when no new account leaf is created because the create failed
// to deploy (revert, halt, or early-fail paths that leave no address).
let create_failed = result.created_address.is_none() || !result.stop.is_success();
if (create_failed || result.created_target_was_alive) && state.feature(EvmFeatures::EIP8037) {
gas.refill_reservoir(state.gas_params().create_state_gas());
if create_failed && charged_create_state_gas != 0 {
gas.refill_reservoir(charged_create_state_gas);
}
// EIP-211 exposes CREATE failure data only for REVERT; other failures clear returndata.
if result.stop == InstrStop::Revert {
Expand Down
1 change: 1 addition & 0 deletions crates/evm2/src/test_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ impl Host<TestTypes> for TestHost {
}
Ok(AccountLoad {
balance: address.into_word().into(),
nonce: 0,
code_hash: self.code_hash,
code: if load_code {
Bytecode::new_legacy(self.code.clone())
Expand Down
61 changes: 44 additions & 17 deletions crates/jit/builtins/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -664,33 +664,60 @@ pub unsafe extern "C" fn __revmc_builtin_create(
version.gas_params.get(GasId::Create).into()
};
ecx.gas.spend(create_cost)?;
if ecx.enables(EvmFeatures::EIP8037) {
ecx.gas.spend_state(version.gas_params.create_state_gas())?;
}

let mut gas_limit = ecx.gas.remaining();
if ecx.enables(EvmFeatures::EIP150) {
gas_limit = version.gas_params.call_stipend_reduction(gas_limit);
}
ecx.gas.spend(gas_limit)?;
let salt = if is_create2 {
pop!(sp; salt);
B256::from(salt.to_be_bytes())
} else {
B256::ZERO
};

let value = value.to_u256();
let current = ecx.message();
let current_destination = current.destination;
let depth = current.depth.saturating_add(1);

let charged_create_state_gas = if ecx.enables(EvmFeatures::EIP8037) {
let caller_account = ecx.host().load_account(&current_destination, false, false)?;
if caller_account.balance < value || caller_account.nonce == u64::MAX {
unsafe {
sp.write(EvmWord::ZERO);
}
ecx.set_return_data(Bytes::new());
return Ok(());
}

let destination = if is_create2 {
current_destination.create2(salt, keccak256(code.as_ref()))
} else {
current_destination.create(caller_account.nonce)
};
let destination_account = ecx.host().load_account(&destination, false, false)?;
if destination_account.is_empty {
let create_state_gas = version.gas_params.create_state_gas();
ecx.gas.spend_state(create_state_gas)?;
create_state_gas
} else {
0
}
} else {
0
};

let mut gas_limit = ecx.gas.remaining();
if ecx.enables(EvmFeatures::EIP150) {
gas_limit = version.gas_params.call_stipend_reduction(gas_limit);
}
ecx.gas.spend(gas_limit)?;

let mut message = Message {
kind: if is_create2 { MessageKind::Create2 } else { MessageKind::Create },
depth: current.depth.saturating_add(1),
depth,
gas_limit,
reservoir: ecx.gas.reservoir(),
destination: current.destination,
caller: current.destination,
destination: current_destination,
caller: current_destination,
input: code,
value: value.to_u256(),
code_address: current.destination,
value,
code_address: current_destination,
disable_precompiles: false,
caller_is_static: false,
salt,
Expand All @@ -702,8 +729,8 @@ pub unsafe extern "C" fn __revmc_builtin_create(
let mut result = ecx.host().execute_message(tx_env, bytecode, &mut message);
ecx.gas.merge_child_gas(result.gas, result.stop);
let create_failed = result.created_address.is_none() || !result.stop.is_success();
if (create_failed || result.created_target_was_alive) && ecx.enables(EvmFeatures::EIP8037) {
ecx.gas.refill_reservoir(ecx.gas_params().create_state_gas());
if create_failed && charged_create_state_gas != 0 {
ecx.gas.refill_reservoir(charged_create_state_gas);
}

let address = EvmWord::from(result.created_address_for_parent());
Expand Down
65 changes: 65 additions & 0 deletions crates/jit/runtime/src/evm2_evm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,71 @@ mod tests {
assert_eq!(interpreter.output()[31], 1);
}

#[test]
#[cfg(feature = "llvm")]
fn compiled_amsterdam_create_checks_balance_before_state_gas() {
let config = <BaseEvmConfigSelector as EvmConfigSelector<BaseEvmTypes>>::execution_config(
SpecId::AMSTERDAM,
);
let creator = Address::from([0x33; 20]);
let code = [
op::PUSH0,
op::PUSH0,
op::PUSH1,
2,
op::CREATE,
op::PUSH0,
op::MSTORE,
op::PUSH1,
0x20,
op::PUSH0,
op::RETURN,
];

let run = |with_jit: bool| {
let mut database = InMemoryDB::default();
database
.insert_account_info(&creator, AccountInfo::default().with_balance(Word::from(1)));
let backend = blocking_backend();
let mut host = Evm::<BaseEvmTypes>::new(
SpecId::AMSTERDAM,
BlockEnv::default(),
ethereum_tx_registry(SpecId::AMSTERDAM),
database,
Precompiles::base(SpecId::AMSTERDAM),
);
host.state_mut().prewarm(&creator);
if with_jit {
host.set_interpreter_runner(JitInterpreterRunner::new(backend.clone()));
}
let tx_env = TxEnv::default();
let message =
Message { gas_limit: 100_000, destination: creator, ..Message::default() };
let mut interpreter = Interpreter::<BaseEvmTypes>::new(
Bytecode::new_legacy(Bytes::copy_from_slice(&code)),
&tx_env,
&message,
);
let stop = if with_jit {
run_interpreter(&backend, &config, &mut interpreter, &mut host).unwrap()
} else {
interpreter.run(&config, &mut host)
};
(
stop,
interpreter.gas().spent(),
interpreter.gas().refunded(),
interpreter.output().to_vec(),
)
};

let interpreter = run(false);
let jit = run(true);
assert_eq!(jit, interpreter);
assert_eq!(jit.0, InstrStop::Return);
assert_eq!(jit.3, vec![0; 32]);
}

#[test]
#[cfg(feature = "llvm")]
fn compiled_staticcall_zeros_child_callvalue() {
Expand Down