Overview
Solana CCTP programs are written in Rust and leverage the Anchor framework. The Solana CCTP protocol implementation is split into two programs:MessageTransmitterV2 and TokenMessengerMinterV2. TokenMessengerMinterV2
encapsulates the capabilities of both TokenMessengerV2 and TokenMinterV2
contracts on EVM chains. To ensure alignment with EVM contracts’ logic and
state, and to facilitate upgrades and maintenance, the code and state of Solana
programs reflect the EVM counterparts as closely as possible.
Mainnet program addresses
| Program | Domain | Address |
|---|---|---|
MessageTransmitterV2 | 5 | CCTPV2Sm4AdWt5296sk4P66VBZ7bEhcARwFaaS9YPbeC |
TokenMessengerMinterV2 | 5 | CCTPV2vPZJS2u2BBsUoscuikbYjnpFmbFsvVuJdgUMQe |
Devnet program addresses
| Program | Domain | Address |
|---|---|---|
MessageTransmitterV2 | 5 | CCTPV2Sm4AdWt5296sk4P66VBZ7bEhcARwFaaS9YPbeC |
TokenMessengerMinterV2 | 5 | CCTPV2vPZJS2u2BBsUoscuikbYjnpFmbFsvVuJdgUMQe |
CCTP interface
The interface below serves as a reference for permissionless messaging functions exposed by theTokenMessengerMinter and MessageTransmitter programs. The
full IDLs can be found onchain using a block explorer.
MessageTransmitterV2 IDL
and
TokenMessengerMinterV2 IDL.
See the instruction rust files or quick-start for PDA information.
TokenMessengerMinterV2
depositForBurn
Deposits and burns tokens from sender to be minted on destination domain. Minted
tokens will be transferred to mintRecipient.
Parameters
| Field | Type | Description |
|---|---|---|
amount | u64 | Amount of tokens to deposit and burn. |
destinationDomain | u32 | Destination domain identifier. |
mintRecipient | Pubkey | Public Key of token account mint recipient on destination domain. Address should be the 32 byte version of the hex address in base58. See Additional Notes on mintRecipient section for more information. |
destinationCaller | Pubkey | Address which can call receiveMessage on destination domain. If set to PublicKey.default, any address can call receiveMessage Address should be the 32 byte version of the hex address in base58. See Additional Notes on mintRecipient section for more information. |
maxFee | u64 | Max fee paid for the transfer, specified in units of the burn token. |
minFinalityThreshold | u32 | Minimum finality threshold at which burn will be attested |
FeesA fee may be charged for standard USDC transfers. Fees for standard transfers
are set to 0, but are subject to change. See
CCTP Fees for more information.MessageSent event storageTo ensure persistent and reliable message storage, MessageSent events are stored
in accounts. MessageSent event accounts are generated client-side, passed into
the instruction call, and assigned to have the
MessageTransmitterV2 program as
the owner. See the
Quickstart Guide
for how to generate this account and pass it to the instruction call.Message nonces are generated offchain, meaning the source messages cannot be
identified from the attestation. Due to this, there is a 5 day window after
sending a message that callers must wait before reclaim_event_account can be
called. This is to ensure that the message has been fully processed by Circle’s
offchain services.depositForBurnWithHook
Deposits and burns tokens from sender to be minted on destination domain, and emits a crosschain message with additional hook data appended. In addition to the standarddeposit_for_burn parameters, deposit_for_burn_with_hook accepts
a dynamic-length hookData parameter, allowing the caller to include additional
metadata to the attested message, which can be used to trigger custom logic on
the destination chain.
Parameters
| Field | Type | Description |
|---|---|---|
amount | u64 | Amount of tokens to deposit and burn. |
destinationDomain | u32 | Destination domain identifier. |
mintRecipient | Pubkey | Public Key of token account mint recipient on destination domain. Address should be the 32 byte version of the hex address in base58. See Additional Notes on mintRecipient section for more information. |
destinationCaller | Pubkey | Address which can call receiveMessage on destination domain. If set to PublicKey.default, any address can call receiveMessage Address should be the 32 byte version of the hex address in base58. See Additional Notes on mintRecipient section for more information. |
maxFee | u64 | Max fee paid for fast burn, specified in units of the burn token. |
minFinalityThreshold | u32 | Minimum finality threshold at which burn will be attested |
hookData | Vec<u8> | Additional metadata attached to the attested message, which can be used to trigger custom logic on the destination chain |
handleReceiveFinalizedMessage
Handles incoming message received by the local MessageTransmitter, and takes the appropriate action. For a burn message, mints the associated token to the requested recipient on the local domain. Validates the function sender is the local MessageTransmitter, and the remote sender is a registered remote TokenMessenger forremoteDomain.
Additionally, reads the feeExecuted parameter from the BurnMessage. If
nonzero, the feeExecuted amount is minted to the feeRecipient.
Parameters
| Field | Type | Description |
|---|---|---|
remoteDomain | u32 | The domain where the message originated from |
sender | Pubkey | The sender of the message (remote TokenMessenger) |
finalityThresholdExecuted | u32 | Specifies the level of finality Iris signed the message with |
messageBody | Vec<u8> (dynamic length) | The message body bytes |
handleReceiveUnfinalizedMessage
Handles incoming message received by the local MessageTransmitter, and takes the appropriate action. For a burn message, mints the associated token to the requested recipient on the local domain. Validates the function sender is the local MessageTransmitter, and the remote sender is a registered remote TokenMessenger forremoteDomain.
Similar to handleReceiveFinalizedMessage, but is called for messages which are
not finalized (finalityThresholdExecuted < 2000).
Unlike handleReceiveFinalizedMessage, handleReceiveUnfinalizedMessage has
the following messageBody parameter:
expirationBlock. IfexpirationBlock≤blockNumberon the destination domain, the message will revert and must be re-signed without the expiration block.
| Field | Type | Description |
|---|---|---|
remoteDomain | u32 | The domain where the message originated from |
sender | Pubkey | The sender of the message (remote TokenMessenger) |
finalityThresholdExecuted | u32 | Specifies the level of finality Iris signed the message with |
messageBody | Vec<u8> (dynamic length) | The message body bytes (see Message format) |
MessageTransmitterV2
receiveMessage
Messages with a given nonce can only be broadcast successfully once for a pair of domains. The message body of a valid message is passed to the specified recipient for further processing. Parameters| Field | Type | Description |
|---|---|---|
message | Vec<u8> | Message bytes. |
attestation | Vec<u8> | Signed attestation of message. |
receiveMessage instruction is being called with a deposit for burn
message that will be received by the TokenMessengerMinterV2, additional
remainingAccounts are required so they can be passed with the CPI to
TokenMessengerMinter#handle_receive_finalized_message or
TokenMessengerMinter#handle_receive_unfinalized_message:
| Account Name | PDA Seeds | PDA ProgramId | isSigner? | isWritable? | Description |
|---|---|---|---|---|---|
token_messenger | ["token_messenger"] | tokenMessengerMinter | false | false | TokenMessenger Program Account |
remote_token_messenger | ["remote_token_messenger", sourceDomainId] | tokenMessengerMinter | false | false | Remote token messenger account where the remote token messenger address is stored for the given source domain id |
token_minter | ["token_minter"] | tokenMessengerMinter | false | true | TokenMinter Program Account |
local_token | ["local_token", localTokenMint.publicKey] | tokenMessengerMinter | false | true | Local token account where the information for the local token (e.g. USDCSOL) being minted is stored |
token_pair | ["token_pair", sourceDomainId, sourceTokenInBase58] | tokenMessengerMinter | false | false | Token pair account where the info for the local and remote tokens are stored. sourceTokenInBase58 is the remote token that was burned and converted into base58 format. |
user_token_account | N/A | N/A | false | true | User token account that will receive the minted tokens. This address must match the mintRecipient from the source chain depositForBurn call. |
custody_token_account | ["custody", localTokenMint.publicKey] | tokenMessengerMinter | false | true | Custody account that holds the pre-minted USDCSOL that can be minted for CCTP usage. |
SPL.token_program_id | N/A | N/A | false | false | The native SPL token program ID. |
token_program_event_authority | ["__event_authority"] | tokenMessengerMinter | false | false | Event authority account for the TokenMessengerMinter program. Needed to emit Anchor CPI events. |
program | N/A | N/A | false | false | Program id for the TokenMessengerMinter program. |
sendMessage
Sends a message to the destination domain and recipient. Stores message in aMessageSent account which will be attested by Circle’s attestation service.
Parameters
| Field | Type | Description |
|---|---|---|
destinationDomain | u32 | Destination domain identifier. |
recipient | Pubkey | Address to handle message body on destination domain. |
messageBody | Vec<u8> | App-specific message to be handled by recipient. |
Additional Notes
These notes are applicable to all CCTP versions.Mint Recipient for Solana as Destination Chain Transfers
When callingdepositForBurn on a non-Solana chain with Solana as the
destination, the mintRecipient should be a hex encoded USDC token account
address. The token account* must exist at the time receiveMessage is called
on Solana* or else this instruction will revert. An example of converting an
address from Base58 to hex taken from the Solana quickstart tutorial in
TypeScript can be seen below:
TypeScript
Mint Recipient for Solana as Source Chain Transfers
When specifying themintRecipient for Solana deposit_for_burn instruction
calls, the address must be given as the 32 byte version of the hex address in
base58 format. An example taken from the Solana quickstart tutorial in
TypeScript can be seen below:
TypeScript
Program Events
Program events like DepositForBurn , MintAndWithdraw , and MessageReceived are emitted as Anchor CPI events. This means a self-CPI is made into the program with the serialized event as instruction data so it is persisted in the transaction and can be fetched later on as needed. More information can be seen in the Anchor implementation PR, and an example of reading CPI events can be seen in thesolana-cctp-contracts repository.
MessageSent
events are different, as they are stored in accounts. See the
MessageSent Event Storage section for more info.