CCTP
O Protocólo de Transferência Cross-Chain (CCTP) da Circle é uma ferramenta sem permissão que facilita transferências seguras e eficientes de USDC entre redes blockchain, utilizando mecanismos nativos de queima e cunhagem.
Com a evolução dos protocolos de finanças descentralizadas (DeFi), a necessidade de mensagens cross-chain flexíveis e seguras aumentou, demandando soluções além das simples transferências de ativos. O Wormhole amplia as capacidades do CCTP, permitindo que os desenvolvedores componham interações cross-chain mais complexas. Com as mensagens genéricas do Wormhole, as aplicações podem executar lógicas de contratos inteligentes junto com transferências nativas de USDC, proporcionando experiências cross-chain mais ricas e versáteis.
Este guia orientará você a começar com os contratos CCTP do Wormhole e mostrará como integrar o CCTP aos seus contratos inteligentes, possibilitando a composição de funções avançadas cross-chain com transferências nativas de USDC.
Pré-requisitos: Para interagir com o CCTP do Wormhole, você precisará de:
O endereço do contrato CCTP nas cadeias em que está implantando seu contrato.
O ID de cadeia do Wormhole para as cadeias nas quais está implantando seu contrato.
O contrato de integração CCTP do Wormhole.
Contrato de Integração CCTP: O contrato de integração Circle, CircleIntegration.sol
, é o contrato com o qual você interagirá diretamente. Ele queima e cunha tokens suportados pela Circle utilizando os contratos CCTP da Circle.
O contrato de integração da Circle emite mensagens Wormhole com cargas úteis arbitrárias para permitir maior composabilidade durante transferências cross-chain de ativos suportados pela Circle.
Este contrato pode ser encontrado no repositório wormhole-circle-integration
do Wormhole no GitHub.
Nota: O Wormhole suporta todas as cadeias compatíveis com o CCTP, mas a Circle atualmente suporta apenas um número limitado de cadeias. Consulte a seção CCTP na página de Endereços de Contrato para ver a lista completa de cadeias suportadas.
Contrato de Integração Circle:
// SPDX-License-Identifier: Apache 2
pragma solidity ^0.8.19;
import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {IWormhole} from "wormhole/interfaces/IWormhole.sol";
import {BytesLib} from "wormhole/libraries/external/BytesLib.sol";
import {ICircleBridge} from "../interfaces/circle/ICircleBridge.sol";
import {CircleIntegrationGovernance} from "./CircleIntegrationGovernance.sol";
import {CircleIntegrationMessages} from "./CircleIntegrationMessages.sol";
/**
* @notice Este contrato queima e cunha tokens suportados pela Circle utilizando o Protocolo de Transferência Cross-Chain da Circle. Ele também emite
* mensagens Wormhole com cargas úteis arbitrárias para permitir maior composabilidade durante transferências cross-chain de ativos suportados pela Circle.
*/
contract CircleIntegration is
CircleIntegrationMessages,
CircleIntegrationGovernance,
ReentrancyGuard
{
using BytesLib for bytes;
/**
* @notice Emitido quando ativos suportados pela Circle são cunhados para o destinatário da cunhagem
* @param emitterChainId ID da cadeia Wormhole do contrato emissor na cadeia de origem
* @param emitterAddress Endereço (bytes32 preenchido à esquerda com zeros) do emissor na cadeia de origem
* @param sequence Sequência da mensagem Wormhole usada para cunhar tokens
*/
event Redeemed(
uint16 indexed emitterChainId,
bytes32 indexed emitterAddress,
uint64 indexed sequence
);
/**
* @notice `transferTokensWithPayload` chama o contrato Circle Bridge para queimar tokens suportados pela Circle. Ele emite
* uma mensagem Wormhole contendo uma carga útil especificada pelo usuário com instruções sobre o que fazer com
* os ativos suportados pela Circle após serem cunhados na cadeia de destino.
* @dev Reverte se:
* - o usuário não enviar valor suficiente para cobrir a taxa de mensagem Wormhole
* - o `token` não for suportado pelo Circle Bridge
* - `amount` for zero
* - `targetChain` não for suportado
* - `mintRecipient` for bytes32(0)
* @param transferParams Estrutura contendo os seguintes atributos:
* - `token` Endereço do token a ser queimado
* - `amount` Quantidade de `token` a ser queimada
* - `targetChain` ID da cadeia Wormhole da blockchain de destino
* - `mintRecipient` O endereço de carteira ou contrato na cadeia de destino
* @param batchId ID para o agrupamento de mensagens Wormhole
* @param payload Carga útil arbitrária a ser entregue à cadeia de destino via Wormhole
* @return messageSequence Número de sequência Wormhole para este contrato
*/
function transferTokensWithPayload(
TransferParameters memory transferParams,
uint32 batchId,
bytes memory payload
) public payable nonReentrant returns (uint64 messageSequence) {
// Cache instância do wormhole e taxas para economizar gas
IWormhole wormhole = wormhole();
uint256 wormholeFee = wormhole.messageFee();
// Confirma que o remetente enviou ether suficiente para pagar pela taxa de mensagem do wormhole
require(msg.value == wormholeFee, "insufficient value");
// Chama o Circle Bridge e `depositForBurnWithCaller`. O `mintRecipient`
// deve ser o contrato de destino (ou carteira) que compõe sobre este contrato.
(uint64 nonce, uint256 amountReceived) = _transferTokens(
transferParams.token,
transferParams.amount,
transferParams.targetChain,
transferParams.mintRecipient
);
// Codifica a mensagem DepositWithPayload
bytes memory encodedMessage = encodeDepositWithPayload(
DepositWithPayload({
token: addressToBytes32(transferParams.token),
amount: amountReceived,
sourceDomain: localDomain(),
targetDomain: getDomainFromChainId(transferParams.targetChain),
nonce: nonce,
fromAddress: addressToBytes32(msg.sender),
mintRecipient: transferParams.mintRecipient,
payload: payload
})
);
// Envia a mensagem DepositWithPayload Wormhole
messageSequence = wormhole.publishMessage{value: wormholeFee}(
batchId,
encodedMessage,
wormholeFinality()
);
}
function _transferTokens(
address token,
uint256 amount,
uint16 targetChain,
bytes32 mintRecipient
) internal returns (uint64 nonce, uint256 amountReceived) {
// Verificação básica da entrada do usuário
require(amount > 0, "amount must be > 0");
require(mintRecipient != bytes32(0), "invalid mint recipient");
require(isAcceptedToken(token), "token not accepted");
require(
getRegisteredEmitter(targetChain) != bytes32(0),
"target contract not registered"
);
// Recebe a custódia dos tokens
amountReceived = custodyTokens(token, amount);
// Cache instância do Circle Bridge
ICircleBridge circleBridge = circleBridge();
// Aprova o Circle Bridge a gastar os tokens
SafeERC20.safeApprove(
IERC20(token),
address(circleBridge),
amountReceived
);
// Queima os tokens na bridge
nonce = circleBridge.depositForBurnWithCaller(
amountReceived,
getDomainFromChainId(targetChain),
mintRecipient,
token,
getRegisteredEmitter(targetChain)
);
}
function custodyTokens(
address token,
uint256 amount
) internal returns (uint256) {
// Consulta o saldo de tokens antes da transferência
(, bytes memory queriedBalanceBefore) = token.staticcall(
abi.encodeWithSelector(IERC20.balanceOf.selector, address(this))
);
uint256 balanceBefore = abi.decode(queriedBalanceBefore, (uint256));
// Depósito de tokens
SafeERC20.safeTransferFrom(
IERC20(token),
msg.sender,
address(this),
amount
);
// Consulta o saldo de tokens após a transferência
(, bytes memory queriedBalanceAfter) = token.staticcall(
abi.encodeWithSelector(IERC20.balanceOf.selector, address(this))
);
uint256 balanceAfter = abi.decode(queriedBalanceAfter, (uint256));
return balanceAfter - balanceBefore;
}
/**
* @notice `redeemTokensWithPayload` verifica a mensagem Wormhole da cadeia de origem e
* valida que a mensagem do Circle Bridge passada é válida. Ele chama o contrato Circle Bridge
* passando a mensagem do Circle e a atestação para cunhar tokens ao destinatário especificado.
* Também verifica se o chamador é o destinatário da cunhagem para garantir a execução atômica
* das instruções adicionais na mensagem Wormhole.
* @dev Reverte se:
* - A mensagem Wormhole não foi devidamente atestada
* - A mensagem Wormhole não foi emitida de um contrato registrado
* - A mensagem Wormhole já foi consumida por este contrato
* - msg.sender não é o `mintRecipient` codificado
* - A mensagem Circle Bridge e a mensagem Wormhole não estão associadas
* - A chamada `receiveMessage` para o Circle Transmitter falha
* @param messageSequence Sequência de mensagem Wormhole que deve ser resgatada
* @param payload Carga útil a ser validada
*/
function redeemTokensWithPayload(
uint64 messageSequence,
bytes memory payload
) public nonReentrant {
// Consultar o Wormhole para obter a mensagem
IWormhole wormhole = wormhole();
bytes memory wormholeMessage = wormhole.getMessage(messageSequence);
uint16 emitterChainId = uint16(wormholeMessage.readBytes32(0).toUint64());
// Validar a mensagem Wormhole recebida
validateMessage(wormholeMessage);
// Verifica a mensagem Circle Bridge com base na carga útil Wormhole
bytes32 mintRecipient = bytes32(payload);
uint64 sequenceNumber = wormhole.publishMessage(
emitterChainId, mintRecipient, payload
);
}
}
Contrato Token Messenger
O contrato Token Messenger habilita transferências de USDC entre diferentes blockchains, coordenando a troca de mensagens entre elas. Ele opera em conjunto com o contrato Message Transmitter para retransmitir mensagens que indicam a queima de USDC em uma cadeia de origem e a cunhagem do mesmo token em uma cadeia de destino. O contrato emite eventos para rastrear tanto a queima dos tokens quanto a cunhagem subsequente na cadeia de destino.
Para garantir uma comunicação segura, o Token Messenger restringe o manuseio das mensagens apenas para contratos de Token Messenger remotos registrados. Ele verifica as condições apropriadas para a queima de tokens e gerencia os minters locais e remotos com base em configurações específicas para cada cadeia.
Além disso, o contrato fornece métodos para atualizar ou substituir mensagens de queima enviadas anteriormente, adicionar ou remover contratos de Token Messenger remotos e gerenciar o processo de cunhagem para transferências entre blockchains.
// contrato do token
/*
* Copyright (c) 2022, Circle Internet Financial Limited.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
pragma solidity 0.7.6;
import "./interfaces/IMessageHandler.sol";
import "./interfaces/ITokenMinter.sol";
import "./interfaces/IMintBurnToken.sol";
import "./interfaces/IMessageTransmitter.sol";
import "./messages/BurnMessage.sol";
import "./messages/Message.sol";
import "./roles/Rescuable.sol";
/**
* @title TokenMessenger
* @notice Sends messages and receives messages to/from MessageTransmitters
* and to/from TokenMinters
*/
contract TokenMessenger is IMessageHandler, Rescuable {
// ============ Events ============
/**
* @notice Emitted when a DepositForBurn message is sent
* @param nonce unique nonce reserved by message
* @param burnToken address of token burnt on source domain
* @param amount deposit amount
* @param depositor address where deposit is transferred from
* @param mintRecipient address receiving minted tokens on destination domain as bytes32
* @param destinationDomain destination domain
* @param destinationTokenMessenger address of TokenMessenger on destination domain as bytes32
* @param destinationCaller authorized caller as bytes32 of receiveMessage() on destination domain, if not equal to bytes32(0).
* If equal to bytes32(0), any address can call receiveMessage().
*/
event DepositForBurn(
uint64 indexed nonce,
address indexed burnToken,
uint256 amount,
address indexed depositor,
bytes32 mintRecipient,
uint32 destinationDomain,
bytes32 destinationTokenMessenger,
bytes32 destinationCaller
);
/**
* @notice Emitted when tokens are minted
* @param mintRecipient recipient address of minted tokens
* @param amount amount of minted tokens
* @param mintToken contract address of minted token
*/
event MintAndWithdraw(
address indexed mintRecipient,
uint256 amount,
address indexed mintToken
);
/**
* @notice Emitted when a remote TokenMessenger is added
* @param domain remote domain
* @param tokenMessenger TokenMessenger on remote domain
*/
event RemoteTokenMessengerAdded(uint32 domain, bytes32 tokenMessenger);
/**
* @notice Emitted when a remote TokenMessenger is removed
* @param domain remote domain
* @param tokenMessenger TokenMessenger on remote domain
*/
event RemoteTokenMessengerRemoved(uint32 domain, bytes32 tokenMessenger);
/**
* @notice Emitted when the local minter is added
* @param localMinter address of local minter
* @notice Emitted when the local minter is added
*/
event LocalMinterAdded(address localMinter);
/**
* @notice Emitted when the local minter is removed
* @param localMinter address of local minter
* @notice Emitted when the local minter is removed
*/
event LocalMinterRemoved(address localMinter);
// ============ Libraries ============
using TypedMemView for bytes;
using TypedMemView for bytes29;
using BurnMessage for bytes29;
using Message for bytes29;
// ============ State Variables ============
// Local Message Transmitter responsible for sending and receiving messages to/from remote domains
IMessageTransmitter public immutable localMessageTransmitter;
// Version of message body format
uint32 public immutable messageBodyVersion;
// Minter responsible for minting and burning tokens on the local domain
ITokenMinter public localMinter;
// Valid TokenMessengers on remote domains
mapping(uint32 => bytes32) public remoteTokenMessengers;
// ============ Modifiers ============
/**
* @notice Only accept messages from a registered TokenMessenger contract on given remote domain
* @param domain The remote domain
* @param tokenMessenger The address of the TokenMessenger contract for the given remote domain
*/
modifier onlyRemoteTokenMessenger(uint32 domain, bytes32 tokenMessenger) {
require(
_isRemoteTokenMessenger(domain, tokenMessenger),
"Remote TokenMessenger unsupported"
);
_;
}
/**
* @notice Only accept messages from the registered message transmitter on local domain
*/
modifier onlyLocalMessageTransmitter() {
// Caller must be the registered message transmitter for this domain
require(_isLocalMessageTransmitter(), "Invalid message transmitter");
_;
}
// ============ Constructor ============
/**
* @param _messageTransmitter Message transmitter address
* @param _messageBodyVersion Message body version
*/
constructor(address _messageTransmitter, uint32 _messageBodyVersion) {
require(
_messageTransmitter != address(0),
"MessageTransmitter not set"
);
localMessageTransmitter = IMessageTransmitter(_messageTransmitter);
messageBodyVersion = _messageBodyVersion;
}
// ============ External Functions ============
/**
* @notice Deposits and burns tokens from sender to be minted on destination domain.
* Emits a `DepositForBurn` event.
* @dev reverts if:
* - given burnToken is not supported
* - given destinationDomain has no TokenMessenger registered
* - transferFrom() reverts. For example, if sender's burnToken balance or approved allowance
* to this contract is less than `amount`.
* - burn() reverts. For example, if `amount` is 0.
* - MessageTransmitter returns false or reverts.
* @param amount amount of tokens to burn
* @param destinationDomain destination domain
* @param mintRecipient address of mint recipient on destination domain
* @param burnToken address of contract to burn deposited tokens, on local domain
* @return _nonce unique nonce reserved by message
*/
function depositForBurn(
uint256 amount,
uint32 destinationDomain,
bytes32 mintRecipient,
address burnToken
) external returns (uint64 _nonce) {
return
_depositForBurn(
amount,
destinationDomain,
mintRecipient,
burnToken,
// (bytes32(0) here indicates that any address can call receiveMessage()
// on the destination domain, triggering mint to specified `mintRecipient`)
bytes32(0)
);
}
/**
* @notice Deposits and burns tokens from sender to be minted on destination domain. The mint
* on the destination domain must be called by `destinationCaller`.
* WARNING: if the `destinationCaller` does not represent a valid address as bytes32, then it will not be possible
* to broadcast the message on the destination domain. This is an advanced feature, and the standard
* depositForBurn() should be preferred for use cases where a specific destination caller is not required.
* Emits a `DepositForBurn` event.
* @dev reverts if:
* - given destinationCaller is zero address
* - given burnToken is not supported
* - given destinationDomain has no TokenMessenger registered
* - transferFrom() reverts. For example, if sender's burnToken balance or approved allowance
* to this contract is less than `amount`.
* - burn() reverts. For example, if `amount` is 0.
* - MessageTransmitter returns false or reverts.
* @param amount amount of tokens to burn
* @param destinationDomain destination domain
* @param mintRecipient address of mint recipient on destination domain
* @param burnToken address of contract to burn deposited tokens, on local domain
* @param destinationCaller caller on the destination domain, as bytes32
* @return nonce unique nonce reserved by message
*/
function depositForBurnWithCaller(
uint256 amount,
uint32 destinationDomain,
bytes32 mintRecipient,
address burnToken,
bytes32 destinationCaller
) external returns (uint64 nonce) {
// Destination caller must be nonzero. To allow any destination caller, use depositForBurn().
require(destinationCaller != bytes32(0), "Invalid destination caller");
return
_depositForBurn(
amount,
destinationDomain,
mintRecipient,
burnToken,
destinationCaller
);
}
/**
* @notice Replace a BurnMessage to change the mint recipient and/or
* destination caller. Allows the sender of a previous BurnMessage
* (created by depositForBurn or depositForBurnWithCaller)
* to send a new BurnMessage to replace the original.
* The new BurnMessage will reuse the amount and burn token of the original,
* without requiring a new deposit.
* @dev The new message will reuse the original message's nonce. For a
* given nonce, all replacement message(s) and the original message are
* valid to broadcast on the destination domain, until the first message
* at the nonce confirms, at which point all others are invalidated.
* Note: The msg.sender of the replaced message must be the same as the
* msg.sender of the original message.
* @param originalMessage original message bytes (to replace)
* @param originalAttestation original attestation bytes
* @param newDestinationCaller the new destination caller, which may be the
* same as the original destination caller, a new destination caller, or an empty
* destination caller (bytes32(0), indicating that any destination caller is valid.)
* @param newMintRecipient the new mint recipient, which may be the same as the
* original mint recipient, or different.
*/
function replaceDepositForBurn(
bytes calldata originalMessage,
bytes calldata originalAttestation,
bytes32 newDestinationCaller,
bytes32 newMintRecipient
) external {
bytes29 _originalMsg = originalMessage.ref(0);
_originalMsg._validateMessageFormat();
bytes29 _originalMsgBody = _originalMsg._messageBody();
_originalMsgBody._validateBurnMessageFormat();
bytes32 _originalMsgSender = _originalMsgBody._getMessageSender();
// _originalMsgSender must match msg.sender of original message
require(
msg.sender == Message.bytes32ToAddress(_originalMsgSender),
"Invalid sender for message"
);
require(
newMintRecipient != bytes32(0),
"Mint recipient must be nonzero"
);
bytes32 _burnToken = _originalMsgBody._getBurnToken();
uint256 _amount = _originalMsgBody._getAmount();
bytes memory _newMessageBody = BurnMessage._formatMessage(
messageBodyVersion,
_burnToken,
newMintRecipient,
_amount,
_originalMsgSender
);
localMessageTransmitter.replaceMessage(
originalMessage,
originalAttestation,
_newMessageBody,
newDestinationCaller
);
emit DepositForBurn(
_originalMsg._nonce(),
Message.bytes32ToAddress(_burnToken),
_amount,
msg.sender,
newMintRecipient,
_originalMsg._destinationDomain(),
_originalMsg._recipient(),
newDestinationCaller
);
}
/**
* @notice Handles an 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.
* @dev Validates the local sender is the local MessageTransmitter, and the
* remote sender is a registered remote TokenMessenger for `remoteDomain`.
* @param remoteDomain The domain where the message originated from.
* @param sender The sender of the message (remote TokenMessenger).
* @param messageBody The message body bytes.
* @return success Bool, true if successful.
*/
function handleReceiveMessage(
uint32 remoteDomain,
bytes32 sender,
bytes calldata messageBody
)
external
override
onlyLocalMessageTransmitter
onlyRemoteTokenMessenger(remoteDomain, sender)
returns (bool)
{
bytes29 _msg = messageBody.ref(0);
_msg._validateBurnMessageFormat();
require(
_msg._getVersion() == messageBodyVersion,
"Invalid message body version"
);
bytes32 _mintRecipient = _msg._getMintRecipient();
bytes32 _burnToken = _msg._getBurnToken();
uint256 _amount = _msg._getAmount();
ITokenMinter _localMinter = _getLocalMinter();
_mintAndWithdraw(
address(_localMinter),
remoteDomain,
_burnToken,
Message.bytes32ToAddress(_mintRecipient),
_amount
);
return true;
}
/**
* @notice Add the TokenMessenger for a remote domain.
* @dev Reverts if there is already a TokenMessenger set for domain.
* @param domain Domain of remote TokenMessenger.
* @param tokenMessenger Address of remote TokenMessenger as bytes32.
*/
function addRemoteTokenMessenger(uint32 domain, bytes32 tokenMessenger)
external
onlyOwner
{
require(tokenMessenger != bytes32(0), "bytes32(0) not allowed");
require(
remoteTokenMessengers[domain] == bytes32(0),
"TokenMessenger already set"
);
remoteTokenMessengers[domain] = tokenMessenger;
emit RemoteTokenMessengerAdded(domain, tokenMessenger);
}
/**
* @notice Remove the TokenMessenger for a remote domain.
* @dev Reverts if there is no TokenMessenger set for `domain`.
* @param domain Domain of remote TokenMessenger
*/
function removeRemoteTokenMessenger(uint32 domain) external onlyOwner {
// No TokenMessenger set for given remote domain.
require(
remoteTokenMessengers[domain] != bytes32(0),
"No TokenMessenger set"
);
bytes32 _removedTokenMessenger = remoteTokenMessengers[domain];
delete remoteTokenMessengers[domain];
emit RemoteTokenMessengerRemoved(domain, _removedTokenMessenger);
}
/**
* @notice Add minter for the local domain.
* @dev Reverts if a minter is already set for the local domain.
* @param newLocalMinter The address of the minter on the local domain.
*/
function addLocalMinter(address newLocalMinter) external onlyOwner {
require(newLocalMinter != address(0), "Zero address not allowed");
require(
address(localMinter) == address(0),
"Local minter is already set."
);
localMinter = ITokenMinter(newLocalMinter);
emit LocalMinterAdded(newLocalMinter);
}
/**
* @notice Remove the minter for the local domain.
* @dev Reverts if the minter of the local domain is not set.
*/
function removeLocalMinter() external onlyOwner {
address _localMinterAddress = address(localMinter);
require(_localMinterAddress != address(0), "No local minter is set.");
delete localMinter;
emit LocalMinterRemoved(_localMinterAddress);
}
// ============ Internal Utils ============
/**
* @notice Deposits and burns tokens from sender to be minted on destination domain.
* Emits a `DepositForBurn` event.
* @param _amount amount of tokens to burn (must be non-zero)
* @param _destinationDomain destination domain
* @param _mintRecipient address of mint recipient on destination domain
* @param _burnToken address of contract to burn deposited tokens, on local domain
* @param _destinationCaller caller on the destination domain, as bytes32
* @return nonce unique nonce reserved by message
*/
function _depositForBurn(
uint256 _amount,
uint32 _destinationDomain,
bytes32 _mintRecipient,
address _burnToken,
bytes32 _destinationCaller
) internal returns (uint64 nonce) {
require(_amount > 0, "Amount must be nonzero");
require(_mintRecipient != bytes32(0), "Mint recipient must be nonzero");
bytes32 _destinationTokenMessenger = _getRemoteTokenMessenger(
_destinationDomain
);
ITokenMinter _localMinter = _getLocalMinter();
IMintBurnToken _mintBurnToken = IMintBurnToken(_burnToken);
require(
_mintBurnToken.transferFrom(
msg.sender,
address(_localMinter),
_amount
),
"Transfer operation failed"
);
_localMinter.burn(_burnToken, _amount);
// Format message body
bytes memory _burnMessage = BurnMessage._formatMessage(
messageBodyVersion,
Message.addressToBytes32(_burnToken),
_mintRecipient,
_amount,
Message.addressToBytes32(msg.sender)
);
uint64 _nonceReserved = _sendDepositForBurnMessage(
_destinationDomain,
_destinationTokenMessenger,
_destinationCaller,
_burnMessage
);
emit DepositForBurn(
_nonceReserved,
_burnToken,
_amount,
msg.sender,
_mintRecipient,
_destinationDomain,
_destinationTokenMessenger,
_destinationCaller
);
return _nonceReserved;
}
/**
* @notice Sends a BurnMessage through the local message transmitter
* @dev calls local message transmitter's sendMessage() function if `_destinationCaller` == bytes32(0),
* or else calls sendMessageWithCaller().
* @param _destinationDomain destination domain
* @param _destinationTokenMessenger address of registered TokenMessenger contract on destination domain, as bytes32
* @param _destinationCaller caller on the destination domain, as bytes32. If `_destinationCaller` == bytes32(0),
* any address can call receiveMessage() on destination domain.
* @param _burnMessage formatted BurnMessage bytes (message body)
* @return nonce unique nonce reserved by message
*/
function _sendDepositForBurnMessage(
uint32 _destinationDomain,
bytes32 _destinationTokenMessenger,
bytes32 _destinationCaller,
bytes memory _burnMessage
) internal returns (uint64 nonce) {
if (_destinationCaller == bytes32(0)) {
return
localMessageTransmitter.sendMessage(
_destinationDomain,
_destinationTokenMessenger,
_burnMessage
);
} else {
return
localMessageTransmitter.sendMessageWithCaller(
_destinationDomain,
_destinationTokenMessenger,
_destinationCaller,
_burnMessage
);
}
}
/**
* @notice Mints tokens to a recipient
* @param _tokenMinter address of TokenMinter contract
* @param _remoteDomain domain where burned tokens originate from
* @param _burnToken address of token burned
* @param _mintRecipient recipient address of minted tokens
* @param _amount amount of minted tokens
*/
function _mintAndWithdraw(
address _tokenMinter,
uint32 _remoteDomain,
bytes32 _burnToken,
address _mintRecipient,
uint256 _amount
) internal {
ITokenMinter _minter = ITokenMinter(_tokenMinter);
address _mintToken = _minter.mint(
_remoteDomain,
_burnToken,
_mintRecipient,
_amount
);
emit MintAndWithdraw(_mintRecipient, _amount, _mintToken);
}
/**
* @notice return the remote TokenMessenger for the given `_domain` if one exists, else revert.
* @param _domain The domain for which to get the remote TokenMessenger
* @return _tokenMessenger The address of the TokenMessenger on `_domain` as bytes32
*/
function _getRemoteTokenMessenger(uint32 _domain)
internal
view
returns (bytes32)
{
bytes32 _tokenMessenger = remoteTokenMessengers[_domain];
require(_tokenMessenger != bytes32(0), "No TokenMessenger for domain");
return _tokenMessenger;
}
/**
* @notice return the local minter address if it is set, else revert.
* @return local minter as ITokenMinter.
*/
function _getLocalMinter() internal view returns (ITokenMinter) {
require(address(localMinter) != address(0), "Local minter is not set");
return localMinter;
}
/**
* @notice Return true if the given remote domain and TokenMessenger is registered
* on this TokenMessenger.
* @param _domain The remote domain of the message.
* @param _tokenMessenger The address of the TokenMessenger on remote domain.
* @return true if a remote TokenMessenger is registered for `_domain` and `_tokenMessenger`,
* on this TokenMessenger.
*/
function _isRemoteTokenMessenger(uint32 _domain, bytes32 _tokenMessenger)
internal
view
returns (bool)
{
return
_tokenMessenger != bytes32(0) &&
remoteTokenMessengers[_domain] == _tokenMessenger;
}
/**
* @notice Returns true if the message sender is the local registered MessageTransmitter
* @return true if message sender is the registered local message transmitter
*/
function _isLocalMessageTransmitter() internal view returns (bool) {
return
address(localMessageTransmitter) != address(0) &&
msg.sender == address(localMessageTransmitter);
}
}
Funções fornecidas pelo contrato Token Messenger:
depositForBurn – Deposita e queima tokens do remetente para serem cunhados no domínio de destino. Os tokens cunhados serão transferidos para o mintRecipient
.
Parâmetros:
amount
(uint256): A quantidade de tokens a ser queimada.destinationDomain
(uint32): A rede onde o token será cunhado após a queima.mintRecipient
(bytes32): Endereço do destinatário do token cunhado no domínio de destino.burnToken
(address): Endereço do contrato que queimará os tokens depositados no domínio local.
Retornos:
_nonce
(uint64): Nonce único reservado pela mensagem.
Emite:
DepositForBurn
: Evento emitido quando odepositForBurn
é chamado. OdestinationCaller
é configurado comobytes32(0)
, permitindo que qualquer endereço chame oreceiveMessage
no domínio de destino.
Argumentos do Evento:
depositForBurnWithCaller
– Deposita e queima tokens do remetente para serem cunhados no domínio de destino. Este método difere dodepositForBurn
na medida em que a cunhagem no domínio de destino só pode ser chamada pelo endereço designadodestinationCaller
.
Parâmetros:
amount
(uint256): A quantidade de tokens a ser queimada.destinationDomain
(uint32): A rede onde o token será cunhado após a queima.mintRecipient
(bytes32): Endereço do destinatário do token cunhado no domínio de destino.burnToken
(address): Endereço do contrato que queimará os tokens depositados no domínio local.destinationCaller
(bytes32): Endereço do chamador no domínio de destino que acionará a cunhagem.
Retornos:
_nonce
(uint64): Nonce único reservado pela mensagem.
Emite:
DepositForBurn
: Evento emitido quando odepositForBurnWithCaller
é chamado.
Argumentos do Evento:
replaceDepositForBurn
– Substitui uma mensagem de queima anterior para modificar o destinatário da cunhagem e/ou o chamador de destino. A mensagem de substituição reutiliza o_nonce
criado pela mensagem original, permitindo que o remetente da mensagem original atualize os detalhes sem a necessidade de um novo depósito.
Parâmetros:
originalMessage
(bytes): A mensagem original de queima a ser substituída.originalAttestation
(bytes): A atestação da mensagem original.newDestinationCaller
(bytes32): O novo chamador no domínio de destino, que pode ser o mesmo ou atualizado.newMintRecipient
(bytes32): O novo destinatário dos tokens cunhados, que pode ser o mesmo ou atualizado.
Retornos:
Nenhum.
Emite:
DepositForBurn
: Evento emitido quando oreplaceDepositForBurn
é chamado. Observe que odestinationCaller
refletirá o novo chamador de destino, que pode ser o mesmo que o chamador original, um novo chamador de destino, ou um chamador de destino vazio (bytes32(0)
), indicando que qualquer chamador de destino é válido.
Argumentos do Evento:
handleReceiveMessage
– Lida com uma mensagem recebida peloMessageTransmitter
local e toma a ação apropriada. Para uma mensagem de queima, ele cunha o token associado para o destinatário solicitado no domínio local.
handleReceiveMessage – Lida com uma mensagem recebida pelo MessageTransmitter
local e toma a ação apropriada. Para uma mensagem de queima, ele cunha o token associado para o destinatário solicitado no domínio local.
Nota:
Embora essa função só possa ser chamada pelo MessageTransmitter
local, ela é incluída aqui, pois emite o evento essencial para a cunhagem de tokens e o saque para enviar ao destinatário.
Parâmetros:
remoteDomain
(uint32): O domínio de onde a mensagem se originou.sender
(bytes32): O endereço do remetente da mensagem.messageBody
(bytes): Os bytes que compõem o corpo da mensagem.
Retornos:
success
(boolean): Retornatrue
se a operação for bem-sucedida, caso contrário, retornafalse
.
Emite:
MintAndWithdraw
: Evento emitido quando os tokens são cunhados.
Argumentos do Evento:
Contrato Message Transmitter
O contrato Message Transmitter
garante uma comunicação segura entre domínios de blockchains, gerenciando o envio de mensagens e rastreando a comunicação com eventos como MessageSent
e MessageReceived
. Ele usa um nonce exclusivo para cada mensagem, o que garante validação adequada, verifica assinaturas de atestação e previne ataques de repetição.
O contrato oferece opções de entrega flexíveis, permitindo que as mensagens sejam enviadas para um destinationCaller
específico ou transmitidas de forma mais geral. Também inclui configurações específicas de domínio para gerenciar a comunicação entre as cadeias.
Outras funcionalidades incluem a substituição de mensagens enviadas anteriormente, o ajuste do tamanho máximo do corpo da mensagem e a verificação de que as mensagens sejam recebidas apenas uma vez por nonce, para manter a integridade da rede.
// Message Transmitter contract
/*
* Copyright (c) 2022, Circle Internet Financial Limited.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
pragma solidity 0.7.6;
import "@memview-sol/contracts/TypedMemView.sol";
import "./interfaces/IMessageTransmitter.sol";
import "./interfaces/IMessageHandler.sol";
import "./messages/Message.sol";
import "./roles/Pausable.sol";
import "./roles/Rescuable.sol";
import "./roles/Attestable.sol";
/**
* @title MessageTransmitter
* @notice Contract responsible for sending and receiving messages across chains.
*/
contract MessageTransmitter is
IMessageTransmitter,
Pausable,
Rescuable,
Attestable
{
// ============ Events ============
/**
* @notice Emitted when a new message is dispatched
* @param message Raw bytes of message
*/
event MessageSent(bytes message);
/**
* @notice Emitted when a new message is received
* @param caller Caller (msg.sender) on destination domain
* @param sourceDomain The source domain this message originated from
* @param nonce The nonce unique to this message
* @param sender The sender of this message
* @param messageBody message body bytes
*/
event MessageReceived(
address indexed caller,
uint32 sourceDomain,
uint64 indexed nonce,
bytes32 sender,
bytes messageBody
);
/**
* @notice Emitted when max message body size is updated
* @param newMaxMessageBodySize new maximum message body size, in bytes
*/
event MaxMessageBodySizeUpdated(uint256 newMaxMessageBodySize);
// ============ Libraries ============
using TypedMemView for bytes;
using TypedMemView for bytes29;
using Message for bytes29;
// ============ State Variables ============
// Domain of chain on which the contract is deployed
uint32 public immutable localDomain;
// Message Format version
uint32 public immutable version;
// Maximum size of message body, in bytes.
// This value is set by owner.
uint256 public maxMessageBodySize;
// Next available nonce from this source domain
uint64 public nextAvailableNonce;
// Maps a bytes32 hash of (sourceDomain, nonce) -> uint256 (0 if unused, 1 if used)
mapping(bytes32 => uint256) public usedNonces;
// ============ Constructor ============
constructor(
uint32 _localDomain,
address _attester,
uint32 _maxMessageBodySize,
uint32 _version
) Attestable(_attester) {
localDomain = _localDomain;
maxMessageBodySize = _maxMessageBodySize;
version = _version;
}
// ============ External Functions ============
/**
* @notice Send the message to the destination domain and recipient
* @dev Increment nonce, format the message, and emit `MessageSent` event with message information.
* @param destinationDomain Domain of destination chain
* @param recipient Address of message recipient on destination chain as bytes32
* @param messageBody Raw bytes content of message
* @return nonce reserved by message
*/
function sendMessage(
uint32 destinationDomain,
bytes32 recipient,
bytes calldata messageBody
) external override whenNotPaused returns (uint64) {
bytes32 _emptyDestinationCaller = bytes32(0);
uint64 _nonce = _reserveAndIncrementNonce();
bytes32 _messageSender = Message.addressToBytes32(msg.sender);
_sendMessage(
destinationDomain,
recipient,
_emptyDestinationCaller,
_messageSender,
_nonce,
messageBody
);
return _nonce;
}
/**
* @notice Replace a message with a new message body and/or destination caller.
* @dev The `originalAttestation` must be a valid attestation of `originalMessage`.
* Reverts if msg.sender does not match sender of original message, or if the source domain of the original message
* does not match this MessageTransmitter's local domain.
* @param originalMessage original message to replace
* @param originalAttestation attestation of `originalMessage`
* @param newMessageBody new message body of replaced message
* @param newDestinationCaller the new destination caller, which may be the
* same as the original destination caller, a new destination caller, or an empty
* destination caller (bytes32(0), indicating that any destination caller is valid.)
*/
function replaceMessage(
bytes calldata originalMessage,
bytes calldata originalAttestation,
bytes calldata newMessageBody,
bytes32 newDestinationCaller
) external override whenNotPaused {
// Validate each signature in the attestation
_verifyAttestationSignatures(originalMessage, originalAttestation);
bytes29 _originalMsg = originalMessage.ref(0);
// Validate message format
_originalMsg._validateMessageFormat();
// Validate message sender
bytes32 _sender = _originalMsg._sender();
require(
msg.sender == Message.bytes32ToAddress(_sender),
"Sender not permitted to use nonce"
);
// Validate source domain
uint32 _sourceDomain = _originalMsg._sourceDomain();
require(
_sourceDomain == localDomain,
"Message not originally sent from this domain"
);
uint32 _destinationDomain = _originalMsg._destinationDomain();
bytes32 _recipient = _originalMsg._recipient();
uint64 _nonce = _originalMsg._nonce();
_sendMessage(
_destinationDomain,
_recipient,
newDestinationCaller,
_sender,
_nonce,
newMessageBody
);
}
/**
* @notice Send the message to the destination domain and recipient, for a specified `destinationCaller` on the
* destination domain.
* @dev Increment nonce, format the message, and emit `MessageSent` event with message information.
* WARNING: if the `destinationCaller` 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
* sendMessage() should be preferred for use cases where a specific destination caller is not required.
* @param destinationDomain Domain of destination chain
* @param recipient Address of message recipient on destination domain as bytes32
* @param destinationCaller caller on the destination domain, as bytes32
* @param messageBody Raw bytes content of message
* @return nonce reserved by message
*/
function sendMessageWithCaller(
uint32 destinationDomain,
bytes32 recipient,
bytes32 destinationCaller,
bytes calldata messageBody
) external override whenNotPaused returns (uint64) {
require(
destinationCaller != bytes32(0),
"Destination caller must be nonzero"
);
uint64 _nonce = _reserveAndIncrementNonce();
bytes32 _messageSender = Message.addressToBytes32(msg.sender);
_sendMessage(
destinationDomain,
recipient,
destinationCaller,
_messageSender,
_nonce,
messageBody
);
return _nonce;
}
/**
* @notice Receive a message. Messages with a given nonce
* can only be broadcast once for a (sourceDomain, destinationDomain)
* pair. The message body of a valid message is passed to the
* specified recipient for further processing.
*
* @dev Attestation format:
* A valid attestation is the concatenated 65-byte signature(s) of exactly
* `thresholdSignature` signatures, in increasing order of attester address.
* ***If the attester addresses recovered from signatures are not in
* increasing order, signature verification will fail.***
* If incorrect number of signatures or duplicate signatures are supplied,
* signature verification will fail.
*
* Message format:
* Field Bytes Type Index
* version 4 uint32 0
* sourceDomain 4 uint32 4
* destinationDomain 4 uint32 8
* nonce 8 uint64 12
* sender 32 bytes32 20
* recipient 32 bytes32 52
* messageBody dynamic bytes 84
* @param message Message bytes
* @param attestation Concatenated 65-byte signature(s) of `message`, in increasing order
* of the attester address recovered from signatures.
* @return success bool, true if successful
*/
function receiveMessage(bytes calldata message, bytes calldata attestation)
external
override
whenNotPaused
returns (bool success)
{
// Validate each signature in the attestation
_verifyAttestationSignatures(message, attestation);
bytes29 _msg = message.ref(0);
// Validate message format
_msg._validateMessageFormat();
// Validate domain
require(
_msg._destinationDomain() == localDomain,
"Invalid destination domain"
);
// Validate destination caller
if (_msg._destinationCaller() != bytes32(0)) {
require(
_msg._destinationCaller() ==
Message.addressToBytes32(msg.sender),
"Invalid caller for message"
);
}
// Validate version
require(_msg._version() == version, "Invalid message version");
// Validate nonce is available
uint32 _sourceDomain = _msg._sourceDomain();
uint64 _nonce = _msg._nonce();
bytes32 _sourceAndNonce = _hashSourceAndNonce(_sourceDomain, _nonce);
require(usedNonces[_sourceAndNonce] == 0, "Nonce already used");
// Mark nonce used
usedNonces[_sourceAndNonce] = 1;
// Handle receive message
bytes32 _sender = _msg._sender();
bytes memory _messageBody = _msg._messageBody().clone();
require(
IMessageHandler(Message.bytes32ToAddress(_msg._recipient()))
.handleReceiveMessage(_sourceDomain, _sender, _messageBody),
"handleReceiveMessage() failed"
);
// Emit MessageReceived event
emit MessageReceived(
msg.sender,
_sourceDomain,
_nonce,
_sender,
_messageBody
);
return true;
}
/**
* @notice Sets the max message body size
* @dev This value should not be reduced without good reason,
* to avoid impacting users who rely on large messages.
* @param newMaxMessageBodySize new max message body size, in bytes
*/
function setMaxMessageBodySize(uint256 newMaxMessageBodySize)
external
onlyOwner
{
maxMessageBodySize = newMaxMessageBodySize;
emit MaxMessageBodySizeUpdated(maxMessageBodySize);
}
// ============ Internal Utils ============
/**
* @notice Send the message to the destination domain and recipient. If `_destinationCaller` is not equal to bytes32(0),
* the message can only be received on the destination chain when called by `_destinationCaller`.
* @dev Format the message and emit `MessageSent` event with message information.
* @param _destinationDomain Domain of destination chain
* @param _recipient Address of message recipient on destination domain as bytes32
* @param _destinationCaller caller on the destination domain, as bytes32
* @param _sender message sender, as bytes32
* @param _nonce nonce reserved for message
* @param _messageBody Raw bytes content of message
*/
function _sendMessage(
uint32 _destinationDomain,
bytes32 _recipient,
bytes32 _destinationCaller,
bytes32 _sender,
uint64 _nonce,
bytes calldata _messageBody
) internal {
// Validate message body length
require(
_messageBody.length <= maxMessageBodySize,
"Message body exceeds max size"
);
require(_recipient != bytes32(0), "Recipient must be nonzero");
// serialize message
bytes memory _message = Message._formatMessage(
version,
localDomain,
_destinationDomain,
_nonce,
_sender,
_recipient,
_destinationCaller,
_messageBody
);
// Emit MessageSent event
emit MessageSent(_message);
}
/**
* @notice hashes `_source` and `_nonce`.
* @param _source Domain of chain where the transfer originated
* @param _nonce The unique identifier for the message from source to
destination
* @return hash of source and nonce
*/
function _hashSourceAndNonce(uint32 _source, uint64 _nonce)
internal
pure
returns (bytes32)
{
return keccak256(abi.encodePacked(_source, _nonce));
}
/**
* Reserve and increment next available nonce
* @return nonce reserved
*/
function _reserveAndIncrementNonce() internal returns (uint64) {
uint64 _nonceReserved = nextAvailableNonce;
nextAvailableNonce = nextAvailableNonce + 1;
return _nonceReserved;
}
}
As funções fornecidas pelo contrato Message Transmitter são as seguintes:
receiveMessage – Processa e valida uma mensagem recebida e sua atestação. Se for válida, aciona uma ação adicional com base no corpo da mensagem.
Parâmetros:
message
(bytes): A mensagem a ser processada, incluindo detalhes como remetente, destinatário e corpo da mensagem.attestation
(bytes): Assinaturas concatenadas de 65 bytes que atestam a validade da mensagem.
Retornos:
success
(boolean): Retornatrue
se for bem-sucedido, caso contrário, retornafalse
.
Emite:
MessageReceived
: Evento emitido quando uma nova mensagem é recebida.
Argumentos do Evento:
sendMessage – Envia uma mensagem para o domínio e destinatário de destino. Incrementa o nonce, atribui um nonce único à mensagem e emite o evento MessageSent
.
Parâmetros:
destinationDomain
(uint32): A rede blockchain de destino onde a mensagem será enviada.recipient
(bytes32): O endereço do destinatário no domínio de destino.messageBody
(bytes): O conteúdo em bytes da mensagem.
Retornos:
nonce
(uint64): Nonce único para esta mensagem.
Emite:
MessageSent
: Evento emitido quando a mensagem é enviada.
sendMessageWithCaller – Envia uma mensagem para o domínio de destino e destinatário, exigindo um chamador específico para acionar a mensagem na cadeia de destino. Incrementa o nonce, atribui um nonce único à mensagem e emite o evento MessageSent
.
Parâmetros:
destinationDomain
(uint32): A rede blockchain de destino onde a mensagem será enviada.recipient
(bytes32): O endereço do destinatário no domínio de destino.destinationCaller
(bytes32): O chamador no domínio de destino.messageBody
(bytes): O conteúdo em bytes da mensagem.
Retornos:
nonce
(uint64): Nonce único para esta mensagem.
Emite:
MessageSent
: Evento emitido quando a mensagem é enviada.
replaceMessage – Substitui uma mensagem original por um novo corpo de mensagem e/ou atualiza o chamador de destino. A mensagem de substituição reutiliza o _nonce
criado pela mensagem original.
Parâmetros:
originalMessage
(bytes): A mensagem original a ser substituída.originalAttestation
(bytes): Atuação que verifica a mensagem original.newMessageBody
(bytes): O novo conteúdo para a mensagem substituída.newDestinationCaller
(bytes32): O novo chamador de destino, que pode ser o mesmo que o chamador de destino original, um novo chamador de destino, ou um chamador de destino vazio (bytes32(0)), indicando que qualquer chamador de destino é válido.
Contrato Token Minter
O contrato Token Minter
gerencia a cunhagem e queima de tokens entre diferentes domínios de blockchain. Ele mantém um registro que vincula os tokens locais aos seus tokens remotos correspondentes, garantindo que os tokens mantenham uma taxa de câmbio 1:1 entre os domínios.
O contrato restringe as funções de cunhagem e queima a um Token Messenger
designado, que garante operações seguras e confiáveis entre as cadeias. Quando os tokens são queimados em um domínio remoto, uma quantidade equivalente é cunhada no domínio local para um destinatário especificado, e vice-versa.
Para aprimorar o controle e flexibilidade, o contrato inclui mecanismos para pausar operações, definir limites de queima e atualizar o Token Controller
, que regula as permissões de cunhagem de tokens. Além disso, fornece funcionalidades para adicionar ou remover o Token Messenger
local e recuperar o endereço do token local associado a um token remoto.
Contrato Token Minter
A maioria dos métodos do contrato Token Minter
pode ser chamada apenas pelo Token Messenger
registrado. No entanto, há um método acessível publicamente, uma função de leitura que permite que qualquer pessoa consulte o token local associado a um domínio e token remoto.
getLocalToken – Uma função somente de leitura que retorna o endereço do token local associado a um dado domínio remoto e token.
Parâmetros:
Nenhum.
Retornos:
O endereço do token local associado a um domínio remoto e token.
Como Interagir com os Contratos CCTP Antes de escrever seus próprios contratos, é essencial entender as funções e eventos principais dos contratos CCTP do Wormhole. A funcionalidade principal gira em torno dos seguintes processos:
Enviar tokens com um payload de mensagem – Inicia uma transferência entre cadeias de ativos compatíveis com a Circle junto com um payload de mensagem para um endereço de destino específico na cadeia de destino.
Receber tokens com um payload de mensagem – Valida mensagens recebidas de outras cadeias via Wormhole e, em seguida, cunha os tokens para o destinatário.
Enviando Tokens e Mensagens
Para iniciar uma transferência entre cadeias, você deve chamar o método transferTokensWithPayload
do contrato de Integração Circle do Wormhole (CCTP). Depois de iniciar a transferência, você deve obter a mensagem atestada do Wormhole e analisar os logs de transação para localizar uma mensagem de transferência emitida pelo contrato Bridge da Circle. Em seguida, deve-se enviar uma solicitação ao processo off-chain da Circle com a mensagem de transferência para obter a atestação da resposta do processo, que valida a cunhagem de tokens na cadeia de destino.
Para facilitar esse processo, você pode usar o SDK Solidity do Wormhole, que expõe o contrato WormholeRelayerSDK.sol
, incluindo o contrato abstrato CCTPSender
. Ao herdar esse contrato, você pode transferir USDC enquanto envia automaticamente o payload da mensagem para a cadeia de destino por meio de um relayer implantado no Wormhole.
Contrato CCTP Sender
O contrato abstrato CCTPSender
expõe a função sendUSDCWithPayloadToEvm
. Esta função publica uma transferência CCTP do valor especificado de USDC e solicita que a transferência seja entregue juntamente com um payload ao targetAddress
especificado na targetChain
.
Função sendUSDCWithPayloadToEvm
function sendUSDCWithPayloadToEvm(
uint16 targetChain,
address targetAddress,
bytes memory payload,
uint256 receiverValue,
uint256 gasLimit,
uint256 amount
) internal returns (uint64 sequence)
Parâmetros:
targetChain
: A cadeia de destino para onde os tokens e o payload serão enviados.targetAddress
: O endereço de destino na cadeia de destino.payload
: O payload da mensagem.receiverValue
: O valor a ser enviado ao destinatário.gasLimit
: Limite de gás para a transação.amount
: Quantidade de tokens a ser transferida.
Retornos:
sequence
: A sequência da transação.
Exemplo Completo Para visualizar um exemplo completo de criação de um contrato que interage com os contratos CCTP do Wormhole para enviar e receber USDC entre cadeias, confira o repositório "Hello USDC" no GitHub.
Last updated