Big thanks to Reece & the Spawn team for their valuable contributions to this guide.
This guide provides instructions for adding EVM compatibility to a new Cosmos SDK chain. It targets chains being built from scratch with EVM support.
For existing live chains, adding EVM compatibility involves significant additional considerations:
  • Account system changes requiring address migration or mapping between Cosmos and Ethereum formats
  • Token decimal changes (from Cosmos standard 6 to Ethereum standard 18) impacting all existing balances
  • Asset migration where existing assets need to be initialized and mirrored in the EVM
Contact Interchain Labs for production chain upgrade guidance.

Prerequisites

  • Cosmos SDK chain on v0.53.x
  • IBC-Go v10
  • Go 1.23+ installed
  • Basic knowledge of Go and Cosmos SDK
Throughout this guide, appd refers to your chain’s binary (e.g., gaiad, dydxd, etc.).

Version Compatibility

These version numbers may change as development continues. Check github.com/cosmos/evm for the latest releases.
require (
    github.com/cosmos/cosmos-sdk v0.53.0
    github.com/cosmos/ibc-go/v10 v10.2.0
    github.com/cosmos/evm v0.3.0
)

replace (
    // Use the Cosmos fork of go-ethereum
    github.com/ethereum/go-ethereum => github.com/cosmos/go-ethereum v1.15.11-cosmos-0
)

Step 1: Update Dependencies

// go.mod
require (
    github.com/cosmos/cosmos-sdk v0.53.0
    github.com/ethereum/go-ethereum v1.15.10

    // for IBC functionality in EVM
    github.com/cosmos/ibc-go/modules/capability v1.0.1
    github.com/cosmos/ibc-go/v10 v10.2.0
)

Step 2: Update Chain Configuration

Chain ID Configuration

Cosmos EVM requires two separate chain IDs:
  • Cosmos Chain ID (string): Used for CometBFT RPC, IBC, and native Cosmos SDK transactions (e.g., “mychain-1”)
  • EVM Chain ID (integer): Used for EVM transactions and EIP-155 tooling (e.g., 9000)
Ensure your EVM chain ID is not already in use by checking chainlist.org.
Files to Update:
  1. app/app.go: Set chain ID constants
const CosmosChainID = "mychain-1" // Standard Cosmos format
const EVMChainID = 9000           // EIP-155 integer
  1. Update Makefile, scripts, and genesis.json with correct chain IDs

Account Configuration

Use eth_secp256k1 as the standard account type with coin type 60 for Ethereum compatibility. Files to Update:
  1. app/app.go:
const CoinType uint32 = 60
  1. chain_registry.json:
"slip44": 60

Base Denomination and Power Reduction

Changing from 6 decimals (Cosmos convention) to 18 decimals (EVM standard) is highly recommended for full compatibility.
  1. Set the denomination in app/app.go:
const BaseDenomUnit int64 = 18
  1. Update the init() function:
import (
    "math/big"
    "cosmossdk.io/math"
    sdk "github.com/cosmos/cosmos-sdk/types"
)

func init() {
    // Update power reduction for 18-decimal base unit
    sdk.DefaultPowerReduction = math.NewIntFromBigInt(
        new(big.Int).Exp(big.NewInt(10), big.NewInt(BaseDenomUnit), nil),
    )
}

Step 3: Handle EVM Decimal Precision

The mismatch between EVM’s 18-decimal standard and Cosmos SDK’s 6-decimal standard is critical. The default behavior (flooring) discards any value below 10^-6, causing asset loss and breaking DeFi applications.

Solution: x/precisebank Module

The x/precisebank module wraps the native x/bank module to maintain fractional balances for EVM denominations, handling full 18-decimal precision without loss. Benefits:
  • Lossless precision preventing invisible asset loss
  • High DApp compatibility ensuring DeFi protocols function correctly
  • Simple integration requiring minimal changes
Integration in app.go:
// Initialize PreciseBankKeeper
app.PreciseBankKeeper = precisebankkeeper.NewKeeper(
    appCodec,
    keys[precisebanktypes.StoreKey],
    app.BankKeeper,
    authtypes.NewModuleAddress(govtypes.ModuleName).String(),
)

// Pass PreciseBankKeeper to EVMKeeper instead of BankKeeper
app.EVMKeeper = evmkeeper.NewKeeper(
    appCodec,
    keys[evmtypes.StoreKey],
    tkeys[evmtypes.TransientKey],
    authtypes.NewModuleAddress(govtypes.ModuleName),
    app.AccountKeeper,
    app.PreciseBankKeeper, // Use PreciseBankKeeper here
    app.StakingKeeper,
    app.FeeMarketKeeper,
    &app.Erc20Keeper,
    tracer,
    app.GetSubspace(evmtypes.ModuleName),
)

Step 4: Configure Automatic ERC20 Token Registration

The Cosmos EVM x/erc20 module can automatically register ERC20 token pairs for incoming single-hop IBC tokens (prefixed with “ibc/”).

Configuration Requirements

  1. Use the Extended IBC Transfer Module: Import and use the transfer module from github.com/cosmos/evm/x/ibc/transfer
  2. Enable ERC20 Module Parameters in genesis:
erc20Params := erc20types.DefaultParams()
erc20Params.EnableErc20 = true
erc20Params.EnableEVMHook = true
  1. Proper Module Wiring: Ensure correct keeper wiring as detailed in Step 8

Step 5: Create EVM Configuration File

Create app/config.go to set up global EVM configuration:
package app

import (
    "fmt"
    "math/big"

    "cosmossdk.io/math"
    sdk "github.com/cosmos/cosmos-sdk/types"
    evmtypes "github.com/cosmos/evm/x/vm/types"
)

type EVMOptionsFn func(string) error

func NoOpEVMOptions(_ string) error {
    return nil
}

var sealed = false

// ChainsCoinInfo maps EVM chain IDs to coin configuration
// IMPORTANT: Uses uint64 EVM chain IDs as keys, not Cosmos chain ID strings
var ChainsCoinInfo = map[uint64]evmtypes.EvmCoinInfo{
    EVMChainID: { // Your numeric EVM chain ID (e.g., 9000)
        Denom:        BaseDenom,
        DisplayDenom: DisplayDenom,
        Decimals:     evmtypes.EighteenDecimals,
    },
}

// EVMAppOptions sets up global configuration
func EVMAppOptions(chainID string) error {
    if sealed {
        return nil
    }

    // IMPORTANT: Lookup uses numeric EVMChainID, not Cosmos chainID string
    coinInfo, found := ChainsCoinInfo[EVMChainID]
    if !found {
        return fmt.Errorf("unknown EVM chain id: %d", EVMChainID)
    }

    // Set denom info for the chain
    if err := setBaseDenom(coinInfo); err != nil {
        return err
    }

    baseDenom, err := sdk.GetBaseDenom()
    if err != nil {
        return err
    }

    ethCfg := evmtypes.DefaultChainConfig(EVMChainID)

    err = evmtypes.NewEVMConfigurator().
        WithChainConfig(ethCfg).
        WithEVMCoinInfo(baseDenom, uint8(coinInfo.Decimals)).
        Configure()
    if err != nil {
        return err
    }

    sealed = true
    return nil
}

// setBaseDenom registers display and base denoms
func setBaseDenom(ci evmtypes.EvmCoinInfo) error {
    if err := sdk.RegisterDenom(ci.DisplayDenom, math.LegacyOneDec()); err != nil {
        return err
    }
    return sdk.RegisterDenom(ci.Denom, math.LegacyNewDecWithPrec(1, int64(ci.Decimals)))
}

Step 6: Create Precompiles Configuration

Create app/precompiles.go to define available precompiled contracts:
package app

import (
    "fmt"
    "maps"

    evidencekeeper "cosmossdk.io/x/evidence/keeper"
    authzkeeper "github.com/cosmos/cosmos-sdk/x/authz/keeper"
    bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper"
    distributionkeeper "github.com/cosmos/cosmos-sdk/x/distribution/keeper"
    govkeeper "github.com/cosmos/cosmos-sdk/x/gov/keeper"
    slashingkeeper "github.com/cosmos/cosmos-sdk/x/slashing/keeper"
    stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper"
    bankprecompile "github.com/cosmos/evm/precompiles/bank"
    "github.com/cosmos/evm/precompiles/bech32"
    distprecompile "github.com/cosmos/evm/precompiles/distribution"
    evidenceprecompile "github.com/cosmos/evm/precompiles/evidence"
    govprecompile "github.com/cosmos/evm/precompiles/gov"
    ics20precompile "github.com/cosmos/evm/precompiles/ics20"
    "github.com/cosmos/evm/precompiles/p256"
    slashingprecompile "github.com/cosmos/evm/precompiles/slashing"
    stakingprecompile "github.com/cosmos/evm/precompiles/staking"
    erc20Keeper "github.com/cosmos/evm/x/erc20/keeper"
    transferkeeper "github.com/cosmos/evm/x/ibc/transfer/keeper"
    "github.com/cosmos/evm/x/vm/core/vm"
    evmkeeper "github.com/cosmos/evm/x/vm/keeper"
    channelkeeper "github.com/cosmos/ibc-go/v8/modules/core/04-channel/keeper"
    "github.com/ethereum/go-ethereum/common"
)

const bech32PrecompileBaseGas = 6_000

// NewAvailableStaticPrecompiles returns all available static precompiled contracts
func NewAvailableStaticPrecompiles(
    stakingKeeper stakingkeeper.Keeper,
    distributionKeeper distributionkeeper.Keeper,
    bankKeeper bankkeeper.Keeper,
    erc20Keeper erc20Keeper.Keeper,
    authzKeeper authzkeeper.Keeper,
    transferKeeper transferkeeper.Keeper,
    channelKeeper channelkeeper.Keeper,
    evmKeeper *evmkeeper.Keeper,
    govKeeper govkeeper.Keeper,
    slashingKeeper slashingkeeper.Keeper,
    evidenceKeeper evidencekeeper.Keeper,
) map[common.Address]vm.PrecompiledContract {
    precompiles := maps.Clone(vm.PrecompiledContractsBerlin)

    p256Precompile := &p256.Precompile{}

    bech32Precompile, err := bech32.NewPrecompile(bech32PrecompileBaseGas)
    if err != nil {
        panic(fmt.Errorf("failed to instantiate bech32 precompile: %w", err))
    }

    stakingPrecompile, err := stakingprecompile.NewPrecompile(stakingKeeper, authzKeeper)
    if err != nil {
        panic(fmt.Errorf("failed to instantiate staking precompile: %w", err))
    }

    distributionPrecompile, err := distprecompile.NewPrecompile(distributionKeeper, stakingKeeper, authzKeeper, evmKeeper)
    if err != nil {
        panic(fmt.Errorf("failed to instantiate distribution precompile: %w", err))
    }

    ibcTransferPrecompile, err := ics20precompile.NewPrecompile(stakingKeeper, transferKeeper, channelKeeper, authzKeeper, evmKeeper)
    if err != nil {
        panic(fmt.Errorf("failed to instantiate ICS20 precompile: %w", err))
    }

    bankPrecompile, err := bankprecompile.NewPrecompile(bankKeeper, erc20Keeper)
    if err != nil {
        panic(fmt.Errorf("failed to instantiate bank precompile: %w", err))
    }

    govPrecompile, err := govprecompile.NewPrecompile(govKeeper, authzKeeper)
    if err != nil {
        panic(fmt.Errorf("failed to instantiate gov precompile: %w", err))
    }

    slashingPrecompile, err := slashingprecompile.NewPrecompile(slashingKeeper, authzKeeper)
    if err != nil {
        panic(fmt.Errorf("failed to instantiate slashing precompile: %w", err))
    }

    evidencePrecompile, err := evidenceprecompile.NewPrecompile(evidenceKeeper, authzKeeper)
    if err != nil {
        panic(fmt.Errorf("failed to instantiate evidence precompile: %w", err))
    }

    // Stateless precompiles
    precompiles[bech32Precompile.Address()] = bech32Precompile
    precompiles[p256Precompile.Address()] = p256Precompile

    // Stateful precompiles
    precompiles[stakingPrecompile.Address()] = stakingPrecompile
    precompiles[distributionPrecompile.Address()] = distributionPrecompile
    precompiles[ibcTransferPrecompile.Address()] = ibcTransferPrecompile
    precompiles[bankPrecompile.Address()] = bankPrecompile
    precompiles[govPrecompile.Address()] = govPrecompile
    precompiles[slashingPrecompile.Address()] = slashingPrecompile
    precompiles[evidencePrecompile.Address()] = evidencePrecompile

    return precompiles
}

Step 7: Update app.go Wiring

Add EVM Imports

import (
    // ... other imports
    ante "github.com/your-repo/your-chain/ante"
    evmante "github.com/cosmos/evm/ante"
    evmcosmosante "github.com/cosmos/evm/ante/cosmos"
    evmevmante "github.com/cosmos/evm/ante/evm"
    evmencoding "github.com/cosmos/evm/encoding"
    srvflags "github.com/cosmos/evm/server/flags"
    cosmosevmtypes "github.com/cosmos/evm/types"
    "github.com/cosmos/evm/x/erc20"
    erc20keeper "github.com/cosmos/evm/x/erc20/keeper"
    erc20types "github.com/cosmos/evm/x/erc20/types"
    "github.com/cosmos/evm/x/feemarket"
    feemarketkeeper "github.com/cosmos/evm/x/feemarket/keeper"
    feemarkettypes "github.com/cosmos/evm/x/feemarket/types"
    evm "github.com/cosmos/evm/x/vm"
    evmkeeper "github.com/cosmos/evm/x/vm/keeper"
    evmtypes "github.com/cosmos/evm/x/vm/types"
    _ "github.com/cosmos/evm/x/vm/core/tracers/js"
    _ "github.com/cosmos/evm/x/vm/core/tracers/native"

    // Replace default transfer with EVM's extended transfer module
    transfer "github.com/cosmos/evm/x/ibc/transfer"
    ibctransferkeeper "github.com/cosmos/evm/x/ibc/transfer/keeper"
    ibctransfertypes "github.com/cosmos/ibc-go/v8/modules/apps/transfer/types"

    // Add authz for precompiles
    authzkeeper "github.com/cosmos/cosmos-sdk/x/authz/keeper"
)

Add Module Permissions

var maccPerms = map[string][]string{
    // ... existing permissions
    evmtypes.ModuleName:       {authtypes.Minter, authtypes.Burner},
    feemarkettypes.ModuleName: nil,
    erc20types.ModuleName:     {authtypes.Minter, authtypes.Burner},
}

Update App Struct

type ChainApp struct {
    // ... existing fields
    FeeMarketKeeper feemarketkeeper.Keeper
    EVMKeeper       *evmkeeper.Keeper
    Erc20Keeper     erc20keeper.Keeper
    AuthzKeeper     authzkeeper.Keeper
}

Update NewChainApp Constructor

func NewChainApp(
    // ... existing params
    appOpts servertypes.AppOptions,
    evmAppOptions EVMOptionsFn, // Add this parameter
    baseAppOptions ...func(*baseapp.BaseApp),
) *ChainApp {
    // ...
}

Replace SDK Encoding

encodingConfig := evmencoding.MakeConfig()
appCodec := encodingConfig.Codec
legacyAmino := encodingConfig.Amino
txConfig := encodingConfig.TxConfig

Add Store Keys

keys := storetypes.NewKVStoreKeys(
    // ... existing keys
    evmtypes.StoreKey,
    feemarkettypes.StoreKey,
    erc20types.StoreKey,
)

tkeys := storetypes.NewTransientStoreKeys(
    paramstypes.TStoreKey,
    evmtypes.TransientKey,
    feemarkettypes.TransientKey,
)

Initialize Keepers (Critical Order)

Keepers must be initialized in exact order: FeeMarket → EVM → Erc20 → Transfer
// Initialize AuthzKeeper if not already done
app.AuthzKeeper = authzkeeper.NewKeeper(
    keys[authz.StoreKey],
    appCodec,
    app.MsgServiceRouter(),
    app.AccountKeeper,
)

// Initialize FeeMarketKeeper
app.FeeMarketKeeper = feemarketkeeper.NewKeeper(
    appCodec,
    authtypes.NewModuleAddress(govtypes.ModuleName),
    keys[feemarkettypes.StoreKey],
    tkeys[feemarkettypes.TransientKey],
    app.GetSubspace(feemarkettypes.ModuleName),
)

// Initialize EVMKeeper
tracer := cast.ToString(appOpts.Get(srvflags.EVMTracer))
app.EVMKeeper = evmkeeper.NewKeeper(
    appCodec,
    keys[evmtypes.StoreKey],
    tkeys[evmtypes.TransientKey],
    authtypes.NewModuleAddress(govtypes.ModuleName),
    app.AccountKeeper,
    app.BankKeeper,
    app.StakingKeeper,
    app.FeeMarketKeeper,
    &app.Erc20Keeper, // Pass pointer for circular dependency
    tracer,
    app.GetSubspace(evmtypes.ModuleName),
)

// Initialize Erc20Keeper
app.Erc20Keeper = erc20keeper.NewKeeper(
    keys[erc20types.StoreKey],
    appCodec,
    authtypes.NewModuleAddress(govtypes.ModuleName),
    app.AccountKeeper,
    app.BankKeeper,
    app.EVMKeeper,
    app.StakingKeeper,
    app.AuthzKeeper,
    &app.TransferKeeper, // Pass pointer for circular dependency
)

// Initialize extended TransferKeeper
app.TransferKeeper = ibctransferkeeper.NewKeeper(
    appCodec,
    keys[ibctransfertypes.StoreKey],
    app.GetSubspace(ibctransfertypes.ModuleName),
    app.IBCKeeper.ChannelKeeper,
    app.IBCKeeper.ChannelKeeper,
    app.IBCKeeper.PortKeeper,
    app.AccountKeeper,
    app.BankKeeper,
    scopedTransferKeeper,
    app.Erc20Keeper,
    authtypes.NewModuleAddress(govtypes.ModuleName).String(),
)

// CRITICAL: Wire IBC callbacks for automatic ERC20 registration
transferModule := transfer.NewIBCModule(app.TransferKeeper)
app.Erc20Keeper.SetICS20Module(transferModule)

// Configure EVM Precompiles
corePrecompiles := NewAvailableStaticPrecompiles(
    *app.StakingKeeper,
    app.DistrKeeper,
    app.BankKeeper,
    app.Erc20Keeper,
    app.AuthzKeeper,
    app.TransferKeeper,
    app.IBCKeeper.ChannelKeeper,
    app.EVMKeeper,
    app.GovKeeper,
    app.SlashingKeeper,
    app.EvidenceKeeper,
)
app.EVMKeeper.WithStaticPrecompiles(corePrecompiles)

Add Modules to Module Manager

app.ModuleManager = module.NewManager(
    // ... existing modules
    evm.NewAppModule(app.EVMKeeper, app.AccountKeeper, app.GetSubspace(evmtypes.ModuleName)),
    feemarket.NewAppModule(app.FeeMarketKeeper, app.GetSubspace(feemarkettypes.ModuleName)),
    erc20.NewAppModule(app.Erc20Keeper, app.AccountKeeper, app.GetSubspace(erc20types.ModuleName)),
    transfer.NewAppModule(app.TransferKeeper),
)

Update Module Ordering

// SetOrderBeginBlockers - EVM must come after feemarket
app.ModuleManager.SetOrderBeginBlockers(
    // ... other modules
    erc20types.ModuleName,
    feemarkettypes.ModuleName,
    evmtypes.ModuleName,
    // ...
)

// SetOrderEndBlockers
app.ModuleManager.SetOrderEndBlockers(
    // ... other modules
    evmtypes.ModuleName,
    feemarkettypes.ModuleName,
    erc20types.ModuleName,
    // ...
)

// SetOrderInitGenesis - feemarket must be before genutil
genesisModuleOrder := []string{
    // ... other modules
    evmtypes.ModuleName,
    feemarkettypes.ModuleName,
    erc20types.ModuleName,
    // ...
}

Update Ante Handler

options := ante.HandlerOptions{
    AccountKeeper:          app.AccountKeeper,
    BankKeeper:             app.BankKeeper,
    SignModeHandler:        txConfig.SignModeHandler(),
    FeegrantKeeper:         app.FeeGrantKeeper,
    SigGasConsumer:         ante.DefaultSigVerificationGasConsumer,
    FeeMarketKeeper:        app.FeeMarketKeeper,
    EvmKeeper:              app.EVMKeeper,
    ExtensionOptionChecker: cosmosevmtypes.HasDynamicFeeExtensionOption,
    MaxTxGasWanted:         cast.ToUint64(appOpts.Get(srvflags.EVMMaxTxGasWanted)),
    TxFeeChecker:           evmevmante.NewDynamicFeeChecker(app.FeeMarketKeeper),
    // ... other options
}

anteHandler, err := ante.NewAnteHandler(options)
if err != nil {
    panic(err)
}
app.SetAnteHandler(anteHandler)

Update DefaultGenesis

func (a *ChainApp) DefaultGenesis() map[string]json.RawMessage {
    genesis := a.BasicModuleManager.DefaultGenesis(a.appCodec)

    // Add EVM genesis config
    evmGenState := evmtypes.DefaultGenesisState()
    evmGenState.Params.ActiveStaticPrecompiles = evmtypes.AvailableStaticPrecompiles
    genesis[evmtypes.ModuleName] = a.appCodec.MustMarshalJSON(evmGenState)

    // Add ERC20 genesis config
    erc20GenState := erc20types.DefaultGenesisState()
    genesis[erc20types.ModuleName] = a.appCodec.MustMarshalJSON(erc20GenState)

    return genesis
}

Step 8: Create Ante Handler Files

Create new ante/ directory in your project root.

ante/handler_options.go

package ante

import (
    errorsmod "cosmossdk.io/errors"
    storetypes "cosmossdk.io/store/types"
    txsigning "cosmossdk.io/x/tx/signing"

    "github.com/cosmos/cosmos-sdk/codec"
    errortypes "github.com/cosmos/cosmos-sdk/types/errors"
    "github.com/cosmos/cosmos-sdk/x/auth/ante"
    "github.com/cosmos/cosmos-sdk/x/auth/signing"
    authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"

    anteinterfaces "github.com/cosmos/evm/ante/interfaces"
    ibckeeper "github.com/cosmos/ibc-go/v10/modules/core/keeper"
)

type HandlerOptions struct {
    Cdc                    codec.BinaryCodec
    AccountKeeper          anteinterfaces.AccountKeeper
    BankKeeper             anteinterfaces.BankKeeper
    IBCKeeper              *ibckeeper.Keeper
    FeeMarketKeeper        anteinterfaces.FeeMarketKeeper
    EvmKeeper              anteinterfaces.EVMKeeper
    FeegrantKeeper         ante.FeegrantKeeper
    ExtensionOptionChecker ante.ExtensionOptionChecker
    SignModeHandler        *txsigning.HandlerMap
    SigGasConsumer         func(meter storetypes.GasMeter, sig signing.SignatureV2, params authtypes.Params) error
    MaxTxGasWanted         uint64
    TxFeeChecker           ante.TxFeeChecker
}

func (options HandlerOptions) Validate() error {
    if options.Cdc == nil {
        return errorsmod.Wrap(errortypes.ErrLogic, "codec is required for ante builder")
    }
    if options.AccountKeeper == nil {
        return errorsmod.Wrap(errortypes.ErrLogic, "account keeper is required for ante builder")
    }
    if options.BankKeeper == nil {
        return errorsmod.Wrap(errortypes.ErrLogic, "bank keeper is required for ante builder")
    }
    if options.SignModeHandler == nil {
        return errorsmod.Wrap(errortypes.ErrLogic, "sign mode handler is required for ante builder")
    }
    if options.EvmKeeper == nil {
        return errorsmod.Wrap(errortypes.ErrLogic, "evm keeper is required for ante builder")
    }
    if options.FeeMarketKeeper == nil {
        return errorsmod.Wrap(errortypes.ErrLogic, "feemarket keeper is required for ante builder")
    }
    return nil
}

ante/ante_cosmos.go

package ante

import (
    sdk "github.com/cosmos/cosmos-sdk/types"
    "github.com/cosmos/cosmos-sdk/x/auth/ante"
    ibcante "github.com/cosmos/ibc-go/v10/modules/core/ante"

    cosmosante "github.com/cosmos/evm/ante/cosmos"
)

// newCosmosAnteHandler creates the default SDK ante handler for Cosmos transactions
func newCosmosAnteHandler(options HandlerOptions) sdk.AnteHandler {
    return sdk.ChainAnteDecorators(
        ante.NewSetUpContextDecorator(),
        ante.NewExtensionOptionsDecorator(options.ExtensionOptionChecker),
        cosmosante.NewValidateBasicDecorator(options.EvmKeeper),
        ante.NewTxTimeoutHeightDecorator(),
        ante.NewValidateMemoDecorator(options.AccountKeeper),
        ante.NewConsumeGasForTxSizeDecorator(options.AccountKeeper),
        cosmosante.NewDeductFeeDecorator(
            options.AccountKeeper,
            options.BankKeeper,
            options.FeegrantKeeper,
            options.TxFeeChecker,
        ),
        ante.NewSetPubKeyDecorator(options.AccountKeeper),
        ante.NewValidateSigCountDecorator(options.AccountKeeper),
        ante.NewSigGasConsumeDecorator(options.AccountKeeper, options.SigGasConsumer),
        ante.NewSigVerificationDecorator(options.AccountKeeper, options.SignModeHandler),
        ante.NewIncrementSequenceDecorator(options.AccountKeeper),
        ibcante.NewRedundantRelayDecorator(options.IBCKeeper),
        cosmosante.NewGasWantedDecorator(options.EvmKeeper, options.FeeMarketKeeper),
    )
}

ante/ante_evm.go

package ante

import (
    sdk "github.com/cosmos/cosmos-sdk/types"
    evmante "github.com/cosmos/evm/ante/evm"
)

// newMonoEVMAnteHandler creates the sdk.AnteHandler for EVM transactions
func newMonoEVMAnteHandler(options HandlerOptions) sdk.AnteHandler {
    return sdk.ChainAnteDecorators(
        evmante.NewEVMMonoDecorator(
            options.AccountKeeper,
            options.FeeMarketKeeper,
            options.EvmKeeper,
            options.MaxTxGasWanted,
        ),
    )
}

ante/ante.go

package ante

import (
    errorsmod "cosmossdk.io/errors"
    sdk "github.com/cosmos/cosmos-sdk/types"
    errortypes "github.com/cosmos/cosmos-sdk/types/errors"
    authante "github.com/cosmos/cosmos-sdk/x/auth/ante"
    "github.com/cosmos/evm/ante/evm"
)

// NewAnteHandler routes Ethereum or SDK transactions to the appropriate handler
func NewAnteHandler(options HandlerOptions) (sdk.AnteHandler, error) {
    if err := options.Validate(); err != nil {
        return nil, err
    }

    return func(ctx sdk.Context, tx sdk.Tx, sim bool) (newCtx sdk.Context, err error) {
        var anteHandler sdk.AnteHandler

        if ethTx, ok := tx.(*evm.EthTx); ok {
            // Handle as Ethereum transaction
            anteHandler = newMonoEVMAnteHandler(options)
        } else {
            // Handle as normal Cosmos SDK transaction
            anteHandler = newCosmosAnteHandler(options)
        }

        return anteHandler(ctx, tx, sim)
    }, nil
}

Step 9: Update Command Files

Update cmd/appd/commands.go

import (
    // Add imports
    evmcmd "github.com/cosmos/evm/client"
    evmserver "github.com/cosmos/evm/server"
    evmserverconfig "github.com/cosmos/evm/server/config"
    srvflags "github.com/cosmos/evm/server/flags"
)

// Define custom app config struct
type CustomAppConfig struct {
    serverconfig.Config
    EVM     evmserverconfig.EVMConfig
    JSONRPC evmserverconfig.JSONRPCConfig
    TLS     evmserverconfig.TLSConfig
}

// Update initAppConfig to include EVM config
func initAppConfig() (string, interface{}) {
    srvCfg, customAppTemplate := serverconfig.AppConfig(DefaultDenom)
    customAppConfig := CustomAppConfig{
        Config:  *srvCfg,
        EVM:     *evmserverconfig.DefaultEVMConfig(),
        JSONRPC: *evmserverconfig.DefaultJSONRPCConfig(),
        TLS:     *evmserverconfig.DefaultTLSConfig(),
    }
    customAppTemplate += evmserverconfig.DefaultEVMConfigTemplate
    return customAppTemplate, customAppConfig
}

// In initRootCmd, replace server.AddCommands with evmserver.AddCommands
func initRootCmd(...) {
    // ...
    evmserver.AddCommands(
        rootCmd,
        evmserver.NewDefaultStartOptions(newApp, app.DefaultNodeHome),
        appExport,
        addModuleInitFlags,
    )

    rootCmd.AddCommand(
        // ... existing commands
        evmcmd.KeyCommands(app.DefaultNodeHome, true),
    )

    var err error
    rootCmd, err = srvflags.AddTxFlags(rootCmd)
    if err != nil {
        panic(err)
    }
}

Update cmd/appd/root.go

import (
    // ... existing imports
    evmkeyring "github.com/cosmos/evm/crypto/keyring"
    evmtypes "github.com/cosmos/evm/x/vm/types"
    sdk "github.com/cosmos/cosmos-sdk/types"
    "github.com/cosmos/cosmos-sdk/client/flags"
)

func NewRootCmd() *cobra.Command {
    // ...
    // In client context setup:
    clientCtx = clientCtx.
        WithKeyringOptions(evmkeyring.Option()).
        WithBroadcastMode(flags.FlagBroadcastMode).
        WithLedgerHasProtobuf(true)

    // Update the coin type
    cfg := sdk.GetConfig()
    cfg.SetCoinType(evmtypes.Bip44CoinType) // Sets coin type to 60
    cfg.Seal()

    // ...
    return rootCmd
}

Step 10: Sign Mode Configuration (Optional)

Sign Mode Textual is a new Cosmos SDK signing method that may not be compatible with all Ethereum signing workflows.
// In app.go
import (
    "github.com/cosmos/cosmos-sdk/types/tx"
    "github.com/cosmos/cosmos-sdk/x/auth/tx"
)

// ... in NewChainApp, where you set up your txConfig:
txConfig := tx.NewTxConfigWithOptions(
    appCodec,
    tx.ConfigOptions{
        // Remove SignMode_SIGN_MODE_TEXTUAL from enabled sign modes
        EnabledSignModes: []signing.SignMode{
            signing.SignMode_SIGN_MODE_DIRECT,
            signing.SignMode_SIGN_MODE_LEGACY_AMINO_JSON,
            signing.SignMode_SIGN_MODE_EIP_191,
        },
        // ...
    },
)

Option B: Enable Sign Mode Textual

If your chain requires Sign Mode Textual support, ensure your ante handler and configuration support it. The reference implementation in evmd enables it by default.

Step 11: Testing Your Integration

Build and Run Tests

# Run all unit tests
make test-all

# Run EVM-specific tests
make test-appd

# Run integration tests
make test-integration

Local Node Testing

# Copy and adapt the script from Cosmos EVM repo
curl -O https://raw.githubusercontent.com/cosmos/evm/main/local_node.sh
chmod +x local_node.sh
./local_node.sh

Verify EVM Functionality

  • Check JSON-RPC server starts on configured port (default: 8545)
  • Verify MetaMask connection to your local node
  • Test precompiles accessibility at expected addresses
  • Confirm IBC tokens automatically register as ERC20s

Genesis Validation

appd genesis validate-genesis

Troubleshooting

Common Issues and Solutions

  1. “Unknown extension option” errors
    • Cause: Ante handler not correctly routing EVM transactions
    • Solution: Verify ante.go correctly identifies extension options /cosmos.evm.vm.v1.ExtensionOptionsEthereumTx
  2. Keeper initialization panics
    • Cause: Incorrect initialization order
    • Solution: Ensure keepers are initialized in order: FeeMarket → EVM → Erc20 → Transfer
  3. IBC tokens not registering as ERC20s
    • Cause: Missing SetICS20Module call
    • Solution: Add app.Erc20Keeper.SetICS20Module(transferModule) after keeper initialization
  4. EVM transactions failing with decimal precision errors
    • Cause: Not using precisebank module
    • Solution: Wire PreciseBankKeeper and pass it to EVMKeeper instead of BankKeeper
  5. Module not found errors during compilation
    • Cause: Missing or incorrect go.mod replacements
    • Solution: Ensure correct replace directive for go-ethereum
  6. Chain ID mismatch errors
    • Cause: Confusion between Cosmos and EVM chain IDs
    • Solution: Use string Cosmos chain ID for IBC/Cosmos operations, integer EVM chain ID for EVM operations
  7. Authz-related precompile errors
    • Cause: Missing authz keeper in precompile initialization
    • Solution: Ensure AuthzKeeper is initialized and passed to precompiles that require it
Remember to always check github.com/cosmos/evm for the latest updates and examples.