snapshot index out of bounds
- Dominant language
- Go
- Stars
- 164
- Forks
- 215
- Avg merge
- 3d 58m
- Merged PRs (30d)
- 12
Description
`snapshotmulti.Store` causes the EVM snapshot stack to be corrupted when code running inside a precompile uses the standard Cosmos SDK `ctx.CacheContext()` pattern. This causes errors when the EVM tries to rollback upon errors like out of gas or logic errors.
```
snapshot index 0 out of bound [0..0)
```
From the ethers `estimateGas` function, it returned a "missing revert data" error, but upon inspection, the actual JSON RPC estimate gas call returned the snapshot out of bounds error above.
This may be related to #903 as well.
---
## Technical Details
When an EVM transaction calls a precompile via `RunNativeAction`:
1. The StateDB creates a `snapshotmulti.Store` wrapper and takes a snapshot (index 0)
2. If the underlying precompile code calls `ctx.CacheContext()` (standard Cosmos SDK pattern), it takes another snapshot (index 1) but returns itself, not an isolated cache
3. When the underlying precompile code calls `writeCache()`, `Commit()` clears the entire snapshot stack (`cacheStores = nil`), not just the current cache snapshot (which is what is expected)
4. Later EVM revert attempts panic because the stack is empty. This happens for out of gas or other errors. We found this out through `eth_estimateGas` calls (successful precompile -> writeCache() but clears stack -> later runs out of gas -> revert -> panic)
**File:** `x/vm/store/snapshotmulti/store.go`
```go
// CacheMultiStore returns ITSELF after taking a snapshot
func (s *Store) CacheMultiStore() storetypes.CacheMultiStore {
s.Snapshot()
return s // Returns self, not a new isolated cache!
}
// Write() clears the ENTIRE snapshot stack
func (s *Store) Write() {
for _, key := range s.storeKeys {
s.stores[key].Commit() // Each Commit() sets cacheStores = nil
}
s.head = types.InitialHead
}
```
**File:** `x/vm/store/snapshotkv/store.go`
```go
func (cs *Store) Commit() {
for i := len(cs.cacheStores) - 1; i >= 0; i-- {
cs.cacheStores[i].Write()
}
cs.initialStore.Write()
cs.cacheStores = nil // CLEARS THE ENTIRE STACK
}
func (cs *Store) RevertToSnapshot(target int) {
if target < 0 || target >= len(cs.cacheStores) {
panic(fmt.Errorf("snapshot index %d out of bound [%d..%d)",
target, 0, len(cs.cacheStores))) // PANIC when stack is empty
}
cs.cacheStores = cs.cacheStores[:target]
}
```
### Problematic Code Pattern
Any precompile calling code that uses the standard Cosmos SDK atomic operation pattern:
```go
// Inside a precompile's action function
func (k Keeper) SomeOperation(ctx sdk.Context) error {
cachedCtx, writeCache := ctx.CacheContext()
if err := k.doSomething(cachedCtx); err != nil {
return err // Don't commit
}
writeCache() // THIS CORRUPTS THE EVM SNAPSHOT STACK
return nil
}
```
Contributor guide
Research direction
Start with x/vm/store/snapshotmulti/store.go and x/vm/store/snapshotkv/store.go, then trace RunNativeAction and the precompile CacheContext pattern described in the issue. Reproduce the failure through an eth_estimateGas call that commits a cached context before an EVM rollback. Done means the snapshot stack remains valid and rollback no longer panics with an out-of-bounds error.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- blockchain
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100