ERC20

Standard ERC20 token functionality for native Ontomir tokens

Overview

The ERC20 precompile provides a standard ERC20-compliant interface that allows EVM tooling and libraries to interact with native Ontomir SDK tokens stored in the bank module as if they were standard ERC20 tokens. Each registered token pair gets its own unique precompile contract address that directly interfaces with the bank module - no token conversion or wrapping occurs. All balances, transfers, and operations work directly on the native bank module token balances.

Address: Dynamic (assigned per token pair registration)

Gas Costs

Gas costs are approximated and may vary based on token complexity and chain settings.

Method
Gas Cost

name()

~3,000 gas

symbol()

~3,000 gas

decimals()

~2,000 gas

totalSupply()

~2,500 gas

balanceOf(address)

~2,900 gas

allowance(address,address)

~3,000 gas

transfer(address,uint256)

~35,000 gas

transferFrom(address,address,uint256)

~40,000 gas

approve(address,uint256)

~30,000 gas

increaseAllowance(address,uint256)

~30,500 gas

decreaseAllowance(address,uint256)

~30,500 gas

Token Pair Registration

The ERC20 precompile works through a token pair registration system:

  1. Native Ontomir Token: Each Ontomir SDK denomination (e.g., test, atest) exists as a native token in the bank module

  2. ERC20 Interface: A corresponding ERC20 precompile contract provides an interface at a unique address

  3. Direct Bank Module Access: The ERC20 interface operates directly on bank module balances - there is no separate ERC20 token state

  4. Dynamic Addresses: Each token pair gets its own precompile address when registered

The precompile address is deterministically generated based on the token denomination. Query the x/erc20 module to find the precompile address for a specific token.

Methods

totalSupply

Returns the total amount of tokens in existence.

```solidity Solidity expandable lines // SPDX-License-Identifier: MIT pragma solidity ^0.8.0;

contract ERC20Example { address public immutable tokenContract; constructor(address _tokenContract) { tokenContract = _tokenContract; } function getTotalSupply() external view returns (uint256 totalSupply) { (bool success, bytes memory result) = tokenContract.staticcall( abi.encodeWithSignature("totalSupply()") ); require(success, "Total supply query failed"); totalSupply = abi.decode(result, (uint256)); return totalSupply; }

}


```javascript Ethers.js expandable lines
import { ethers } from "ethers";

// The address of the ERC20 precompile is dynamic, assigned when a token pair is registered.
const erc20PrecompileAddress = "<ERC20_PRECOMPILE_ADDRESS>";
const provider = new ethers.JsonRpcProvider("<RPC_URL>");

// A generic ERC20 ABI is sufficient for read-only calls
const erc20Abi = ["function totalSupply() view returns (uint256)"];
const contract = new ethers.Contract(erc20PrecompileAddress, erc20Abi, provider);

async function getTotalSupply() {
  try {
    const totalSupply = await contract.totalSupply();
    console.log("Total Supply:", totalSupply.toString());
  } catch (error) {
    console.error("Error fetching total supply:", error);
  }
}

getTotalSupply();
# Note: Replace <RPC_URL> and <ERC20_PRECOMPILE_ADDRESS> with your actual data.
# Data is the function selector for totalSupply()
curl -X POST --data '{
    "jsonrpc": "2.0",
    "method": "eth_call",
    "params": [
        {
            "to": "<ERC20_PRECOMPILE_ADDRESS>",
            "data": "0x18160ddd"
        },
        "latest"
    ],
    "id": 1
}' -H "Content-Type: application/json" <RPC_URL>

balanceOf

Returns the token balance of a specified account.

```solidity Solidity expandable lines // SPDX-License-Identifier: MIT pragma solidity ^0.8.0;

contract ERC20Example { address public immutable tokenContract; constructor(address _tokenContract) { tokenContract = _tokenContract; } function getBalance(address account) external view returns (uint256 balance) { (bool success, bytes memory result) = tokenContract.staticcall( abi.encodeWithSignature("balanceOf(address)", account) ); require(success, "Balance query failed"); balance = abi.decode(result, (uint256)); return balance; } // Get caller's balance function getMyBalance() external view returns (uint256) { return this.getBalance(msg.sender); }

}


```javascript Ethers.js expandable lines
import { ethers } from "ethers";

const erc20PrecompileAddress = "<ERC20_PRECOMPILE_ADDRESS>";
const accountAddress = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"; // Placeholder
const provider = new ethers.JsonRpcProvider("<RPC_URL>");

const erc20Abi = ["function balanceOf(address account) view returns (uint256)"];
const contract = new ethers.Contract(erc20PrecompileAddress, erc20Abi, provider);

async function getBalance() {
  try {
    const balance = await contract.balanceOf(accountAddress);
    console.log(`Balance of ${accountAddress}:`, balance.toString());
  } catch (error) {
    console.error("Error fetching balance:", error);
  }
}

getBalance();
# Note: Replace <RPC_URL>, <ERC20_PRECOMPILE_ADDRESS>, and the placeholder address with your actual data.
# Data is ABI-encoded: function selector + padded address
curl -X POST --data '{
    "jsonrpc": "2.0",
    "method": "eth_call",
    "params": [
        {
            "to": "<ERC20_PRECOMPILE_ADDRESS>",
            "data": "0x70a08231000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa96045"
        },
        "latest"
    ],
    "id": 1
}' -H "Content-Type: application/json" <RPC_URL>

transfer

Moves tokens from the caller's account to a recipient.

```solidity Solidity expandable lines // SPDX-License-Identifier: MIT pragma solidity ^0.8.0;

contract ERC20Example { address public immutable tokenContract; constructor(address _tokenContract) { tokenContract = _tokenContract; } event TokenTransfer(address indexed from, address indexed to, uint256 amount); function transferTokens(address to, uint256 amount) external returns (bool success) { require(to != address(0), "Cannot transfer to zero address"); require(amount > 0, "Amount must be positive"); (bool callSuccess, bytes memory result) = tokenContract.call( abi.encodeWithSignature("transfer(address,uint256)", to, amount) ); require(callSuccess, "Transfer call failed"); success = abi.decode(result, (bool)); require(success, "Transfer returned false"); emit TokenTransfer(msg.sender, to, amount); return success; }

}


```bash cURL expandable lines
# Note: Transaction methods require signatures - use ethers.js or other Web3 library
echo "Token transfer requires a signed transaction"

transferFrom

Moves tokens from one account to another using an allowance.

```solidity Solidity expandable lines // SPDX-License-Identifier: MIT pragma solidity ^0.8.0;

contract ERC20Example { address public immutable tokenContract; constructor(address _tokenContract) { tokenContract = _tokenContract; } event TokenTransferFrom(address indexed from, address indexed to, uint256 amount, address indexed spender); function transferFromTokens(address from, address to, uint256 amount) external returns (bool success) { require(from != address(0), "Cannot transfer from zero address"); require(to != address(0), "Cannot transfer to zero address"); require(amount > 0, "Amount must be positive"); (bool callSuccess, bytes memory result) = tokenContract.call( abi.encodeWithSignature("transferFrom(address,address,uint256)", from, to, amount) ); require(callSuccess, "TransferFrom call failed"); success = abi.decode(result, (bool)); require(success, "TransferFrom returned false"); emit TokenTransferFrom(from, to, amount, msg.sender); return success; }

}


```bash cURL expandable lines
# Note: Transaction methods require signatures - use ethers.js or other Web3 library
echo "Token transferFrom requires a signed transaction"

approve

Sets the allowance of a spender over the caller's tokens.

```solidity Solidity expandable lines // SPDX-License-Identifier: MIT pragma solidity ^0.8.0;

contract ERC20Example { address public immutable tokenContract; constructor(address _tokenContract) { tokenContract = _tokenContract; } event TokenApproval(address indexed owner, address indexed spender, uint256 amount); function approveSpender(address spender, uint256 amount) external returns (bool success) { require(spender != address(0), "Cannot approve zero address"); (bool callSuccess, bytes memory result) = tokenContract.call( abi.encodeWithSignature("approve(address,uint256)", spender, amount) ); require(callSuccess, "Approve call failed"); success = abi.decode(result, (bool)); require(success, "Approve returned false"); emit TokenApproval(msg.sender, spender, amount); return success; } // Helper function to approve maximum amount function approveMax(address spender) external returns (bool) { return this.approveSpender(spender, type(uint256).max); }

}


```bash cURL expandable lines
# Note: Transaction methods require signatures - use ethers.js or other Web3 library
echo "Token approval requires a signed transaction"

allowance

Returns the remaining number of tokens that a spender is allowed to spend on behalf of an owner.

```solidity Solidity expandable lines // SPDX-License-Identifier: MIT pragma solidity ^0.8.0;

contract ERC20Example { address public immutable tokenContract; constructor(address _tokenContract) { tokenContract = _tokenContract; } function checkAllowance(address owner, address spender) external view returns (uint256 allowance) { (bool success, bytes memory result) = tokenContract.staticcall( abi.encodeWithSignature("allowance(address,address)", owner, spender) ); require(success, "Allowance query failed"); allowance = abi.decode(result, (uint256)); return allowance; }

}


```javascript Ethers.js expandable lines
import { ethers } from "ethers";

const erc20PrecompileAddress = "<ERC20_PRECOMPILE_ADDRESS>";
const ownerAddress = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"; // Placeholder
const spenderAddress = "0x27f320b7280911c7987a421a8138997a48d4b315"; // Placeholder
const provider = new ethers.JsonRpcProvider("<RPC_URL>");

const erc20Abi = ["function allowance(address owner, address spender) view returns (uint256)"];
const contract = new ethers.Contract(erc20PrecompileAddress, erc20Abi, provider);

async function getAllowance() {
  try {
    const allowance = await contract.allowance(ownerAddress, spenderAddress);
    console.log("Allowance:", allowance.toString());
  } catch (error) {
    console.error("Error fetching allowance:", error);
  }
}

getAllowance();
# Note: Replace <RPC_URL>, <ERC20_PRECOMPILE_ADDRESS>, and the placeholder addresses with your actual data.
# Data is ABI-encoded: function selector + padded owner address + padded spender address
curl -X POST --data '{
    "jsonrpc": "2.0",
    "method": "eth_call",
    "params": [
        {
            "to": "<ERC20_PRECOMPILE_ADDRESS>",
            "data": "0xdd62ed3e000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa9604500000000000000000000000027f320b7280911c7987a421a8138997a48d4b315"
        },
        "latest"
    ],
    "id": 1
}' -H "Content-Type: application/json" <RPC_URL>

name

Returns the name of the token.

```solidity Solidity expandable lines // SPDX-License-Identifier: MIT pragma solidity ^0.8.0;

contract ERC20Example { address public immutable tokenContract; constructor(address _tokenContract) { tokenContract = _tokenContract; } function getTokenName() external view returns (string memory name) { (bool success, bytes memory result) = tokenContract.staticcall( abi.encodeWithSignature("name()") ); require(success, "Name query failed"); name = abi.decode(result, (string)); return name; }

}


```javascript Ethers.js expandable lines
import { ethers } from "ethers";

const erc20PrecompileAddress = "<ERC20_PRECOMPILE_ADDRESS>";
const provider = new ethers.JsonRpcProvider("<RPC_URL>");

const erc20Abi = ["function name() view returns (string)"];
const contract = new ethers.Contract(erc20PrecompileAddress, erc20Abi, provider);

async function getName() {
  try {
    const name = await contract.name();
    console.log("Token Name:", name);
  } catch (error) {
    console.error("Error fetching name:", error);
  }
}

getName();
# Note: Replace <RPC_URL> and <ERC20_PRECOMPILE_ADDRESS> with your actual data.
# Data is the function selector for name()
curl -X POST --data '{
    "jsonrpc": "2.0",
    "method": "eth_call",
    "params": [
        {
            "to": "<ERC20_PRECOMPILE_ADDRESS>",
            "data": "0x06fdde03"
        },
        "latest"
    ],
    "id": 1
}' -H "Content-Type: application/json" <RPC_URL>

symbol

Returns the symbol of the token.

```solidity Solidity expandable lines // SPDX-License-Identifier: MIT pragma solidity ^0.8.0;

contract ERC20Example { address public immutable tokenContract; constructor(address _tokenContract) { tokenContract = _tokenContract; } function getTokenSymbol() external view returns (string memory symbol) { (bool success, bytes memory result) = tokenContract.staticcall( abi.encodeWithSignature("symbol()") ); require(success, "Symbol query failed"); symbol = abi.decode(result, (string)); return symbol; }

}


```javascript Ethers.js expandable lines
import { ethers } from "ethers";

const erc20PrecompileAddress = "<ERC20_PRECOMPILE_ADDRESS>";
const provider = new ethers.JsonRpcProvider("<RPC_URL>");

const erc20Abi = ["function symbol() view returns (string)"];
const contract = new ethers.Contract(erc20PrecompileAddress, erc20Abi, provider);

async function getSymbol() {
  try {
    const symbol = await contract.symbol();
    console.log("Token Symbol:", symbol);
  } catch (error) {
    console.error("Error fetching symbol:", error);
  }
}

getSymbol();
# Note: Replace <RPC_URL> and <ERC20_PRECOMPILE_ADDRESS> with your actual data.
# Data is the function selector for symbol()
curl -X POST --data '{
    "jsonrpc": "2.0",
    "method": "eth_call",
    "params": [
        {
            "to": "<ERC20_PRECOMPILE_ADDRESS>",
            "data": "0x95d89b41"
        },
        "latest"
    ],
    "id": 1
}' -H "Content-Type: application/json" <RPC_URL>

decimals

Returns the number of decimals used for the token.

**Decimal Handling**: The ERC20 precompile may need to handle complex decimal conversions between Ontomir native tokens and ERC20 representation. Some Ontomir tokens use 6 decimals (e.g., `test`) while ERC20 typically uses 18. Always verify the decimal count for accurate amount calculations. ```solidity Solidity expandable lines // SPDX-License-Identifier: MIT pragma solidity ^0.8.0;

contract ERC20Example { address public immutable tokenContract; constructor(address _tokenContract) { tokenContract = _tokenContract; } function getTokenDecimals() external view returns (uint8 decimals) { (bool success, bytes memory result) = tokenContract.staticcall( abi.encodeWithSignature("decimals()") ); require(success, "Decimals query failed"); decimals = abi.decode(result, (uint8)); return decimals; }

}


```javascript Ethers.js expandable lines
import { ethers } from "ethers";

const erc20PrecompileAddress = "<ERC20_PRECOMPILE_ADDRESS>";
const provider = new ethers.JsonRpcProvider("<RPC_URL>");

const erc20Abi = ["function decimals() view returns (uint8)"];
const contract = new ethers.Contract(erc20PrecompileAddress, erc20Abi, provider);

async function getDecimals() {
  try {
    const decimals = await contract.decimals();
    console.log("Token Decimals:", decimals.toString());
  } catch (error) {
    console.error("Error fetching decimals:", error);
  }
}

getDecimals();
# Note: Replace <RPC_URL> and <ERC20_PRECOMPILE_ADDRESS> with your actual data.
# Data is the function selector for decimals()
curl -X POST --data '{
    "jsonrpc": "2.0",
    "method": "eth_call",
    "params": [
        {
            "to": "<ERC20_PRECOMPILE_ADDRESS>",
            "data": "0x313ce567"
        },
        "latest"
    ],
    "id": 1
}' -H "Content-Type: application/json" <RPC_URL>

Full Solidity Interface & ABI

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.6.0) (token/ERC20/IERC20.sol)

pragma solidity ^0.8.0;

/**
 * @dev Interface of the ERC20 standard as defined in the EIP.
 */
interface IERC20 {
    /**
     * @dev Emitted when `value` tokens are moved from one account (`from`) to
     * another (`to`).
     *
     * Note that `value` may be zero.
     */
    event Transfer(address indexed from, address indexed to, uint256 value);

    /**
     * @dev Emitted when the allowance of a `spender` for an `owner` is set by
     * a call to {approve}. `value` is the new allowance.
     */
    event Approval(address indexed owner, address indexed spender, uint256 value);

    /**
     * @dev Returns the amount of tokens in existence.
     */
    function totalSupply() external view returns (uint256);

    /**
     * @dev Returns the amount of tokens owned by `account`.
     */
    function balanceOf(address account) external view returns (uint256);

    /**
     * @dev Moves `amount` tokens from the caller's account to `to`.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * Emits a {Transfer} event.
     */
    function transfer(address to, uint256 amount) external returns (bool);

    /**
     * @dev Returns the remaining number of tokens that `spender` will be
     * allowed to spend on behalf of `owner` through {transferFrom}. This is
     * zero by default.
     *
     * This value changes when {approve} or {transferFrom} are called.
     */
    function allowance(address owner, address spender) external view returns (uint256);

    /**
     * @dev Sets `amount` as the allowance of `spender` over the caller's tokens.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * IMPORTANT: Beware that changing an allowance with this method brings the risk
     * that someone may use both the old and the new allowance by unfortunate
     * transaction ordering. One possible solution to mitigate this race
     * condition is to first reduce the spender's allowance to 0 and set the
     * desired value afterwards:
     * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
     *
     * Emits an {Approval} event.
     */
    function approve(address spender, uint256 amount) external returns (bool);

    /**
     * @dev Moves `amount` tokens from `from` to `to` using the
     * allowance mechanism. `amount` is then deducted from the caller's
     * allowance.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * Emits a {Transfer} event.
     */
    function transferFrom(address from, address to, uint256 amount) external returns (bool);
}
{
  "_format": "hh-sol-artifact-1",
  "contractName": "IERC20MetadataAllowance",
  "sourceName": "solidity/precompiles/erc20/IERC20MetadataAllowance.sol",
  "abi": [
    {
      "anonymous": false,
      "inputs": [
        {
          "indexed": true,
          "internalType": "address",
          "name": "owner",
          "type": "address"
        },
        {
          "indexed": true,
          "internalType": "address",
          "name": "spender",
          "type": "address"
        },
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "value",
          "type": "uint256"
        }
      ],
      "name": "Approval",
      "type": "event"
    },
    {
      "anonymous": false,
      "inputs": [
        {
          "indexed": true,
          "internalType": "address",
          "name": "from",
          "type": "address"
        },
        {
          "indexed": true,
          "internalType": "address",
          "name": "to",
          "type": "address"
        },
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "value",
          "type": "uint256"
        }
      ],
      "name": "Transfer",
      "type": "event"
    },
    {
      "inputs": [
        {
          "internalType": "address",
          "name": "owner",
          "type": "address"
        },
        {
          "internalType": "address",
          "name": "spender",
          "type": "address"
        }
      ],
      "name": "allowance",
      "outputs": [
        {
          "internalType": "uint256",
          "name": "",
          "type": "uint256"
        }
      ],
      "stateMutability": "view",
      "type": "function"
    },
    {
      "inputs": [
        {
          "internalType": "address",
          "name": "spender",
          "type": "address"
        },
        {
          "internalType": "uint256",
          "name": "amount",
          "type": "uint256"
        }
      ],
      "name": "approve",
      "outputs": [
        {
          "internalType": "bool",
          "name": "",
          "type": "bool"
        }
      ],
      "stateMutability": "nonpayable",
      "type": "function"
    },
    {
      "inputs": [
        {
          "internalType": "address",
          "name": "account",
          "type": "address"
        }
      ],
      "name": "balanceOf",
      "outputs": [
        {
          "internalType": "uint256",
          "name": "",
          "type": "uint256"
        }
      ],
      "stateMutability": "view",
      "type": "function"
    },
    {
      "inputs": [],
      "name": "decimals",
      "outputs": [
        {
          "internalType": "uint8",
          "name": "",
          "type": "uint8"
        }
      ],
      "stateMutability": "view",
      "type": "function"
    },
    {
      "inputs": [
        {
          "internalType": "address",
          "name": "spender",
          "type": "address"
        },
        {
          "internalType": "uint256",
          "name": "subtractedValue",
          "type": "uint256"
        }
      ],
      "name": "decreaseAllowance",
      "outputs": [
        {
          "internalType": "bool",
          "name": "approved",
          "type": "bool"
        }
      ],
      "stateMutability": "nonpayable",
      "type": "function"
    },
    {
      "inputs": [
        {
          "internalType": "address",
          "name": "spender",
          "type": "address"
        },
        {
          "internalType": "uint256",
          "name": "addedValue",
          "type": "uint256"
        }
      ],
      "name": "increaseAllowance",
      "outputs": [
        {
          "internalType": "bool",
          "name": "approved",
          "type": "bool"
        }
      ],
      "stateMutability": "nonpayable",
      "type": "function"
    },
    {
      "inputs": [],
      "name": "name",
      "outputs": [
        {
          "internalType": "string",
          "name": "",
          "type": "string"
        }
      ],
      "stateMutability": "view",
      "type": "function"
    },
    {
      "inputs": [],
      "name": "symbol",
      "outputs": [
        {
          "internalType": "string",
          "name": "",
          "type": "string"
        }
      ],
      "stateMutability": "view",
      "type": "function"
    },
    {
      "inputs": [],
      "name": "totalSupply",
      "outputs": [
        {
          "internalType": "uint256",
          "name": "",
          "type": "uint256"
        }
      ],
      "stateMutability": "view",
      "type": "function"
    },
    {
      "inputs": [
        {
          "internalType": "address",
          "name": "to",
          "type": "address"
        },
        {
          "internalType": "uint256",
          "name": "amount",
          "type": "uint256"
        }
      ],
      "name": "transfer",
      "outputs": [
        {
          "internalType": "bool",
          "name": "",
          "type": "bool"
        }
      ],
      "stateMutability": "nonpayable",
      "type": "function"
    },
    {
      "inputs": [
        {
          "internalType": "address",
          "name": "from",
          "type": "address"
        },
        {
          "internalType": "address",
          "name": "to",
          "type": "address"
        },
        {
          "internalType": "uint256",
          "name": "amount",
          "type": "uint256"
        }
      ],
      "name": "transferFrom",
      "outputs": [
        {
          "internalType": "bool",
          "name": "",
          "type": "bool"
        }
      ],
      "stateMutability": "nonpayable",
      "type": "function"
    }
  ]
}