Skip to content

Commit

Permalink
feat(forge verify-contract): --guess-constructor-args (#6724)
Browse files Browse the repository at this point in the history
* Add RpcOpts to VerifyArgs

* --guess-constructor-args

* Add support for CREATE2 deployer

* Fix artifact lookup

* Update verification tests + Test for --guess-constructor-args

* chore: clippy

* update compilation + separate function

* doc

---------

Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
  • Loading branch information
klkvr and mattsse authored Mar 3, 2024
1 parent acdc57a commit 3df3e0c
Show file tree
Hide file tree
Showing 5 changed files with 214 additions and 40 deletions.
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
87 changes: 81 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,74 @@ 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())
}

/// Uses Etherscan API to fetch contract creation transaction.
/// If transaction is a create transaction or a invocation of default CREATE2 deployer, tries to
/// match provided creation code with local bytecode of the target contract.
/// If bytecode match, returns latest bytes of on-chain creation code as constructor arguments.
async fn guess_constructor_args(
&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 {
#[arg(long, value_hint = ValueHint::FilePath, value_name = "PATH")]
pub constructor_args_path: Option<PathBuf>,

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

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

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

#[command(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);
});

0 comments on commit 3df3e0c

Please sign in to comment.