Skip to content

Latest commit

 

History

History
376 lines (270 loc) · 10.8 KB

File metadata and controls

376 lines (270 loc) · 10.8 KB

Cosmos EVM Migration

✅ Executive Checklist (TL;DR)

  • Create an upgrade branch and freeze schema-affecting changes.
  • Export a pre-upgrade state and archive node configs.
  • Bump cosmos/evm to v0.4.0 and align Cosmos SDK / IBC / CometBFT constraints.
  • Rewire keepers and AppModule (imports, constructors, RegisterServices).
  • Audit and migrate EVM & FeeMarket params (EIP-1559 knobs, denom/decimals).
  • Migrate existing ERC20 dynamic/native precompiles to new storage format (see Section 9).
  • Implement store/params migrations in your UpgradeHandler.

0) Prep

  • Create a branch: git switch -c upgrade/evm-v0.4.
  • Ensure a clean build + tests green pre-upgrade.
  • Snapshot your current params/genesis for comparison later.

1) Dependency bumps (go.mod)

1.1 Pin EVM and tidy

Bump the cosmos/evm dependency in go.mod and tidy modules: (go.mod)

- github.com/cosmos/evm v0.3.1
+ github.com/cosmos/evm v0.4.0

1.2 Transitive bumps (observed in go.sum)

Check for minor dependency bumps (e.g., google.golang.org/protobuf, github.com/gofrs/flock, github.com/consensys/gnark-crypto). Run the following commands:

go mod tidy

Resolve any version conflicts here before moving on.


2) App constructor return type & CLI command wiring

Update your app’s newApp should return an evmserver.Application // from evmserver "github.com/cosmos/evm/server" rather than servertypes.Application, and CLI commands that still expect an SDK app creator require a wrapper.

2.1 Change the return type (cmd/MYAPP/cmd/root.go)

(cmd/myapp/cmd/root.go)

 func (a appCreator) newApp(
   l log.Logger, db dbm.DB, traceStore io.Writer, appOpts servertypes.AppOptions,
-) servertypes.Application {
+) evmserver.Application // from evmserver "github.com/cosmos/evm/server" {
   ...
 }

2.2 Provide a wrapper for commands that expect the SDK type

Create a thin wrapper and use it for pruning.Cmd and snapshot.Cmd: (cmd/myapp/cmd/root.go)

sdkAppCreatorWrapper := func(l log.Logger, d dbm.DB, w io.Writer, ao servertypes.AppOptions) servertypes.Application {
    return ac.newApp(l, d, w, ao)
}

(cmd/myapp/cmd/root.go)

- pruning.Cmd(ac.newApp, myapp.DefaultNodeHome),
- snapshot.Cmd(ac.newApp),
+ pruning.Cmd(sdkAppCreatorWrapper, myapp.DefaultNodeHome),
+ snapshot.Cmd(sdkAppCreatorWrapper),

2.3 Add clientCtx and SetClientCtx to app.go

Add the clientCtx to your app object.

(app/app.go)

type MyApp struct {
    ...
+   clientCtx client.Context
    ...
}

Add a setter in the file.

+ func (app *EVMD) SetClientCtx(clientCtx client.Context) {
+    app.clientCtx = clientCtx
+ }

3) Pending-tx listener support in the app (app/app.go)

3.1 Imports

Import the EVM ante package and geth common: (app/app.go)

+ "github.com/cosmos/evm/ante"
+ "github.com/ethereum/go-ethereum/common"

3.2 App state: listeners slice

Add a new field for listeners (uses ante.PendingTxListener // from "github.com/cosmos/evm/ante"): (app/app.go)

 type MyApp struct {
   ...
+  pendingTxListeners []ante.PendingTxListener // from "github.com/cosmos/evm/ante"
 }

3.3 Registration method

Add a public method to register a listener by txHash: (app/app.go)

// from "github.com/ethereum/go-ethereum/common")) {
func (app *MyApp) RegisterPendingTxListener(listener func(common.Hash
    app.pendingTxListeners = append(app.pendingTxListeners, listener)
}

4) Precompiles: optionals + codec injection (app/keepers/precompiles.go)

4.1 New imports

(app/keepers/precompiles.go)

+ "cosmossdk.io/core/address"
+ addresscodec "github.com/cosmos/cosmos-sdk/codec/address"
+ sdk "github.com/cosmos/cosmos-sdk/types"

4.2 Define Optionals + defaults + functional options

Create a small options container with sane defaults pulled from the app’s bech32 config: (app/keepers/precompiles.go)

type Optionals struct {
    AddressCodec       address.Codec // from "cosmossdk.io/core/address" // used by gov/staking
    ValidatorAddrCodec address.Codec // from "cosmossdk.io/core/address" // used by slashing
    ConsensusAddrCodec address.Codec // from "cosmossdk.io/core/address" // used by slashing
}

func defaultOptionals() Optionals {
    return Optionals{
        // from addresscodec "github.com/cosmos/cosmos-sdk/codec/address"(sdk.GetConfig()
		// from sdk "github.com/cosmos/cosmos-sdk/types".GetBech32AccountAddrPrefix()),
        AddressCodec:       addresscodec.NewBech32Codec
		// from addresscodec "github.com/cosmos/cosmos-sdk/codec/address"(sdk.GetConfig()
		// from sdk "github.com/cosmos/cosmos-sdk/types".GetBech32ValidatorAddrPrefix()),
        ValidatorAddrCodec: addresscodec.NewBech32Codec
		// from addresscodec "github.com/cosmos/cosmos-sdk/codec/address"(sdk.GetConfig()
		// from sdk "github.com/cosmos/cosmos-sdk/types".GetBech32ConsensusAddrPrefix()),
        ConsensusAddrCodec: addresscodec.NewBech32Codec
    }
}

type Option func(*Optionals)

// from "cosmossdk.io/core/address")
// Option { return func(o *Optionals){ o.AddressCodec = c } }
func WithAddressCodec(c address.Codec

// from "cosmossdk.io/core/address")
// Option { return func(o *Optionals){ o.ValidatorAddrCodec = c } }
func WithValidatorAddrCodec(c address.Codec

// from "cosmossdk.io/core/address")
// Option { return func(o *Optionals){ o.ConsensusAddrCodec = c } }
func WithConsensusAddrCodec(c address.Codec

4.3 Update the precompile factory to accept options

(app/keepers/precompiles.go)

-func NewAvailableStaticPrecompiles(
+func NewAvailableStaticPrecompiles(
     ctx context.Context,
     ...
- ) map[common.Address]vm.PrecompiledContract {
+    opts ...Option,
+) map[common.Address]vm.PrecompiledContract {
+    options := defaultOptionals()
+    for _, opt := range opts { opt(&options) }
     ...

4.4 Modify individual precompile constructors

  • ICS-20 precompile: now also needs bankKeeper (ensure you pass it first, as shown). (app/keepers/precompiles.go)
- ibcTransferPrecompile, err := ics20precompile.NewPrecompile // from ics20precompile "github.com/cosmos/evm/precompiles/ics20"(
-     stakingKeeper,
+ ibcTransferPrecompile, err := ics20precompile.NewPrecompile // from ics20precompile "github.com/cosmos/evm/precompiles/ics20"(
+     bankKeeper,
+     stakingKeeper,
      transferKeeper,
      &channelKeeper,
      ...
  • Gov precompile: now requires an AddressCodec. (app/keepers/precompiles.go)
- govPrecompile, err := govprecompile.NewPrecompile
-     // from govprecompile "github.com/cosmos/evm/precompiles/gov"(govKeeper, cdc)
+ govPrecompile, err := govprecompile.NewPrecompile
+     // from govprecompile "github.com/cosmos/evm/precompiles/gov"(govKeeper, cdc, options.AddressCodec)

5) Build & quick tests

  1. Compile:

    go build ./...
  2. Smoke tests (local single-node):

    • Start your node; ensure RPC starts cleanly.
    • Deploy a trivial contract; verify events and logs.
    • Send a couple 1559 txs and confirm base-fee behavior looks sane.
    • (Optional) register a pending-tx listener and log hashes as they enter the mempool.

6) Rollout checklist (operational)

  • Package the new binary (and Cosmovisor upgrade folder if you use it).
  • Confirm all validators build the same commit (no replace lines).
  • Share an app.toml diff only if you changed defaults; otherwise regenerate the file from the new binary and re-apply customizations.
  • Post-upgrade: monitor mempool/pending tx logs, base-fee progression, and contract events for the first 20–50 blocks.

7) Pitfalls & remedies

  • Forgot wrapper for CLI commandspruning/snapshot panic or wrong type:

    • Ensure you pass sdkAppCreatorWrapper (not ac.newApp) into those commands.
  • ICS-20 precompile build error:

    • You likely didn’t pass bankKeeper first; update the call site.
  • Governance precompile address parsing fails:

    • Provide the correct AddressCodec via defaults or WithAddressCodec(...).
  • Listeners never fire:

    • Register with RegisterPendingTxListener during app construction or module init.

8) Minimal code snippets

App listeners (app/app.go)

import (
    "github.com/cosmos/evm/ante"
    "github.com/ethereum/go-ethereum/common"
)

type MyApp struct {
    // ...
    pendingTxListeners []ante.PendingTxListener // from "github.com/cosmos/evm/ante"
}

func (app *MYAPP) RegisterPendingTxListener(l func(common.Hash // from "github.com/ethereum/go-ethereum/common")) {
    app.pendingTxListeners = append(app.pendingTxListeners, l)
}

CLI wrapper (cmd/myapp/cmd/root.go)

sdkAppCreatorWrapper := func(l log.Logger, d dbm.DB, w io.Writer, ao servertypes.AppOptions) servertypes.Application {
    return ac.newApp(l, d, w, ao)
}

rootCmd.AddCommand(
    // ...
    pruning.Cmd(sdkAppCreatorWrapper,  myapp.DefaultNodeHome),
    snapshot.Cmd(sdkAppCreatorWrapper),
)

Precompile options & usage (app/keepers/precompiles.go)

opts := []Option{
    // override defaults only if you use non-standard prefixes/codecs
    WithAddressCodec(myAcctCodec),
    WithValidatorAddrCodec(myValCodec),
    WithConsensusAddrCodec(myConsCodec),
}

pcs := NewAvailableStaticPrecompiles(ctx, /* ... keepers ... */, opts...)

9) ERC20 Precompiles Migration

This is a breaking change for chains with existing ERC20 token pairs.

The storage mechanism for ERC20 precompiles has fundamentally changed in v0.4.0. Without proper migration, your ERC20 tokens will become inaccessible via EVM, showing zero balances and failing all operations.

Quick Impact Check

Your chain needs this migration if you have:

  • IBC tokens converted to ERC20
  • Token factory tokens with ERC20 representations
  • Any existing DynamicPrecompiles or NativePrecompiles in storage

Migration

For full details see: ERC20 Precompiles Migration Guide

Thanks to Mantra team for their work on this: MANTRA-Chain Implementation

Quick Verification

Post-upgrade, verify your migration succeeded:

# Check ERC20 balance (should NOT be 0 if tokens existed before)
cast call $TOKEN_ADDRESS "balanceOf(address)" $USER_ADDRESS --rpc-url http://localhost:8545

# Verify precompiles in state
evmd export | jq '.app_state.erc20.dynamic_precompiles'

10) Verify before tagging

  • go.mod has no replace lines for github.com/cosmos/evm.
  • Node boots with expected RPC namespaces.
  • Contracts deploy/call; events stream; fee market behaves.
  • (If applicable) ICS-20 transfers work and precompiles execute.