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