Skip to content

Adding support to etherscan-v2 #10298

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

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
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
97 changes: 94 additions & 3 deletions crates/verify/src/etherscan/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -261,19 +261,29 @@ impl EtherscanVerificationProvider {
let etherscan_config = config.get_etherscan_config_with_chain(Some(chain))?;

let etherscan_api_url = verifier_url
.or_else(|| etherscan_config.as_ref().map(|c| c.api_url.as_str()))
.or_else(|| {
if verifier_type.is_etherscan_v2() {
None
} else {
etherscan_config.as_ref().map(|c| c.api_url.as_str())
}
})
.map(str::to_owned);

let api_url = etherscan_api_url.as_deref();
let base_url = etherscan_config
.as_ref()
.and_then(|c| c.browser_url.as_deref())
.or_else(|| chain.etherscan_urls().map(|(_, url)| url));

let etherscan_key =
etherscan_key.or_else(|| etherscan_config.as_ref().map(|c| c.key.as_str()));
let etherscan_api_version = if verifier_type.is_etherscan_v2() {
foundry_block_explorers::EtherscanApiVersion::V2
} else {
foundry_block_explorers::EtherscanApiVersion::V1
};

let mut builder = Client::builder();
let mut builder = Client::builder().with_api_version(etherscan_api_version);

builder = if let Some(api_url) = api_url {
// we don't want any trailing slashes because this can cause cloudflare issues: <https://github.com/foundry-rs/foundry/pull/6079>
Expand Down Expand Up @@ -466,6 +476,7 @@ async fn ensure_solc_build_metadata(version: Version) -> Result<Version> {
mod tests {
use super::*;
use clap::Parser;
use foundry_block_explorers::EtherscanApiVersion;
use foundry_common::fs;
use foundry_test_utils::{forgetest_async, str};
use tempfile::tempdir;
Expand Down Expand Up @@ -539,6 +550,86 @@ mod tests {
assert!(format!("{client:?}").contains("dummykey"));
}

#[test]
fn can_extract_etherscan_v2_verify_config() {
let temp = tempdir().unwrap();
let root = temp.path();

let config = r#"
[profile.default]

[etherscan]
mumbai = { key = "dummykey", chain = 80001, url = "https://api-testnet.polygonscan.com/" }
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think would be nice to have an optional version here in EtherscanOpts too, defaults to v1. @zerosnacks wdyt?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Makes sense, I can add that.

Testing this it’s going to be nice to use one API key against all chains.

I made a tool called chains at zora which manages api keys and rpc urls globally. May be less useful now https://github.com/ourzora/chains-cli

Copy link
Member

@zerosnacks zerosnacks Apr 15, 2025

Choose a reason for hiding this comment

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

I'll get in touch with Etherscan to see whether switching to V2 by default and deprecating V1 support altogether makes sense. From the documentation it appears that V2 is available on all chains I would expect them to be and all the relevant RPC methods are available. If not I would agree that using an optional version field here defaulting to V1 would be a good idea.

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 think the transition should be gradual afaik the API key they accept is only the mainnet key and it will fail with other keys which need to hit the specific instances instead.

I can reach out and confirm with them.

Choose a reason for hiding this comment

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

we'd recommend teams to switch to V2 entirely, all other API methods are available as well for existing code to check if a contract is verified and so on

Copy link
Member

@zerosnacks zerosnacks Apr 15, 2025

Choose a reason for hiding this comment

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

Given the above, I would be in favor of making this a breaking change for Foundry v1.2. We can point users to how they can get a new V2 compatible API key upon verification failure and we will document it in the release notes.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Should I default to v2 and then prepare this PR to merge in?

Copy link
Contributor

@sakulstra sakulstra Apr 21, 2025

Choose a reason for hiding this comment

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

I'm all for using v2, but just to note sth that we noticed on migration from v1 to v2 on our internal services:
Etherscan has the same api limits on v2 vs v1. For us this caused some breakage as on v1 the limit is per chain and on v2 (given it's only one key) it is for all combined. Not sure if this can cause problems on multichain scripts.

Choose a reason for hiding this comment

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

good concern @sakulstra, it is something we're thinking about and monitoring as users adopt V2

for now we observe most users fare well with a few chains and retry/backoff mechanisms

Copy link
Collaborator

Choose a reason for hiding this comment

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

@iainnash given the v1 will be eol at end of May, we should go for v2. Would you be able to wrap up this PR, becomes quite a priority now. thank you!

"#;

let toml_file = root.join(Config::FILE_NAME);
fs::write(toml_file, config).unwrap();

let args: VerifyArgs = VerifyArgs::parse_from([
"foundry-cli",
"0xd8509bee9c9bf012282ad33aba0d87241baf5064",
"src/Counter.sol:Counter",
"--verifier",
"etherscan-v2",
"--chain",
"mumbai",
"--root",
root.as_os_str().to_str().unwrap(),
]);

let config = args.load_config().unwrap();

let etherscan = EtherscanVerificationProvider::default();

let client = etherscan
.client(
args.etherscan.chain.unwrap_or_default(),
&args.verifier.verifier,
args.verifier.verifier_url.as_deref(),
args.etherscan.key().as_deref(),
&config,
)
.unwrap();

assert_eq!(
client.etherscan_api_url().as_str(),
"https://api.etherscan.io/v2/api?chainid=80001"
);
assert!(format!("{client:?}").contains("dummykey"));

let args: VerifyArgs = VerifyArgs::parse_from([
"foundry-cli",
"0xd8509bee9c9bf012282ad33aba0d87241baf5064",
"src/Counter.sol:Counter",
"--verifier",
"etherscan-v2",
"--chain",
"mumbai",
"--verifier-url",
"https://verifier-url.com/",
"--root",
root.as_os_str().to_str().unwrap(),
]);

let config = args.load_config().unwrap();

assert_eq!(args.verifier.verifier, VerificationProviderType::EtherscanV2);

let etherscan = EtherscanVerificationProvider::default();
let client = etherscan
.client(
args.etherscan.chain.unwrap_or_default(),
&args.verifier.verifier,
args.verifier.verifier_url.as_deref(),
args.etherscan.key().as_deref(),
&config,
)
.unwrap();
assert_eq!(client.etherscan_api_url().as_str(), "https://verifier-url.com/");
assert_eq!(*client.etherscan_api_version(), EtherscanApiVersion::V2);
assert!(format!("{client:?}").contains("dummykey"));
}

#[tokio::test(flavor = "multi_thread")]
async fn fails_on_disabled_cache_and_missing_info() {
let temp = tempdir().unwrap();
Expand Down
11 changes: 10 additions & 1 deletion crates/verify/src/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ impl FromStr for VerificationProviderType {
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"e" | "etherscan" => Ok(Self::Etherscan),
"ev2" | "etherscan-v2" => Ok(Self::EtherscanV2),
"s" | "sourcify" => Ok(Self::Sourcify),
"b" | "blockscout" => Ok(Self::Blockscout),
"o" | "oklink" => Ok(Self::Oklink),
Expand All @@ -139,6 +140,9 @@ impl fmt::Display for VerificationProviderType {
Self::Etherscan => {
write!(f, "etherscan")?;
}
Self::EtherscanV2 => {
write!(f, "etherscan-v2")?;
}
Self::Sourcify => {
write!(f, "sourcify")?;
}
Expand All @@ -159,6 +163,7 @@ impl fmt::Display for VerificationProviderType {
#[derive(Clone, Debug, Default, PartialEq, Eq, clap::ValueEnum)]
pub enum VerificationProviderType {
Etherscan,
EtherscanV2,
#[default]
Sourcify,
Blockscout,
Expand Down Expand Up @@ -208,6 +213,10 @@ impl VerificationProviderType {
}

pub fn is_etherscan(&self) -> bool {
matches!(self, Self::Etherscan)
matches!(self, Self::Etherscan) || matches!(self, Self::EtherscanV2)
}

pub fn is_etherscan_v2(&self) -> bool {
matches!(self, Self::EtherscanV2)
}
}