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
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