-
Notifications
You must be signed in to change notification settings - Fork 1.7k
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
Changes from all commits
7efeaec
5f86a8f
057495e
b2807ed
0c78d60
6185a74
e4e7110
92732e8
be07698
6e934da
24bac27
b6ff628
0c930af
f5d1ae2
261d82f
6bde573
daf6013
c210f97
94d0b82
dd8bada
c70088b
0ab37fc
a4ac984
268636a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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(ð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<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:?}")) | ||
} |
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(ð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<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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 I'm thinking to add |
||
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) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Elegant solution w/ DynSolType 👍