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(forge verify-contract): --guess-constructor-args #6724

Merged
merged 9 commits into from
Mar 3, 2024
Merged
Show file tree
Hide file tree
Changes from 7 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: 8 additions & 1 deletion crates/forge/bin/cmd/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,11 @@ impl CreateArgs {
constructor_args,
constructor_args_path: None,
num_of_optimizations: None,
etherscan: EtherscanOpts { key: self.eth.etherscan.key(), chain: Some(chain.into()) },
etherscan: EtherscanOpts {
key: self.eth.etherscan.key.clone(),
chain: Some(chain.into()),
},
rpc: Default::default(),
flatten: false,
force: false,
skip_is_verified_check: true,
Expand All @@ -188,6 +192,7 @@ impl CreateArgs {
via_ir: self.opts.via_ir,
evm_version: self.opts.compiler.evm_version,
show_standard_json_input: self.show_standard_json_input,
guess_constructor_args: false,
};

// Check config for Etherscan API Keys to avoid preflight check failing if no
Expand Down Expand Up @@ -326,6 +331,7 @@ impl CreateArgs {
constructor_args_path: None,
num_of_optimizations,
etherscan: EtherscanOpts { key: self.eth.etherscan.key(), chain: Some(chain.into()) },
rpc: Default::default(),
flatten: false,
force: false,
skip_is_verified_check: false,
Expand All @@ -337,6 +343,7 @@ impl CreateArgs {
via_ir: self.opts.via_ir,
evm_version: self.opts.compiler.evm_version,
show_standard_json_input: self.show_standard_json_input,
guess_constructor_args: false,
};
println!("Waiting for {} to detect contract deployment...", verify.verifier.verifier);
verify.run().await
Expand Down
2 changes: 2 additions & 0 deletions crates/forge/bin/cmd/script/verify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ impl VerifyBundle {
constructor_args_path: None,
num_of_optimizations: self.num_of_optimizations,
etherscan: self.etherscan.clone(),
rpc: Default::default(),
flatten: false,
force: false,
skip_is_verified_check: true,
Expand All @@ -116,6 +117,7 @@ impl VerifyBundle {
via_ir: self.via_ir,
evm_version: None,
show_standard_json_input: false,
guess_constructor_args: false,
};

return Some(verify)
Expand Down
83 changes: 77 additions & 6 deletions crates/forge/bin/cmd/verify/etherscan/mod.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
use super::{provider::VerificationProvider, VerifyArgs, VerifyCheckArgs};
use crate::cmd::retry::RETRY_CHECK_ON_VERIFY;
use alloy_json_abi::Function;
use eyre::{eyre, Context, Result};
use ethers_providers::Middleware;
use eyre::{eyre, Context, OptionExt, Result};
use forge::hashbrown::HashSet;
use foundry_block_explorers::{
errors::EtherscanError,
utils::lookup_compiler_version,
verify::{CodeFormat, VerifyContract},
Client,
};
use foundry_cli::utils::{get_cached_entry_by_name, read_constructor_args_file, LoadConfig};
use foundry_common::{abi::encode_function_args, retry::Retry};
use foundry_cli::utils::{self, get_cached_entry_by_name, read_constructor_args_file, LoadConfig};
use foundry_common::{abi::encode_function_args, retry::Retry, types::ToEthers};
use foundry_compilers::{
artifacts::CompactContract, cache::CacheEntry, info::ContractInfo, Project, Solc,
artifacts::{BytecodeObject, CompactContract},
cache::CacheEntry,
info::ContractInfo,
Artifact, Project, Solc,
};
use foundry_config::{Chain, Config, SolcReq};
use foundry_evm::constants::DEFAULT_CREATE2_DEPLOYER;
use futures::FutureExt;
use once_cell::sync::Lazy;
use regex::Regex;
Expand Down Expand Up @@ -332,7 +337,7 @@ impl EtherscanVerificationProvider {
self.source_provider(args).source(args, &project, &contract_path, &compiler_version)?;

let compiler_version = format!("v{}", ensure_solc_build_metadata(compiler_version).await?);
let constructor_args = self.constructor_args(args, &project)?;
let constructor_args = self.constructor_args(args, &project, &config).await?;
let mut verify_args =
VerifyContract::new(args.address, contract_name, source, compiler_version)
.constructor_arguments(constructor_args)
Expand Down Expand Up @@ -435,7 +440,12 @@ impl EtherscanVerificationProvider {
/// Return the optional encoded constructor arguments. If the path to
/// constructor arguments was provided, read them and encode. Otherwise,
/// return whatever was set in the [VerifyArgs] args.
fn constructor_args(&mut self, args: &VerifyArgs, project: &Project) -> Result<Option<String>> {
async fn constructor_args(
&mut self,
args: &VerifyArgs,
project: &Project,
config: &Config,
) -> Result<Option<String>> {
if let Some(ref constructor_args_path) = args.constructor_args_path {
let (_, _, contract) = self.cache_entry(project, &args.contract).wrap_err(
"Cache must be enabled in order to use the `--constructor-args-path` option",
Expand All @@ -459,9 +469,70 @@ impl EtherscanVerificationProvider {
let encoded_args = hex::encode(encoded_args);
return Ok(Some(encoded_args[8..].into()))
}
if args.guess_constructor_args {
return Ok(Some(self.guess_constructor_args(args, project, config).await?))
}

Ok(args.constructor_args.clone())
}

async fn guess_constructor_args(
mattsse marked this conversation as resolved.
Show resolved Hide resolved
&mut self,
args: &VerifyArgs,
project: &Project,
config: &Config,
) -> Result<String> {
let provider = utils::get_provider(config)?;
let client = self.client(
args.etherscan.chain.unwrap_or_default(),
args.verifier.verifier_url.as_deref(),
args.etherscan.key.as_deref(),
config,
)?;

let creation_data = client.contract_creation_data(args.address).await?;
let transaction = provider
.get_transaction(creation_data.transaction_hash.to_ethers())
.await?
.ok_or_eyre("Couldn't fetch transaction data from RPC")?;
let receipt = provider
.get_transaction_receipt(creation_data.transaction_hash.to_ethers())
.await?
.ok_or_eyre("Couldn't fetch transaction receipt from RPC")?;

let maybe_creation_code: &[u8];

if receipt.contract_address == Some(args.address.to_ethers()) {
maybe_creation_code = &transaction.input;
} else if transaction.to == Some(DEFAULT_CREATE2_DEPLOYER.to_ethers()) {
maybe_creation_code = &transaction.input[32..];
} else {
eyre::bail!("Fetching of constructor arguments is not supported for contracts created by contracts")
}

let contract_path = self.contract_path(args, project)?.to_string_lossy().into_owned();
let output = project.compile()?;
let artifact = output
.find(contract_path, &args.contract.name)
.ok_or_eyre("Contract artifact wasn't found locally")?;
let bytecode = artifact
.get_bytecode_object()
.ok_or_eyre("Contract artifact does not contain bytecode")?;

let bytecode = match bytecode.as_ref() {
BytecodeObject::Bytecode(bytes) => Ok(bytes),
BytecodeObject::Unlinked(_) => {
Err(eyre!("You have to provide correct libraries to use --guess-constructor-args"))
}
}?;

if maybe_creation_code.starts_with(bytecode) {
let constructor_args = &maybe_creation_code[bytecode.len()..];
Ok(hex::encode(constructor_args))
} else {
eyre::bail!("Local bytecode doesn't match on-chain bytecode")
}
}
}

/// Given any solc [Version] return a [Version] with build metadata
Expand Down
33 changes: 31 additions & 2 deletions crates/forge/bin/cmd/verify/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ use super::retry::RetryArgs;
use alloy_primitives::Address;
use clap::{Parser, ValueHint};
use eyre::Result;
use foundry_cli::{opts::EtherscanOpts, utils::LoadConfig};
use foundry_cli::{
opts::{EtherscanOpts, RpcOpts},
utils,
utils::LoadConfig,
};
use foundry_compilers::{info::ContractInfo, EvmVersion};
use foundry_config::{figment, impl_figment_convert, impl_figment_convert_cast, Config};
use provider::VerificationProviderType;
Expand Down Expand Up @@ -57,6 +61,10 @@ pub struct VerifyArgs {
#[clap(long, value_hint = ValueHint::FilePath, value_name = "PATH")]
pub constructor_args_path: Option<PathBuf>,

/// Try to extract constructor arguments from on-chain creation code.
#[clap(long)]
pub guess_constructor_args: bool,

/// The `solc` version to use to build the smart contract.
#[clap(long, value_name = "VERSION")]
pub compiler_version: Option<String>,
Expand Down Expand Up @@ -112,6 +120,9 @@ pub struct VerifyArgs {
#[clap(flatten)]
pub etherscan: EtherscanOpts,

#[clap(flatten)]
pub rpc: RpcOpts,

#[clap(flatten)]
pub retry: RetryArgs,

Expand All @@ -130,6 +141,8 @@ impl figment::Provider for VerifyArgs {
&self,
) -> Result<figment::value::Map<figment::Profile, figment::value::Dict>, figment::Error> {
let mut dict = self.etherscan.dict();
dict.extend(self.rpc.dict());

if let Some(root) = self.root.as_ref() {
dict.insert("root".to_string(), figment::value::Value::serialize(root)?);
}
Expand All @@ -154,7 +167,23 @@ impl VerifyArgs {
/// Run the verify command to submit the contract's source code for verification on etherscan
pub async fn run(mut self) -> Result<()> {
let config = self.load_config_emit_warnings();
let chain = config.chain.unwrap_or_default();

if self.guess_constructor_args && config.get_rpc_url().is_none() {
eyre::bail!(
"You have to provide a valid RPC URL to use --guess-constructor-args feature"
)
}

// If chain is not set, we try to get it from the RPC
// If RPC is not set, the default chain is used
let chain = match config.get_rpc_url() {
Some(_) => {
let provider = utils::get_provider(&config)?;
utils::get_chain(config.chain, provider).await?
}
None => config.chain.unwrap_or_default(),
};

self.etherscan.chain = Some(chain);
self.etherscan.key = config.get_etherscan_config_with_chain(Some(chain))?.map(|c| c.key);

Expand Down
123 changes: 92 additions & 31 deletions crates/forge/tests/cli/verify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,24 @@ function doStuff() external {{}}
prj.add_source("Verify.sol", &contract).unwrap();
}

fn add_verify_target_with_constructor(prj: &TestProject) {
prj.add_source(
"Verify.sol",
r#"
import {Unique} from "./unique.sol";
contract Verify is Unique {
struct SomeStruct {
uint256 a;
string str;
}

constructor(SomeStruct memory st, address owner) {}
}
"#,
)
.unwrap();
}

fn parse_verification_result(cmd: &mut TestCommand, retries: u32) -> eyre::Result<()> {
// give etherscan some time to verify the contract
let retry = Retry::new(retries, Some(Duration::from_secs(30)));
Expand All @@ -73,6 +91,39 @@ fn parse_verification_result(cmd: &mut TestCommand, retries: u32) -> eyre::Resul
})
}

fn await_verification_response(info: EnvExternalities, mut cmd: TestCommand) {
let guid = {
// give etherscan some time to detect the transaction
let retry = Retry::new(5, Some(Duration::from_secs(60)));
retry
.run(|| -> eyre::Result<String> {
let output = cmd.unchecked_output();
let out = String::from_utf8_lossy(&output.stdout);
utils::parse_verification_guid(&out).ok_or_else(|| {
eyre::eyre!(
"Failed to get guid, stdout: {}, stderr: {}",
out,
String::from_utf8_lossy(&output.stderr)
)
})
})
.expect("Failed to get verify guid")
};

// verify-check
cmd.forge_fuse()
.arg("verify-check")
.arg(guid)
.arg("--chain-id")
.arg(info.chain.to_string())
.arg("--etherscan-api-key")
.arg(info.etherscan)
.arg("--verifier")
.arg(info.verifier);

parse_verification_result(&mut cmd, 6).expect("Failed to verify check")
}

fn verify_on_chain(info: Option<EnvExternalities>, prj: TestProject, mut cmd: TestCommand) {
// only execute if keys present
if let Some(info) = info {
Expand All @@ -98,37 +149,41 @@ fn verify_on_chain(info: Option<EnvExternalities>, prj: TestProject, mut cmd: Te
info.verifier.to_string(),
]);

// `verify-contract`
let guid = {
// give etherscan some time to detect the transaction
let retry = Retry::new(5, Some(Duration::from_secs(60)));
retry
.run(|| -> eyre::Result<String> {
let output = cmd.unchecked_output();
let out = String::from_utf8_lossy(&output.stdout);
utils::parse_verification_guid(&out).ok_or_else(|| {
eyre::eyre!(
"Failed to get guid, stdout: {}, stderr: {}",
out,
String::from_utf8_lossy(&output.stderr)
)
})
})
.expect("Failed to get verify guid")
};

// verify-check
cmd.forge_fuse()
.arg("verify-check")
.arg(guid)
.arg("--chain-id")
.arg(info.chain.to_string())
.arg("--etherscan-api-key")
.arg(info.etherscan)
.arg("--verifier")
.arg(info.verifier);

parse_verification_result(&mut cmd, 6).expect("Failed to verify check")
await_verification_response(info, cmd)
}
}

fn guess_constructor_args(info: Option<EnvExternalities>, prj: TestProject, mut cmd: TestCommand) {
// only execute if keys present
if let Some(info) = info {
println!("verifying on {}", info.chain);
add_unique(&prj);
add_verify_target_with_constructor(&prj);

let contract_path = "src/Verify.sol:Verify";
cmd.arg("create").args(info.create_args()).arg(contract_path).args(vec![
"--constructor-args",
"(239,SomeString)",
"0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
]);

let out = cmd.stdout_lossy();
let address = utils::parse_deployed_address(out.as_str())
.unwrap_or_else(|| panic!("Failed to parse deployer {out}"));

cmd.forge_fuse().arg("verify-contract").root_arg().args([
"--rpc-url".to_string(),
info.rpc.to_string(),
address,
contract_path.to_string(),
"--etherscan-api-key".to_string(),
info.etherscan.to_string(),
"--verifier".to_string(),
info.verifier.to_string(),
"--guess-constructor-args".to_string(),
]);

await_verification_response(info, cmd)
}
}

Expand Down Expand Up @@ -174,3 +229,9 @@ forgetest!(can_verify_random_contract_sepolia, |prj, cmd| {
forgetest!(can_create_verify_random_contract_sepolia, |prj, cmd| {
create_verify_on_chain(EnvExternalities::sepolia(), prj, cmd);
});

// tests `create && contract-verify --guess-constructor-args && verify-check` on Goerli testnet if
// correct env vars are set
forgetest!(can_guess_constructor_args, |prj, cmd| {
guess_constructor_args(EnvExternalities::goerli(), prj, cmd);
});
Loading