Skip to main content

foundry_block_explorers/
contract.rs

1use crate::{
2    Client, EtherscanError, Response, Result,
3    serde_helpers::{deserialize_stringified_bool_or_u64, deserialize_stringified_u64},
4    source_tree::{SourceTree, SourceTreeEntry},
5    utils::{deserialize_address_opt, deserialize_source_code},
6};
7use alloy_json_abi::JsonAbi;
8use alloy_primitives::{Address, B256, Bytes};
9use semver::Version;
10use serde::{Deserialize, Serialize};
11use std::{collections::HashMap, path::Path};
12
13#[cfg(feature = "foundry-compilers")]
14use foundry_compilers::{
15    ProjectBuilder, SolcConfig,
16    artifacts::{EvmVersion, Settings},
17    compilers::solc::SolcCompiler,
18    solc::SolcSettings,
19};
20
21#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)]
22pub enum SourceCodeLanguage {
23    #[default]
24    Solidity,
25    Vyper,
26}
27
28#[derive(Clone, Debug, Serialize, Deserialize)]
29pub struct SourceCodeEntry {
30    pub content: String,
31}
32
33impl<T: Into<String>> From<T> for SourceCodeEntry {
34    fn from(s: T) -> Self {
35        Self { content: s.into() }
36    }
37}
38
39/// The contract metadata's SourceCode field.
40#[derive(Clone, Debug, Serialize, Deserialize)]
41#[serde(untagged)]
42pub enum SourceCodeMetadata {
43    /// Contains just mapped source code.
44    // NOTE: this must come before `Metadata`
45    Sources(HashMap<String, SourceCodeEntry>),
46    /// Contains metadata and path mapped source code.
47    Metadata {
48        /// Programming language of the sources.
49        #[serde(default, skip_serializing_if = "Option::is_none")]
50        language: Option<SourceCodeLanguage>,
51        /// Source path => source code
52        #[serde(default)]
53        sources: HashMap<String, SourceCodeEntry>,
54        /// Compiler settings, None if the language is not Solidity.
55        #[serde(default, skip_serializing_if = "Option::is_none")]
56        settings: Option<serde_json::Value>,
57    },
58    /// Contains only the source code.
59    SourceCode(String),
60}
61
62impl SourceCodeMetadata {
63    pub fn source_code(&self) -> String {
64        match self {
65            Self::Metadata { sources, .. } | Self::Sources(sources) => {
66                sources.values().map(|s| s.content.clone()).collect::<Vec<_>>().join("\n")
67            }
68            Self::SourceCode(s) => s.clone(),
69        }
70    }
71
72    pub fn language(&self) -> Option<SourceCodeLanguage> {
73        match self {
74            Self::Metadata { language, .. } => *language,
75            Self::Sources(_) => None,
76            Self::SourceCode(_) => None,
77        }
78    }
79
80    pub fn sources(&self) -> HashMap<String, SourceCodeEntry> {
81        match self {
82            Self::Metadata { sources, .. } => sources.clone(),
83            Self::Sources(sources) => sources.clone(),
84            Self::SourceCode(s) => HashMap::from([("Contract".into(), s.into())]),
85        }
86    }
87
88    #[cfg(feature = "foundry-compilers")]
89    pub fn settings(&self) -> Result<Option<Settings>> {
90        match self {
91            Self::Metadata { settings, .. } => match settings {
92                Some(value) => {
93                    if value.is_null() {
94                        Ok(None)
95                    } else {
96                        let settings =
97                            serde_json::from_value(value.to_owned()).map_err(|error| {
98                                EtherscanError::Serde { error, content: value.to_string() }
99                            })?;
100                        Ok(Some(settings))
101                    }
102                }
103                None => Ok(None),
104            },
105            Self::Sources(_) => Ok(None),
106            Self::SourceCode(_) => Ok(None),
107        }
108    }
109
110    #[cfg(not(feature = "foundry-compilers"))]
111    pub fn settings(&self) -> Option<&serde_json::Value> {
112        match self {
113            Self::Metadata { settings, .. } => settings.as_ref(),
114            Self::Sources(_) => None,
115            Self::SourceCode(_) => None,
116        }
117    }
118}
119
120/// Etherscan contract metadata.
121#[derive(Clone, Debug, Serialize, Deserialize)]
122#[serde(rename_all = "PascalCase")]
123pub struct Metadata {
124    /// Includes metadata for compiler settings and language.
125    #[serde(deserialize_with = "deserialize_source_code")]
126    pub source_code: SourceCodeMetadata,
127
128    /// The ABI of the contract.
129    #[serde(rename = "ABI")]
130    pub abi: String,
131
132    /// The name of the contract.
133    pub contract_name: String,
134
135    /// The version that this contract was compiled with. If it is a Vyper contract, it will start
136    /// with "vyper:".
137    pub compiler_version: String,
138
139    /// Whether the optimizer was used. This value should only be 0 or 1.
140    #[serde(deserialize_with = "deserialize_stringified_bool_or_u64")]
141    pub optimization_used: u64,
142
143    /// The number of optimizations performed.
144    #[serde(deserialize_with = "deserialize_stringified_u64", alias = "OptimizationRuns", default)]
145    pub runs: u64,
146
147    /// The constructor arguments the contract was deployed with.
148    #[serde(default)]
149    pub constructor_arguments: Bytes,
150
151    /// The version of the EVM the contract was deployed in. Can be either a variant of EvmVersion
152    /// or "Default" which indicates the compiler's default.
153    #[serde(rename = "EVMVersion")]
154    pub evm_version: String,
155
156    // ?
157    #[serde(default)]
158    pub library: String,
159
160    /// The license of the contract.
161    #[serde(default)]
162    pub license_type: String,
163
164    /// Whether this contract is a proxy. This value should only be 0 or 1.
165    #[serde(deserialize_with = "deserialize_stringified_bool_or_u64", alias = "IsProxy")]
166    pub proxy: u64,
167
168    /// If this contract is a proxy, the address of its implementation.
169    #[serde(
170        default,
171        skip_serializing_if = "Option::is_none",
172        deserialize_with = "deserialize_address_opt"
173    )]
174    pub implementation: Option<Address>,
175
176    /// The swarm source of the contract.
177    #[serde(default)]
178    pub swarm_source: String,
179}
180
181impl Metadata {
182    /// Returns the contract's source code.
183    pub fn source_code(&self) -> String {
184        self.source_code.source_code()
185    }
186
187    /// Returns the contract's programming language.
188    pub fn language(&self) -> SourceCodeLanguage {
189        self.source_code.language().unwrap_or_else(|| {
190            if self.is_vyper() { SourceCodeLanguage::Vyper } else { SourceCodeLanguage::Solidity }
191        })
192    }
193
194    /// Returns the contract's path mapped source code.
195    pub fn sources(&self) -> HashMap<String, SourceCodeEntry> {
196        self.source_code.sources()
197    }
198
199    /// Parses the ABI string into a [`JsonAbi`] struct.
200    pub fn abi(&self) -> Result<JsonAbi> {
201        serde_json::from_str(&self.abi)
202            .map_err(|error| EtherscanError::Serde { error, content: self.abi.clone() })
203    }
204
205    /// Parses the compiler version.
206    pub fn compiler_version(&self) -> Result<Version> {
207        let v = &self.compiler_version;
208        let v = v.strip_prefix("vyper:").unwrap_or(v);
209        let v = v.strip_prefix('v').unwrap_or(v);
210        match v.parse() {
211            Err(e) => {
212                let v = v.replace('a', "-alpha.");
213                let v = v.replace('b', "-beta.");
214                v.parse().map_err(|_| EtherscanError::Unknown(format!("bad compiler version: {e}")))
215            }
216            Ok(v) => Ok(v),
217        }
218    }
219
220    /// Returns whether this contract is a Vyper or a Solidity contract.
221    pub fn is_vyper(&self) -> bool {
222        self.compiler_version.starts_with("vyper:")
223    }
224
225    /// Maps this contract's sources to a [SourceTreeEntry] vector.
226    pub fn source_entries(&self) -> Vec<SourceTreeEntry> {
227        let root = Path::new(&self.contract_name);
228        self.sources()
229            .into_iter()
230            .map(|(path, entry)| {
231                // This is relevant because the etherscan [Metadata](crate::contract::Metadata) can
232                // contain absolute paths (supported by standard-json-input). See also: <https://github.com/foundry-rs/foundry/issues/6541>
233                // for example, we want to ensure "/contracts/SimpleToken.sol" is mapped to
234                // `<root_dir>/contracts/SimpleToken.sol`.
235                let sanitized_path = crate::source_tree::sanitize_path(path);
236                let path = root.join(sanitized_path);
237                SourceTreeEntry { path, contents: entry.content }
238            })
239            .collect()
240    }
241
242    /// Returns the source tree of this contract's sources.
243    pub fn source_tree(&self) -> SourceTree {
244        SourceTree { entries: self.source_entries() }
245    }
246
247    /// Returns the contract's compiler settings.
248    #[cfg(feature = "foundry-compilers")]
249    pub fn settings(&self) -> Result<Settings> {
250        let mut settings = self.source_code.settings()?.unwrap_or_default();
251
252        if self.optimization_used == 1 && !settings.optimizer.enabled.unwrap_or_default() {
253            settings.optimizer.enable();
254            settings.optimizer.runs(self.runs as usize);
255        }
256
257        settings.evm_version = self.evm_version()?;
258
259        Ok(settings)
260    }
261
262    /// Creates a Solc [ProjectBuilder] with this contract's settings.
263    #[cfg(feature = "foundry-compilers")]
264    pub fn project_builder(&self) -> Result<ProjectBuilder<SolcCompiler>> {
265        let solc_config = SolcConfig::builder().settings(self.settings()?).build();
266
267        Ok(ProjectBuilder::new(Default::default())
268            .settings(SolcSettings { settings: solc_config, ..Default::default() }))
269    }
270
271    /// Parses the EVM version.
272    #[cfg(feature = "foundry-compilers")]
273    pub fn evm_version(&self) -> Result<Option<EvmVersion>> {
274        match self.evm_version.to_lowercase().as_str() {
275            "" | "default" => Ok(EvmVersion::default_version_solc(&self.compiler_version()?)),
276            _ => {
277                let evm_version = self
278                    .evm_version
279                    .parse()
280                    .map_err(|e| EtherscanError::Unknown(format!("bad evm version: {e}")))?;
281                Ok(Some(evm_version))
282            }
283        }
284    }
285}
286
287#[derive(Clone, Debug, Serialize, Deserialize)]
288#[serde(transparent)]
289pub struct ContractMetadata {
290    pub items: Vec<Metadata>,
291}
292
293impl IntoIterator for ContractMetadata {
294    type Item = Metadata;
295    type IntoIter = std::vec::IntoIter<Metadata>;
296
297    fn into_iter(self) -> Self::IntoIter {
298        self.items.into_iter()
299    }
300}
301
302impl ContractMetadata {
303    /// Returns the ABI of all contracts.
304    pub fn abis(&self) -> Result<Vec<JsonAbi>> {
305        self.items.iter().map(|c| c.abi()).collect()
306    }
307
308    /// Returns the combined source code of all contracts.
309    pub fn source_code(&self) -> String {
310        self.items.iter().map(|c| c.source_code()).collect::<Vec<_>>().join("\n")
311    }
312
313    /// Returns the combined [SourceTree] of all contracts.
314    pub fn source_tree(&self) -> SourceTree {
315        SourceTree { entries: self.items.iter().flat_map(|item| item.source_entries()).collect() }
316    }
317}
318
319/// Contract creation data.
320#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
321#[serde(rename_all = "camelCase")]
322pub struct ContractCreationData {
323    /// The contract's address.
324    pub contract_address: Address,
325
326    /// The contract's deployer address.
327    /// NOTE: This field contains the address of an EOA that initiated the creation transaction.
328    /// For contracts deployed by other contracts, the direct deployer address may vary.
329    pub contract_creator: Address,
330
331    /// The hash of the contract creation transaction.
332    #[serde(rename = "txHash")]
333    pub transaction_hash: B256,
334}
335
336impl Client {
337    /// Fetches a verified contract's ABI.
338    ///
339    /// # Example
340    ///
341    /// ```no_run
342    /// # async fn foo(client: foundry_block_explorers::Client) -> Result<(), Box<dyn std::error::Error>> {
343    /// let address = "0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413".parse()?;
344    /// let abi = client.contract_abi(address).await?;
345    /// # Ok(()) }
346    /// ```
347    pub async fn contract_abi(&self, address: Address) -> Result<JsonAbi> {
348        // apply caching
349        if let Some(ref cache) = self.cache {
350            // If this is None, then we have a cache miss
351            if let Some(src) = cache.get_abi(address) {
352                // If this is None, then the contract is not verified
353                return match src {
354                    Some(src) => Ok(src),
355                    None => Err(EtherscanError::ContractCodeNotVerified(address)),
356                };
357            }
358        }
359        trace!(target: "etherscan", ?address, "GET contract abi");
360
361        let query = self.create_query("contract", "getabi", HashMap::from([("address", address)]));
362
363        let resp: Response<Option<String>> = self.get_json(&query).await.inspect_err(|err| {
364            error!(target: "etherscan", ?err, "Failed to deserialize ABI response");
365        })?;
366
367        let result = match resp.result {
368            Some(result) => result,
369            None => {
370                if resp.message.contains("Contract source code not verified") {
371                    return Err(EtherscanError::ContractCodeNotVerified(address));
372                }
373                return Err(EtherscanError::EmptyResult {
374                    message: resp.message,
375                    status: resp.status,
376                });
377            }
378        };
379
380        if resp.status == "0" && result.to_lowercase().contains("invalid api key") {
381            return Err(EtherscanError::InvalidApiKey);
382        }
383
384        if result.starts_with("Max rate limit reached") {
385            return Err(EtherscanError::RateLimitExceeded);
386        }
387
388        if result.starts_with("Contract source code not verified")
389            || resp.message.starts_with("Contract source code not verified")
390        {
391            if let Some(ref cache) = self.cache {
392                cache.set_abi(address, None);
393            }
394            return Err(EtherscanError::ContractCodeNotVerified(address));
395        }
396        let abi = serde_json::from_str(&result)
397            .map_err(|error| EtherscanError::Serde { error, content: result })?;
398
399        if let Some(ref cache) = self.cache {
400            cache.set_abi(address, Some(&abi));
401        }
402
403        Ok(abi)
404    }
405
406    /// Fetches a contract's verified source code and its metadata.
407    ///
408    /// # Example
409    ///
410    /// ```no_run
411    /// # async fn foo(client: foundry_block_explorers::Client) -> Result<(), Box<dyn std::error::Error>> {
412    /// let address = "0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413".parse()?;
413    /// let metadata = client.contract_source_code(address).await?;
414    /// assert_eq!(metadata.items[0].contract_name, "DAO");
415    /// # Ok(()) }
416    /// ```
417    pub async fn contract_source_code(&self, address: Address) -> Result<ContractMetadata> {
418        // apply caching
419        if let Some(ref cache) = self.cache {
420            // If this is None, then we have a cache miss
421            if let Some(src) = cache.get_source(address) {
422                // If this is None, then the contract is not verified
423                return match src {
424                    Some(src) => Ok(src),
425                    None => Err(EtherscanError::ContractCodeNotVerified(address)),
426                };
427            }
428        }
429
430        let query =
431            self.create_query("contract", "getsourcecode", HashMap::from([("address", address)]));
432        let response = self.get(&query).await?;
433
434        // Source code is not verified
435        if response.contains("Contract source code not verified") {
436            if let Some(ref cache) = self.cache {
437                cache.set_source(address, None);
438            }
439            return Err(EtherscanError::ContractCodeNotVerified(address));
440        }
441        // Some Blockscout-compatible explorers return Address-only payloads for
442        // non-contract addresses. Treat those as "not verified" instead of
443        // surfacing a deserialization error.
444        if is_address_only_source_code_response(&response) {
445            if let Some(ref cache) = self.cache {
446                cache.set_source(address, None);
447            }
448            return Err(EtherscanError::ContractCodeNotVerified(address));
449        }
450
451        let response: Response<ContractMetadata> = self.sanitize_response(response)?;
452        let result = response.result;
453
454        if let Some(ref cache) = self.cache {
455            cache.set_source(address, Some(&result));
456        }
457
458        Ok(result)
459    }
460
461    /// Fetches a contract's creation transaction hash and deployer address.
462    ///
463    /// # Example
464    ///
465    /// ```no_run
466    /// # async fn foo(client: foundry_block_explorers::Client) -> Result<(), Box<dyn std::error::Error>> {
467    /// let address = "0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413".parse()?;
468    /// let creation_data = client.contract_creation_data(address).await?;
469    /// let deployment_tx = creation_data.transaction_hash;
470    /// let deployer = creation_data.contract_creator;
471    /// # Ok(()) }
472    /// ```
473    pub async fn contract_creation_data(&self, address: Address) -> Result<ContractCreationData> {
474        let query = self.create_query(
475            "contract",
476            "getcontractcreation",
477            HashMap::from([("contractaddresses", address)]),
478        );
479
480        let response = self.get(&query).await?;
481
482        // Address is not a contract or contract wasn't indexed yet
483        if response.contains("No data found") {
484            return Err(EtherscanError::ContractNotFound(address));
485        }
486
487        let response: Response<Vec<ContractCreationData>> = self.sanitize_response(response)?;
488
489        // We are expecting the API to return exactly one result.
490        let data = response.result.first().ok_or(EtherscanError::EmptyResult {
491            message: response.message,
492            status: response.status,
493        })?;
494
495        Ok(*data)
496    }
497}
498
499fn is_address_only_source_code_response(response: &str) -> bool {
500    let Ok(value) = serde_json::from_str::<serde_json::Value>(response) else {
501        return false;
502    };
503    let Some(result) = value.get("result").and_then(|v| v.as_array()) else {
504        return false;
505    };
506    !result.is_empty()
507        && result.iter().all(|entry| {
508            let Some(obj) = entry.as_object() else {
509                return false;
510            };
511            obj.len() == 1 && obj.contains_key("Address")
512        })
513}
514
515#[cfg(test)]
516mod tests {
517    use super::is_address_only_source_code_response;
518
519    #[test]
520    fn detects_address_only_result_shape() {
521        let response = r#"{"message":"OK","result":[{"Address":"0x0000000000000000000000000000000000000000"}],"status":"1"}"#;
522        assert!(is_address_only_source_code_response(response));
523    }
524
525    #[test]
526    fn ignores_valid_contract_metadata_shape() {
527        let response = r#"{"message":"OK","result":[{"ABI":"[]","ContractName":"C","CompilerVersion":"v0.8.26+commit.8a97fa7a","OptimizationUsed":"0","OptimizationRuns":"0","SourceCode":"","EVMVersion":"Default","IsProxy":"0","ConstructorArguments":""}],"status":"1"}"#;
528        assert!(!is_address_only_source_code_response(response));
529    }
530}