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

fix(traces): identify artifacts using both deployed and creation code #9050

Merged
merged 8 commits into from
Oct 14, 2024
5 changes: 3 additions & 2 deletions crates/evm/traces/src/decoder/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -262,17 +262,18 @@ impl CallTraceDecoder {
pub fn trace_addresses<'a>(
&'a self,
arena: &'a CallTraceArena,
) -> impl Iterator<Item = (&'a Address, Option<&'a [u8]>)> + Clone + 'a {
) -> impl Iterator<Item = (&'a Address, Option<&'a [u8]>, Option<&'a [u8]>)> + Clone + 'a {
arena
.nodes()
.iter()
.map(|node| {
(
&node.trace.address,
node.trace.kind.is_any_create().then_some(&node.trace.output[..]),
node.trace.kind.is_any_create().then_some(&node.trace.data[..]),
)
})
.filter(|&(address, _)| {
.filter(|&(address, _, _)| {
!self.labels.contains_key(address) || !self.contracts.contains_key(address)
})
}
Expand Down
4 changes: 2 additions & 2 deletions crates/evm/traces/src/identifier/etherscan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ impl EtherscanIdentifier {
impl TraceIdentifier for EtherscanIdentifier {
fn identify_addresses<'a, A>(&mut self, addresses: A) -> Vec<AddressIdentity<'_>>
where
A: Iterator<Item = (&'a Address, Option<&'a [u8]>)>,
A: Iterator<Item = (&'a Address, Option<&'a [u8]>, Option<&'a [u8]>)>,
{
trace!(target: "evm::traces", "identify {:?} addresses", addresses.size_hint().1);

Expand All @@ -114,7 +114,7 @@ impl TraceIdentifier for EtherscanIdentifier {
Arc::clone(&self.invalid_api_key),
);

for (addr, _) in addresses {
for (addr, _, _) in addresses {
if let Some(metadata) = self.contracts.get(addr) {
let label = metadata.contract_name.clone();
let abi = metadata.abi().ok().map(Cow::Owned);
Expand Down
46 changes: 33 additions & 13 deletions crates/evm/traces/src/identifier/local.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,23 +32,34 @@ impl<'a> LocalTraceIdentifier<'a> {
self.known_contracts
}

/// Tries to the bytecode most similar to the given one.
pub fn identify_code(&self, code: &[u8]) -> Option<(&'a ArtifactId, &'a JsonAbi)> {
let len = code.len();
/// Identifies the artifact based on score computed for both creation and deployed bytecodes.
pub fn identify_code(
&self,
runtime_code: &[u8],
creation_code: &[u8],
) -> Option<(&'a ArtifactId, &'a JsonAbi)> {
let len = runtime_code.len();

let mut min_score = f64::MAX;
let mut min_score_id = None;

let mut check = |id| {
let mut check = |id, is_creation, min_score: &mut f64| {
let contract = self.known_contracts.get(id)?;
if let Some(deployed_bytecode) = contract.deployed_bytecode() {
let score = bytecode_diff_score(deployed_bytecode, code);
// Select bytecodes to compare based on `is_creation` flag.
let (contract_bytecode, current_bytecode) = if is_creation {
(contract.bytecode(), creation_code)
} else {
(contract.deployed_bytecode(), runtime_code)
};

if let Some(bytecode) = contract_bytecode {
let score = bytecode_diff_score(bytecode, current_bytecode);
if score == 0.0 {
trace!(target: "evm::traces", "found exact match");
return Some((id, &contract.abi));
}
if score < min_score {
min_score = score;
if score < *min_score {
*min_score = score;
min_score_id = Some((id, &contract.abi));
}
}
Expand All @@ -65,7 +76,7 @@ impl<'a> LocalTraceIdentifier<'a> {
if len > max_len {
break;
}
if let found @ Some(_) = check(id) {
if let found @ Some(_) = check(id, true, &mut min_score) {
return found;
}
}
Expand All @@ -75,11 +86,20 @@ impl<'a> LocalTraceIdentifier<'a> {
let idx = self.find_index(min_len);
for i in idx..same_length_idx {
let (id, _) = self.ordered_ids[i];
if let found @ Some(_) = check(id) {
if let found @ Some(_) = check(id, true, &mut min_score) {
return found;
}
}

// Fallback to comparing deployed code if min score greater than threshold.
if min_score >= 0.85 {
for (artifact, _) in &self.ordered_ids {
if let found @ Some(_) = check(artifact, false, &mut min_score) {
return found;
}
}
}

trace!(target: "evm::traces", %min_score, "no exact match found");

// Note: the diff score can be inaccurate for small contracts so we're using a relatively
Expand Down Expand Up @@ -109,16 +129,16 @@ impl<'a> LocalTraceIdentifier<'a> {
impl TraceIdentifier for LocalTraceIdentifier<'_> {
fn identify_addresses<'a, A>(&mut self, addresses: A) -> Vec<AddressIdentity<'_>>
where
A: Iterator<Item = (&'a Address, Option<&'a [u8]>)>,
A: Iterator<Item = (&'a Address, Option<&'a [u8]>, Option<&'a [u8]>)>,
{
trace!(target: "evm::traces", "identify {:?} addresses", addresses.size_hint().1);

addresses
.filter_map(|(address, code)| {
.filter_map(|(address, runtime_code, creation_code)| {
let _span = trace_span!(target: "evm::traces", "identify", %address).entered();

trace!(target: "evm::traces", "identifying");
let (id, abi) = self.identify_code(code?)?;
let (id, abi) = self.identify_code(runtime_code?, creation_code?)?;
trace!(target: "evm::traces", id=%id.identifier(), "identified");

Some(AddressIdentity {
Expand Down
4 changes: 2 additions & 2 deletions crates/evm/traces/src/identifier/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ pub trait TraceIdentifier {
/// Attempts to identify an address in one or more call traces.
fn identify_addresses<'a, A>(&mut self, addresses: A) -> Vec<AddressIdentity<'_>>
where
A: Iterator<Item = (&'a Address, Option<&'a [u8]>)> + Clone;
A: Iterator<Item = (&'a Address, Option<&'a [u8]>, Option<&'a [u8]>)> + Clone;
}

/// A collection of trace identifiers.
Expand All @@ -55,7 +55,7 @@ impl Default for TraceIdentifiers<'_> {
impl TraceIdentifier for TraceIdentifiers<'_> {
fn identify_addresses<'a, A>(&mut self, addresses: A) -> Vec<AddressIdentity<'_>>
where
A: Iterator<Item = (&'a Address, Option<&'a [u8]>)> + Clone,
A: Iterator<Item = (&'a Address, Option<&'a [u8]>, Option<&'a [u8]>)> + Clone,
{
let mut identities = Vec::new();
if let Some(local) = &mut self.local {
Expand Down
99 changes: 99 additions & 0 deletions crates/forge/tests/cli/test_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2274,3 +2274,102 @@ Logs:
...
"#]]);
});

// <https://github.com/foundry-rs/foundry/issues/8995>
forgetest_init!(metadata_bytecode_traces, |prj, cmd| {
prj.add_source(
"ParentProxy.sol",
r#"
import {Counter} from "./Counter.sol";

abstract contract ParentProxy {
Counter impl;
bytes data;

constructor(Counter _implementation, bytes memory _data) {
impl = _implementation;
data = _data;
}
}
"#,
)
.unwrap();
prj.add_source(
"Proxy.sol",
r#"
import {ParentProxy} from "./ParentProxy.sol";
import {Counter} from "./Counter.sol";

contract Proxy is ParentProxy {
constructor(Counter _implementation, bytes memory _data)
ParentProxy(_implementation, _data)
{}
}
"#,
)
.unwrap();

prj.add_test(
"MetadataTraceTest.t.sol",
r#"
import {Counter} from "src/Counter.sol";
import {Proxy} from "src/Proxy.sol";

import {Test} from "forge-std/Test.sol";

contract MetadataTraceTest is Test {
function test_proxy_trace() public {
Counter counter = new Counter();
new Proxy(counter, "");
}
}
"#,
)
.unwrap();

cmd.args(["test", "--mt", "test_proxy_trace", "-vvvv"]).assert_success().stdout_eq(str![[r#"
[COMPILING_FILES] with [SOLC_VERSION]
[SOLC_VERSION] [ELAPSED]
Compiler run successful!

Ran 1 test for test/MetadataTraceTest.t.sol:MetadataTraceTest
[PASS] test_proxy_trace() ([GAS])
Traces:
[152142] MetadataTraceTest::test_proxy_trace()
├─ [49499] → new Counter@0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f
│ └─ ← [Return] 247 bytes of code
├─ [37978] → new Proxy@0x2e234DAe75C793f67A35089C9d99245E1C58470b
│ └─ ← [Return] 63 bytes of code
└─ ← [Stop]

Suite result: ok. 1 passed; 0 failed; 0 skipped; [ELAPSED]

Ran 1 test suite [ELAPSED]: 1 tests passed, 0 failed, 0 skipped (1 total tests)

"#]]);

// Check consistent traces for running with no metadata.
cmd.forge_fuse()
.args(["test", "--mt", "test_proxy_trace", "-vvvv", "--no-metadata"])
.assert_success()
.stdout_eq(str![[r#"
[COMPILING_FILES] with [SOLC_VERSION]
[SOLC_VERSION] [ELAPSED]
Compiler run successful!

Ran 1 test for test/MetadataTraceTest.t.sol:MetadataTraceTest
[PASS] test_proxy_trace() ([GAS])
Traces:
[130521] MetadataTraceTest::test_proxy_trace()
├─ [38693] → new Counter@0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f
│ └─ ← [Return] 193 bytes of code
├─ [27175] → new Proxy@0x2e234DAe75C793f67A35089C9d99245E1C58470b
│ └─ ← [Return] 9 bytes of code
└─ ← [Stop]

Suite result: ok. 1 passed; 0 failed; 0 skipped; [ELAPSED]

Ran 1 test suite [ELAPSED]: 1 tests passed, 0 failed, 0 skipped (1 total tests)

"#]]);
});
Loading