Overview
The CCTP V1 Sui smart contract implementation is written in Move. The Sui CCTP V1 implementation is split into two packages:MessageTransmitter and TokenMessengerMinter.
TokenMessengerMinter encapsulates the functionality of both TokenMessenger
and TokenMinter contracts on EVM chains. To ensure alignment with EVM
contracts logic and state, and to facilitate future upgrades and maintenance,
the code and state of the Sui packages reflect the EVM counterparts as closely
as possible.
There are a few key differences with Sui packages from EVM and other CCTP V1
implementations:
Receive Message Flow
Since the Move language does not have interfaces, themessage_transmitter::receive_message() function cannot call directly into the
receiver package (e.g. TokenMessenger for USDC transfers). The workaround for
this limitation is that callers of receive_message() must also atomically (in
the same
Programmable Transaction Block (PTB))
call into the receiver package’s handle_receive_message() function with a
Receipt struct, call stamp_receipt() with the StampReceiptTicket struct
returned from handle_receive_message(), and then pass the StampedReceipt
back into the message_transmitter::complete_receive_message() function to
complete the message and destroy the Receipt object. This flow ensures
atomicity and guarantees message receipt by the receiver packages. Please see
the interface and examples below for more information on this flow.
Interacting with TokenMessengerMinter from other Packages
On Sui, when a package is upgraded, the new version is deployed with a new package ID. This means if another package is directly calling a version-gated function, when the package is upgraded, the dependent packages must also be upgraded. To address this, all CCTP V1 functions that are intended to be called from a dependent package follow aTicket struct pattern. In this pattern, the
dependent package can call non version-gated create_ticket() functions with
the intended function parameters, including an Auth struct (used to uniquely
identify the package), and receive back a Ticket struct. This struct can then
be returned from the dependent package and used in a PTB to call the intended
CCTP V1 function. This allows integrators to securely integrate with CCTP V1
functions from their packages, and only have to update PTBs when CCTP V1
packages are upgraded rather than having to upgrade their packages as well. For
more information, see the functions below with the _with_package_auth suffix.
Testnet
Package IDs
Shared Object IDs
Branch with testnet
Automated Address Management:
github.com/circlefin/sui-cctp/tree/testnet.
Mainnet
Package IDs
Shared Object IDs
Branch with mainnet
Automated Address Management:
github.com/circlefin/sui-cctp/tree/mainnet.
Interface
The Sui CCTP V1 source code is available on GitHub. The interface below serves as a reference for permissionless messaging functions exposed by the programs.TokenMessengerMinter
deposit_for_burn
Burns passed in tokens from sender to be minted on destination domain. Minted tokens will be transferred tomint_recipient on the destination chain. The
deposit_for_burn interface and functionality is very similar to the EVM
implementation. The coins parameter is the key difference due to how passing
tokens around on Sui works. message_transmitter_state, deny_list, and
treasury parameters are all shared objects.
Remarks:
- Intended to be called directly by EOA (rather than a dependent package). The
initiating EOA will be the “owner” (e.g. message sender) of the message and
have the ability to call
replace_deposit_for_burn()to update themint_recipientordestination_caller. If the calling EOA is not trusted by the mint recipient or destination caller,deposit_for_burn_with_package_auth()should be called instead with the integrating package owning the message. - The generic type T is the coin’s one-time witness (OTW) type for the specific coin type to be burned.
BurnMessageandMessagestructs are returned, but it is not required to do anything with these structs; they are returned for convenience.
deposit_for_burn_with_package_auth
Same asdeposit_for_burn(), but intended to be called with an Auth struct
from a dependent package. The calling package will be the “owner” (e.g.
message_sender) of the message and have the ability to call
replace_deposit_for_burn_with_package_auth() to update the mint recipient or
destination caller. This would be similar to a wrapper contract on EVM chains
calling into TokenMessenger and being the message sender. Direct callers
(where EOAs are trusted and should be the owner) should use deposit_for_burn()
instead.
Remarks:
- This function uses a
DepositForBurnTicketstruct for parameters so that the calling package can callcreate_deposit_for_burn_ticket()(not version-gated) from their package with parameters, and calldeposit_for_burn_with_package_auth()(version-gated) from a PTB so packages don’t have to be updated during CCTP V1 package upgrades. DepositForBurnTicketalso requires anAuthparameter. This is required to securely assign a sender address associated with the calling contract to the message. Any struct that implements the drop trait can be used as an authenticator, but it is recommended to use a dedicatedAuthstruct. Calling contracts should be careful to not expose these structs to the public or else messages from their package could be replaced. An example can be found inTokenMessengerMinteron GitHub.- The returned structs -
BurnMessageandMessageboth have the copy ability. There is also no guarantee of execution ordering, so your package could create 5 DepositForBurnTickets in one transaction and they could be executed in any order depending on the PTB. Integrating packages should account for both of these scenarios.
deposit_for_burn_with_caller
Same asdeposit_for_burn but with an additional parameter,
destination_caller. This parameter specifies which address has permission to
call receive_message on the destination domain for the message.
Remarks:
- Intended to be called directly by EOA (rather than a dependent package). The
initiating EOA will be the “owner” (e.g. message sender) of the message and
have the ability to call
replace_deposit_for_burn()to update themint_recipientordestination_caller. If the calling EOA is not trusted by the mint recipient or destination caller,deposit_for_burn_with_caller_with_package_auth()should be called instead with the integrating package owning the message.
Destination Caller NotesIf the
destination_caller does not represent a valid address, then it will not
be possible to broadcast the message on the destination domain. This is an
advanced feature, and the standard deposit_for_burn should be preferred for
use cases where a specific destination caller is not required.Note: If destination is a non-Move chain, destination_caller address should
be converted to hex and passed in using the @0x123 address format.deposit_for_burn_with_caller_with_package_auth
The same asdeposit_for_burn_with_caller(), but intended to be called with an
Auth struct from a dependent package. The calling package will be the “owner”
(e.g. message_sender) of the message and have the ability to call
replace_deposit_for_burn_with_package_auth() to update the mint_recipient or
destination_caller. This would be similar to a wrapper contract on EVM chains
calling into TokenMessenger and being the message sender. Direct callers
(where EOAs are trusted and should be the owner) should use
deposit_for_burn_with_caller() instead.
Remarks:
- This function uses a
DepositForBurnWithCallerTicketstruct for parameters so that the calling package can callcreate_deposit_for_burn_with_caller_ticket()(not version-gated) from their package, and calldeposit_for_burn_with_caller_with_package_auth()(version-gated) from a PTB so dependent packages don’t have to be updated during upgrades. DepositForBurnWithCallerTicketalso requires anAuthparameter. This is required to securely assign a sender address associated with the calling contract to the message. Any struct that implements the drop trait can be used as an authenticator, but it is recommended to use a dedicated struct. Calling contracts should be careful to not expose these structs to the public or else messages from their package could be replaced. An example can be found inTokenMessengerMinteron GitHub.
Destination Caller NotesIf the
destination_caller does not represent a valid address, then it will not
be possible to broadcast the message on the destination domain. This is an
advanced feature, and the standard deposit_for_burn should be preferred for
use cases where a specific destination caller is not required.Note: If destination is a non-Move chain, destination_caller address should
be converted to hex and passed in using the @0x123 address format.replace_deposit_for_burn
Replace aBurnMessage to change the mint recipient and/or destination caller.
Allows the sender of a previous BurnMessage (created by deposit_for_burn or
deposit_for_burn_with_caller) to send a new BurnMessage to replace the
original.
Remarks:
- The new
BurnMessagewill reuse the amount and burn token of the original, without requiring a newCoin<T>deposit. - The resulting mint will supersede the original mint, as long as the original mint has not confirmed yet onchain.
- A valid attestation is required before calling this function.
- This is useful in situations where the user specified an incorrect address and has no way to safely mint the previously burned USDC.
replace_deposit_for_burn_with_package_auth
Same asreplace_deposit_for_burn(), but intended to be called when
deposit_for_burn_with_package_auth() or
deposit_for_burn_with_caller_with_package_auth() was called for the original
message where the calling package is the message sender.
Remarks:
- This function uses a
ReplaceDepositForBurnTicketstruct for parameters so that the calling package can callcreate_replace_deposit_for_burn_ticket()(not version-gated) from their package with parameters, and calldeposit_for_burn_with_package_auth()(version-gated) from a PTB so packages don’t have to be updated during CCTP V1 package upgrades.
handle_receive_message
Handles an incoming message fromMessageTransmitter, and mints USDC to the
recipient for valid messages. This function can only be called with a mutable
reference to a Receipt object, which can only be created via a call with a
valid message to the message_transmitter::receive_message() function.
state, mt_state, deny_list, and treasury parameters are all shared
objects.
Remarks:
- Returns a
StampReceiptTicketWithBurnMessagestruct that can be deconstructed in a dependent package (or in a PTB) viadeconstruct_stamp_receipt_ticket_with_burn_message(). This struct is returned so that dependent packages can associate theBurnMessageandStampReceiptTickettogether from a PTB call and guarantee thatstamp_receipt()was called. - This must be called in a single PTB after calling
receive_message()and before callingcomplete_receive_message(). See the Examples page for the entire flow of receiving a message.
MessageTransmitter
receive_message
Receives a message emitted from a source chain. Messages with a given nonce can only be received once for a (sourceDomain, destinationDomain) pair.
Remarks:
- This function returns a
ReceiptHot Potato struct after validating the attestation and marking the nonce as used. - In order to destroy the
Receiptand complete the message, in a single PTB,stamp_receipt()must be called with theReceiptand anAuthstruct (see message_transmitter_authenticator for an example of this), and thencomplete_receive_message()must be called with theStampedReceiptto emit theMessageReceivedevent and complete the message. - The receipt/stamp pattern is used to enforce atomicity and ensure the intended receiver contract is called.
- Intended to be called directly from an EOA when a package
destination_calleris not specified on the message. Please usereceive_message_with_package_auth()if a packagedestination_calleris specified.
receive_message_with_package_auth
Same asreceive_message(), except intended to be used by a dependent package
when a package is specified as destination_caller (rather than an EOA).
Remarks:
- This function is version-gated and should be called from a PTB to prevent breaking changes when an upgrade occurs.
- This function uses a
ReceiveMessageTicketfor parameters so that the calling package can callcreate_receive_message_ticket()(not version-gated) from their package with parameters, and callreceive_message_with_package_auth()(version-gated) from a PTB so packages don’t have to be upgraded during CCTP V1 package upgrades. ReceiveMessageTicketalso requires anAuthparameter. This is required whenever a package is assigned as adestination_caller.destination_calleraddress should be set to theAuthidentifier returned from theauth_caller_identifier()function with the package’sAuthstruct. Any struct that implements the drop trait can be used as an authenticator, but it is recommended to use a dedicatedAuthstruct. Calling contracts should be careful to not expose these structs to the public or else messages intended for their package could be received by others. An example can be found inTokenMessengerMinteron GitHub.
stamp_receipt
Stamps aReceipt struct after verifying the intended package acknowledged the
message (through the Auth struct) by returning a StampedReceipt struct that
can be used to complete the message via complete_receive_message().
Remarks:
- This function is version-gated and should be called from a PTB to prevent breaking changes in dependent packages when a CCTP V1 upgrade occurs.
create_stamp_receipt_ticket()is safe to be called directly from a package (not version-gated), and it’s returnedTicketstruct can be passed intostamp_receipt()in a PTB.
Auth Parameter NotesThis is required for the
MessageTransmitter module to approve a Receipt
prior to its deletion. Any struct that implements the drop trait can be used as
an authenticator, but it is recommended to use a dedicated Auth struct.
Calling contracts should be careful to not expose these Auth structs to the
public to avoid messages being wrongly stamped. An example implementation exists
in the token_messenger_minter::message_transmitter_authenticator module.complete_receive_message
Completes the message by emitting aMessageReceived event for a stamped
receipt and destroying the receipt. Cannot be called without a StampedReceipt
(returned from stamp_receipt()).
Parameters
send_message
Sends a message to the destination domain and recipient. The createdMessage
struct is returned, but it is not required to do anything with this struct, it
is returned for convenience.
Remarks:
- This function uses a
SendMessageTicketfor parameters so that the calling package can callcreate_send_message_ticket()(not version-gated) from their package with parameters, and callsend_message()(version-gated) from a PTB so packages don’t have to be updated during CCTP V1 package upgrades. - For USDC transfers, this function is called directly by the
TokenMessengerMinterpackage indeposit_for_burn(). SendMessageTicketalso requires anAuthparameter. This is required in order to assign asenderto the message. Any struct that implements the drop trait can be used as an authenticator, but it is recommended to use a dedicatedAuthstruct. Calling contracts should be careful to not expose these objects to the public or else their messages could be replaced. An example can be found inTokenMessengerMinteron GitHub.- The returned struct (
Message) has the copy ability. There is also no guarantee of execution ordering, so your package could create 5SendMessageTicketsin one transaction and they could be executed in any order depending on the PTB. Integrating packages should account for both of these scenarios.
send_message_with_caller
Same assend_message() but with an additional parameter, destination_caller.
This parameter specifies which address has permission to call
receive_message() on the destination domain for the message.
Parameters
replace_message
Replace a message with a new message body and/or destination caller. Theoriginal_attestation must be a valid attestation of original_message,
produced by Circle’s attestation service.
Remarks:
- The sender package of the replaced message must be the same as the caller of
the original message. This is identified using the
Authgeneric parameter. See stamp_receipt for more info onAuthstructs.
Additional Notes
Destination Callers for Sui as Destination Chain
Destination caller is a message field that specifies which address has permission to callreceive_message() on the destination domain for the given
message. On Sui this can either be an EOA (use receive_message()) or an Auth
struct address for a package (use receive_message_with_package_auth()). Using
a package destination caller allows integrators to run any atomic action in the
same transaction that the message is received in.
In order to determine the address to use for the destination caller field for
Sui destination messages, please call
message_transmitter::auth::auth_caller_identifier() with your Auth struct
type.
In order to use a package destination caller with Sui destination messages,
integrators must create an
Auth struct in their own package. Any struct that
implements the drop trait can be used as an authenticator, but it is recommended
to use a dedicated Auth struct. Integrators should be careful to not expose
these structs to the public or else messages with their package as destination
caller could be received by others. An example can be found in
TokenMessengerMinter
on GitHub.Mint Recipient Addresses for Sui as Source Chain
Outgoing mint recipient addresses from Sui are passed as Sui address types and can be treated the same as abytes32 mint recipient parameter on EVM
implementations.
Mint Recipient Addresses for Sui as Destination Chain
Sui mint recipient addresses from other chains should be treated the same as a hexbytes32 parameter.
CCTP V1 Package Upgrades and Versioning
CCTP V1 packages on Sui are upgradable. Public functions likedeposit_for_burn(), receive_message(), etc. are version-gated. This means if
the CCTP V1 packages are upgraded, the old versions of these functions will no
longer be callable. Because of this, we do not recommend calling these functions
directly from packages, and instead recommend calling the create ticket
functions (not version-gated) directly from dependent packages, returning the
created Ticket from your package, and then calling the main public function
(e.g. deposit_for_burn() or receive_message()) from a PTB. By using the
create ticket functions, dependent packages can securely set the parameters and
Auth struct for the function call from within the package, and only have to
update PTBs when CCTP V1 packages are upgraded.
Integrating with CCTP V1 Sui from other Packages
Integrating with the CCTP V1 Sui packages from other packages is different from non-Sui implementations. Rather than directly wrapping the CCTP-Sui packages like one would in Solidity, on Sui packages should interact with CCTP V1 packages in a more composable way. Third party packages should follow theTicket pattern with a dedicated and private Auth struct as described below.
Private Auth Structs
Auth structs are used throughout the CCTP V1 packages in functions intended to
be called from other dependent packages. The auth_caller_identifier() function
is used to uniquely identify other packages by hashing the full object type of
the type passed in. Any struct that implements the drop trait can be used as an
authenticator, but it is recommended to use a dedicated auth struct. Calling
contracts should be careful to not expose these structs to the public or else
messages from their package could be forged. An example can be found in
TokenMessengerMinter
on GitHub.
Ticket Pattern
TheTicket pattern is a pattern used in CCTP-Sui that enables the
composability of CCTP V1 with third-party packages. The pattern enables a
third-party integrator (package) to create a Ticket (“hot potato”) for a
designated operation directly in their package without having to upgrade their
packages with future CCTP V1 upgrades. Only PTBs would need to be updated.
Ticket structs contain parameters for specific interactions with CCTP V1
packages. They can only be created from and consumed by the CCTP V1 packages in
a specific interaction, and do not have drop or store abilities, so must be used
in the PTB where they are created. They also contain an Auth field that should
only be created by the third-party package. The calling PTB should handle the
Ticket by calling the relevant CCTP V1 package, which will recognize the
third-party integrator as the action initiator.
The following public functions (designed for third-party integrators, EOAs
should use the entry versions) are implemented following the Ticket pattern.
Each of them creates or consumes their own specific Ticket type:
message_transmitter:
receive_message_with_package_auth()stamp_receipt()
token_messenger_minter:
deposit_for_burn_with_package_auth()deposit_for_burn_with_caller_with_package_auth()replace_deposit_for_burn_with_package_auth()
- The integrating package calls
create_replace_deposit_for_burn_ticket()with anAuthstruct it defined, and returns this ticket. - The PTB calls
deposit_for_burn_with_caller_with_package_auth()with the ticket on behalf of the integrator. token_messenger_minterwill validate if the type hash ofAuthmatches the original sender in the burn message.
PTB Function Call Ordering
Due to the composability of Sui and PTBs, along with theTicket pattern, there
is no guarantee of ordering of calls within PTBs. The Ticket pattern
introduces behaviors similar to asynchronous functions in ordinary programming
contexts: when an integrator creates a ticket and returns it to the PTB, it is
signaling an intention to execute the logic function, and the properties of the
Move type system carry the guarantee that the function will indeed be eventually
executed before the end of the transaction. However, no guarantee is given
regarding the relative order of execution: the PTB is free to consume the
tickets in any order it sees fit. While this has no security implications on the
internal coherence of CCTP V1 itself, integrators should carefully evaluate
whether their own logic is somehow dependent on a specific order of execution of
the CCTP V1 functions.
For example, a PTB could create 5 DepositForBurnTicket structs and execute
them in any order. Similarly on the Sui destination side, 5 messages could be
received in MessageTransmitter, and then received (and thus the USDC minted)
in TokenMessengerMinter in a completely different order. If any pre or post
actions are taken in third party packages, these could also come in an
unexpected ordering, so this scenario should be handled accordingly in third
party packages.
Ticket Pattern Examples
An example of this with receivingdeposit_for_burn() messages on Sui can be
seen below.
This example assumes the destination_caller for the message is set to the auth
address for your package’s Auth struct.
JavaScript
deposit_for_burn() side:
JavaScript