Documentation Index
Fetch the complete documentation index at: https://mintlify.com/tempoxyz/tempo/llms.txt
Use this file to discover all available pages before exploring further.
The TIP-403 Registry precompile provides a centralized registry for transfer policies that control which addresses can send or receive TIP-20 tokens. This enables compliance requirements like KYC/AML whitelists and sanctions blacklists.
Address
0x403C000000000000000000000000000000000000
Overview
The registry manages policies that TIP-20 tokens can reference:
- Whitelists: Only listed addresses are authorized
- Blacklists: All addresses except listed ones are authorized
- Compound Policies: Separate sender/recipient policies (T2+)
- Immutability: Policies can be created but not deleted
Interface
interface ITIP403Registry {
enum PolicyType {
WHITELIST,
BLACKLIST,
COMPOUND // T2+ only
}
struct PolicyData {
PolicyType policyType;
address admin;
}
// Policy creation
function createPolicy(
address admin,
PolicyType policyType
) external returns (uint64 newPolicyId);
function createPolicyWithAccounts(
address admin,
PolicyType policyType,
address[] calldata accounts
) external returns (uint64 newPolicyId);
// Policy management
function setPolicyAdmin(uint64 policyId, address admin) external;
function modifyPolicyWhitelist(
uint64 policyId,
address account,
bool allowed
) external;
function modifyPolicyBlacklist(
uint64 policyId,
address account,
bool restricted
) external;
// View functions
function policyIdCounter() external view returns (uint64);
function policyExists(uint64 policyId) external view returns (bool);
function policyData(uint64 policyId)
external view returns (PolicyType policyType, address admin);
function isAuthorized(uint64 policyId, address user)
external view returns (bool);
// TIP-1015: Compound policies (T2+)
function createCompoundPolicy(
uint64 senderPolicyId,
uint64 recipientPolicyId,
uint64 mintRecipientPolicyId
) external returns (uint64 newPolicyId);
function isAuthorizedSender(uint64 policyId, address user)
external view returns (bool);
function isAuthorizedRecipient(uint64 policyId, address user)
external view returns (bool);
function isAuthorizedMintRecipient(uint64 policyId, address user)
external view returns (bool);
}
Events
event PolicyCreated(
uint64 indexed policyId,
address indexed updater,
PolicyType policyType
);
event PolicyAdminUpdated(
uint64 indexed policyId,
address indexed updater,
address indexed admin
);
event WhitelistUpdated(
uint64 indexed policyId,
address indexed updater,
address indexed account,
bool allowed
);
event BlacklistUpdated(
uint64 indexed policyId,
address indexed updater,
address indexed account,
bool restricted
);
event CompoundPolicyCreated(
uint64 indexed policyId,
address indexed creator,
uint64 senderPolicyId,
uint64 recipientPolicyId,
uint64 mintRecipientPolicyId
);
Errors
error Unauthorized();
error PolicyNotFound();
error PolicyNotSimple();
error IncompatiblePolicyType();
error InvalidPolicyType();
Built-in Policies
Two special policies are always available:
// Policy 0: Always reject
uint64 constant ALWAYS_REJECT = 0;
// Policy 1: Always allow
uint64 constant ALWAYS_ALLOW = 1;
// Custom policies start at 2
uint64 firstCustomPolicy = 2;
Simple Policies
Creating a Whitelist
contract Compliance {
ITIP403Registry constant REGISTRY =
ITIP403Registry(0x403C000000000000000000000000000000000000);
function createKYCWhitelist() external returns (uint64) {
// Create empty whitelist
uint64 policyId = REGISTRY.createPolicy(
msg.sender, // admin
ITIP403Registry.PolicyType.WHITELIST
);
// Add KYC'd addresses
address[] memory kycUsers = getKYCdUsers();
for (uint i = 0; i < kycUsers.length; i++) {
REGISTRY.modifyPolicyWhitelist(
policyId,
kycUsers[i],
true // allowed
);
}
return policyId;
}
}
Creating a Blacklist
function createSanctionsList() external returns (uint64) {
// Get sanctioned addresses
address[] memory sanctioned = getSanctionedAddresses();
// Create blacklist with initial accounts
uint64 policyId = REGISTRY.createPolicyWithAccounts(
msg.sender,
ITIP403Registry.PolicyType.BLACKLIST,
sanctioned
);
return policyId;
}
Checking Authorization
function isUserAuthorized(
uint64 policyId,
address user
) public view returns (bool) {
return REGISTRY.isAuthorized(policyId, user);
}
// Whitelist: returns true if user is in whitelist
// Blacklist: returns true if user is NOT in blacklist
Compound Policies (T2+)
Compound policies allow different rules for senders, recipients, and mint recipients:
Creating a Compound Policy
function createVendorCreditsPolicy() external returns (uint64) {
// 1. Create vendor whitelist (only vendor can receive)
uint64 vendorWhitelist = REGISTRY.createPolicy(
msg.sender,
ITIP403Registry.PolicyType.WHITELIST
);
REGISTRY.modifyPolicyWhitelist(vendorWhitelist, vendorAddress, true);
// 2. Create compound policy
// - Anyone can send (policy 1 = always allow)
// - Only vendor can receive transfers (vendorWhitelist)
// - Anyone can receive mints (policy 1 = always allow)
uint64 compoundId = REGISTRY.createCompoundPolicy(
1, // senderPolicyId: anyone can send
vendorWhitelist, // recipientPolicyId: only vendor
1 // mintRecipientPolicyId: anyone
);
return compoundId;
}
Authorization Checks
// Check sender authorization
bool canSend = REGISTRY.isAuthorizedSender(policyId, sender);
// Check recipient authorization
bool canReceive = REGISTRY.isAuthorizedRecipient(policyId, recipient);
// Check mint recipient authorization
bool canReceiveMint = REGISTRY.isAuthorizedMintRecipient(policyId, recipient);
// Generic check (sender AND recipient)
bool authorized = REGISTRY.isAuthorized(policyId, user);
Policy Management
Updating Policy Admin
function transferPolicyOwnership(
uint64 policyId,
address newAdmin
) external {
REGISTRY.setPolicyAdmin(policyId, newAdmin);
// Only current admin can call this
}
Adding/Removing from Whitelist
function updateWhitelist(
uint64 policyId,
address user,
bool allowed
) external {
REGISTRY.modifyPolicyWhitelist(policyId, user, allowed);
// Only policy admin can call this
}
// Add user
updateWhitelist(policyId, user, true);
// Remove user
updateWhitelist(policyId, user, false);
Adding/Removing from Blacklist
function updateBlacklist(
uint64 policyId,
address user,
bool restricted
) external {
REGISTRY.modifyPolicyBlacklist(policyId, user, restricted);
// Only policy admin can call this
}
// Add to blacklist
updateBlacklist(policyId, user, true);
// Remove from blacklist
updateBlacklist(policyId, user, false);
TIP-20 Integration
TIP-20 tokens reference policy IDs for transfer restrictions:
contract TIP20Token {
uint64 public policyId;
function setPolicy(uint64 newPolicyId) external onlyAdmin {
require(
REGISTRY.policyExists(newPolicyId),
"Policy not found"
);
policyId = newPolicyId;
}
function _beforeTokenTransfer(
address from,
address to
) internal view {
if (policyId == 0) revert("Transfers disabled");
if (policyId == 1) return; // No restrictions
// Check sender authorization
require(
REGISTRY.isAuthorizedSender(policyId, from),
"Sender not authorized"
);
// Check recipient authorization
require(
REGISTRY.isAuthorizedRecipient(policyId, to),
"Recipient not authorized"
);
}
}
Gas Costs
| Operation | Cold | Warm |
|---|
createPolicy | ~45,000 gas | ~25,000 gas |
createPolicyWithAccounts | +5,000 gas per account | +2,500 gas per account |
createCompoundPolicy | ~30,000 gas | ~15,000 gas |
modifyPolicyWhitelist | ~25,000 gas | ~10,000 gas |
modifyPolicyBlacklist | ~25,000 gas | ~10,000 gas |
isAuthorized | ~3,500 gas | ~400 gas |
isAuthorizedSender | ~3,500 gas | ~400 gas |
Storage Layout
// Slot 0: Policy ID counter (starts at 2)
uint64 policy_id_counter;
// Slot 1: Policy records
mapping(uint64 => PolicyRecord) policy_records;
// PolicyRecord struct (packed):
struct PolicyRecord {
PolicyData base; // policy_type (u8) + admin (20 bytes)
CompoundPolicyData compound; // Only for COMPOUND type
}
// Slot 2: Address sets (whitelist/blacklist)
mapping(uint64 => mapping(address => bool)) policy_set;
Use Cases
KYC/AML Compliance
// Create KYC whitelist
uint64 kycPolicy = REGISTRY.createPolicy(
complianceOfficer,
ITIP403Registry.PolicyType.WHITELIST
);
// Add KYC'd users
for (address user in kycUsers) {
REGISTRY.modifyPolicyWhitelist(kycPolicy, user, true);
}
// Set on token
token.setPolicy(kycPolicy);
Sanctions Screening
// Create sanctions blacklist
uint64 sanctionsPolicy = REGISTRY.createPolicyWithAccounts(
complianceOfficer,
ITIP403Registry.PolicyType.BLACKLIST,
sanctionedAddresses
);
// Set on token
token.setPolicy(sanctionsPolicy);
Vendor Credits (T2+)
// Create compound policy for vendor credits:
// - Anyone can send (pay vendor)
// - Only vendor can receive transfers
// - Anyone can receive mints (get credits)
uint64 vendorPolicy = REGISTRY.createCompoundPolicy(
1, // anyone can send
vendorWhitelist, // only vendor receives transfers
1 // anyone receives mints
);
token.setPolicy(vendorPolicy);
Best Practices
Policy Immutability
// ✅ Good: Plan for policy evolution
uint64 kycPolicyV1 = createPolicy(...);
uint64 kycPolicyV2 = createPolicy(...); // Updated rules
// Migrate token
token.setPolicy(kycPolicyV2);
// ❌ Bad: Can't delete policies
// Policies remain in storage forever
Admin Management
// ✅ Good: Use multi-sig for admin
address admin = multiSigWallet;
// ✅ Good: Transfer admin to DAO
REGISTRY.setPolicyAdmin(policyId, daoAddress);
// ❌ Bad: Single EOA admin (key compromise risk)
address admin = myEOA;
Authorization Caching
// ✅ Good: Cache authorization checks
mapping(address => uint256) lastChecked;
function isAuthorizedCached(address user) public returns (bool) {
if (block.timestamp - lastChecked[user] < 1 hours) {
return cachedResult[user];
}
bool auth = REGISTRY.isAuthorized(policyId, user);
cachedResult[user] = auth;
lastChecked[user] = block.timestamp;
return auth;
}
Security Considerations
- Admin Compromise: Policy admin can modify lists; use multi-sig
- Policy Immutability: Policies can’t be deleted; plan versioning
- Gas Costs: Large policy updates can be expensive; batch operations
- Compound Policies: Are immutable (no admin); choose sub-policies carefully
See Also