edb_engine/tweak.rs
1// EDB - Ethereum Debugger
2// Copyright (C) 2024 Zhuo Zhang and Wuqi Zhang
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU Affero General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU Affero General Public License for more details.
13//
14// You should have received a copy of the GNU Affero General Public License
15// along with this program. If not, see <https://www.gnu.org/licenses/>.
16
17//! Contract bytecode modification for debugging through creation transaction replay.
18//!
19//! This module provides the [`CodeTweaker`] utility for modifying deployed contract bytecode
20//! by replaying their creation transactions with replacement init code. This enables debugging
21//! with instrumented or modified contracts without requiring redeployment to the network.
22//!
23//! # Core Functionality
24//!
25//! ## Contract Bytecode Replacement
26//! The [`CodeTweaker`] handles the complete process of:
27//! 1. **Creation Transaction Discovery**: Finding the original deployment transaction
28//! 2. **Transaction Replay**: Re-executing the creation with modified init code
29//! 3. **Bytecode Extraction**: Capturing the resulting runtime bytecode
30//! 4. **State Update**: Replacing the deployed bytecode in the debugging database
31//!
32//! ## Etherscan Integration
33//! - **Creation Data Caching**: Local caching of contract creation transaction data
34//! - **API Key Management**: Automatic API key rotation for rate limit handling
35//! - **Chain Support**: Multi-chain support through configurable Etherscan endpoints
36//!
37//! # Workflow Integration
38//!
39//! The code tweaking process is typically used in the debugging workflow to:
40//! 1. Replace original contracts with instrumented versions for hook-based debugging
41//! 2. Substitute contracts with modified versions for testing different scenarios
42//! 3. Enable debugging of contracts that weren't originally compiled with debug information
43//!
44//! # Usage Example
45//!
46//! ```rust,ignore
47//! let mut tweaker = CodeTweaker::new(&mut edb_context, rpc_url, etherscan_api_key);
48//! tweaker.tweak(&contract_address, &original_artifact, &instrumented_artifact, false).await?;
49//! ```
50//!
51//! This replaces the deployed bytecode at `contract_address` with the instrumented version,
52//! enabling advanced debugging features on the modified contract.
53
54use std::path::PathBuf;
55
56use alloy_primitives::{Address, Bytes, TxHash};
57use edb_common::{
58 fork_and_prepare, relax_evm_constraints, Cache, CachePath, EdbCache, EdbCachePath, EdbContext,
59 ForkResult,
60};
61use eyre::Result;
62use foundry_block_explorers::{contract::ContractCreationData, Client};
63use revm::{
64 context::{Cfg, ContextTr},
65 database::CacheDB,
66 primitives::KECCAK_EMPTY,
67 state::Bytecode,
68 Database, DatabaseCommit, DatabaseRef, InspectEvm, MainBuilder,
69};
70use tracing::{debug, error};
71
72use crate::{next_etherscan_api_key, Artifact, TweakInspector};
73
74/// Utility for modifying deployed contract bytecode through creation transaction replay.
75///
76/// The [`CodeTweaker`] enables replacing deployed contract bytecode by:
77/// 1. Finding the original contract creation transaction
78/// 2. Replaying that transaction with modified init code from recompiled artifacts
79/// 3. Extracting the resulting runtime bytecode
80/// 4. Updating the contract's bytecode in the debugging database
81///
82/// This allows debugging with instrumented contracts without requiring network redeployment.
83pub struct CodeTweaker<'a, DB>
84where
85 DB: Database + DatabaseCommit + DatabaseRef + Clone,
86 <CacheDB<DB> as Database>::Error: Clone,
87 <DB as Database>::Error: Clone,
88{
89 ctx: &'a mut EdbContext<DB>,
90 rpc_url: String,
91 etherscan_api_key: Option<String>,
92}
93
94impl<'a, DB> CodeTweaker<'a, DB>
95where
96 DB: Database + DatabaseCommit + DatabaseRef + Clone,
97 <CacheDB<DB> as Database>::Error: Clone,
98 <DB as Database>::Error: Clone,
99{
100 /// Creates a new `CodeTweaker` instance.
101 ///
102 /// # Arguments
103 ///
104 /// * `ctx` - Mutable reference to the EDB context containing the database
105 /// * `rpc_url` - RPC endpoint URL for fetching blockchain data
106 /// * `etherscan_api_key` - Optional Etherscan API key for fetching contract creation data
107 pub fn new(
108 ctx: &'a mut EdbContext<DB>,
109 rpc_url: String,
110 etherscan_api_key: Option<String>,
111 ) -> Self {
112 Self { ctx, rpc_url, etherscan_api_key }
113 }
114
115 /// Replaces deployed contract bytecode with instrumented bytecode from artifacts.
116 ///
117 /// This method performs the complete bytecode replacement workflow:
118 /// 1. Finds the contract creation transaction using Etherscan API
119 /// 2. Replays the transaction with the recompiled artifact's init code
120 /// 3. Extracts the resulting runtime bytecode
121 /// 4. Updates the contract's bytecode in the debugging database
122 ///
123 /// # Arguments
124 ///
125 /// * `addr` - Address of the deployed contract to modify
126 /// * `artifact` - Original compiled artifact for constructor argument extraction
127 /// * `recompiled_artifact` - Recompiled artifact containing the replacement init code
128 /// * `quick` - Whether to use quick mode (faster but potentially less accurate)
129 ///
130 /// # Returns
131 ///
132 /// Returns `Ok(())` if the bytecode replacement succeeds, or an error if any step fails.
133 pub async fn tweak(
134 &mut self,
135 addr: &Address,
136 artifact: &Artifact,
137 recompiled_artifact: &Artifact,
138 quick: bool,
139 ) -> Result<()> {
140 let tweaked_code =
141 self.get_tweaked_code(addr, artifact, recompiled_artifact, quick).await?;
142 if tweaked_code.is_empty() {
143 error!(addr=?addr, quick=?quick, "Tweaked code is empty");
144 }
145
146 let db = self.ctx.db_mut();
147
148 let mut info = db
149 .basic(*addr)
150 .map_err(|e| eyre::eyre!("Failed to get account info for {}: {}", addr, e))?
151 .unwrap_or_default();
152 // Code hash will be update within `db.insert_account_info(&mut info);`
153 info.code_hash = KECCAK_EMPTY;
154 info.code = Some(Bytecode::new_raw(tweaked_code));
155 db.insert_account_info(*addr, info);
156
157 Ok(())
158 }
159
160 /// Generate the tweaked runtime bytecode by replaying the creation transaction.
161 ///
162 /// This internal method handles the complex process of:
163 /// 1. Forking the blockchain state at the creation transaction
164 /// 2. Setting up the replay environment with modified constraints
165 /// 3. Using the TweakInspector to intercept and modify the deployment
166 /// 4. Extracting the resulting runtime bytecode
167 async fn get_tweaked_code(
168 &self,
169 addr: &Address,
170 artifact: &Artifact,
171 recompiled_artifact: &Artifact,
172 quick: bool,
173 ) -> Result<Bytes> {
174 let creation_tx_hash = self.get_creation_tx(addr).await?;
175 debug!("Creation tx: {} -> {}", creation_tx_hash, addr);
176
177 // Create replay environment
178 let ForkResult { context: mut replay_ctx, target_tx_env: mut creation_tx_env, .. } =
179 fork_and_prepare(&self.rpc_url, creation_tx_hash, quick).await?;
180 relax_evm_constraints(&mut replay_ctx, &mut creation_tx_env);
181
182 // Get init code
183 let contract = artifact.contract().ok_or(eyre::eyre!("Failed to get contract"))?;
184
185 let recompiled_contract =
186 recompiled_artifact.contract().ok_or(eyre::eyre!("Failed to get contract"))?;
187
188 let constructor_args = recompiled_artifact.constructor_arguments();
189
190 let mut inspector =
191 TweakInspector::new(*addr, contract, recompiled_contract, constructor_args);
192
193 let mut evm = replay_ctx.build_mainnet_with_inspector(&mut inspector);
194
195 evm.inspect_one_tx(creation_tx_env)
196 .map_err(|e| eyre::eyre!("Failed to inspect the target transaction: {:?}", e))?;
197
198 inspector.into_deployed_code()
199 }
200
201 /// Retrieves the transaction hash that created a contract at the given address.
202 ///
203 /// This method first checks the local cache for the creation transaction data.
204 /// If not cached, it queries Etherscan API and caches the result for future use.
205 ///
206 /// # Arguments
207 ///
208 /// * `addr` - Address of the deployed contract
209 ///
210 /// # Returns
211 ///
212 /// Returns the transaction hash that deployed the contract, or an error if not found.
213 pub async fn get_creation_tx(&self, addr: &Address) -> Result<TxHash> {
214 let chain_id = self.ctx.cfg().chain_id();
215
216 // Cache directory
217 let etherscan_cache_dir =
218 EdbCachePath::new(None as Option<PathBuf>).etherscan_chain_cache_dir(chain_id);
219
220 let cache = EdbCache::<ContractCreationData>::new(etherscan_cache_dir, None)?;
221 let label = format!("contract_creation_{addr}");
222
223 if let Some(creation_data) = cache.load_cache(&label) {
224 Ok(creation_data.transaction_hash)
225 } else {
226 let etherscan_api_key =
227 self.etherscan_api_key.clone().unwrap_or(next_etherscan_api_key());
228
229 // Build client
230 let etherscan = Client::builder()
231 .with_api_key(etherscan_api_key)
232 .chain(chain_id.into())?
233 .build()?;
234
235 // Get creation tx
236 let creation_data = etherscan.contract_creation_data(*addr).await?;
237 cache.save_cache(&label, &creation_data)?;
238 Ok(creation_data.transaction_hash)
239 }
240 }
241}