Skip to main content

forge/cmd/
create.rs

1use crate::cmd::install;
2use alloy_chains::Chain;
3use alloy_consensus::{SignableTransaction, Signed};
4use alloy_dyn_abi::{DynSolValue, JsonAbiExt, Specifier};
5use alloy_json_abi::{Constructor, JsonAbi};
6use alloy_network::{Ethereum, EthereumWallet, Network, ReceiptResponse, TransactionBuilder};
7use alloy_primitives::{Address, Bytes, U256, hex};
8use alloy_provider::{PendingTransactionError, Provider, ProviderBuilder as AlloyProviderBuilder};
9use alloy_signer::{Signature, Signer};
10use alloy_transport::TransportError;
11use clap::{Parser, ValueHint};
12use eyre::{Context, ContextCompat, Result};
13use forge_verify::{RetryArgs, VerifierArgs, VerifyArgs, parse_etherscan_license_type};
14use foundry_cli::{
15    opts::{BuildOpts, EthereumOpts, EtherscanOpts, TransactionOpts},
16    utils::{
17        LoadConfig, ResolvedLane, find_contract_artifacts, maybe_print_resolved_lane,
18        read_constructor_args_file, resolve_lane,
19    },
20};
21use foundry_common::{
22    FoundryTransactionBuilder,
23    compile::{self},
24    fmt::parse_tokens,
25    provider::{
26        ProviderBuilder,
27        fee::{estimate_eip1559_fees, resolve_broadcast_eip1559_fees},
28    },
29    shell,
30    tempo::{TEMPO_BROWSER_GAS_BUFFER, maybe_print_fee_token, resolve_and_set_fee_token},
31};
32use foundry_compilers::{
33    ArtifactId, artifacts::BytecodeObject, info::ContractInfo, utils::canonicalize,
34};
35use foundry_config::{
36    Config, Eip1559FeeEstimatePreset,
37    figment::{
38        self, Metadata, Profile,
39        value::{Dict, Map},
40    },
41    merge_impl_figment_convert,
42};
43use foundry_wallets::{
44    BrowserWalletOpts, TempoAccessKeyConfig, WalletSigner, wallet_browser::signer::BrowserSigner,
45};
46use serde_json::json;
47use std::{borrow::Borrow, marker::PhantomData, path::PathBuf, sync::Arc, time::Duration};
48use tempo_alloy::TempoNetwork;
49
50merge_impl_figment_convert!(CreateArgs, build, eth);
51
52/// CLI arguments for `forge create`.
53#[derive(Clone, Debug, Parser)]
54pub struct CreateArgs {
55    /// The contract identifier in the form `<path>:<contractname>`.
56    contract: ContractInfo,
57
58    /// The constructor arguments.
59    #[arg(
60        long,
61        num_args(1..),
62        conflicts_with = "constructor_args_path",
63        value_name = "ARGS",
64        allow_hyphen_values = true,
65    )]
66    constructor_args: Vec<String>,
67
68    /// The path to a file containing the constructor arguments.
69    #[arg(
70        long,
71        value_hint = ValueHint::FilePath,
72        value_name = "PATH",
73    )]
74    constructor_args_path: Option<PathBuf>,
75
76    /// Broadcast the transaction.
77    #[arg(long)]
78    pub broadcast: bool,
79
80    /// Verify contract after creation.
81    #[arg(long)]
82    verify: bool,
83
84    /// Send via `eth_sendTransaction` using the `--from` argument or `$ETH_FROM` as sender
85    #[arg(long, requires = "from")]
86    unlocked: bool,
87
88    /// Prints the standard json compiler input if `--verify` is provided.
89    ///
90    /// The standard json compiler input can be used to manually submit contract verification in
91    /// the browser.
92    #[arg(long, requires = "verify")]
93    show_standard_json_input: bool,
94
95    /// The Etherscan license type code or SPDX identifier to include with the verification
96    /// request.
97    ///
98    /// Accepts either an Etherscan numeric license code or a common SPDX identifier such as `MIT`.
99    /// This is only used for Etherscan-style verifiers when `--verify` is enabled.
100    #[arg(
101        long,
102        requires = "verify",
103        value_name = "LICENSE",
104        help_heading = "Verifier options",
105        value_parser = parse_etherscan_license_type,
106    )]
107    license_type: Option<String>,
108
109    /// Timeout to use for broadcasting transactions.
110    #[arg(long, env = "ETH_TIMEOUT")]
111    pub timeout: Option<u64>,
112
113    #[command(flatten)]
114    build: BuildOpts,
115
116    #[command(flatten)]
117    tx: TransactionOpts,
118
119    #[command(flatten)]
120    eth: EthereumOpts,
121
122    #[command(flatten)]
123    pub verifier: VerifierArgs,
124
125    #[command(flatten)]
126    retry: RetryArgs,
127
128    /// Browser wallet options
129    #[command(flatten)]
130    browser: BrowserWalletOpts,
131}
132
133impl CreateArgs {
134    /// Executes the command to create a contract
135    pub async fn run(mut self) -> Result<()> {
136        let (signer, tempo_access_key) = self.eth.wallet.maybe_signer().await?;
137
138        // Resolve chain early so we can dispatch to the correct network type.
139        if self.chain_id().is_none() {
140            let config = self.load_config()?;
141            let provider = ProviderBuilder::<Ethereum>::from_config(&config)?.build()?;
142            let chain_id = provider.get_chain_id().await?;
143            self.eth.etherscan.chain = Some(chain_id.into());
144        }
145
146        if tempo_access_key.is_some()
147            || self.tx.tempo.is_tempo()
148            || self.chain_id().is_some_and(|c| c.is_tempo())
149        {
150            self.run_generic::<TempoNetwork>(signer, tempo_access_key).await
151        } else {
152            self.run_generic::<Ethereum>(signer, None).await
153        }
154    }
155
156    async fn run_generic<N: Network>(
157        mut self,
158        pre_resolved_signer: Option<WalletSigner>,
159        access_key: Option<TempoAccessKeyConfig>,
160    ) -> Result<()>
161    where
162        N::TxEnvelope: From<Signed<N::UnsignedTx>>,
163        N::UnsignedTx: SignableTransaction<Signature>,
164        N::TransactionRequest: FoundryTransactionBuilder<N> + serde::Serialize,
165        N::ReceiptResponse: serde::Serialize,
166    {
167        let mut config = self.load_config()?;
168        let resolve_unknown_fee_token_symbol = !config.eth_rpc_curl;
169
170        // Install missing dependencies.
171        if install::install_missing_dependencies(&mut config).await && config.auto_detect_remappings
172        {
173            // need to re-configure here to also catch additional remappings
174            config = self.load_config()?;
175        }
176
177        // Find Project & Compile
178        let project = config.project()?;
179
180        let target_path = if let Some(ref mut path) = self.contract.path {
181            canonicalize(project.root().join(path))?
182        } else {
183            project.find_contract_path(&self.contract.name)?
184        };
185
186        let output = compile::compile_target(&target_path, &project, shell::is_json())?;
187
188        let (abi, bin, id) = find_contract_artifacts(output, &target_path, &self.contract.name)?;
189
190        let bin = match bin.object {
191            BytecodeObject::Bytecode(_) => bin.object,
192            _ => {
193                let link_refs = bin
194                    .link_references
195                    .iter()
196                    .flat_map(|(path, names)| {
197                        names.keys().map(move |name| format!("\t{name}: {path}"))
198                    })
199                    .collect::<Vec<String>>()
200                    .join("\n");
201                eyre::bail!(
202                    "Dynamic linking not supported in `create` command - deploy the following library contracts first, then provide the address to link at compile time\n{}",
203                    link_refs
204                );
205            }
206        };
207
208        // Add arguments to constructor
209        let params = if let Some(constructor) = &abi.constructor {
210            let constructor_args =
211                self.constructor_args_path.clone().map(read_constructor_args_file).transpose()?;
212            self.parse_constructor_args(
213                constructor,
214                constructor_args.as_deref().unwrap_or(&self.constructor_args),
215            )?
216        } else {
217            if !self.constructor_args.is_empty() || self.constructor_args_path.is_some() {
218                sh_warn!(
219                    "`{}` has no constructor; ignoring provided constructor arguments",
220                    self.contract.name
221                )?;
222            }
223            vec![]
224        };
225
226        let provider = ProviderBuilder::<N>::from_config(&config)?.build()?;
227
228        // Inject access key ID into TempoOpts so it's set before gas estimation.
229        if let Some(ref ak) = access_key {
230            self.tx.tempo.key_id = Some(ak.key_address);
231        }
232
233        // Resolve `--tempo.lane <name>` against the lanes file (default
234        // `<root>/tempo.lanes.toml`) and populate `self.tx.tempo.nonce_key` from the lane.
235        // Must happen before `self.deploy(...)` so `TempoOpts::apply` picks up the nonce_key.
236        let resolved_lane = resolve_lane(&mut self.tx.tempo, &config.root)?;
237        let expires_at = self.tx.tempo.resolve_expires();
238
239        // Whether to broadcast the transaction or not
240        let dry_run = !self.broadcast;
241
242        // Launch browser signer if `--browser` flag is set
243        let browser = self.browser.run::<N>().await?;
244
245        if let Some(browser) = browser {
246            // Deploy with browser wallet
247            let deployer_address = browser.address();
248            self.deploy(
249                abi,
250                bin,
251                params,
252                provider,
253                deployer_address,
254                config.transaction_timeout,
255                id,
256                dry_run,
257                None,
258                Some(browser),
259                resolved_lane,
260                expires_at,
261                resolve_unknown_fee_token_symbol,
262                config.eip1559_fee_estimate,
263            )
264            .await
265        } else if self.unlocked {
266            // Deploy with unlocked account
267            let sender = self.eth.wallet.from.expect("required");
268            self.deploy(
269                abi,
270                bin,
271                params,
272                provider,
273                sender,
274                config.transaction_timeout,
275                id,
276                dry_run,
277                None,
278                None,
279                resolved_lane,
280                expires_at,
281                resolve_unknown_fee_token_symbol,
282                config.eip1559_fee_estimate,
283            )
284            .await
285        } else if let Some(ak) = access_key {
286            // Tempo keychain mode: sign with access key and send raw
287            let signer = match pre_resolved_signer {
288                Some(s) => s,
289                None => self.eth.wallet.signer().await?,
290            };
291            let deployer_address = ak.wallet_address;
292            self.deploy(
293                abi,
294                bin,
295                params,
296                provider,
297                deployer_address,
298                config.transaction_timeout,
299                id,
300                dry_run,
301                Some((signer, ak)),
302                None,
303                resolved_lane,
304                expires_at,
305                resolve_unknown_fee_token_symbol,
306                config.eip1559_fee_estimate,
307            )
308            .await
309        } else {
310            // Deploy with signer
311            let signer = match pre_resolved_signer {
312                Some(s) => s,
313                None => self.eth.wallet.signer().await?,
314            };
315            let deployer = signer.address();
316            let provider = AlloyProviderBuilder::<_, _, N>::default()
317                .wallet(EthereumWallet::new(signer))
318                .connect_provider(provider);
319            self.deploy(
320                abi,
321                bin,
322                params,
323                provider,
324                deployer,
325                config.transaction_timeout,
326                id,
327                dry_run,
328                None,
329                None,
330                resolved_lane,
331                expires_at,
332                resolve_unknown_fee_token_symbol,
333                config.eip1559_fee_estimate,
334            )
335            .await
336        }
337    }
338
339    /// Returns the resolved chain, if any.
340    const fn chain_id(&self) -> Option<Chain> {
341        self.eth.etherscan.chain
342    }
343
344    /// Ensures the verify command can be executed.
345    ///
346    /// This is supposed to check any things that might go wrong when preparing a verify request
347    /// before the contract is deployed. This should prevent situations where a contract is deployed
348    /// successfully, but we fail to prepare a verify request which would require manual
349    /// verification.
350    async fn verify_preflight_check(
351        &self,
352        constructor_args: Option<String>,
353        id: &ArtifactId,
354    ) -> Result<()> {
355        // NOTE: this does not represent the same `VerifyArgs` that would be sent after deployment,
356        // since we don't know the address yet.
357        let mut verify = VerifyArgs {
358            address: Default::default(),
359            contract: Some(self.contract.clone()),
360            compiler_version: Some(id.version.to_string()),
361            constructor_args,
362            constructor_args_path: None,
363            no_auto_detect: false,
364            use_solc: None,
365            num_of_optimizations: None,
366            etherscan: EtherscanOpts {
367                key: self.eth.etherscan.key.clone(),
368                chain: self.chain_id(),
369            },
370            rpc: Default::default(),
371            flatten: false,
372            force: false,
373            skip_is_verified_check: true,
374            watch: true,
375            print_submission_result_to_stdout: false,
376            retry: self.retry,
377            libraries: self.build.libraries.clone(),
378            root: None,
379            verifier: self.verifier.clone(),
380            via_ir: self.build.compiler.via_ir,
381            license_type: self.license_type.clone(),
382            evm_version: self.build.compiler.evm_version,
383            show_standard_json_input: self.show_standard_json_input,
384            guess_constructor_args: false,
385            compilation_profile: Some(id.profile.clone()),
386            language: None,
387            creation_transaction_hash: None,
388        };
389
390        // Check config for Etherscan API Keys to avoid preflight check failing if no
391        // ETHERSCAN_API_KEY value set.
392        let config = verify.load_config()?;
393        verify.etherscan.key = config
394            .get_etherscan_config_with_chain(self.chain_id())?
395            .map(|c| c.key)
396            .or_else(|| config.etherscan_api_key.clone());
397
398        let context = verify.resolve_context().await?;
399
400        verify.verification_provider()?.preflight_verify_check(verify.clone(), context).await?;
401
402        let api_key = verify.verifier.resolve_api_key(verify.etherscan.key.as_deref());
403        let chain = verify.etherscan.chain.context("chain ID not resolved")?;
404        verify
405            .verifier
406            .check_credentials(api_key, chain, &config)
407            .await
408            .wrap_err("Verification preflight check failed")?;
409
410        Ok(())
411    }
412
413    /// Deploys the contract
414    #[expect(clippy::too_many_arguments)]
415    async fn deploy<N: Network, P: Provider<N>>(
416        self,
417        abi: JsonAbi,
418        bin: BytecodeObject,
419        args: Vec<DynSolValue>,
420        provider: P,
421        deployer_address: Address,
422        timeout: u64,
423        id: ArtifactId,
424        dry_run: bool,
425        tempo_keychain: Option<(WalletSigner, TempoAccessKeyConfig)>,
426        browser_signer: Option<BrowserSigner<N>>,
427        resolved_lane: Option<ResolvedLane>,
428        expires_at: Option<u64>,
429        resolve_unknown_fee_token_symbol: bool,
430        eip1559_fee_estimate: Eip1559FeeEstimatePreset,
431    ) -> Result<()>
432    where
433        N::TransactionRequest: FoundryTransactionBuilder<N> + serde::Serialize,
434        N::ReceiptResponse: serde::Serialize,
435    {
436        let chain = self.chain_id().context("chain ID not resolved")?;
437
438        let bin = bin.into_bytes().unwrap_or_default();
439        if bin.is_empty() {
440            eyre::bail!("no bytecode found in bin object for {}", self.contract.name);
441        }
442
443        let provider = Arc::new(provider);
444        let factory =
445            ContractFactory::<N, _>::new(abi.clone(), bin.clone(), provider.clone(), timeout);
446
447        let is_args_empty = args.is_empty();
448        let mut deployer =
449            factory.deploy_tokens(args.clone()).context("failed to deploy contract").map_err(|e| {
450                if is_args_empty {
451                    e.wrap_err("no arguments provided for contract constructor; consider --constructor-args or --constructor-args-path")
452                } else {
453                    e
454                }
455            })?;
456        let is_legacy = self.tx.legacy || chain.is_legacy();
457
458        deployer.tx.set_from(deployer_address);
459        deployer.tx.set_chain_id(chain.id());
460        // `to` field must be set explicitly, cannot be None.
461        if deployer.tx.to().is_none() {
462            deployer.tx.set_create();
463        }
464
465        // Apply user-provided gas, fee, nonce, and Tempo options.
466        self.tx.apply::<N>(&mut deployer.tx, is_legacy);
467
468        // Convert the CREATE into an AA-compatible call entry since Tempo AA
469        // transactions use a `calls` list instead of `to`+`input`.
470        if chain.is_tempo() {
471            deployer.tx.convert_create_to_call();
472        }
473
474        // For keychain mode, set key_id and nonce_key before gas estimation.
475        if let Some((_, ref ak)) = tempo_keychain {
476            deployer.tx.set_key_id(ak.key_address);
477            if deployer.tx.nonce_key().is_none() {
478                deployer.tx.set_nonce_key(U256::ZERO);
479            }
480        }
481
482        // Fetch defaults from provider for values not specified by user.
483        if self.tx.nonce.is_none() && !self.tx.tempo.expiring_nonce {
484            deployer.tx.set_nonce(provider.get_transaction_count(deployer_address).await?);
485        }
486
487        maybe_print_resolved_lane(resolved_lane.as_ref(), deployer.tx.nonce().unwrap_or_default())?;
488
489        if let Some((_, ref ak)) = tempo_keychain {
490            deployer
491                .tx
492                .prepare_access_key_authorization(
493                    provider.as_ref(),
494                    ak.wallet_address,
495                    ak.key_address,
496                    ak.key_authorization.as_ref(),
497                )
498                .await?;
499        }
500
501        if is_legacy {
502            if self.tx.gas_price.is_none() {
503                deployer.tx.set_gas_price(provider.get_gas_price().await?);
504            }
505        } else {
506            if self.tx.gas_price.is_none() || self.tx.priority_gas_price.is_none() {
507                let estimate = estimate_eip1559_fees(&provider, eip1559_fee_estimate).await.wrap_err("Failed to estimate EIP1559 fees. This chain might not support EIP1559, try adding --legacy to your command.")?;
508
509                // Only honor the browser-suggested tip when the user has not pinned
510                // a priority fee; `resolve_broadcast_eip1559_fees` ignores a lower tip.
511                let browser_suggested_tip =
512                    if browser_signer.is_some() && self.tx.priority_gas_price.is_none() {
513                        provider.get_max_priority_fee_per_gas().await.ok()
514                    } else {
515                        None
516                    };
517
518                // User `--gas-price`/`--priority-gas-price` overrides are applied
519                // below only for unset fields; pass `None` to avoid double-applying.
520                let estimate =
521                    resolve_broadcast_eip1559_fees(estimate, None, None, browser_suggested_tip)?;
522
523                if self.tx.priority_gas_price.is_none() {
524                    deployer.tx.set_max_priority_fee_per_gas(estimate.max_priority_fee_per_gas);
525                }
526                if self.tx.gas_price.is_none() {
527                    deployer.tx.set_max_fee_per_gas(estimate.max_fee_per_gas);
528                }
529            }
530            if let (Some(max_fee), Some(priority)) =
531                (deployer.tx.max_fee_per_gas(), deployer.tx.max_priority_fee_per_gas())
532            {
533                eyre::ensure!(
534                    priority <= max_fee,
535                    "max priority fee per gas ({priority}) cannot exceed max fee per gas ({max_fee})"
536                );
537            }
538        }
539
540        // set access list if specified
541        if let Some(access_list) = match self.tx.access_list {
542            None => None,
543            Some(None) => Some(provider.create_access_list(&deployer.tx).await?.access_list),
544            Some(Some(ref access_list)) => Some(access_list.clone()),
545        } {
546            deployer.tx.set_access_list(access_list);
547        }
548
549        if self.tx.gas_limit.is_none() {
550            let mut estimated = provider.estimate_gas(deployer.tx.clone()).await?;
551
552            // Browser wallets may sign with P256/WebAuthn instead of secp256k1, which
553            // costs more gas for signature verification on Tempo chains. Add a
554            // conservative buffer since we can't determine the signature type beforehand.
555            if browser_signer.is_some() && chain.is_tempo() {
556                estimated += TEMPO_BROWSER_GAS_BUFFER;
557            }
558
559            deployer.tx.set_gas_limit(estimated);
560        }
561
562        // Before we actually deploy the contract we try check if the verify settings are valid
563        let mut constructor_args = None;
564        if self.verify {
565            if !args.is_empty() {
566                let encoded_args = abi
567                    .constructor()
568                    .ok_or_else(|| eyre::eyre!("could not find constructor"))?
569                    .abi_encode_input(&args)?;
570                constructor_args = Some(hex::encode(encoded_args));
571            }
572
573            self.verify_preflight_check(constructor_args.clone(), &id).await?;
574        }
575
576        if dry_run {
577            if shell::is_json() {
578                let output = json!({
579                    "contract": self.contract.name,
580                    "transaction": &deployer.tx,
581                    "abi":&abi
582                });
583                sh_println!("{}", serde_json::to_string_pretty(&output)?)?;
584            } else {
585                sh_warn!("Dry run enabled, not broadcasting transaction\n")?;
586
587                sh_println!("Contract: {}", self.contract.name)?;
588                sh_println!(
589                    "Transaction: {}",
590                    serde_json::to_string_pretty(&deployer.tx.clone())?
591                )?;
592                sh_println!("ABI: {}\n", serde_json::to_string_pretty(&abi)?)?;
593
594                sh_warn!(
595                    "To broadcast this transaction, add --broadcast to the previous command. See forge create --help for more."
596                )?;
597            }
598
599            return Ok(());
600        }
601
602        if let Some(ts) = expires_at {
603            sh_status!("Transaction expires at unix timestamp {ts}")?;
604        }
605
606        let tempo_sponsor = self.tx.tempo.sponsor_config().await?;
607        if let Some(sponsor) = &tempo_sponsor {
608            sponsor
609                .resolve_and_set_fee_token(
610                    resolve_unknown_fee_token_symbol.then_some(&provider),
611                    Some(chain),
612                    &mut deployer.tx,
613                )
614                .await?;
615            sponsor.attach_and_print::<N>(&mut deployer.tx, deployer_address).await?;
616        } else {
617            let fee_token = resolve_and_set_fee_token(
618                resolve_unknown_fee_token_symbol.then_some(&provider),
619                Some(chain),
620                &mut deployer.tx,
621                Some(deployer_address),
622            )
623            .await?;
624            maybe_print_fee_token(resolve_unknown_fee_token_symbol.then_some(&provider), fee_token)
625                .await?;
626        }
627
628        // Deploy the actual contract
629        let (deployed_contract, receipt) = if let Some(browser) = browser_signer {
630            // Browser wallet signs and sends the transaction
631            let tx_hash = browser.send_transaction_via_browser(deployer.tx).await?;
632
633            // Wait for the transaction to be confirmed, then fetch the receipt.
634            provider
635                .watch_pending_transaction(alloy_provider::PendingTransactionConfig::new(tx_hash))
636                .await?
637                .await?;
638
639            let receipt = provider
640                .get_transaction_receipt(tx_hash)
641                .await?
642                .ok_or_else(|| eyre::eyre!("could not get transaction receipt for {tx_hash}"))?;
643
644            if !receipt.status() {
645                eyre::bail!("deployment transaction failed (receipt status 0): {tx_hash}");
646            }
647
648            let address = receipt
649                .contract_address()
650                .ok_or_else(|| eyre::eyre!("contract was not deployed"))?;
651
652            (address, receipt)
653        } else if let Some((signer, ak)) = tempo_keychain {
654            // Tempo keychain mode: sign with access key provisioning and send raw
655            let raw_tx = deployer
656                .tx
657                .sign_with_access_key(
658                    &provider,
659                    &signer,
660                    ak.wallet_address,
661                    ak.key_address,
662                    ak.key_authorization.as_ref(),
663                )
664                .await?;
665
666            let receipt = provider
667                .send_raw_transaction(&raw_tx)
668                .await?
669                .with_required_confirmations(1)
670                .with_timeout(Some(Duration::from_secs(timeout)))
671                .get_receipt()
672                .await?;
673
674            let address = receipt
675                .contract_address()
676                .ok_or_else(|| eyre::eyre!("contract was not deployed"))?;
677
678            (address, receipt)
679        } else {
680            deployer.send_with_receipt().await?
681        };
682
683        let address = deployed_contract;
684        let tx_hash = receipt.transaction_hash();
685        if shell::is_json() {
686            let output = json!({
687                "deployer": deployer_address.to_string(),
688                "deployedTo": address.to_string(),
689                "transactionHash": tx_hash
690            });
691            sh_println!("{}", serde_json::to_string_pretty(&output)?)?;
692        } else {
693            sh_println!("Deployer: {deployer_address}")?;
694            sh_println!("Deployed to: {address}")?;
695            sh_println!("Transaction hash: {tx_hash:?}")?;
696        };
697
698        if !self.verify {
699            return Ok(());
700        }
701
702        sh_status!("Starting contract verification...")?;
703
704        let num_of_optimizations = if let Some(optimizer) = self.build.compiler.optimize {
705            optimizer.then(|| self.build.compiler.optimizer_runs.unwrap_or(200))
706        } else {
707            self.build.compiler.optimizer_runs
708        };
709
710        let verify = VerifyArgs {
711            address,
712            contract: Some(self.contract),
713            compiler_version: Some(id.version.to_string()),
714            constructor_args,
715            constructor_args_path: None,
716            no_auto_detect: false,
717            use_solc: None,
718            num_of_optimizations,
719            etherscan: EtherscanOpts { key: self.eth.etherscan.key(), chain: Some(chain) },
720            rpc: Default::default(),
721            flatten: false,
722            force: false,
723            skip_is_verified_check: true,
724            watch: true,
725            print_submission_result_to_stdout: false,
726            retry: self.retry,
727            libraries: self.build.libraries.clone(),
728            root: None,
729            verifier: self.verifier,
730            via_ir: self.build.compiler.via_ir,
731            license_type: self.license_type,
732            evm_version: self.build.compiler.evm_version,
733            show_standard_json_input: self.show_standard_json_input,
734            guess_constructor_args: false,
735            compilation_profile: Some(id.profile.clone()),
736            language: None,
737            creation_transaction_hash: Some(tx_hash),
738        };
739        // Load the full config (including foundry.toml) so the key used for resolution matches
740        // what `verify.run()` will actually use, preventing a "Waiting for sourcify..." message
741        // when the run will actually use Etherscan (or vice versa for unknown chains).
742        let verify_config = verify.load_config()?;
743        let effective_key = verify_config
744            .get_etherscan_config_with_chain(Some(chain))?
745            .map(|c| c.key)
746            .or_else(|| verify_config.etherscan_api_key.clone());
747        let resolved_verifier = verify.verifier.resolve(effective_key.as_deref(), Some(chain));
748        sh_status!("Waiting for {resolved_verifier} to detect contract deployment...")?;
749        verify.run().await
750    }
751
752    /// Parses the given constructor arguments into a vector of `DynSolValue`s, by matching them
753    /// against the constructor's input params.
754    ///
755    /// Returns a list of parsed values that match the constructor's input params.
756    fn parse_constructor_args(
757        &self,
758        constructor: &Constructor,
759        constructor_args: &[String],
760    ) -> Result<Vec<DynSolValue>> {
761        if constructor.inputs.len() != constructor_args.len() {
762            eyre::bail!(
763                "Constructor argument count mismatch: expected {} but got {}",
764                constructor.inputs.len(),
765                constructor_args.len()
766            );
767        }
768
769        let mut params = Vec::with_capacity(constructor.inputs.len());
770        for (input, arg) in constructor.inputs.iter().zip(constructor_args) {
771            // resolve the input type directly
772            let ty = input
773                .resolve()
774                .wrap_err_with(|| format!("Could not resolve constructor arg: input={input}"))?;
775            params.push((ty, arg));
776        }
777        let params = params.iter().map(|(ty, arg)| (ty, arg.as_str()));
778        parse_tokens(params).map_err(Into::into)
779    }
780}
781
782impl figment::Provider for CreateArgs {
783    fn metadata(&self) -> Metadata {
784        Metadata::named("Create Args Provider")
785    }
786
787    fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
788        let mut dict = Dict::default();
789        if let Some(timeout) = self.timeout {
790            dict.insert("transaction_timeout".to_string(), timeout.into());
791        }
792        Ok(Map::from([(Config::selected_profile(), dict)]))
793    }
794}
795
796/// `ContractFactory` is a [`DeploymentTxFactory`] object with an
797/// [`Arc`] middleware. This type alias exists to preserve backwards
798/// compatibility with less-abstract Contracts.
799///
800/// For full usage docs, see [`DeploymentTxFactory`].
801pub type ContractFactory<N, P> = DeploymentTxFactory<N, P>;
802
803/// Helper which manages the deployment transaction of a smart contract. It
804/// wraps a deployment transaction, and retrieves the contract address output
805/// by it.
806#[derive(Debug)]
807#[must_use = "ContractDeploymentTx does nothing unless you `send` it"]
808pub struct ContractDeploymentTx<N: Network, P, C> {
809    /// the actual deployer, exposed for overriding the defaults
810    pub deployer: Deployer<N, P>,
811    /// marker for the `Contract` type to create afterwards
812    ///
813    /// this type will be used to construct it via `From::from(Contract)`
814    _contract: PhantomData<C>,
815}
816
817impl<N: Network, P: Clone, C> Clone for ContractDeploymentTx<N, P, C> {
818    fn clone(&self) -> Self {
819        Self { deployer: self.deployer.clone(), _contract: self._contract }
820    }
821}
822
823impl<N: Network, P, C> From<Deployer<N, P>> for ContractDeploymentTx<N, P, C> {
824    fn from(deployer: Deployer<N, P>) -> Self {
825        Self { deployer, _contract: PhantomData }
826    }
827}
828
829/// Helper which manages the deployment transaction of a smart contract
830#[derive(Clone, Debug)]
831#[must_use = "Deployer does nothing unless you `send` it"]
832pub struct Deployer<N: Network, P> {
833    /// The deployer's transaction, exposed for overriding the defaults
834    pub tx: N::TransactionRequest,
835    client: P,
836    confs: usize,
837    timeout: u64,
838}
839
840impl<N: Network, P: Provider<N>> Deployer<N, P> {
841    /// Broadcasts the contract deployment transaction and after waiting for it to
842    /// be sufficiently confirmed (default: 1), it returns a tuple with the [`Address`] at the
843    /// deployed contract's address and the corresponding receipt.
844    pub async fn send_with_receipt(
845        self,
846    ) -> Result<(Address, N::ReceiptResponse), ContractDeploymentError> {
847        let receipt = self
848            .client
849            .borrow()
850            .send_transaction(self.tx)
851            .await?
852            .with_required_confirmations(self.confs as u64)
853            .with_timeout(Some(Duration::from_secs(self.timeout)))
854            .get_receipt()
855            .await?;
856
857        if !receipt.status() {
858            return Err(ContractDeploymentError::DeploymentFailed(receipt.transaction_hash()));
859        }
860
861        let address =
862            receipt.contract_address().ok_or(ContractDeploymentError::ContractNotDeployed)?;
863
864        Ok((address, receipt))
865    }
866}
867
868/// To deploy a contract to the Ethereum network, a [`ContractFactory`] can be
869/// created which manages the Contract bytecode and Application Binary Interface
870/// (ABI), usually generated from the Solidity compiler.
871#[derive(Clone, Debug)]
872pub struct DeploymentTxFactory<N: Network, P> {
873    client: P,
874    abi: JsonAbi,
875    bytecode: Bytes,
876    timeout: u64,
877    _network: PhantomData<N>,
878}
879
880impl<N: Network, P: Provider<N> + Clone> DeploymentTxFactory<N, P> {
881    /// Creates a factory for deployment of the Contract with bytecode, and the
882    /// constructor defined in the abi. The client will be used to send any deployment
883    /// transaction.
884    pub const fn new(abi: JsonAbi, bytecode: Bytes, client: P, timeout: u64) -> Self {
885        Self { client, abi, bytecode, timeout, _network: PhantomData }
886    }
887
888    /// Create a deployment tx using the provided tokens as constructor
889    /// arguments
890    pub fn deploy_tokens(
891        self,
892        params: Vec<DynSolValue>,
893    ) -> Result<Deployer<N, P>, ContractDeploymentError>
894    where
895        N::TransactionRequest: FoundryTransactionBuilder<N>,
896    {
897        // Encode the constructor args & concatenate with the bytecode if necessary
898        let data: Bytes = match (self.abi.constructor(), params.is_empty()) {
899            (None, false) => return Err(ContractDeploymentError::ConstructorError),
900            (None, true) => self.bytecode.clone(),
901            (Some(constructor), _) => {
902                let input: Bytes = constructor
903                    .abi_encode_input(&params)
904                    .map_err(ContractDeploymentError::DetokenizationError)?
905                    .into();
906                // Concatenate the bytecode and abi-encoded constructor call.
907                self.bytecode.iter().copied().chain(input).collect()
908            }
909        };
910
911        // create the tx object. Since we're deploying a contract, `to` is `None`
912        let mut tx = N::TransactionRequest::default();
913        tx.set_input(data);
914        Ok(Deployer { client: self.client.clone(), tx, confs: 1, timeout: self.timeout })
915    }
916}
917
918#[derive(thiserror::Error, Debug)]
919/// An Error which is thrown when interacting with a smart contract
920pub enum ContractDeploymentError {
921    #[error("constructor is not defined in the ABI")]
922    ConstructorError,
923    #[error(transparent)]
924    DetokenizationError(#[from] alloy_dyn_abi::Error),
925    #[error("contract was not deployed")]
926    ContractNotDeployed,
927    #[error("deployment transaction failed (receipt status 0): {0}")]
928    DeploymentFailed(alloy_primitives::TxHash),
929    #[error(transparent)]
930    RpcError(#[from] TransportError),
931}
932
933impl From<PendingTransactionError> for ContractDeploymentError {
934    fn from(_err: PendingTransactionError) -> Self {
935        Self::ContractNotDeployed
936    }
937}
938
939#[cfg(test)]
940mod tests {
941    use super::*;
942    use alloy_primitives::I256;
943
944    #[test]
945    fn can_parse_create() {
946        let args: CreateArgs = CreateArgs::parse_from([
947            "foundry-cli",
948            "src/Domains.sol:Domains",
949            "--verify",
950            "--retries",
951            "10",
952            "--delay",
953            "30",
954            "--license-type",
955            "13",
956        ]);
957        assert_eq!(args.retry.retries, 10);
958        assert_eq!(args.retry.delay, 30);
959        assert_eq!(args.license_type.as_deref(), Some("13"));
960    }
961
962    #[test]
963    fn can_parse_create_license_type_spdx() {
964        let args: CreateArgs = CreateArgs::parse_from([
965            "foundry-cli",
966            "src/Domains.sol:Domains",
967            "--verify",
968            "--license-type",
969            "MIT",
970        ]);
971        assert_eq!(args.license_type.as_deref(), Some("3"));
972    }
973
974    #[test]
975    fn errors_on_invalid_create_license_type() {
976        let err = CreateArgs::try_parse_from([
977            "foundry-cli",
978            "src/Domains.sol:Domains",
979            "--verify",
980            "--license-type",
981            "definitely-not-a-license",
982        ])
983        .unwrap_err();
984        assert!(err.to_string().contains("unsupported Etherscan license type"));
985    }
986
987    #[test]
988    fn can_parse_chain_id() {
989        let args: CreateArgs = CreateArgs::parse_from([
990            "foundry-cli",
991            "src/Domains.sol:Domains",
992            "--verify",
993            "--retries",
994            "10",
995            "--delay",
996            "30",
997            "--chain-id",
998            "9999",
999        ]);
1000        assert_eq!(args.chain_id().map(|c| c.id()), Some(9999));
1001    }
1002
1003    #[test]
1004    fn test_parse_constructor_args() {
1005        let args: CreateArgs = CreateArgs::parse_from([
1006            "foundry-cli",
1007            "src/Domains.sol:Domains",
1008            "--constructor-args",
1009            "Hello",
1010        ]);
1011        let constructor: Constructor = serde_json::from_str(r#"{"type":"constructor","inputs":[{"name":"_name","type":"string","internalType":"string"}],"stateMutability":"nonpayable"}"#).unwrap();
1012        let params = args.parse_constructor_args(&constructor, &args.constructor_args).unwrap();
1013        assert_eq!(params, vec![DynSolValue::String("Hello".to_string())]);
1014    }
1015
1016    #[test]
1017    fn test_parse_tuple_constructor_args() {
1018        let args: CreateArgs = CreateArgs::parse_from([
1019            "foundry-cli",
1020            "src/Domains.sol:Domains",
1021            "--constructor-args",
1022            "[(1,2), (2,3), (3,4)]",
1023        ]);
1024        let constructor: Constructor = serde_json::from_str(r#"{"type":"constructor","inputs":[{"name":"_points","type":"tuple[]","internalType":"struct Point[]","components":[{"name":"x","type":"uint256","internalType":"uint256"},{"name":"y","type":"uint256","internalType":"uint256"}]}],"stateMutability":"nonpayable"}"#).unwrap();
1025        let _params = args.parse_constructor_args(&constructor, &args.constructor_args).unwrap();
1026    }
1027
1028    #[test]
1029    fn test_parse_int_constructor_args() {
1030        let args: CreateArgs = CreateArgs::parse_from([
1031            "foundry-cli",
1032            "src/Domains.sol:Domains",
1033            "--constructor-args",
1034            "-5",
1035        ]);
1036        let constructor: Constructor = serde_json::from_str(r#"{"type":"constructor","inputs":[{"name":"_name","type":"int256","internalType":"int256"}],"stateMutability":"nonpayable"}"#).unwrap();
1037        let params = args.parse_constructor_args(&constructor, &args.constructor_args).unwrap();
1038        assert_eq!(params, vec![DynSolValue::Int(I256::unchecked_from(-5), 256)]);
1039    }
1040}