Skip to main content

forge_script/
build.rs

1use crate::{
2    ScriptArgs, ScriptConfig, broadcast::BundledState, execute::LinkedState,
3    multi_sequence::MultiChainSequence, sequence::ScriptSequenceKind,
4};
5use alloy_network::AnyNetwork;
6use alloy_primitives::{B256, Bytes};
7use alloy_provider::Provider;
8use eyre::{OptionExt, Result};
9use forge_script_sequence::ScriptSequence;
10use foundry_cheatcodes::Wallets;
11use foundry_common::{
12    ContractData, ContractsByArtifact, compile::ProjectCompiler, provider::ProviderBuilder,
13};
14use foundry_compilers::{
15    ArtifactId, ProjectCompileOutput,
16    artifacts::{BytecodeObject, Libraries},
17    compilers::{Language, multi::MultiCompilerLanguage},
18    info::ContractInfo,
19    utils::source_files_iter,
20};
21use foundry_evm::{core::evm::FoundryEvmNetwork, traces::debug::ContractSources};
22use foundry_linking::Linker;
23use foundry_wallets::wallet_browser::signer::BrowserSigner;
24use std::{path::PathBuf, str::FromStr, sync::Arc};
25
26/// Container for the compiled contracts.
27#[derive(Debug)]
28pub struct BuildData {
29    /// Root of the project.
30    pub project_root: PathBuf,
31    /// The compiler output.
32    pub output: ProjectCompileOutput,
33    /// ID of target contract artifact.
34    pub target: ArtifactId,
35}
36
37impl BuildData {
38    pub fn get_linker(&self) -> Linker<'_> {
39        Linker::new(self.project_root.clone(), self.output.artifact_ids().collect())
40    }
41
42    /// Links contracts. Uses CREATE2 linking when possible, otherwise falls back to
43    /// default linking with sender nonce and address.
44    pub async fn link<FEN: FoundryEvmNetwork>(
45        self,
46        script_config: &ScriptConfig<FEN>,
47    ) -> Result<LinkedBuildData> {
48        let create2_deployer = script_config.evm_opts.create2_deployer;
49        let can_use_create2 = if let Some(fork_url) = &script_config.evm_opts.fork_url {
50            let provider = ProviderBuilder::<AnyNetwork>::new(fork_url).build()?;
51            let deployer_code = provider.get_code_at(create2_deployer).await?;
52
53            !deployer_code.is_empty()
54        } else {
55            // If --fork-url is not provided, we are just simulating the script.
56            true
57        };
58
59        let known_libraries = script_config.config.libraries_with_remappings()?;
60
61        let maybe_create2_link_output = can_use_create2
62            .then(|| {
63                self.get_linker()
64                    .link_with_create2(
65                        known_libraries.clone(),
66                        create2_deployer,
67                        script_config.config.create2_library_salt,
68                        &self.target,
69                    )
70                    .ok()
71            })
72            .flatten();
73
74        let (libraries, predeploy_libs) = if let Some(output) = maybe_create2_link_output {
75            (
76                output.libraries,
77                ScriptPredeployLibraries::Create2(
78                    output.libs_to_deploy,
79                    script_config.config.create2_library_salt,
80                ),
81            )
82        } else {
83            let output = self.get_linker().link_with_nonce_or_address(
84                known_libraries,
85                script_config.evm_opts.sender,
86                script_config.sender_nonce,
87                [&self.target],
88            )?;
89
90            (output.libraries, ScriptPredeployLibraries::Default(output.libs_to_deploy))
91        };
92
93        LinkedBuildData::new(libraries, predeploy_libs, self)
94    }
95
96    /// Links the build data with the given libraries. Expects supplied libraries set being enough
97    /// to fully link target contract.
98    pub fn link_with_libraries(self, libraries: Libraries) -> Result<LinkedBuildData> {
99        LinkedBuildData::new(libraries, ScriptPredeployLibraries::Default(Vec::new()), self)
100    }
101}
102
103#[derive(Debug)]
104pub enum ScriptPredeployLibraries {
105    Default(Vec<Bytes>),
106    Create2(Vec<Bytes>, B256),
107}
108
109impl ScriptPredeployLibraries {
110    pub fn libraries_count(&self) -> usize {
111        match self {
112            Self::Default(libs) => libs.len(),
113            Self::Create2(libs, _) => libs.len(),
114        }
115    }
116}
117
118/// Container for the linked contracts and their dependencies
119#[derive(Debug)]
120pub struct LinkedBuildData {
121    /// Original build data, might be used to relink this object with different libraries.
122    pub build_data: BuildData,
123    /// Known fully linked contracts.
124    pub known_contracts: ContractsByArtifact,
125    /// Libraries used to link the contracts.
126    pub libraries: Libraries,
127    /// Libraries that need to be deployed by sender before script execution.
128    pub predeploy_libraries: ScriptPredeployLibraries,
129    /// Source files of the contracts. Used by debugger.
130    pub sources: ContractSources,
131}
132
133impl LinkedBuildData {
134    pub fn new(
135        libraries: Libraries,
136        predeploy_libraries: ScriptPredeployLibraries,
137        build_data: BuildData,
138    ) -> Result<Self> {
139        let sources = ContractSources::from_project_output(
140            &build_data.output,
141            &build_data.project_root,
142            Some(&libraries),
143        )?;
144
145        let known_contracts =
146            ContractsByArtifact::new(build_data.get_linker().get_linked_artifacts(&libraries)?);
147
148        Ok(Self { build_data, known_contracts, libraries, predeploy_libraries, sources })
149    }
150
151    /// Fetches target bytecode from linked contracts.
152    pub fn get_target_contract(&self) -> Result<&ContractData> {
153        self.known_contracts
154            .get(&self.build_data.target)
155            .ok_or_eyre("target not found in linked artifacts")
156    }
157}
158
159/// First state basically containing only inputs of the user.
160pub struct PreprocessedState<FEN: FoundryEvmNetwork> {
161    pub args: ScriptArgs,
162    pub script_config: ScriptConfig<FEN>,
163    pub script_wallets: Wallets,
164    pub browser_wallet: Option<BrowserSigner<FEN::Network>>,
165}
166
167impl<FEN: FoundryEvmNetwork> PreprocessedState<FEN> {
168    /// Parses user input and compiles the contracts depending on script target.
169    /// After compilation, finds exact [ArtifactId] of the target contract.
170    pub fn compile(self) -> Result<CompiledState<FEN>> {
171        let Self { args, script_config, script_wallets, browser_wallet } = self;
172        let project = script_config.config.project()?;
173
174        let mut target_name = args.target_contract.clone();
175
176        // If we've received correct path, use it as target_path
177        // Otherwise, parse input as <path>:<name> and use the path from the contract info, if
178        // present.
179        let target_path = if let Ok(path) = dunce::canonicalize(&args.path) {
180            path
181        } else {
182            let contract = ContractInfo::from_str(&args.path)?;
183            target_name = Some(contract.name.clone());
184            if let Some(path) = contract.path {
185                dunce::canonicalize(path)?
186            } else {
187                project.find_contract_path(contract.name.as_str())?
188            }
189        };
190
191        let sources_to_compile = source_files_iter(
192            project.paths.sources.as_path(),
193            MultiCompilerLanguage::FILE_EXTENSIONS,
194        )
195        .chain([target_path.clone()]);
196
197        let output = ProjectCompiler::new().files(sources_to_compile).compile(&project)?;
198
199        let mut target_id: Option<ArtifactId> = None;
200
201        // Find target artifact id by name and path in compilation artifacts.
202        for (id, contract) in output.artifact_ids().filter(|(id, _)| id.source == target_path) {
203            if let Some(name) = &target_name {
204                if id.name != *name {
205                    continue;
206                }
207            } else if contract.abi.as_ref().is_none_or(|abi| abi.is_empty())
208                || contract.bytecode.as_ref().is_none_or(|b| match &b.object {
209                    BytecodeObject::Bytecode(b) => b.is_empty(),
210                    BytecodeObject::Unlinked(_) => false,
211                })
212            {
213                // Ignore contracts with empty abi or linked bytecode of length 0 which are
214                // interfaces/abstract contracts/libraries.
215                continue;
216            }
217
218            if let Some(target) = target_id {
219                // We might have multiple artifacts for the same contract but with different
220                // solc versions. Their names will have form of {name}.0.X.Y, so we are
221                // stripping versions off before comparing them.
222                let target_name = target.name.split('.').next().unwrap();
223                let id_name = id.name.split('.').next().unwrap();
224                if target_name != id_name {
225                    eyre::bail!(
226                        "Multiple contracts in the target path. Please specify the contract name with `--tc ContractName`"
227                    )
228                }
229            }
230            target_id = Some(id);
231        }
232
233        let target = target_id.ok_or_eyre("Could not find target contract")?;
234
235        Ok(CompiledState {
236            args,
237            script_config,
238            script_wallets,
239            browser_wallet,
240            build_data: BuildData { output, target, project_root: project.root().to_path_buf() },
241        })
242    }
243}
244
245/// State after we have determined and compiled target contract to be executed.
246pub struct CompiledState<FEN: FoundryEvmNetwork> {
247    pub args: ScriptArgs,
248    pub script_config: ScriptConfig<FEN>,
249    pub script_wallets: Wallets,
250    pub browser_wallet: Option<BrowserSigner<FEN::Network>>,
251    pub build_data: BuildData,
252}
253
254impl<FEN: FoundryEvmNetwork> CompiledState<FEN> {
255    /// Uses provided sender address to compute library addresses and link contracts with them.
256    pub async fn link(self) -> Result<LinkedState<FEN>> {
257        let Self { args, script_config, script_wallets, browser_wallet, build_data } = self;
258
259        let build_data = build_data.link(&script_config).await?;
260
261        Ok(LinkedState { args, script_config, script_wallets, browser_wallet, build_data })
262    }
263
264    /// Tries loading the resumed state from the cache files, skipping simulation stage.
265    pub async fn resume(self) -> Result<BundledState<FEN>> {
266        let chain = if self.args.multi {
267            None
268        } else {
269            let fork_url = self.script_config.evm_opts.fork_url.clone().ok_or_eyre("Missing --fork-url field, if you were trying to broadcast a multi-chain sequence, please use --multi flag")?;
270            let provider = Arc::new(ProviderBuilder::<AnyNetwork>::new(&fork_url).build()?);
271            Some(provider.get_chain_id().await?)
272        };
273
274        let sequence = match self.try_load_sequence(chain, false) {
275            Ok(sequence) => sequence,
276            Err(_) => {
277                // If the script was simulated, but there was no attempt to broadcast yet,
278                // try to read the script sequence from the `dry-run/` folder
279                let mut sequence = self.try_load_sequence(chain, true)?;
280
281                // If sequence was in /dry-run, Update its paths so it is not saved into /dry-run
282                // this time as we are about to broadcast it.
283                sequence.update_paths_to_broadcasted(
284                    &self.script_config.config,
285                    &self.args.sig,
286                    &self.build_data.target,
287                )?;
288
289                sequence.save(true, true)?;
290                sequence
291            }
292        };
293
294        let (args, build_data, script_wallets, browser_wallet, script_config) =
295            if self.args.unlocked {
296                (
297                    self.args,
298                    self.build_data,
299                    self.script_wallets,
300                    self.browser_wallet,
301                    self.script_config,
302                )
303            } else {
304                let mut froms = sequence.sequences().iter().flat_map(|s| {
305                    s.transactions
306                        .iter()
307                        .skip(s.receipts.len())
308                        .map(|t| t.transaction.from().expect("from is missing in script artifact"))
309                });
310
311                let available_signers = self
312                    .script_wallets
313                    .signers()
314                    .map_err(|e| eyre::eyre!("Failed to get available signers: {}", e))?;
315
316                if froms.all(|from| available_signers.contains(&from)) {
317                    (
318                        self.args,
319                        self.build_data,
320                        self.script_wallets,
321                        self.browser_wallet,
322                        self.script_config,
323                    )
324                } else {
325                    // IF we are missing required signers, execute script as we might need to
326                    // collect private keys from the execution.
327                    let executed = self.link().await?.prepare_execution().await?.execute().await?;
328                    (
329                        executed.args,
330                        executed.build_data.build_data,
331                        executed.script_wallets,
332                        executed.browser_wallet,
333                        executed.script_config,
334                    )
335                }
336            };
337
338        // Collect libraries from sequence and link contracts with them.
339        let libraries = match sequence {
340            ScriptSequenceKind::Single(ref seq) => Libraries::parse(&seq.libraries)?,
341            // Library linking is not supported for multi-chain sequences
342            ScriptSequenceKind::Multi(_) => Libraries::default(),
343        };
344
345        let linked_build_data = build_data.link_with_libraries(libraries)?;
346
347        Ok(BundledState {
348            args,
349            script_config,
350            script_wallets,
351            browser_wallet,
352            build_data: linked_build_data,
353            sequence,
354        })
355    }
356
357    fn try_load_sequence(
358        &self,
359        chain: Option<u64>,
360        dry_run: bool,
361    ) -> Result<ScriptSequenceKind<FEN::Network>> {
362        if let Some(chain) = chain {
363            let sequence = ScriptSequence::load(
364                &self.script_config.config,
365                &self.args.sig,
366                &self.build_data.target,
367                chain,
368                dry_run,
369            )?;
370            Ok(ScriptSequenceKind::Single(sequence))
371        } else {
372            let sequence = MultiChainSequence::load(
373                &self.script_config.config,
374                &self.args.sig,
375                &self.build_data.target,
376                dry_run,
377            )?;
378            Ok(ScriptSequenceKind::Multi(sequence))
379        }
380    }
381}