Skip to main content

forge_script/
build.rs

1use crate::{
2    ScriptArgs, ScriptConfig,
3    broadcast::{BundledState, remaining_unsigned_transactions_for_recovery},
4    execute::LinkedState,
5    multi_sequence::MultiChainSequence,
6    progress::ScriptProgress,
7    receipts::is_mined_receipt_for,
8    recovery::recovery_exists,
9    sequence::ScriptSequenceKind,
10    session::{SignerScope, script_session_expected_sender_if_configured},
11};
12use alloy_network::{AnyNetwork, ReceiptResponse};
13use alloy_primitives::{Address, B256, map::AddressHashSet};
14use alloy_provider::Provider;
15use eyre::{ContextCompat, OptionExt, Result};
16use forge_script_sequence::ScriptSequence;
17use foundry_cheatcodes::Wallets;
18use foundry_cli::opts::TempoOpts;
19use foundry_common::{
20    ContractData, ContractsByArtifact, ContractsByArtifactBuilder, compile::ProjectCompiler,
21    external_compiler::is_builtin_compiler_source, fs::canonicalize_path,
22    provider::ProviderBuilder,
23};
24use foundry_compilers::{
25    ArtifactId, ProjectCompileOutput,
26    artifacts::{BytecodeObject, Libraries},
27    compilers::{Language, multi::MultiCompilerLanguage},
28    info::ContractInfo,
29    utils::source_files_iter,
30};
31use foundry_evm::{core::evm::FoundryEvmNetwork, traces::debug::ContractSources};
32use foundry_linking::Linker;
33use foundry_wallets::{MultiWalletOpts, wallet_browser::signer::BrowserSigner};
34use std::{
35    path::{Path, PathBuf},
36    str::FromStr,
37    sync::Arc,
38};
39
40/// Container for the compiled contracts.
41#[derive(Clone, Debug)]
42pub struct BuildData {
43    /// Root of the project.
44    pub project_root: PathBuf,
45    /// The compiler output.
46    pub output: ProjectCompileOutput,
47    /// ID of target contract artifact.
48    pub target: ArtifactId,
49}
50
51impl BuildData {
52    pub fn get_linker(&self) -> Linker<'_> {
53        Linker::new(self.project_root.clone(), self.output.artifact_ids().collect())
54    }
55
56    /// Links contracts. Uses CREATE2 linking when possible, otherwise falls back to
57    /// default linking with sender nonce and address.
58    pub async fn link<FEN: FoundryEvmNetwork>(
59        self,
60        script_config: &ScriptConfig<FEN>,
61    ) -> Result<LinkedBuildData> {
62        let create2_deployer = script_config.evm_opts.create2_deployer;
63        let can_use_create2 = script_config
64            .backend
65            .can_use_create2_deployer(script_config.evm_opts.create2_deployer)
66            .await?;
67
68        let known_libraries = script_config.config.libraries_with_remappings()?;
69
70        let maybe_create2_link_output = can_use_create2
71            .then(|| {
72                self.get_linker()
73                    .link_with_create2_detailed(
74                        known_libraries.clone(),
75                        create2_deployer,
76                        script_config.config.create2_library_salt,
77                        [&self.target],
78                    )
79                    .ok()
80            })
81            .flatten();
82
83        let (libraries, predeploy_libs) = if let Some(output) = maybe_create2_link_output {
84            (
85                output.output.libraries,
86                ScriptPredeployLibraries::Create2 {
87                    onchain: output.linked_libraries,
88                    salt: script_config.config.create2_library_salt,
89                    local: Vec::new(),
90                },
91            )
92        } else {
93            let output = self.get_linker().link_with_nonce_or_address_detailed(
94                known_libraries,
95                script_config.evm_opts.sender,
96                script_config.sender_nonce,
97                [&self.target],
98            )?;
99
100            (
101                output.output.libraries,
102                ScriptPredeployLibraries::Default {
103                    onchain: output.linked_libraries,
104                    local: Vec::new(),
105                },
106            )
107        };
108
109        LinkedBuildData::new(libraries, predeploy_libs, self)
110    }
111
112    /// Links the build data with the given libraries. Expects supplied libraries set being enough
113    /// to fully link target contract.
114    pub fn link_with_libraries(self, libraries: Libraries) -> Result<LinkedBuildData> {
115        LinkedBuildData::new(
116            libraries,
117            ScriptPredeployLibraries::Default { onchain: Vec::new(), local: Vec::new() },
118            self,
119        )
120    }
121}
122
123#[derive(Clone, Debug)]
124pub enum ScriptPredeployLibraries {
125    Default {
126        onchain: Vec<foundry_linking::LinkedLibrary>,
127        local: Vec<foundry_linking::LinkedLibrary>,
128    },
129    Create2 {
130        onchain: Vec<foundry_linking::LinkedLibrary>,
131        salt: B256,
132        local: Vec<foundry_linking::LinkedLibrary>,
133    },
134}
135
136impl ScriptPredeployLibraries {
137    pub const fn libraries_count(&self) -> usize {
138        match self {
139            Self::Default { onchain, .. } => onchain.len(),
140            Self::Create2 { onchain, .. } => onchain.len(),
141        }
142    }
143}
144
145/// Container for the linked contracts and their dependencies
146#[derive(Clone, Debug)]
147pub struct LinkedBuildData {
148    /// Original build data, might be used to relink this object with different libraries.
149    pub build_data: BuildData,
150    /// Known fully linked contracts.
151    pub known_contracts: ContractsByArtifact,
152    /// Libraries used to link the contracts.
153    pub libraries: Libraries,
154    /// Libraries that need to be deployed by sender before script execution.
155    pub predeploy_libraries: ScriptPredeployLibraries,
156    /// Source files of the contracts. Used by debugger.
157    pub sources: ContractSources,
158}
159
160impl LinkedBuildData {
161    pub fn new(
162        libraries: Libraries,
163        predeploy_libraries: ScriptPredeployLibraries,
164        build_data: BuildData,
165    ) -> Result<Self> {
166        let sources = ContractSources::from_project_output(
167            &build_data.output,
168            &build_data.project_root,
169            Some(&libraries),
170        )?;
171
172        let linked_contracts = build_data.get_linker().get_linked_artifacts(&libraries)?;
173        let known_contracts = ContractsByArtifactBuilder::new(
174            linked_contracts.iter().map(|(id, artifact)| (id.clone(), artifact.into())),
175        )
176        .with_storage_layouts(build_data.output.artifact_ids().filter_map(|(id, artifact)| {
177            artifact.storage_layout.as_ref().map(|layout| (id, layout.clone()))
178        }))
179        .build();
180
181        Ok(Self { build_data, known_contracts, libraries, predeploy_libraries, sources })
182    }
183
184    /// Fetches target bytecode from linked contracts.
185    pub fn get_target_contract(&self) -> Result<&ContractData> {
186        self.known_contracts
187            .get(&self.build_data.target)
188            .ok_or_eyre("target not found in linked artifacts")
189    }
190}
191
192/// First state basically containing only inputs of the user.
193pub struct PreprocessedState<FEN: FoundryEvmNetwork> {
194    pub args: ScriptArgs,
195    pub script_config: ScriptConfig<FEN>,
196    pub script_wallets: Wallets,
197    pub browser_wallet: Option<BrowserSigner<FEN::Network>>,
198}
199
200impl<FEN: FoundryEvmNetwork> PreprocessedState<FEN> {
201    /// Parses user input and compiles the contracts depending on script target.
202    /// After compilation, finds exact [ArtifactId] of the target contract.
203    pub fn compile(self) -> Result<CompiledState<FEN>> {
204        let Self { args, script_config, script_wallets, browser_wallet } = self;
205        let project = script_config.config.project()?;
206
207        let mut target_name = args.target_contract.clone();
208
209        // If we've received correct path, use it as target_path
210        // Otherwise, parse input as <path>:<name> and use the path from the contract info, if
211        // present.
212        let target_path = if let Ok(path) = canonicalize_path(&args.path) {
213            path
214        } else {
215            let contract = ContractInfo::from_str(&args.path)?;
216            target_name = Some(contract.name.clone());
217            if let Some(path) = contract.path {
218                canonicalize_path(path)?
219            } else {
220                project.find_contract_path(contract.name.as_str())?
221            }
222        };
223
224        let sources_to_compile = source_files_iter(
225            project.paths.sources.as_path(),
226            MultiCompilerLanguage::FILE_EXTENSIONS,
227        );
228
229        let compiler = ProjectCompiler::new()
230            .external_compilers(&script_config.config)
231            .files(sources_to_compile)
232            .dynamic_test_linking(script_config.config.dynamic_test_linking);
233        let compiler = if is_builtin_compiler_source(&target_path) {
234            compiler.files([target_path.clone()])
235        } else {
236            compiler.target_files([target_path.clone()])
237        };
238        let output = compiler.compile(&project)?;
239
240        let mut target_id: Option<ArtifactId> = None;
241
242        // Find target artifact id by name and path in compilation artifacts.
243        for (id, contract) in output.artifact_ids().filter(|(id, _)| id.source == target_path) {
244            if let Some(name) = &target_name {
245                if id.name != *name {
246                    continue;
247                }
248            } else if contract.abi.as_ref().is_none_or(|abi| abi.is_empty())
249                || contract.bytecode.as_ref().is_none_or(|b| match &b.object {
250                    BytecodeObject::Bytecode(b) => b.is_empty(),
251                    BytecodeObject::Unlinked(_) => false,
252                })
253            {
254                // Ignore contracts with empty abi or linked bytecode of length 0 which are
255                // interfaces/abstract contracts/libraries.
256                continue;
257            }
258
259            if let Some(target) = target_id {
260                // We might have multiple artifacts for the same contract but with different
261                // solc versions. Their names will have form of {name}.0.X.Y, so we are
262                // stripping versions off before comparing them.
263                let target_name = target.name.split('.').next().unwrap();
264                let id_name = id.name.split('.').next().unwrap();
265                if target_name != id_name {
266                    eyre::bail!(
267                        "Multiple contracts in the target path. Please specify the contract name with `--tc ContractName`"
268                    );
269                }
270            }
271            target_id = Some(id);
272        }
273
274        let target = target_id.ok_or_eyre("Could not find target contract")?;
275
276        Ok(CompiledState {
277            args,
278            script_config,
279            script_wallets,
280            browser_wallet,
281            build_data: BuildData { output, target, project_root: project.root().to_path_buf() },
282        })
283    }
284}
285
286/// State after we have determined and compiled target contract to be executed.
287pub struct CompiledState<FEN: FoundryEvmNetwork> {
288    pub args: ScriptArgs,
289    pub script_config: ScriptConfig<FEN>,
290    pub script_wallets: Wallets,
291    pub browser_wallet: Option<BrowserSigner<FEN::Network>>,
292    pub build_data: BuildData,
293}
294
295impl<FEN: FoundryEvmNetwork> CompiledState<FEN> {
296    /// Uses provided sender address to compute library addresses and link contracts with them.
297    pub async fn link(self) -> Result<LinkedState<FEN>> {
298        let Self { args, script_config, script_wallets, browser_wallet, build_data } = self;
299
300        let build_data = build_data.link(&script_config).await?;
301
302        Ok(LinkedState { args, script_config, script_wallets, browser_wallet, build_data })
303    }
304
305    /// Tries loading the resumed state from the cache files, skipping simulation stage.
306    pub async fn resume(mut self) -> Result<BundledState<FEN>> {
307        let chain = if self.args.multi {
308            None
309        } else {
310            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")?;
311            let provider = Arc::new(ProviderBuilder::<AnyNetwork>::new(&fork_url).build()?);
312            Some(provider.get_chain_id().await?)
313        };
314
315        let mut sequence = if self.sequence_exists(chain, false)? {
316            self.try_load_sequence(chain, false)?
317        } else {
318            // If the script was simulated, but there was no attempt to broadcast yet,
319            // read the script sequence from the `dry-run/` folder.
320            let mut sequence = self.try_load_sequence(chain, true)?;
321
322            // Promote the complete dry-run sequence before broadcasting it.
323            sequence.promote_to_broadcasted(
324                &self.script_config.config,
325                &self.args.sig,
326                &self.build_data.target,
327            )?;
328            sequence
329        };
330
331        let resolution = sequence.restore_delegated_pending(
332            self.args.resume_attempt,
333            self.args.resume_tx_hash,
334            self.args.resume_retry,
335        )?;
336        if !self.args.batch {
337            if let Some((sequence_index, _, attempt_id, hash)) = resolution {
338                let provider = ProviderBuilder::<FEN::Network>::from_config_with_url(
339                    &self.script_config.config,
340                    sequence.sequences()[sequence_index].rpc_url(),
341                )?
342                .build()?;
343                let transaction = provider
344                    .get_transaction_by_hash(hash)
345                    .await?
346                    .context("resolved transaction is not available from the recovery endpoint")?;
347                sequence.resolve_delegated_hash(attempt_id, hash, &transaction)?;
348            }
349            let progress = ScriptProgress::default();
350            for index in 0..sequence.sequences().len() {
351                let provider = ProviderBuilder::<FEN::Network>::from_config_with_url(
352                    &self.script_config.config,
353                    sequence.sequences()[index].rpc_url(),
354                )?
355                .build()?;
356                // A saved signed attempt whose response was lost before its hash was recorded is
357                // reconciled before requesting signers when its receipt shows it was mined.
358                for operation in 0..sequence.sequences()[index].transactions.len() {
359                    let deployment = &sequence.sequences()[index];
360                    if let Some(hash) = sequence.signed_payload(index, operation).map(|s| s.hash)
361                        && !deployment.pending.contains(&hash)
362                        && !deployment.receipts.iter().any(|r| r.transaction_hash() == hash)
363                        && let Some(receipt) = provider.get_transaction_receipt(hash).await?
364                        && is_mined_receipt_for(&receipt, hash)
365                    {
366                        sequence.sequences_mut()[index].add_pending(operation, hash);
367                    }
368                }
369                if sequence.sequences()[index].pending.is_empty() {
370                    continue;
371                }
372                let (durable_hashes, replayable_hashes) = sequence.submission_hashes(index);
373                let result = progress
374                    .wait_for_pending(
375                        index,
376                        &mut sequence.sequences_mut()[index],
377                        &provider,
378                        self.script_config.config.transaction_timeout,
379                        self.args.confirmations,
380                        (&durable_hashes, &replayable_hashes),
381                    )
382                    .await;
383                sequence.save(true, false)?;
384                result?;
385                sequence.ensure_delegated_outcomes_known(index)?;
386            }
387        }
388
389        if !self.args.unlocked
390            && !remaining_unsigned_transactions_for_recovery(&sequence).is_empty()
391        {
392            self.script_wallets =
393                Wallets::new(self.args.wallets.get_multi_wallet().await?, self.args.evm.sender);
394            self.browser_wallet = self.args.wallets.browser_signer::<FEN::Network>().await?;
395
396            if self.args.evm.sender.is_none() {
397                let addresses = self.script_wallets.addresses();
398                let sender = self
399                    .args
400                    .maybe_load_private_key()?
401                    .or_else(|| (addresses.len() == 1).then(|| addresses[0]))
402                    .or_else(|| self.browser_wallet.as_ref().map(|wallet| wallet.address()));
403                if let Some(sender) = sender {
404                    self.script_config.update_sender(sender).await?;
405                }
406            }
407        }
408
409        let (args, build_data, script_wallets, browser_wallet, script_config) =
410            if self.args.unlocked {
411                (
412                    self.args,
413                    self.build_data,
414                    self.script_wallets,
415                    self.browser_wallet,
416                    self.script_config,
417                )
418            } else {
419                let remaining_transactions =
420                    remaining_unsigned_transactions_for_recovery(&sequence);
421                let remaining_froms =
422                    remaining_transactions.iter().map(|tx| tx.sender).collect::<AddressHashSet>();
423                let expected_session_sender = script_session_expected_sender_if_configured(
424                    &self.script_config.tempo,
425                    &remaining_froms,
426                )?;
427                let has_available_signers = has_available_script_signers(
428                    &self.script_config.tempo,
429                    &self.args.wallets,
430                    &self.script_wallets,
431                    expected_session_sender,
432                    &remaining_transactions,
433                )?;
434
435                if has_available_signers {
436                    (
437                        self.args,
438                        self.build_data,
439                        self.script_wallets,
440                        self.browser_wallet,
441                        self.script_config,
442                    )
443                } else {
444                    // IF we are missing required signers, execute script as we might need to
445                    // collect private keys from the execution.
446                    let mut state = self;
447                    state
448                        .script_config
449                        .update_tempo_session_sender(&state.args.wallets, state.args.evm.sender)
450                        .await?;
451                    let executed = state.link().await?.prepare_execution().await?.execute().await?;
452                    (
453                        executed.args,
454                        executed.build_data.build_data,
455                        executed.script_wallets,
456                        executed.browser_wallet,
457                        executed.script_config,
458                    )
459                }
460            };
461
462        // Collect libraries from sequence and link contracts with them.
463        let libraries = if sequence.is_multi() {
464            // Library linking is not supported for multi-chain sequences.
465            Libraries::default()
466        } else {
467            Libraries::parse(&sequence.sequences()[0].libraries)?
468        };
469
470        let linked_build_data = build_data.link_with_libraries(libraries)?;
471
472        Ok(BundledState {
473            args,
474            script_config,
475            script_wallets,
476            browser_wallet,
477            build_data: linked_build_data,
478            sequence,
479        })
480    }
481
482    fn try_load_sequence(
483        &self,
484        chain: Option<u64>,
485        dry_run: bool,
486    ) -> Result<ScriptSequenceKind<FEN::Network>> {
487        if let Some(chain) = chain {
488            ScriptSequenceKind::load_single(
489                &self.script_config.config,
490                &self.args.sig,
491                &self.build_data.target,
492                chain,
493                dry_run,
494                self.args.batch,
495            )
496        } else {
497            ScriptSequenceKind::load_multi(
498                &self.script_config.config,
499                &self.args.sig,
500                &self.build_data.target,
501                dry_run,
502                self.args.batch,
503            )
504        }
505    }
506
507    fn sequence_exists(&self, chain: Option<u64>, dry_run: bool) -> Result<bool> {
508        let paths = if let Some(chain) = chain {
509            ScriptSequence::<FEN::Network>::get_paths(
510                &self.script_config.config,
511                &self.args.sig,
512                &self.build_data.target,
513                chain,
514                dry_run,
515            )?
516        } else {
517            MultiChainSequence::<FEN::Network>::get_paths(
518                &self.script_config.config,
519                &self.args.sig,
520                &self.build_data.target,
521                dry_run,
522            )?
523        };
524        Ok(compatibility_progress_exists(&paths) || recovery_exists(&paths)?)
525    }
526}
527
528fn compatibility_progress_exists(paths: &(PathBuf, PathBuf)) -> bool {
529    [&paths.0, &paths.1].into_iter().any(|path| progress_exists(path))
530}
531
532fn progress_exists(path: &Path) -> bool {
533    path.exists() || path.with_extension("previous").exists()
534}
535
536/// Returns whether every scoped signer needed for resume is already available.
537///
538/// `Wallets` only tracks signers collected from CLI options and script cheatcodes. A Tempo
539/// session signer lives in the Accounts store instead, so resume needs to treat the session
540/// root account as available only on the chain covered by the session.
541fn has_available_script_signers(
542    tempo: &TempoOpts,
543    wallets: &MultiWalletOpts,
544    script_wallets: &Wallets,
545    expected_sender: Option<Address>,
546    remaining: &[SignerScope],
547) -> Result<bool> {
548    let signers = script_wallets
549        .signers()
550        .map_err(|e| eyre::eyre!("Failed to get available signers: {}", e))?;
551    if remaining.is_empty() {
552        return Ok(true);
553    }
554
555    let session_scope = tempo
556        .session_signer_for_multi_wallet_any_chain(wallets, expected_sender)?
557        .map(|s| SignerScope::new(s.session.chain_id, s.access_key.account()));
558
559    Ok(remaining.iter().all(|tx| signers.contains(&tx.sender) || session_scope == Some(*tx)))
560}
561
562#[cfg(test)]
563mod tests {
564    use super::*;
565
566    #[test]
567    fn backup_counts_as_recoverable_progress() {
568        let dir = tempfile::tempdir().unwrap();
569        let path = dir.path().join("run-latest.json");
570        std::fs::write(path.with_extension("previous"), b"{}").unwrap();
571
572        assert!(progress_exists(&path));
573    }
574
575    #[test]
576    fn sensitive_export_counts_as_recoverable_progress() {
577        let dir = tempfile::tempdir().unwrap();
578        let paths = (dir.path().join("run-latest.json"), dir.path().join("run-latest-cache.json"));
579        std::fs::write(&paths.1, b"{}").unwrap();
580
581        assert!(compatibility_progress_exists(&paths));
582    }
583
584    #[test]
585    fn has_available_script_signers_skips_session_resolution_when_remaining_empty() {
586        let has_available = has_available_script_signers(
587            &TempoOpts { session: Some(B256::repeat_byte(0x99)), ..Default::default() },
588            &MultiWalletOpts::default(),
589            &Wallets::new(Default::default(), None),
590            None,
591            &[],
592        )
593        .unwrap();
594
595        assert!(has_available);
596    }
597}