Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cast) add creation-code method [#8973] #9029

Merged
merged 24 commits into from
Oct 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Elegant solution w/ DynSolType 👍

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.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should document whether or not this includes constructor args appended to initcode

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mds1 right, I should probably strip them to make the bytecode useful for local deployment. Maybe worth returning constructor args separately?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm reading up there's is no a single unified convention for encoding constructor args with different solidity versions. So I think I'll just add a comment that they are appended.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the use case for this method? I think that impacts whether or not you want to strip the constructor args. If you do want to strip them you'll likely need to use blockscout or etherscan to fetch constructor args

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to get bytecode that I can use for deploying contracts locally, without compiling them myself. So prefer creation code without constructor args appended. But, optionally knowing what were the args values is also useful.

Eventually I want to add artifact method that will combine creation bytecode with JSON ABI, for simply use with sol! Alloy macro.

I'm thinking to add --without-args and --only-args flags. I think it's possible to know the size of appended args from the ABI. WDYT?

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
Loading