Skip to content

Commit da1f9f4

Browse files
ckoopmannmattsse
andauthored
forge script Avoid bulk gasEstimation when setting --skip-simulation (#2601)
* Extend test for --skip-simulation script call * fix: Only estimate gas immediately before tx submission when is set Co-authored-by: Matthias Seitz <[email protected]>
1 parent d153812 commit da1f9f4

File tree

2 files changed

+167
-96
lines changed

2 files changed

+167
-96
lines changed

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

Lines changed: 96 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ use crate::{
99
update_progress,
1010
};
1111
use ethers::{
12-
prelude::{Signer, SignerMiddleware, TxHash},
13-
providers::Middleware,
12+
prelude::{Provider, Signer, SignerMiddleware, TxHash},
13+
providers::{JsonRpcClient, Middleware},
1414
types::transaction::eip2718::TypedTransaction,
1515
utils::format_units,
1616
};
17-
use eyre::ContextCompat;
17+
use eyre::{ContextCompat, WrapErr};
1818
use foundry_common::{get_http_provider, RetryProvider};
1919
use foundry_config::Chain;
2020
use futures::StreamExt;
@@ -188,9 +188,9 @@ impl ScriptArgs {
188188
}
189189

190190
match signer {
191-
WalletType::Local(signer) => broadcast(signer, tx).await,
192-
WalletType::Ledger(signer) => broadcast(signer, tx).await,
193-
WalletType::Trezor(signer) => broadcast(signer, tx).await,
191+
WalletType::Local(signer) => self.broadcast(signer, tx).await,
192+
WalletType::Ledger(signer) => self.broadcast(signer, tx).await,
193+
WalletType::Trezor(signer) => self.broadcast(signer, tx).await,
194194
}
195195
}
196196

@@ -277,43 +277,106 @@ impl ScriptArgs {
277277
for mut tx in txes.into_iter() {
278278
tx.change_type(is_legacy);
279279

280-
let typed_tx = tx.typed_tx_mut();
280+
if !self.skip_simulation {
281+
let typed_tx = tx.typed_tx_mut();
281282

282-
if has_different_gas_calc(chain) || self.skip_simulation {
283-
typed_tx.set_gas(
284-
provider.estimate_gas(typed_tx).await? * self.gas_estimate_multiplier / 100,
285-
);
286-
}
283+
if has_different_gas_calc(chain) {
284+
self.estimate_gas(typed_tx, &provider).await?;
285+
}
287286

288-
total_gas += *typed_tx.gas().expect("gas is set");
287+
total_gas += *typed_tx.gas().expect("gas is set");
288+
}
289289

290290
new_txes.push_back(tx);
291291
}
292292

293-
// We don't store it in the transactions, since we want the most updated value. Right before
294-
// broadcasting.
295-
let per_gas = if let Some(gas_price) = self.with_gas_price {
296-
gas_price
297-
} else {
298-
match new_txes.front().unwrap().typed_tx() {
299-
TypedTransaction::Legacy(_) | TypedTransaction::Eip2930(_) => {
300-
provider.get_gas_price().await?
293+
if !self.skip_simulation {
294+
// We don't store it in the transactions, since we want the most updated value. Right
295+
// before broadcasting.
296+
let per_gas = if let Some(gas_price) = self.with_gas_price {
297+
gas_price
298+
} else {
299+
match new_txes.front().unwrap().typed_tx() {
300+
TypedTransaction::Legacy(_) | TypedTransaction::Eip2930(_) => {
301+
provider.get_gas_price().await?
302+
}
303+
TypedTransaction::Eip1559(_) => provider.estimate_eip1559_fees(None).await?.0,
301304
}
302-
TypedTransaction::Eip1559(_) => provider.estimate_eip1559_fees(None).await?.0,
303-
}
304-
};
305+
};
305306

306-
println!("\n==========================");
307-
println!("\nEstimated total gas used for script: {}", total_gas);
308-
println!(
309-
"\nEstimated amount required: {} ETH",
310-
format_units(total_gas.saturating_mul(per_gas), 18)
311-
.unwrap_or_else(|_| "[Could not calculate]".to_string())
312-
.trim_end_matches('0')
313-
);
314-
println!("\n==========================");
307+
println!("\n==========================");
308+
println!("\nEstimated total gas used for script: {}", total_gas);
309+
println!(
310+
"\nEstimated amount required: {} ETH",
311+
format_units(total_gas.saturating_mul(per_gas), 18)
312+
.unwrap_or_else(|_| "[Could not calculate]".to_string())
313+
.trim_end_matches('0')
314+
);
315+
println!("\n==========================");
316+
}
315317
Ok(new_txes)
316318
}
319+
/// Uses the signer to submit a transaction to the network. If it fails, it tries to retrieve
320+
/// the transaction hash that can be used on a later run with `--resume`.
321+
async fn broadcast<T, U>(
322+
&self,
323+
signer: &SignerMiddleware<T, U>,
324+
mut legacy_or_1559: TypedTransaction,
325+
) -> Result<TxHash, BroadcastError>
326+
where
327+
T: Middleware,
328+
U: Signer,
329+
{
330+
tracing::debug!("sending transaction: {:?}", legacy_or_1559);
331+
332+
// Chains which use `eth_estimateGas` are being sent sequentially and require their gas to
333+
// be re-estimated right before broadcasting.
334+
if has_different_gas_calc(signer.signer().chain_id()) || self.skip_simulation {
335+
// if already set, some RPC endpoints might simply return the gas value that is already
336+
// set in the request and omit the estimate altogether, so we remove it here
337+
let _ = legacy_or_1559.gas_mut().take();
338+
339+
self.estimate_gas(&mut legacy_or_1559, signer.provider()).await?;
340+
}
341+
342+
// Signing manually so we skip `fill_transaction` and its `eth_createAccessList` request.
343+
let signature = signer
344+
.sign_transaction(
345+
&legacy_or_1559,
346+
*legacy_or_1559.from().expect("Tx should have a `from`."),
347+
)
348+
.await
349+
.map_err(|err| BroadcastError::Simple(err.to_string()))?;
350+
351+
// Submit the raw transaction
352+
let pending = signer
353+
.provider()
354+
.send_raw_transaction(legacy_or_1559.rlp_signed(&signature))
355+
.await
356+
.map_err(|err| BroadcastError::Simple(err.to_string()))?;
357+
358+
Ok(pending.tx_hash())
359+
}
360+
361+
async fn estimate_gas<T>(
362+
&self,
363+
tx: &mut TypedTransaction,
364+
provider: &Provider<T>,
365+
) -> Result<(), BroadcastError>
366+
where
367+
T: JsonRpcClient,
368+
{
369+
tx.set_gas(
370+
provider
371+
.estimate_gas(tx)
372+
.await
373+
.wrap_err_with(|| format!("Failed to estimate gas for tx: {}", tx.sighash()))
374+
.map_err(|err| BroadcastError::Simple(err.to_string()))? *
375+
self.gas_estimate_multiplier /
376+
100,
377+
);
378+
Ok(())
379+
}
317380
}
318381

319382
#[derive(thiserror::Error, Debug, Clone)]
@@ -332,50 +395,3 @@ impl fmt::Display for BroadcastError {
332395
}
333396
}
334397
}
335-
336-
/// Uses the signer to submit a transaction to the network. If it fails, it tries to retrieve the
337-
/// transaction hash that can be used on a later run with `--resume`.
338-
async fn broadcast<T, U>(
339-
signer: &SignerMiddleware<T, U>,
340-
mut legacy_or_1559: TypedTransaction,
341-
) -> Result<TxHash, BroadcastError>
342-
where
343-
T: Middleware,
344-
U: Signer,
345-
{
346-
tracing::debug!("sending transaction: {:?}", legacy_or_1559);
347-
348-
// Chains which use `eth_estimateGas` are being sent sequentially and require their gas to be
349-
// re-estimated right before broadcasting.
350-
if has_different_gas_calc(signer.signer().chain_id()) {
351-
// if already set, some RPC endpoints might simply return the gas value that is already set
352-
// in the request and omit the estimate altogether, so we remove it here
353-
let _ = legacy_or_1559.gas_mut().take();
354-
355-
legacy_or_1559.set_gas(
356-
signer
357-
.provider()
358-
.estimate_gas(&legacy_or_1559)
359-
.await
360-
.map_err(|err| BroadcastError::Simple(err.to_string()))?,
361-
);
362-
}
363-
364-
// Signing manually so we skip `fill_transaction` and its `eth_createAccessList` request.
365-
let signature = signer
366-
.sign_transaction(
367-
&legacy_or_1559,
368-
*legacy_or_1559.from().expect("Tx should have a `from`."),
369-
)
370-
.await
371-
.map_err(|err| BroadcastError::Simple(err.to_string()))?;
372-
373-
// Submit the raw transaction
374-
let pending = signer
375-
.provider()
376-
.send_raw_transaction(legacy_or_1559.rlp_signed(&signature))
377-
.await
378-
.map_err(|err| BroadcastError::Simple(err.to_string()))?;
379-
380-
Ok(pending.tx_hash())
381-
}

cli/tests/it/script.rs

Lines changed: 71 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
//! Contains various tests related to forge script
22
use anvil::{spawn, NodeConfig};
3+
use cast::SimpleCast;
34
use ethers::abi::Address;
45
use foundry_cli_test_utils::{
56
forgetest, forgetest_async, forgetest_init,
@@ -8,6 +9,7 @@ use foundry_cli_test_utils::{
89
};
910
use foundry_utils::rpc;
1011
use regex::Regex;
12+
use serde_json::Value;
1113
use std::{env, path::PathBuf, str::FromStr};
1214

1315
// Tests that fork cheat codes can be used in script
@@ -180,10 +182,10 @@ forgetest_async!(
180182
|prj: TestProject, mut cmd: TestCommand| async move {
181183
foundry_cli_test_utils::util::initialize(prj.root());
182184
// This example script would fail in on-chain simulation
183-
let script = prj
185+
let deploy_script = prj
184186
.inner()
185187
.add_source(
186-
"Foo",
188+
"DeployScript",
187189
r#"
188190
// SPDX-License-Identifier: UNLICENSED
189191
pragma solidity 0.8.10;
@@ -196,36 +198,90 @@ contract HashChecker {
196198
require(newHash != lastHash, "Hash didn't change");
197199
lastHash = newHash;
198200
}
201+
202+
function checkLastHash() public {
203+
require(lastHash != bytes32(0), "Hash shouldn't be zero");
204+
}
199205
}
200-
contract Demo is Script {
206+
contract DeployScript is Script {
201207
function run() external returns (uint256 result, uint8) {
202208
vm.startBroadcast();
203209
HashChecker hashChecker = new HashChecker();
204-
uint numUpdates = 8;
205-
vm.roll(block.number - numUpdates);
206-
for(uint i = 0; i < numUpdates; i++) {
207-
vm.roll(block.number + 1);
208-
hashChecker.update();
209-
}
210210
}
211211
}"#,
212212
)
213213
.unwrap();
214214

215+
let deploy_contract = deploy_script.display().to_string() + ":DeployScript";
216+
215217
let node_config = NodeConfig::test()
216218
.with_eth_rpc_url(Some(rpc::next_http_archive_rpc_endpoint()))
217219
.silent();
218-
219220
let (_api, handle) = spawn(node_config).await;
220-
let target_contract = script.display().to_string() + ":Demo";
221-
222-
let wallet = handle.dev_wallets().next().unwrap();
223-
let private_key = hex::encode(wallet.signer().to_bytes());
221+
let private_key =
222+
"ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80".to_string();
224223
cmd.set_current_dir(prj.root());
225224

226225
cmd.args([
227226
"script",
228-
&target_contract,
227+
&deploy_contract,
228+
"--root",
229+
prj.root().to_str().unwrap(),
230+
"--fork-url",
231+
&handle.http_endpoint(),
232+
"-vvvvv",
233+
"--broadcast",
234+
"--slow",
235+
"--skip-simulation",
236+
"--private-key",
237+
&private_key,
238+
]);
239+
240+
let output = cmd.stdout_lossy();
241+
242+
assert!(output.contains("SKIPPING ON CHAIN SIMULATION"));
243+
assert!(output.contains("ONCHAIN EXECUTION COMPLETE & SUCCESSFUL"));
244+
245+
let run_log =
246+
std::fs::read_to_string("broadcast/DeployScript.sol/1/run-latest.json").unwrap();
247+
let run_object: Value = serde_json::from_str(&run_log).unwrap();
248+
let contract_address = SimpleCast::checksum_address(
249+
&ethers::prelude::H160::from_str(
250+
run_object["receipts"][0]["contractAddress"].as_str().unwrap(),
251+
)
252+
.unwrap(),
253+
)
254+
.unwrap();
255+
256+
let run_code = r#"
257+
// SPDX-License-Identifier: UNLICENSED
258+
pragma solidity 0.8.10;
259+
import "forge-std/Script.sol";
260+
import { HashChecker } from "./DeployScript.sol";
261+
262+
contract RunScript is Script {
263+
function run() external returns (uint256 result, uint8) {
264+
vm.startBroadcast();
265+
HashChecker hashChecker = HashChecker(CONTRACT_ADDRESS);
266+
uint numUpdates = 8;
267+
vm.roll(block.number - numUpdates);
268+
for(uint i = 0; i < numUpdates; i++) {
269+
vm.roll(block.number + 1);
270+
hashChecker.update();
271+
hashChecker.checkLastHash();
272+
}
273+
}
274+
}"#
275+
.replace("CONTRACT_ADDRESS", &contract_address);
276+
277+
let run_script = prj.inner().add_source("RunScript", run_code).unwrap();
278+
let run_contract = run_script.display().to_string() + ":RunScript";
279+
280+
cmd.forge_fuse();
281+
cmd.set_current_dir(prj.root());
282+
cmd.args([
283+
"script",
284+
&run_contract,
229285
"--root",
230286
prj.root().to_str().unwrap(),
231287
"--fork-url",
@@ -241,7 +297,6 @@ contract Demo is Script {
241297
]);
242298

243299
let output = cmd.stdout_lossy();
244-
245300
assert!(output.contains("SKIPPING ON CHAIN SIMULATION"));
246301
assert!(output.contains("ONCHAIN EXECUTION COMPLETE & SUCCESSFUL"));
247302
}

0 commit comments

Comments
 (0)