Skip to main content

ERC20 Module Reference

The ERC20 module (x/erc20) implements Single Token Representation v2 (STRv2), ensuring seamless interoperability between Cosmos coins and EVM ERC-20 tokens. It automatically creates ERC-20 representations for IBC tokens and maintains bidirectional conversion.

Module Overview

Purpose: Provide unified token representation across Cosmos and EVM environments Key Functionality:
  • Automatic ERC-20 wrapper creation for Cosmos coins and IBC tokens
  • Bidirectional token conversion (Cosmos ↔ EVM)
  • Native token precompiles at deterministic addresses
  • Dynamic precompile generation for IBC tokens
  • ERC-20 allowance tracking for cross-environment compatibility
  • Single token representation prevents fragmentation
Source Code: x/erc20 Parameter Defaults: x/erc20/types/params.go

Single Token Representation v2 (STRv2)

Problem Solved

Without STRv2, tokens exist in separate “worlds”:
  • Cosmos side: Bank module tracks ibc/ABC... denominations
  • EVM side: Separate ERC-20 contracts with different addresses
  • Result: Token fragmentation, broken liquidity, complex accounting

STRv2 Solution

STRv2 ensures a single token representation:
  1. IBC token arrives → Automatically gets ERC-20 wrapper at deterministic address
  2. User wants EVM access → Convert bank balance to ERC-20 (same total supply)
  3. User wants Cosmos access → Convert ERC-20 back to bank balance
  4. Result: Same token, accessible from both environments, unified liquidity
Implementation: token_pair.go:18-29

Configuration Methods

The ERC20 module is configured through genesis.json before chain launch.

Method 1: Direct JSON Editing

Edit ~/.evmd/config/genesis.json directly:
{
  "app_state": {
    "erc20": {
      "params": {
        "enable_erc20": true,
        "permissionless_registration": true
      },
      "token_pairs": [
        {
          "erc20_address": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
          "denom": "atest",
          "enabled": true,
          "contract_owner": 1
        }
      ],
      "native_precompiles": ["0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"],
      "dynamic_precompiles": [],
      "allowances": []
    }
  }
}

Method 2: Using jq Command-Line Tool

From local_node.sh:247-248:
GENESIS="$HOME/.evmd/config/genesis.json"
TMP="$HOME/.evmd/config/tmp_genesis.json"

# Set native precompile for native token
jq '.app_state.erc20.native_precompiles=["0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"]' \
  "$GENESIS" >"$TMP" && mv "$TMP" "$GENESIS"

# Create token pair for native token
jq '.app_state.erc20.token_pairs=[{
  contract_owner:1,
  erc20_address:"0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
  denom:"atest",
  enabled:true
}]' "$GENESIS" >"$TMP" && mv "$TMP" "$GENESIS"

Parameters

enable_erc20

What It Does: Master switch to enable/disable the entire ERC20 module functionality. Type: bool Valid Values:
  • true - Enable ERC-20 conversions and token pairs (recommended)
  • false - Disable all ERC-20 functionality
Default: true (params.go:26) Configuration:
{
  "erc20": {
    "params": {
      "enable_erc20": true
    }
  }
}
Impact:
  • When true: Users can convert between Cosmos and ERC-20 representations
  • When false: All conversions disabled, tokens stay in their native format
  • Cannot be changed after genesis without governance proposal
Use Cases:
  • true - Standard for EVM-compatible chains
  • false - Pure Cosmos chain without EVM token bridging

permissionless_registration

What It Does: Controls who can register new token pairs (create ERC-20 wrappers for Cosmos tokens). Type: bool Valid Values:
  • true - Anyone can register token pairs (recommended for public chains)
  • false - Only governance can register token pairs (permissioned)
Default: true (params.go:27) Configuration:
{
  "erc20": {
    "params": {
      "permissionless_registration": true
    }
  }
}
Impact: When true (permissionless):
  • Any user can call RegisterCoin or RegisterERC20 messages
  • IBC tokens automatically get ERC-20 wrappers
  • Fastest UX for new tokens
  • Standard Ethereum-like behavior
When false (permissioned):
  • Only governance proposals can create token pairs
  • More control over which tokens get ERC-20 representations
  • Slower onboarding for new tokens
  • Useful for curated token lists
Recommendation: Use true for public chains to match Ethereum UX

Genesis State

token_pairs

What It Does: Defines the mappings between Cosmos denominations and ERC-20 contract addresses. Type: Array of TokenPair objects Structure: (token_pair.go:32-39)
message TokenPair {
  string erc20_address = 1;   // ERC-20 contract address (hex)
  string denom = 2;            // Cosmos denomination
  bool enabled = 3;            // Whether conversions are active
  Owner contract_owner = 4;    // Who owns the ERC-20 contract
}
Contract Owner Types:
  • OWNER_MODULE = 1 - Owned by erc20 module (native Cosmos token)
  • OWNER_EXTERNAL = 2 - Owned by external EOA (native ERC-20 token)
Configuration:
# Using jq (from local_node.sh:248)
jq '.app_state.erc20.token_pairs=[{
  contract_owner:1,
  erc20_address:"0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
  denom:"atest",
  enabled:true
}]' genesis.json > tmp.json && mv tmp.json genesis.json
{
  "erc20": {
    "token_pairs": [
      {
        "erc20_address": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
        "denom": "atest",
        "enabled": true,
        "contract_owner": 1
      }
    ]
  }
}
Special Address: 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE - Standard address for native token (matches Ethereum convention) Validation: (genesis.go:30-46)
  • No duplicate ERC-20 addresses
  • No duplicate denominations
  • Valid Cosmos denom format
  • Valid Ethereum address format

native_precompiles

What It Does: List of ERC-20 contract addresses that should be accessible as stateful precompiles for native Cosmos tokens. Type: Array of hex addresses (strings) Purpose: Enable EVM smart contracts to interact with native Cosmos tokens at deterministic addresses Configuration:
# Using jq (from local_node.sh:247)
jq '.app_state.erc20.native_precompiles=["0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"]' \
  genesis.json > tmp.json && mv tmp.json genesis.json
{
  "erc20": {
    "native_precompiles": [
      "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"
    ]
  }
}
Requirements:
  • Each precompile address MUST have a corresponding enabled token pair (genesis.go:54-56)
  • Precompile provides ERC-20 interface backed by bank module
Use Case: Enable Solidity contracts to call IERC20(0xEeee...).transfer() for native token

dynamic_precompiles

What It Does: Similar to native_precompiles but for IBC tokens that arrive dynamically after genesis. Type: Array of hex addresses (strings) Default: [] (empty at genesis, populated automatically during IBC transfers) Configuration:
{
  "erc20": {
    "dynamic_precompiles": []
  }
}
How It Works:
  1. IBC token arrives via ibc/transfer module
  2. ERC20 module automatically creates token pair with deterministic address
  3. Address is derived from IBC denom hash (token_pair.go:18-29)
  4. Dynamic precompile address added to list
  5. EVM contracts can now interact with IBC token
Address Derivation: Uses utils.GetIBCDenomAddress(denom) to compute deterministic address from IBC denom Example:
  • IBC denom: ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2
  • Derived ERC-20 address: 0x... (deterministic from denom hash)

allowances

What It Does: Stores ERC-20 allowances (approve/transferFrom) that need to persist across Cosmos-EVM conversions. Type: Array of Allowance objects Structure:
message Allowance {
  string erc20_address = 1;  // ERC-20 contract
  string owner = 2;          // Token owner address
  string spender = 3;        // Approved spender address
  string amount = 4;         // Approved amount
}
Default: [] (empty at genesis) Configuration:
{
  "erc20": {
    "allowances": [
      {
        "erc20_address": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
        "owner": "0x1234...",
        "spender": "0x5678...",
        "amount": "1000000000000000000"
      }
    ]
  }
}
Why Needed: When users convert tokens between Cosmos/EVM, allowances must be preserved for DeFi protocols to work correctly. Validation: (genesis.go:59-74)
  • No duplicate allowances
  • Allowance must reference existing token pair
  • Valid addresses and amounts

Complete Configuration Example

Based on local_node.sh:247-248:
#!/bin/bash

GENESIS="$HOME/.evmd/config/genesis.json"
TMP="$HOME/.evmd/config/tmp_genesis.json"

# Configure ERC20 module for native token (atest)
jq '.app_state.erc20.native_precompiles=["0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"]' \
  "$GENESIS" >"$TMP" && mv "$TMP" "$GENESIS"

jq '.app_state.erc20.token_pairs=[{
  contract_owner:1,
  erc20_address:"0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
  denom:"atest",
  enabled:true
}]' "$GENESIS" >"$TMP" && mv "$TMP" "$GENESIS"

# Validate genesis
evmd genesis validate-genesis --home "$HOME/.evmd"
Or in genesis.json directly:
{
  "app_state": {
    "erc20": {
      "params": {
        "enable_erc20": true,
        "permissionless_registration": true
      },
      "token_pairs": [
        {
          "erc20_address": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
          "denom": "atest",
          "enabled": true,
          "contract_owner": 1
        }
      ],
      "native_precompiles": [
        "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"
      ],
      "dynamic_precompiles": [],
      "allowances": []
    }
  }
}

Runtime Operations

Query Token Pairs

# List all token pairs
evmd query erc20 token-pairs

# Query specific token pair by denom
evmd query erc20 token-pair atest

# Query specific token pair by ERC-20 address
evmd query erc20 token-pair-by-address 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE

Convert Tokens

# Convert Cosmos coin to ERC-20
evmd tx erc20 convert-coin 1000000000000000000atest 0xRecipientAddress \
  --from mykey --chain-id mychain-1

# Convert ERC-20 back to Cosmos coin
evmd tx erc20 convert-erc20 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE \
  1000000000000000000 cosmos1recipient... --from mykey --chain-id mychain-1

Register New Token Pair

# Register Cosmos coin (if permissionless_registration=true)
evmd tx erc20 register-coin ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2 \
  --from mykey --chain-id mychain-1

# Register ERC-20 token (if permissionless_registration=true)
evmd tx erc20 register-erc20 0xTokenAddress --from mykey --chain-id mychain-1

EVM Integration

Using Native Token in Solidity

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract MyDeFiProtocol {
    // Native token precompile address
    IERC20 constant NATIVE_TOKEN = IERC20(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE);

    function depositNative(uint256 amount) external {
        // Transfer native Cosmos token (atest) using ERC-20 interface
        NATIVE_TOKEN.transferFrom(msg.sender, address(this), amount);

        // ... DeFi logic ...
    }

    function withdrawNative(uint256 amount) external {
        // Transfer back to user
        NATIVE_TOKEN.transfer(msg.sender, amount);
    }
}

Using IBC Token in Solidity

// IBC token addresses are deterministic from denom
// Query the address: evmd query erc20 token-pair ibc/ABC...

IERC20 ibcToken = IERC20(0xDeterministicAddressFromIBCDenom);
ibcToken.transfer(recipient, amount);

Common Issues and Solutions

Issue: Token Pair Not Found

Symptom: error: token pair not found for denom X Cause: No ERC-20 wrapper exists for the Cosmos token Solution:
# If permissionless_registration=true:
evmd tx erc20 register-coin <denom> --from mykey

# If permissionless_registration=false:
# Submit governance proposal to register token pair

Issue: Conversion Disabled

Symptom: error: token pair is not enabled Cause: Token pair exists but enabled=false Solution: Submit governance proposal to enable token pair

Issue: Precompile Not Accessible

Symptom: EVM calls to precompile address fail or return no code Cause: Address not in native_precompiles or dynamic_precompiles Solution:
  • For native tokens: Add to native_precompiles in genesis
  • For IBC tokens: Ensure token pair exists and is enabled

Issue: IBC Token No ERC-20 Address

Symptom: IBC token arrives but no ERC-20 wrapper created Cause: enable_erc20=false or module not properly initialized Solution:
  • Check enable_erc20 parameter is true
  • Check module is in app.go and BeginBlockers/EndBlockers


Source Code References

I