use alloy_dyn_abi::JsonAbiExt;
use alloy_primitives::{Address, Bytes, U256};
use edb_common::EdbContext;
use eyre::Result;
use foundry_compilers::{artifacts::Contract, Artifact as _};
use itertools::Itertools;
use revm::{
bytecode::OpCode,
context::{CreateScheme, JournalTr},
database::CacheDB,
interpreter::{CreateInputs, CreateOutcome},
Database, DatabaseCommit, DatabaseRef, Inspector,
};
use tracing::{debug, error, info, warn};
use crate::utils::disasm::{disassemble, extract_push_value};
static CONSTRUCTOR_ARG_SEARCH_RANGE: usize = 1024;
#[derive(Debug)]
pub struct TweakInspector<'a> {
target_address: Address,
contract: &'a Contract,
recompiled_contract: &'a Contract,
constructor_args: &'a Bytes,
deployed_code: Option<Bytes>,
found_target: bool,
}
impl<'a> TweakInspector<'a> {
pub fn new(
target_address: Address,
contract: &'a Contract,
recompiled_contract: &'a Contract,
constructor_args: &'a Bytes,
) -> Self {
Self {
target_address,
contract,
recompiled_contract,
constructor_args,
deployed_code: None,
found_target: false,
}
}
pub fn deployed_code(&self) -> Option<&Bytes> {
self.deployed_code.as_ref()
}
pub fn found_target(&self) -> bool {
self.found_target
}
pub fn into_deployed_code(self) -> Result<Bytes> {
self.deployed_code.ok_or(eyre::eyre!("No deployed code found"))
}
fn extract_constructor_args(&self, init_code: &Bytes) -> Option<Bytes> {
if self
.contract
.abi
.as_ref()
.and_then(|abi| abi.constructor.as_ref())
.map(|c| c.inputs.is_empty())
.unwrap_or(true)
{
debug!("No constructor args needed, using bytes from Etherscan");
return Some(self.constructor_args.clone());
}
if !self.constructor_args.is_empty() {
debug!("Using constructor args from Etherscan: {} bytes", self.constructor_args.len());
return Some(self.constructor_args.clone());
}
let original_creation_code = self.contract.get_bytecode_bytes()?;
if init_code.len() >= original_creation_code.len() {
let prefix = &init_code[..original_creation_code.len()];
if prefix == original_creation_code.as_ref() {
let constructor_args = &init_code[original_creation_code.len()..];
debug!(
"Original init code is exact prefix, extracted constructor args: {} bytes",
constructor_args.len()
);
return Some(Bytes::from(constructor_args.to_vec()));
}
}
if init_code.len() > original_creation_code.len() {
let constructor_args = &init_code[original_creation_code.len()..];
debug!(
"Using heuristic extraction for constructor args: {} bytes",
constructor_args.len()
);
let k = original_creation_code.len();
for i in (0..=CONSTRUCTOR_ARG_SEARCH_RANGE)
.flat_map(move |d| {
[k.saturating_add(d).min(init_code.len() - 1), k.saturating_sub(d)]
})
.unique()
{
if self.can_use_as_constructor_args(&init_code[i..]) {
debug!(
"Successfully extracted constructor args: {} bytes",
init_code[i..].len()
);
return Some(Bytes::from(init_code[i..].to_vec()));
}
}
}
if let Some(constructor_args) = self.extract_constructor_args_with_k_pattern(init_code) {
debug!("Extracted constructor args using K pattern: {} bytes", constructor_args.len());
return Some(constructor_args);
}
warn!("Could not extract constructor args");
None
}
fn can_use_as_constructor_args(&self, data: &[u8]) -> bool {
let Some(constructor) = self.contract.abi.as_ref().and_then(|abi| abi.constructor.as_ref())
else {
return false;
};
if let Ok(decoded) = constructor.abi_decode_input(data) {
constructor
.abi_encode_input(&decoded)
.ok()
.map(|encoded| encoded == data)
.unwrap_or(false)
} else {
false
}
}
fn extract_constructor_args_with_k_pattern(&self, init_code: &Bytes) -> Option<Bytes> {
let disasm = disassemble(init_code);
let mut k_candidates = Vec::new();
for (i, inst) in disasm.instructions.iter().enumerate() {
if !inst.is_push() {
continue;
}
let Some(k_value) = extract_push_value(inst) else { continue };
if k_value >= U256::from(init_code.len()) {
continue;
}
let Some(codesize_inst) = disasm.instructions.get(i + 1) else { continue };
if codesize_inst.opcode != OpCode::CODESIZE {
continue;
}
let Some(sub_inst) = disasm.instructions.get(i + 2) else { continue };
if sub_inst.opcode != OpCode::SUB {
continue;
}
for j in (i + 3)..(i + CONSTRUCTOR_ARG_SEARCH_RANGE) {
if disasm.instructions[j].opcode != OpCode::CODECOPY {
continue;
}
if let Some(push_before_codecopy) = disasm.instructions.get(j - 2) {
if push_before_codecopy.is_push() {
let Some(k2) = extract_push_value(push_before_codecopy) else { continue };
if k2 == k_value {
k_candidates.push(k_value);
debug!("Found confirmed K value with full pattern: {}", k_value);
break;
}
}
}
if let Some(push_before_codecopy) = disasm.instructions.get(j - 1) {
if push_before_codecopy.is_push() {
let Some(k2) = extract_push_value(push_before_codecopy) else { continue };
if k2 == k_value {
k_candidates.push(k_value);
debug!("Found confirmed K value with full pattern: {}", k_value);
break;
}
}
}
}
}
if k_candidates.is_empty() {
debug!("No K candidates found with complete pattern in init code");
return None;
}
for k_value in k_candidates {
let Ok(k) = TryInto::<usize>::try_into(k_value) else { continue };
debug!("Using confirmed K value: {}", k);
if k >= init_code.len() {
continue;
}
let tail = &init_code[k..];
let Some(deployed) = self
.contract
.evm
.as_ref()
.and_then(|e| e.deployed_bytecode.as_ref())
.and_then(|d| d.bytes())
else {
continue;
};
let runtime_len = deployed.len();
if tail.len() > runtime_len {
let constructor_args = &tail[runtime_len..];
if self.can_use_as_constructor_args(constructor_args) {
return Some(Bytes::from(constructor_args.to_vec()));
}
}
}
warn!("Could not determine runtime/constructor args split in tail");
None
}
fn get_full_init_code(&self, init_code: &Bytes) -> Option<Bytes> {
let constructor_args =
self.extract_constructor_args(init_code).unwrap_or(self.constructor_args.clone());
let Some(recompiled_creation_code) = self.recompiled_contract.get_bytecode_bytes() else {
error!("Failed to get recompiled creation code for {}", self.target_address);
return None;
};
let mut full_code = recompiled_creation_code.to_vec();
full_code.extend_from_slice(&constructor_args);
debug!(
"Created full init code: {} bytes (init: {}, args: {})",
full_code.len(),
recompiled_creation_code.len(),
constructor_args.len()
);
Some(Bytes::from(full_code))
}
}
impl<DB> Inspector<EdbContext<DB>> for TweakInspector<'_>
where
DB: Database + DatabaseCommit + DatabaseRef + Clone,
<CacheDB<DB> as Database>::Error: Clone,
<DB as Database>::Error: Clone,
{
fn create(
&mut self,
context: &mut EdbContext<DB>,
inputs: &mut CreateInputs,
) -> Option<CreateOutcome> {
let account = context.journaled_state.load_account(inputs.caller).ok()?;
let nonce = account.info.nonce;
let predicted_address = inputs.created_address(nonce);
debug!(
"CREATE intercepted: deployer={:?}, predicted={:?}, target={:?}",
inputs.caller, predicted_address, self.target_address
);
if predicted_address == self.target_address {
info!(
"Found target deployment! Replacing init code for address {:?}",
self.target_address
);
self.found_target = true;
inputs.init_code = self.get_full_init_code(&inputs.init_code).unwrap_or_default();
inputs.scheme = CreateScheme::Custom { address: self.target_address };
}
None
}
fn create_end(
&mut self,
_context: &mut EdbContext<DB>,
inputs: &CreateInputs,
outcome: &mut CreateOutcome,
) {
if self.found_target
&& matches!(inputs.scheme, CreateScheme::Custom { address } if address == self.target_address)
{
if outcome.result.is_ok() {
if let Some(created_address) = outcome.address {
if created_address == self.target_address {
self.deployed_code = Some(outcome.result.output.clone());
info!(
"Successfully captured deployed bytecode for {:?}: {} bytes",
self.target_address,
outcome.result.output.len()
);
}
}
} else {
info!(
"Target deployment failed for {:?}: {:?}",
self.target_address, outcome.result
);
}
}
}
}