Skip to content

Commit

Permalink
feat(cast) add creation-code method [#8973] (#9029)
Browse files Browse the repository at this point in the history
* feat(cast) add creation-code method [#8973]

* Fix typo

* Fix CI

* Code review fixes

* Add creation-code flags and creation-args

* Update comments

* eyre style fixes

* typo

* use r#".."# for snapbox

* Apply suggestions from code review

* fix test regression

* tag arguments as mutually exclusive

* use unreachable!

* Rename and add abi_path param

* Decode constructor args

* Update crates/cast/bin/cmd/constructor_args.rs

* fix test

* Update crates/cast/bin/args.rs

Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com>

* Update crates/cast/bin/cmd/creation_code.rs

Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com>

* Update crates/cast/bin/cmd/creation_code.rs

Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com>

* Fix formatting

* Code review fixes

---------

Co-authored-by: zerosnacks <zerosnacks@protonmail.com>
Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com>
  • Loading branch information
3 people authored Oct 29, 2024
1 parent dd443c6 commit 3e901af
Show file tree
Hide file tree
Showing 8 changed files with 348 additions and 5 deletions.
9 changes: 7 additions & 2 deletions crates/cast/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 10 additions & 1 deletion crates/cast/bin/args.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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.
Expand Down
100 changes: 100 additions & 0 deletions crates/cast/bin/cmd/constructor_args.rs
Original file line number Diff line number Diff line change
@@ -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<String>,

#[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(&etherscan);
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, &etherscan, 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<String>,
) -> Result<Vec<String>> {
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<String> = 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<String> {
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:?}"))
}
167 changes: 167 additions & 0 deletions crates/cast/bin/cmd/creation_code.rs
Original file line number Diff line number Diff line change
@@ -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<String>,

/// 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(&etherscan);
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, &etherscan, 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<String>,
without_args: bool,
only_args: bool,
) -> Result<Bytes> {
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<Bytes> {
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)
}
4 changes: 2 additions & 2 deletions crates/cast/bin/cmd/interface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ struct InterfaceSource {
}

/// Load the ABI from a file.
fn load_abi_from_file(path: &str, name: Option<String>) -> Result<Vec<(JsonAbi, String)>> {
pub fn load_abi_from_file(path: &str, name: Option<String>) -> Result<Vec<(JsonAbi, String)>> {
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}"))?;
Expand Down Expand Up @@ -139,7 +139,7 @@ fn load_abi_from_artifact(path_or_contract: &str) -> Result<Vec<(JsonAbi, String
}

/// Fetches the ABI of a contract from Etherscan.
async fn fetch_abi_from_etherscan(
pub async fn fetch_abi_from_etherscan(
address: Address,
etherscan: &EtherscanOpts,
) -> Result<Vec<(JsonAbi, String)>> {
Expand Down
2 changes: 2 additions & 0 deletions crates/cast/bin/cmd/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions crates/cast/bin/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)?;
Expand Down
Loading

0 comments on commit 3e901af

Please sign in to comment.