diff --git a/Cargo.lock b/Cargo.lock index bc710c0118cd..3e76d2253164 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -390,9 +390,9 @@ dependencies = [ [[package]] name = "cargo_metadata" -version = "0.14.1" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba2ae6de944143141f6155a473a6b02f66c7c3f9f47316f802f80204ebfe6e12" +checksum = "4acbb09d9ee8e23699b9634375c72795d095bf268439da88562cf9b501f181fa" dependencies = [ "camino", "cargo-platform", @@ -1088,7 +1088,7 @@ dependencies = [ [[package]] name = "ethabi" version = "16.0.0" -source = "git+https://github.com/rust-ethereum/ethabi?branch=master#321a651e767d7dbe81e8606cbf42f08dedf58c2b" +source = "git+https://github.com/rust-ethereum/ethabi?branch=master#e161e688f1f567b25e829f62df053cf9bff1303a" dependencies = [ "ethereum-types", "hex", @@ -1154,7 +1154,7 @@ dependencies = [ [[package]] name = "ethers" version = "0.6.0" -source = "git+https://github.com/gakonst/ethers-rs#f97a8ca5410331145cb742d40ebfe4a816dd3190" +source = "git+https://github.com/gakonst/ethers-rs#19d2fd115529ec18652f2ce544a9e52d70047883" dependencies = [ "ethers-addressbook", "ethers-contract", @@ -1169,7 +1169,7 @@ dependencies = [ [[package]] name = "ethers-addressbook" version = "0.1.0" -source = "git+https://github.com/gakonst/ethers-rs#f97a8ca5410331145cb742d40ebfe4a816dd3190" +source = "git+https://github.com/gakonst/ethers-rs#19d2fd115529ec18652f2ce544a9e52d70047883" dependencies = [ "ethers-core", "once_cell", @@ -1180,7 +1180,7 @@ dependencies = [ [[package]] name = "ethers-contract" version = "0.6.0" -source = "git+https://github.com/gakonst/ethers-rs#f97a8ca5410331145cb742d40ebfe4a816dd3190" +source = "git+https://github.com/gakonst/ethers-rs#19d2fd115529ec18652f2ce544a9e52d70047883" dependencies = [ "ethers-contract-abigen", "ethers-contract-derive", @@ -1198,7 +1198,7 @@ dependencies = [ [[package]] name = "ethers-contract-abigen" version = "0.6.0" -source = "git+https://github.com/gakonst/ethers-rs#f97a8ca5410331145cb742d40ebfe4a816dd3190" +source = "git+https://github.com/gakonst/ethers-rs#19d2fd115529ec18652f2ce544a9e52d70047883" dependencies = [ "Inflector", "cfg-if 1.0.0", @@ -1221,7 +1221,7 @@ dependencies = [ [[package]] name = "ethers-contract-derive" version = "0.6.0" -source = "git+https://github.com/gakonst/ethers-rs#f97a8ca5410331145cb742d40ebfe4a816dd3190" +source = "git+https://github.com/gakonst/ethers-rs#19d2fd115529ec18652f2ce544a9e52d70047883" dependencies = [ "ethers-contract-abigen", "ethers-core", @@ -1235,7 +1235,7 @@ dependencies = [ [[package]] name = "ethers-core" version = "0.6.0" -source = "git+https://github.com/gakonst/ethers-rs#f97a8ca5410331145cb742d40ebfe4a816dd3190" +source = "git+https://github.com/gakonst/ethers-rs#19d2fd115529ec18652f2ce544a9e52d70047883" dependencies = [ "arrayvec 0.7.2", "bytes", @@ -1263,7 +1263,7 @@ dependencies = [ [[package]] name = "ethers-etherscan" version = "0.2.0" -source = "git+https://github.com/gakonst/ethers-rs#f97a8ca5410331145cb742d40ebfe4a816dd3190" +source = "git+https://github.com/gakonst/ethers-rs#19d2fd115529ec18652f2ce544a9e52d70047883" dependencies = [ "ethers-core", "ethers-solc", @@ -1277,7 +1277,7 @@ dependencies = [ [[package]] name = "ethers-middleware" version = "0.6.0" -source = "git+https://github.com/gakonst/ethers-rs#f97a8ca5410331145cb742d40ebfe4a816dd3190" +source = "git+https://github.com/gakonst/ethers-rs#19d2fd115529ec18652f2ce544a9e52d70047883" dependencies = [ "async-trait", "ethers-contract", @@ -1300,7 +1300,7 @@ dependencies = [ [[package]] name = "ethers-providers" version = "0.6.0" -source = "git+https://github.com/gakonst/ethers-rs#f97a8ca5410331145cb742d40ebfe4a816dd3190" +source = "git+https://github.com/gakonst/ethers-rs#19d2fd115529ec18652f2ce544a9e52d70047883" dependencies = [ "async-trait", "auto_impl", @@ -1332,7 +1332,7 @@ dependencies = [ [[package]] name = "ethers-signers" version = "0.6.0" -source = "git+https://github.com/gakonst/ethers-rs#f97a8ca5410331145cb742d40ebfe4a816dd3190" +source = "git+https://github.com/gakonst/ethers-rs#19d2fd115529ec18652f2ce544a9e52d70047883" dependencies = [ "async-trait", "coins-bip32", @@ -1354,8 +1354,8 @@ dependencies = [ [[package]] name = "ethers-solc" -version = "0.2.0" -source = "git+https://github.com/gakonst/ethers-rs#f97a8ca5410331145cb742d40ebfe4a816dd3190" +version = "0.3.0" +source = "git+https://github.com/gakonst/ethers-rs#19d2fd115529ec18652f2ce544a9e52d70047883" dependencies = [ "colored", "dunce", diff --git a/cli/src/cmd/build.rs b/cli/src/cmd/build.rs index 3cf4048a4030..18776cf041d2 100644 --- a/cli/src/cmd/build.rs +++ b/cli/src/cmd/build.rs @@ -1,6 +1,6 @@ //! build command -use ethers::solc::{MinimalCombinedArtifacts, Project, ProjectCompileOutput}; +use ethers::solc::{Project, ProjectCompileOutput}; use std::path::PathBuf; use crate::{cmd::Cmd, opts::forge::CompilerArgs}; @@ -152,7 +152,7 @@ pub struct BuildArgs { } impl Cmd for BuildArgs { - type Output = ProjectCompileOutput; + type Output = ProjectCompileOutput; fn run(self) -> eyre::Result { let project = self.project()?; super::compile(&project) @@ -220,8 +220,14 @@ impl Provider for BuildArgs { dict.insert("optimizer".to_string(), self.compiler.optimize.into()); } - if let Some(extra) = &self.compiler.extra_output { - dict.insert("extra_output".to_string(), extra.clone().into()); + if let Some(ref extra) = self.compiler.extra_output { + let selection: Vec<_> = extra.iter().map(|s| s.to_string()).collect(); + dict.insert("extra_output".to_string(), selection.into()); + } + + if let Some(ref extra) = self.compiler.extra_output_files { + let selection: Vec<_> = extra.iter().map(|s| s.to_string()).collect(); + dict.insert("extra_output_files".to_string(), selection.into()); } Ok(Map::from([(Config::selected_profile(), dict)])) diff --git a/cli/src/cmd/mod.rs b/cli/src/cmd/mod.rs index bba661fb2aaf..b10fd632c788 100644 --- a/cli/src/cmd/mod.rs +++ b/cli/src/cmd/mod.rs @@ -68,13 +68,11 @@ pub trait Cmd: clap::Parser + Sized { fn run(self) -> eyre::Result; } -use ethers::solc::{ - artifacts::CompactContractBytecode, MinimalCombinedArtifacts, Project, ProjectCompileOutput, -}; +use ethers::solc::{artifacts::CompactContractBytecode, Project, ProjectCompileOutput}; /// Compiles the provided [`Project`], throws if there's any compiler error and logs whether /// compilation was successful or if there was a cache hit. -pub fn compile(project: &Project) -> eyre::Result> { +pub fn compile(project: &Project) -> eyre::Result { if !project.paths.sources.exists() { eyre::bail!( r#"no contracts to compile, contracts folder "{}" does not exist. @@ -101,9 +99,9 @@ If you are in a subdirectory in a Git repository, try adding `--root .`"#, /// Manually compile a project with added sources pub fn manual_compile( - project: &Project, + project: &Project, added_sources: Vec, -) -> eyre::Result> { +) -> eyre::Result { let mut sources = project.paths.read_input_files()?; sources.extend(Source::read_all_files(added_sources)?); println!("compiling..."); @@ -135,7 +133,7 @@ pub fn manual_compile( /// Runtime Bytecode of the given contract. pub fn read_artifact( project: &Project, - compiled: ProjectCompileOutput, + compiled: ProjectCompileOutput, contract: ContractInfo, ) -> eyre::Result<(Abi, CompactBytecode, CompactDeployedBytecode)> { Ok(match contract.path { @@ -149,7 +147,7 @@ pub fn read_artifact( // contract name? fn get_artifact_from_name( contract: ContractInfo, - compiled: ProjectCompileOutput, + compiled: ProjectCompileOutput, ) -> eyre::Result<(Abi, CompactBytecode, CompactDeployedBytecode)> { let mut has_found_contract = false; let mut contract_artifact = None; diff --git a/cli/src/cmd/run.rs b/cli/src/cmd/run.rs index 44705f80295a..62516be93990 100644 --- a/cli/src/cmd/run.rs +++ b/cli/src/cmd/run.rs @@ -7,7 +7,7 @@ use foundry_utils::IntoFunction; use std::{collections::BTreeMap, path::PathBuf}; use ui::{TUIExitReason, Tui, Ui}; -use ethers::solc::{MinimalCombinedArtifacts, Project}; +use ethers::solc::Project; use crate::opts::evm::EvmArgs; use ansi_term::Colour; @@ -264,7 +264,7 @@ struct ExtraLinkingInfo<'a> { } pub struct BuildOutput { - pub project: Project, + pub project: Project, pub contract: CompactContractBytecode, pub highlevel_known_contracts: BTreeMap, pub sources: BTreeMap, diff --git a/cli/src/opts/forge.rs b/cli/src/opts/forge.rs index f9cc4281c57a..7c8f6b1a1f3a 100644 --- a/cli/src/opts/forge.rs +++ b/cli/src/opts/forge.rs @@ -1,6 +1,6 @@ use clap::{Parser, Subcommand, ValueHint}; -use ethers::solc::EvmVersion; +use ethers::solc::{artifacts::output_selection::ContractOutputSelection, EvmVersion}; use std::{path::PathBuf, str::FromStr}; use crate::cmd::{ @@ -138,11 +138,18 @@ pub struct CompilerArgs { pub optimize_runs: Option, #[clap( - help = "Extra output types [evm.assembly, ewasm, ir, irOptimized] eg: `--extra-output evm.assembly`", + help = "Extra output types to include in the contract's json artifact [evm.assembly, ewasm, ir, irOptimized, metadata] eg: `--extra-output evm.assembly`", long )] #[serde(skip_serializing_if = "Option::is_none")] - pub extra_output: Option>, + pub extra_output: Option>, + + #[clap( + help = "Extra output types to write to a separate file [metadata, ir, irOptimized, ewasm] eg: `--extra-output-files metadata`", + long + )] + #[serde(skip_serializing_if = "Option::is_none")] + pub extra_output_files: Option>, } /// Represents the common dapp argument pattern for `:` where `:` is diff --git a/cli/test-utils/src/util.rs b/cli/test-utils/src/util.rs index 11961e76eaaf..cfc913d77b54 100644 --- a/cli/test-utils/src/util.rs +++ b/cli/test-utils/src/util.rs @@ -1,7 +1,7 @@ use ethers_solc::{ cache::SolFilesCache, project_util::{copy_dir, TempProject}, - ArtifactOutput, MinimalCombinedArtifacts, PathStyle, ProjectPathsConfig, + ArtifactOutput, ConfigurableArtifacts, PathStyle, ProjectPathsConfig, }; use foundry_config::Config; use once_cell::sync::Lazy; @@ -75,7 +75,7 @@ pub fn setup_project(test: TestProject) -> (TestProject, TestCommand) { /// /// Test projects are created from a global atomic counter to avoid duplicates. #[derive(Clone, Debug)] -pub struct TestProject { +pub struct TestProject { /// The directory in which this test executable is running. root: PathBuf, /// The project in which the test should run. @@ -267,6 +267,11 @@ impl TestCommand { self } + /// Resets the command + pub fn fuse(&mut self) -> &mut TestCommand { + self.set_cmd(self.project.bin()) + } + /// Sets the current working directory pub fn set_current_dir(&mut self, p: impl AsRef) { drop(self.current_dir_lock.take()); diff --git a/cli/tests/cmd.rs b/cli/tests/cmd.rs index f97f1c5b7446..f2a1a397e150 100644 --- a/cli/tests/cmd.rs +++ b/cli/tests/cmd.rs @@ -1,4 +1,5 @@ //! Contains various tests for checking forge's commands +use ethers::solc::{artifacts::Metadata, ConfigurableContractArtifact}; use evm_adapters::evm_opts::{EvmOpts, EvmType}; use foundry_cli_test_utils::{ ethers_solc::{remappings::Remapping, PathStyle}, @@ -242,6 +243,25 @@ forgetest!(can_clean_hardhat, PathStyle::HardHat, |prj: TestProject, mut cmd: Te prj.assert_cleaned(); }); +// checks that extra output works +forgetest_init!(can_emit_extra_output, |prj: TestProject, mut cmd: TestCommand| { + cmd.args(["build", "--extra-output", "metadata"]); + cmd.assert_non_empty_stdout(); + + let artifact_path = prj.paths().artifacts.join("Contract.sol/Contract.json"); + let artifact: ConfigurableContractArtifact = + ethers::solc::utils::read_json_file(artifact_path).unwrap(); + assert!(artifact.metadata.is_some()); + + cmd.fuse() + .args(["build", "--extra-output-files", "metadata", "--force", "--root"]) + .arg(prj.root()); + cmd.assert_non_empty_stdout(); + + let metadata_path = prj.paths().artifacts.join("Contract.sol/Contract.metadata.json"); + let artifact: Metadata = ethers::solc::utils::read_json_file(metadata_path).unwrap(); +}); + // test against a local checkout, useful to debug with local ethers-rs patch forgetest_ignore!(can_compile_local_spells, |_: TestProject, mut cmd: TestCommand| { let current_dir = std::env::current_dir().unwrap(); diff --git a/config/src/lib.rs b/config/src/lib.rs index b0bcbe46064d..885d826853ae 100644 --- a/config/src/lib.rs +++ b/config/src/lib.rs @@ -1,7 +1,6 @@ //! foundry configuration. use std::{ borrow::Cow, - collections::BTreeMap, path::{Path, PathBuf}, }; @@ -17,10 +16,10 @@ use serde::{Deserialize, Serialize}; use ethers_core::types::{Address, U256}; use ethers_solc::{ - artifacts::{Optimizer, OptimizerDetails, Settings}, + artifacts::{output_selection::ContractOutputSelection, Optimizer, OptimizerDetails, Settings}, error::SolcError, remappings::{RelativeRemapping, Remapping}, - EvmVersion, Project, ProjectPathsConfig, SolcConfig, + ConfigurableArtifacts, EvmVersion, Project, ProjectPathsConfig, SolcConfig, }; use figment::{providers::Data, value::Value}; use inflector::Inflector; @@ -152,26 +151,32 @@ pub struct Config { pub block_difficulty: u64, /// the `block.gaslimit` value during EVM execution pub block_gas_limit: Option, - /// Pass extra output types - pub extra_output: Option>, - /// Settings to pass to the `solc` compiler input - // TODO consider making this more structured https://stackoverflow.com/questions/48998034/does-toml-support-nested-arrays-of-objects-tables - // TODO this needs to work as extension to the defaults: + /// Additional output selection for all contracts + /// such as "ir", "devodc", "storageLayout", etc. + /// See [Solc Compiler Api](https://docs.soliditylang.org/en/latest/using-the-compiler.html#compiler-api) + /// + /// The following values are always set because they're required by `forge` //{ - // "*": { - // "": [ - // "ast" - // ], - // "*": [ + // "*": [ // "abi", // "evm.bytecode", // "evm.deployedBytecode", // "evm.methodIdentifiers" // ] - // } // } // "# - pub solc_settings: Option, + #[serde(default)] + pub extra_output: Vec, + /// If set , a separate `json` file will be emitted for every contract depending on the + /// selection, eg. `extra_output_files = ["metadata"]` will create a `metadata.json` for + /// each contract in the project. See [Contract Metadata](https://docs.soliditylang.org/en/latest/metadata.html) + /// + /// The difference between `extra_output = ["metadata"]` and + /// `extra_output_files = ["metadata]` is that the former will include the + /// contract's metadata in the contract's json artifact, whereas the latter will emit the + /// output selection as separate files. + #[serde(default)] + pub extra_output_files: Vec, /// The maximum number of local test case rejections allowed /// by proptest, to be encountered during usage of `vm.assume` /// cheatcode. @@ -366,6 +371,7 @@ impl Config { fn create_project(&self, cached: bool, no_artifacts: bool) -> Result { let project = Project::builder() + .artifacts(self.configured_artifacts_handler()) .paths(self.project_paths()) .allowed_path(&self.__root.0) .allowed_paths(&self.libs) @@ -414,18 +420,10 @@ impl Config { } } - pub fn output_selection(&self) -> BTreeMap>> { - let mut output_selection = Settings::default_output_selection(); - - if let Some(extras) = &self.extra_output { - output_selection.entry("*".to_string()).and_modify(|e1| { - e1.entry("*".to_string()).and_modify(|e2| { - e2.extend_from_slice(extras.as_slice()); - }); - }); - } - - output_selection + /// returns the [`ethers_solc::ConfigurableArtifacts`] for this config, that includes the + /// `extra_output` fields + pub fn configured_artifacts_handler(&self) -> ConfigurableArtifacts { + ConfigurableArtifacts::new(self.extra_output.clone(), self.extra_output_files.clone()) } /// Returns the configured `solc` `Settings` that includes: @@ -435,16 +433,17 @@ impl Config { pub fn solc_settings(&self) -> Result { let libraries = parse_libraries(&self.libraries)?; let optimizer = self.optimizer(); - let output_selection = self.output_selection(); - Ok((Settings { + let settings = Settings { optimizer, evm_version: Some(self.evm_version), libraries, - output_selection, ..Default::default() - }) - .with_ast()) + } + .with_extra_output(self.configured_artifacts_handler().output_selection()) + .with_ast(); + + Ok(settings) } /// Returns the default figment @@ -724,8 +723,8 @@ impl Default for Config { optimizer: true, optimizer_runs: 200, optimizer_details: None, - extra_output: None, - solc_settings: None, + extra_output: Default::default(), + extra_output_files: Default::default(), fuzz_runs: 256, fuzz_max_local_rejects: 1024, fuzz_max_global_rejects: 65536, @@ -1290,6 +1289,30 @@ mod tests { }); } + #[test] + fn test_output_selection() { + figment::Jail::expect_with(|jail| { + jail.create_file( + "foundry.toml", + r#" + [default] + extra_output = ["metadata", "ir-optimized"] + extra_output_files = ["metadata"] + "#, + )?; + + let config = Config::load(); + + assert_eq!( + config.extra_output, + vec![ContractOutputSelection::Metadata, ContractOutputSelection::IrOptimized] + ); + assert_eq!(config.extra_output_files, vec![ContractOutputSelection::Metadata]); + + Ok(()) + }); + } + #[test] fn test_precedence() { figment::Jail::expect_with(|jail| {