Skip to content

snapshot index out of bounds #1001

@trevormil

Description

@trevormil

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

// 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

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:

// 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
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions