This is a mandatory breaking change for pre-v0.4.x chains with existing ERC20 token pairs.If neglected, existing ERC20 tokens will become inaccessible, return zero balances and fail 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 DynamicPrecompilesorNativePrecompilesin 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 to your existing upgrade handler:
// 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"))
}
Testing
Pre-Upgrade Verification
# 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
# 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
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
References