Inspecting token pools
Every cross-chain token has a token pool: the contract that handles its lock/release or burn/mint mechanics, rate limiting, and cross-chain routing. This page shows how to read a deployed pool's configuration and rate-limit state with the SDK's RPC-based query methods.
These are chain-level read methods (chain.getTokenPoolConfig, chain.getTokenPoolRemotes) for inspecting any deployed pool. During CCT setup, the token managers expose their own read operations (cct.getTokenPoolState, cct.getTokenPoolRemotes, cct.getTokenAdminRegistry). See the EVM, Solana, and Canton walkthroughs.
Find the token pool address
Tokens are registered in a TokenAdminRegistry contract. To find a token's pool address, query the registry in two steps:
import { EVMChain } from '@chainlink/ccip-sdk'
const chain = await EVMChain.fromUrl('https://rpc.sepolia.org')
const router = '0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59'
// Step 1: Get the TokenAdminRegistry for this router
const registry = await chain.getTokenAdminRegistryFor(router)
// Step 2: Get the token's registry entry
const tokenConfig = await chain.getRegistryTokenConfig(registry, tokenAddress)
if (!tokenConfig.tokenPool) {
console.log('No pool configured for this token')
} else {
console.log('Token pool:', tokenConfig.tokenPool)
}
getTokenAdminRegistryFor accepts a Router, OnRamp, or OffRamp address. It auto-detects the contract type and resolves the registry.
Pool configuration
getTokenPoolConfig returns the pool's token, router, and version string:
const poolConfig = await chain.getTokenPoolConfig(poolAddress)
console.log('Token:', poolConfig.token)
console.log('Router:', poolConfig.router)
console.log('Type and version:', poolConfig.typeAndVersion)
// e.g. "BurnMintTokenPool 1.5.0", "LockReleaseTokenPool 2.0.0"
The typeAndVersion string identifies the pool type (BurnMint, LockRelease, USDC, etc.) and version. The SDK does not expose pool types as an enum, so parse the string if you need the pool type. See Pool types for the full set of recognized types and versions.
On v2.0+ pools, getTokenPoolConfig also returns finalityDepth (undefined on pre-v2.0 pools, 0 when Faster-Than-Finality is not enabled, >0 when enabled) and finalitySafe (true when FCR/safe finality is supported). See Faster-Than-Finality for the full FTF workflow.
Remote chain configuration
getTokenPoolRemotes returns how the pool connects to other chains: remote token addresses, remote pool addresses, and rate limiter state.
Query all remote chains
const remotes = await chain.getTokenPoolRemotes(poolAddress)
// Record<string, TokenPoolRemote> — keys are CCIP network names (e.g. "ethereum-mainnet")
for (const [chainName, remote] of Object.entries(remotes)) {
console.log(`Chain ${chainName}:`)
console.log(' Remote token:', remote.remoteToken)
console.log(' Remote pools:', remote.remotePools)
}
Query a specific remote chain
Pass a chain selector to filter to a single destination:
import { networkInfo } from '@chainlink/ccip-sdk'
const destSelector = networkInfo('avalanche-testnet-fuji').chainSelector
const remotes = await chain.getTokenPoolRemotes(poolAddress, destSelector)
const remote = Object.values(remotes)[0]
if (remote) {
console.log('Remote token:', remote.remoteToken)
console.log('Remote pools:', remote.remotePools)
}
Rate limits
Token pools use token-bucket rate limiters to cap throughput in each direction:
- Outbound: limits tokens leaving the chain (checked on source)
- Inbound: limits tokens entering the chain (checked on destination)
All bucket values (tokens, capacity, rate) are in the token's smallest unit, the same scale as a transfer amount. Divide by 10 ** decimals for whole tokens, so a rate of 167000000000000000000 on an 18-decimal token is 167 tokens per second.
Each rate limiter state reads null when rate limiting is disabled for that direction, so check for null before reading its fields.
On pools reporting typeAndVersion 1.5.0, 1.5.1, or 1.6.0, the inbound rate limiter meters a transfer in the source token's decimals instead of the local destination decimals. On a lane between tokens of different decimals, this makes the effective inbound cap wrong by the decimal delta. An 18-to-6 decimal lane debits the inbound bucket 1e12 times too much, so the limit throttles far sooner than configured in that direction and barely at all in reverse. On 1.5.0 the release also mints the raw source amount, so mixed decimals are unsupported outright.
The fix ships in v1.6.1 and every later version, where the inbound bucket is metered on the rescaled destination amount. To check exposure, read the pool's typeAndVersion and whether the lane connects tokens of different decimals. To mitigate, upgrade the pool to v1.6.1 or later, use the same decimals for the token on both chains, or size the inbound capacity and rate in the source token's decimals to compensate.
Check rate limit status
const remotes = await chain.getTokenPoolRemotes(poolAddress, destSelector)
const remote = Object.values(remotes)[0]
if (remote) {
// Outbound rate limit (null if disabled). Values are in the token's smallest unit.
if (remote.outboundRateLimiterState) {
const { tokens, capacity, rate } = remote.outboundRateLimiterState
console.log('Outbound rate limit:')
console.log(` Available: ${tokens} / ${capacity} (smallest unit)`)
console.log(` Refill rate: ${rate} per second (smallest unit)`)
} else {
console.log('Outbound rate limiting: disabled')
}
// Inbound rate limit (null if disabled). Values are in the token's smallest unit.
if (remote.inboundRateLimiterState) {
const { tokens, capacity, rate } = remote.inboundRateLimiterState
console.log('Inbound rate limit:')
console.log(` Available: ${tokens} / ${capacity} (smallest unit)`)
console.log(` Refill rate: ${rate} per second (smallest unit)`)
} else {
console.log('Inbound rate limiting: disabled')
}
}
RateLimiterState type
type RateLimiterState = {
tokens: bigint // Current tokens available in the bucket (smallest unit)
capacity: bigint // Maximum bucket capacity (smallest unit)
rate: bigint // Refill per second (smallest unit)
} | null // null = rate limiting disabled
FTF rate limits
TokenPool v2.0+ contracts may include separate rate limiters for Faster-Than-Finality transfers (fastOutboundRateLimiterState and fastInboundRateLimiterState). See Faster-Than-Finality rate limits for details and examples.
Complete example
Query full token pool information for a lane:
import { EVMChain, networkInfo } from '@chainlink/ccip-sdk'
async function inspectTokenPool(
rpcUrl: string,
routerAddress: string,
tokenAddress: string,
destNetworkName: string
) {
const chain = await EVMChain.fromUrl(rpcUrl)
const destSelector = networkInfo(destNetworkName).chainSelector
// 1. Find the pool
const registry = await chain.getTokenAdminRegistryFor(routerAddress)
const tokenConfig = await chain.getRegistryTokenConfig(registry, tokenAddress)
if (!tokenConfig.tokenPool) {
console.log('No pool configured for this token')
return
}
const poolAddress = tokenConfig.tokenPool
console.log('Pool:', poolAddress)
// 2. Get pool config
const poolConfig = await chain.getTokenPoolConfig(poolAddress)
console.log('Type:', poolConfig.typeAndVersion)
if (poolConfig.finalityDepth != null && poolConfig.finalityDepth > 0) {
console.log('FTF enabled — min confirmations:', poolConfig.finalityDepth)
}
// 3. Get remote configuration for the destination
const remotes = await chain.getTokenPoolRemotes(poolAddress, destSelector)
const remote = Object.values(remotes)[0]
if (!remote) {
console.log('No remote configuration for', destNetworkName)
return
}
console.log('\nRemote token:', remote.remoteToken)
console.log('Remote pools:', remote.remotePools)
// 4. Rate limits
if (remote.outboundRateLimiterState) {
const { tokens, capacity, rate } = remote.outboundRateLimiterState
const pct = Number((tokens * 100n) / capacity)
console.log(`\nOutbound: ${pct}% available (${tokens}/${capacity})`)
console.log(`Refill: ${rate} tokens/sec`)
}
if (remote.inboundRateLimiterState) {
const { tokens, capacity, rate } = remote.inboundRateLimiterState
const pct = Number((tokens * 100n) / capacity)
console.log(`\nInbound: ${pct}% available (${tokens}/${capacity})`)
console.log(`Refill: ${rate} tokens/sec`)
}
// 5. FTF rate limits (v2.0+ only)
if ('fastOutboundRateLimiterState' in remote) {
const ftfOut = remote.fastOutboundRateLimiterState
const ftfIn = remote.fastInboundRateLimiterState
if (ftfOut) {
console.log(`\nFTF outbound: ${ftfOut.tokens}/${ftfOut.capacity}`)
}
if (ftfIn) {
console.log(`FTF inbound: ${ftfIn.tokens}/${ftfIn.capacity}`)
}
}
}
// Usage
await inspectTokenPool(
'https://rpc.sepolia.org',
'0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59',
'0xTokenAddress...',
'avalanche-testnet-fuji'
)
Method reference
| Method | Called on | Purpose |
|---|---|---|
getTokenAdminRegistryFor(address) | Chain | Resolve TokenAdminRegistry from Router/OnRamp/OffRamp |
getRegistryTokenConfig(registry, token) | Chain | Get token's registry entry (pool address, admin) |
getTokenPoolConfig(pool) | Chain | Pool config: token, router, typeAndVersion, finalityDepth, finalitySafe |
getTokenPoolRemotes(pool, chainSelector?) | Chain | Remote chain config: remote token, pools, rate limits |
getSupportedTokens(registry) | Chain | List all tokens in the registry |
getTokenInfo(token) | Chain | Token metadata (symbol, decimals, name) |
Related
- Pool types: deployable and recognized pool types by version
- Querying Data: Token queries, lane features, and balances
- Sending Messages: Send token transfers
- Error Handling: Handle errors and retries