Skip to content

Commit

Permalink
feat: etherscan identifier
Browse files Browse the repository at this point in the history
  • Loading branch information
onbjerg committed Apr 4, 2022
1 parent 83436e8 commit 3e6685a
Show file tree
Hide file tree
Showing 13 changed files with 529 additions and 281 deletions.
375 changes: 236 additions & 139 deletions Cargo.lock

Large diffs are not rendered by default.

15 changes: 7 additions & 8 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,10 @@ panic = "abort"
# We end up stripping away these symbols anyway
debug = 0

## Patch ethers-rs with a local checkout then run `cargo update -p ethers`
#[patch."https://github.com/gakonst/ethers-rs"]
#ethers = { path = "../ethers-rs" }
#ethers-core = { path = "../ethers-rs/ethers-core" }
#ethers-providers = { path = "../ethers-rs/ethers-providers" }
#ethers-signers = { path = "../ethers-rs/ethers-signers" }
#ethers-etherscan = { path = "../ethers-rs/ethers-etherscan" }
#ethers-solc = { path = "../ethers-rs/ethers-solc" }
[patch."https://github.com/gakonst/ethers-rs"]
ethers = { path = "../ethers-rs" }
ethers-core = { path = "../ethers-rs/ethers-core" }
ethers-providers = { path = "../ethers-rs/ethers-providers" }
ethers-signers = { path = "../ethers-rs/ethers-signers" }
ethers-etherscan = { path = "../ethers-rs/ethers-etherscan" }
ethers-solc = { path = "../ethers-rs/ethers-solc" }
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
24 changes: 20 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 @@ -422,40 +425,52 @@ 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
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();
let etherscan_identifier = EtherscanIdentifier::new(
remote_chain_id,
config.etherscan_api_key.unwrap_or_default(),
remote_chain_id.map(|chain_id| Config::foundry_etherscan_cache_dir(chain_id)).flatten(),
);

// 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 @@ -488,6 +503,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
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 @@ -723,6 +725,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 @@ -912,6 +919,7 @@ impl Default for Config {
block_difficulty: 0,
block_gas_limit: None,
eth_rpc_url: None,
etherscan_api_key: None,
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(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(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

0 comments on commit 3e6685a

Please sign in to comment.