> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cosmos.network/llms.txt
> Use this file to discover all available pages before exploring further.

# ERC20 Precompiles Migration

> Migration for ERC20 precompiles when upgrading to v0.4.0

<Warning>
  **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.
</Warning>

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

<Tabs defaultValue="before">
  <TabItem value="before" label="Before (v0.3.x)">
    * Precompiles stored as concatenated hex strings in parameter storage
    * Keys: `"DynamicPrecompiles"` and `"NativePrecompiles"`
    * Format: Multiple addresses concatenated as 42-character hex strings
  </TabItem>

  <TabItem value="after" label="After (v0.4.0)">
    * Dedicated prefix stores for each precompile type
    * Keys: `types.KeyPrefixDynamicPrecompiles` and `types.KeyPrefixNativePrecompiles`
    * Individual storage entries per address
  </TabItem>
</Tabs>

## Implementation

### Quick Start

Add to your existing upgrade handler:

```go theme={"theme":{"light":"github-light-high-contrast","dark":"github-dark-high-contrast"}}
// 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"))
}
```

<details>
  <summary>Complete Implementation Example</summary>

  <Steps>
    ### Create Upgrade Handler

    ```go app/upgrades/v040/handler.go theme={"theme":{"light":"github-light-high-contrast","dark":"github-dark-high-contrast"}}
    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
        }
    }
    ```

    ### Implement Migration Logic

    ```go app/upgrades/v040/erc20_migration.go theme={"theme":{"light":"github-light-high-contrast","dark":"github-dark-high-contrast"}}
    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
    }
    ```

    ### Register Upgrade Handler

    ```go app/app.go theme={"theme":{"light":"github-light-high-contrast","dark":"github-dark-high-contrast"}}
    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,
            ),
        )
    }
    ```
  </Steps>
</details>

## Testing

### Pre-Upgrade Verification

```bash theme={"theme":{"light":"github-light-high-contrast","dark":"github-dark-high-contrast"}}
# 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

```bash theme={"theme":{"light":"github-light-high-contrast","dark":"github-dark-high-contrast"}}
# 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

```go tests/upgrade_test.go theme={"theme":{"light":"github-light-high-contrast","dark":"github-dark-high-contrast"}}
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

* [GitHub Issue #424](https://github.com/cosmos/evm/issues/424)
* [MANTRA-Chain Implementation](https://github.com/MANTRA-Chain/mantrachain/pull/409)
* [E2E Test Suite](https://github.com/MANTRA-Chain/mantrachain-e2e/pull/41)
