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:
- The StateDB creates a
snapshotmulti.Store wrapper and takes a snapshot (index 0)
- 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
- 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)
- 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
}
snapshotmulti.Storecauses the EVM snapshot stack to be corrupted when code running inside a precompile uses the standard Cosmos SDKctx.CacheContext()pattern. This causes errors when the EVM tries to rollback upon errors like out of gas or logic errors.From the ethers
estimateGasfunction, 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:snapshotmulti.Storewrapper and takes a snapshot (index 0)ctx.CacheContext()(standard Cosmos SDK pattern), it takes another snapshot (index 1) but returns itself, not an isolated cachewriteCache(),Commit()clears the entire snapshot stack (cacheStores = nil), not just the current cache snapshot (which is what is expected)eth_estimateGascalls (successful precompile -> writeCache() but clears stack -> later runs out of gas -> revert -> panic)File:
x/vm/store/snapshotmulti/store.goFile:
x/vm/store/snapshotkv/store.goProblematic Code Pattern
Any precompile calling code that uses the standard Cosmos SDK atomic operation pattern: