- Create an upgrade branch and freeze schema-affecting changes.
- Export a pre-upgrade state and archive node configs.
- Bump
cosmos/evmto 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.
- 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.
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.0Check 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 tidyResolve any version conflicts here before moving on.
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.
(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" {
...
}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),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
+ }Import the EVM ante package and geth common:
(app/app.go)
+ "github.com/cosmos/evm/ante"
+ "github.com/ethereum/go-ethereum/common"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"
}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)
}(app/keepers/precompiles.go)
+ "cosmossdk.io/core/address"
+ addresscodec "github.com/cosmos/cosmos-sdk/codec/address"
+ sdk "github.com/cosmos/cosmos-sdk/types"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(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) }
...- 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)-
Compile:
go build ./...
-
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.
- Package the new binary (and Cosmovisor upgrade folder if you use it).
- Confirm all validators build the same commit (no
replacelines). - Share an
app.tomldiff 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.
-
Forgot wrapper for CLI commands →
pruning/snapshotpanic or wrong type:- Ensure you pass
sdkAppCreatorWrapper(notac.newApp) into those commands.
- Ensure you pass
-
ICS-20 precompile build error:
- You likely didn’t pass
bankKeeperfirst; update the call site.
- You likely didn’t pass
-
Governance precompile address parsing fails:
- Provide the correct
AddressCodecvia defaults orWithAddressCodec(...).
- Provide the correct
-
Listeners never fire:
- Register with
RegisterPendingTxListenerduring app construction or module init.
- Register with
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...)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.
Your chain needs this migration if you have:
- IBC tokens converted to ERC20
- Token factory tokens with ERC20 representations
- Any existing
DynamicPrecompilesorNativePrecompilesin storage
For full details see: ERC20 Precompiles Migration Guide
Thanks to Mantra team for their work on this: MANTRA-Chain Implementation
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'-
go.modhas noreplacelines forgithub.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.