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:
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:
**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
Methods
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); } }
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]); } }
}
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); } }
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); }
}
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); }
}
