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

# ERC-20 Interaction

> Reading and writing ERC-20 tokens on Sei with viem and ethers

export const AddSeiButton = ({network = 'mainnet', label = 'Add Sei to MetaMask'}) => {
  const SEI_MAINNET_CHAIN_PARAMS = {
    chainId: '0x531',
    chainName: 'Sei Network',
    rpcUrls: ['https://evm-rpc.sei-apis.com'],
    nativeCurrency: {
      name: 'Sei',
      symbol: 'SEI',
      decimals: 18
    },
    blockExplorerUrls: ['https://seiscan.io']
  };
  const SEI_TESTNET_CHAIN_PARAMS = {
    chainId: '0x530',
    chainName: 'Sei Testnet',
    rpcUrls: ['https://evm-rpc-testnet.sei-apis.com'],
    nativeCurrency: {
      name: 'Sei',
      symbol: 'SEI',
      decimals: 18
    },
    blockExplorerUrls: ['https://testnet.seiscan.io']
  };
  const chainParams = network === 'testnet' ? SEI_TESTNET_CHAIN_PARAMS : SEI_MAINNET_CHAIN_PARAMS;
  const [status, setStatus] = useState(null);
  const [isHovered, setIsHovered] = useState(false);
  const [isBusy, setIsBusy] = useState(false);
  const addOrSwitchSeiNetwork = async params => {
    if (typeof window === 'undefined' || !window.ethereum) {
      throw new Error('MetaMask is not installed');
    }
    const ethereum = window.ethereum;
    try {
      await ethereum.request({
        method: 'wallet_switchEthereumChain',
        params: [{
          chainId: params.chainId
        }]
      });
    } catch (switchError) {
      if (switchError && switchError.code === 4902) {
        await ethereum.request({
          method: 'wallet_addEthereumChain',
          params: [params]
        });
      } else {
        throw switchError;
      }
    }
  };
  const onClick = async e => {
    e.preventDefault();
    setIsBusy(true);
    setStatus(null);
    try {
      await addOrSwitchSeiNetwork(chainParams);
      setStatus({
        type: 'success',
        message: `${chainParams.chainName} added or switched.`
      });
    } catch (err) {
      const message = err && err.message ? err.message : 'Failed to add or switch network.';
      setStatus({
        type: 'error',
        message
      });
    } finally {
      setIsBusy(false);
    }
  };
  return <span className="inline-flex flex-col items-start gap-1">
      <button type="button" onClick={onClick} disabled={isBusy} onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} className="inline-flex items-center gap-1 px-3 py-1.5 text-white transition-colors min-w-[160px]" style={{
    backgroundColor: isHovered ? 'var(--sei-maroon-200)' : 'var(--sei-maroon-100)',
    color: '#ffffff',
    fontFamily: 'var(--sei-font-mono)',
    textTransform: 'uppercase',
    letterSpacing: '0.04em',
    fontSize: '10px',
    opacity: isBusy ? 0.7 : 1,
    cursor: isBusy ? 'default' : 'pointer'
  }}>
        {isBusy ? 'Adding…' : label}
      </button>
      {status && <span className={status.type === 'error' ? 'text-red-600 dark:text-red-400' : 'text-green-600 dark:text-green-400'} style={{
    fontSize: '11px'
  }}>
          {status.message}
        </span>}
    </span>;
};

export const SandboxEmbed = props => {
  const {src, kind = 'codesandbox', title, description, height, label} = props || ({});
  const KINDS = {
    codesandbox: {
      name: 'CodeSandbox',
      host: 'codesandbox.io',
      defaultHeight: 500
    },
    remix: {
      name: 'Remix IDE',
      host: 'remix.ethereum.org',
      defaultHeight: 620
    },
    stackblitz: {
      name: 'StackBlitz',
      host: 'stackblitz.com',
      defaultHeight: 500
    }
  };
  const meta = KINDS[kind] || KINDS.codesandbox;
  const parsedHeight = Number(height);
  const frameHeight = Number.isFinite(parsedHeight) && parsedHeight > 0 ? parsedHeight : meta.defaultHeight;
  const allowAttr = 'clipboard-read; clipboard-write';
  const [loaded, setLoaded] = useState(false);
  const [btnHover, setBtnHover] = useState(false);
  const HAIRLINE = 'rgba(128, 128, 128, 0.25)';
  const surfaceStyle = {
    backgroundColor: 'rgba(128, 128, 128, 0.08)'
  };
  const monoStyle = {
    fontFamily: 'var(--sei-font-mono)'
  };
  const cardClass = 'not-prose w-full rounded-lg border overflow-hidden my-4';
  const headerClass = 'flex items-center justify-between gap-3 px-4 py-2.5 border-b';
  const buttonStyle = {
    backgroundColor: btnHover ? 'var(--sei-maroon-200)' : 'var(--sei-maroon-100)',
    color: '#ffffff',
    fontFamily: 'var(--sei-font-mono)',
    textTransform: 'uppercase',
    letterSpacing: '0.04em',
    fontSize: '10px',
    cursor: 'pointer'
  };
  const PlayIcon = () => <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
			<path d="M8 5v14l11-7z" />
		</svg>;
  const ExternalIcon = () => <svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
			<path d="M14 5h5v5" />
			<path d="M19 5l-9 9" />
			<path d="M19 14v5a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V8a1 1 0 0 1 1-1h5" />
		</svg>;
  return <div className={cardClass} style={{
    borderColor: HAIRLINE
  }}>
			<div className={headerClass} style={{
    ...surfaceStyle,
    borderBottomColor: HAIRLINE
  }}>
				<div className="flex flex-col min-w-0">
					<span className="text-sm font-medium text-neutral-900 dark:text-white truncate" style={monoStyle}>
						{title || meta.name}
					</span>
					<span className="text-xs text-neutral-500 dark:text-neutral-500">{meta.name}</span>
				</div>
				<div className="flex items-center gap-3 shrink-0">
					{src ? <a href={src} target="_blank" rel="noopener noreferrer" className="inline-flex items-center gap-1 text-xs text-neutral-500 hover:text-neutral-800 dark:text-neutral-400 dark:hover:text-neutral-200 transition-colors">
							Open <ExternalIcon />
						</a> : null}
					{!loaded && src ? <button type="button" onClick={() => setLoaded(true)} onMouseEnter={() => setBtnHover(true)} onMouseLeave={() => setBtnHover(false)} className="inline-flex items-center gap-1.5 px-3 py-1.5 transition-colors" style={buttonStyle}>
							<PlayIcon />
							{label || 'Load editor'}
						</button> : null}
				</div>
			</div>

			{description ? <div className="px-4 pt-3 pb-1 text-sm text-neutral-600 dark:text-neutral-400">{description}</div> : null}

			{!src ? <div className="px-4 py-6 text-sm text-red-600 dark:text-red-400" style={monoStyle}>
					SandboxEmbed: missing required `src`.
				</div> : loaded ? <iframe src={src} title={title || meta.name} className="w-full block border-0" style={{
    height: frameHeight + 'px',
    backgroundColor: 'rgba(128, 128, 128, 0.05)'
  }} allow={allowAttr} loading="lazy" allowFullScreen /> : <button type="button" onClick={() => setLoaded(true)} className="w-full flex flex-col items-center justify-center gap-2 text-neutral-500 dark:text-neutral-400 hover:text-neutral-800 dark:hover:text-neutral-200 transition-colors" style={{
    height: frameHeight + 'px',
    cursor: 'pointer',
    ...surfaceStyle
  }}>
					<PlayIcon />
					<span className="text-sm" style={monoStyle}>Click to load {meta.name}</span>
					<span className="text-xs">Loads {meta.host} in an embedded editor</span>
				</button>}
		</div>;
};

# ERC-20 Interaction

Standard ERC-20 contracts work on Sei without modification. This page covers the common read and write operations using viem and ethers.

## Try it: deploy an ERC-20

Compile and deploy a real ERC-20 to Sei testnet from the browser — the Remix sandbox preloads an OpenZeppelin-based `DemoToken`. Then point the read/write patterns below at your deployed address.

First, add Sei testnet to your wallet:

<AddSeiButton network="testnet" label="Add Sei testnet" />

In Remix, compile under **Solidity Compiler**, then **Deploy & Run** with **Environment** set to **Injected Provider — MetaMask**.

<SandboxEmbed kind="remix" src="https://remix.ethereum.org/?#activate=solidity,fileManager&code=Ly8gU1BEWC1MaWNlbnNlLUlkZW50aWZpZXI6IE1JVApwcmFnbWEgc29saWRpdHkgXjAuOC4yNDsKCmltcG9ydCAiQG9wZW56ZXBwZWxpbi9jb250cmFjdHMvdG9rZW4vRVJDMjAvRVJDMjAuc29sIjsKCmNvbnRyYWN0IERlbW9Ub2tlbiBpcyBFUkMyMCB7CiAgICBjb25zdHJ1Y3RvcigpIEVSQzIwKCJEZW1vIFRva2VuIiwgIkRFTU8iKSB7CiAgICAgICAgX21pbnQobXNnLnNlbmRlciwgMV8wMDBfMDAwICogMTAgKiogZGVjaW1hbHMoKSk7CiAgICB9Cn0" title="DemoToken (ERC-20) · deploy to Sei testnet" description="Remix IDE with an OpenZeppelin ERC-20 preloaded. Compile in-browser and deploy to Sei testnet." />

## Setup

<CodeGroup>
  ```ts viem theme={"dark"}
  import { createPublicClient, createWalletClient, http, parseAbi } from 'viem';
  import { privateKeyToAccount } from 'viem/accounts';
  import { sei } from 'viem/chains';

  const client = createPublicClient({ chain: sei, transport: http() });
  const account = privateKeyToAccount('0xYourPrivateKey');
  const walletClient = createWalletClient({ account, chain: sei, transport: http() });

  const ERC20_ABI = parseAbi([
    'function name() view returns (string)',
    'function symbol() view returns (string)',
    'function decimals() view returns (uint8)',
    'function totalSupply() view returns (uint256)',
    'function balanceOf(address owner) view returns (uint256)',
    'function allowance(address owner, address spender) view returns (uint256)',
    'function transfer(address to, uint256 amount) returns (bool)',
    'function approve(address spender, uint256 amount) returns (bool)',
    'function transferFrom(address from, address to, uint256 amount) returns (bool)',
    'event Transfer(address indexed from, address indexed to, uint256 value)',
    'event Approval(address indexed owner, address indexed spender, uint256 value)',
  ]);

  const TOKEN = '0xTokenAddress';
  ```

  ```ts ethers theme={"dark"}
  import { ethers } from 'ethers';

  const provider = new ethers.JsonRpcProvider('https://evm-rpc.sei-apis.com');
  const wallet = new ethers.Wallet('0xYourPrivateKey', provider);

  const ERC20_ABI = [
    'function name() view returns (string)',
    'function symbol() view returns (string)',
    'function decimals() view returns (uint8)',
    'function totalSupply() view returns (uint256)',
    'function balanceOf(address owner) view returns (uint256)',
    'function allowance(address owner, address spender) view returns (uint256)',
    'function transfer(address to, uint256 amount) returns (bool)',
    'function approve(address spender, uint256 amount) returns (bool)',
    'function transferFrom(address from, address to, uint256 amount) returns (bool)',
    'event Transfer(address indexed from, address indexed to, uint256 value)',
    'event Approval(address indexed owner, address indexed spender, uint256 value)',
  ];

  const TOKEN = '0xTokenAddress';
  const readContract = new ethers.Contract(TOKEN, ERC20_ABI, provider);
  const writeContract = new ethers.Contract(TOKEN, ERC20_ABI, wallet);
  ```
</CodeGroup>

## Reading Token Metadata

<CodeGroup>
  ```ts viem theme={"dark"}
  const [name, symbol, decimals, totalSupply] = await Promise.all([
    client.readContract({ address: TOKEN, abi: ERC20_ABI, functionName: 'name' }),
    client.readContract({ address: TOKEN, abi: ERC20_ABI, functionName: 'symbol' }),
    client.readContract({ address: TOKEN, abi: ERC20_ABI, functionName: 'decimals' }),
    client.readContract({ address: TOKEN, abi: ERC20_ABI, functionName: 'totalSupply' }),
  ]);
  ```

  ```ts ethers theme={"dark"}
  const [name, symbol, decimals, totalSupply] = await Promise.all([
    readContract.name(),
    readContract.symbol(),
    readContract.decimals(),
    readContract.totalSupply(),
  ]);
  ```
</CodeGroup>

## Reading Balances and Allowances

<CodeGroup>
  ```ts viem theme={"dark"}
  const balance = await client.readContract({
    address: TOKEN,
    abi: ERC20_ABI,
    functionName: 'balanceOf',
    args: ['0xOwnerAddress'],
  });

  const allowance = await client.readContract({
    address: TOKEN,
    abi: ERC20_ABI,
    functionName: 'allowance',
    args: ['0xOwnerAddress', '0xSpenderAddress'],
  });
  ```

  ```ts ethers theme={"dark"}
  const balance = await readContract.balanceOf('0xOwnerAddress');
  const allowance = await readContract.allowance('0xOwnerAddress', '0xSpenderAddress');
  ```
</CodeGroup>

## Transferring Tokens

<CodeGroup>
  ```ts viem theme={"dark"}
  import { parseUnits } from 'viem';

  const hash = await walletClient.writeContract({
    address: TOKEN,
    abi: ERC20_ABI,
    functionName: 'transfer',
    args: ['0xRecipient', parseUnits('10', 18)],
  });

  const receipt = await client.waitForTransactionReceipt({ hash });
  ```

  ```ts ethers theme={"dark"}
  const tx = await writeContract.transfer('0xRecipient', ethers.parseUnits('10', 18));
  const receipt = await tx.wait();
  ```
</CodeGroup>

## Approving a Spender

<CodeGroup>
  ```ts viem theme={"dark"}
  import { parseUnits, maxUint256 } from 'viem';

  // Approve a specific amount
  const hash = await walletClient.writeContract({
    address: TOKEN,
    abi: ERC20_ABI,
    functionName: 'approve',
    args: ['0xSpenderAddress', parseUnits('100', 18)],
  });

  // Or approve max (unlimited)
  const hashMax = await walletClient.writeContract({
    address: TOKEN,
    abi: ERC20_ABI,
    functionName: 'approve',
    args: ['0xSpenderAddress', maxUint256],
  });
  ```

  ```ts ethers theme={"dark"}
  // Approve a specific amount
  const tx = await writeContract.approve('0xSpenderAddress', ethers.parseUnits('100', 18));

  // Or approve max (unlimited)
  const txMax = await writeContract.approve('0xSpenderAddress', ethers.MaxUint256);
  ```
</CodeGroup>

## Watching Transfer Events

<CodeGroup>
  ```ts viem theme={"dark"}
  const unwatch = client.watchContractEvent({
    address: TOKEN,
    abi: ERC20_ABI,
    eventName: 'Transfer',
    onLogs: (logs) => {
      logs.forEach(({ args }) => {
        console.log(`${args.from} → ${args.to}: ${args.value}`);
      });
    },
  });

  // Stop watching
  unwatch();
  ```

  ```ts ethers theme={"dark"}
  readContract.on('Transfer', (from, to, value) => {
    console.log(`${from} → ${to}: ${value}`);
  });

  // Stop watching
  readContract.off('Transfer');
  ```
</CodeGroup>

## Fetching Historical Transfers

<CodeGroup>
  ```ts viem theme={"dark"}
  const logs = await client.getContractEvents({
    address: TOKEN,
    abi: ERC20_ABI,
    eventName: 'Transfer',
    fromBlock: 0n,
    toBlock: 'latest',
  });
  ```

  ```ts ethers theme={"dark"}
  const filter = readContract.filters.Transfer();
  const logs = await readContract.queryFilter(filter, 0, 'latest');
  ```
</CodeGroup>

## CosmWasm Token Compatibility

CW20 tokens on Sei have ERC-20 pointer contracts that expose the standard ERC-20 interface. You can use all of the patterns above against a CW20 pointer address. See [Pointer Contracts](/evm/evm-parity/examples/pointer-contracts) for how to look up the pointer address.
