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}