WERC20
Single token representation: An ERC20 interface for any token
Overview
The WERC20 precompile provides a standard ERC20 interface to native Ontomir tokens through Ontomir EVM's Single Token Representation architecture. Unlike traditional wrapped tokens that are functionally two separate tokens with unique individual properties and behaviors, Ontomir EVM's WERC20 logic gives smart contracts direct access to native bank module balances through familiar ERC20 methods.
Key Concept: TEST and WTEST are not separate tokens—they are two different interfaces to the same token stored in the bank module. Native Ontomir tokens (including TEST and all IBC tokens) exist in both wrapped and unwrapped states at all times, allowing developers to choose the interaction method that best fits their use case:
Use it normally through Ontomir bank send (unwrapped state)
Use it like you would normally use ether or 'wei' on the EVM (native value transfers)
Use it as ERC20 WTEST with the contract address below (wrapped state)
WTEST Contract Address: 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE Precompile Type: Dynamic (unique address per wrapped token) Related Module: x/bank (via ERC20 module integration)
Gas Costs
Gas costs are approximated and may vary based on token complexity and chain settings.
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
deposit()
~23,000 gas (no-op)
withdraw(uint256)
~9,000 gas (no-op)
For a comprehensive understanding of how single token representation works and its benefits over traditional wrapping, see the [Single Token Representation](/docs/evm/next/documentation/concepts/single-token-representation) documentation.
Technical Implementation
Architecture Deep Dive
The ERC20 module creates a unified token representation that bridges native Ontomir tokens with ERC20 interfaces:
// Simplified conceptual flow (not actual implementation)
func (k Keeper) ERC20Transfer(from, to common.Address, amount *big.Int) error {
// Convert EVM addresses to Ontomir addresses
OntomirFrom := sdk.AccAddress(from.Bytes())
OntomirTo := sdk.AccAddress(to.Bytes())
// Use bank module directly - no separate ERC20 state
coin := sdk.NewCoin(k.denom, sdk.NewIntFromBigInt(amount))
return k.bankKeeper.SendCoins(ctx, OntomirFrom, OntomirTo, sdk.Coins{coin})
}
func (k Keeper) ERC20BalanceOf(account common.Address) *big.Int {
// Query bank module directly
OntomirAddr := sdk.AccAddress(account.Bytes())
balance := k.bankKeeper.GetBalance(ctx, OntomirAddr, k.denom)
return balance.Amount.BigInt()
}Deposit/Withdraw Implementation Details
Since TEST and WTEST provide different interfaces to the same bank module token, deposit/withdraw functions exist for WETH interface compatibility:
// These functions exist for WETH interface compatibility
function deposit() external payable {
// Handles msg.value by sending received coins back to the caller
// Emits Deposit event for interface compatibility
// Your bank balance reflects the same amount accessible via ERC20 interface
}
function withdraw(uint256 amount) external {
// No-op implementation that only emits Withdrawal event
// No actual token movement since bank balance is directly accessible
// Exists purely for WETH interface compatibility
}**Understanding the Deposit/Withdraw Pattern**
Unlike traditional WETH implementations where the contract holds wrapped tokens:
Traditional WETH: Contract receives ETH and mints WETH tokens that it holds
WERC20: Contract never holds tokens - all balances remain in the bank module
Result: The precompile contract address has no balance; tokens stay with users This is why
deposit()andwithdraw()are no-ops - there's no separate wrapped token state to manage.
Real-World Example
// User starts with 100 TEST in bank module
const testBalance = await bankPrecompile.balances(userAddress);
// Returns: [{denom: "atest", amount: "100000000000000000000"}] // 100 TEST (18 decimals)
const wtestBalance = await wtest.balanceOf(userAddress);
// Returns: "100000000000000000000" // Same 100 TEST, accessed via ERC20 interface
// User transfers 50 WTEST via ERC20
await wtest.transfer(recipientAddress, "50000000000000000000");
// Check balances again
const newTestBalance = await bankPrecompile.balances(userAddress);
// Returns: [{denom: "atest", amount: "50000000000000000000"}] // 50 TEST (18 decimals) remaining
const newWtestBalance = await wtest.balanceOf(userAddress);
// Returns: "50000000000000000000" // Same 50 TEST, both queries return identical valuesMethods
Standard ERC20 Interface
All standard ERC20 methods are available and operate on the underlying bank balance:
balanceOf
balanceOfReturns the native token balance for a specific account (same as bank module balance).
```javascript Ethers.js import { ethers } from "ethers";
const provider = new ethers.JsonRpcProvider(""); const wtestAddress = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"; const werc20Abi = ["function balanceOf(address account) view returns (uint256)"];
const wtest = new ethers.Contract(wtestAddress, werc20Abi, provider);
async function getBalance() { try { const userAddress = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"; const balance = await wtest.balanceOf(userAddress); console.log("Balance (both TEST and WTEST):", balance.toString()); } catch (error) { console.error("Error:", error); } }
```bash cURL
curl -X POST <RPC_URL> \
-H "Content-Type: application/json" \
--data '{
"jsonrpc": "2.0",
"method": "eth_call",
"params": [
{
"to": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
"data": "0x70a08231000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa96045"
},
"latest"
],
"id": 1
}'transfer
transferTransfers tokens using the bank module (identical to native Ontomir transfer).
```solidity Solidity expandable lines // SPDX-License-Identifier: MIT pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract WERC20Example { // WTEST contract address address constant WTEST = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; IERC20 public immutable wtest; event TokensTransferred(address indexed from, address indexed to, uint256 amount); constructor() { wtest = IERC20(WTEST); } function transferWTEST(address to, uint256 amount) external returns (bool) { require(to != address(0), "Invalid recipient"); require(amount > 0, "Amount must be greater than 0"); // This directly moves tokens in the bank module // No wrapping/unwrapping - same underlying token balance bool success = wtest.transfer(to, amount); require(success, "Transfer failed"); emit TokensTransferred(msg.sender, to, amount); return true; } function transferFromWTEST(address from, address to, uint256 amount) external returns (bool) { require(from != address(0) && to != address(0), "Invalid addresses"); require(amount > 0, "Amount must be greater than 0"); bool success = wtest.transferFrom(from, to, amount); require(success, "Transfer from failed"); emit TokensTransferred(from, to, amount); return true; } // Batch transfer example function batchTransfer(address[] calldata recipients, uint256[] calldata amounts) external { require(recipients.length == amounts.length, "Arrays length mismatch"); for (uint256 i = 0; i < recipients.length; i++) { wtest.transferFrom(msg.sender, recipients[i], amounts[i]); emit TokensTransferred(msg.sender, recipients[i], amounts[i]); } }
}
```javascript Ethers.js
import { ethers } from "ethers";
const provider = new ethers.JsonRpcProvider("<RPC_URL>");
const signer = new ethers.Wallet("<PRIVATE_KEY>", provider);
const wtestAddress = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
const werc20Abi = ["function transfer(address to, uint256 amount) returns (bool)"];
const wtest = new ethers.Contract(wtestAddress, werc20Abi, signer);
async function transferTokens() {
try {
const recipientAddress = "0x742d35Cc6634C0532925a3b844Bc9e7595f5b899";
const amount = ethers.parseUnits("10.0", 18); // 10 TEST (18 decimals)
const tx = await wtest.transfer(recipientAddress, amount);
const receipt = await tx.wait();
console.log("Transfer successful:", receipt.hash);
} catch (error) {
console.error("Error:", error);
}
}# First, sign the transaction offline, then:
curl -X POST <RPC_URL> \
-H "Content-Type: application/json" \
--data '{
"jsonrpc": "2.0",
"method": "eth_sendRawTransaction",
"params": ["<SIGNED_TRANSACTION_DATA>"],
"id": 1
}'totalSupply
totalSupplyReturns the total supply from the bank module.
```javascript Ethers.js import { ethers } from "ethers";
const provider = new ethers.JsonRpcProvider(""); const wtestAddress = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"; const werc20Abi = ["function totalSupply() view returns (uint256)"];
const wtest = new ethers.Contract(wtestAddress, werc20Abi, provider);
async function getTotalSupply() { try { const supply = await wtest.totalSupply(); console.log("Total Supply:", supply.toString()); } catch (error) { console.error("Error:", error); } }
```bash cURL
curl -X POST <RPC_URL> \
-H "Content-Type: application/json" \
--data '{
"jsonrpc": "2.0",
"method": "eth_call",
"params": [
{
"to": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
"data": "0x18160ddd"
},
"latest"
],
"id": 1
}'approve / allowance / transferFrom
approve / allowance / transferFromStandard ERC20 approval mechanisms for delegated transfers.
```javascript Ethers.js import { ethers } from "ethers";
const provider = new ethers.JsonRpcProvider(""); const signer = new ethers.Wallet("", provider); const wtestAddress = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"; const werc20Abi = [ "function approve(address spender, uint256 amount) returns (bool)", "function allowance(address owner, address spender) view returns (uint256)", "function transferFrom(address from, address to, uint256 amount) returns (bool)" ];
const wtest = new ethers.Contract(wtestAddress, werc20Abi, signer);
async function approveAndTransfer() { try { const spenderAddress = "0x742d35Cc6634C0532925a3b844Bc9e7595f5b899"; const amount = ethers.parseUnits("50.0", 18); // 50 TEST (18 decimals) // Approve spending const approveTx = await wtest.approve(spenderAddress, amount); await approveTx.wait(); // Check allowance const allowance = await wtest.allowance(signer.address, spenderAddress); console.log("Allowance:", allowance.toString()); // Transfer from (would be called by spender) // const transferTx = await wtest.transferFrom(ownerAddress, recipientAddress, amount); } catch (error) { console.error("Error:", error); }
}
</CodeGroup>
### `name` / `symbol` / `decimals`
Token metadata (e.g., "Wrapped Test", "WTEST", 18).
<CodeGroup>
```javascript Ethers.js
import { ethers } from "ethers";
const provider = new ethers.JsonRpcProvider("<RPC_URL>");
const wtestAddress = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
const werc20Abi = [
"function name() view returns (string)",
"function symbol() view returns (string)",
"function decimals() view returns (uint8)"
];
const wtest = new ethers.Contract(wtestAddress, werc20Abi, provider);
async function getTokenInfo() {
try {
const [name, symbol, decimals] = await Promise.all([
wtest.name(),
wtest.symbol(),
wtest.decimals()
]);
console.log(`Token: ${name} (${symbol}) - ${decimals} decimals`);
} catch (error) {
console.error("Error:", error);
}
}# Get token name
curl -X POST <RPC_URL> \
-H "Content-Type: application/json" \
--data '{
"jsonrpc": "2.0",
"method": "eth_call",
"params": [
{"to": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", "data": "0x06fdde03"},
"latest"
],
"id": 1
}'
# Get token symbol
curl -X POST <RPC_URL> \
-H "Content-Type: application/json" \
--data '{
"jsonrpc": "2.0",
"method": "eth_call",
"params": [
{"to": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", "data": "0x95d89b41"},
"latest"
],
"id": 1
}'
# Get decimals
curl -X POST <RPC_URL> \
-H "Content-Type: application/json" \
--data '{
"jsonrpc": "2.0",
"method": "eth_call",
"params": [
{"to": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", "data": "0x313ce567"},
"latest"
],
"id": 1
}'WETH Compatibility Methods
These methods exist for WETH interface compatibility:
deposit
depositWETH compatibility function - Handles payable deposits for interface compatibility.
This function receives msg.value and immediately sends the coins back to the caller via the bank module, then emits a Deposit event. Since WTEST and TEST are the same underlying bank module token, no actual wrapping occurs - your balance is simply accessible through both native and ERC20 interfaces. ```solidity Solidity expandable lines // SPDX-License-Identifier: MIT pragma solidity ^0.8.0;
// Interface for WERC20 precompile interface IWERC20 { event Deposit(address indexed dst, uint256 wad); event Withdrawal(address indexed src, uint256 wad); function deposit() external payable; function withdraw(uint256 wad) external; function balanceOf(address account) external view returns (uint256);
}
contract WERC20Example { address constant WTEST = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; IWERC20 public immutable wtest; constructor() { wtest = IWERC20(WTEST); } function depositToWTEST() external payable { require(msg.value > 0, "Must send tokens to deposit"); // Get balance before deposit uint256 balanceBefore = wtest.balanceOf(msg.sender); // WERC20 deposit is a no-op for compatibility // Your native token balance is immediately accessible as WTEST wtest.deposit{value: msg.value}(); // Verify balance is now accessible via WTEST interface uint256 balanceAfter = wtest.balanceOf(msg.sender); // Both TEST and WTEST balances reflect the same bank module amount // No actual wrapping occurred - same token, different interface require(balanceAfter >= balanceBefore, "Deposit processed"); } function withdrawFromWTEST(uint256 amount) external { require(amount > 0, "Amount must be greater than 0"); require(wtest.balanceOf(msg.sender) >= amount, "Insufficient balance"); // WERC20 withdraw is a no-op that emits event for compatibility // Your bank balance remains accessible as both native TEST and WTEST wtest.withdraw(amount); // Tokens are still in bank module and accessible both ways } // Helper function to demonstrate balance consistency function checkBalanceConsistency(address user) external view returns ( uint256 wtestBalance, string memory explanation ) { wtestBalance = wtest.balanceOf(user); explanation = "This WTEST balance equals the user's native TEST balance in bank module"; return (wtestBalance, explanation); } // Example DeFi integration showing no wrapping needed function addLiquidityWithDeposit() external payable { require(msg.value > 0, "Must send tokens"); // Deposit via WERC20 interface (compatibility no-op) wtest.deposit{value: msg.value}(); // Your tokens are now accessible as WTEST for DeFi protocols // No additional steps needed - same token, ERC20 interface available uint256 availableForDeFi = wtest.balanceOf(msg.sender); // Use in DeFi protocols immediately // wtest.transfer(defiProtocolAddress, availableForDeFi); }
}
</CodeGroup>
### `withdraw`
**No-op function** - Included for interface compatibility with WETH contracts.
<Info>
This function only emits a Withdrawal event but performs no actual token movement. Since WTEST and TEST are the same underlying bank module token, your native token balance is always directly accessible without any unwrapping process.
</Info>
## Usage Examples
### DeFi Integration Example
```solidity
contract LiquidityPool {
IERC20 public immutable WTEST;
constructor(address _wtest) {
WTEST = IERC20(_wtest);
}
function addLiquidity(uint256 amount) external {
// This transfers from the user's bank balance
WTEST.transferFrom(msg.sender, address(this), amount);
// Pool now has tokens in its bank balance
// No wrapping/unwrapping needed - it's all the same token!
}
function removeLiquidity(uint256 amount) external {
// This transfers back to user's bank balance
WTEST.transfer(msg.sender, amount);
// User can now use these tokens as native TEST
// or continue using WTEST interface - both access same balance
}
}Cross-Interface Balance Verification
// Verify that both interfaces show the same balance
async function verifyBalanceConsistency(userAddress) {
// Query via bank precompile (native interface)
const bankBalance = await bankContract.balances(userAddress);
const testAmount = bankBalance.find(b => b.denom === "test")?.amount || "0";
// Query via WERC20 precompile (ERC20 interface)
const wtestAmount = await wtest.balanceOf(userAddress);
// These will always be equal since the ERC20 balance is just
// an abstracted bank module balance query
console.log(`Consistent balance: ${testAmount} (both TEST and WTEST)`);
}Working with IBC Tokens
// IBC tokens work exactly the same way
const ibcTokenAddress = "0x..."; // Each IBC token gets its own WERC20 address
const ibcToken = new ethers.Contract(ibcTokenAddress, werc20Abi, signer);
// Check balance (same as bank module balance)
const balance = await ibcToken.balanceOf(userAddress);
// Transfer IBC tokens via ERC20 interface
await ibcToken.transfer(recipientAddress, amount);
// Use in DeFi protocols just like any ERC20 token
await defiProtocol.stake(ibcTokenAddress, amount);Solidity Interface & ABI
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity >=0.8.18;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
/// @title WERC20 Precompile Contract
/// @dev Provides ERC20 interface to native Ontomir tokens via bank module
/// @notice This is NOT a traditional wrapped token - both native and ERC20 interfaces access the same balance
interface IWERC20 is IERC20 {
/// @dev Emitted when deposit() is called (no-op for compatibility)
/// @param dst The address that called deposit
/// @param wad The amount specified (though no conversion occurs)
event Deposit(address indexed dst, uint256 wad);
/// @dev Emitted when withdraw() is called (no-op for compatibility)
/// @param src The address that called withdraw
/// @param wad The amount specified (though no conversion occurs)
event Withdrawal(address indexed src, uint256 wad);
/// @dev No-op function for WETH compatibility - native tokens automatically update balance
/// @notice This function exists for interface compatibility but performs no conversion
function deposit() external payable;
/// @dev No-op function for WETH compatibility - native tokens always accessible
/// @param wad Amount to "withdraw" (no conversion performed)
/// @notice This function exists for interface compatibility but performs no conversion
function withdraw(uint256 wad) external;
}{
"_format": "hh-sol-artifact-1",
"contractName": "IWERC20",
"sourceName": "solidity/precompiles/werc20/IWERC20.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": "dst",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "wad",
"type": "uint256"
}
],
"name": "Deposit",
"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"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "src",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "wad",
"type": "uint256"
}
],
"name": "Withdrawal",
"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": "deposit",
"outputs": [],
"stateMutability": "payable",
"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"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "wad",
"type": "uint256"
}
],
"name": "withdraw",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
]
}