factor out solc JSON interface crate (#264)

The differential testing framework will make a second consumer. There
seems to be no re-usable Rust crate for this. But we already have
everything here, just needs a small refactor to make it fully re-usable.

- Mostly decouple the solc JSON-input-output interface types from the
`solidity` frontend crate
- Expose the JSON-input-output interface types in a dedicated crate

---------

Signed-off-by: Cyrill Leutwiler <bigcyrill@hotmail.com>
This commit is contained in:
xermicus
2025-03-20 17:11:40 +01:00
committed by GitHub
parent 36ea69b50f
commit 497dae2494
45 changed files with 328 additions and 245 deletions
@@ -0,0 +1,22 @@
//! The `solc --standard-json` input language.
use serde::Deserialize;
use serde::Serialize;
/// The `solc --standard-json` input language.
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Language {
/// The Solidity language.
Solidity,
/// The Yul IR.
Yul,
}
impl std::fmt::Display for Language {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Solidity => write!(f, "Solidity"),
Self::Yul => write!(f, "Yul"),
}
}
}
@@ -0,0 +1,141 @@
//! The `solc --standard-json` input.
pub mod language;
pub mod settings;
pub mod source;
use std::collections::BTreeMap;
use std::collections::BTreeSet;
use std::path::PathBuf;
#[cfg(all(feature = "parallel", feature = "resolc"))]
use rayon::iter::{IntoParallelIterator, ParallelIterator};
use serde::Deserialize;
use serde::Serialize;
use crate::standard_json::input::settings::metadata::Metadata as SolcStandardJsonInputSettingsMetadata;
use crate::standard_json::input::settings::optimizer::Optimizer as SolcStandardJsonInputSettingsOptimizer;
use crate::standard_json::input::settings::selection::Selection as SolcStandardJsonInputSettingsSelection;
#[cfg(feature = "resolc")]
use crate::warning::Warning;
use self::language::Language;
use self::settings::Settings;
use self::source::Source;
/// The `solc --standard-json` input.
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Input {
/// The input language.
pub language: Language,
/// The input source code files hashmap.
pub sources: BTreeMap<String, Source>,
/// The compiler settings.
pub settings: Settings,
/// The suppressed warnings.
#[cfg(feature = "resolc")]
#[serde(skip_serializing)]
pub suppressed_warnings: Option<Vec<Warning>>,
}
impl Input {
/// A shortcut constructor from stdin.
pub fn try_from_stdin() -> anyhow::Result<Self> {
let mut input: Self = serde_json::from_reader(std::io::BufReader::new(std::io::stdin()))?;
input
.settings
.output_selection
.get_or_insert_with(SolcStandardJsonInputSettingsSelection::default)
.extend_with_required();
Ok(input)
}
/// A shortcut constructor from paths.
#[allow(clippy::too_many_arguments)]
pub fn try_from_paths(
language: Language,
evm_version: Option<revive_common::EVMVersion>,
paths: &[PathBuf],
library_map: Vec<String>,
remappings: Option<BTreeSet<String>>,
output_selection: SolcStandardJsonInputSettingsSelection,
optimizer: SolcStandardJsonInputSettingsOptimizer,
metadata: Option<SolcStandardJsonInputSettingsMetadata>,
#[cfg(feature = "resolc")] suppressed_warnings: Option<Vec<Warning>>,
) -> anyhow::Result<Self> {
let mut paths: BTreeSet<PathBuf> = paths.iter().cloned().collect();
let libraries = Settings::parse_libraries(library_map)?;
for library_file in libraries.keys() {
paths.insert(PathBuf::from(library_file));
}
let sources = paths
.iter()
.map(|path| {
let source = Source::try_from(path.as_path()).unwrap_or_else(|error| {
panic!("Source code file {path:?} reading error: {error}")
});
(path.to_string_lossy().to_string(), source)
})
.collect();
Ok(Self {
language,
sources,
settings: Settings::new(
evm_version,
libraries,
remappings,
output_selection,
optimizer,
metadata,
),
#[cfg(feature = "resolc")]
suppressed_warnings,
})
}
/// A shortcut constructor from source code.
/// Only for the integration test purposes.
#[cfg(feature = "resolc")]
#[allow(clippy::too_many_arguments)]
pub fn try_from_sources(
evm_version: Option<revive_common::EVMVersion>,
sources: BTreeMap<String, String>,
libraries: BTreeMap<String, BTreeMap<String, String>>,
remappings: Option<BTreeSet<String>>,
output_selection: SolcStandardJsonInputSettingsSelection,
optimizer: SolcStandardJsonInputSettingsOptimizer,
metadata: Option<SolcStandardJsonInputSettingsMetadata>,
suppressed_warnings: Option<Vec<Warning>>,
) -> anyhow::Result<Self> {
#[cfg(feature = "parallel")]
let iter = sources.into_par_iter(); // Parallel iterator
#[cfg(not(feature = "parallel"))]
let iter = sources.into_iter(); // Sequential iterator
let sources = iter
.map(|(path, content)| (path, Source::from(content)))
.collect();
Ok(Self {
language: Language::Solidity,
sources,
settings: Settings::new(
evm_version,
libraries,
remappings,
output_selection,
optimizer,
metadata,
),
suppressed_warnings,
})
}
/// Sets the necessary defaults.
pub fn normalize(&mut self, version: &semver::Version) {
self.settings.normalize(version);
}
}
@@ -0,0 +1,24 @@
//! The `solc --standard-json` input settings metadata.
use serde::Deserialize;
use serde::Serialize;
use crate::standard_json::input::settings::metadata_hash::MetadataHash;
/// The `solc --standard-json` input settings metadata.
#[derive(Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Metadata {
/// The bytecode hash mode.
#[serde(skip_serializing_if = "Option::is_none")]
pub bytecode_hash: Option<MetadataHash>,
}
impl Metadata {
/// A shortcut constructor.
pub fn new(bytecode_hash: MetadataHash) -> Self {
Self {
bytecode_hash: Some(bytecode_hash),
}
}
}
@@ -0,0 +1,29 @@
//! The metadata hash mode.
use std::str::FromStr;
use serde::Deserialize;
use serde::Serialize;
/// The metadata hash mode.
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash)]
pub enum MetadataHash {
/// Do not include bytecode hash.
#[serde(rename = "none")]
None,
/// The default keccak256 hash.
#[serde(rename = "keccak256")]
Keccak256,
}
impl FromStr for MetadataHash {
type Err = anyhow::Error;
fn from_str(string: &str) -> Result<Self, Self::Err> {
match string {
"none" => Ok(Self::None),
"keccak256" => Ok(Self::Keccak256),
_ => anyhow::bail!("Unknown bytecode hash mode: `{}`", string),
}
}
}
@@ -0,0 +1,101 @@
//! The `solc --standard-json` input settings.
pub mod metadata;
pub mod metadata_hash;
pub mod optimizer;
pub mod selection;
use std::collections::BTreeMap;
use std::collections::BTreeSet;
use serde::Deserialize;
use serde::Serialize;
use self::metadata::Metadata;
use self::optimizer::Optimizer;
use self::selection::Selection;
/// The `solc --standard-json` input settings.
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Settings {
/// The target EVM version.
#[serde(skip_serializing_if = "Option::is_none")]
pub evm_version: Option<revive_common::EVMVersion>,
/// The linker library addresses.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub libraries: Option<BTreeMap<String, BTreeMap<String, String>>>,
/// The sorted list of remappings.
#[serde(skip_serializing_if = "Option::is_none")]
pub remappings: Option<BTreeSet<String>>,
/// The output selection filters.
#[serde(skip_serializing_if = "Option::is_none")]
pub output_selection: Option<Selection>,
/// Whether to compile via IR. Only for testing with solc >=0.8.13.
#[serde(
rename = "viaIR",
skip_serializing_if = "Option::is_none",
skip_deserializing
)]
pub via_ir: Option<bool>,
/// The optimizer settings.
pub optimizer: Optimizer,
/// The metadata settings.
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<Metadata>,
}
impl Settings {
/// A shortcut constructor.
pub fn new(
evm_version: Option<revive_common::EVMVersion>,
libraries: BTreeMap<String, BTreeMap<String, String>>,
remappings: Option<BTreeSet<String>>,
output_selection: Selection,
optimizer: Optimizer,
metadata: Option<Metadata>,
) -> Self {
Self {
evm_version,
libraries: Some(libraries),
remappings,
output_selection: Some(output_selection),
optimizer,
metadata,
via_ir: Some(true),
}
}
/// Sets the necessary defaults.
pub fn normalize(&mut self, version: &semver::Version) {
self.optimizer.normalize(version);
}
/// Parses the library list and returns their double hashmap with path and name as keys.
pub fn parse_libraries(
input: Vec<String>,
) -> anyhow::Result<BTreeMap<String, BTreeMap<String, String>>> {
let mut libraries = BTreeMap::new();
for (index, library) in input.into_iter().enumerate() {
let mut path_and_address = library.split('=');
let path = path_and_address
.next()
.ok_or_else(|| anyhow::anyhow!("The library #{} path is missing", index))?;
let mut file_and_contract = path.split(':');
let file = file_and_contract
.next()
.ok_or_else(|| anyhow::anyhow!("The library `{}` file name is missing", path))?;
let contract = file_and_contract.next().ok_or_else(|| {
anyhow::anyhow!("The library `{}` contract name is missing", path)
})?;
let address = path_and_address
.next()
.ok_or_else(|| anyhow::anyhow!("The library `{}` address is missing", path))?;
libraries
.entry(file.to_owned())
.or_insert_with(BTreeMap::new)
.insert(contract.to_owned(), address.to_owned());
}
Ok(libraries)
}
}
@@ -0,0 +1,59 @@
//! The `solc --standard-json` input settings optimizer details.
use serde::Deserialize;
use serde::Serialize;
/// The `solc --standard-json` input settings optimizer details.
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Details {
/// Whether the pass is enabled.
pub peephole: bool,
/// Whether the pass is enabled.
#[serde(skip_serializing_if = "Option::is_none")]
pub inliner: Option<bool>,
/// Whether the pass is enabled.
pub jumpdest_remover: bool,
/// Whether the pass is enabled.
pub order_literals: bool,
/// Whether the pass is enabled.
pub deduplicate: bool,
/// Whether the pass is enabled.
pub cse: bool,
/// Whether the pass is enabled.
pub constant_optimizer: bool,
}
impl Details {
/// A shortcut constructor.
pub fn new(
peephole: bool,
inliner: Option<bool>,
jumpdest_remover: bool,
order_literals: bool,
deduplicate: bool,
cse: bool,
constant_optimizer: bool,
) -> Self {
Self {
peephole,
inliner,
jumpdest_remover,
order_literals,
deduplicate,
cse,
constant_optimizer,
}
}
/// Creates a set of disabled optimizations.
pub fn disabled(version: &semver::Version) -> Self {
let inliner = if version >= &semver::Version::new(0, 8, 5) {
Some(false)
} else {
None
};
Self::new(false, inliner, false, false, false, false, false)
}
}
@@ -0,0 +1,51 @@
//! The `solc --standard-json` input settings optimizer.
pub mod details;
use serde::Deserialize;
use serde::Serialize;
use self::details::Details;
/// The `solc --standard-json` input settings optimizer.
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Optimizer {
/// Whether the optimizer is enabled.
pub enabled: bool,
/// The optimization mode string.
#[serde(skip_serializing)]
pub mode: Option<char>,
/// The `solc` optimizer details.
#[serde(skip_serializing_if = "Option::is_none")]
pub details: Option<Details>,
/// Whether to try to recompile with -Oz if the bytecode is too large.
#[serde(skip_serializing)]
pub fallback_to_optimizing_for_size: Option<bool>,
}
impl Optimizer {
/// A shortcut constructor.
pub fn new(
enabled: bool,
mode: Option<char>,
version: &semver::Version,
fallback_to_optimizing_for_size: bool,
) -> Self {
Self {
enabled,
mode,
details: Some(Details::disabled(version)),
fallback_to_optimizing_for_size: Some(fallback_to_optimizing_for_size),
}
}
/// Sets the necessary defaults.
pub fn normalize(&mut self, version: &semver::Version) {
self.details = if version >= &semver::Version::new(0, 5, 5) {
Some(Details::disabled(version))
} else {
None
};
}
}
@@ -0,0 +1,66 @@
//! The `solc --standard-json` expected output selection flag.
use serde::Deserialize;
use serde::Serialize;
/// The `solc --standard-json` expected output selection flag.
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Flag {
/// The ABI JSON.
#[serde(rename = "abi")]
ABI,
/// The metadata.
#[serde(rename = "metadata")]
Metadata,
/// The developer documentation.
#[serde(rename = "devdoc")]
Devdoc,
/// The user documentation.
#[serde(rename = "userdoc")]
Userdoc,
/// The function signature hashes JSON.
#[serde(rename = "evm.methodIdentifiers")]
MethodIdentifiers,
/// The storage layout.
#[serde(rename = "storageLayout")]
StorageLayout,
/// The AST JSON.
#[serde(rename = "ast")]
AST,
/// The Yul IR.
#[serde(rename = "irOptimized")]
Yul,
/// The EVM legacy assembly JSON.
#[serde(rename = "evm.legacyAssembly")]
EVMLA,
#[serde(rename = "evm.bytecode")]
EVMBC,
#[serde(rename = "evm.deployedBytecode")]
EVMDBC,
/// The assembly code
#[serde(rename = "evm.assembly")]
Assembly,
/// The Ir
#[serde(rename = "ir")]
Ir,
}
impl std::fmt::Display for Flag {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ABI => write!(f, "abi"),
Self::Metadata => write!(f, "metadata"),
Self::Devdoc => write!(f, "devdoc"),
Self::Userdoc => write!(f, "userdoc"),
Self::MethodIdentifiers => write!(f, "evm.methodIdentifiers"),
Self::StorageLayout => write!(f, "storageLayout"),
Self::AST => write!(f, "ast"),
Self::Yul => write!(f, "irOptimized"),
Self::EVMLA => write!(f, "evm.legacyAssembly"),
Self::EVMBC => write!(f, "evm.bytecode"),
Self::EVMDBC => write!(f, "evm.deployedBytecode"),
Self::Assembly => write!(f, "evm.assembly"),
Self::Ir => write!(f, "ir"),
}
}
}
@@ -0,0 +1,51 @@
//! The `solc --standard-json` output file selection.
pub mod flag;
use std::collections::HashSet;
use serde::Deserialize;
use serde::Serialize;
use self::flag::Flag as SelectionFlag;
/// The `solc --standard-json` output file selection.
#[derive(Debug, Default, Serialize, Deserialize, PartialEq)]
pub struct File {
/// The per-file output selections.
#[serde(rename = "", skip_serializing_if = "Option::is_none")]
pub per_file: Option<HashSet<SelectionFlag>>,
/// The per-contract output selections.
#[serde(rename = "*", skip_serializing_if = "Option::is_none")]
pub per_contract: Option<HashSet<SelectionFlag>>,
}
impl File {
/// Creates the selection required by our compilation process.
pub fn new_required() -> Self {
Self {
per_file: Some(HashSet::from_iter([SelectionFlag::AST])),
per_contract: Some(HashSet::from_iter([
SelectionFlag::EVMBC,
SelectionFlag::EVMDBC,
SelectionFlag::MethodIdentifiers,
SelectionFlag::Metadata,
SelectionFlag::Yul,
])),
}
}
/// Extends the user's output selection with flag required by our compilation process.
pub fn extend_with_required(&mut self) -> &mut Self {
let required = Self::new_required();
self.per_file
.get_or_insert_with(HashSet::default)
.extend(required.per_file.unwrap_or_default());
self.per_contract
.get_or_insert_with(HashSet::default)
.extend(required.per_contract.unwrap_or_default());
self
}
}
@@ -0,0 +1,99 @@
//! The `solc --standard-json` output selection.
pub mod file;
use std::collections::BTreeMap;
use serde::Deserialize;
use serde::Serialize;
use self::file::File as FileSelection;
/// The `solc --standard-json` output selection.
#[derive(Debug, Serialize, Deserialize, Default, PartialEq)]
pub struct Selection {
/// Only the 'all' wildcard is available for robustness reasons.
#[serde(rename = "*", skip_serializing_if = "Option::is_none")]
all: Option<FileSelection>,
#[serde(skip_serializing_if = "BTreeMap::is_empty", flatten)]
pub files: BTreeMap<String, FileSelection>,
}
impl Selection {
/// Creates the selection required by our compilation process.
pub fn new_required() -> Self {
Self {
all: Some(FileSelection::new_required()),
files: BTreeMap::new(),
}
}
/// Extends the user's output selection with flag required by our compilation process.
pub fn extend_with_required(&mut self) -> &mut Self {
self.all
.get_or_insert_with(FileSelection::new_required)
.extend_with_required();
for (_, v) in self.files.iter_mut() {
v.extend_with_required();
}
self
}
}
#[cfg(test)]
mod test {
use std::collections::BTreeMap;
use crate::SolcStandardJsonInputSettingsSelectionFile;
use super::Selection;
#[test]
fn per_file() {
let init = Selection {
all: None,
files: BTreeMap::from([(
"Test".to_owned(),
SolcStandardJsonInputSettingsSelectionFile::new_required(),
)]),
};
let deser = serde_json::to_string(&init)
.and_then(|string| serde_json::from_str(&string))
.unwrap();
assert_eq!(init, deser)
}
#[test]
fn all() {
let init = Selection {
all: Some(SolcStandardJsonInputSettingsSelectionFile::new_required()),
files: BTreeMap::new(),
};
let deser = serde_json::to_string(&init)
.and_then(|string| serde_json::from_str(&string))
.unwrap();
assert_eq!(init, deser)
}
#[test]
fn all_and_override() {
let init = Selection {
all: Some(SolcStandardJsonInputSettingsSelectionFile::new_required()),
files: BTreeMap::from([(
"Test".to_owned(),
SolcStandardJsonInputSettingsSelectionFile::new_required(),
)]),
};
let deser = serde_json::to_string(&init)
.and_then(|string| serde_json::from_str(&string))
.unwrap();
assert_eq!(init, deser)
}
}
@@ -0,0 +1,40 @@
//! The `solc --standard-json` input source.
use std::io::Read;
use std::path::Path;
use serde::Deserialize;
use serde::Serialize;
/// The `solc --standard-json` input source.
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Source {
/// The source code file content.
pub content: String,
}
impl From<String> for Source {
fn from(content: String) -> Self {
Self { content }
}
}
impl TryFrom<&Path> for Source {
type Error = anyhow::Error;
fn try_from(path: &Path) -> Result<Self, Self::Error> {
let content = if path.to_string_lossy() == "-" {
let mut solidity_code = String::with_capacity(16384);
std::io::stdin()
.read_to_string(&mut solidity_code)
.map_err(|error| anyhow::anyhow!("<stdin> reading error: {}", error))?;
solidity_code
} else {
std::fs::read_to_string(path)
.map_err(|error| anyhow::anyhow!("File {:?} reading error: {}", path, error))?
};
Ok(Self { content })
}
}
@@ -0,0 +1,4 @@
//! The `solc <input>.sol --standard-json` interface input and output.
pub mod input;
pub mod output;
@@ -0,0 +1,34 @@
//! The `solc --standard-json` output contract EVM bytecode.
use serde::Deserialize;
use serde::Serialize;
/// The `solc --standard-json` output contract EVM bytecode.
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Bytecode {
/// The bytecode object.
pub object: String,
}
impl Bytecode {
/// A shortcut constructor.
pub fn new(object: String) -> Self {
Self { object }
}
}
/// The `solc --standard-json` output contract EVM deployed bytecode.
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct DeployedBytecode {
/// The bytecode object.
pub object: String,
}
impl DeployedBytecode {
/// A shortcut constructor.
pub fn new(object: String) -> Self {
Self { object }
}
}
@@ -0,0 +1,42 @@
//! The `solc --standard-json` output contract EVM data.
pub mod bytecode;
use std::collections::BTreeMap;
use serde::Deserialize;
use serde::Serialize;
use self::bytecode::Bytecode;
use self::bytecode::DeployedBytecode;
/// The `solc --standard-json` output contract EVM data.
/// It is replaced by PolkaVM data after compiling.
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct EVM {
/// The contract PolkaVM assembly code.
#[serde(rename = "assembly", skip_serializing_if = "Option::is_none")]
pub assembly_text: Option<String>,
/// The contract bytecode.
/// Is reset by that of PolkaVM before yielding the compiled project artifacts.
#[serde(skip_serializing_if = "Option::is_none")]
pub bytecode: Option<Bytecode>,
/// The deployed bytecode of the contract.
/// It is overwritten with the PolkaVM blob before yielding the compiled project artifacts.
/// Hence it will be the same as the runtime code but we keep both for compatibility reasons.
#[serde(skip_serializing_if = "Option::is_none")]
pub deployed_bytecode: Option<DeployedBytecode>,
/// The contract function signatures.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub method_identifiers: Option<BTreeMap<String, String>>,
}
impl EVM {
/// Sets the PolkaVM assembly and bytecode.
pub fn modify(&mut self, assembly_text: String, bytecode: String) {
self.assembly_text = Some(assembly_text);
self.bytecode = Some(Bytecode::new(bytecode.clone()));
self.deployed_bytecode = Some(DeployedBytecode::new(bytecode));
}
}
@@ -0,0 +1,50 @@
//! The `solc --standard-json` output contract.
pub mod evm;
use std::collections::BTreeMap;
use std::collections::HashSet;
use serde::Deserialize;
use serde::Serialize;
use self::evm::EVM;
/// The `solc --standard-json` output contract.
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Contract {
/// The contract ABI.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub abi: Option<serde_json::Value>,
/// The contract metadata.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata: Option<serde_json::Value>,
/// The contract developer documentation.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub devdoc: Option<serde_json::Value>,
/// The contract user documentation.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub userdoc: Option<serde_json::Value>,
/// The contract storage layout.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub storage_layout: Option<serde_json::Value>,
/// Contract's bytecode and related objects
#[serde(default, skip_serializing_if = "Option::is_none")]
pub evm: Option<EVM>,
/// The contract IR code.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ir: Option<String>,
/// The contract optimized IR code.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ir_optimized: Option<String>,
/// The contract PolkaVM bytecode hash.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub hash: Option<String>,
/// The contract factory dependencies.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub factory_dependencies: Option<BTreeMap<String, String>>,
/// The contract missing libraries.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub missing_libraries: Option<HashSet<String>>,
}
@@ -0,0 +1,135 @@
//! The `solc --standard-json` output error.
pub mod source_location;
use std::str::FromStr;
use serde::Deserialize;
use serde::Serialize;
use self::source_location::SourceLocation;
/// The `solc --standard-json` output error.
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Error {
/// The component type.
pub component: String,
/// The error code.
pub error_code: Option<String>,
/// The formatted error message.
pub formatted_message: String,
/// The non-formatted error message.
pub message: String,
/// The error severity.
pub severity: String,
/// The error location data.
pub source_location: Option<SourceLocation>,
/// The error type.
pub r#type: String,
}
impl Error {
/// Returns the `ecrecover` function usage warning.
pub fn message_ecrecover(src: Option<&str>) -> Self {
let message = r#"
Warning: It looks like you are using 'ecrecover' to validate a signature of a user account.
Polkadot comes with native account abstraction support, therefore it is highly recommended NOT
to rely on the fact that the account has an ECDSA private key attached to it since accounts might
implement other signature schemes.
"#
.to_owned();
Self {
component: "general".to_owned(),
error_code: None,
formatted_message: message.clone(),
message,
severity: "warning".to_owned(),
source_location: src.map(SourceLocation::from_str).and_then(Result::ok),
r#type: "Warning".to_owned(),
}
}
/// Returns the `<address payable>`'s `send` and `transfer` methods usage error.
pub fn message_send_and_transfer(src: Option<&str>) -> Self {
let message = r#"
Warning: It looks like you are using '<address payable>.send/transfer(<X>)'.
Using '<address payable>.send/transfer(<X>)' is deprecated and strongly discouraged!
The resolc compiler uses a heuristic to detect '<address payable>.send/transfer(<X>)' calls,
which disables call re-entrancy and supplies all remaining gas instead of the 2300 gas stipend.
However, detection is not guaranteed. You are advised to carefully test this, employ
re-entrancy guards or use the withdrawal pattern instead!
Learn more on https://docs.soliditylang.org/en/latest/security-considerations.html#reentrancy
and https://docs.soliditylang.org/en/latest/common-patterns.html#withdrawal-from-contracts
"#
.to_owned();
Self {
component: "general".to_owned(),
error_code: None,
formatted_message: message.clone(),
message,
severity: "warning".to_owned(),
source_location: src.map(SourceLocation::from_str).and_then(Result::ok),
r#type: "Warning".to_owned(),
}
}
/// Returns the `extcodesize` instruction usage warning.
pub fn message_extcodesize(src: Option<&str>) -> Self {
let message = r#"
Warning: Your code or one of its dependencies uses the 'extcodesize' instruction, which is
usually needed in the following cases:
1. To detect whether an address belongs to a smart contract.
2. To detect whether the deploy code execution has finished.
Polkadot comes with native account abstraction support (so smart contracts are just accounts
coverned by code), and you should avoid differentiating between contracts and non-contract
addresses.
"#
.to_owned();
Self {
component: "general".to_owned(),
error_code: None,
formatted_message: message.clone(),
message,
severity: "warning".to_owned(),
source_location: src.map(SourceLocation::from_str).and_then(Result::ok),
r#type: "Warning".to_owned(),
}
}
/// Returns the `origin` instruction usage warning.
pub fn message_tx_origin(src: Option<&str>) -> Self {
let message = r#"
Warning: You are checking for 'tx.origin' in your code, which might lead to unexpected behavior.
Polkadot comes with native account abstraction support, and therefore the initiator of a
transaction might be different from the contract calling your code. It is highly recommended NOT
to rely on tx.origin, but use msg.sender instead.
"#
.to_owned();
Self {
component: "general".to_owned(),
error_code: None,
formatted_message: message.clone(),
message,
severity: "warning".to_owned(),
source_location: src.map(SourceLocation::from_str).and_then(Result::ok),
r#type: "Warning".to_owned(),
}
}
/// Appends the contract path to the message..
pub fn push_contract_path(&mut self, path: &str) {
self.formatted_message
.push_str(format!("\n--> {path}\n").as_str());
}
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.formatted_message)
}
}
@@ -0,0 +1,43 @@
//! The `solc --standard-json` output error source location.
use std::str::FromStr;
use serde::Deserialize;
use serde::Serialize;
/// The `solc --standard-json` output error source location.
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct SourceLocation {
/// The source file path.
pub file: String,
/// The start location.
pub start: isize,
/// The end location.
pub end: isize,
}
impl FromStr for SourceLocation {
type Err = anyhow::Error;
fn from_str(string: &str) -> Result<Self, Self::Err> {
let mut parts = string.split(':');
let start = parts
.next()
.map(|string| string.parse::<isize>())
.and_then(Result::ok)
.unwrap_or_default();
let length = parts
.next()
.map(|string| string.parse::<isize>())
.and_then(Result::ok)
.unwrap_or_default();
let file = parts.next().unwrap_or_default().to_owned();
Ok(Self {
file,
start,
end: start + length,
})
}
}
@@ -0,0 +1,71 @@
//! The `solc --standard-json` output.
pub mod contract;
pub mod error;
pub mod source;
use std::collections::BTreeMap;
use serde::Deserialize;
use serde::Serialize;
#[cfg(feature = "resolc")]
use crate::warning::Warning;
use self::contract::Contract;
use self::error::Error as SolcStandardJsonOutputError;
use self::source::Source;
/// The `solc --standard-json` output.
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct Output {
/// The file-contract hashmap.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub contracts: Option<BTreeMap<String, BTreeMap<String, Contract>>>,
/// The source code mapping data.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sources: Option<BTreeMap<String, Source>>,
/// The compilation errors and warnings.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub errors: Option<Vec<SolcStandardJsonOutputError>>,
/// The `solc` compiler version.
#[serde(skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
/// The `solc` compiler long version.
#[serde(skip_serializing_if = "Option::is_none")]
pub long_version: Option<String>,
/// The `resolc` compiler version.
#[serde(skip_serializing_if = "Option::is_none")]
pub revive_version: Option<String>,
}
impl Output {
/// Traverses the AST and returns the list of additional errors and warnings.
#[cfg(feature = "resolc")]
pub fn preprocess_ast(&mut self, suppressed_warnings: &[Warning]) -> anyhow::Result<()> {
let sources = match self.sources.as_ref() {
Some(sources) => sources,
None => return Ok(()),
};
let mut messages = Vec::new();
for (path, source) in sources.iter() {
if let Some(ast) = source.ast.as_ref() {
let mut polkavm_messages = Source::get_messages(ast, suppressed_warnings);
for message in polkavm_messages.iter_mut() {
message.push_contract_path(path.as_str());
}
messages.extend(polkavm_messages);
}
}
self.errors = match self.errors.take() {
Some(mut errors) => {
errors.extend(messages);
Some(errors)
}
None => Some(messages),
};
Ok(())
}
}
@@ -0,0 +1,202 @@
//! The `solc --standard-json` output source.
use serde::Deserialize;
use serde::Serialize;
use crate::standard_json::output::error::Error as SolcStandardJsonOutputError;
#[cfg(feature = "resolc")]
use crate::warning::Warning;
/// The `solc --standard-json` output source.
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Source {
/// The source code ID.
pub id: usize,
/// The source code AST.
pub ast: Option<serde_json::Value>,
}
impl Source {
/// Checks the AST node for the `ecrecover` function usage.
pub fn check_ecrecover(ast: &serde_json::Value) -> Option<SolcStandardJsonOutputError> {
let ast = ast.as_object()?;
if ast.get("nodeType")?.as_str()? != "FunctionCall" {
return None;
}
let expression = ast.get("expression")?.as_object()?;
if expression.get("nodeType")?.as_str()? != "Identifier" {
return None;
}
if expression.get("name")?.as_str()? != "ecrecover" {
return None;
}
Some(SolcStandardJsonOutputError::message_ecrecover(
ast.get("src")?.as_str(),
))
}
/// Checks the AST node for the `<address payable>`'s `send` and `transfer` methods usage.
pub fn check_send_and_transfer(ast: &serde_json::Value) -> Option<SolcStandardJsonOutputError> {
let ast = ast.as_object()?;
if ast.get("nodeType")?.as_str()? != "FunctionCall" {
return None;
}
let expression = ast.get("expression")?.as_object()?;
if expression.get("nodeType")?.as_str()? != "MemberAccess" {
return None;
}
let member_name = expression.get("memberName")?.as_str()?;
if member_name != "send" && member_name != "transfer" {
return None;
}
Some(SolcStandardJsonOutputError::message_send_and_transfer(
ast.get("src")?.as_str(),
))
}
/// Checks the AST node for the `extcodesize` assembly instruction usage.
pub fn check_assembly_extcodesize(
ast: &serde_json::Value,
) -> Option<SolcStandardJsonOutputError> {
let ast = ast.as_object()?;
if ast.get("nodeType")?.as_str()? != "YulFunctionCall" {
return None;
}
if ast
.get("functionName")?
.as_object()?
.get("name")?
.as_str()?
!= "extcodesize"
{
return None;
}
Some(SolcStandardJsonOutputError::message_extcodesize(
ast.get("src")?.as_str(),
))
}
/// Checks the AST node for the `origin` assembly instruction usage.
pub fn check_assembly_origin(ast: &serde_json::Value) -> Option<SolcStandardJsonOutputError> {
let ast = ast.as_object()?;
if ast.get("nodeType")?.as_str()? != "YulFunctionCall" {
return None;
}
if ast
.get("functionName")?
.as_object()?
.get("name")?
.as_str()?
!= "origin"
{
return None;
}
Some(SolcStandardJsonOutputError::message_tx_origin(
ast.get("src")?.as_str(),
))
}
/// Checks the AST node for the `tx.origin` value usage.
pub fn check_tx_origin(ast: &serde_json::Value) -> Option<SolcStandardJsonOutputError> {
let ast = ast.as_object()?;
if ast.get("nodeType")?.as_str()? != "MemberAccess" {
return None;
}
if ast.get("memberName")?.as_str()? != "origin" {
return None;
}
let expression = ast.get("expression")?.as_object()?;
if expression.get("nodeType")?.as_str()? != "Identifier" {
return None;
}
if expression.get("name")?.as_str()? != "tx" {
return None;
}
Some(SolcStandardJsonOutputError::message_tx_origin(
ast.get("src")?.as_str(),
))
}
/// Returns the list of messages for some specific parts of the AST.
#[cfg(feature = "resolc")]
pub fn get_messages(
ast: &serde_json::Value,
suppressed_warnings: &[Warning],
) -> Vec<SolcStandardJsonOutputError> {
let mut messages = Vec::new();
if !suppressed_warnings.contains(&Warning::EcRecover) {
if let Some(message) = Self::check_ecrecover(ast) {
messages.push(message);
}
}
if !suppressed_warnings.contains(&Warning::SendTransfer) {
if let Some(message) = Self::check_send_and_transfer(ast) {
messages.push(message);
}
}
if !suppressed_warnings.contains(&Warning::ExtCodeSize) {
if let Some(message) = Self::check_assembly_extcodesize(ast) {
messages.push(message);
}
}
if !suppressed_warnings.contains(&Warning::TxOrigin) {
if let Some(message) = Self::check_assembly_origin(ast) {
messages.push(message);
}
if let Some(message) = Self::check_tx_origin(ast) {
messages.push(message);
}
}
match ast {
serde_json::Value::Array(array) => {
for element in array.iter() {
messages.extend(Self::get_messages(element, suppressed_warnings));
}
}
serde_json::Value::Object(object) => {
for (_key, value) in object.iter() {
messages.extend(Self::get_messages(value, suppressed_warnings));
}
}
_ => {}
}
messages
}
/// Returns the name of the last contract.
pub fn last_contract_name(&self) -> anyhow::Result<String> {
self.ast
.as_ref()
.ok_or_else(|| anyhow::anyhow!("The AST is empty"))?
.get("nodes")
.and_then(|value| value.as_array())
.ok_or_else(|| {
anyhow::anyhow!("The last contract cannot be found in an empty list of nodes")
})?
.iter()
.filter_map(
|node| match node.get("nodeType").and_then(|node| node.as_str()) {
Some("ContractDefinition") => Some(node.get("name")?.as_str()?.to_owned()),
_ => None,
},
)
.last()
.ok_or_else(|| anyhow::anyhow!("The last contract not found in the AST"))
}
}