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