Skip to content

## fix: match expected/actual state changes, overrides, and balance changes by identity, not array position - #190

Open
batuhankocyigit wants to merge 2 commits into
base:mainfrom
batuhankocyigit:fix/state-change-matching-by-identity
Open

## fix: match expected/actual state changes, overrides, and balance changes by identity, not array position#190
batuhankocyigit wants to merge 2 commits into
base:mainfrom
batuhankocyigit:fix/state-change-matching-by-identity

Conversation

@batuhankocyigit

Copy link
Copy Markdown

The bug

buildOverrideComparisons, buildChangeComparisons, and
buildBalanceComparisons in src/lib/validation-results-utils.ts pair each
expected entry with an actual entry purely by array index:

actual: actualChanges[scIndex]?.changes?.[cIndex]

The two sides don't share an ordering guarantee, though:

  • actual comes from the state-diff simulation pipeline
    (state-diff.ts), which always sorts a contract's overrides/changes by
    key ascending before returning them (.sort((a, b) => a.key.localeCompare(b.key))).
  • expected comes straight from the task's config JSON
    (getExpectedData in validation-service.ts returns
    parsedConfig.stateChanges as-is, unsorted).

A config author has no reason to write state changes in raw-hex-key
ascending order — listing them in whatever order is narratively useful
(e.g. "the important flag first, then the owner slot") is completely
reasonable. When that order doesn't happen to match the pipeline's sorted
output, index-based pairing cross-wires unrelated entries. matchesChange
/ matchesOverride (which check expected.key === actual.key) then
correctly detect the key mismatch on the wrongly-paired entries — and
hasBlockingErrors reports a blocking validation failure for a task whose
actual on-chain effect is byte-for-byte identical to what the config
expects.

To be clear about the direction of the bug: this is a false positive (a
correct task gets flagged as mismatched), not a false negative — it
doesn't cause an incorrect task to be accepted. But it directly undermines
the tool's job of telling a signer whether a task is safe to sign, and a
signer who sees a reported mismatch has no way to tell, from the tool's
output alone, whether it's this ordering artifact or a real problem.

Reproduction

Added as the first case in the new test file: two storage-slot changes on
one contract, identical values on both the expected and actual side, only
the order differs (config lists them as [paused, owner]; the simulation
naturally returns them key-sorted as [owner, paused]). Before this fix,
hasBlockingErrors(items) returns true for this input despite the task
being entirely correct.

The fix

Match each expected entry to its actual counterpart by identity instead of
position:

  • Overrides/changes: look up the actual contract entry by address
    (case-insensitively), then look up the specific override/change by key
    within that contract via a Map.
  • Balance changes: look up by address + field together, since a
    contract can have more than one tracked balance-type field.

matchesOverride / matchesChange / matchesBalance and
hasBlockingErrors are untouched — this only changes which actual entry
gets handed to them for comparison.

Testing

New file src/lib/__tests__/validation-results-utils.test.ts:

  • Reproduces the bug directly against buildValidationItems +
    hasBlockingErrors (fails before the fix, passes after): reordered but
    semantically-identical expected/actual data no longer produces a
    blocking mismatch, and each entry is verified to be paired with its
    correct counterpart by key.
  • A second case with a genuinely different after value confirms the fix
    doesn't mask real mismatches — hasBlockingErrors still correctly
    returns true.

Full verification:

  • npx tsc --noEmit — clean
  • npx eslint src/lib/validation-results-utils.ts src/lib/__tests__/validation-results-utils.test.ts — clean
  • npx prettier --check — clean
  • npx jest --runInBand (full suite) — 108/108 passing, no regressions

Scope

Two files: the fix itself and its test. No changes to the matching
functions' semantics (matchesChange etc.), the blocking-decision logic,
or any UI code.

@cb-heimdall

Copy link
Copy Markdown
Collaborator

🟡 Heimdall Review Status

Requirement Status More Info
Reviews 🟡 0/1
Denominator calculation
Show calculation
1 if user is bot 0
1 if user is external 0
2 if repo is sensitive 0
From .codeflow.yml 1
Additional review requirements
Show calculation
Max 0
0
From CODEOWNERS 0
Global minimum 0
Max 1
1
1 if commit is unverified 0
Sum 1

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants