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