arb_evm/
evm.rs

1use alloy_evm::{
2    eth::EthEvmContext, precompiles::PrecompilesMap, Database, Evm, EvmEnv, EvmFactory,
3};
4use alloy_primitives::{Address, Bytes, B256, U256};
5use arb_precompiles::register_arb_precompiles;
6use arb_stylus::{
7    config::StylusConfig, ink::Gas as StylusGas, meter::MeteredMachine, run::RunProgram,
8    StylusEvmApi,
9};
10use arbos::programs::types::EvmData;
11use core::fmt::Debug;
12use revm::{
13    context::result::EVMError,
14    context_interface::{
15        host::LoadError,
16        result::{HaltReason, ResultAndState},
17    },
18    handler::{instructions::EthInstructions, EthFrame, PrecompileProvider},
19    inspector::NoOpInspector,
20    interpreter::{
21        interpreter::EthInterpreter,
22        interpreter_types::{InputsTr, ReturnData, RuntimeFlag, StackTr},
23        CallInput, CallInputs, CallScheme, Gas as EvmGas, Host, InstructionContext,
24        InstructionResult, InterpreterResult, InterpreterTypes,
25    },
26    primitives::hardfork::SpecId,
27};
28
29use crate::transaction::ArbTransaction;
30
31/// BLOBBASEFEE opcode (0x4a).
32const BLOBBASEFEE_OPCODE: u8 = 0x4a;
33
34/// SELFDESTRUCT opcode (0xff).
35const SELFDESTRUCT_OPCODE: u8 = 0xff;
36
37/// NUMBER opcode (0x43).
38const NUMBER_OPCODE: u8 = 0x43;
39
40/// BLOCKHASH opcode (0x40).
41const BLOCKHASH_OPCODE: u8 = 0x40;
42
43/// BALANCE opcode (0x31).
44const BALANCE_OPCODE: u8 = 0x31;
45
46/// Arbitrum NUMBER: returns the L1 block number from ArbOS state.
47///
48/// Nitro's NUMBER reads from `ProcessingHook.L1BlockNumber()` which returns
49/// the value stored by `record_new_l1_block` during StartBlock. The mixHash
50/// L1 block number in the header can differ from this value, so we read from
51/// the thread-local set after StartBlock processing.
52fn arb_number<WIRE: InterpreterTypes, H: Host + ?Sized>(ctx: InstructionContext<'_, H, WIRE>) {
53    let l1_block = arb_precompiles::get_l1_block_number_for_evm();
54    if !ctx.interpreter.stack.push(U256::from(l1_block)) {
55        ctx.interpreter.halt(InstructionResult::StackOverflow);
56    }
57}
58
59/// Arbitrum BLOCKHASH: uses L1 block number for range check.
60///
61/// Standard BLOCKHASH compares the requested block number against block_env.number,
62/// which is the L2 block number. Since Arbitrum's NUMBER opcode returns the L1
63/// block number, BLOCKHASH must also use L1 block numbers for the range check.
64/// Otherwise requests for L1 block hashes would always be out of range.
65fn arb_blockhash<WIRE: InterpreterTypes, H: Host + ?Sized>(ctx: InstructionContext<'_, H, WIRE>) {
66    use revm::interpreter::InstructionResult;
67
68    let requested = match ctx.interpreter.stack.pop() {
69        Some(v) => v,
70        None => {
71            ctx.interpreter.halt(InstructionResult::StackUnderflow);
72            return;
73        }
74    };
75
76    let l1_block_number = U256::from(arb_precompiles::get_l1_block_number_for_evm());
77
78    let Some(diff) = l1_block_number.checked_sub(requested) else {
79        if !ctx.interpreter.stack.push(U256::ZERO) {
80            ctx.interpreter.halt(InstructionResult::StackOverflow);
81        }
82        return;
83    };
84
85    let diff_u64: u64 = diff.try_into().unwrap_or(u64::MAX);
86    if diff_u64 == 0 || diff_u64 > 256 {
87        if !ctx.interpreter.stack.push(U256::ZERO) {
88            ctx.interpreter.halt(InstructionResult::StackOverflow);
89        }
90        return;
91    }
92
93    let requested_u64: u64 = requested.try_into().unwrap_or(u64::MAX);
94    match ctx.host.block_hash(requested_u64) {
95        Some(hash) => {
96            if !ctx.interpreter.stack.push(U256::from_be_bytes(hash.0)) {
97                ctx.interpreter.halt(InstructionResult::StackOverflow);
98            }
99        }
100        None => {
101            ctx.interpreter.halt_fatal();
102        }
103    }
104}
105
106// SHA3 tracer removed — can't easily wrap standard handler
107
108/// Arbitrum BALANCE: adjusts the sender's balance by the poster fee correction.
109///
110/// Nitro's BuyGas charges gas_limit * baseFee, but our reduced gas_limit
111/// charges posterGas * baseFee less. When a contract checks BALANCE(sender),
112/// we subtract the correction from the result to match Nitro.
113fn arb_balance<WIRE: InterpreterTypes, H: Host + ?Sized>(ctx: InstructionContext<'_, H, WIRE>) {
114    // Pop address from stack
115    let addr_u256 = match ctx.interpreter.stack.pop() {
116        Some(v) => v,
117        None => {
118            ctx.interpreter
119                .halt(revm::interpreter::InstructionResult::StackUnderflow);
120            return;
121        }
122    };
123
124    let addr = alloy_primitives::Address::from_word(alloy_primitives::B256::from(
125        addr_u256.to_be_bytes::<32>(),
126    ));
127
128    // Load account via host (handles cold/warm tracking)
129    let spec_id = ctx.interpreter.runtime_flag.spec_id();
130    if spec_id.is_enabled_in(revm::primitives::hardfork::SpecId::BERLIN) {
131        // Berlin+: use balance() which tracks cold/warm
132        let Some(state_load) = ctx.host.balance(addr) else {
133            ctx.interpreter.halt_fatal();
134            return;
135        };
136        // Charge gas: 2600 for cold, 100 for warm
137        let gas_cost = if state_load.is_cold { 2600u64 } else { 100u64 };
138        if !ctx.interpreter.gas.record_cost(gas_cost) {
139            ctx.interpreter
140                .halt(revm::interpreter::InstructionResult::OutOfGas);
141            return;
142        }
143
144        // Apply poster fee correction for sender
145        let balance = if addr == arb_precompiles::get_current_tx_sender() {
146            state_load
147                .data
148                .saturating_sub(arb_precompiles::get_poster_balance_correction())
149        } else {
150            state_load.data
151        };
152
153        if !ctx.interpreter.stack.push(balance) {
154            ctx.interpreter
155                .halt(revm::interpreter::InstructionResult::StackOverflow);
156        }
157    } else {
158        // Pre-Berlin: always 400 gas, load via basic path
159        let Some(state_load) = ctx.host.balance(addr) else {
160            ctx.interpreter.halt_fatal();
161            return;
162        };
163
164        let balance = if addr == arb_precompiles::get_current_tx_sender() {
165            state_load
166                .data
167                .saturating_sub(arb_precompiles::get_poster_balance_correction())
168        } else {
169            state_load.data
170        };
171
172        if !ctx.interpreter.stack.push(balance) {
173            ctx.interpreter
174                .halt(revm::interpreter::InstructionResult::StackOverflow);
175        }
176    }
177}
178
179/// SELFBALANCE opcode (0x47).
180const SELFBALANCE_OPCODE: u8 = 0x47;
181
182/// Arbitrum SELFBALANCE: adjusts for poster fee correction if the executing
183/// contract IS the tx sender (edge case: sender calls own address).
184fn arb_selfbalance<WIRE: InterpreterTypes, H: Host + ?Sized>(ctx: InstructionContext<'_, H, WIRE>) {
185    let target = ctx.interpreter.input.target_address();
186
187    let Some(state_load) = ctx.host.balance(target) else {
188        ctx.interpreter.halt_fatal();
189        return;
190    };
191
192    // Apply poster fee correction if the contract being executed is the tx sender
193    let balance = if target == arb_precompiles::get_current_tx_sender() {
194        state_load
195            .data
196            .saturating_sub(arb_precompiles::get_poster_balance_correction())
197    } else {
198        state_load.data
199    };
200
201    if !ctx.interpreter.stack.push(balance) {
202        ctx.interpreter
203            .halt(revm::interpreter::InstructionResult::StackOverflow);
204    }
205}
206
207/// BLOBBASEFEE is not supported on Arbitrum — execution halts.
208fn arb_blob_basefee<WIRE: InterpreterTypes, H: Host + ?Sized>(
209    ctx: InstructionContext<'_, H, WIRE>,
210) {
211    ctx.interpreter.halt(InstructionResult::OpcodeNotFound);
212}
213
214/// Arbitrum SELFDESTRUCT: reverts if the acting account is a Stylus program,
215/// otherwise delegates to the standard EIP-6780 selfdestruct logic.
216fn arb_selfdestruct<WIRE: InterpreterTypes, H: Host + ?Sized>(
217    ctx: InstructionContext<'_, H, WIRE>,
218) {
219    if ctx.interpreter.runtime_flag.is_static() {
220        ctx.interpreter
221            .halt(InstructionResult::StateChangeDuringStaticCall);
222        return;
223    }
224
225    // Stylus programs cannot be self-destructed.
226    let acting_addr = ctx.interpreter.input.target_address();
227    match ctx.host.load_account_code(acting_addr) {
228        Some(code_load) => {
229            if arb_stylus::is_stylus_program(&code_load.data) {
230                ctx.interpreter.halt(InstructionResult::Revert);
231                return;
232            }
233        }
234        None => {
235            ctx.interpreter.halt_fatal();
236            return;
237        }
238    }
239
240    // Standard selfdestruct logic (matching revm's EIP-6780 implementation).
241    // Pop U256 and convert to Address manually (avoids pop_address() which
242    // triggers a ruint 1.17 const eval panic due to U256->Address byte size mismatch).
243    let Some(raw) = ctx.interpreter.stack.pop() else {
244        ctx.interpreter.halt(InstructionResult::StackUnderflow);
245        return;
246    };
247    let target = Address::from_word(alloy_primitives::B256::from(raw.to_be_bytes()));
248
249    let spec = ctx.interpreter.runtime_flag.spec_id();
250    let cold_load_gas = ctx.host.gas_params().selfdestruct_cold_cost();
251    let skip_cold_load = ctx.interpreter.gas.remaining() < cold_load_gas;
252
253    let res = match ctx.host.selfdestruct(acting_addr, target, skip_cold_load) {
254        Ok(res) => res,
255        Err(LoadError::ColdLoadSkipped) => {
256            ctx.interpreter.halt_oog();
257            return;
258        }
259        Err(LoadError::DBError) => {
260            ctx.interpreter.halt_fatal();
261            return;
262        }
263    };
264
265    // EIP-161: State trie clearing.
266    let should_charge_topup = if spec.is_enabled_in(SpecId::SPURIOUS_DRAGON) {
267        res.had_value && !res.target_exists
268    } else {
269        !res.target_exists
270    };
271
272    let gas_cost = ctx
273        .host
274        .gas_params()
275        .selfdestruct_cost(should_charge_topup, res.is_cold);
276    if !ctx.interpreter.gas.record_cost(gas_cost) {
277        ctx.interpreter.halt_oog();
278        return;
279    }
280
281    if !res.previously_destroyed {
282        ctx.interpreter
283            .gas
284            .record_refund(ctx.host.gas_params().selfdestruct_refund());
285    }
286
287    ctx.interpreter.halt(InstructionResult::SelfDestruct);
288}
289
290// ── Stylus page tracking & reentrancy ───────────────────────────────
291//
292// Page tracking and reentrancy state lives in arb_stylus::pages so that
293// both arb-evm (dispatch) and arb-stylus (EvmApi add_pages) can access it.
294
295pub use arb_stylus::pages::{
296    add_stylus_pages, get_stylus_pages, pop_stylus_program, push_stylus_program,
297    reset_stylus_pages, set_stylus_pages_open,
298};
299
300// ── Stylus storage helpers ───────────────────────────────────────────
301
302use arb_precompiles::storage_slot::{
303    derive_subspace_key, map_slot, map_slot_b256, ARBOS_STATE_ADDRESS, PROGRAMS_DATA_KEY,
304    PROGRAMS_PARAMS_KEY, PROGRAMS_SUBSPACE, ROOT_STORAGE_KEY,
305};
306use arbos::programs::{memory::MemoryModel, params::StylusParams, Program};
307
308/// Read a storage slot from ArbOS state via the journal.
309fn sload_arbos<DB: Database>(journal: &mut revm::Journal<DB>, slot: U256) -> Option<U256> {
310    let _ = journal
311        .inner
312        .load_account(&mut journal.database, ARBOS_STATE_ADDRESS)
313        .ok()?;
314    let result = journal
315        .inner
316        .sload(&mut journal.database, ARBOS_STATE_ADDRESS, slot, false)
317        .ok()?;
318    Some(result.data)
319}
320
321/// Read the StylusParams packed word from storage.
322fn read_params_word<DB: Database>(journal: &mut revm::Journal<DB>) -> Option<[u8; 32]> {
323    let programs_key = derive_subspace_key(ROOT_STORAGE_KEY, PROGRAMS_SUBSPACE);
324    let params_key = derive_subspace_key(programs_key.as_slice(), PROGRAMS_PARAMS_KEY);
325    let slot = map_slot(params_key.as_slice(), 0);
326    sload_arbos(journal, slot).map(|v| v.to_be_bytes::<32>())
327}
328
329/// Read program data word by code hash.
330fn read_program_word<DB: Database>(
331    journal: &mut revm::Journal<DB>,
332    code_hash: B256,
333) -> Option<B256> {
334    let programs_key = derive_subspace_key(ROOT_STORAGE_KEY, PROGRAMS_SUBSPACE);
335    let data_key = derive_subspace_key(programs_key.as_slice(), PROGRAMS_DATA_KEY);
336    let slot = map_slot_b256(data_key.as_slice(), &code_hash);
337    sload_arbos(journal, slot).map(|v| B256::from(v.to_be_bytes::<32>()))
338}
339
340/// Parse essential StylusParams fields from the packed storage word.
341/// This mirrors `StylusParams::load()` but works with raw bytes from journal sload.
342fn parse_stylus_params(word: &[u8; 32], arbos_version: u64) -> StylusParams {
343    StylusParams {
344        arbos_version,
345        version: u16::from_be_bytes([word[0], word[1]]),
346        ink_price: (word[2] as u32) << 16 | (word[3] as u32) << 8 | word[4] as u32,
347        max_stack_depth: u32::from_be_bytes([word[5], word[6], word[7], word[8]]),
348        free_pages: u16::from_be_bytes([word[9], word[10]]),
349        page_gas: u16::from_be_bytes([word[11], word[12]]),
350        page_ramp: arbos::programs::params::INITIAL_PAGE_RAMP,
351        page_limit: u16::from_be_bytes([word[13], word[14]]),
352        min_init_gas: word[15],
353        min_cached_init_gas: word[16],
354        init_cost_scalar: word[17],
355        cached_cost_scalar: word[18],
356        expiry_days: u16::from_be_bytes([word[19], word[20]]),
357        keepalive_days: u16::from_be_bytes([word[21], word[22]]),
358        block_cache_size: u16::from_be_bytes([word[23], word[24]]),
359        // These fields span to word 2; not needed for dispatch.
360        max_wasm_size: 0,
361        max_fragment_count: 0,
362    }
363}
364
365/// Compute upfront gas cost for a Stylus call, per `Programs.CallProgram`.
366fn stylus_call_gas_cost(params: &StylusParams, program: &Program, pages_open: u16) -> u64 {
367    let model = MemoryModel::new(params.free_pages, params.page_gas);
368    let mut cost = model.gas_cost(program.footprint, pages_open, pages_open);
369
370    let cached = program.cached;
371    if cached || program.version > 1 {
372        cost = cost.saturating_add(program.cached_gas(params));
373    }
374    if !cached {
375        cost = cost.saturating_add(program.init_gas(params));
376    }
377    cost
378}
379
380// ── Stylus sub-call trampolines ─────────────────────────────────────
381
382use arb_stylus::evm_api_impl::{SubCallResult, SubCreateResult};
383
384/// Monomorphized trampoline for Stylus sub-calls (CALL/DELEGATECALL/STATICCALL).
385///
386/// This function is created as a concrete `fn(...)` pointer by monomorphizing
387/// generic type parameters at the call site in `execute_stylus_program`.
388/// The `ctx` pointer is cast back to the concrete Context type.
389fn stylus_call_trampoline<BlockEnv, TxEnv, CfgEnv, DB, Chain>(
390    ctx: *mut (),
391    call_type: u8,
392    contract: Address,
393    caller: Address,
394    input: &[u8],
395    gas: u64,
396    value: U256,
397) -> SubCallResult
398where
399    BlockEnv: revm::context::Block,
400    TxEnv: revm::context::Transaction,
401    CfgEnv: revm::context::Cfg,
402    DB: Database,
403{
404    let context = unsafe {
405        &mut *(ctx as *mut revm::Context<BlockEnv, TxEnv, CfgEnv, DB, revm::Journal<DB>, Chain>)
406    };
407
408    let is_static = call_type == 2;
409    let is_delegate = call_type == 1;
410
411    // Create a journal checkpoint for the sub-call
412    let checkpoint = context.journaled_state.inner.checkpoint();
413
414    // For CALL with value, transfer ETH
415    if !is_delegate && !value.is_zero() {
416        let transfer_result = context.journaled_state.inner.transfer(
417            &mut context.journaled_state.database,
418            caller,
419            contract,
420            value,
421        );
422        if transfer_result.is_err() {
423            context.journaled_state.inner.checkpoint_revert(checkpoint);
424            return SubCallResult {
425                output: Vec::new(),
426                gas_cost: 0,
427                success: false,
428            };
429        }
430    }
431
432    // Determine the code address (same as contract for CALL/STATICCALL, target for DELEGATE)
433    let code_address = contract;
434
435    // Load the target's bytecode
436    let bytecode = match context
437        .journaled_state
438        .inner
439        .load_code(&mut context.journaled_state.database, code_address)
440    {
441        Ok(acc) => acc
442            .data
443            .info
444            .code
445            .as_ref()
446            .map(|c| c.original_bytes())
447            .unwrap_or_default(),
448        Err(_) => {
449            context.journaled_state.inner.checkpoint_revert(checkpoint);
450            return SubCallResult {
451                output: Vec::new(),
452                gas_cost: 0,
453                success: false,
454            };
455        }
456    };
457
458    // Empty code — just a value transfer, already done above
459    if bytecode.is_empty() {
460        context.journaled_state.inner.checkpoint_commit();
461        return SubCallResult {
462            output: Vec::new(),
463            gas_cost: 0,
464            success: true,
465        };
466    }
467
468    // Determine target address (for DELEGATECALL, execution happens at caller's address)
469    let target_address = if is_delegate { caller } else { contract };
470
471    // Build CallInputs for dispatch
472    let call_scheme = match call_type {
473        0 => CallScheme::Call,
474        1 => CallScheme::DelegateCall,
475        2 => CallScheme::StaticCall,
476        _ => CallScheme::Call,
477    };
478
479    let call_value = if is_delegate {
480        revm::interpreter::CallValue::Apparent(value)
481    } else {
482        revm::interpreter::CallValue::Transfer(value)
483    };
484
485    let sub_inputs = CallInputs {
486        input: CallInput::Bytes(input.to_vec().into()),
487        gas_limit: gas,
488        target_address,
489        bytecode_address: code_address,
490        caller,
491        value: call_value,
492        scheme: call_scheme,
493        is_static,
494        return_memory_offset: 0..0,
495        known_bytecode: None,
496    };
497
498    // Dispatch through ArbPrecompilesMap (handles precompiles + Stylus)
499    {
500        arb_precompiles::set_evm_depth(context.journaled_state.inner.depth);
501        let mut precompiles = alloy_evm::precompiles::PrecompilesMap::new(Default::default());
502        <alloy_evm::precompiles::PrecompilesMap as PrecompileProvider<
503            revm::Context<BlockEnv, TxEnv, CfgEnv, DB, revm::Journal<DB>, Chain>,
504        >>::set_spec(&mut precompiles, context.cfg.spec());
505        register_arb_precompiles(&mut precompiles);
506        let mut arb_map = ArbPrecompilesMap(precompiles);
507        let dispatch_result = <ArbPrecompilesMap as PrecompileProvider<
508            revm::Context<BlockEnv, TxEnv, CfgEnv, DB, revm::Journal<DB>, Chain>,
509        >>::run(&mut arb_map, context, &sub_inputs);
510
511        match dispatch_result {
512            Ok(Some(result)) => {
513                let success = result.result.is_ok();
514                let output = result.output.to_vec();
515                let gas_used = gas.saturating_sub(result.gas.remaining());
516                if success {
517                    context.journaled_state.inner.checkpoint_commit();
518                } else {
519                    context.journaled_state.inner.checkpoint_revert(checkpoint);
520                }
521                return SubCallResult {
522                    output,
523                    gas_cost: gas_used,
524                    success,
525                };
526            }
527            Ok(None) => {
528                // Not a precompile or Stylus — fall through to EVM
529            }
530            Err(_) => {
531                context.journaled_state.inner.checkpoint_revert(checkpoint);
532                return SubCallResult {
533                    output: Vec::new(),
534                    gas_cost: 0,
535                    success: false,
536                };
537            }
538        }
539    }
540
541    // EVM bytecode execution — ArbPrecompilesMap didn't handle it
542    let result = run_evm_bytecode(context, &sub_inputs, &bytecode, gas);
543    let success = result.result.is_ok();
544    let output = result.output.to_vec();
545    let gas_used = gas.saturating_sub(result.gas.remaining());
546    if success {
547        context.journaled_state.inner.checkpoint_commit();
548    } else {
549        context.journaled_state.inner.checkpoint_revert(checkpoint);
550    }
551    SubCallResult {
552        output,
553        gas_cost: gas_used,
554        success,
555    }
556}
557
558/// Monomorphized trampoline for Stylus CREATE/CREATE2 operations.
559fn stylus_create_trampoline<BlockEnv, TxEnv, CfgEnv, DB, Chain>(
560    ctx: *mut (),
561    caller: Address,
562    code: &[u8],
563    gas: u64,
564    endowment: U256,
565    salt: Option<B256>,
566) -> SubCreateResult
567where
568    BlockEnv: revm::context::Block,
569    TxEnv: revm::context::Transaction,
570    CfgEnv: revm::context::Cfg,
571    DB: Database,
572{
573    let context = unsafe {
574        &mut *(ctx as *mut revm::Context<BlockEnv, TxEnv, CfgEnv, DB, revm::Journal<DB>, Chain>)
575    };
576
577    let checkpoint = context.journaled_state.inner.checkpoint();
578
579    // Compute CREATE/CREATE2 address
580    let caller_nonce = {
581        let acc = context
582            .journaled_state
583            .inner
584            .load_account(&mut context.journaled_state.database, caller);
585        acc.map(|a| a.data.info.nonce).unwrap_or(0)
586    };
587
588    let created_address = if let Some(salt) = salt {
589        // CREATE2: keccak256(0xff ++ sender ++ salt ++ keccak256(code))
590        let code_hash = alloy_primitives::keccak256(code);
591        let mut buf = Vec::with_capacity(1 + 20 + 32 + 32);
592        buf.push(0xff);
593        buf.extend_from_slice(caller.as_slice());
594        buf.extend_from_slice(salt.as_slice());
595        buf.extend_from_slice(code_hash.as_slice());
596        Address::from_slice(&alloy_primitives::keccak256(&buf)[12..])
597    } else {
598        // CREATE: RLP([sender, nonce])
599        use alloy_rlp::Encodable;
600        let mut rlp_buf = Vec::with_capacity(64);
601        alloy_rlp::Header {
602            list: true,
603            payload_length: caller.length() + caller_nonce.length(),
604        }
605        .encode(&mut rlp_buf);
606        caller.encode(&mut rlp_buf);
607        caller_nonce.encode(&mut rlp_buf);
608        Address::from_slice(&alloy_primitives::keccak256(&rlp_buf)[12..])
609    };
610
611    // Increment caller nonce
612    let _ = context
613        .journaled_state
614        .inner
615        .load_account(&mut context.journaled_state.database, caller);
616    if let Some(acc) = context.journaled_state.inner.state.get_mut(&caller) {
617        acc.info.nonce += 1;
618        context
619            .journaled_state
620            .inner
621            .nonce_bump_journal_entry(caller);
622    }
623
624    // Transfer endowment
625    if !endowment.is_zero()
626        && context
627            .journaled_state
628            .inner
629            .transfer(
630                &mut context.journaled_state.database,
631                caller,
632                created_address,
633                endowment,
634            )
635            .is_err()
636    {
637        context.journaled_state.inner.checkpoint_revert(checkpoint);
638        return SubCreateResult {
639            address: None,
640            output: Vec::new(),
641            gas_cost: gas,
642        };
643    }
644
645    // Run init code as EVM
646    let init_inputs = CallInputs {
647        input: CallInput::Bytes(code.to_vec().into()),
648        gas_limit: gas,
649        target_address: created_address,
650        bytecode_address: created_address,
651        caller,
652        value: revm::interpreter::CallValue::Transfer(endowment),
653        scheme: CallScheme::Call,
654        is_static: false,
655        return_memory_offset: 0..0,
656        known_bytecode: None,
657    };
658
659    let result = run_evm_bytecode(context, &init_inputs, code, gas);
660    let success = result.result.is_ok();
661    let gas_used = gas.saturating_sub(result.gas.remaining());
662
663    if success {
664        // Store the returned bytecode as the contract's code
665        let deployed_code = result.output.to_vec();
666        let code_hash = alloy_primitives::keccak256(&deployed_code);
667        let bytecode = revm::bytecode::Bytecode::new_raw(deployed_code.into());
668        // Ensure the account is loaded into state
669        let _ = context
670            .journaled_state
671            .inner
672            .load_account(&mut context.journaled_state.database, created_address);
673        context
674            .journaled_state
675            .inner
676            .set_code_with_hash(created_address, bytecode, code_hash);
677        context.journaled_state.inner.checkpoint_commit();
678        SubCreateResult {
679            address: Some(created_address),
680            output: Vec::new(), // success doesn't return data
681            gas_cost: gas_used,
682        }
683    } else {
684        let output = result.output.to_vec();
685        context.journaled_state.inner.checkpoint_revert(checkpoint);
686        SubCreateResult {
687            address: None,
688            output, // revert returns data
689            gas_cost: gas_used,
690        }
691    }
692}
693
694/// Run EVM bytecode from a Stylus sub-call.
695///
696/// Creates an interpreter and runs in a loop, dispatching nested CALL/CREATE
697/// actions through the Stylus call trampoline (which in turn uses
698/// ArbPrecompilesMap for precompile/Stylus dispatch).
699fn run_evm_bytecode<BlockEnv, TxEnv, CfgEnv, DB, Chain>(
700    context: &mut revm::Context<BlockEnv, TxEnv, CfgEnv, DB, revm::Journal<DB>, Chain>,
701    inputs: &CallInputs,
702    bytecode: &[u8],
703    gas_limit: u64,
704) -> InterpreterResult
705where
706    BlockEnv: revm::context::Block,
707    TxEnv: revm::context::Transaction,
708    CfgEnv: revm::context::Cfg,
709    DB: Database,
710{
711    use revm::{
712        bytecode::Bytecode,
713        interpreter::{
714            interpreter::{ExtBytecode, InputsImpl},
715            FrameInput, InterpreterAction, SharedMemory,
716        },
717    };
718
719    let code = Bytecode::new_raw(bytecode.to_vec().into());
720    let ext_bytecode = ExtBytecode::new(code);
721
722    let call_value = inputs.value.get();
723    let interp_input = InputsImpl {
724        target_address: inputs.target_address,
725        bytecode_address: Some(inputs.bytecode_address),
726        caller_address: inputs.caller,
727        input: inputs.input.clone(),
728        call_value,
729    };
730
731    let spec = context.cfg.spec();
732
733    let mut interpreter = revm::interpreter::Interpreter::new(
734        SharedMemory::new(),
735        ext_bytecode,
736        interp_input,
737        inputs.is_static,
738        spec.clone().into(),
739        gas_limit,
740    );
741
742    // Build instruction table with our custom opcodes (BLOBBASEFEE, SELFDESTRUCT)
743    type Ctx<B, T, C, D, Ch> = revm::Context<B, T, C, D, revm::Journal<D>, Ch>;
744    let mut instructions = EthInstructions::<
745        EthInterpreter,
746        Ctx<BlockEnv, TxEnv, CfgEnv, DB, Chain>,
747    >::new_mainnet_with_spec(spec.into());
748    instructions.insert_instruction(
749        BLOBBASEFEE_OPCODE,
750        revm::interpreter::Instruction::new(arb_blob_basefee, 2),
751    );
752    instructions.insert_instruction(
753        SELFDESTRUCT_OPCODE,
754        revm::interpreter::Instruction::new(arb_selfdestruct, 5000),
755    );
756
757    // Run the interpreter in a loop, handling nested calls/creates
758    loop {
759        let action = interpreter.run_plain(&instructions.instruction_table, context);
760
761        match action {
762            InterpreterAction::Return(result) => {
763                return result;
764            }
765            InterpreterAction::NewFrame(FrameInput::Call(sub_call)) => {
766                // Dispatch nested call through our trampoline
767                let sub_result = stylus_call_trampoline::<BlockEnv, TxEnv, CfgEnv, DB, Chain>(
768                    context as *mut _ as *mut (),
769                    match sub_call.scheme {
770                        CallScheme::Call | CallScheme::CallCode => 0,
771                        CallScheme::DelegateCall => 1,
772                        CallScheme::StaticCall => 2,
773                    },
774                    sub_call.target_address,
775                    sub_call.caller,
776                    match &sub_call.input {
777                        CallInput::Bytes(b) => b,
778                        CallInput::SharedBuffer(_) => &[],
779                    },
780                    sub_call.gas_limit,
781                    sub_call.value.get(),
782                );
783
784                // Inject result back into interpreter (matching EthFrame::return_result)
785                let gas_remaining = sub_call.gas_limit.saturating_sub(sub_result.gas_cost);
786                let ins_result = if sub_result.success {
787                    InstructionResult::Return
788                } else {
789                    InstructionResult::Revert
790                };
791
792                let output: Bytes = sub_result.output.into();
793                let returned_len = output.len();
794                let mem_start = sub_call.return_memory_offset.start;
795                let mem_length = sub_call.return_memory_offset.len();
796                let target_len = mem_length.min(returned_len);
797
798                interpreter.return_data.set_buffer(output);
799
800                let item = if ins_result.is_ok() {
801                    U256::from(1)
802                } else {
803                    U256::ZERO
804                };
805                let _ = interpreter.stack.push(item);
806
807                if ins_result.is_ok_or_revert() {
808                    interpreter.gas.erase_cost(gas_remaining);
809                    if target_len > 0 {
810                        interpreter
811                            .memory
812                            .set(mem_start, &interpreter.return_data.buffer()[..target_len]);
813                    }
814                }
815
816                if ins_result.is_ok() {
817                    // No refund tracking for sub-calls in this simple loop
818                }
819            }
820            InterpreterAction::NewFrame(FrameInput::Create(sub_create)) => {
821                // Dispatch create through our trampoline
822                let salt = match sub_create.scheme() {
823                    revm::interpreter::CreateScheme::Create2 { salt } => {
824                        Some(B256::from(salt.to_be_bytes()))
825                    }
826                    _ => None,
827                };
828
829                let sub_result = stylus_create_trampoline::<BlockEnv, TxEnv, CfgEnv, DB, Chain>(
830                    context as *mut _ as *mut (),
831                    sub_create.caller(),
832                    sub_create.init_code(),
833                    sub_create.gas_limit(),
834                    sub_create.value(),
835                    salt,
836                );
837
838                let gas_remaining = sub_create.gas_limit().saturating_sub(sub_result.gas_cost);
839                let created_addr = sub_result.address;
840
841                let ins_result = if created_addr.is_some() {
842                    InstructionResult::Return
843                } else if !sub_result.output.is_empty() {
844                    InstructionResult::Revert
845                } else {
846                    InstructionResult::CreateInitCodeStartingEF00
847                };
848
849                let output: Bytes = sub_result.output.into();
850                interpreter.return_data.set_buffer(output);
851
852                // Push created address or zero
853                let item = match created_addr {
854                    Some(addr) => addr.into_word().into(),
855                    None => U256::ZERO,
856                };
857                let _ = interpreter.stack.push(item);
858
859                if ins_result.is_ok_or_revert() {
860                    interpreter.gas.erase_cost(gas_remaining);
861                }
862            }
863            InterpreterAction::NewFrame(FrameInput::Empty) => {
864                // Should not happen
865                return InterpreterResult::new(
866                    InstructionResult::Revert,
867                    Bytes::new(),
868                    EvmGas::new(0),
869                );
870            }
871        }
872    }
873}
874
875// ── Stylus WASM dispatch ────────────────────────────────────────────
876
877/// Execute a Stylus WASM program by creating a NativeInstance and running it.
878///
879/// Validates the program, computes upfront gas costs (memory pages + init/cached
880/// gas), deducts them, then runs the WASM.
881fn execute_stylus_program<BlockEnv, TxEnv, CfgEnv, DB, Chain>(
882    context: &mut revm::Context<BlockEnv, TxEnv, CfgEnv, DB, revm::Journal<DB>, Chain>,
883    inputs: &CallInputs,
884    bytecode: &[u8],
885) -> InterpreterResult
886where
887    BlockEnv: revm::context::Block,
888    TxEnv: revm::context::Transaction,
889    CfgEnv: revm::context::Cfg,
890    DB: Database,
891{
892    use arbos::programs::types::UserOutcome;
893
894    let zero_gas = || EvmGas::new(0);
895
896    // Strip the 4-byte Stylus prefix to get the serialized module.
897    let (module_bytes, _version_byte) = match arb_stylus::strip_stylus_prefix(bytecode) {
898        Ok(v) => v,
899        Err(_) => {
900            return InterpreterResult::new(InstructionResult::Revert, Bytes::new(), zero_gas());
901        }
902    };
903
904    let code_hash = alloy_primitives::keccak256(bytecode);
905    let arbos_version = arb_precompiles::get_arbos_version();
906    let block_timestamp = arb_precompiles::get_block_timestamp();
907
908    // ── Read and validate program metadata ──────────────────────────
909    let params_word = match read_params_word(&mut context.journaled_state) {
910        Some(w) => w,
911        None => {
912            tracing::warn!(target: "stylus", "failed to read StylusParams from storage");
913            return InterpreterResult::new(InstructionResult::Revert, Bytes::new(), zero_gas());
914        }
915    };
916    let params = parse_stylus_params(&params_word, arbos_version);
917
918    let program_word = match read_program_word(&mut context.journaled_state, code_hash) {
919        Some(w) => w,
920        None => {
921            tracing::warn!(target: "stylus", codehash = %code_hash, "failed to read program data");
922            return InterpreterResult::new(InstructionResult::Revert, Bytes::new(), zero_gas());
923        }
924    };
925    let program = Program::from_storage(program_word, block_timestamp);
926
927    // Validate: program must be activated, correct version, not expired.
928    if program.version == 0 || program.version != params.version {
929        tracing::warn!(target: "stylus", codehash = %code_hash, program_ver = program.version, params_ver = params.version, "program version mismatch");
930        return InterpreterResult::new(InstructionResult::Revert, Bytes::new(), zero_gas());
931    }
932    let expiry_seconds = (params.expiry_days as u64) * 24 * 3600;
933    if program.age_seconds > expiry_seconds {
934        tracing::warn!(target: "stylus", codehash = %code_hash, "program expired");
935        return InterpreterResult::new(InstructionResult::Revert, Bytes::new(), zero_gas());
936    }
937
938    // ── Compute and deduct upfront gas costs ────────────────────────
939    let (pages_open, _pages_ever) = get_stylus_pages();
940    let upfront_cost = stylus_call_gas_cost(&params, &program, pages_open);
941    let total_gas = inputs.gas_limit;
942
943    if total_gas < upfront_cost {
944        return InterpreterResult::new(InstructionResult::OutOfGas, Bytes::new(), zero_gas());
945    }
946    let gas_for_wasm = total_gas - upfront_cost;
947
948    let stylus_config = StylusConfig::new(params.version, params.max_stack_depth, params.ink_price);
949
950    // ── Track reentrancy ────────────────────────────────────────────
951    let target_addr = inputs.target_address;
952    let is_delegate = matches!(
953        inputs.scheme,
954        CallScheme::DelegateCall | CallScheme::CallCode
955    );
956    let reentrant = if !is_delegate {
957        push_stylus_program(target_addr)
958    } else {
959        false
960    };
961
962    // Build EvmData from the execution context.
963    let mut evm_data = build_evm_data(context, inputs);
964    evm_data.reentrant = reentrant as u32;
965    evm_data.cached = program.cached;
966    evm_data.module_hash = code_hash;
967
968    // Track pages — add this program's footprint.
969    let (prev_open, _prev_ever) = add_stylus_pages(program.footprint);
970
971    // Create the type-erased StylusEvmApi bridge.
972    let journal_ptr = &mut context.journaled_state as *mut revm::Journal<DB>;
973    let is_static = inputs.is_static || matches!(inputs.scheme, CallScheme::StaticCall);
974    let ctx_ptr = context as *mut _ as *mut ();
975    let caller = inputs.caller;
976    let call_value = inputs.value.get();
977    let evm_api = unsafe {
978        StylusEvmApi::new(
979            journal_ptr,
980            target_addr,
981            caller,
982            call_value,
983            is_static,
984            params.free_pages,
985            params.page_gas,
986            ctx_ptr,
987            Some(stylus_call_trampoline::<BlockEnv, TxEnv, CfgEnv, DB, Chain>),
988            Some(stylus_create_trampoline::<BlockEnv, TxEnv, CfgEnv, DB, Chain>),
989        )
990    };
991
992    // Try the module cache first; compile from WASM on miss and populate cache.
993    let long_term_tag = if program.cached { 1u32 } else { 0u32 };
994    let mut instance = if let Some((module, store)) =
995        arb_stylus::cache::InitCache::get(code_hash, params.version, long_term_tag, false)
996    {
997        let compile = arb_stylus::CompileConfig::version(params.version, false);
998        let env = arb_stylus::env::WasmEnv::new(compile, Some(stylus_config), evm_api, evm_data);
999        match arb_stylus::NativeInstance::from_module(module, store, env) {
1000            Ok(inst) => inst,
1001            Err(e) => {
1002                tracing::warn!(target: "stylus", codehash = %code_hash, err = %e, "failed from cached module");
1003                set_stylus_pages_open(prev_open);
1004                if !is_delegate {
1005                    pop_stylus_program(target_addr);
1006                }
1007                return InterpreterResult::new(InstructionResult::Revert, Bytes::new(), zero_gas());
1008            }
1009        }
1010    } else {
1011        // Compile from WASM source.
1012        let compile = arb_stylus::CompileConfig::version(params.version, false);
1013        match arb_stylus::NativeInstance::from_bytes(
1014            module_bytes,
1015            evm_api,
1016            evm_data,
1017            &compile,
1018            stylus_config,
1019        ) {
1020            Ok(inst) => inst,
1021            Err(e) => {
1022                tracing::warn!(target: "stylus", codehash = %code_hash, err = %e, "failed to compile WASM");
1023                set_stylus_pages_open(prev_open);
1024                if !is_delegate {
1025                    pop_stylus_program(target_addr);
1026                }
1027                return InterpreterResult::new(InstructionResult::Revert, Bytes::new(), zero_gas());
1028            }
1029        }
1030    };
1031
1032    // Convert EVM gas (after upfront deduction) to ink.
1033    let ink = stylus_config.pricing.gas_to_ink(StylusGas(gas_for_wasm));
1034
1035    // Get calldata from CallInput enum.
1036    let calldata: &[u8] = match &inputs.input {
1037        CallInput::Bytes(bytes) => bytes,
1038        CallInput::SharedBuffer(_) => &[],
1039    };
1040
1041    // Execute the WASM program.
1042    let outcome = match instance.run_main(calldata, stylus_config, ink) {
1043        Ok(outcome) => outcome,
1044        Err(e) => {
1045            tracing::warn!(target: "stylus", codehash = %code_hash, err = %e, "WASM execution failed");
1046            set_stylus_pages_open(prev_open);
1047            if !is_delegate {
1048                pop_stylus_program(target_addr);
1049            }
1050            return InterpreterResult::new(InstructionResult::Revert, Bytes::new(), zero_gas());
1051        }
1052    };
1053
1054    // Restore page count and pop reentrancy.
1055    set_stylus_pages_open(prev_open);
1056    if !is_delegate {
1057        pop_stylus_program(target_addr);
1058    }
1059
1060    // Convert remaining ink back to gas.
1061    let ink_left = match instance.ink_left() {
1062        arb_stylus::MachineMeter::Ready(ink_val) => ink_val,
1063        arb_stylus::MachineMeter::Exhausted => arb_stylus::Ink(0),
1064    };
1065    let gas_left = stylus_config.pricing.ink_to_gas(ink_left).0;
1066
1067    // Return data cost parity with EVM (ArbOS >= StylusFixes).
1068    let output: Bytes = instance.env().outs.clone().into();
1069    let gas_left = if !output.is_empty()
1070        && arbos_version >= arb_chainspec::arbos_version::ARBOS_VERSION_STYLUS_FIXES
1071    {
1072        let evm_cost = arbos::programs::types::evm_memory_cost(output.len() as u64);
1073        if total_gas < evm_cost {
1074            0
1075        } else {
1076            gas_left.min(total_gas - evm_cost)
1077        }
1078    } else {
1079        gas_left
1080    };
1081
1082    let gas_result = EvmGas::new(gas_left);
1083
1084    // Map UserOutcome to InterpreterResult.
1085    match outcome {
1086        UserOutcome::Success => {
1087            InterpreterResult::new(InstructionResult::Return, output, gas_result)
1088        }
1089        UserOutcome::Revert => {
1090            InterpreterResult::new(InstructionResult::Revert, output, gas_result)
1091        }
1092        UserOutcome::OutOfInk => {
1093            InterpreterResult::new(InstructionResult::OutOfGas, Bytes::new(), zero_gas())
1094        }
1095        UserOutcome::OutOfStack => {
1096            InterpreterResult::new(InstructionResult::CallTooDeep, Bytes::new(), zero_gas())
1097        }
1098        UserOutcome::Failure => {
1099            InterpreterResult::new(InstructionResult::Revert, Bytes::new(), zero_gas())
1100        }
1101    }
1102}
1103
1104/// Build [`EvmData`] from the current execution context.
1105fn build_evm_data<BlockEnv, TxEnv, CfgEnv, DB, Chain>(
1106    context: &revm::Context<BlockEnv, TxEnv, CfgEnv, DB, revm::Journal<DB>, Chain>,
1107    inputs: &CallInputs,
1108) -> EvmData
1109where
1110    BlockEnv: revm::context::Block,
1111    TxEnv: revm::context::Transaction,
1112    CfgEnv: revm::context::Cfg,
1113    DB: Database,
1114{
1115    let basefee = U256::from(context.block.basefee());
1116    let gas_price = U256::from(context.tx.gas_price());
1117    let value = inputs.value.get();
1118
1119    EvmData {
1120        arbos_version: arb_precompiles::get_arbos_version(),
1121        block_basefee: B256::from(basefee.to_be_bytes()),
1122        chain_id: context.cfg.chain_id(),
1123        block_coinbase: context.block.beneficiary(),
1124        block_gas_limit: context.block.gas_limit(),
1125        block_number: context.block.number().saturating_to(),
1126        block_timestamp: context.block.timestamp().saturating_to(),
1127        contract_address: inputs.target_address,
1128        module_hash: alloy_primitives::keccak256(b""),
1129        msg_sender: inputs.caller,
1130        msg_value: B256::from(value.to_be_bytes()),
1131        tx_gas_price: B256::from(gas_price.to_be_bytes()),
1132        tx_origin: context.tx.caller(),
1133        reentrant: 0,
1134        cached: false,
1135        tracing: false,
1136    }
1137}
1138
1139// ── Depth-tracking precompile provider ─────────────────────────────
1140
1141/// Wraps [`PrecompilesMap`] to set the thread-local EVM call depth before
1142/// each precompile invocation. The depth is read from revm's journal, which
1143/// mirrors the `evm.Depth()` counter used by `ArbSys.isTopLevelCall`.
1144#[derive(Clone, Debug)]
1145pub struct ArbPrecompilesMap(pub PrecompilesMap);
1146
1147impl<BlockEnv, TxEnv, CfgEnv, DB, Chain>
1148    PrecompileProvider<revm::Context<BlockEnv, TxEnv, CfgEnv, DB, revm::Journal<DB>, Chain>>
1149    for ArbPrecompilesMap
1150where
1151    BlockEnv: revm::context::Block,
1152    TxEnv: revm::context::Transaction,
1153    CfgEnv: revm::context::Cfg,
1154    DB: Database,
1155{
1156    type Output = InterpreterResult;
1157
1158    fn set_spec(&mut self, spec: CfgEnv::Spec) -> bool {
1159        <PrecompilesMap as PrecompileProvider<
1160            revm::Context<BlockEnv, TxEnv, CfgEnv, DB, revm::Journal<DB>, Chain>,
1161        >>::set_spec(&mut self.0, spec)
1162    }
1163
1164    fn run(
1165        &mut self,
1166        context: &mut revm::Context<BlockEnv, TxEnv, CfgEnv, DB, revm::Journal<DB>, Chain>,
1167        inputs: &CallInputs,
1168    ) -> Result<Option<Self::Output>, String> {
1169        // Sync the thread-local depth from revm's journal before the precompile runs.
1170        arb_precompiles::set_evm_depth(context.journaled_state.inner.depth);
1171
1172        // Check precompiles first.
1173        if let result @ Some(_) = <PrecompilesMap as PrecompileProvider<
1174            revm::Context<BlockEnv, TxEnv, CfgEnv, DB, revm::Journal<DB>, Chain>,
1175        >>::run(&mut self.0, context, inputs)?
1176        {
1177            return Ok(result);
1178        }
1179
1180        // Check for Stylus WASM programs (active at ArbOS v31+).
1181        let arbos_version = arb_precompiles::get_arbos_version();
1182        if arbos_version >= arb_chainspec::arbos_version::ARBOS_VERSION_STYLUS {
1183            // Load code for the target address
1184            let code_opt = context
1185                .journaled_state
1186                .inner
1187                .load_code(
1188                    &mut context.journaled_state.database,
1189                    inputs.bytecode_address,
1190                )
1191                .ok()
1192                .and_then(|acc| acc.data.info.code.as_ref().map(|c| c.original_bytes()));
1193
1194            if let Some(bytecode) = code_opt {
1195                if arb_stylus::is_stylus_program(&bytecode) {
1196                    return Ok(Some(execute_stylus_program(context, inputs, &bytecode)));
1197                }
1198            }
1199        }
1200
1201        Ok(None)
1202    }
1203
1204    fn warm_addresses(&self) -> Box<impl Iterator<Item = Address>> {
1205        <PrecompilesMap as PrecompileProvider<
1206            revm::Context<BlockEnv, TxEnv, CfgEnv, DB, revm::Journal<DB>, Chain>,
1207        >>::warm_addresses(&self.0)
1208    }
1209
1210    fn contains(&self, address: &Address) -> bool {
1211        <PrecompilesMap as PrecompileProvider<
1212            revm::Context<BlockEnv, TxEnv, CfgEnv, DB, revm::Journal<DB>, Chain>,
1213        >>::contains(&self.0, address)
1214    }
1215}
1216
1217// ── ArbEvm ─────────────────────────────────────────────────────────
1218
1219/// Arbitrum EVM wrapper with depth-tracking precompiles and custom opcodes.
1220pub struct ArbEvm<DB: Database, I> {
1221    inner: alloy_evm::EthEvm<DB, I, ArbPrecompilesMap>,
1222}
1223
1224impl<DB, I> ArbEvm<DB, I>
1225where
1226    DB: Database,
1227{
1228    pub fn new(inner: alloy_evm::EthEvm<DB, I, ArbPrecompilesMap>) -> Self {
1229        Self { inner }
1230    }
1231
1232    pub fn into_inner(self) -> alloy_evm::EthEvm<DB, I, ArbPrecompilesMap> {
1233        self.inner
1234    }
1235}
1236
1237impl<DB, I> Evm for ArbEvm<DB, I>
1238where
1239    DB: Database,
1240    I: revm::inspector::Inspector<EthEvmContext<DB>>,
1241{
1242    type DB = DB;
1243    type Tx = ArbTransaction;
1244    type Error = EVMError<<DB as revm::Database>::Error>;
1245    type HaltReason = HaltReason;
1246    type Spec = SpecId;
1247    type Precompiles = PrecompilesMap;
1248    type Inspector = I;
1249    type BlockEnv = revm::context::BlockEnv;
1250
1251    fn block(&self) -> &revm::context::BlockEnv {
1252        self.inner.block()
1253    }
1254
1255    fn chain_id(&self) -> u64 {
1256        self.inner.chain_id()
1257    }
1258
1259    fn transact_raw(
1260        &mut self,
1261        tx: Self::Tx,
1262    ) -> Result<ResultAndState<Self::HaltReason>, Self::Error> {
1263        self.inner.transact_raw(tx.into_inner())
1264    }
1265
1266    fn transact_system_call(
1267        &mut self,
1268        caller: Address,
1269        contract: Address,
1270        data: Bytes,
1271    ) -> Result<ResultAndState<Self::HaltReason>, Self::Error> {
1272        self.inner.transact_system_call(caller, contract, data)
1273    }
1274
1275    fn finish(self) -> (Self::DB, EvmEnv<Self::Spec>) {
1276        self.inner.finish()
1277    }
1278
1279    fn set_inspector_enabled(&mut self, enabled: bool) {
1280        self.inner.set_inspector_enabled(enabled)
1281    }
1282
1283    fn components(&self) -> (&Self::DB, &Self::Inspector, &Self::Precompiles) {
1284        let (db, inspector, arb_precompiles) = self.inner.components();
1285        (db, inspector, &arb_precompiles.0)
1286    }
1287
1288    fn components_mut(&mut self) -> (&mut Self::DB, &mut Self::Inspector, &mut Self::Precompiles) {
1289        let (db, inspector, arb_precompiles) = self.inner.components_mut();
1290        (db, inspector, &mut arb_precompiles.0)
1291    }
1292}
1293
1294// ── ArbEvmFactory ──────────────────────────────────────────────────
1295
1296/// Factory for creating Arbitrum EVM instances with custom precompiles.
1297#[derive(Default, Debug, Clone, Copy)]
1298pub struct ArbEvmFactory(pub alloy_evm::EthEvmFactory);
1299
1300impl ArbEvmFactory {
1301    pub fn new() -> Self {
1302        Self::default()
1303    }
1304}
1305
1306fn build_arb_evm<DB: Database, I>(
1307    inner: revm::context::Evm<
1308        EthEvmContext<DB>,
1309        I,
1310        EthInstructions<EthInterpreter, EthEvmContext<DB>>,
1311        PrecompilesMap,
1312        EthFrame,
1313    >,
1314    inspect: bool,
1315) -> ArbEvm<DB, I> {
1316    let revm::context::Evm {
1317        ctx,
1318        inspector,
1319        mut instruction,
1320        mut precompiles,
1321        frame_stack: _,
1322    } = inner;
1323
1324    instruction.insert_instruction(
1325        BLOBBASEFEE_OPCODE,
1326        revm::interpreter::Instruction::new(arb_blob_basefee, 2),
1327    );
1328    instruction.insert_instruction(
1329        SELFDESTRUCT_OPCODE,
1330        revm::interpreter::Instruction::new(arb_selfdestruct, 5000),
1331    );
1332    // NUMBER returns L1 block number from ArbOS state (updated by StartBlock),
1333    // not the mixHash L1 block number which can differ.
1334    instruction.insert_instruction(
1335        NUMBER_OPCODE,
1336        revm::interpreter::Instruction::new(arb_number, 2),
1337    );
1338    // BLOCKHASH uses L1 block number for the 256-block range check,
1339    // matching Nitro where block.number IS the L1 block number.
1340    instruction.insert_instruction(
1341        BLOCKHASH_OPCODE,
1342        revm::interpreter::Instruction::new(arb_blockhash, 20),
1343    );
1344    // BALANCE adjusts the sender's balance by the poster fee correction,
1345    // matching Nitro's BuyGas which charges the full gas_limit.
1346    // BALANCE/SELFBALANCE adjust sender's balance by the poster fee correction
1347    // to match Nitro's BuyGas which charges the full gas_limit.
1348    instruction.insert_instruction(
1349        BALANCE_OPCODE,
1350        revm::interpreter::Instruction::new(arb_balance, 0),
1351    );
1352    instruction.insert_instruction(
1353        SELFBALANCE_OPCODE,
1354        revm::interpreter::Instruction::new(arb_selfbalance, 5),
1355    );
1356    register_arb_precompiles(&mut precompiles);
1357    let arb_precompiles = ArbPrecompilesMap(precompiles);
1358
1359    let revm_evm =
1360        revm::context::Evm::new_with_inspector(ctx, inspector, instruction, arb_precompiles);
1361    let eth_evm = alloy_evm::eth::EthEvm::new(revm_evm, inspect);
1362    ArbEvm::new(eth_evm)
1363}
1364
1365impl EvmFactory for ArbEvmFactory {
1366    type Evm<DB: Database, I: revm::inspector::Inspector<EthEvmContext<DB>>> = ArbEvm<DB, I>;
1367    type Context<DB: Database> = EthEvmContext<DB>;
1368    type Tx = ArbTransaction;
1369    type Error<DBError: core::error::Error + Send + Sync + 'static> = EVMError<DBError>;
1370    type HaltReason = HaltReason;
1371    type Spec = SpecId;
1372    type Precompiles = PrecompilesMap;
1373    type BlockEnv = revm::context::BlockEnv;
1374
1375    fn create_evm<DB: Database>(
1376        &self,
1377        db: DB,
1378        input: EvmEnv<Self::Spec>,
1379    ) -> Self::Evm<DB, NoOpInspector> {
1380        let eth_evm = self.0.create_evm(db, input);
1381        build_arb_evm(eth_evm.into_inner(), false)
1382    }
1383
1384    fn create_evm_with_inspector<DB: Database, I: revm::inspector::Inspector<Self::Context<DB>>>(
1385        &self,
1386        db: DB,
1387        input: EvmEnv<Self::Spec>,
1388        inspector: I,
1389    ) -> Self::Evm<DB, I> {
1390        let eth_evm = self.0.create_evm_with_inspector(db, input, inspector);
1391        build_arb_evm(eth_evm.into_inner(), true)
1392    }
1393}