diff --git a/crates/evm2/src/evm/inspector.rs b/crates/evm2/src/evm/inspector.rs index 3551b1b8..bff4f2a1 100644 --- a/crates/evm2/src/evm/inspector.rs +++ b/crates/evm2/src/evm/inspector.rs @@ -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::::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::().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]); diff --git a/crates/evm2/src/evm/mod.rs b/crates/evm2/src/evm/mod.rs index fe6e6e56..bcf3020d 100644 --- a/crates/evm2/src/evm/mod.rs +++ b/crates/evm2/src/evm/mod.rs @@ -1563,6 +1563,7 @@ impl<'a, T: EvmTypes> Host for Evm<'a, T> { }; Ok(AccountLoad { balance: info.balance, + nonce: info.nonce, code_hash: if exists { info.code_hash } else { B256::ZERO }, code, exists, @@ -1736,6 +1737,8 @@ impl<'a, T: EvmTypes> Host 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. diff --git a/crates/evm2/src/interpreter/instructions/system.rs b/crates/evm2/src/interpreter/instructions/system.rs index c3635c0b..f2a626d6 100644 --- a/crates/evm2/src/interpreter/instructions/system.rs +++ b/crates/evm2/src/interpreter/instructions/system.rs @@ -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; @@ -70,6 +70,44 @@ fn memory_range_bytes( Ok(Bytes::copy_from_slice(state.memory().slice(range.start, range.len()))) } +fn prepare_eip8037_create_state_gas( + gas: &mut Gas, + state: &mut InterpreterState<'_, '_, T>, + caller: Address, + value: Word, + input: &Bytes, + salt: Option, + is_create2: bool, +) -> Result> { + 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( gas: &mut Gas, state: &mut InterpreterState<'_, '_, T>, @@ -315,12 +353,24 @@ fn create_inner( 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 { @@ -328,17 +378,16 @@ fn create_inner( }; 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, @@ -355,14 +404,12 @@ fn create_inner( 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 { diff --git a/crates/evm2/src/test_utils.rs b/crates/evm2/src/test_utils.rs index 9897bf2e..19667685 100644 --- a/crates/evm2/src/test_utils.rs +++ b/crates/evm2/src/test_utils.rs @@ -98,6 +98,7 @@ impl Host 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()) diff --git a/crates/jit/builtins/src/lib.rs b/crates/jit/builtins/src/lib.rs index 8ae87cce..acc727be 100644 --- a/crates/jit/builtins/src/lib.rs +++ b/crates/jit/builtins/src/lib.rs @@ -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(¤t_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, @@ -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()); diff --git a/crates/jit/runtime/src/evm2_evm.rs b/crates/jit/runtime/src/evm2_evm.rs index 2326b7ef..2ed6ae29 100644 --- a/crates/jit/runtime/src/evm2_evm.rs +++ b/crates/jit/runtime/src/evm2_evm.rs @@ -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 = >::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::::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::::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() {