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