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

}

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.

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 o depositForBurn é chamado. O destinationCaller é configurado como bytes32(0), permitindo que qualquer endereço chame o receiveMessage 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 do depositForBurn na medida em que a cunhagem no domínio de destino só pode ser chamada pelo endereço designado destinationCaller.

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 o depositForBurnWithCaller é 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 o replaceDepositForBurn é chamado. Observe que o destinationCaller 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 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.

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): Retorna true se a operação for bem-sucedida, caso contrário, retorna false.

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.

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): Retorna true se for bem-sucedido, caso contrário, retorna false.

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

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