chisel/
runner.rs

1//! ChiselRunner
2//!
3//! This module contains the `ChiselRunner` struct, which assists with deploying
4//! and calling the REPL contract on a in-memory REVM instance.
5
6use alloy_primitives::{map::AddressHashMap, Address, Bytes, Log, U256};
7use eyre::Result;
8use foundry_evm::{
9    executors::{DeployResult, Executor, RawCallResult},
10    traces::{TraceKind, Traces},
11};
12use revm::interpreter::{return_ok, InstructionResult};
13
14/// The function selector of the REPL contract's entrypoint, the `run()` function.
15static RUN_SELECTOR: [u8; 4] = [0xc0, 0x40, 0x62, 0x26];
16
17/// The Chisel Runner
18///
19/// Based off of foundry's forge cli runner for scripting.
20/// See: [runner](cli::cmd::forge::script::runner.rs)
21#[derive(Debug)]
22pub struct ChiselRunner {
23    /// The Executor
24    pub executor: Executor,
25    /// An initial balance
26    pub initial_balance: U256,
27    /// The sender
28    pub sender: Address,
29    /// Input calldata appended to `RUN_SELECTOR`
30    pub input: Option<Vec<u8>>,
31}
32
33/// Represents the result of a Chisel REPL run
34#[derive(Debug, Default)]
35pub struct ChiselResult {
36    /// Was the run a success?
37    pub success: bool,
38    /// Transaction logs
39    pub logs: Vec<Log>,
40    /// Call traces
41    pub traces: Traces,
42    /// Amount of gas used in the transaction
43    pub gas_used: u64,
44    /// Map of addresses to their labels
45    pub labeled_addresses: AddressHashMap<String>,
46    /// Return data
47    pub returned: Bytes,
48    /// Called address
49    pub address: Option<Address>,
50    /// EVM State at the final instruction of the `run()` function
51    pub state: Option<(Vec<U256>, Vec<u8>, InstructionResult)>,
52}
53
54/// ChiselRunner implementation
55impl ChiselRunner {
56    /// Create a new [ChiselRunner]
57    ///
58    /// ### Takes
59    ///
60    /// An [Executor], the initial balance of the sender, and the sender's [Address].
61    ///
62    /// ### Returns
63    ///
64    /// A new [ChiselRunner]
65    pub fn new(
66        executor: Executor,
67        initial_balance: U256,
68        sender: Address,
69        input: Option<Vec<u8>>,
70    ) -> Self {
71        Self { executor, initial_balance, sender, input }
72    }
73
74    /// Run a contract as a REPL session
75    ///
76    /// ### Takes
77    ///
78    /// The creation bytecode of the REPL contract
79    ///
80    /// ### Returns
81    ///
82    /// Optionally, a tuple containing the deployed address of the bytecode as well as a
83    /// [ChiselResult] containing information about the result of the call to the deployed REPL
84    /// contract.
85    pub fn run(&mut self, bytecode: Bytes) -> Result<(Address, ChiselResult)> {
86        // Set the sender's balance to [U256::MAX] for deployment of the REPL contract.
87        self.executor.set_balance(self.sender, U256::MAX)?;
88
89        // Deploy an instance of the REPL contract
90        // We don't care about deployment traces / logs here
91        let DeployResult { address, .. } = self
92            .executor
93            .deploy(self.sender, bytecode, U256::ZERO, None)
94            .map_err(|err| eyre::eyre!("Failed to deploy REPL contract:\n{}", err))?;
95
96        // Reset the sender's balance to the initial balance for calls.
97        self.executor.set_balance(self.sender, self.initial_balance)?;
98
99        // Append the input to the `RUN_SELECTOR` to form the calldata
100        let mut calldata = RUN_SELECTOR.to_vec();
101        if let Some(mut input) = self.input.clone() {
102            calldata.append(&mut input);
103        }
104
105        // Call the "run()" function of the REPL contract
106        let call_res = self.call(self.sender, address, Bytes::from(calldata), U256::from(0), true);
107
108        call_res.map(|res| (address, res))
109    }
110
111    /// Executes the call.
112    ///
113    /// This will commit the changes if `commit` is true.
114    ///
115    /// This will return _estimated_ gas instead of the precise gas the call would consume, so it
116    /// can be used as `gas_limit`.
117    ///
118    /// Taken from Forge's script runner.
119    fn call(
120        &mut self,
121        from: Address,
122        to: Address,
123        calldata: Bytes,
124        value: U256,
125        commit: bool,
126    ) -> eyre::Result<ChiselResult> {
127        let fs_commit_changed =
128            if let Some(cheatcodes) = &mut self.executor.inspector_mut().cheatcodes {
129                let original_fs_commit = cheatcodes.fs_commit;
130                cheatcodes.fs_commit = false;
131                original_fs_commit != cheatcodes.fs_commit
132            } else {
133                false
134            };
135
136        let mut res = self.executor.call_raw(from, to, calldata.clone(), value)?;
137        let mut gas_used = res.gas_used;
138        if matches!(res.exit_reason, return_ok!()) {
139            // store the current gas limit and reset it later
140            let init_gas_limit = self.executor.env().tx.gas_limit;
141
142            // the executor will return the _exact_ gas value this transaction consumed, setting
143            // this value as gas limit will result in `OutOfGas` so to come up with a
144            // better estimate we search over a possible range we pick a higher gas
145            // limit 3x of a succeeded call should be safe
146            let mut highest_gas_limit = gas_used * 3;
147            let mut lowest_gas_limit = gas_used;
148            let mut last_highest_gas_limit = highest_gas_limit;
149            while (highest_gas_limit - lowest_gas_limit) > 1 {
150                let mid_gas_limit = (highest_gas_limit + lowest_gas_limit) / 2;
151                self.executor.env_mut().tx.gas_limit = mid_gas_limit;
152                let res = self.executor.call_raw(from, to, calldata.clone(), value)?;
153                match res.exit_reason {
154                    InstructionResult::Revert |
155                    InstructionResult::OutOfGas |
156                    InstructionResult::OutOfFunds => {
157                        lowest_gas_limit = mid_gas_limit;
158                    }
159                    _ => {
160                        highest_gas_limit = mid_gas_limit;
161                        // if last two successful estimations only vary by 10%, we consider this to
162                        // sufficiently accurate
163                        const ACCURACY: u64 = 10;
164                        if (last_highest_gas_limit - highest_gas_limit) * ACCURACY /
165                            last_highest_gas_limit <
166                            1
167                        {
168                            // update the gas
169                            gas_used = highest_gas_limit;
170                            break;
171                        }
172                        last_highest_gas_limit = highest_gas_limit;
173                    }
174                }
175            }
176            // reset gas limit in the
177            self.executor.env_mut().tx.gas_limit = init_gas_limit;
178        }
179
180        // if we changed `fs_commit` during gas limit search, re-execute the call with original
181        // value
182        if fs_commit_changed {
183            if let Some(cheatcodes) = &mut self.executor.inspector_mut().cheatcodes {
184                cheatcodes.fs_commit = !cheatcodes.fs_commit;
185            }
186
187            res = self.executor.call_raw(from, to, calldata.clone(), value)?;
188        }
189
190        if commit {
191            // if explicitly requested we can now commit the call
192            res = self.executor.transact_raw(from, to, calldata, value)?;
193        }
194
195        let RawCallResult { result, reverted, logs, traces, labels, chisel_state, .. } = res;
196
197        Ok(ChiselResult {
198            returned: result,
199            success: !reverted,
200            gas_used,
201            logs,
202            traces: traces
203                .map(|traces| {
204                    // Manually adjust gas for the trace to add back the stipend/real used gas
205
206                    vec![(TraceKind::Execution, traces)]
207                })
208                .unwrap_or_default(),
209            labeled_addresses: labels,
210            address: None,
211            state: chisel_state,
212        })
213    }
214}