diff --git a/crates/cast/Cargo.toml b/crates/cast/Cargo.toml index 5fadb49675ce..4e6b001ecfaf 100644 --- a/crates/cast/Cargo.toml +++ b/crates/cast/Cargo.toml @@ -44,9 +44,14 @@ alloy-json-abi.workspace = true alloy-json-rpc.workspace = true alloy-network.workspace = true alloy-primitives.workspace = true -alloy-provider = { workspace = true, features = ["reqwest", "ws", "ipc"] } +alloy-provider = { workspace = true, features = [ + "reqwest", + "ws", + "ipc", + "trace-api", +] } alloy-rlp.workspace = true -alloy-rpc-types = { workspace = true, features = ["eth"] } +alloy-rpc-types = { workspace = true, features = ["eth", "trace"] } alloy-serde.workspace = true alloy-signer-local = { workspace = true, features = ["mnemonic", "keystore"] } alloy-signer.workspace = true diff --git a/crates/cast/bin/args.rs b/crates/cast/bin/args.rs index 386201e5b0ee..7552c3bd5f03 100644 --- a/crates/cast/bin/args.rs +++ b/crates/cast/bin/args.rs @@ -1,5 +1,6 @@ use crate::cmd::{ - access_list::AccessListArgs, bind::BindArgs, call::CallArgs, create2::Create2Args, + access_list::AccessListArgs, bind::BindArgs, call::CallArgs, + constructor_args::ConstructorArgsArgs, create2::Create2Args, creation_code::CreationCodeArgs, estimate::EstimateArgs, find_block::FindBlockArgs, interface::InterfaceArgs, logs::LogsArgs, mktx::MakeTxArgs, rpc::RpcArgs, run::RunArgs, send::SendTxArgs, storage::StorageArgs, wallet::WalletSubcommands, @@ -937,6 +938,14 @@ pub enum CastSubcommand { command: WalletSubcommands, }, + /// Download a contract creation code from Etherscan and RPC. + #[command(visible_alias = "cc")] + CreationCode(CreationCodeArgs), + + /// Display constructor arguments used for the contract initialization. + #[command(visible_alias = "cra")] + ConstructorArgs(ConstructorArgsArgs), + /// Generate a Solidity interface from a given ABI. /// /// Currently does not support ABI encoder v2. diff --git a/crates/cast/bin/cmd/constructor_args.rs b/crates/cast/bin/cmd/constructor_args.rs new file mode 100644 index 000000000000..b0a08704fb3c --- /dev/null +++ b/crates/cast/bin/cmd/constructor_args.rs @@ -0,0 +1,100 @@ +use alloy_dyn_abi::DynSolType; +use alloy_primitives::{Address, Bytes}; +use clap::{command, Parser}; +use eyre::{eyre, OptionExt, Result}; +use foundry_block_explorers::Client; +use foundry_cli::{ + opts::{EtherscanOpts, RpcOpts}, + utils, +}; +use foundry_config::Config; + +use super::{ + creation_code::fetch_creation_code, + interface::{fetch_abi_from_etherscan, load_abi_from_file}, +}; + +/// CLI arguments for `cast creation-args`. +#[derive(Parser)] +pub struct ConstructorArgsArgs { + /// An Ethereum address, for which the bytecode will be fetched. + contract: Address, + + /// Path to file containing the contract's JSON ABI. It's necessary if the target contract is + /// not verified on Etherscan + #[arg(long)] + abi_path: Option, + + #[command(flatten)] + etherscan: EtherscanOpts, + + #[command(flatten)] + rpc: RpcOpts, +} + +impl ConstructorArgsArgs { + pub async fn run(self) -> Result<()> { + let Self { contract, etherscan, rpc, abi_path } = self; + + let config = Config::from(ðerscan); + let chain = config.chain.unwrap_or_default(); + let api_key = config.get_etherscan_api_key(Some(chain)).unwrap_or_default(); + let client = Client::new(chain, api_key)?; + + let config = Config::from(&rpc); + let provider = utils::get_provider(&config)?; + + let bytecode = fetch_creation_code(contract, client, provider).await?; + + let args_arr = parse_constructor_args(bytecode, contract, ðerscan, abi_path).await?; + for arg in args_arr { + let _ = sh_println!("{arg}"); + } + + Ok(()) + } +} + +/// Fetches the constructor arguments values and types from the creation bytecode and ABI. +async fn parse_constructor_args( + bytecode: Bytes, + contract: Address, + etherscan: &EtherscanOpts, + abi_path: Option, +) -> Result> { + let abi = if let Some(abi_path) = abi_path { + load_abi_from_file(&abi_path, None)? + } else { + fetch_abi_from_etherscan(contract, etherscan).await? + }; + + let abi = abi.into_iter().next().ok_or_eyre("No ABI found.")?; + let (abi, _) = abi; + + let constructor = abi.constructor.ok_or_else(|| eyre!("No constructor found."))?; + + if constructor.inputs.is_empty() { + return Err(eyre!("No constructor arguments found.")); + } + + let args_size = constructor.inputs.len() * 32; + let args_bytes = Bytes::from(bytecode[bytecode.len() - args_size..].to_vec()); + + let display_args: Vec = args_bytes + .chunks(32) + .enumerate() + .map(|(i, arg)| { + format_arg(&constructor.inputs[i].ty, arg).expect("Failed to format argument.") + }) + .collect(); + + Ok(display_args) +} + +fn format_arg(ty: &str, arg: &[u8]) -> Result { + let arg_type: DynSolType = ty.parse().expect("Invalid ABI type."); + let decoded = arg_type.abi_decode(arg)?; + let bytes = Bytes::from(arg.to_vec()); + + Ok(format!("{bytes} → {decoded:?}")) +} diff --git a/crates/cast/bin/cmd/creation_code.rs b/crates/cast/bin/cmd/creation_code.rs new file mode 100644 index 000000000000..bcafeac9488a --- /dev/null +++ b/crates/cast/bin/cmd/creation_code.rs @@ -0,0 +1,167 @@ +use alloy_primitives::{Address, Bytes}; +use alloy_provider::{ext::TraceApi, Provider}; +use alloy_rpc_types::trace::parity::{Action, CreateAction, CreateOutput, TraceOutput}; +use cast::SimpleCast; +use clap::{command, Parser}; +use eyre::{eyre, OptionExt, Result}; +use foundry_block_explorers::Client; +use foundry_cli::{ + opts::{EtherscanOpts, RpcOpts}, + utils, +}; +use foundry_common::provider::RetryProvider; +use foundry_config::Config; + +use super::interface::{fetch_abi_from_etherscan, load_abi_from_file}; + +/// CLI arguments for `cast creation-code`. +#[derive(Parser)] +pub struct CreationCodeArgs { + /// An Ethereum address, for which the bytecode will be fetched. + contract: Address, + + /// Path to file containing the contract's JSON ABI. It's necessary if the target contract is + /// not verified on Etherscan. + #[arg(long)] + abi_path: Option, + + /// Disassemble bytecodes into individual opcodes. + #[arg(long)] + disassemble: bool, + + /// Return creation bytecode without constructor arguments appended. + #[arg(long, conflicts_with = "only_args")] + without_args: bool, + + /// Return only constructor arguments. + #[arg(long)] + only_args: bool, + + #[command(flatten)] + etherscan: EtherscanOpts, + + #[command(flatten)] + rpc: RpcOpts, +} + +impl CreationCodeArgs { + pub async fn run(self) -> Result<()> { + let Self { contract, etherscan, rpc, disassemble, without_args, only_args, abi_path } = + self; + + let config = Config::from(ðerscan); + let chain = config.chain.unwrap_or_default(); + let api_key = config.get_etherscan_api_key(Some(chain)).unwrap_or_default(); + let client = Client::new(chain, api_key)?; + + let config = Config::from(&rpc); + let provider = utils::get_provider(&config)?; + + let bytecode = fetch_creation_code(contract, client, provider).await?; + + let bytecode = + parse_code_output(bytecode, contract, ðerscan, abi_path, without_args, only_args) + .await?; + + if disassemble { + let _ = sh_println!("{}", SimpleCast::disassemble(&bytecode)?); + } else { + let _ = sh_println!("{bytecode}"); + } + + Ok(()) + } +} + +/// Parses the creation bytecode and returns one of the following: +/// - The complete bytecode +/// - The bytecode without constructor arguments +/// - Only the constructor arguments +async fn parse_code_output( + bytecode: Bytes, + contract: Address, + etherscan: &EtherscanOpts, + abi_path: Option, + without_args: bool, + only_args: bool, +) -> Result { + if !without_args && !only_args { + return Ok(bytecode); + } + + let abi = if let Some(abi_path) = abi_path { + load_abi_from_file(&abi_path, None)? + } else { + fetch_abi_from_etherscan(contract, etherscan).await? + }; + + let abi = abi.into_iter().next().ok_or_eyre("No ABI found.")?; + let (abi, _) = abi; + + if abi.constructor.is_none() { + if only_args { + return Err(eyre!("No constructor found.")); + } + return Ok(bytecode); + } + + let constructor = abi.constructor.unwrap(); + if constructor.inputs.is_empty() { + if only_args { + return Err(eyre!("No constructor arguments found.")); + } + return Ok(bytecode); + } + + let args_size = constructor.inputs.len() * 32; + + let bytecode = if without_args { + Bytes::from(bytecode[..bytecode.len() - args_size].to_vec()) + } else if only_args { + Bytes::from(bytecode[bytecode.len() - args_size..].to_vec()) + } else { + unreachable!(); + }; + + Ok(bytecode) +} + +/// Fetches the creation code of a contract from Etherscan and RPC. +pub async fn fetch_creation_code( + contract: Address, + client: Client, + provider: RetryProvider, +) -> Result { + let creation_data = client.contract_creation_data(contract).await?; + let creation_tx_hash = creation_data.transaction_hash; + let tx_data = provider.get_transaction_by_hash(creation_tx_hash).await?; + let tx_data = tx_data.ok_or_eyre("Could not find creation tx data.")?; + + let bytecode = if tx_data.inner.to.is_none() { + // Contract was created using a standard transaction + tx_data.inner.input + } else { + // Contract was created using a factory pattern or create2 + // Extract creation code from tx traces + let mut creation_bytecode = None; + + let traces = provider.trace_transaction(creation_tx_hash).await.map_err(|e| { + eyre!("Could not fetch traces for transaction {}: {}", creation_tx_hash, e) + })?; + + for trace in traces { + if let Some(TraceOutput::Create(CreateOutput { address, .. })) = trace.trace.result { + if address == contract { + creation_bytecode = match trace.trace.action { + Action::Create(CreateAction { init, .. }) => Some(init), + _ => None, + }; + } + } + } + + creation_bytecode.ok_or_else(|| eyre!("Could not find contract creation trace."))? + }; + + Ok(bytecode) +} diff --git a/crates/cast/bin/cmd/interface.rs b/crates/cast/bin/cmd/interface.rs index 1d9b392964b5..45df4983e4f0 100644 --- a/crates/cast/bin/cmd/interface.rs +++ b/crates/cast/bin/cmd/interface.rs @@ -108,7 +108,7 @@ struct InterfaceSource { } /// Load the ABI from a file. -fn load_abi_from_file(path: &str, name: Option) -> Result> { +pub fn load_abi_from_file(path: &str, name: Option) -> Result> { let file = std::fs::read_to_string(path).wrap_err("unable to read abi file")?; let obj: ContractObject = serde_json::from_str(&file)?; let abi = obj.abi.ok_or_else(|| eyre::eyre!("could not find ABI in file {path}"))?; @@ -139,7 +139,7 @@ fn load_abi_from_artifact(path_or_contract: &str) -> Result Result> { diff --git a/crates/cast/bin/cmd/mod.rs b/crates/cast/bin/cmd/mod.rs index 6c904417407c..49e3ed2efe96 100644 --- a/crates/cast/bin/cmd/mod.rs +++ b/crates/cast/bin/cmd/mod.rs @@ -8,7 +8,9 @@ pub mod access_list; pub mod bind; pub mod call; +pub mod constructor_args; pub mod create2; +pub mod creation_code; pub mod estimate; pub mod find_block; pub mod interface; diff --git a/crates/cast/bin/main.rs b/crates/cast/bin/main.rs index 0338d9ff26f0..a7cd013c8b2f 100644 --- a/crates/cast/bin/main.rs +++ b/crates/cast/bin/main.rs @@ -206,6 +206,8 @@ async fn main_args(args: CastArgs) -> Result<()> { sh_println!("{}", SimpleCast::calldata_encode(sig, &args)?)?; } CastSubcommand::Interface(cmd) => cmd.run().await?, + CastSubcommand::CreationCode(cmd) => cmd.run().await?, + CastSubcommand::ConstructorArgs(cmd) => cmd.run().await?, CastSubcommand::Bind(cmd) => cmd.run().await?, CastSubcommand::PrettyCalldata { calldata, offline } => { let calldata = stdin::unwrap_line(calldata)?; diff --git a/crates/cast/tests/cli/main.rs b/crates/cast/tests/cli/main.rs index 6d5a6fd6a821..9ff8a6d49c6d 100644 --- a/crates/cast/tests/cli/main.rs +++ b/crates/cast/tests/cli/main.rs @@ -1435,3 +1435,61 @@ casttest!(format_units, |_prj, cmd| { "#]]); }); + +// tests that fetches a sample contract creation code +// +casttest!(fetch_creation_code_from_etherscan, |_prj, cmd| { + let eth_rpc_url = next_http_rpc_endpoint(); + cmd.args([ + "creation-code", + "--etherscan-api-key", + &next_mainnet_etherscan_api_key(), + "0x0923cad07f06b2d0e5e49e63b8b35738d4156b95", + "--rpc-url", + eth_rpc_url.as_str(), + ]) + .assert_success() + .stdout_eq(str![[r#" +0x60566050600b82828239805160001a6073146043577f4e487b7100000000000000000000000000000000000000000000000000000000600052600060045260246000fd5b30600052607381538281f3fe73000000000000000000000000000000000000000030146080604052600080fdfea264697066735822122074c61e8e4eefd410ca92eec26e8112ec6e831d0a4bf35718fdd78b45d68220d064736f6c63430008070033 + +"#]]); +}); + +// tests that fetches a sample contract creation args bytes +// +casttest!(fetch_creation_code_only_args_from_etherscan, |_prj, cmd| { + let eth_rpc_url = next_http_rpc_endpoint(); + cmd.args([ + "creation-code", + "--etherscan-api-key", + &next_mainnet_etherscan_api_key(), + "0x6982508145454ce325ddbe47a25d4ec3d2311933", + "--rpc-url", + eth_rpc_url.as_str(), + "--only-args", + ]) + .assert_success() + .stdout_eq(str![[r#" +0x00000000000000000000000000000000000014bddab3e51a57cff87a50000000 + +"#]]); +}); + +// tests that displays a sample contract creation args +// +casttest!(fetch_constructor_args_from_etherscan, |_prj, cmd| { + let eth_rpc_url = next_http_rpc_endpoint(); + cmd.args([ + "constructor-args", + "--etherscan-api-key", + &next_mainnet_etherscan_api_key(), + "0x6982508145454ce325ddbe47a25d4ec3d2311933", + "--rpc-url", + eth_rpc_url.as_str(), + ]) + .assert_success() + .stdout_eq(str![[r#" +0x00000000000000000000000000000000000014bddab3e51a57cff87a50000000 → Uint(420690000000000000000000000000000, 256) + +"#]]); +});