//! Solc artifact types use ethers_core::{abi::Abi, types::Bytes}; use colored::Colorize; use md5::Digest; use semver::Version; use std::{ collections::BTreeMap, fmt, fs, path::{Path, PathBuf}, str::FromStr, }; use crate::{compile::*, error::SolcIoError, remappings::Remapping, utils}; use ethers_core::abi::Address; use serde::{de::Visitor, Deserialize, Deserializer, Serialize, Serializer}; /// An ordered list of files and their source pub type Sources = BTreeMap; pub type Contracts = BTreeMap>; /// Input type `solc` expects #[derive(Clone, Debug, Serialize, Deserialize)] pub struct CompilerInput { pub language: String, pub sources: Sources, pub settings: Settings, } impl CompilerInput { /// Reads all contracts found under the path pub fn new(path: impl AsRef) -> Result { Source::read_all_from(path.as_ref()).map(Self::with_sources) } /// Creates a new Compiler input with default settings and the given sources pub fn with_sources(sources: Sources) -> Self { Self { language: "Solidity".to_string(), sources, settings: Default::default() } } /// Sets the settings for compilation #[must_use] pub fn settings(mut self, settings: Settings) -> Self { self.settings = settings; self } /// Sets the EVM version for compilation #[must_use] pub fn evm_version(mut self, version: EvmVersion) -> Self { self.settings.evm_version = Some(version); self } /// Sets the optimizer runs (default = 200) #[must_use] pub fn optimizer(mut self, runs: usize) -> Self { self.settings.optimizer.runs(runs); self } /// Normalizes the EVM version used in the settings to be up to the latest one /// supported by the provided compiler version. #[must_use] pub fn normalize_evm_version(mut self, version: &Version) -> Self { if let Some(ref mut evm_version) = self.settings.evm_version { self.settings.evm_version = evm_version.normalize_version(version); } self } #[must_use] pub fn with_remappings(mut self, remappings: Vec) -> Self { self.settings.remappings = remappings; self } } impl Default for CompilerInput { fn default() -> Self { Self::with_sources(Default::default()) } } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Settings { #[serde(default, skip_serializing_if = "Vec::is_empty")] pub remappings: Vec, pub optimizer: Optimizer, #[serde(default, skip_serializing_if = "Option::is_none")] pub metadata: Option, /// This field can be used to select desired outputs based /// on file and contract names. /// If this field is omitted, then the compiler loads and does type /// checking, but will not generate any outputs apart from errors. /// The first level key is the file name and the second level key is the /// contract name. An empty contract name is used for outputs that are /// not tied to a contract but to the whole source file like the AST. /// A star as contract name refers to all contracts in the file. /// Similarly, a star as a file name matches all files. /// To select all outputs the compiler can possibly generate, use /// "outputSelection: { "*": { "*": [ "*" ], "": [ "*" ] } }" /// but note that this might slow down the compilation process needlessly. /// /// The available output types are as follows: /// /// File level (needs empty string as contract name): /// ast - AST of all source files /// /// Contract level (needs the contract name or "*"): /// abi - ABI /// devdoc - Developer documentation (natspec) /// userdoc - User documentation (natspec) /// metadata - Metadata /// ir - Yul intermediate representation of the code before optimization /// irOptimized - Intermediate representation after optimization /// storageLayout - Slots, offsets and types of the contract's state /// variables. /// evm.assembly - New assembly format /// evm.legacyAssembly - Old-style assembly format in JSON /// evm.bytecode.functionDebugData - Debugging information at function level /// evm.bytecode.object - Bytecode object /// evm.bytecode.opcodes - Opcodes list /// evm.bytecode.sourceMap - Source mapping (useful for debugging) /// evm.bytecode.linkReferences - Link references (if unlinked object) /// evm.bytecode.generatedSources - Sources generated by the compiler /// evm.deployedBytecode* - Deployed bytecode (has all the options that /// evm.bytecode has) /// evm.deployedBytecode.immutableReferences - Map from AST ids to /// bytecode ranges that reference immutables /// evm.methodIdentifiers - The list of function hashes /// evm.gasEstimates - Function gas estimates /// ewasm.wast - Ewasm in WebAssembly S-expressions format /// ewasm.wasm - Ewasm in WebAssembly binary format /// /// Note that using a using `evm`, `evm.bytecode`, `ewasm`, etc. will select /// every target part of that output. Additionally, `*` can be used as a /// wildcard to request everything. /// /// The default output selection is /// /// ```json /// { /// "*": { /// "*": [ /// "abi", /// "evm.bytecode", /// "evm.deployedBytecode", /// "evm.methodIdentifiers" /// ], /// "": [ /// "ast" /// ] /// } /// } /// ``` #[serde(default)] pub output_selection: BTreeMap>>, #[serde(default, with = "display_from_str_opt", skip_serializing_if = "Option::is_none")] pub evm_version: Option, #[serde(default, skip_serializing_if = "::std::collections::BTreeMap::is_empty")] pub libraries: BTreeMap>, } impl Settings { /// Default output selection for compiler output pub fn default_output_selection() -> BTreeMap>> { let mut output_selection = BTreeMap::default(); let mut output = BTreeMap::default(); output.insert( "*".to_string(), vec![ "abi".to_string(), "evm.bytecode".to_string(), "evm.deployedBytecode".to_string(), "evm.methodIdentifiers".to_string(), ], ); output_selection.insert("*".to_string(), output); output_selection } /// Adds `ast` to output #[must_use] pub fn with_ast(mut self) -> Self { let output = self.output_selection.entry("*".to_string()).or_insert_with(BTreeMap::default); output.insert("".to_string(), vec!["ast".to_string()]); self } } impl Default for Settings { fn default() -> Self { Self { optimizer: Default::default(), metadata: None, output_selection: Self::default_output_selection(), evm_version: Some(EvmVersion::default()), libraries: Default::default(), remappings: Default::default(), } .with_ast() } } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct Optimizer { #[serde(default, skip_serializing_if = "Option::is_none")] pub enabled: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub runs: Option, } impl Optimizer { pub fn runs(&mut self, runs: usize) { self.runs = Some(runs); } pub fn disable(&mut self) { self.enabled.take(); } pub fn enable(&mut self) { self.enabled = Some(true) } } impl Default for Optimizer { fn default() -> Self { Self { enabled: Some(false), runs: Some(200) } } } #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] pub enum EvmVersion { Homestead, TangerineWhistle, SpuriousDragon, Byzantium, Constantinople, Petersburg, Istanbul, Berlin, London, } impl Default for EvmVersion { fn default() -> Self { Self::London } } impl EvmVersion { /// Checks against the given solidity `semver::Version` pub fn normalize_version(self, version: &Version) -> Option { // the EVM version flag was only added at 0.4.21 // we work our way backwards if version >= &CONSTANTINOPLE_SOLC { // If the Solc is at least at london, it supports all EVM versions Some(if version >= &LONDON_SOLC { self // For all other cases, cap at the at-the-time highest possible // fork } else if version >= &BERLIN_SOLC && self >= EvmVersion::Berlin { EvmVersion::Berlin } else if version >= &ISTANBUL_SOLC && self >= EvmVersion::Istanbul { EvmVersion::Istanbul } else if version >= &PETERSBURG_SOLC && self >= EvmVersion::Petersburg { EvmVersion::Petersburg } else if self >= EvmVersion::Constantinople { EvmVersion::Constantinople } else { self }) } else { None } } } impl fmt::Display for EvmVersion { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let string = match self { EvmVersion::Homestead => "homestead", EvmVersion::TangerineWhistle => "tangerineWhistle", EvmVersion::SpuriousDragon => "spuriousDragon", EvmVersion::Constantinople => "constantinople", EvmVersion::Petersburg => "petersburg", EvmVersion::Istanbul => "istanbul", EvmVersion::Berlin => "berlin", EvmVersion::London => "london", EvmVersion::Byzantium => "byzantium", }; write!(f, "{}", string) } } impl FromStr for EvmVersion { type Err = String; fn from_str(s: &str) -> Result { match s { "homestead" => Ok(EvmVersion::Homestead), "tangerineWhistle" => Ok(EvmVersion::TangerineWhistle), "spuriousDragon" => Ok(EvmVersion::SpuriousDragon), "constantinople" => Ok(EvmVersion::Constantinople), "petersburg" => Ok(EvmVersion::Petersburg), "istanbul" => Ok(EvmVersion::Istanbul), "berlin" => Ok(EvmVersion::Berlin), "london" => Ok(EvmVersion::London), "byzantium" => Ok(EvmVersion::Byzantium), s => Err(format!("Unknown evm version: {}", s)), } } } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct SettingsMetadata { #[serde(default, rename = "useLiteralContent", skip_serializing_if = "Option::is_none")] pub use_literal_content: Option, #[serde(default, rename = "bytecodeHash", skip_serializing_if = "Option::is_none")] pub bytecode_hash: Option, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct Metadata { pub compiler: Compiler, pub language: String, pub output: Output, pub settings: Settings, pub sources: MetadataSources, pub version: i64, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct MetadataSources { #[serde(flatten)] pub inner: BTreeMap, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct Compiler { pub version: String, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct Output { pub abi: Vec, pub devdoc: Option, pub userdoc: Option, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct SolcAbi { pub inputs: Vec, #[serde(rename = "stateMutability")] pub state_mutability: Option, #[serde(rename = "type")] pub abi_type: String, pub name: Option, pub outputs: Option>, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct Item { #[serde(rename = "internalType")] pub internal_type: String, pub name: String, #[serde(rename = "type")] pub put_type: String, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct Doc { #[serde(default, skip_serializing_if = "Option::is_none")] pub kind: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub methods: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub version: Option, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)] pub struct Libraries { #[serde(flatten)] pub libs: BTreeMap, } #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] pub struct Source { pub content: String, } impl Source { /// this is a heuristically measured threshold at which we can generally expect a speedup by /// using rayon's `par_iter`, See `Self::read_all_files` pub const NUM_READ_PAR: usize = 8; /// Reads the file content pub fn read(file: impl AsRef) -> Result { let file = file.as_ref(); Ok(Self { content: fs::read_to_string(file).map_err(|err| SolcIoError::new(err, file))? }) } /// Recursively finds all source files under the given dir path and reads them all pub fn read_all_from(dir: impl AsRef) -> Result { Self::read_all_files(utils::source_files(dir)) } /// Reads all source files of the given vec /// /// Depending on the len of the vec it will try to read the files in parallel pub fn read_all_files(files: Vec) -> Result { use rayon::prelude::*; if files.len() < Self::NUM_READ_PAR { Self::read_all(files) } else { files .par_iter() .map(Into::into) .map(|file| Self::read(&file).map(|source| (file, source))) .collect() } } /// Reads all files pub fn read_all(files: I) -> Result where I: IntoIterator, T: Into, { files .into_iter() .map(Into::into) .map(|file| Self::read(&file).map(|source| (file, source))) .collect() } /// Parallelized version of `Self::read_all` that reads all files using a parallel iterator /// /// NOTE: this is only expected to be faster than `Self::read_all` if the given iterator /// contains at least several paths. see also `Self::read_all_files`. pub fn par_read_all(files: I) -> Result where I: IntoIterator, ::IntoIter: Send, T: Into + Send, { use rayon::{iter::ParallelBridge, prelude::ParallelIterator}; files .into_iter() .par_bridge() .map(Into::into) .map(|file| Self::read(&file).map(|source| (file, source))) .collect() } /// Generate a non-cryptographically secure checksum of the file's content pub fn content_hash(&self) -> String { let mut hasher = md5::Md5::new(); hasher.update(&self.content); let result = hasher.finalize(); hex::encode(result) } /// Returns all import statements of the file pub fn parse_imports(&self) -> Vec<&str> { utils::find_import_paths(self.as_ref()) } } #[cfg(feature = "async")] impl Source { /// async version of `Self::read` pub async fn async_read(file: impl AsRef) -> Result { let file = file.as_ref(); Ok(Self { content: tokio::fs::read_to_string(file) .await .map_err(|err| SolcIoError::new(err, file))?, }) } /// Finds all source files under the given dir path and reads them all pub async fn async_read_all_from(dir: impl AsRef) -> Result { Self::async_read_all(utils::source_files(dir.as_ref())).await } /// async version of `Self::read_all` pub async fn async_read_all(files: I) -> Result where I: IntoIterator, T: Into, { futures_util::future::join_all( files .into_iter() .map(Into::into) .map(|file| async { Self::async_read(&file).await.map(|source| (file, source)) }), ) .await .into_iter() .collect() } } impl AsRef for Source { fn as_ref(&self) -> &str { &self.content } } /// Output type `solc` produces #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)] pub struct CompilerOutput { #[serde(default, skip_serializing_if = "Vec::is_empty")] pub errors: Vec, #[serde(default)] pub sources: BTreeMap, #[serde(default)] pub contracts: Contracts, } impl CompilerOutput { /// Whether the output contains an compiler error pub fn has_error(&self) -> bool { self.errors.iter().any(|err| err.severity.is_error()) } pub fn diagnostics<'a>(&'a self, ignored_error_codes: &'a [u64]) -> OutputDiagnostics { OutputDiagnostics { errors: &self.errors, ignored_error_codes } } /// Finds the first contract with the given name pub fn find(&self, contract: impl AsRef) -> Option { let contract_name = contract.as_ref(); self.contracts_iter().find_map(|(name, contract)| { (name == contract_name).then(|| CompactContractRef::from(contract)) }) } /// Finds the first contract with the given name and removes it from the set pub fn remove(&mut self, contract: impl AsRef) -> Option { let contract_name = contract.as_ref(); self.contracts.values_mut().find_map(|c| c.remove(contract_name)) } /// Iterate over all contracts and their names pub fn contracts_iter(&self) -> impl Iterator { self.contracts.values().flatten() } /// Iterate over all contracts and their names pub fn contracts_into_iter(self) -> impl Iterator { self.contracts.into_values().flatten() } /// Given the contract file's path and the contract's name, tries to return the contract's /// bytecode, runtime bytecode, and abi pub fn get(&self, path: &str, contract: &str) -> Option { self.contracts .get(path) .and_then(|contracts| contracts.get(contract)) .map(CompactContractRef::from) } } /// Helper type to implement display for solc errors #[derive(Clone, Debug)] pub struct OutputDiagnostics<'a> { errors: &'a [Error], ignored_error_codes: &'a [u64], } impl<'a> OutputDiagnostics<'a> { /// Returns true if there is at least one error of high severity pub fn has_error(&self) -> bool { self.errors.iter().any(|err| err.severity.is_error()) } } impl<'a> fmt::Display for OutputDiagnostics<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if !self.has_error() { f.write_str("Compiler run successful")?; } for err in self.errors { // Do not log any ignored error codes if let Some(error_code) = err.error_code { if !self.ignored_error_codes.contains(&error_code) { writeln!(f, "\n{}", err)?; } } else { writeln!(f, "\n{}", err)?; } } Ok(()) } } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct Contract { /// The Ethereum Contract Metadata. /// See https://docs.soliditylang.org/en/develop/metadata.html pub abi: Option, #[serde(default, skip_serializing_if = "Option::is_none", with = "json_string_opt")] pub metadata: Option, #[serde(default)] pub userdoc: UserDoc, #[serde(default)] pub devdoc: DevDoc, #[serde(default, skip_serializing_if = "Option::is_none")] pub ir: Option, #[serde(default, rename = "storageLayout", skip_serializing_if = "StorageLayout::is_empty")] pub storage_layout: StorageLayout, /// EVM-related outputs #[serde(default, skip_serializing_if = "Option::is_none")] pub evm: Option, /// Ewasm related outputs #[serde(default, skip_serializing_if = "Option::is_none")] pub ewasm: Option, } /// Minimal representation of a contract's abi with bytecode #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)] pub struct CompactContract { /// The Ethereum Contract ABI. If empty, it is represented as an empty /// array. See https://docs.soliditylang.org/en/develop/abi-spec.html pub abi: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub bin: Option, #[serde(default, rename = "bin-runtime", skip_serializing_if = "Option::is_none")] pub bin_runtime: Option, } impl CompactContract { /// Returns the contents of this type as a single tuple of abi, bytecode and deployed bytecode pub fn into_parts(self) -> (Option, Option, Option) { ( self.abi, self.bin.and_then(|bin| bin.into_bytes()), self.bin_runtime.and_then(|bin| bin.into_bytes()), ) } /// Returns the individual parts of this contract. /// /// If the values are `None`, then `Default` is returned. pub fn into_parts_or_default(self) -> (Abi, Bytes, Bytes) { ( self.abi.unwrap_or_default(), self.bin.and_then(|bin| bin.into_bytes()).unwrap_or_default(), self.bin_runtime.and_then(|bin| bin.into_bytes()).unwrap_or_default(), ) } } impl From for CompactContract { fn from(mut val: serde_json::Value) -> Self { if let Some(map) = val.as_object_mut() { let abi = map.remove("abi").and_then(|val| serde_json::from_value(val).ok()); let bin = map.remove("bin").and_then(|val| serde_json::from_value(val).ok()); let bin_runtime = map.remove("bin-runtime").and_then(|val| serde_json::from_value(val).ok()); Self { abi, bin, bin_runtime } } else { CompactContract::default() } } } impl From for CompactContract { fn from(c: Contract) -> Self { let (bin, bin_runtime) = if let Some(evm) = c.evm { ( evm.bytecode.map(|c| c.object), evm.deployed_bytecode.and_then(|deployed| deployed.bytecode.map(|evm| evm.object)), ) } else { (None, None) }; Self { abi: c.abi, bin, bin_runtime } } } impl<'a> From> for CompactContract { fn from(c: CompactContractRef<'a>) -> Self { Self { abi: c.abi.cloned(), bin: c.bin.cloned(), bin_runtime: c.bin_runtime.cloned() } } } /// Helper type to serialize while borrowing from `Contract` #[derive(Copy, Clone, Debug, Serialize)] pub struct CompactContractRef<'a> { pub abi: Option<&'a Abi>, #[serde(default, skip_serializing_if = "Option::is_none")] pub bin: Option<&'a BytecodeObject>, #[serde(default, rename = "bin-runtime", skip_serializing_if = "Option::is_none")] pub bin_runtime: Option<&'a BytecodeObject>, } impl<'a> CompactContractRef<'a> { /// Clones the referenced values and returns as tuples pub fn into_parts(self) -> (Option, Option, Option) { CompactContract::from(self).into_parts() } /// Returns the individual parts of this contract. /// /// If the values are `None`, then `Default` is returned. pub fn into_parts_or_default(self) -> (Abi, Bytes, Bytes) { CompactContract::from(self).into_parts_or_default() } pub fn bytecode(&self) -> Option<&Bytes> { self.bin.as_ref().and_then(|bin| bin.as_bytes()) } pub fn runtime_bytecode(&self) -> Option<&Bytes> { self.bin_runtime.as_ref().and_then(|bin| bin.as_bytes()) } } impl<'a> From<&'a Contract> for CompactContractRef<'a> { fn from(c: &'a Contract) -> Self { let (bin, bin_runtime) = if let Some(ref evm) = c.evm { ( evm.bytecode.as_ref().map(|c| &c.object), evm.deployed_bytecode .as_ref() .and_then(|deployed| deployed.bytecode.as_ref().map(|evm| &evm.object)), ) } else { (None, None) }; Self { abi: c.abi.as_ref(), bin, bin_runtime } } } #[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq)] pub struct UserDoc { #[serde(default, skip_serializing_if = "Option::is_none")] pub version: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub kind: Option, #[serde(default, skip_serializing_if = "::std::collections::BTreeMap::is_empty")] pub methods: BTreeMap, #[serde(default, skip_serializing_if = "Option::is_none")] pub notice: Option, } #[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq)] pub struct DevDoc { #[serde(default, skip_serializing_if = "Option::is_none")] pub version: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub kind: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub author: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub details: Option, #[serde(default, rename = "custom:experimental", skip_serializing_if = "Option::is_none")] pub custom_experimental: Option, #[serde(default, skip_serializing_if = "::std::collections::BTreeMap::is_empty")] pub methods: BTreeMap, #[serde(default, skip_serializing_if = "Option::is_none")] pub title: Option, } #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub struct Evm { #[serde(default, skip_serializing_if = "Option::is_none")] pub assembly: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub legacy_assembly: Option, pub bytecode: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub deployed_bytecode: Option, /// The list of function hashes #[serde(default, skip_serializing_if = "::std::collections::BTreeMap::is_empty")] pub method_identifiers: BTreeMap, /// Function gas estimates #[serde(default, skip_serializing_if = "Option::is_none")] pub gas_estimates: Option, } #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub struct Bytecode { /// Debugging information at function level #[serde(default, skip_serializing_if = "::std::collections::BTreeMap::is_empty")] pub function_debug_data: BTreeMap, /// The bytecode as a hex string. pub object: BytecodeObject, /// Opcodes list (string) #[serde(default, skip_serializing_if = "Option::is_none")] pub opcodes: Option, /// The source mapping as a string. See the source mapping definition. #[serde(default, skip_serializing_if = "Option::is_none")] pub source_map: Option, /// Array of sources generated by the compiler. Currently only contains a /// single Yul file. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub generated_sources: Vec, /// If given, this is an unlinked object. #[serde(default)] pub link_references: BTreeMap>>, } impl Bytecode { /// Same as `Bytecode::link` but with fully qualified name (`file.sol:Math`) pub fn link_fully_qualified(&mut self, name: impl AsRef, addr: Address) -> bool { if let Some((file, lib)) = name.as_ref().split_once(':') { self.link(file, lib, addr) } else { false } } /// Tries to link the bytecode object with the `file` and `library` name. /// Replaces all library placeholders with the given address. /// /// Returns true if the bytecode object is fully linked, false otherwise /// This is a noop if the bytecode object is already fully linked. pub fn link( &mut self, file: impl AsRef, library: impl AsRef, address: Address, ) -> bool { if !self.object.is_unlinked() { return true } let file = file.as_ref(); let library = library.as_ref(); if let Some((key, mut contracts)) = self.link_references.remove_entry(file) { if contracts.remove(library).is_some() { self.object.link(file, library, address); } if !contracts.is_empty() { self.link_references.insert(key, contracts); } if self.link_references.is_empty() { return self.object.resolve().is_some() } } false } /// Links the bytecode object with all provided `(file, lib, addr)` pub fn link_all(&mut self, libs: I) -> bool where I: IntoIterator, S: AsRef, T: AsRef, { for (file, lib, addr) in libs.into_iter() { if self.link(file, lib, addr) { return true } } false } /// Links the bytecode object with all provided `(fully_qualified, addr)` pub fn link_all_fully_qualified(&mut self, libs: I) -> bool where I: IntoIterator, S: AsRef, { for (name, addr) in libs.into_iter() { if self.link_fully_qualified(name, addr) { return true } } false } } /// Represents the bytecode of a contracts that might be not fully linked yet. #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] #[serde(untagged)] pub enum BytecodeObject { /// Fully linked bytecode object #[serde(deserialize_with = "deserialize_bytes")] Bytecode(Bytes), /// Bytecode as hex string that's not fully linked yet and contains library placeholders Unlinked(String), } impl BytecodeObject { pub fn into_bytes(self) -> Option { match self { BytecodeObject::Bytecode(bytes) => Some(bytes), BytecodeObject::Unlinked(_) => None, } } pub fn as_bytes(&self) -> Option<&Bytes> { match self { BytecodeObject::Bytecode(bytes) => Some(bytes), BytecodeObject::Unlinked(_) => None, } } pub fn into_unlinked(self) -> Option { match self { BytecodeObject::Bytecode(_) => None, BytecodeObject::Unlinked(code) => Some(code), } } /// Tries to resolve the unlinked string object a valid bytecode object in place /// /// Returns the string if it is a valid pub fn resolve(&mut self) -> Option<&Bytes> { if let BytecodeObject::Unlinked(unlinked) = self { if let Ok(linked) = hex::decode(unlinked) { *self = BytecodeObject::Bytecode(linked.into()); } } self.as_bytes() } /// Link using the fully qualified name of a library /// The fully qualified library name is the path of its source file and the library name /// separated by `:` like `file.sol:Math` /// /// This will replace all occurrences of the library placeholder with the given address. /// /// See also: https://docs.soliditylang.org/en/develop/using-the-compiler.html#library-linking pub fn link_fully_qualified(&mut self, name: impl AsRef, addr: Address) -> &mut Self { if let BytecodeObject::Unlinked(ref mut unlinked) = self { let name = name.as_ref(); let place_holder = utils::library_hash_placeholder(name); // the address as hex without prefix let hex_addr = hex::encode(addr); // the library placeholder used to be the fully qualified name of the library instead of // the hash. This is also still supported by `solc` so we handle this as well let fully_qualified_placeholder = utils::library_fully_qualified_placeholder(name); *unlinked = unlinked .replace(&format!("__{}__", fully_qualified_placeholder), &hex_addr) .replace(&format!("__{}__", place_holder), &hex_addr) } self } /// Link using the `file` and `library` names as fully qualified name `:` /// See `BytecodeObject::link_fully_qualified` pub fn link( &mut self, file: impl AsRef, library: impl AsRef, addr: Address, ) -> &mut Self { self.link_fully_qualified(format!("{}:{}", file.as_ref(), library.as_ref(),), addr) } /// Links the bytecode object with all provided `(file, lib, addr)` pub fn link_all(&mut self, libs: I) -> &mut Self where I: IntoIterator, S: AsRef, T: AsRef, { for (file, lib, addr) in libs.into_iter() { self.link(file, lib, addr); } self } /// Whether this object is still unlinked pub fn is_unlinked(&self) -> bool { matches!(self, BytecodeObject::Unlinked(_)) } /// Whether the bytecode contains a matching placeholder using the qualified name pub fn contains_fully_qualified_placeholder(&self, name: impl AsRef) -> bool { if let BytecodeObject::Unlinked(unlinked) = self { let name = name.as_ref(); unlinked.contains(&utils::library_hash_placeholder(name)) || unlinked.contains(&utils::library_fully_qualified_placeholder(name)) } else { false } } /// Whether the bytecode contains a matching placeholder pub fn contains_placeholder(&self, file: impl AsRef, library: impl AsRef) -> bool { self.contains_fully_qualified_placeholder(format!("{}:{}", file.as_ref(), library.as_ref())) } } // Returns a not deployable bytecode by default as "0x" impl Default for BytecodeObject { fn default() -> Self { BytecodeObject::Unlinked("0x".to_string()) } } impl AsRef<[u8]> for BytecodeObject { fn as_ref(&self) -> &[u8] { match self { BytecodeObject::Bytecode(code) => code.as_ref(), BytecodeObject::Unlinked(code) => code.as_bytes(), } } } #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub struct FunctionDebugData { pub entry_point: Option, pub id: Option, pub parameter_slots: Option, pub return_slots: Option, } #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] pub struct GeneratedSource { pub ast: serde_json::Value, pub contents: String, pub id: u32, pub language: String, pub name: String, } /// Byte offsets into the bytecode. /// Linking replaces the 20 bytes located there. #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] pub struct Offsets { pub start: u32, pub length: u32, } #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] pub struct DeployedBytecode { #[serde(flatten)] pub bytecode: Option, #[serde( default, rename = "immutableReferences", skip_serializing_if = "::std::collections::BTreeMap::is_empty" )] pub immutable_references: BTreeMap>, } #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] pub struct GasEstimates { pub creation: Creation, #[serde(default)] pub external: BTreeMap, #[serde(default)] pub internal: BTreeMap, } #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub struct Creation { pub code_deposit_cost: String, pub execution_cost: String, pub total_cost: String, } #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] pub struct Ewasm { pub wast: String, pub wasm: String, } #[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq)] pub struct StorageLayout { pub storage: Vec, #[serde(default)] pub types: BTreeMap, } impl StorageLayout { fn is_empty(&self) -> bool { self.storage.is_empty() && self.types.is_empty() } } #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] pub struct Storage { #[serde(rename = "astId")] pub ast_id: u64, pub contract: String, pub label: String, pub offset: i64, pub slot: String, #[serde(rename = "type")] pub storage_type: String, } #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] pub struct StorageType { pub encoding: String, pub label: String, #[serde(rename = "numberOfBytes")] pub number_of_bytes: String, } #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub struct Error { #[serde(default, skip_serializing_if = "Option::is_none")] pub source_location: Option, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub secondary_source_locations: Vec, pub r#type: String, pub component: String, pub severity: Severity, #[serde(default, with = "display_from_str_opt")] pub error_code: Option, pub message: String, pub formatted_message: Option, } impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if let Some(msg) = &self.formatted_message { match self.severity { Severity::Error => msg.as_str().red().fmt(f), Severity::Warning | Severity::Info => msg.as_str().yellow().fmt(f), } } else { self.severity.fmt(f)?; writeln!(f, ": {}", self.message) } } } #[derive(Clone, Debug, Eq, PartialEq)] pub enum Severity { Error, Warning, Info, } impl fmt::Display for Severity { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Severity::Error => f.write_str(&"Error".red()), Severity::Warning => f.write_str(&"Warning".yellow()), Severity::Info => f.write_str("Info"), } } } impl Severity { pub fn is_error(&self) -> bool { matches!(self, Severity::Error) } pub fn is_warning(&self) -> bool { matches!(self, Severity::Warning) } pub fn is_info(&self) -> bool { matches!(self, Severity::Info) } } impl FromStr for Severity { type Err = String; fn from_str(s: &str) -> Result { match s { "error" => Ok(Severity::Error), "warning" => Ok(Severity::Warning), "info" => Ok(Severity::Info), s => Err(format!("Invalid severity: {}", s)), } } } impl Serialize for Severity { fn serialize(&self, serializer: S) -> Result where S: Serializer, { match self { Severity::Error => serializer.serialize_str("error"), Severity::Warning => serializer.serialize_str("warning"), Severity::Info => serializer.serialize_str("info"), } } } impl<'de> Deserialize<'de> for Severity { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { struct SeverityVisitor; impl<'de> Visitor<'de> for SeverityVisitor { type Value = Severity; fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { write!(formatter, "severity string") } fn visit_str(self, value: &str) -> Result where E: serde::de::Error, { value.parse().map_err(serde::de::Error::custom) } } deserializer.deserialize_str(SeverityVisitor) } } #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] pub struct SourceLocation { pub file: String, pub start: i32, pub end: i32, pub message: Option, } #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] pub struct SourceFile { pub id: u32, pub ast: serde_json::Value, } mod display_from_str_opt { use serde::{de, Deserialize, Deserializer, Serializer}; use std::{fmt, str::FromStr}; pub fn serialize(value: &Option, serializer: S) -> Result where T: fmt::Display, S: Serializer, { if let Some(value) = value { serializer.collect_str(value) } else { serializer.serialize_none() } } pub fn deserialize<'de, T, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, T: FromStr, T::Err: fmt::Display, { if let Some(s) = Option::::deserialize(deserializer)? { s.parse().map_err(de::Error::custom).map(Some) } else { Ok(None) } } } mod json_string_opt { use serde::{ de::{self, DeserializeOwned}, ser, Deserialize, Deserializer, Serialize, Serializer, }; pub fn serialize(value: &Option, serializer: S) -> Result where S: Serializer, T: Serialize, { if let Some(value) = value { let value = serde_json::to_string(value).map_err(ser::Error::custom)?; serializer.serialize_str(&value) } else { serializer.serialize_none() } } pub fn deserialize<'de, T, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, T: DeserializeOwned, { if let Some(s) = Option::::deserialize(deserializer)? { serde_json::from_str(&s).map_err(de::Error::custom).map(Some) } else { Ok(None) } } } pub fn deserialize_bytes<'de, D>(d: D) -> std::result::Result where D: Deserializer<'de>, { let value = String::deserialize(d)?; if let Some(value) = value.strip_prefix("0x") { hex::decode(value) } else { hex::decode(&value) } .map(Into::into) .map_err(|e| serde::de::Error::custom(e.to_string())) } pub fn deserialize_opt_bytes<'de, D>(d: D) -> std::result::Result, D::Error> where D: Deserializer<'de>, { let value = Option::::deserialize(d)?; if let Some(value) = value { Ok(Some( if let Some(value) = value.strip_prefix("0x") { hex::decode(value) } else { hex::decode(&value) } .map_err(|e| serde::de::Error::custom(e.to_string()))? .into(), )) } else { Ok(None) } } #[cfg(test)] mod tests { use super::*; use std::{fs, path::PathBuf}; #[test] fn can_link_bytecode() { // test cases taken from https://github.com/ethereum/solc-js/blob/master/test/linker.js #[derive(Serialize, Deserialize)] struct Mockject { object: BytecodeObject, } fn parse_bytecode(bytecode: &str) -> BytecodeObject { let object: Mockject = serde_json::from_value(serde_json::json!({ "object": bytecode })).unwrap(); object.object } let bytecode = "6060604052341561000f57600080fd5b60f48061001d6000396000f300606060405260043610603e5763ffffffff7c010000000000000000000000000000000000000000000000000000000060003504166326121ff081146043575b600080fd5b3415604d57600080fd5b60536055565b005b73__lib2.sol:L____________________________6326121ff06040518163ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040160006040518083038186803b151560b357600080fd5b6102c65a03f4151560c357600080fd5b5050505600a165627a7a723058207979b30bd4a07c77b02774a511f2a1dd04d7e5d65b5c2735b5fc96ad61d43ae40029"; let mut object = parse_bytecode(bytecode); assert!(object.is_unlinked()); assert!(object.contains_placeholder("lib2.sol", "L")); assert!(object.contains_fully_qualified_placeholder("lib2.sol:L")); assert!(object.link("lib2.sol", "L", Address::random()).resolve().is_some()); assert!(!object.is_unlinked()); let mut code = Bytecode { function_debug_data: Default::default(), object: parse_bytecode(bytecode), opcodes: None, source_map: None, generated_sources: vec![], link_references: BTreeMap::from([( "lib2.sol".to_string(), BTreeMap::from([("L".to_string(), vec![])]), )]), }; assert!(!code.link("lib2.sol", "Y", Address::random())); assert!(code.link("lib2.sol", "L", Address::random())); assert!(code.link("lib2.sol", "L", Address::random())); let hashed_placeholder = "6060604052341561000f57600080fd5b60f48061001d6000396000f300606060405260043610603e5763ffffffff7c010000000000000000000000000000000000000000000000000000000060003504166326121ff081146043575b600080fd5b3415604d57600080fd5b60536055565b005b73__$cb901161e812ceb78cfe30ca65050c4337$__6326121ff06040518163ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040160006040518083038186803b151560b357600080fd5b6102c65a03f4151560c357600080fd5b5050505600a165627a7a723058207979b30bd4a07c77b02774a511f2a1dd04d7e5d65b5c2735b5fc96ad61d43ae40029"; let mut object = parse_bytecode(hashed_placeholder); assert!(object.is_unlinked()); assert!(object.contains_placeholder("lib2.sol", "L")); assert!(object.contains_fully_qualified_placeholder("lib2.sol:L")); assert!(object.link("lib2.sol", "L", Address::default()).resolve().is_some()); assert!(!object.is_unlinked()); } #[test] fn can_parse_compiler_output() { let mut dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); dir.push("test-data/out"); for path in fs::read_dir(dir).unwrap() { let path = path.unwrap().path(); let compiler_output = fs::read_to_string(&path).unwrap(); serde_json::from_str::(&compiler_output).unwrap_or_else(|err| { panic!("Failed to read compiler output of {} {}", path.display(), err) }); } } #[test] fn can_parse_compiler_input() { let mut dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); dir.push("test-data/in"); for path in fs::read_dir(dir).unwrap() { let path = path.unwrap().path(); let compiler_output = fs::read_to_string(&path).unwrap(); serde_json::from_str::(&compiler_output).unwrap_or_else(|err| { panic!("Failed to read compiler output of {} {}", path.display(), err) }); } } #[test] fn test_evm_version_normalization() { for (solc_version, evm_version, expected) in &[ // Ensure 0.4.21 it always returns None ("0.4.20", EvmVersion::Homestead, None), // Constantinople clipping ("0.4.21", EvmVersion::Homestead, Some(EvmVersion::Homestead)), ("0.4.21", EvmVersion::Constantinople, Some(EvmVersion::Constantinople)), ("0.4.21", EvmVersion::London, Some(EvmVersion::Constantinople)), // Petersburg ("0.5.5", EvmVersion::Homestead, Some(EvmVersion::Homestead)), ("0.5.5", EvmVersion::Petersburg, Some(EvmVersion::Petersburg)), ("0.5.5", EvmVersion::London, Some(EvmVersion::Petersburg)), // Istanbul ("0.5.14", EvmVersion::Homestead, Some(EvmVersion::Homestead)), ("0.5.14", EvmVersion::Istanbul, Some(EvmVersion::Istanbul)), ("0.5.14", EvmVersion::London, Some(EvmVersion::Istanbul)), // Berlin ("0.8.5", EvmVersion::Homestead, Some(EvmVersion::Homestead)), ("0.8.5", EvmVersion::Berlin, Some(EvmVersion::Berlin)), ("0.8.5", EvmVersion::London, Some(EvmVersion::Berlin)), // London ("0.8.7", EvmVersion::Homestead, Some(EvmVersion::Homestead)), ("0.8.7", EvmVersion::London, Some(EvmVersion::London)), ("0.8.7", EvmVersion::London, Some(EvmVersion::London)), ] { assert_eq!( &evm_version.normalize_version(&Version::from_str(solc_version).unwrap()), expected ) } } }