Skip to main content

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: In Remix, compile under Solidity Compiler, then Deploy & Run with Environment set to Injected Provider — MetaMask.

Setup

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';
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);

Reading Token Metadata

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' }),
]);
const [name, symbol, decimals, totalSupply] = await Promise.all([
  readContract.name(),
  readContract.symbol(),
  readContract.decimals(),
  readContract.totalSupply(),
]);

Reading Balances and Allowances

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'],
});
const balance = await readContract.balanceOf('0xOwnerAddress');
const allowance = await readContract.allowance('0xOwnerAddress', '0xSpenderAddress');

Transferring Tokens

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 });
const tx = await writeContract.transfer('0xRecipient', ethers.parseUnits('10', 18));
const receipt = await tx.wait();

Approving a Spender

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],
});
// 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);

Watching Transfer Events

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();
readContract.on('Transfer', (from, to, value) => {
  console.log(`${from}${to}: ${value}`);
});

// Stop watching
readContract.off('Transfer');

Fetching Historical Transfers

const logs = await client.getContractEvents({
  address: TOKEN,
  abi: ERC20_ABI,
  eventName: 'Transfer',
  fromBlock: 0n,
  toBlock: 'latest',
});
const filter = readContract.filters.Transfer();
const logs = await readContract.queryFilter(filter, 0, 'latest');

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 for how to look up the pointer address.