Skip to content

Commit e028b92

Browse files
authored
fix(trace): check fn sigs for contract with fallbacks (#9287)
* fix(trace): check fn sigs for contract with fallbacks * Add Json test * Execute test with traces * Simplify, check only for decoded function
1 parent 765969d commit e028b92

File tree

2 files changed

+166
-1
lines changed

2 files changed

+166
-1
lines changed

crates/evm/traces/src/decoder/mod.rs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,8 @@ pub struct CallTraceDecoder {
121121
pub labels: HashMap<Address, String>,
122122
/// Contract addresses that have a receive function.
123123
pub receive_contracts: Vec<Address>,
124+
/// Contract addresses that have fallback functions, mapped to function sigs.
125+
pub fallback_contracts: HashMap<Address, Vec<String>>,
124126

125127
/// All known functions.
126128
pub functions: HashMap<Selector, Vec<Function>>,
@@ -188,6 +190,7 @@ impl CallTraceDecoder {
188190
(POINT_EVALUATION, "PointEvaluation".to_string()),
189191
]),
190192
receive_contracts: Default::default(),
193+
fallback_contracts: Default::default(),
191194

192195
functions: hh_funcs()
193196
.chain(
@@ -222,6 +225,7 @@ impl CallTraceDecoder {
222225
}
223226

224227
self.receive_contracts.clear();
228+
self.fallback_contracts.clear();
225229
}
226230

227231
/// Identify unknown addresses in the specified call trace using the specified identifier.
@@ -317,6 +321,14 @@ impl CallTraceDecoder {
317321
if abi.receive.is_some() {
318322
self.receive_contracts.push(*address);
319323
}
324+
325+
if abi.fallback.is_some() {
326+
let mut functions_sig = vec![];
327+
for function in abi.functions() {
328+
functions_sig.push(function.signature());
329+
}
330+
self.fallback_contracts.insert(*address, functions_sig);
331+
}
320332
}
321333
}
322334

@@ -379,9 +391,18 @@ impl CallTraceDecoder {
379391
};
380392
};
381393

394+
// If traced contract is a fallback contract, check if it has the decoded function.
395+
// If not, then replace call data signature with `fallback`.
396+
let mut call_data = self.decode_function_input(trace, func);
397+
if let Some(fallback_functions) = self.fallback_contracts.get(&trace.address) {
398+
if !fallback_functions.contains(&func.signature()) {
399+
call_data.signature = "fallback()".into();
400+
}
401+
}
402+
382403
DecodedCallTrace {
383404
label,
384-
call_data: Some(self.decode_function_input(trace, func)),
405+
call_data: Some(call_data),
385406
return_data: self.decode_function_output(trace, functions),
386407
}
387408
} else {

crates/forge/tests/cli/cmd.rs

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2390,6 +2390,150 @@ contract CounterTest is DSTest {
23902390
);
23912391
});
23922392

2393+
// <https://github.com/foundry-rs/foundry/issues/9115>
2394+
forgetest_init!(gas_report_with_fallback, |prj, cmd| {
2395+
prj.add_test(
2396+
"DelegateProxyTest.sol",
2397+
r#"
2398+
import {Test} from "forge-std/Test.sol";
2399+
2400+
contract ProxiedContract {
2401+
uint256 public amount;
2402+
2403+
function deposit(uint256 aba) external {
2404+
amount = amount * 2;
2405+
}
2406+
2407+
function deposit() external {
2408+
}
2409+
}
2410+
2411+
contract DelegateProxy {
2412+
address internal implementation;
2413+
2414+
constructor(address counter) {
2415+
implementation = counter;
2416+
}
2417+
2418+
function deposit() external {
2419+
}
2420+
2421+
fallback() external payable {
2422+
address addr = implementation;
2423+
2424+
assembly {
2425+
calldatacopy(0, 0, calldatasize())
2426+
let result := delegatecall(gas(), addr, 0, calldatasize(), 0, 0)
2427+
returndatacopy(0, 0, returndatasize())
2428+
switch result
2429+
case 0 { revert(0, returndatasize()) }
2430+
default { return(0, returndatasize()) }
2431+
}
2432+
}
2433+
}
2434+
2435+
contract GasReportFallbackTest is Test {
2436+
function test_fallback_gas_report() public {
2437+
ProxiedContract proxied = ProxiedContract(address(new DelegateProxy(address(new ProxiedContract()))));
2438+
proxied.deposit(100);
2439+
proxied.deposit();
2440+
}
2441+
}
2442+
"#,
2443+
)
2444+
.unwrap();
2445+
2446+
cmd.args(["test", "--mt", "test_fallback_gas_report", "-vvvv", "--gas-report"])
2447+
.assert_success()
2448+
.stdout_eq(str![[r#"
2449+
...
2450+
Ran 1 test for test/DelegateProxyTest.sol:GasReportFallbackTest
2451+
[PASS] test_fallback_gas_report() ([GAS])
2452+
Traces:
2453+
[331067] GasReportFallbackTest::test_fallback_gas_report()
2454+
├─ [106511] → new ProxiedContract@[..]
2455+
│ └─ ← [Return] 246 bytes of code
2456+
├─ [108698] → new DelegateProxy@[..]
2457+
│ └─ ← [Return] 143 bytes of code
2458+
├─ [29396] DelegateProxy::fallback(100)
2459+
│ ├─ [3320] ProxiedContract::deposit(100) [delegatecall]
2460+
│ │ └─ ← [Stop]
2461+
│ └─ ← [Return]
2462+
├─ [21160] DelegateProxy::deposit()
2463+
│ └─ ← [Stop]
2464+
└─ ← [Stop]
2465+
2466+
Suite result: ok. 1 passed; 0 failed; 0 skipped; [ELAPSED]
2467+
| test/DelegateProxyTest.sol:DelegateProxy contract | | | | | |
2468+
|---------------------------------------------------|-----------------|-------|--------|-------|---------|
2469+
| Deployment Cost | Deployment Size | | | | |
2470+
| 108698 | 315 | | | | |
2471+
| Function Name | min | avg | median | max | # calls |
2472+
| deposit | 21160 | 21160 | 21160 | 21160 | 1 |
2473+
| fallback | 29396 | 29396 | 29396 | 29396 | 1 |
2474+
2475+
2476+
| test/DelegateProxyTest.sol:ProxiedContract contract | | | | | |
2477+
|-----------------------------------------------------|-----------------|------|--------|------|---------|
2478+
| Deployment Cost | Deployment Size | | | | |
2479+
| 106511 | 276 | | | | |
2480+
| Function Name | min | avg | median | max | # calls |
2481+
| deposit | 3320 | 3320 | 3320 | 3320 | 1 |
2482+
...
2483+
2484+
"#]]);
2485+
2486+
cmd.forge_fuse()
2487+
.args(["test", "--mt", "test_fallback_gas_report", "--gas-report", "--json"])
2488+
.assert_success()
2489+
.stdout_eq(
2490+
str![[r#"
2491+
[
2492+
{
2493+
"contract": "test/DelegateProxyTest.sol:DelegateProxy",
2494+
"deployment": {
2495+
"gas": 108698,
2496+
"size": 315
2497+
},
2498+
"functions": {
2499+
"deposit()": {
2500+
"calls": 1,
2501+
"min": 21160,
2502+
"mean": 21160,
2503+
"median": 21160,
2504+
"max": 21160
2505+
},
2506+
"fallback()": {
2507+
"calls": 1,
2508+
"min": 29396,
2509+
"mean": 29396,
2510+
"median": 29396,
2511+
"max": 29396
2512+
}
2513+
}
2514+
},
2515+
{
2516+
"contract": "test/DelegateProxyTest.sol:ProxiedContract",
2517+
"deployment": {
2518+
"gas": 106511,
2519+
"size": 276
2520+
},
2521+
"functions": {
2522+
"deposit(uint256)": {
2523+
"calls": 1,
2524+
"min": 3320,
2525+
"mean": 3320,
2526+
"median": 3320,
2527+
"max": 3320
2528+
}
2529+
}
2530+
}
2531+
]
2532+
"#]]
2533+
.is_json(),
2534+
);
2535+
});
2536+
23932537
forgetest_init!(can_use_absolute_imports, |prj, cmd| {
23942538
let remapping = prj.paths().libraries[0].join("myDependency");
23952539
let config = Config {

0 commit comments

Comments
 (0)