Skip to content

Commit 6aa9047

Browse files
authored
perf(script): binary search gas estimation (#2676)
* perf(script): binary search gas estimation * chore(clippy): make clippy happy * update tests * update tests * use tests with fixture
1 parent 4243e0a commit 6aa9047

File tree

7 files changed

+125
-76
lines changed

7 files changed

+125
-76
lines changed

cli/src/cmd/forge/script/runner.rs

Lines changed: 57 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use super::*;
22
use ethers::types::{Address, Bytes, NameOrAddress, U256};
33
use forge::{
44
executor::{CallResult, DeployResult, EvmError, Executor, RawCallResult},
5+
revm::{return_ok, Return},
56
trace::{CallTraceArena, TraceKind},
67
CALLER,
78
};
@@ -185,6 +186,12 @@ impl ScriptRunner {
185186
}
186187
}
187188

189+
/// Executes the call
190+
///
191+
/// This will commit the changes if `commit` is true.
192+
///
193+
/// This will return _estimated_ gas instead of the precise gas the call would consume, so it
194+
/// can be used as `gas_limit`.
188195
fn call(
189196
&mut self,
190197
from: Address,
@@ -193,24 +200,57 @@ impl ScriptRunner {
193200
value: U256,
194201
commit: bool,
195202
) -> eyre::Result<ScriptResult> {
196-
let RawCallResult {
197-
result,
198-
reverted,
199-
gas: tx_gas,
200-
stipend,
201-
logs,
202-
traces,
203-
labels,
204-
debug,
205-
transactions,
206-
..
207-
} = if !commit {
208-
self.executor.call_raw(from, to, calldata.0, value)?
209-
} else {
210-
self.executor.call_raw_committing(from, to, calldata.0, value)?
211-
};
203+
let mut res = self.executor.call_raw(from, to, calldata.0.clone(), value)?;
204+
let mut gas = res.gas;
205+
if matches!(res.status, return_ok!()) {
206+
// store the current gas limit and reset it later
207+
let init_gas_limit = self.executor.env_mut().tx.gas_limit;
208+
209+
// the executor will return the _exact_ gas value this transaction consumed, setting
210+
// this value as gas limit will result in `OutOfGas` so to come up with a
211+
// better estimate we search over a possible range we pick a higher gas
212+
// limit 3x of a succeeded call should be safe
213+
let mut highest_gas_limit = gas * 3;
214+
let mut lowest_gas_limit = gas;
215+
let mut last_highest_gas_limit = highest_gas_limit;
216+
while (highest_gas_limit - lowest_gas_limit) > 1 {
217+
let mid_gas_limit = (highest_gas_limit + lowest_gas_limit) / 2;
218+
self.executor.env_mut().tx.gas_limit = mid_gas_limit;
219+
let res = self.executor.call_raw(from, to, calldata.0.clone(), value)?;
220+
match res.status {
221+
Return::Revert |
222+
Return::OutOfGas |
223+
Return::LackOfFundForGasLimit |
224+
Return::OutOfFund => {
225+
lowest_gas_limit = mid_gas_limit;
226+
}
227+
_ => {
228+
highest_gas_limit = mid_gas_limit;
229+
// if last two successful estimations only vary by 10%, we consider this to
230+
// sufficiently accurate
231+
const ACCURACY: u64 = 10;
232+
if (last_highest_gas_limit - highest_gas_limit) * ACCURACY /
233+
last_highest_gas_limit <
234+
1
235+
{
236+
// update the gas
237+
gas = highest_gas_limit;
238+
break
239+
}
240+
last_highest_gas_limit = highest_gas_limit;
241+
}
242+
}
243+
}
244+
// reset gas limit in the
245+
self.executor.env_mut().tx.gas_limit = init_gas_limit;
246+
}
247+
248+
if commit {
249+
// if explicitly requested we can now commit the call
250+
res = self.executor.call_raw_committing(from, to, calldata.0, value)?;
251+
}
212252

213-
let gas = if commit { tx_gas } else { tx_gas.overflowing_sub(stipend).0 };
253+
let RawCallResult { result, reverted, logs, traces, labels, debug, transactions, .. } = res;
214254

215255
Ok(ScriptResult {
216256
returned: result,
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Compiling 1 files with 0.8.10
2+
Solc 0.8.10 finished in 23.34ms
3+
Compiler run successful
4+
Script ran successfully.
5+
Gas used: 24240
6+
7+
== Logs ==
8+
script ran
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Compiling 1 files with 0.8.10
2+
Solc 0.8.10 finished in 23.70ms
3+
Compiler run successful
4+
Script ran successfully.
5+
Gas used: 24240
6+
7+
== Logs ==
8+
script ran
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
Compiling 1 files with 0.8.10
2+
Solc 0.8.10 finished in 35.28ms
3+
Compiler run successful
4+
Script ran successfully.
5+
Gas used: 26882
6+
7+
== Logs ==
8+
script ran
9+
1
10+
2
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
Compiling 1 files with 0.8.10
2+
Solc 0.8.10 finished in 1.27s
3+
Compiler run successful
4+
Script ran successfully.
5+
Gas used: 24331
6+
7+
== Return ==
8+
result: uint256 255
9+
1: uint8 3
10+
11+
== Logs ==
12+
script ran
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Compiling 1 files with 0.8.10
2+
Solc 0.8.10 finished in 24.49ms
3+
Compiler run successful
4+
Script ran successfully.
5+
Gas used: 24240
6+
7+
== Logs ==
8+
script ran

cli/tests/it/script.rs

Lines changed: 22 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,10 @@ use anvil::{spawn, NodeConfig};
33
use ethers::abi::Address;
44
use foundry_cli_test_utils::{
55
forgetest, forgetest_async, forgetest_init,
6-
util::{TestCommand, TestProject},
6+
util::{OutputExt, TestCommand, TestProject},
77
ScriptOutcome, ScriptTester,
88
};
99
use foundry_utils::rpc;
10-
1110
use regex::Regex;
1211
use std::{env, path::PathBuf, str::FromStr};
1312

@@ -45,7 +44,7 @@ contract ContractScript is Script {
4544
);
4645

4746
// Tests that the `run` command works correctly
48-
forgetest!(can_execute_script_command, |prj: TestProject, mut cmd: TestCommand| {
47+
forgetest!(can_execute_script_command2, |prj: TestProject, mut cmd: TestCommand| {
4948
let script = prj
5049
.inner()
5150
.add_source(
@@ -64,16 +63,10 @@ contract Demo {
6463
.unwrap();
6564

6665
cmd.arg("script").arg(script);
67-
let output = cmd.stdout_lossy();
68-
assert!(output.ends_with(
69-
"Compiler run successful
70-
Script ran successfully.
71-
Gas used: 1751
72-
73-
== Logs ==
74-
script ran
75-
"
76-
));
66+
cmd.unchecked_output().stdout_matches_path(
67+
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
68+
.join("tests/fixtures/can_execute_script_command.stdout"),
69+
);
7770
});
7871

7972
// Tests that the `run` command works correctly when path *and* script name is specified
@@ -96,16 +89,10 @@ contract Demo {
9689
.unwrap();
9790

9891
cmd.arg("script").arg(format!("{}:Demo", script.display()));
99-
let output = cmd.stdout_lossy();
100-
assert!(output.ends_with(
101-
"Compiler run successful
102-
Script ran successfully.
103-
Gas used: 1751
104-
105-
== Logs ==
106-
script ran
107-
"
108-
));
92+
cmd.unchecked_output().stdout_matches_path(
93+
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
94+
.join("tests/fixtures/can_execute_script_command_fqn.stdout"),
95+
);
10996
});
11097

11198
// Tests that the run command can run arbitrary functions
@@ -128,16 +115,10 @@ contract Demo {
128115
.unwrap();
129116

130117
cmd.arg("script").arg(script).arg("--sig").arg("myFunction()");
131-
let output = cmd.stdout_lossy();
132-
assert!(output.ends_with(
133-
"Compiler run successful
134-
Script ran successfully.
135-
Gas used: 1751
136-
137-
== Logs ==
138-
script ran
139-
"
140-
));
118+
cmd.unchecked_output().stdout_matches_path(
119+
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
120+
.join("tests/fixtures/can_execute_script_command_with_sig.stdout"),
121+
);
141122
});
142123

143124
// Tests that the run command can run functions with arguments
@@ -163,18 +144,10 @@ contract Demo {
163144
.unwrap();
164145

165146
cmd.arg("script").arg(script).arg("--sig").arg("run(uint256,uint256)").arg("1").arg("2");
166-
let output = cmd.stdout_lossy();
167-
assert!(output.ends_with(
168-
"Compiler run successful
169-
Script ran successfully.
170-
Gas used: 3957
171-
172-
== Logs ==
173-
script ran
174-
1
175-
2
176-
"
177-
));
147+
cmd.unchecked_output().stdout_matches_path(
148+
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
149+
.join("tests/fixtures/can_execute_script_command_with_args.stdout"),
150+
);
178151
});
179152

180153
// Tests that the run command can run functions with return values
@@ -196,20 +169,10 @@ contract Demo {
196169
)
197170
.unwrap();
198171
cmd.arg("script").arg(script);
199-
let output = cmd.stdout_lossy();
200-
assert!(output.ends_with(
201-
"Compiler run successful
202-
Script ran successfully.
203-
Gas used: 1836
204-
205-
== Return ==
206-
result: uint256 255
207-
1: uint8 3
208-
209-
== Logs ==
210-
script ran
211-
"
212-
));
172+
cmd.unchecked_output().stdout_matches_path(
173+
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
174+
.join("tests/fixtures/can_execute_script_command_with_returned.stdout"),
175+
);
213176
});
214177

215178
forgetest_async!(

0 commit comments

Comments
 (0)