Chainlink CCIP (Hedera)
CCT burn-and-mint pattern: deploy a burn-mint token + token pool on each chain, register the token with CCIP, wire remote pools with chain selectors, then ccipSend paying native fees.
BurnMint token + TokenPool (source)
→ Router.ccipSend(destSelector, message)
→ TokenPool lockOrBurn / releaseOrMint (dest)
On Hedera you can use a vanilla ERC-20 CCT or an HTS-backed wrapper. CCIP always registers the ERC-20 the pool sees — for HTS that is the wrapper, while users hold the native HTS token.
This skill is cross-chain tokens/messages, not Chainlink Data Feeds.
Quick Reference
| Piece | Role |
|---|---|
| Router | getFee + ccipSend{value} |
| Chain selector | uint64 CCIP id — not EVM chain ID |
| Token + pool | Burn-mint ERC-20 + BurnMintTokenPool (or Hedera HTS variants) |
| Admin registry | RegistryModuleOwnerCustom → TokenAdminRegistry.acceptAdminRole → setPool |
| Lane config | TokenPool.applyChainUpdates both directions |
| Fee | Native gas (feeToken = address(0)); attach msg.value |
| Receiver | abi.encode(address) in EVM2AnyMessage.receiver |
See references/hedera-ccip.md for where to source selectors and registry addresses. See references/examples.md for register / wire / send sketches.
Critical: Selectors, Not Chain IDs
ccipSend and pool remoteChainSelector take CCIP chain selectors. Hedera testnet (296) and Sepolia (11155111) have different selector values than their EVM chain IDs. Look them up in the CCIP directory; do not pass 296 as a selector.
Do not reuse Axelar GMP names or LayerZero EIDs here.
Critical: Register Then Wire Both Lanes
Incomplete registry/pool config bricks the token. Per chain:
- Deploy token + pool
- Grant the pool mint/burn roles on the token
registerAdminViaGetCCIPAdmin(token)thenacceptAdminRole(token)TokenAdminRegistry.setPool(token, pool)- After both chains have token+pool addresses:
applyChainUpdateson each pool (remote selector, remote pool, remote token)
Then send a small test amount and track it on CCIP Explorer.
Critical: Hedera HTS-Backed CCT
Vanilla path: BurnMintERC20 + BurnMintTokenPool on Hedera EVM, same as Sepolia.
HTS-backed path:
- Users hold a native HTS token created via precompile
0x167 - CCIP registers the wrapper (
HtsBurnMintERC20), not the HTS token id/address users see in wallets - Pool (
HtsBurnMintTokenPool) must associate itself with the native HTS token and approve the wrapper - Hedera → remote send needs two approvals: HTS → wrapper, wrapper → Router
- Receiver accounts must associate the native HTS token before inbound mints
- Mint/burn/transfer amounts must fit HTS
int64
Do not mix Axelar or LayerZero token addresses into CCIP env/config.
Send Flow (native fee)
Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({
receiver: abi.encode(receiver),
data: "",
tokenAmounts: tokenAmounts, // token is the CCIP-registered ERC-20 / wrapper
feeToken: address(0),
extraArgs: Client._argsToBytes(
Client.EVMExtraArgsV2({ gasLimit: 0, allowOutOfOrderExecution: true })
)
});
uint256 fee = IRouterClient(router).getFee(destinationChainSelector, message);
IERC20(tokenToSend).approve(router, amount);
IRouterClient(router).ccipSend{ value: fee }(destinationChainSelector, message);
Checklist
- Chain selectors from the CCIP directory (not EVM chain IDs, Axelar names, or LZ EIDs)
- Token admin registered and pool set on both chains
-
applyChainUpdateswired both directions before sending - Native fee attached to
ccipSend(feeToken = address(0)) - HTS path: wrapper registered with CCIP; users hold native HTS; dual approve + associate
- Amounts fit
int64when touching HTS mint/burn - CCIP token/pool addresses not copied from Axelar/LayerZero configs
References
- references/examples.md — register, wire, send, HTS dual-approve sketches
- references/hedera-ccip.md — selectors vs chain IDs, where to source router/registry
- CCIP directory
- CCIP burn-and-mint CCT
- CCIP Explorer