KeeperHub / KeeperHub/keeperhub

fix: simulate-sequence's state-overrides fallback traces calls against raw state, dropping earlier calls' writes

Open Beginner friendly
#2,517 3 comments 0 reactions 0 assignees View on GitHub
accepted bug confirmed
Dominant language
TypeScript
Stars
24
Forks
93
Avg merge
1d 8h
Merged PRs (30d)
266

Description

## Where

`lib/execute/simulate-sequence.ts`, `runWithStateOverrides` — the fallback path for chains whose RPC does not implement `eth_simulateV1`, introduced by #2452 (#2427).

For every call in the sequence, the loop runs:

1. `eth_call` and `eth_estimateGas` **with** `stateAtThisCall` (the accumulated state overrides from all earlier calls) — correct.
2. `debug_traceCall` with `prestateTracer` + `diffMode: true` to produce the state diff that seeds the next call — **without** any overrides:

```ts
provider.send("debug_traceCall", [
tx,
"latest",
{ tracer: "prestateTracer", tracerConfig: { diffMode: true } },
])
```

So the diff is computed against the raw `latest` chain state, not against the state the call was actually simulated against.

## Reason

`debug_traceCall` accepts `stateOverrides` in its options object (same per-account shape as `eth_call`'s third argument; geth `StateOverride`), so tracing against the accumulated state is supported. Without it:

- Any call that only succeeds because an earlier call set up state (approve before transferFrom, set-config before use, ...) **reverts inside the trace** even though the `eth_call` directly above it (run with the overrides) succeeded.
- Its `post` diff is then empty or wrong, `mergeDiffIntoOverrides` merges nothing, and every later call silently answers against state that lacks the earlier calls' writes.

The answer is internally inconsistent: the sequence reports call N as success (from the overridden `eth_call`) while not carrying call N's state into call N+1. That is exactly the failure mode this fallback exists to avoid — the feature's commit message says a node offering neither mechanism should report the later calls as unavailable "rather than quietly answering against latest state".

## Scope

- Fallback path only (chains without `eth_simulateV1`); the primary path is unaffected.
- Only from the 2nd call onward: the first call has nothing to carry, and a 2-call sequence never consumes the last call's diff — so the canonical approve→deposit pair answers correctly today.
- 3+ call sequences where an intermediate call depends on earlier state are wrong (e.g. approve → transferFrom → read balance: the read returns the pre-transfer balance). `MAX_SEQUENCE_CALLS` is 10, so this is reachable through `POST /api/execute/contract-call` with `simulate: true` and `calls[]`.

## Proposed fix

Pass the same snapshot to the trace:

```ts
{
tracer: "prestateTracer",
tracerConfig: { diffMode: true },
...(Object.keys(stateAtThisCall).length > 0 && { stateOverrides: stateAtThisCall }),
}
```

If a node that supports `debug_traceCall` rejects `stateOverrides`, the existing catch already degrades the remaining calls to `unavailable` — better than state-blind answers.

## Test

Assert in `tests/unit/execute-simulate-sequence.test.ts` that the second `debug_traceCall` request carries the first call's accumulated `stateOverrides`, while the first request sends none.

Contributor guide

Open the contributing guide

Research direction

Start in lib/execute/simulate-sequence.ts at runWithStateOverrides and run tests/unit/execute-simulate-sequence.test.ts. Read the fallback loop and existing trace request handling, then run the focused test. Done means the first debug_traceCall has no overrides, the second carries the first call's accumulated overrides, and the test passes.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
blockchain
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
88/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.