1use crate::cmd::install;
2use alloy_chains::Chain;
3use alloy_consensus::{SignableTransaction, Signed};
4use alloy_dyn_abi::{DynSolValue, JsonAbiExt, Specifier};
5use alloy_json_abi::{Constructor, JsonAbi};
6use alloy_network::{Ethereum, EthereumWallet, Network, ReceiptResponse, TransactionBuilder};
7use alloy_primitives::{Address, Bytes, U256, hex};
8use alloy_provider::{PendingTransactionError, Provider, ProviderBuilder as AlloyProviderBuilder};
9use alloy_signer::{Signature, Signer};
10use alloy_transport::TransportError;
11use clap::{Parser, ValueHint};
12use eyre::{Context, Result};
13use forge_verify::{RetryArgs, VerifierArgs, VerifyArgs};
14use foundry_cli::{
15 opts::{BuildOpts, EthereumOpts, EtherscanOpts, TransactionOpts},
16 utils::{LoadConfig, find_contract_artifacts, read_constructor_args_file},
17};
18use foundry_common::{
19 FoundryTransactionBuilder,
20 compile::{self},
21 fmt::parse_tokens,
22 provider::ProviderBuilder,
23 shell,
24};
25use foundry_compilers::{
26 ArtifactId, artifacts::BytecodeObject, info::ContractInfo, utils::canonicalize,
27};
28use foundry_config::{
29 Config,
30 figment::{
31 self, Metadata, Profile,
32 value::{Dict, Map},
33 },
34 merge_impl_figment_convert,
35};
36use foundry_wallets::{TempoAccessKeyConfig, WalletSigner};
37use serde_json::json;
38use std::{borrow::Borrow, marker::PhantomData, path::PathBuf, sync::Arc, time::Duration};
39use tempo_alloy::TempoNetwork;
40
41merge_impl_figment_convert!(CreateArgs, build, eth);
42
43#[derive(Clone, Debug, Parser)]
45pub struct CreateArgs {
46 contract: ContractInfo,
48
49 #[arg(
51 long,
52 num_args(1..),
53 conflicts_with = "constructor_args_path",
54 value_name = "ARGS",
55 allow_hyphen_values = true,
56 )]
57 constructor_args: Vec<String>,
58
59 #[arg(
61 long,
62 value_hint = ValueHint::FilePath,
63 value_name = "PATH",
64 )]
65 constructor_args_path: Option<PathBuf>,
66
67 #[arg(long)]
69 pub broadcast: bool,
70
71 #[arg(long)]
73 verify: bool,
74
75 #[arg(long, requires = "from")]
77 unlocked: bool,
78
79 #[arg(long, requires = "verify")]
84 show_standard_json_input: bool,
85
86 #[arg(long, env = "ETH_TIMEOUT")]
88 pub timeout: Option<u64>,
89
90 #[command(flatten)]
91 build: BuildOpts,
92
93 #[command(flatten)]
94 tx: TransactionOpts,
95
96 #[command(flatten)]
97 eth: EthereumOpts,
98
99 #[command(flatten)]
100 pub verifier: VerifierArgs,
101
102 #[command(flatten)]
103 retry: RetryArgs,
104}
105
106impl CreateArgs {
107 pub async fn run(self) -> Result<()> {
109 let (signer, tempo_access_key) = self.eth.wallet.maybe_signer().await?;
110
111 if tempo_access_key.is_some() || self.tx.tempo.is_tempo() {
112 self.run_generic::<TempoNetwork>(signer, tempo_access_key).await
113 } else {
114 self.run_generic::<Ethereum>(signer, None).await
115 }
116 }
117
118 async fn run_generic<N: Network>(
119 mut self,
120 pre_resolved_signer: Option<WalletSigner>,
121 access_key: Option<TempoAccessKeyConfig>,
122 ) -> Result<()>
123 where
124 N::TxEnvelope: From<Signed<N::UnsignedTx>>,
125 N::UnsignedTx: SignableTransaction<Signature>,
126 N::TransactionRequest: FoundryTransactionBuilder<N> + serde::Serialize,
127 N::ReceiptResponse: serde::Serialize,
128 {
129 let mut config = self.load_config()?;
130
131 if install::install_missing_dependencies(&mut config).await && config.auto_detect_remappings
133 {
134 config = self.load_config()?;
136 }
137
138 let project = config.project()?;
140
141 let target_path = if let Some(ref mut path) = self.contract.path {
142 canonicalize(project.root().join(path))?
143 } else {
144 project.find_contract_path(&self.contract.name)?
145 };
146
147 let output = compile::compile_target(&target_path, &project, shell::is_json())?;
148
149 let (abi, bin, id) = find_contract_artifacts(output, &target_path, &self.contract.name)?;
150
151 let bin = match bin.object {
152 BytecodeObject::Bytecode(_) => bin.object,
153 _ => {
154 let link_refs = bin
155 .link_references
156 .iter()
157 .flat_map(|(path, names)| {
158 names.keys().map(move |name| format!("\t{name}: {path}"))
159 })
160 .collect::<Vec<String>>()
161 .join("\n");
162 eyre::bail!(
163 "Dynamic linking not supported in `create` command - deploy the following library contracts first, then provide the address to link at compile time\n{}",
164 link_refs
165 )
166 }
167 };
168
169 let params = if let Some(constructor) = &abi.constructor {
171 let constructor_args =
172 self.constructor_args_path.clone().map(read_constructor_args_file).transpose()?;
173 self.parse_constructor_args(
174 constructor,
175 constructor_args.as_deref().unwrap_or(&self.constructor_args),
176 )?
177 } else {
178 vec![]
179 };
180
181 let provider = ProviderBuilder::<N>::from_config(&config)?.build()?;
182
183 if let Some(ref ak) = access_key {
185 self.tx.tempo.key_id = Some(ak.key_address);
186 }
187
188 let chain_id = if let Some(chain_id) = self.chain_id() {
190 chain_id
191 } else {
192 provider.get_chain_id().await?
193 };
194
195 let dry_run = !self.broadcast;
197
198 if self.unlocked {
199 let sender = self.eth.wallet.from.expect("required");
201 self.deploy(
202 abi,
203 bin,
204 params,
205 provider,
206 chain_id,
207 sender,
208 config.transaction_timeout,
209 id,
210 dry_run,
211 None,
212 )
213 .await
214 } else if let Some(ak) = access_key {
215 let signer = match pre_resolved_signer {
217 Some(s) => s,
218 None => self.eth.wallet.signer().await?,
219 };
220 let deployer_address = ak.wallet_address;
221 self.deploy(
222 abi,
223 bin,
224 params,
225 provider,
226 chain_id,
227 deployer_address,
228 config.transaction_timeout,
229 id,
230 dry_run,
231 Some((signer, ak)),
232 )
233 .await
234 } else {
235 let signer = match pre_resolved_signer {
237 Some(s) => s,
238 None => self.eth.wallet.signer().await?,
239 };
240 let deployer = signer.address();
241 let provider = AlloyProviderBuilder::<_, _, N>::default()
242 .wallet(EthereumWallet::new(signer))
243 .connect_provider(provider);
244 self.deploy(
245 abi,
246 bin,
247 params,
248 provider,
249 chain_id,
250 deployer,
251 config.transaction_timeout,
252 id,
253 dry_run,
254 None,
255 )
256 .await
257 }
258 }
259
260 fn chain_id(&self) -> Option<u64> {
262 self.eth.etherscan.chain.map(|chain| chain.id())
263 }
264
265 async fn verify_preflight_check(
272 &self,
273 constructor_args: Option<String>,
274 chain: u64,
275 id: &ArtifactId,
276 ) -> Result<()> {
277 let mut verify = VerifyArgs {
280 address: Default::default(),
281 contract: Some(self.contract.clone()),
282 compiler_version: Some(id.version.to_string()),
283 constructor_args,
284 constructor_args_path: None,
285 no_auto_detect: false,
286 use_solc: None,
287 num_of_optimizations: None,
288 etherscan: EtherscanOpts {
289 key: self.eth.etherscan.key.clone(),
290 chain: Some(chain.into()),
291 },
292 rpc: Default::default(),
293 flatten: false,
294 force: false,
295 skip_is_verified_check: true,
296 watch: true,
297 retry: self.retry,
298 libraries: self.build.libraries.clone(),
299 root: None,
300 verifier: self.verifier.clone(),
301 via_ir: self.build.via_ir,
302 evm_version: self.build.compiler.evm_version,
303 show_standard_json_input: self.show_standard_json_input,
304 guess_constructor_args: false,
305 compilation_profile: Some(id.profile.clone()),
306 language: None,
307 creation_transaction_hash: None,
308 };
309
310 let config = verify.load_config()?;
313 verify.etherscan.key =
314 config.get_etherscan_config_with_chain(Some(chain.into()))?.map(|c| c.key);
315
316 let context = verify.resolve_context().await?;
317
318 verify.verification_provider()?.preflight_verify_check(verify, context).await?;
319 Ok(())
320 }
321
322 #[expect(clippy::too_many_arguments)]
324 async fn deploy<N: Network, P: Provider<N>>(
325 self,
326 abi: JsonAbi,
327 bin: BytecodeObject,
328 args: Vec<DynSolValue>,
329 provider: P,
330 chain: u64,
331 deployer_address: Address,
332 timeout: u64,
333 id: ArtifactId,
334 dry_run: bool,
335 tempo_keychain: Option<(WalletSigner, TempoAccessKeyConfig)>,
336 ) -> Result<()>
337 where
338 N::TransactionRequest: FoundryTransactionBuilder<N> + serde::Serialize,
339 N::ReceiptResponse: serde::Serialize,
340 {
341 let bin = bin.into_bytes().unwrap_or_default();
342 if bin.is_empty() {
343 eyre::bail!("no bytecode found in bin object for {}", self.contract.name)
344 }
345
346 let provider = Arc::new(provider);
347 let factory =
348 ContractFactory::<N, _>::new(abi.clone(), bin.clone(), provider.clone(), timeout);
349
350 let is_args_empty = args.is_empty();
351 let mut deployer =
352 factory.deploy_tokens(args.clone(), self.tx.tempo.fee_token).context("failed to deploy contract").map_err(|e| {
353 if is_args_empty {
354 e.wrap_err("no arguments provided for contract constructor; consider --constructor-args or --constructor-args-path")
355 } else {
356 e
357 }
358 })?;
359 let is_legacy = self.tx.legacy || Chain::from(chain).is_legacy();
360
361 deployer.tx.set_from(deployer_address);
362 deployer.tx.set_chain_id(chain);
363 if deployer.tx.to().is_none() {
365 deployer.tx.set_create();
366 }
367
368 self.tx.apply::<N>(&mut deployer.tx, is_legacy);
370
371 if let Some((_, ref ak)) = tempo_keychain {
375 deployer.tx.set_key_id(ak.key_address);
376 if deployer.tx.nonce_key().is_none() {
377 deployer.tx.set_nonce_key(U256::ZERO);
378 }
379 deployer.tx.convert_create_to_call();
380 }
381
382 if self.tx.nonce.is_none() && !self.tx.tempo.expiring_nonce {
384 deployer.tx.set_nonce(provider.get_transaction_count(deployer_address).await?);
385 }
386
387 if let Some(access_list) = match self.tx.access_list {
389 None => None,
390 Some(None) => Some(provider.create_access_list(&deployer.tx).await?.access_list),
391 Some(Some(ref access_list)) => Some(access_list.clone()),
392 } {
393 deployer.tx.set_access_list(access_list);
394 }
395
396 if self.tx.gas_limit.is_none() {
397 deployer.tx.set_gas_limit(provider.estimate_gas(deployer.tx.clone()).await?);
398 }
399
400 if is_legacy {
401 if self.tx.gas_price.is_none() {
402 deployer.tx.set_gas_price(provider.get_gas_price().await?);
403 }
404 } else if self.tx.gas_price.is_none() || self.tx.priority_gas_price.is_none() {
405 let estimate = provider.estimate_eip1559_fees().await.wrap_err("Failed to estimate EIP1559 fees. This chain might not support EIP1559, try adding --legacy to your command.")?;
406 if self.tx.priority_gas_price.is_none() {
407 deployer.tx.set_max_priority_fee_per_gas(estimate.max_priority_fee_per_gas);
408 }
409 if self.tx.gas_price.is_none() {
410 deployer.tx.set_max_fee_per_gas(estimate.max_fee_per_gas);
411 }
412 }
413
414 let mut constructor_args = None;
416 if self.verify {
417 if !args.is_empty() {
418 let encoded_args = abi
419 .constructor()
420 .ok_or_else(|| eyre::eyre!("could not find constructor"))?
421 .abi_encode_input(&args)?;
422 constructor_args = Some(hex::encode(encoded_args));
423 }
424
425 self.verify_preflight_check(constructor_args.clone(), chain, &id).await?;
426 }
427
428 if dry_run {
429 if shell::is_json() {
430 let output = json!({
431 "contract": self.contract.name,
432 "transaction": &deployer.tx,
433 "abi":&abi
434 });
435 sh_println!("{}", serde_json::to_string_pretty(&output)?)?;
436 } else {
437 sh_warn!("Dry run enabled, not broadcasting transaction\n")?;
438
439 sh_println!("Contract: {}", self.contract.name)?;
440 sh_println!(
441 "Transaction: {}",
442 serde_json::to_string_pretty(&deployer.tx.clone())?
443 )?;
444 sh_println!("ABI: {}\n", serde_json::to_string_pretty(&abi)?)?;
445
446 sh_warn!(
447 "To broadcast this transaction, add --broadcast to the previous command. See forge create --help for more."
448 )?;
449 }
450
451 return Ok(());
452 }
453
454 let (deployed_contract, receipt) = if let Some((signer, ak)) = tempo_keychain {
456 let raw_tx = deployer
458 .tx
459 .sign_with_access_key(
460 &provider,
461 &signer,
462 ak.wallet_address,
463 ak.key_address,
464 ak.key_authorization.as_ref(),
465 )
466 .await?;
467
468 let receipt = provider
469 .send_raw_transaction(&raw_tx)
470 .await?
471 .with_required_confirmations(1)
472 .with_timeout(Some(Duration::from_secs(timeout)))
473 .get_receipt()
474 .await?;
475
476 let address = receipt
477 .contract_address()
478 .ok_or_else(|| eyre::eyre!("contract was not deployed"))?;
479
480 (address, receipt)
481 } else {
482 deployer.send_with_receipt().await?
483 };
484
485 let address = deployed_contract;
486 let tx_hash = receipt.transaction_hash();
487 if shell::is_json() {
488 let output = json!({
489 "deployer": deployer_address.to_string(),
490 "deployedTo": address.to_string(),
491 "transactionHash": tx_hash
492 });
493 sh_println!("{}", serde_json::to_string_pretty(&output)?)?;
494 } else {
495 sh_println!("Deployer: {deployer_address}")?;
496 sh_println!("Deployed to: {address}")?;
497 sh_println!("Transaction hash: {tx_hash:?}")?;
498 };
499
500 if !self.verify {
501 return Ok(());
502 }
503
504 sh_println!("Starting contract verification...")?;
505
506 let num_of_optimizations = if let Some(optimizer) = self.build.compiler.optimize {
507 optimizer.then(|| self.build.compiler.optimizer_runs.unwrap_or(200))
508 } else {
509 self.build.compiler.optimizer_runs
510 };
511
512 let verify = VerifyArgs {
513 address,
514 contract: Some(self.contract),
515 compiler_version: Some(id.version.to_string()),
516 constructor_args,
517 constructor_args_path: None,
518 no_auto_detect: false,
519 use_solc: None,
520 num_of_optimizations,
521 etherscan: EtherscanOpts { key: self.eth.etherscan.key(), chain: Some(chain.into()) },
522 rpc: Default::default(),
523 flatten: false,
524 force: false,
525 skip_is_verified_check: true,
526 watch: true,
527 retry: self.retry,
528 libraries: self.build.libraries.clone(),
529 root: None,
530 verifier: self.verifier,
531 via_ir: self.build.via_ir,
532 evm_version: self.build.compiler.evm_version,
533 show_standard_json_input: self.show_standard_json_input,
534 guess_constructor_args: false,
535 compilation_profile: Some(id.profile.clone()),
536 language: None,
537 creation_transaction_hash: Some(tx_hash),
538 };
539 sh_println!("Waiting for {} to detect contract deployment...", verify.verifier.verifier)?;
540 verify.run().await
541 }
542
543 fn parse_constructor_args(
548 &self,
549 constructor: &Constructor,
550 constructor_args: &[String],
551 ) -> Result<Vec<DynSolValue>> {
552 if constructor.inputs.len() != constructor_args.len() {
553 eyre::bail!(
554 "Constructor argument count mismatch: expected {} but got {}",
555 constructor.inputs.len(),
556 constructor_args.len()
557 );
558 }
559
560 let mut params = Vec::with_capacity(constructor.inputs.len());
561 for (input, arg) in constructor.inputs.iter().zip(constructor_args) {
562 let ty = input
564 .resolve()
565 .wrap_err_with(|| format!("Could not resolve constructor arg: input={input}"))?;
566 params.push((ty, arg));
567 }
568 let params = params.iter().map(|(ty, arg)| (ty, arg.as_str()));
569 parse_tokens(params).map_err(Into::into)
570 }
571}
572
573impl figment::Provider for CreateArgs {
574 fn metadata(&self) -> Metadata {
575 Metadata::named("Create Args Provider")
576 }
577
578 fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
579 let mut dict = Dict::default();
580 if let Some(timeout) = self.timeout {
581 dict.insert("transaction_timeout".to_string(), timeout.into());
582 }
583 Ok(Map::from([(Config::selected_profile(), dict)]))
584 }
585}
586
587pub type ContractFactory<N, P> = DeploymentTxFactory<N, P>;
593
594#[derive(Debug)]
598#[must_use = "ContractDeploymentTx does nothing unless you `send` it"]
599pub struct ContractDeploymentTx<N: Network, P, C> {
600 pub deployer: Deployer<N, P>,
602 _contract: PhantomData<C>,
606}
607
608impl<N: Network, P: Clone, C> Clone for ContractDeploymentTx<N, P, C> {
609 fn clone(&self) -> Self {
610 Self { deployer: self.deployer.clone(), _contract: self._contract }
611 }
612}
613
614impl<N: Network, P, C> From<Deployer<N, P>> for ContractDeploymentTx<N, P, C> {
615 fn from(deployer: Deployer<N, P>) -> Self {
616 Self { deployer, _contract: PhantomData }
617 }
618}
619
620#[derive(Clone, Debug)]
622#[must_use = "Deployer does nothing unless you `send` it"]
623pub struct Deployer<N: Network, P> {
624 pub tx: N::TransactionRequest,
626 client: P,
627 confs: usize,
628 timeout: u64,
629}
630
631impl<N: Network, P: Provider<N>> Deployer<N, P> {
632 pub async fn send_with_receipt(
636 self,
637 ) -> Result<(Address, N::ReceiptResponse), ContractDeploymentError> {
638 let receipt = self
639 .client
640 .borrow()
641 .send_transaction(self.tx)
642 .await?
643 .with_required_confirmations(self.confs as u64)
644 .with_timeout(Some(Duration::from_secs(self.timeout)))
645 .get_receipt()
646 .await?;
647
648 if !receipt.status() {
649 return Err(ContractDeploymentError::DeploymentFailed(receipt.transaction_hash()));
650 }
651
652 let address =
653 receipt.contract_address().ok_or(ContractDeploymentError::ContractNotDeployed)?;
654
655 Ok((address, receipt))
656 }
657}
658
659#[derive(Clone, Debug)]
663pub struct DeploymentTxFactory<N: Network, P> {
664 client: P,
665 abi: JsonAbi,
666 bytecode: Bytes,
667 timeout: u64,
668 _network: PhantomData<N>,
669}
670
671impl<N: Network, P: Provider<N> + Clone> DeploymentTxFactory<N, P> {
672 pub const fn new(abi: JsonAbi, bytecode: Bytes, client: P, timeout: u64) -> Self {
676 Self { client, abi, bytecode, timeout, _network: PhantomData }
677 }
678
679 pub fn deploy_tokens(
682 self,
683 params: Vec<DynSolValue>,
684 fee_token: Option<Address>,
685 ) -> Result<Deployer<N, P>, ContractDeploymentError>
686 where
687 N::TransactionRequest: FoundryTransactionBuilder<N>,
688 {
689 let data: Bytes = match (self.abi.constructor(), params.is_empty()) {
691 (None, false) => return Err(ContractDeploymentError::ConstructorError),
692 (None, true) => self.bytecode.clone(),
693 (Some(constructor), _) => {
694 let input: Bytes = constructor
695 .abi_encode_input(¶ms)
696 .map_err(ContractDeploymentError::DetokenizationError)?
697 .into();
698 self.bytecode.iter().copied().chain(input).collect()
700 }
701 };
702
703 let mut tx = N::TransactionRequest::default();
705 tx.set_input(data);
706 if let Some(fee_token) = fee_token {
707 tx.set_fee_token(fee_token);
708 }
709 Ok(Deployer { client: self.client.clone(), tx, confs: 1, timeout: self.timeout })
710 }
711}
712
713#[derive(thiserror::Error, Debug)]
714pub enum ContractDeploymentError {
716 #[error("constructor is not defined in the ABI")]
717 ConstructorError,
718 #[error(transparent)]
719 DetokenizationError(#[from] alloy_dyn_abi::Error),
720 #[error("contract was not deployed")]
721 ContractNotDeployed,
722 #[error("deployment transaction failed (receipt status 0): {0}")]
723 DeploymentFailed(alloy_primitives::TxHash),
724 #[error(transparent)]
725 RpcError(#[from] TransportError),
726}
727
728impl From<PendingTransactionError> for ContractDeploymentError {
729 fn from(_err: PendingTransactionError) -> Self {
730 Self::ContractNotDeployed
731 }
732}
733
734#[cfg(test)]
735mod tests {
736 use super::*;
737 use alloy_primitives::I256;
738
739 #[test]
740 fn can_parse_create() {
741 let args: CreateArgs = CreateArgs::parse_from([
742 "foundry-cli",
743 "src/Domains.sol:Domains",
744 "--verify",
745 "--retries",
746 "10",
747 "--delay",
748 "30",
749 ]);
750 assert_eq!(args.retry.retries, 10);
751 assert_eq!(args.retry.delay, 30);
752 }
753 #[test]
754 fn can_parse_chain_id() {
755 let args: CreateArgs = CreateArgs::parse_from([
756 "foundry-cli",
757 "src/Domains.sol:Domains",
758 "--verify",
759 "--retries",
760 "10",
761 "--delay",
762 "30",
763 "--chain-id",
764 "9999",
765 ]);
766 assert_eq!(args.chain_id(), Some(9999));
767 }
768
769 #[test]
770 fn test_parse_constructor_args() {
771 let args: CreateArgs = CreateArgs::parse_from([
772 "foundry-cli",
773 "src/Domains.sol:Domains",
774 "--constructor-args",
775 "Hello",
776 ]);
777 let constructor: Constructor = serde_json::from_str(r#"{"type":"constructor","inputs":[{"name":"_name","type":"string","internalType":"string"}],"stateMutability":"nonpayable"}"#).unwrap();
778 let params = args.parse_constructor_args(&constructor, &args.constructor_args).unwrap();
779 assert_eq!(params, vec![DynSolValue::String("Hello".to_string())]);
780 }
781
782 #[test]
783 fn test_parse_tuple_constructor_args() {
784 let args: CreateArgs = CreateArgs::parse_from([
785 "foundry-cli",
786 "src/Domains.sol:Domains",
787 "--constructor-args",
788 "[(1,2), (2,3), (3,4)]",
789 ]);
790 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();
791 let _params = args.parse_constructor_args(&constructor, &args.constructor_args).unwrap();
792 }
793
794 #[test]
795 fn test_parse_int_constructor_args() {
796 let args: CreateArgs = CreateArgs::parse_from([
797 "foundry-cli",
798 "src/Domains.sol:Domains",
799 "--constructor-args",
800 "-5",
801 ]);
802 let constructor: Constructor = serde_json::from_str(r#"{"type":"constructor","inputs":[{"name":"_name","type":"int256","internalType":"int256"}],"stateMutability":"nonpayable"}"#).unwrap();
803 let params = args.parse_constructor_args(&constructor, &args.constructor_args).unwrap();
804 assert_eq!(params, vec![DynSolValue::Int(I256::unchecked_from(-5), 256)]);
805 }
806}