Skip to content

Commit

Permalink
test(verify): add testing support for etherscan verification (#1284)
Browse files Browse the repository at this point in the history
* feat: add verify tests

* feat: add commands

* test: add verify test

* Update cli/tests/it/verify.rs

Co-authored-by: Georgios Konstantopoulos <[email protected]>
  • Loading branch information
mattsse and gakonst authored Apr 12, 2022
1 parent 8c4ab48 commit 64087b5
Show file tree
Hide file tree
Showing 5 changed files with 255 additions and 5 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

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

3 changes: 2 additions & 1 deletion cli/test-utils/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ walkdir = "2.3.2"
once_cell = "1.9.0"
foundry-config = { path = "../../config" }
serde_json = "1.0.67"
parking_lot = "0.12.0"
parking_lot = "0.12.0"
eyre = "0.6"
2 changes: 1 addition & 1 deletion cli/test-utils/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ mod macros;

// Utilities for making it easier to handle tests.
pub mod util;
pub use util::{TestCommand, TestProject};
pub use util::{Retry, TestCommand, TestProject};

pub use ethers_solc;
61 changes: 58 additions & 3 deletions cli/test-utils/src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use std::{
atomic::{AtomicUsize, Ordering},
Arc,
},
time::Duration,
};

static CURRENT_DIR_LOCK: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
Expand Down Expand Up @@ -198,8 +199,7 @@ impl TestProject {

/// Creates a new command that is set to use the forge executable for this project
pub fn forge_command(&self) -> TestCommand {
let mut cmd = self.forge_bin();
cmd.current_dir(&self.inner.root());
let cmd = self.forge_bin();
let _lock = CURRENT_DIR_LOCK.lock();
TestCommand {
project: self.clone(),
Expand All @@ -225,7 +225,9 @@ impl TestProject {
/// Returns the path to the forge executable.
pub fn forge_bin(&self) -> process::Command {
let forge = self.root.join(format!("../forge{}", env::consts::EXE_SUFFIX));
process::Command::new(forge)
let mut cmd = process::Command::new(forge);
cmd.current_dir(self.inner.root());
cmd
}

/// Returns the path to the cast executable.
Expand Down Expand Up @@ -292,6 +294,7 @@ pub struct TestCommand {
project: TestProject,
/// The actual command we use to control the process.
cmd: Command,
// initial: Command,
current_dir_lock: Option<parking_lot::lock_api::MutexGuard<'static, parking_lot::RawMutex, ()>>,
}

Expand Down Expand Up @@ -400,6 +403,11 @@ impl TestCommand {
String::from_utf8_lossy(&self.output().stdout).to_string()
}

/// Returns the output but does not expect that the command was successful
pub fn unchecked_output(&mut self) -> process::Output {
self.cmd.output().unwrap()
}

/// Gets the output of a command. If the command failed, then this panics.
pub fn output(&mut self) -> process::Output {
let output = self.cmd.output().unwrap();
Expand Down Expand Up @@ -532,3 +540,50 @@ pub fn dir_list<P: AsRef<Path>>(dir: P) -> Vec<String> {
.map(|result| result.unwrap().path().to_string_lossy().into_owned())
.collect()
}

/// A type that keeps track of attempts
#[derive(Debug, Clone)]
pub struct Retry {
/// how many attempts there are left
remaining: u32,
/// Optional timeout to apply inbetween attempts
delay: Option<Duration>,
}

impl Retry {
pub fn new(remaining: u32, delay: Option<Duration>) -> Self {
Self { remaining, delay }
}

fn r#try<T>(&mut self, f: impl FnOnce() -> eyre::Result<T>) -> eyre::Result<Option<T>> {
match f() {
Err(ref e) if self.remaining > 0 => {
println!(
"erroneous attempt ({} tries remaining): {}",
self.remaining,
e.root_cause()
);
self.remaining -= 1;
if let Some(delay) = self.delay {
std::thread::sleep(delay);
}
Ok(None)
}
other => other.map(Some),
}
}

pub fn run<T, F>(mut self, mut callback: F) -> eyre::Result<T>
where
F: FnMut() -> eyre::Result<T>,
{
// if let Some(delay) = self.delay {
// std::thread::sleep(delay);
// }
loop {
if let Some(ret) = self.r#try(&mut callback)? {
return Ok(ret)
}
}
}
}
193 changes: 193 additions & 0 deletions cli/tests/it/verify.rs
Original file line number Diff line number Diff line change
@@ -1 +1,194 @@
//! Contains various tests for checking forge commands related to verifying contracts on etherscan
use ethers::types::Chain;
use foundry_cli_test_utils::{
forgetest,
util::{TestCommand, TestProject},
Retry,
};
use std::time::Duration;

fn etherscan_key() -> Option<String> {
std::env::var("ETHERSCAN_API_KEY").ok()
}

fn network_rpc_key(chain: &str) -> Option<String> {
let key = format!("{}_RPC_URL", chain.to_uppercase());
std::env::var(&key).ok()
}

fn network_private_key(chain: &str) -> Option<String> {
let key = format!("{}_PRIVATE_KEY", chain.to_uppercase());
std::env::var(&key).ok()
}

/// Represents external input required for executing verification requests
struct VerifyExternalities {
chain: Chain,
rpc: String,
pk: String,
etherscan: String,
}

impl VerifyExternalities {
fn goerli() -> Option<Self> {
Some(Self {
chain: Chain::Goerli,
rpc: network_rpc_key("goerli")?,
pk: network_private_key("goerli")?,
etherscan: etherscan_key()?,
})
}

/// Returns the arguments required to deploy the contract
fn create_args(&self) -> Vec<String> {
vec![
"--chain".to_string(),
self.chain.to_string(),
"--rpc-url".to_string(),
self.rpc.clone(),
"--private-key".to_string(),
self.pk.clone(),
]
}
}

/// Returns the current millis since unix epoch.
///
/// This way we generate unique contracts so, etherscan will always have to verify them
fn millis_since_epoch() -> u128 {
let now = std::time::SystemTime::now();
now.duration_since(std::time::SystemTime::UNIX_EPOCH)
.unwrap_or_else(|err| panic!("Current time {:?} is invalid: {:?}", now, err))
.as_millis()
}

/// Adds a `Unique` contract to the source directory of the project that can be imported as
/// `import {Unique} from "./unique.sol";`
fn add_unique(prj: &TestProject) {
let timestamp = millis_since_epoch();
prj.inner()
.add_source(
"unique",
format!(
r#"
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.4.0;
contract Unique {{
uint public _timestamp = {};
}}
"#,
timestamp
),
)
.unwrap();
}

// tests that direct import paths are handled correctly
forgetest!(can_verify_random_contract, |prj: TestProject, mut cmd: TestCommand| {
// only execute if keys present
if let Some(info) = VerifyExternalities::goerli() {
add_unique(&prj);

prj.inner()
.add_source(
"Verify.sol",
r#"
// SPDX-License-Identifier: UNLICENSED
pragma solidity =0.8.10;
import {Unique} from "./unique.sol";
contract Verify is Unique {
function doStuff() external {}
}
"#,
)
.unwrap();

let contract_path = "src/Verify.sol:Verify";

cmd.arg("create");
cmd.args(info.create_args()).arg(contract_path);

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

cmd.forge_fuse().arg("verify-contract").root_arg().args([
"--chain-id".to_string(),
info.chain.to_string(),
"--compiler-version".to_string(),
"v0.8.10+commit.fc410830".to_string(),
"--optimizer-runs".to_string(),
"200".to_string(),
address.clone(),
contract_path.to_string(),
info.etherscan.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);
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("--chain-id")
.arg(info.chain.to_string())
.arg(guid)
.arg(info.etherscan);

// give etherscan some time to verify the contract
let retry = Retry::new(6, Some(Duration::from_secs(30)));
retry
.run(|| -> eyre::Result<()> {
let output = cmd.unchecked_output();
let out = String::from_utf8_lossy(&output.stdout);
if out.contains("Contract successfully verified") {
return Ok(())
}
eyre::bail!(
"Failed to get verfiication, stdout: {}, stderr: {}",
out,
String::from_utf8_lossy(&output.stderr)
)
})
.expect("Failed to verify check")
}
}
});

/// Parses the address the contract was deployed to
fn parse_deployed_address(out: &str) -> Option<String> {
for line in out.lines() {
if line.starts_with("Deployed to") {
return Some(line.trim_start_matches("Deployed to: ").to_string())
}
}
None
}

fn parse_verification_guid(out: &str) -> Option<String> {
for line in out.lines() {
if line.contains("GUID") {
return Some(line.replace("GUID:", "").replace("`", "").trim().to_string())
}
}
None
}

0 comments on commit 64087b5

Please sign in to comment.