Skip to content

feat: Etherscan V2 support #10440

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

Merged
merged 20 commits into from
May 8, 2025
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
6 changes: 4 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ foundry-wallets = { path = "crates/wallets" }
foundry-linking = { path = "crates/linking" }

# solc & compilation utilities
foundry-block-explorers = { version = "0.13.0", default-features = false }
foundry-block-explorers = { version = "0.13.3", default-features = false }
foundry-compilers = { version = "0.14.0", default-features = false }
foundry-fork-db = "0.12"
solang-parser = "=0.3.3"
Expand Down
1 change: 1 addition & 0 deletions crates/anvil/tests/it/fork.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1324,6 +1324,7 @@ async fn test_fork_execution_reverted() {

// <https://github.com/foundry-rs/foundry/issues/8227>
#[tokio::test(flavor = "multi_thread")]
#[ignore]
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

unrelated, flaky immutable test

async fn test_immutable_fork_transaction_hash() {
use std::str::FromStr;

Expand Down
10 changes: 3 additions & 7 deletions crates/cast/src/cmd/artifact.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
use super::{
creation_code::{fetch_creation_code, parse_code_output},
creation_code::{fetch_creation_code_from_etherscan, parse_code_output},
interface::{fetch_abi_from_etherscan, load_abi_from_file},
};
use alloy_primitives::Address;
use alloy_provider::Provider;
use clap::{command, Parser};
use eyre::Result;
use foundry_block_explorers::Client;
use foundry_cli::{
opts::{EtherscanOpts, RpcOpts},
utils::{self, LoadConfig},
Expand Down Expand Up @@ -46,15 +45,12 @@ pub struct ArtifactArgs {

impl ArtifactArgs {
pub async fn run(self) -> Result<()> {
let Self { contract, etherscan, rpc, output: output_location, abi_path } = self;
let Self { contract, mut etherscan, rpc, output: output_location, abi_path } = self;

let mut etherscan = etherscan;
let config = rpc.load_config()?;
let provider = utils::get_provider(&config)?;
let api_key = etherscan.key().unwrap_or_default();
let chain = provider.get_chain_id().await?;
etherscan.chain = Some(chain.into());
let client = Client::new(chain.into(), api_key)?;

let abi = if let Some(ref abi_path) = abi_path {
load_abi_from_file(abi_path, None)?
Expand All @@ -64,7 +60,7 @@ impl ArtifactArgs {

let (abi, _) = abi.first().ok_or_else(|| eyre::eyre!("No ABI found"))?;

let bytecode = fetch_creation_code(contract, client, provider).await?;
let bytecode = fetch_creation_code_from_etherscan(contract, &etherscan, provider).await?;
let bytecode =
parse_code_output(bytecode, contract, &etherscan, abi_path.as_deref(), true, false)
.await?;
Expand Down
7 changes: 2 additions & 5 deletions crates/cast/src/cmd/constructor_args.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
use super::{
creation_code::fetch_creation_code,
creation_code::fetch_creation_code_from_etherscan,
interface::{fetch_abi_from_etherscan, load_abi_from_file},
};
use alloy_dyn_abi::DynSolType;
use alloy_primitives::{Address, Bytes};
use alloy_provider::Provider;
use clap::{command, Parser};
use eyre::{eyre, OptionExt, Result};
use foundry_block_explorers::Client;
use foundry_cli::{
opts::{EtherscanOpts, RpcOpts},
utils::{self, LoadConfig},
Expand Down Expand Up @@ -37,12 +36,10 @@ impl ConstructorArgsArgs {

let config = rpc.load_config()?;
let provider = utils::get_provider(&config)?;
let api_key = etherscan.key().unwrap_or_default();
let chain = provider.get_chain_id().await?;
etherscan.chain = Some(chain.into());
let client = Client::new(chain.into(), api_key)?;

let bytecode = fetch_creation_code(contract, client, provider).await?;
let bytecode = fetch_creation_code_from_etherscan(contract, &etherscan, provider).await?;

let args_arr = parse_constructor_args(bytecode, contract, &etherscan, abi_path).await?;
for arg in args_arr {
Expand Down
13 changes: 8 additions & 5 deletions crates/cast/src/cmd/creation_code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,10 @@ impl CreationCodeArgs {

let config = rpc.load_config()?;
let provider = utils::get_provider(&config)?;
let api_key = etherscan.key().unwrap_or_default();
let chain = provider.get_chain_id().await?;
etherscan.chain = Some(chain.into());
let client = Client::new(chain.into(), api_key)?;

let bytecode = fetch_creation_code(contract, client, provider).await?;
let bytecode = fetch_creation_code_from_etherscan(contract, &etherscan, provider).await?;

let bytecode = parse_code_output(
bytecode,
Expand Down Expand Up @@ -131,11 +129,16 @@ pub async fn parse_code_output(
}

/// Fetches the creation code of a contract from Etherscan and RPC.
pub async fn fetch_creation_code(
pub async fn fetch_creation_code_from_etherscan(
contract: Address,
client: Client,
etherscan: &EtherscanOpts,
provider: RetryProvider,
) -> Result<Bytes> {
let config = etherscan.load_config()?;
let chain = config.chain.unwrap_or_default();
let api_version = config.get_etherscan_api_version(Some(chain));
let api_key = config.get_etherscan_api_key(Some(chain)).unwrap_or_default();
Comment on lines +138 to +140
Copy link
Member

Choose a reason for hiding this comment

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

it's funny how when you try to unify things you now suddenly need to handle another case -.-

let client = Client::new_with_api_version(chain, api_key, api_version)?;
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?;
Expand Down
3 changes: 2 additions & 1 deletion crates/cast/src/cmd/interface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,8 +143,9 @@ pub async fn fetch_abi_from_etherscan(
) -> Result<Vec<(JsonAbi, String)>> {
let config = etherscan.load_config()?;
let chain = config.chain.unwrap_or_default();
let api_version = config.get_etherscan_api_version(Some(chain));
let api_key = config.get_etherscan_api_key(Some(chain)).unwrap_or_default();
let client = Client::new(chain, api_key)?;
let client = Client::new_with_api_version(chain, api_key, api_version)?;
let source = client.contract_source_code(address).await?;
source.items.into_iter().map(|item| Ok((item.abi()?, item.contract_name))).collect()
}
Expand Down
4 changes: 4 additions & 0 deletions crates/cast/src/cmd/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,10 @@ impl figment::Provider for RunArgs {
map.insert("etherscan_api_key".into(), api_key.as_str().into());
}

if let Some(api_version) = &self.etherscan.api_version {
map.insert("etherscan_api_version".into(), api_version.to_string().into());
}

if let Some(evm_version) = self.evm_version {
map.insert("evm_version".into(), figment::value::Value::serialize(evm_version)?);
}
Expand Down
3 changes: 2 additions & 1 deletion crates/cast/src/cmd/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,9 @@ impl StorageArgs {
}

let chain = utils::get_chain(config.chain, &provider).await?;
let api_version = config.get_etherscan_api_version(Some(chain));
let api_key = config.get_etherscan_api_key(Some(chain)).unwrap_or_default();
let client = Client::new(chain, api_key)?;
let client = Client::new_with_api_version(chain, api_key, api_version)?;
let source = if let Some(proxy) = self.proxy {
find_source(client, proxy.resolve(&provider).await?).await?
} else {
Expand Down
7 changes: 7 additions & 0 deletions crates/cast/src/tx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use alloy_serde::WithOtherFields;
use alloy_signer::Signer;
use alloy_transport::TransportError;
use eyre::Result;
use foundry_block_explorers::EtherscanApiVersion;
use foundry_cli::{
opts::{CliAuthorizationList, TransactionOpts},
utils::{self, parse_function_args},
Expand Down Expand Up @@ -141,6 +142,7 @@ pub struct CastTxBuilder<P, S> {
auth: Option<CliAuthorizationList>,
chain: Chain,
etherscan_api_key: Option<String>,
etherscan_api_version: EtherscanApiVersion,
access_list: Option<Option<AccessList>>,
state: S,
}
Expand All @@ -152,6 +154,7 @@ impl<P: Provider<AnyNetwork>> CastTxBuilder<P, InitState> {
let mut tx = WithOtherFields::<TransactionRequest>::default();

let chain = utils::get_chain(config.chain, &provider).await?;
let etherscan_api_version = config.get_etherscan_api_version(Some(chain));
let etherscan_api_key = config.get_etherscan_api_key(Some(chain));
let legacy = tx_opts.legacy || chain.is_legacy();

Expand Down Expand Up @@ -192,6 +195,7 @@ impl<P: Provider<AnyNetwork>> CastTxBuilder<P, InitState> {
blob: tx_opts.blob,
chain,
etherscan_api_key,
etherscan_api_version,
auth: tx_opts.auth,
access_list: tx_opts.access_list,
state: InitState,
Expand All @@ -208,6 +212,7 @@ impl<P: Provider<AnyNetwork>> CastTxBuilder<P, InitState> {
blob: self.blob,
chain: self.chain,
etherscan_api_key: self.etherscan_api_key,
etherscan_api_version: self.etherscan_api_version,
auth: self.auth,
access_list: self.access_list,
state: ToState { to },
Expand All @@ -233,6 +238,7 @@ impl<P: Provider<AnyNetwork>> CastTxBuilder<P, ToState> {
self.chain,
&self.provider,
self.etherscan_api_key.as_deref(),
self.etherscan_api_version,
)
.await?
} else {
Expand Down Expand Up @@ -264,6 +270,7 @@ impl<P: Provider<AnyNetwork>> CastTxBuilder<P, ToState> {
blob: self.blob,
chain: self.chain,
etherscan_api_key: self.etherscan_api_key,
etherscan_api_version: self.etherscan_api_version,
auth: self.auth,
access_list: self.access_list,
state: InputState { kind: self.state.to.into(), input, func },
Expand Down
26 changes: 13 additions & 13 deletions crates/cast/tests/cli/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use anvil::{EthereumHardfork, NodeConfig};
use foundry_test_utils::{
rpc::{
next_etherscan_api_key, next_http_archive_rpc_url, next_http_rpc_endpoint,
next_mainnet_etherscan_api_key, next_rpc_endpoint, next_ws_rpc_endpoint,
next_rpc_endpoint, next_ws_rpc_endpoint,
},
str,
util::OutputExt,
Expand Down Expand Up @@ -1378,7 +1378,7 @@ casttest!(storage_layout_simple, |_prj, cmd| {
"--block",
"21034138",
"--etherscan-api-key",
next_mainnet_etherscan_api_key().as_str(),
next_etherscan_api_key().as_str(),
"0x13b0D85CcB8bf860b6b79AF3029fCA081AE9beF2",
])
.assert_success()
Expand All @@ -1405,7 +1405,7 @@ casttest!(storage_layout_simple_json, |_prj, cmd| {
"--block",
"21034138",
"--etherscan-api-key",
next_mainnet_etherscan_api_key().as_str(),
next_etherscan_api_key().as_str(),
"0x13b0D85CcB8bf860b6b79AF3029fCA081AE9beF2",
"--json",
])
Expand All @@ -1422,7 +1422,7 @@ casttest!(storage_layout_complex, |_prj, cmd| {
"--block",
"21034138",
"--etherscan-api-key",
next_mainnet_etherscan_api_key().as_str(),
next_etherscan_api_key().as_str(),
"0xBA12222222228d8Ba445958a75a0704d566BF2C8",
])
.assert_success()
Expand Down Expand Up @@ -1470,7 +1470,7 @@ casttest!(storage_layout_complex_proxy, |_prj, cmd| {
"--block",
"7857852",
"--etherscan-api-key",
next_mainnet_etherscan_api_key().as_str(),
next_etherscan_api_key().as_str(),
"0xE2588A9CAb7Ea877206E35f615a39f84a64A7A3b",
"--proxy",
"0x29fcb43b46531bca003ddc8fcb67ffe91900c762"
Expand Down Expand Up @@ -1512,7 +1512,7 @@ casttest!(storage_layout_complex_json, |_prj, cmd| {
"--block",
"21034138",
"--etherscan-api-key",
next_mainnet_etherscan_api_key().as_str(),
next_etherscan_api_key().as_str(),
"0xBA12222222228d8Ba445958a75a0704d566BF2C8",
"--json",
])
Expand Down Expand Up @@ -1601,7 +1601,7 @@ casttest!(fetch_weth_interface_from_etherscan, |_prj, cmd| {
cmd.args([
"interface",
"--etherscan-api-key",
&next_mainnet_etherscan_api_key(),
&next_etherscan_api_key(),
"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
])
.assert_success()
Expand Down Expand Up @@ -1880,7 +1880,7 @@ casttest!(fetch_creation_code_from_etherscan, |_prj, cmd| {
cmd.args([
"creation-code",
"--etherscan-api-key",
&next_mainnet_etherscan_api_key(),
&next_etherscan_api_key(),
"0x0923cad07f06b2d0e5e49e63b8b35738d4156b95",
"--rpc-url",
eth_rpc_url.as_str(),
Expand All @@ -1899,7 +1899,7 @@ casttest!(fetch_creation_code_only_args_from_etherscan, |_prj, cmd| {
cmd.args([
"creation-code",
"--etherscan-api-key",
&next_mainnet_etherscan_api_key(),
&next_etherscan_api_key(),
"0x6982508145454ce325ddbe47a25d4ec3d2311933",
"--rpc-url",
eth_rpc_url.as_str(),
Expand All @@ -1919,7 +1919,7 @@ casttest!(fetch_constructor_args_from_etherscan, |_prj, cmd| {
cmd.args([
"constructor-args",
"--etherscan-api-key",
&next_mainnet_etherscan_api_key(),
&next_etherscan_api_key(),
"0x6982508145454ce325ddbe47a25d4ec3d2311933",
"--rpc-url",
eth_rpc_url.as_str(),
Expand All @@ -1940,7 +1940,7 @@ casttest!(test_non_mainnet_traces, |prj, cmd| {
"--rpc-url",
next_rpc_endpoint(NamedChain::Optimism).as_str(),
"--etherscan-api-key",
next_etherscan_api_key(NamedChain::Optimism).as_str(),
next_etherscan_api_key().as_str(),
])
.assert_success()
.stdout_eq(str![[r#"
Expand All @@ -1963,7 +1963,7 @@ casttest!(fetch_artifact_from_etherscan, |_prj, cmd| {
cmd.args([
"artifact",
"--etherscan-api-key",
&next_mainnet_etherscan_api_key(),
&next_etherscan_api_key(),
"0x0923cad07f06b2d0e5e49e63b8b35738d4156b95",
"--rpc-url",
eth_rpc_url.as_str(),
Expand Down Expand Up @@ -2444,7 +2444,7 @@ contract WETH9 {

casttest!(fetch_src_default, |_prj, cmd| {
let weth = address!("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
let etherscan_api_key = next_mainnet_etherscan_api_key();
let etherscan_api_key = next_etherscan_api_key();

cmd.args(["source", &weth.to_string(), "--flatten", "--etherscan-api-key", &etherscan_api_key])
.assert_success()
Expand Down
1 change: 1 addition & 0 deletions crates/cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ foundry-evm.workspace = true
foundry-wallets.workspace = true

foundry-compilers = { workspace = true, features = ["full"] }
foundry-block-explorers.workspace = true

alloy-eips.workspace = true
alloy-dyn-abi.workspace = true
Expand Down
16 changes: 16 additions & 0 deletions crates/cli/src/opts/rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use crate::opts::ChainValueParser;
use alloy_chains::ChainKind;
use clap::Parser;
use eyre::Result;
use foundry_block_explorers::EtherscanApiVersion;
use foundry_config::{
figment::{
self,
Expand Down Expand Up @@ -114,6 +115,16 @@ pub struct EtherscanOpts {
#[serde(rename = "etherscan_api_key", skip_serializing_if = "Option::is_none")]
pub key: Option<String>,

/// The Etherscan API version.
#[arg(
short,
long = "etherscan-api-version",
alias = "api-version",
env = "ETHERSCAN_API_VERSION"
)]
#[serde(rename = "etherscan_api_version", skip_serializing_if = "Option::is_none")]
pub api_version: Option<EtherscanApiVersion>,

/// The chain name or EIP-155 chain ID.
#[arg(
short,
Expand Down Expand Up @@ -154,6 +165,11 @@ impl EtherscanOpts {
if let Some(key) = self.key() {
dict.insert("etherscan_api_key".into(), key.into());
}

if let Some(api_version) = &self.api_version {
dict.insert("etherscan_api_version".into(), api_version.to_string().into());
}

if let Some(chain) = self.chain {
if let ChainKind::Id(id) = chain.kind() {
dict.insert("chain_id".into(), (*id).into());
Expand Down
Loading
Loading