edb_engine/utils/
compilation.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//! On-chain contract compilation utilities.
18//!
19//! This module provides utilities for compiling smart contracts from verified
20//! source code obtained from blockchain explorers like Etherscan. It handles
21//! the complete compilation workflow including source retrieval, compiler
22//! configuration, and artifact generation.
23//!
24//! # Core Features
25//!
26//! - **Source Retrieval**: Fetch verified source code from Etherscan
27//! - **Compiler Configuration**: Set up Solidity compiler with proper settings
28//! - **Multi-file Compilation**: Handle complex projects with dependencies
29//! - **Library Support**: Manage library dependencies and linking
30//! - **Caching**: Cache compilation results for performance
31//!
32//! # Workflow
33//!
34//! 1. Retrieve contract metadata and source code from Etherscan
35//! 2. Configure Solidity compiler with matching settings
36//! 3. Compile the contract with all dependencies
37//! 4. Generate artifact with metadata and compilation output
38
39use std::path::PathBuf;
40
41use alloy_primitives::Address;
42use edb_common::{Cache, EdbCache};
43use eyre::Result;
44use foundry_block_explorers::{contract::Metadata, errors::EtherscanError, Client};
45use foundry_compilers::{
46    artifacts::{output_selection::OutputSelection, Libraries, SolcInput, Source, Sources},
47    solc::{Solc, SolcLanguage},
48};
49use itertools::Itertools;
50use tracing::{debug, trace};
51
52use crate::{etherscan_rate_limit_guard, Artifact};
53
54/// Onchain compiler.
55#[derive(Debug, Clone)]
56pub struct OnchainCompiler {
57    /// Cache for the compiled contracts.
58    pub cache: Option<EdbCache<Option<Artifact>>>,
59}
60
61impl OnchainCompiler {
62    /// New onchain compiler.
63    pub fn new(cache_root: Option<PathBuf>) -> Result<Self> {
64        Ok(Self {
65            // None for no expiry
66            cache: EdbCache::new(cache_root, None)?,
67        })
68    }
69
70    /// Compile the contract at the given address.
71    /// Returns `Some`` if the contract is successfully compiled.
72    /// Returns `None` if the contract is not verified, is a Vyper contract, or it is a Solidity
73    /// 0.4.x contract which does not support --stand-json option.
74    pub async fn compile(&self, etherscan: &Client, addr: Address) -> Result<Option<Artifact>> {
75        // Get the cache_root. If not provided, use the default cache directory.
76        if let Some(output) = self.cache.load_cache(addr.to_string()) {
77            Ok(output)
78        } else {
79            let mut meta =
80                match etherscan_rate_limit_guard!(etherscan.contract_source_code(addr).await) {
81                    Ok(meta) => meta,
82                    Err(EtherscanError::ContractCodeNotVerified(_)) => {
83                        // We do not cache the fact that the contract is not verified, since it may be
84                        // verified later.
85                        return Ok(None);
86                    }
87                    Err(e) => return Err(e.into()),
88                };
89            eyre::ensure!(meta.items.len() == 1, "contract not found or ill-formed");
90            let meta = meta.items.remove(0);
91
92            if meta.is_vyper() {
93                // We do not cache if the contract is a Vyper contract.
94                return Ok(None);
95            }
96
97            let input = get_compilation_input_from_metadata(&meta, addr)?;
98
99            // prepare the compiler
100            let version = meta.compiler_version()?;
101            let compiler = Solc::find_or_install(&version)?;
102            trace!(addr=?addr, compiler=?compiler, "using compiler");
103
104            // compile the source code
105            let output = match compiler.compile_exact(&input) {
106                Ok(output) => Some(Artifact { meta, input, output }),
107                Err(_) if version.major == 0 && version.minor == 4 => None,
108                Err(e) => {
109                    return Err(eyre::eyre!("failed to compile contract: {}", e));
110                }
111            };
112
113            self.cache.save_cache(addr.to_string(), &output)?;
114            Ok(output)
115        }
116    }
117}
118
119/// Prepare the input for solc using metadate downloaded from Etherscan.
120pub fn get_compilation_input_from_metadata(meta: &Metadata, addr: Address) -> Result<SolcInput> {
121    let mut settings = meta.settings()?;
122
123    // Enforce compiler output all possible outputs
124    settings.output_selection = OutputSelection::complete_output_selection();
125    trace!(addr=?addr, settings=?settings, "using settings");
126
127    // Prepare the sources
128    let sources: Sources =
129        meta.sources().into_iter().map(|(k, v)| (k.into(), Source::new(v.content))).collect();
130
131    // Check library
132    if !meta.library.is_empty() {
133        let prefix = if sources.keys().unique().count() == 1 {
134            sources.keys().next().unwrap().to_string_lossy().to_string()
135        } else {
136            // When multiple source files are present, the library string should include the path.
137            String::new()
138        };
139
140        let libs = meta
141            .library
142            .split(';')
143            .filter_map(|lib| {
144                debug!(lib=?lib, addr=?addr, "parsing library");
145                let mut parts = lib.split(':');
146
147                let file =
148                    if parts.clone().count() == 2 { prefix.as_str() } else { parts.next()? };
149                let name = parts.next()?;
150                let addr = parts.next()?;
151
152                if addr.starts_with("0x") {
153                    Some(format!("{file}:{name}:{addr}"))
154                } else {
155                    Some(format!("{file}:{name}:0x{addr}"))
156                }
157            })
158            .collect::<Vec<_>>();
159
160        settings.libraries = Libraries::parse(&libs)?;
161    }
162
163    let input = SolcInput::new(SolcLanguage::Solidity, sources, settings);
164
165    Ok(input)
166}
167
168#[cfg(test)]
169mod tests {
170    use std::{str::FromStr, time::Duration};
171
172    use alloy_chains::Chain;
173    use serial_test::serial;
174
175    use crate::utils::next_etherscan_api_key;
176
177    use super::*;
178
179    async fn run_compile(chain_id: Chain, addr: &str) -> eyre::Result<Option<Artifact>> {
180        let etherscan_cache_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
181            .join("../../testdata/cache/etherscan")
182            .join(chain_id.to_string());
183        let etherscan = Client::builder()
184            .with_api_key(next_etherscan_api_key())
185            .with_cache(Some(etherscan_cache_root), Duration::from_secs(24 * 60 * 60)) // 24 hours
186            .chain(chain_id)?
187            .build()?;
188
189        // We disable the cache for testing.
190        let compiler = OnchainCompiler::new(None)?;
191        compiler.compile(&etherscan, Address::from_str(addr)?).await
192    }
193
194    #[tokio::test(flavor = "multi_thread")]
195    #[serial]
196    async fn test_tailing_slash() {
197        run_compile(Chain::mainnet(), "0x22F9dCF4647084d6C31b2765F6910cd85C178C18").await.unwrap();
198    }
199}