use std::path::PathBuf;
use eyre::{OptionExt, Result};
use foundry_compilers::{
artifacts::{
output_selection::OutputSelection, Ast, Node, NodeType, Settings, Severity, Source,
SourceUnit, Sources,
},
solc::{SolcCompiler, SolcLanguage, SolcSettings, SolcVersionedInput},
CompilationError, Compiler, CompilerInput,
};
use semver::Version;
pub fn compile_contract_source_to_source_unit(
solc_version: Version,
source: &str,
prune: bool,
) -> Result<SourceUnit> {
let phantom_file_name = PathBuf::from("Contract.sol");
let sources = Sources::from_iter([(phantom_file_name.clone(), Source::new(source))]);
let settings = SolcSettings {
settings: Settings::new(OutputSelection::complete_output_selection()),
cli_settings: Default::default(),
};
let solc_input =
SolcVersionedInput::build(sources, settings, SolcLanguage::Solidity, solc_version);
let compiler = SolcCompiler::AutoDetect;
let output = compiler.compile(&solc_input)?;
let errors = output
.errors
.iter()
.filter(|e| e.severity() == Severity::Error)
.map(|e| format!("{e}"))
.collect::<Vec<_>>();
if !errors.is_empty() {
return Err(eyre::eyre!("Compiler error: {}", errors.join("\n")));
}
let mut ast = output
.sources
.get(&phantom_file_name)
.expect("No AST found")
.ast
.clone()
.expect("AST is not selected as output");
let source_unit = ASTPruner::convert(&mut ast, prune)?;
Ok(source_unit)
}
pub struct ASTPruner {}
impl ASTPruner {
pub fn convert(ast: &mut Ast, prune: bool) -> Result<SourceUnit> {
if prune {
Self::prune(ast)?;
}
let serialized = serde_json::to_string(ast)?;
Ok(serde_json::from_str(&serialized)?)
}
fn prune(ast: &mut Ast) -> Result<()> {
for node in ast.nodes.iter_mut() {
Self::prune_node(node)?;
}
for (field, value) in ast.other.iter_mut() {
if field == "documentation" {
*value = serde_json::Value::Null;
} else {
Self::prune_value(value)?;
}
}
Ok(())
}
fn prune_node(node: &mut Node) -> Result<()> {
if matches!(node.node_type, NodeType::InlineAssembly) && !node.other.contains_key("AST") {
let ast = serde_json::json!({
"nodeType": "YulBlock",
"src": node.src,
"statements": [],
});
node.other.insert("AST".to_string(), ast);
node.other.insert("externalReferences".to_string(), serde_json::json!([]));
node.other.remove("operations");
}
if matches!(node.node_type, NodeType::ImportDirective) {
node.other.insert("symbolAliases".to_string(), serde_json::json!([]));
}
for (field, value) in node.other.iter_mut() {
if field == "documentation" {
*value = serde_json::Value::Null;
} else {
Self::prune_value(value)?;
}
}
if let Some(body) = &mut node.body {
Self::prune_node(body)?;
}
for node in node.nodes.iter_mut() {
Self::prune_node(node)?;
}
Ok(())
}
fn prune_value(value: &mut serde_json::Value) -> Result<()> {
match value {
serde_json::Value::Object(obj) => {
if let Some(node_type) = obj.get("nodeType") {
if node_type.as_str() == Some("InlineAssembly") {
if !obj.contains_key("AST") {
let ast = serde_json::json!({
"nodeType": "YulBlock",
"src": obj.get("src").ok_or_eyre("missing src")?.clone(),
"statements": [],
});
obj.insert("AST".to_string(), ast);
}
obj.insert("externalReferences".to_string(), serde_json::json!([]));
obj.remove("operations");
}
}
if let Some(node_type) = obj.get("nodeType") {
if node_type.as_str() == Some("ImportDirective") {
obj.insert("symbolAliases".to_string(), serde_json::json!([]));
}
}
for (field, value) in obj.iter_mut() {
if field == "documentation" {
*value = serde_json::Value::Null;
} else {
Self::prune_value(value)?;
}
}
}
serde_json::Value::Array(arr) => {
for value in arr.iter_mut() {
Self::prune_value(value)?;
}
}
_ => {}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::{path::PathBuf, str::FromStr, time::Duration};
use alloy_chains::Chain;
use alloy_primitives::Address;
use eyre::Result;
use foundry_block_explorers::Client;
use crate::utils::OnchainCompiler;
use super::*;
async fn download_and_compile(chain: Chain, addr: Address) -> Result<()> {
let cache_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../testdata/cache/etherscan")
.join(chain.to_string());
let cache_ttl = Duration::from_secs(u32::MAX as u64); let client =
Client::builder().chain(chain)?.with_cache(Some(cache_root), cache_ttl).build()?;
let compiler_cache_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../testdata/cache/solc")
.join(chain.to_string());
let compiler = OnchainCompiler::new(Some(compiler_cache_root))?;
let mut artifact =
compiler.compile(&client, addr).await?.ok_or_eyre("missing compiler output")?;
for (_, contract) in artifact.output.sources.iter_mut() {
ASTPruner::convert(contract.ast.as_mut().ok_or_eyre("AST does not exist")?, true)?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_solidity_external_library() {
let addr = Address::from_str("0x0F6E8eF18FB5bb61D545fEe60f779D8aED60408F").unwrap();
download_and_compile(Chain::default(), addr).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn test_solidity_v0_8_18() {
let addr = Address::from_str("0xe45dfc26215312edc131e34ea9299fbca53275ca").unwrap();
download_and_compile(Chain::default(), addr).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn test_solidity_v0_8_17() {
let addr = Address::from_str("0x1111111254eeb25477b68fb85ed929f73a960582").unwrap();
download_and_compile(Chain::default(), addr).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn test_solidity_v0_7_6() {
let addr = Address::from_str("0x1f98431c8ad98523631ae4a59f267346ea31f984").unwrap();
download_and_compile(Chain::default(), addr).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn test_solidity_v0_6_12() {
let addr = Address::from_str("0x1eb4cf3a948e7d72a198fe073ccb8c7a948cd853").unwrap();
download_and_compile(Chain::default(), addr).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn test_solidity_v0_5_17() {
let addr = Address::from_str("0xee39E4A6820FFc4eDaA80fD3b5A59788D515832b").unwrap();
download_and_compile(Chain::default(), addr).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn test_solidity_v0_4_24() {
let addr = Address::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
download_and_compile(Chain::default(), addr).await.unwrap();
}
#[test]
fn test_compile_contract_source() {
let source_code = r#"
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SimpleStorage {
uint256 private storedData;
function set(uint256 x) public {
storedData = x;
}
function get() public view returns (uint256) {
return storedData;
}
}
"#;
let solc_version = Version::parse("0.8.0").expect("Invalid version");
let result = compile_contract_source_to_source_unit(solc_version, source_code, true);
assert!(result.is_ok(), "Compilation failed: {result:?}");
let source_unit = result.unwrap();
assert!(!source_unit.nodes.is_empty(), "No AST nodes found in source unit");
}
}