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}