Skip to main content

forge_script/
build.rs

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