KeeperHub / KeeperHub/keeperhub

simulate-sequence keeps a slot an earlier call cleared, so a drained allowance simulates as available

Open
#2,541 1 comment 0 reactions 0 assignees View on GitHub
accepted bug confirmed
Dominant language
TypeScript
Stars
24
Forks
93
Avg merge
1d 4h
Merged PRs (30d)
253

Description

`simulateCallSequence`'s state-overrides fallback accumulates each call's writes by merging the `prestateTracer` diff into an override map (`mergeDiffIntoOverrides`, `lib/execute/simulate-sequence.ts:311-339`). The merge is additive: it copies every slot present in `post.storage` over the accumulator and leaves every other slot alone.

geth never reports a slot cleared to zero. In `processDiffState` (`eth/tracers/native/prestate.go`) the storage branch is:

```go
} else {
modified = true
if newVal != (common.Hash{}) {
postAccount.Storage[key] = newVal
}
}
```

A slot whose new value is zero is marked modified and then omitted from `post`. So a call that zeroes a slot leaves the accumulator holding the stale non-zero value, and every later call in the sequence simulates against state that call already cleared.

The reachable case is an allowance drain. `approve(vault, 1000)`, then `deposit(1000)` which spends the allowance to zero, then a second `deposit`: call three runs against an override still claiming an allowance of 1000 and is reported as succeeding, where on chain it reverts. That is a silent wrong answer on the preflight an agent uses to decide whether to broadcast.

This is independent of #2517 - it is still wrong once the trace runs on the correct base - and it is the remaining hole in the promise that each call resolves against the state the one before it produced.

The fix has to read cleared slots out of `pre` rather than `post`: a key present in `pre[addr].Storage` and absent from `post[addr].Storage`, on an account that `post` reports as modified, has been set to zero.

Contributor guide

Open the contributing guide

Research direction

Start in lib/execute/simulate-sequence.ts:311-339 at mergeDiffIntoOverrides, then read geth's processDiffState in eth/tracers/native/prestate.go to understand how cleared storage is represented. Trace the approve, allowance-draining deposit, and second deposit sequence; done means a slot absent from modified post storage is treated as zero so the final simulation matches on-chain behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
go, typescript
Domain
backend, blockchain
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
72/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.