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