From 6179036bf8eb6a04ddceaaede50b16a8821509cf Mon Sep 17 00:00:00 2001 From: Arsenii Kulikov Date: Tue, 27 Feb 2024 20:56:56 +0400 Subject: [PATCH 01/33] [wip] script refactoring --- crates/common/src/contracts.rs | 17 -- crates/forge/bin/cmd/script/build.rs | 335 ++++++++++++++------------- crates/forge/bin/cmd/script/cmd.rs | 146 +++++------- crates/forge/bin/cmd/script/mod.rs | 4 +- 4 files changed, 240 insertions(+), 262 deletions(-) diff --git a/crates/common/src/contracts.rs b/crates/common/src/contracts.rs index a1b251b768dd..2687b93e3784 100644 --- a/crates/common/src/contracts.rs +++ b/crates/common/src/contracts.rs @@ -135,23 +135,6 @@ unsafe fn count_different_bytes(a: &[u8], b: &[u8]) -> usize { sum } -/// Flattens the contracts into (`id` -> (`JsonAbi`, `Vec`)) pairs -pub fn flatten_contracts( - contracts: &BTreeMap, - deployed_code: bool, -) -> ContractsByArtifact { - ContractsByArtifact( - contracts - .iter() - .filter_map(|(id, c)| { - let bytecode = - if deployed_code { c.deployed_bytecode.bytes() } else { c.bytecode.bytes() }; - bytecode.cloned().map(|code| (id.clone(), (c.abi.clone(), code.into()))) - }) - .collect(), - ) -} - /// Artifact/Contract identifier can take the following form: /// `:`, the `artifact file name` is the name of the json file of /// the contract's artifact and the contract name is the name of the solidity contract, like diff --git a/crates/forge/bin/cmd/script/build.rs b/crates/forge/bin/cmd/script/build.rs index a2bdc1490d4b..6ddfc86e881a 100644 --- a/crates/forge/bin/cmd/script/build.rs +++ b/crates/forge/bin/cmd/script/build.rs @@ -1,134 +1,162 @@ use super::{ScriptArgs, ScriptConfig}; use alloy_primitives::{Address, Bytes}; -use eyre::{Context, ContextCompat, Result}; +use eyre::{Context, OptionExt, Result}; use foundry_cli::utils::get_cached_entry_by_name; -use foundry_common::compile::{self, ContractSources, ProjectCompiler}; +use foundry_common::{ + compile::{self, ContractSources, ProjectCompiler}, + ContractsByArtifact, +}; use foundry_compilers::{ artifacts::{ContractBytecode, ContractBytecodeSome, Libraries}, cache::SolFilesCache, contracts::ArtifactContracts, info::ContractInfo, - ArtifactId, Project, ProjectCompileOutput, + ArtifactId, }; use foundry_linking::{LinkOutput, Linker}; use std::str::FromStr; -impl ScriptArgs { - /// Compiles the file or project and the verify metadata. - pub fn compile(&mut self, script_config: &mut ScriptConfig) -> Result { - trace!(target: "script", "compiling script"); - - self.build(script_config) - } - - /// Compiles the file with auto-detection and compiler params. - pub fn build(&mut self, script_config: &mut ScriptConfig) -> Result { - let (project, output) = self.get_project_and_output(script_config)?; - let root = project.root(); - let output = output.with_stripped_file_prefixes(root); - let sources = ContractSources::from_project_output(&output, root)?; - let contracts = output.into_artifacts().collect(); - - let target = self.find_target(&project, &contracts)?.clone(); - script_config.target_contract = Some(target.clone()); +pub struct PreprocessedState { + pub args: ScriptArgs, + pub script_config: ScriptConfig, +} - let libraries = script_config.config.libraries_with_remappings()?; - let linker = Linker::new(project.root(), contracts); +impl PreprocessedState { + pub fn compile(self) -> Result { + let project = self.script_config.config.project()?; + let filters = self.args.opts.skip.clone().unwrap_or_default(); - let (highlevel_known_contracts, libraries, predeploy_libraries) = self.link_script_target( - &linker, - libraries, - script_config.evm_opts.sender, - script_config.sender_nonce, - target.clone(), - )?; + let mut target_name = self.args.target_contract.clone(); - let contract = highlevel_known_contracts.get(&target).unwrap(); + // If we've received correct path, use it as target_path + // Otherwise, parse input as : and use the path from the contract info, if + // present. + let target_path = if let Ok(path) = dunce::canonicalize(&self.args.path) { + Ok::<_, eyre::Report>(Some(path)) + } else { + let contract = ContractInfo::from_str(&self.args.path)?; + target_name = Some(contract.name.clone()); + if let Some(path) = contract.path { + Ok(Some(dunce::canonicalize(path)?)) + } else { + Ok(None) + } + }?; - Ok(BuildOutput { - project, - linker, - contract: contract.clone(), - highlevel_known_contracts, - libraries, - predeploy_libraries, - sources, - }) - } + // If we've found target path above, only compile it. + // Otherwise, compile everything to match contract by name later. + let output = if let Some(target_path) = target_path.clone() { + compile::compile_target_with_filter( + &target_path, + &project, + self.args.opts.args.silent, + self.args.verify, + filters, + ) + } else if !project.paths.has_input_files() { + Err(eyre::eyre!("The project doesn't have any input files. Make sure the `script` directory is configured properly in foundry.toml. Otherwise, provide the path to the file.")) + } else { + ProjectCompiler::new().compile(&project) + }?; - /// Tries to find artifact for the target script contract. - pub fn find_target<'a>( - &self, - project: &Project, - contracts: &'a ArtifactContracts, - ) -> Result<&'a ArtifactId> { - let mut target_fname = dunce::canonicalize(&self.path) - .wrap_err("Couldn't convert contract path to absolute path.")? - .strip_prefix(project.root()) - .wrap_err("Couldn't strip project root from contract path.")? - .to_str() - .wrap_err("Bad path to string.")? - .to_string(); - - let no_target_name = if let Some(target_name) = &self.target_contract { - target_fname = target_fname + ":" + target_name; - false + // If we still don't have target path, find it by name in the compilation cache. + let target_path = if let Some(target_path) = target_path { + target_path } else { - true + let target_name = target_name.clone().expect("was set above"); + let cache = SolFilesCache::read_joined(&project.paths) + .wrap_err("Could not open compiler cache")?; + let (path, _) = get_cached_entry_by_name(&cache, &target_name) + .wrap_err("Could not find target contract in cache")?; + path }; - let mut target: Option<&ArtifactId> = None; - - for (id, contract) in contracts.iter() { - if no_target_name { - // Match artifact source, and ignore interfaces - if id.source == std::path::Path::new(&target_fname) && - contract.bytecode.as_ref().map_or(false, |b| b.object.bytes_len() > 0) - { - if let Some(target) = target { - // We might have multiple artifacts for the same contract but with different - // solc versions. Their names will have form of {name}.0.X.Y, so we are - // stripping versions off before comparing them. - let target_name = target.name.split('.').next().unwrap(); - let id_name = id.name.split('.').next().unwrap(); - if target_name != id_name { - eyre::bail!("Multiple contracts in the target path. Please specify the contract name with `--tc ContractName`") - } - } - target = Some(id); + let target_path = project.root().join(target_path); + + let mut target_id: Option = None; + + // Find target artfifact id by name and path in compilation artifacts. + for (id, contract) in output.artifact_ids().filter(|(id, _)| id.source == target_path) { + if let Some(name) = &target_name { + if id.name != *name { + continue; } - } else { - let (path, name) = - target_fname.rsplit_once(':').expect("The target specifier is malformed."); - let path = std::path::Path::new(path); - if path == id.source && name == id.name { - target = Some(id); + } else if !contract.bytecode.as_ref().map_or(false, |b| b.object.bytes_len() > 0) { + // Ignore contracts with empty/missing bytecode, e.g. interfaces. + continue; + } + + if let Some(target) = target_id { + // We might have multiple artifacts for the same contract but with different + // solc versions. Their names will have form of {name}.0.X.Y, so we are + // stripping versions off before comparing them. + let target_name = target.name.split('.').next().unwrap(); + let id_name = id.name.split('.').next().unwrap(); + if target_name != id_name { + eyre::bail!("Multiple contracts in the target path. Please specify the contract name with `--tc ContractName`") } } + target_id = Some(id); } - target.ok_or_else(|| eyre::eyre!("Could not find target contract: {}", target_fname)) + let sources = ContractSources::from_project_output(&output, project.root())?; + let contracts = output.into_artifacts().collect(); + let target = target_id.ok_or_eyre("Could not find target contract")?; + let linker = Linker::new(project.root(), contracts); + + Ok(CompiledState { + args: self.args, + script_config: self.script_config, + build_data: BuildData { sources, linker, target }, + }) } +} + +pub struct BuildData { + pub linker: Linker, + pub target: ArtifactId, + pub sources: ContractSources, +} - /// Links script artifact with given libraries or library addresses computed from script sender - /// and nonce. - /// - /// Populates [BuildOutput] with linked target contract, libraries, bytes of libs that need to - /// be predeployed and `highlevel_known_contracts` - set of known fully linked contracts - pub fn link_script_target( - &self, - linker: &Linker, - libraries: Libraries, +impl BuildData { + /// Links the build data with given libraries, sender and nonce. + pub fn link( + self, + known_libraries: Libraries, sender: Address, nonce: u64, - target: ArtifactId, - ) -> Result<(ArtifactContracts, Libraries, Vec)> { - let LinkOutput { libs_to_deploy, libraries } = - linker.link_with_nonce_or_address(libraries, sender, nonce, &target)?; - - // Collect all linked contracts with non-empty bytecode - let highlevel_known_contracts = linker - .get_linked_artifacts(&libraries)? + ) -> Result { + let link_output = + self.linker.link_with_nonce_or_address(known_libraries, sender, nonce, &self.target)?; + + LinkedBuildData::new(link_output, self) + } + + /// Links the build data with the given libraries. + pub fn link_with_libraries(self, libraries: Libraries) -> Result { + let link_output = + self.linker.link_with_nonce_or_address(libraries, Address::ZERO, 0, &self.target)?; + + if !link_output.libs_to_deploy.is_empty() { + eyre::bail!("incomplete libraries set"); + } + + LinkedBuildData::new(link_output, self) + } +} + +pub struct LinkedBuildData { + pub build_data: BuildData, + pub highlevel_known_contracts: ArtifactContracts, + pub libraries: Libraries, + pub predeploy_libraries: Vec, +} + +impl LinkedBuildData { + pub fn new(link_output: LinkOutput, build_data: BuildData) -> Result { + let highlevel_known_contracts = build_data + .linker + .get_linked_artifacts(&link_output.libraries)? .iter() .filter_map(|(id, contract)| { ContractBytecodeSome::try_from(ContractBytecode::from(contract.clone())) @@ -138,71 +166,58 @@ impl ScriptArgs { .filter(|(_, tc)| tc.bytecode.object.is_non_empty_bytecode()) .collect(); - Ok((highlevel_known_contracts, libraries, libs_to_deploy)) + Ok(Self { + build_data, + highlevel_known_contracts, + libraries: link_output.libraries, + predeploy_libraries: link_output.libs_to_deploy, + }) } - pub fn get_project_and_output( - &mut self, - script_config: &ScriptConfig, - ) -> Result<(Project, ProjectCompileOutput)> { - let project = script_config.config.project()?; - - let filters = self.opts.skip.clone().unwrap_or_default(); - // We received a valid file path. - // If this file does not exist, `dunce::canonicalize` will - // result in an error and it will be handled below. - if let Ok(target_contract) = dunce::canonicalize(&self.path) { - let output = compile::compile_target_with_filter( - &target_contract, - &project, - self.opts.args.silent, - self.verify, - filters, - )?; - return Ok((project, output)) - } - - if !project.paths.has_input_files() { - eyre::bail!("The project doesn't have any input files. Make sure the `script` directory is configured properly in foundry.toml. Otherwise, provide the path to the file.") - } - - let contract = ContractInfo::from_str(&self.path)?; - self.target_contract = Some(contract.name.clone()); + /// Flattens the contracts into (`id` -> (`JsonAbi`, `Vec`)) pairs + pub fn get_flattened_contracts(&self, deployed_code: bool) -> ContractsByArtifact { + ContractsByArtifact( + self.highlevel_known_contracts + .iter() + .filter_map(|(id, c)| { + let bytecode = if deployed_code { + c.deployed_bytecode.bytes() + } else { + c.bytecode.bytes() + }; + bytecode.cloned().map(|code| (id.clone(), (c.abi.clone(), code.into()))) + }) + .collect(), + ) + } - // We received `contract_path:contract_name` - if let Some(path) = contract.path { - let path = - dunce::canonicalize(path).wrap_err("Could not canonicalize the target path")?; - let output = compile::compile_target_with_filter( - &path, - &project, - self.opts.args.silent, - self.verify, - filters, - )?; - self.path = path.to_string_lossy().to_string(); - return Ok((project, output)) - } + /// Fetches target bytecode from linked contracts. + pub fn get_target_contract(&self) -> Result { + self.highlevel_known_contracts + .get(&self.build_data.target) + .cloned() + .ok_or_eyre("target not found in linked artifacts") + } +} - // We received `contract_name`, and need to find its file path. - let output = ProjectCompiler::new().compile(&project)?; - let cache = - SolFilesCache::read_joined(&project.paths).wrap_err("Could not open compiler cache")?; +pub struct CompiledState { + args: ScriptArgs, + script_config: ScriptConfig, + build_data: BuildData, +} - let (path, _) = get_cached_entry_by_name(&cache, &contract.name) - .wrap_err("Could not find target contract in cache")?; - self.path = path.to_string_lossy().to_string(); +impl CompiledState { + pub fn link(self) -> Result { + let sender = self.script_config.evm_opts.sender; + let nonce = self.script_config.sender_nonce; + let known_libraries = self.script_config.config.libraries_with_remappings()?; + let build_data = self.build_data.link(known_libraries, sender, nonce)?; - Ok((project, output)) + Ok(LinkedState { args: self.args, build_data }) } } -pub struct BuildOutput { - pub project: Project, - pub contract: ContractBytecodeSome, - pub linker: Linker, - pub highlevel_known_contracts: ArtifactContracts, - pub libraries: Libraries, - pub predeploy_libraries: Vec, - pub sources: ContractSources, +pub struct LinkedState { + pub args: ScriptArgs, + pub build_data: LinkedBuildData, } diff --git a/crates/forge/bin/cmd/script/cmd.rs b/crates/forge/bin/cmd/script/cmd.rs index f864f29f8bc0..1bf456621621 100644 --- a/crates/forge/bin/cmd/script/cmd.rs +++ b/crates/forge/bin/cmd/script/cmd.rs @@ -2,29 +2,23 @@ use super::{ multi::MultiChainSequence, sequence::ScriptSequence, verify::VerifyBundle, ScriptArgs, ScriptConfig, ScriptResult, }; -use crate::cmd::script::{build::BuildOutput, receipts}; -use alloy_primitives::{Address, Bytes}; +use crate::cmd::script::{ + build::{LinkedBuildData, LinkedState, PreprocessedState}, + receipts, +}; +use alloy_primitives::Address; use ethers_providers::Middleware; use ethers_signers::Signer; -use eyre::{OptionExt, Result}; +use eyre::Result; use forge::traces::CallTraceDecoder; use foundry_cli::utils::LoadConfig; -use foundry_common::{ - contracts::flatten_contracts, provider::ethers::try_get_http_provider, types::ToAlloy, -}; -use foundry_compilers::{ - artifacts::{ContractBytecodeSome, Libraries}, - contracts::ArtifactContracts, -}; +use foundry_common::{provider::ethers::try_get_http_provider, types::ToAlloy}; +use foundry_compilers::artifacts::Libraries; use foundry_debugger::Debugger; use foundry_evm::inspectors::cheatcodes::{BroadcastableTransaction, ScriptWallets}; -use foundry_linking::Linker; use foundry_wallets::WalletSigner; use std::{collections::HashMap, sync::Arc}; -/// Helper alias type for the collection of data changed due to the new sender. -type NewSenderChanges = (CallTraceDecoder, Libraries, ArtifactContracts); - impl ScriptArgs { /// Executes the script pub async fn run_script(mut self) -> Result<()> { @@ -53,32 +47,27 @@ impl ScriptArgs { script_config.config.libraries = Default::default(); } - let build_output = self.compile(&mut script_config)?; + let state = PreprocessedState { args: self.clone(), script_config: script_config.clone() }; + + let LinkedState { build_data, .. } = state.compile()?.link()?; + script_config.target_contract = Some(build_data.build_data.target.clone()); let mut verify = VerifyBundle::new( - &build_output.project, + &script_config.config.project()?, &script_config.config, - flatten_contracts(&build_output.highlevel_known_contracts, false), + build_data.get_flattened_contracts(false), self.retry, self.verifier.clone(), ); - let BuildOutput { - contract, - mut highlevel_known_contracts, - predeploy_libraries, - linker, - sources, - mut libraries, - .. - } = build_output; - // Execute once with default sender. let sender = script_config.evm_opts.sender; let multi_wallet = self.wallets.get_multi_wallet().await?; let script_wallets = ScriptWallets::new(multi_wallet, self.evm_opts.sender); + let contract = build_data.get_target_contract()?; + // We need to execute the script even if just resuming, in case we need to collect private // keys from the execution. let mut result = self @@ -86,42 +75,40 @@ impl ScriptArgs { &mut script_config, contract, sender, - &predeploy_libraries, + &build_data.predeploy_libraries, script_wallets.clone(), ) .await?; if self.resume || (self.verify && !self.broadcast) { let signers = script_wallets.into_multi_wallet().into_signers()?; - return self.resume_deployment(script_config, linker, libraries, verify, &signers).await; + return self.resume_deployment(script_config, build_data, verify, &signers).await; } - let known_contracts = flatten_contracts(&highlevel_known_contracts, true); + let known_contracts = build_data.get_flattened_contracts(true); let mut decoder = self.decode_traces(&script_config, &mut result, &known_contracts)?; if self.debug { let mut debugger = Debugger::builder() .debug_arenas(result.debug.as_deref().unwrap_or_default()) .decoder(&decoder) - .sources(sources) + .sources(build_data.build_data.sources.clone()) .breakpoints(result.breakpoints.clone()) .build(); debugger.try_run()?; } - if let Some((new_traces, updated_libraries, updated_contracts)) = self + let (maybe_new_traces, build_data) = self .maybe_prepare_libraries( &mut script_config, - linker, - predeploy_libraries, + build_data, &mut result, script_wallets.clone(), ) - .await? - { + .await?; + + if let Some(new_traces) = maybe_new_traces { decoder = new_traces; - highlevel_known_contracts = updated_contracts; - libraries = updated_libraries; } if self.json { @@ -130,14 +117,14 @@ impl ScriptArgs { self.show_traces(&script_config, &decoder, &mut result).await?; } - verify.known_contracts = flatten_contracts(&highlevel_known_contracts, false); - self.check_contract_sizes(&result, &highlevel_known_contracts)?; + verify.known_contracts = build_data.get_flattened_contracts(false); + self.check_contract_sizes(&result, &build_data.highlevel_known_contracts)?; let signers = script_wallets.into_multi_wallet().into_signers()?; self.handle_broadcastable_transactions( result, - libraries, + build_data.libraries, &decoder, script_config, verify, @@ -151,36 +138,41 @@ impl ScriptArgs { async fn maybe_prepare_libraries( &mut self, script_config: &mut ScriptConfig, - linker: Linker, - predeploy_libraries: Vec, + build_data: LinkedBuildData, result: &mut ScriptResult, script_wallets: ScriptWallets, - ) -> Result> { + ) -> Result<(Option, LinkedBuildData)> { if let Some(new_sender) = self.maybe_new_sender( &script_config.evm_opts, result.transactions.as_ref(), - &predeploy_libraries, + !build_data.predeploy_libraries.is_empty(), )? { // We have a new sender, so we need to relink all the predeployed libraries. - let (libraries, highlevel_known_contracts) = self - .rerun_with_new_deployer(script_config, new_sender, result, linker, script_wallets) + let build_data = self + .rerun_with_new_deployer( + script_config, + new_sender, + result, + build_data, + script_wallets, + ) .await?; // redo traces for the new addresses let new_traces = self.decode_traces( &*script_config, result, - &flatten_contracts(&highlevel_known_contracts, true), + &build_data.get_flattened_contracts(true), )?; - return Ok(Some((new_traces, libraries, highlevel_known_contracts))); + return Ok((Some(new_traces), build_data)); } // Add predeploy libraries to the list of broadcastable transactions. let mut lib_deploy = self.create_deploy_transactions( script_config.evm_opts.sender, script_config.sender_nonce, - &predeploy_libraries, + &build_data.predeploy_libraries, &script_config.evm_opts.fork_url, ); @@ -194,15 +186,14 @@ impl ScriptArgs { *txs = lib_deploy; } - Ok(None) + Ok((None, build_data)) } /// Resumes the deployment and/or verification of the script. async fn resume_deployment( &mut self, script_config: ScriptConfig, - linker: Linker, - libraries: Libraries, + build_data: LinkedBuildData, verify: VerifyBundle, signers: &HashMap, ) -> Result<()> { @@ -214,7 +205,7 @@ impl ScriptArgs { &self.sig, script_config.target_contract(), )?, - libraries, + build_data.libraries, &script_config.config, verify, signers, @@ -223,7 +214,7 @@ impl ScriptArgs { } self.resume_single_deployment( script_config, - linker, + build_data, verify, signers, ) @@ -237,7 +228,7 @@ impl ScriptArgs { async fn resume_single_deployment( &mut self, script_config: ScriptConfig, - linker: Linker, + build_data: LinkedBuildData, mut verify: VerifyBundle, signers: &HashMap, ) -> Result<()> { @@ -285,25 +276,14 @@ impl ScriptArgs { } if self.verify { - let target = script_config.target_contract(); let libraries = Libraries::parse(&deployment_sequence.libraries)? - .with_stripped_file_prefixes(linker.root.as_path()); + .with_stripped_file_prefixes(build_data.build_data.linker.root.as_path()); // We might have predeployed libraries from the broadcasting, so we need to // relink the contracts with them, since their mapping is // not included in the solc cache files. - let (highlevel_known_contracts, _, predeploy_libraries) = self.link_script_target( - &linker, - libraries, - script_config.config.sender, // irrelevant, since we're not creating any - 0, // irrelevant, since we're not creating any - target.clone(), - )?; - - if !predeploy_libraries.is_empty() { - eyre::bail!("Incomplete set of libraries in deployment artifact."); - } + let build_data = build_data.build_data.link_with_libraries(libraries)?; - verify.known_contracts = flatten_contracts(&highlevel_known_contracts, false); + verify.known_contracts = build_data.get_flattened_contracts(true); deployment_sequence.verify_contracts(&script_config.config, verify).await?; } @@ -317,9 +297,9 @@ impl ScriptArgs { script_config: &mut ScriptConfig, new_sender: Address, first_run_result: &mut ScriptResult, - linker: Linker, + build_data: LinkedBuildData, script_wallets: ScriptWallets, - ) -> Result<(Libraries, ArtifactContracts)> { + ) -> Result { // if we had a new sender that requires relinking, we need to // get the nonce mainnet for accurate addresses for predeploy libs let nonce = forge::next_nonce( @@ -331,27 +311,27 @@ impl ScriptArgs { ) .await?; script_config.sender_nonce = nonce; - let target = script_config.target_contract(); let libraries = script_config.config.libraries_with_remappings()?; - let (highlevel_known_contracts, libraries, predeploy_libraries) = - self.link_script_target(&linker, libraries, new_sender, nonce, target.clone())?; - - let contract = highlevel_known_contracts - .get(target) - .ok_or_eyre("target not found in linked artifacts")? - .clone(); + let build_data = build_data.build_data.link(libraries, new_sender, nonce)?; + let contract = build_data.get_target_contract()?; let mut txs = self.create_deploy_transactions( new_sender, nonce, - &predeploy_libraries, + &build_data.predeploy_libraries, &script_config.evm_opts.fork_url, ); let result = self - .execute(script_config, contract, new_sender, &predeploy_libraries, script_wallets) + .execute( + script_config, + contract, + new_sender, + &build_data.predeploy_libraries, + script_wallets, + ) .await?; if let Some(new_txs) = &result.transactions { @@ -366,7 +346,7 @@ impl ScriptArgs { *first_run_result = result; first_run_result.transactions = Some(txs); - Ok((libraries, highlevel_known_contracts)) + Ok(build_data) } /// In case the user has loaded *only* one private-key, we can assume that he's using it as the diff --git a/crates/forge/bin/cmd/script/mod.rs b/crates/forge/bin/cmd/script/mod.rs index 79c8f937ff12..05f0ca3626e0 100644 --- a/crates/forge/bin/cmd/script/mod.rs +++ b/crates/forge/bin/cmd/script/mod.rs @@ -379,13 +379,13 @@ impl ScriptArgs { &self, evm_opts: &EvmOpts, transactions: Option<&BroadcastableTransactions>, - predeploy_libraries: &[Bytes], + has_predeploy_libraries: bool, ) -> Result> { let mut new_sender = None; if let Some(txs) = transactions { // If the user passed a `--sender` don't check anything. - if !predeploy_libraries.is_empty() && self.evm_opts.sender.is_none() { + if has_predeploy_libraries && self.evm_opts.sender.is_none() { for tx in txs.iter() { if tx.transaction.to.is_none() { let sender = tx.transaction.from.expect("no sender"); From ab2001b77ce19ae4c186524a1c8dbf9a6d01ef95 Mon Sep 17 00:00:00 2001 From: Arsenii Kulikov Date: Thu, 29 Feb 2024 01:58:27 +0400 Subject: [PATCH 02/33] execution refactor --- Cargo.lock | 1 + crates/forge/Cargo.toml | 24 ++- crates/forge/bin/cmd/script/build.rs | 9 +- crates/forge/bin/cmd/script/cmd.rs | 175 ++-------------- crates/forge/bin/cmd/script/executor.rs | 265 ++++++++++++++++++++---- crates/forge/bin/cmd/script/mod.rs | 64 +----- 6 files changed, 265 insertions(+), 273 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c35e148ff2ba..5cd851c7128e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2905,6 +2905,7 @@ dependencies = [ "alloy-primitives", "alloy-rpc-types", "anvil", + "async-recursion", "async-trait", "axum", "clap", diff --git a/crates/forge/Cargo.toml b/crates/forge/Cargo.toml index 6d4e40f6b97f..033d9125b458 100644 --- a/crates/forge/Cargo.toml +++ b/crates/forge/Cargo.toml @@ -15,7 +15,11 @@ name = "forge" path = "bin/main.rs" [build-dependencies] -vergen = { workspace = true, default-features = false, features = ["build", "git", "gitcl"] } +vergen = { workspace = true, default-features = false, features = [ + "build", + "git", + "gitcl", +] } [dependencies] # lib @@ -56,6 +60,7 @@ alloy-primitives = { workspace = true, features = ["serde"] } alloy-rpc-types.workspace = true async-trait = "0.1" +async-recursion = "1.0.5" clap = { version = "4", features = ["derive", "env", "unicode", "wrap_help"] } clap_complete = "4" clap_complete_fig = "4" @@ -94,14 +99,25 @@ globset = "0.4" paste = "1.0" path-slash = "0.2" pretty_assertions.workspace = true -svm = { package = "svm-rs", version = "0.3", default-features = false, features = ["rustls"] } +svm = { package = "svm-rs", version = "0.3", default-features = false, features = [ + "rustls", +] } tempfile = "3" tracing-subscriber = { workspace = true, features = ["env-filter", "fmt"] } [features] default = ["rustls"] -rustls = ["foundry-cli/rustls", "foundry-wallets/rustls", "reqwest/rustls-tls", "reqwest/rustls-tls-native-roots"] -openssl = ["foundry-cli/openssl", "reqwest/default-tls", "foundry-wallets/openssl"] +rustls = [ + "foundry-cli/rustls", + "foundry-wallets/rustls", + "reqwest/rustls-tls", + "reqwest/rustls-tls-native-roots", +] +openssl = [ + "foundry-cli/openssl", + "reqwest/default-tls", + "foundry-wallets/openssl", +] asm-keccak = ["alloy-primitives/asm-keccak"] [[bench]] diff --git a/crates/forge/bin/cmd/script/build.rs b/crates/forge/bin/cmd/script/build.rs index 6ddfc86e881a..0eac94c0ef4e 100644 --- a/crates/forge/bin/cmd/script/build.rs +++ b/crates/forge/bin/cmd/script/build.rs @@ -201,9 +201,9 @@ impl LinkedBuildData { } pub struct CompiledState { - args: ScriptArgs, - script_config: ScriptConfig, - build_data: BuildData, + pub args: ScriptArgs, + pub script_config: ScriptConfig, + pub build_data: BuildData, } impl CompiledState { @@ -213,11 +213,12 @@ impl CompiledState { let known_libraries = self.script_config.config.libraries_with_remappings()?; let build_data = self.build_data.link(known_libraries, sender, nonce)?; - Ok(LinkedState { args: self.args, build_data }) + Ok(LinkedState { args: self.args, script_config: self.script_config, build_data }) } } pub struct LinkedState { pub args: ScriptArgs, + pub script_config: ScriptConfig, pub build_data: LinkedBuildData, } diff --git a/crates/forge/bin/cmd/script/cmd.rs b/crates/forge/bin/cmd/script/cmd.rs index 1bf456621621..256d98163568 100644 --- a/crates/forge/bin/cmd/script/cmd.rs +++ b/crates/forge/bin/cmd/script/cmd.rs @@ -1,21 +1,20 @@ use super::{ multi::MultiChainSequence, sequence::ScriptSequence, verify::VerifyBundle, ScriptArgs, - ScriptConfig, ScriptResult, + ScriptConfig, }; use crate::cmd::script::{ - build::{LinkedBuildData, LinkedState, PreprocessedState}, + build::{LinkedBuildData, PreprocessedState}, + executor::ExecutedState, receipts, }; use alloy_primitives::Address; use ethers_providers::Middleware; use ethers_signers::Signer; use eyre::Result; -use forge::traces::CallTraceDecoder; use foundry_cli::utils::LoadConfig; use foundry_common::{provider::ethers::try_get_http_provider, types::ToAlloy}; use foundry_compilers::artifacts::Libraries; use foundry_debugger::Debugger; -use foundry_evm::inspectors::cheatcodes::{BroadcastableTransaction, ScriptWallets}; use foundry_wallets::WalletSigner; use std::{collections::HashMap, sync::Arc}; @@ -25,32 +24,23 @@ impl ScriptArgs { trace!(target: "script", "executing script command"); let (config, evm_opts) = self.load_config_and_evm_opts_emit_warnings()?; - let mut script_config = ScriptConfig { - // dapptools compatibility - sender_nonce: 1, - config, - evm_opts, - debug: self.debug, - ..Default::default() - }; + let mut script_config = + ScriptConfig { config, evm_opts, debug: self.debug, ..Default::default() }; if let Some(sender) = self.maybe_load_private_key()? { script_config.evm_opts.sender = sender; } - if let Some(ref fork_url) = script_config.evm_opts.fork_url { - // when forking, override the sender's nonce to the onchain value - script_config.sender_nonce = - forge::next_nonce(script_config.evm_opts.sender, fork_url, None).await? - } else { + if script_config.evm_opts.fork_url.is_none() { // if not forking, then ignore any pre-deployed library addresses script_config.config.libraries = Default::default(); } let state = PreprocessedState { args: self.clone(), script_config: script_config.clone() }; - - let LinkedState { build_data, .. } = state.compile()?.link()?; + let ExecutedState { build_data, execution_result, execution_data, .. } = + state.compile()?.link()?.prepare_execution().await?.execute().await?; script_config.target_contract = Some(build_data.build_data.target.clone()); + script_config.called_function = Some(execution_data.func); let mut verify = VerifyBundle::new( &script_config.config.project()?, @@ -60,25 +50,11 @@ impl ScriptArgs { self.verifier.clone(), ); - // Execute once with default sender. - let sender = script_config.evm_opts.sender; - - let multi_wallet = self.wallets.get_multi_wallet().await?; - let script_wallets = ScriptWallets::new(multi_wallet, self.evm_opts.sender); - - let contract = build_data.get_target_contract()?; + let script_wallets = execution_data.script_wallets; // We need to execute the script even if just resuming, in case we need to collect private // keys from the execution. - let mut result = self - .execute( - &mut script_config, - contract, - sender, - &build_data.predeploy_libraries, - script_wallets.clone(), - ) - .await?; + let mut result = execution_result; if self.resume || (self.verify && !self.broadcast) { let signers = script_wallets.into_multi_wallet().into_signers()?; @@ -86,7 +62,7 @@ impl ScriptArgs { } let known_contracts = build_data.get_flattened_contracts(true); - let mut decoder = self.decode_traces(&script_config, &mut result, &known_contracts)?; + let decoder = self.decode_traces(&script_config, &mut result, &known_contracts)?; if self.debug { let mut debugger = Debugger::builder() @@ -98,19 +74,6 @@ impl ScriptArgs { debugger.try_run()?; } - let (maybe_new_traces, build_data) = self - .maybe_prepare_libraries( - &mut script_config, - build_data, - &mut result, - script_wallets.clone(), - ) - .await?; - - if let Some(new_traces) = maybe_new_traces { - decoder = new_traces; - } - if self.json { self.show_json(&script_config, &result)?; } else { @@ -133,62 +96,6 @@ impl ScriptArgs { .await } - // In case there are libraries to be deployed, it makes sure that these are added to the list of - // broadcastable transactions with the appropriate sender. - async fn maybe_prepare_libraries( - &mut self, - script_config: &mut ScriptConfig, - build_data: LinkedBuildData, - result: &mut ScriptResult, - script_wallets: ScriptWallets, - ) -> Result<(Option, LinkedBuildData)> { - if let Some(new_sender) = self.maybe_new_sender( - &script_config.evm_opts, - result.transactions.as_ref(), - !build_data.predeploy_libraries.is_empty(), - )? { - // We have a new sender, so we need to relink all the predeployed libraries. - let build_data = self - .rerun_with_new_deployer( - script_config, - new_sender, - result, - build_data, - script_wallets, - ) - .await?; - - // redo traces for the new addresses - let new_traces = self.decode_traces( - &*script_config, - result, - &build_data.get_flattened_contracts(true), - )?; - - return Ok((Some(new_traces), build_data)); - } - - // Add predeploy libraries to the list of broadcastable transactions. - let mut lib_deploy = self.create_deploy_transactions( - script_config.evm_opts.sender, - script_config.sender_nonce, - &build_data.predeploy_libraries, - &script_config.evm_opts.fork_url, - ); - - if let Some(txs) = &mut result.transactions { - for tx in txs.iter() { - lib_deploy.push_back(BroadcastableTransaction { - rpc: tx.rpc.clone(), - transaction: tx.transaction.clone(), - }); - } - *txs = lib_deploy; - } - - Ok((None, build_data)) - } - /// Resumes the deployment and/or verification of the script. async fn resume_deployment( &mut self, @@ -291,64 +198,6 @@ impl ScriptArgs { Ok(()) } - /// Reruns the execution with a new sender and relinks the libraries accordingly - async fn rerun_with_new_deployer( - &mut self, - script_config: &mut ScriptConfig, - new_sender: Address, - first_run_result: &mut ScriptResult, - build_data: LinkedBuildData, - script_wallets: ScriptWallets, - ) -> Result { - // if we had a new sender that requires relinking, we need to - // get the nonce mainnet for accurate addresses for predeploy libs - let nonce = forge::next_nonce( - new_sender, - script_config.evm_opts.fork_url.as_ref().ok_or_else(|| { - eyre::eyre!("You must provide an RPC URL (see --fork-url) when broadcasting.") - })?, - None, - ) - .await?; - script_config.sender_nonce = nonce; - - let libraries = script_config.config.libraries_with_remappings()?; - - let build_data = build_data.build_data.link(libraries, new_sender, nonce)?; - let contract = build_data.get_target_contract()?; - - let mut txs = self.create_deploy_transactions( - new_sender, - nonce, - &build_data.predeploy_libraries, - &script_config.evm_opts.fork_url, - ); - - let result = self - .execute( - script_config, - contract, - new_sender, - &build_data.predeploy_libraries, - script_wallets, - ) - .await?; - - if let Some(new_txs) = &result.transactions { - for new_tx in new_txs.iter() { - txs.push_back(BroadcastableTransaction { - rpc: new_tx.rpc.clone(), - transaction: new_tx.transaction.clone(), - }); - } - } - - *first_run_result = result; - first_run_result.transactions = Some(txs); - - Ok(build_data) - } - /// In case the user has loaded *only* one private-key, we can assume that he's using it as the /// `--sender` fn maybe_load_private_key(&mut self) -> Result> { diff --git a/crates/forge/bin/cmd/script/executor.rs b/crates/forge/bin/cmd/script/executor.rs index a78f5b9f451a..0335b7a3b650 100644 --- a/crates/forge/bin/cmd/script/executor.rs +++ b/crates/forge/bin/cmd/script/executor.rs @@ -1,15 +1,23 @@ use super::{ artifacts::ArtifactInfo, + build::{CompiledState, LinkedBuildData, LinkedState}, runner::{ScriptRunner, SimulationStage}, transaction::{AdditionalContract, TransactionWithMetadata}, ScriptArgs, ScriptConfig, ScriptResult, }; -use alloy_primitives::{Address, Bytes, U256}; +use alloy_json_abi::{Function, JsonAbi}; +use alloy_primitives::{Address, Bytes, U256, U64}; +use alloy_rpc_types::request::TransactionRequest; +use async_recursion::async_recursion; use eyre::{Context, Result}; use forge::{ - backend::Backend, + backend::{Backend, DatabaseExt}, executors::ExecutorBuilder, - inspectors::{cheatcodes::BroadcastableTransactions, CheatsConfig}, + inspectors::{ + cheatcodes::{BroadcastableTransaction, BroadcastableTransactions}, + CheatsConfig, + }, + revm::Database, traces::{render_trace_arena, CallTraceDecoder}, }; use foundry_cli::utils::{ensure_clean_constructor, needs_setup}; @@ -23,69 +31,244 @@ use std::{ sync::Arc, }; -impl ScriptArgs { - /// Locally deploys and executes the contract method that will collect all broadcastable - /// transactions. - pub async fn execute( - &self, - script_config: &mut ScriptConfig, - contract: ContractBytecodeSome, - sender: Address, - predeploy_libraries: &[Bytes], - script_wallets: ScriptWallets, - ) -> Result { - trace!(target: "script", "start executing script"); +pub struct ExecutionData { + pub script_wallets: ScriptWallets, + pub func: Function, + pub calldata: Bytes, + pub bytecode: Bytes, + pub abi: JsonAbi, +} + +pub struct PreExecutionState { + pub args: ScriptArgs, + pub script_config: ScriptConfig, + pub build_data: LinkedBuildData, + pub execution_data: ExecutionData, +} + +impl LinkedState { + /// Given linked and compiled artifacts, prepares data we need for execution. + pub async fn prepare_execution(self) -> Result { + let multi_wallet = self.args.wallets.get_multi_wallet().await?; + let script_wallets = ScriptWallets::new(multi_wallet, self.args.evm_opts.sender); - let ContractBytecodeSome { abi, bytecode, .. } = contract; + let ContractBytecodeSome { abi, bytecode, .. } = self.build_data.get_target_contract()?; let bytecode = bytecode.into_bytes().ok_or_else(|| { eyre::eyre!("expected fully linked bytecode, found unlinked bytecode") })?; + let (func, calldata) = self.args.get_method_and_calldata(&abi)?; + ensure_clean_constructor(&abi)?; + Ok(PreExecutionState { + args: self.args, + script_config: self.script_config, + build_data: self.build_data, + execution_data: ExecutionData { script_wallets, func, calldata, bytecode, abi }, + }) + } +} + +pub struct ExecutedState { + pub args: ScriptArgs, + pub script_config: ScriptConfig, + pub build_data: LinkedBuildData, + pub execution_data: ExecutionData, + pub execution_result: ScriptResult, +} + +impl PreExecutionState { + #[async_recursion] + pub async fn execute(mut self) -> Result { let mut runner = self - .prepare_runner(script_config, sender, SimulationStage::Local, Some(script_wallets)) + .prepare_runner( + SimulationStage::Local, + Some(self.execution_data.script_wallets.clone()), + ) .await?; - let (address, mut result) = runner.setup( - predeploy_libraries, - bytecode, - needs_setup(&abi), - script_config.sender_nonce, - self.broadcast, - script_config.evm_opts.fork_url.is_none(), - )?; - let (func, calldata) = self.get_method_and_calldata(&abi)?; - script_config.called_function = Some(func); + self.script_config.sender_nonce = if self.script_config.evm_opts.fork_url.is_none() { + // dapptools compatibility + 1 + } else { + runner + .executor + .backend + .basic(self.script_config.evm_opts.sender)? + .unwrap_or_default() + .nonce + }; + + let mut result = self.execute_with_runner(&mut runner).await?; + + // If we have a new sender from execution, we need to use it to deploy libraries and relink + // contracts. + if let Some(new_sender) = self.maybe_new_sender(result.transactions.as_ref())? { + self.script_config.evm_opts.sender = new_sender; + + // Rollback to linking state to relink contracts with the new sender. + let state = CompiledState { + args: self.args, + script_config: self.script_config, + build_data: self.build_data.build_data, + }; + + return state.link()?.prepare_execution().await?.execute().await; + } + + // Add library deployment transactions to broadcastable transactions list. + if let Some(txs) = &mut result.transactions { + let mut library_txs = self + .build_data + .predeploy_libraries + .iter() + .enumerate() + .map(|(i, bytes)| BroadcastableTransaction { + rpc: self.script_config.evm_opts.fork_url.clone(), + transaction: TransactionRequest { + from: Some(self.script_config.evm_opts.sender), + input: Some(bytes.clone()).into(), + nonce: Some(U64::from(self.script_config.sender_nonce + i as u64)), + ..Default::default() + }, + }) + .collect::>(); + + for tx in txs.iter() { + library_txs.push_back(BroadcastableTransaction { + rpc: tx.rpc.clone(), + transaction: tx.transaction.clone(), + }); + } + *txs = library_txs; + } + + Ok(ExecutedState { + args: self.args, + script_config: self.script_config, + build_data: self.build_data, + execution_data: self.execution_data, + execution_result: result, + }) + } + + pub async fn execute_with_runner(&self, runner: &mut ScriptRunner) -> Result { + let (address, mut setup_result) = runner.setup( + &self.build_data.predeploy_libraries, + self.execution_data.bytecode.clone(), + needs_setup(&self.execution_data.abi), + self.script_config.sender_nonce, + self.args.broadcast, + self.script_config.evm_opts.fork_url.is_none(), + )?; - // Only call the method if `setUp()` succeeded. - if result.success { - let script_result = runner.script(address, calldata)?; + if setup_result.success { + let script_result = runner.script(address, self.execution_data.calldata.clone())?; - result.success &= script_result.success; - result.gas_used = script_result.gas_used; - result.logs.extend(script_result.logs); - result.traces.extend(script_result.traces); - result.debug = script_result.debug; - result.labeled_addresses.extend(script_result.labeled_addresses); - result.returned = script_result.returned; - result.breakpoints = script_result.breakpoints; + setup_result.success &= script_result.success; + setup_result.gas_used = script_result.gas_used; + setup_result.logs.extend(script_result.logs); + setup_result.traces.extend(script_result.traces); + setup_result.debug = script_result.debug; + setup_result.labeled_addresses.extend(script_result.labeled_addresses); + setup_result.returned = script_result.returned; + setup_result.breakpoints = script_result.breakpoints; - match (&mut result.transactions, script_result.transactions) { + match (&mut setup_result.transactions, script_result.transactions) { (Some(txs), Some(new_txs)) => { txs.extend(new_txs); } (None, Some(new_txs)) => { - result.transactions = Some(new_txs); + setup_result.transactions = Some(new_txs); } _ => {} } } - Ok(result) + Ok(setup_result) + } + + fn maybe_new_sender( + &self, + transactions: Option<&BroadcastableTransactions>, + ) -> Result> { + let mut new_sender = None; + + if let Some(txs) = transactions { + // If the user passed a `--sender` don't check anything. + if !self.build_data.predeploy_libraries.is_empty() && + self.args.evm_opts.sender.is_none() + { + for tx in txs.iter() { + if tx.transaction.to.is_none() { + let sender = tx.transaction.from.expect("no sender"); + if let Some(ns) = new_sender { + if sender != ns { + shell::println("You have more than one deployer who could predeploy libraries. Using `--sender` instead.")?; + return Ok(None); + } + } else if sender != self.script_config.evm_opts.sender { + new_sender = Some(sender); + } + } + } + } + } + Ok(new_sender) + } + + /// Creates the Runner that drives script execution + async fn prepare_runner( + &mut self, + stage: SimulationStage, + script_wallets: Option, + ) -> Result { + trace!("preparing script runner"); + let env = self.script_config.evm_opts.evm_env().await?; + + let fork = if self.script_config.evm_opts.fork_url.is_some() { + self.script_config.evm_opts.get_fork(&self.script_config.config, env.clone()) + } else { + None + }; + + let backend = Backend::spawn(fork).await; + + // Cache forks + if let Some(fork_url) = backend.active_fork_url() { + self.script_config.backends.insert(fork_url.clone(), backend.clone()); + } + + // We need to enable tracing to decode contract names: local or external. + let mut builder = ExecutorBuilder::new() + .inspectors(|stack| stack.trace(true)) + .spec(self.script_config.config.evm_spec_id()) + .gas_limit(self.script_config.evm_opts.gas_limit()); + + if let SimulationStage::Local = stage { + builder = builder.inspectors(|stack| { + stack.debug(self.args.debug).cheatcodes( + CheatsConfig::new( + &self.script_config.config, + self.script_config.evm_opts.clone(), + script_wallets, + ) + .into(), + ) + }); + } + + Ok(ScriptRunner::new( + builder.build(env, backend), + self.script_config.evm_opts.initial_balance, + self.script_config.evm_opts.sender, + )) } +} +impl ScriptArgs { /// Simulates onchain state by executing a list of transactions locally and persisting their /// state. Returns the transactions and any CREATE2 contract address created. pub async fn onchain_simulation( diff --git a/crates/forge/bin/cmd/script/mod.rs b/crates/forge/bin/cmd/script/mod.rs index 05f0ca3626e0..8594fdacced7 100644 --- a/crates/forge/bin/cmd/script/mod.rs +++ b/crates/forge/bin/cmd/script/mod.rs @@ -1,8 +1,7 @@ use super::build::BuildArgs; use alloy_dyn_abi::FunctionExt; use alloy_json_abi::{Function, InternalType, JsonAbi}; -use alloy_primitives::{Address, Bytes, Log, U256, U64}; -use alloy_rpc_types::request::TransactionRequest; +use alloy_primitives::{Address, Bytes, Log, U256}; use clap::{Parser, ValueHint}; use dialoguer::Confirm; use ethers_providers::{Http, Middleware}; @@ -39,9 +38,8 @@ use foundry_config::{ Config, NamedChain, }; use foundry_evm::{ - constants::DEFAULT_CREATE2_DEPLOYER, - decode::RevertDecoder, - inspectors::cheatcodes::{BroadcastableTransaction, BroadcastableTransactions}, + constants::DEFAULT_CREATE2_DEPLOYER, decode::RevertDecoder, + inspectors::cheatcodes::BroadcastableTransactions, }; use foundry_wallets::MultiWalletOpts; use futures::future; @@ -371,62 +369,6 @@ impl ScriptArgs { Ok(()) } - /// It finds the deployer from the running script and uses it to predeploy libraries. - /// - /// If there are multiple candidate addresses, it skips everything and lets `--sender` deploy - /// them instead. - fn maybe_new_sender( - &self, - evm_opts: &EvmOpts, - transactions: Option<&BroadcastableTransactions>, - has_predeploy_libraries: bool, - ) -> Result> { - let mut new_sender = None; - - if let Some(txs) = transactions { - // If the user passed a `--sender` don't check anything. - if has_predeploy_libraries && self.evm_opts.sender.is_none() { - for tx in txs.iter() { - if tx.transaction.to.is_none() { - let sender = tx.transaction.from.expect("no sender"); - if let Some(ns) = new_sender { - if sender != ns { - shell::println("You have more than one deployer who could predeploy libraries. Using `--sender` instead.")?; - return Ok(None); - } - } else if sender != evm_opts.sender { - new_sender = Some(sender); - } - } - } - } - } - Ok(new_sender) - } - - /// Helper for building the transactions for any libraries that need to be deployed ahead of - /// linking - fn create_deploy_transactions( - &self, - from: Address, - nonce: u64, - data: &[Bytes], - fork_url: &Option, - ) -> BroadcastableTransactions { - data.iter() - .enumerate() - .map(|(i, bytes)| BroadcastableTransaction { - rpc: fork_url.clone(), - transaction: TransactionRequest { - from: Some(from), - input: Some(bytes.clone()).into(), - nonce: Some(U64::from(nonce + i as u64)), - ..Default::default() - }, - }) - .collect() - } - /// Returns the Function and calldata based on the signature /// /// If the `sig` is a valid human-readable function we find the corresponding function in the From 43c7bc4db298b4fa6c700422a5a7aa8de75af0e0 Mon Sep 17 00:00:00 2001 From: Arsenii Kulikov Date: Thu, 29 Feb 2024 17:25:03 +0400 Subject: [PATCH 03/33] refactor simulation --- crates/forge/bin/cmd/script/broadcast.rs | 712 ++++++++++++++--------- crates/forge/bin/cmd/script/cmd.rs | 96 ++- crates/forge/bin/cmd/script/executor.rs | 523 +++++++---------- crates/forge/bin/cmd/script/mod.rs | 291 ++++----- crates/forge/bin/cmd/script/runner.rs | 6 - crates/forge/bin/cmd/script/sequence.rs | 3 +- 6 files changed, 799 insertions(+), 832 deletions(-) diff --git a/crates/forge/bin/cmd/script/broadcast.rs b/crates/forge/bin/cmd/script/broadcast.rs index a7ab056332ad..9395f4cc7d9a 100644 --- a/crates/forge/bin/cmd/script/broadcast.rs +++ b/crates/forge/bin/cmd/script/broadcast.rs @@ -1,37 +1,428 @@ use super::{ - multi::MultiChainSequence, providers::ProvidersManager, receipts::clear_pendings, - sequence::ScriptSequence, transaction::TransactionWithMetadata, verify::VerifyBundle, - NestedValue, ScriptArgs, ScriptConfig, ScriptResult, + artifacts::ArtifactInfo, + build::LinkedBuildData, + executor::{ExecutionArtifacts, ExecutionData, PreSimulationState}, + multi::MultiChainSequence, + providers::ProvidersManager, + receipts::clear_pendings, + runner::ScriptRunner, + sequence::ScriptSequence, + transaction::TransactionWithMetadata, + verify::VerifyBundle, + ScriptArgs, ScriptConfig, }; +use crate::cmd::script::transaction::AdditionalContract; use alloy_primitives::{utils::format_units, Address, TxHash, U256}; use ethers_core::types::transaction::eip2718::TypedTransaction; use ethers_providers::{JsonRpcClient, Middleware, Provider}; use ethers_signers::Signer; use eyre::{bail, Context, ContextCompat, Result}; -use forge::{inspectors::cheatcodes::BroadcastableTransactions, traces::CallTraceDecoder}; +use forge::{inspectors::cheatcodes::BroadcastableTransactions, traces::render_trace_arena}; use foundry_cli::{ init_progress, update_progress, utils::{has_batch_support, has_different_gas_calc}, }; use foundry_common::{ + get_contract_name, provider::{ alloy::RpcUrl, ethers::{estimate_eip1559_fees, try_get_http_provider, RetryProvider}, }, shell, types::{ToAlloy, ToEthers}, - ContractsByArtifact, }; -use foundry_compilers::{artifacts::Libraries, ArtifactId}; +use foundry_compilers::artifacts::Libraries; use foundry_config::Config; use foundry_wallets::WalletSigner; -use futures::StreamExt; +use futures::{future::join_all, StreamExt}; +use parking_lot::RwLock; use std::{ cmp::min, - collections::{HashMap, HashSet, VecDeque}, + collections::{BTreeMap, HashMap, HashSet, VecDeque}, sync::Arc, }; +impl PreSimulationState { + pub async fn fill_metadata(self) -> Result { + let transactions = if let Some(txs) = self.execution_result.transactions.as_ref() { + if self.args.skip_simulation { + self.no_simulation(txs.clone())? + } else { + self.onchain_simulation(txs.clone()).await? + } + } else { + VecDeque::new() + }; + + Ok(FilledTransactionsState { + args: self.args, + script_config: self.script_config, + build_data: self.build_data, + execution_data: self.execution_data, + execution_artifacts: self.execution_artifacts, + transactions, + }) + } + + pub async fn onchain_simulation( + &self, + transactions: BroadcastableTransactions, + ) -> Result> { + trace!(target: "script", "executing onchain simulation"); + + let runners = Arc::new( + self.build_runners() + .await? + .into_iter() + .map(|(rpc, runner)| (rpc, Arc::new(RwLock::new(runner)))) + .collect::>(), + ); + + if self.script_config.evm_opts.verbosity > 3 { + println!("=========================="); + println!("Simulated On-chain Traces:\n"); + } + + let contracts = self.build_data.get_flattened_contracts(false); + let address_to_abi: BTreeMap = self + .execution_artifacts + .decoder + .contracts + .iter() + .filter_map(|(addr, contract_id)| { + let contract_name = get_contract_name(contract_id); + if let Ok(Some((_, (abi, code)))) = + contracts.find_by_name_or_identifier(contract_name) + { + let info = ArtifactInfo { + contract_name: contract_name.to_string(), + contract_id: contract_id.to_string(), + abi, + code, + }; + return Some((*addr, info)); + } + None + }) + .collect(); + + let mut final_txs = VecDeque::new(); + + // Executes all transactions from the different forks concurrently. + let futs = transactions + .into_iter() + .map(|transaction| async { + let rpc = transaction.rpc.as_ref().expect("missing broadcastable tx rpc url"); + let mut runner = runners.get(rpc).expect("invalid rpc url").write(); + + let mut tx = transaction.transaction; + let result = runner + .simulate( + tx.from + .expect("transaction doesn't have a `from` address at execution time"), + tx.to, + tx.input.clone().into_input(), + tx.value, + ) + .wrap_err("Internal EVM error during simulation")?; + + if !result.success || result.traces.is_empty() { + return Ok((None, result.traces)); + } + + let created_contracts = result + .traces + .iter() + .flat_map(|(_, traces)| { + traces.nodes().iter().filter_map(|node| { + if node.trace.kind.is_any_create() { + return Some(AdditionalContract { + opcode: node.trace.kind, + address: node.trace.address, + init_code: node.trace.data.clone(), + }); + } + None + }) + }) + .collect(); + + // Simulate mining the transaction if the user passes `--slow`. + if self.args.slow { + runner.executor.env.block.number += U256::from(1); + } + + let is_fixed_gas_limit = tx.gas.is_some(); + match tx.gas { + // If tx.gas is already set that means it was specified in script + Some(gas) => { + println!("Gas limit was set in script to {gas}"); + } + // We inflate the gas used by the user specified percentage + None => { + let gas = + U256::from(result.gas_used * self.args.gas_estimate_multiplier / 100); + tx.gas = Some(gas); + } + } + + let tx = TransactionWithMetadata::new( + tx, + transaction.rpc, + &result, + &address_to_abi, + &self.execution_artifacts.decoder, + created_contracts, + is_fixed_gas_limit, + )?; + + eyre::Ok((Some(tx), result.traces)) + }) + .collect::>(); + + let mut abort = false; + for res in join_all(futs).await { + let (tx, traces) = res?; + + // Transaction will be `None`, if execution didn't pass. + if tx.is_none() || self.script_config.evm_opts.verbosity > 3 { + // Identify all contracts created during the call. + if traces.is_empty() { + eyre::bail!( + "forge script requires tracing enabled to collect created contracts" + ); + } + + for (_, trace) in &traces { + println!( + "{}", + render_trace_arena(trace, &self.execution_artifacts.decoder).await? + ); + } + } + + if let Some(tx) = tx { + final_txs.push_back(tx); + } else { + abort = true; + } + } + + if abort { + eyre::bail!("Simulated execution failed.") + } + + Ok(final_txs) + } + + /// Build the multiple runners from different forks. + async fn build_runners(&self) -> Result> { + if !shell::verbosity().is_silent() { + let n = self.script_config.total_rpcs.len(); + let s = if n != 1 { "s" } else { "" }; + println!("\n## Setting up {n} EVM{s}."); + } + + let futs = self + .script_config + .total_rpcs + .iter() + .map(|rpc| async { + let mut script_config = self.script_config.clone(); + let runner = script_config.get_runner(Some(rpc.clone()), false).await?; + Ok((rpc.clone(), runner)) + }) + .collect::>(); + + join_all(futs).await.into_iter().collect() + } + + fn no_simulation( + &self, + transactions: BroadcastableTransactions, + ) -> Result> { + Ok(transactions + .into_iter() + .map(|tx| TransactionWithMetadata::from_tx_request(tx.transaction)) + .collect()) + } +} + +pub struct FilledTransactionsState { + pub args: ScriptArgs, + pub script_config: ScriptConfig, + pub build_data: LinkedBuildData, + pub execution_data: ExecutionData, + pub execution_artifacts: ExecutionArtifacts, + pub transactions: VecDeque, +} + +impl FilledTransactionsState { + /// Returns all transactions of the [`TransactionWithMetadata`] type in a list of + /// [`ScriptSequence`]. List length will be higher than 1, if we're dealing with a multi + /// chain deployment. + /// + /// Each transaction will be added with the correct transaction type and gas estimation. + pub async fn bundle(self) -> Result { + // User might be using both "in-code" forks and `--fork-url`. + let last_rpc = &self.transactions.back().expect("exists; qed").rpc; + let is_multi_deployment = self.transactions.iter().any(|tx| &tx.rpc != last_rpc); + + let mut total_gas_per_rpc: HashMap = HashMap::new(); + + // Batches sequence of transactions from different rpcs. + let mut new_sequence = VecDeque::new(); + let mut manager = ProvidersManager::default(); + let mut sequences = vec![]; + + // Peeking is used to check if the next rpc url is different. If so, it creates a + // [`ScriptSequence`] from all the collected transactions up to this point. + let mut txes_iter = self.transactions.clone().into_iter().peekable(); + + while let Some(mut tx) = txes_iter.next() { + let tx_rpc = match tx.rpc.clone() { + Some(rpc) => rpc, + None => { + let rpc = self.args.evm_opts.ensure_fork_url()?.clone(); + // Fills the RPC inside the transaction, if missing one. + tx.rpc = Some(rpc.clone()); + rpc + } + }; + + let provider_info = manager.get_or_init_provider(&tx_rpc, self.args.legacy).await?; + + // Handles chain specific requirements. + tx.change_type(provider_info.is_legacy); + tx.transaction.set_chain_id(provider_info.chain); + + if !self.args.skip_simulation { + let typed_tx = tx.typed_tx_mut(); + + if has_different_gas_calc(provider_info.chain) { + trace!("estimating with different gas calculation"); + let gas = *typed_tx.gas().expect("gas is set by simulation."); + + // We are trying to show the user an estimation of the total gas usage. + // + // However, some transactions might depend on previous ones. For + // example, tx1 might deploy a contract that tx2 uses. That + // will result in the following `estimate_gas` call to fail, + // since tx1 hasn't been broadcasted yet. + // + // Not exiting here will not be a problem when actually broadcasting, because + // for chains where `has_different_gas_calc` returns true, + // we await each transaction before broadcasting the next + // one. + if let Err(err) = self.estimate_gas(typed_tx, &provider_info.provider).await { + trace!("gas estimation failed: {err}"); + + // Restore gas value, since `estimate_gas` will remove it. + typed_tx.set_gas(gas); + } + } + + let total_gas = total_gas_per_rpc.entry(tx_rpc.clone()).or_insert(U256::ZERO); + *total_gas += (*typed_tx.gas().expect("gas is set")).to_alloy(); + } + + new_sequence.push_back(tx); + // We only create a [`ScriptSequence`] object when we collect all the rpc related + // transactions. + if let Some(next_tx) = txes_iter.peek() { + if next_tx.rpc == Some(tx_rpc) { + continue; + } + } + + let sequence = ScriptSequence::new( + new_sequence, + self.execution_artifacts.returns.clone(), + &self.args.sig, + &self.build_data.build_data.target, + provider_info.chain.into(), + &self.script_config.config, + self.args.broadcast, + is_multi_deployment, + )?; + + sequences.push(sequence); + + new_sequence = VecDeque::new(); + } + + if !self.args.skip_simulation { + // Present gas information on a per RPC basis. + for (rpc, total_gas) in total_gas_per_rpc { + let provider_info = manager.get(&rpc).expect("provider is set."); + + // We don't store it in the transactions, since we want the most updated value. + // Right before broadcasting. + let per_gas = if let Some(gas_price) = self.args.with_gas_price { + gas_price + } else { + provider_info.gas_price()? + }; + + shell::println("\n==========================")?; + shell::println(format!("\nChain {}", provider_info.chain))?; + + shell::println(format!( + "\nEstimated gas price: {} gwei", + format_units(per_gas, 9) + .unwrap_or_else(|_| "[Could not calculate]".to_string()) + .trim_end_matches('0') + .trim_end_matches('.') + ))?; + shell::println(format!("\nEstimated total gas used for script: {total_gas}"))?; + shell::println(format!( + "\nEstimated amount required: {} ETH", + format_units(total_gas.saturating_mul(per_gas), 18) + .unwrap_or_else(|_| "[Could not calculate]".to_string()) + .trim_end_matches('0') + ))?; + shell::println("\n==========================")?; + } + } + Ok(BundledState { + args: self.args, + script_config: self.script_config, + build_data: self.build_data, + execution_data: self.execution_data, + execution_artifacts: self.execution_artifacts, + sequences, + }) + } + + async fn estimate_gas(&self, tx: &mut TypedTransaction, provider: &Provider) -> Result<()> + where + T: JsonRpcClient, + { + // if already set, some RPC endpoints might simply return the gas value that is already + // set in the request and omit the estimate altogether, so we remove it here + let _ = tx.gas_mut().take(); + + tx.set_gas( + provider + .estimate_gas(tx, None) + .await + .wrap_err_with(|| format!("Failed to estimate gas for tx: {:?}", tx.sighash()))? * + self.args.gas_estimate_multiplier / + 100, + ); + Ok(()) + } +} + +pub struct BundledState { + pub args: ScriptArgs, + pub script_config: ScriptConfig, + pub build_data: LinkedBuildData, + pub execution_data: ExecutionData, + pub execution_artifacts: ExecutionArtifacts, + pub sequences: Vec, +} + impl ScriptArgs { /// Sends the transactions which haven't been broadcasted yet. pub async fn send_transactions( @@ -297,69 +688,42 @@ impl ScriptArgs { /// them. pub async fn handle_broadcastable_transactions( &self, - mut result: ScriptResult, - libraries: Libraries, - decoder: &CallTraceDecoder, - mut script_config: ScriptConfig, + mut state: BundledState, verify: VerifyBundle, - signers: &HashMap, ) -> Result<()> { - if let Some(txs) = result.transactions.take() { - script_config.collect_rpcs(&txs); - script_config.check_multi_chain_constraints(&libraries)?; - script_config.check_shanghai_support().await?; - - if !script_config.missing_rpc { - trace!(target: "script", "creating deployments"); - - let mut deployments = self - .create_script_sequences( - txs, - &result, - &mut script_config, - decoder, - &verify.known_contracts, - ) - .await?; - - if script_config.has_multiple_rpcs() { - trace!(target: "script", "broadcasting multi chain deployment"); - - let multi = MultiChainSequence::new( - deployments.clone(), - &self.sig, - script_config.target_contract(), - &script_config.config, - self.broadcast, - )?; - - if self.broadcast { - self.multi_chain_deployment( - multi, - libraries, - &script_config.config, - verify, - signers, - ) - .await?; - } - } else if self.broadcast { - self.single_deployment( - deployments.first_mut().expect("missing deployment"), - script_config, - libraries, - verify, - signers, - ) - .await?; - } + if state.script_config.has_multiple_rpcs() { + trace!(target: "script", "broadcasting multi chain deployment"); - if !self.broadcast { - shell::println("\nSIMULATION COMPLETE. To broadcast these transactions, add --broadcast and wallet configuration(s) to the previous command. See forge script --help for more.")?; - } - } else { - shell::println("\nIf you wish to simulate on-chain transactions pass a RPC URL.")?; + let multi = MultiChainSequence::new( + state.sequences.clone(), + &self.sig, + &state.build_data.build_data.target, + &state.script_config.config, + self.broadcast, + )?; + + if self.broadcast { + self.multi_chain_deployment( + multi, + state.build_data.libraries, + &state.script_config.config, + verify, + &state.script_config.script_wallets.into_multi_wallet().into_signers()?, + ) + .await?; } + } else if self.broadcast { + self.single_deployment( + state.sequences.first_mut().expect("missing deployment"), + state.script_config, + state.build_data.libraries, + verify, + ) + .await?; + } + + if !self.broadcast { + shell::println("\nSIMULATION COMPLETE. To broadcast these transactions, add --broadcast and wallet configuration(s) to the previous command. See forge script --help for more.")?; } Ok(()) } @@ -371,7 +735,6 @@ impl ScriptArgs { script_config: ScriptConfig, libraries: Libraries, verify: VerifyBundle, - signers: &HashMap, ) -> Result<()> { trace!(target: "script", "broadcasting single chain deployment"); @@ -383,7 +746,9 @@ impl ScriptArgs { deployment_sequence.add_libraries(libraries); - self.send_transactions(deployment_sequence, &rpc, signers).await?; + let signers = script_config.script_wallets.into_multi_wallet().into_signers()?; + + self.send_transactions(deployment_sequence, &rpc, &signers).await?; if self.verify { return deployment_sequence.verify_contracts(&script_config.config, verify).await; @@ -391,215 +756,6 @@ impl ScriptArgs { Ok(()) } - /// Given the collected transactions it creates a list of [`ScriptSequence`]. List length will - /// be higher than 1, if we're dealing with a multi chain deployment. - /// - /// If `--skip-simulation` is not passed, it will make an onchain simulation of the transactions - /// before adding them to [`ScriptSequence`]. - async fn create_script_sequences( - &self, - txs: BroadcastableTransactions, - script_result: &ScriptResult, - script_config: &mut ScriptConfig, - decoder: &CallTraceDecoder, - known_contracts: &ContractsByArtifact, - ) -> Result> { - if !txs.is_empty() { - let gas_filled_txs = self - .fills_transactions_with_gas(txs, script_config, decoder, known_contracts) - .await?; - - let returns = self.get_returns(&*script_config, &script_result.returned)?; - - return self - .bundle_transactions( - gas_filled_txs, - &script_config.target_contract().clone(), - &mut script_config.config, - returns, - ) - .await; - } else if self.broadcast { - eyre::bail!("No onchain transactions generated in script"); - } - - Ok(vec![]) - } - - /// Takes the collected transactions and executes them locally before converting them to - /// [`TransactionWithMetadata`] with the appropriate gas execution estimation. If - /// `--skip-simulation` is passed, then it will skip the execution. - async fn fills_transactions_with_gas( - &self, - txs: BroadcastableTransactions, - script_config: &ScriptConfig, - decoder: &CallTraceDecoder, - known_contracts: &ContractsByArtifact, - ) -> Result> { - let gas_filled_txs = if self.skip_simulation { - shell::println("\nSKIPPING ON CHAIN SIMULATION.")?; - txs.into_iter() - .map(|btx| { - let mut tx = TransactionWithMetadata::from_tx_request(btx.transaction); - tx.rpc = btx.rpc; - tx - }) - .collect() - } else { - self.onchain_simulation( - txs, - script_config, - decoder, - known_contracts, - ) - .await - .wrap_err("\nTransaction failed when running the on-chain simulation. Check the trace above for more information.")? - }; - Ok(gas_filled_txs) - } - - /// Returns all transactions of the [`TransactionWithMetadata`] type in a list of - /// [`ScriptSequence`]. List length will be higher than 1, if we're dealing with a multi - /// chain deployment. - /// - /// Each transaction will be added with the correct transaction type and gas estimation. - async fn bundle_transactions( - &self, - transactions: VecDeque, - target: &ArtifactId, - config: &mut Config, - returns: HashMap, - ) -> Result> { - // User might be using both "in-code" forks and `--fork-url`. - let last_rpc = &transactions.back().expect("exists; qed").rpc; - let is_multi_deployment = transactions.iter().any(|tx| &tx.rpc != last_rpc); - - let mut total_gas_per_rpc: HashMap = HashMap::new(); - - // Batches sequence of transactions from different rpcs. - let mut new_sequence = VecDeque::new(); - let mut manager = ProvidersManager::default(); - let mut deployments = vec![]; - - // Config is used to initialize the sequence chain, so we need to change when handling a new - // sequence. This makes sure we don't lose the original value. - let original_config_chain = config.chain; - - // Peeking is used to check if the next rpc url is different. If so, it creates a - // [`ScriptSequence`] from all the collected transactions up to this point. - let mut txes_iter = transactions.into_iter().peekable(); - - while let Some(mut tx) = txes_iter.next() { - let tx_rpc = match tx.rpc.clone() { - Some(rpc) => rpc, - None => { - let rpc = self.evm_opts.ensure_fork_url()?.clone(); - // Fills the RPC inside the transaction, if missing one. - tx.rpc = Some(rpc.clone()); - rpc - } - }; - - let provider_info = manager.get_or_init_provider(&tx_rpc, self.legacy).await?; - - // Handles chain specific requirements. - tx.change_type(provider_info.is_legacy); - tx.transaction.set_chain_id(provider_info.chain); - - if !self.skip_simulation { - let typed_tx = tx.typed_tx_mut(); - - if has_different_gas_calc(provider_info.chain) { - trace!("estimating with different gas calculation"); - let gas = *typed_tx.gas().expect("gas is set by simulation."); - - // We are trying to show the user an estimation of the total gas usage. - // - // However, some transactions might depend on previous ones. For - // example, tx1 might deploy a contract that tx2 uses. That - // will result in the following `estimate_gas` call to fail, - // since tx1 hasn't been broadcasted yet. - // - // Not exiting here will not be a problem when actually broadcasting, because - // for chains where `has_different_gas_calc` returns true, - // we await each transaction before broadcasting the next - // one. - if let Err(err) = self.estimate_gas(typed_tx, &provider_info.provider).await { - trace!("gas estimation failed: {err}"); - - // Restore gas value, since `estimate_gas` will remove it. - typed_tx.set_gas(gas); - } - } - - let total_gas = total_gas_per_rpc.entry(tx_rpc.clone()).or_insert(U256::ZERO); - *total_gas += (*typed_tx.gas().expect("gas is set")).to_alloy(); - } - - new_sequence.push_back(tx); - // We only create a [`ScriptSequence`] object when we collect all the rpc related - // transactions. - if let Some(next_tx) = txes_iter.peek() { - if next_tx.rpc == Some(tx_rpc) { - continue; - } - } - - config.chain = Some(provider_info.chain.into()); - let sequence = ScriptSequence::new( - new_sequence, - returns.clone(), - &self.sig, - target, - config, - self.broadcast, - is_multi_deployment, - )?; - - deployments.push(sequence); - - new_sequence = VecDeque::new(); - } - - // Restore previous config chain. - config.chain = original_config_chain; - - if !self.skip_simulation { - // Present gas information on a per RPC basis. - for (rpc, total_gas) in total_gas_per_rpc { - let provider_info = manager.get(&rpc).expect("provider is set."); - - // We don't store it in the transactions, since we want the most updated value. - // Right before broadcasting. - let per_gas = if let Some(gas_price) = self.with_gas_price { - gas_price - } else { - provider_info.gas_price()? - }; - - shell::println("\n==========================")?; - shell::println(format!("\nChain {}", provider_info.chain))?; - - shell::println(format!( - "\nEstimated gas price: {} gwei", - format_units(per_gas, 9) - .unwrap_or_else(|_| "[Could not calculate]".to_string()) - .trim_end_matches('0') - .trim_end_matches('.') - ))?; - shell::println(format!("\nEstimated total gas used for script: {total_gas}"))?; - shell::println(format!( - "\nEstimated amount required: {} ETH", - format_units(total_gas.saturating_mul(per_gas), 18) - .unwrap_or_else(|_| "[Could not calculate]".to_string()) - .trim_end_matches('0') - ))?; - shell::println("\n==========================")?; - } - } - Ok(deployments) - } - /// Uses the signer to submit a transaction to the network. If it fails, it tries to retrieve /// the transaction hash that can be used on a later run with `--resume`. async fn broadcast( diff --git a/crates/forge/bin/cmd/script/cmd.rs b/crates/forge/bin/cmd/script/cmd.rs index 256d98163568..bb947fd8f317 100644 --- a/crates/forge/bin/cmd/script/cmd.rs +++ b/crates/forge/bin/cmd/script/cmd.rs @@ -4,96 +4,83 @@ use super::{ }; use crate::cmd::script::{ build::{LinkedBuildData, PreprocessedState}, - executor::ExecutedState, receipts, }; use alloy_primitives::Address; use ethers_providers::Middleware; use ethers_signers::Signer; use eyre::Result; +use forge::inspectors::cheatcodes::ScriptWallets; use foundry_cli::utils::LoadConfig; use foundry_common::{provider::ethers::try_get_http_provider, types::ToAlloy}; use foundry_compilers::artifacts::Libraries; -use foundry_debugger::Debugger; -use foundry_wallets::WalletSigner; -use std::{collections::HashMap, sync::Arc}; +use std::sync::Arc; impl ScriptArgs { /// Executes the script pub async fn run_script(mut self) -> Result<()> { trace!(target: "script", "executing script command"); - let (config, evm_opts) = self.load_config_and_evm_opts_emit_warnings()?; - let mut script_config = - ScriptConfig { config, evm_opts, debug: self.debug, ..Default::default() }; + let script_wallets = + ScriptWallets::new(self.wallets.get_multi_wallet().await?, self.evm_opts.sender); + let (config, mut evm_opts) = self.load_config_and_evm_opts_emit_warnings()?; if let Some(sender) = self.maybe_load_private_key()? { - script_config.evm_opts.sender = sender; + evm_opts.sender = sender; } + let mut script_config = ScriptConfig::new(config, evm_opts, script_wallets).await?; + if script_config.evm_opts.fork_url.is_none() { // if not forking, then ignore any pre-deployed library addresses script_config.config.libraries = Default::default(); } let state = PreprocessedState { args: self.clone(), script_config: script_config.clone() }; - let ExecutedState { build_data, execution_result, execution_data, .. } = - state.compile()?.link()?.prepare_execution().await?.execute().await?; - script_config.target_contract = Some(build_data.build_data.target.clone()); - script_config.called_function = Some(execution_data.func); + let state = state + .compile()? + .link()? + .prepare_execution() + .await? + .execute() + .await? + .prepare_simulation() + .await?; + + script_config.target_contract = Some(state.build_data.build_data.target.clone()); + script_config.called_function = Some(state.execution_data.func.clone()); let mut verify = VerifyBundle::new( &script_config.config.project()?, &script_config.config, - build_data.get_flattened_contracts(false), + state.build_data.get_flattened_contracts(false), self.retry, self.verifier.clone(), ); - let script_wallets = execution_data.script_wallets; - - // We need to execute the script even if just resuming, in case we need to collect private - // keys from the execution. - let mut result = execution_result; - if self.resume || (self.verify && !self.broadcast) { - let signers = script_wallets.into_multi_wallet().into_signers()?; - return self.resume_deployment(script_config, build_data, verify, &signers).await; + return self.resume_deployment(script_config, state.build_data, verify).await; } - let known_contracts = build_data.get_flattened_contracts(true); - let decoder = self.decode_traces(&script_config, &mut result, &known_contracts)?; - if self.debug { - let mut debugger = Debugger::builder() - .debug_arenas(result.debug.as_deref().unwrap_or_default()) - .decoder(&decoder) - .sources(build_data.build_data.sources.clone()) - .breakpoints(result.breakpoints.clone()) - .build(); - debugger.try_run()?; - } - - if self.json { - self.show_json(&script_config, &result)?; + state.run_debugger() } else { - self.show_traces(&script_config, &decoder, &mut result).await?; - } + if self.json { + state.show_json()?; + } else { + state.show_traces().await?; + } - verify.known_contracts = build_data.get_flattened_contracts(false); - self.check_contract_sizes(&result, &build_data.highlevel_known_contracts)?; + verify.known_contracts = state.build_data.get_flattened_contracts(false); + self.check_contract_sizes( + &state.execution_result, + &state.build_data.highlevel_known_contracts, + )?; - let signers = script_wallets.into_multi_wallet().into_signers()?; + let state = state.fill_metadata().await?.bundle().await?; - self.handle_broadcastable_transactions( - result, - build_data.libraries, - &decoder, - script_config, - verify, - &signers, - ) - .await + self.handle_broadcastable_transactions(state, verify).await + } } /// Resumes the deployment and/or verification of the script. @@ -102,7 +89,6 @@ impl ScriptArgs { script_config: ScriptConfig, build_data: LinkedBuildData, verify: VerifyBundle, - signers: &HashMap, ) -> Result<()> { if self.multi { return self @@ -115,7 +101,7 @@ impl ScriptArgs { build_data.libraries, &script_config.config, verify, - signers, + &script_config.script_wallets.into_multi_wallet().into_signers()?, ) .await; } @@ -123,7 +109,6 @@ impl ScriptArgs { script_config, build_data, verify, - signers, ) .await .map_err(|err| { @@ -137,7 +122,6 @@ impl ScriptArgs { script_config: ScriptConfig, build_data: LinkedBuildData, mut verify: VerifyBundle, - signers: &HashMap, ) -> Result<()> { trace!(target: "script", "resuming single deployment"); @@ -178,8 +162,10 @@ impl ScriptArgs { receipts::wait_for_pending(provider, &mut deployment_sequence).await?; + let signers = script_config.script_wallets.into_multi_wallet().into_signers()?; + if self.resume { - self.send_transactions(&mut deployment_sequence, fork_url, signers).await?; + self.send_transactions(&mut deployment_sequence, fork_url, &signers).await?; } if self.verify { @@ -190,7 +176,7 @@ impl ScriptArgs { // not included in the solc cache files. let build_data = build_data.build_data.link_with_libraries(libraries)?; - verify.known_contracts = build_data.get_flattened_contracts(true); + verify.known_contracts = build_data.get_flattened_contracts(false); deployment_sequence.verify_contracts(&script_config.config, verify).await?; } diff --git a/crates/forge/bin/cmd/script/executor.rs b/crates/forge/bin/cmd/script/executor.rs index 0335b7a3b650..cdaeb78dc0b7 100644 --- a/crates/forge/bin/cmd/script/executor.rs +++ b/crates/forge/bin/cmd/script/executor.rs @@ -1,38 +1,34 @@ use super::{ - artifacts::ArtifactInfo, build::{CompiledState, LinkedBuildData, LinkedState}, - runner::{ScriptRunner, SimulationStage}, - transaction::{AdditionalContract, TransactionWithMetadata}, - ScriptArgs, ScriptConfig, ScriptResult, + runner::ScriptRunner, + JsonResult, NestedValue, ScriptArgs, ScriptConfig, ScriptResult, }; -use alloy_json_abi::{Function, JsonAbi}; -use alloy_primitives::{Address, Bytes, U256, U64}; +use alloy_dyn_abi::FunctionExt; +use alloy_json_abi::{Function, InternalType, JsonAbi}; +use alloy_primitives::{Address, Bytes, U64}; use alloy_rpc_types::request::TransactionRequest; use async_recursion::async_recursion; -use eyre::{Context, Result}; +use eyre::Result; use forge::{ - backend::{Backend, DatabaseExt}, - executors::ExecutorBuilder, - inspectors::{ - cheatcodes::{BroadcastableTransaction, BroadcastableTransactions}, - CheatsConfig, + decode::{decode_console_logs, RevertDecoder}, + inspectors::cheatcodes::{BroadcastableTransaction, BroadcastableTransactions}, + traces::{ + identifier::{EtherscanIdentifier, LocalTraceIdentifier, SignaturesIdentifier}, + render_trace_arena, CallTraceDecoder, CallTraceDecoderBuilder, TraceKind, }, - revm::Database, - traces::{render_trace_arena, CallTraceDecoder}, }; use foundry_cli::utils::{ensure_clean_constructor, needs_setup}; -use foundry_common::{get_contract_name, provider::ethers::RpcUrl, shell, ContractsByArtifact}; -use foundry_compilers::artifacts::ContractBytecodeSome; -use foundry_evm::inspectors::cheatcodes::ScriptWallets; -use futures::future::join_all; -use parking_lot::RwLock; -use std::{ - collections::{BTreeMap, HashMap, VecDeque}, - sync::Arc, +use foundry_common::{ + fmt::{format_token, format_token_raw}, + shell, ContractsByArtifact, }; +use foundry_compilers::artifacts::ContractBytecodeSome; +use foundry_config::Config; +use foundry_debugger::Debugger; +use std::collections::{HashMap, VecDeque}; +use yansi::Paint; pub struct ExecutionData { - pub script_wallets: ScriptWallets, pub func: Function, pub calldata: Bytes, pub bytecode: Bytes, @@ -49,9 +45,6 @@ pub struct PreExecutionState { impl LinkedState { /// Given linked and compiled artifacts, prepares data we need for execution. pub async fn prepare_execution(self) -> Result { - let multi_wallet = self.args.wallets.get_multi_wallet().await?; - let script_wallets = ScriptWallets::new(multi_wallet, self.args.evm_opts.sender); - let ContractBytecodeSome { abi, bytecode, .. } = self.build_data.get_target_contract()?; let bytecode = bytecode.into_bytes().ok_or_else(|| { @@ -66,7 +59,7 @@ impl LinkedState { args: self.args, script_config: self.script_config, build_data: self.build_data, - execution_data: ExecutionData { script_wallets, func, calldata, bytecode, abi }, + execution_data: ExecutionData { func, calldata, bytecode, abi }, }) } } @@ -83,30 +76,15 @@ impl PreExecutionState { #[async_recursion] pub async fn execute(mut self) -> Result { let mut runner = self - .prepare_runner( - SimulationStage::Local, - Some(self.execution_data.script_wallets.clone()), - ) + .script_config + .get_runner(self.script_config.evm_opts.fork_url.clone(), true) .await?; - - self.script_config.sender_nonce = if self.script_config.evm_opts.fork_url.is_none() { - // dapptools compatibility - 1 - } else { - runner - .executor - .backend - .basic(self.script_config.evm_opts.sender)? - .unwrap_or_default() - .nonce - }; - let mut result = self.execute_with_runner(&mut runner).await?; // If we have a new sender from execution, we need to use it to deploy libraries and relink // contracts. if let Some(new_sender) = self.maybe_new_sender(result.transactions.as_ref())? { - self.script_config.evm_opts.sender = new_sender; + self.script_config.update_sender(new_sender).await?; // Rollback to linking state to relink contracts with the new sender. let state = CompiledState { @@ -218,291 +196,236 @@ impl PreExecutionState { } Ok(new_sender) } +} - /// Creates the Runner that drives script execution - async fn prepare_runner( - &mut self, - stage: SimulationStage, - script_wallets: Option, - ) -> Result { - trace!("preparing script runner"); - let env = self.script_config.evm_opts.evm_env().await?; - - let fork = if self.script_config.evm_opts.fork_url.is_some() { - self.script_config.evm_opts.get_fork(&self.script_config.config, env.clone()) - } else { - None - }; +impl ExecutedState { + pub async fn prepare_simulation(mut self) -> Result { + let known_contracts = self.build_data.get_flattened_contracts(true); - let backend = Backend::spawn(fork).await; + let returns = self.get_returns()?; + let decoder = self.decode_traces(&known_contracts)?; - // Cache forks - if let Some(fork_url) = backend.active_fork_url() { - self.script_config.backends.insert(fork_url.clone(), backend.clone()); + if let Some(txs) = self.execution_result.transactions.as_ref() { + self.script_config.collect_rpcs(txs); } - // We need to enable tracing to decode contract names: local or external. - let mut builder = ExecutorBuilder::new() - .inspectors(|stack| stack.trace(true)) - .spec(self.script_config.config.evm_spec_id()) - .gas_limit(self.script_config.evm_opts.gas_limit()); - - if let SimulationStage::Local = stage { - builder = builder.inspectors(|stack| { - stack.debug(self.args.debug).cheatcodes( - CheatsConfig::new( - &self.script_config.config, - self.script_config.evm_opts.clone(), - script_wallets, - ) - .into(), - ) - }); + if self.execution_result.transactions.as_ref().map_or(true, |txs| txs.is_empty()) && + self.args.broadcast + { + eyre::bail!("No onchain transactions generated in script"); } - Ok(ScriptRunner::new( - builder.build(env, backend), - self.script_config.evm_opts.initial_balance, - self.script_config.evm_opts.sender, - )) + self.script_config.check_multi_chain_constraints(&self.build_data.libraries)?; + self.script_config.check_shanghai_support().await?; + + Ok(PreSimulationState { + args: self.args, + script_config: self.script_config, + build_data: self.build_data, + execution_data: self.execution_data, + execution_result: self.execution_result, + execution_artifacts: ExecutionArtifacts { known_contracts, decoder, returns }, + }) } -} -impl ScriptArgs { - /// Simulates onchain state by executing a list of transactions locally and persisting their - /// state. Returns the transactions and any CREATE2 contract address created. - pub async fn onchain_simulation( - &self, - transactions: BroadcastableTransactions, - script_config: &ScriptConfig, - decoder: &CallTraceDecoder, - contracts: &ContractsByArtifact, - ) -> Result> { - trace!(target: "script", "executing onchain simulation"); - - let runners = Arc::new( - self.build_runners(script_config) - .await? - .into_iter() - .map(|(rpc, runner)| (rpc, Arc::new(RwLock::new(runner)))) - .collect::>(), - ); - - if script_config.evm_opts.verbosity > 3 { - println!("=========================="); - println!("Simulated On-chain Traces:\n"); - } + fn decode_traces(&self, known_contracts: &ContractsByArtifact) -> Result { + let verbosity = self.script_config.evm_opts.verbosity; + let mut etherscan_identifier = EtherscanIdentifier::new( + &self.script_config.config, + self.script_config.evm_opts.get_remote_chain_id(), + )?; - let address_to_abi: BTreeMap = decoder - .contracts - .iter() - .filter_map(|(addr, contract_id)| { - let contract_name = get_contract_name(contract_id); - if let Ok(Some((_, (abi, code)))) = - contracts.find_by_name_or_identifier(contract_name) - { - let info = ArtifactInfo { - contract_name: contract_name.to_string(), - contract_id: contract_id.to_string(), - abi, - code, - }; - return Some((*addr, info)); - } - None - }) - .collect(); - - let mut final_txs = VecDeque::new(); - - // Executes all transactions from the different forks concurrently. - let futs = transactions - .into_iter() - .map(|transaction| async { - let rpc = transaction.rpc.as_ref().expect("missing broadcastable tx rpc url"); - let mut runner = runners.get(rpc).expect("invalid rpc url").write(); - - let mut tx = transaction.transaction; - let result = runner - .simulate( - tx.from - .expect("transaction doesn't have a `from` address at execution time"), - tx.to, - tx.input.clone().into_input(), - tx.value, - ) - .wrap_err("Internal EVM error during simulation")?; - - if !result.success || result.traces.is_empty() { - return Ok((None, result.traces)); - } + let mut local_identifier = LocalTraceIdentifier::new(&known_contracts); + let mut decoder = CallTraceDecoderBuilder::new() + .with_labels(self.execution_result.labeled_addresses.clone()) + .with_verbosity(verbosity) + .with_local_identifier_abis(&local_identifier) + .with_signature_identifier(SignaturesIdentifier::new( + Config::foundry_cache_dir(), + self.script_config.config.offline, + )?) + .build(); + + // Decoding traces using etherscan is costly as we run into rate limits, + // causing scripts to run for a very long time unnecessarily. + // Therefore, we only try and use etherscan if the user has provided an API key. + let should_use_etherscan_traces = self.script_config.config.etherscan_api_key.is_some(); + + for (_, trace) in &self.execution_result.traces { + decoder.identify(trace, &mut local_identifier); + if should_use_etherscan_traces { + decoder.identify(trace, &mut etherscan_identifier); + } + } - let created_contracts = result - .traces - .iter() - .flat_map(|(_, traces)| { - traces.nodes().iter().filter_map(|node| { - if node.trace.kind.is_any_create() { - return Some(AdditionalContract { - opcode: node.trace.kind, - address: node.trace.address, - init_code: node.trace.data.clone(), - }); - } - None - }) - }) - .collect(); - - // Simulate mining the transaction if the user passes `--slow`. - if self.slow { - runner.executor.env.block.number += U256::from(1); - } + Ok(decoder) + } - let is_fixed_gas_limit = tx.gas.is_some(); - match tx.gas { - // If tx.gas is already set that means it was specified in script - Some(gas) => { - println!("Gas limit was set in script to {gas}"); - } - // We inflate the gas used by the user specified percentage - None => { - let gas = U256::from(result.gas_used * self.gas_estimate_multiplier / 100); - tx.gas = Some(gas); - } - } + fn get_returns(&self) -> Result> { + let mut returns = HashMap::new(); + let returned = &self.execution_result.returned; + let func = &self.execution_data.func; + + match func.abi_decode_output(&returned, false) { + Ok(decoded) => { + for (index, (token, output)) in decoded.iter().zip(&func.outputs).enumerate() { + let internal_type = + output.internal_type.clone().unwrap_or(InternalType::Other { + contract: None, + ty: "unknown".to_string(), + }); + + let label = if !output.name.is_empty() { + output.name.to_string() + } else { + index.to_string() + }; - let tx = TransactionWithMetadata::new( - tx, - transaction.rpc, - &result, - &address_to_abi, - decoder, - created_contracts, - is_fixed_gas_limit, - )?; - - eyre::Ok((Some(tx), result.traces)) - }) - .collect::>(); - - let mut abort = false; - for res in join_all(futs).await { - let (tx, traces) = res?; - - // Transaction will be `None`, if execution didn't pass. - if tx.is_none() || script_config.evm_opts.verbosity > 3 { - // Identify all contracts created during the call. - if traces.is_empty() { - eyre::bail!( - "forge script requires tracing enabled to collect created contracts" + returns.insert( + label, + NestedValue { + internal_type: internal_type.to_string(), + value: format_token_raw(token), + }, ); } - - for (_, trace) in &traces { - println!("{}", render_trace_arena(trace, decoder).await?); - } } - - if let Some(tx) = tx { - final_txs.push_back(tx); - } else { - abort = true; + Err(_) => { + shell::println(format!("{returned:?}"))?; } } - if abort { - eyre::bail!("Simulated execution failed.") + Ok(returns) + } +} + +pub struct PreSimulationState { + pub args: ScriptArgs, + pub script_config: ScriptConfig, + pub build_data: LinkedBuildData, + pub execution_data: ExecutionData, + pub execution_result: ScriptResult, + pub execution_artifacts: ExecutionArtifacts, +} + +pub struct ExecutionArtifacts { + pub known_contracts: ContractsByArtifact, + pub decoder: CallTraceDecoder, + pub returns: HashMap, +} + +impl PreSimulationState { + pub fn show_json(&self) -> Result<()> { + let result = &self.execution_result; + + let console_logs = decode_console_logs(&result.logs); + let output = JsonResult { + logs: console_logs, + gas_used: result.gas_used, + returns: self.execution_artifacts.returns.clone(), + }; + let j = serde_json::to_string(&output)?; + shell::println(j)?; + + if !self.execution_result.success { + return Err(eyre::eyre!( + "script failed: {}", + RevertDecoder::new().decode(&self.execution_result.returned[..], None) + )); } - Ok(final_txs) + Ok(()) } - /// Build the multiple runners from different forks. - async fn build_runners( - &self, - script_config: &ScriptConfig, - ) -> Result> { - let sender = script_config.evm_opts.sender; - - if !shell::verbosity().is_silent() { - let n = script_config.total_rpcs.len(); - let s = if n != 1 { "s" } else { "" }; - println!("\n## Setting up {n} EVM{s}."); + pub async fn show_traces(&self) -> Result<()> { + let verbosity = self.script_config.evm_opts.verbosity; + let func = &self.execution_data.func; + let result = &self.execution_result; + let decoder = &self.execution_artifacts.decoder; + + if !result.success || verbosity > 3 { + if result.traces.is_empty() { + warn!(verbosity, "no traces"); + } + + shell::println("Traces:")?; + for (kind, trace) in &result.traces { + let should_include = match kind { + TraceKind::Setup => verbosity >= 5, + TraceKind::Execution => verbosity > 3, + _ => false, + } || !result.success; + + if should_include { + shell::println(render_trace_arena(trace, &decoder).await?)?; + } + } + shell::println(String::new())?; } - let futs = script_config - .total_rpcs - .iter() - .map(|rpc| async { - let mut script_config = script_config.clone(); - script_config.evm_opts.fork_url = Some(rpc.clone()); - let runner = self - .prepare_runner(&mut script_config, sender, SimulationStage::OnChain, None) - .await?; - Ok((rpc.clone(), runner)) - }) - .collect::>(); - - join_all(futs).await.into_iter().collect() - } + if result.success { + shell::println(format!("{}", Paint::green("Script ran successfully.")))?; + } - /// Creates the Runner that drives script execution - async fn prepare_runner( - &self, - script_config: &mut ScriptConfig, - sender: Address, - stage: SimulationStage, - script_wallets: Option, - ) -> Result { - trace!("preparing script runner"); - let env = script_config.evm_opts.evm_env().await?; - - // The db backend that serves all the data. - let db = match &script_config.evm_opts.fork_url { - Some(url) => match script_config.backends.get(url) { - Some(db) => db.clone(), - None => { - let fork = script_config.evm_opts.get_fork(&script_config.config, env.clone()); - let backend = Backend::spawn(fork); - script_config.backends.insert(url.clone(), backend.clone()); - backend + if self.script_config.evm_opts.fork_url.is_none() { + shell::println(format!("Gas used: {}", result.gas_used))?; + } + + if result.success && !result.returned.is_empty() { + shell::println("\n== Return ==")?; + match func.abi_decode_output(&result.returned, false) { + Ok(decoded) => { + for (index, (token, output)) in decoded.iter().zip(&func.outputs).enumerate() { + let internal_type = + output.internal_type.clone().unwrap_or(InternalType::Other { + contract: None, + ty: "unknown".to_string(), + }); + + let label = if !output.name.is_empty() { + output.name.to_string() + } else { + index.to_string() + }; + shell::println(format!( + "{}: {internal_type} {}", + label.trim_end(), + format_token(token) + ))?; + } + } + Err(_) => { + shell::println(format!("{:x?}", (&result.returned)))?; } - }, - None => { - // It's only really `None`, when we don't pass any `--fork-url`. And if so, there is - // no need to cache it, since there won't be any onchain simulation that we'd need - // to cache the backend for. - Backend::spawn(script_config.evm_opts.get_fork(&script_config.config, env.clone())) } - }; + } + + let console_logs = decode_console_logs(&result.logs); + if !console_logs.is_empty() { + shell::println("\n== Logs ==")?; + for log in console_logs { + shell::println(format!(" {log}"))?; + } + } - // We need to enable tracing to decode contract names: local or external. - let mut builder = ExecutorBuilder::new() - .inspectors(|stack| stack.trace(true)) - .spec(script_config.config.evm_spec_id()) - .gas_limit(script_config.evm_opts.gas_limit()); - - if let SimulationStage::Local = stage { - builder = builder.inspectors(|stack| { - stack - .debug(self.debug) - .cheatcodes( - CheatsConfig::new( - &script_config.config, - script_config.evm_opts.clone(), - script_wallets, - ) - .into(), - ) - .enable_isolation(script_config.evm_opts.isolate) - }); + if !result.success { + return Err(eyre::eyre!( + "script failed: {}", + RevertDecoder::new().decode(&result.returned[..], None) + )); } - Ok(ScriptRunner::new( - builder.build(env, db), - script_config.evm_opts.initial_balance, - sender, - )) + Ok(()) + } + + pub fn run_debugger(&self) -> Result<()> { + let mut debugger = Debugger::builder() + .debug_arenas(self.execution_result.debug.as_deref().unwrap_or_default()) + .decoder(&self.execution_artifacts.decoder) + .sources(self.build_data.build_data.sources.clone()) + .breakpoints(self.execution_result.breakpoints.clone()) + .build(); + debugger.try_run()?; + Ok(()) } } diff --git a/crates/forge/bin/cmd/script/mod.rs b/crates/forge/bin/cmd/script/mod.rs index 8594fdacced7..74facb47f65c 100644 --- a/crates/forge/bin/cmd/script/mod.rs +++ b/crates/forge/bin/cmd/script/mod.rs @@ -1,6 +1,6 @@ +use crate::cmd::script::runner::ScriptRunner; use super::build::BuildArgs; -use alloy_dyn_abi::FunctionExt; -use alloy_json_abi::{Function, InternalType, JsonAbi}; +use alloy_json_abi::{Function, JsonAbi}; use alloy_primitives::{Address, Bytes, Log, U256}; use clap::{Parser, ValueHint}; use dialoguer::Confirm; @@ -9,21 +9,18 @@ use eyre::{ContextCompat, Result, WrapErr}; use forge::{ backend::Backend, debug::DebugArena, - decode::decode_console_logs, + executors::ExecutorBuilder, + inspectors::{cheatcodes::ScriptWallets, CheatsConfig}, opts::EvmOpts, - traces::{ - identifier::{EtherscanIdentifier, LocalTraceIdentifier, SignaturesIdentifier}, - render_trace_arena, CallTraceDecoder, CallTraceDecoderBuilder, TraceKind, Traces, - }, + traces::Traces, }; use forge_verify::RetryArgs; use foundry_common::{ abi::{encode_function_args, get_func}, errors::UnlinkedByteCode, evm::{Breakpoints, EvmArgs}, - fmt::{format_token, format_token_raw}, provider::ethers::RpcUrl, - shell, ContractsByArtifact, CONTRACT_MAX_SIZE, SELECTOR_LEN, + shell, CONTRACT_MAX_SIZE, SELECTOR_LEN, }; use foundry_compilers::{ artifacts::{ContractBytecodeSome, Libraries}, @@ -38,8 +35,7 @@ use foundry_config::{ Config, NamedChain, }; use foundry_evm::{ - constants::DEFAULT_CREATE2_DEPLOYER, decode::RevertDecoder, - inspectors::cheatcodes::BroadcastableTransactions, + constants::DEFAULT_CREATE2_DEPLOYER, inspectors::cheatcodes::BroadcastableTransactions, }; use foundry_wallets::MultiWalletOpts; use futures::future; @@ -190,185 +186,6 @@ pub struct ScriptArgs { // === impl ScriptArgs === impl ScriptArgs { - fn decode_traces( - &self, - script_config: &ScriptConfig, - result: &mut ScriptResult, - known_contracts: &ContractsByArtifact, - ) -> Result { - let verbosity = script_config.evm_opts.verbosity; - let mut etherscan_identifier = EtherscanIdentifier::new( - &script_config.config, - script_config.evm_opts.get_remote_chain_id(), - )?; - - let mut local_identifier = LocalTraceIdentifier::new(known_contracts); - let mut decoder = CallTraceDecoderBuilder::new() - .with_labels(result.labeled_addresses.clone()) - .with_verbosity(verbosity) - .with_local_identifier_abis(&local_identifier) - .with_signature_identifier(SignaturesIdentifier::new( - Config::foundry_cache_dir(), - script_config.config.offline, - )?) - .build(); - - // Decoding traces using etherscan is costly as we run into rate limits, - // causing scripts to run for a very long time unnecessarily. - // Therefore, we only try and use etherscan if the user has provided an API key. - let should_use_etherscan_traces = script_config.config.etherscan_api_key.is_some(); - - for (_, trace) in &mut result.traces { - decoder.identify(trace, &mut local_identifier); - if should_use_etherscan_traces { - decoder.identify(trace, &mut etherscan_identifier); - } - } - Ok(decoder) - } - - fn get_returns( - &self, - script_config: &ScriptConfig, - returned: &Bytes, - ) -> Result> { - let func = script_config.called_function.as_ref().expect("There should be a function."); - let mut returns = HashMap::new(); - - match func.abi_decode_output(returned, false) { - Ok(decoded) => { - for (index, (token, output)) in decoded.iter().zip(&func.outputs).enumerate() { - let internal_type = - output.internal_type.clone().unwrap_or(InternalType::Other { - contract: None, - ty: "unknown".to_string(), - }); - - let label = if !output.name.is_empty() { - output.name.to_string() - } else { - index.to_string() - }; - - returns.insert( - label, - NestedValue { - internal_type: internal_type.to_string(), - value: format_token_raw(token), - }, - ); - } - } - Err(_) => { - shell::println(format!("{returned:?}"))?; - } - } - - Ok(returns) - } - - async fn show_traces( - &self, - script_config: &ScriptConfig, - decoder: &CallTraceDecoder, - result: &mut ScriptResult, - ) -> Result<()> { - let verbosity = script_config.evm_opts.verbosity; - let func = script_config.called_function.as_ref().expect("There should be a function."); - - if !result.success || verbosity > 3 { - if result.traces.is_empty() { - warn!(verbosity, "no traces"); - } - - shell::println("Traces:")?; - for (kind, trace) in &result.traces { - let should_include = match kind { - TraceKind::Setup => verbosity >= 5, - TraceKind::Execution => verbosity > 3, - _ => false, - } || !result.success; - - if should_include { - shell::println(render_trace_arena(trace, decoder).await?)?; - } - } - shell::println(String::new())?; - } - - if result.success { - shell::println(format!("{}", Paint::green("Script ran successfully.")))?; - } - - if script_config.evm_opts.fork_url.is_none() { - shell::println(format!("Gas used: {}", result.gas_used))?; - } - - if result.success && !result.returned.is_empty() { - shell::println("\n== Return ==")?; - match func.abi_decode_output(&result.returned, false) { - Ok(decoded) => { - for (index, (token, output)) in decoded.iter().zip(&func.outputs).enumerate() { - let internal_type = - output.internal_type.clone().unwrap_or(InternalType::Other { - contract: None, - ty: "unknown".to_string(), - }); - - let label = if !output.name.is_empty() { - output.name.to_string() - } else { - index.to_string() - }; - shell::println(format!( - "{}: {internal_type} {}", - label.trim_end(), - format_token(token) - ))?; - } - } - Err(_) => { - shell::println(format!("{:x?}", (&result.returned)))?; - } - } - } - - let console_logs = decode_console_logs(&result.logs); - if !console_logs.is_empty() { - shell::println("\n== Logs ==")?; - for log in console_logs { - shell::println(format!(" {log}"))?; - } - } - - if !result.success { - return Err(eyre::eyre!( - "script failed: {}", - RevertDecoder::new().decode(&result.returned[..], None) - )); - } - - Ok(()) - } - - fn show_json(&self, script_config: &ScriptConfig, result: &ScriptResult) -> Result<()> { - let returns = self.get_returns(script_config, &result.returned)?; - - let console_logs = decode_console_logs(&result.logs); - let output = JsonResult { logs: console_logs, gas_used: result.gas_used, returns }; - let j = serde_json::to_string(&output)?; - shell::println(j)?; - - if !result.success { - return Err(eyre::eyre!( - "script failed: {}", - RevertDecoder::new().decode(&result.returned[..], None) - )); - } - - Ok(()) - } - /// Returns the Function and calldata based on the signature /// /// If the `sig` is a valid human-readable function we find the corresponding function in the @@ -551,7 +368,7 @@ pub struct NestedValue { pub value: String, } -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug)] pub struct ScriptConfig { pub config: Config, pub evm_opts: EvmOpts, @@ -568,9 +385,47 @@ pub struct ScriptConfig { pub missing_rpc: bool, /// Should return some debug information pub debug: bool, + /// Container for wallets needed through script execution + pub script_wallets: ScriptWallets, } impl ScriptConfig { + pub async fn new( + config: Config, + evm_opts: EvmOpts, + script_wallets: ScriptWallets, + ) -> Result { + let sender_nonce = if let Some(fork_url) = evm_opts.fork_url.as_ref() { + forge::next_nonce(evm_opts.sender, fork_url, None).await? + } else { + // dapptools compatibility + 1 + }; + Ok(Self { + config, + evm_opts, + sender_nonce, + backends: HashMap::new(), + target_contract: None, + called_function: None, + total_rpcs: HashSet::new(), + missing_rpc: false, + debug: false, + script_wallets, + }) + } + + pub async fn update_sender(&mut self, sender: Address) -> Result<()> { + self.sender_nonce = if let Some(fork_url) = self.evm_opts.fork_url.as_ref() { + forge::next_nonce(sender, fork_url, None).await? + } else { + // dapptools compatibility + 1 + }; + self.evm_opts.sender = sender; + Ok(()) + } + fn collect_rpcs(&mut self, txs: &BroadcastableTransactions) { self.missing_rpc = txs.iter().any(|tx| tx.rpc.is_none()); @@ -637,6 +492,60 @@ For more information, please see https://eips.ethereum.org/EIPS/eip-3855", } Ok(()) } + + async fn get_runner( + &mut self, + fork_url: Option, + cheatcodes: bool, + ) -> Result { + trace!("preparing script runner"); + let env = self.evm_opts.evm_env().await?; + + let db = if let Some(fork_url) = fork_url { + match self.backends.get(&fork_url) { + Some(db) => db.clone(), + None => { + let fork = self.evm_opts.get_fork(&self.config, env.clone()); + let backend = Backend::spawn(fork); + self.backends.insert(fork_url.clone(), backend.clone()); + backend + } + } + } else { + // It's only really `None`, when we don't pass any `--fork-url`. And if so, there is + // no need to cache it, since there won't be any onchain simulation that we'd need + // to cache the backend for. + Backend::spawn(self.evm_opts.get_fork(&self.config, env.clone())) + }; + + // We need to enable tracing to decode contract names: local or external. + let mut builder = ExecutorBuilder::new() + .inspectors(|stack| stack.trace(true)) + .spec(self.config.evm_spec_id()) + .gas_limit(self.evm_opts.gas_limit()); + + if cheatcodes { + builder = builder.inspectors(|stack| { + stack + .debug(self.debug) + .cheatcodes( + CheatsConfig::new( + &self.config, + self.evm_opts.clone(), + Some(self.script_wallets.clone()), + ) + .into(), + ) + .enable_isolation(self.evm_opts.isolate) + }); + } + + Ok(ScriptRunner::new( + builder.build(env, db), + self.evm_opts.initial_balance, + self.evm_opts.sender, + )) + } } #[cfg(test)] diff --git a/crates/forge/bin/cmd/script/runner.rs b/crates/forge/bin/cmd/script/runner.rs index 96937bfdbc97..4caa4e39366b 100644 --- a/crates/forge/bin/cmd/script/runner.rs +++ b/crates/forge/bin/cmd/script/runner.rs @@ -10,12 +10,6 @@ use forge::{ use foundry_config::Config; use yansi::Paint; -/// Represents which simulation stage is the script execution at. -pub enum SimulationStage { - Local, - OnChain, -} - /// Drives script execution #[derive(Debug)] pub struct ScriptRunner { diff --git a/crates/forge/bin/cmd/script/sequence.rs b/crates/forge/bin/cmd/script/sequence.rs index 0e97862bcf10..fc6901768e0b 100644 --- a/crates/forge/bin/cmd/script/sequence.rs +++ b/crates/forge/bin/cmd/script/sequence.rs @@ -79,12 +79,11 @@ impl ScriptSequence { returns: HashMap, sig: &str, target: &ArtifactId, + chain: u64, config: &Config, broadcasted: bool, is_multi: bool, ) -> Result { - let chain = config.chain.unwrap_or_default().id(); - let (path, sensitive_path) = ScriptSequence::get_paths( &config.broadcast, &config.cache_path, From fc3a391ac96dfb26b84559d982245664350955a5 Mon Sep 17 00:00:00 2001 From: Arsenii Kulikov Date: Thu, 29 Feb 2024 22:41:20 +0400 Subject: [PATCH 04/33] wip --- crates/forge/bin/cmd/script/broadcast.rs | 407 +----------------- crates/forge/bin/cmd/script/cmd.rs | 17 +- .../cmd/script/{executor.rs => execute.rs} | 17 +- crates/forge/bin/cmd/script/mod.rs | 35 +- crates/forge/bin/cmd/script/sequence.rs | 1 + crates/forge/bin/cmd/script/simulate.rs | 391 +++++++++++++++++ 6 files changed, 437 insertions(+), 431 deletions(-) rename crates/forge/bin/cmd/script/{executor.rs => execute.rs} (97%) create mode 100644 crates/forge/bin/cmd/script/simulate.rs diff --git a/crates/forge/bin/cmd/script/broadcast.rs b/crates/forge/bin/cmd/script/broadcast.rs index 9395f4cc7d9a..cf052cd232e7 100644 --- a/crates/forge/bin/cmd/script/broadcast.rs +++ b/crates/forge/bin/cmd/script/broadcast.rs @@ -1,428 +1,31 @@ use super::{ - artifacts::ArtifactInfo, - build::LinkedBuildData, - executor::{ExecutionArtifacts, ExecutionData, PreSimulationState}, - multi::MultiChainSequence, - providers::ProvidersManager, - receipts::clear_pendings, - runner::ScriptRunner, - sequence::ScriptSequence, - transaction::TransactionWithMetadata, - verify::VerifyBundle, - ScriptArgs, ScriptConfig, + multi::MultiChainSequence, receipts::clear_pendings, sequence::ScriptSequence, + simulate::BundledState, verify::VerifyBundle, ScriptArgs, ScriptConfig, }; -use crate::cmd::script::transaction::AdditionalContract; use alloy_primitives::{utils::format_units, Address, TxHash, U256}; use ethers_core::types::transaction::eip2718::TypedTransaction; use ethers_providers::{JsonRpcClient, Middleware, Provider}; use ethers_signers::Signer; use eyre::{bail, Context, ContextCompat, Result}; -use forge::{inspectors::cheatcodes::BroadcastableTransactions, traces::render_trace_arena}; use foundry_cli::{ init_progress, update_progress, utils::{has_batch_support, has_different_gas_calc}, }; use foundry_common::{ - get_contract_name, - provider::{ - alloy::RpcUrl, - ethers::{estimate_eip1559_fees, try_get_http_provider, RetryProvider}, - }, + provider::ethers::{estimate_eip1559_fees, try_get_http_provider, RetryProvider}, shell, types::{ToAlloy, ToEthers}, }; use foundry_compilers::artifacts::Libraries; use foundry_config::Config; use foundry_wallets::WalletSigner; -use futures::{future::join_all, StreamExt}; -use parking_lot::RwLock; +use futures::StreamExt; use std::{ cmp::min, - collections::{BTreeMap, HashMap, HashSet, VecDeque}, + collections::{HashMap, HashSet}, sync::Arc, }; -impl PreSimulationState { - pub async fn fill_metadata(self) -> Result { - let transactions = if let Some(txs) = self.execution_result.transactions.as_ref() { - if self.args.skip_simulation { - self.no_simulation(txs.clone())? - } else { - self.onchain_simulation(txs.clone()).await? - } - } else { - VecDeque::new() - }; - - Ok(FilledTransactionsState { - args: self.args, - script_config: self.script_config, - build_data: self.build_data, - execution_data: self.execution_data, - execution_artifacts: self.execution_artifacts, - transactions, - }) - } - - pub async fn onchain_simulation( - &self, - transactions: BroadcastableTransactions, - ) -> Result> { - trace!(target: "script", "executing onchain simulation"); - - let runners = Arc::new( - self.build_runners() - .await? - .into_iter() - .map(|(rpc, runner)| (rpc, Arc::new(RwLock::new(runner)))) - .collect::>(), - ); - - if self.script_config.evm_opts.verbosity > 3 { - println!("=========================="); - println!("Simulated On-chain Traces:\n"); - } - - let contracts = self.build_data.get_flattened_contracts(false); - let address_to_abi: BTreeMap = self - .execution_artifacts - .decoder - .contracts - .iter() - .filter_map(|(addr, contract_id)| { - let contract_name = get_contract_name(contract_id); - if let Ok(Some((_, (abi, code)))) = - contracts.find_by_name_or_identifier(contract_name) - { - let info = ArtifactInfo { - contract_name: contract_name.to_string(), - contract_id: contract_id.to_string(), - abi, - code, - }; - return Some((*addr, info)); - } - None - }) - .collect(); - - let mut final_txs = VecDeque::new(); - - // Executes all transactions from the different forks concurrently. - let futs = transactions - .into_iter() - .map(|transaction| async { - let rpc = transaction.rpc.as_ref().expect("missing broadcastable tx rpc url"); - let mut runner = runners.get(rpc).expect("invalid rpc url").write(); - - let mut tx = transaction.transaction; - let result = runner - .simulate( - tx.from - .expect("transaction doesn't have a `from` address at execution time"), - tx.to, - tx.input.clone().into_input(), - tx.value, - ) - .wrap_err("Internal EVM error during simulation")?; - - if !result.success || result.traces.is_empty() { - return Ok((None, result.traces)); - } - - let created_contracts = result - .traces - .iter() - .flat_map(|(_, traces)| { - traces.nodes().iter().filter_map(|node| { - if node.trace.kind.is_any_create() { - return Some(AdditionalContract { - opcode: node.trace.kind, - address: node.trace.address, - init_code: node.trace.data.clone(), - }); - } - None - }) - }) - .collect(); - - // Simulate mining the transaction if the user passes `--slow`. - if self.args.slow { - runner.executor.env.block.number += U256::from(1); - } - - let is_fixed_gas_limit = tx.gas.is_some(); - match tx.gas { - // If tx.gas is already set that means it was specified in script - Some(gas) => { - println!("Gas limit was set in script to {gas}"); - } - // We inflate the gas used by the user specified percentage - None => { - let gas = - U256::from(result.gas_used * self.args.gas_estimate_multiplier / 100); - tx.gas = Some(gas); - } - } - - let tx = TransactionWithMetadata::new( - tx, - transaction.rpc, - &result, - &address_to_abi, - &self.execution_artifacts.decoder, - created_contracts, - is_fixed_gas_limit, - )?; - - eyre::Ok((Some(tx), result.traces)) - }) - .collect::>(); - - let mut abort = false; - for res in join_all(futs).await { - let (tx, traces) = res?; - - // Transaction will be `None`, if execution didn't pass. - if tx.is_none() || self.script_config.evm_opts.verbosity > 3 { - // Identify all contracts created during the call. - if traces.is_empty() { - eyre::bail!( - "forge script requires tracing enabled to collect created contracts" - ); - } - - for (_, trace) in &traces { - println!( - "{}", - render_trace_arena(trace, &self.execution_artifacts.decoder).await? - ); - } - } - - if let Some(tx) = tx { - final_txs.push_back(tx); - } else { - abort = true; - } - } - - if abort { - eyre::bail!("Simulated execution failed.") - } - - Ok(final_txs) - } - - /// Build the multiple runners from different forks. - async fn build_runners(&self) -> Result> { - if !shell::verbosity().is_silent() { - let n = self.script_config.total_rpcs.len(); - let s = if n != 1 { "s" } else { "" }; - println!("\n## Setting up {n} EVM{s}."); - } - - let futs = self - .script_config - .total_rpcs - .iter() - .map(|rpc| async { - let mut script_config = self.script_config.clone(); - let runner = script_config.get_runner(Some(rpc.clone()), false).await?; - Ok((rpc.clone(), runner)) - }) - .collect::>(); - - join_all(futs).await.into_iter().collect() - } - - fn no_simulation( - &self, - transactions: BroadcastableTransactions, - ) -> Result> { - Ok(transactions - .into_iter() - .map(|tx| TransactionWithMetadata::from_tx_request(tx.transaction)) - .collect()) - } -} - -pub struct FilledTransactionsState { - pub args: ScriptArgs, - pub script_config: ScriptConfig, - pub build_data: LinkedBuildData, - pub execution_data: ExecutionData, - pub execution_artifacts: ExecutionArtifacts, - pub transactions: VecDeque, -} - -impl FilledTransactionsState { - /// Returns all transactions of the [`TransactionWithMetadata`] type in a list of - /// [`ScriptSequence`]. List length will be higher than 1, if we're dealing with a multi - /// chain deployment. - /// - /// Each transaction will be added with the correct transaction type and gas estimation. - pub async fn bundle(self) -> Result { - // User might be using both "in-code" forks and `--fork-url`. - let last_rpc = &self.transactions.back().expect("exists; qed").rpc; - let is_multi_deployment = self.transactions.iter().any(|tx| &tx.rpc != last_rpc); - - let mut total_gas_per_rpc: HashMap = HashMap::new(); - - // Batches sequence of transactions from different rpcs. - let mut new_sequence = VecDeque::new(); - let mut manager = ProvidersManager::default(); - let mut sequences = vec![]; - - // Peeking is used to check if the next rpc url is different. If so, it creates a - // [`ScriptSequence`] from all the collected transactions up to this point. - let mut txes_iter = self.transactions.clone().into_iter().peekable(); - - while let Some(mut tx) = txes_iter.next() { - let tx_rpc = match tx.rpc.clone() { - Some(rpc) => rpc, - None => { - let rpc = self.args.evm_opts.ensure_fork_url()?.clone(); - // Fills the RPC inside the transaction, if missing one. - tx.rpc = Some(rpc.clone()); - rpc - } - }; - - let provider_info = manager.get_or_init_provider(&tx_rpc, self.args.legacy).await?; - - // Handles chain specific requirements. - tx.change_type(provider_info.is_legacy); - tx.transaction.set_chain_id(provider_info.chain); - - if !self.args.skip_simulation { - let typed_tx = tx.typed_tx_mut(); - - if has_different_gas_calc(provider_info.chain) { - trace!("estimating with different gas calculation"); - let gas = *typed_tx.gas().expect("gas is set by simulation."); - - // We are trying to show the user an estimation of the total gas usage. - // - // However, some transactions might depend on previous ones. For - // example, tx1 might deploy a contract that tx2 uses. That - // will result in the following `estimate_gas` call to fail, - // since tx1 hasn't been broadcasted yet. - // - // Not exiting here will not be a problem when actually broadcasting, because - // for chains where `has_different_gas_calc` returns true, - // we await each transaction before broadcasting the next - // one. - if let Err(err) = self.estimate_gas(typed_tx, &provider_info.provider).await { - trace!("gas estimation failed: {err}"); - - // Restore gas value, since `estimate_gas` will remove it. - typed_tx.set_gas(gas); - } - } - - let total_gas = total_gas_per_rpc.entry(tx_rpc.clone()).or_insert(U256::ZERO); - *total_gas += (*typed_tx.gas().expect("gas is set")).to_alloy(); - } - - new_sequence.push_back(tx); - // We only create a [`ScriptSequence`] object when we collect all the rpc related - // transactions. - if let Some(next_tx) = txes_iter.peek() { - if next_tx.rpc == Some(tx_rpc) { - continue; - } - } - - let sequence = ScriptSequence::new( - new_sequence, - self.execution_artifacts.returns.clone(), - &self.args.sig, - &self.build_data.build_data.target, - provider_info.chain.into(), - &self.script_config.config, - self.args.broadcast, - is_multi_deployment, - )?; - - sequences.push(sequence); - - new_sequence = VecDeque::new(); - } - - if !self.args.skip_simulation { - // Present gas information on a per RPC basis. - for (rpc, total_gas) in total_gas_per_rpc { - let provider_info = manager.get(&rpc).expect("provider is set."); - - // We don't store it in the transactions, since we want the most updated value. - // Right before broadcasting. - let per_gas = if let Some(gas_price) = self.args.with_gas_price { - gas_price - } else { - provider_info.gas_price()? - }; - - shell::println("\n==========================")?; - shell::println(format!("\nChain {}", provider_info.chain))?; - - shell::println(format!( - "\nEstimated gas price: {} gwei", - format_units(per_gas, 9) - .unwrap_or_else(|_| "[Could not calculate]".to_string()) - .trim_end_matches('0') - .trim_end_matches('.') - ))?; - shell::println(format!("\nEstimated total gas used for script: {total_gas}"))?; - shell::println(format!( - "\nEstimated amount required: {} ETH", - format_units(total_gas.saturating_mul(per_gas), 18) - .unwrap_or_else(|_| "[Could not calculate]".to_string()) - .trim_end_matches('0') - ))?; - shell::println("\n==========================")?; - } - } - Ok(BundledState { - args: self.args, - script_config: self.script_config, - build_data: self.build_data, - execution_data: self.execution_data, - execution_artifacts: self.execution_artifacts, - sequences, - }) - } - - async fn estimate_gas(&self, tx: &mut TypedTransaction, provider: &Provider) -> Result<()> - where - T: JsonRpcClient, - { - // if already set, some RPC endpoints might simply return the gas value that is already - // set in the request and omit the estimate altogether, so we remove it here - let _ = tx.gas_mut().take(); - - tx.set_gas( - provider - .estimate_gas(tx, None) - .await - .wrap_err_with(|| format!("Failed to estimate gas for tx: {:?}", tx.sighash()))? * - self.args.gas_estimate_multiplier / - 100, - ); - Ok(()) - } -} - -pub struct BundledState { - pub args: ScriptArgs, - pub script_config: ScriptConfig, - pub build_data: LinkedBuildData, - pub execution_data: ExecutionData, - pub execution_artifacts: ExecutionArtifacts, - pub sequences: Vec, -} - impl ScriptArgs { /// Sends the transactions which haven't been broadcasted yet. pub async fn send_transactions( diff --git a/crates/forge/bin/cmd/script/cmd.rs b/crates/forge/bin/cmd/script/cmd.rs index bb947fd8f317..a90c645a62a5 100644 --- a/crates/forge/bin/cmd/script/cmd.rs +++ b/crates/forge/bin/cmd/script/cmd.rs @@ -36,7 +36,7 @@ impl ScriptArgs { script_config.config.libraries = Default::default(); } - let state = PreprocessedState { args: self.clone(), script_config: script_config.clone() }; + let state = PreprocessedState { args: self.clone(), script_config }; let state = state .compile()? .link()? @@ -47,19 +47,16 @@ impl ScriptArgs { .prepare_simulation() .await?; - script_config.target_contract = Some(state.build_data.build_data.target.clone()); - script_config.called_function = Some(state.execution_data.func.clone()); - let mut verify = VerifyBundle::new( - &script_config.config.project()?, - &script_config.config, + &state.script_config.config.project()?, + &state.script_config.config, state.build_data.get_flattened_contracts(false), self.retry, self.verifier.clone(), ); if self.resume || (self.verify && !self.broadcast) { - return self.resume_deployment(script_config, state.build_data, verify).await; + return self.resume_deployment(state.script_config, state.build_data, verify).await; } if self.debug { @@ -96,7 +93,7 @@ impl ScriptArgs { MultiChainSequence::load( &script_config.config, &self.sig, - script_config.target_contract(), + &build_data.build_data.target, )?, build_data.libraries, &script_config.config, @@ -139,7 +136,7 @@ impl ScriptArgs { let mut deployment_sequence = match ScriptSequence::load( &script_config.config, &self.sig, - script_config.target_contract(), + &build_data.build_data.target, chain, broadcasted, ) { @@ -149,7 +146,7 @@ impl ScriptArgs { Err(_) if broadcasted => ScriptSequence::load( &script_config.config, &self.sig, - script_config.target_contract(), + &build_data.build_data.target, chain, false, )?, diff --git a/crates/forge/bin/cmd/script/executor.rs b/crates/forge/bin/cmd/script/execute.rs similarity index 97% rename from crates/forge/bin/cmd/script/executor.rs rename to crates/forge/bin/cmd/script/execute.rs index cdaeb78dc0b7..1ec952be4bea 100644 --- a/crates/forge/bin/cmd/script/executor.rs +++ b/crates/forge/bin/cmd/script/execute.rs @@ -200,10 +200,10 @@ impl PreExecutionState { impl ExecutedState { pub async fn prepare_simulation(mut self) -> Result { - let known_contracts = self.build_data.get_flattened_contracts(true); - let returns = self.get_returns()?; - let decoder = self.decode_traces(&known_contracts)?; + + let known_contracts = self.build_data.get_flattened_contracts(true); + let decoder = self.build_trace_decoder(&known_contracts)?; if let Some(txs) = self.execution_result.transactions.as_ref() { self.script_config.collect_rpcs(txs); @@ -228,14 +228,17 @@ impl ExecutedState { }) } - fn decode_traces(&self, known_contracts: &ContractsByArtifact) -> Result { + fn build_trace_decoder( + &self, + known_contracts: &ContractsByArtifact, + ) -> Result { let verbosity = self.script_config.evm_opts.verbosity; let mut etherscan_identifier = EtherscanIdentifier::new( &self.script_config.config, self.script_config.evm_opts.get_remote_chain_id(), )?; - let mut local_identifier = LocalTraceIdentifier::new(&known_contracts); + let mut local_identifier = LocalTraceIdentifier::new(known_contracts); let mut decoder = CallTraceDecoderBuilder::new() .with_labels(self.execution_result.labeled_addresses.clone()) .with_verbosity(verbosity) @@ -266,7 +269,7 @@ impl ExecutedState { let returned = &self.execution_result.returned; let func = &self.execution_data.func; - match func.abi_decode_output(&returned, false) { + match func.abi_decode_output(returned, false) { Ok(decoded) => { for (index, (token, output)) in decoded.iter().zip(&func.outputs).enumerate() { let internal_type = @@ -357,7 +360,7 @@ impl PreSimulationState { } || !result.success; if should_include { - shell::println(render_trace_arena(trace, &decoder).await?)?; + shell::println(render_trace_arena(trace, decoder).await?)?; } } shell::println(String::new())?; diff --git a/crates/forge/bin/cmd/script/mod.rs b/crates/forge/bin/cmd/script/mod.rs index 74facb47f65c..2103a7c88ad5 100644 --- a/crates/forge/bin/cmd/script/mod.rs +++ b/crates/forge/bin/cmd/script/mod.rs @@ -1,5 +1,6 @@ use crate::cmd::script::runner::ScriptRunner; use super::build::BuildArgs; +use self::transaction::AdditionalContract; use alloy_json_abi::{Function, JsonAbi}; use alloy_primitives::{Address, Bytes, Log, U256}; use clap::{Parser, ValueHint}; @@ -48,12 +49,13 @@ mod artifacts; mod broadcast; mod build; mod cmd; -mod executor; +mod execute; mod multi; mod providers; mod receipts; mod runner; mod sequence; +mod simulate; pub mod transaction; mod verify; @@ -355,6 +357,26 @@ pub struct ScriptResult { pub breakpoints: Breakpoints, } +impl ScriptResult { + pub fn get_created_contracts(&self) -> Vec { + self.traces + .iter() + .flat_map(|(_, traces)| { + traces.nodes().iter().filter_map(|node| { + if node.trace.kind.is_any_create() { + return Some(AdditionalContract { + opcode: node.trace.kind, + address: node.trace.address, + init_code: node.trace.data.clone(), + }); + } + None + }) + }) + .collect() + } +} + #[derive(Serialize, Deserialize)] struct JsonResult { logs: Vec, @@ -375,10 +397,6 @@ pub struct ScriptConfig { pub sender_nonce: u64, /// Maps a rpc url to a backend pub backends: HashMap, - /// Script target contract - pub target_contract: Option, - /// Function called by the script - pub called_function: Option, /// Unique list of rpc urls present pub total_rpcs: HashSet, /// If true, one of the transactions did not have a rpc @@ -406,8 +424,6 @@ impl ScriptConfig { evm_opts, sender_nonce, backends: HashMap::new(), - target_contract: None, - called_function: None, total_rpcs: HashSet::new(), missing_rpc: false, debug: false, @@ -460,11 +476,6 @@ impl ScriptConfig { Ok(()) } - /// Returns the script target contract - fn target_contract(&self) -> &ArtifactId { - self.target_contract.as_ref().expect("should exist after building") - } - /// Checks if the RPCs used point to chains that support EIP-3855. /// If not, warns the user. async fn check_shanghai_support(&self) -> Result<()> { diff --git a/crates/forge/bin/cmd/script/sequence.rs b/crates/forge/bin/cmd/script/sequence.rs index fc6901768e0b..e35de396ce64 100644 --- a/crates/forge/bin/cmd/script/sequence.rs +++ b/crates/forge/bin/cmd/script/sequence.rs @@ -74,6 +74,7 @@ impl From<&mut ScriptSequence> for SensitiveScriptSequence { } impl ScriptSequence { + #[allow(clippy::too_many_arguments)] pub fn new( transactions: VecDeque, returns: HashMap, diff --git a/crates/forge/bin/cmd/script/simulate.rs b/crates/forge/bin/cmd/script/simulate.rs new file mode 100644 index 000000000000..ad63c744f4c6 --- /dev/null +++ b/crates/forge/bin/cmd/script/simulate.rs @@ -0,0 +1,391 @@ +use super::{ + artifacts::ArtifactInfo, + build::LinkedBuildData, + execute::{ExecutionArtifacts, ExecutionData, PreSimulationState}, + providers::ProvidersManager, + runner::ScriptRunner, + sequence::ScriptSequence, + transaction::TransactionWithMetadata, + ScriptArgs, ScriptConfig, +}; +use alloy_primitives::{utils::format_units, Address, U256}; +use ethers_core::types::transaction::eip2718::TypedTransaction; +use ethers_providers::{JsonRpcClient, Middleware, Provider}; +use eyre::{Context, Result}; +use forge::{inspectors::cheatcodes::BroadcastableTransactions, traces::render_trace_arena}; +use foundry_cli::utils::has_different_gas_calc; +use foundry_common::{ + get_contract_name, provider::alloy::RpcUrl, shell, types::ToAlloy, ContractsByArtifact, +}; +use futures::future::join_all; +use parking_lot::RwLock; +use std::{ + collections::{BTreeMap, HashMap, VecDeque}, + sync::Arc, +}; + +impl PreSimulationState { + pub async fn fill_metadata(self) -> Result { + let transactions = if let Some(txs) = self.execution_result.transactions.as_ref() { + if self.args.skip_simulation { + self.no_simulation(txs.clone())? + } else { + self.onchain_simulation(txs.clone()).await? + } + } else { + VecDeque::new() + }; + + Ok(FilledTransactionsState { + args: self.args, + script_config: self.script_config, + build_data: self.build_data, + execution_data: self.execution_data, + execution_artifacts: self.execution_artifacts, + transactions, + }) + } + + pub async fn onchain_simulation( + &self, + transactions: BroadcastableTransactions, + ) -> Result> { + trace!(target: "script", "executing onchain simulation"); + + let runners = Arc::new( + self.build_runners() + .await? + .into_iter() + .map(|(rpc, runner)| (rpc, Arc::new(RwLock::new(runner)))) + .collect::>(), + ); + + let contracts = self.build_data.get_flattened_contracts(false); + let address_to_abi: BTreeMap = + self.build_address_to_abi_map(&contracts); + + let mut final_txs = VecDeque::new(); + + // Executes all transactions from the different forks concurrently. + let futs = transactions + .into_iter() + .map(|transaction| async { + let rpc = transaction.rpc.as_ref().expect("missing broadcastable tx rpc url"); + let mut runner = runners.get(rpc).expect("invalid rpc url").write(); + + let mut tx = transaction.transaction; + let result = runner + .simulate( + tx.from + .expect("transaction doesn't have a `from` address at execution time"), + tx.to, + tx.input.clone().into_input(), + tx.value, + ) + .wrap_err("Internal EVM error during simulation")?; + + if !result.success { + return Ok((None, result.traces)); + } + + let created_contracts = result.get_created_contracts(); + + // Simulate mining the transaction if the user passes `--slow`. + if self.args.slow { + runner.executor.env.block.number += U256::from(1); + } + + let is_fixed_gas_limit = tx.gas.is_some(); + match tx.gas { + // If tx.gas is already set that means it was specified in script + Some(gas) => { + println!("Gas limit was set in script to {gas}"); + } + // We inflate the gas used by the user specified percentage + None => { + let gas = + U256::from(result.gas_used * self.args.gas_estimate_multiplier / 100); + tx.gas = Some(gas); + } + } + + let tx = TransactionWithMetadata::new( + tx, + transaction.rpc, + &result, + &address_to_abi, + &self.execution_artifacts.decoder, + created_contracts, + is_fixed_gas_limit, + )?; + + eyre::Ok((Some(tx), result.traces)) + }) + .collect::>(); + + if self.script_config.evm_opts.verbosity > 3 { + println!("=========================="); + println!("Simulated On-chain Traces:\n"); + } + + let mut abort = false; + for res in join_all(futs).await { + let (tx, traces) = res?; + + // Transaction will be `None`, if execution didn't pass. + if tx.is_none() || self.script_config.evm_opts.verbosity > 3 { + for (_, trace) in &traces { + println!( + "{}", + render_trace_arena(trace, &self.execution_artifacts.decoder).await? + ); + } + } + + if let Some(tx) = tx { + final_txs.push_back(tx); + } else { + abort = true; + } + } + + if abort { + eyre::bail!("Simulated execution failed.") + } + + Ok(final_txs) + } + + fn build_address_to_abi_map<'a>( + &self, + contracts: &'a ContractsByArtifact, + ) -> BTreeMap> { + self.execution_artifacts + .decoder + .contracts + .iter() + .filter_map(move |(addr, contract_id)| { + let contract_name = get_contract_name(contract_id); + if let Ok(Some((_, (abi, code)))) = + contracts.find_by_name_or_identifier(contract_name) + { + let info = ArtifactInfo { + contract_name: contract_name.to_string(), + contract_id: contract_id.to_string(), + abi, + code, + }; + return Some((*addr, info)); + } + None + }) + .collect() + } + + /// Build the multiple runners from different forks. + async fn build_runners(&self) -> Result> { + if !shell::verbosity().is_silent() { + let n = self.script_config.total_rpcs.len(); + let s = if n != 1 { "s" } else { "" }; + println!("\n## Setting up {n} EVM{s}."); + } + + let futs = self + .script_config + .total_rpcs + .iter() + .map(|rpc| async { + let mut script_config = self.script_config.clone(); + let runner = script_config.get_runner(Some(rpc.clone()), false).await?; + Ok((rpc.clone(), runner)) + }) + .collect::>(); + + join_all(futs).await.into_iter().collect() + } + + fn no_simulation( + &self, + transactions: BroadcastableTransactions, + ) -> Result> { + Ok(transactions + .into_iter() + .map(|tx| TransactionWithMetadata::from_tx_request(tx.transaction)) + .collect()) + } +} + +pub struct FilledTransactionsState { + pub args: ScriptArgs, + pub script_config: ScriptConfig, + pub build_data: LinkedBuildData, + pub execution_data: ExecutionData, + pub execution_artifacts: ExecutionArtifacts, + pub transactions: VecDeque, +} + +impl FilledTransactionsState { + /// Returns all transactions of the [`TransactionWithMetadata`] type in a list of + /// [`ScriptSequence`]. List length will be higher than 1, if we're dealing with a multi + /// chain deployment. + /// + /// Each transaction will be added with the correct transaction type and gas estimation. + pub async fn bundle(self) -> Result { + // User might be using both "in-code" forks and `--fork-url`. + let last_rpc = &self.transactions.back().expect("exists; qed").rpc; + let is_multi_deployment = self.transactions.iter().any(|tx| &tx.rpc != last_rpc); + + let mut total_gas_per_rpc: HashMap = HashMap::new(); + + // Batches sequence of transactions from different rpcs. + let mut new_sequence = VecDeque::new(); + let mut manager = ProvidersManager::default(); + let mut sequences = vec![]; + + // Peeking is used to check if the next rpc url is different. If so, it creates a + // [`ScriptSequence`] from all the collected transactions up to this point. + let mut txes_iter = self.transactions.clone().into_iter().peekable(); + + while let Some(mut tx) = txes_iter.next() { + let tx_rpc = match tx.rpc.clone() { + Some(rpc) => rpc, + None => { + let rpc = self.args.evm_opts.ensure_fork_url()?.clone(); + // Fills the RPC inside the transaction, if missing one. + tx.rpc = Some(rpc.clone()); + rpc + } + }; + + let provider_info = manager.get_or_init_provider(&tx_rpc, self.args.legacy).await?; + + // Handles chain specific requirements. + tx.change_type(provider_info.is_legacy); + tx.transaction.set_chain_id(provider_info.chain); + + if !self.args.skip_simulation { + let typed_tx = tx.typed_tx_mut(); + + if has_different_gas_calc(provider_info.chain) { + trace!("estimating with different gas calculation"); + let gas = *typed_tx.gas().expect("gas is set by simulation."); + + // We are trying to show the user an estimation of the total gas usage. + // + // However, some transactions might depend on previous ones. For + // example, tx1 might deploy a contract that tx2 uses. That + // will result in the following `estimate_gas` call to fail, + // since tx1 hasn't been broadcasted yet. + // + // Not exiting here will not be a problem when actually broadcasting, because + // for chains where `has_different_gas_calc` returns true, + // we await each transaction before broadcasting the next + // one. + if let Err(err) = self.estimate_gas(typed_tx, &provider_info.provider).await { + trace!("gas estimation failed: {err}"); + + // Restore gas value, since `estimate_gas` will remove it. + typed_tx.set_gas(gas); + } + } + + let total_gas = total_gas_per_rpc.entry(tx_rpc.clone()).or_insert(U256::ZERO); + *total_gas += (*typed_tx.gas().expect("gas is set")).to_alloy(); + } + + new_sequence.push_back(tx); + // We only create a [`ScriptSequence`] object when we collect all the rpc related + // transactions. + if let Some(next_tx) = txes_iter.peek() { + if next_tx.rpc == Some(tx_rpc) { + continue; + } + } + + let sequence = ScriptSequence::new( + new_sequence, + self.execution_artifacts.returns.clone(), + &self.args.sig, + &self.build_data.build_data.target, + provider_info.chain, + &self.script_config.config, + self.args.broadcast, + is_multi_deployment, + )?; + + sequences.push(sequence); + + new_sequence = VecDeque::new(); + } + + if !self.args.skip_simulation { + // Present gas information on a per RPC basis. + for (rpc, total_gas) in total_gas_per_rpc { + let provider_info = manager.get(&rpc).expect("provider is set."); + + // We don't store it in the transactions, since we want the most updated value. + // Right before broadcasting. + let per_gas = if let Some(gas_price) = self.args.with_gas_price { + gas_price + } else { + provider_info.gas_price()? + }; + + shell::println("\n==========================")?; + shell::println(format!("\nChain {}", provider_info.chain))?; + + shell::println(format!( + "\nEstimated gas price: {} gwei", + format_units(per_gas, 9) + .unwrap_or_else(|_| "[Could not calculate]".to_string()) + .trim_end_matches('0') + .trim_end_matches('.') + ))?; + shell::println(format!("\nEstimated total gas used for script: {total_gas}"))?; + shell::println(format!( + "\nEstimated amount required: {} ETH", + format_units(total_gas.saturating_mul(per_gas), 18) + .unwrap_or_else(|_| "[Could not calculate]".to_string()) + .trim_end_matches('0') + ))?; + shell::println("\n==========================")?; + } + } + Ok(BundledState { + args: self.args, + script_config: self.script_config, + build_data: self.build_data, + execution_data: self.execution_data, + execution_artifacts: self.execution_artifacts, + sequences, + }) + } + + async fn estimate_gas(&self, tx: &mut TypedTransaction, provider: &Provider) -> Result<()> + where + T: JsonRpcClient, + { + // if already set, some RPC endpoints might simply return the gas value that is already + // set in the request and omit the estimate altogether, so we remove it here + let _ = tx.gas_mut().take(); + + tx.set_gas( + provider + .estimate_gas(tx, None) + .await + .wrap_err_with(|| format!("Failed to estimate gas for tx: {:?}", tx.sighash()))? * + self.args.gas_estimate_multiplier / + 100, + ); + Ok(()) + } +} + +pub struct BundledState { + pub args: ScriptArgs, + pub script_config: ScriptConfig, + pub build_data: LinkedBuildData, + pub execution_data: ExecutionData, + pub execution_artifacts: ExecutionArtifacts, + pub sequences: Vec, +} From 602474371447d7964e5ad39dca90776f66cb1075 Mon Sep 17 00:00:00 2001 From: Arsenii Kulikov Date: Fri, 1 Mar 2024 00:34:41 +0400 Subject: [PATCH 05/33] wip --- crates/forge/bin/cmd/script/cmd.rs | 15 +++++++++++++-- crates/forge/bin/cmd/script/execute.rs | 5 +---- crates/forge/bin/cmd/script/mod.rs | 12 ++++-------- crates/forge/bin/cmd/script/simulate.rs | 15 +++++++++++---- 4 files changed, 29 insertions(+), 18 deletions(-) diff --git a/crates/forge/bin/cmd/script/cmd.rs b/crates/forge/bin/cmd/script/cmd.rs index a90c645a62a5..10841a12b213 100644 --- a/crates/forge/bin/cmd/script/cmd.rs +++ b/crates/forge/bin/cmd/script/cmd.rs @@ -12,7 +12,7 @@ use ethers_signers::Signer; use eyre::Result; use forge::inspectors::cheatcodes::ScriptWallets; use foundry_cli::utils::LoadConfig; -use foundry_common::{provider::ethers::try_get_http_provider, types::ToAlloy}; +use foundry_common::{provider::ethers::try_get_http_provider, shell, types::ToAlloy}; use foundry_compilers::artifacts::Libraries; use std::sync::Arc; @@ -74,7 +74,18 @@ impl ScriptArgs { &state.build_data.highlevel_known_contracts, )?; - let state = state.fill_metadata().await?.bundle().await?; + if state.script_config.missing_rpc { + shell::println("\nIf you wish to simulate on-chain transactions pass a RPC URL.")?; + return Ok(()); + } + + let state = state.fill_metadata().await?; + + if state.transactions.is_empty() { + return Ok(()); + } + + let state = state.bundle().await?; self.handle_broadcastable_transactions(state, verify).await } diff --git a/crates/forge/bin/cmd/script/execute.rs b/crates/forge/bin/cmd/script/execute.rs index 1ec952be4bea..98f4ccb330fe 100644 --- a/crates/forge/bin/cmd/script/execute.rs +++ b/crates/forge/bin/cmd/script/execute.rs @@ -75,10 +75,7 @@ pub struct ExecutedState { impl PreExecutionState { #[async_recursion] pub async fn execute(mut self) -> Result { - let mut runner = self - .script_config - .get_runner(self.script_config.evm_opts.fork_url.clone(), true) - .await?; + let mut runner = self.script_config.get_runner(true).await?; let mut result = self.execute_with_runner(&mut runner).await?; // If we have a new sender from execution, we need to use it to deploy libraries and relink diff --git a/crates/forge/bin/cmd/script/mod.rs b/crates/forge/bin/cmd/script/mod.rs index 2103a7c88ad5..6f86319fd298 100644 --- a/crates/forge/bin/cmd/script/mod.rs +++ b/crates/forge/bin/cmd/script/mod.rs @@ -504,16 +504,12 @@ For more information, please see https://eips.ethereum.org/EIPS/eip-3855", Ok(()) } - async fn get_runner( - &mut self, - fork_url: Option, - cheatcodes: bool, - ) -> Result { + async fn get_runner(&mut self, cheatcodes: bool) -> Result { trace!("preparing script runner"); let env = self.evm_opts.evm_env().await?; - let db = if let Some(fork_url) = fork_url { - match self.backends.get(&fork_url) { + let db = if let Some(fork_url) = self.evm_opts.fork_url.as_ref() { + match self.backends.get(fork_url) { Some(db) => db.clone(), None => { let fork = self.evm_opts.get_fork(&self.config, env.clone()); @@ -526,7 +522,7 @@ For more information, please see https://eips.ethereum.org/EIPS/eip-3855", // It's only really `None`, when we don't pass any `--fork-url`. And if so, there is // no need to cache it, since there won't be any onchain simulation that we'd need // to cache the backend for. - Backend::spawn(self.evm_opts.get_fork(&self.config, env.clone())) + Backend::spawn(None) }; // We need to enable tracing to decode contract names: local or external. diff --git a/crates/forge/bin/cmd/script/simulate.rs b/crates/forge/bin/cmd/script/simulate.rs index ad63c744f4c6..0128b079d189 100644 --- a/crates/forge/bin/cmd/script/simulate.rs +++ b/crates/forge/bin/cmd/script/simulate.rs @@ -25,9 +25,12 @@ use std::{ }; impl PreSimulationState { + /// If simulation is enabled, simulates transactions against fork and fills gas estimation and metadata. + /// Otherwise, metadata (e.g. additional contracts, created contract names) is left empty. pub async fn fill_metadata(self) -> Result { let transactions = if let Some(txs) = self.execution_result.transactions.as_ref() { if self.args.skip_simulation { + shell::println("\nSKIPPING ON CHAIN SIMULATION.")?; self.no_simulation(txs.clone())? } else { self.onchain_simulation(txs.clone()).await? @@ -108,7 +111,6 @@ impl PreSimulationState { tx.gas = Some(gas); } } - let tx = TransactionWithMetadata::new( tx, transaction.rpc, @@ -196,7 +198,8 @@ impl PreSimulationState { .iter() .map(|rpc| async { let mut script_config = self.script_config.clone(); - let runner = script_config.get_runner(Some(rpc.clone()), false).await?; + script_config.evm_opts.fork_url = Some(rpc.clone()); + let runner = script_config.get_runner(false).await?; Ok((rpc.clone(), runner)) }) .collect::>(); @@ -210,7 +213,11 @@ impl PreSimulationState { ) -> Result> { Ok(transactions .into_iter() - .map(|tx| TransactionWithMetadata::from_tx_request(tx.transaction)) + .map(|btx| { + let mut tx = TransactionWithMetadata::from_tx_request(btx.transaction); + tx.rpc = btx.rpc; + tx + }) .collect()) } } @@ -225,7 +232,7 @@ pub struct FilledTransactionsState { } impl FilledTransactionsState { - /// Returns all transactions of the [`TransactionWithMetadata`] type in a list of + /// Bundles all transactions of the [`TransactionWithMetadata`] type in a list of /// [`ScriptSequence`]. List length will be higher than 1, if we're dealing with a multi /// chain deployment. /// From a584d480555b2bf7b9f2204d42d108a154db20d1 Mon Sep 17 00:00:00 2001 From: Arsenii Kulikov Date: Fri, 1 Mar 2024 01:29:34 +0400 Subject: [PATCH 06/33] address #7244 --- crates/forge/bin/cmd/script/build.rs | 12 +++++++++--- crates/forge/bin/cmd/script/simulate.rs | 5 +++-- crates/forge/tests/cli/script.rs | 22 ++++++++++++++++++++++ 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/crates/forge/bin/cmd/script/build.rs b/crates/forge/bin/cmd/script/build.rs index 0eac94c0ef4e..2f6d589fed31 100644 --- a/crates/forge/bin/cmd/script/build.rs +++ b/crates/forge/bin/cmd/script/build.rs @@ -7,7 +7,7 @@ use foundry_common::{ ContractsByArtifact, }; use foundry_compilers::{ - artifacts::{ContractBytecode, ContractBytecodeSome, Libraries}, + artifacts::{BytecodeObject, ContractBytecode, ContractBytecodeSome, Libraries}, cache::SolFilesCache, contracts::ArtifactContracts, info::ContractInfo, @@ -81,8 +81,14 @@ impl PreprocessedState { if id.name != *name { continue; } - } else if !contract.bytecode.as_ref().map_or(false, |b| b.object.bytes_len() > 0) { - // Ignore contracts with empty/missing bytecode, e.g. interfaces. + } else if contract.abi.as_ref().map_or(true, |abi| abi.is_empty()) || + contract.bytecode.as_ref().map_or(true, |b| match &b.object { + BytecodeObject::Bytecode(b) => b.is_empty(), + BytecodeObject::Unlinked(_) => false, + }) + { + // Ignore contracts with empty abi or linked bytecode of length 0 which are + // interfaces/abstract contracts/libraries. continue; } diff --git a/crates/forge/bin/cmd/script/simulate.rs b/crates/forge/bin/cmd/script/simulate.rs index 0128b079d189..42da72e5b33a 100644 --- a/crates/forge/bin/cmd/script/simulate.rs +++ b/crates/forge/bin/cmd/script/simulate.rs @@ -25,8 +25,9 @@ use std::{ }; impl PreSimulationState { - /// If simulation is enabled, simulates transactions against fork and fills gas estimation and metadata. - /// Otherwise, metadata (e.g. additional contracts, created contract names) is left empty. + /// If simulation is enabled, simulates transactions against fork and fills gas estimation and + /// metadata. Otherwise, metadata (e.g. additional contracts, created contract names) is + /// left empty. pub async fn fill_metadata(self) -> Result { let transactions = if let Some(txs) = self.execution_result.transactions.as_ref() { if self.args.skip_simulation { diff --git a/crates/forge/tests/cli/script.rs b/crates/forge/tests/cli/script.rs index b45ce52cf445..e212683c1a2b 100644 --- a/crates/forge/tests/cli/script.rs +++ b/crates/forge/tests/cli/script.rs @@ -1080,6 +1080,28 @@ interface Interface {} assert!(cmd.stdout_lossy().contains("Script ran successfully.")); }); +forgetest_async!(assert_can_detect_unlinked_target_with_libraries, |prj, cmd| { + let script = prj + .add_script( + "ScriptWithExtLib.s.sol", + r#" +library Lib { + function f() public {} +} + +contract Script { + function run() external { + Lib.f(); + } +} + "#, + ) + .unwrap(); + + cmd.arg("script").arg(script); + assert!(cmd.stdout_lossy().contains("Script ran successfully.")); +}); + forgetest_async!(assert_can_resume_with_additional_contracts, |prj, cmd| { let (_api, handle) = spawn(NodeConfig::test()).await; let mut tester = ScriptTester::new_broadcast(cmd, &handle.http_endpoint(), prj.root()); From 3723eb72120c7d7adacdc6179ed0740346f60e5f Mon Sep 17 00:00:00 2001 From: Arsenii Kulikov Date: Fri, 1 Mar 2024 03:19:30 +0400 Subject: [PATCH 07/33] wip: enum for multi/single sequences --- crates/forge/bin/cmd/script/broadcast.rs | 53 +++++++++----------- crates/forge/bin/cmd/script/cmd.rs | 2 +- crates/forge/bin/cmd/script/multi.rs | 11 ++--- crates/forge/bin/cmd/script/sequence.rs | 40 +++++++++------ crates/forge/bin/cmd/script/simulate.rs | 57 ++++++++++++---------- crates/forge/bin/cmd/script/transaction.rs | 4 +- crates/forge/tests/cli/multi_script.rs | 2 +- 7 files changed, 87 insertions(+), 82 deletions(-) diff --git a/crates/forge/bin/cmd/script/broadcast.rs b/crates/forge/bin/cmd/script/broadcast.rs index cf052cd232e7..675101362aa1 100644 --- a/crates/forge/bin/cmd/script/broadcast.rs +++ b/crates/forge/bin/cmd/script/broadcast.rs @@ -1,5 +1,4 @@ -use super::{ - multi::MultiChainSequence, receipts::clear_pendings, sequence::ScriptSequence, +use super::{receipts::clear_pendings, sequence::{ScriptSequence, ScriptSequenceKind}, simulate::BundledState, verify::VerifyBundle, ScriptArgs, ScriptConfig, }; use alloy_primitives::{utils::format_units, Address, TxHash, U256}; @@ -294,35 +293,29 @@ impl ScriptArgs { mut state: BundledState, verify: VerifyBundle, ) -> Result<()> { - if state.script_config.has_multiple_rpcs() { - trace!(target: "script", "broadcasting multi chain deployment"); - - let multi = MultiChainSequence::new( - state.sequences.clone(), - &self.sig, - &state.build_data.build_data.target, - &state.script_config.config, - self.broadcast, - )?; - - if self.broadcast { - self.multi_chain_deployment( - multi, - state.build_data.libraries, - &state.script_config.config, - verify, - &state.script_config.script_wallets.into_multi_wallet().into_signers()?, - ) - .await?; + if self.broadcast { + match &mut state.sequence { + ScriptSequenceKind::Multi(sequence) => { + trace!(target: "script", "broadcasting multi chain deployment"); + self.multi_chain_deployment( + sequence, + state.build_data.libraries, + &state.script_config.config, + verify, + &state.script_config.script_wallets.into_multi_wallet().into_signers()?, + ) + .await?; + } + ScriptSequenceKind::Single(sequence) => { + self.single_deployment( + sequence, + state.script_config, + state.build_data.libraries, + verify, + ) + .await?; + } } - } else if self.broadcast { - self.single_deployment( - state.sequences.first_mut().expect("missing deployment"), - state.script_config, - state.build_data.libraries, - verify, - ) - .await?; } if !self.broadcast { diff --git a/crates/forge/bin/cmd/script/cmd.rs b/crates/forge/bin/cmd/script/cmd.rs index 10841a12b213..2676bac5e56e 100644 --- a/crates/forge/bin/cmd/script/cmd.rs +++ b/crates/forge/bin/cmd/script/cmd.rs @@ -101,7 +101,7 @@ impl ScriptArgs { if self.multi { return self .multi_chain_deployment( - MultiChainSequence::load( + &mut MultiChainSequence::load( &script_config.config, &self.sig, &build_data.build_data.target, diff --git a/crates/forge/bin/cmd/script/multi.rs b/crates/forge/bin/cmd/script/multi.rs index 874dd24ba636..59bbbda1fad4 100644 --- a/crates/forge/bin/cmd/script/multi.rs +++ b/crates/forge/bin/cmd/script/multi.rs @@ -43,13 +43,6 @@ fn to_sensitive(sequence: &mut MultiChainSequence) -> SensitiveMultiChainSequenc } } -impl Drop for MultiChainSequence { - fn drop(&mut self) { - self.deployments.iter_mut().for_each(|sequence| sequence.sort_receipts()); - self.save().expect("could not save multi deployment sequence"); - } -} - impl MultiChainSequence { pub fn new( deployments: Vec, @@ -138,6 +131,8 @@ impl MultiChainSequence { /// Saves the transactions as file if it's a standalone deployment. pub fn save(&mut self) -> Result<()> { + self.deployments.iter_mut().for_each(|sequence| sequence.sort_receipts()); + self.timestamp = now().as_secs(); let sensitive_sequence: SensitiveMultiChainSequence = to_sensitive(self); @@ -178,7 +173,7 @@ impl ScriptArgs { /// all in parallel. Supports `--resume` and `--verify`. pub async fn multi_chain_deployment( &self, - mut deployments: MultiChainSequence, + deployments: &mut MultiChainSequence, libraries: Libraries, config: &Config, verify: VerifyBundle, diff --git a/crates/forge/bin/cmd/script/sequence.rs b/crates/forge/bin/cmd/script/sequence.rs index e35de396ce64..1bece7a12a86 100644 --- a/crates/forge/bin/cmd/script/sequence.rs +++ b/crates/forge/bin/cmd/script/sequence.rs @@ -1,4 +1,4 @@ -use super::NestedValue; +use super::{multi::MultiChainSequence, NestedValue}; use crate::cmd::{ init::get_commit_hash, script::{ @@ -26,6 +26,26 @@ use std::{ }; use yansi::Paint; +pub enum ScriptSequenceKind { + Single(ScriptSequence), + Multi(MultiChainSequence), +} + +impl ScriptSequenceKind { + pub fn save(&mut self) -> Result<()> { + match self { + ScriptSequenceKind::Single(sequence) => sequence.save(), + ScriptSequenceKind::Multi(sequence) => sequence.save(), + } + } +} + +impl Drop for ScriptSequenceKind { + fn drop(&mut self) { + self.save().expect("could not save deployment sequence"); + } +} + pub const DRY_RUN_DIR: &str = "dry-run"; /// Helper that saves the transactions sequence and its state on which transactions have been @@ -44,15 +64,13 @@ pub struct ScriptSequence { pub returns: HashMap, pub timestamp: u64, pub chain: u64, - /// If `True`, the sequence belongs to a `MultiChainSequence` and won't save to disk as usual. - pub multi: bool, pub commit: Option, } /// Sensitive values from the transactions in a script sequence #[derive(Clone, Default, Serialize, Deserialize)] pub struct SensitiveTransactionMetadata { - pub rpc: Option, + pub rpc: String, } /// Sensitive info from the script sequence which is saved into the cache folder @@ -106,7 +124,6 @@ impl ScriptSequence { timestamp: now().as_secs(), libraries: vec![], chain, - multi: is_multi, commit, }) } @@ -146,7 +163,9 @@ impl ScriptSequence { /// Saves the transactions as file if it's a standalone deployment. pub fn save(&mut self) -> Result<()> { - if self.multi || self.transactions.is_empty() { + self.sort_receipts(); + + if self.transactions.is_empty() { return Ok(()) } @@ -355,7 +374,7 @@ impl ScriptSequence { /// Returns the first RPC URL of this sequence. pub fn rpc_url(&self) -> Option<&str> { - self.transactions.front().and_then(|tx| tx.rpc.as_deref()) + self.transactions.front().map(|tx| tx.rpc.as_str()) } /// Returns the list of the transactions without the metadata. @@ -371,13 +390,6 @@ impl ScriptSequence { } } -impl Drop for ScriptSequence { - fn drop(&mut self) { - self.sort_receipts(); - self.save().expect("not able to save deployment sequence"); - } -} - /// Converts the `sig` argument into the corresponding file path. /// /// This accepts either the signature of the function or the raw calldata diff --git a/crates/forge/bin/cmd/script/simulate.rs b/crates/forge/bin/cmd/script/simulate.rs index 42da72e5b33a..1d2a2648c5d8 100644 --- a/crates/forge/bin/cmd/script/simulate.rs +++ b/crates/forge/bin/cmd/script/simulate.rs @@ -1,12 +1,5 @@ use super::{ - artifacts::ArtifactInfo, - build::LinkedBuildData, - execute::{ExecutionArtifacts, ExecutionData, PreSimulationState}, - providers::ProvidersManager, - runner::ScriptRunner, - sequence::ScriptSequence, - transaction::TransactionWithMetadata, - ScriptArgs, ScriptConfig, + artifacts::ArtifactInfo, build::LinkedBuildData, execute::{ExecutionArtifacts, ExecutionData, PreSimulationState}, multi::MultiChainSequence, providers::ProvidersManager, runner::ScriptRunner, sequence::{ScriptSequence, ScriptSequenceKind}, transaction::TransactionWithMetadata, ScriptArgs, ScriptConfig }; use alloy_primitives::{utils::format_units, Address, U256}; use ethers_core::types::transaction::eip2718::TypedTransaction; @@ -28,6 +21,8 @@ impl PreSimulationState { /// If simulation is enabled, simulates transactions against fork and fills gas estimation and /// metadata. Otherwise, metadata (e.g. additional contracts, created contract names) is /// left empty. + /// + /// Both modes will panic if any of the transactions have None for the `rpc` field. pub async fn fill_metadata(self) -> Result { let transactions = if let Some(txs) = self.execution_result.transactions.as_ref() { if self.args.skip_simulation { @@ -74,8 +69,8 @@ impl PreSimulationState { let futs = transactions .into_iter() .map(|transaction| async { - let rpc = transaction.rpc.as_ref().expect("missing broadcastable tx rpc url"); - let mut runner = runners.get(rpc).expect("invalid rpc url").write(); + let rpc = transaction.rpc.expect("missing broadcastable tx rpc url"); + let mut runner = runners.get(&rpc).expect("invalid rpc url").write(); let mut tx = transaction.transaction; let result = runner @@ -114,7 +109,7 @@ impl PreSimulationState { } let tx = TransactionWithMetadata::new( tx, - transaction.rpc, + rpc, &result, &address_to_abi, &self.execution_artifacts.decoder, @@ -216,7 +211,7 @@ impl PreSimulationState { .into_iter() .map(|btx| { let mut tx = TransactionWithMetadata::from_tx_request(btx.transaction); - tx.rpc = btx.rpc; + tx.rpc = btx.rpc.expect("missing broadcastable tx rpc url"); tx }) .collect()) @@ -255,17 +250,8 @@ impl FilledTransactionsState { let mut txes_iter = self.transactions.clone().into_iter().peekable(); while let Some(mut tx) = txes_iter.next() { - let tx_rpc = match tx.rpc.clone() { - Some(rpc) => rpc, - None => { - let rpc = self.args.evm_opts.ensure_fork_url()?.clone(); - // Fills the RPC inside the transaction, if missing one. - tx.rpc = Some(rpc.clone()); - rpc - } - }; - - let provider_info = manager.get_or_init_provider(&tx_rpc, self.args.legacy).await?; + let tx_rpc = tx.rpc.clone(); + let provider_info = manager.get_or_init_provider(&tx.rpc, self.args.legacy).await?; // Handles chain specific requirements. tx.change_type(provider_info.is_legacy); @@ -305,7 +291,7 @@ impl FilledTransactionsState { // We only create a [`ScriptSequence`] object when we collect all the rpc related // transactions. if let Some(next_tx) = txes_iter.peek() { - if next_tx.rpc == Some(tx_rpc) { + if next_tx.rpc == tx_rpc { continue; } } @@ -359,13 +345,26 @@ impl FilledTransactionsState { shell::println("\n==========================")?; } } + + let sequence = if sequences.len() == 1 { + ScriptSequenceKind::Single(sequences.pop().expect("empty sequences")) + } else { + ScriptSequenceKind::Multi(MultiChainSequence::new( + sequences, + &self.args.sig, + &self.build_data.build_data.target, + &self.script_config.config, + self.args.broadcast, + )?) + }; + Ok(BundledState { args: self.args, script_config: self.script_config, build_data: self.build_data, execution_data: self.execution_data, execution_artifacts: self.execution_artifacts, - sequences, + sequence, }) } @@ -395,5 +394,11 @@ pub struct BundledState { pub build_data: LinkedBuildData, pub execution_data: ExecutionData, pub execution_artifacts: ExecutionArtifacts, - pub sequences: Vec, + pub sequence: ScriptSequenceKind, +} + +impl BundledState { + pub async fn wait_for_pending(self) -> Result<()> { + Ok(()) + } } diff --git a/crates/forge/bin/cmd/script/transaction.rs b/crates/forge/bin/cmd/script/transaction.rs index fec73b22f001..77ef00a741a8 100644 --- a/crates/forge/bin/cmd/script/transaction.rs +++ b/crates/forge/bin/cmd/script/transaction.rs @@ -44,7 +44,7 @@ pub struct TransactionWithMetadata { #[serde(default = "default_vec_of_strings")] pub arguments: Option>, #[serde(skip)] - pub rpc: Option, + pub rpc: RpcUrl, pub transaction: TypedTransaction, pub additional_contracts: Vec, pub is_fixed_gas_limit: bool, @@ -80,7 +80,7 @@ impl TransactionWithMetadata { pub fn new( transaction: TransactionRequest, - rpc: Option, + rpc: RpcUrl, result: &ScriptResult, local_contracts: &BTreeMap, decoder: &CallTraceDecoder, diff --git a/crates/forge/tests/cli/multi_script.rs b/crates/forge/tests/cli/multi_script.rs index d6f7628da169..6565f0fa2284 100644 --- a/crates/forge/tests/cli/multi_script.rs +++ b/crates/forge/tests/cli/multi_script.rs @@ -58,7 +58,7 @@ forgetest_async!(can_resume_multi_chain_script, |prj, cmd| { tester .add_sig("MultiChainBroadcastNoLink", "deploy(string memory,string memory)") .args(&[&handle1.http_endpoint(), &handle2.http_endpoint()]) - .broadcast(ScriptOutcome::MissingWallet) + .broadcast(ScriptOutcome::WarnSpecifyDeployer) .load_private_keys(&[0, 1]) .await .arg("--multi") From a6cf296f8303bf86b62c2ce60a2257a612f16652 Mon Sep 17 00:00:00 2001 From: Arsenii Kulikov Date: Sat, 2 Mar 2024 00:44:44 +0400 Subject: [PATCH 08/33] refactor execution + resume --- crates/forge/bin/cmd/script/broadcast.rs | 688 ++++++++---------- crates/forge/bin/cmd/script/build.rs | 40 +- crates/forge/bin/cmd/script/cmd.rs | 70 +- crates/forge/bin/cmd/script/execute.rs | 25 +- crates/forge/bin/cmd/script/mod.rs | 29 +- .../script/{multi.rs => multi_sequence.rs} | 86 +-- crates/forge/bin/cmd/script/resume.rs | 78 ++ crates/forge/bin/cmd/script/sequence.rs | 44 +- crates/forge/bin/cmd/script/simulate.rs | 29 +- crates/forge/tests/cli/multi_script.rs | 2 +- 10 files changed, 539 insertions(+), 552 deletions(-) rename crates/forge/bin/cmd/script/{multi.rs => multi_sequence.rs} (64%) create mode 100644 crates/forge/bin/cmd/script/resume.rs diff --git a/crates/forge/bin/cmd/script/broadcast.rs b/crates/forge/bin/cmd/script/broadcast.rs index 675101362aa1..18ff94a339b2 100644 --- a/crates/forge/bin/cmd/script/broadcast.rs +++ b/crates/forge/bin/cmd/script/broadcast.rs @@ -1,431 +1,129 @@ -use super::{receipts::clear_pendings, sequence::{ScriptSequence, ScriptSequenceKind}, - simulate::BundledState, verify::VerifyBundle, ScriptArgs, ScriptConfig, +use super::{ + build::LinkedBuildData, + execute::{ExecutionArtifacts, ExecutionData}, + receipts::{self, clear_pendings}, + sequence::{ScriptSequence, ScriptSequenceKind}, + simulate::BundledState, + verify::VerifyBundle, + ScriptArgs, ScriptConfig, }; use alloy_primitives::{utils::format_units, Address, TxHash, U256}; use ethers_core::types::transaction::eip2718::TypedTransaction; use ethers_providers::{JsonRpcClient, Middleware, Provider}; use ethers_signers::Signer; -use eyre::{bail, Context, ContextCompat, Result}; +use eyre::{bail, Context, Result}; use foundry_cli::{ init_progress, update_progress, utils::{has_batch_support, has_different_gas_calc}, }; use foundry_common::{ - provider::ethers::{estimate_eip1559_fees, try_get_http_provider, RetryProvider}, + provider::ethers::{ + estimate_eip1559_fees, get_http_provider, try_get_http_provider, RetryProvider, + }, shell, types::{ToAlloy, ToEthers}, }; -use foundry_compilers::artifacts::Libraries; -use foundry_config::Config; use foundry_wallets::WalletSigner; -use futures::StreamExt; +use futures::{future::join_all, StreamExt}; use std::{ - cmp::min, collections::{HashMap, HashSet}, sync::Arc, }; -impl ScriptArgs { - /// Sends the transactions which haven't been broadcasted yet. - pub async fn send_transactions( - &self, - deployment_sequence: &mut ScriptSequence, - fork_url: &str, - signers: &HashMap, - ) -> Result<()> { - let provider = Arc::new(try_get_http_provider(fork_url)?); - let already_broadcasted = deployment_sequence.receipts.len(); - - if already_broadcasted < deployment_sequence.transactions.len() { - let required_addresses: HashSet
= deployment_sequence - .typed_transactions() - .skip(already_broadcasted) - .map(|tx| (*tx.from().expect("No sender for onchain transaction!")).to_alloy()) - .collect(); - - let (send_kind, chain) = if self.unlocked { - let chain = provider.get_chainid().await?; - let mut senders = HashSet::from([self - .evm_opts - .sender - .wrap_err("--sender must be set with --unlocked")?]); - // also take all additional senders that where set manually via broadcast - senders.extend( - deployment_sequence - .typed_transactions() - .filter_map(|tx| tx.from().copied().map(|addr| addr.to_alloy())), - ); - (SendTransactionsKind::Unlocked(senders), chain.as_u64()) - } else { - let mut missing_addresses = Vec::new(); - - println!("\n###\nFinding wallets for all the necessary addresses..."); - for addr in &required_addresses { - if !signers.contains_key(addr) { - missing_addresses.push(addr); - } - } - - if !missing_addresses.is_empty() { - let mut error_msg = String::new(); - - // This is an actual used address - if required_addresses.contains(&Config::DEFAULT_SENDER) { - error_msg += "\nYou seem to be using Foundry's default sender. Be sure to set your own --sender.\n"; - } - - eyre::bail!( - "{}No associated wallet for addresses: {:?}. Unlocked wallets: {:?}", - error_msg, - missing_addresses, - signers.keys().collect::>() - ); - } - - let chain = provider.get_chainid().await?.as_u64(); - - (SendTransactionsKind::Raw(signers), chain) - }; - - // We only wait for a transaction receipt before sending the next transaction, if there - // is more than one signer. There would be no way of assuring their order - // otherwise. Or if the chain does not support batched transactions (eg. Arbitrum). - let sequential_broadcast = - send_kind.signers_count() != 1 || self.slow || !has_batch_support(chain); - - // Make a one-time gas price estimation - let (gas_price, eip1559_fees) = { - match deployment_sequence.transactions.front().unwrap().typed_tx() { - TypedTransaction::Eip1559(_) => { - let fees = estimate_eip1559_fees(&provider, Some(chain)) - .await - .wrap_err("Failed to estimate EIP1559 fees. This chain might not support EIP1559, try adding --legacy to your command.")?; - - (None, Some(fees)) - } - _ => (provider.get_gas_price().await.ok(), None), - } - }; - - // Iterate through transactions, matching the `from` field with the associated - // wallet. Then send the transaction. Panics if we find a unknown `from` - let sequence = deployment_sequence - .transactions - .iter() - .skip(already_broadcasted) - .map(|tx_with_metadata| { - let tx = tx_with_metadata.typed_tx(); - let from = (*tx.from().expect("No sender for onchain transaction!")).to_alloy(); - - let kind = send_kind.for_sender(&from)?; - let is_fixed_gas_limit = tx_with_metadata.is_fixed_gas_limit; - - let mut tx = tx.clone(); - - tx.set_chain_id(chain); - - if let Some(gas_price) = self.with_gas_price { - tx.set_gas_price(gas_price.to_ethers()); - } else { - // fill gas price - match tx { - TypedTransaction::Eip1559(ref mut inner) => { - let eip1559_fees = - eip1559_fees.expect("Could not get eip1559 fee estimation."); - if let Some(priority_gas_price) = self.priority_gas_price { - inner.max_priority_fee_per_gas = - Some(priority_gas_price.to_ethers()); - } else { - inner.max_priority_fee_per_gas = Some(eip1559_fees.1); - } - inner.max_fee_per_gas = Some(eip1559_fees.0); - } - _ => { - tx.set_gas_price(gas_price.expect("Could not get gas_price.")); - } - } - } - - Ok((tx, kind, is_fixed_gas_limit)) - }) - .collect::>>()?; - - let pb = init_progress!(deployment_sequence.transactions, "txes"); - - // We send transactions and wait for receipts in batches of 100, since some networks - // cannot handle more than that. - let batch_size = 100; - let mut index = 0; - - for (batch_number, batch) in sequence.chunks(batch_size).map(|f| f.to_vec()).enumerate() - { - let mut pending_transactions = vec![]; - - shell::println(format!( - "##\nSending transactions [{} - {}].", - batch_number * batch_size, - batch_number * batch_size + min(batch_size, batch.len()) - 1 - ))?; - for (tx, kind, is_fixed_gas_limit) in batch.into_iter() { - let tx_hash = self.send_transaction( - provider.clone(), - tx, - kind, - sequential_broadcast, - fork_url, - is_fixed_gas_limit, - ); - - if sequential_broadcast { - let tx_hash = tx_hash.await?; - deployment_sequence.add_pending(index, tx_hash); - - update_progress!(pb, (index + already_broadcasted)); - index += 1; - - clear_pendings(provider.clone(), deployment_sequence, Some(vec![tx_hash])) - .await?; - } else { - pending_transactions.push(tx_hash); - } - } - - if !pending_transactions.is_empty() { - let mut buffer = futures::stream::iter(pending_transactions).buffered(7); - - while let Some(tx_hash) = buffer.next().await { - let tx_hash = tx_hash?; - deployment_sequence.add_pending(index, tx_hash); - - update_progress!(pb, (index + already_broadcasted)); - index += 1; - } - - // Checkpoint save - deployment_sequence.save()?; - - if !sequential_broadcast { - shell::println("##\nWaiting for receipts.")?; - clear_pendings(provider.clone(), deployment_sequence, None).await?; - } - } +async fn estimate_gas( + tx: &mut TypedTransaction, + provider: &Provider, + estimate_multiplier: u64, +) -> Result<()> +where + T: JsonRpcClient, +{ + // if already set, some RPC endpoints might simply return the gas value that is already + // set in the request and omit the estimate altogether, so we remove it here + let _ = tx.gas_mut().take(); + + tx.set_gas( + provider + .estimate_gas(tx, None) + .await + .wrap_err_with(|| format!("Failed to estimate gas for tx: {:?}", tx.sighash()))? * + estimate_multiplier / + 100, + ); + Ok(()) +} - // Checkpoint save - deployment_sequence.save()?; - } +pub async fn send_transaction( + provider: Arc, + mut tx: TypedTransaction, + kind: SendTransactionKind<'_>, + sequential_broadcast: bool, + is_fixed_gas_limit: bool, + skip_simulation: bool, + estimate_multiplier: u64, +) -> Result { + let from = tx.from().expect("no sender"); + + if sequential_broadcast { + let nonce = provider.get_transaction_count(*from, None).await?; + + let tx_nonce = tx.nonce().expect("no nonce"); + if nonce != *tx_nonce { + bail!("EOA nonce changed unexpectedly while sending transactions. Expected {tx_nonce} got {nonce} from provider.") } - - shell::println("\n\n==========================")?; - shell::println("\nONCHAIN EXECUTION COMPLETE & SUCCESSFUL.")?; - - let (total_gas, total_gas_price, total_paid) = deployment_sequence.receipts.iter().fold( - (U256::ZERO, U256::ZERO, U256::ZERO), - |acc, receipt| { - let gas_used = receipt.gas_used.unwrap_or_default().to_alloy(); - let gas_price = receipt.effective_gas_price.unwrap_or_default().to_alloy(); - (acc.0 + gas_used, acc.1 + gas_price, acc.2 + gas_used * gas_price) - }, - ); - let paid = format_units(total_paid, 18).unwrap_or_else(|_| "N/A".to_string()); - let avg_gas_price = - format_units(total_gas_price / U256::from(deployment_sequence.receipts.len()), 9) - .unwrap_or_else(|_| "N/A".to_string()); - shell::println(format!( - "Total Paid: {} ETH ({} gas * avg {} gwei)", - paid.trim_end_matches('0'), - total_gas, - avg_gas_price.trim_end_matches('0').trim_end_matches('.') - ))?; - - Ok(()) } - async fn send_transaction( - &self, - provider: Arc, - mut tx: TypedTransaction, - kind: SendTransactionKind<'_>, - sequential_broadcast: bool, - fork_url: &str, - is_fixed_gas_limit: bool, - ) -> Result { - let from = tx.from().expect("no sender"); - - if sequential_broadcast { - let nonce = forge::next_nonce((*from).to_alloy(), fork_url, None) - .await - .map_err(|_| eyre::eyre!("Not able to query the EOA nonce."))?; - - let tx_nonce = tx.nonce().expect("no nonce"); - if let Ok(tx_nonce) = u64::try_from(tx_nonce.to_alloy()) { - if nonce != tx_nonce { - bail!("EOA nonce changed unexpectedly while sending transactions. Expected {tx_nonce} got {nonce} from provider.") - } - } - } - - match kind { - SendTransactionKind::Unlocked(addr) => { - debug!("sending transaction from unlocked account {:?}: {:?}", addr, tx); - - // Chains which use `eth_estimateGas` are being sent sequentially and require their - // gas to be re-estimated right before broadcasting. - if !is_fixed_gas_limit && - (has_different_gas_calc(provider.get_chainid().await?.as_u64()) || - self.skip_simulation) - { - self.estimate_gas(&mut tx, &provider).await?; - } - - // Submit the transaction - let pending = provider.send_transaction(tx, None).await?; - - Ok(pending.tx_hash().to_alloy()) - } - SendTransactionKind::Raw(signer) => self.broadcast(provider, signer, tx).await, - } + // Chains which use `eth_estimateGas` are being sent sequentially and require their + // gas to be re-estimated right before broadcasting. + if !is_fixed_gas_limit && + (has_different_gas_calc(provider.get_chainid().await?.as_u64()) || skip_simulation) + { + estimate_gas(&mut tx, &provider, estimate_multiplier).await?; } - /// Executes the created transactions, and if no error has occurred, broadcasts - /// them. - pub async fn handle_broadcastable_transactions( - &self, - mut state: BundledState, - verify: VerifyBundle, - ) -> Result<()> { - if self.broadcast { - match &mut state.sequence { - ScriptSequenceKind::Multi(sequence) => { - trace!(target: "script", "broadcasting multi chain deployment"); - self.multi_chain_deployment( - sequence, - state.build_data.libraries, - &state.script_config.config, - verify, - &state.script_config.script_wallets.into_multi_wallet().into_signers()?, - ) - .await?; - } - ScriptSequenceKind::Single(sequence) => { - self.single_deployment( - sequence, - state.script_config, - state.build_data.libraries, - verify, - ) - .await?; - } - } - } + let pending = match kind { + SendTransactionKind::Unlocked(addr) => { + debug!("sending transaction from unlocked account {:?}: {:?}", addr, tx); - if !self.broadcast { - shell::println("\nSIMULATION COMPLETE. To broadcast these transactions, add --broadcast and wallet configuration(s) to the previous command. See forge script --help for more.")?; + // Submit the transaction + provider.send_transaction(tx, None).await? } - Ok(()) - } - - /// Broadcasts a single chain script. - async fn single_deployment( - &self, - deployment_sequence: &mut ScriptSequence, - script_config: ScriptConfig, - libraries: Libraries, - verify: VerifyBundle, - ) -> Result<()> { - trace!(target: "script", "broadcasting single chain deployment"); - - if self.verify { - deployment_sequence.verify_preflight_check(&script_config.config, &verify)?; - } - - let rpc = script_config.total_rpcs.into_iter().next().expect("exists; qed"); - - deployment_sequence.add_libraries(libraries); + SendTransactionKind::Raw(signer) => { + debug!("sending transaction: {:?}", tx); - let signers = script_config.script_wallets.into_multi_wallet().into_signers()?; + // Signing manually so we skip `fill_transaction` and its `eth_createAccessList` + // request. + let signature = + signer.sign_transaction(&tx).await.wrap_err("Failed to sign transaction")?; - self.send_transactions(deployment_sequence, &rpc, &signers).await?; - - if self.verify { - return deployment_sequence.verify_contracts(&script_config.config, verify).await; - } - Ok(()) - } - - /// Uses the signer to submit a transaction to the network. If it fails, it tries to retrieve - /// the transaction hash that can be used on a later run with `--resume`. - async fn broadcast( - &self, - provider: Arc, - signer: &WalletSigner, - mut legacy_or_1559: TypedTransaction, - ) -> Result { - debug!("sending transaction: {:?}", legacy_or_1559); - - // Chains which use `eth_estimateGas` are being sent sequentially and require their gas - // to be re-estimated right before broadcasting. - if has_different_gas_calc(signer.chain_id()) || self.skip_simulation { - // if already set, some RPC endpoints might simply return the gas value that is - // already set in the request and omit the estimate altogether, so - // we remove it here - let _ = legacy_or_1559.gas_mut().take(); - - self.estimate_gas(&mut legacy_or_1559, &provider).await?; + // Submit the raw transaction + provider.send_raw_transaction(tx.rlp_signed(&signature)).await? } + }; - // Signing manually so we skip `fill_transaction` and its `eth_createAccessList` - // request. - let signature = signer - .sign_transaction(&legacy_or_1559) - .await - .wrap_err("Failed to sign transaction")?; - - // Submit the raw transaction - let pending = provider.send_raw_transaction(legacy_or_1559.rlp_signed(&signature)).await?; - - Ok(pending.tx_hash().to_alloy()) - } - - async fn estimate_gas(&self, tx: &mut TypedTransaction, provider: &Provider) -> Result<()> - where - T: JsonRpcClient, - { - // if already set, some RPC endpoints might simply return the gas value that is already - // set in the request and omit the estimate altogether, so we remove it here - let _ = tx.gas_mut().take(); - - tx.set_gas( - provider - .estimate_gas(tx, None) - .await - .wrap_err_with(|| format!("Failed to estimate gas for tx: {:?}", tx.sighash()))? * - self.gas_estimate_multiplier / - 100, - ); - Ok(()) - } + Ok(pending.tx_hash().to_alloy()) } /// How to send a single transaction #[derive(Clone)] -enum SendTransactionKind<'a> { +pub enum SendTransactionKind<'a> { Unlocked(Address), Raw(&'a WalletSigner), } /// Represents how to send _all_ transactions -enum SendTransactionsKind<'a> { +pub enum SendTransactionsKind { /// Send via `eth_sendTransaction` and rely on the `from` address being unlocked. Unlocked(HashSet
), /// Send a signed transaction via `eth_sendRawTransaction` - Raw(&'a HashMap), + Raw(HashMap), } -impl SendTransactionsKind<'_> { +impl SendTransactionsKind { /// Returns the [`SendTransactionKind`] for the given address /// /// Returns an error if no matching signer is found or the address is not unlocked - fn for_sender(&self, addr: &Address) -> Result> { + pub fn for_sender(&self, addr: &Address) -> Result> { match self { SendTransactionsKind::Unlocked(unlocked) => { if !unlocked.contains(addr) { @@ -444,10 +142,238 @@ impl SendTransactionsKind<'_> { } /// How many signers are set - fn signers_count(&self) -> usize { + pub fn signers_count(&self) -> usize { match self { SendTransactionsKind::Unlocked(addr) => addr.len(), SendTransactionsKind::Raw(signers) => signers.len(), } } } + +impl BundledState { + pub async fn wait_for_pending(mut self) -> Result { + let futs = self + .sequence + .iter_sequeneces_mut() + .map(|sequence| async move { + let rpc_url = sequence.rpc_url(); + let provider = Arc::new(get_http_provider(rpc_url)); + receipts::wait_for_pending(provider, sequence).await + }) + .collect::>(); + + let errors = + join_all(futs).await.into_iter().filter(|res| res.is_err()).collect::>(); + + if !errors.is_empty() { + return Err(eyre::eyre!("{errors:?}")); + } + + Ok(self) + } + + pub async fn broadcast(mut self) -> Result { + let required_addresses = self + .sequence + .iter_sequences() + .flat_map(|sequence| { + sequence + .typed_transactions() + .map(|tx| (*tx.from().expect("No sender for onchain transaction!")).to_alloy()) + }) + .collect::>(); + + let send_kind = if self.args.unlocked { + SendTransactionsKind::Unlocked(required_addresses) + } else { + let signers = self.script_wallets.into_multi_wallet().into_signers()?; + let mut missing_addresses = Vec::new(); + + for addr in &required_addresses { + if !signers.contains_key(addr) { + missing_addresses.push(addr); + } + } + + if !missing_addresses.is_empty() { + eyre::bail!( + "No associated wallet for addresses: {:?}. Unlocked wallets: {:?}", + missing_addresses, + signers.keys().collect::>() + ); + } + + SendTransactionsKind::Raw(signers) + }; + + for sequence in self.sequence.iter_sequeneces_mut() { + let provider = Arc::new(try_get_http_provider(sequence.rpc_url())?); + let already_broadcasted = sequence.receipts.len(); + + if already_broadcasted < sequence.transactions.len() { + let chain = provider.get_chainid().await?.as_u64(); + + // We only wait for a transaction receipt before sending the next transaction, if + // there is more than one signer. There would be no way of assuring + // their order otherwise. Or if the chain does not support batched + // transactions (eg. Arbitrum). + let sequential_broadcast = + send_kind.signers_count() != 1 || self.args.slow || !has_batch_support(chain); + + // Make a one-time gas price estimation + let (gas_price, eip1559_fees) = match self.args.with_gas_price { + None => match sequence.transactions.front().unwrap().typed_tx() { + TypedTransaction::Eip1559(_) => { + let mut fees = estimate_eip1559_fees(&provider, Some(chain)) + .await + .wrap_err("Failed to estimate EIP1559 fees. This chain might not support EIP1559, try adding --legacy to your command.")?; + + if let Some(priority_gas_price) = self.args.priority_gas_price { + fees.1 = priority_gas_price.to_ethers(); + } + + (None, Some(fees)) + } + _ => (provider.get_gas_price().await.ok(), None), + }, + Some(gas_price) => (Some(gas_price.to_ethers()), None), + }; + + // Iterate through transactions, matching the `from` field with the associated + // wallet. Then send the transaction. Panics if we find a unknown `from` + let transactions = sequence + .transactions + .iter() + .skip(already_broadcasted) + .map(|tx_with_metadata| { + let tx = tx_with_metadata.typed_tx(); + let from = + (*tx.from().expect("No sender for onchain transaction!")).to_alloy(); + + let kind = send_kind.for_sender(&from)?; + let is_fixed_gas_limit = tx_with_metadata.is_fixed_gas_limit; + + let mut tx = tx.clone(); + + tx.set_chain_id(chain); + + if let Some(gas_price) = gas_price { + tx.set_gas_price(gas_price); + } else { + let eip1559_fees = eip1559_fees.expect("was set above"); + // fill gas price + match tx { + TypedTransaction::Eip1559(ref mut inner) => { + inner.max_priority_fee_per_gas = Some(eip1559_fees.1); + inner.max_fee_per_gas = Some(eip1559_fees.0); + } + _ => { + // If we're here, it means that first transaction of the + // sequence was EIP1559 transaction (see match statement above), + // however, we can only have transactions of the same type in + // the sequence. + unreachable!() + } + } + } + + Ok((tx, kind, is_fixed_gas_limit)) + }) + .collect::>>()?; + + let pb = init_progress!(transactions, "txes"); + + // We send transactions and wait for receipts in batches of 100, since some networks + // cannot handle more than that. + let batch_size = if sequential_broadcast { 1 } else { 100 }; + let mut index = 0; + + for (batch_number, batch) in + transactions.chunks(batch_size).map(|f| f.to_vec()).enumerate() + { + let mut pending_transactions = vec![]; + + shell::println(format!( + "##\nSending transactions [{} - {}].", + batch_number * batch_size, + batch_number * batch_size + std::cmp::min(batch_size, batch.len()) - 1 + ))?; + for (tx, kind, is_fixed_gas_limit) in batch.into_iter() { + let tx_hash = send_transaction( + provider.clone(), + tx, + kind, + sequential_broadcast, + is_fixed_gas_limit, + self.args.skip_simulation, + self.args.gas_estimate_multiplier, + ); + pending_transactions.push(tx_hash); + } + + if !pending_transactions.is_empty() { + let mut buffer = futures::stream::iter(pending_transactions).buffered(7); + + while let Some(tx_hash) = buffer.next().await { + let tx_hash = tx_hash?; + sequence.add_pending(index, tx_hash); + + update_progress!(pb, (index + already_broadcasted)); + index += 1; + } + + // Checkpoint save + sequence.save(true)?; + + shell::println("##\nWaiting for receipts.")?; + receipts::clear_pendings(provider.clone(), sequence, None).await?; + } + + // Checkpoint save + sequence.save(true)?; + } + } + + shell::println("\n\n==========================")?; + shell::println("\nONCHAIN EXECUTION COMPLETE & SUCCESSFUL.")?; + + let (total_gas, total_gas_price, total_paid) = sequence.receipts.iter().fold( + (U256::ZERO, U256::ZERO, U256::ZERO), + |acc, receipt| { + let gas_used = receipt.gas_used.unwrap_or_default().to_alloy(); + let gas_price = receipt.effective_gas_price.unwrap_or_default().to_alloy(); + (acc.0 + gas_used, acc.1 + gas_price, acc.2 + gas_used * gas_price) + }, + ); + let paid = format_units(total_paid, 18).unwrap_or_else(|_| "N/A".to_string()); + let avg_gas_price = + format_units(total_gas_price / U256::from(sequence.receipts.len()), 9) + .unwrap_or_else(|_| "N/A".to_string()); + + shell::println(format!( + "Total Paid: {} ETH ({} gas * avg {} gwei)", + paid.trim_end_matches('0'), + total_gas, + avg_gas_price.trim_end_matches('0').trim_end_matches('.') + ))?; + } + + Ok(BroadcastedState { + args: self.args, + script_config: self.script_config, + build_data: self.build_data, + execution_data: self.execution_data, + execution_artifacts: self.execution_artifacts, + sequence: self.sequence, + }) + } +} + +pub struct BroadcastedState { + pub args: ScriptArgs, + pub script_config: ScriptConfig, + pub build_data: LinkedBuildData, + pub execution_data: ExecutionData, + pub execution_artifacts: ExecutionArtifacts, + pub sequence: ScriptSequenceKind, +} diff --git a/crates/forge/bin/cmd/script/build.rs b/crates/forge/bin/cmd/script/build.rs index 2f6d589fed31..250da4cf8fc1 100644 --- a/crates/forge/bin/cmd/script/build.rs +++ b/crates/forge/bin/cmd/script/build.rs @@ -1,6 +1,9 @@ use super::{ScriptArgs, ScriptConfig}; use alloy_primitives::{Address, Bytes}; use eyre::{Context, OptionExt, Result}; +use forge::{ + inspectors::cheatcodes::ScriptWallets, +}; use foundry_cli::utils::get_cached_entry_by_name; use foundry_common::{ compile::{self, ContractSources, ProjectCompiler}, @@ -19,22 +22,24 @@ use std::str::FromStr; pub struct PreprocessedState { pub args: ScriptArgs, pub script_config: ScriptConfig, + pub script_wallets: ScriptWallets, } impl PreprocessedState { pub fn compile(self) -> Result { - let project = self.script_config.config.project()?; - let filters = self.args.opts.skip.clone().unwrap_or_default(); + let Self { args, script_config, script_wallets } = self; + let project = script_config.config.project()?; + let filters = args.opts.skip.clone().unwrap_or_default(); - let mut target_name = self.args.target_contract.clone(); + let mut target_name = args.target_contract.clone(); // If we've received correct path, use it as target_path // Otherwise, parse input as : and use the path from the contract info, if // present. - let target_path = if let Ok(path) = dunce::canonicalize(&self.args.path) { + let target_path = if let Ok(path) = dunce::canonicalize(&args.path) { Ok::<_, eyre::Report>(Some(path)) } else { - let contract = ContractInfo::from_str(&self.args.path)?; + let contract = ContractInfo::from_str(&args.path)?; target_name = Some(contract.name.clone()); if let Some(path) = contract.path { Ok(Some(dunce::canonicalize(path)?)) @@ -49,8 +54,8 @@ impl PreprocessedState { compile::compile_target_with_filter( &target_path, &project, - self.args.opts.args.silent, - self.args.verify, + args.opts.args.silent, + args.verify, filters, ) } else if !project.paths.has_input_files() { @@ -111,9 +116,10 @@ impl PreprocessedState { let linker = Linker::new(project.root(), contracts); Ok(CompiledState { - args: self.args, - script_config: self.script_config, - build_data: BuildData { sources, linker, target }, + args, + script_config, + script_wallets, + build_data: BuildData { linker, target, sources }, }) } } @@ -209,22 +215,26 @@ impl LinkedBuildData { pub struct CompiledState { pub args: ScriptArgs, pub script_config: ScriptConfig, + pub script_wallets: ScriptWallets, pub build_data: BuildData, } impl CompiledState { pub fn link(self) -> Result { - let sender = self.script_config.evm_opts.sender; - let nonce = self.script_config.sender_nonce; - let known_libraries = self.script_config.config.libraries_with_remappings()?; - let build_data = self.build_data.link(known_libraries, sender, nonce)?; + let Self { args, script_config, script_wallets, build_data } = self; + + let sender = script_config.evm_opts.sender; + let nonce = script_config.sender_nonce; + let known_libraries = script_config.config.libraries_with_remappings()?; + let build_data = build_data.link(known_libraries, sender, nonce)?; - Ok(LinkedState { args: self.args, script_config: self.script_config, build_data }) + Ok(LinkedState { args, script_config, script_wallets, build_data }) } } pub struct LinkedState { pub args: ScriptArgs, pub script_config: ScriptConfig, + pub script_wallets: ScriptWallets, pub build_data: LinkedBuildData, } diff --git a/crates/forge/bin/cmd/script/cmd.rs b/crates/forge/bin/cmd/script/cmd.rs index 2676bac5e56e..c4de098083bf 100644 --- a/crates/forge/bin/cmd/script/cmd.rs +++ b/crates/forge/bin/cmd/script/cmd.rs @@ -1,13 +1,9 @@ -use super::{ - multi::MultiChainSequence, sequence::ScriptSequence, verify::VerifyBundle, ScriptArgs, - ScriptConfig, -}; +use super::{verify::VerifyBundle, ScriptArgs, ScriptConfig}; use crate::cmd::script::{ build::{LinkedBuildData, PreprocessedState}, receipts, }; use alloy_primitives::Address; -use ethers_providers::Middleware; use ethers_signers::Signer; use eyre::Result; use forge::inspectors::cheatcodes::ScriptWallets; @@ -17,27 +13,27 @@ use foundry_compilers::artifacts::Libraries; use std::sync::Arc; impl ScriptArgs { - /// Executes the script - pub async fn run_script(mut self) -> Result<()> { - trace!(target: "script", "executing script command"); - + async fn preprocess(self) -> Result { let script_wallets = ScriptWallets::new(self.wallets.get_multi_wallet().await?, self.evm_opts.sender); + let (config, mut evm_opts) = self.load_config_and_evm_opts_emit_warnings()?; if let Some(sender) = self.maybe_load_private_key()? { evm_opts.sender = sender; } - let mut script_config = ScriptConfig::new(config, evm_opts, script_wallets).await?; + let script_config = ScriptConfig::new(config, evm_opts).await?; - if script_config.evm_opts.fork_url.is_none() { - // if not forking, then ignore any pre-deployed library addresses - script_config.config.libraries = Default::default(); - } + Ok(PreprocessedState { args: self, script_config, script_wallets }) + } + + /// Executes the script + pub async fn run_script(mut self) -> Result<()> { + trace!(target: "script", "executing script command"); - let state = PreprocessedState { args: self.clone(), script_config }; - let state = state + let state = self.preprocess() + .await? .compile()? .link()? .prepare_execution() @@ -47,29 +43,28 @@ impl ScriptArgs { .prepare_simulation() .await?; + if state.args.debug { + state.run_debugger()?; + } + let mut verify = VerifyBundle::new( &state.script_config.config.project()?, &state.script_config.config, state.build_data.get_flattened_contracts(false), - self.retry, - self.verifier.clone(), + state.args.retry, + state.args.verifier.clone(), ); - if self.resume || (self.verify && !self.broadcast) { - return self.resume_deployment(state.script_config, state.build_data, verify).await; - } - - if self.debug { - state.run_debugger() + let state = if state.args.resume || (state.args.verify && !state.args.broadcast) { + state.resume().await? } else { - if self.json { + if state.args.json { state.show_json()?; } else { state.show_traces().await?; } - verify.known_contracts = state.build_data.get_flattened_contracts(false); - self.check_contract_sizes( + state.args.check_contract_sizes( &state.execution_result, &state.build_data.highlevel_known_contracts, )?; @@ -85,13 +80,20 @@ impl ScriptArgs { return Ok(()); } - let state = state.bundle().await?; + state.bundle().await? + }; - self.handle_broadcastable_transactions(state, verify).await + if !state.args.broadcast { + shell::println("\nSIMULATION COMPLETE. To broadcast these transactions, add --broadcast and wallet configuration(s) to the previous command. See forge script --help for more.")?; + return Ok(()); } + + let state = state.wait_for_pending().await?.broadcast().await?; + + Ok(()) } - /// Resumes the deployment and/or verification of the script. + /*/// Resumes the deployment and/or verification of the script. async fn resume_deployment( &mut self, script_config: ScriptConfig, @@ -122,9 +124,9 @@ impl ScriptArgs { .map_err(|err| { eyre::eyre!("{err}\n\nIf you were trying to resume or verify a multi chain deployment, add `--multi` to your command invocation.") }) - } + }*/ - /// Resumes the deployment and/or verification of a single RPC script. + /*/// Resumes the deployment and/or verification of a single RPC script. async fn resume_single_deployment( &mut self, script_config: ScriptConfig, @@ -190,11 +192,11 @@ impl ScriptArgs { } Ok(()) - } + }*/ /// In case the user has loaded *only* one private-key, we can assume that he's using it as the /// `--sender` - fn maybe_load_private_key(&mut self) -> Result> { + fn maybe_load_private_key(&self) -> Result> { let maybe_sender = self .wallets .private_keys()? diff --git a/crates/forge/bin/cmd/script/execute.rs b/crates/forge/bin/cmd/script/execute.rs index 98f4ccb330fe..1438567ccc20 100644 --- a/crates/forge/bin/cmd/script/execute.rs +++ b/crates/forge/bin/cmd/script/execute.rs @@ -1,6 +1,7 @@ use super::{ build::{CompiledState, LinkedBuildData, LinkedState}, runner::ScriptRunner, + simulate::BundledState, JsonResult, NestedValue, ScriptArgs, ScriptConfig, ScriptResult, }; use alloy_dyn_abi::FunctionExt; @@ -11,7 +12,7 @@ use async_recursion::async_recursion; use eyre::Result; use forge::{ decode::{decode_console_logs, RevertDecoder}, - inspectors::cheatcodes::{BroadcastableTransaction, BroadcastableTransactions}, + inspectors::cheatcodes::{BroadcastableTransaction, BroadcastableTransactions, ScriptWallets}, traces::{ identifier::{EtherscanIdentifier, LocalTraceIdentifier, SignaturesIdentifier}, render_trace_arena, CallTraceDecoder, CallTraceDecoderBuilder, TraceKind, @@ -38,6 +39,7 @@ pub struct ExecutionData { pub struct PreExecutionState { pub args: ScriptArgs, pub script_config: ScriptConfig, + pub script_wallets: ScriptWallets, pub build_data: LinkedBuildData, pub execution_data: ExecutionData, } @@ -45,20 +47,23 @@ pub struct PreExecutionState { impl LinkedState { /// Given linked and compiled artifacts, prepares data we need for execution. pub async fn prepare_execution(self) -> Result { - let ContractBytecodeSome { abi, bytecode, .. } = self.build_data.get_target_contract()?; + let Self { args, script_config, script_wallets, build_data } = self; + + let ContractBytecodeSome { abi, bytecode, .. } = build_data.get_target_contract()?; let bytecode = bytecode.into_bytes().ok_or_else(|| { eyre::eyre!("expected fully linked bytecode, found unlinked bytecode") })?; - let (func, calldata) = self.args.get_method_and_calldata(&abi)?; + let (func, calldata) = args.get_method_and_calldata(&abi)?; ensure_clean_constructor(&abi)?; Ok(PreExecutionState { - args: self.args, - script_config: self.script_config, - build_data: self.build_data, + args, + script_config, + script_wallets, + build_data, execution_data: ExecutionData { func, calldata, bytecode, abi }, }) } @@ -67,6 +72,7 @@ impl LinkedState { pub struct ExecutedState { pub args: ScriptArgs, pub script_config: ScriptConfig, + pub script_wallets: ScriptWallets, pub build_data: LinkedBuildData, pub execution_data: ExecutionData, pub execution_result: ScriptResult, @@ -75,7 +81,8 @@ pub struct ExecutedState { impl PreExecutionState { #[async_recursion] pub async fn execute(mut self) -> Result { - let mut runner = self.script_config.get_runner(true).await?; + let mut runner = + self.script_config.get_runner_with_cheatcodes(self.script_wallets.clone()).await?; let mut result = self.execute_with_runner(&mut runner).await?; // If we have a new sender from execution, we need to use it to deploy libraries and relink @@ -87,6 +94,7 @@ impl PreExecutionState { let state = CompiledState { args: self.args, script_config: self.script_config, + script_wallets: self.script_wallets, build_data: self.build_data.build_data, }; @@ -123,6 +131,7 @@ impl PreExecutionState { Ok(ExecutedState { args: self.args, script_config: self.script_config, + script_wallets: self.script_wallets, build_data: self.build_data, execution_data: self.execution_data, execution_result: result, @@ -218,6 +227,7 @@ impl ExecutedState { Ok(PreSimulationState { args: self.args, script_config: self.script_config, + script_wallets: self.script_wallets, build_data: self.build_data, execution_data: self.execution_data, execution_result: self.execution_result, @@ -302,6 +312,7 @@ impl ExecutedState { pub struct PreSimulationState { pub args: ScriptArgs, pub script_config: ScriptConfig, + pub script_wallets: ScriptWallets, pub build_data: LinkedBuildData, pub execution_data: ExecutionData, pub execution_result: ScriptResult, diff --git a/crates/forge/bin/cmd/script/mod.rs b/crates/forge/bin/cmd/script/mod.rs index 6f86319fd298..8a71ce54be52 100644 --- a/crates/forge/bin/cmd/script/mod.rs +++ b/crates/forge/bin/cmd/script/mod.rs @@ -50,9 +50,10 @@ mod broadcast; mod build; mod cmd; mod execute; -mod multi; +mod multi_sequence; mod providers; mod receipts; +mod resume; mod runner; mod sequence; mod simulate; @@ -403,16 +404,10 @@ pub struct ScriptConfig { pub missing_rpc: bool, /// Should return some debug information pub debug: bool, - /// Container for wallets needed through script execution - pub script_wallets: ScriptWallets, } impl ScriptConfig { - pub async fn new( - config: Config, - evm_opts: EvmOpts, - script_wallets: ScriptWallets, - ) -> Result { + pub async fn new(config: Config, evm_opts: EvmOpts) -> Result { let sender_nonce = if let Some(fork_url) = evm_opts.fork_url.as_ref() { forge::next_nonce(evm_opts.sender, fork_url, None).await? } else { @@ -427,7 +422,6 @@ impl ScriptConfig { total_rpcs: HashSet::new(), missing_rpc: false, debug: false, - script_wallets, }) } @@ -504,7 +498,18 @@ For more information, please see https://eips.ethereum.org/EIPS/eip-3855", Ok(()) } - async fn get_runner(&mut self, cheatcodes: bool) -> Result { + async fn get_runner(&mut self) -> Result { + self._get_runner(None).await + } + + async fn get_runner_with_cheatcodes( + &mut self, + script_wallets: ScriptWallets, + ) -> Result { + self._get_runner(Some(script_wallets)).await + } + + async fn _get_runner(&mut self, script_wallets: Option) -> Result { trace!("preparing script runner"); let env = self.evm_opts.evm_env().await?; @@ -531,7 +536,7 @@ For more information, please see https://eips.ethereum.org/EIPS/eip-3855", .spec(self.config.evm_spec_id()) .gas_limit(self.evm_opts.gas_limit()); - if cheatcodes { + if let Some(script_wallets) = script_wallets { builder = builder.inspectors(|stack| { stack .debug(self.debug) @@ -539,7 +544,7 @@ For more information, please see https://eips.ethereum.org/EIPS/eip-3855", CheatsConfig::new( &self.config, self.evm_opts.clone(), - Some(self.script_wallets.clone()), + Some(script_wallets), ) .into(), ) diff --git a/crates/forge/bin/cmd/script/multi.rs b/crates/forge/bin/cmd/script/multi_sequence.rs similarity index 64% rename from crates/forge/bin/cmd/script/multi.rs rename to crates/forge/bin/cmd/script/multi_sequence.rs index 59bbbda1fad4..7a0e925f587b 100644 --- a/crates/forge/bin/cmd/script/multi.rs +++ b/crates/forge/bin/cmd/script/multi_sequence.rs @@ -1,23 +1,13 @@ -use super::{ - receipts, - sequence::{sig_to_file_name, ScriptSequence, SensitiveScriptSequence, DRY_RUN_DIR}, - verify::VerifyBundle, - ScriptArgs, -}; -use alloy_primitives::Address; -use eyre::{ContextCompat, Report, Result, WrapErr}; +use super::sequence::{sig_to_file_name, ScriptSequence, SensitiveScriptSequence, DRY_RUN_DIR}; +use eyre::{ContextCompat, Result, WrapErr}; use foundry_cli::utils::now; -use foundry_common::{fs, provider::ethers::get_http_provider}; -use foundry_compilers::{artifacts::Libraries, ArtifactId}; +use foundry_common::fs; +use foundry_compilers::ArtifactId; use foundry_config::Config; -use foundry_wallets::WalletSigner; -use futures::future::join_all; use serde::{Deserialize, Serialize}; use std::{ - collections::HashMap, io::{BufWriter, Write}, path::{Path, PathBuf}, - sync::Arc, }; /// Holds the sequences of multiple chain deployments. @@ -130,7 +120,7 @@ impl MultiChainSequence { } /// Saves the transactions as file if it's a standalone deployment. - pub fn save(&mut self) -> Result<()> { + pub fn save(&mut self, silent: bool) -> Result<()> { self.deployments.iter_mut().for_each(|sequence| sequence.sort_receipts()); self.timestamp = now().as_secs(); @@ -167,69 +157,3 @@ impl MultiChainSequence { Ok(()) } } - -impl ScriptArgs { - /// Given a [`MultiChainSequence`] with multiple sequences of different chains, it executes them - /// all in parallel. Supports `--resume` and `--verify`. - pub async fn multi_chain_deployment( - &self, - deployments: &mut MultiChainSequence, - libraries: Libraries, - config: &Config, - verify: VerifyBundle, - signers: &HashMap, - ) -> Result<()> { - if !libraries.is_empty() { - eyre::bail!("Libraries are currently not supported on multi deployment setups."); - } - - if self.verify { - for sequence in &deployments.deployments { - sequence.verify_preflight_check(config, &verify)?; - } - } - - if self.resume { - trace!(target: "script", "resuming multi chain deployment"); - - let futs = deployments - .deployments - .iter_mut() - .map(|sequence| async move { - let rpc_url = sequence.rpc_url().unwrap(); - let provider = Arc::new(get_http_provider(rpc_url)); - receipts::wait_for_pending(provider, sequence).await - }) - .collect::>(); - - let errors = - join_all(futs).await.into_iter().filter(|res| res.is_err()).collect::>(); - - if !errors.is_empty() { - return Err(eyre::eyre!("{errors:?}")); - } - } - - trace!(target: "script", "broadcasting multi chain deployments"); - - let mut results: Vec> = Vec::new(); - - for sequence in deployments.deployments.iter_mut() { - let rpc_url = sequence.rpc_url().unwrap().to_string(); - let result = match self.send_transactions(sequence, &rpc_url, signers).await { - Ok(_) if self.verify => sequence.verify_contracts(config, verify.clone()).await, - Ok(_) => Ok(()), - Err(err) => Err(err), - }; - results.push(result); - } - - let errors = results.into_iter().filter(|res| res.is_err()).collect::>(); - - if !errors.is_empty() { - return Err(eyre::eyre!("{errors:?}")); - } - - Ok(()) - } -} diff --git a/crates/forge/bin/cmd/script/resume.rs b/crates/forge/bin/cmd/script/resume.rs new file mode 100644 index 000000000000..6e674a3b4d4b --- /dev/null +++ b/crates/forge/bin/cmd/script/resume.rs @@ -0,0 +1,78 @@ +use std::sync::Arc; + +use ethers_providers::Middleware; +use eyre::{OptionExt, Result}; +use foundry_common::{provider::ethers::try_get_http_provider, shell}; + +use super::{ + execute::PreSimulationState, + multi_sequence::MultiChainSequence, + sequence::{ScriptSequence, ScriptSequenceKind}, + simulate::BundledState, +}; + +impl PreSimulationState { + /// Tries loading the resumed state from the cache files, skipping simulation stage. + pub async fn resume(self) -> Result { + let Self { + args, + script_config, + script_wallets, + build_data, + execution_data, + execution_result: _, + execution_artifacts, + } = self; + + let sequence = if args.multi { + ScriptSequenceKind::Multi(MultiChainSequence::load( + &script_config.config, + &args.sig, + &build_data.build_data.target, + )?) + } else { + let fork_url = script_config + .evm_opts + .fork_url + .as_deref() + .ok_or_eyre("Missing `--fork-url` field.")?; + + let provider = Arc::new(try_get_http_provider(fork_url)?); + let chain = provider.get_chainid().await?.as_u64(); + + let seq = match ScriptSequence::load( + &script_config.config, + &args.sig, + &build_data.build_data.target, + chain, + args.broadcast, + ) { + Ok(seq) => seq, + // If the script was simulated, but there was no attempt to broadcast yet, + // try to read the script sequence from the `dry-run/` folder + Err(_) if args.broadcast => ScriptSequence::load( + &script_config.config, + &args.sig, + &build_data.build_data.target, + chain, + false, + )?, + Err(err) => { + eyre::bail!(err.wrap_err("If you were trying to resume or verify a multi chain deployment, add `--multi` to your command invocation.")) + } + }; + + ScriptSequenceKind::Single(seq) + }; + + Ok(BundledState { + args, + script_config, + script_wallets, + build_data, + execution_data, + execution_artifacts, + sequence, + }) + } +} diff --git a/crates/forge/bin/cmd/script/sequence.rs b/crates/forge/bin/cmd/script/sequence.rs index 1bece7a12a86..ded98924f6a9 100644 --- a/crates/forge/bin/cmd/script/sequence.rs +++ b/crates/forge/bin/cmd/script/sequence.rs @@ -1,4 +1,4 @@ -use super::{multi::MultiChainSequence, NestedValue}; +use super::{multi_sequence::MultiChainSequence, NestedValue}; use crate::cmd::{ init::get_commit_hash, script::{ @@ -32,17 +32,31 @@ pub enum ScriptSequenceKind { } impl ScriptSequenceKind { - pub fn save(&mut self) -> Result<()> { + pub fn save(&mut self, silent: bool) -> Result<()> { match self { - ScriptSequenceKind::Single(sequence) => sequence.save(), - ScriptSequenceKind::Multi(sequence) => sequence.save(), + ScriptSequenceKind::Single(sequence) => sequence.save(silent), + ScriptSequenceKind::Multi(sequence) => sequence.save(silent), + } + } + + pub fn iter_sequences(&self) -> impl Iterator { + match self { + ScriptSequenceKind::Single(sequence) => std::slice::from_ref(sequence).iter(), + ScriptSequenceKind::Multi(sequence) => sequence.deployments.iter(), + } + } + + pub fn iter_sequeneces_mut(&mut self) -> impl Iterator { + match self { + ScriptSequenceKind::Single(sequence) => std::slice::from_mut(sequence).iter_mut(), + ScriptSequenceKind::Multi(sequence) => sequence.deployments.iter_mut(), } } } impl Drop for ScriptSequenceKind { fn drop(&mut self) { - self.save().expect("could not save deployment sequence"); + self.save(false).expect("could not save deployment sequence"); } } @@ -64,6 +78,8 @@ pub struct ScriptSequence { pub returns: HashMap, pub timestamp: u64, pub chain: u64, + /// If `True`, the sequence belongs to a `MultiChainSequence` and won't save to disk as usual. + pub multi: bool, pub commit: Option, } @@ -125,6 +141,7 @@ impl ScriptSequence { libraries: vec![], chain, commit, + multi: is_multi, }) } @@ -162,10 +179,10 @@ impl ScriptSequence { } /// Saves the transactions as file if it's a standalone deployment. - pub fn save(&mut self) -> Result<()> { + pub fn save(&mut self, silent: bool) -> Result<()> { self.sort_receipts(); - if self.transactions.is_empty() { + if self.multi || self.transactions.is_empty() { return Ok(()) } @@ -190,8 +207,13 @@ impl ScriptSequence { //../run-[timestamp].json fs::copy(&self.sensitive_path, self.sensitive_path.with_file_name(&ts_name))?; - shell::println(format!("\nTransactions saved to: {}\n", self.path.display()))?; - shell::println(format!("Sensitive values saved to: {}\n", self.sensitive_path.display()))?; + if !silent { + shell::println(format!("\nTransactions saved to: {}\n", self.path.display()))?; + shell::println(format!( + "Sensitive values saved to: {}\n", + self.sensitive_path.display() + ))?; + } Ok(()) } @@ -373,8 +395,8 @@ impl ScriptSequence { } /// Returns the first RPC URL of this sequence. - pub fn rpc_url(&self) -> Option<&str> { - self.transactions.front().map(|tx| tx.rpc.as_str()) + pub fn rpc_url(&self) -> &str { + self.transactions.front().expect("empty sequence").rpc.as_str() } /// Returns the list of the transactions without the metadata. diff --git a/crates/forge/bin/cmd/script/simulate.rs b/crates/forge/bin/cmd/script/simulate.rs index 1d2a2648c5d8..99572401c342 100644 --- a/crates/forge/bin/cmd/script/simulate.rs +++ b/crates/forge/bin/cmd/script/simulate.rs @@ -1,14 +1,25 @@ use super::{ - artifacts::ArtifactInfo, build::LinkedBuildData, execute::{ExecutionArtifacts, ExecutionData, PreSimulationState}, multi::MultiChainSequence, providers::ProvidersManager, runner::ScriptRunner, sequence::{ScriptSequence, ScriptSequenceKind}, transaction::TransactionWithMetadata, ScriptArgs, ScriptConfig + artifacts::ArtifactInfo, + build::LinkedBuildData, + execute::{ExecutionArtifacts, ExecutionData, PreSimulationState}, + multi_sequence::MultiChainSequence, + providers::ProvidersManager, + runner::ScriptRunner, + sequence::{ScriptSequence, ScriptSequenceKind}, + transaction::TransactionWithMetadata, + ScriptArgs, ScriptConfig, }; use alloy_primitives::{utils::format_units, Address, U256}; use ethers_core::types::transaction::eip2718::TypedTransaction; use ethers_providers::{JsonRpcClient, Middleware, Provider}; use eyre::{Context, Result}; -use forge::{inspectors::cheatcodes::BroadcastableTransactions, traces::render_trace_arena}; +use forge::{ + inspectors::cheatcodes::{BroadcastableTransactions, ScriptWallets}, + traces::render_trace_arena, +}; use foundry_cli::utils::has_different_gas_calc; use foundry_common::{ - get_contract_name, provider::alloy::RpcUrl, shell, types::ToAlloy, ContractsByArtifact, + get_contract_name, provider::ethers::RpcUrl, shell, types::ToAlloy, ContractsByArtifact, }; use futures::future::join_all; use parking_lot::RwLock; @@ -38,6 +49,7 @@ impl PreSimulationState { Ok(FilledTransactionsState { args: self.args, script_config: self.script_config, + script_wallets: self.script_wallets, build_data: self.build_data, execution_data: self.execution_data, execution_artifacts: self.execution_artifacts, @@ -195,7 +207,7 @@ impl PreSimulationState { .map(|rpc| async { let mut script_config = self.script_config.clone(); script_config.evm_opts.fork_url = Some(rpc.clone()); - let runner = script_config.get_runner(false).await?; + let runner = script_config.get_runner().await?; Ok((rpc.clone(), runner)) }) .collect::>(); @@ -221,6 +233,7 @@ impl PreSimulationState { pub struct FilledTransactionsState { pub args: ScriptArgs, pub script_config: ScriptConfig, + pub script_wallets: ScriptWallets, pub build_data: LinkedBuildData, pub execution_data: ExecutionData, pub execution_artifacts: ExecutionArtifacts, @@ -361,6 +374,7 @@ impl FilledTransactionsState { Ok(BundledState { args: self.args, script_config: self.script_config, + script_wallets: self.script_wallets, build_data: self.build_data, execution_data: self.execution_data, execution_artifacts: self.execution_artifacts, @@ -391,14 +405,9 @@ impl FilledTransactionsState { pub struct BundledState { pub args: ScriptArgs, pub script_config: ScriptConfig, + pub script_wallets: ScriptWallets, pub build_data: LinkedBuildData, pub execution_data: ExecutionData, pub execution_artifacts: ExecutionArtifacts, pub sequence: ScriptSequenceKind, } - -impl BundledState { - pub async fn wait_for_pending(self) -> Result<()> { - Ok(()) - } -} diff --git a/crates/forge/tests/cli/multi_script.rs b/crates/forge/tests/cli/multi_script.rs index 6565f0fa2284..d6f7628da169 100644 --- a/crates/forge/tests/cli/multi_script.rs +++ b/crates/forge/tests/cli/multi_script.rs @@ -58,7 +58,7 @@ forgetest_async!(can_resume_multi_chain_script, |prj, cmd| { tester .add_sig("MultiChainBroadcastNoLink", "deploy(string memory,string memory)") .args(&[&handle1.http_endpoint(), &handle2.http_endpoint()]) - .broadcast(ScriptOutcome::WarnSpecifyDeployer) + .broadcast(ScriptOutcome::MissingWallet) .load_private_keys(&[0, 1]) .await .arg("--multi") From 908b0c591d0b3af7df89c16ba48636f2f0fff23d Mon Sep 17 00:00:00 2001 From: Arsenii Kulikov Date: Sat, 2 Mar 2024 01:46:20 +0400 Subject: [PATCH 09/33] wip: refactor verification --- crates/forge/bin/cmd/script/broadcast.rs | 7 ++++ crates/forge/bin/cmd/script/cmd.rs | 24 +++++++------- crates/forge/bin/cmd/script/resume.rs | 8 ++++- crates/forge/bin/cmd/script/sequence.rs | 14 -------- crates/forge/bin/cmd/script/verify.rs | 42 +++++++++++++++++++++++- 5 files changed, 67 insertions(+), 28 deletions(-) diff --git a/crates/forge/bin/cmd/script/broadcast.rs b/crates/forge/bin/cmd/script/broadcast.rs index 18ff94a339b2..bfe263d9131d 100644 --- a/crates/forge/bin/cmd/script/broadcast.rs +++ b/crates/forge/bin/cmd/script/broadcast.rs @@ -23,6 +23,7 @@ use foundry_common::{ shell, types::{ToAlloy, ToEthers}, }; +use foundry_config::Config; use foundry_wallets::WalletSigner; use futures::{future::join_all, StreamExt}; use std::{ @@ -183,6 +184,12 @@ impl BundledState { }) .collect::>(); + if required_addresses.contains(&Config::DEFAULT_SENDER) { + eyre::bail!( + "You seem to be using Foundry's default sender. Be sure to set your own --sender." + ); + } + let send_kind = if self.args.unlocked { SendTransactionsKind::Unlocked(required_addresses) } else { diff --git a/crates/forge/bin/cmd/script/cmd.rs b/crates/forge/bin/cmd/script/cmd.rs index c4de098083bf..2b0b5f47cc25 100644 --- a/crates/forge/bin/cmd/script/cmd.rs +++ b/crates/forge/bin/cmd/script/cmd.rs @@ -29,10 +29,11 @@ impl ScriptArgs { } /// Executes the script - pub async fn run_script(mut self) -> Result<()> { + pub async fn run_script(self) -> Result<()> { trace!(target: "script", "executing script command"); - let state = self.preprocess() + let state = self + .preprocess() .await? .compile()? .link()? @@ -47,14 +48,6 @@ impl ScriptArgs { state.run_debugger()?; } - let mut verify = VerifyBundle::new( - &state.script_config.config.project()?, - &state.script_config.config, - state.build_data.get_flattened_contracts(false), - state.args.retry, - state.args.verifier.clone(), - ); - let state = if state.args.resume || (state.args.verify && !state.args.broadcast) { state.resume().await? } else { @@ -63,7 +56,6 @@ impl ScriptArgs { } else { state.show_traces().await?; } - verify.known_contracts = state.build_data.get_flattened_contracts(false); state.args.check_contract_sizes( &state.execution_result, &state.build_data.highlevel_known_contracts, @@ -83,13 +75,21 @@ impl ScriptArgs { state.bundle().await? }; - if !state.args.broadcast { + if !state.args.broadcast && !state.args.resume { shell::println("\nSIMULATION COMPLETE. To broadcast these transactions, add --broadcast and wallet configuration(s) to the previous command. See forge script --help for more.")?; return Ok(()); } + if state.args.verify { + state.verify_preflight_check()?; + } + let state = state.wait_for_pending().await?.broadcast().await?; + if state.args.verify { + state.verify().await?; + } + Ok(()) } diff --git a/crates/forge/bin/cmd/script/resume.rs b/crates/forge/bin/cmd/script/resume.rs index 6e674a3b4d4b..1a22ad08a306 100644 --- a/crates/forge/bin/cmd/script/resume.rs +++ b/crates/forge/bin/cmd/script/resume.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use ethers_providers::Middleware; use eyre::{OptionExt, Result}; use foundry_common::{provider::ethers::try_get_http_provider, shell}; +use foundry_compilers::artifacts::Libraries; use super::{ execute::PreSimulationState, @@ -18,7 +19,7 @@ impl PreSimulationState { args, script_config, script_wallets, - build_data, + mut build_data, execution_data, execution_result: _, execution_artifacts, @@ -62,6 +63,11 @@ impl PreSimulationState { } }; + // We might have predeployed libraries from the broadcasting, so we need to + // relink the contracts with them, since their mapping is + // not included in the solc cache files. + build_data = build_data.build_data.link_with_libraries(Libraries::parse(&seq.libraries)?)?; + ScriptSequenceKind::Single(seq) }; diff --git a/crates/forge/bin/cmd/script/sequence.rs b/crates/forge/bin/cmd/script/sequence.rs index ded98924f6a9..add94c02a373 100644 --- a/crates/forge/bin/cmd/script/sequence.rs +++ b/crates/forge/bin/cmd/script/sequence.rs @@ -286,20 +286,6 @@ impl ScriptSequence { Ok((broadcast, cache)) } - /// Checks that there is an Etherscan key for the chain id of this sequence. - pub fn verify_preflight_check(&self, config: &Config, verify: &VerifyBundle) -> Result<()> { - if config.get_etherscan_api_key(Some(self.chain.into())).is_none() && - verify.verifier.verifier == VerificationProviderType::Etherscan - { - eyre::bail!( - "Etherscan API key wasn't found for chain id {}. On-chain execution aborted", - self.chain - ) - } - - Ok(()) - } - /// Given the broadcast log, it matches transactions with receipts, and tries to verify any /// created contract on etherscan. pub async fn verify_contracts( diff --git a/crates/forge/bin/cmd/script/verify.rs b/crates/forge/bin/cmd/script/verify.rs index 43293268d5c1..6e3daf158840 100644 --- a/crates/forge/bin/cmd/script/verify.rs +++ b/crates/forge/bin/cmd/script/verify.rs @@ -1,11 +1,51 @@ use alloy_primitives::Address; -use forge_verify::{RetryArgs, VerifierArgs, VerifyArgs}; +use eyre::Result; +use forge_verify::{provider::VerificationProviderType, RetryArgs, VerifierArgs, VerifyArgs}; use foundry_cli::opts::{EtherscanOpts, ProjectPathsArgs}; use foundry_common::ContractsByArtifact; use foundry_compilers::{info::ContractInfo, Project}; use foundry_config::{Chain, Config}; use semver::Version; +use super::{broadcast::BroadcastedState, simulate::BundledState}; + +impl BundledState { + pub fn verify_preflight_check(&self) -> Result<()> { + for sequence in self.sequence.iter_sequences() { + if self.args.verifier.verifier == VerificationProviderType::Etherscan && + self.script_config + .config + .get_etherscan_api_key(Some(sequence.chain.into())) + .is_none() + { + eyre::bail!("Missing etherscan key for chain {}", sequence.chain); + } + } + + Ok(()) + } +} + +impl BroadcastedState { + pub async fn verify(self) -> Result<()> { + let Self { args, script_config, build_data, mut sequence, .. } = self; + + let verify = VerifyBundle::new( + &script_config.config.project()?, + &script_config.config, + build_data.get_flattened_contracts(false), + args.retry, + args.verifier, + ); + + for sequence in sequence.iter_sequeneces_mut() { + sequence.verify_contracts(&script_config.config, verify.clone()).await?; + } + + Ok(()) + } +} + /// Data struct to help `ScriptSequence` verify contracts on `etherscan`. #[derive(Clone)] pub struct VerifyBundle { From 580234a3745ded4cb0666df3c4fda5cac63ab652 Mon Sep 17 00:00:00 2001 From: Arsenii Kulikov Date: Sat, 2 Mar 2024 02:51:30 +0400 Subject: [PATCH 10/33] wip: cleaning up --- crates/forge/bin/cmd/script/broadcast.rs | 16 ++- crates/forge/bin/cmd/script/cmd.rs | 112 +----------------- crates/forge/bin/cmd/script/execute.rs | 13 +- crates/forge/bin/cmd/script/mod.rs | 16 +-- crates/forge/bin/cmd/script/multi_sequence.rs | 6 +- crates/forge/bin/cmd/script/resume.rs | 5 +- crates/forge/bin/cmd/script/sequence.rs | 23 ++-- crates/forge/bin/cmd/script/simulate.rs | 13 +- crates/forge/bin/cmd/script/transaction.rs | 11 +- 9 files changed, 56 insertions(+), 159 deletions(-) diff --git a/crates/forge/bin/cmd/script/broadcast.rs b/crates/forge/bin/cmd/script/broadcast.rs index bfe263d9131d..8c4b5856ac41 100644 --- a/crates/forge/bin/cmd/script/broadcast.rs +++ b/crates/forge/bin/cmd/script/broadcast.rs @@ -1,10 +1,9 @@ use super::{ build::LinkedBuildData, execute::{ExecutionArtifacts, ExecutionData}, - receipts::{self, clear_pendings}, - sequence::{ScriptSequence, ScriptSequenceKind}, + receipts, + sequence::ScriptSequenceKind, simulate::BundledState, - verify::VerifyBundle, ScriptArgs, ScriptConfig, }; use alloy_primitives::{utils::format_units, Address, TxHash, U256}; @@ -166,6 +165,8 @@ impl BundledState { let errors = join_all(futs).await.into_iter().filter(|res| res.is_err()).collect::>(); + self.sequence.save(true)?; + if !errors.is_empty() { return Err(eyre::eyre!("{errors:?}")); } @@ -293,7 +294,7 @@ impl BundledState { // We send transactions and wait for receipts in batches of 100, since some networks // cannot handle more than that. let batch_size = if sequential_broadcast { 1 } else { 100 }; - let mut index = 0; + let mut index = already_broadcasted; for (batch_number, batch) in transactions.chunks(batch_size).map(|f| f.to_vec()).enumerate() @@ -322,10 +323,13 @@ impl BundledState { let mut buffer = futures::stream::iter(pending_transactions).buffered(7); while let Some(tx_hash) = buffer.next().await { - let tx_hash = tx_hash?; + let tx_hash = tx_hash.wrap_err("Failed to send transaction")?; sequence.add_pending(index, tx_hash); - update_progress!(pb, (index + already_broadcasted)); + // Checkpoint save + sequence.save(true)?; + + update_progress!(pb, index - already_broadcasted); index += 1; } diff --git a/crates/forge/bin/cmd/script/cmd.rs b/crates/forge/bin/cmd/script/cmd.rs index 2b0b5f47cc25..936e5fd5b054 100644 --- a/crates/forge/bin/cmd/script/cmd.rs +++ b/crates/forge/bin/cmd/script/cmd.rs @@ -1,16 +1,11 @@ -use super::{verify::VerifyBundle, ScriptArgs, ScriptConfig}; -use crate::cmd::script::{ - build::{LinkedBuildData, PreprocessedState}, - receipts, -}; +use super::{ScriptArgs, ScriptConfig}; +use crate::cmd::script::build::PreprocessedState; use alloy_primitives::Address; use ethers_signers::Signer; use eyre::Result; use forge::inspectors::cheatcodes::ScriptWallets; use foundry_cli::utils::LoadConfig; -use foundry_common::{provider::ethers::try_get_http_provider, shell, types::ToAlloy}; -use foundry_compilers::artifacts::Libraries; -use std::sync::Arc; +use foundry_common::{shell, types::ToAlloy}; impl ScriptArgs { async fn preprocess(self) -> Result { @@ -93,107 +88,6 @@ impl ScriptArgs { Ok(()) } - /*/// Resumes the deployment and/or verification of the script. - async fn resume_deployment( - &mut self, - script_config: ScriptConfig, - build_data: LinkedBuildData, - verify: VerifyBundle, - ) -> Result<()> { - if self.multi { - return self - .multi_chain_deployment( - &mut MultiChainSequence::load( - &script_config.config, - &self.sig, - &build_data.build_data.target, - )?, - build_data.libraries, - &script_config.config, - verify, - &script_config.script_wallets.into_multi_wallet().into_signers()?, - ) - .await; - } - self.resume_single_deployment( - script_config, - build_data, - verify, - ) - .await - .map_err(|err| { - eyre::eyre!("{err}\n\nIf you were trying to resume or verify a multi chain deployment, add `--multi` to your command invocation.") - }) - }*/ - - /*/// Resumes the deployment and/or verification of a single RPC script. - async fn resume_single_deployment( - &mut self, - script_config: ScriptConfig, - build_data: LinkedBuildData, - mut verify: VerifyBundle, - ) -> Result<()> { - trace!(target: "script", "resuming single deployment"); - - let fork_url = script_config - .evm_opts - .fork_url - .as_deref() - .ok_or_else(|| eyre::eyre!("Missing `--fork-url` field."))?; - let provider = Arc::new(try_get_http_provider(fork_url)?); - - let chain = provider.get_chainid().await?.as_u64(); - verify.set_chain(&script_config.config, chain.into()); - - let broadcasted = self.broadcast || self.resume; - let mut deployment_sequence = match ScriptSequence::load( - &script_config.config, - &self.sig, - &build_data.build_data.target, - chain, - broadcasted, - ) { - Ok(seq) => seq, - // If the script was simulated, but there was no attempt to broadcast yet, - // try to read the script sequence from the `dry-run/` folder - Err(_) if broadcasted => ScriptSequence::load( - &script_config.config, - &self.sig, - &build_data.build_data.target, - chain, - false, - )?, - Err(err) => eyre::bail!(err), - }; - - if self.verify { - deployment_sequence.verify_preflight_check(&script_config.config, &verify)?; - } - - receipts::wait_for_pending(provider, &mut deployment_sequence).await?; - - let signers = script_config.script_wallets.into_multi_wallet().into_signers()?; - - if self.resume { - self.send_transactions(&mut deployment_sequence, fork_url, &signers).await?; - } - - if self.verify { - let libraries = Libraries::parse(&deployment_sequence.libraries)? - .with_stripped_file_prefixes(build_data.build_data.linker.root.as_path()); - // We might have predeployed libraries from the broadcasting, so we need to - // relink the contracts with them, since their mapping is - // not included in the solc cache files. - let build_data = build_data.build_data.link_with_libraries(libraries)?; - - verify.known_contracts = build_data.get_flattened_contracts(false); - - deployment_sequence.verify_contracts(&script_config.config, verify).await?; - } - - Ok(()) - }*/ - /// In case the user has loaded *only* one private-key, we can assume that he's using it as the /// `--sender` fn maybe_load_private_key(&self) -> Result> { diff --git a/crates/forge/bin/cmd/script/execute.rs b/crates/forge/bin/cmd/script/execute.rs index 1438567ccc20..7e6e8d52d1e4 100644 --- a/crates/forge/bin/cmd/script/execute.rs +++ b/crates/forge/bin/cmd/script/execute.rs @@ -1,7 +1,6 @@ use super::{ build::{CompiledState, LinkedBuildData, LinkedState}, runner::ScriptRunner, - simulate::BundledState, JsonResult, NestedValue, ScriptArgs, ScriptConfig, ScriptResult, }; use alloy_dyn_abi::FunctionExt; @@ -81,8 +80,10 @@ pub struct ExecutedState { impl PreExecutionState { #[async_recursion] pub async fn execute(mut self) -> Result { - let mut runner = - self.script_config.get_runner_with_cheatcodes(self.script_wallets.clone()).await?; + let mut runner = self + .script_config + .get_runner_with_cheatcodes(self.script_wallets.clone(), self.args.debug) + .await?; let mut result = self.execute_with_runner(&mut runner).await?; // If we have a new sender from execution, we need to use it to deploy libraries and relink @@ -215,12 +216,6 @@ impl ExecutedState { self.script_config.collect_rpcs(txs); } - if self.execution_result.transactions.as_ref().map_or(true, |txs| txs.is_empty()) && - self.args.broadcast - { - eyre::bail!("No onchain transactions generated in script"); - } - self.script_config.check_multi_chain_constraints(&self.build_data.libraries)?; self.script_config.check_shanghai_support().await?; diff --git a/crates/forge/bin/cmd/script/mod.rs b/crates/forge/bin/cmd/script/mod.rs index 8a71ce54be52..fc8b19fb1b7a 100644 --- a/crates/forge/bin/cmd/script/mod.rs +++ b/crates/forge/bin/cmd/script/mod.rs @@ -402,8 +402,6 @@ pub struct ScriptConfig { pub total_rpcs: HashSet, /// If true, one of the transactions did not have a rpc pub missing_rpc: bool, - /// Should return some debug information - pub debug: bool, } impl ScriptConfig { @@ -421,7 +419,6 @@ impl ScriptConfig { backends: HashMap::new(), total_rpcs: HashSet::new(), missing_rpc: false, - debug: false, }) } @@ -499,17 +496,22 @@ For more information, please see https://eips.ethereum.org/EIPS/eip-3855", } async fn get_runner(&mut self) -> Result { - self._get_runner(None).await + self._get_runner(None, false).await } async fn get_runner_with_cheatcodes( &mut self, script_wallets: ScriptWallets, + debug: bool, ) -> Result { - self._get_runner(Some(script_wallets)).await + self._get_runner(Some(script_wallets), debug).await } - async fn _get_runner(&mut self, script_wallets: Option) -> Result { + async fn _get_runner( + &mut self, + script_wallets: Option, + debug: bool, + ) -> Result { trace!("preparing script runner"); let env = self.evm_opts.evm_env().await?; @@ -539,7 +541,7 @@ For more information, please see https://eips.ethereum.org/EIPS/eip-3855", if let Some(script_wallets) = script_wallets { builder = builder.inspectors(|stack| { stack - .debug(self.debug) + .debug(debug) .cheatcodes( CheatsConfig::new( &self.config, diff --git a/crates/forge/bin/cmd/script/multi_sequence.rs b/crates/forge/bin/cmd/script/multi_sequence.rs index 7a0e925f587b..0286179a43e5 100644 --- a/crates/forge/bin/cmd/script/multi_sequence.rs +++ b/crates/forge/bin/cmd/script/multi_sequence.rs @@ -151,8 +151,10 @@ impl MultiChainSequence { fs::create_dir_all(file.parent().unwrap())?; fs::copy(&self.sensitive_path, &file)?; - println!("\nTransactions saved to: {}\n", self.path.display()); - println!("Sensitive details saved to: {}\n", self.sensitive_path.display()); + if !silent { + println!("\nTransactions saved to: {}\n", self.path.display()); + println!("Sensitive details saved to: {}\n", self.sensitive_path.display()); + } Ok(()) } diff --git a/crates/forge/bin/cmd/script/resume.rs b/crates/forge/bin/cmd/script/resume.rs index 1a22ad08a306..f3bdac2f240f 100644 --- a/crates/forge/bin/cmd/script/resume.rs +++ b/crates/forge/bin/cmd/script/resume.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use ethers_providers::Middleware; use eyre::{OptionExt, Result}; -use foundry_common::{provider::ethers::try_get_http_provider, shell}; +use foundry_common::provider::ethers::try_get_http_provider; use foundry_compilers::artifacts::Libraries; use super::{ @@ -66,7 +66,8 @@ impl PreSimulationState { // We might have predeployed libraries from the broadcasting, so we need to // relink the contracts with them, since their mapping is // not included in the solc cache files. - build_data = build_data.build_data.link_with_libraries(Libraries::parse(&seq.libraries)?)?; + build_data = + build_data.build_data.link_with_libraries(Libraries::parse(&seq.libraries)?)?; ScriptSequenceKind::Single(seq) }; diff --git a/crates/forge/bin/cmd/script/sequence.rs b/crates/forge/bin/cmd/script/sequence.rs index add94c02a373..dae33eeec6ff 100644 --- a/crates/forge/bin/cmd/script/sequence.rs +++ b/crates/forge/bin/cmd/script/sequence.rs @@ -118,6 +118,7 @@ impl ScriptSequence { config: &Config, broadcasted: bool, is_multi: bool, + libraries: Libraries, ) -> Result { let (path, sensitive_path) = ScriptSequence::get_paths( &config.broadcast, @@ -130,6 +131,15 @@ impl ScriptSequence { let commit = get_commit_hash(&config.__root.0); + let libraries = libraries + .libs + .iter() + .flat_map(|(file, libs)| { + libs.iter() + .map(|(name, address)| format!("{}:{name}:{address}", file.to_string_lossy())) + }) + .collect(); + Ok(ScriptSequence { transactions, returns, @@ -138,7 +148,7 @@ impl ScriptSequence { path, sensitive_path, timestamp: now().as_secs(), - libraries: vec![], + libraries, chain, commit, multi: is_multi, @@ -238,17 +248,6 @@ impl ScriptSequence { self.pending.retain(|element| element != &tx_hash); } - pub fn add_libraries(&mut self, libraries: Libraries) { - self.libraries = libraries - .libs - .iter() - .flat_map(|(file, libs)| { - libs.iter() - .map(|(name, address)| format!("{}:{name}:{address}", file.to_string_lossy())) - }) - .collect(); - } - /// Gets paths in the formats /// ./broadcast/[contract_filename]/[chain_id]/[sig]-[timestamp].json and /// ./cache/[contract_filename]/[chain_id]/[sig]-[timestamp].json diff --git a/crates/forge/bin/cmd/script/simulate.rs b/crates/forge/bin/cmd/script/simulate.rs index 99572401c342..8facd04f8469 100644 --- a/crates/forge/bin/cmd/script/simulate.rs +++ b/crates/forge/bin/cmd/script/simulate.rs @@ -12,7 +12,7 @@ use super::{ use alloy_primitives::{utils::format_units, Address, U256}; use ethers_core::types::transaction::eip2718::TypedTransaction; use ethers_providers::{JsonRpcClient, Middleware, Provider}; -use eyre::{Context, Result}; +use eyre::{Context, OptionExt, Result}; use forge::{ inspectors::cheatcodes::{BroadcastableTransactions, ScriptWallets}, traces::render_trace_arena, @@ -248,9 +248,17 @@ impl FilledTransactionsState { /// Each transaction will be added with the correct transaction type and gas estimation. pub async fn bundle(self) -> Result { // User might be using both "in-code" forks and `--fork-url`. - let last_rpc = &self.transactions.back().expect("exists; qed").rpc; + let last_rpc = &self + .transactions + .back() + .ok_or_eyre("No onchain transactions generated in script")? + .rpc; let is_multi_deployment = self.transactions.iter().any(|tx| &tx.rpc != last_rpc); + if is_multi_deployment && !self.build_data.libraries.is_empty() { + eyre::bail!("Multi-chain deployment is not supported with libraries."); + } + let mut total_gas_per_rpc: HashMap = HashMap::new(); // Batches sequence of transactions from different rpcs. @@ -318,6 +326,7 @@ impl FilledTransactionsState { &self.script_config.config, self.args.broadcast, is_multi_deployment, + self.build_data.libraries.clone(), )?; sequences.push(sequence); diff --git a/crates/forge/bin/cmd/script/transaction.rs b/crates/forge/bin/cmd/script/transaction.rs index 77ef00a741a8..3f92e2f31be2 100644 --- a/crates/forge/bin/cmd/script/transaction.rs +++ b/crates/forge/bin/cmd/script/transaction.rs @@ -195,6 +195,7 @@ impl TransactionWithMetadata { decoder: &CallTraceDecoder, ) -> Result<()> { self.opcode = CallKind::Call; + self.contract_address = Some(target); let Some(data) = self.transaction.data() else { return Ok(()) }; if data.len() < SELECTOR_LEN { @@ -211,10 +212,6 @@ impl TransactionWithMetadata { decoder.functions.get(selector).and_then(|v| v.first()) }; if let Some(function) = function { - if self.contract_address.is_none() { - self.contract_name = decoder.contracts.get(&target).cloned(); - } - self.function = Some(function.signature()); let values = function.abi_decode_input(data, false).map_err(|e| { @@ -229,15 +226,9 @@ impl TransactionWithMetadata { self.arguments = Some(values.iter().map(format_token_raw).collect()); } - self.contract_address = Some(target); - Ok(()) } - pub fn set_tx(&mut self, tx: TypedTransaction) { - self.transaction = tx; - } - pub fn change_type(&mut self, is_legacy: bool) { self.transaction = if is_legacy { TypedTransaction::Legacy(self.transaction.clone().into()) From 8f0032858996ef199e6b9902f3409e5161a4de2c Mon Sep 17 00:00:00 2001 From: Arsenii Kulikov Date: Sat, 2 Mar 2024 04:12:16 +0400 Subject: [PATCH 11/33] naming --- crates/forge/bin/cmd/script/cmd.rs | 42 ++++++++++++++++-------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/crates/forge/bin/cmd/script/cmd.rs b/crates/forge/bin/cmd/script/cmd.rs index 936e5fd5b054..d5f13746c1ba 100644 --- a/crates/forge/bin/cmd/script/cmd.rs +++ b/crates/forge/bin/cmd/script/cmd.rs @@ -27,7 +27,7 @@ impl ScriptArgs { pub async fn run_script(self) -> Result<()> { trace!(target: "script", "executing script command"); - let state = self + let pre_simulation = self .preprocess() .await? .compile()? @@ -39,29 +39,33 @@ impl ScriptArgs { .prepare_simulation() .await?; - if state.args.debug { - state.run_debugger()?; + if pre_simulation.args.debug { + pre_simulation.run_debugger()?; } - let state = if state.args.resume || (state.args.verify && !state.args.broadcast) { - state.resume().await? + // Move from `PreSimulationState` to `BundledState` either by resuming or simulating + // transactions. + let bundled = if pre_simulation.args.resume || + (pre_simulation.args.verify && !pre_simulation.args.broadcast) + { + pre_simulation.resume().await? } else { - if state.args.json { - state.show_json()?; + if pre_simulation.args.json { + pre_simulation.show_json()?; } else { - state.show_traces().await?; + pre_simulation.show_traces().await?; } - state.args.check_contract_sizes( - &state.execution_result, - &state.build_data.highlevel_known_contracts, + pre_simulation.args.check_contract_sizes( + &pre_simulation.execution_result, + &pre_simulation.build_data.highlevel_known_contracts, )?; - if state.script_config.missing_rpc { + if pre_simulation.script_config.missing_rpc { shell::println("\nIf you wish to simulate on-chain transactions pass a RPC URL.")?; return Ok(()); } - let state = state.fill_metadata().await?; + let state = pre_simulation.fill_metadata().await?; if state.transactions.is_empty() { return Ok(()); @@ -70,19 +74,19 @@ impl ScriptArgs { state.bundle().await? }; - if !state.args.broadcast && !state.args.resume { + if !bundled.args.broadcast && !bundled.args.resume { shell::println("\nSIMULATION COMPLETE. To broadcast these transactions, add --broadcast and wallet configuration(s) to the previous command. See forge script --help for more.")?; return Ok(()); } - if state.args.verify { - state.verify_preflight_check()?; + if bundled.args.verify { + bundled.verify_preflight_check()?; } - let state = state.wait_for_pending().await?.broadcast().await?; + let broadcasted = bundled.wait_for_pending().await?.broadcast().await?; - if state.args.verify { - state.verify().await?; + if broadcasted.args.verify { + broadcasted.verify().await?; } Ok(()) From 18a1ace4e6add2a64d4d96ba87f2bd950bbc42da Mon Sep 17 00:00:00 2001 From: Arsenii Kulikov Date: Sat, 2 Mar 2024 15:06:13 +0400 Subject: [PATCH 12/33] wip: clean up --- crates/forge/bin/cmd/script/broadcast.rs | 13 ++++---- crates/forge/bin/cmd/script/cmd.rs | 11 ++++--- crates/forge/bin/cmd/script/execute.rs | 42 ++++++++++-------------- crates/forge/bin/cmd/script/simulate.rs | 29 +++++----------- 4 files changed, 39 insertions(+), 56 deletions(-) diff --git a/crates/forge/bin/cmd/script/broadcast.rs b/crates/forge/bin/cmd/script/broadcast.rs index 8c4b5856ac41..ec805d928d13 100644 --- a/crates/forge/bin/cmd/script/broadcast.rs +++ b/crates/forge/bin/cmd/script/broadcast.rs @@ -30,7 +30,7 @@ use std::{ sync::Arc, }; -async fn estimate_gas( +pub async fn estimate_gas( tx: &mut TypedTransaction, provider: &Provider, estimate_multiplier: u64, @@ -219,20 +219,19 @@ impl BundledState { let already_broadcasted = sequence.receipts.len(); if already_broadcasted < sequence.transactions.len() { - let chain = provider.get_chainid().await?.as_u64(); - // We only wait for a transaction receipt before sending the next transaction, if // there is more than one signer. There would be no way of assuring // their order otherwise. Or if the chain does not support batched // transactions (eg. Arbitrum). - let sequential_broadcast = - send_kind.signers_count() != 1 || self.args.slow || !has_batch_support(chain); + let sequential_broadcast = send_kind.signers_count() != 1 || + self.args.slow || + !has_batch_support(sequence.chain); // Make a one-time gas price estimation let (gas_price, eip1559_fees) = match self.args.with_gas_price { None => match sequence.transactions.front().unwrap().typed_tx() { TypedTransaction::Eip1559(_) => { - let mut fees = estimate_eip1559_fees(&provider, Some(chain)) + let mut fees = estimate_eip1559_fees(&provider, Some(sequence.chain)) .await .wrap_err("Failed to estimate EIP1559 fees. This chain might not support EIP1559, try adding --legacy to your command.")?; @@ -263,7 +262,7 @@ impl BundledState { let mut tx = tx.clone(); - tx.set_chain_id(chain); + tx.set_chain_id(sequence.chain); if let Some(gas_price) = gas_price { tx.set_gas_price(gas_price); diff --git a/crates/forge/bin/cmd/script/cmd.rs b/crates/forge/bin/cmd/script/cmd.rs index d5f13746c1ba..dc6c4e7060e6 100644 --- a/crates/forge/bin/cmd/script/cmd.rs +++ b/crates/forge/bin/cmd/script/cmd.rs @@ -43,6 +43,12 @@ impl ScriptArgs { pre_simulation.run_debugger()?; } + if pre_simulation.args.json { + pre_simulation.show_json()?; + } else { + pre_simulation.show_traces().await?; + } + // Move from `PreSimulationState` to `BundledState` either by resuming or simulating // transactions. let bundled = if pre_simulation.args.resume || @@ -50,11 +56,6 @@ impl ScriptArgs { { pre_simulation.resume().await? } else { - if pre_simulation.args.json { - pre_simulation.show_json()?; - } else { - pre_simulation.show_traces().await?; - } pre_simulation.args.check_contract_sizes( &pre_simulation.execution_result, &pre_simulation.build_data.highlevel_known_contracts, diff --git a/crates/forge/bin/cmd/script/execute.rs b/crates/forge/bin/cmd/script/execute.rs index 7e6e8d52d1e4..89e583da8d64 100644 --- a/crates/forge/bin/cmd/script/execute.rs +++ b/crates/forge/bin/cmd/script/execute.rs @@ -103,30 +103,24 @@ impl PreExecutionState { } // Add library deployment transactions to broadcastable transactions list. - if let Some(txs) = &mut result.transactions { - let mut library_txs = self - .build_data - .predeploy_libraries - .iter() - .enumerate() - .map(|(i, bytes)| BroadcastableTransaction { - rpc: self.script_config.evm_opts.fork_url.clone(), - transaction: TransactionRequest { - from: Some(self.script_config.evm_opts.sender), - input: Some(bytes.clone()).into(), - nonce: Some(U64::from(self.script_config.sender_nonce + i as u64)), - ..Default::default() - }, - }) - .collect::>(); - - for tx in txs.iter() { - library_txs.push_back(BroadcastableTransaction { - rpc: tx.rpc.clone(), - transaction: tx.transaction.clone(), - }); - } - *txs = library_txs; + if let Some(txs) = result.transactions.take() { + result.transactions = Some( + self.build_data + .predeploy_libraries + .iter() + .enumerate() + .map(|(i, bytes)| BroadcastableTransaction { + rpc: self.script_config.evm_opts.fork_url.clone(), + transaction: TransactionRequest { + from: Some(self.script_config.evm_opts.sender), + input: Some(bytes.clone()).into(), + nonce: Some(U64::from(self.script_config.sender_nonce + i as u64)), + ..Default::default() + }, + }) + .chain(txs) + .collect(), + ); } Ok(ExecutedState { diff --git a/crates/forge/bin/cmd/script/simulate.rs b/crates/forge/bin/cmd/script/simulate.rs index 8facd04f8469..8327dbc868ea 100644 --- a/crates/forge/bin/cmd/script/simulate.rs +++ b/crates/forge/bin/cmd/script/simulate.rs @@ -1,3 +1,5 @@ +use crate::cmd::script::broadcast::estimate_gas; + use super::{ artifacts::ArtifactInfo, build::LinkedBuildData, @@ -296,7 +298,13 @@ impl FilledTransactionsState { // for chains where `has_different_gas_calc` returns true, // we await each transaction before broadcasting the next // one. - if let Err(err) = self.estimate_gas(typed_tx, &provider_info.provider).await { + if let Err(err) = estimate_gas( + typed_tx, + &provider_info.provider, + self.args.gas_estimate_multiplier, + ) + .await + { trace!("gas estimation failed: {err}"); // Restore gas value, since `estimate_gas` will remove it. @@ -390,25 +398,6 @@ impl FilledTransactionsState { sequence, }) } - - async fn estimate_gas(&self, tx: &mut TypedTransaction, provider: &Provider) -> Result<()> - where - T: JsonRpcClient, - { - // if already set, some RPC endpoints might simply return the gas value that is already - // set in the request and omit the estimate altogether, so we remove it here - let _ = tx.gas_mut().take(); - - tx.set_gas( - provider - .estimate_gas(tx, None) - .await - .wrap_err_with(|| format!("Failed to estimate gas for tx: {:?}", tx.sighash()))? * - self.args.gas_estimate_multiplier / - 100, - ); - Ok(()) - } } pub struct BundledState { From e1fe09c80f4574c3201da4dccef3357c846cc5b3 Mon Sep 17 00:00:00 2001 From: Arsenii Kulikov Date: Sun, 3 Mar 2024 16:40:21 +0400 Subject: [PATCH 13/33] cleanup ScriptSequence --- crates/forge/bin/cmd/script/broadcast.rs | 28 +++---- crates/forge/bin/cmd/script/execute.rs | 2 +- crates/forge/bin/cmd/script/sequence.rs | 97 ++++++------------------ crates/forge/bin/cmd/script/simulate.rs | 62 +++++++++++---- 4 files changed, 87 insertions(+), 102 deletions(-) diff --git a/crates/forge/bin/cmd/script/broadcast.rs b/crates/forge/bin/cmd/script/broadcast.rs index ec805d928d13..6eae1ba15535 100644 --- a/crates/forge/bin/cmd/script/broadcast.rs +++ b/crates/forge/bin/cmd/script/broadcast.rs @@ -59,7 +59,7 @@ pub async fn send_transaction( kind: SendTransactionKind<'_>, sequential_broadcast: bool, is_fixed_gas_limit: bool, - skip_simulation: bool, + estimate_via_rpc: bool, estimate_multiplier: u64, ) -> Result { let from = tx.from().expect("no sender"); @@ -75,9 +75,7 @@ pub async fn send_transaction( // Chains which use `eth_estimateGas` are being sent sequentially and require their // gas to be re-estimated right before broadcasting. - if !is_fixed_gas_limit && - (has_different_gas_calc(provider.get_chainid().await?.as_u64()) || skip_simulation) - { + if !is_fixed_gas_limit && estimate_via_rpc { estimate_gas(&mut tx, &provider, estimate_multiplier).await?; } @@ -219,14 +217,6 @@ impl BundledState { let already_broadcasted = sequence.receipts.len(); if already_broadcasted < sequence.transactions.len() { - // We only wait for a transaction receipt before sending the next transaction, if - // there is more than one signer. There would be no way of assuring - // their order otherwise. Or if the chain does not support batched - // transactions (eg. Arbitrum). - let sequential_broadcast = send_kind.signers_count() != 1 || - self.args.slow || - !has_batch_support(sequence.chain); - // Make a one-time gas price estimation let (gas_price, eip1559_fees) = match self.args.with_gas_price { None => match sequence.transactions.front().unwrap().typed_tx() { @@ -288,6 +278,18 @@ impl BundledState { }) .collect::>>()?; + let estimate_via_rpc = + has_different_gas_calc(sequence.chain) || self.args.skip_simulation; + + // We only wait for a transaction receipt before sending the next transaction, if + // there is more than one signer. There would be no way of assuring + // their order otherwise. + // Or if the chain does not support batched transactions (eg. Arbitrum). + // Or if we need to invoke eth_estimateGas before sending transactions. + let sequential_broadcast = estimate_via_rpc || + send_kind.signers_count() != 1 || + !has_batch_support(sequence.chain); + let pb = init_progress!(transactions, "txes"); // We send transactions and wait for receipts in batches of 100, since some networks @@ -312,7 +314,7 @@ impl BundledState { kind, sequential_broadcast, is_fixed_gas_limit, - self.args.skip_simulation, + estimate_via_rpc, self.args.gas_estimate_multiplier, ); pending_transactions.push(tx_hash); diff --git a/crates/forge/bin/cmd/script/execute.rs b/crates/forge/bin/cmd/script/execute.rs index 89e583da8d64..5dc5bcc54616 100644 --- a/crates/forge/bin/cmd/script/execute.rs +++ b/crates/forge/bin/cmd/script/execute.rs @@ -25,7 +25,7 @@ use foundry_common::{ use foundry_compilers::artifacts::ContractBytecodeSome; use foundry_config::Config; use foundry_debugger::Debugger; -use std::collections::{HashMap, VecDeque}; +use std::collections::HashMap; use yansi::Paint; pub struct ExecutionData { diff --git a/crates/forge/bin/cmd/script/sequence.rs b/crates/forge/bin/cmd/script/sequence.rs index dae33eeec6ff..63a7886a0c83 100644 --- a/crates/forge/bin/cmd/script/sequence.rs +++ b/crates/forge/bin/cmd/script/sequence.rs @@ -16,13 +16,13 @@ use foundry_common::{ types::{ToAlloy, ToEthers}, SELECTOR_LEN, }; -use foundry_compilers::{artifacts::Libraries, ArtifactId}; +use foundry_compilers::ArtifactId; use foundry_config::Config; use serde::{Deserialize, Serialize}; use std::{ collections::{HashMap, VecDeque}, io::{BufWriter, Write}, - path::{Path, PathBuf}, + path::PathBuf, }; use yansi::Paint; @@ -72,14 +72,12 @@ pub struct ScriptSequence { pub libraries: Vec, pub pending: Vec, #[serde(skip)] - pub path: PathBuf, - #[serde(skip)] - pub sensitive_path: PathBuf, + /// Contains paths to the sequence files + /// None if sequence should not be saved to disk (e.g. part of a multi-chain sequence) + pub paths: Option<(PathBuf, PathBuf)>, pub returns: HashMap, pub timestamp: u64, pub chain: u64, - /// If `True`, the sequence belongs to a `MultiChainSequence` and won't save to disk as usual. - pub multi: bool, pub commit: Option, } @@ -108,53 +106,6 @@ impl From<&mut ScriptSequence> for SensitiveScriptSequence { } impl ScriptSequence { - #[allow(clippy::too_many_arguments)] - pub fn new( - transactions: VecDeque, - returns: HashMap, - sig: &str, - target: &ArtifactId, - chain: u64, - config: &Config, - broadcasted: bool, - is_multi: bool, - libraries: Libraries, - ) -> Result { - let (path, sensitive_path) = ScriptSequence::get_paths( - &config.broadcast, - &config.cache_path, - sig, - target, - chain, - broadcasted && !is_multi, - )?; - - let commit = get_commit_hash(&config.__root.0); - - let libraries = libraries - .libs - .iter() - .flat_map(|(file, libs)| { - libs.iter() - .map(|(name, address)| format!("{}:{name}:{address}", file.to_string_lossy())) - }) - .collect(); - - Ok(ScriptSequence { - transactions, - returns, - receipts: vec![], - pending: vec![], - path, - sensitive_path, - timestamp: now().as_secs(), - libraries, - chain, - commit, - multi: is_multi, - }) - } - /// Loads The sequence for the corresponding json file pub fn load( config: &Config, @@ -163,14 +114,8 @@ impl ScriptSequence { chain_id: u64, broadcasted: bool, ) -> Result { - let (path, sensitive_path) = ScriptSequence::get_paths( - &config.broadcast, - &config.cache_path, - sig, - target, - chain_id, - broadcasted, - )?; + let (path, sensitive_path) = + ScriptSequence::get_paths(config, sig, target, chain_id, broadcasted)?; let mut script_sequence: Self = foundry_compilers::utils::read_json_file(&path) .wrap_err(format!("Deployment not found for chain `{chain_id}`."))?; @@ -182,8 +127,7 @@ impl ScriptSequence { script_sequence.fill_sensitive(&sensitive_script_sequence); - script_sequence.path = path; - script_sequence.sensitive_path = sensitive_path; + script_sequence.paths = Some((path, sensitive_path)); Ok(script_sequence) } @@ -192,10 +136,14 @@ impl ScriptSequence { pub fn save(&mut self, silent: bool) -> Result<()> { self.sort_receipts(); - if self.multi || self.transactions.is_empty() { + if self.transactions.is_empty() { return Ok(()) } + let Some((path, sensitive_path)) = self.paths.clone() else { + return Ok(()) + }; + self.timestamp = now().as_secs(); let ts_name = format!("run-{}.json", self.timestamp); @@ -203,25 +151,25 @@ impl ScriptSequence { // broadcast folder writes //../run-latest.json - let mut writer = BufWriter::new(fs::create_file(&self.path)?); + let mut writer = BufWriter::new(fs::create_file(&path)?); serde_json::to_writer_pretty(&mut writer, &self)?; writer.flush()?; //../run-[timestamp].json - fs::copy(&self.path, self.path.with_file_name(&ts_name))?; + fs::copy(&path, path.with_file_name(&ts_name))?; // cache folder writes //../run-latest.json - let mut writer = BufWriter::new(fs::create_file(&self.sensitive_path)?); + let mut writer = BufWriter::new(fs::create_file(&sensitive_path)?); serde_json::to_writer_pretty(&mut writer, &sensitive_script_sequence)?; writer.flush()?; //../run-[timestamp].json - fs::copy(&self.sensitive_path, self.sensitive_path.with_file_name(&ts_name))?; + fs::copy(&sensitive_path, sensitive_path.with_file_name(&ts_name))?; if !silent { - shell::println(format!("\nTransactions saved to: {}\n", self.path.display()))?; + shell::println(format!("\nTransactions saved to: {}\n", path.display()))?; shell::println(format!( "Sensitive values saved to: {}\n", - self.sensitive_path.display() + sensitive_path.display() ))?; } @@ -252,15 +200,14 @@ impl ScriptSequence { /// ./broadcast/[contract_filename]/[chain_id]/[sig]-[timestamp].json and /// ./cache/[contract_filename]/[chain_id]/[sig]-[timestamp].json pub fn get_paths( - broadcast: &Path, - cache: &Path, + config: &Config, sig: &str, target: &ArtifactId, chain_id: u64, broadcasted: bool, ) -> Result<(PathBuf, PathBuf)> { - let mut broadcast = broadcast.to_path_buf(); - let mut cache = cache.to_path_buf(); + let mut broadcast = config.broadcast.to_path_buf(); + let mut cache = config.cache_path.to_path_buf(); let mut common = PathBuf::new(); let target_fname = target.source.file_name().wrap_err("No filename.")?; diff --git a/crates/forge/bin/cmd/script/simulate.rs b/crates/forge/bin/cmd/script/simulate.rs index 8327dbc868ea..11f46e28ca54 100644 --- a/crates/forge/bin/cmd/script/simulate.rs +++ b/crates/forge/bin/cmd/script/simulate.rs @@ -1,4 +1,4 @@ -use crate::cmd::script::broadcast::estimate_gas; +use crate::cmd::{init::get_commit_hash, script::broadcast::estimate_gas}; use super::{ artifacts::ArtifactInfo, @@ -12,14 +12,12 @@ use super::{ ScriptArgs, ScriptConfig, }; use alloy_primitives::{utils::format_units, Address, U256}; -use ethers_core::types::transaction::eip2718::TypedTransaction; -use ethers_providers::{JsonRpcClient, Middleware, Provider}; use eyre::{Context, OptionExt, Result}; use forge::{ inspectors::cheatcodes::{BroadcastableTransactions, ScriptWallets}, traces::render_trace_arena, }; -use foundry_cli::utils::has_different_gas_calc; +use foundry_cli::utils::{has_different_gas_calc, now}; use foundry_common::{ get_contract_name, provider::ethers::RpcUrl, shell, types::ToAlloy, ContractsByArtifact, }; @@ -325,16 +323,10 @@ impl FilledTransactionsState { } } - let sequence = ScriptSequence::new( - new_sequence, - self.execution_artifacts.returns.clone(), - &self.args.sig, - &self.build_data.build_data.target, - provider_info.chain, - &self.script_config.config, - self.args.broadcast, + let sequence = self.create_sequence( is_multi_deployment, - self.build_data.libraries.clone(), + provider_info.chain, + new_sequence, )?; sequences.push(sequence); @@ -398,6 +390,50 @@ impl FilledTransactionsState { sequence, }) } + + fn create_sequence( + &self, + multi: bool, + chain: u64, + transactions: VecDeque, + ) -> Result { + let paths = if multi { + None + } else { + Some(ScriptSequence::get_paths( + &self.script_config.config, + &self.args.sig, + &self.build_data.build_data.target, + chain, + self.args.broadcast, + )?) + }; + + let commit = get_commit_hash(&self.script_config.config.__root.0); + + let libraries = self + .build_data + .libraries + .libs + .iter() + .flat_map(|(file, libs)| { + libs.iter() + .map(|(name, address)| format!("{}:{name}:{address}", file.to_string_lossy())) + }) + .collect(); + + Ok(ScriptSequence { + transactions, + returns: self.execution_artifacts.returns.clone(), + receipts: vec![], + pending: vec![], + paths, + timestamp: now().as_secs(), + libraries, + chain, + commit, + }) + } } pub struct BundledState { From 8c50ce5ec456066027b2b2f012fdebcb287317fa Mon Sep 17 00:00:00 2001 From: Arsenii Kulikov Date: Mon, 4 Mar 2024 14:51:10 +0400 Subject: [PATCH 14/33] fmt --- crates/forge/bin/cmd/script/sequence.rs | 9 ++------- crates/forge/bin/cmd/script/simulate.rs | 7 ++----- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/crates/forge/bin/cmd/script/sequence.rs b/crates/forge/bin/cmd/script/sequence.rs index 63a7886a0c83..7111d9ac480f 100644 --- a/crates/forge/bin/cmd/script/sequence.rs +++ b/crates/forge/bin/cmd/script/sequence.rs @@ -140,9 +140,7 @@ impl ScriptSequence { return Ok(()) } - let Some((path, sensitive_path)) = self.paths.clone() else { - return Ok(()) - }; + let Some((path, sensitive_path)) = self.paths.clone() else { return Ok(()) }; self.timestamp = now().as_secs(); let ts_name = format!("run-{}.json", self.timestamp); @@ -167,10 +165,7 @@ impl ScriptSequence { if !silent { shell::println(format!("\nTransactions saved to: {}\n", path.display()))?; - shell::println(format!( - "Sensitive values saved to: {}\n", - sensitive_path.display() - ))?; + shell::println(format!("Sensitive values saved to: {}\n", sensitive_path.display()))?; } Ok(()) diff --git a/crates/forge/bin/cmd/script/simulate.rs b/crates/forge/bin/cmd/script/simulate.rs index 11f46e28ca54..150155aaec28 100644 --- a/crates/forge/bin/cmd/script/simulate.rs +++ b/crates/forge/bin/cmd/script/simulate.rs @@ -323,11 +323,8 @@ impl FilledTransactionsState { } } - let sequence = self.create_sequence( - is_multi_deployment, - provider_info.chain, - new_sequence, - )?; + let sequence = + self.create_sequence(is_multi_deployment, provider_info.chain, new_sequence)?; sequences.push(sequence); From 993e6d321c6e2a8419a0019c4ca86108787e9834 Mon Sep 17 00:00:00 2001 From: Arsenii Kulikov Date: Mon, 4 Mar 2024 16:26:43 +0400 Subject: [PATCH 15/33] better rpc tracking --- crates/forge/bin/cmd/script/broadcast.rs | 9 +- crates/forge/bin/cmd/script/cmd.rs | 2 +- crates/forge/bin/cmd/script/execute.rs | 81 +++++++++++++++-- crates/forge/bin/cmd/script/mod.rs | 87 +------------------ crates/forge/bin/cmd/script/multi_sequence.rs | 26 +++--- crates/forge/bin/cmd/script/resume.rs | 2 +- crates/forge/bin/cmd/script/sequence.rs | 24 +++-- crates/forge/bin/cmd/script/simulate.rs | 5 +- 8 files changed, 116 insertions(+), 120 deletions(-) diff --git a/crates/forge/bin/cmd/script/broadcast.rs b/crates/forge/bin/cmd/script/broadcast.rs index 6eae1ba15535..9eb6dd721e7b 100644 --- a/crates/forge/bin/cmd/script/broadcast.rs +++ b/crates/forge/bin/cmd/script/broadcast.rs @@ -163,7 +163,7 @@ impl BundledState { let errors = join_all(futs).await.into_iter().filter(|res| res.is_err()).collect::>(); - self.sequence.save(true)?; + self.sequence.save(true, false)?; if !errors.is_empty() { return Err(eyre::eyre!("{errors:?}")); @@ -287,6 +287,7 @@ impl BundledState { // Or if the chain does not support batched transactions (eg. Arbitrum). // Or if we need to invoke eth_estimateGas before sending transactions. let sequential_broadcast = estimate_via_rpc || + self.args.slow || send_kind.signers_count() != 1 || !has_batch_support(sequence.chain); @@ -328,21 +329,21 @@ impl BundledState { sequence.add_pending(index, tx_hash); // Checkpoint save - sequence.save(true)?; + sequence.save(true, false)?; update_progress!(pb, index - already_broadcasted); index += 1; } // Checkpoint save - sequence.save(true)?; + sequence.save(true, false)?; shell::println("##\nWaiting for receipts.")?; receipts::clear_pendings(provider.clone(), sequence, None).await?; } // Checkpoint save - sequence.save(true)?; + sequence.save(true, false)?; } } diff --git a/crates/forge/bin/cmd/script/cmd.rs b/crates/forge/bin/cmd/script/cmd.rs index dc6c4e7060e6..1b7b54fada6a 100644 --- a/crates/forge/bin/cmd/script/cmd.rs +++ b/crates/forge/bin/cmd/script/cmd.rs @@ -61,7 +61,7 @@ impl ScriptArgs { &pre_simulation.build_data.highlevel_known_contracts, )?; - if pre_simulation.script_config.missing_rpc { + if pre_simulation.execution_artifacts.rpc_data.missing_rpc { shell::println("\nIf you wish to simulate on-chain transactions pass a RPC URL.")?; return Ok(()); } diff --git a/crates/forge/bin/cmd/script/execute.rs b/crates/forge/bin/cmd/script/execute.rs index 5dc5bcc54616..de080e6d1092 100644 --- a/crates/forge/bin/cmd/script/execute.rs +++ b/crates/forge/bin/cmd/script/execute.rs @@ -8,6 +8,7 @@ use alloy_json_abi::{Function, InternalType, JsonAbi}; use alloy_primitives::{Address, Bytes, U64}; use alloy_rpc_types::request::TransactionRequest; use async_recursion::async_recursion; +use ethers_providers::Middleware; use eyre::Result; use forge::{ decode::{decode_console_logs, RevertDecoder}, @@ -20,12 +21,15 @@ use forge::{ use foundry_cli::utils::{ensure_clean_constructor, needs_setup}; use foundry_common::{ fmt::{format_token, format_token_raw}, + provider::ethers::{get_http_provider, RpcUrl}, shell, ContractsByArtifact, }; use foundry_compilers::artifacts::ContractBytecodeSome; -use foundry_config::Config; +use foundry_config::{Config, NamedChain}; use foundry_debugger::Debugger; -use std::collections::HashMap; +use futures::future::join_all; +use itertools::Itertools; +use std::collections::{HashMap, HashSet}; use yansi::Paint; pub struct ExecutionData { @@ -200,18 +204,29 @@ impl PreExecutionState { } impl ExecutedState { - pub async fn prepare_simulation(mut self) -> Result { + pub async fn prepare_simulation(self) -> Result { let returns = self.get_returns()?; let known_contracts = self.build_data.get_flattened_contracts(true); let decoder = self.build_trace_decoder(&known_contracts)?; - if let Some(txs) = self.execution_result.transactions.as_ref() { - self.script_config.collect_rpcs(txs); + let txs = self.execution_result.transactions.clone().unwrap_or_default(); + let rpc_data = RpcData::from_transactions(&txs); + + if rpc_data.is_multi_chain() { + shell::eprintln(format!( + "{}", + Paint::yellow( + "Multi chain deployment is still under development. Use with caution." + ) + ))?; + if !self.build_data.libraries.is_empty() { + eyre::bail!( + "Multi chain deployment does not support library linking at the moment." + ) + } } - - self.script_config.check_multi_chain_constraints(&self.build_data.libraries)?; - self.script_config.check_shanghai_support().await?; + rpc_data.check_shanghai_support().await?; Ok(PreSimulationState { args: self.args, @@ -220,7 +235,7 @@ impl ExecutedState { build_data: self.build_data, execution_data: self.execution_data, execution_result: self.execution_result, - execution_artifacts: ExecutionArtifacts { known_contracts, decoder, returns }, + execution_artifacts: ExecutionArtifacts { known_contracts, decoder, returns, rpc_data }, }) } @@ -308,10 +323,58 @@ pub struct PreSimulationState { pub execution_artifacts: ExecutionArtifacts, } +pub struct RpcData { + /// Unique list of rpc urls present + pub total_rpcs: HashSet, + /// If true, one of the transactions did not have a rpc + pub missing_rpc: bool, +} + +impl RpcData { + fn from_transactions(txs: &BroadcastableTransactions) -> Self { + let missing_rpc = txs.iter().any(|tx| tx.rpc.is_none()); + let total_rpcs = + txs.iter().filter_map(|tx| tx.rpc.as_ref().cloned()).collect::>(); + + Self { total_rpcs, missing_rpc } + } + + pub fn is_multi_chain(&self) -> bool { + self.total_rpcs.len() > 1 || (self.missing_rpc && self.total_rpcs.len() > 0) + } + + async fn check_shanghai_support(&self) -> Result<()> { + let chain_ids = self.total_rpcs.iter().map(|rpc| async move { + let provider = get_http_provider(rpc); + let id = provider.get_chainid().await.ok()?; + let id_u64: u64 = id.try_into().ok()?; + NamedChain::try_from(id_u64).ok() + }); + + let chains = join_all(chain_ids).await; + let iter = chains.iter().flatten().map(|c| (c.supports_shanghai(), c)); + if iter.clone().any(|(s, _)| !s) { + let msg = format!( + "\ +EIP-3855 is not supported in one or more of the RPCs used. +Unsupported Chain IDs: {}. +Contracts deployed with a Solidity version equal or higher than 0.8.20 might not work properly. +For more information, please see https://eips.ethereum.org/EIPS/eip-3855", + iter.filter(|(supported, _)| !supported) + .map(|(_, chain)| *chain as u64) + .format(", ") + ); + shell::println(Paint::yellow(msg))?; + } + Ok(()) + } +} + pub struct ExecutionArtifacts { pub known_contracts: ContractsByArtifact, pub decoder: CallTraceDecoder, pub returns: HashMap, + pub rpc_data: RpcData, } impl PreSimulationState { diff --git a/crates/forge/bin/cmd/script/mod.rs b/crates/forge/bin/cmd/script/mod.rs index fc8b19fb1b7a..a83a560eb238 100644 --- a/crates/forge/bin/cmd/script/mod.rs +++ b/crates/forge/bin/cmd/script/mod.rs @@ -5,7 +5,6 @@ use alloy_json_abi::{Function, JsonAbi}; use alloy_primitives::{Address, Bytes, Log, U256}; use clap::{Parser, ValueHint}; use dialoguer::Confirm; -use ethers_providers::{Http, Middleware}; use eyre::{ContextCompat, Result, WrapErr}; use forge::{ backend::Backend, @@ -23,26 +22,21 @@ use foundry_common::{ provider::ethers::RpcUrl, shell, CONTRACT_MAX_SIZE, SELECTOR_LEN, }; -use foundry_compilers::{ - artifacts::{ContractBytecodeSome, Libraries}, - ArtifactId, -}; +use foundry_compilers::{artifacts::ContractBytecodeSome, ArtifactId}; use foundry_config::{ figment, figment::{ value::{Dict, Map}, Metadata, Profile, Provider, }, - Config, NamedChain, + Config, }; use foundry_evm::{ constants::DEFAULT_CREATE2_DEPLOYER, inspectors::cheatcodes::BroadcastableTransactions, }; use foundry_wallets::MultiWalletOpts; -use futures::future; -use itertools::Itertools; use serde::{Deserialize, Serialize}; -use std::collections::{BTreeMap, HashMap, HashSet}; +use std::collections::{BTreeMap, HashMap}; use yansi::Paint; mod artifacts; @@ -398,10 +392,6 @@ pub struct ScriptConfig { pub sender_nonce: u64, /// Maps a rpc url to a backend pub backends: HashMap, - /// Unique list of rpc urls present - pub total_rpcs: HashSet, - /// If true, one of the transactions did not have a rpc - pub missing_rpc: bool, } impl ScriptConfig { @@ -412,14 +402,7 @@ impl ScriptConfig { // dapptools compatibility 1 }; - Ok(Self { - config, - evm_opts, - sender_nonce, - backends: HashMap::new(), - total_rpcs: HashSet::new(), - missing_rpc: false, - }) + Ok(Self { config, evm_opts, sender_nonce, backends: HashMap::new() }) } pub async fn update_sender(&mut self, sender: Address) -> Result<()> { @@ -433,68 +416,6 @@ impl ScriptConfig { Ok(()) } - fn collect_rpcs(&mut self, txs: &BroadcastableTransactions) { - self.missing_rpc = txs.iter().any(|tx| tx.rpc.is_none()); - - self.total_rpcs - .extend(txs.iter().filter_map(|tx| tx.rpc.as_ref().cloned()).collect::>()); - - if let Some(rpc) = &self.evm_opts.fork_url { - self.total_rpcs.insert(rpc.clone()); - } - } - - fn has_multiple_rpcs(&self) -> bool { - self.total_rpcs.len() > 1 - } - - /// Certain features are disabled for multi chain deployments, and if tried, will return - /// error. [library support] - fn check_multi_chain_constraints(&self, libraries: &Libraries) -> Result<()> { - if self.has_multiple_rpcs() || (self.missing_rpc && !self.total_rpcs.is_empty()) { - shell::eprintln(format!( - "{}", - Paint::yellow( - "Multi chain deployment is still under development. Use with caution." - ) - ))?; - if !libraries.libs.is_empty() { - eyre::bail!( - "Multi chain deployment does not support library linking at the moment." - ) - } - } - Ok(()) - } - - /// Checks if the RPCs used point to chains that support EIP-3855. - /// If not, warns the user. - async fn check_shanghai_support(&self) -> Result<()> { - let chain_ids = self.total_rpcs.iter().map(|rpc| async move { - let provider = ethers_providers::Provider::::try_from(rpc).ok()?; - let id = provider.get_chainid().await.ok()?; - let id_u64: u64 = id.try_into().ok()?; - NamedChain::try_from(id_u64).ok() - }); - - let chains = future::join_all(chain_ids).await; - let iter = chains.iter().flatten().map(|c| (c.supports_shanghai(), c)); - if iter.clone().any(|(s, _)| !s) { - let msg = format!( - "\ -EIP-3855 is not supported in one or more of the RPCs used. -Unsupported Chain IDs: {}. -Contracts deployed with a Solidity version equal or higher than 0.8.20 might not work properly. -For more information, please see https://eips.ethereum.org/EIPS/eip-3855", - iter.filter(|(supported, _)| !supported) - .map(|(_, chain)| *chain as u64) - .format(", ") - ); - shell::println(Paint::yellow(msg))?; - } - Ok(()) - } - async fn get_runner(&mut self) -> Result { self._get_runner(None, false).await } diff --git a/crates/forge/bin/cmd/script/multi_sequence.rs b/crates/forge/bin/cmd/script/multi_sequence.rs index 0286179a43e5..f2bcca69bdcf 100644 --- a/crates/forge/bin/cmd/script/multi_sequence.rs +++ b/crates/forge/bin/cmd/script/multi_sequence.rs @@ -120,7 +120,7 @@ impl MultiChainSequence { } /// Saves the transactions as file if it's a standalone deployment. - pub fn save(&mut self, silent: bool) -> Result<()> { + pub fn save(&mut self, silent: bool, save_ts: bool) -> Result<()> { self.deployments.iter_mut().for_each(|sequence| sequence.sort_receipts()); self.timestamp = now().as_secs(); @@ -133,11 +133,13 @@ impl MultiChainSequence { serde_json::to_writer_pretty(&mut writer, &self)?; writer.flush()?; - //../Contract-[timestamp]/run.json - let path = self.path.to_string_lossy(); - let file = PathBuf::from(&path.replace("-latest", &format!("-{}", self.timestamp))); - fs::create_dir_all(file.parent().unwrap())?; - fs::copy(&self.path, &file)?; + if save_ts { + //../Contract-[timestamp]/run.json + let path = self.path.to_string_lossy(); + let file = PathBuf::from(&path.replace("-latest", &format!("-{}", self.timestamp))); + fs::create_dir_all(file.parent().unwrap())?; + fs::copy(&self.path, &file)?; + } // cache writes //../Contract-latest/run.json @@ -145,11 +147,13 @@ impl MultiChainSequence { serde_json::to_writer_pretty(&mut writer, &sensitive_sequence)?; writer.flush()?; - //../Contract-[timestamp]/run.json - let path = self.sensitive_path.to_string_lossy(); - let file = PathBuf::from(&path.replace("-latest", &format!("-{}", self.timestamp))); - fs::create_dir_all(file.parent().unwrap())?; - fs::copy(&self.sensitive_path, &file)?; + if save_ts { + //../Contract-[timestamp]/run.json + let path = self.sensitive_path.to_string_lossy(); + let file = PathBuf::from(&path.replace("-latest", &format!("-{}", self.timestamp))); + fs::create_dir_all(file.parent().unwrap())?; + fs::copy(&self.sensitive_path, &file)?; + } if !silent { println!("\nTransactions saved to: {}\n", self.path.display()); diff --git a/crates/forge/bin/cmd/script/resume.rs b/crates/forge/bin/cmd/script/resume.rs index f3bdac2f240f..4e4c93b00c99 100644 --- a/crates/forge/bin/cmd/script/resume.rs +++ b/crates/forge/bin/cmd/script/resume.rs @@ -25,7 +25,7 @@ impl PreSimulationState { execution_artifacts, } = self; - let sequence = if args.multi { + let sequence = if args.multi || execution_artifacts.rpc_data.is_multi_chain() { ScriptSequenceKind::Multi(MultiChainSequence::load( &script_config.config, &args.sig, diff --git a/crates/forge/bin/cmd/script/sequence.rs b/crates/forge/bin/cmd/script/sequence.rs index 7111d9ac480f..ee3b0c01a42e 100644 --- a/crates/forge/bin/cmd/script/sequence.rs +++ b/crates/forge/bin/cmd/script/sequence.rs @@ -32,10 +32,10 @@ pub enum ScriptSequenceKind { } impl ScriptSequenceKind { - pub fn save(&mut self, silent: bool) -> Result<()> { + pub fn save(&mut self, silent: bool, save_ts: bool) -> Result<()> { match self { - ScriptSequenceKind::Single(sequence) => sequence.save(silent), - ScriptSequenceKind::Multi(sequence) => sequence.save(silent), + ScriptSequenceKind::Single(sequence) => sequence.save(silent, save_ts), + ScriptSequenceKind::Multi(sequence) => sequence.save(silent, save_ts), } } @@ -56,7 +56,7 @@ impl ScriptSequenceKind { impl Drop for ScriptSequenceKind { fn drop(&mut self) { - self.save(false).expect("could not save deployment sequence"); + self.save(false, true).expect("could not save deployment sequence"); } } @@ -133,7 +133,9 @@ impl ScriptSequence { } /// Saves the transactions as file if it's a standalone deployment. - pub fn save(&mut self, silent: bool) -> Result<()> { + /// `save_ts` should be set to true for checkpoint updates, which might happen many times and + /// could result in us saving many identical files. + pub fn save(&mut self, silent: bool, save_ts: bool) -> Result<()> { self.sort_receipts(); if self.transactions.is_empty() { @@ -152,16 +154,20 @@ impl ScriptSequence { let mut writer = BufWriter::new(fs::create_file(&path)?); serde_json::to_writer_pretty(&mut writer, &self)?; writer.flush()?; - //../run-[timestamp].json - fs::copy(&path, path.with_file_name(&ts_name))?; + if save_ts { + //../run-[timestamp].json + fs::copy(&path, path.with_file_name(&ts_name))?; + } // cache folder writes //../run-latest.json let mut writer = BufWriter::new(fs::create_file(&sensitive_path)?); serde_json::to_writer_pretty(&mut writer, &sensitive_script_sequence)?; writer.flush()?; - //../run-[timestamp].json - fs::copy(&sensitive_path, sensitive_path.with_file_name(&ts_name))?; + if save_ts { + //../run-[timestamp].json + fs::copy(&sensitive_path, sensitive_path.with_file_name(&ts_name))?; + } if !silent { shell::println(format!("\nTransactions saved to: {}\n", path.display()))?; diff --git a/crates/forge/bin/cmd/script/simulate.rs b/crates/forge/bin/cmd/script/simulate.rs index 150155aaec28..e444577a8da8 100644 --- a/crates/forge/bin/cmd/script/simulate.rs +++ b/crates/forge/bin/cmd/script/simulate.rs @@ -195,13 +195,14 @@ impl PreSimulationState { /// Build the multiple runners from different forks. async fn build_runners(&self) -> Result> { if !shell::verbosity().is_silent() { - let n = self.script_config.total_rpcs.len(); + let n = self.execution_artifacts.rpc_data.total_rpcs.len(); let s = if n != 1 { "s" } else { "" }; println!("\n## Setting up {n} EVM{s}."); } let futs = self - .script_config + .execution_artifacts + .rpc_data .total_rpcs .iter() .map(|rpc| async { From 8d6e4f2d606f12ed0bbad6d474a2fd5d07889bf7 Mon Sep 17 00:00:00 2001 From: Arsenii Kulikov Date: Mon, 4 Mar 2024 17:24:24 +0400 Subject: [PATCH 16/33] fix rpc logic + extract states to separate file --- crates/forge/bin/cmd/script/broadcast.rs | 15 +--- crates/forge/bin/cmd/script/build.rs | 25 +------ crates/forge/bin/cmd/script/cmd.rs | 26 ++++--- crates/forge/bin/cmd/script/execute.rs | 37 ++-------- crates/forge/bin/cmd/script/mod.rs | 3 +- crates/forge/bin/cmd/script/resume.rs | 75 ++++++++++---------- crates/forge/bin/cmd/script/simulate.rs | 39 ++--------- crates/forge/bin/cmd/script/states.rs | 87 ++++++++++++++++++++++++ crates/forge/bin/cmd/script/verify.rs | 2 +- 9 files changed, 152 insertions(+), 157 deletions(-) create mode 100644 crates/forge/bin/cmd/script/states.rs diff --git a/crates/forge/bin/cmd/script/broadcast.rs b/crates/forge/bin/cmd/script/broadcast.rs index 9eb6dd721e7b..3a651cf11f2b 100644 --- a/crates/forge/bin/cmd/script/broadcast.rs +++ b/crates/forge/bin/cmd/script/broadcast.rs @@ -1,10 +1,6 @@ use super::{ - build::LinkedBuildData, - execute::{ExecutionArtifacts, ExecutionData}, receipts, - sequence::ScriptSequenceKind, - simulate::BundledState, - ScriptArgs, ScriptConfig, + states::{BroadcastedState, BundledState}, }; use alloy_primitives::{utils::format_units, Address, TxHash, U256}; use ethers_core::types::transaction::eip2718::TypedTransaction; @@ -381,12 +377,3 @@ impl BundledState { }) } } - -pub struct BroadcastedState { - pub args: ScriptArgs, - pub script_config: ScriptConfig, - pub build_data: LinkedBuildData, - pub execution_data: ExecutionData, - pub execution_artifacts: ExecutionArtifacts, - pub sequence: ScriptSequenceKind, -} diff --git a/crates/forge/bin/cmd/script/build.rs b/crates/forge/bin/cmd/script/build.rs index 250da4cf8fc1..e4d05b28431d 100644 --- a/crates/forge/bin/cmd/script/build.rs +++ b/crates/forge/bin/cmd/script/build.rs @@ -1,9 +1,6 @@ -use super::{ScriptArgs, ScriptConfig}; +use super::states::{CompiledState, LinkedState, PreprocessedState}; use alloy_primitives::{Address, Bytes}; use eyre::{Context, OptionExt, Result}; -use forge::{ - inspectors::cheatcodes::ScriptWallets, -}; use foundry_cli::utils::get_cached_entry_by_name; use foundry_common::{ compile::{self, ContractSources, ProjectCompiler}, @@ -19,12 +16,6 @@ use foundry_compilers::{ use foundry_linking::{LinkOutput, Linker}; use std::str::FromStr; -pub struct PreprocessedState { - pub args: ScriptArgs, - pub script_config: ScriptConfig, - pub script_wallets: ScriptWallets, -} - impl PreprocessedState { pub fn compile(self) -> Result { let Self { args, script_config, script_wallets } = self; @@ -212,13 +203,6 @@ impl LinkedBuildData { } } -pub struct CompiledState { - pub args: ScriptArgs, - pub script_config: ScriptConfig, - pub script_wallets: ScriptWallets, - pub build_data: BuildData, -} - impl CompiledState { pub fn link(self) -> Result { let Self { args, script_config, script_wallets, build_data } = self; @@ -231,10 +215,3 @@ impl CompiledState { Ok(LinkedState { args, script_config, script_wallets, build_data }) } } - -pub struct LinkedState { - pub args: ScriptArgs, - pub script_config: ScriptConfig, - pub script_wallets: ScriptWallets, - pub build_data: LinkedBuildData, -} diff --git a/crates/forge/bin/cmd/script/cmd.rs b/crates/forge/bin/cmd/script/cmd.rs index 1b7b54fada6a..9d6cc3e8264a 100644 --- a/crates/forge/bin/cmd/script/cmd.rs +++ b/crates/forge/bin/cmd/script/cmd.rs @@ -1,5 +1,4 @@ -use super::{ScriptArgs, ScriptConfig}; -use crate::cmd::script::build::PreprocessedState; +use super::{states::PreprocessedState, ScriptArgs, ScriptConfig}; use alloy_primitives::Address; use ethers_signers::Signer; use eyre::Result; @@ -49,6 +48,16 @@ impl ScriptArgs { pre_simulation.show_traces().await?; } + if pre_simulation.execution_result.transactions.as_ref().map_or(true, |txs| txs.is_empty()) + { + return Ok(()); + } + + if pre_simulation.execution_artifacts.rpc_data.missing_rpc { + shell::println("\nIf you wish to simulate on-chain transactions pass a RPC URL.")?; + return Ok(()); + } + // Move from `PreSimulationState` to `BundledState` either by resuming or simulating // transactions. let bundled = if pre_simulation.args.resume || @@ -61,18 +70,7 @@ impl ScriptArgs { &pre_simulation.build_data.highlevel_known_contracts, )?; - if pre_simulation.execution_artifacts.rpc_data.missing_rpc { - shell::println("\nIf you wish to simulate on-chain transactions pass a RPC URL.")?; - return Ok(()); - } - - let state = pre_simulation.fill_metadata().await?; - - if state.transactions.is_empty() { - return Ok(()); - } - - state.bundle().await? + pre_simulation.fill_metadata().await?.bundle().await? }; if !bundled.args.broadcast && !bundled.args.resume { diff --git a/crates/forge/bin/cmd/script/execute.rs b/crates/forge/bin/cmd/script/execute.rs index de080e6d1092..c00deba19c68 100644 --- a/crates/forge/bin/cmd/script/execute.rs +++ b/crates/forge/bin/cmd/script/execute.rs @@ -1,7 +1,7 @@ use super::{ - build::{CompiledState, LinkedBuildData, LinkedState}, runner::ScriptRunner, - JsonResult, NestedValue, ScriptArgs, ScriptConfig, ScriptResult, + states::{CompiledState, ExecutedState, LinkedState, PreExecutionState, PreSimulationState}, + JsonResult, NestedValue, ScriptResult, }; use alloy_dyn_abi::FunctionExt; use alloy_json_abi::{Function, InternalType, JsonAbi}; @@ -12,7 +12,7 @@ use ethers_providers::Middleware; use eyre::Result; use forge::{ decode::{decode_console_logs, RevertDecoder}, - inspectors::cheatcodes::{BroadcastableTransaction, BroadcastableTransactions, ScriptWallets}, + inspectors::cheatcodes::{BroadcastableTransaction, BroadcastableTransactions}, traces::{ identifier::{EtherscanIdentifier, LocalTraceIdentifier, SignaturesIdentifier}, render_trace_arena, CallTraceDecoder, CallTraceDecoderBuilder, TraceKind, @@ -39,14 +39,6 @@ pub struct ExecutionData { pub abi: JsonAbi, } -pub struct PreExecutionState { - pub args: ScriptArgs, - pub script_config: ScriptConfig, - pub script_wallets: ScriptWallets, - pub build_data: LinkedBuildData, - pub execution_data: ExecutionData, -} - impl LinkedState { /// Given linked and compiled artifacts, prepares data we need for execution. pub async fn prepare_execution(self) -> Result { @@ -72,15 +64,6 @@ impl LinkedState { } } -pub struct ExecutedState { - pub args: ScriptArgs, - pub script_config: ScriptConfig, - pub script_wallets: ScriptWallets, - pub build_data: LinkedBuildData, - pub execution_data: ExecutionData, - pub execution_result: ScriptResult, -} - impl PreExecutionState { #[async_recursion] pub async fn execute(mut self) -> Result { @@ -313,16 +296,6 @@ impl ExecutedState { } } -pub struct PreSimulationState { - pub args: ScriptArgs, - pub script_config: ScriptConfig, - pub script_wallets: ScriptWallets, - pub build_data: LinkedBuildData, - pub execution_data: ExecutionData, - pub execution_result: ScriptResult, - pub execution_artifacts: ExecutionArtifacts, -} - pub struct RpcData { /// Unique list of rpc urls present pub total_rpcs: HashSet, @@ -339,8 +312,10 @@ impl RpcData { Self { total_rpcs, missing_rpc } } + /// Returns true if script might be multi-chain. + /// Returns false positive in case when missing rpc is the same as the only rpc present. pub fn is_multi_chain(&self) -> bool { - self.total_rpcs.len() > 1 || (self.missing_rpc && self.total_rpcs.len() > 0) + self.total_rpcs.len() > 1 || (self.missing_rpc && !self.total_rpcs.is_empty()) } async fn check_shanghai_support(&self) -> Result<()> { diff --git a/crates/forge/bin/cmd/script/mod.rs b/crates/forge/bin/cmd/script/mod.rs index a83a560eb238..6192b0e1cb8c 100644 --- a/crates/forge/bin/cmd/script/mod.rs +++ b/crates/forge/bin/cmd/script/mod.rs @@ -51,6 +51,7 @@ mod resume; mod runner; mod sequence; mod simulate; +mod states; pub mod transaction; mod verify; @@ -487,7 +488,7 @@ impl ScriptConfig { mod tests { use super::*; use foundry_cli::utils::LoadConfig; - use foundry_config::UnresolvedEnvVarError; + use foundry_config::{NamedChain, UnresolvedEnvVarError}; use std::fs; use tempfile::tempdir; diff --git a/crates/forge/bin/cmd/script/resume.rs b/crates/forge/bin/cmd/script/resume.rs index 4e4c93b00c99..3f0b0ff3f6ad 100644 --- a/crates/forge/bin/cmd/script/resume.rs +++ b/crates/forge/bin/cmd/script/resume.rs @@ -1,15 +1,14 @@ use std::sync::Arc; use ethers_providers::Middleware; -use eyre::{OptionExt, Result}; +use eyre::Result; use foundry_common::provider::ethers::try_get_http_provider; use foundry_compilers::artifacts::Libraries; use super::{ - execute::PreSimulationState, multi_sequence::MultiChainSequence, sequence::{ScriptSequence, ScriptSequenceKind}, - simulate::BundledState, + states::{BundledState, PreSimulationState}, }; impl PreSimulationState { @@ -25,51 +24,53 @@ impl PreSimulationState { execution_artifacts, } = self; - let sequence = if args.multi || execution_artifacts.rpc_data.is_multi_chain() { - ScriptSequenceKind::Multi(MultiChainSequence::load( + if execution_artifacts.rpc_data.missing_rpc { + eyre::bail!("Missing `--fork-url` field.") + } + + let sequence = match execution_artifacts.rpc_data.total_rpcs.len() { + 2.. => ScriptSequenceKind::Multi(MultiChainSequence::load( &script_config.config, &args.sig, &build_data.build_data.target, - )?) - } else { - let fork_url = script_config - .evm_opts - .fork_url - .as_deref() - .ok_or_eyre("Missing `--fork-url` field.")?; + )?), + 1 => { + let fork_url = execution_artifacts.rpc_data.total_rpcs.iter().next().unwrap(); - let provider = Arc::new(try_get_http_provider(fork_url)?); - let chain = provider.get_chainid().await?.as_u64(); + let provider = Arc::new(try_get_http_provider(fork_url)?); + let chain = provider.get_chainid().await?.as_u64(); - let seq = match ScriptSequence::load( - &script_config.config, - &args.sig, - &build_data.build_data.target, - chain, - args.broadcast, - ) { - Ok(seq) => seq, - // If the script was simulated, but there was no attempt to broadcast yet, - // try to read the script sequence from the `dry-run/` folder - Err(_) if args.broadcast => ScriptSequence::load( + let seq = match ScriptSequence::load( &script_config.config, &args.sig, &build_data.build_data.target, chain, - false, - )?, - Err(err) => { - eyre::bail!(err.wrap_err("If you were trying to resume or verify a multi chain deployment, add `--multi` to your command invocation.")) - } - }; + args.broadcast, + ) { + Ok(seq) => seq, + // If the script was simulated, but there was no attempt to broadcast yet, + // try to read the script sequence from the `dry-run/` folder + Err(_) if args.broadcast => ScriptSequence::load( + &script_config.config, + &args.sig, + &build_data.build_data.target, + chain, + false, + )?, + Err(err) => { + eyre::bail!(err.wrap_err("If you were trying to resume or verify a multi chain deployment, add `--multi` to your command invocation.")) + } + }; - // We might have predeployed libraries from the broadcasting, so we need to - // relink the contracts with them, since their mapping is - // not included in the solc cache files. - build_data = - build_data.build_data.link_with_libraries(Libraries::parse(&seq.libraries)?)?; + // We might have predeployed libraries from the broadcasting, so we need to + // relink the contracts with them, since their mapping is + // not included in the solc cache files. + build_data = + build_data.build_data.link_with_libraries(Libraries::parse(&seq.libraries)?)?; - ScriptSequenceKind::Single(seq) + ScriptSequenceKind::Single(seq) + }, + 0 => eyre::bail!("No RPC URLs"), }; Ok(BundledState { diff --git a/crates/forge/bin/cmd/script/simulate.rs b/crates/forge/bin/cmd/script/simulate.rs index e444577a8da8..7fec39d46bcf 100644 --- a/crates/forge/bin/cmd/script/simulate.rs +++ b/crates/forge/bin/cmd/script/simulate.rs @@ -2,21 +2,16 @@ use crate::cmd::{init::get_commit_hash, script::broadcast::estimate_gas}; use super::{ artifacts::ArtifactInfo, - build::LinkedBuildData, - execute::{ExecutionArtifacts, ExecutionData, PreSimulationState}, multi_sequence::MultiChainSequence, providers::ProvidersManager, runner::ScriptRunner, sequence::{ScriptSequence, ScriptSequenceKind}, + states::{BundledState, FilledTransactionsState, PreSimulationState}, transaction::TransactionWithMetadata, - ScriptArgs, ScriptConfig, }; use alloy_primitives::{utils::format_units, Address, U256}; -use eyre::{Context, OptionExt, Result}; -use forge::{ - inspectors::cheatcodes::{BroadcastableTransactions, ScriptWallets}, - traces::render_trace_arena, -}; +use eyre::{Context, Result}; +use forge::{inspectors::cheatcodes::BroadcastableTransactions, traces::render_trace_arena}; use foundry_cli::utils::{has_different_gas_calc, now}; use foundry_common::{ get_contract_name, provider::ethers::RpcUrl, shell, types::ToAlloy, ContractsByArtifact, @@ -231,16 +226,6 @@ impl PreSimulationState { } } -pub struct FilledTransactionsState { - pub args: ScriptArgs, - pub script_config: ScriptConfig, - pub script_wallets: ScriptWallets, - pub build_data: LinkedBuildData, - pub execution_data: ExecutionData, - pub execution_artifacts: ExecutionArtifacts, - pub transactions: VecDeque, -} - impl FilledTransactionsState { /// Bundles all transactions of the [`TransactionWithMetadata`] type in a list of /// [`ScriptSequence`]. List length will be higher than 1, if we're dealing with a multi @@ -248,13 +233,7 @@ impl FilledTransactionsState { /// /// Each transaction will be added with the correct transaction type and gas estimation. pub async fn bundle(self) -> Result { - // User might be using both "in-code" forks and `--fork-url`. - let last_rpc = &self - .transactions - .back() - .ok_or_eyre("No onchain transactions generated in script")? - .rpc; - let is_multi_deployment = self.transactions.iter().any(|tx| &tx.rpc != last_rpc); + let is_multi_deployment = self.execution_artifacts.rpc_data.total_rpcs.len() > 1; if is_multi_deployment && !self.build_data.libraries.is_empty() { eyre::bail!("Multi-chain deployment is not supported with libraries."); @@ -433,13 +412,3 @@ impl FilledTransactionsState { }) } } - -pub struct BundledState { - pub args: ScriptArgs, - pub script_config: ScriptConfig, - pub script_wallets: ScriptWallets, - pub build_data: LinkedBuildData, - pub execution_data: ExecutionData, - pub execution_artifacts: ExecutionArtifacts, - pub sequence: ScriptSequenceKind, -} diff --git a/crates/forge/bin/cmd/script/states.rs b/crates/forge/bin/cmd/script/states.rs new file mode 100644 index 000000000000..c32b5f5c058e --- /dev/null +++ b/crates/forge/bin/cmd/script/states.rs @@ -0,0 +1,87 @@ +use std::collections::VecDeque; + +use forge::inspectors::cheatcodes::ScriptWallets; + +use super::{ + build::{BuildData, LinkedBuildData}, + execute::{ExecutionArtifacts, ExecutionData}, + sequence::ScriptSequenceKind, + transaction::TransactionWithMetadata, + ScriptArgs, ScriptConfig, ScriptResult, +}; + +pub struct PreprocessedState { + pub args: ScriptArgs, + pub script_config: ScriptConfig, + pub script_wallets: ScriptWallets, +} + +pub struct CompiledState { + pub args: ScriptArgs, + pub script_config: ScriptConfig, + pub script_wallets: ScriptWallets, + pub build_data: BuildData, +} + +pub struct LinkedState { + pub args: ScriptArgs, + pub script_config: ScriptConfig, + pub script_wallets: ScriptWallets, + pub build_data: LinkedBuildData, +} + +pub struct PreExecutionState { + pub args: ScriptArgs, + pub script_config: ScriptConfig, + pub script_wallets: ScriptWallets, + pub build_data: LinkedBuildData, + pub execution_data: ExecutionData, +} + +pub struct ExecutedState { + pub args: ScriptArgs, + pub script_config: ScriptConfig, + pub script_wallets: ScriptWallets, + pub build_data: LinkedBuildData, + pub execution_data: ExecutionData, + pub execution_result: ScriptResult, +} + +pub struct PreSimulationState { + pub args: ScriptArgs, + pub script_config: ScriptConfig, + pub script_wallets: ScriptWallets, + pub build_data: LinkedBuildData, + pub execution_data: ExecutionData, + pub execution_result: ScriptResult, + pub execution_artifacts: ExecutionArtifacts, +} + +pub struct FilledTransactionsState { + pub args: ScriptArgs, + pub script_config: ScriptConfig, + pub script_wallets: ScriptWallets, + pub build_data: LinkedBuildData, + pub execution_data: ExecutionData, + pub execution_artifacts: ExecutionArtifacts, + pub transactions: VecDeque, +} + +pub struct BundledState { + pub args: ScriptArgs, + pub script_config: ScriptConfig, + pub script_wallets: ScriptWallets, + pub build_data: LinkedBuildData, + pub execution_data: ExecutionData, + pub execution_artifacts: ExecutionArtifacts, + pub sequence: ScriptSequenceKind, +} + +pub struct BroadcastedState { + pub args: ScriptArgs, + pub script_config: ScriptConfig, + pub build_data: LinkedBuildData, + pub execution_data: ExecutionData, + pub execution_artifacts: ExecutionArtifacts, + pub sequence: ScriptSequenceKind, +} diff --git a/crates/forge/bin/cmd/script/verify.rs b/crates/forge/bin/cmd/script/verify.rs index 6e3daf158840..086440bec07c 100644 --- a/crates/forge/bin/cmd/script/verify.rs +++ b/crates/forge/bin/cmd/script/verify.rs @@ -7,7 +7,7 @@ use foundry_compilers::{info::ContractInfo, Project}; use foundry_config::{Chain, Config}; use semver::Version; -use super::{broadcast::BroadcastedState, simulate::BundledState}; +use super::states::{BroadcastedState, BundledState}; impl BundledState { pub fn verify_preflight_check(&self) -> Result<()> { From 2b8a311a7dc7d0155d22659ec55fd5a05f2c6b38 Mon Sep 17 00:00:00 2001 From: Arsenii Kulikov Date: Mon, 4 Mar 2024 17:31:01 +0400 Subject: [PATCH 17/33] fmt --- crates/forge/bin/cmd/script/resume.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/forge/bin/cmd/script/resume.rs b/crates/forge/bin/cmd/script/resume.rs index 3f0b0ff3f6ad..f745e1256dd3 100644 --- a/crates/forge/bin/cmd/script/resume.rs +++ b/crates/forge/bin/cmd/script/resume.rs @@ -69,7 +69,7 @@ impl PreSimulationState { build_data.build_data.link_with_libraries(Libraries::parse(&seq.libraries)?)?; ScriptSequenceKind::Single(seq) - }, + } 0 => eyre::bail!("No RPC URLs"), }; From df7fecc0b757f35814ef6092d93ba8635654bd7d Mon Sep 17 00:00:00 2001 From: Arsenii Kulikov Date: Mon, 4 Mar 2024 17:36:48 +0400 Subject: [PATCH 18/33] some docs --- crates/forge/bin/cmd/script/states.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/crates/forge/bin/cmd/script/states.rs b/crates/forge/bin/cmd/script/states.rs index c32b5f5c058e..d2a7e8fc9327 100644 --- a/crates/forge/bin/cmd/script/states.rs +++ b/crates/forge/bin/cmd/script/states.rs @@ -10,12 +10,14 @@ use super::{ ScriptArgs, ScriptConfig, ScriptResult, }; +/// First state basically containing only inputs of the user. pub struct PreprocessedState { pub args: ScriptArgs, pub script_config: ScriptConfig, pub script_wallets: ScriptWallets, } +/// State after we have determined and compiled target contract to be executed. pub struct CompiledState { pub args: ScriptArgs, pub script_config: ScriptConfig, @@ -23,6 +25,8 @@ pub struct CompiledState { pub build_data: BuildData, } +/// State after linking, contains the linked build data along with library addresses and optional +/// array of libraries that need to be predeployed. pub struct LinkedState { pub args: ScriptArgs, pub script_config: ScriptConfig, @@ -30,6 +34,7 @@ pub struct LinkedState { pub build_data: LinkedBuildData, } +/// Same as [LinkedState], but also contains [ExecutionData]. pub struct PreExecutionState { pub args: ScriptArgs, pub script_config: ScriptConfig, @@ -38,6 +43,7 @@ pub struct PreExecutionState { pub execution_data: ExecutionData, } +/// State after the script has been executed. pub struct ExecutedState { pub args: ScriptArgs, pub script_config: ScriptConfig, @@ -47,6 +53,11 @@ pub struct ExecutedState { pub execution_result: ScriptResult, } +/// Same as [ExecutedState], but also contains [ExecutionArtifacts] which are obtained from +/// [ScriptResult]. +/// +/// Can be either converted directly to [BundledState] via [PreSimulationState::resume] or driven to +/// it through [FilledTransactionsState]. pub struct PreSimulationState { pub args: ScriptArgs, pub script_config: ScriptConfig, @@ -57,6 +68,9 @@ pub struct PreSimulationState { pub execution_artifacts: ExecutionArtifacts, } +/// At this point we have converted transactions collected during script execution to +/// [TransactionWithMetadata] objects which contain additional metadata needed for broadcasting and +/// verification. pub struct FilledTransactionsState { pub args: ScriptArgs, pub script_config: ScriptConfig, @@ -67,6 +81,8 @@ pub struct FilledTransactionsState { pub transactions: VecDeque, } +/// State after we have bundled all [TransactionWithMetadata] objects into a single +/// [ScriptSequenceKind] object containing one or more script sequences. pub struct BundledState { pub args: ScriptArgs, pub script_config: ScriptConfig, @@ -77,6 +93,9 @@ pub struct BundledState { pub sequence: ScriptSequenceKind, } +/// State after we have broadcasted the script. +/// It is assumed that at this point [BroadcastedState::sequence] contains receipts for all +/// broadcasted transactions. pub struct BroadcastedState { pub args: ScriptArgs, pub script_config: ScriptConfig, From 43458c990cf98d007d49941391f0671a0906fbd5 Mon Sep 17 00:00:00 2001 From: Arsenii Kulikov Date: Mon, 4 Mar 2024 17:56:20 +0400 Subject: [PATCH 19/33] remove --multi flag mentions --- crates/forge/bin/cmd/script/resume.rs | 2 +- crates/forge/tests/cli/multi_script.rs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/forge/bin/cmd/script/resume.rs b/crates/forge/bin/cmd/script/resume.rs index f745e1256dd3..4756715d0bc8 100644 --- a/crates/forge/bin/cmd/script/resume.rs +++ b/crates/forge/bin/cmd/script/resume.rs @@ -58,7 +58,7 @@ impl PreSimulationState { false, )?, Err(err) => { - eyre::bail!(err.wrap_err("If you were trying to resume or verify a multi chain deployment, add `--multi` to your command invocation.")) + eyre::bail!(err.wrap_err("Failed to resume the script.")) } }; diff --git a/crates/forge/tests/cli/multi_script.rs b/crates/forge/tests/cli/multi_script.rs index d6f7628da169..121fa986269a 100644 --- a/crates/forge/tests/cli/multi_script.rs +++ b/crates/forge/tests/cli/multi_script.rs @@ -61,6 +61,5 @@ forgetest_async!(can_resume_multi_chain_script, |prj, cmd| { .broadcast(ScriptOutcome::MissingWallet) .load_private_keys(&[0, 1]) .await - .arg("--multi") .resume(ScriptOutcome::OkBroadcast); }); From 304bdf50c0aae77dcbd81fdf44552c5f84830a6e Mon Sep 17 00:00:00 2001 From: Arsenii Kulikov Date: Mon, 4 Mar 2024 21:26:40 +0400 Subject: [PATCH 20/33] docs --- crates/forge/bin/cmd/script/broadcast.rs | 1 + crates/forge/bin/cmd/script/build.rs | 190 ++++++++++++----------- crates/forge/bin/cmd/script/execute.rs | 138 +++++++++------- crates/forge/bin/cmd/script/simulate.rs | 24 ++- 4 files changed, 200 insertions(+), 153 deletions(-) diff --git a/crates/forge/bin/cmd/script/broadcast.rs b/crates/forge/bin/cmd/script/broadcast.rs index 3a651cf11f2b..329f9a612ee7 100644 --- a/crates/forge/bin/cmd/script/broadcast.rs +++ b/crates/forge/bin/cmd/script/broadcast.rs @@ -168,6 +168,7 @@ impl BundledState { Ok(self) } + /// Broadcasts transactions from all sequences. pub async fn broadcast(mut self) -> Result { let required_addresses = self .sequence diff --git a/crates/forge/bin/cmd/script/build.rs b/crates/forge/bin/cmd/script/build.rs index e4d05b28431d..966fa8f5a353 100644 --- a/crates/forge/bin/cmd/script/build.rs +++ b/crates/forge/bin/cmd/script/build.rs @@ -16,7 +16,108 @@ use foundry_compilers::{ use foundry_linking::{LinkOutput, Linker}; use std::str::FromStr; +/// Container for the compiled contracts. +pub struct BuildData { + /// Linker which can be used to link contracts, owns [ArtifactContracts] map. + pub linker: Linker, + /// Id of target contract artifact. + pub target: ArtifactId, + /// Source files of the contracts. Used by debugger. + pub sources: ContractSources, +} + +impl BuildData { + /// Links the build data with given libraries, using sender and nonce to compute addresses of + /// missing libraries. + pub fn link( + self, + known_libraries: Libraries, + sender: Address, + nonce: u64, + ) -> Result { + let link_output = + self.linker.link_with_nonce_or_address(known_libraries, sender, nonce, &self.target)?; + + LinkedBuildData::new(link_output, self) + } + + /// Links the build data with the given libraries. Expects supplied libraries set being enough + /// to fully link target contract. + pub fn link_with_libraries(self, libraries: Libraries) -> Result { + let link_output = + self.linker.link_with_nonce_or_address(libraries, Address::ZERO, 0, &self.target)?; + + if !link_output.libs_to_deploy.is_empty() { + eyre::bail!("incomplete libraries set"); + } + + LinkedBuildData::new(link_output, self) + } +} + +/// Container for the linked contracts and their dependencies +pub struct LinkedBuildData { + /// Original build data, might be used to relink this object with different libraries. + pub build_data: BuildData, + /// Known fully linked contracts. + pub highlevel_known_contracts: ArtifactContracts, + /// Libraries used to link the contracts. + pub libraries: Libraries, + /// Libraries that need to be deployed by sender before script execution. + pub predeploy_libraries: Vec, +} + +impl LinkedBuildData { + pub fn new(link_output: LinkOutput, build_data: BuildData) -> Result { + let highlevel_known_contracts = build_data + .linker + .get_linked_artifacts(&link_output.libraries)? + .iter() + .filter_map(|(id, contract)| { + ContractBytecodeSome::try_from(ContractBytecode::from(contract.clone())) + .ok() + .map(|tc| (id.clone(), tc)) + }) + .filter(|(_, tc)| tc.bytecode.object.is_non_empty_bytecode()) + .collect(); + + Ok(Self { + build_data, + highlevel_known_contracts, + libraries: link_output.libraries, + predeploy_libraries: link_output.libs_to_deploy, + }) + } + + /// Flattens the contracts into (`id` -> (`JsonAbi`, `Vec`)) pairs + pub fn get_flattened_contracts(&self, deployed_code: bool) -> ContractsByArtifact { + ContractsByArtifact( + self.highlevel_known_contracts + .iter() + .filter_map(|(id, c)| { + let bytecode = if deployed_code { + c.deployed_bytecode.bytes() + } else { + c.bytecode.bytes() + }; + bytecode.cloned().map(|code| (id.clone(), (c.abi.clone(), code.into()))) + }) + .collect(), + ) + } + + /// Fetches target bytecode from linked contracts. + pub fn get_target_contract(&self) -> Result { + self.highlevel_known_contracts + .get(&self.build_data.target) + .cloned() + .ok_or_eyre("target not found in linked artifacts") + } +} + impl PreprocessedState { + /// Parses user input and compiles the contracts depending on script target. + /// After compilation, finds exact [ArtifactId] of the target contract. pub fn compile(self) -> Result { let Self { args, script_config, script_wallets } = self; let project = script_config.config.project()?; @@ -115,95 +216,8 @@ impl PreprocessedState { } } -pub struct BuildData { - pub linker: Linker, - pub target: ArtifactId, - pub sources: ContractSources, -} - -impl BuildData { - /// Links the build data with given libraries, sender and nonce. - pub fn link( - self, - known_libraries: Libraries, - sender: Address, - nonce: u64, - ) -> Result { - let link_output = - self.linker.link_with_nonce_or_address(known_libraries, sender, nonce, &self.target)?; - - LinkedBuildData::new(link_output, self) - } - - /// Links the build data with the given libraries. - pub fn link_with_libraries(self, libraries: Libraries) -> Result { - let link_output = - self.linker.link_with_nonce_or_address(libraries, Address::ZERO, 0, &self.target)?; - - if !link_output.libs_to_deploy.is_empty() { - eyre::bail!("incomplete libraries set"); - } - - LinkedBuildData::new(link_output, self) - } -} - -pub struct LinkedBuildData { - pub build_data: BuildData, - pub highlevel_known_contracts: ArtifactContracts, - pub libraries: Libraries, - pub predeploy_libraries: Vec, -} - -impl LinkedBuildData { - pub fn new(link_output: LinkOutput, build_data: BuildData) -> Result { - let highlevel_known_contracts = build_data - .linker - .get_linked_artifacts(&link_output.libraries)? - .iter() - .filter_map(|(id, contract)| { - ContractBytecodeSome::try_from(ContractBytecode::from(contract.clone())) - .ok() - .map(|tc| (id.clone(), tc)) - }) - .filter(|(_, tc)| tc.bytecode.object.is_non_empty_bytecode()) - .collect(); - - Ok(Self { - build_data, - highlevel_known_contracts, - libraries: link_output.libraries, - predeploy_libraries: link_output.libs_to_deploy, - }) - } - - /// Flattens the contracts into (`id` -> (`JsonAbi`, `Vec`)) pairs - pub fn get_flattened_contracts(&self, deployed_code: bool) -> ContractsByArtifact { - ContractsByArtifact( - self.highlevel_known_contracts - .iter() - .filter_map(|(id, c)| { - let bytecode = if deployed_code { - c.deployed_bytecode.bytes() - } else { - c.bytecode.bytes() - }; - bytecode.cloned().map(|code| (id.clone(), (c.abi.clone(), code.into()))) - }) - .collect(), - ) - } - - /// Fetches target bytecode from linked contracts. - pub fn get_target_contract(&self) -> Result { - self.highlevel_known_contracts - .get(&self.build_data.target) - .cloned() - .ok_or_eyre("target not found in linked artifacts") - } -} - impl CompiledState { + /// Uses provided sender address to compute library addresses and link contracts with them. pub fn link(self) -> Result { let Self { args, script_config, script_wallets, build_data } = self; diff --git a/crates/forge/bin/cmd/script/execute.rs b/crates/forge/bin/cmd/script/execute.rs index c00deba19c68..e9d00ad30125 100644 --- a/crates/forge/bin/cmd/script/execute.rs +++ b/crates/forge/bin/cmd/script/execute.rs @@ -32,15 +32,21 @@ use itertools::Itertools; use std::collections::{HashMap, HashSet}; use yansi::Paint; +/// Container for data we need for execution which can only be obtained after linking stage. pub struct ExecutionData { + /// Function to call. pub func: Function, + /// Calldata to pass to the target contract. pub calldata: Bytes, + /// Bytecode of the target contract. pub bytecode: Bytes, + /// ABI of the target contract. pub abi: JsonAbi, } impl LinkedState { /// Given linked and compiled artifacts, prepares data we need for execution. + /// This includes the function to call and the calldata to pass to it. pub async fn prepare_execution(self) -> Result { let Self { args, script_config, script_wallets, build_data } = self; @@ -65,6 +71,8 @@ impl LinkedState { } impl PreExecutionState { + /// Executes the script and returns the state after execution. + /// Might require executing script twice in cases when we determine sender from execution. #[async_recursion] pub async fn execute(mut self) -> Result { let mut runner = self @@ -78,7 +86,7 @@ impl PreExecutionState { if let Some(new_sender) = self.maybe_new_sender(result.transactions.as_ref())? { self.script_config.update_sender(new_sender).await?; - // Rollback to linking state to relink contracts with the new sender. + // Rollback to rerun linking with the new sender. let state = CompiledState { args: self.args, script_config: self.script_config, @@ -120,6 +128,7 @@ impl PreExecutionState { }) } + /// Executes the script using the provided runner and returns the [ScriptResult]. pub async fn execute_with_runner(&self, runner: &mut ScriptRunner) -> Result { let (address, mut setup_result) = runner.setup( &self.build_data.predeploy_libraries, @@ -156,6 +165,10 @@ impl PreExecutionState { Ok(setup_result) } + /// It finds the deployer from the running script and uses it to predeploy libraries. + /// + /// If there are multiple candidate addresses, it skips everything and lets `--sender` deploy + /// them instead. fn maybe_new_sender( &self, transactions: Option<&BroadcastableTransactions>, @@ -186,7 +199,72 @@ impl PreExecutionState { } } +/// Container for information about RPC-endpoints used during script execution. +pub struct RpcData { + /// Unique list of rpc urls present. + pub total_rpcs: HashSet, + /// If true, one of the transactions did not have a rpc. + pub missing_rpc: bool, +} + +impl RpcData { + /// Iterates over script transactions and collects RPC urls. + fn from_transactions(txs: &BroadcastableTransactions) -> Self { + let missing_rpc = txs.iter().any(|tx| tx.rpc.is_none()); + let total_rpcs = + txs.iter().filter_map(|tx| tx.rpc.as_ref().cloned()).collect::>(); + + Self { total_rpcs, missing_rpc } + } + + /// Returns true if script might be multi-chain. + /// Returns false positive in case when missing rpc is the same as the only rpc present. + pub fn is_multi_chain(&self) -> bool { + self.total_rpcs.len() > 1 || (self.missing_rpc && !self.total_rpcs.is_empty()) + } + + /// Checks if all RPCs support EIP-3855. Prints a warning if not. + async fn check_shanghai_support(&self) -> Result<()> { + let chain_ids = self.total_rpcs.iter().map(|rpc| async move { + let provider = get_http_provider(rpc); + let id = provider.get_chainid().await.ok()?; + let id_u64: u64 = id.try_into().ok()?; + NamedChain::try_from(id_u64).ok() + }); + + let chains = join_all(chain_ids).await; + let iter = chains.iter().flatten().map(|c| (c.supports_shanghai(), c)); + if iter.clone().any(|(s, _)| !s) { + let msg = format!( + "\ +EIP-3855 is not supported in one or more of the RPCs used. +Unsupported Chain IDs: {}. +Contracts deployed with a Solidity version equal or higher than 0.8.20 might not work properly. +For more information, please see https://eips.ethereum.org/EIPS/eip-3855", + iter.filter(|(supported, _)| !supported) + .map(|(_, chain)| *chain as u64) + .format(", ") + ); + shell::println(Paint::yellow(msg))?; + } + Ok(()) + } +} + +/// Container for data being collected after execution. +pub struct ExecutionArtifacts { + /// Mapping from contract to its runtime code. + pub known_contracts: ContractsByArtifact, + /// Trace decoder used to decode traces. + pub decoder: CallTraceDecoder, + /// Return values from the execution result. + pub returns: HashMap, + /// Information about RPC endpoints used during script execution. + pub rpc_data: RpcData, +} + impl ExecutedState { + /// Collects the data we need for simulation and various post-execution tasks. pub async fn prepare_simulation(self) -> Result { let returns = self.get_returns()?; @@ -222,6 +300,7 @@ impl ExecutedState { }) } + /// Builds [CallTraceDecoder] from the execution result and known contracts. fn build_trace_decoder( &self, known_contracts: &ContractsByArtifact, @@ -258,6 +337,7 @@ impl ExecutedState { Ok(decoder) } + /// Collects the return values from the execution result. fn get_returns(&self) -> Result> { let mut returns = HashMap::new(); let returned = &self.execution_result.returned; @@ -296,62 +376,6 @@ impl ExecutedState { } } -pub struct RpcData { - /// Unique list of rpc urls present - pub total_rpcs: HashSet, - /// If true, one of the transactions did not have a rpc - pub missing_rpc: bool, -} - -impl RpcData { - fn from_transactions(txs: &BroadcastableTransactions) -> Self { - let missing_rpc = txs.iter().any(|tx| tx.rpc.is_none()); - let total_rpcs = - txs.iter().filter_map(|tx| tx.rpc.as_ref().cloned()).collect::>(); - - Self { total_rpcs, missing_rpc } - } - - /// Returns true if script might be multi-chain. - /// Returns false positive in case when missing rpc is the same as the only rpc present. - pub fn is_multi_chain(&self) -> bool { - self.total_rpcs.len() > 1 || (self.missing_rpc && !self.total_rpcs.is_empty()) - } - - async fn check_shanghai_support(&self) -> Result<()> { - let chain_ids = self.total_rpcs.iter().map(|rpc| async move { - let provider = get_http_provider(rpc); - let id = provider.get_chainid().await.ok()?; - let id_u64: u64 = id.try_into().ok()?; - NamedChain::try_from(id_u64).ok() - }); - - let chains = join_all(chain_ids).await; - let iter = chains.iter().flatten().map(|c| (c.supports_shanghai(), c)); - if iter.clone().any(|(s, _)| !s) { - let msg = format!( - "\ -EIP-3855 is not supported in one or more of the RPCs used. -Unsupported Chain IDs: {}. -Contracts deployed with a Solidity version equal or higher than 0.8.20 might not work properly. -For more information, please see https://eips.ethereum.org/EIPS/eip-3855", - iter.filter(|(supported, _)| !supported) - .map(|(_, chain)| *chain as u64) - .format(", ") - ); - shell::println(Paint::yellow(msg))?; - } - Ok(()) - } -} - -pub struct ExecutionArtifacts { - pub known_contracts: ContractsByArtifact, - pub decoder: CallTraceDecoder, - pub returns: HashMap, - pub rpc_data: RpcData, -} - impl PreSimulationState { pub fn show_json(&self) -> Result<()> { let result = &self.execution_result; diff --git a/crates/forge/bin/cmd/script/simulate.rs b/crates/forge/bin/cmd/script/simulate.rs index 7fec39d46bcf..3edc7681a686 100644 --- a/crates/forge/bin/cmd/script/simulate.rs +++ b/crates/forge/bin/cmd/script/simulate.rs @@ -52,6 +52,10 @@ impl PreSimulationState { }) } + /// Builds separate runners and environments for each RPC used in script and executes all + /// transactions in those environments. + /// + /// Collects gas usage and metadata for each transaction. pub async fn onchain_simulation( &self, transactions: BroadcastableTransactions, @@ -161,6 +165,7 @@ impl PreSimulationState { Ok(final_txs) } + /// Build mapping from contract address to its ABI, code and contract name. fn build_address_to_abi_map<'a>( &self, contracts: &'a ContractsByArtifact, @@ -187,20 +192,18 @@ impl PreSimulationState { .collect() } - /// Build the multiple runners from different forks. + /// Build [ScriptRunner] forking given RPC for each RPC used in the script. async fn build_runners(&self) -> Result> { + let rpcs = self.execution_artifacts.rpc_data.total_rpcs.clone(); if !shell::verbosity().is_silent() { - let n = self.execution_artifacts.rpc_data.total_rpcs.len(); + let n = rpcs.len(); let s = if n != 1 { "s" } else { "" }; println!("\n## Setting up {n} EVM{s}."); } - let futs = self - .execution_artifacts - .rpc_data - .total_rpcs - .iter() - .map(|rpc| async { + let futs = rpcs + .into_iter() + .map(|rpc| async move { let mut script_config = self.script_config.clone(); script_config.evm_opts.fork_url = Some(rpc.clone()); let runner = script_config.get_runner().await?; @@ -211,6 +214,8 @@ impl PreSimulationState { join_all(futs).await.into_iter().collect() } + /// If simulation is disabled, converts transactions into [TransactionWithMetadata] type + /// skipping metadata filling. fn no_simulation( &self, transactions: BroadcastableTransactions, @@ -368,12 +373,15 @@ impl FilledTransactionsState { }) } + /// Creates a [ScriptSequence] object from the given transactions. fn create_sequence( &self, multi: bool, chain: u64, transactions: VecDeque, ) -> Result { + // Paths are set to None for multi-chain sequences parts, because they don't need to be + // saved to a separate file. let paths = if multi { None } else { From 1f449ecc1a24c84551f5a5321810e99e2beb2499 Mon Sep 17 00:00:00 2001 From: Arsenii Kulikov Date: Mon, 4 Mar 2024 22:05:05 +0400 Subject: [PATCH 21/33] checkpoint saves for multichain sequences --- crates/forge/bin/cmd/script/broadcast.rs | 14 +++++++++----- crates/forge/bin/cmd/script/sequence.rs | 20 ++++++++++++++++++++ 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/crates/forge/bin/cmd/script/broadcast.rs b/crates/forge/bin/cmd/script/broadcast.rs index 329f9a612ee7..cc900f2a2481 100644 --- a/crates/forge/bin/cmd/script/broadcast.rs +++ b/crates/forge/bin/cmd/script/broadcast.rs @@ -209,7 +209,9 @@ impl BundledState { SendTransactionsKind::Raw(signers) }; - for sequence in self.sequence.iter_sequeneces_mut() { + for i in 0..self.sequence.sequences_len() { + let mut sequence = self.sequence.get_sequence_mut(i).unwrap(); + let provider = Arc::new(try_get_http_provider(sequence.rpc_url())?); let already_broadcasted = sequence.receipts.len(); @@ -326,21 +328,23 @@ impl BundledState { sequence.add_pending(index, tx_hash); // Checkpoint save - sequence.save(true, false)?; + self.sequence.save(true, false)?; + sequence = self.sequence.get_sequence_mut(i).unwrap(); update_progress!(pb, index - already_broadcasted); index += 1; } // Checkpoint save - sequence.save(true, false)?; + self.sequence.save(true, false)?; + sequence = self.sequence.get_sequence_mut(i).unwrap(); shell::println("##\nWaiting for receipts.")?; receipts::clear_pendings(provider.clone(), sequence, None).await?; } - // Checkpoint save - sequence.save(true, false)?; + self.sequence.save(true, false)?; + sequence = self.sequence.get_sequence_mut(i).unwrap(); } } diff --git a/crates/forge/bin/cmd/script/sequence.rs b/crates/forge/bin/cmd/script/sequence.rs index ee3b0c01a42e..69850639f4d3 100644 --- a/crates/forge/bin/cmd/script/sequence.rs +++ b/crates/forge/bin/cmd/script/sequence.rs @@ -52,6 +52,26 @@ impl ScriptSequenceKind { ScriptSequenceKind::Multi(sequence) => sequence.deployments.iter_mut(), } } + + pub fn sequences_len(&self) -> usize { + match self { + ScriptSequenceKind::Single(_) => 1, + ScriptSequenceKind::Multi(sequence) => sequence.deployments.len(), + } + } + + pub fn get_sequence_mut(&mut self, index: usize) -> Option<&mut ScriptSequence> { + match self { + ScriptSequenceKind::Single(sequence) => { + if index == 0 { + Some(sequence) + } else { + None + } + } + ScriptSequenceKind::Multi(sequence) => sequence.deployments.get_mut(index), + } + } } impl Drop for ScriptSequenceKind { From 4f2d6d4163ada8e0cc99e1cab252f89d087a9740 Mon Sep 17 00:00:00 2001 From: Arsenii Kulikov Date: Tue, 5 Mar 2024 23:54:08 +0400 Subject: [PATCH 22/33] docs + broadcasted renamed into dry_run --- crates/forge/bin/cmd/script/cmd.rs | 9 +- crates/forge/bin/cmd/script/multi_sequence.rs | 33 ++--- crates/forge/bin/cmd/script/resume.rs | 119 ++++++++++-------- crates/forge/bin/cmd/script/sequence.rs | 29 ++++- crates/forge/bin/cmd/script/simulate.rs | 4 +- 5 files changed, 115 insertions(+), 79 deletions(-) diff --git a/crates/forge/bin/cmd/script/cmd.rs b/crates/forge/bin/cmd/script/cmd.rs index 9d6cc3e8264a..4c853c444489 100644 --- a/crates/forge/bin/cmd/script/cmd.rs +++ b/crates/forge/bin/cmd/script/cmd.rs @@ -26,6 +26,7 @@ impl ScriptArgs { pub async fn run_script(self) -> Result<()> { trace!(target: "script", "executing script command"); + // Drive state machine to point at which we have everything needed for simulation/resuming. let pre_simulation = self .preprocess() .await? @@ -48,11 +49,14 @@ impl ScriptArgs { pre_simulation.show_traces().await?; } + // Ensure that we have transactions to simulate/broadcast, otherwise exit early to avoid + // hard error. if pre_simulation.execution_result.transactions.as_ref().map_or(true, |txs| txs.is_empty()) { return Ok(()); } + // Check if there are any missing RPCs and exit early to avoid hard error. if pre_simulation.execution_artifacts.rpc_data.missing_rpc { shell::println("\nIf you wish to simulate on-chain transactions pass a RPC URL.")?; return Ok(()); @@ -73,15 +77,18 @@ impl ScriptArgs { pre_simulation.fill_metadata().await?.bundle().await? }; - if !bundled.args.broadcast && !bundled.args.resume { + // Exit early in case user didn't provide any broadcast/verify related flags. + if !bundled.args.broadcast && !bundled.args.resume && !bundled.args.verify { shell::println("\nSIMULATION COMPLETE. To broadcast these transactions, add --broadcast and wallet configuration(s) to the previous command. See forge script --help for more.")?; return Ok(()); } + // Exit early if something is wrong with verification options. if bundled.args.verify { bundled.verify_preflight_check()?; } + // Wait for pending txes and broadcast others. let broadcasted = bundled.wait_for_pending().await?.broadcast().await?; if broadcasted.args.verify { diff --git a/crates/forge/bin/cmd/script/multi_sequence.rs b/crates/forge/bin/cmd/script/multi_sequence.rs index f2bcca69bdcf..fd58d30f24c6 100644 --- a/crates/forge/bin/cmd/script/multi_sequence.rs +++ b/crates/forge/bin/cmd/script/multi_sequence.rs @@ -7,7 +7,7 @@ use foundry_config::Config; use serde::{Deserialize, Serialize}; use std::{ io::{BufWriter, Write}, - path::{Path, PathBuf}, + path::PathBuf, }; /// Holds the sequences of multiple chain deployments. @@ -39,15 +39,9 @@ impl MultiChainSequence { sig: &str, target: &ArtifactId, config: &Config, - broadcasted: bool, + dry_run: bool, ) -> Result { - let (path, sensitive_path) = MultiChainSequence::get_paths( - &config.broadcast, - &config.cache_path, - sig, - target, - broadcasted, - )?; + let (path, sensitive_path) = MultiChainSequence::get_paths(config, sig, target, dry_run)?; Ok(MultiChainSequence { deployments, path, sensitive_path, timestamp: now().as_secs() }) } @@ -56,19 +50,18 @@ impl MultiChainSequence { /// ./broadcast/multi/contract_filename[-timestamp]/sig.json and /// ./cache/multi/contract_filename[-timestamp]/sig.json pub fn get_paths( - broadcast: &Path, - cache: &Path, + config: &Config, sig: &str, target: &ArtifactId, - broadcasted: bool, + dry_run: bool, ) -> Result<(PathBuf, PathBuf)> { - let mut broadcast = broadcast.to_path_buf(); - let mut cache = cache.to_path_buf(); + let mut broadcast = config.broadcast.to_path_buf(); + let mut cache = config.cache_path.to_path_buf(); let mut common = PathBuf::new(); common.push("multi"); - if !broadcasted { + if dry_run { common.push(DRY_RUN_DIR); } @@ -95,14 +88,8 @@ impl MultiChainSequence { } /// Loads the sequences for the multi chain deployment. - pub fn load(config: &Config, sig: &str, target: &ArtifactId) -> Result { - let (path, sensitive_path) = MultiChainSequence::get_paths( - &config.broadcast, - &config.cache_path, - sig, - target, - true, - )?; + pub fn load(config: &Config, sig: &str, target: &ArtifactId, dry_run: bool) -> Result { + let (path, sensitive_path) = MultiChainSequence::get_paths(config, sig, target, dry_run)?; let mut sequence: MultiChainSequence = foundry_compilers::utils::read_json_file(&path) .wrap_err("Multi-chain deployment not found.")?; let sensitive_sequence: SensitiveMultiChainSequence = diff --git a/crates/forge/bin/cmd/script/resume.rs b/crates/forge/bin/cmd/script/resume.rs index 4756715d0bc8..7f4d030e4c0c 100644 --- a/crates/forge/bin/cmd/script/resume.rs +++ b/crates/forge/bin/cmd/script/resume.rs @@ -13,66 +13,66 @@ use super::{ impl PreSimulationState { /// Tries loading the resumed state from the cache files, skipping simulation stage. - pub async fn resume(self) -> Result { - let Self { - args, - script_config, - script_wallets, - mut build_data, - execution_data, - execution_result: _, - execution_artifacts, - } = self; - - if execution_artifacts.rpc_data.missing_rpc { + pub async fn resume(mut self) -> Result { + if self.execution_artifacts.rpc_data.missing_rpc { eyre::bail!("Missing `--fork-url` field.") } - let sequence = match execution_artifacts.rpc_data.total_rpcs.len() { - 2.. => ScriptSequenceKind::Multi(MultiChainSequence::load( - &script_config.config, - &args.sig, - &build_data.build_data.target, - )?), + let chain = match self.execution_artifacts.rpc_data.total_rpcs.len() { + 2.. => None, 1 => { - let fork_url = execution_artifacts.rpc_data.total_rpcs.iter().next().unwrap(); + let fork_url = self.execution_artifacts.rpc_data.total_rpcs.iter().next().unwrap(); let provider = Arc::new(try_get_http_provider(fork_url)?); - let chain = provider.get_chainid().await?.as_u64(); - - let seq = match ScriptSequence::load( - &script_config.config, - &args.sig, - &build_data.build_data.target, - chain, - args.broadcast, - ) { - Ok(seq) => seq, - // If the script was simulated, but there was no attempt to broadcast yet, - // try to read the script sequence from the `dry-run/` folder - Err(_) if args.broadcast => ScriptSequence::load( - &script_config.config, - &args.sig, - &build_data.build_data.target, - chain, - false, - )?, - Err(err) => { - eyre::bail!(err.wrap_err("Failed to resume the script.")) - } - }; + Some(provider.get_chainid().await?.as_u64()) + } + 0 => eyre::bail!("No RPC URLs"), + }; + + let sequence = match self.try_load_sequence(chain, false) { + Ok(sequence) => sequence, + Err(_) => { + // If the script was simulated, but there was no attempt to broadcast yet, + // try to read the script sequence from the `dry-run/` folder + let mut sequence = self.try_load_sequence(chain, true)?; - // We might have predeployed libraries from the broadcasting, so we need to - // relink the contracts with them, since their mapping is - // not included in the solc cache files. - build_data = - build_data.build_data.link_with_libraries(Libraries::parse(&seq.libraries)?)?; + // If sequence was in /dry-run, Update its paths so it is not saved into /dry-run + // this time as we are about to broadcast it. + sequence.update_paths_to_broadcasted( + &self.script_config.config, + &self.args.sig, + &self.build_data.build_data.target, + )?; - ScriptSequenceKind::Single(seq) + sequence.save(true, true)?; + sequence } - 0 => eyre::bail!("No RPC URLs"), }; + match sequence { + ScriptSequenceKind::Single(ref seq) => { + // We might have predeployed libraries from the broadcasting, so we need to + // relink the contracts with them, since their mapping is not included in the solc + // cache files. + self.build_data = self + .build_data + .build_data + .link_with_libraries(Libraries::parse(&seq.libraries)?)?; + } + // Library linking is not supported for multi-chain sequences + ScriptSequenceKind::Multi(_) => {} + } + + let Self { + args, + script_config, + script_wallets, + build_data, + execution_data, + execution_result: _, + execution_artifacts, + } = self; + Ok(BundledState { args, script_config, @@ -83,4 +83,25 @@ impl PreSimulationState { sequence, }) } + + fn try_load_sequence(&self, chain: Option, dry_run: bool) -> Result { + if let Some(chain) = chain { + let sequence = ScriptSequence::load( + &self.script_config.config, + &self.args.sig, + &self.build_data.build_data.target, + chain, + dry_run, + )?; + Ok(ScriptSequenceKind::Single(sequence)) + } else { + let sequence = MultiChainSequence::load( + &self.script_config.config, + &self.args.sig, + &self.build_data.build_data.target, + dry_run, + )?; + Ok(ScriptSequenceKind::Multi(sequence)) + } + } } diff --git a/crates/forge/bin/cmd/script/sequence.rs b/crates/forge/bin/cmd/script/sequence.rs index 69850639f4d3..b07d57a2a111 100644 --- a/crates/forge/bin/cmd/script/sequence.rs +++ b/crates/forge/bin/cmd/script/sequence.rs @@ -72,6 +72,27 @@ impl ScriptSequenceKind { ScriptSequenceKind::Multi(sequence) => sequence.deployments.get_mut(index), } } + + /// Updates underlying sequence paths to not be under /dry-run directory. + pub fn update_paths_to_broadcasted( + &mut self, + config: &Config, + sig: &str, + target: &ArtifactId, + ) -> Result<()> { + match self { + ScriptSequenceKind::Single(sequence) => { + sequence.paths = + Some(ScriptSequence::get_paths(config, sig, target, sequence.chain, false)?); + } + ScriptSequenceKind::Multi(sequence) => { + (sequence.path, sequence.sensitive_path) = + MultiChainSequence::get_paths(config, sig, target, false)?; + } + }; + + Ok(()) + } } impl Drop for ScriptSequenceKind { @@ -132,10 +153,10 @@ impl ScriptSequence { sig: &str, target: &ArtifactId, chain_id: u64, - broadcasted: bool, + dry_run: bool, ) -> Result { let (path, sensitive_path) = - ScriptSequence::get_paths(config, sig, target, chain_id, broadcasted)?; + ScriptSequence::get_paths(config, sig, target, chain_id, dry_run)?; let mut script_sequence: Self = foundry_compilers::utils::read_json_file(&path) .wrap_err(format!("Deployment not found for chain `{chain_id}`."))?; @@ -225,7 +246,7 @@ impl ScriptSequence { sig: &str, target: &ArtifactId, chain_id: u64, - broadcasted: bool, + dry_run: bool, ) -> Result<(PathBuf, PathBuf)> { let mut broadcast = config.broadcast.to_path_buf(); let mut cache = config.cache_path.to_path_buf(); @@ -234,7 +255,7 @@ impl ScriptSequence { let target_fname = target.source.file_name().wrap_err("No filename.")?; common.push(target_fname); common.push(chain_id.to_string()); - if !broadcasted { + if dry_run { common.push(DRY_RUN_DIR); } diff --git a/crates/forge/bin/cmd/script/simulate.rs b/crates/forge/bin/cmd/script/simulate.rs index 3edc7681a686..a2850cc75372 100644 --- a/crates/forge/bin/cmd/script/simulate.rs +++ b/crates/forge/bin/cmd/script/simulate.rs @@ -358,7 +358,7 @@ impl FilledTransactionsState { &self.args.sig, &self.build_data.build_data.target, &self.script_config.config, - self.args.broadcast, + !self.args.broadcast, )?) }; @@ -390,7 +390,7 @@ impl FilledTransactionsState { &self.args.sig, &self.build_data.build_data.target, chain, - self.args.broadcast, + !self.args.broadcast, )?) }; From 89d6e73c75c8048410f6c869389552d5b01ea47c Mon Sep 17 00:00:00 2001 From: Arsenii Kulikov Date: Wed, 6 Mar 2024 00:18:32 +0400 Subject: [PATCH 23/33] fmt --- crates/forge/bin/cmd/script/resume.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/forge/bin/cmd/script/resume.rs b/crates/forge/bin/cmd/script/resume.rs index 7f4d030e4c0c..fd1a29c2292f 100644 --- a/crates/forge/bin/cmd/script/resume.rs +++ b/crates/forge/bin/cmd/script/resume.rs @@ -28,7 +28,7 @@ impl PreSimulationState { } 0 => eyre::bail!("No RPC URLs"), }; - + let sequence = match self.try_load_sequence(chain, false) { Ok(sequence) => sequence, Err(_) => { From 0fd0c4246b52dfcfa93f14fff716f7a91f74dc3c Mon Sep 17 00:00:00 2001 From: Arsenii Kulikov Date: Wed, 6 Mar 2024 00:19:07 +0400 Subject: [PATCH 24/33] Update crates/forge/bin/cmd/script/resume.rs Co-authored-by: Matthias Seitz --- crates/forge/bin/cmd/script/resume.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/forge/bin/cmd/script/resume.rs b/crates/forge/bin/cmd/script/resume.rs index fd1a29c2292f..26f49b70eb8b 100644 --- a/crates/forge/bin/cmd/script/resume.rs +++ b/crates/forge/bin/cmd/script/resume.rs @@ -1,5 +1,4 @@ use std::sync::Arc; - use ethers_providers::Middleware; use eyre::Result; use foundry_common::provider::ethers::try_get_http_provider; From 1ccf3abb3e62996eaa637f598e8e4d3dc88e04ca Mon Sep 17 00:00:00 2001 From: Arsenii Kulikov Date: Wed, 6 Mar 2024 00:33:05 +0400 Subject: [PATCH 25/33] fmt --- crates/forge/bin/cmd/script/resume.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/forge/bin/cmd/script/resume.rs b/crates/forge/bin/cmd/script/resume.rs index 26f49b70eb8b..13714004930c 100644 --- a/crates/forge/bin/cmd/script/resume.rs +++ b/crates/forge/bin/cmd/script/resume.rs @@ -1,8 +1,8 @@ -use std::sync::Arc; use ethers_providers::Middleware; use eyre::Result; use foundry_common::provider::ethers::try_get_http_provider; use foundry_compilers::artifacts::Libraries; +use std::sync::Arc; use super::{ multi_sequence::MultiChainSequence, From 8e21bba29c46d4412d121f2208eef96f18e813f1 Mon Sep 17 00:00:00 2001 From: Arsenii Kulikov Date: Wed, 6 Mar 2024 19:47:11 +0400 Subject: [PATCH 26/33] review fixes --- crates/forge/bin/cmd/script/broadcast.rs | 4 ++-- crates/forge/bin/cmd/script/resume.rs | 11 +++++------ crates/forge/bin/cmd/script/sequence.rs | 4 ++-- crates/forge/bin/cmd/script/simulate.rs | 3 +-- crates/forge/bin/cmd/script/states.rs | 6 ++---- crates/forge/bin/cmd/script/verify.rs | 7 +++---- 6 files changed, 15 insertions(+), 20 deletions(-) diff --git a/crates/forge/bin/cmd/script/broadcast.rs b/crates/forge/bin/cmd/script/broadcast.rs index cc900f2a2481..b29bc2350df1 100644 --- a/crates/forge/bin/cmd/script/broadcast.rs +++ b/crates/forge/bin/cmd/script/broadcast.rs @@ -148,7 +148,7 @@ impl BundledState { pub async fn wait_for_pending(mut self) -> Result { let futs = self .sequence - .iter_sequeneces_mut() + .sequeneces_mut() .map(|sequence| async move { let rpc_url = sequence.rpc_url(); let provider = Arc::new(get_http_provider(rpc_url)); @@ -172,7 +172,7 @@ impl BundledState { pub async fn broadcast(mut self) -> Result { let required_addresses = self .sequence - .iter_sequences() + .sequences() .flat_map(|sequence| { sequence .typed_transactions() diff --git a/crates/forge/bin/cmd/script/resume.rs b/crates/forge/bin/cmd/script/resume.rs index 13714004930c..dba2588a74e5 100644 --- a/crates/forge/bin/cmd/script/resume.rs +++ b/crates/forge/bin/cmd/script/resume.rs @@ -1,14 +1,13 @@ -use ethers_providers::Middleware; -use eyre::Result; -use foundry_common::provider::ethers::try_get_http_provider; -use foundry_compilers::artifacts::Libraries; -use std::sync::Arc; - use super::{ multi_sequence::MultiChainSequence, sequence::{ScriptSequence, ScriptSequenceKind}, states::{BundledState, PreSimulationState}, }; +use ethers_providers::Middleware; +use eyre::Result; +use foundry_common::provider::ethers::try_get_http_provider; +use foundry_compilers::artifacts::Libraries; +use std::sync::Arc; impl PreSimulationState { /// Tries loading the resumed state from the cache files, skipping simulation stage. diff --git a/crates/forge/bin/cmd/script/sequence.rs b/crates/forge/bin/cmd/script/sequence.rs index b07d57a2a111..9825e4c92242 100644 --- a/crates/forge/bin/cmd/script/sequence.rs +++ b/crates/forge/bin/cmd/script/sequence.rs @@ -39,14 +39,14 @@ impl ScriptSequenceKind { } } - pub fn iter_sequences(&self) -> impl Iterator { + pub fn sequences(&self) -> impl Iterator { match self { ScriptSequenceKind::Single(sequence) => std::slice::from_ref(sequence).iter(), ScriptSequenceKind::Multi(sequence) => sequence.deployments.iter(), } } - pub fn iter_sequeneces_mut(&mut self) -> impl Iterator { + pub fn sequeneces_mut(&mut self) -> impl Iterator { match self { ScriptSequenceKind::Single(sequence) => std::slice::from_mut(sequence).iter_mut(), ScriptSequenceKind::Multi(sequence) => sequence.deployments.iter_mut(), diff --git a/crates/forge/bin/cmd/script/simulate.rs b/crates/forge/bin/cmd/script/simulate.rs index a2850cc75372..7d3f3106d2ea 100644 --- a/crates/forge/bin/cmd/script/simulate.rs +++ b/crates/forge/bin/cmd/script/simulate.rs @@ -1,5 +1,3 @@ -use crate::cmd::{init::get_commit_hash, script::broadcast::estimate_gas}; - use super::{ artifacts::ArtifactInfo, multi_sequence::MultiChainSequence, @@ -9,6 +7,7 @@ use super::{ states::{BundledState, FilledTransactionsState, PreSimulationState}, transaction::TransactionWithMetadata, }; +use crate::cmd::{init::get_commit_hash, script::broadcast::estimate_gas}; use alloy_primitives::{utils::format_units, Address, U256}; use eyre::{Context, Result}; use forge::{inspectors::cheatcodes::BroadcastableTransactions, traces::render_trace_arena}; diff --git a/crates/forge/bin/cmd/script/states.rs b/crates/forge/bin/cmd/script/states.rs index d2a7e8fc9327..61936ddd788b 100644 --- a/crates/forge/bin/cmd/script/states.rs +++ b/crates/forge/bin/cmd/script/states.rs @@ -1,7 +1,3 @@ -use std::collections::VecDeque; - -use forge::inspectors::cheatcodes::ScriptWallets; - use super::{ build::{BuildData, LinkedBuildData}, execute::{ExecutionArtifacts, ExecutionData}, @@ -9,6 +5,8 @@ use super::{ transaction::TransactionWithMetadata, ScriptArgs, ScriptConfig, ScriptResult, }; +use forge::inspectors::cheatcodes::ScriptWallets; +use std::collections::VecDeque; /// First state basically containing only inputs of the user. pub struct PreprocessedState { diff --git a/crates/forge/bin/cmd/script/verify.rs b/crates/forge/bin/cmd/script/verify.rs index 086440bec07c..927854f8f23b 100644 --- a/crates/forge/bin/cmd/script/verify.rs +++ b/crates/forge/bin/cmd/script/verify.rs @@ -1,3 +1,4 @@ +use super::states::{BroadcastedState, BundledState}; use alloy_primitives::Address; use eyre::Result; use forge_verify::{provider::VerificationProviderType, RetryArgs, VerifierArgs, VerifyArgs}; @@ -7,11 +8,9 @@ use foundry_compilers::{info::ContractInfo, Project}; use foundry_config::{Chain, Config}; use semver::Version; -use super::states::{BroadcastedState, BundledState}; - impl BundledState { pub fn verify_preflight_check(&self) -> Result<()> { - for sequence in self.sequence.iter_sequences() { + for sequence in self.sequence.sequences() { if self.args.verifier.verifier == VerificationProviderType::Etherscan && self.script_config .config @@ -38,7 +37,7 @@ impl BroadcastedState { args.verifier, ); - for sequence in sequence.iter_sequeneces_mut() { + for sequence in sequence.sequeneces_mut() { sequence.verify_contracts(&script_config.config, verify.clone()).await?; } From be812de763567bb6fa3e33753415fb1524fc8252 Mon Sep 17 00:00:00 2001 From: Arsenii Kulikov Date: Wed, 6 Mar 2024 22:43:58 +0400 Subject: [PATCH 27/33] fmt --- crates/forge/bin/cmd/script/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/forge/bin/cmd/script/mod.rs b/crates/forge/bin/cmd/script/mod.rs index 6192b0e1cb8c..d9c70c667af5 100644 --- a/crates/forge/bin/cmd/script/mod.rs +++ b/crates/forge/bin/cmd/script/mod.rs @@ -1,6 +1,6 @@ -use crate::cmd::script::runner::ScriptRunner; -use super::build::BuildArgs; use self::transaction::AdditionalContract; +use super::build::BuildArgs; +use crate::cmd::script::runner::ScriptRunner; use alloy_json_abi::{Function, JsonAbi}; use alloy_primitives::{Address, Bytes, Log, U256}; use clap::{Parser, ValueHint}; From d430da970f9fd8f38392eb541529aa558edd1520 Mon Sep 17 00:00:00 2001 From: Arsenii Kulikov Date: Thu, 7 Mar 2024 00:31:09 +0400 Subject: [PATCH 28/33] wip: start extracting to separate crate --- Cargo.lock | 41 +++++++++++++++- Cargo.toml | 1 + crates/forge/Cargo.toml | 2 +- crates/forge/bin/cmd/init.rs | 5 -- crates/forge/bin/opts.rs | 3 +- crates/forge/src/lib.rs | 20 -------- crates/script/Cargo.toml | 47 +++++++++++++++++++ .../cmd/script => script/src}/artifacts.rs | 0 .../cmd/script => script/src}/broadcast.rs | 13 ++++- .../bin/cmd/script => script/src}/build.rs | 0 .../bin/cmd/script => script/src}/cmd.rs | 2 +- .../bin/cmd/script => script/src}/execute.rs | 16 +++---- .../cmd/script/mod.rs => script/src/lib.rs} | 33 ++++++++----- .../script => script/src}/multi_sequence.rs | 0 .../cmd/script => script/src}/providers.rs | 0 .../bin/cmd/script => script/src}/receipts.rs | 0 .../bin/cmd/script => script/src}/resume.rs | 0 .../bin/cmd/script => script/src}/runner.rs | 4 +- .../bin/cmd/script => script/src}/sequence.rs | 18 +++---- .../bin/cmd/script => script/src}/simulate.rs | 5 +- .../bin/cmd/script => script/src}/states.rs | 2 +- .../cmd/script => script/src}/transaction.rs | 0 .../bin/cmd/script => script/src}/verify.rs | 0 23 files changed, 148 insertions(+), 64 deletions(-) create mode 100644 crates/script/Cargo.toml rename crates/{forge/bin/cmd/script => script/src}/artifacts.rs (100%) rename crates/{forge/bin/cmd/script => script/src}/broadcast.rs (97%) rename crates/{forge/bin/cmd/script => script/src}/build.rs (100%) rename crates/{forge/bin/cmd/script => script/src}/cmd.rs (98%) rename crates/{forge/bin/cmd/script => script/src}/execute.rs (99%) rename crates/{forge/bin/cmd/script/mod.rs => script/src/lib.rs} (98%) rename crates/{forge/bin/cmd/script => script/src}/multi_sequence.rs (100%) rename crates/{forge/bin/cmd/script => script/src}/providers.rs (100%) rename crates/{forge/bin/cmd/script => script/src}/receipts.rs (100%) rename crates/{forge/bin/cmd/script => script/src}/resume.rs (100%) rename crates/{forge/bin/cmd/script => script/src}/runner.rs (99%) rename crates/{forge/bin/cmd/script => script/src}/sequence.rs (97%) rename crates/{forge/bin/cmd/script => script/src}/simulate.rs (98%) rename crates/{forge/bin/cmd/script => script/src}/states.rs (98%) rename crates/{forge/bin/cmd/script => script/src}/transaction.rs (100%) rename crates/{forge/bin/cmd/script => script/src}/verify.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index 5cd851c7128e..2ba1addf6cf5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2905,7 +2905,6 @@ dependencies = [ "alloy-primitives", "alloy-rpc-types", "anvil", - "async-recursion", "async-trait", "axum", "clap", @@ -2925,6 +2924,7 @@ dependencies = [ "eyre", "forge-doc", "forge-fmt", + "forge-script", "forge-verify", "foundry-block-explorers", "foundry-cli", @@ -3011,6 +3011,45 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "forge-script" +version = "0.2.0" +dependencies = [ + "alloy-dyn-abi", + "alloy-json-abi", + "alloy-primitives", + "alloy-rpc-types", + "async-recursion", + "clap", + "const-hex", + "dialoguer", + "dunce", + "ethers-core", + "ethers-providers", + "ethers-signers", + "eyre", + "forge-verify", + "foundry-cheatcodes", + "foundry-cli", + "foundry-common", + "foundry-compilers", + "foundry-config", + "foundry-debugger", + "foundry-evm", + "foundry-wallets", + "futures", + "indicatif", + "itertools 0.11.0", + "parking_lot", + "revm-inspectors", + "semver 1.0.22", + "serde", + "serde_json", + "thiserror", + "tracing", + "yansi 0.5.1", +] + [[package]] name = "forge-verify" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index e0c94e7466dc..68240fde2657 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -119,6 +119,7 @@ forge = { path = "crates/forge" } forge-doc = { path = "crates/doc" } forge-fmt = { path = "crates/fmt" } forge-verify = { path = "crates/verify" } +forge-script = { path = "crates/script" } foundry-cheatcodes = { path = "crates/cheatcodes" } foundry-cheatcodes-spec = { path = "crates/cheatcodes/spec" } foundry-cli = { path = "crates/cli" } diff --git a/crates/forge/Cargo.toml b/crates/forge/Cargo.toml index 033d9125b458..0a120f975408 100644 --- a/crates/forge/Cargo.toml +++ b/crates/forge/Cargo.toml @@ -51,6 +51,7 @@ yansi = "0.5" forge-doc.workspace = true forge-fmt.workspace = true forge-verify.workspace = true +forge-script.workspace = true foundry-cli.workspace = true foundry-debugger.workspace = true @@ -60,7 +61,6 @@ alloy-primitives = { workspace = true, features = ["serde"] } alloy-rpc-types.workspace = true async-trait = "0.1" -async-recursion = "1.0.5" clap = { version = "4", features = ["derive", "env", "unicode", "wrap_help"] } clap_complete = "4" clap_complete_fig = "4" diff --git a/crates/forge/bin/cmd/init.rs b/crates/forge/bin/cmd/init.rs index 9c8c3fc90c11..96144dc63d7e 100644 --- a/crates/forge/bin/cmd/init.rs +++ b/crates/forge/bin/cmd/init.rs @@ -164,11 +164,6 @@ impl InitArgs { } } -/// Returns the commit hash of the project if it exists -pub fn get_commit_hash(root: &Path) -> Option { - Git::new(root).commit_hash(true, "HEAD").ok() -} - /// Initialises `root` as a git repository, if it isn't one already. /// /// Creates `.gitignore` and `.github/workflows/test.yml`, if they don't exist already. diff --git a/crates/forge/bin/opts.rs b/crates/forge/bin/opts.rs index 5e5cfda7c8b6..49cc516dbd62 100644 --- a/crates/forge/bin/opts.rs +++ b/crates/forge/bin/opts.rs @@ -2,10 +2,11 @@ use crate::cmd::{ bind::BindArgs, build::BuildArgs, cache::CacheArgs, config, coverage, create::CreateArgs, debug::DebugArgs, doc::DocArgs, flatten, fmt::FmtArgs, geiger, generate, init::InitArgs, inspect, install::InstallArgs, remappings::RemappingArgs, remove::RemoveArgs, - script::ScriptArgs, selectors::SelectorsSubcommands, snapshot, test, tree, update, + selectors::SelectorsSubcommands, snapshot, test, tree, update, }; use clap::{Parser, Subcommand, ValueHint}; use forge_verify::{VerifyArgs, VerifyCheckArgs}; +use forge_script::ScriptArgs; use std::path::PathBuf; const VERSION_MESSAGE: &str = concat!( diff --git a/crates/forge/src/lib.rs b/crates/forge/src/lib.rs index 39854abac8a1..f6b9a54a8e68 100644 --- a/crates/forge/src/lib.rs +++ b/crates/forge/src/lib.rs @@ -207,23 +207,3 @@ impl TestOptionsBuilder { TestOptions::new(output, root, profiles, base_fuzz, base_invariant) } } - -mod utils2 { - use alloy_primitives::Address; - use ethers_core::types::BlockId; - use ethers_providers::{Middleware, Provider}; - use eyre::Context; - use foundry_common::types::{ToAlloy, ToEthers}; - - pub async fn next_nonce( - caller: Address, - provider_url: &str, - block: Option, - ) -> eyre::Result { - let provider = Provider::try_from(provider_url) - .wrap_err_with(|| format!("bad fork_url provider: {provider_url}"))?; - let res = provider.get_transaction_count(caller.to_ethers(), block).await?.to_alloy(); - res.try_into().map_err(Into::into) - } -} -pub use utils2::*; diff --git a/crates/script/Cargo.toml b/crates/script/Cargo.toml new file mode 100644 index 000000000000..1bac51fa8541 --- /dev/null +++ b/crates/script/Cargo.toml @@ -0,0 +1,47 @@ +[package] +name = "forge-script" +description = "Solidity scripting" + +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true + +[dependencies] +forge-verify.workspace = true +foundry-cli.workspace = true +foundry-config.workspace = true +foundry-common.workspace = true +foundry-evm.workspace = true +foundry-debugger.workspace = true +foundry-cheatcodes.workspace = true +foundry-wallets.workspace = true + +hex.workspace = true +serde.workspace = true +eyre.workspace = true +serde_json.workspace = true +thiserror = "1" +dunce = "1" +foundry-compilers = { workspace = true, features = ["full"] } +tracing.workspace = true +clap = { version = "4", features = ["derive", "env", "unicode", "wrap_help"] } +semver = "1" +futures = "0.3" +async-recursion = "1.0.5" +alloy-primitives.workspace = true +alloy-dyn-abi.workspace = true +itertools.workspace = true +parking_lot = "0.12" +yansi = "0.5" +ethers-core.workspace = true +ethers-providers.workspace = true +ethers-signers.workspace = true +revm-inspectors.workspace = true +alloy-rpc-types.workspace = true +alloy-json-abi.workspace = true +dialoguer = { version = "0.11", default-features = false } +indicatif = "0.17" \ No newline at end of file diff --git a/crates/forge/bin/cmd/script/artifacts.rs b/crates/script/src/artifacts.rs similarity index 100% rename from crates/forge/bin/cmd/script/artifacts.rs rename to crates/script/src/artifacts.rs diff --git a/crates/forge/bin/cmd/script/broadcast.rs b/crates/script/src/broadcast.rs similarity index 97% rename from crates/forge/bin/cmd/script/broadcast.rs rename to crates/script/src/broadcast.rs index b29bc2350df1..e1b74031399e 100644 --- a/crates/forge/bin/cmd/script/broadcast.rs +++ b/crates/script/src/broadcast.rs @@ -3,7 +3,7 @@ use super::{ states::{BroadcastedState, BundledState}, }; use alloy_primitives::{utils::format_units, Address, TxHash, U256}; -use ethers_core::types::transaction::eip2718::TypedTransaction; +use ethers_core::types::{transaction::eip2718::TypedTransaction, BlockId}; use ethers_providers::{JsonRpcClient, Middleware, Provider}; use ethers_signers::Signer; use eyre::{bail, Context, Result}; @@ -49,6 +49,17 @@ where Ok(()) } +pub async fn next_nonce( + caller: Address, + provider_url: &str, + block: Option, +) -> eyre::Result { + let provider = Provider::try_from(provider_url) + .wrap_err_with(|| format!("bad fork_url provider: {provider_url}"))?; + let res = provider.get_transaction_count(caller.to_ethers(), block).await?.to_alloy(); + res.try_into().map_err(Into::into) +} + pub async fn send_transaction( provider: Arc, mut tx: TypedTransaction, diff --git a/crates/forge/bin/cmd/script/build.rs b/crates/script/src/build.rs similarity index 100% rename from crates/forge/bin/cmd/script/build.rs rename to crates/script/src/build.rs diff --git a/crates/forge/bin/cmd/script/cmd.rs b/crates/script/src/cmd.rs similarity index 98% rename from crates/forge/bin/cmd/script/cmd.rs rename to crates/script/src/cmd.rs index 4c853c444489..165c5c0cc7bd 100644 --- a/crates/forge/bin/cmd/script/cmd.rs +++ b/crates/script/src/cmd.rs @@ -2,7 +2,7 @@ use super::{states::PreprocessedState, ScriptArgs, ScriptConfig}; use alloy_primitives::Address; use ethers_signers::Signer; use eyre::Result; -use forge::inspectors::cheatcodes::ScriptWallets; +use foundry_cheatcodes::ScriptWallets; use foundry_cli::utils::LoadConfig; use foundry_common::{shell, types::ToAlloy}; diff --git a/crates/forge/bin/cmd/script/execute.rs b/crates/script/src/execute.rs similarity index 99% rename from crates/forge/bin/cmd/script/execute.rs rename to crates/script/src/execute.rs index e9d00ad30125..7b6534d9de5f 100644 --- a/crates/forge/bin/cmd/script/execute.rs +++ b/crates/script/src/execute.rs @@ -10,14 +10,6 @@ use alloy_rpc_types::request::TransactionRequest; use async_recursion::async_recursion; use ethers_providers::Middleware; use eyre::Result; -use forge::{ - decode::{decode_console_logs, RevertDecoder}, - inspectors::cheatcodes::{BroadcastableTransaction, BroadcastableTransactions}, - traces::{ - identifier::{EtherscanIdentifier, LocalTraceIdentifier, SignaturesIdentifier}, - render_trace_arena, CallTraceDecoder, CallTraceDecoderBuilder, TraceKind, - }, -}; use foundry_cli::utils::{ensure_clean_constructor, needs_setup}; use foundry_common::{ fmt::{format_token, format_token_raw}, @@ -27,6 +19,14 @@ use foundry_common::{ use foundry_compilers::artifacts::ContractBytecodeSome; use foundry_config::{Config, NamedChain}; use foundry_debugger::Debugger; +use foundry_evm::{ + decode::{decode_console_logs, RevertDecoder}, + inspectors::cheatcodes::{BroadcastableTransaction, BroadcastableTransactions}, + traces::{ + identifier::{EtherscanIdentifier, LocalTraceIdentifier, SignaturesIdentifier}, + render_trace_arena, CallTraceDecoder, CallTraceDecoderBuilder, TraceKind, + }, +}; use futures::future::join_all; use itertools::Itertools; use std::collections::{HashMap, HashSet}; diff --git a/crates/forge/bin/cmd/script/mod.rs b/crates/script/src/lib.rs similarity index 98% rename from crates/forge/bin/cmd/script/mod.rs rename to crates/script/src/lib.rs index d9c70c667af5..bc2912721b17 100644 --- a/crates/forge/bin/cmd/script/mod.rs +++ b/crates/script/src/lib.rs @@ -1,19 +1,17 @@ +#![cfg_attr(not(test), warn(unused_crate_dependencies))] + +#[macro_use] +extern crate tracing; + use self::transaction::AdditionalContract; use super::build::BuildArgs; -use crate::cmd::script::runner::ScriptRunner; +use crate::runner::ScriptRunner; use alloy_json_abi::{Function, JsonAbi}; use alloy_primitives::{Address, Bytes, Log, U256}; +use broadcast::next_nonce; use clap::{Parser, ValueHint}; use dialoguer::Confirm; use eyre::{ContextCompat, Result, WrapErr}; -use forge::{ - backend::Backend, - debug::DebugArena, - executors::ExecutorBuilder, - inspectors::{cheatcodes::ScriptWallets, CheatsConfig}, - opts::EvmOpts, - traces::Traces, -}; use forge_verify::RetryArgs; use foundry_common::{ abi::{encode_function_args, get_func}, @@ -32,7 +30,16 @@ use foundry_config::{ Config, }; use foundry_evm::{ - constants::DEFAULT_CREATE2_DEPLOYER, inspectors::cheatcodes::BroadcastableTransactions, + backend::Backend, + constants::DEFAULT_CREATE2_DEPLOYER, + debug::DebugArena, + executors::ExecutorBuilder, + inspectors::{ + cheatcodes::{BroadcastableTransactions, ScriptWallets}, + CheatsConfig, + }, + opts::EvmOpts, + traces::Traces, }; use foundry_wallets::MultiWalletOpts; use serde::{Deserialize, Serialize}; @@ -52,7 +59,7 @@ mod runner; mod sequence; mod simulate; mod states; -pub mod transaction; +mod transaction; mod verify; // Loads project's figment and merges the build cli arguments into it @@ -398,7 +405,7 @@ pub struct ScriptConfig { impl ScriptConfig { pub async fn new(config: Config, evm_opts: EvmOpts) -> Result { let sender_nonce = if let Some(fork_url) = evm_opts.fork_url.as_ref() { - forge::next_nonce(evm_opts.sender, fork_url, None).await? + next_nonce(evm_opts.sender, fork_url, None).await? } else { // dapptools compatibility 1 @@ -408,7 +415,7 @@ impl ScriptConfig { pub async fn update_sender(&mut self, sender: Address) -> Result<()> { self.sender_nonce = if let Some(fork_url) = self.evm_opts.fork_url.as_ref() { - forge::next_nonce(sender, fork_url, None).await? + next_nonce(sender, fork_url, None).await? } else { // dapptools compatibility 1 diff --git a/crates/forge/bin/cmd/script/multi_sequence.rs b/crates/script/src/multi_sequence.rs similarity index 100% rename from crates/forge/bin/cmd/script/multi_sequence.rs rename to crates/script/src/multi_sequence.rs diff --git a/crates/forge/bin/cmd/script/providers.rs b/crates/script/src/providers.rs similarity index 100% rename from crates/forge/bin/cmd/script/providers.rs rename to crates/script/src/providers.rs diff --git a/crates/forge/bin/cmd/script/receipts.rs b/crates/script/src/receipts.rs similarity index 100% rename from crates/forge/bin/cmd/script/receipts.rs rename to crates/script/src/receipts.rs diff --git a/crates/forge/bin/cmd/script/resume.rs b/crates/script/src/resume.rs similarity index 100% rename from crates/forge/bin/cmd/script/resume.rs rename to crates/script/src/resume.rs diff --git a/crates/forge/bin/cmd/script/runner.rs b/crates/script/src/runner.rs similarity index 99% rename from crates/forge/bin/cmd/script/runner.rs rename to crates/script/src/runner.rs index 4caa4e39366b..59f6402d368a 100644 --- a/crates/forge/bin/cmd/script/runner.rs +++ b/crates/script/src/runner.rs @@ -1,13 +1,13 @@ use super::ScriptResult; use alloy_primitives::{Address, Bytes, U256}; use eyre::Result; -use forge::{ +use foundry_config::Config; +use foundry_evm::{ constants::CALLER, executors::{CallResult, DeployResult, EvmError, ExecutionErr, Executor, RawCallResult}, revm::interpreter::{return_ok, InstructionResult}, traces::{TraceKind, Traces}, }; -use foundry_config::Config; use yansi::Paint; /// Drives script execution diff --git a/crates/forge/bin/cmd/script/sequence.rs b/crates/script/src/sequence.rs similarity index 97% rename from crates/forge/bin/cmd/script/sequence.rs rename to crates/script/src/sequence.rs index 9825e4c92242..19dfdb76b722 100644 --- a/crates/forge/bin/cmd/script/sequence.rs +++ b/crates/script/src/sequence.rs @@ -1,16 +1,13 @@ use super::{multi_sequence::MultiChainSequence, NestedValue}; -use crate::cmd::{ - init::get_commit_hash, - script::{ - transaction::{wrapper, AdditionalContract, TransactionWithMetadata}, - verify::VerifyBundle, - }, +use crate::{ + transaction::{wrapper, AdditionalContract, TransactionWithMetadata}, + verify::VerifyBundle, }; use alloy_primitives::{Address, TxHash}; use ethers_core::types::{transaction::eip2718::TypedTransaction, TransactionReceipt}; use eyre::{ContextCompat, Result, WrapErr}; use forge_verify::provider::VerificationProviderType; -use foundry_cli::utils::now; +use foundry_cli::utils::{now, Git}; use foundry_common::{ fs, shell, types::{ToAlloy, ToEthers}, @@ -22,10 +19,15 @@ use serde::{Deserialize, Serialize}; use std::{ collections::{HashMap, VecDeque}, io::{BufWriter, Write}, - path::PathBuf, + path::{Path, PathBuf}, }; use yansi::Paint; +/// Returns the commit hash of the project if it exists +pub fn get_commit_hash(root: &Path) -> Option { + Git::new(root).commit_hash(true, "HEAD").ok() +} + pub enum ScriptSequenceKind { Single(ScriptSequence), Multi(MultiChainSequence), diff --git a/crates/forge/bin/cmd/script/simulate.rs b/crates/script/src/simulate.rs similarity index 98% rename from crates/forge/bin/cmd/script/simulate.rs rename to crates/script/src/simulate.rs index 7d3f3106d2ea..8349f7670804 100644 --- a/crates/forge/bin/cmd/script/simulate.rs +++ b/crates/script/src/simulate.rs @@ -7,14 +7,15 @@ use super::{ states::{BundledState, FilledTransactionsState, PreSimulationState}, transaction::TransactionWithMetadata, }; -use crate::cmd::{init::get_commit_hash, script::broadcast::estimate_gas}; +use crate::{broadcast::estimate_gas, sequence::get_commit_hash}; use alloy_primitives::{utils::format_units, Address, U256}; use eyre::{Context, Result}; -use forge::{inspectors::cheatcodes::BroadcastableTransactions, traces::render_trace_arena}; +use foundry_cheatcodes::BroadcastableTransactions; use foundry_cli::utils::{has_different_gas_calc, now}; use foundry_common::{ get_contract_name, provider::ethers::RpcUrl, shell, types::ToAlloy, ContractsByArtifact, }; +use foundry_evm::traces::render_trace_arena; use futures::future::join_all; use parking_lot::RwLock; use std::{ diff --git a/crates/forge/bin/cmd/script/states.rs b/crates/script/src/states.rs similarity index 98% rename from crates/forge/bin/cmd/script/states.rs rename to crates/script/src/states.rs index 61936ddd788b..ed887028ff60 100644 --- a/crates/forge/bin/cmd/script/states.rs +++ b/crates/script/src/states.rs @@ -5,7 +5,7 @@ use super::{ transaction::TransactionWithMetadata, ScriptArgs, ScriptConfig, ScriptResult, }; -use forge::inspectors::cheatcodes::ScriptWallets; +use foundry_cheatcodes::ScriptWallets; use std::collections::VecDeque; /// First state basically containing only inputs of the user. diff --git a/crates/forge/bin/cmd/script/transaction.rs b/crates/script/src/transaction.rs similarity index 100% rename from crates/forge/bin/cmd/script/transaction.rs rename to crates/script/src/transaction.rs diff --git a/crates/forge/bin/cmd/script/verify.rs b/crates/script/src/verify.rs similarity index 100% rename from crates/forge/bin/cmd/script/verify.rs rename to crates/script/src/verify.rs From 300a181dbbc05a3e52d31e7ef7d1b76be0098613 Mon Sep 17 00:00:00 2001 From: Arsenii Kulikov Date: Thu, 7 Mar 2024 02:37:51 +0400 Subject: [PATCH 29/33] Use CoreBuildArgs + skip --- Cargo.lock | 3 ++- crates/forge/bin/cmd/debug.rs | 4 ++-- crates/forge/bin/cmd/mod.rs | 1 - crates/forge/bin/main.rs | 2 +- crates/script/Cargo.toml | 7 +++++-- crates/script/src/build.rs | 4 ++-- crates/script/src/lib.rs | 16 +++++++++------- 7 files changed, 21 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2ba1addf6cf5..c9e4363613b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3036,6 +3036,7 @@ dependencies = [ "foundry-config", "foundry-debugger", "foundry-evm", + "foundry-linking", "foundry-wallets", "futures", "indicatif", @@ -3045,7 +3046,7 @@ dependencies = [ "semver 1.0.22", "serde", "serde_json", - "thiserror", + "tempfile", "tracing", "yansi 0.5.1", ] diff --git a/crates/forge/bin/cmd/debug.rs b/crates/forge/bin/cmd/debug.rs index 75f27da53919..7caa40ac8fd5 100644 --- a/crates/forge/bin/cmd/debug.rs +++ b/crates/forge/bin/cmd/debug.rs @@ -1,4 +1,4 @@ -use super::{build::BuildArgs, script::ScriptArgs}; +use forge_script::ScriptArgs; use clap::{Parser, ValueHint}; use forge_verify::retry::RETRY_VERIFY_ON_CREATE; use foundry_cli::opts::CoreBuildArgs; @@ -48,7 +48,7 @@ impl DebugArgs { target_contract: self.target_contract, sig: self.sig, gas_estimate_multiplier: 130, - opts: BuildArgs { args: self.opts, ..Default::default() }, + opts: self.opts, evm_opts: self.evm_opts, debug: true, retry: RETRY_VERIFY_ON_CREATE, diff --git a/crates/forge/bin/cmd/mod.rs b/crates/forge/bin/cmd/mod.rs index 1e1a91cbf9c4..b01366aa7ce8 100644 --- a/crates/forge/bin/cmd/mod.rs +++ b/crates/forge/bin/cmd/mod.rs @@ -56,7 +56,6 @@ pub mod inspect; pub mod install; pub mod remappings; pub mod remove; -pub mod script; pub mod selectors; pub mod snapshot; pub mod test; diff --git a/crates/forge/bin/main.rs b/crates/forge/bin/main.rs index 5fdc7c408a83..0743d69e61aa 100644 --- a/crates/forge/bin/main.rs +++ b/crates/forge/bin/main.rs @@ -31,7 +31,7 @@ fn main() -> Result<()> { ForgeSubcommand::Script(cmd) => { // install the shell before executing the command foundry_common::shell::set_shell(foundry_common::shell::Shell::from_args( - cmd.opts.args.silent, + cmd.opts.silent, cmd.json, ))?; utils::block_on(cmd.run_script()) diff --git a/crates/script/Cargo.toml b/crates/script/Cargo.toml index 1bac51fa8541..5ae57482200e 100644 --- a/crates/script/Cargo.toml +++ b/crates/script/Cargo.toml @@ -19,12 +19,12 @@ foundry-evm.workspace = true foundry-debugger.workspace = true foundry-cheatcodes.workspace = true foundry-wallets.workspace = true +foundry-linking.workspace = true hex.workspace = true serde.workspace = true eyre.workspace = true serde_json.workspace = true -thiserror = "1" dunce = "1" foundry-compilers = { workspace = true, features = ["full"] } tracing.workspace = true @@ -44,4 +44,7 @@ revm-inspectors.workspace = true alloy-rpc-types.workspace = true alloy-json-abi.workspace = true dialoguer = { version = "0.11", default-features = false } -indicatif = "0.17" \ No newline at end of file +indicatif = "0.17" + +[dev-dependencies] +tempfile = "3" \ No newline at end of file diff --git a/crates/script/src/build.rs b/crates/script/src/build.rs index 966fa8f5a353..3e979937dc9d 100644 --- a/crates/script/src/build.rs +++ b/crates/script/src/build.rs @@ -121,7 +121,7 @@ impl PreprocessedState { pub fn compile(self) -> Result { let Self { args, script_config, script_wallets } = self; let project = script_config.config.project()?; - let filters = args.opts.skip.clone().unwrap_or_default(); + let filters = args.skip.clone().unwrap_or_default(); let mut target_name = args.target_contract.clone(); @@ -146,7 +146,7 @@ impl PreprocessedState { compile::compile_target_with_filter( &target_path, &project, - args.opts.args.silent, + args.opts.silent, args.verify, filters, ) diff --git a/crates/script/src/lib.rs b/crates/script/src/lib.rs index bc2912721b17..5d8b43526208 100644 --- a/crates/script/src/lib.rs +++ b/crates/script/src/lib.rs @@ -4,7 +4,6 @@ extern crate tracing; use self::transaction::AdditionalContract; -use super::build::BuildArgs; use crate::runner::ScriptRunner; use alloy_json_abi::{Function, JsonAbi}; use alloy_primitives::{Address, Bytes, Log, U256}; @@ -13,12 +12,9 @@ use clap::{Parser, ValueHint}; use dialoguer::Confirm; use eyre::{ContextCompat, Result, WrapErr}; use forge_verify::RetryArgs; +use foundry_cli::opts::CoreBuildArgs; use foundry_common::{ - abi::{encode_function_args, get_func}, - errors::UnlinkedByteCode, - evm::{Breakpoints, EvmArgs}, - provider::ethers::RpcUrl, - shell, CONTRACT_MAX_SIZE, SELECTOR_LEN, + abi::{encode_function_args, get_func}, compile::SkipBuildFilter, errors::UnlinkedByteCode, evm::{Breakpoints, EvmArgs}, provider::ethers::RpcUrl, shell, CONTRACT_MAX_SIZE, SELECTOR_LEN }; use foundry_compilers::{artifacts::ContractBytecodeSome, ArtifactId}; use foundry_config::{ @@ -172,8 +168,14 @@ pub struct ScriptArgs { )] pub with_gas_price: Option, + /// Skip building files whose names contain the given filter. + /// + /// `test` and `script` are aliases for `.t.sol` and `.s.sol`. + #[arg(long, num_args(1..))] + pub skip: Option>, + #[command(flatten)] - pub opts: BuildArgs, + pub opts: CoreBuildArgs, #[command(flatten)] pub wallets: MultiWalletOpts, From 120433cc1acc5770de80f3ed23dcf85b0780c959 Mon Sep 17 00:00:00 2001 From: Arsenii Kulikov Date: Thu, 7 Mar 2024 02:39:27 +0400 Subject: [PATCH 30/33] fmt --- crates/forge/bin/cmd/debug.rs | 2 +- crates/forge/bin/opts.rs | 2 +- crates/script/src/lib.rs | 7 ++++++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/crates/forge/bin/cmd/debug.rs b/crates/forge/bin/cmd/debug.rs index 7caa40ac8fd5..8fe1d2e32a25 100644 --- a/crates/forge/bin/cmd/debug.rs +++ b/crates/forge/bin/cmd/debug.rs @@ -1,5 +1,5 @@ -use forge_script::ScriptArgs; use clap::{Parser, ValueHint}; +use forge_script::ScriptArgs; use forge_verify::retry::RETRY_VERIFY_ON_CREATE; use foundry_cli::opts::CoreBuildArgs; use foundry_common::evm::EvmArgs; diff --git a/crates/forge/bin/opts.rs b/crates/forge/bin/opts.rs index 49cc516dbd62..03ed4d551f4b 100644 --- a/crates/forge/bin/opts.rs +++ b/crates/forge/bin/opts.rs @@ -5,8 +5,8 @@ use crate::cmd::{ selectors::SelectorsSubcommands, snapshot, test, tree, update, }; use clap::{Parser, Subcommand, ValueHint}; -use forge_verify::{VerifyArgs, VerifyCheckArgs}; use forge_script::ScriptArgs; +use forge_verify::{VerifyArgs, VerifyCheckArgs}; use std::path::PathBuf; const VERSION_MESSAGE: &str = concat!( diff --git a/crates/script/src/lib.rs b/crates/script/src/lib.rs index 5d8b43526208..173ed85b10b0 100644 --- a/crates/script/src/lib.rs +++ b/crates/script/src/lib.rs @@ -14,7 +14,12 @@ use eyre::{ContextCompat, Result, WrapErr}; use forge_verify::RetryArgs; use foundry_cli::opts::CoreBuildArgs; use foundry_common::{ - abi::{encode_function_args, get_func}, compile::SkipBuildFilter, errors::UnlinkedByteCode, evm::{Breakpoints, EvmArgs}, provider::ethers::RpcUrl, shell, CONTRACT_MAX_SIZE, SELECTOR_LEN + abi::{encode_function_args, get_func}, + compile::SkipBuildFilter, + errors::UnlinkedByteCode, + evm::{Breakpoints, EvmArgs}, + provider::ethers::RpcUrl, + shell, CONTRACT_MAX_SIZE, SELECTOR_LEN, }; use foundry_compilers::{artifacts::ContractBytecodeSome, ArtifactId}; use foundry_config::{ From ee2c83236800aa0f207b8c7c6c2389d04662f1cd Mon Sep 17 00:00:00 2001 From: Arsenii Kulikov Date: Thu, 7 Mar 2024 18:10:20 +0400 Subject: [PATCH 31/33] review fixes --- crates/script/src/broadcast.rs | 40 +++++++++++- crates/script/src/build.rs | 27 ++++++-- crates/script/src/cmd.rs | 111 -------------------------------- crates/script/src/execute.rs | 39 ++++++++++-- crates/script/src/lib.rs | 112 +++++++++++++++++++++++++++++++-- crates/script/src/resume.rs | 3 +- crates/script/src/simulate.rs | 39 +++++++++++- crates/script/src/states.rs | 104 ------------------------------ crates/script/src/verify.rs | 35 ++++++----- 9 files changed, 258 insertions(+), 252 deletions(-) delete mode 100644 crates/script/src/cmd.rs delete mode 100644 crates/script/src/states.rs diff --git a/crates/script/src/broadcast.rs b/crates/script/src/broadcast.rs index e1b74031399e..03c9bf67bf8a 100644 --- a/crates/script/src/broadcast.rs +++ b/crates/script/src/broadcast.rs @@ -1,12 +1,19 @@ -use super::{ - receipts, - states::{BroadcastedState, BundledState}, +use crate::{ + build::LinkedBuildData, + execute::{ExecutionArtifacts, ExecutionData}, + sequence::ScriptSequenceKind, + verify::BroadcastedState, + ScriptArgs, ScriptConfig, }; + +use super::receipts; use alloy_primitives::{utils::format_units, Address, TxHash, U256}; use ethers_core::types::{transaction::eip2718::TypedTransaction, BlockId}; use ethers_providers::{JsonRpcClient, Middleware, Provider}; use ethers_signers::Signer; use eyre::{bail, Context, Result}; +use forge_verify::provider::VerificationProviderType; +use foundry_cheatcodes::ScriptWallets; use foundry_cli::{ init_progress, update_progress, utils::{has_batch_support, has_different_gas_calc}, @@ -155,6 +162,18 @@ impl SendTransactionsKind { } } +/// State after we have bundled all [TransactionWithMetadata] objects into a single +/// [ScriptSequenceKind] object containing one or more script sequences. +pub struct BundledState { + pub args: ScriptArgs, + pub script_config: ScriptConfig, + pub script_wallets: ScriptWallets, + pub build_data: LinkedBuildData, + pub execution_data: ExecutionData, + pub execution_artifacts: ExecutionArtifacts, + pub sequence: ScriptSequenceKind, +} + impl BundledState { pub async fn wait_for_pending(mut self) -> Result { let futs = self @@ -392,4 +411,19 @@ impl BundledState { sequence: self.sequence, }) } + + pub fn verify_preflight_check(&self) -> Result<()> { + for sequence in self.sequence.sequences() { + if self.args.verifier.verifier == VerificationProviderType::Etherscan && + self.script_config + .config + .get_etherscan_api_key(Some(sequence.chain.into())) + .is_none() + { + eyre::bail!("Missing etherscan key for chain {}", sequence.chain); + } + } + + Ok(()) + } } diff --git a/crates/script/src/build.rs b/crates/script/src/build.rs index 3e979937dc9d..4dc78b0cdc56 100644 --- a/crates/script/src/build.rs +++ b/crates/script/src/build.rs @@ -1,6 +1,8 @@ -use super::states::{CompiledState, LinkedState, PreprocessedState}; +use crate::{execute::LinkedState, ScriptArgs, ScriptConfig}; + use alloy_primitives::{Address, Bytes}; use eyre::{Context, OptionExt, Result}; +use foundry_cheatcodes::ScriptWallets; use foundry_cli::utils::get_cached_entry_by_name; use foundry_common::{ compile::{self, ContractSources, ProjectCompiler}, @@ -115,6 +117,13 @@ impl LinkedBuildData { } } +/// First state basically containing only inputs of the user. +pub struct PreprocessedState { + pub args: ScriptArgs, + pub script_config: ScriptConfig, + pub script_wallets: ScriptWallets, +} + impl PreprocessedState { /// Parses user input and compiles the contracts depending on script target. /// After compilation, finds exact [ArtifactId] of the target contract. @@ -129,16 +138,16 @@ impl PreprocessedState { // Otherwise, parse input as : and use the path from the contract info, if // present. let target_path = if let Ok(path) = dunce::canonicalize(&args.path) { - Ok::<_, eyre::Report>(Some(path)) + Some(path) } else { let contract = ContractInfo::from_str(&args.path)?; target_name = Some(contract.name.clone()); if let Some(path) = contract.path { - Ok(Some(dunce::canonicalize(path)?)) + Some(dunce::canonicalize(path)?) } else { - Ok(None) + None } - }?; + }; // If we've found target path above, only compile it. // Otherwise, compile everything to match contract by name later. @@ -216,6 +225,14 @@ impl PreprocessedState { } } +/// State after we have determined and compiled target contract to be executed. +pub struct CompiledState { + pub args: ScriptArgs, + pub script_config: ScriptConfig, + pub script_wallets: ScriptWallets, + pub build_data: BuildData, +} + impl CompiledState { /// Uses provided sender address to compute library addresses and link contracts with them. pub fn link(self) -> Result { diff --git a/crates/script/src/cmd.rs b/crates/script/src/cmd.rs deleted file mode 100644 index 165c5c0cc7bd..000000000000 --- a/crates/script/src/cmd.rs +++ /dev/null @@ -1,111 +0,0 @@ -use super::{states::PreprocessedState, ScriptArgs, ScriptConfig}; -use alloy_primitives::Address; -use ethers_signers::Signer; -use eyre::Result; -use foundry_cheatcodes::ScriptWallets; -use foundry_cli::utils::LoadConfig; -use foundry_common::{shell, types::ToAlloy}; - -impl ScriptArgs { - async fn preprocess(self) -> Result { - let script_wallets = - ScriptWallets::new(self.wallets.get_multi_wallet().await?, self.evm_opts.sender); - - let (config, mut evm_opts) = self.load_config_and_evm_opts_emit_warnings()?; - - if let Some(sender) = self.maybe_load_private_key()? { - evm_opts.sender = sender; - } - - let script_config = ScriptConfig::new(config, evm_opts).await?; - - Ok(PreprocessedState { args: self, script_config, script_wallets }) - } - - /// Executes the script - pub async fn run_script(self) -> Result<()> { - trace!(target: "script", "executing script command"); - - // Drive state machine to point at which we have everything needed for simulation/resuming. - let pre_simulation = self - .preprocess() - .await? - .compile()? - .link()? - .prepare_execution() - .await? - .execute() - .await? - .prepare_simulation() - .await?; - - if pre_simulation.args.debug { - pre_simulation.run_debugger()?; - } - - if pre_simulation.args.json { - pre_simulation.show_json()?; - } else { - pre_simulation.show_traces().await?; - } - - // Ensure that we have transactions to simulate/broadcast, otherwise exit early to avoid - // hard error. - if pre_simulation.execution_result.transactions.as_ref().map_or(true, |txs| txs.is_empty()) - { - return Ok(()); - } - - // Check if there are any missing RPCs and exit early to avoid hard error. - if pre_simulation.execution_artifacts.rpc_data.missing_rpc { - shell::println("\nIf you wish to simulate on-chain transactions pass a RPC URL.")?; - return Ok(()); - } - - // Move from `PreSimulationState` to `BundledState` either by resuming or simulating - // transactions. - let bundled = if pre_simulation.args.resume || - (pre_simulation.args.verify && !pre_simulation.args.broadcast) - { - pre_simulation.resume().await? - } else { - pre_simulation.args.check_contract_sizes( - &pre_simulation.execution_result, - &pre_simulation.build_data.highlevel_known_contracts, - )?; - - pre_simulation.fill_metadata().await?.bundle().await? - }; - - // Exit early in case user didn't provide any broadcast/verify related flags. - if !bundled.args.broadcast && !bundled.args.resume && !bundled.args.verify { - shell::println("\nSIMULATION COMPLETE. To broadcast these transactions, add --broadcast and wallet configuration(s) to the previous command. See forge script --help for more.")?; - return Ok(()); - } - - // Exit early if something is wrong with verification options. - if bundled.args.verify { - bundled.verify_preflight_check()?; - } - - // Wait for pending txes and broadcast others. - let broadcasted = bundled.wait_for_pending().await?.broadcast().await?; - - if broadcasted.args.verify { - broadcasted.verify().await?; - } - - Ok(()) - } - - /// In case the user has loaded *only* one private-key, we can assume that he's using it as the - /// `--sender` - fn maybe_load_private_key(&self) -> Result> { - let maybe_sender = self - .wallets - .private_keys()? - .filter(|pks| pks.len() == 1) - .map(|pks| pks.first().unwrap().address().to_alloy()); - Ok(maybe_sender) - } -} diff --git a/crates/script/src/execute.rs b/crates/script/src/execute.rs index 7b6534d9de5f..6ef7f18aabfe 100644 --- a/crates/script/src/execute.rs +++ b/crates/script/src/execute.rs @@ -1,8 +1,10 @@ -use super::{ - runner::ScriptRunner, - states::{CompiledState, ExecutedState, LinkedState, PreExecutionState, PreSimulationState}, - JsonResult, NestedValue, ScriptResult, +use crate::{ + build::{CompiledState, LinkedBuildData}, + simulate::PreSimulationState, + ScriptArgs, ScriptConfig, }; + +use super::{runner::ScriptRunner, JsonResult, NestedValue, ScriptResult}; use alloy_dyn_abi::FunctionExt; use alloy_json_abi::{Function, InternalType, JsonAbi}; use alloy_primitives::{Address, Bytes, U64}; @@ -10,6 +12,7 @@ use alloy_rpc_types::request::TransactionRequest; use async_recursion::async_recursion; use ethers_providers::Middleware; use eyre::Result; +use foundry_cheatcodes::ScriptWallets; use foundry_cli::utils::{ensure_clean_constructor, needs_setup}; use foundry_common::{ fmt::{format_token, format_token_raw}, @@ -32,6 +35,15 @@ use itertools::Itertools; use std::collections::{HashMap, HashSet}; use yansi::Paint; +/// State after linking, contains the linked build data along with library addresses and optional +/// array of libraries that need to be predeployed. +pub struct LinkedState { + pub args: ScriptArgs, + pub script_config: ScriptConfig, + pub script_wallets: ScriptWallets, + pub build_data: LinkedBuildData, +} + /// Container for data we need for execution which can only be obtained after linking stage. pub struct ExecutionData { /// Function to call. @@ -70,6 +82,15 @@ impl LinkedState { } } +/// Same as [LinkedState], but also contains [ExecutionData]. +pub struct PreExecutionState { + pub args: ScriptArgs, + pub script_config: ScriptConfig, + pub script_wallets: ScriptWallets, + pub build_data: LinkedBuildData, + pub execution_data: ExecutionData, +} + impl PreExecutionState { /// Executes the script and returns the state after execution. /// Might require executing script twice in cases when we determine sender from execution. @@ -263,6 +284,16 @@ pub struct ExecutionArtifacts { pub rpc_data: RpcData, } +/// State after the script has been executed. +pub struct ExecutedState { + pub args: ScriptArgs, + pub script_config: ScriptConfig, + pub script_wallets: ScriptWallets, + pub build_data: LinkedBuildData, + pub execution_data: ExecutionData, + pub execution_result: ScriptResult, +} + impl ExecutedState { /// Collects the data we need for simulation and various post-execution tasks. pub async fn prepare_simulation(self) -> Result { diff --git a/crates/script/src/lib.rs b/crates/script/src/lib.rs index 173ed85b10b0..6d8fb87b8329 100644 --- a/crates/script/src/lib.rs +++ b/crates/script/src/lib.rs @@ -8,18 +8,22 @@ use crate::runner::ScriptRunner; use alloy_json_abi::{Function, JsonAbi}; use alloy_primitives::{Address, Bytes, Log, U256}; use broadcast::next_nonce; +use build::PreprocessedState; use clap::{Parser, ValueHint}; use dialoguer::Confirm; +use ethers_signers::Signer; use eyre::{ContextCompat, Result, WrapErr}; use forge_verify::RetryArgs; -use foundry_cli::opts::CoreBuildArgs; +use foundry_cli::{opts::CoreBuildArgs, utils::LoadConfig}; use foundry_common::{ abi::{encode_function_args, get_func}, compile::SkipBuildFilter, errors::UnlinkedByteCode, evm::{Breakpoints, EvmArgs}, provider::ethers::RpcUrl, - shell, CONTRACT_MAX_SIZE, SELECTOR_LEN, + shell, + types::ToAlloy, + CONTRACT_MAX_SIZE, SELECTOR_LEN, }; use foundry_compilers::{artifacts::ContractBytecodeSome, ArtifactId}; use foundry_config::{ @@ -50,7 +54,6 @@ use yansi::Paint; mod artifacts; mod broadcast; mod build; -mod cmd; mod execute; mod multi_sequence; mod providers; @@ -59,7 +62,6 @@ mod resume; mod runner; mod sequence; mod simulate; -mod states; mod transaction; mod verify; @@ -198,6 +200,108 @@ pub struct ScriptArgs { // === impl ScriptArgs === impl ScriptArgs { + async fn preprocess(self) -> Result { + let script_wallets = + ScriptWallets::new(self.wallets.get_multi_wallet().await?, self.evm_opts.sender); + + let (config, mut evm_opts) = self.load_config_and_evm_opts_emit_warnings()?; + + if let Some(sender) = self.maybe_load_private_key()? { + evm_opts.sender = sender; + } + + let script_config = ScriptConfig::new(config, evm_opts).await?; + + Ok(PreprocessedState { args: self, script_config, script_wallets }) + } + + /// Executes the script + pub async fn run_script(self) -> Result<()> { + trace!(target: "script", "executing script command"); + + // Drive state machine to point at which we have everything needed for simulation/resuming. + let pre_simulation = self + .preprocess() + .await? + .compile()? + .link()? + .prepare_execution() + .await? + .execute() + .await? + .prepare_simulation() + .await?; + + if pre_simulation.args.debug { + pre_simulation.run_debugger()?; + } + + if pre_simulation.args.json { + pre_simulation.show_json()?; + } else { + pre_simulation.show_traces().await?; + } + + // Ensure that we have transactions to simulate/broadcast, otherwise exit early to avoid + // hard error. + if pre_simulation.execution_result.transactions.as_ref().map_or(true, |txs| txs.is_empty()) + { + return Ok(()); + } + + // Check if there are any missing RPCs and exit early to avoid hard error. + if pre_simulation.execution_artifacts.rpc_data.missing_rpc { + shell::println("\nIf you wish to simulate on-chain transactions pass a RPC URL.")?; + return Ok(()); + } + + // Move from `PreSimulationState` to `BundledState` either by resuming or simulating + // transactions. + let bundled = if pre_simulation.args.resume || + (pre_simulation.args.verify && !pre_simulation.args.broadcast) + { + pre_simulation.resume().await? + } else { + pre_simulation.args.check_contract_sizes( + &pre_simulation.execution_result, + &pre_simulation.build_data.highlevel_known_contracts, + )?; + + pre_simulation.fill_metadata().await?.bundle().await? + }; + + // Exit early in case user didn't provide any broadcast/verify related flags. + if !bundled.args.broadcast && !bundled.args.resume && !bundled.args.verify { + shell::println("\nSIMULATION COMPLETE. To broadcast these transactions, add --broadcast and wallet configuration(s) to the previous command. See forge script --help for more.")?; + return Ok(()); + } + + // Exit early if something is wrong with verification options. + if bundled.args.verify { + bundled.verify_preflight_check()?; + } + + // Wait for pending txes and broadcast others. + let broadcasted = bundled.wait_for_pending().await?.broadcast().await?; + + if broadcasted.args.verify { + broadcasted.verify().await?; + } + + Ok(()) + } + + /// In case the user has loaded *only* one private-key, we can assume that he's using it as the + /// `--sender` + fn maybe_load_private_key(&self) -> Result> { + let maybe_sender = self + .wallets + .private_keys()? + .filter(|pks| pks.len() == 1) + .map(|pks| pks.first().unwrap().address().to_alloy()); + Ok(maybe_sender) + } + /// Returns the Function and calldata based on the signature /// /// If the `sig` is a valid human-readable function we find the corresponding function in the diff --git a/crates/script/src/resume.rs b/crates/script/src/resume.rs index dba2588a74e5..4f704ed601fa 100644 --- a/crates/script/src/resume.rs +++ b/crates/script/src/resume.rs @@ -1,7 +1,8 @@ +use crate::{broadcast::BundledState, simulate::PreSimulationState}; + use super::{ multi_sequence::MultiChainSequence, sequence::{ScriptSequence, ScriptSequenceKind}, - states::{BundledState, PreSimulationState}, }; use ethers_providers::Middleware; use eyre::Result; diff --git a/crates/script/src/simulate.rs b/crates/script/src/simulate.rs index 8349f7670804..99bd39ad9515 100644 --- a/crates/script/src/simulate.rs +++ b/crates/script/src/simulate.rs @@ -4,13 +4,18 @@ use super::{ providers::ProvidersManager, runner::ScriptRunner, sequence::{ScriptSequence, ScriptSequenceKind}, - states::{BundledState, FilledTransactionsState, PreSimulationState}, transaction::TransactionWithMetadata, }; -use crate::{broadcast::estimate_gas, sequence::get_commit_hash}; +use crate::{ + broadcast::{estimate_gas, BundledState}, + build::LinkedBuildData, + execute::{ExecutionArtifacts, ExecutionData}, + sequence::get_commit_hash, + ScriptArgs, ScriptConfig, ScriptResult, +}; use alloy_primitives::{utils::format_units, Address, U256}; use eyre::{Context, Result}; -use foundry_cheatcodes::BroadcastableTransactions; +use foundry_cheatcodes::{BroadcastableTransactions, ScriptWallets}; use foundry_cli::utils::{has_different_gas_calc, now}; use foundry_common::{ get_contract_name, provider::ethers::RpcUrl, shell, types::ToAlloy, ContractsByArtifact, @@ -23,6 +28,21 @@ use std::{ sync::Arc, }; +/// Same as [ExecutedState], but also contains [ExecutionArtifacts] which are obtained from +/// [ScriptResult]. +/// +/// Can be either converted directly to [BundledState] via [PreSimulationState::resume] or driven to +/// it through [FilledTransactionsState]. +pub struct PreSimulationState { + pub args: ScriptArgs, + pub script_config: ScriptConfig, + pub script_wallets: ScriptWallets, + pub build_data: LinkedBuildData, + pub execution_data: ExecutionData, + pub execution_result: ScriptResult, + pub execution_artifacts: ExecutionArtifacts, +} + impl PreSimulationState { /// If simulation is enabled, simulates transactions against fork and fills gas estimation and /// metadata. Otherwise, metadata (e.g. additional contracts, created contract names) is @@ -231,6 +251,19 @@ impl PreSimulationState { } } +/// At this point we have converted transactions collected during script execution to +/// [TransactionWithMetadata] objects which contain additional metadata needed for broadcasting and +/// verification. +pub struct FilledTransactionsState { + pub args: ScriptArgs, + pub script_config: ScriptConfig, + pub script_wallets: ScriptWallets, + pub build_data: LinkedBuildData, + pub execution_data: ExecutionData, + pub execution_artifacts: ExecutionArtifacts, + pub transactions: VecDeque, +} + impl FilledTransactionsState { /// Bundles all transactions of the [`TransactionWithMetadata`] type in a list of /// [`ScriptSequence`]. List length will be higher than 1, if we're dealing with a multi diff --git a/crates/script/src/states.rs b/crates/script/src/states.rs deleted file mode 100644 index ed887028ff60..000000000000 --- a/crates/script/src/states.rs +++ /dev/null @@ -1,104 +0,0 @@ -use super::{ - build::{BuildData, LinkedBuildData}, - execute::{ExecutionArtifacts, ExecutionData}, - sequence::ScriptSequenceKind, - transaction::TransactionWithMetadata, - ScriptArgs, ScriptConfig, ScriptResult, -}; -use foundry_cheatcodes::ScriptWallets; -use std::collections::VecDeque; - -/// First state basically containing only inputs of the user. -pub struct PreprocessedState { - pub args: ScriptArgs, - pub script_config: ScriptConfig, - pub script_wallets: ScriptWallets, -} - -/// State after we have determined and compiled target contract to be executed. -pub struct CompiledState { - pub args: ScriptArgs, - pub script_config: ScriptConfig, - pub script_wallets: ScriptWallets, - pub build_data: BuildData, -} - -/// State after linking, contains the linked build data along with library addresses and optional -/// array of libraries that need to be predeployed. -pub struct LinkedState { - pub args: ScriptArgs, - pub script_config: ScriptConfig, - pub script_wallets: ScriptWallets, - pub build_data: LinkedBuildData, -} - -/// Same as [LinkedState], but also contains [ExecutionData]. -pub struct PreExecutionState { - pub args: ScriptArgs, - pub script_config: ScriptConfig, - pub script_wallets: ScriptWallets, - pub build_data: LinkedBuildData, - pub execution_data: ExecutionData, -} - -/// State after the script has been executed. -pub struct ExecutedState { - pub args: ScriptArgs, - pub script_config: ScriptConfig, - pub script_wallets: ScriptWallets, - pub build_data: LinkedBuildData, - pub execution_data: ExecutionData, - pub execution_result: ScriptResult, -} - -/// Same as [ExecutedState], but also contains [ExecutionArtifacts] which are obtained from -/// [ScriptResult]. -/// -/// Can be either converted directly to [BundledState] via [PreSimulationState::resume] or driven to -/// it through [FilledTransactionsState]. -pub struct PreSimulationState { - pub args: ScriptArgs, - pub script_config: ScriptConfig, - pub script_wallets: ScriptWallets, - pub build_data: LinkedBuildData, - pub execution_data: ExecutionData, - pub execution_result: ScriptResult, - pub execution_artifacts: ExecutionArtifacts, -} - -/// At this point we have converted transactions collected during script execution to -/// [TransactionWithMetadata] objects which contain additional metadata needed for broadcasting and -/// verification. -pub struct FilledTransactionsState { - pub args: ScriptArgs, - pub script_config: ScriptConfig, - pub script_wallets: ScriptWallets, - pub build_data: LinkedBuildData, - pub execution_data: ExecutionData, - pub execution_artifacts: ExecutionArtifacts, - pub transactions: VecDeque, -} - -/// State after we have bundled all [TransactionWithMetadata] objects into a single -/// [ScriptSequenceKind] object containing one or more script sequences. -pub struct BundledState { - pub args: ScriptArgs, - pub script_config: ScriptConfig, - pub script_wallets: ScriptWallets, - pub build_data: LinkedBuildData, - pub execution_data: ExecutionData, - pub execution_artifacts: ExecutionArtifacts, - pub sequence: ScriptSequenceKind, -} - -/// State after we have broadcasted the script. -/// It is assumed that at this point [BroadcastedState::sequence] contains receipts for all -/// broadcasted transactions. -pub struct BroadcastedState { - pub args: ScriptArgs, - pub script_config: ScriptConfig, - pub build_data: LinkedBuildData, - pub execution_data: ExecutionData, - pub execution_artifacts: ExecutionArtifacts, - pub sequence: ScriptSequenceKind, -} diff --git a/crates/script/src/verify.rs b/crates/script/src/verify.rs index 927854f8f23b..58921de70a68 100644 --- a/crates/script/src/verify.rs +++ b/crates/script/src/verify.rs @@ -1,28 +1,29 @@ -use super::states::{BroadcastedState, BundledState}; +use crate::{ + build::LinkedBuildData, + execute::{ExecutionArtifacts, ExecutionData}, + sequence::ScriptSequenceKind, + ScriptArgs, ScriptConfig, +}; + use alloy_primitives::Address; use eyre::Result; -use forge_verify::{provider::VerificationProviderType, RetryArgs, VerifierArgs, VerifyArgs}; +use forge_verify::{RetryArgs, VerifierArgs, VerifyArgs}; use foundry_cli::opts::{EtherscanOpts, ProjectPathsArgs}; use foundry_common::ContractsByArtifact; use foundry_compilers::{info::ContractInfo, Project}; use foundry_config::{Chain, Config}; use semver::Version; -impl BundledState { - pub fn verify_preflight_check(&self) -> Result<()> { - for sequence in self.sequence.sequences() { - if self.args.verifier.verifier == VerificationProviderType::Etherscan && - self.script_config - .config - .get_etherscan_api_key(Some(sequence.chain.into())) - .is_none() - { - eyre::bail!("Missing etherscan key for chain {}", sequence.chain); - } - } - - Ok(()) - } +/// State after we have broadcasted the script. +/// It is assumed that at this point [BroadcastedState::sequence] contains receipts for all +/// broadcasted transactions. +pub struct BroadcastedState { + pub args: ScriptArgs, + pub script_config: ScriptConfig, + pub build_data: LinkedBuildData, + pub execution_data: ExecutionData, + pub execution_artifacts: ExecutionArtifacts, + pub sequence: ScriptSequenceKind, } impl BroadcastedState { From 532e9c85e548bee8816bfa9fd954d488d3bebf51 Mon Sep 17 00:00:00 2001 From: Arsenii Kulikov Date: Fri, 8 Mar 2024 02:10:35 +0400 Subject: [PATCH 32/33] review fixes --- crates/script/src/broadcast.rs | 6 +++--- crates/script/src/multi_sequence.rs | 10 ++++++---- crates/script/src/sequence.rs | 10 ++++++---- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/crates/script/src/broadcast.rs b/crates/script/src/broadcast.rs index 03c9bf67bf8a..81fc2a409be1 100644 --- a/crates/script/src/broadcast.rs +++ b/crates/script/src/broadcast.rs @@ -28,6 +28,7 @@ use foundry_common::{ use foundry_config::Config; use foundry_wallets::WalletSigner; use futures::{future::join_all, StreamExt}; +use itertools::Itertools; use std::{ collections::{HashMap, HashSet}, sync::Arc, @@ -186,13 +187,12 @@ impl BundledState { }) .collect::>(); - let errors = - join_all(futs).await.into_iter().filter(|res| res.is_err()).collect::>(); + let errors = join_all(futs).await.into_iter().filter_map(Result::err).collect::>(); self.sequence.save(true, false)?; if !errors.is_empty() { - return Err(eyre::eyre!("{errors:?}")); + return Err(eyre::eyre!("{}", errors.iter().format("\n"))); } Ok(self) diff --git a/crates/script/src/multi_sequence.rs b/crates/script/src/multi_sequence.rs index fd58d30f24c6..ea6dfd0d4c5f 100644 --- a/crates/script/src/multi_sequence.rs +++ b/crates/script/src/multi_sequence.rs @@ -27,9 +27,11 @@ pub struct SensitiveMultiChainSequence { pub deployments: Vec, } -fn to_sensitive(sequence: &mut MultiChainSequence) -> SensitiveMultiChainSequence { - SensitiveMultiChainSequence { - deployments: sequence.deployments.iter_mut().map(|sequence| sequence.into()).collect(), +impl SensitiveMultiChainSequence { + fn from_multi_sequence(sequence: MultiChainSequence) -> SensitiveMultiChainSequence { + SensitiveMultiChainSequence { + deployments: sequence.deployments.into_iter().map(|sequence| sequence.into()).collect(), + } } } @@ -112,7 +114,7 @@ impl MultiChainSequence { self.timestamp = now().as_secs(); - let sensitive_sequence: SensitiveMultiChainSequence = to_sensitive(self); + let sensitive_sequence = SensitiveMultiChainSequence::from_multi_sequence(self.clone()); // broadcast writes //../Contract-latest/run.json diff --git a/crates/script/src/sequence.rs b/crates/script/src/sequence.rs index 19dfdb76b722..f5874002b03d 100644 --- a/crates/script/src/sequence.rs +++ b/crates/script/src/sequence.rs @@ -99,7 +99,9 @@ impl ScriptSequenceKind { impl Drop for ScriptSequenceKind { fn drop(&mut self) { - self.save(false, true).expect("could not save deployment sequence"); + if let Err(err) = self.save(false, true) { + error!(?err, "could not save deployment sequence"); + } } } @@ -136,8 +138,8 @@ pub struct SensitiveScriptSequence { pub transactions: VecDeque, } -impl From<&mut ScriptSequence> for SensitiveScriptSequence { - fn from(sequence: &mut ScriptSequence) -> Self { +impl From for SensitiveScriptSequence { + fn from(sequence: ScriptSequence) -> Self { SensitiveScriptSequence { transactions: sequence .transactions @@ -190,7 +192,7 @@ impl ScriptSequence { self.timestamp = now().as_secs(); let ts_name = format!("run-{}.json", self.timestamp); - let sensitive_script_sequence: SensitiveScriptSequence = self.into(); + let sensitive_script_sequence: SensitiveScriptSequence = self.clone().into(); // broadcast folder writes //../run-latest.json From 5bd705d3ca951b6b0828fa7e4e74ed58c90b9814 Mon Sep 17 00:00:00 2001 From: Arsenii Kulikov Date: Fri, 8 Mar 2024 16:36:08 +0400 Subject: [PATCH 33/33] remove redundant methods --- crates/script/src/broadcast.rs | 14 ++++++++------ crates/script/src/sequence.rs | 33 ++++++--------------------------- crates/script/src/verify.rs | 2 +- 3 files changed, 15 insertions(+), 34 deletions(-) diff --git a/crates/script/src/broadcast.rs b/crates/script/src/broadcast.rs index 81fc2a409be1..fb21276c2838 100644 --- a/crates/script/src/broadcast.rs +++ b/crates/script/src/broadcast.rs @@ -179,7 +179,8 @@ impl BundledState { pub async fn wait_for_pending(mut self) -> Result { let futs = self .sequence - .sequeneces_mut() + .sequences_mut() + .iter_mut() .map(|sequence| async move { let rpc_url = sequence.rpc_url(); let provider = Arc::new(get_http_provider(rpc_url)); @@ -203,6 +204,7 @@ impl BundledState { let required_addresses = self .sequence .sequences() + .iter() .flat_map(|sequence| { sequence .typed_transactions() @@ -239,8 +241,8 @@ impl BundledState { SendTransactionsKind::Raw(signers) }; - for i in 0..self.sequence.sequences_len() { - let mut sequence = self.sequence.get_sequence_mut(i).unwrap(); + for i in 0..self.sequence.sequences().len() { + let mut sequence = self.sequence.sequences_mut().get_mut(i).unwrap(); let provider = Arc::new(try_get_http_provider(sequence.rpc_url())?); let already_broadcasted = sequence.receipts.len(); @@ -359,7 +361,7 @@ impl BundledState { // Checkpoint save self.sequence.save(true, false)?; - sequence = self.sequence.get_sequence_mut(i).unwrap(); + sequence = self.sequence.sequences_mut().get_mut(i).unwrap(); update_progress!(pb, index - already_broadcasted); index += 1; @@ -367,14 +369,14 @@ impl BundledState { // Checkpoint save self.sequence.save(true, false)?; - sequence = self.sequence.get_sequence_mut(i).unwrap(); + sequence = self.sequence.sequences_mut().get_mut(i).unwrap(); shell::println("##\nWaiting for receipts.")?; receipts::clear_pendings(provider.clone(), sequence, None).await?; } // Checkpoint save self.sequence.save(true, false)?; - sequence = self.sequence.get_sequence_mut(i).unwrap(); + sequence = self.sequence.sequences_mut().get_mut(i).unwrap(); } } diff --git a/crates/script/src/sequence.rs b/crates/script/src/sequence.rs index f5874002b03d..6a78d1ac4f58 100644 --- a/crates/script/src/sequence.rs +++ b/crates/script/src/sequence.rs @@ -41,40 +41,19 @@ impl ScriptSequenceKind { } } - pub fn sequences(&self) -> impl Iterator { + pub fn sequences(&self) -> &[ScriptSequence] { match self { - ScriptSequenceKind::Single(sequence) => std::slice::from_ref(sequence).iter(), - ScriptSequenceKind::Multi(sequence) => sequence.deployments.iter(), + ScriptSequenceKind::Single(sequence) => std::slice::from_ref(sequence), + ScriptSequenceKind::Multi(sequence) => &sequence.deployments, } } - pub fn sequeneces_mut(&mut self) -> impl Iterator { + pub fn sequences_mut(&mut self) -> &mut [ScriptSequence] { match self { - ScriptSequenceKind::Single(sequence) => std::slice::from_mut(sequence).iter_mut(), - ScriptSequenceKind::Multi(sequence) => sequence.deployments.iter_mut(), + ScriptSequenceKind::Single(sequence) => std::slice::from_mut(sequence), + ScriptSequenceKind::Multi(sequence) => &mut sequence.deployments, } } - - pub fn sequences_len(&self) -> usize { - match self { - ScriptSequenceKind::Single(_) => 1, - ScriptSequenceKind::Multi(sequence) => sequence.deployments.len(), - } - } - - pub fn get_sequence_mut(&mut self, index: usize) -> Option<&mut ScriptSequence> { - match self { - ScriptSequenceKind::Single(sequence) => { - if index == 0 { - Some(sequence) - } else { - None - } - } - ScriptSequenceKind::Multi(sequence) => sequence.deployments.get_mut(index), - } - } - /// Updates underlying sequence paths to not be under /dry-run directory. pub fn update_paths_to_broadcasted( &mut self, diff --git a/crates/script/src/verify.rs b/crates/script/src/verify.rs index 58921de70a68..be5825dfc7e8 100644 --- a/crates/script/src/verify.rs +++ b/crates/script/src/verify.rs @@ -38,7 +38,7 @@ impl BroadcastedState { args.verifier, ); - for sequence in sequence.sequeneces_mut() { + for sequence in sequence.sequences_mut() { sequence.verify_contracts(&script_config.config, verify.clone()).await?; }