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 or NativePrecompiles 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:
    // 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

    tests/upgrade_test.go
    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

    References