Skip to main content

forge_script/
runner.rs

1use super::{ScriptConfig, ScriptResult};
2use crate::build::ScriptPredeployLibraries;
3use alloy_eips::eip7702::SignedAuthorization;
4use alloy_evm::{
5    FromRecoveredTx,
6    revm::context::{Block, Transaction},
7};
8use alloy_network::TransactionBuilder;
9use alloy_primitives::{Address, Bytes, TxKind, U256, map::AddressHashMap};
10use eyre::Result;
11use foundry_cheatcodes::BroadcastableTransaction;
12use foundry_common::{LIBRARY_DEPLOYER, TransactionMaybeSigned};
13use foundry_config::Config;
14use foundry_evm::{
15    constants::CALLER,
16    core::{
17        FoundryBlock, FoundryTransaction,
18        evm::{EvmEnvFor, FoundryEvmNetwork, TransactionRequestFor, TxEnvFor},
19    },
20    executors::{DeployResult, EvmError, ExecutionErr, Executor, RawCallResult},
21    opts::EvmOpts,
22    revm::interpreter::{InstructionResult, return_ok},
23    traces::{TraceKind, Traces},
24};
25use std::collections::VecDeque;
26
27/// Drives script execution
28#[derive(Debug)]
29pub struct ScriptRunner<FEN: FoundryEvmNetwork> {
30    pub executor: Executor<FEN>,
31    pub evm_opts: EvmOpts,
32    collect_debug_bytecodes: bool,
33}
34
35impl<FEN: FoundryEvmNetwork> ScriptRunner<FEN> {
36    pub const fn new(executor: Executor<FEN>, evm_opts: EvmOpts) -> Self {
37        Self { executor, evm_opts, collect_debug_bytecodes: false }
38    }
39
40    pub const fn with_debug_bytecodes(mut self, collect_debug_bytecodes: bool) -> Self {
41        self.collect_debug_bytecodes = collect_debug_bytecodes;
42        self
43    }
44
45    fn maybe_debug_bytecodes(
46        &self,
47        debug_bytecodes: AddressHashMap<Bytes>,
48    ) -> AddressHashMap<Bytes> {
49        if self.collect_debug_bytecodes { debug_bytecodes } else { Default::default() }
50    }
51
52    fn extend_debug_bytecodes(
53        &self,
54        target: &mut AddressHashMap<Bytes>,
55        debug_bytecodes: AddressHashMap<Bytes>,
56    ) {
57        if self.collect_debug_bytecodes {
58            target.extend(debug_bytecodes);
59        }
60    }
61
62    fn deploy_local_libraries(
63        &mut self,
64        libraries: &[foundry_linking::LinkedLibrary],
65        debug_bytecodes: &mut AddressHashMap<Bytes>,
66    ) -> Result<()> {
67        if libraries.is_empty() {
68            return Ok(());
69        }
70        let balance = self.executor.get_balance(LIBRARY_DEPLOYER)?;
71        let nonce = self.executor.get_nonce(LIBRARY_DEPLOYER)?;
72        self.executor.set_balance(LIBRARY_DEPLOYER, U256::MAX)?;
73        self.executor.set_nonce(LIBRARY_DEPLOYER, 0)?;
74        for library in libraries {
75            let DeployResult { address, raw } = self
76                .executor
77                .deploy(LIBRARY_DEPLOYER, library.bytecode.clone(), U256::ZERO, None)
78                .map_err(|err| eyre::eyre!("couldn't deploy local library: {err}"))?;
79            eyre::ensure!(
80                library.address == address,
81                "local library deployed at an unexpected address"
82            );
83            self.extend_debug_bytecodes(debug_bytecodes, raw.debug_bytecodes);
84        }
85        self.executor.set_balance(LIBRARY_DEPLOYER, balance)?;
86        self.executor.set_nonce(LIBRARY_DEPLOYER, nonce)?;
87        Ok(())
88    }
89
90    /// Deploys the libraries and broadcast contract. Calls setUp method if requested.
91    pub fn setup(
92        &mut self,
93        libraries: &ScriptPredeployLibraries,
94        code: Bytes,
95        setup: bool,
96        script_config: &ScriptConfig<FEN>,
97        is_broadcast: bool,
98    ) -> Result<(Address, ScriptResult<FEN::Network>)> {
99        trace!(target: "script", "executing setUP()");
100
101        if !is_broadcast {
102            if self.evm_opts.sender == Config::DEFAULT_SENDER {
103                // We max out their balance so that they can deploy and make calls.
104                self.executor.set_balance(self.evm_opts.sender, U256::MAX)?;
105            }
106
107            if script_config.evm_opts.fork_url.is_none()
108                && !script_config.evm_opts.networks.is_tempo()
109            {
110                self.executor.deploy_create2_deployer()?;
111            }
112        }
113
114        let sender_nonce = script_config.sender_nonce;
115        self.executor.set_nonce(self.evm_opts.sender, sender_nonce)?;
116
117        // We max out their balance so that they can deploy and make calls.
118        self.executor.set_balance(CALLER, U256::MAX)?;
119
120        let mut library_transactions = VecDeque::new();
121        let mut traces = Traces::default();
122        let mut debug_bytecodes: AddressHashMap<Bytes> = Default::default();
123
124        // Deploy libraries
125        match libraries {
126            ScriptPredeployLibraries::Default { onchain, local } => {
127                self.deploy_local_libraries(local, &mut debug_bytecodes)?;
128                for library in onchain {
129                    let code = &library.bytecode;
130                    let RawCallResult {
131                        traces: deploy_traces,
132                        debug_bytecodes: deploy_debug_bytecodes,
133                        ..
134                    } = self
135                        .executor
136                        .deploy(self.evm_opts.sender, code.clone(), U256::ZERO, None)
137                        .map_err(|err| eyre::eyre!("couldn't deploy library: {err}"))?
138                        .raw;
139
140                    self.extend_debug_bytecodes(&mut debug_bytecodes, deploy_debug_bytecodes);
141
142                    if let Some(deploy_traces) = deploy_traces {
143                        traces.push((TraceKind::Deployment, deploy_traces));
144                    }
145
146                    let mut tx_req = TransactionRequestFor::<FEN>::default()
147                        .with_from(self.evm_opts.sender)
148                        .with_input(code.clone())
149                        .with_nonce(sender_nonce + library_transactions.len() as u64);
150
151                    script_config.tempo.apply::<FEN::Network>(&mut tx_req, None);
152
153                    library_transactions.push_back(BroadcastableTransaction {
154                        rpc: self.evm_opts.fork_url.clone(),
155                        transaction: TransactionMaybeSigned::new(tx_req),
156                        contract_address: None,
157                    })
158                }
159            }
160            ScriptPredeployLibraries::Create2 { onchain, salt, local } => {
161                self.deploy_local_libraries(local, &mut debug_bytecodes)?;
162                let create2_deployer = self.executor.create2_deployer();
163                for library in onchain {
164                    let address =
165                        create2_deployer.create2_from_code(salt, library.bytecode.as_ref());
166                    // Skip if already deployed
167                    if !self.executor.is_empty_code(address)? {
168                        continue;
169                    }
170                    let calldata = [salt.as_ref(), library.bytecode.as_ref()].concat();
171                    let RawCallResult {
172                        traces: deploy_traces,
173                        debug_bytecodes: deploy_debug_bytecodes,
174                        ..
175                    } = self
176                        .executor
177                        .transact_raw(
178                            self.evm_opts.sender,
179                            create2_deployer,
180                            calldata.clone().into(),
181                            U256::ZERO,
182                        )
183                        .map_err(|err| eyre::eyre!("couldn't deploy library: {err}"))?;
184
185                    self.extend_debug_bytecodes(&mut debug_bytecodes, deploy_debug_bytecodes);
186
187                    if let Some(deploy_traces) = deploy_traces {
188                        traces.push((TraceKind::Deployment, deploy_traces));
189                    }
190
191                    let mut tx_req = TransactionRequestFor::<FEN>::default()
192                        .with_from(self.evm_opts.sender)
193                        .with_input(calldata)
194                        .with_nonce(sender_nonce + library_transactions.len() as u64)
195                        .with_to(create2_deployer);
196
197                    script_config.tempo.apply::<FEN::Network>(&mut tx_req, None);
198
199                    library_transactions.push_back(BroadcastableTransaction {
200                        rpc: self.evm_opts.fork_url.clone(),
201                        transaction: TransactionMaybeSigned::new(tx_req),
202                        contract_address: None,
203                    });
204                }
205
206                // Sender nonce is not incremented when performing CALLs. We need to manually
207                // increase it.
208                self.executor.set_nonce(
209                    self.evm_opts.sender,
210                    sender_nonce + library_transactions.len() as u64,
211                )?;
212            }
213        };
214
215        let address = CALLER.create(self.executor.get_nonce(CALLER)?);
216
217        // Set the contracts initial balance before deployment, so it is available during the
218        // construction
219        self.executor.set_balance(address, self.evm_opts.initial_balance)?;
220
221        // HACK: if the current sender is the default script sender (which is a default value), we
222        // set its nonce to a very large value before deploying the script contract. This
223        // ensures that the nonce increase during this CREATE does not affect deployment
224        // addresses of contracts that are deployed in the script, Otherwise, we'd have a
225        // nonce mismatch during script execution and onchain simulation, potentially
226        // resulting in weird errors like <https://github.com/foundry-rs/foundry/issues/8960>.
227        let prev_sender_nonce = self.executor.get_nonce(self.evm_opts.sender)?;
228        if self.evm_opts.sender == CALLER {
229            self.executor.set_nonce(self.evm_opts.sender, u64::MAX / 2)?;
230        }
231
232        // Deploy an instance of the contract
233        let DeployResult {
234            address,
235            raw:
236                RawCallResult {
237                    mut logs,
238                    traces: constructor_traces,
239                    debug_bytecodes: constructor_debug_bytecodes,
240                    ..
241                },
242        } = self
243            .executor
244            .deploy(CALLER, code, U256::ZERO, None)
245            .map_err(|err| eyre::eyre!("Failed to deploy script:\n{}", err))?;
246
247        if self.evm_opts.sender == CALLER {
248            self.executor.set_nonce(self.evm_opts.sender, prev_sender_nonce)?;
249        }
250
251        // set script address to be used by execution inspector
252        if script_config.config.script_execution_protection {
253            self.executor.set_script_execution(address);
254        }
255
256        traces.extend(constructor_traces.map(|traces| (TraceKind::Deployment, traces)));
257        self.extend_debug_bytecodes(&mut debug_bytecodes, constructor_debug_bytecodes);
258
259        // Optionally call the `setUp` function
260        let (success, returned, exit_reason, gas_used, labeled_addresses, transactions) = if setup {
261            let result = match self.executor.setup(Some(self.evm_opts.sender), address, None) {
262                Ok(result) => result,
263                Err(EvmError::Execution(err)) => err.raw,
264                Err(err) => return Err(err.into()),
265            };
266            let RawCallResult {
267                reverted,
268                result,
269                exit_reason,
270                traces: setup_traces,
271                labels,
272                logs: setup_logs,
273                gas_used,
274                debug_bytecodes: setup_debug_bytecodes,
275                transactions: setup_transactions,
276                ..
277            } = result;
278            traces.extend(setup_traces.map(|traces| (TraceKind::Setup, traces)));
279            logs.extend_from_slice(&setup_logs);
280            self.extend_debug_bytecodes(&mut debug_bytecodes, setup_debug_bytecodes);
281            if let Some(txs) = setup_transactions {
282                library_transactions.extend(txs);
283            }
284            (!reverted, result, exit_reason, gas_used, labels, Some(library_transactions))
285        } else {
286            self.executor.backend_mut().set_test_contract(address);
287            (true, Bytes::new(), None, 0, Default::default(), Some(library_transactions))
288        };
289
290        Ok((
291            address,
292            ScriptResult {
293                returned,
294                exit_reason,
295                success,
296                gas_used,
297                labeled_addresses,
298                debug_bytecodes: self.maybe_debug_bytecodes(debug_bytecodes),
299                transactions,
300                logs,
301                traces,
302                address: None,
303                ..Default::default()
304            },
305        ))
306    }
307
308    /// Executes the method that will collect all broadcastable transactions.
309    pub fn script(
310        &mut self,
311        address: Address,
312        calldata: Bytes,
313    ) -> Result<ScriptResult<FEN::Network>> {
314        self.call(self.evm_opts.sender, address, calldata, U256::ZERO, None, false)
315    }
316
317    /// Runs a broadcastable transaction locally and persists its state.
318    pub fn simulate(
319        &mut self,
320        transaction: &TransactionMaybeSigned<FEN::Network>,
321        is_fixed_gas_limit: bool,
322    ) -> Result<ScriptResult<FEN::Network>> {
323        let Some(blob_hashes) = transaction.blob_versioned_hashes() else {
324            return self.simulate_with_tx_env(transaction, is_fixed_gas_limit);
325        };
326
327        // Every simulated call clones the executor's tx env, so a blob transaction is run as one
328        // there, making `BLOBHASH` match the published transaction.
329        let original_tx_env = self.executor.tx_env().clone();
330        let blob_gasprice = self.executor.evm_env().block_env.blob_gasprice().unwrap_or_default();
331        let tx_env = self.executor.tx_env_mut();
332        tx_env.set_blob_hashes(blob_hashes.to_vec());
333        tx_env.set_max_fee_per_blob_gas(blob_gasprice);
334        tx_env.set_tx_type(3);
335        let result = self.simulate_with_tx_env(transaction, is_fixed_gas_limit);
336        *self.executor.tx_env_mut() = original_tx_env;
337        result
338    }
339
340    fn simulate_with_tx_env(
341        &mut self,
342        transaction: &TransactionMaybeSigned<FEN::Network>,
343        is_fixed_gas_limit: bool,
344    ) -> Result<ScriptResult<FEN::Network>> {
345        if is_fixed_gas_limit {
346            let (env, tx) = self.prepare_fixed_gas_env(transaction);
347            return if tx.is_single_create() {
348                let result = self.executor.deploy_with_env(env, tx, None);
349                self.deployment_result(result)
350            } else {
351                let result = self.executor.transact_with_env(env, tx)?;
352                let gas_used = result.gas_used;
353                Ok(self.call_result(result, gas_used))
354            };
355        }
356
357        let from = transaction.from().expect("transaction sender missing");
358        if let Some(to) = transaction.to() {
359            self.call(
360                from,
361                to,
362                transaction.input().cloned().unwrap_or_default(),
363                transaction.value().unwrap_or_default(),
364                transaction.authorization_list(),
365                true,
366            )
367        } else {
368            let res = self.executor.deploy(
369                from,
370                transaction.input().cloned().expect("No data for create transaction"),
371                transaction.value().unwrap_or_default(),
372                None,
373            );
374            self.deployment_result(res)
375        }
376    }
377
378    pub(crate) fn deployment_result(
379        &self,
380        res: Result<DeployResult<FEN>, EvmError<FEN>>,
381    ) -> Result<ScriptResult<FEN::Network>> {
382        let (address, RawCallResult { gas_used, logs, traces, debug_bytecodes, exit_reason, .. }) =
383            match res {
384                Ok(DeployResult { address, raw }) => (address, raw),
385                Err(EvmError::Execution(err)) => {
386                    let ExecutionErr { raw, reason } = *err;
387                    sh_err!("Failed with `{reason}`:\n")?;
388                    (Address::ZERO, raw)
389                }
390                Err(e) => {
391                    eyre::bail!("Failed deploying contract: {e:?}");
392                }
393            };
394
395        Ok(ScriptResult {
396            returned: Bytes::new(),
397            success: !address.is_zero(),
398            gas_used,
399            logs,
400            debug_bytecodes: self.maybe_debug_bytecodes(debug_bytecodes),
401            // Manually adjust gas for the trace to add back the stipend/real used gas
402            traces: traces.map(|traces| vec![(TraceKind::Execution, traces)]).unwrap_or_default(),
403            exit_reason,
404            address: Some(address),
405            ..Default::default()
406        })
407    }
408
409    /// Executes the call
410    ///
411    /// This will commit the changes if `commit` is true.
412    ///
413    /// This will return _estimated_ gas instead of the precise gas the call would consume, so it
414    /// can be used as `gas_limit`.
415    fn call(
416        &mut self,
417        from: Address,
418        to: Address,
419        calldata: Bytes,
420        value: U256,
421        authorization_list: Option<Vec<SignedAuthorization>>,
422        commit: bool,
423    ) -> Result<ScriptResult<FEN::Network>> {
424        let mut res = if let Some(authorization_list) = &authorization_list {
425            self.executor.call_raw_with_authorization(
426                from,
427                to,
428                calldata.clone(),
429                value,
430                authorization_list.clone(),
431            )?
432        } else {
433            self.executor.call_raw(from, to, calldata.clone(), value)?
434        };
435        let mut gas_used = res.gas_used;
436
437        // We should only need to calculate realistic gas costs when preparing to broadcast
438        // something. This happens during the onchain simulation stage, where we commit each
439        // collected transactions.
440        //
441        // Otherwise don't re-execute, or some usecases might be broken: https://github.com/foundry-rs/foundry/issues/3921
442        if commit {
443            gas_used = self.search_optimal_gas_usage(&res, from, to, &calldata, value)?;
444            res = if let Some(authorization_list) = authorization_list {
445                self.executor.transact_raw_with_authorization(
446                    from,
447                    to,
448                    calldata,
449                    value,
450                    authorization_list,
451                )?
452            } else {
453                self.executor.transact_raw(from, to, calldata, value)?
454            }
455        }
456
457        Ok(self.call_result(res, gas_used))
458    }
459
460    pub(crate) fn call_result(
461        &self,
462        res: RawCallResult<FEN>,
463        gas_used: u64,
464    ) -> ScriptResult<FEN::Network> {
465        let RawCallResult {
466            result,
467            reverted,
468            logs,
469            traces,
470            labels,
471            transactions,
472            debug_bytecodes,
473            exit_reason,
474            cheatcodes,
475            ..
476        } = res;
477        let breakpoints = cheatcodes.map(|cheats| cheats.breakpoints).unwrap_or_default();
478
479        ScriptResult {
480            returned: result,
481            success: !reverted,
482            gas_used,
483            logs,
484            debug_bytecodes: self.maybe_debug_bytecodes(debug_bytecodes),
485            traces: traces
486                .map(|traces| {
487                    // Manually adjust gas for the trace to add back the stipend/real used gas
488
489                    vec![(TraceKind::Execution, traces)]
490                })
491                .unwrap_or_default(),
492            labeled_addresses: labels,
493            transactions,
494            exit_reason,
495            address: None,
496            breakpoints,
497        }
498    }
499
500    /// The executor will return the _exact_ gas value this transaction consumed, setting this value
501    /// as gas limit will result in `OutOfGas` so to come up with a better estimate we search over a
502    /// possible range we pick a higher gas limit 3x of a succeeded call should be safe.
503    ///
504    /// This might result in executing the same script multiple times. Depending on the user's goal,
505    /// it might be problematic when using `ffi`.
506    fn search_optimal_gas_usage(
507        &mut self,
508        res: &RawCallResult<FEN>,
509        from: Address,
510        to: Address,
511        calldata: &Bytes,
512        value: U256,
513    ) -> Result<u64> {
514        let mut gas_used = res.gas_used;
515        if matches!(res.exit_reason, Some(return_ok!())) {
516            // Store the current gas limit and reset it later.
517            let init_gas_limit = self.executor.tx_env().gas_limit();
518
519            let mut search = GasSearch::new(gas_used);
520            while let Some(limit) = search.next_limit() {
521                self.executor.tx_env_mut().set_gas_limit(limit);
522                let res = self.executor.call_raw(from, to, calldata.0.clone().into(), value)?;
523                search.record(limit, needs_more_gas(res.exit_reason));
524            }
525            gas_used = search.gas_used();
526            // Reset gas limit in the executor.
527            self.executor.tx_env_mut().set_gas_limit(init_gas_limit);
528        }
529        Ok(gas_used)
530    }
531
532    /// Prepares a fixed-limit transaction without changing the block or executor gas limit.
533    pub(crate) fn prepare_fixed_gas_env(
534        &self,
535        transaction: &TransactionMaybeSigned<FEN::Network>,
536    ) -> (EvmEnvFor<FEN>, TxEnvFor<FEN>) {
537        let from = transaction.from().expect("transaction sender missing");
538        let kind = transaction.to().map_or(TxKind::Create, TxKind::Call);
539        let (mut env, mut tx) = self.executor.prepare_call_env(
540            from,
541            kind,
542            transaction.input().cloned().unwrap_or_default(),
543            transaction.value().unwrap_or_default(),
544        );
545        env.block_env.set_gas_limit(self.executor.evm_env().block_env.gas_limit());
546        match transaction {
547            TransactionMaybeSigned::Signed { tx: signed, .. } => {
548                // Keep the complete signed payload, including access lists and network fields.
549                tx = TxEnvFor::<FEN>::from_recovered_tx(signed, from);
550            }
551            TransactionMaybeSigned::Unsigned(request) => {
552                let access_list = request.access_list().cloned().unwrap_or_default();
553                if !access_list.is_empty() && tx.tx_type() == 0 {
554                    tx.set_tx_type(1);
555                }
556                tx.set_access_list(access_list);
557                if let Some(authorization_list) = transaction.authorization_list() {
558                    tx.set_signed_authorization(authorization_list);
559                    tx.set_tx_type(4);
560                }
561            }
562        }
563        // Retain the synthetic call's fee and chain normalization.
564        tx.set_gas_price(0);
565        tx.set_gas_priority_fee(None);
566        tx.set_chain_id(Some(env.cfg_env.chain_id));
567        tx.set_gas_limit(transaction.gas().expect("fixed gas limit missing") as u64);
568        (env, tx)
569    }
570}
571
572/// Gas-search arithmetic shared by ordinary and Monad simulation.
573pub(crate) struct GasSearch {
574    gas_used: u64,
575    highest: u64,
576    lowest: u64,
577    last_highest: u64,
578    done: bool,
579}
580
581impl GasSearch {
582    pub(crate) const fn new(gas_used: u64) -> Self {
583        Self {
584            gas_used,
585            highest: gas_used * 3,
586            lowest: gas_used,
587            last_highest: gas_used * 3,
588            done: false,
589        }
590    }
591
592    pub(crate) const fn next_limit(&self) -> Option<u64> {
593        if !self.done && self.highest - self.lowest > 1 {
594            Some((self.highest + self.lowest) / 2)
595        } else {
596            None
597        }
598    }
599
600    /// Records the outcome of a probe at `limit`, where `needs_more_gas` means the limit was not
601    /// enough.
602    pub(crate) const fn record(&mut self, limit: u64, needs_more_gas: bool) {
603        if needs_more_gas {
604            self.lowest = limit;
605        } else {
606            self.highest = limit;
607            // Stop when successive successful estimates differ by less than ten percent.
608            if (self.last_highest - self.highest) * 10 / self.last_highest < 1 {
609                self.gas_used = self.highest;
610                self.done = true;
611            } else {
612                self.last_highest = self.highest;
613            }
614        }
615    }
616
617    pub(crate) const fn gas_used(&self) -> u64 {
618        self.gas_used
619    }
620}
621
622/// Returns whether the gas search treats `exit_reason` as needing more gas.
623pub(crate) const fn needs_more_gas(exit_reason: Option<InstructionResult>) -> bool {
624    matches!(
625        exit_reason,
626        Some(
627            InstructionResult::Revert | InstructionResult::OutOfGas | InstructionResult::OutOfFunds
628        )
629    )
630}
631
632#[cfg(test)]
633mod tests {
634    use super::*;
635    use alloy_primitives::bytes;
636    use alloy_signer::SignerSync;
637    use alloy_signer_local::PrivateKeySigner;
638    use foundry_evm::{
639        backend::Backend, core::evm::TempoEvmNetwork, executors::ExecutorBuilder, revm::DatabaseRef,
640    };
641    use foundry_evm_networks::NetworkConfigs;
642    use tempo_alloy::primitives::{TempoTransaction, TempoTxEnvelope, transaction::Call};
643
644    #[test]
645    fn successful_probes_keep_existing_ten_percent_stop() {
646        let mut search = GasSearch::new(100);
647        for expected in [200, 150, 125, 112, 106] {
648            assert_eq!(search.next_limit(), Some(expected));
649            search.record(expected, false);
650        }
651        assert_eq!(search.next_limit(), None);
652        assert_eq!(search.gas_used(), 106);
653    }
654
655    #[test]
656    fn unsuccessful_probes_keep_original_estimate() {
657        let mut search = GasSearch::new(100);
658        while let Some(limit) = search.next_limit() {
659            search.record(limit, true);
660        }
661        assert_eq!(search.gas_used(), 100);
662        assert_eq!(GasSearch::new(0).next_limit(), None);
663    }
664
665    #[test]
666    fn only_revert_and_running_out_mean_too_little_gas() {
667        for reason in
668            [InstructionResult::Revert, InstructionResult::OutOfGas, InstructionResult::OutOfFunds]
669        {
670            assert!(needs_more_gas(Some(reason)));
671        }
672        for reason in [
673            Some(InstructionResult::Return),
674            Some(InstructionResult::Stop),
675            Some(InstructionResult::InvalidFEOpcode),
676            None,
677        ] {
678            assert!(!needs_more_gas(reason));
679        }
680    }
681
682    #[test]
683    fn fixed_gas_tempo_create_first_batch() {
684        let signer = PrivateKeySigner::random();
685        let sender = signer.address();
686        let target = sender.create(0);
687        // The constructor stores seven; the runtime increments that slot when called.
688        let initcode = bytes!("60075f556008600e5f3960085ff35f546001015f5500");
689        for batch in [false, true] {
690            let mut env = EvmEnvFor::<TempoEvmNetwork>::default();
691            env.cfg_env.chain_id = 31337;
692            env.cfg_env.disable_nonce_check = true;
693            let executor = ExecutorBuilder::<TempoEvmNetwork>::new().gas_limit(30_000_000).build(
694                env,
695                TxEnvFor::<TempoEvmNetwork>::default(),
696                Backend::spawn(None).unwrap(),
697                NetworkConfigs::with_tempo(),
698            );
699            let mut runner = ScriptRunner::new(executor, EvmOpts::default());
700            let mut calls =
701                vec![Call { to: TxKind::Create, value: U256::ZERO, input: initcode.clone() }];
702            if batch {
703                calls.push(Call {
704                    to: TxKind::Call(target),
705                    value: U256::ZERO,
706                    input: Bytes::new(),
707                });
708            }
709            let transaction = TempoTransaction {
710                chain_id: 31337,
711                gas_limit: 2_000_000,
712                calls,
713                ..Default::default()
714            };
715            let signature = signer.sign_hash_sync(&transaction.signature_hash()).unwrap();
716            let transaction = TransactionMaybeSigned::Signed {
717                tx: TempoTxEnvelope::AA(transaction.into_signed(signature.into())),
718                from: sender,
719            };
720            let result = runner.simulate(&transaction, true).unwrap();
721            assert!(result.success);
722            assert_eq!(
723                runner.executor.backend().storage_ref(target, U256::ZERO).unwrap(),
724                U256::from(if batch { 8 } else { 7 }),
725            );
726            assert!(!runner.executor.is_empty_code(target).unwrap());
727            assert_eq!(transaction.gas(), Some(2_000_000));
728            assert_eq!(runner.executor.gas_limit(), 30_000_000);
729        }
730    }
731}