What is ERC-20 on EmpoorioChain?

If you're coming from the EVM ecosystem, you know ERC-20 well — it's usually the first contract you learn to deploy. EmpoorioChain doesn't have an ERC-20 equivalent in the sense of "a contract standard you deploy." Instead, fungible tokens are a native runtime feature. Here's how it actually works.

ERC-20 standardizes fungible tokens on Ethereum: every unit of a given token is interchangeable, and each token type is its own deployed contract.

pallet-emp-assets

pallet-emp-assets is the native runtime module EmpoorioChain uses for fungible tokens. Instead of deploying a new contract per token, you register a new asset ID in this pallet's storage. This is different from Ethereum, where every token is its own independently deployed and audited contract.

Why one pallet instead of one contract per token? Because EmpoorioChain's account model separates pallet logic (compiled into the runtime) from application data (stored in each pallet's own storage maps) — see the Accounts and Smart Contracts pages for the full picture.

Tokens Don't Have Contract Addresses, Only Asset IDs

On Ethereum, each token is uniquely identified by its contract address. On EmpoorioChain, each token is identified by an asset ID registered in pallet-emp-assets — there's no separate contract address to deploy, because the logic already lives in the pallet.

No Separate Approval Flow for Basic Transfers

On Ethereum, transferring ERC-20 tokens on someone's behalf often requires approve + transferFrom. On EmpoorioChain, each account already holds its balance directly under its own AccountId32 in the pallet's storage — a single extrinsic can move tokens without a prior approval step for a direct transfer. (Delegated spending, where it's needed, is handled by the pallet's own approval-style calls, not a bespoke pattern per token.)

What Would an ERC-20-equivalent Look Like in Solidity?

For comparison, here's roughly what the same fungible-token behavior would look like written as a single reusable Solidity contract (this is illustrative, not literal EmpoorioChain code — the real logic lives in pallet-emp-assets, written in Rust):

solidity
// SPDX-License-Identifier: MIT license
pragma solidity =0.8.28;

struct Mint {
    uint8 decimals;
    uint256 supply;
    address mintAuthority;
    address freezeAuthority;
    address mintAddress;
}

struct TokenAccount {
    address mintAddress;
    address owner;
    uint256 balance;
    bool isFrozen;
}

contract Spl20 {
    mapping(address => Mint) public mints;
    mapping(address => TokenAccount) public tokenAccounts;
    mapping(address => bool) public mintAddresses;
    mapping(address => bool) public tokenAddresses;

    function initializeMint(uint8 decimals, address mintAuthority, address freezeAuthority, address mintAddress)
        public
        returns (Mint memory)
    {
        require(mintAddresses[mintAddress] == false, "Mint already exists");
        mints[mintAddress] = Mint(decimals, 0, mintAuthority, freezeAuthority, mintAddress);
        mintAddresses[mintAddress] = true;
        return Mint(decimals, 0, mintAuthority, freezeAuthority, mintAddress);
    }

    function mintTokens(address toMintTokens, address mintAddress, uint256 amount) public {
        require(mints[mintAddress].mintAuthority == msg.sender, "Only the mint authority can mint tokens");
        require(mints[mintAddress].mintAddress != address(0), "Token does not exist");
        require(mints[mintAddress].supply + amount <= type(uint256).max, "Supply overflow");

        mints[mintAddress].supply += amount;

        address tokenAddress = address(uint160(uint256(keccak256(abi.encodePacked(toMintTokens, mintAddress)))));

        if (tokenAccounts[tokenAddress].mintAddress == address(0)) {
            tokenAccounts[tokenAddress] = TokenAccount(mintAddress, toMintTokens, 0, false);
            tokenAddresses[tokenAddress] = true;
        }
        tokenAccounts[tokenAddress].balance += amount;
        tokenAccounts[tokenAddress].owner = toMintTokens;
    }

    function transfer(address to, address mintAddress, uint256 amount) public {
        address toTokenAddress = address(uint160(uint256(keccak256(abi.encodePacked(to, mintAddress)))));
        address fromTokenAddress = address(uint160(uint256(keccak256(abi.encodePacked(msg.sender, mintAddress)))));

        require(tokenAccounts[fromTokenAddress].balance >= amount, "Insufficient balance");
        require(tokenAccounts[toTokenAddress].balance + amount <= type(uint256).max, "Supply overflow");
        require(tokenAccounts[fromTokenAddress].owner == msg.sender, "fromToken owner is not msg.sender");
        require(tokenAccounts[fromTokenAddress].isFrozen == false, "fromToken is frozen");
        require(tokenAccounts[toTokenAddress].isFrozen == false, "toToken is frozen");

        if (tokenAccounts[toTokenAddress].mintAddress == address(0)) {
            tokenAccounts[toTokenAddress] = TokenAccount(mintAddress, to, 0, false);
            tokenAddresses[toTokenAddress] = true;
        }

        tokenAccounts[fromTokenAddress].balance -= amount;
        tokenAccounts[toTokenAddress].balance += amount;
    }

    function getMint(address token) public view returns (Mint memory) {
        return mints[token];
    }

    function getTokenAccount(address owner, address token) public view returns (TokenAccount memory) {
        return tokenAccounts[address(uint160(uint256(keccak256(abi.encodePacked(owner, token)))))];
    }
}

As shown, a single reusable contract can express the same core operations (mint, transfer, balance lookup) that pallet-emp-assets exposes as extrinsics — the difference is that on EmpoorioChain this logic ships once with the runtime, rather than once per deployed token contract.

Where's the Token Metadata?

pallet-emp-assets stores basic metadata (name, symbol, decimals) directly in its own metadata storage item per asset ID, set via a dedicated extrinsic at registration time — there's no separate metadata contract to deploy or maintain.

Tradeoffs of Each Approach

ERC-20

  • Highly customizable per token (arbitrary logic in the contract)
  • Requires a new audited contract deployment per token
  • Approval flow needed for delegated transfers
  • Indexing requires tracking every contract address separately

pallet-emp-assets

  • One audited pallet covers every asset
  • New tokens are created by registering an asset ID, not deploying code
  • Indexing is simpler — one pallet, one set of storage maps
  • Custom per-token logic requires a pallet extension or governance change, not a quick redeploy

High-Level Comparison

Below is a comparison of the basic steps for issuing a fungible token on Ethereum vs. EmpoorioChain.

StepEthereum (ERC-20)EmpoorioChain (pallet-emp-assets)
1. Prepare Token CodeWrite/import a Solidity contract (e.g. via OpenZeppelin).No custom code needed — the pallet is already part of the runtime.
2. Compile & DeployCompile and deploy with Hardhat/Foundry.Register a new asset ID via an extrinsic — no separate deployment step.
3. Initial MintCall the contract's constructor or mint().Call the pallet's mint extrinsic for the registered asset ID.
4. Recipient SetupA plain Ethereum address; wallets often need the contract address added manually.Any AccountId32 can hold the asset directly; no separate account derivation.
5. Check ResultsView on Etherscan by contract address.Query via EmpooScan or api.query.assets.account by asset ID.
6. Code UpdatesProxy pattern or redeploy.Pallet logic is fixed at runtime-upgrade granularity, governed on-chain.
7. Audit RequirementsEach contract typically needs its own audit.The shared pallet is audited once; per-asset parameters carry much less custom-code risk.

Want to register your own asset on EmpoorioChain? Check the pallet reference and emp-cli docs linked below.

How to interact with pallet-emp-assets

The examples below use @polkadot/api, the standard JavaScript/TypeScript client library for Substrate chains, pointed at EmpoorioChain's testnet RPC. Exact extrinsic and storage-item names should be verified against the current EmpoorioChain SDK/pallet reference, since they can evolve.

Reading name/symbol

bash
function name() public view returns (string) // Returns the name of the token

Name and symbol are read from the asset's metadata storage item, via api.query.assets.metadata(assetId).

jsx
// EmpoorioChain: query on-chain asset metadata via @polkadot/api
// against the pallet_emp_assets metadata storage for the given assetId.
// See the EmpoorioChain SDK docs for the exact metadata query shape.
import { ApiPromise, WsProvider } from "@polkadot/api";

async function name(assetId) {
  const api = await ApiPromise.create({ provider: new WsProvider("wss://rpc.testnet.empooriochain.org") });
  const metadata = await api.query.assets.metadata(assetId);
  return metadata.toHuman();
}

name().then(name => {
    console.log("token Name:", name);
  })
  .catch(error => {
    console.error("Error:", error);
  });

How the ERC-20 interface signature compares

bash
function symbol() public view returns (string) // Returns the symbol of the token

The Ethereum equivalent is a plain view function on the ERC-20 contract.

jsx
// EmpoorioChain: query on-chain asset metadata via @polkadot/api
// against the pallet_emp_assets metadata storage for the given assetId.
// See the EmpoorioChain SDK docs for the exact metadata query shape.
import { ApiPromise, WsProvider } from "@polkadot/api";

async function symbol(assetId) {
  const api = await ApiPromise.create({ provider: new WsProvider("wss://rpc.testnet.empooriochain.org") });
  const metadata = await api.query.assets.metadata(assetId);
  return metadata.toHuman();
}

symbol().then(symbol => {
  console.log("Symbol:", symbol);
}).catch(error => {
  console.error("Error:", error);
});

Reading decimals

bash
function decimals() public view returns (uint8) // Returns the number of decimals the token use

Decimals are part of the same asset metadata / asset-info storage item.

jsx
import { ApiPromise, WsProvider } from "@polkadot/api";

const api = await ApiPromise.create({
  provider: new WsProvider("wss://rpc.testnet.empooriochain.org"),
});

async function decimals(assetId) {
  const asset = await api.query.assets.asset(assetId);
  return asset.unwrap().decimals?.toNumber?.() ?? asset.toHuman();
}

decimals(1).then(console.log).catch(console.error);

Reading a balance

bash
function balanceOf(address _owner) public view returns (uint256 balance) // Returns the number of tokens in owner's account
jsx
import { ApiPromise, WsProvider } from "@polkadot/api";

const api = await ApiPromise.create({
  provider: new WsProvider("wss://rpc.testnet.empooriochain.org"),
});

async function balanceOf(assetId, accountId) {
  const account = await api.query.assets.account(assetId, accountId);
  return account.isSome ? account.unwrap().balance.toString() : "0";
}

balanceOf(1, "5Grwv...").then(console.log).catch(console.error);

An account's balance for a given asset is read via api.query.assets.account(assetId, accountId).

Reading total supply

bash
function totalSupply() public view returns (uint256) // Returns the total issuance of tokens

Total supply is part of the asset's on-chain info, via api.query.assets.asset(assetId).

jsx
import { ApiPromise, WsProvider } from "@polkadot/api";

const api = await ApiPromise.create({
  provider: new WsProvider("wss://rpc.testnet.empooriochain.org"),
});

async function totalSupply(assetId) {
  const asset = await api.query.assets.asset(assetId);
  return asset.unwrap().supply.toString();
}

totalSupply(1).then(console.log).catch(console.error);

Transferring

solidity
function transfer(address _to, uint256 _value) public returns (bool success) // Moves a value amount of tokens from the caller’s account to _to 

On EmpoorioChain, each account holds its balance directly under its AccountId32 — there's no derived token-account address to compute first. A single assets.transfer extrinsic, signed with @polkadot/api, moves the asset.

jsx
import { Keypair, Transaction, Connection, PublicKey } from "@polkadot/api";
import { createTransferCheckedInstruction } from "@polkadot/api";

const connection = new Connection("https://rpc.testnet.empooriochain.org", "confirmed");

// Must contain your private key as a Uint8Array
const ownerSecretkey = [];
const ownerPrivatekeypair = Keypair.fromSecretKey(new Uint8Array(ownerSecretkey));

const receiverAddress = new PublicKey("Receiver's Wallet Address");
const mintAddress = new PublicKey("Token Address");
const ownerTokenAccount = new PublicKey("Your Associated Token Account Address");
const receiverTokenAccount = new PublicKey("Receiver's Associated Token Account Address");

// For a token with 9 decimals, transferring 1 => 1 * 10^9
const amount = 1;

async function transfer(_to, _value) {
  try {
    // Create a transaction with the transfer instruction
    const tx = new Transaction().add(
      createTransferCheckedInstruction(
        ownerTokenAccount,
        mintAddress,
        receiverTokenAccount,
        ownerPrivatekeypair.publicKey,
        _value * Math.pow(10, 9), // Decimal correction
        9 // decimals
      )
    );

    // Send the transaction (simplified, no explicit blockhash or feePayer set)
    await connection.sendTransaction(tx, [ownerPrivatekeypair]);
    return true;
  } catch (error) {
    console.error("Error in transfer:", error);
    return false;
  }
}

transfer(receiverAddress, amount)
  .then(result => {
    console.log(result);
  })
  .catch(error => {
    console.error("Error:", error);
  });

Fee-sponsored (relayer-paid) transfers are supported natively via EmpoorioChain's paymaster pallet, rather than requiring a bespoke meta-transaction pattern per token.

jsx
import { Keypair, Transaction, Connection, PublicKey } from "@polkadot/api";
import { createTransferCheckedInstruction, getOrCreateAssociatedTokenAccount } from "@polkadot/api";

const connection = new Connection("https://rpc.testnet.empooriochain.org", "confirmed");

// Must contain your private key as a Uint8Array
const ownerSecretkey = [];
const ownerPrivatekeypair = Keypair.fromSecretKey(new Uint8Array(ownerSecretkey));

const receiverAddress = new PublicKey("Receiver's Wallet Address");
const mintAddress = new PublicKey("Token Address");
const amount = 1; // Amount to transfer

async function transfer(_to, _value) {
  try {
    // Get or create the sender's ATA
    const ownerTokenAccount = await getOrCreateAssociatedTokenAccount(
      connection,
      ownerPrivatekeypair, // Fee payer
      mintAddress,
      ownerPrivatekeypair.publicKey
    );

    // Get or create the receiver's ATA
    const receiverTokenAccount = await getOrCreateAssociatedTokenAccount(
      connection,
      ownerPrivatekeypair, // Fee payer
      mintAddress,
      _to
    );

    // Build the transaction
    const tx = new Transaction().add(
      createTransferCheckedInstruction(
        ownerTokenAccount.address,
        mintAddress,
        receiverTokenAccount.address,
        ownerPrivatekeypair.publicKey,
        _value * Math.pow(10, 9), // Decimal correction (9 decimals)
        9 // decimals
      )
    );

    // Send the transaction (simple version)
    await connection.sendTransaction(tx, [ownerPrivatekeypair]);
    return true;
  } catch (error) {
    console.error("Error in transfer:", error);
    return false;
  }
}

// Execute the transfer function
transfer(receiverAddress, amount)
  .then(result => {
    console.log("Transaction result:", result);
  })
  .catch(error => {
    console.error("Error:", error);
  });

Want to explore more? See the EmpoorioChain pallet reference for the full pallet-emp-assets extrinsic and storage-item list.

Node storing all history and participating in consensus

  • Ethereum: Archive Node
  • EmpoorioChain: Full node with archive pruning enabled

Node producing/finalizing blocks

  • Ethereum: Full Node (execution + consensus client)
  • EmpoorioChain: Validator node (Aura block production + GRANDPA finality, single Rust binary)

Node serving RPC without validating

  • Ethereum: Light Node
  • EmpoorioChain: RPC node (non-validator, full state)

EVM TO EMPOORIOCHAIN

Start building on EmpoorioChain

Intro to EmpoorioChain Development

Read

EmpoorioChain Pallet Reference

Read

emp-cli Quickstart

Read

More EmpoorioChain Developer Tools

Read
What is ERC-20 on EmpoorioChain? | empoorio