Skip to main content

forge/cmd/
create.rs

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