This is a mandatory breaking change for chains with existing ERC20 token pairs.Without this migration, your ERC20 tokens will become inaccessible via EVM, showing zero balances and failing all operations.
Impact Assessment
Affected Chains
Your chain needs this migration if you have:- IBC tokens converted to ERC20
- Token factory tokens with ERC20 representations
- Any existing
DynamicPrecompiles
orNativePrecompiles
in storage
Symptoms if Not Migrated
- ERC20 balances will show as 0 when queried via EVM
totalSupply()
calls return 0- Token transfers via ERC20 interface fail
- Native Cosmos balances remain intact but inaccessible via EVM
Storage Changes
Implementation
Quick Start
Add this essential migration logic to your existing upgrade handler:Copy
Ask AI
/ In your upgrade handler
store := ctx.KVStore(storeKeys[erc20types.StoreKey])
const addressLength = 42 / "0x" + 40 hex characters
/ Migrate dynamic precompiles (IBC tokens, token factory)
if oldData := store.Get([]byte("DynamicPrecompiles")); len(oldData) > 0 {
for i := 0; i < len(oldData); i += addressLength {
address := common.HexToAddress(string(oldData[i : i+addressLength]))
erc20Keeper.SetDynamicPrecompile(ctx, address)
}
store.Delete([]byte("DynamicPrecompiles"))
}
/ Migrate native precompiles
if oldData := store.Get([]byte("NativePrecompiles")); len(oldData) > 0 {
for i := 0; i < len(oldData); i += addressLength {
address := common.HexToAddress(string(oldData[i : i+addressLength]))
erc20Keeper.SetNativePrecompile(ctx, address)
}
store.Delete([]byte("NativePrecompiles"))
}
Show Complete Implementation Example
Show Complete Implementation Example
1
Create Upgrade Handler
2
package v040
import (
"context"
storetypes "cosmossdk.io/store/types"
upgradetypes "cosmossdk.io/x/upgrade/types"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/module"
erc20keeper "github.com/cosmos/evm/x/erc20/keeper"
erc20types "github.com/cosmos/evm/x/erc20/types"
"github.com/ethereum/go-ethereum/common"
)
const UpgradeName = "v0.4.0"
func CreateUpgradeHandler(
mm *module.Manager,
configurator module.Configurator,
keepers *UpgradeKeepers,
storeKeys map[string]*storetypes.KVStoreKey,
) upgradetypes.UpgradeHandler {
return func(c context.Context, plan upgradetypes.Plan, vm module.VersionMap) (module.VersionMap, error) {
ctx := sdk.UnwrapSDKContext(c)
ctx.Logger().Info("Starting v0.4.0 upgrade...")
/ Run standard module migrations
vm, err := mm.RunMigrations(ctx, configurator, vm)
if err != nil {
return vm, err
}
/ Migrate ERC20 precompiles
if err := migrateERC20Precompiles(ctx, storeKeys[erc20types.StoreKey], keepers.Erc20Keeper); err != nil {
return vm, err
}
ctx.Logger().Info("v0.4.0 upgrade complete")
return vm, nil
}
}
3
Implement Migration Logic
4
package v040
import (
sdk "github.com/cosmos/cosmos-sdk/types"
storetypes "cosmossdk.io/store/types"
erc20keeper "github.com/cosmos/evm/x/erc20/keeper"
"github.com/ethereum/go-ethereum/common"
)
func migrateERC20Precompiles(
ctx sdk.Context,
storeKey *storetypes.KVStoreKey,
erc20Keeper erc20keeper.Keeper,
) error {
store := ctx.KVStore(storeKey)
const addressLength = 42 / "0x" + 40 hex characters
migrations := []struct {
oldKey string
setter func(sdk.Context, common.Address)
description string
}{
{
oldKey: "DynamicPrecompiles",
setter: erc20Keeper.SetDynamicPrecompile,
description: "dynamic precompiles (token factory, IBC tokens)",
},
{
oldKey: "NativePrecompiles",
setter: erc20Keeper.SetNativePrecompile,
description: "native precompiles",
},
}
for _, migration := range migrations {
oldData := store.Get([]byte(migration.oldKey))
if len(oldData) == 0 {
ctx.Logger().Info("No legacy data found", "type", migration.description)
continue
}
addressCount := len(oldData) / addressLength
ctx.Logger().Info("Migrating precompiles",
"type", migration.description,
"count", addressCount,
)
migratedCount := 0
for i := 0; i < len(oldData); i += addressLength {
if i+addressLength > len(oldData) {
ctx.Logger().Error("Invalid data length",
"type", migration.description,
"position", i,
)
break
}
addressStr := string(oldData[i : i+addressLength])
address := common.HexToAddress(addressStr)
/ Validate address
if address == (common.Address{}) {
ctx.Logger().Warn("Skipping zero address",
"type", migration.description,
"raw", addressStr,
)
continue
}
/ Migrate to new storage
migration.setter(ctx, address)
migratedCount++
ctx.Logger().Debug("Migrated precompile",
"type", migration.description,
"address", address.String(),
"index", migratedCount,
)
}
/ Clean up old storage
store.Delete([]byte(migration.oldKey))
ctx.Logger().Info("Migration complete",
"type", migration.description,
"migrated", migratedCount,
"expected", addressCount,
)
}
return nil
}
5
Register Upgrade Handler
6
import (
v040 "github.com/yourchain/app/upgrades/v040"
)
func (app *App) RegisterUpgradeHandlers() {
app.UpgradeKeeper.SetUpgradeHandler(
v040.UpgradeName,
v040.CreateUpgradeHandler(
app.ModuleManager,
app.configurator,
&v040.UpgradeKeepers{
Erc20Keeper: app.Erc20Keeper,
},
app.keys,
),
)
}
Testing
Pre-Upgrade Verification
Copy
Ask AI
# Query existing token pairs
mantrachaind query erc20 token-pairs --output json | jq
# Check ERC20 balances for a known address
cast call $TOKEN_ADDRESS "balanceOf(address)" $USER_ADDRESS --rpc-url http://localhost:8545
# Export state for backup
mantrachaind export > pre-upgrade-state.json
Post-Upgrade Verification
Copy
Ask AI
# Verify precompiles are accessible
cast call $TOKEN_ADDRESS "totalSupply()" --rpc-url http://localhost:8545
# Check balance restoration
cast call $TOKEN_ADDRESS "balanceOf(address)" $USER_ADDRESS --rpc-url http://localhost:8545
# Test token transfer
cast send $TOKEN_ADDRESS "transfer(address,uint256)" $RECIPIENT 1000 \
--private-key $PRIVATE_KEY --rpc-url http://localhost:8545
# Verify in exported state
mantrachaind export | jq '.app_state.erc20.dynamic_precompiles'
Integration Test
tests/upgrade_test.go
Copy
Ask AI
func TestERC20PrecompileMigration(t *testing.T) {
/ Setup test environment
app, ctx := setupTestApp(t)
/ Create legacy storage entries
store := ctx.KVStore(app.keys[erc20types.StoreKey])
/ Add test addresses in old format
dynamicAddresses := []string{
"0x6eC942095eCD4948d9C094337ABd59Dc3c521005",
"0x1234567890123456789012345678901234567890",
}
dynamicData := ""
for _, addr := range dynamicAddresses {
dynamicData += addr
}
store.Set([]byte("DynamicPrecompiles"), []byte(dynamicData))
/ Run migration
err := migrateERC20Precompiles(ctx, app.keys[erc20types.StoreKey], app.Erc20Keeper)
require.NoError(t, err)
/ Verify migration
migratedAddresses := app.Erc20Keeper.GetDynamicPrecompiles(ctx)
require.Len(t, migratedAddresses, len(dynamicAddresses))
/ Verify old storage is cleaned
oldData := store.Get([]byte("DynamicPrecompiles"))
require.Nil(t, oldData)
}
Verification Checklist
- Test migration on testnet first
- Document all existing token pairs
- Verify ERC20 balances post-upgrade
- Test token transfers work
- Confirm IBC token conversions function