Skip to main content

foundry_evm_coverage/
anchors.rs

1use super::{CoverageItemKind, ExecutionAnchor, ExecutionAnchorKind, ItemAnchor, SourceLocation};
2use crate::analysis::{EmptySpecialFunctionKind, SourceAnalysis};
3use alloy_primitives::map::rustc_hash::FxHashSet;
4use eyre::ensure;
5use foundry_compilers::artifacts::sourcemap::{SourceElement, SourceMap};
6use foundry_evm_core::{bytecode::InstIter, ic::IcPcMap};
7use revm::bytecode::opcode;
8
9/// Attempts to find anchors for the given items using the given source map and bytecode.
10pub fn find_anchors(
11    bytecode: &[u8],
12    source_map: &SourceMap,
13    ic_pc_map: &IcPcMap,
14    analysis: &SourceAnalysis,
15) -> Vec<ItemAnchor> {
16    let mut seen_sources = FxHashSet::default();
17    source_map
18        .iter()
19        .filter_map(|element| element.index())
20        .filter(|&source| seen_sources.insert(source))
21        .flat_map(|source| analysis.items_for_source_enumerated(source))
22        .filter_map(|(item_id, item)| {
23            if analysis.empty_special_function_kind(item_id).is_some() {
24                return None;
25            }
26            let anchor_loc = item.anchor_loc.as_ref().unwrap_or(&item.loc);
27            match item.kind {
28                CoverageItemKind::Branch { path_id, is_first_opcode: true, .. }
29                    if item.anchor_loc.is_some() =>
30                {
31                    find_anchor_simple(source_map, ic_pc_map, item_id, anchor_loc).or_else(|_| {
32                        find_anchor_branch(bytecode, source_map, item_id, &item.loc).map(
33                            |anchors| match path_id {
34                                0 => anchors.0,
35                                1 => anchors.1,
36                                _ => panic!("too many path IDs for branch"),
37                            },
38                        )
39                    })
40                }
41                CoverageItemKind::Branch { path_id, is_first_opcode: false, .. } => {
42                    find_anchor_branch(bytecode, source_map, item_id, anchor_loc).map(|anchors| {
43                        match path_id {
44                            0 => anchors.0,
45                            1 => anchors.1,
46                            _ => panic!("too many path IDs for branch"),
47                        }
48                    })
49                }
50                _ => find_anchor_simple(source_map, ic_pc_map, item_id, anchor_loc),
51            }
52            .inspect_err(|err| warn!(%item, %err, "could not find anchor"))
53            .ok()
54        })
55        .collect()
56}
57
58/// Finds execution-based anchors for empty constructors, receive functions, and fallbacks in a
59/// contract.
60pub fn find_execution_anchors(
61    source_id: u32,
62    contract_name: &str,
63    analysis: &SourceAnalysis,
64) -> Vec<ExecutionAnchor> {
65    analysis
66        .empty_special_function_ids(source_id, contract_name)
67        .filter_map(|item_id| {
68            analysis.empty_special_function_kind(item_id).map(|kind| ExecutionAnchor {
69                item_id,
70                kind: match kind {
71                    EmptySpecialFunctionKind::Constructor => ExecutionAnchorKind::Constructor,
72                    EmptySpecialFunctionKind::Receive => ExecutionAnchorKind::Receive,
73                    EmptySpecialFunctionKind::Fallback => ExecutionAnchorKind::Fallback,
74                },
75            })
76        })
77        .collect()
78}
79
80/// Find an anchor representing the first opcode within the given source range.
81pub fn find_anchor_simple(
82    source_map: &SourceMap,
83    ic_pc_map: &IcPcMap,
84    item_id: u32,
85    loc: &SourceLocation,
86) -> eyre::Result<ItemAnchor> {
87    let instruction =
88        source_map.iter().position(|element| is_in_source_range(element, loc)).ok_or_else(
89            || eyre::eyre!("Could not find anchor: No matching instruction in range {loc}"),
90        )?;
91
92    Ok(ItemAnchor {
93        instruction: ic_pc_map.get(instruction as u32).ok_or_else(|| {
94            eyre::eyre!("We found an anchor, but we can't translate it to a program counter")
95        })?,
96        item_id,
97    })
98}
99
100/// Finds the anchor corresponding to a branch item.
101///
102/// This finds the relevant anchors for a branch coverage item. These anchors
103/// are found using the bytecode of the contract in the range of the branching node.
104///
105/// For `IfStatement` nodes, the template is generally:
106/// ```text
107/// <condition>
108/// PUSH <ic if false>
109/// JUMPI
110/// <true branch>
111/// <...>
112/// <false branch>
113/// ```
114///
115/// For `assert` and `require`, the template is generally:
116///
117/// ```text
118/// PUSH <ic if true>
119/// JUMPI
120/// <revert>
121/// <...>
122/// <true branch>
123/// ```
124///
125/// This function will look for the last JUMPI instruction, backtrack to find the program
126/// counter of the first branch, and return an item for that program counter, and the
127/// program counter immediately after the JUMPI instruction.
128pub fn find_anchor_branch(
129    bytecode: &[u8],
130    source_map: &SourceMap,
131    item_id: u32,
132    loc: &SourceLocation,
133) -> eyre::Result<(ItemAnchor, ItemAnchor)> {
134    let mut anchors: Option<(ItemAnchor, ItemAnchor)> = None;
135    for (ic, (pc, inst)) in InstIter::new(bytecode).with_pc().enumerate() {
136        // We found a push, so we do some PC -> IC translation accounting, but we also check if
137        // this push is coupled with the JUMPI we are interested in.
138
139        // Check if Opcode is PUSH
140        if (opcode::PUSH1..=opcode::PUSH32).contains(&inst.opcode.get()) {
141            let Some(element) = source_map.get(ic) else {
142                // NOTE(onbjerg): For some reason the last few bytes of the bytecode do not have
143                // a source map associated, so at that point we just stop searching
144                break;
145            };
146
147            // Check if we are in the source range we are interested in, and if the next opcode
148            // is a JUMPI
149            let next_pc = pc + inst.immediate.len() + 1;
150            let push_size = inst.immediate.len();
151            if bytecode.get(next_pc).copied() == Some(opcode::JUMPI)
152                && is_in_source_range(element, loc)
153            {
154                // We do not support program counters bigger than u32.
155                ensure!(push_size <= 4, "jump destination overflow");
156
157                // Convert the push bytes for the second branch's PC to a u32.
158                let mut pc_bytes = [0u8; 4];
159                pc_bytes[4 - push_size..].copy_from_slice(inst.immediate);
160                let pc_jump = u32::from_be_bytes(pc_bytes);
161                anchors = Some((
162                    ItemAnchor {
163                        item_id,
164                        // The first branch is the opcode directly after JUMPI
165                        instruction: (next_pc + 1) as u32,
166                    },
167                    ItemAnchor { item_id, instruction: pc_jump },
168                ));
169            }
170        }
171    }
172
173    anchors.ok_or_else(|| eyre::eyre!("Could not detect branches in source: {}", loc))
174}
175
176/// Calculates whether `element` is within the range of the target `location`.
177fn is_in_source_range(element: &SourceElement, location: &SourceLocation) -> bool {
178    // Source IDs must match.
179    let source_ids_match = element.index_i32() == location.source_id as i32;
180    if !source_ids_match {
181        return false;
182    }
183
184    // Needed because some source ranges in the source map mark the entire contract...
185    let is_within_start = element.offset() >= location.bytes.start;
186    if !is_within_start {
187        return false;
188    }
189
190    let start_of_ranges = location.bytes.start.max(element.offset());
191    let end_of_ranges =
192        (location.bytes.start + location.len()).min(element.offset() + element.length());
193    start_of_ranges <= end_of_ranges
194}