edb_engine/utils/
compilation.rs1use 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#[derive(Debug, Clone)]
56pub struct OnchainCompiler {
57 pub cache: Option<EdbCache<Option<Artifact>>>,
59}
60
61impl OnchainCompiler {
62 pub fn new(cache_root: Option<PathBuf>) -> Result<Self> {
64 Ok(Self {
65 cache: EdbCache::new(cache_root, None)?,
67 })
68 }
69
70 pub async fn compile(&self, etherscan: &Client, addr: Address) -> Result<Option<Artifact>> {
75 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 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 return Ok(None);
95 }
96
97 let input = get_compilation_input_from_metadata(&meta, addr)?;
98
99 let version = meta.compiler_version()?;
101 let compiler = Solc::find_or_install(&version)?;
102 trace!(addr=?addr, compiler=?compiler, "using compiler");
103
104 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
119pub fn get_compilation_input_from_metadata(meta: &Metadata, addr: Address) -> Result<SolcInput> {
121 let mut settings = meta.settings()?;
122
123 settings.output_selection = OutputSelection::complete_output_selection();
125 trace!(addr=?addr, settings=?settings, "using settings");
126
127 let sources: Sources =
129 meta.sources().into_iter().map(|(k, v)| (k.into(), Source::new(v.content))).collect();
130
131 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 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)) .chain(chain_id)?
187 .build()?;
188
189 let compiler = OnchainCompiler::new(None)?;
191 compiler.compile(ðerscan, 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}