1use crate::cmd::install;
2use alloy_chains::Chain;
3use alloy_dyn_abi::{DynSolValue, JsonAbiExt, Specifier};
4use alloy_json_abi::{Constructor, JsonAbi};
5use alloy_network::{AnyNetwork, AnyTransactionReceipt, EthereumWallet, TransactionBuilder};
6use alloy_primitives::{Address, Bytes, hex};
7use alloy_provider::{PendingTransactionError, Provider, ProviderBuilder};
8use alloy_rpc_types::TransactionRequest;
9use alloy_serde::WithOtherFields;
10use alloy_signer::Signer;
11use alloy_transport::TransportError;
12use clap::{Parser, ValueHint};
13use eyre::{Context, Result};
14use forge_verify::{RetryArgs, VerifierArgs, VerifyArgs};
15use foundry_cli::{
16 opts::{BuildOpts, EthereumOpts, EtherscanOpts, TransactionOpts},
17 utils::{self, LoadConfig, read_constructor_args_file, remove_contract},
18};
19use foundry_common::{
20 compile::{self},
21 fmt::parse_tokens,
22 shell,
23};
24use foundry_compilers::{
25 ArtifactId, artifacts::BytecodeObject, info::ContractInfo, utils::canonicalize,
26};
27use foundry_config::{
28 Config,
29 figment::{
30 self, Metadata, Profile,
31 value::{Dict, Map},
32 },
33 merge_impl_figment_convert,
34};
35use serde_json::json;
36use std::{borrow::Borrow, marker::PhantomData, path::PathBuf, sync::Arc, time::Duration};
37
38merge_impl_figment_convert!(CreateArgs, build, eth);
39
40#[derive(Clone, Debug, Parser)]
42pub struct CreateArgs {
43 contract: ContractInfo,
45
46 #[arg(
48 long,
49 num_args(1..),
50 conflicts_with = "constructor_args_path",
51 value_name = "ARGS",
52 allow_hyphen_values = true,
53 )]
54 constructor_args: Vec<String>,
55
56 #[arg(
58 long,
59 value_hint = ValueHint::FilePath,
60 value_name = "PATH",
61 )]
62 constructor_args_path: Option<PathBuf>,
63
64 #[arg(long)]
66 pub broadcast: bool,
67
68 #[arg(long)]
70 verify: bool,
71
72 #[arg(long, requires = "from")]
74 unlocked: bool,
75
76 #[arg(long, requires = "verify")]
81 show_standard_json_input: bool,
82
83 #[arg(long, env = "ETH_TIMEOUT")]
85 pub timeout: Option<u64>,
86
87 #[command(flatten)]
88 build: BuildOpts,
89
90 #[command(flatten)]
91 tx: TransactionOpts,
92
93 #[command(flatten)]
94 eth: EthereumOpts,
95
96 #[command(flatten)]
97 pub verifier: VerifierArgs,
98
99 #[command(flatten)]
100 retry: RetryArgs,
101}
102
103impl CreateArgs {
104 pub async fn run(mut self) -> Result<()> {
106 let mut config = self.load_config()?;
107
108 if install::install_missing_dependencies(&mut config) && config.auto_detect_remappings {
110 config = self.load_config()?;
112 }
113
114 let project = config.project()?;
116
117 let target_path = if let Some(ref mut path) = self.contract.path {
118 canonicalize(project.root().join(path))?
119 } else {
120 project.find_contract_path(&self.contract.name)?
121 };
122
123 let output = compile::compile_target(&target_path, &project, shell::is_json())?;
124
125 let (abi, bin, id) = remove_contract(output, &target_path, &self.contract.name)?;
126
127 let bin = match bin.object {
128 BytecodeObject::Bytecode(_) => bin.object,
129 _ => {
130 let link_refs = bin
131 .link_references
132 .iter()
133 .flat_map(|(path, names)| {
134 names.keys().map(move |name| format!("\t{name}: {path}"))
135 })
136 .collect::<Vec<String>>()
137 .join("\n");
138 eyre::bail!(
139 "Dynamic linking not supported in `create` command - deploy the following library contracts first, then provide the address to link at compile time\n{}",
140 link_refs
141 )
142 }
143 };
144
145 let params = if let Some(constructor) = &abi.constructor {
147 let constructor_args =
148 self.constructor_args_path.clone().map(read_constructor_args_file).transpose()?;
149 self.parse_constructor_args(
150 constructor,
151 constructor_args.as_deref().unwrap_or(&self.constructor_args),
152 )?
153 } else {
154 vec![]
155 };
156
157 let provider = utils::get_provider(&config)?;
158
159 let chain_id = if let Some(chain_id) = self.chain_id() {
161 chain_id
162 } else {
163 provider.get_chain_id().await?
164 };
165
166 let dry_run = !self.broadcast;
168
169 if self.unlocked {
170 let sender = self.eth.wallet.from.expect("required");
172 self.deploy(
173 abi,
174 bin,
175 params,
176 provider,
177 chain_id,
178 sender,
179 config.transaction_timeout,
180 id,
181 dry_run,
182 )
183 .await
184 } else {
185 let signer = self.eth.wallet.signer().await?;
187 let deployer = signer.address();
188 let provider = ProviderBuilder::<_, _, AnyNetwork>::default()
189 .wallet(EthereumWallet::new(signer))
190 .connect_provider(provider);
191 self.deploy(
192 abi,
193 bin,
194 params,
195 provider,
196 chain_id,
197 deployer,
198 config.transaction_timeout,
199 id,
200 dry_run,
201 )
202 .await
203 }
204 }
205
206 fn chain_id(&self) -> Option<u64> {
208 self.eth.etherscan.chain.map(|chain| chain.id())
209 }
210
211 async fn verify_preflight_check(
218 &self,
219 constructor_args: Option<String>,
220 chain: u64,
221 id: &ArtifactId,
222 ) -> Result<()> {
223 let mut verify = VerifyArgs {
226 address: Default::default(),
227 contract: Some(self.contract.clone()),
228 compiler_version: Some(id.version.to_string()),
229 constructor_args,
230 constructor_args_path: None,
231 num_of_optimizations: None,
232 etherscan: EtherscanOpts {
233 key: self.eth.etherscan.key.clone(),
234 api_version: self.eth.etherscan.api_version,
235 chain: Some(chain.into()),
236 },
237 rpc: Default::default(),
238 flatten: false,
239 force: false,
240 skip_is_verified_check: true,
241 watch: true,
242 retry: self.retry,
243 libraries: self.build.libraries.clone(),
244 root: None,
245 verifier: self.verifier.clone(),
246 via_ir: self.build.via_ir,
247 evm_version: self.build.compiler.evm_version,
248 show_standard_json_input: self.show_standard_json_input,
249 guess_constructor_args: false,
250 compilation_profile: Some(id.profile.to_string()),
251 language: None,
252 };
253
254 let config = verify.load_config()?;
257 verify.etherscan.key =
258 config.get_etherscan_config_with_chain(Some(chain.into()))?.map(|c| c.key);
259
260 let context = verify.resolve_context().await?;
261
262 verify.verification_provider()?.preflight_verify_check(verify, context).await?;
263 Ok(())
264 }
265
266 #[expect(clippy::too_many_arguments)]
268 async fn deploy<P: Provider<AnyNetwork>>(
269 self,
270 abi: JsonAbi,
271 bin: BytecodeObject,
272 args: Vec<DynSolValue>,
273 provider: P,
274 chain: u64,
275 deployer_address: Address,
276 timeout: u64,
277 id: ArtifactId,
278 dry_run: bool,
279 ) -> Result<()> {
280 let bin = bin.into_bytes().unwrap_or_default();
281 if bin.is_empty() {
282 eyre::bail!("no bytecode found in bin object for {}", self.contract.name)
283 }
284
285 let provider = Arc::new(provider);
286 let factory = ContractFactory::new(abi.clone(), bin.clone(), provider.clone(), timeout);
287
288 let is_args_empty = args.is_empty();
289 let mut deployer =
290 factory.deploy_tokens(args.clone()).context("failed to deploy contract").map_err(|e| {
291 if is_args_empty {
292 e.wrap_err("no arguments provided for contract constructor; consider --constructor-args or --constructor-args-path")
293 } else {
294 e
295 }
296 })?;
297 let is_legacy = self.tx.legacy || Chain::from(chain).is_legacy();
298
299 deployer.tx.set_from(deployer_address);
300 deployer.tx.set_chain_id(chain);
301 if deployer.tx.to.is_none() {
303 deployer.tx.set_create();
304 }
305 deployer.tx.set_nonce(if let Some(nonce) = self.tx.nonce {
306 Ok(nonce.to())
307 } else {
308 provider.get_transaction_count(deployer_address).await
309 }?);
310
311 if let Some(value) = self.tx.value {
313 deployer.tx.set_value(value);
314 }
315
316 deployer.tx.set_gas_limit(if let Some(gas_limit) = self.tx.gas_limit {
317 Ok(gas_limit.to())
318 } else {
319 provider.estimate_gas(deployer.tx.clone()).await
320 }?);
321
322 if is_legacy {
323 let gas_price = if let Some(gas_price) = self.tx.gas_price {
324 gas_price.to()
325 } else {
326 provider.get_gas_price().await?
327 };
328 deployer.tx.set_gas_price(gas_price);
329 } else {
330 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.")?;
331 let priority_fee = if let Some(priority_fee) = self.tx.priority_gas_price {
332 priority_fee.to()
333 } else {
334 estimate.max_priority_fee_per_gas
335 };
336 let max_fee = if let Some(max_fee) = self.tx.gas_price {
337 max_fee.to()
338 } else {
339 estimate.max_fee_per_gas
340 };
341
342 deployer.tx.set_max_fee_per_gas(max_fee);
343 deployer.tx.set_max_priority_fee_per_gas(priority_fee);
344 }
345
346 let mut constructor_args = None;
348 if self.verify {
349 if !args.is_empty() {
350 let encoded_args = abi
351 .constructor()
352 .ok_or_else(|| eyre::eyre!("could not find constructor"))?
353 .abi_encode_input(&args)?;
354 constructor_args = Some(hex::encode(encoded_args));
355 }
356
357 self.verify_preflight_check(constructor_args.clone(), chain, &id).await?;
358 }
359
360 if dry_run {
361 if !shell::is_json() {
362 sh_warn!("Dry run enabled, not broadcasting transaction\n")?;
363
364 sh_println!("Contract: {}", self.contract.name)?;
365 sh_println!(
366 "Transaction: {}",
367 serde_json::to_string_pretty(&deployer.tx.clone())?
368 )?;
369 sh_println!("ABI: {}\n", serde_json::to_string_pretty(&abi)?)?;
370
371 sh_warn!(
372 "To broadcast this transaction, add --broadcast to the previous command. See forge create --help for more."
373 )?;
374 } else {
375 let output = json!({
376 "contract": self.contract.name,
377 "transaction": &deployer.tx,
378 "abi":&abi
379 });
380 sh_println!("{}", serde_json::to_string_pretty(&output)?)?;
381 }
382
383 return Ok(());
384 }
385
386 let (deployed_contract, receipt) = deployer.send_with_receipt().await?;
388
389 let address = deployed_contract;
390 if shell::is_json() {
391 let output = json!({
392 "deployer": deployer_address.to_string(),
393 "deployedTo": address.to_string(),
394 "transactionHash": receipt.transaction_hash
395 });
396 sh_println!("{}", serde_json::to_string_pretty(&output)?)?;
397 } else {
398 sh_println!("Deployer: {deployer_address}")?;
399 sh_println!("Deployed to: {address}")?;
400 sh_println!("Transaction hash: {:?}", receipt.transaction_hash)?;
401 };
402
403 if !self.verify {
404 return Ok(());
405 }
406
407 sh_println!("Starting contract verification...")?;
408
409 let num_of_optimizations = if let Some(optimizer) = self.build.compiler.optimize {
410 if optimizer { Some(self.build.compiler.optimizer_runs.unwrap_or(200)) } else { None }
411 } else {
412 self.build.compiler.optimizer_runs
413 };
414
415 let verify = VerifyArgs {
416 address,
417 contract: Some(self.contract),
418 compiler_version: Some(id.version.to_string()),
419 constructor_args,
420 constructor_args_path: None,
421 num_of_optimizations,
422 etherscan: EtherscanOpts {
423 key: self.eth.etherscan.key(),
424 api_version: self.eth.etherscan.api_version,
425 chain: Some(chain.into()),
426 },
427 rpc: Default::default(),
428 flatten: false,
429 force: false,
430 skip_is_verified_check: true,
431 watch: true,
432 retry: self.retry,
433 libraries: self.build.libraries.clone(),
434 root: None,
435 verifier: self.verifier,
436 via_ir: self.build.via_ir,
437 evm_version: self.build.compiler.evm_version,
438 show_standard_json_input: self.show_standard_json_input,
439 guess_constructor_args: false,
440 compilation_profile: Some(id.profile.to_string()),
441 language: None,
442 };
443 sh_println!("Waiting for {} to detect contract deployment...", verify.verifier.verifier)?;
444 verify.run().await
445 }
446
447 fn parse_constructor_args(
452 &self,
453 constructor: &Constructor,
454 constructor_args: &[String],
455 ) -> Result<Vec<DynSolValue>> {
456 let mut params = Vec::with_capacity(constructor.inputs.len());
457 for (input, arg) in constructor.inputs.iter().zip(constructor_args) {
458 let ty = input
460 .resolve()
461 .wrap_err_with(|| format!("Could not resolve constructor arg: input={input}"))?;
462 params.push((ty, arg));
463 }
464 let params = params.iter().map(|(ty, arg)| (ty, arg.as_str()));
465 parse_tokens(params).map_err(Into::into)
466 }
467}
468
469impl figment::Provider for CreateArgs {
470 fn metadata(&self) -> Metadata {
471 Metadata::named("Create Args Provider")
472 }
473
474 fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
475 let mut dict = Dict::default();
476 if let Some(timeout) = self.timeout {
477 dict.insert("transaction_timeout".to_string(), timeout.into());
478 }
479 Ok(Map::from([(Config::selected_profile(), dict)]))
480 }
481}
482
483pub type ContractFactory<P> = DeploymentTxFactory<P>;
489
490#[derive(Debug)]
494#[must_use = "ContractDeploymentTx does nothing unless you `send` it"]
495pub struct ContractDeploymentTx<P, C> {
496 pub deployer: Deployer<P>,
498 _contract: PhantomData<C>,
502}
503
504impl<P: Clone, C> Clone for ContractDeploymentTx<P, C> {
505 fn clone(&self) -> Self {
506 Self { deployer: self.deployer.clone(), _contract: self._contract }
507 }
508}
509
510impl<P, C> From<Deployer<P>> for ContractDeploymentTx<P, C> {
511 fn from(deployer: Deployer<P>) -> Self {
512 Self { deployer, _contract: PhantomData }
513 }
514}
515
516#[derive(Clone, Debug)]
518#[must_use = "Deployer does nothing unless you `send` it"]
519pub struct Deployer<P> {
520 pub tx: WithOtherFields<TransactionRequest>,
522 client: P,
523 confs: usize,
524 timeout: u64,
525}
526
527impl<P: Provider<AnyNetwork>> Deployer<P> {
528 pub async fn send_with_receipt(
532 self,
533 ) -> Result<(Address, AnyTransactionReceipt), ContractDeploymentError> {
534 let receipt = self
535 .client
536 .borrow()
537 .send_transaction(self.tx)
538 .await?
539 .with_required_confirmations(self.confs as u64)
540 .with_timeout(Some(Duration::from_secs(self.timeout)))
541 .get_receipt()
542 .await?;
543
544 let address =
545 receipt.contract_address.ok_or(ContractDeploymentError::ContractNotDeployed)?;
546
547 Ok((address, receipt))
548 }
549}
550
551#[derive(Clone, Debug)]
555pub struct DeploymentTxFactory<P> {
556 client: P,
557 abi: JsonAbi,
558 bytecode: Bytes,
559 timeout: u64,
560}
561
562impl<P: Provider<AnyNetwork> + Clone> DeploymentTxFactory<P> {
563 pub fn new(abi: JsonAbi, bytecode: Bytes, client: P, timeout: u64) -> Self {
567 Self { client, abi, bytecode, timeout }
568 }
569
570 pub fn deploy_tokens(
573 self,
574 params: Vec<DynSolValue>,
575 ) -> Result<Deployer<P>, ContractDeploymentError> {
576 let data: Bytes = match (self.abi.constructor(), params.is_empty()) {
578 (None, false) => return Err(ContractDeploymentError::ConstructorError),
579 (None, true) => self.bytecode.clone(),
580 (Some(constructor), _) => {
581 let input: Bytes = constructor
582 .abi_encode_input(¶ms)
583 .map_err(ContractDeploymentError::DetokenizationError)?
584 .into();
585 self.bytecode.iter().copied().chain(input).collect()
587 }
588 };
589
590 let tx = WithOtherFields::new(TransactionRequest::default().input(data.into()));
592
593 Ok(Deployer { client: self.client.clone(), tx, confs: 1, timeout: self.timeout })
594 }
595}
596
597#[derive(thiserror::Error, Debug)]
598pub enum ContractDeploymentError {
600 #[error("constructor is not defined in the ABI")]
601 ConstructorError,
602 #[error(transparent)]
603 DetokenizationError(#[from] alloy_dyn_abi::Error),
604 #[error("contract was not deployed")]
605 ContractNotDeployed,
606 #[error(transparent)]
607 RpcError(#[from] TransportError),
608}
609
610impl From<PendingTransactionError> for ContractDeploymentError {
611 fn from(_err: PendingTransactionError) -> Self {
612 Self::ContractNotDeployed
613 }
614}
615
616#[cfg(test)]
617mod tests {
618 use super::*;
619 use alloy_primitives::I256;
620
621 #[test]
622 fn can_parse_create() {
623 let args: CreateArgs = CreateArgs::parse_from([
624 "foundry-cli",
625 "src/Domains.sol:Domains",
626 "--verify",
627 "--retries",
628 "10",
629 "--delay",
630 "30",
631 ]);
632 assert_eq!(args.retry.retries, 10);
633 assert_eq!(args.retry.delay, 30);
634 }
635 #[test]
636 fn can_parse_chain_id() {
637 let args: CreateArgs = CreateArgs::parse_from([
638 "foundry-cli",
639 "src/Domains.sol:Domains",
640 "--verify",
641 "--retries",
642 "10",
643 "--delay",
644 "30",
645 "--chain-id",
646 "9999",
647 ]);
648 assert_eq!(args.chain_id(), Some(9999));
649 }
650
651 #[test]
652 fn test_parse_constructor_args() {
653 let args: CreateArgs = CreateArgs::parse_from([
654 "foundry-cli",
655 "src/Domains.sol:Domains",
656 "--constructor-args",
657 "Hello",
658 ]);
659 let constructor: Constructor = serde_json::from_str(r#"{"type":"constructor","inputs":[{"name":"_name","type":"string","internalType":"string"}],"stateMutability":"nonpayable"}"#).unwrap();
660 let params = args.parse_constructor_args(&constructor, &args.constructor_args).unwrap();
661 assert_eq!(params, vec![DynSolValue::String("Hello".to_string())]);
662 }
663
664 #[test]
665 fn test_parse_tuple_constructor_args() {
666 let args: CreateArgs = CreateArgs::parse_from([
667 "foundry-cli",
668 "src/Domains.sol:Domains",
669 "--constructor-args",
670 "[(1,2), (2,3), (3,4)]",
671 ]);
672 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();
673 let _params = args.parse_constructor_args(&constructor, &args.constructor_args).unwrap();
674 }
675
676 #[test]
677 fn test_parse_int_constructor_args() {
678 let args: CreateArgs = CreateArgs::parse_from([
679 "foundry-cli",
680 "src/Domains.sol:Domains",
681 "--constructor-args",
682 "-5",
683 ]);
684 let constructor: Constructor = serde_json::from_str(r#"{"type":"constructor","inputs":[{"name":"_name","type":"int256","internalType":"int256"}],"stateMutability":"nonpayable"}"#).unwrap();
685 let params = args.parse_constructor_args(&constructor, &args.constructor_args).unwrap();
686 assert_eq!(params, vec![DynSolValue::Int(I256::unchecked_from(-5), 256)]);
687 }
688}