Skip to content

Commit c0914e4

Browse files
authored
fix(forge): coverage for contracts with ctor with args (#10270)
1 parent 256cc50 commit c0914e4

File tree

2 files changed

+110
-2
lines changed

2 files changed

+110
-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: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1753,6 +1753,98 @@ 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+
if (values.length > MAX_SIZE) {
1776+
revert TooLarge();
1777+
}
1778+
1779+
// Store the array length
1780+
_arrayLength = values.length;
1781+
}
1782+
1783+
function getArrayLength() external view returns (uint256) {
1784+
return _arrayLength;
1785+
}
1786+
}
1787+
"#,
1788+
)
1789+
.unwrap();
1790+
1791+
prj.add_source(
1792+
"ArrayConditionTest.sol",
1793+
r#"
1794+
import "./test.sol";
1795+
import {ArrayCondition} from "./ArrayCondition.sol";
1796+
1797+
interface Vm {
1798+
function expectRevert(bytes4 revertData) external;
1799+
}
1800+
1801+
contract ArrayConditionTest is DSTest {
1802+
Vm constant vm = Vm(HEVM_ADDRESS);
1803+
1804+
function testValidSize() public {
1805+
uint256[] memory values = new uint256[](10);
1806+
ArrayCondition condition = new ArrayCondition(values);
1807+
assertEq(condition.getArrayLength(), 10);
1808+
}
1809+
1810+
// Test with maximum array size (should NOT revert)
1811+
function testMaxSize() public {
1812+
uint256[] memory values = new uint256[](32);
1813+
ArrayCondition condition = new ArrayCondition(values);
1814+
assertEq(condition.getArrayLength(), 32);
1815+
}
1816+
1817+
// Test with too large array size (should revert)
1818+
function testTooLarge() public {
1819+
uint256[] memory values = new uint256[](33);
1820+
vm.expectRevert(ArrayCondition.TooLarge.selector);
1821+
new ArrayCondition(values);
1822+
}
1823+
1824+
// Test with empty array (should revert)
1825+
function testEmptyArray() public {
1826+
uint256[] memory values = new uint256[](0);
1827+
vm.expectRevert(ArrayCondition.EmptyArray.selector);
1828+
new ArrayCondition(values);
1829+
}
1830+
}
1831+
"#,
1832+
)
1833+
.unwrap();
1834+
1835+
cmd.arg("coverage").assert_success().stdout_eq(str![[r#"
1836+
...
1837+
╭------------------------+---------------+---------------+---------------+---------------╮
1838+
| File | % Lines | % Statements | % Branches | % Funcs |
1839+
+========================================================================================+
1840+
| src/ArrayCondition.sol | 100.00% (8/8) | 100.00% (6/6) | 100.00% (2/2) | 100.00% (2/2) |
1841+
|------------------------+---------------+---------------+---------------+---------------|
1842+
| Total | 100.00% (8/8) | 100.00% (6/6) | 100.00% (2/2) | 100.00% (2/2) |
1843+
╰------------------------+---------------+---------------+---------------+---------------╯
1844+
...
1845+
"#]]);
1846+
});
1847+
17561848
#[track_caller]
17571849
fn assert_lcov(cmd: &mut TestCommand, data: impl IntoData) {
17581850
cmd.args(["--report=lcov", "--report-file"]).assert_file(data.into_data());

0 commit comments

Comments
 (0)