Manage Your Stablecoin
Configure your stablecoin's permissions, supply limits, and compliance policies after deployment. This guide covers granting roles to manage token operations, setting supply caps, configuring transfer policies, and controlling token transfers through pause/unpause functionality.
TIP-20 tokens use a role-based access control system that allows you to delegate different administrative functions to different addresses. For detailed information about the role system, see the TIP-20 specification.
Steps
In this guide, we'll walk through how to assign and check the issuer role, but the process is identical for other roles like pause, unpause, burnBlocked, and defaultAdmin.
Setup
Before you can manage roles on your stablecoin, you need to create one. Follow the Create a Stablecoin guide to deploy your token.
Once you've created your token, you can proceed to grant roles to specific addresses.
Grant Roles to an Address
Assign roles to specific addresses to delegate token management capabilities.
Grant Roles to an Address
demoimport React from 'react'
import { Hooks } from 'tempo.ts/wagmi'
import { useQueryClient } from '@tanstack/react-query'
export function GrantRoles() {
const queryClient = useQueryClient()
const tokenAddress = '0x...' // Your token address
const treasuryAddress = '0x...' // Address to grant the issuer role
const grant = Hooks.token.useGrantRolesSync({})
const handleGrantIssuer = async () => {
await grant.mutate({
token: tokenAddress,
roles: ['issuer'],
to: treasuryAddress,
})
}
return (
<button
disabled={grant.isPending}
onClick={handleGrantIssuer}
type="button"
>
{grant.isPending ? 'Granting...' : 'Grant Issuer Role'}
</button> {}
)
}Check if an Address Has a Role
Use hasRole to verify whether an address has been granted a specific role.
import React from 'react'
import { Hooks } from 'tempo.ts/wagmi'
import { useQueryClient } from '@tanstack/react-query'
export function GrantRoles() {
const queryClient = useQueryClient()
const tokenAddress = '0x...' // Your token address
const treasuryAddress = '0x...' // Address to grant the issuer role
// Grant the issuer role
const grant = Hooks.token.useGrantRolesSync({
mutation: {
onSettled() {
queryClient.refetchQueries({ queryKey: ['hasRole'] })
},
},
})
const handleGrantIssuer = async () => {
await grant.mutate({
token: tokenAddress,
roles: ['issuer'],
to: treasuryAddress,
})
}
const { data: hasIssuerRole } = Hooks.token.useHasRole({
account: treasuryAddress,
token: tokenAddress,
role: 'issuer',
})
return (
<div>
<button
disabled={grant.isPending}
onClick={handleGrantIssuer}
type="button"
>
{grant.isPending ? 'Granting...' : 'Grant Issuer Role'}
</button>
{hasIssuerRole !== undefined && (
<div>
Treasury {hasIssuerRole ? 'has' : 'does not have'} the issuer role
</div> {}
)}
</div>
)
}Revoke the Issuer Role
Revoke roles from addresses when you need to remove their permissions.
Revoke Issuer Role
demoimport React from 'react'
import { Hooks } from 'tempo.ts/wagmi'
import { useQueryClient } from '@tanstack/react-query'
export function RevokeRoles() {
const queryClient = useQueryClient()
const tokenAddress = '0x...' // Your token address
const treasuryAddress = '0x...' // Address to grant/revoke the issuer role
// Grant the issuer role
const grant = Hooks.token.useGrantRolesSync({
mutation: {
onSettled() {
queryClient.refetchQueries({ queryKey: ['hasRole'] })
},
},
})
const handleGrantIssuer = async () => {
await grant.mutate({
token: tokenAddress,
roles: ['issuer'],
to: treasuryAddress,
})
}
// Check if the treasury has the issuer role
const { data: hasIssuerRole } = Hooks.token.useHasRole({
account: treasuryAddress,
token: tokenAddress,
role: 'issuer',
})
// Revoke the issuer role
const revoke = Hooks.token.useRevokeRolesSync({
mutation: {
onSettled() {
queryClient.refetchQueries({ queryKey: ['hasRole'] })
},
},
})
const handleRevokeIssuer = async () => {
await revoke.mutate({
token: tokenAddress,
roles: ['issuer'],
from: treasuryAddress,
})
}
return (
<div>
<button
disabled={grant.isPending}
onClick={handleGrantIssuer}
type="button"
>
{grant.isPending ? 'Granting...' : 'Grant Issuer Role'}
</button>
<button
disabled={revoke.isPending || !hasIssuerRole}
onClick={handleRevokeIssuer}
type="button"
>
{revoke.isPending ? 'Revoking...' : 'Revoke Issuer Role'}
</button>
{hasIssuerRole !== undefined && (
<div>
Treasury {hasIssuerRole ? 'has' : 'does not have'} the issuer role
</div>
)}
</div>
)
}Recipes
Set Supply Cap
Limit the maximum total supply of your token. Setting supply caps requires the DEFAULT_ADMIN_ROLE. The new cap cannot be less than the current total supply.
Set Supply Cap
demoimport React from 'react'
import { Hooks } from 'tempo.ts/wagmi'
import { parseUnits } from 'viem'
export function SetSupplyCap() {
const tokenAddress = '0x...' // Your token address
const { data: metadata, refetch: refetchMetadata } =
Hooks.token.useGetMetadata({ token: tokenAddress })
const setSupplyCap = Hooks.token.useSetSupplyCapSync({
mutation: { onSettled: () => refetchMetadata() },
})
const handleSetSupplyCap = () => {
setSupplyCap.mutate({
token: tokenAddress,
supplyCap: parseUnits('1000', metadata?.decimals || 6),
})
}
return (
<button
disabled={setSupplyCap.isPending}
onClick={handleSetSupplyCap}
type="button"
>
{setSupplyCap.isPending ? 'Setting...' : 'Set Cap'}
</button> {}
)
}Configure Transfer Policies
Control who can send and receive your stablecoin for compliance and regulatory requirements. Setting transfer policies requires the DEFAULT_ADMIN_ROLE.
Transfer policies can be:
- Always allow: Anyone can send/receive (default)
- Always reject: Nobody can send/receive
- Whitelist: Only authorized addresses can send/receive
- Blacklist: Blocked addresses cannot send/receive
Learn more about configuring transfer policies in the TIP-403 specification.
Create and Link Transfer Policy
demoimport React from 'react'
import { Hooks } from 'tempo.ts/wagmi'
export function CreateTokenPolicy() {
const tokenAddress = '0x...' // Your token address
const createPolicy = Hooks.policy.useCreateSync({
mutation: {
onSuccess(result) {
// Store policyId for the next step
console.log('Policy ID:', result.policyId)
},
},
})
const handleCreatePolicy = async () => {
await createPolicy.mutateAsync({
addresses: [
'0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb',
],
type: 'blacklist',
})
}
return (
<button
disabled={createPolicy.isPending}
onClick={handleCreatePolicy}
type="button"
>
{createPolicy.isPending ? 'Creating...' : 'Create Policy'}
</button>
)
}Pause and Unpause Token Transfers
Temporarily halt all token transfers during emergency situations or maintenance windows. Pausing transfers requires the PAUSE_ROLE. Unpausing transfers requires the UNPAUSE_ROLE.
Pause and Unpause Your Token
demoimport React from 'react'
import { Hooks } from 'tempo.ts/wagmi'
export function PauseUnpauseTransfers() {
const tokenAddress = '0x...' // Your token address
const { data: metadata, refetch: refetchMetadata } =
Hooks.token.useGetMetadata({ token: tokenAddress })
const pause = Hooks.token.usePauseSync({
mutation: { onSettled: () => refetchMetadata() },
})
const unpause = Hooks.token.useUnpauseSync({
mutation: { onSettled: () => refetchMetadata() },
})
const paused = metadata?.paused || false
const handleToggle = () => {
if (paused) {
unpause.mutate({
token: tokenAddress,
})
} else {
pause.mutate({
token: tokenAddress,
})
}
}
const isProcessing = pause.isPending || unpause.isPending
return (
<button
disabled={isProcessing}
onClick={handleToggle}
type="button"
>
{isProcessing ? 'Processing...' : paused ? 'Unpause' : 'Pause'}
</button>
)
}Using the Burn Blocked Role
The Burn Blocked role allows your team to burn tokens from blocked or frozen addresses. This is useful for regulatory compliance when you need to remove tokens from addresses that violate terms of service or legal requirements.
Create and Link Transfer Policy
demoimport React from 'react'
import { Hooks } from 'tempo.ts/wagmi'
import { parseUnits } from 'viem'
export function BurnBlocked() {
const tokenAddress = '0x...' // Your token address
const blockedAddress = '0x...' // The blocked address to burn tokens from
const { data: metadata } = Hooks.token.useGetMetadata({
token: tokenAddress,
})
const burnBlocked = Hooks.token.useBurnBlockedSync()
const handleBurnBlocked = async () => {
if (!metadata) return
await burnBlocked.mutateAsync({
token: tokenAddress,
from: blockedAddress,
amount: parseUnits('100', metadata.decimals),
})
}
return (
<button
disabled={burnBlocked.isPending}
onClick={handleBurnBlocked}
type="button"
>
{burnBlocked.isPending ? 'Burning...' : 'Burn Blocked Tokens'}
</button>
)
}Best Practices
Role Separation
Use different addresses for different roles to enhance security. For example, assign the issuer role to your treasury address for minting, and the pause role to your security team for emergency controls.
Event Monitoring
Monitor onchain events for role changes, mints, burns, and administrative actions to maintain visibility into token operations and detect unauthorized activities.
Emergency Procedures
Ensure pause and unpause roles are assigned to trusted addresses and that your team has documented procedures for responding to security incidents requiring token transfers to be halted.