foundry_block_explorers/
contract.rs

1use crate::{
2    serde_helpers::{deserialize_stringified_bool_or_u64, deserialize_stringified_u64},
3    source_tree::{SourceTree, SourceTreeEntry},
4    utils::{deserialize_address_opt, deserialize_source_code},
5    Client, EtherscanError, Response, Result,
6};
7use alloy_json_abi::JsonAbi;
8use alloy_primitives::{Address, Bytes, B256};
9use semver::Version;
10use serde::{Deserialize, Serialize};
11use std::{collections::HashMap, path::Path};
12
13#[cfg(feature = "foundry-compilers")]
14use foundry_compilers::{
15    artifacts::{EvmVersion, Settings},
16    compilers::solc::SolcCompiler,
17    solc::SolcSettings,
18    ProjectBuilder, SolcConfig,
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() {
191                SourceCodeLanguage::Vyper
192            } else {
193                SourceCodeLanguage::Solidity
194            }
195        })
196    }
197
198    /// Returns the contract's path mapped source code.
199    pub fn sources(&self) -> HashMap<String, SourceCodeEntry> {
200        self.source_code.sources()
201    }
202
203    /// Parses the ABI string into a [`JsonAbi`] struct.
204    pub fn abi(&self) -> Result<JsonAbi> {
205        serde_json::from_str(&self.abi)
206            .map_err(|error| EtherscanError::Serde { error, content: self.abi.clone() })
207    }
208
209    /// Parses the compiler version.
210    pub fn compiler_version(&self) -> Result<Version> {
211        let v = &self.compiler_version;
212        let v = v.strip_prefix("vyper:").unwrap_or(v);
213        let v = v.strip_prefix('v').unwrap_or(v);
214        match v.parse() {
215            Err(e) => {
216                let v = v.replace('a', "-alpha.");
217                let v = v.replace('b', "-beta.");
218                v.parse().map_err(|_| EtherscanError::Unknown(format!("bad compiler version: {e}")))
219            }
220            Ok(v) => Ok(v),
221        }
222    }
223
224    /// Returns whether this contract is a Vyper or a Solidity contract.
225    pub fn is_vyper(&self) -> bool {
226        self.compiler_version.starts_with("vyper:")
227    }
228
229    /// Maps this contract's sources to a [SourceTreeEntry] vector.
230    pub fn source_entries(&self) -> Vec<SourceTreeEntry> {
231        let root = Path::new(&self.contract_name);
232        self.sources()
233            .into_iter()
234            .map(|(path, entry)| {
235                // This is relevant because the etherscan [Metadata](crate::contract::Metadata) can
236                // contain absolute paths (supported by standard-json-input). See also: <https://github.com/foundry-rs/foundry/issues/6541>
237                // for example, we want to ensure "/contracts/SimpleToken.sol" is mapped to
238                // `<root_dir>/contracts/SimpleToken.sol`.
239                let sanitized_path = crate::source_tree::sanitize_path(path);
240                let path = root.join(sanitized_path);
241                SourceTreeEntry { path, contents: entry.content }
242            })
243            .collect()
244    }
245
246    /// Returns the source tree of this contract's sources.
247    pub fn source_tree(&self) -> SourceTree {
248        SourceTree { entries: self.source_entries() }
249    }
250
251    /// Returns the contract's compiler settings.
252    #[cfg(feature = "foundry-compilers")]
253    pub fn settings(&self) -> Result<Settings> {
254        let mut settings = self.source_code.settings()?.unwrap_or_default();
255
256        if self.optimization_used == 1 && !settings.optimizer.enabled.unwrap_or_default() {
257            settings.optimizer.enable();
258            settings.optimizer.runs(self.runs as usize);
259        }
260
261        settings.evm_version = self.evm_version()?;
262
263        Ok(settings)
264    }
265
266    /// Creates a Solc [ProjectBuilder] with this contract's settings.
267    #[cfg(feature = "foundry-compilers")]
268    pub fn project_builder(&self) -> Result<ProjectBuilder<SolcCompiler>> {
269        let solc_config = SolcConfig::builder().settings(self.settings()?).build();
270
271        Ok(ProjectBuilder::new(Default::default())
272            .settings(SolcSettings { settings: solc_config, ..Default::default() }))
273    }
274
275    /// Parses the EVM version.
276    #[cfg(feature = "foundry-compilers")]
277    pub fn evm_version(&self) -> Result<Option<EvmVersion>> {
278        match self.evm_version.to_lowercase().as_str() {
279            "" | "default" => Ok(EvmVersion::default_version_solc(&self.compiler_version()?)),
280            _ => {
281                let evm_version = self
282                    .evm_version
283                    .parse()
284                    .map_err(|e| EtherscanError::Unknown(format!("bad evm version: {e}")))?;
285                Ok(Some(evm_version))
286            }
287        }
288    }
289}
290
291#[derive(Clone, Debug, Serialize, Deserialize)]
292#[serde(transparent)]
293pub struct ContractMetadata {
294    pub items: Vec<Metadata>,
295}
296
297impl IntoIterator for ContractMetadata {
298    type Item = Metadata;
299    type IntoIter = std::vec::IntoIter<Metadata>;
300
301    fn into_iter(self) -> Self::IntoIter {
302        self.items.into_iter()
303    }
304}
305
306impl ContractMetadata {
307    /// Returns the ABI of all contracts.
308    pub fn abis(&self) -> Result<Vec<JsonAbi>> {
309        self.items.iter().map(|c| c.abi()).collect()
310    }
311
312    /// Returns the combined source code of all contracts.
313    pub fn source_code(&self) -> String {
314        self.items.iter().map(|c| c.source_code()).collect::<Vec<_>>().join("\n")
315    }
316
317    /// Returns the combined [SourceTree] of all contracts.
318    pub fn source_tree(&self) -> SourceTree {
319        SourceTree { entries: self.items.iter().flat_map(|item| item.source_entries()).collect() }
320    }
321}
322
323/// Contract creation data.
324#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
325#[serde(rename_all = "camelCase")]
326pub struct ContractCreationData {
327    /// The contract's address.
328    pub contract_address: Address,
329
330    /// The contract's deployer address.
331    /// NOTE: This field contains the address of an EOA that initiated the creation transaction.
332    /// For contracts deployed by other contracts, the direct deployer address may vary.
333    pub contract_creator: Address,
334
335    /// The hash of the contract creation transaction.
336    #[serde(rename = "txHash")]
337    pub transaction_hash: B256,
338}
339
340impl Client {
341    /// Fetches a verified contract's ABI.
342    ///
343    /// # Example
344    ///
345    /// ```no_run
346    /// # async fn foo(client: foundry_block_explorers::Client) -> Result<(), Box<dyn std::error::Error>> {
347    /// let address = "0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413".parse()?;
348    /// let abi = client.contract_abi(address).await?;
349    /// # Ok(()) }
350    /// ```
351    pub async fn contract_abi(&self, address: Address) -> Result<JsonAbi> {
352        // apply caching
353        if let Some(ref cache) = self.cache {
354            // If this is None, then we have a cache miss
355            if let Some(src) = cache.get_abi(address) {
356                // If this is None, then the contract is not verified
357                return match src {
358                    Some(src) => Ok(src),
359                    None => Err(EtherscanError::ContractCodeNotVerified(address)),
360                };
361            }
362        }
363        trace!(target: "etherscan", ?address, "GET contract abi");
364
365        let query = self.create_query("contract", "getabi", HashMap::from([("address", address)]));
366
367        let resp: Response<Option<String>> = self.get_json(&query).await.inspect_err(|err| {
368            error!(target: "etherscan", ?err, "Failed to deserialize ABI response");
369        })?;
370
371        let result = match resp.result {
372            Some(result) => result,
373            None => {
374                if resp.message.contains("Contract source code not verified") {
375                    return Err(EtherscanError::ContractCodeNotVerified(address));
376                }
377                return Err(EtherscanError::EmptyResult {
378                    message: resp.message,
379                    status: resp.status,
380                });
381            }
382        };
383
384        if resp.status == "0" && result.to_lowercase().contains("invalid api key") {
385            return Err(EtherscanError::InvalidApiKey);
386        }
387
388        if result.starts_with("Max rate limit reached") {
389            return Err(EtherscanError::RateLimitExceeded);
390        }
391
392        if result.starts_with("Contract source code not verified")
393            || resp.message.starts_with("Contract source code not verified")
394        {
395            if let Some(ref cache) = self.cache {
396                cache.set_abi(address, None);
397            }
398            return Err(EtherscanError::ContractCodeNotVerified(address));
399        }
400        let abi = serde_json::from_str(&result)
401            .map_err(|error| EtherscanError::Serde { error, content: result })?;
402
403        if let Some(ref cache) = self.cache {
404            cache.set_abi(address, Some(&abi));
405        }
406
407        Ok(abi)
408    }
409
410    /// Fetches a contract's verified source code and its metadata.
411    ///
412    /// # Example
413    ///
414    /// ```no_run
415    /// # async fn foo(client: foundry_block_explorers::Client) -> Result<(), Box<dyn std::error::Error>> {
416    /// let address = "0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413".parse()?;
417    /// let metadata = client.contract_source_code(address).await?;
418    /// assert_eq!(metadata.items[0].contract_name, "DAO");
419    /// # Ok(()) }
420    /// ```
421    pub async fn contract_source_code(&self, address: Address) -> Result<ContractMetadata> {
422        // apply caching
423        if let Some(ref cache) = self.cache {
424            // If this is None, then we have a cache miss
425            if let Some(src) = cache.get_source(address) {
426                // If this is None, then the contract is not verified
427                return match src {
428                    Some(src) => Ok(src),
429                    None => Err(EtherscanError::ContractCodeNotVerified(address)),
430                };
431            }
432        }
433
434        let query =
435            self.create_query("contract", "getsourcecode", HashMap::from([("address", address)]));
436        let response = self.get(&query).await?;
437
438        // Source code is not verified
439        if response.contains("Contract source code not verified") {
440            if let Some(ref cache) = self.cache {
441                cache.set_source(address, None);
442            }
443            return Err(EtherscanError::ContractCodeNotVerified(address));
444        }
445
446        let response: Response<ContractMetadata> = self.sanitize_response(response)?;
447        let result = response.result;
448
449        if let Some(ref cache) = self.cache {
450            cache.set_source(address, Some(&result));
451        }
452
453        Ok(result)
454    }
455
456    /// Fetches a contract's creation transaction hash and deployer address.
457    ///
458    /// # Example
459    ///
460    /// ```no_run
461    /// # async fn foo(client: foundry_block_explorers::Client) -> Result<(), Box<dyn std::error::Error>> {
462    /// let address = "0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413".parse()?;
463    /// let creation_data = client.contract_creation_data(address).await?;
464    /// let deployment_tx = creation_data.transaction_hash;
465    /// let deployer = creation_data.contract_creator;
466    /// # Ok(()) }
467    /// ```
468    pub async fn contract_creation_data(&self, address: Address) -> Result<ContractCreationData> {
469        let query = self.create_query(
470            "contract",
471            "getcontractcreation",
472            HashMap::from([("contractaddresses", address)]),
473        );
474
475        let response = self.get(&query).await?;
476
477        // Address is not a contract or contract wasn't indexed yet
478        if response.contains("No data found") {
479            return Err(EtherscanError::ContractNotFound(address));
480        }
481
482        let response: Response<Vec<ContractCreationData>> = self.sanitize_response(response)?;
483
484        // We are expecting the API to return exactly one result.
485        let data = response.result.first().ok_or(EtherscanError::EmptyResult {
486            message: response.message,
487            status: response.status,
488        })?;
489
490        Ok(*data)
491    }
492}