Skip to content

Commit 643bb92

Browse files
committed
fix(forge): coverage for contracts with ctor with args
1 parent 256cc50 commit 643bb92

File tree

2 files changed

+111
-2
lines changed

2 files changed

+111
-2
lines changed

crates/common/src/contracts.rs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//! Commonly used contract types and functions.
22
33
use crate::compile::PathOrContractInfo;
4+
use alloy_dyn_abi::JsonAbiExt;
45
use alloy_json_abi::{Event, Function, JsonAbi};
56
use alloy_primitives::{hex, Address, Bytes, Selector, B256};
67
use eyre::{OptionExt, Result};
@@ -123,24 +124,39 @@ impl ContractsByArtifact {
123124

124125
/// Finds a contract which has a similar bytecode as `code`.
125126
pub fn find_by_creation_code(&self, code: &[u8]) -> Option<ArtifactWithContractRef<'_>> {
126-
self.find_by_code(code, 0.1, ContractData::bytecode)
127+
self.find_by_code(code, 0.1, true, ContractData::bytecode)
127128
}
128129

129130
/// Finds a contract which has a similar deployed bytecode as `code`.
130131
pub fn find_by_deployed_code(&self, code: &[u8]) -> Option<ArtifactWithContractRef<'_>> {
131-
self.find_by_code(code, 0.15, ContractData::deployed_bytecode)
132+
self.find_by_code(code, 0.15, false, ContractData::deployed_bytecode)
132133
}
133134

134135
/// Finds a contract based on provided bytecode and accepted match score.
136+
/// If strip constructor args flag is true then removes args from bytecode to compare.
135137
fn find_by_code(
136138
&self,
137139
code: &[u8],
138140
accepted_score: f64,
141+
strip_ctor_args: bool,
139142
get: impl Fn(&ContractData) -> Option<&Bytes>,
140143
) -> Option<ArtifactWithContractRef<'_>> {
141144
self.iter()
142145
.filter_map(|(id, contract)| {
143146
if let Some(deployed_bytecode) = get(contract) {
147+
let mut code = code;
148+
if strip_ctor_args && code.len() > deployed_bytecode.len() {
149+
// Try to decode ctor args with contract abi.
150+
if let Some(constructor) = contract.abi.constructor() {
151+
let constructor_args = &code[deployed_bytecode.len()..];
152+
if constructor.abi_decode_input(constructor_args, false).is_ok() {
153+
// If we can decode args with current abi then remove args from
154+
// code to compare.
155+
code = &code[..deployed_bytecode.len()]
156+
}
157+
}
158+
};
159+
144160
let score = bytecode_diff_score(deployed_bytecode.as_ref(), code);
145161
(score <= accepted_score).then_some((score, (id, contract)))
146162
} else {

crates/forge/tests/cli/coverage.rs

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1753,6 +1753,99 @@ contract AContractTest is DSTest {
17531753
assert!(files.is_empty());
17541754
});
17551755

1756+
// <https://github.com/foundry-rs/foundry/issues/10172>
1757+
forgetest!(constructor_with_args, |prj, cmd| {
1758+
prj.insert_ds_test();
1759+
prj.add_source(
1760+
"ArrayCondition.sol",
1761+
r#"
1762+
contract ArrayCondition {
1763+
uint8 public constant MAX_SIZE = 32;
1764+
error TooLarge();
1765+
error EmptyArray();
1766+
// Storage variable to ensure the constructor does something
1767+
uint256 private _arrayLength;
1768+
1769+
constructor(uint256[] memory values) {
1770+
// Check for empty array
1771+
if (values.length == 0) {
1772+
revert EmptyArray();
1773+
}
1774+
1775+
// This is the branch that's not being recognized by the coverage tool
1776+
if (values.length > MAX_SIZE) {
1777+
revert TooLarge();
1778+
}
1779+
1780+
// Store the array length
1781+
_arrayLength = values.length;
1782+
}
1783+
1784+
function getArrayLength() external view returns (uint256) {
1785+
return _arrayLength;
1786+
}
1787+
}
1788+
"#,
1789+
)
1790+
.unwrap();
1791+
1792+
prj.add_source(
1793+
"ArrayConditionTest.sol",
1794+
r#"
1795+
import "./test.sol";
1796+
import {ArrayCondition} from "./ArrayCondition.sol";
1797+
1798+
interface Vm {
1799+
function expectRevert(bytes4 revertData) external;
1800+
}
1801+
1802+
contract ArrayConditionTest is DSTest {
1803+
Vm constant vm = Vm(HEVM_ADDRESS);
1804+
1805+
function testValidSize() public {
1806+
uint256[] memory values = new uint256[](10);
1807+
ArrayCondition condition = new ArrayCondition(values);
1808+
assertEq(condition.getArrayLength(), 10);
1809+
}
1810+
1811+
// Test with maximum array size (should NOT revert)
1812+
function testMaxSize() public {
1813+
uint256[] memory values = new uint256[](32);
1814+
ArrayCondition condition = new ArrayCondition(values);
1815+
assertEq(condition.getArrayLength(), 32);
1816+
}
1817+
1818+
// Test with too large array size (should revert)
1819+
function testTooLarge() public {
1820+
uint256[] memory values = new uint256[](33);
1821+
vm.expectRevert(ArrayCondition.TooLarge.selector);
1822+
new ArrayCondition(values);
1823+
}
1824+
1825+
// Test with empty array (should revert)
1826+
function testEmptyArray() public {
1827+
uint256[] memory values = new uint256[](0);
1828+
vm.expectRevert(ArrayCondition.EmptyArray.selector);
1829+
new ArrayCondition(values);
1830+
}
1831+
}
1832+
"#,
1833+
)
1834+
.unwrap();
1835+
1836+
cmd.arg("coverage").assert_success().stdout_eq(str![[r#"
1837+
...
1838+
╭------------------------+---------------+---------------+---------------+---------------╮
1839+
| File | % Lines | % Statements | % Branches | % Funcs |
1840+
+========================================================================================+
1841+
| src/ArrayCondition.sol | 100.00% (8/8) | 100.00% (6/6) | 100.00% (2/2) | 100.00% (2/2) |
1842+
|------------------------+---------------+---------------+---------------+---------------|
1843+
| Total | 100.00% (8/8) | 100.00% (6/6) | 100.00% (2/2) | 100.00% (2/2) |
1844+
╰------------------------+---------------+---------------+---------------+---------------╯
1845+
...
1846+
"#]]);
1847+
});
1848+
17561849
#[track_caller]
17571850
fn assert_lcov(cmd: &mut TestCommand, data: impl IntoData) {
17581851
cmd.args(["--report=lcov", "--report-file"]).assert_file(data.into_data());

0 commit comments

Comments
 (0)