Skip to content

Commit 2c806ee

Browse files
committed
cast: Strip placeholder from bytecode-object for source-map and matching
If a contract contains some libraries, and thus has an "unlinked" bytecode object, it will never be matched against a deployed instance, and the source map will never be set. This fixes this issue by striping from the unlinked bytecode all placeholders, replacing them with the `0x00..00` address. It doesn't change anything regarding source-maps, but could change the matching of the runtime bytecode. The changes are usually minimal in this case, though.
1 parent 90e89c0 commit 2c806ee

File tree

6 files changed

+44
-9
lines changed

6 files changed

+44
-9
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/common/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ itertools.workspace = true
5757
jiff.workspace = true
5858
num-format.workspace = true
5959
path-slash.workspace = true
60+
regex.workspace = true
6061
reqwest.workspace = true
6162
semver.workspace = true
6263
serde = { workspace = true, features = ["derive"] }

crates/common/src/contracts.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! Commonly used contract types and functions.
22
3-
use crate::compile::PathOrContractInfo;
3+
use crate::{compile::PathOrContractInfo, strip_bytecode_placeholders};
44
use alloy_dyn_abi::JsonAbiExt;
55
use alloy_json_abi::{Event, Function, JsonAbi};
66
use alloy_primitives::{hex, Address, Bytes, Selector, B256};
@@ -87,6 +87,16 @@ impl ContractData {
8787
pub fn deployed_bytecode(&self) -> Option<&Bytes> {
8888
self.deployed_bytecode.as_ref()?.bytes().filter(|b| !b.is_empty())
8989
}
90+
91+
/// Returns the bytecode without placeholders, if present.
92+
pub fn bytecode_without_placeholders(&self) -> Option<Bytes> {
93+
strip_bytecode_placeholders(self.bytecode.as_ref()?.object.as_ref()?)
94+
}
95+
96+
/// Returns the deployed bytecode without placeholders, if present.
97+
pub fn deployed_bytecode_without_placeholders(&self) -> Option<Bytes> {
98+
strip_bytecode_placeholders(self.deployed_bytecode.as_ref()?.object.as_ref()?)
99+
}
90100
}
91101

92102
type ArtifactWithContractRef<'a> = (&'a ArtifactId, &'a ContractData);

crates/common/src/utils.rs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
//! Uncategorised utilities.
22
3-
use alloy_primitives::{keccak256, B256, U256};
3+
use alloy_primitives::{hex, keccak256, Bytes, B256, U256};
4+
use foundry_compilers::artifacts::BytecodeObject;
5+
use regex::Regex;
6+
use std::sync::LazyLock;
7+
8+
static BYTECODE_PLACEHOLDER_RE: LazyLock<Regex> =
9+
LazyLock::new(|| Regex::new(r"__\$.{34}\$__").expect("invalid regex"));
10+
411
/// Block on a future using the current tokio runtime on the current thread.
512
pub fn block_on<F: std::future::Future>(future: F) -> F::Output {
613
block_on_handle(&tokio::runtime::Handle::current(), future)
@@ -54,3 +61,19 @@ pub fn ignore_metadata_hash(bytecode: &[u8]) -> &[u8] {
5461
bytecode
5562
}
5663
}
64+
65+
/// Strips all __$xxx$__ placeholders from the bytecode if it's an unlinked bytecode.
66+
/// by replacing them with 20 zero bytes.
67+
/// This is useful for matching bytecodes to a contract source, and for the source map,
68+
/// in which the actual address of the placeholder isn't important.
69+
pub fn strip_bytecode_placeholders(bytecode: &BytecodeObject) -> Option<Bytes> {
70+
match &bytecode {
71+
BytecodeObject::Bytecode(bytes) => Some(bytes.clone()),
72+
BytecodeObject::Unlinked(s) => {
73+
// Replace all __$xxx$__ placeholders with 32 zero bytes
74+
let s = (*BYTECODE_PLACEHOLDER_RE).replace_all(s, "00".repeat(40));
75+
let bytes = hex::decode(s.as_bytes());
76+
Some(bytes.ok()?.into())
77+
}
78+
}
79+
}

crates/evm/traces/src/debug/sources.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use eyre::{Context, Result};
2-
use foundry_common::compact_to_contract;
2+
use foundry_common::{compact_to_contract, strip_bytecode_placeholders};
33
use foundry_compilers::{
44
artifacts::{
55
sourcemap::{SourceElement, SourceMap},
@@ -94,9 +94,9 @@ impl ArtifactData {
9494
})
9595
};
9696

97-
// Only parse bytecode if it's not empty.
98-
let pc_ic_map = if let Some(bytes) = b.bytes() {
99-
(!bytes.is_empty()).then(|| PcIcMap::new(bytes))
97+
// Only parse bytecode if it's not empty, stripping placeholders if necessary.
98+
let pc_ic_map = if let Some(bytes) = strip_bytecode_placeholders(&b.object) {
99+
(!bytes.is_empty()).then(|| PcIcMap::new(bytes.as_ref()))
100100
} else {
101101
None
102102
};

crates/evm/traces/src/identifier/local.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,9 @@ impl<'a> LocalTraceIdentifier<'a> {
5656
let contract = self.known_contracts.get(id)?;
5757
// Select bytecodes to compare based on `is_creation` flag.
5858
let (contract_bytecode, current_bytecode) = if is_creation {
59-
(contract.bytecode(), creation_code)
59+
(contract.bytecode_without_placeholders(), creation_code)
6060
} else {
61-
(contract.deployed_bytecode(), runtime_code)
61+
(contract.deployed_bytecode_without_placeholders(), runtime_code)
6262
};
6363

6464
if let Some(bytecode) = contract_bytecode {
@@ -75,7 +75,7 @@ impl<'a> LocalTraceIdentifier<'a> {
7575
}
7676
}
7777

78-
let score = bytecode_diff_score(bytecode, current_bytecode);
78+
let score = bytecode_diff_score(&bytecode, current_bytecode);
7979
if score == 0.0 {
8080
trace!(target: "evm::traces::local", "found exact match");
8181
return Some((id, &contract.abi));

0 commit comments

Comments
 (0)