Inspecting tokens
Before you register a token, grant a pool its roles, or debug why a transfer reverts, you need to read the token contract itself: what type and version it is, which accounts hold its mint and burn roles, and how many decimals it uses. This page covers the token reads the SDK exposes and what a healthy value looks like for each.
The role reads live on EVMTokenManager; type, version, and metadata are chain-level reads, reached through cct.chain or a standalone EVMChain. See Inspecting the registry for the registry side and the EVM walkthrough for the full setup flow.
import { EVMChain } from '@chainlink/ccip-sdk'
import { EVMTokenManager } from '@chainlink/ccip-sdk/cct/evm'
const chain = await EVMChain.fromUrl(process.env.RPC_URL!)
const cct = EVMTokenManager.fromChain(chain)
const tokenAddress = '0xYourToken...'
Type and version
chain.typeAndVersion reads a contract's on-chain typeAndVersion() and returns a tuple: the parsed type, the parsed version, the original string, and an optional suffix.
const [type, version] = await chain.typeAndVersion(tokenAddress)
console.log(type, version) // e.g. "CrossChainToken" "2.0.0"
The SDK deploys CrossChainToken at version 2.0.0, an OpenZeppelin AccessControl token. Tokens from the older factory report a FactoryBurnMintERC20 / BurnMintERC677 family type at 1.5.1 or 1.6.2, and those govern roles through an owner rather than AccessControl. A v1.5.1 token predates typeAndVersion() and may not answer this call at all, so treat a read failure on an old token as "assume v1.x" rather than as a hard error. The type and version decide which role model applies below.
Mint and burn roles
For a burn/mint token, the pool must hold both the mint and the burn role or transfers into and out of the chain will revert. Two shapes of read answer different questions: check one account, or list every holder.
Check one account
isMinter and isBurner answer whether a single account holds the role. Pass the pool address as account to confirm the pool is authorized. Both handle v1 and v2 tokens: they read the v1 isMinter(address) / isBurner(address) predicate on a factory token, and the AccessControl hasRole(MINTER_ROLE, account) / hasRole(BURNER_ROLE, account) on a CrossChainToken.
const poolAddress = '0xYourPool...'
const canMint = await cct.isMinter({ tokenAddress, account: poolAddress })
const canBurn = await cct.isBurner({ tokenAddress, account: poolAddress })
if (!canMint || !canBurn) {
console.log('pool is missing a role; run grantMintAndBurnRoles')
}
For a healthy burn/mint setup, both return true for the registered pool. If either is false, the pool cannot move the token; grant the roles with grantMintAndBurnRoles. A zero-address account is rejected as a bad argument, since the token can never grant a role to it.
List every holder
getMinters and getBurners enumerate the full role set, checksummed, in the token's own order. Use them for an audit or a UI, and isMinter / isBurner for a single yes/no check.
const minters = await cct.getMinters({ tokenAddress })
const burners = await cct.getBurners({ tokenAddress })
These enumerate only on the v1 FactoryBurnMintERC20 family (v1.5.1 / v1.6.2). A CrossChainToken (v2.0.0) does not expose a role holder list, so calling these against one throws CCTContractTypeInvalidError. For a v2 token, check specific accounts with isMinter / isBurner instead.
Decimals and metadata
chain.getTokenInfo returns the token's symbol, decimals, and an optional name.
const info = await chain.getTokenInfo(tokenAddress)
console.log(info.symbol, info.decimals, info.name)
Decimals matter for a cross-chain lane: the pool's localTokenDecimals at deploy time must match the token's real decimals, and the SDK scales amounts between a source and destination token whose decimals differ. A correct value is the same decimal count you passed when deploying the pool.
Ownership reads
EVMTokenManager exposes a getter for each token authority, so you no longer need to reach for a raw ethers Contract.
getTokenOwner reads the token's Ownable2Step owner(), checksummed. It works on every supported version: a v1 factory token's owner, or the DEFAULT_ADMIN_ROLE holder that owner() aliases on a CrossChainToken. On the v1 factory family the owner is also the mint/burn role admin, so that one address both administers roles and can grant them.
const owner = await cct.getTokenOwner({ tokenAddress })
getCCIPAdmin reads the token's getCCIPAdmin(), checksummed. This is a single-step authority: there is no pending CCIP admin, so the returned address is the full state. It is the value the ccip-admin registration method authorizes against.
const ccipAdmin = await cct.getCCIPAdmin({ tokenAddress })
getTokenDefaultAdmin reads a CrossChainToken's AccessControl default admin, returning the current defaultAdmin and any scheduled pendingDefaultAdmin in one result. Ownership is two-step on v2, so pendingDefaultAdmin reports the address that beginDefaultAdminTransfer proposed together with the schedule, a Unix timestamp (bigint) at which that address may accept. The field is omitted when no transfer is pending, so test 'pendingDefaultAdmin' in result rather than comparing against the zero address. A v1 factory token has no default admin; read its owner with getTokenOwner instead.
const result = await cct.getTokenDefaultAdmin({ tokenAddress })
if ('pendingDefaultAdmin' in result) {
const { newAdmin, schedule } = result.pendingDefaultAdmin
console.log('pending admin', newAdmin, 'accepts at', schedule)
}
A token's proposed owner is not readable on EVM. Both a v1 token's Ownable2Step pending owner and a token pool's pending owner live in a private slot with no getter, so getTokenOwner reports the current owner only. The v2 default admin is the exception, since its pending transfer is public and surfaced by getTokenDefaultAdmin.
Reading tokens on Solana
Solana cross-chain tokens are SPL mints, so the role model is different: there is no isMinter / getMinters equivalent. A burn/mint pool's authority is the mint's SPL mintAuthority, which points at the pool program's PDA rather than at a role list on the token. Read the mint account (for example with @solana/spl-token's getMint) to confirm the authority, and use SolanaTokenManager.getTokenPoolState for the pool side.
Related
- Inspecting the registry: read the token admin registry entry
- Verify your setup: assert registry, pool, and roles agree for a new CCT
- Inspecting pools: read deployed pool configuration and rate limits
- EVMTokenManager API reference