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::{hex, Address, Bytes};
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, read_constructor_args_file, remove_contract, LoadConfig},
18};
19use foundry_common::{
20 compile::{self},
21 fmt::parse_tokens,
22 shell,
23};
24use foundry_compilers::{
25 artifacts::BytecodeObject, info::ContractInfo, utils::canonicalize, ArtifactId,
26};
27use foundry_config::{
28 figment::{
29 self,
30 value::{Dict, Map},
31 Metadata, Profile,
32 },
33 merge_impl_figment_convert, Config,
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!("Dynamic linking not supported in `create` command - deploy the following library contracts first, then provide the address to link at compile time\n{}", link_refs)
139 }
140 };
141
142 let params = if let Some(constructor) = &abi.constructor {
144 let constructor_args =
145 self.constructor_args_path.clone().map(read_constructor_args_file).transpose()?;
146 self.parse_constructor_args(
147 constructor,
148 constructor_args.as_deref().unwrap_or(&self.constructor_args),
149 )?
150 } else {
151 vec![]
152 };
153
154 let provider = utils::get_provider(&config)?;
155
156 let chain_id = if let Some(chain_id) = self.chain_id() {
158 chain_id
159 } else {
160 provider.get_chain_id().await?
161 };
162
163 let dry_run = !self.broadcast;
165
166 if self.unlocked {
167 let sender = self.eth.wallet.from.expect("required");
169 self.deploy(
170 abi,
171 bin,
172 params,
173 provider,
174 chain_id,
175 sender,
176 config.transaction_timeout,
177 id,
178 dry_run,
179 )
180 .await
181 } else {
182 let signer = self.eth.wallet.signer().await?;
184 let deployer = signer.address();
185 let provider = ProviderBuilder::<_, _, AnyNetwork>::default()
186 .wallet(EthereumWallet::new(signer))
187 .connect_provider(provider);
188 self.deploy(
189 abi,
190 bin,
191 params,
192 provider,
193 chain_id,
194 deployer,
195 config.transaction_timeout,
196 id,
197 dry_run,
198 )
199 .await
200 }
201 }
202
203 fn chain_id(&self) -> Option<u64> {
205 self.eth.etherscan.chain.map(|chain| chain.id())
206 }
207
208 async fn verify_preflight_check(
215 &self,
216 constructor_args: Option<String>,
217 chain: u64,
218 id: &ArtifactId,
219 ) -> Result<()> {
220 let mut verify = VerifyArgs {
223 address: Default::default(),
224 contract: Some(self.contract.clone()),
225 compiler_version: Some(id.version.to_string()),
226 constructor_args,
227 constructor_args_path: None,
228 num_of_optimizations: None,
229 etherscan: EtherscanOpts {
230 key: self.eth.etherscan.key.clone(),
231 api_version: self.eth.etherscan.api_version,
232 chain: Some(chain.into()),
233 },
234 rpc: Default::default(),
235 flatten: false,
236 force: false,
237 skip_is_verified_check: true,
238 watch: true,
239 retry: self.retry,
240 libraries: self.build.libraries.clone(),
241 root: None,
242 verifier: self.verifier.clone(),
243 via_ir: self.build.via_ir,
244 evm_version: self.build.compiler.evm_version,
245 show_standard_json_input: self.show_standard_json_input,
246 guess_constructor_args: false,
247 compilation_profile: Some(id.profile.to_string()),
248 };
249
250 let config = verify.load_config()?;
253 verify.etherscan.key =
254 config.get_etherscan_config_with_chain(Some(chain.into()))?.map(|c| c.key);
255
256 let context = verify.resolve_context().await?;
257
258 verify.verification_provider()?.preflight_verify_check(verify, context).await?;
259 Ok(())
260 }
261
262 #[expect(clippy::too_many_arguments)]
264 async fn deploy<P: Provider<AnyNetwork>>(
265 self,
266 abi: JsonAbi,
267 bin: BytecodeObject,
268 args: Vec<DynSolValue>,
269 provider: P,
270 chain: u64,
271 deployer_address: Address,
272 timeout: u64,
273 id: ArtifactId,
274 dry_run: bool,
275 ) -> Result<()> {
276 let bin = bin.into_bytes().unwrap_or_default();
277 if bin.is_empty() {
278 eyre::bail!("no bytecode found in bin object for {}", self.contract.name)
279 }
280
281 let provider = Arc::new(provider);
282 let factory = ContractFactory::new(abi.clone(), bin.clone(), provider.clone(), timeout);
283
284 let is_args_empty = args.is_empty();
285 let mut deployer =
286 factory.deploy_tokens(args.clone()).context("failed to deploy contract").map_err(|e| {
287 if is_args_empty {
288 e.wrap_err("no arguments provided for contract constructor; consider --constructor-args or --constructor-args-path")
289 } else {
290 e
291 }
292 })?;
293 let is_legacy = self.tx.legacy || Chain::from(chain).is_legacy();
294
295 deployer.tx.set_from(deployer_address);
296 deployer.tx.set_chain_id(chain);
297 if deployer.tx.to.is_none() {
299 deployer.tx.set_create();
300 }
301 deployer.tx.set_nonce(if let Some(nonce) = self.tx.nonce {
302 Ok(nonce.to())
303 } else {
304 provider.get_transaction_count(deployer_address).await
305 }?);
306
307 if let Some(value) = self.tx.value {
309 deployer.tx.set_value(value);
310 }
311
312 deployer.tx.set_gas_limit(if let Some(gas_limit) = self.tx.gas_limit {
313 Ok(gas_limit.to())
314 } else {
315 provider.estimate_gas(deployer.tx.clone()).await
316 }?);
317
318 if is_legacy {
319 let gas_price = if let Some(gas_price) = self.tx.gas_price {
320 gas_price.to()
321 } else {
322 provider.get_gas_price().await?
323 };
324 deployer.tx.set_gas_price(gas_price);
325 } else {
326 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.")?;
327 let priority_fee = if let Some(priority_fee) = self.tx.priority_gas_price {
328 priority_fee.to()
329 } else {
330 estimate.max_priority_fee_per_gas
331 };
332 let max_fee = if let Some(max_fee) = self.tx.gas_price {
333 max_fee.to()
334 } else {
335 estimate.max_fee_per_gas
336 };
337
338 deployer.tx.set_max_fee_per_gas(max_fee);
339 deployer.tx.set_max_priority_fee_per_gas(priority_fee);
340 }
341
342 let mut constructor_args = None;
344 if self.verify {
345 if !args.is_empty() {
346 let encoded_args = abi
347 .constructor()
348 .ok_or_else(|| eyre::eyre!("could not find constructor"))?
349 .abi_encode_input(&args)?;
350 constructor_args = Some(hex::encode(encoded_args));
351 }
352
353 self.verify_preflight_check(constructor_args.clone(), chain, &id).await?;
354 }
355
356 if dry_run {
357 if !shell::is_json() {
358 sh_warn!("Dry run enabled, not broadcasting transaction\n")?;
359
360 sh_println!("Contract: {}", self.contract.name)?;
361 sh_println!(
362 "Transaction: {}",
363 serde_json::to_string_pretty(&deployer.tx.clone())?
364 )?;
365 sh_println!("ABI: {}\n", serde_json::to_string_pretty(&abi)?)?;
366
367 sh_warn!("To broadcast this transaction, add --broadcast to the previous command. See forge create --help for more.")?;
368 } else {
369 let output = json!({
370 "contract": self.contract.name,
371 "transaction": &deployer.tx,
372 "abi":&abi
373 });
374 sh_println!("{}", serde_json::to_string_pretty(&output)?)?;
375 }
376
377 return Ok(());
378 }
379
380 let (deployed_contract, receipt) = deployer.send_with_receipt().await?;
382
383 let address = deployed_contract;
384 if shell::is_json() {
385 let output = json!({
386 "deployer": deployer_address.to_string(),
387 "deployedTo": address.to_string(),
388 "transactionHash": receipt.transaction_hash
389 });
390 sh_println!("{}", serde_json::to_string_pretty(&output)?)?;
391 } else {
392 sh_println!("Deployer: {deployer_address}")?;
393 sh_println!("Deployed to: {address}")?;
394 sh_println!("Transaction hash: {:?}", receipt.transaction_hash)?;
395 };
396
397 if !self.verify {
398 return Ok(());
399 }
400
401 sh_println!("Starting contract verification...")?;
402
403 let num_of_optimizations = if let Some(optimizer) = self.build.compiler.optimize {
404 if optimizer {
405 Some(self.build.compiler.optimizer_runs.unwrap_or(200))
406 } else {
407 None
408 }
409 } else {
410 self.build.compiler.optimizer_runs
411 };
412
413 let verify = VerifyArgs {
414 address,
415 contract: Some(self.contract),
416 compiler_version: Some(id.version.to_string()),
417 constructor_args,
418 constructor_args_path: None,
419 num_of_optimizations,
420 etherscan: EtherscanOpts {
421 key: self.eth.etherscan.key(),
422 api_version: self.eth.etherscan.api_version,
423 chain: Some(chain.into()),
424 },
425 rpc: Default::default(),
426 flatten: false,
427 force: false,
428 skip_is_verified_check: true,
429 watch: true,
430 retry: self.retry,
431 libraries: self.build.libraries.clone(),
432 root: None,
433 verifier: self.verifier,
434 via_ir: self.build.via_ir,
435 evm_version: self.build.compiler.evm_version,
436 show_standard_json_input: self.show_standard_json_input,
437 guess_constructor_args: false,
438 compilation_profile: Some(id.profile.to_string()),
439 };
440 sh_println!("Waiting for {} to detect contract deployment...", verify.verifier.verifier)?;
441 verify.run().await
442 }
443
444 fn parse_constructor_args(
449 &self,
450 constructor: &Constructor,
451 constructor_args: &[String],
452 ) -> Result<Vec<DynSolValue>> {
453 let mut params = Vec::with_capacity(constructor.inputs.len());
454 for (input, arg) in constructor.inputs.iter().zip(constructor_args) {
455 let ty = input
457 .resolve()
458 .wrap_err_with(|| format!("Could not resolve constructor arg: input={input}"))?;
459 params.push((ty, arg));
460 }
461 let params = params.iter().map(|(ty, arg)| (ty, arg.as_str()));
462 parse_tokens(params).map_err(Into::into)
463 }
464}
465
466impl figment::Provider for CreateArgs {
467 fn metadata(&self) -> Metadata {
468 Metadata::named("Create Args Provider")
469 }
470
471 fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
472 let mut dict = Dict::default();
473 if let Some(timeout) = self.timeout {
474 dict.insert("transaction_timeout".to_string(), timeout.into());
475 }
476 Ok(Map::from([(Config::selected_profile(), dict)]))
477 }
478}
479
480pub type ContractFactory<P> = DeploymentTxFactory<P>;
486
487#[derive(Debug)]
491#[must_use = "ContractDeploymentTx does nothing unless you `send` it"]
492pub struct ContractDeploymentTx<P, C> {
493 pub deployer: Deployer<P>,
495 _contract: PhantomData<C>,
499}
500
501impl<P: Clone, C> Clone for ContractDeploymentTx<P, C> {
502 fn clone(&self) -> Self {
503 Self { deployer: self.deployer.clone(), _contract: self._contract }
504 }
505}
506
507impl<P, C> From<Deployer<P>> for ContractDeploymentTx<P, C> {
508 fn from(deployer: Deployer<P>) -> Self {
509 Self { deployer, _contract: PhantomData }
510 }
511}
512
513#[derive(Clone, Debug)]
515#[must_use = "Deployer does nothing unless you `send` it"]
516pub struct Deployer<P> {
517 pub tx: WithOtherFields<TransactionRequest>,
519 client: P,
520 confs: usize,
521 timeout: u64,
522}
523
524impl<P: Provider<AnyNetwork>> Deployer<P> {
525 pub async fn send_with_receipt(
529 self,
530 ) -> Result<(Address, AnyTransactionReceipt), ContractDeploymentError> {
531 let receipt = self
532 .client
533 .borrow()
534 .send_transaction(self.tx)
535 .await?
536 .with_required_confirmations(self.confs as u64)
537 .with_timeout(Some(Duration::from_secs(self.timeout)))
538 .get_receipt()
539 .await?;
540
541 let address =
542 receipt.contract_address.ok_or(ContractDeploymentError::ContractNotDeployed)?;
543
544 Ok((address, receipt))
545 }
546}
547
548#[derive(Clone, Debug)]
552pub struct DeploymentTxFactory<P> {
553 client: P,
554 abi: JsonAbi,
555 bytecode: Bytes,
556 timeout: u64,
557}
558
559impl<P: Provider<AnyNetwork> + Clone> DeploymentTxFactory<P> {
560 pub fn new(abi: JsonAbi, bytecode: Bytes, client: P, timeout: u64) -> Self {
564 Self { client, abi, bytecode, timeout }
565 }
566
567 pub fn deploy_tokens(
570 self,
571 params: Vec<DynSolValue>,
572 ) -> Result<Deployer<P>, ContractDeploymentError> {
573 let data: Bytes = match (self.abi.constructor(), params.is_empty()) {
575 (None, false) => return Err(ContractDeploymentError::ConstructorError),
576 (None, true) => self.bytecode.clone(),
577 (Some(constructor), _) => {
578 let input: Bytes = constructor
579 .abi_encode_input(¶ms)
580 .map_err(ContractDeploymentError::DetokenizationError)?
581 .into();
582 self.bytecode.iter().copied().chain(input).collect()
584 }
585 };
586
587 let tx = WithOtherFields::new(TransactionRequest::default().input(data.into()));
589
590 Ok(Deployer { client: self.client.clone(), tx, confs: 1, timeout: self.timeout })
591 }
592}
593
594#[derive(thiserror::Error, Debug)]
595pub enum ContractDeploymentError {
597 #[error("constructor is not defined in the ABI")]
598 ConstructorError,
599 #[error(transparent)]
600 DetokenizationError(#[from] alloy_dyn_abi::Error),
601 #[error("contract was not deployed")]
602 ContractNotDeployed,
603 #[error(transparent)]
604 RpcError(#[from] TransportError),
605}
606
607impl From<PendingTransactionError> for ContractDeploymentError {
608 fn from(_err: PendingTransactionError) -> Self {
609 Self::ContractNotDeployed
610 }
611}
612
613#[cfg(test)]
614mod tests {
615 use super::*;
616 use alloy_primitives::I256;
617
618 #[test]
619 fn can_parse_create() {
620 let args: CreateArgs = CreateArgs::parse_from([
621 "foundry-cli",
622 "src/Domains.sol:Domains",
623 "--verify",
624 "--retries",
625 "10",
626 "--delay",
627 "30",
628 ]);
629 assert_eq!(args.retry.retries, 10);
630 assert_eq!(args.retry.delay, 30);
631 }
632 #[test]
633 fn can_parse_chain_id() {
634 let args: CreateArgs = CreateArgs::parse_from([
635 "foundry-cli",
636 "src/Domains.sol:Domains",
637 "--verify",
638 "--retries",
639 "10",
640 "--delay",
641 "30",
642 "--chain-id",
643 "9999",
644 ]);
645 assert_eq!(args.chain_id(), Some(9999));
646 }
647
648 #[test]
649 fn test_parse_constructor_args() {
650 let args: CreateArgs = CreateArgs::parse_from([
651 "foundry-cli",
652 "src/Domains.sol:Domains",
653 "--constructor-args",
654 "Hello",
655 ]);
656 let constructor: Constructor = serde_json::from_str(r#"{"type":"constructor","inputs":[{"name":"_name","type":"string","internalType":"string"}],"stateMutability":"nonpayable"}"#).unwrap();
657 let params = args.parse_constructor_args(&constructor, &args.constructor_args).unwrap();
658 assert_eq!(params, vec![DynSolValue::String("Hello".to_string())]);
659 }
660
661 #[test]
662 fn test_parse_tuple_constructor_args() {
663 let args: CreateArgs = CreateArgs::parse_from([
664 "foundry-cli",
665 "src/Domains.sol:Domains",
666 "--constructor-args",
667 "[(1,2), (2,3), (3,4)]",
668 ]);
669 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();
670 let _params = args.parse_constructor_args(&constructor, &args.constructor_args).unwrap();
671 }
672
673 #[test]
674 fn test_parse_int_constructor_args() {
675 let args: CreateArgs = CreateArgs::parse_from([
676 "foundry-cli",
677 "src/Domains.sol:Domains",
678 "--constructor-args",
679 "-5",
680 ]);
681 let constructor: Constructor = serde_json::from_str(r#"{"type":"constructor","inputs":[{"name":"_name","type":"int256","internalType":"int256"}],"stateMutability":"nonpayable"}"#).unwrap();
682 let params = args.parse_constructor_args(&constructor, &args.constructor_args).unwrap();
683 assert_eq!(params, vec![DynSolValue::Int(I256::unchecked_from(-5), 256)]);
684 }
685}