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: identify addresses from etherscan when forking #1190

Merged
merged 15 commits into from
Apr 7, 2022
Merged
390 changes: 244 additions & 146 deletions Cargo.lock

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions cli/src/cmd/forge/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,9 @@ impl Cmd for RunArgs {
};

// Identify addresses in each trace
// TODO: Could we use the Etherscan identifier here? Main issue: Pulling source code and
// bytecode. Might be better to wait for an interactive debugger where we can do this on
// the fly while retaining access to the database?
let local_identifier = LocalTraceIdentifier::new(&known_contracts);
let mut decoder = CallTraceDecoder::new_with_labels(result.labeled_addresses.clone());
for (_, trace) in &mut result.traces {
Expand Down
29 changes: 25 additions & 4 deletions cli/src/cmd/forge/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ use forge::{
decode::decode_console_logs,
executor::opts::EvmOpts,
gas_report::GasReport,
trace::{identifier::LocalTraceIdentifier, CallTraceDecoder, TraceKind},
trace::{
identifier::{EtherscanIdentifier, LocalTraceIdentifier},
CallTraceDecoder, TraceKind,
},
MultiContractRunner, MultiContractRunnerBuilder, SuiteResult, TestFilter, TestKind,
};
use foundry_config::{figment::Figment, Config};
Expand Down Expand Up @@ -426,40 +429,57 @@ pub fn custom_run(mut args: TestArgs, include_fuzz_tests: bool) -> eyre::Result<
} else {
let TestArgs { filter, .. } = args;
test(
config,
runner,
verbosity,
filter,
args.json,
args.allow_failure,
include_fuzz_tests,
(args.gas_report, config.gas_reports),
args.gas_report,
)
}
}

/// Runs all the tests
#[allow(clippy::too_many_arguments)]
fn test(
config: Config,
mut runner: MultiContractRunner,
verbosity: u8,
filter: Filter,
json: bool,
allow_failure: bool,
include_fuzz_tests: bool,
(gas_reporting, gas_reports): (bool, Vec<String>),
gas_reporting: bool,
) -> eyre::Result<TestOutcome> {
if json {
let results = runner.test(&filter, None, include_fuzz_tests)?;
println!("{}", serde_json::to_string(&results)?);
Ok(TestOutcome::new(results, allow_failure))
} else {
// Set up identifiers
let local_identifier = LocalTraceIdentifier::new(&runner.known_contracts);
let remote_chain_id = runner.evm_opts.get_remote_chain_id();
// Do not re-query etherscan for contracts that you've already queried today.
// TODO: Make this configurable.
let cache_ttl = Duration::from_secs(24 * 60 * 60);
let etherscan_identifier = EtherscanIdentifier::new(
remote_chain_id,
config.etherscan_api_key.unwrap_or_default(),
remote_chain_id.and_then(Config::foundry_etherscan_cache_dir),
cache_ttl,
);

// Set up test reporter channel
let (tx, rx) = channel::<(String, SuiteResult)>();

// Run tests
let handle =
thread::spawn(move || runner.test(&filter, Some(tx), include_fuzz_tests).unwrap());

let mut results: BTreeMap<String, SuiteResult> = BTreeMap::new();
let mut gas_report = GasReport::new(gas_reports);
let mut gas_report = GasReport::new(config.gas_reports);
for (contract_name, suite_result) in rx {
let mut tests = suite_result.test_results.clone();
println!();
Expand Down Expand Up @@ -492,6 +512,7 @@ fn test(
let mut decoded_traces = Vec::new();
for (kind, trace) in &mut result.traces {
decoder.identify(trace, &local_identifier);
decoder.identify(trace, &etherscan_identifier);

let should_include = match kind {
// At verbosity level 3, we only display traces for failed tests
Expand Down
1 change: 1 addition & 0 deletions cli/tests/it/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ forgetest!(can_extract_config_values, |prj: TestProject, mut cmd: TestCommand| {
block_difficulty: 10,
block_gas_limit: Some(100),
eth_rpc_url: Some("localhost".to_string()),
etherscan_api_key: None,
verbosity: 4,
remappings: vec![Remapping::from_str("ds-test=lib/ds-test/").unwrap().into()],
libraries: vec![
Expand Down
8 changes: 8 additions & 0 deletions config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ pub struct Config {
pub verbosity: u8,
/// url of the rpc server that should be used for any rpc calls
pub eth_rpc_url: Option<String>,
/// etherscan API key
pub etherscan_api_key: Option<String>,
/// list of solidity error codes to always silence in the compiler output
pub ignored_error_codes: Vec<SolidityErrorCode>,
/// The number of test cases that must execute for each property test
Expand Down Expand Up @@ -729,6 +731,11 @@ impl Config {
Self::foundry_dir().map(|p| p.join("cache"))
}

/// Returns the path to foundry's etherscan cache dir `~/.foundry/cache/<chain>/etherscan`
pub fn foundry_etherscan_cache_dir(chain_id: impl Into<Chain>) -> Option<PathBuf> {
Some(Self::foundry_cache_dir()?.join(chain_id.into().to_string()).join("etherscan"))
}

/// Returns the path to the cache file of the `block` on the `chain`
/// `~/.foundry/cache/<chain>/<block>/storage.json`
pub fn foundry_block_cache_file(chain_id: impl Into<Chain>, block: u64) -> Option<PathBuf> {
Expand Down Expand Up @@ -918,6 +925,7 @@ impl Default for Config {
block_difficulty: 0,
block_gas_limit: None,
eth_rpc_url: None,
etherscan_api_key: None,
onbjerg marked this conversation as resolved.
Show resolved Hide resolved
verbosity: 0,
remappings: vec![],
libraries: vec![],
Expand Down
15 changes: 10 additions & 5 deletions evm/src/executor/opts.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use ethers::{
providers::{Middleware, Provider},
types::{Address, U256},
types::{Address, Chain, U256},
};
use foundry_utils::RuntimeOrHandle;
use revm::{BlockEnv, CfgEnv, SpecId, TxEnv};
Expand Down Expand Up @@ -86,23 +86,28 @@ impl EvmOpts {
/// - the chain if `fork_url` is set and the endpoints returned its chain id successfully
/// - mainnet otherwise
pub fn get_chain_id(&self) -> u64 {
use ethers::types::Chain;
if let Some(id) = self.env.chain_id {
return id
}
self.get_remote_chain_id().map_or(Chain::Mainnet as u64, |id| id as u64)
}

/// Returns the chain ID from the RPC, if any.
pub fn get_remote_chain_id(&self) -> Option<Chain> {
if let Some(ref url) = self.fork_url {
if url.contains("mainnet") {
tracing::trace!("auto detected mainnet chain from url {}", url);
return Chain::Mainnet as u64
return Some(Chain::Mainnet)
}
let provider = Provider::try_from(url.as_str())
.unwrap_or_else(|_| panic!("Failed to establish provider to {}", url));

if let Ok(id) = foundry_utils::RuntimeOrHandle::new().block_on(provider.get_chainid()) {
return id.as_u64()
return Chain::try_from(id.as_u64()).ok()
}
}
Chain::Mainnet as u64

None
}
}

Expand Down
72 changes: 38 additions & 34 deletions evm/src/trace/decoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,45 +140,49 @@ impl CallTraceDecoder {
///
/// Unknown contracts are contracts that either lack a label or an ABI.
pub fn identify(&mut self, trace: &CallTraceArena, identifier: &impl TraceIdentifier) {
trace.addresses_iter().for_each(|(address, code)| {
// We only try to identify addresses with missing data
if self.labels.contains_key(address) && self.contracts.contains_key(address) {
return
}
let unidentified_addresses = trace
.addresses()
.into_iter()
.filter(|(address, _)| {
!self.labels.contains_key(address) || !self.contracts.contains_key(address)
})
.collect();

let (contract, label, abi) = identifier.identify_address(address, code);
if let Some(contract) = contract {
self.contracts.entry(*address).or_insert(contract);
}
identifier.identify_addresses(unidentified_addresses).iter().for_each(
|(address, contract, label, abi)| {
if let Some(contract) = contract {
self.contracts.entry(*address).or_insert_with(|| contract.to_string());
}

if let Some(label) = label {
self.labels.entry(*address).or_insert(label);
}
if let Some(label) = label {
self.labels.entry(*address).or_insert_with(|| label.to_string());
}

if let Some(abi) = abi {
// Store known functions for the address
abi.functions()
.map(|func| (func.short_signature(), func.clone()))
.for_each(|(sig, func)| self.functions.entry(sig).or_default().push(func));
if let Some(abi) = abi {
// Store known functions for the address
abi.functions()
.map(|func| (func.short_signature(), func.clone()))
.for_each(|(sig, func)| self.functions.entry(sig).or_default().push(func));

// Flatten events from all ABIs
abi.events()
.map(|event| ((event.signature(), indexed_inputs(event)), event.clone()))
.for_each(|(sig, event)| {
self.events.entry(sig).or_default().push(event);
});
// Flatten events from all ABIs
abi.events()
.map(|event| ((event.signature(), indexed_inputs(event)), event.clone()))
.for_each(|(sig, event)| {
self.events.entry(sig).or_default().push(event);
});

// Flatten errors from all ABIs
abi.errors().for_each(|error| {
let entry = self
.errors
.errors
.entry(error.name.clone())
.or_insert_with(Default::default);
entry.push(error.clone());
});
}
});
// Flatten errors from all ABIs
abi.errors().for_each(|error| {
let entry = self
.errors
.errors
.entry(error.name.clone())
.or_insert_with(Default::default);
entry.push(error.clone());
});
}
},
);
}

pub fn decode(&self, traces: &mut CallTraceArena) {
Expand Down
78 changes: 0 additions & 78 deletions evm/src/trace/identifier.rs

This file was deleted.

Loading