Tempo uses TIP-20Tempo's enshrined token standard, purpose-built for stablecoins. All TIP-20 tokens have exactly 6 decimals and are deployed through a protocol-level factory contract. as its native token standard instead of ERC-20. TIP-20 tokens are not deployed as arbitrary smart contracts. They're created through an enshrined factory precompile, which means every token gets the same interface, the same 6-decimal precision, and access to protocol features like payment lanes and memo transfers.
This guide covers creating a TIP-20 token on Moderato, configuring its roles and supply cap, minting tokens, and querying token data through the Transfa Data API.
Prerequisites
- Foundry installed (
forge,cast) - A funded wallet on Moderato (see How to Get Started Building on Tempo)
- A Transfa API key (app.transfa.com)
How TIP-20 differs from ERC-20
Key differences:
| ERC-20 | TIP-20 | |
|---|---|---|
| Decimals | Developer chooses (usually 18) | Always 6 |
| Deployment | Any smart contract | Factory precompile only |
| Address prefix | Any | 0x20c0 |
| Memo support | None (application-level hack) | Protocol-level transferWithMemo |
| Compliance | Per-contract | Shared policy system (TIP-403) |
| Fee payment | Cannot pay gas | USD-denominated TIP-20 tokens can pay transaction fees |
| Blockspace | Competes with all transactions | Dedicated payment lanes |
TIP-20 tokens share the standard Transfer event signature with ERC-20, so existing tools that listen for ERC-20 transfers will pick up TIP-20 transfers. But the extended features (memos, policies, enshrined roles) are TIP-20-specific.
Create a token with the factory
The TIP-20 Factory lives at 0x20Fc000000000000000000000000000000000000. You call createToken with your token's name, symbol, currency denomination, and a salt for deterministic address generation.
cast send 0x20Fc000000000000000000000000000000000000 \
"createToken(string,string,string,address,address,bytes32)" \
"Example USD" "XUSD" "USD" \
0x0000000000000000000000000000000000000000 \
$MY_ADDRESS \
$(cast keccak "my-unique-salt-v1") \
--rpc-url https://rpc.moderato.tempo.xyz \
--private-key $PRIVATE_KEYParameters:
- name: Human-readable token name
- symbol: Short ticker symbol
- currency: ISO 4217 currency code (e.g.,
"USD"). This is immutable after creation, so choose carefully. Only USD-denominated tokens can be used to pay transaction fees. - quoteToken: The TIP-20 token this pairs with on the Stablecoin DEX. Pass the zero address if you don't need DEX integration.
- admin: Address that receives
DEFAULT_ADMIN_ROLE. This is typically your deployer address. - salt: Used for deterministic address derivation. The resulting token address is
TIP20_PREFIX || highest64BitsOf(keccak256(sender, salt)).
The first 1,000 token addresses (where the lower bytes are < 1000) are reserved for the protocol. The four testnet stablecoins (pathUSD, AlphaUSD, BetaUSD, ThetaUSD) use these.
Find your token's address
Check the transaction receipt for the token address. Alternatively, you can predict it:
# Get the transaction receipt
cast receipt $TX_HASH --rpc-url https://rpc.moderato.tempo.xyzThe new token address will start with 0x20c0. This prefix identifies any TIP-20 token on-chain.
Configure roles
TIP-20 tokens use a role-based access control system. The admin address set during creation can grant these roles:
| Role | Permissions |
|---|---|
DEFAULT_ADMIN_ROLE | Grant/revoke all roles, set supply cap, change transfer policy |
ISSUER_ROLE | Mint and burn tokens |
PAUSE_ROLE | Pause all token transfers |
UNPAUSE_ROLE | Unpause token transfers |
BURN_BLOCKED_ROLE | Burn tokens from blocked addresses (compliance) |
Grant the issuer role to your minting address:
# keccak256("ISSUER_ROLE")
ISSUER_ROLE=$(cast keccak "ISSUER_ROLE")
cast send $TOKEN_ADDRESS \
"grantRole(bytes32,address)" \
$ISSUER_ROLE $MINTER_ADDRESS \
--rpc-url https://rpc.moderato.tempo.xyz \
--private-key $ADMIN_PRIVATE_KEYSet a supply cap
By default, tokens are created with no practical supply cap (type(uint128).max). To set a cap:
# Set supply cap to 1,000,000 tokens (1_000_000 * 10^6)
cast send $TOKEN_ADDRESS \
"setSupplyCap(uint256)" \
1000000000000 \
--rpc-url https://rpc.moderato.tempo.xyz \
--private-key $ADMIN_PRIVATE_KEYAll TIP-20 amounts use 6 decimals. 1,000,000 tokens = 1000000000000 (1e12).
Mint tokens
With the ISSUER_ROLE granted, mint tokens to any address:
# Mint 10,000 XUSD to a recipient
cast send $TOKEN_ADDRESS \
"mint(address,uint256)" \
0xRECIPIENT_ADDRESS 10000000000 \
--rpc-url https://rpc.moderato.tempo.xyz \
--private-key $MINTER_PRIVATE_KEYTo mint with an attached memo (for audit trails):
cast send $TOKEN_ADDRESS \
"mintWithMemo(address,uint256,bytes32)" \
0xRECIPIENT_ADDRESS 10000000000 \
$(cast to-bytes32 "MINT-2026-001") \
--rpc-url https://rpc.moderato.tempo.xyz \
--private-key $MINTER_PRIVATE_KEYQuery token data with the Transfa Data API
Once your token exists on-chain, you can query its metadata, holders, and allowances through the Transfa Data API instead of making multiple RPC calls.
Get token metadata
curl -X GET "https://api.transfa.com/v1/tokens/$TOKEN_ADDRESS?chain=moderato" \
-H "x-api-key: YOUR_API_KEY"{
"data": {
"address": "0x20c0...",
"name": "Example USD",
"symbol": "XUSD",
"decimals": 6,
"totalSupply": "10000000000",
"currency": "USD",
"supplyCap": "1000000000000",
"paused": false,
"transferPolicyId": "1"
}
}List token holders
curl -X GET "https://api.transfa.com/v1/tokens/$TOKEN_ADDRESS/holders?chain=moderato" \
-H "x-api-key: YOUR_API_KEY"This returns every address holding a non-zero balance of your token, sorted by balance. You can filter with minBalance to exclude dust amounts.
Check allowances
curl -X GET "https://api.transfa.com/v1/tokens/$TOKEN_ADDRESS/allowances?chain=moderato" \
-H "x-api-key: YOUR_API_KEY"Returns all active approve() allowances for your token, useful for monitoring which addresses have spending permissions and how much they can move.
Transfer policy and compliance
Every TIP-20 token has a transferPolicyId that references the TIP-403 Policy RegistryA protocol-level precompile at 0x403c000000000000000000000000000000000000 that manages compliance policies. A single policy can govern multiple tokens.. By default, new tokens use policy ID 1 (always-allow).
When a transfer occurs, the protocol calls isAuthorized(policyId, from) and isAuthorized(policyId, to) on the registry. Both must return true or the transaction reverts with PolicyForbids().
To change the transfer policy:
cast send $TOKEN_ADDRESS \
"changeTransferPolicyId(uint64)" \
42 \
--rpc-url https://rpc.moderato.tempo.xyz \
--private-key $ADMIN_PRIVATE_KEYThis allows issuers to implement compliance controls (KYC, sanctions screening) at the protocol level without modifying the token contract itself.
What to read next
- How to Get Started Building on Tempo: set up your development environment and send your first transfer
- How to Track Wallet Balances and Transfers on Tempo: monitor your token's transfer activity
- Transfa Data API reference: full token endpoint documentation