TIP20
Abstract
TIP20 is a suite of precompiles that provide a built-in optimized token implementation in the core protocol. It extends the ERC-20 token standard with built-in functionality like memo fields and reward distribution.
Motivation
All major stablecoins today use the ERC-20 token standard. While ERC-20 provides a solid foundation for fungible tokens, it lacks features critical for stablecoin issuers today such as memos, transfer policies, and rewards distribution. Additionally, since each ERC-20 token has its own implementation, integrators can't depend on consistent behavior across tokens. TIP-20 extends ERC-20, building these features into precompiled contracts that anyone can permissionlessly deploy on Tempo. This makes token operations much more efficient, allows issuers to quickly set up on Tempo, and simplifies integrations since it ensures standardized behavior across tokens. It also enables deeper integration with token-specific Tempo features like paying gas in stablecoins and payment lanes.
Specification
TIP-20 tokens support standard fungible token operations such as transfers, mints, and burns. They also support transfers, mints, and burns with an attached 32-byte memo; a role-based access control system for token administrative operations; and a system for opt-in reward distribution.
TIP20
The core TIP-20 contract exposes standard ERC-20 functions for balances, allowances, transfers, and delegated transfers, and also adds:
- 32-byte memo support on transfers, mints, and burns.
- A
TIP20Rolesmodule for permissioned actions like issuing, pausing, unpausing, and burning blocked balances. - Configuration options for currencies, quote tokens, and transfer policies.
The complete TIP20 interface is defined below:
interface ITIP20 {
// =========================================================================
// ERC-20 standard functions
// =========================================================================
/// @notice Returns the name of the token
/// @return The token name
function name() external view returns (string memory);
/// @notice Returns the symbol of the token
/// @return The token symbol
function symbol() external view returns (string memory);
/// @notice Returns the number of decimals for the token
/// @return Always returns 6 for TIP-20 tokens
function decimals() external pure returns (uint8);
/// @notice Returns the total amount of tokens in circulation
/// @return The total supply of tokens
function totalSupply() external view returns (uint256);
/// @notice Returns the token balance of an account
/// @param account The address to check the balance for
/// @return The token balance of the account
function balanceOf(address account) external view returns (uint256);
/// @notice Transfers tokens from caller to recipient
/// @param to The recipient address
/// @param amount The amount of tokens to transfer
/// @return True if successful
function transfer(address to, uint256 amount) external returns (bool);
/// @notice Returns the remaining allowance for a spender
/// @param owner The token owner address
/// @param spender The spender address
/// @return The remaining allowance amount
function allowance(address owner, address spender) external view returns (uint256);
/// @notice Approves a spender to spend tokens on behalf of caller
/// @param spender The address to approve
/// @param amount The amount to approve
/// @return True if successful
function approve(address spender, uint256 amount) external returns (bool);
/// @notice Transfers tokens from one address to another using allowance
/// @param from The sender address
/// @param to The recipient address
/// @param amount The amount to transfer
/// @return True if successful
function transferFrom(address from, address to, uint256 amount) external returns (bool);
/// @notice Mints new tokens to an address (requires ISSUER_ROLE)
/// @param to The recipient address
/// @param amount The amount of tokens to mint
function mint(address to, uint256 amount) external;
/// @notice Burns tokens from caller's balance (requires ISSUER_ROLE)
/// @param amount The amount of tokens to burn
function burn(uint256 amount) external;
// =========================================================================
// TIP-20 extended functions
// =========================================================================
/// @notice Transfers tokens from caller to recipient with a memo
/// @param to The recipient address
/// @param amount The amount of tokens to transfer
/// @param memo A 32-byte memo attached to the transfer
function transferWithMemo(address to, uint256 amount, bytes32 memo) external;
/// @notice Transfers tokens from one address to another with a memo using allowance
/// @param from The sender address
/// @param to The recipient address
/// @param amount The amount to transfer
/// @param memo A 32-byte memo attached to the transfer
/// @return True if successful
function transferFromWithMemo(address from, address to, uint256 amount, bytes32 memo) external returns (bool);
/// @notice Mints new tokens to an address with a memo (requires ISSUER_ROLE)
/// @param to The recipient address
/// @param amount The amount of tokens to mint
/// @param memo A 32-byte memo attached to the mint
function mintWithMemo(address to, uint256 amount, bytes32 memo) external;
/// @notice Burns tokens from caller's balance with a memo (requires ISSUER_ROLE)
/// @param amount The amount of tokens to burn
/// @param memo A 32-byte memo attached to the burn
function burnWithMemo(uint256 amount, bytes32 memo) external;
/// @notice Burns tokens from a blocked address (requires BURN_BLOCKED_ROLE)
/// @param from The address to burn tokens from (must be unauthorized by transfer policy)
/// @param amount The amount of tokens to burn
function burnBlocked(address from, uint256 amount) external;
/// @notice Returns the quote token used for DEX pairing
/// @return The quote token address
function quoteToken() external view returns (ITIP20);
/// @notice Returns the next quote token staged for update
/// @return The next quote token address (zero if none staged)
function nextQuoteToken() external view returns (ITIP20);
/// @notice Returns the currency identifier for this token
/// @return The currency string
function currency() external view returns (string memory);
/// @notice Returns whether the token is currently paused
/// @return True if paused, false otherwise
function paused() external view returns (bool);
/// @notice Returns the maximum supply cap for the token
/// @return The supply cap (checked on mint operations)
function supplyCap() external view returns (uint256);
/// @notice Returns the current transfer policy ID from TIP-403 registry
/// @return The transfer policy ID
function transferPolicyId() external view returns (uint64);
// =========================================================================
// Admin Functions
// =========================================================================
/// @notice Pauses the contract, blocking transfers (requires PAUSE_ROLE)
function pause() external;
/// @notice Unpauses the contract, allowing transfers (requires UNPAUSE_ROLE)
function unpause() external;
/// @notice Changes the transfer policy ID (requires DEFAULT_ADMIN_ROLE)
/// @param newPolicyId The new policy ID from TIP-403 registry
function changeTransferPolicyId(uint64 newPolicyId) external;
/// @notice Stages a new quote token for update (requires DEFAULT_ADMIN_ROLE)
/// @param newQuoteToken The new quote token address
function setNextQuoteToken(ITIP20 newQuoteToken) external;
/// @notice Completes the quote token update process (requires DEFAULT_ADMIN_ROLE)
function completeQuoteTokenUpdate() external;
/// @notice Sets the maximum supply cap (requires DEFAULT_ADMIN_ROLE)
/// @param newSupplyCap The new supply cap (cannot be less than current supply)
function setSupplyCap(uint256 newSupplyCap) external;
// =========================================================================
// Role Management
// =========================================================================
/// @notice Returns the BURN_BLOCKED_ROLE constant
/// @return keccak256("BURN_BLOCKED_ROLE")
function BURN_BLOCKED_ROLE() external view returns (bytes32);
/// @notice Returns the ISSUER_ROLE constant
/// @return keccak256("ISSUER_ROLE")
function ISSUER_ROLE() external view returns (bytes32);
/// @notice Returns the PAUSE_ROLE constant
/// @return keccak256("PAUSE_ROLE")
function PAUSE_ROLE() external view returns (bytes32);
/// @notice Returns the UNPAUSE_ROLE constant
/// @return keccak256("UNPAUSE_ROLE")
function UNPAUSE_ROLE() external view returns (bytes32);
/// @notice Grants a role to an account (requires role admin)
/// @param role The role to grant (keccak256 hash)
/// @param account The account to grant the role to
function grantRole(bytes32 role, address account) external;
/// @notice Revokes a role from an account (requires role admin)
/// @param role The role to revoke (keccak256 hash)
/// @param account The account to revoke the role from
function revokeRole(bytes32 role, address account) external;
/// @notice Allows an account to remove a role from itself
/// @param role The role to renounce (keccak256 hash)
function renounceRole(bytes32 role) external;
/// @notice Changes the admin role for a specific role (requires current role admin)
/// @param role The role whose admin is being changed
/// @param adminRole The new admin role
function setRoleAdmin(bytes32 role, bytes32 adminRole) external;
// =========================================================================
// System Functions
// =========================================================================
/// @notice System-level transfer function (restricted to precompiles)
/// @param from The sender address
/// @param to The recipient address
/// @param amount The amount to transfer
/// @return True if successful
function systemTransferFrom(address from, address to, uint256 amount) external returns (bool);
/// @notice Pre-transaction fee transfer (restricted to precompiles)
/// @param from The account to charge fees from
/// @param amount The fee amount
function transferFeePreTx(address from, uint256 amount) external;
/// @notice Post-transaction fee handling (restricted to precompiles)
/// @param to The account to refund
/// @param refund The refund amount
/// @param actualUsed The actual fee used
function transferFeePostTx(address to, uint256 refund, uint256 actualUsed) external;
// =========================================================================
// Events
// =========================================================================
/// @notice Emitted when a new allowance is set by `owner` for `spender`
/// @param owner The account granting the allowance
/// @param spender The account being approved to spend tokens
/// @param amount The new allowance amount
event Approval(address indexed owner, address indexed spender, uint256 amount);
/// @notice Emitted when tokens are burned from an address
/// @param from The address whose tokens were burned
/// @param amount The amount of tokens that were burned
event Burn(address indexed from, uint256 amount);
/// @notice Emitted when tokens are burned from a blocked address
/// @param from The blocked address whose tokens were burned
/// @param amount The amount of tokens that were burned
event BurnBlocked(address indexed from, uint256 amount);
/// @notice Emitted when new tokens are minted to an address
/// @param to The address receiving the minted tokens
/// @param amount The amount of tokens that were minted
event Mint(address indexed to, uint256 amount);
/// @notice Emitted when a new quote token is staged for this token
/// @param updater The account that staged the new quote token
/// @param nextQuoteToken The quote token that has been staged
event NextQuoteTokenSet(address indexed updater, ITIP20 indexed nextQuoteToken);
/// @notice Emitted when the pause state of the token changes
/// @param updater The account that changed the pause state
/// @param isPaused The new pause state; true if paused, false if unpaused
event PauseStateUpdate(address indexed updater, bool isPaused);
/// @notice Emitted when the quote token update process is completed
/// @param updater The account that completed the quote token update
/// @param newQuoteToken The new quote token that has been set
event QuoteTokenUpdate(address indexed updater, ITIP20 indexed newQuoteToken);
/// @notice Emitted when a holder sets or updates their reward recipient address
/// @param holder The token holder configuring the recipient
/// @param recipient The address that will receive claimed rewards
event RewardRecipientSet(address indexed holder, address indexed recipient);
/// @notice Emitted when a reward distribution is scheduled
/// @param funder The account funding the reward distribution
/// @param id The identifier of the reward (0 for instant distributions)
/// @param amount The total amount of tokens allocated to the reward
/// @param durationSeconds The duration in seconds (must be 0 in current version)
event RewardScheduled(
address indexed funder,
uint64 indexed id,
uint256 amount,
uint32 durationSeconds
);
/// @notice Emitted when the token's supply cap is updated
/// @param updater The account that updated the supply cap
/// @param newSupplyCap The new maximum total supply
event SupplyCapUpdate(address indexed updater, uint256 indexed newSupplyCap);
/// @notice Emitted for all token movements, including mints and burns
/// @param from The address sending tokens (address(0) for mints)
/// @param to The address receiving tokens (address(0) for burns)
/// @param amount The amount of tokens transferred
event Transfer(address indexed from, address indexed to, uint256 amount);
/// @notice Emitted when the transfer policy ID is updated
/// @param updater The account that updated the transfer policy
/// @param newPolicyId The new transfer policy ID from the TIP-403 registry
event TransferPolicyUpdate(address indexed updater, uint64 indexed newPolicyId);
/// @notice Emitted when a transfer, mint, or burn is performed with an attached memo
/// @param from The address sending tokens (address(0) for mints)
/// @param to The address receiving tokens (address(0) for burns)
/// @param amount The amount of tokens transferred
/// @param memo The 32-byte memo associated with this movement
event TransferWithMemo(
address indexed from,
address indexed to,
uint256 amount,
bytes32 indexed memo
);
/// @notice Emitted when the membership of a role changes for an account
/// @param role The role being granted or revoked
/// @param account The account whose membership was changed
/// @param sender The account that performed the change
/// @param hasRole True if the role was granted, false if it was revoked
event RoleMembershipUpdated(
bytes32 indexed role,
address indexed account,
address indexed sender,
bool hasRole
);
/// @notice Emitted when the admin role for a role is updated
/// @param role The role whose admin role was changed
/// @param newAdminRole The new admin role for the given role
/// @param sender The account that performed the update
event RoleAdminUpdated(
bytes32 indexed role,
bytes32 indexed newAdminRole,
address indexed sender
);
// =========================================================================
// Errors
// =========================================================================
/// @notice The token operation is blocked because the contract is currently paused
error ContractPaused();
/// @notice The spender does not have enough allowance for the attempted transfer
error InsufficientAllowance();
/// @notice The account does not have the required token balance for the operation
/// @param currentBalance The current balance of the account
/// @param expectedBalance The required balance for the operation to succeed
/// @param token The address of the token contract
error InsufficientBalance(uint256 currentBalance, uint256 expectedBalance, address token);
/// @notice The provided amount is zero or otherwise invalid for the attempted operation
error InvalidAmount();
/// @notice The provided currency identifier is invalid or unsupported
error InvalidCurrency();
/// @notice The specified quote token is invalid, incompatible, or would create a circular reference
error InvalidQuoteToken();
/// @notice The recipient address is not a valid destination for this operation
/// (for example, another TIP-20 token contract)
error InvalidRecipient();
/// @notice The new supply cap is invalid, for example lower than the current total supply
error InvalidSupplyCap();
/// @notice A rewards operation was attempted when no opted-in supply exists
error NoOptedInSupply();
/// @notice The configured transfer policy denies authorization for the sender or recipient
error PolicyForbids();
/// @notice Attempted to start a timed reward distribution; streaming is disabled
error ScheduledRewardsDisabled();
/// @notice The attempted operation would cause total supply to exceed the configured supply cap
error SupplyCapExceeded();
/// @notice The caller does not have the required role or permission for this operation
error Unauthorized();
}Memos
Memo functions transferWithMemo, transferFromWithMemo, mintWithMemo, and burnWithMemo behave like their ERC-20 equivalents but additionally emit memo data in dedicated events. The memo is always a fixed 32-byte field. Callers should pack shorter strings or identifiers directly into this field, and use hashes or external references when the underlying payload exceeds 32 bytes.
TIP-403 Transfer Policies
All operations that move tokens: transfer, transferFrom, transferWithMemo, transferFromWithMemo, mint, burn, mintWithMemo, and burnWithMemo — enforce the token’s configured TIP-403 transfer policy.
Internally, this is implemented via a transferAuthorized modifier that:
- Calls
TIP403_REGISTRY.isAuthorized(transferPolicyId, from)for the sender. - Calls
TIP403_REGISTRY.isAuthorized(transferPolicyId, to)for the recipient.
Both checks must return true, otherwise the call reverts with PolicyForbids.
Reward operations (startReward, setRewardRecipient, claimRewards) also perform the same TIP-403 authorization checks before moving any funds.
Invalid Recipient Protection
TIP-20 tokens cannot be sent to other TIP-20 token contract addresses. The implementation uses a notTokenAddress guard that rejects recipients whose address has the TIP-20 prefix (0x20c000000000000000000000).
Any attempt to transfer to a TIP-20 token address must revert with InvalidRecipient. This prevents accidental token loss by sending funds to token contracts instead of user accounts.
Currencies and Quote Tokens
Each TIP-20 token declares a currency identifier and a corresponding quoteToken used for pricing and routing in the Stablecoin DEX. Tokens with currency == "USD" must pair with a USD-denominated TIP-20 token.
Updating the quote token occurs in two phases:
setNextQuoteTokenstages a new quote token.completeQuoteTokenUpdatefinalizes the change.
The implementation must validate that the new quote token is a TIP-20 token, matches currency rules, and does not create circular quote-token chains.
Pause Controls
Pause controls pause and unpause govern all transfer operations and reward related flows. When paused, transfers and memo transfers halt, but administrative and configuration functions remain allowed. The paused() getter reflects the current state and must be checked by all affected entrypoints.
TIP-20 Roles
TIP-20 uses a role-based authorization system. The main roles are:
ISSUER_ROLE: controls minting and burning.PAUSE_ROLE/UNPAUSE_ROLE: controls the token’s paused state.BURN_BLOCKED_ROLE: allows burning balances belonging to addresses that fail TIP-403 authorization.
Roles are assigned and managed through grantRole, revokeRole, renounceRole, and setRoleAdmin, via the contract admin.
System Functions
System level functions systemTransferFrom, transferFeePreTx, and transferFeePostTx are only callable by other Tempo protocol precompiles. These entrypoints power transaction fee collection, refunds, and internal accounting within the Fee AMM and Stablecoin DEX. They must not be callable by general contracts or users.
Token Rewards Distribution
See rewards distribution for more information.
TIP20Factory
The TIP20Factory contract is the canonical entrypoint for creating new TIP-20 tokens on Tempo. The factory maintains an internal tokenIdCounter that increments with each deployment, and uses this counter to derive deterministic “vanity” deployment addresses under a fixed 12-byte TIP-20 prefix. This ensures that every TIP-20 token exists at a predictable, collision-free address, and that integrators can infer a token’s identifier directly from its address. The TIP20Factory precompile is deployed at 0x20Fc000000000000000000000000000000000000. Newly created TIP-20 addresses are deployed a vanity address derived from TIP20_PREFIX || tokenId`, where:
TIP20_PREFIXis the 12-byte prefix0x20C0000000000000000000000000tokenIdis the current monotonically increasing counter value, encoded into the least significant bytes of the address (eg.0x20C0000000000000000000000000000000000001)
When creating a token, the factory performs several checks to guarantee consistency across the TIP-20 ecosystem:
- The specified Quote token must be a currently deployed TIP20.
- Tokens that specify their currency as USD must also specify a quote token that is denoted in USD.
- At deployment, the factory initializes defaults on the TIP-20:
transferPolicyId = 1,supplyCap = type(uint128).max,paused = false, andtotalSupply = 0. - The provided
adminaddress receivesDEFAULT_ADMIN_ROLE, enabling it to manage roles and token configurations.
The complete TIP20Factory interface is defined below:
/// @title TIP-20 Factory Interface
/// @notice Deploys and initializes new TIP-20 tokens at deterministic vanity addresses
interface ITIP20Factory {
/// @notice Creates and deploys a new TIP-20 token
/// @param name The token's ERC-20 name
/// @param symbol The token's ERC-20 symbol
/// @param currency The token's currency identifier (e.g. "USD")
/// @param quoteToken The TIP-20 quote token used for exchange pricing
/// @param admin The address to receive DEFAULT_ADMIN_ROLE on the new token
///
/// @return token The deployed TIP-20 token address
/// @dev
/// - Computes the TIP-20 deployment address as TIP20_PREFIX || tokenId
/// - Ensures the provided quote token is itself a valid TIP-20
/// - Enforces USD-denomination rules (USD tokens must use USD quote tokens)
/// - Rejects configurations that would form circular quote-token chains
/// - Initializes the token with default settings:
/// transferPolicyId = 1 (always-allow)
/// supplyCap = type(uint128).max
/// paused = false
/// totalSupply = 0
/// - Grants DEFAULT_ADMIN_ROLE on the new token to `admin`
/// - Emits a {TokenCreated} event
function createToken(
string memory name,
string memory symbol,
string memory currency,
ITIP20 quoteToken,
address admin
) external returns (address token);
// =========================================================================
// Helpers
// =========================================================================
/// @notice Returns true if `token` is a valid TIP-20 address
/// @param token The address to check
/// @return True if the address is a well-formed TIP-20
/// @dev Checks the TIP-20 prefix and ensures its embedded ID <= tokenIdCounter
function isTIP20(address token) external view returns (bool);
/// @notice Returns the next token ID that will be assigned on creation
/// @return The current tokenIdCounter value
function tokenIdCounter() external view returns (uint256);
// =========================================================================
// Events
// =========================================================================
/// @notice Emitted when a new TIP-20 token is created
/// @param token The newly deployed TIP-20 address
/// @param id The assigned token ID used in address construction
/// @param name The token name
/// @param symbol The token symbol
/// @param currency The token currency
/// @param quoteToken The token's assigned quote token
/// @param admin The address receiving DEFAULT_ADMIN_ROLE
event TokenCreated(
address indexed token,
uint256 indexed id,
string name,
string symbol,
string currency,
ITIP20 indexed quoteToken,
address admin
);
// =========================================================================
// Errors
// =========================================================================
/// @notice The provided quote token address is invalid or not a TIP-20
error InvalidQuoteToken();
}Invariants
totalSupply()must always equal to the sum of allbalanceOf(account)over all accounts.totalSupply()must always be<= supplyCap- When
pausedistrue, no functions that move tokens (transfer,transferFrom, memo variants,systemTransferFrom,startReward,setRewardRecipient,claimRewards) can succeed. - TIP20 tokens can not send any amount another TIP20 token address.
systemTransferFrom,transferFeePreTx, andtransferFeePostTxnever changetotalSupply().