arb_stylus/
evm_api_impl.rs

1use alloy_primitives::{Address, Log, B256, U256};
2use arbos::programs::memory::MemoryModel;
3use revm::Database;
4
5use crate::{
6    evm_api::{CreateResponse, EvmApi, UserOutcomeKind},
7    ink::Gas,
8    pages,
9};
10
11/// EIP-2929 gas costs for storage operations.
12const COLD_SLOAD_COST: u64 = 2100;
13const WARM_STORAGE_READ_COST: u64 = 100;
14const COLD_ACCOUNT_ACCESS_COST: u64 = 2600;
15const WARM_ACCOUNT_ACCESS_COST: u64 = 100;
16
17/// Extra gas charged when loading account code in Stylus.
18/// Matches Go: `cfg.MaxCodeSize() / params.DefaultMaxCodeSize * params.ExtcodeSizeGasEIP150`
19/// = 24576 / 24576 * 700 = 700.
20const WASM_EXT_CODE_COST: u64 = 700;
21
22// ── Type-erased journal access ──────────────────────────────────────
23
24/// Flattened SSTORE result without revm generics.
25pub struct SStoreInfo {
26    pub is_cold: bool,
27    pub original_value: U256,
28    pub present_value: U256,
29    pub new_value: U256,
30}
31
32/// Object-safe trait wrapping `Journal<DB>` operations needed by Stylus.
33///
34/// By erasing the `DB` type parameter, [`StylusEvmApi`] becomes non-generic
35/// and trivially satisfies `'static` (required by wasmer's `FunctionEnv`).
36pub trait JournalAccess {
37    fn sload(&mut self, addr: Address, key: U256) -> eyre::Result<(U256, bool)>;
38    fn sstore(&mut self, addr: Address, key: U256, value: U256) -> eyre::Result<SStoreInfo>;
39    fn tload(&mut self, addr: Address, key: U256) -> U256;
40    fn tstore(&mut self, addr: Address, key: U256, value: U256);
41    fn log(&mut self, log: Log);
42    fn account_balance(&mut self, addr: Address) -> eyre::Result<(U256, bool)>;
43    fn account_code(&mut self, addr: Address) -> eyre::Result<(Vec<u8>, bool)>;
44    fn account_codehash(&mut self, addr: Address) -> eyre::Result<(B256, bool)>;
45    fn address_in_access_list(&self, addr: Address) -> bool;
46    fn add_address_to_access_list(&mut self, addr: Address);
47    fn is_account_empty(&mut self, addr: Address) -> eyre::Result<bool>;
48}
49
50impl<DB: Database> JournalAccess for revm::Journal<DB> {
51    fn sload(&mut self, addr: Address, key: U256) -> eyre::Result<(U256, bool)> {
52        let result = self
53            .inner
54            .sload(&mut self.database, addr, key, false)
55            .map_err(|e| eyre::eyre!("sload failed: {e:?}"))?;
56        Ok((result.data, result.is_cold))
57    }
58
59    fn sstore(&mut self, addr: Address, key: U256, value: U256) -> eyre::Result<SStoreInfo> {
60        let result = self
61            .inner
62            .sstore(&mut self.database, addr, key, value, false)
63            .map_err(|e| eyre::eyre!("sstore failed: {e:?}"))?;
64        Ok(SStoreInfo {
65            is_cold: result.is_cold,
66            original_value: result.data.original_value,
67            present_value: result.data.present_value,
68            new_value: result.data.new_value,
69        })
70    }
71
72    fn tload(&mut self, addr: Address, key: U256) -> U256 {
73        self.inner.tload(addr, key)
74    }
75
76    fn tstore(&mut self, addr: Address, key: U256, value: U256) {
77        self.inner.tstore(addr, key, value);
78    }
79
80    fn log(&mut self, log: Log) {
81        self.inner.log(log);
82    }
83
84    fn account_balance(&mut self, addr: Address) -> eyre::Result<(U256, bool)> {
85        let result = self
86            .inner
87            .load_account(&mut self.database, addr)
88            .map_err(|e| eyre::eyre!("load_account failed: {e:?}"))?;
89        Ok((result.data.info.balance, result.is_cold))
90    }
91
92    fn account_code(&mut self, addr: Address) -> eyre::Result<(Vec<u8>, bool)> {
93        let result = self
94            .inner
95            .load_code(&mut self.database, addr)
96            .map_err(|e| eyre::eyre!("load_code failed: {e:?}"))?;
97        let code = result
98            .data
99            .info
100            .code
101            .as_ref()
102            .map(|c: &revm::bytecode::Bytecode| c.original_bytes().to_vec())
103            .unwrap_or_default();
104        Ok((code, result.is_cold))
105    }
106
107    fn account_codehash(&mut self, addr: Address) -> eyre::Result<(B256, bool)> {
108        let result = self
109            .inner
110            .load_account(&mut self.database, addr)
111            .map_err(|e| eyre::eyre!("load_account failed: {e:?}"))?;
112        Ok((result.data.info.code_hash, result.is_cold))
113    }
114
115    fn address_in_access_list(&self, addr: Address) -> bool {
116        // An address is "warm" if it's in the loaded state or warm_addresses
117        self.inner.state.contains_key(&addr) || self.inner.warm_addresses.is_warm(&addr)
118    }
119
120    fn add_address_to_access_list(&mut self, addr: Address) {
121        // Load the account to mark it warm in the state map.
122        let _ = self.inner.load_account(&mut self.database, addr);
123    }
124
125    fn is_account_empty(&mut self, addr: Address) -> eyre::Result<bool> {
126        let result = self
127            .inner
128            .load_account(&mut self.database, addr)
129            .map_err(|e| eyre::eyre!("load_account failed: {e:?}"))?;
130        let acc = result.data;
131        Ok(acc.info.balance.is_zero()
132            && acc.info.nonce == 0
133            && acc.info.code_hash == revm::primitives::KECCAK_EMPTY)
134    }
135}
136
137// ── StylusEvmApi ────────────────────────────────────────────────────
138
139/// Result from a sub-call (CALL, DELEGATECALL, STATICCALL).
140pub struct SubCallResult {
141    pub output: Vec<u8>,
142    pub gas_cost: u64,
143    pub success: bool,
144}
145
146/// Result from a CREATE/CREATE2 operation.
147pub struct SubCreateResult {
148    pub address: Option<Address>,
149    pub output: Vec<u8>,
150    pub gas_cost: u64,
151}
152
153/// Type-erased function pointer for executing sub-calls from Stylus.
154///
155/// Parameters: (ctx_ptr, call_type, contract, caller, input, gas, value)
156/// call_type: 0=CALL, 1=DELEGATECALL, 2=STATICCALL
157pub type DoCallFn = fn(*mut (), u8, Address, Address, &[u8], u64, U256) -> SubCallResult;
158
159/// Type-erased function pointer for executing CREATE/CREATE2 from Stylus.
160///
161/// Parameters: (ctx_ptr, caller, code, gas, endowment, salt)
162/// salt=None for CREATE, Some for CREATE2.
163pub type DoCreateFn = fn(*mut (), Address, &[u8], u64, U256, Option<B256>) -> SubCreateResult;
164
165/// Concrete [`EvmApi`] bridging WASM host function calls to revm's journaled state.
166///
167/// Uses a type-erased raw pointer to [`JournalAccess`] so that the `DB` generic
168/// parameter is erased. This allows `StylusEvmApi` to be `'static` without
169/// requiring `DB: 'static`, which is needed for wasmer's `FunctionEnv`.
170///
171/// # Safety
172///
173/// Wasmer executes WASM programs synchronously on the calling thread, so no
174/// cross-thread sharing occurs despite the `Send` bound on [`EvmApi`].
175/// The raw pointer must remain valid for the lifetime of the Stylus execution.
176pub struct StylusEvmApi {
177    /// Type-erased raw pointer to the journal (implements [`JournalAccess`]).
178    journal: *mut dyn JournalAccess,
179    /// The contract address being executed.
180    address: Address,
181    /// The caller (msg.sender) of the current contract.
182    caller: Address,
183    /// Value of the current call (needed for DELEGATECALL forwarding).
184    call_value: U256,
185    /// Cached storage writes (key, value pairs), flushed on demand.
186    storage_cache: Vec<(B256, B256)>,
187    /// Return data from the last sub-call.
188    return_data: Vec<u8>,
189    /// Whether the current execution context is read-only (STATICCALL).
190    read_only: bool,
191    /// MemoryModel params for add_pages gas computation.
192    free_pages: u16,
193    page_gas: u16,
194    /// Type-erased context pointer and callbacks for sub-calls.
195    ctx_ptr: *mut (),
196    do_call: Option<DoCallFn>,
197    do_create: Option<DoCreateFn>,
198}
199
200// Safety: Wasmer executes synchronously on the calling thread. No cross-thread access occurs.
201unsafe impl Send for StylusEvmApi {}
202
203impl StylusEvmApi {
204    /// Create a new StylusEvmApi from a raw pointer to a revm Journal.
205    ///
206    /// # Safety
207    ///
208    /// The `journal` pointer must remain valid for the lifetime of this struct.
209    /// The caller must ensure exclusive mutable access through this pointer.
210    /// If `ctx_ptr` is provided, it must also remain valid.
211    pub unsafe fn new<DB: Database>(
212        journal: *mut revm::Journal<DB>,
213        address: Address,
214        caller: Address,
215        call_value: U256,
216        read_only: bool,
217        free_pages: u16,
218        page_gas: u16,
219        ctx_ptr: *mut (),
220        do_call: Option<DoCallFn>,
221        do_create: Option<DoCreateFn>,
222    ) -> Self {
223        // Annotate with '_ to avoid the default 'static bound on dyn trait objects.
224        // Raw pointers carry no lifetime; this is safe as long as the pointer
225        // remains valid for the duration of StylusEvmApi's use (guaranteed by
226        // synchronous WASM execution scoped within the caller).
227        let journal: *mut dyn JournalAccess = {
228            let r: &mut (dyn JournalAccess + '_) = &mut *journal;
229            #[allow(clippy::unnecessary_cast)]
230            {
231                r as *mut (dyn JournalAccess + '_) as *mut dyn JournalAccess
232            }
233        };
234        Self {
235            journal,
236            address,
237            caller,
238            call_value,
239            storage_cache: Vec::new(),
240            return_data: Vec::new(),
241            read_only,
242            free_pages,
243            page_gas,
244            ctx_ptr,
245            do_call,
246            do_create,
247        }
248    }
249
250    /// Get a mutable reference to the type-erased journal.
251    fn journal(&mut self) -> &mut dyn JournalAccess {
252        unsafe { &mut *self.journal }
253    }
254}
255
256impl std::fmt::Debug for StylusEvmApi {
257    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
258        f.debug_struct("StylusEvmApi")
259            .field("address", &self.address)
260            .field("read_only", &self.read_only)
261            .field("cache_size", &self.storage_cache.len())
262            .finish()
263    }
264}
265
266impl EvmApi for StylusEvmApi {
267    fn get_bytes32(&mut self, key: B256, _evm_api_gas_to_use: Gas) -> eyre::Result<(B256, Gas)> {
268        let storage_key = U256::from_be_bytes(key.0);
269        let addr = self.address;
270        let (value_u256, is_cold) = self.journal().sload(addr, storage_key)?;
271        let value = B256::from(value_u256.to_be_bytes());
272        let gas_cost = if is_cold {
273            COLD_SLOAD_COST
274        } else {
275            WARM_STORAGE_READ_COST
276        };
277        Ok((value, Gas(gas_cost)))
278    }
279
280    fn cache_bytes32(&mut self, key: B256, value: B256) -> eyre::Result<Gas> {
281        self.storage_cache.push((key, value));
282        Ok(Gas(0))
283    }
284
285    fn flush_storage_cache(
286        &mut self,
287        clear: bool,
288        gas_left: Gas,
289    ) -> eyre::Result<(Gas, UserOutcomeKind)> {
290        let entries: Vec<(B256, B256)> = if clear {
291            std::mem::take(&mut self.storage_cache)
292        } else {
293            self.storage_cache.clone()
294        };
295
296        if self.read_only && !entries.is_empty() {
297            return Ok((Gas(0), UserOutcomeKind::Failure));
298        }
299
300        let mut total_gas = 0u64;
301        let mut remaining = gas_left.0;
302
303        for (key, value) in &entries {
304            let storage_key = U256::from_be_bytes(key.0);
305            let storage_value = U256::from_be_bytes(value.0);
306
307            let addr = self.address;
308            let info = self.journal().sstore(addr, storage_key, storage_value)?;
309
310            let sstore_cost = sstore_gas_cost(&info);
311            if sstore_cost > remaining {
312                return Ok((Gas(total_gas), UserOutcomeKind::OutOfInk));
313            }
314            remaining -= sstore_cost;
315            total_gas += sstore_cost;
316        }
317
318        Ok((Gas(total_gas), UserOutcomeKind::Success))
319    }
320
321    fn get_transient_bytes32(&mut self, key: B256) -> eyre::Result<B256> {
322        let storage_key = U256::from_be_bytes(key.0);
323        let addr = self.address;
324        let value = self.journal().tload(addr, storage_key);
325        Ok(B256::from(value.to_be_bytes()))
326    }
327
328    fn set_transient_bytes32(&mut self, key: B256, value: B256) -> eyre::Result<UserOutcomeKind> {
329        if self.read_only {
330            return Ok(UserOutcomeKind::Failure);
331        }
332        let storage_key = U256::from_be_bytes(key.0);
333        let storage_value = U256::from_be_bytes(value.0);
334        let addr = self.address;
335        self.journal().tstore(addr, storage_key, storage_value);
336        Ok(UserOutcomeKind::Success)
337    }
338
339    fn contract_call(
340        &mut self,
341        contract: Address,
342        calldata: &[u8],
343        gas_left: Gas,
344        gas_req: Gas,
345        value: U256,
346    ) -> eyre::Result<(u32, Gas, UserOutcomeKind)> {
347        if self.read_only && !value.is_zero() {
348            self.return_data = Vec::new();
349            return Ok((0, Gas(0), UserOutcomeKind::Failure));
350        }
351
352        let do_call = match self.do_call {
353            Some(f) => f,
354            None => {
355                self.return_data = b"sub-calls not available".to_vec();
356                return Ok((
357                    self.return_data.len() as u32,
358                    Gas(0),
359                    UserOutcomeKind::Failure,
360                ));
361            }
362        };
363
364        // WasmCallCost equivalent: warm/cold access + value transfer + new account
365        let (base_cost, oog) = wasm_call_cost(self.journal(), contract, &value, gas_left.0);
366        if oog {
367            self.return_data = Vec::new();
368            return Ok((0, Gas(gas_left.0), UserOutcomeKind::Failure));
369        }
370
371        // 63/64ths rule
372        let start_gas = gas_left.0.saturating_sub(base_cost) * 63 / 64;
373        let gas = start_gas.min(gas_req.0);
374
375        // Stipend for value transfers
376        let gas = if !value.is_zero() {
377            gas.saturating_add(2300) // CallStipend
378        } else {
379            gas
380        };
381
382        let result = (do_call)(
383            self.ctx_ptr,
384            0, // CALL
385            contract,
386            self.address, // caller = current contract
387            calldata,
388            gas,
389            value,
390        );
391
392        self.return_data = result.output;
393        // cost = baseCost + (gas_given - gas_returned) = baseCost + gas_used
394        let cost = base_cost.saturating_add(result.gas_cost);
395
396        let outcome = if result.success {
397            UserOutcomeKind::Success
398        } else {
399            UserOutcomeKind::Failure
400        };
401        Ok((self.return_data.len() as u32, Gas(cost), outcome))
402    }
403
404    fn delegate_call(
405        &mut self,
406        contract: Address,
407        calldata: &[u8],
408        gas_left: Gas,
409        gas_req: Gas,
410    ) -> eyre::Result<(u32, Gas, UserOutcomeKind)> {
411        let do_call = match self.do_call {
412            Some(f) => f,
413            None => {
414                self.return_data = b"sub-calls not available".to_vec();
415                return Ok((
416                    self.return_data.len() as u32,
417                    Gas(0),
418                    UserOutcomeKind::Failure,
419                ));
420            }
421        };
422
423        // For DELEGATECALL, no value transfer cost
424        let (base_cost, oog) = wasm_call_cost(self.journal(), contract, &U256::ZERO, gas_left.0);
425        if oog {
426            self.return_data = Vec::new();
427            return Ok((0, Gas(gas_left.0), UserOutcomeKind::Failure));
428        }
429
430        let start_gas = gas_left.0.saturating_sub(base_cost) * 63 / 64;
431        let gas = start_gas.min(gas_req.0);
432
433        let result = (do_call)(
434            self.ctx_ptr,
435            1, // DELEGATECALL
436            contract,
437            self.caller, // original caller
438            calldata,
439            gas,
440            self.call_value, // forward current call value
441        );
442
443        self.return_data = result.output;
444        // cost = baseCost + (gas_given - gas_returned) = baseCost + gas_used
445        let cost = base_cost.saturating_add(result.gas_cost);
446
447        let outcome = if result.success {
448            UserOutcomeKind::Success
449        } else {
450            UserOutcomeKind::Failure
451        };
452        Ok((self.return_data.len() as u32, Gas(cost), outcome))
453    }
454
455    fn static_call(
456        &mut self,
457        contract: Address,
458        calldata: &[u8],
459        gas_left: Gas,
460        gas_req: Gas,
461    ) -> eyre::Result<(u32, Gas, UserOutcomeKind)> {
462        let do_call = match self.do_call {
463            Some(f) => f,
464            None => {
465                self.return_data = b"sub-calls not available".to_vec();
466                return Ok((
467                    self.return_data.len() as u32,
468                    Gas(0),
469                    UserOutcomeKind::Failure,
470                ));
471            }
472        };
473
474        let (base_cost, oog) = wasm_call_cost(self.journal(), contract, &U256::ZERO, gas_left.0);
475        if oog {
476            self.return_data = Vec::new();
477            return Ok((0, Gas(gas_left.0), UserOutcomeKind::Failure));
478        }
479
480        let start_gas = gas_left.0.saturating_sub(base_cost) * 63 / 64;
481        let gas = start_gas.min(gas_req.0);
482
483        let result = (do_call)(
484            self.ctx_ptr,
485            2, // STATICCALL
486            contract,
487            self.address,
488            calldata,
489            gas,
490            U256::ZERO,
491        );
492
493        self.return_data = result.output;
494        // cost = baseCost + (gas_given - gas_returned) = baseCost + gas_used
495        let cost = base_cost.saturating_add(result.gas_cost);
496
497        let outcome = if result.success {
498            UserOutcomeKind::Success
499        } else {
500            UserOutcomeKind::Failure
501        };
502        Ok((self.return_data.len() as u32, Gas(cost), outcome))
503    }
504
505    fn create1(
506        &mut self,
507        code: Vec<u8>,
508        endowment: U256,
509        gas: Gas,
510    ) -> eyre::Result<(CreateResponse, u32, Gas)> {
511        if self.read_only {
512            self.return_data = Vec::new();
513            return Ok((CreateResponse::Fail("write protection".into()), 0, Gas(0)));
514        }
515
516        let do_create = match self.do_create {
517            Some(f) => f,
518            None => {
519                self.return_data = b"creates not available".to_vec();
520                return Ok((
521                    CreateResponse::Fail("not available".into()),
522                    self.return_data.len() as u32,
523                    Gas(0),
524                ));
525            }
526        };
527
528        // CREATE base cost = 32000
529        let base_cost: u64 = 32000;
530        if gas.0 < base_cost {
531            self.return_data = Vec::new();
532            return Ok((CreateResponse::Fail("out of gas".into()), 0, Gas(gas.0)));
533        }
534        let remaining = gas.0 - base_cost;
535
536        // 63/64ths rule
537        let one_64th = remaining / 64;
538        let call_gas = remaining - one_64th;
539
540        let result = (do_create)(
541            self.ctx_ptr,
542            self.address,
543            &code,
544            call_gas,
545            endowment,
546            None, // CREATE
547        );
548
549        self.return_data = result.output.clone();
550        // cost = baseCost + gas_used (Go: startGas - returnGas - one_64th)
551        let cost = base_cost.saturating_add(result.gas_cost);
552
553        let response = match result.address {
554            Some(addr) => CreateResponse::Success(addr),
555            None => {
556                // On non-revert failure, clear return data
557                if self.return_data.is_empty() {
558                    CreateResponse::Fail("create failed".into())
559                } else {
560                    CreateResponse::Fail("reverted".into())
561                }
562            }
563        };
564
565        Ok((response, self.return_data.len() as u32, Gas(cost)))
566    }
567
568    fn create2(
569        &mut self,
570        code: Vec<u8>,
571        endowment: U256,
572        salt: B256,
573        gas: Gas,
574    ) -> eyre::Result<(CreateResponse, u32, Gas)> {
575        if self.read_only {
576            self.return_data = Vec::new();
577            return Ok((CreateResponse::Fail("write protection".into()), 0, Gas(0)));
578        }
579
580        let do_create = match self.do_create {
581            Some(f) => f,
582            None => {
583                self.return_data = b"creates not available".to_vec();
584                return Ok((
585                    CreateResponse::Fail("not available".into()),
586                    self.return_data.len() as u32,
587                    Gas(0),
588                ));
589            }
590        };
591
592        // CREATE2 base cost = 32000 + keccak cost
593        let keccak_words = (code.len() as u64).div_ceil(32);
594        let keccak_cost = keccak_words.saturating_mul(6); // Keccak256WordGas
595        let base_cost = 32000u64.saturating_add(keccak_cost);
596        if gas.0 < base_cost {
597            self.return_data = Vec::new();
598            return Ok((CreateResponse::Fail("out of gas".into()), 0, Gas(gas.0)));
599        }
600        let remaining = gas.0 - base_cost;
601
602        let one_64th = remaining / 64;
603        let call_gas = remaining - one_64th;
604
605        let result = (do_create)(
606            self.ctx_ptr,
607            self.address,
608            &code,
609            call_gas,
610            endowment,
611            Some(salt),
612        );
613
614        self.return_data = result.output.clone();
615        // cost = baseCost + gas_used (Go: startGas - returnGas - one_64th)
616        let cost = base_cost.saturating_add(result.gas_cost);
617
618        let response = match result.address {
619            Some(addr) => CreateResponse::Success(addr),
620            None => {
621                if self.return_data.is_empty() {
622                    CreateResponse::Fail("create failed".into())
623                } else {
624                    CreateResponse::Fail("reverted".into())
625                }
626            }
627        };
628
629        Ok((response, self.return_data.len() as u32, Gas(cost)))
630    }
631
632    fn get_return_data(&self) -> Vec<u8> {
633        self.return_data.clone()
634    }
635
636    fn emit_log(&mut self, data: Vec<u8>, topics: u32) -> eyre::Result<()> {
637        if self.read_only {
638            return Err(eyre::eyre!("cannot emit log in static context"));
639        }
640
641        let topic_bytes = (topics as usize) * 32;
642        if data.len() < topic_bytes {
643            return Err(eyre::eyre!("log data too short for {topics} topics"));
644        }
645
646        let mut topic_list = Vec::with_capacity(topics as usize);
647        for i in 0..topics as usize {
648            let start = i * 32;
649            let mut bytes = [0u8; 32];
650            bytes.copy_from_slice(&data[start..start + 32]);
651            topic_list.push(B256::from(bytes));
652        }
653
654        let log_data = data[topic_bytes..].to_vec();
655
656        let addr = self.address;
657        let log = Log::new(addr, topic_list, log_data.into()).expect("too many log topics");
658
659        self.journal().log(log);
660        Ok(())
661    }
662
663    fn account_balance(&mut self, address: Address) -> eyre::Result<(U256, Gas)> {
664        let (balance, is_cold) = self.journal().account_balance(address)?;
665        // WasmAccountTouchCost(withCode=false): cold/warm access cost
666        let gas_cost = if is_cold {
667            COLD_ACCOUNT_ACCESS_COST
668        } else {
669            WARM_ACCOUNT_ACCESS_COST
670        };
671        Ok((balance, Gas(gas_cost)))
672    }
673
674    fn account_code(
675        &mut self,
676        _arbos_version: u64,
677        address: Address,
678        gas_left: Gas,
679    ) -> eyre::Result<(Vec<u8>, Gas)> {
680        let (code, is_cold) = self.journal().account_code(address)?;
681        // WasmAccountTouchCost(withCode=true): extCodeCost + cold/warm access cost
682        let access_cost = if is_cold {
683            COLD_ACCOUNT_ACCESS_COST
684        } else {
685            WARM_ACCOUNT_ACCESS_COST
686        };
687        let gas_cost = WASM_EXT_CODE_COST + access_cost;
688        // If insufficient gas, return empty code but still charge
689        if gas_left.0 < gas_cost {
690            return Ok((Vec::new(), Gas(gas_cost)));
691        }
692        Ok((code, Gas(gas_cost)))
693    }
694
695    fn account_codehash(&mut self, address: Address) -> eyre::Result<(B256, Gas)> {
696        let (hash, is_cold) = self.journal().account_codehash(address)?;
697        // WasmAccountTouchCost(withCode=false)
698        let gas_cost = if is_cold {
699            COLD_ACCOUNT_ACCESS_COST
700        } else {
701            WARM_ACCOUNT_ACCESS_COST
702        };
703        Ok((hash, Gas(gas_cost)))
704    }
705
706    fn add_pages(&mut self, new_pages: u16) -> eyre::Result<Gas> {
707        // add_stylus_pages returns previous (open, ever) before updating
708        let (prev_open, prev_ever) = pages::add_stylus_pages(new_pages);
709        let model = MemoryModel::new(self.free_pages, self.page_gas);
710        let cost = model.gas_cost(new_pages, prev_open, prev_ever);
711        Ok(Gas(cost))
712    }
713
714    fn capture_hostio(
715        &mut self,
716        _name: &str,
717        _args: &[u8],
718        _outs: &[u8],
719        _start_ink: crate::ink::Ink,
720        _end_ink: crate::ink::Ink,
721    ) {
722        // Debug tracing — no-op in production.
723    }
724}
725
726/// Compute the base gas cost for a CALL from Stylus.
727///
728/// Matches Go's `WasmCallCost`: EIP-2929 warm/cold access + value transfer +
729/// new account creation cost. Returns `(cost, out_of_gas)`.
730fn wasm_call_cost(
731    journal: &mut dyn JournalAccess,
732    contract: Address,
733    value: &U256,
734    budget: u64,
735) -> (u64, bool) {
736    let mut total: u64 = 0;
737
738    // Static cost: warm storage read (computation)
739    total += WARM_ACCOUNT_ACCESS_COST; // 100
740    if total > budget {
741        return (total, true);
742    }
743
744    // Cold access cost
745    let warm = journal.address_in_access_list(contract);
746    if !warm {
747        journal.add_address_to_access_list(contract);
748        let cold_cost = COLD_ACCOUNT_ACCESS_COST - WARM_ACCOUNT_ACCESS_COST; // 2500
749        total = total.saturating_add(cold_cost);
750        if total > budget {
751            return (total, true);
752        }
753    }
754
755    let transfers_value = !value.is_zero();
756    if transfers_value {
757        // Check if target is empty (for new account cost)
758        if let Ok(empty) = journal.is_account_empty(contract) {
759            if empty {
760                total = total.saturating_add(25000); // CallNewAccountGas
761                if total > budget {
762                    return (total, true);
763                }
764            }
765        }
766        // Value transfer cost
767        total = total.saturating_add(9000); // CallValueTransferGas
768        if total > budget {
769            return (total, true);
770        }
771    }
772
773    (total, false)
774}
775
776/// Compute SSTORE gas cost following EIP-2929 + EIP-3529 (post-London).
777fn sstore_gas_cost(info: &SStoreInfo) -> u64 {
778    let base = if info.original_value == info.new_value {
779        WARM_STORAGE_READ_COST
780    } else if info.original_value == info.present_value {
781        if info.original_value.is_zero() {
782            20_000 // SSTORE_SET_GAS
783        } else {
784            2_900 // SSTORE_RESET_GAS (5000 - 2100)
785        }
786    } else {
787        WARM_STORAGE_READ_COST
788    };
789
790    let cold_cost = if info.is_cold { COLD_SLOAD_COST } else { 0 };
791    base + cold_cost
792}