From 0c2b9c8e8d05a3cd9493e1953ba9278fb9ca6148 Mon Sep 17 00:00:00 2001 From: Brock Date: Sun, 30 Jan 2022 12:46:26 -0500 Subject: [PATCH 1/5] add gas reports --- cli/src/cmd/test.rs | 43 +++++++++- config/src/lib.rs | 3 + evm-adapters/Cargo.toml | 1 + evm-adapters/src/gas_report.rs | 145 +++++++++++++++++++++++++++++++++ evm-adapters/src/lib.rs | 2 + 5 files changed, 191 insertions(+), 3 deletions(-) create mode 100644 evm-adapters/src/gas_report.rs diff --git a/cli/src/cmd/test.rs b/cli/src/cmd/test.rs index 6cd4dba0ef72f..1d1dfb330dbd7 100644 --- a/cli/src/cmd/test.rs +++ b/cli/src/cmd/test.rs @@ -7,7 +7,9 @@ use crate::{ use ansi_term::Colour; use clap::{AppSettings, Parser}; use ethers::solc::{ArtifactOutput, Project}; -use evm_adapters::{call_tracing::ExecutionInfo, evm_opts::EvmOpts, sputnik::helpers::vm}; +use evm_adapters::{ + call_tracing::ExecutionInfo, evm_opts::EvmOpts, gas_report::GasReport, sputnik::helpers::vm, +}; use forge::{MultiContractRunnerBuilder, TestFilter}; use foundry_config::{figment::Figment, Config}; use std::collections::BTreeMap; @@ -88,6 +90,9 @@ pub struct TestArgs { #[clap(help = "print the test results in json format", long, short)] json: bool, + #[clap(help = "print a gas report", long = "gas-report")] + gas_report: bool, + #[clap(flatten)] evm_opts: EvmArgs, @@ -138,7 +143,15 @@ impl Cmd for TestArgs { .evm_cfg(evm_cfg) .sender(evm_opts.sender); - test(builder, project, evm_opts, filter, json, allow_failure) + test( + builder, + project, + evm_opts, + filter, + json, + allow_failure, + (self.gas_report, config.gas_reports), + ) } } @@ -251,16 +264,26 @@ fn short_test_result(name: &str, result: &forge::TestResult) { fn test( builder: MultiContractRunnerBuilder, project: Project, - evm_opts: EvmOpts, + mut evm_opts: EvmOpts, filter: Filter, json: bool, allow_failure: bool, + gas_reports: (bool, Vec), ) -> eyre::Result { let verbosity = evm_opts.verbosity; + let gas_reporting = gas_reports.0; + + if gas_reporting && evm_opts.verbosity < 3 { + // force evm to do tracing, but dont hit the verbosity print path + evm_opts.verbosity = 3; + } + let mut runner = builder.build(project, evm_opts)?; let results = runner.test(&filter)?; + let mut gas_report = GasReport::new(gas_reports.1); + let (funcs, events, errors) = runner.execution_info; if json { let res = serde_json::to_string(&results)?; @@ -277,6 +300,15 @@ fn test( } for (name, result) in tests { + // build up gas report + if gas_reporting { + if let (Some(traces), Some(identified_contracts)) = + (&result.traces, &result.identified_contracts) + { + gas_report.analyze(traces, identified_contracts); + } + } + short_test_result(name, result); // adds a linebreak only if there were any traces or logs, so that the @@ -339,5 +371,10 @@ fn test( } } + if gas_reporting { + gas_report.finalize(); + println!("{}", gas_report); + } + Ok(TestOutcome::new(results, allow_failure)) } diff --git a/config/src/lib.rs b/config/src/lib.rs index a7995b291f8af..8bb59d27f0d77 100644 --- a/config/src/lib.rs +++ b/config/src/lib.rs @@ -91,6 +91,8 @@ pub struct Config { /// evm version to use #[serde(with = "from_str_lowercase")] pub evm_version: EvmVersion, + /// list of contracts to report gas of + pub gas_reports: Vec, /// Concrete solc version to use if any. /// /// This takes precedence over `auto_detect_solc`, if a version is set then this overrides @@ -638,6 +640,7 @@ impl Default for Config { cache: true, force: false, evm_version: Default::default(), + gas_reports: vec![], solc_version: None, auto_detect_solc: true, optimizer: true, diff --git a/evm-adapters/Cargo.toml b/evm-adapters/Cargo.toml index 91e25ab4b7c9a..142a6e475e6d1 100644 --- a/evm-adapters/Cargo.toml +++ b/evm-adapters/Cargo.toml @@ -29,6 +29,7 @@ revm_precompiles = { git = "https://github.com/bluealloy/revm", default-features serde_json = "1.0.72" serde = "1.0.130" ansi_term = "0.12.1" +comfy-table = "5.0.0" [dev-dependencies] evmodin = { git = "https://github.com/vorot93/evmodin", features = ["util"] } diff --git a/evm-adapters/src/gas_report.rs b/evm-adapters/src/gas_report.rs new file mode 100644 index 0000000000000..81ad5dc594c25 --- /dev/null +++ b/evm-adapters/src/gas_report.rs @@ -0,0 +1,145 @@ +use crate::CallTraceArena; +use ethers::{ + abi::Abi, + types::{H160, U256}, +}; +use serde::{Deserialize, Serialize}; +use std::{collections::BTreeMap, fmt::Display}; + +use comfy_table::{modifiers::UTF8_ROUND_CORNERS, presets::UTF8_FULL, *}; + +#[derive(Default, Debug, Serialize, Deserialize)] +pub struct GasReport { + pub report_for: Vec, + pub contracts: BTreeMap, +} + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct ContractInfo { + pub gas: U256, + pub size: U256, + pub functions: BTreeMap, +} + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct GasInfo { + pub calls: Vec, + pub min: U256, + pub mean: U256, + pub median: U256, + pub max: U256, +} + +impl GasReport { + pub fn new(report_for: Vec) -> Self { + Self { report_for, ..Default::default() } + } + + pub fn analyze( + &mut self, + traces: &[CallTraceArena], + identified_contracts: &BTreeMap, + ) { + let report_for_all = self.report_for.iter().any(|s| s == "*"); + traces.iter().for_each(|trace| { + self.analyze_trace(trace, identified_contracts, report_for_all); + }); + } + + fn analyze_trace( + &mut self, + trace: &CallTraceArena, + identified_contracts: &BTreeMap, + report_for_all: bool, + ) { + self.analyze_node(trace.entry, trace, identified_contracts, report_for_all); + } + + fn analyze_node( + &mut self, + node_index: usize, + arena: &CallTraceArena, + identified_contracts: &BTreeMap, + report_for_all: bool, + ) { + let node = &arena.arena[node_index]; + let trace = &node.trace; + if let Some((name, abi)) = identified_contracts.get(&trace.addr) { + if self.report_for.iter().any(|s| s == name) || report_for_all { + // report for this contract + let mut contract = + self.contracts.entry(name.to_string()).or_insert_with(Default::default); + + if trace.created { + contract.gas = trace.cost.into(); + contract.size = trace.data.len().into(); + } else { + let func = + abi.functions().find(|func| func.short_signature() == trace.data[0..4]); + + if let Some(func) = func { + let function = contract + .functions + .entry(func.name.clone()) + .or_insert_with(Default::default); + function.calls.push(trace.cost.into()); + } + } + } + } + node.children.iter().for_each(|index| { + self.analyze_node(*index, arena, identified_contracts, report_for_all); + }); + } + + pub fn finalize(&mut self) { + self.contracts.iter_mut().for_each(|(_name, contract)| { + contract.functions.iter_mut().for_each(|(_name, func)| { + func.calls.sort(); + func.min = func.calls.first().cloned().unwrap_or_default(); + func.max = func.calls.last().cloned().unwrap_or_default(); + func.mean = + func.calls.iter().fold(U256::zero(), |acc, x| acc + x) / func.calls.len(); + func.median = func.calls[func.calls.len() / 2]; + }); + }); + } +} + +impl Display for GasReport { + fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + for (name, contract) in self.contracts.iter() { + let mut table = Table::new(); + table.load_preset(UTF8_FULL).apply_modifier(UTF8_ROUND_CORNERS); + table.set_header(vec![Cell::new(format!("{} contract", name)) + .add_attribute(Attribute::Bold) + .fg(Color::Green)]); + table.add_row(vec![ + Cell::new("Deployment Cost").add_attribute(Attribute::Bold).fg(Color::Cyan), + Cell::new("Deployment Size").add_attribute(Attribute::Bold).fg(Color::Cyan), + ]); + table.add_row(vec![contract.gas.to_string(), contract.size.to_string()]); + + table.add_row(vec![ + Cell::new("Function Name").add_attribute(Attribute::Bold).fg(Color::Magenta), + Cell::new("min").add_attribute(Attribute::Bold).fg(Color::Green), + Cell::new("avg").add_attribute(Attribute::Bold).fg(Color::Yellow), + Cell::new("median").add_attribute(Attribute::Bold).fg(Color::Yellow), + Cell::new("max").add_attribute(Attribute::Bold).fg(Color::Red), + Cell::new("# calls").add_attribute(Attribute::Bold), + ]); + contract.functions.iter().for_each(|(fname, function)| { + table.add_row(vec![ + Cell::new(fname.to_string()).add_attribute(Attribute::Bold), + Cell::new(function.min.to_string()).fg(Color::Green), + Cell::new(function.mean.to_string()).fg(Color::Yellow), + Cell::new(function.median.to_string()).fg(Color::Yellow), + Cell::new(function.max.to_string()).fg(Color::Red), + Cell::new(function.calls.len().to_string()), + ]); + }); + writeln!(f, "{}", table)? + } + Ok(()) + } +} diff --git a/evm-adapters/src/lib.rs b/evm-adapters/src/lib.rs index c36fc376a67f8..dfb453ffaeb6f 100644 --- a/evm-adapters/src/lib.rs +++ b/evm-adapters/src/lib.rs @@ -18,6 +18,8 @@ pub mod fuzz; pub mod call_tracing; +pub mod gas_report; + /// Helpers for easily constructing EVM objects. pub mod evm_opts; From ad4c602081dfeb950c5c8635ed863a3d85d89f73 Mon Sep 17 00:00:00 2001 From: Brock Date: Sun, 30 Jan 2022 13:33:52 -0500 Subject: [PATCH 2/5] filter out tests, default all contracts for gas report --- config/src/lib.rs | 2 +- evm-adapters/src/gas_report.rs | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/config/src/lib.rs b/config/src/lib.rs index 8bb59d27f0d77..32f0ded33c1d5 100644 --- a/config/src/lib.rs +++ b/config/src/lib.rs @@ -640,7 +640,7 @@ impl Default for Config { cache: true, force: false, evm_version: Default::default(), - gas_reports: vec![], + gas_reports: vec!["*".to_string()], solc_version: None, auto_detect_solc: true, optimizer: true, diff --git a/evm-adapters/src/gas_report.rs b/evm-adapters/src/gas_report.rs index 81ad5dc594c25..40550e6568383 100644 --- a/evm-adapters/src/gas_report.rs +++ b/evm-adapters/src/gas_report.rs @@ -65,7 +65,10 @@ impl GasReport { let node = &arena.arena[node_index]; let trace = &node.trace; if let Some((name, abi)) = identified_contracts.get(&trace.addr) { - if self.report_for.iter().any(|s| s == name) || report_for_all { + let report_for = self.report_for.iter().any(|s| s == name); + if !report_for && abi.functions().any(|func| func.name == "IS_TEST") { + // do nothing + } else if report_for || report_for_all { // report for this contract let mut contract = self.contracts.entry(name.to_string()).or_insert_with(Default::default); @@ -73,7 +76,7 @@ impl GasReport { if trace.created { contract.gas = trace.cost.into(); contract.size = trace.data.len().into(); - } else { + } else if trace.data.len() >= 4 { let func = abi.functions().find(|func| func.short_signature() == trace.data[0..4]); From cb3019f3bda9a0ef6d1020b41dacf01545203c39 Mon Sep 17 00:00:00 2001 From: Brock Date: Sun, 30 Jan 2022 14:19:31 -0500 Subject: [PATCH 3/5] add aliases for test commands, have empty mean report_all --- cli/src/cmd/test.rs | 4 ++++ evm-adapters/src/gas_report.rs | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/cli/src/cmd/test.rs b/cli/src/cmd/test.rs index 1d1dfb330dbd7..81e67bf28c870 100644 --- a/cli/src/cmd/test.rs +++ b/cli/src/cmd/test.rs @@ -25,6 +25,7 @@ pub struct Filter { #[clap( long = "match-test", + alias = "mt", help = "only run test methods matching regex", conflicts_with = "pattern" )] @@ -32,6 +33,7 @@ pub struct Filter { #[clap( long = "no-match-test", + alias = "nmt", help = "only run test methods not matching regex", conflicts_with = "pattern" )] @@ -39,6 +41,7 @@ pub struct Filter { #[clap( long = "match-contract", + alias = "mc", help = "only run test methods in contracts matching regex", conflicts_with = "pattern" )] @@ -46,6 +49,7 @@ pub struct Filter { #[clap( long = "no-match-contract", + alias = "nmc", help = "only run test methods in contracts not matching regex", conflicts_with = "pattern" )] diff --git a/evm-adapters/src/gas_report.rs b/evm-adapters/src/gas_report.rs index 40550e6568383..c0c17f1432c99 100644 --- a/evm-adapters/src/gas_report.rs +++ b/evm-adapters/src/gas_report.rs @@ -40,7 +40,7 @@ impl GasReport { traces: &[CallTraceArena], identified_contracts: &BTreeMap, ) { - let report_for_all = self.report_for.iter().any(|s| s == "*"); + let report_for_all = self.report_for.is_empty() || self.report_for.iter().any(|s| s == "*"); traces.iter().for_each(|trace| { self.analyze_trace(trace, identified_contracts, report_for_all); }); From 01c33cc4d849b8ab3f6faf4759f8fb2a50a9c8d9 Mon Sep 17 00:00:00 2001 From: Brock Date: Sun, 30 Jan 2022 14:23:19 -0500 Subject: [PATCH 4/5] update config readme --- config/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/config/README.md b/config/README.md index 5a9f7a3ba52fe..2ed1465adad04 100644 --- a/config/README.md +++ b/config/README.md @@ -69,6 +69,7 @@ libraries = [] cache = true force = false evm_version = 'london' +gas_reports = ["*"] ## Sets the concrete solc version to use, this overrides the `auto_detect_solc` value # solc_version = '0.8.10' auto_detect_solc = true From 2d1713f4052cae92a178a3c8e3bacb7ad6a4e359 Mon Sep 17 00:00:00 2001 From: Brock Date: Sun, 30 Jan 2022 14:45:56 -0500 Subject: [PATCH 5/5] no vm report + correct median calc --- evm-adapters/src/gas_report.rs | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/evm-adapters/src/gas_report.rs b/evm-adapters/src/gas_report.rs index c0c17f1432c99..0fac08ee93796 100644 --- a/evm-adapters/src/gas_report.rs +++ b/evm-adapters/src/gas_report.rs @@ -6,6 +6,9 @@ use ethers::{ use serde::{Deserialize, Serialize}; use std::{collections::BTreeMap, fmt::Display}; +#[cfg(feature = "sputnik")] +use crate::sputnik::cheatcodes::cheatcode_handler::{CHEATCODE_ADDRESS, CONSOLE_ADDRESS}; + use comfy_table::{modifiers::UTF8_ROUND_CORNERS, presets::UTF8_FULL, *}; #[derive(Default, Debug, Serialize, Deserialize)] @@ -64,6 +67,12 @@ impl GasReport { ) { let node = &arena.arena[node_index]; let trace = &node.trace; + + #[cfg(feature = "sputnik")] + if trace.addr == *CHEATCODE_ADDRESS || trace.addr == *CONSOLE_ADDRESS { + return + } + if let Some((name, abi)) = identified_contracts.get(&trace.addr) { let report_for = self.report_for.iter().any(|s| s == name); if !report_for && abi.functions().any(|func| func.name == "IS_TEST") { @@ -103,7 +112,17 @@ impl GasReport { func.max = func.calls.last().cloned().unwrap_or_default(); func.mean = func.calls.iter().fold(U256::zero(), |acc, x| acc + x) / func.calls.len(); - func.median = func.calls[func.calls.len() / 2]; + + let len = func.calls.len(); + func.median = if len > 0 { + if len % 2 == 0 { + (func.calls[len / 2 - 1] + func.calls[len / 2]) / 2 + } else { + func.calls[len / 2] + } + } else { + 0.into() + }; }); }); }