1use crate::{
4 constants::{CHEATCODE_ADDRESS, HARDHAT_CONSOLE_ADDRESS},
5 traces::{CallTraceArena, CallTraceDecoder, CallTraceNode},
6};
7use alloy_primitives::{Address, map::HashSet};
8use comfy_table::{
9 Cell, CellAlignment, Color, Table,
10 presets::{ASCII_FULL, ASCII_MARKDOWN},
11};
12use foundry_common::{TestFunctionExt, calc, get_contract_name, shell};
13use foundry_evm::traces::CallKind;
14use serde::{Deserialize, Serialize};
15use serde_json::json;
16use std::{collections::BTreeMap, fmt::Display};
17
18#[derive(Clone, Debug, Default, Serialize, Deserialize)]
20pub struct GasReport {
21 report_any: bool,
23 report_for: HashSet<String>,
25 ignore: HashSet<String>,
27 include_tests: bool,
29 #[serde(skip)]
31 extra_cheatcode_addresses: HashSet<Address>,
32 pub contracts: BTreeMap<String, ContractInfo>,
35}
36
37impl GasReport {
38 pub fn new(
39 report_for: impl IntoIterator<Item = String>,
40 ignore: impl IntoIterator<Item = String>,
41 include_tests: bool,
42 extra_cheatcode_addresses: impl IntoIterator<Item = Address>,
43 ) -> Self {
44 let report_for = report_for.into_iter().collect::<HashSet<_>>();
45 let report_any = report_for.is_empty() || report_for.contains("*");
46 Self {
47 report_any,
48 report_for,
49 ignore: ignore.into_iter().collect(),
50 include_tests,
51 extra_cheatcode_addresses: extra_cheatcode_addresses.into_iter().collect(),
52 contracts: BTreeMap::new(),
53 }
54 }
55
56 #[instrument(level = "trace", skip(self), ret)]
58 fn should_report(&self, contract_name: &str) -> bool {
59 if self.ignore.contains(contract_name) {
60 let contains_anyway = self.report_for.contains(contract_name);
61 if contains_anyway {
62 let _ = sh_warn!(
68 "{contract_name} is listed in both 'gas_reports' and 'gas_reports_ignore'."
69 );
70 }
71 return contains_anyway;
72 }
73 self.report_any || self.report_for.contains(contract_name)
74 }
75
76 fn is_internal_address(&self, address: Address) -> bool {
77 address == CHEATCODE_ADDRESS
78 || address == HARDHAT_CONSOLE_ADDRESS
79 || self.extra_cheatcode_addresses.contains(&address)
80 }
81
82 pub async fn analyze(
84 &mut self,
85 arenas: impl IntoIterator<Item = &CallTraceArena>,
86 decoder: &CallTraceDecoder,
87 ) {
88 for arena in arenas {
89 let nodes = arena.nodes();
90 for node in nodes {
91 let deploy_code = match node.parent.map(|parent| &nodes[parent].trace) {
93 Some(parent)
94 if node.trace.kind.is_any_create()
95 && parent.address == CHEATCODE_ADDRESS =>
96 {
97 decoder
98 .decode_function_signature(parent)
99 .await
100 .is_some_and(|signature| signature.starts_with("deployCode("))
101 }
102 _ => false,
103 };
104 self.analyze_node(node, deploy_code, decoder).await;
105 }
106 }
107 }
108
109 async fn analyze_node(
110 &mut self,
111 node: &CallTraceNode,
112 deploy_code: bool,
113 decoder: &CallTraceDecoder,
114 ) {
115 let trace = &node.trace;
116 if self.is_internal_address(trace.address) {
117 return;
118 }
119 let Some(name) = decoder.contracts.get(&trace.address) else { return };
120 let contract_name = get_contract_name(name);
121 if !self.should_report(contract_name) {
122 return;
123 }
124
125 let contract_info = self.contracts.entry(name.clone()).or_default();
126 let is_create_call = trace.kind.is_any_create();
127 if is_create_call {
128 trace!(contract_name, "adding create size info");
129 contract_info.size = trace.data.len();
130 }
131
132 let depth = trace.depth - usize::from(deploy_code);
136 if depth > 1 && (trace.kind == CallKind::Call || is_create_call) {
137 return;
138 }
139
140 if is_create_call {
141 trace!(contract_name, "adding create gas info");
142 contract_info.gas = trace.gas_used;
143 } else if let Some(signature) = decoder.decode_function_signature(trace).await {
144 let name = signature.split('(').next().unwrap();
145 if self.include_tests || !name.test_function_kind().is_known() {
147 trace!(contract_name, signature, "adding gas info");
148 contract_info
149 .functions
150 .entry(name.to_string())
151 .or_default()
152 .entry(signature)
153 .or_default()
154 .frames
155 .push(trace.gas_used);
156 }
157 }
158 }
159
160 #[must_use]
162 pub fn finalize(mut self) -> Self {
163 trace!("finalizing gas report");
164 for func in self
165 .contracts
166 .values_mut()
167 .flat_map(|c| c.functions.values_mut().flat_map(|s| s.values_mut()))
168 {
169 func.frames.sort_unstable();
170 func.min = func.frames.first().copied().unwrap_or_default();
171 func.max = func.frames.last().copied().unwrap_or_default();
172 func.mean = calc::mean(&func.frames);
173 func.median = calc::median_sorted(&func.frames);
174 func.calls = func.frames.len() as u64;
175 }
176 self
177 }
178
179 fn reported_contracts(&self) -> impl Iterator<Item = (&String, &ContractInfo)> {
181 self.contracts.iter().filter(|(name, contract)| {
182 if contract.functions.is_empty() {
183 trace!(name, "gas report contract without functions");
184 }
185 !contract.functions.is_empty()
186 })
187 }
188}
189
190impl Display for GasReport {
191 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
192 if shell::is_json() {
193 return writeln!(f, "{}", self.format_json_output());
194 }
195 for (name, contract) in self.reported_contracts() {
196 writeln!(f, "\n{}", format_table_output(contract, name))?;
197 }
198 Ok(())
199 }
200}
201
202impl GasReport {
203 fn format_json_output(&self) -> String {
204 let contracts = self
205 .reported_contracts()
206 .map(|(name, contract)| {
207 let functions = contract
208 .functions
209 .values()
210 .flat_map(|sigs| sigs.iter().map(|(sig, info)| (sig.replace(':', ""), info)))
211 .collect::<BTreeMap<_, _>>();
212 json!({
213 "contract": name,
214 "deployment": { "gas": contract.gas, "size": contract.size },
215 "functions": functions,
216 })
217 })
218 .collect::<Vec<_>>();
219 serde_json::to_string(&contracts).unwrap()
220 }
221}
222
223fn format_table_output(contract: &ContractInfo, name: &str) -> Table {
224 let num = |value: &dyn Display, color: Option<Color>| {
225 let cell = Cell::new(value.to_string()).set_alignment(CellAlignment::Right);
226 match color {
227 Some(color) => cell.fg(color),
228 None => cell,
229 }
230 };
231
232 let mut table = Table::new();
233 if shell::is_markdown() {
234 table.load_style(ASCII_MARKDOWN);
235 } else {
236 table.load_style(ASCII_FULL.with_rounded_corners());
237 }
238 table.set_header(vec![Cell::new(format!("{name} Contract")).fg(Color::Magenta)]);
239 table.add_row(vec![
240 Cell::new("Deployment Cost").fg(Color::Cyan),
241 Cell::new("Deployment Size").fg(Color::Cyan),
242 ]);
243 table.add_row(vec![num(&contract.gas, None), num(&contract.size, None)]);
244 table.add_row(vec![Cell::new("")]);
246 table.add_row(vec![
247 Cell::new("Function Name"),
248 Cell::new("Min").fg(Color::Green),
249 Cell::new("Avg").fg(Color::Yellow),
250 Cell::new("Median").fg(Color::Yellow),
251 Cell::new("Max").fg(Color::Red),
252 Cell::new("# Calls").fg(Color::Cyan),
253 ]);
254 for (fname, sigs) in &contract.functions {
255 for (sig, gas_info) in sigs {
256 let display_name = if sigs.len() == 1 { fname.clone() } else { sig.replace(':', "") };
258 table.add_row(vec![
259 Cell::new(display_name),
260 num(&gas_info.min, Some(Color::Green)),
261 num(&gas_info.mean, Some(Color::Yellow)),
262 num(&gas_info.median, Some(Color::Yellow)),
263 num(&gas_info.max, Some(Color::Red)),
264 num(&gas_info.calls, None),
265 ]);
266 }
267 }
268 table
269}
270
271#[derive(Clone, Debug, Default, Serialize, Deserialize)]
272pub struct ContractInfo {
273 pub gas: u64,
274 pub size: usize,
275 pub functions: BTreeMap<String, BTreeMap<String, GasInfo>>,
277}
278
279#[derive(Clone, Debug, Default, Serialize, Deserialize)]
280pub struct GasInfo {
281 pub calls: u64,
282 pub min: u64,
283 pub mean: u64,
284 pub median: u64,
285 pub max: u64,
286
287 #[serde(skip)]
288 pub frames: Vec<u64>,
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294 use foundry_evm::constants::MONAD_CHEATCODE_ADDRESS;
295
296 #[test]
297 fn network_cheatcode_addresses_are_opt_in() {
298 let ethereum = GasReport::new([], [], false, []);
299 assert!(!ethereum.is_internal_address(MONAD_CHEATCODE_ADDRESS));
300
301 let monad = GasReport::new([], [], false, [MONAD_CHEATCODE_ADDRESS]);
302 assert!(monad.is_internal_address(MONAD_CHEATCODE_ADDRESS));
303 }
304}