Skip to content

fix(symbolic): re-registering an identical vm.mockCall replaces the stale entry - #16660

Open
gomesalexandre wants to merge 2 commits into
foundry-rs:masterfrom
gomesalexandre:fix_symbolic_mockcall_dedupe
Open

fix(symbolic): re-registering an identical vm.mockCall replaces the stale entry#16660
gomesalexandre wants to merge 2 commits into
foundry-rs:masterfrom
gomesalexandre:fix_symbolic_mockcall_dedupe

Conversation

@gomesalexandre

Copy link
Copy Markdown
Contributor

tl;dr

Under --symbolic, re-registering the exact same vm.mockCall/vm.mockCalls (identical callee/value/calldata) left the stale mock winning forever, instead of replacing it like concrete mode does.

The bug

add_call_mock (crates/evm/symbolic/src/executor/cheatcodes.rs) unconditionally pushed onto state.call_mocks: Vec<CallMock> on every vm.mockCall/vm.mockCalls call. Re-registering the same (callee, value, calldata) therefore left BOTH the old and new entry in the vec.

Selection (executor/calls.rs, untouched by this diff) resolves ties on specificity() -- (data.len(), value.is_some()) -- by keeping the FIRST-inserted candidate on a strict > comparison. So the stale mock always won over its own replacement, diverging from the concrete cheatcode's BTreeMap-keyed map-insert (replace) semantics, which are already pinned by testMockCallWithValue in testdata/default/cheats/MockCall.t.sol.

The fix

Dedupe at insert time, not at selection: add_call_mock now searches call_mocks for an existing entry whose (callee, value, data) structurally matches (new CallMock::matches_definition, mirroring the already-correct sibling set_function_mock/FunctionMock::matches_definition pattern in the same file) and replaces it in place; otherwise it pushes as before.

Deliberately does not touch the specificity-based tie-break in calls.rs -- that logic is relied on by other tests for genuinely-distinct overlapping mocks, and changing it would risk disturbing that unrelated precedence behavior.

Known limitation (by design, out of scope): dedup is exact structural equality, not solver-level equivalence. Two symbolic expressions that are equal only under a vm.assume constraint won't dedupe. This matches how the rest of the mock-matching machinery already works and is a safe, conservative choice -- it never merges two definitions that aren't syntactically identical.

Testing

  • Added checkMockCallRemockReplacesStaleValue to symbolic_cheatcodes.rs, exercising the exact repro (mock a value, re-mock the identical callee/calldata with a new return, assert the new value wins). Verified genuinely red against the old push-only logic ([FAIL: symbolic counterexample did not replay...]), green after the fix ([PASS] checkMockCallRemockReplacesStaleValue).
  • Full symbolic_cheatcodes CLI suite: 60/61 passed. The one failure (symbolic_mapping_storage_hooks, a fuzz.runs must be greater than 0 config panic) is pre-existing and unrelated -- confirmed identical against unmodified master via git stash.
  • cargo clippy -p foundry-evm-symbolic -- -D warnings: clean.
  • cargo fmt --check: clean (only the usual nightly-only-feature notices, no diff on stable).
  • Synchronous adversarial Codex review of the diff: no correctness or borrow-safety issues found. It flagged two pre-existing, out-of-scope limitations (solver-equivalent-but-syntactically-distinct keys don't dedupe; empty-mockCalls-array fallthrough behavior differs from concrete mode) -- both predate this diff and aren't touched by it.

closes nothing (no tracked issue -- found via direct code inspection comparing concrete vs. symbolic mock-call semantics)

…tale entry

`add_call_mock` unconditionally pushed onto `state.call_mocks` on every
`vm.mockCall`/`vm.mockCalls` invocation under `--symbolic`, so re-registering
the same (callee, value, calldata) mock left both the stale and new entry in
the vec. Selection resolves ties on `specificity()` by keeping the
first-inserted candidate on a strict `>` comparison, so the stale mock always
won over its own replacement -- diverging from the concrete cheatcode's
map-insert (replace) semantics, already pinned by `testMockCallWithValue` in
MockCall.t.sol.

Dedupe at insert instead: search `call_mocks` for a structurally-matching
entry and replace it in place, mirroring the sibling `set_function_mock`
pattern in the same file. Deliberately leaves the specificity-based tie-break
in calls.rs untouched, since that logic is relied on by other tests for
genuinely-distinct overlapping mocks.

Adds a regression test proving replace-on-remock, verified red (fails against
the old push-only logic) before green.
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

✅ Changelog found

The deterministic check will validate the changed entry.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

2 participants