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, evmd
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:
app/app.go
: Set chain ID constants
const CosmosChainID = "mychain-1" // Standard Cosmos format
const EVMChainID = 9000 // EIP-155 integer
- 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:
app/app.go
:
const CoinType uint32 = 60
chain_registry.json
:
Base Denomination and Power Reduction
Changing from 6 decimals (Cosmos convention) to 18 decimals (EVM standard) is highly recommended for full compatibility.
- Set the denomination in
app/app.go
:
const BaseDenomUnit int64 = 18
- 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),
)
The Cosmos EVM x/erc20
module can automatically register ERC20 token pairs for incoming single-hop IBC tokens (prefixed with “ibc/”).
Configuration Requirements
-
Use the Extended IBC Transfer Module: Import and use the transfer module from
github.com/cosmos/evm/x/ibc/transfer
-
Enable ERC20 Module Parameters in genesis:
erc20Params := erc20types.DefaultParams()
erc20Params.EnableErc20 = true
erc20Params.EnableEVMHook = true
- 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/evmd/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/evmd/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.
Option A: Disable Sign Mode Textual (Recommended for pure EVM compatibility)
// 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-evmd
# 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
evmd genesis validate-genesis
Check github.com/cosmos/evm for the latest updates or to open an issue.