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 8 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
14 changes: 11 additions & 3 deletions crates/cast/bin/args.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use crate::cmd::{
access_list::AccessListArgs, bind::BindArgs, call::CallArgs, create2::Create2Args,
estimate::EstimateArgs, find_block::FindBlockArgs, interface::InterfaceArgs, logs::LogsArgs,
mktx::MakeTxArgs, rpc::RpcArgs, run::RunArgs, send::SendTxArgs, storage::StorageArgs,
wallet::WalletSubcommands,
creation_args::CreationArgsArgs, creation_code::CreationCodeArgs, estimate::EstimateArgs,
find_block::FindBlockArgs, interface::InterfaceArgs, logs::LogsArgs, mktx::MakeTxArgs,
rpc::RpcArgs, run::RunArgs, send::SendTxArgs, storage::StorageArgs, wallet::WalletSubcommands,
};
use alloy_primitives::{Address, B256, U256};
use alloy_rpc_types::BlockId;
Expand Down Expand Up @@ -898,6 +898,14 @@ pub enum CastSubcommand {
command: WalletSubcommands,
},

/// Download a contract creation code from Etherscan and RPC.
#[command(visible_alias = "cc")]
CreationCode(CreationCodeArgs),

/// Display args used for the contract initialization
pawurb marked this conversation as resolved.
Show resolved Hide resolved
#[command(visible_alias = "cra")]
CreationArgs(CreationArgsArgs),
zerosnacks marked this conversation as resolved.
Show resolved Hide resolved

/// Generate a Solidity interface from a given ABI.
///
/// Currently does not support ABI encoder v2.
Expand Down
81 changes: 81 additions & 0 deletions crates/cast/bin/cmd/creation_args.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
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};

/// CLI arguments for `cast creation-args`.
#[derive(Parser)]
pub struct CreationArgsArgs {
/// An Ethereum address, for which the bytecode will be fetched.
contract: Address,

#[command(flatten)]
etherscan: EtherscanOpts,
#[command(flatten)]
rpc: RpcOpts,
zerosnacks marked this conversation as resolved.
Show resolved Hide resolved
}

impl CreationArgsArgs {
pub async fn run(self) -> Result<()> {
let Self { contract, etherscan, rpc } = 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_creation_args(bytecode, contract, &etherscan).await?;

for arg in args_arr {
println!("{arg}");
}

Ok(())
}
}

/// Fetches the constructor arguments values and types from the creation bytecode and ABI.
async fn parse_creation_args(
zerosnacks marked this conversation as resolved.
Show resolved Hide resolved
bytecode: Bytes,
contract: Address,
etherscan: &EtherscanOpts,
) -> Result<Vec<String>> {
let abi = 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() {
return Err(eyre!("No constructor found."));
}

let constructor = abi.constructor.unwrap();
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)| {
let arg = arg.to_vec();
format!("{} {}", constructor.inputs[i].ty, Bytes::from(arg))
zerosnacks marked this conversation as resolved.
Show resolved Hide resolved
})
.collect();

Ok(display_args)
}
159 changes: 159 additions & 0 deletions crates/cast/bin/cmd/creation_code.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
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;

/// CLI arguments for `cast creation-code`.
#[derive(Parser)]
pub struct CreationCodeArgs {
/// An Ethereum address, for which the bytecode will be fetched.
contract: Address,

/// Disassemble bytecodes into individual opcodes.
#[arg(long)]
disassemble: bool,

/// Return creation bytecode without constructor arguments appended.
#[arg(long)]
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 } = self;

if without_args && only_args {
return Err(eyre!("--without-args and --only-args are mutually exclusive."));
}
zerosnacks marked this conversation as resolved.
Show resolved Hide resolved

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, without_args, only_args).await?;

if disassemble {
println!("{}", SimpleCast::disassemble(&bytecode)?);
} else {
print!("{bytecode}");
}

Ok(())
}
}

/// Parses the creation bytecode to return either the bytecode, or bytecode without constructor
/// arguments or only the constructor arguments.
pawurb marked this conversation as resolved.
Show resolved Hide resolved
async fn parse_code_output(
bytecode: Bytes,
contract: Address,
etherscan: &EtherscanOpts,
without_args: bool,
only_args: bool,
) -> Result<Bytes> {
if !without_args && !only_args {
return Ok(bytecode);
}

let abi = 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 {
panic!("Unreachable.")
zerosnacks marked this conversation as resolved.
Show resolved Hide resolved
};

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, code: _, gas_used: _ })) =
zerosnacks marked this conversation as resolved.
Show resolved Hide resolved
trace.trace.result
{
if address == contract {
creation_bytecode = match trace.trace.action {
Action::Create(CreateAction { init, value: _, from: _, gas: _ }) => {
zerosnacks marked this conversation as resolved.
Show resolved Hide resolved
Some(init)
}
_ => None,
};
}
}
}

creation_bytecode.ok_or_else(|| eyre!("Could not find contract creation trace."))?
};

Ok(bytecode)
}
2 changes: 1 addition & 1 deletion crates/cast/bin/cmd/interface.rs
Original file line number Diff line number Diff line change
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 @@ -9,6 +9,8 @@ pub mod access_list;
pub mod bind;
pub mod call;
pub mod create2;
pub mod creation_args;
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 @@ -186,6 +186,8 @@ async fn main_args(args: CastArgs) -> Result<()> {
println!("{}", SimpleCast::calldata_encode(sig, &args)?);
}
CastSubcommand::Interface(cmd) => cmd.run().await?,
CastSubcommand::CreationCode(cmd) => cmd.run().await?,
CastSubcommand::CreationArgs(cmd) => cmd.run().await?,
CastSubcommand::Bind(cmd) => cmd.run().await?,
CastSubcommand::PrettyCalldata { calldata, offline } => {
let calldata = stdin::unwrap_line(calldata)?;
Expand Down
53 changes: 53 additions & 0 deletions crates/cast/tests/cli/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1326,3 +1326,56 @@ casttest!(hash_message, |_prj, cmd| {

"#]]);
});

// tests that fetches a sample contract creation code
// <https://etherscan.io/address/0x0923cad07f06b2d0e5e49e63b8b35738d4156b95>
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![["0x60566050600b82828239805160001a6073146043577f4e487b7100000000000000000000000000000000000000000000000000000000600052600060045260246000fd5b30600052607381538281f3fe73000000000000000000000000000000000000000030146080604052600080fdfea264697066735822122074c61e8e4eefd410ca92eec26e8112ec6e831d0a4bf35718fdd78b45d68220d064736f6c63430008070033"]]);
});

// tests that fetches a sample contract creation args bytes
// <https://etherscan.io/address/0x0923cad07f06b2d0e5e49e63b8b35738d4156b95>
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![["0x00000000000000000000000000000000000014bddab3e51a57cff87a50000000"]]);
});

// tests that displays a sample contract creation args
// <https://etherscan.io/address/0x0923cad07f06b2d0e5e49e63b8b35738d4156b95>
casttest!(fetch_creation_args_from_etherscan, |_prj, cmd| {
let eth_rpc_url = next_http_rpc_endpoint();
cmd.args([
"creation-args",
"--etherscan-api-key",
&next_mainnet_etherscan_api_key(),
"0x6982508145454ce325ddbe47a25d4ec3d2311933",
"--rpc-url",
eth_rpc_url.as_str(),
])
.assert_success()
.stdout_eq(str![[
"uint256 0x00000000000000000000000000000000000014bddab3e51a57cff87a50000000

"
]]);
});
Loading