Criação de contratos de transferência de tokens Cross-Chain
Neste parte, você aprenderá a criar um sistema simples de transferência de tokens cross-chain utilizando o protocolo Wormhole através do Wormhole Solidity SDK. Vamos guiá-la na construção e implantação de smart contracts que permitem transferências fluidas de tokens compatíveis com IERC-20 entre blockchains. Seja você uma desenvolvedora interessada em explorar aplicações cross-chain ou simplesmente curiosa sobre o protocolo Wormhole, este guia ajudará a compreender os fundamentos.
Ao final deste tutorial, você terá um sistema funcional de transferência de tokens cross-chain construído com as ferramentas poderosas fornecidas pelo Wormhole Solidity SDK, que poderá personalizar e integrar aos seus projetos.
Pré-requisitos
Antes de começar, certifique-se de ter o seguinte:
Node.js e npm instalados em sua máquina
Foundry para implantar contratos
Tokens de teste para Avalanche-Fuji e Celo-Alfajores para cobrir taxas de gas
Tokens USDC de teste no Avalanche-Fuji e/ou Celo-Alfajores para transferência cross-chain
Chave privada de uma wallet
Tokens Válidos para Transferência
É importante notar que este tutorial utiliza o TokenBridge da Wormhole para transferir tokens entre as blockchains. Assim, os tokens que você deseja transferir devem possuir uma attestation no contrato TokenBridge da blockchain de destino.
Para simplificar este processo, incluímos uma ferramenta para verificar se um token possui uma attestation na chain de destino. Esta ferramenta utiliza a função wrappedAsset do contrato TokenBridge. Se o token possuir uma attestation, a função wrappedAsset retorna o endereço do token "empacotado" na blockchain de destino; caso contrário, retorna o endereço zero.
Teste os tokens Válidos
Clone o repositório e navegue até o diretório do projeto
git clone https://github.com/wormhole-foundation/demo-cross-chain-token-transfer.git
cd cross-chain-token-transfers
Instale as dependências:
npm install
Execute o script para verificar a attestation do token:
npm run verify
Siga as instruções:
Insira a URL RPC da blockchain de destino
Insira o endereço do contrato TokenBridge na blockchain de destino
Insira o endereço do contrato do token na blockchain de origem
Insira o ID da blockchain de origem
A saída esperada quando o token tem uma atestação:
Usar esta ferramenta garante que você só tente transferir tokens com atestados verificados, evitando possíveis problemas durante o processo de transferência entre cadeias.
Configuração do Projeto
Vamos começar inicializando um novo projeto Foundry. Isso configurará uma estrutura básica para nossos contratos inteligentes.
Abra o terminal e execute o seguinte comando para inicializar um novo projeto com o Foundry:
forge init cross-chain-token-transfers
Isso criará um novo diretório chamado cross-chain-token-transfers com uma estrutura básica de projeto e também inicializará um repositório Git.
Para facilitar o desenvolvimento, utilizaremos o Wormhole Solidity SDK, que oferece ferramentas úteis para desenvolvimento cross-chain. Este SDK inclui as classes abstratas TokenSender e TokenReceiver, que simplificam o envio e o recebimento de tokens entre blockchains.
Construir Contratos Cross-Chain
Nesta seção, construiremos dois contratos inteligentes para enviar tokens de uma cadeia de origem e recebê-los em uma cadeia de destino. Esses contratos irão interagir com o protocolo Wormhole para facilitar transferências de tokens cross-chain de forma segura e eficiente.
De forma geral, nossos contratos irão:
Enviar tokens de uma blockchain para outra utilizando o protocolo Wormhole.
Receber e processar os tokens na cadeia de destino, garantindo que sejam transferidos corretamente para o destinatário previsto.
Antes de implementar os contratos, vamos detalhar os principais componentes.
Contrato de Envio: CrossChainSender
O contrato CrossChainSender é responsável por calcular o custo de envio de tokens entre cadeias e facilitar a transferência dos tokens.
Passo inicial para escrever o contrato CrossChainSender:
Crie um novo arquivo chamado CrossChainSender.sol no diretório /src:
touch src/CrossChainSender.sol
Abra o arquivo. Primeiro, começaremos com as importações e a configuração do contrato:
Isso configura a estrutura básica do contrato, incluindo os imports necessários e o construtor, que inicializa o contrato com os endereços relacionados ao Wormhole.
Agora, vamos adicionar uma função que estima o custo de envio de tokens entre blockchains:
Essa função, quoteCrossChainDeposit, auxilia no cálculo do custo de transferência de tokens para outra blockchain. Ela considera o custo de entrega e o custo de publicação de uma mensagem utilizando o protocolo Wormhole.
Por fim, adicionaremos a função que envia os tokens através das cadeias:
A função sendCrossChainDeposit é o núcleo do processo de transferência de tokens entre blockchains. Ela utiliza o protocolo Wormhole para enviar tokens ao destinatário na cadeia-alvo. Aqui está uma explicação detalhada das etapas envolvidas:
1. Cálculo do custo
A função inicia calculando o custo da transferência entre blockchains usando quoteCrossChainDeposit(targetChain).
Esse custo inclui a taxa de entrega e a taxa de publicação da mensagem no protocolo Wormhole.
Em seguida, a função verifica se o usuário enviou a quantidade correta de Ether para cobrir o custo, utilizando msg.value.
2. Transferência de tokens para o contrato
A próxima etapa transfere a quantidade especificada de tokens do usuário para o próprio contrato.
Isso é feito com IERC-20(token).transferFrom(msg.sender, address(this), amount).
Essa transferência garante que o contrato detenha os tokens antes de iniciar a transferência cross-chain.
3. Codificação do payload
O endereço do destinatário na cadeia-alvo é codificado em um payload usando abi.encode(recipient).
Esse payload acompanha a transferência dos tokens, permitindo que o contrato de destino saiba quem deve receber os tokens na cadeia de destino.
4. Transferência cross-chain
A função sendTokenWithPayloadToEvm é chamada para iniciar a transferência cross-chain. Ela:
Especifica a targetChain, que é o ID da cadeia Wormhole da blockchain de destino.
Envia o endereço do contrato targetReceiver na cadeia-alvo que receberá os tokens.
Anexa o payload contendo o endereço do destinatário.
Define o GAS_LIMITpara a transação.
Passa o endereço do token e a quantidade a ser transferida.
Essa chamada ativa o protocolo Wormhole para lidar com a mensagem cross-chain e a transferência de tokens, garantindo que os tokens e o payload cheguem ao destino correto na cadeia-alvo.
Código completo
O código completo para o arquivo CrossChainSender.sol está disponível e contém todas essas etapas implementadas detalhadamente.
// MessageSender.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
import "lib/wormhole-solidity-sdk/src/WormholeRelayerSDK.sol";
import "lib/wormhole-solidity-sdk/src/interfaces/IERC20.sol";
contract CrossChainSender is TokenSender {
uint256 constant GAS_LIMIT = 250_000;
constructor(
address _wormholeRelayer,
address _tokenBridge,
address _wormhole
) TokenBase(_wormholeRelayer, _tokenBridge, _wormhole) {}
// Function to get the estimated cost for cross-chain deposit
function quoteCrossChainDeposit(
uint16 targetChain
) public view returns (uint256 cost) {
uint256 deliveryCost;
(deliveryCost, ) = wormholeRelayer.quoteEVMDeliveryPrice(
targetChain,
0,
GAS_LIMIT
);
cost = deliveryCost + wormhole.messageFee();
}
// Function to send tokens and payload across chains
function sendCrossChainDeposit(
uint16 targetChain,
address targetReceiver,
address recipient,
uint256 amount,
address token
) public payable {
uint256 cost = quoteCrossChainDeposit(targetChain);
require(
msg.value == cost,
"msg.value must equal quoteCrossChainDeposit(targetChain)"
);
IERC20(token).transferFrom(msg.sender, address(this), amount);
bytes memory payload = abi.encode(recipient);
sendTokenWithPayloadToEvm(
targetChain,
targetReceiver,
payload,
0,
GAS_LIMIT,
token,
amount
);
}
}
Contrato Receptor: CrossChainReceiver
O contrato CrossChainReceiver é projetado para lidar com o recebimento de tokens e payloads de outra blockchain. Ele garante que os tokens sejam transferidos corretamente para o destinatário designado na cadeia de recebimento.
Vamos começar a escrever o contrato CrossChainReceiver:
Crie um novo arquivo chamado CrossChainReceiver.sol no diretório /src:
touch src/CrossChainReceiver.sol
Abra o arquivo. Primeiro, começaremos com os imports e a configuração do contrato:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
import "lib/wormhole-solidity-sdk/src/WormholeRelayerSDK.sol";
import "lib/wormhole-solidity-sdk/src/interfaces/IERC20.sol";
contract CrossChainReceiver is TokenReceiver {
// O Wormhole relayer e os registeredSenders são herdados do contrato Base.sol
constructor(
address _wormholeRelayer,
address _tokenBridge,
address _wormhole
) TokenBase(_wormholeRelayer, _tokenBridge, _wormhole) {}
Semelhante ao contrato CrossChainSender, isso configura a estrutura básica do contrato, incluindo os imports necessários e o construtor que inicializa o contrato com os endereços relacionados ao Wormhole.
Em seguida, vamos adicionar uma função para lidar com o recebimento da payload e dos tokens:
function receivePayloadAndTokens(
bytes memory payload,
TokenReceived[] memory receivedTokens,
bytes32 sourceAddress,
uint16 sourceChain,
bytes32 // deliveryHash
)
internal
override
onlyWormholeRelayer
isRegisteredSender(sourceChain, sourceAddress)
{
require(receivedTokens.length == 1, "Expected 1 token transfer");
// Decodifica o endereço do destinatário a partir da payload
address recipient = abi.decode(payload, (address));
// Transfere os tokens recebidos para o destinatário
IERC20(receivedTokens[0].tokenAddress).transfer(
recipient,
receivedTokens[0].amount
);
}
A função receivePayloadAndTokens processa os tokens e a payload enviados de outra cadeia, decodifica o endereço do destinatário e transfere os tokens para ele usando o protocolo Wormhole. Esta função também valida o emissor (sourceAddress) para garantir que a mensagem veio de um remetente confiável.
Essa função garante que:
Apenas uma transferência de token é processada por vez.
O sourceAddress é verificado em uma lista de remetentes registrados usando o modificador isRegisteredSender, que confirma se o emissor tem permissão para enviar tokens para este contrato.
O endereço do destinatário é decodificado da payload, e os tokens recebidos são transferidos para ele utilizando a interface ERC-20.
Após chamarmos sendTokenWithPayloadToEvm na cadeia de origem, a mensagem passa pelo ciclo de vida padrão da mensagem Wormhole. Quando um VAA (Verifiable Action Approval) estiver disponível, o provedor de entrega chamará receivePayloadAndTokens na cadeia de destino e no endereço de destino especificado, com os parâmetros apropriados.
Início da implementação
Para começar a implementar o CrossChainReceiver, siga os passos abaixo:
Criação do arquivo do contrato:
Crie um novo arquivo chamado CrossChainReceiver.sol no diretório /src do projeto
touch src/CrossChainReceiver.sol
Estrutura inicial do contrato:
O contrato incluirá as seguintes partes:
Importações necessárias do Wormhole Solidity SDK.
Declaração do contrato e configuração inicial.
Definição inicial do contrato:
Aqui está a estrutura básica:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@wormhole-foundation/wormhole-solidity-sdk/contracts/interfaces/IWormholeReceiver.sol";
import "@wormhole-foundation/wormhole-solidity-sdk/contracts/interfaces/IERC20.sol";
contract CrossChainReceiver is IWormholeReceiver {
address public tokenAddress;
constructor(address _tokenAddress) {
tokenAddress = _tokenAddress;
}
}
Descrição do código:
O contrato implementa a interface IWormholeReceiver para garantir compatibilidade com o protocolo Wormhole.
A variável tokenAddress armazena o endereço do token suportado pelo contrato.
O construtor inicializa o contrato com o endereço do token.
Próximos passos
Com a estrutura básica definida, podemos avançar para implementar as funções principais, como:
Processar mensagens recebidas do protocolo Wormhole.
Transferir tokens para o destinatário correto na cadeia receptora.
Entendendo a Tokenreceived Struct
Vamos detalhar os campos fornecidos na estrutura TokenReceived:
tokenHomeAddress – o endereço original do token na sua cadeia nativa. Este é o mesmo endereço do token utilizado na chamada para sendTokenWithPayloadToEvm, a menos que o token enviado originalmente seja um token embalado no Wormhole. Nesse caso, este será o endereço da versão original do token (na sua cadeia nativa) no formato de endereço do Wormhole (com 12 zeros à esquerda).
tokenHomeChain – o ID da cadeia do Wormhole correspondente ao endereço original acima. Normalmente, isso será a cadeia de origem, a menos que o token original enviado seja um ativo embalado no Wormhole, caso em que o ID da cadeia será o da versão desembrulhada do token.
tokenAddress – o endereço do token IERC-20 na cadeia de destino que foi transferido para este contrato. Se tokenHomeChain for igual à cadeia de destino, este valor será o mesmo que o tokenHomeAddress; caso contrário, será a versão embalado no Wormhole do token enviado.
amount – a quantidade de tokens enviada para este contrato, com as mesmas unidades do token original. Como o TokenBridge envia com precisão de oito casas decimais, se o token tiver 18 casas decimais, esse valor será a quantidade enviada, arredondada para o múltiplo mais próximo de 10^10.
amountNormalized – a quantidade de tokens dividida por (1 se o número de casas decimais for ≤ 8, caso contrário, 10^(decimais - 8)).
Você pode encontrar o código completo para o contrato CrossChainReceiver.sol abaixo:
CrossChainReceiver.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
import "lib/wormhole-solidity-sdk/src/WormholeRelayerSDK.sol";
import "lib/wormhole-solidity-sdk/src/interfaces/IERC20.sol";
contract CrossChainReceiver is TokenReceiver {
// O Wormhole relayer e os registeredSenders são herdados do contrato Base.sol
constructor(
address _wormholeRelayer,
address _tokenBridge,
address _wormhole
) TokenBase(_wormholeRelayer, _tokenBridge, _wormhole) {}
// Função para receber o payload cross-chain e os tokens com validação do emitter
function receivePayloadAndTokens(
bytes memory payload,
TokenReceived[] memory receivedTokens,
bytes32 sourceAddress,
uint16 sourceChain,
bytes32 // deliveryHash
)
internal
override
onlyWormholeRelayer
isRegisteredSender(sourceChain, sourceAddress)
{
require(receivedTokens.length == 1, "Expected 1 token transfer");
// Decodifica o endereço do destinatário a partir do payload
address recipient = abi.decode(payload, (address));
// Transfere os tokens recebidos para o destinatário pretendido
IERC20(receivedTokens[0].tokenAddress).transfer(
recipient,
receivedTokens[0].amount
);
}
}
Implantação dos Contratos
Agora que você escreveu os contratos CrossChainSender e CrossChainReceiver, é hora de implantá-los nas redes escolhidas.
Configuração da Implantação
Antes de realizar a implantação, você precisa configurar as redes e o ambiente de implantação. Essas informações serão armazenadas em um arquivo de configuração.
Crie um diretório chamado deploy-config no diretório raiz do seu projeto:
bashCopiar códigomkdir deploy-config
Crie um arquivo config.json no diretório deploy-config:
bashCopiar códigotouch deploy-config/config.json
Abra o arquivo config.json e adicione a seguinte configuração:
Este arquivo especifica os detalhes de cada cadeia onde você planeja implantar seus contratos, incluindo a URL RPC, o endereço do TokenBridge, o Wormhole relayer, e o Wormhole Core Contract.
Nota: Você pode adicionar as cadeias desejadas a este arquivo, especificando os campos necessários para cada uma. Neste exemplo, usamos as testnets Avalanche Fuji e Celo Alfajores.
Crie um arquivo contracts.json no diretório deploy-config:
echo '{}' > deploy-config/contracts.json
Este arquivo pode ser deixado em branco inicialmente. Ele será atualizado automaticamente com os endereços dos contratos implantados após uma implantação bem-sucedida.
Configuração do Ambiente Node.js
Você precisará configurar o ambiente Node.js para executar o script de implantação.
Essas dependências são necessárias para o correto funcionamento do script de implantação.
Compile seus Contratos Inteligentes
Compile seus contratos usando Foundry. Isso garante que seus contratos estejam atualizados e prontos para a implantação.
Execute o seguinte comando para compilar seus contratos:
forge build
Isso gerará os arquivos necessários ABI e bytecode em um diretório chamado /out.
Escreva o Script de Implantação
Você precisará de um script para automatizar a implantação de seus contratos. Vamos criar o script de implantação.
Crie um novo arquivo chamado deploy.ts no diretório /script:
touch script/deploy.ts
Abra o arquivo e carregue as importações e a configuração:
import { BytesLike, ethers } from 'ethers';
import * as fs from 'fs';
import * as path from 'path';
import * as dotenv from 'dotenv';
import readlineSync from 'readline-sync';
dotenv.config();
Aqui, estamos importando as bibliotecas necessárias para interagir com o Ethereum, manipular caminhos de arquivos, carregar variáveis de ambiente e permitir interação com o usuário via terminal.
Defina as interfaces para a configuração da cadeia e implantação do contrato:
Essas interfaces definem a estrutura da configuração da cadeia e os detalhes da implantação do contrato.
Carregue e selecione as cadeias para a implantação:
function loadConfig(): ChainConfig[] {
const configPath = path.resolve(__dirname, '../deploy-config/config.json');
return JSON.parse(fs.readFileSync(configPath, 'utf8')).chains;
}
function selectChain(
chains: ChainConfig[],
role: 'source' | 'target'
): ChainConfig {
console.log(`\nSelect the ${role.toUpperCase()} chain:`);
chains.forEach((chain, index) => {
console.log(`${index + 1}: ${chain.description}`);
});
const chainIndex =
readlineSync.questionInt(
`\nEnter the number for the ${role.toUpperCase()} chain: `
) - 1;
return chains[chainIndex];
}
A função loadConfig lê a configuração da cadeia a partir do arquivo config.json, e a função selectChain permite ao usuário escolher as cadeias de origem e destino para a implantação interativamente.
Defina a função principal para a implantação e carregue a configuração da cadeia:
código const sourceProvider = new ethers.JsonRpcProvider(sourceChain.rpc);
const targetProvider = new ethers.JsonRpcProvider(targetChain.rpc);
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY!, sourceProvider);
Os scripts estabelecem uma conexão com a blockchain usando um provedor e criam uma instância de carteira usando uma chave privada. Esta carteira será responsável por assinar a transação de implantação na cadeia de origem.
Após implantar o contrato CrossChainReceiver na rede de destino, o endereço do contrato do remetente da cadeia de origem precisa ser registrado. Isso garante que apenas mensagens do contrato CrossChainSender registrado sejam processadas.
Este passo adicional é essencial para aplicar a validação do emissor, impedindo que remetentes não autorizados entreguem mensagens ao contrato CrossChainReceiver.
Salvar os detalhes do deploy:
Exemplo de Salvamento dos Detalhes de Implantação:
const deployedContractsPath = path.resolve(
__dirname,
'../deploy-config/contracts.json'
);
let deployedContracts: DeployedContracts = {};
if (fs.existsSync(deployedContractsPath)) {
deployedContracts = JSON.parse(
fs.readFileSync(deployedContractsPath, 'utf8')
);
}
// Atualiza o arquivo contracts.json:
// Se um contrato já existe em uma blockchain, atualiza seu endereço; caso contrário, adiciona uma nova entrada.
if (!deployedContracts[sourceChain.chainId]) {
deployedContracts[sourceChain.chainId] = {
networkName: sourceChain.description,
deployedAt: new Date().toISOString(),
};
}
deployedContracts[sourceChain.chainId].CrossChainSender =
senderAddress.toString();
deployedContracts[sourceChain.chainId].deployedAt =
new Date().toISOString();
if (!deployedContracts[targetChain.chainId]) {
deployedContracts[targetChain.chainId] = {
networkName: targetChain.description,
deployedAt: new Date().toISOString(),
};
}
deployedContracts[targetChain.chainId].CrossChainReceiver =
receiverAddress.toString();
deployedContracts[targetChain.chainId].deployedAt =
new Date().toISOString();
// Salva o arquivo contracts.json atualizado
fs.writeFileSync(
deployedContractsPath,
JSON.stringify(deployedContracts, null, 2)
);
Esta função salva os detalhes de implantação, registrando as informações sobre o contrato implantado, incluindo os endereços do CrossChainSender e CrossChainReceiver, e atualiza o arquivo contracts.json com os dados mais recentes. Se o contrato já existir na rede, ele será atualizado, caso contrário, uma nova entrada será adicionada.
Adicione sua lógica desejada para salvar os endereços dos contratos implantados em um arquivo JSON (ou outro formato). Isso será importante mais tarde ao transferir tokens, pois você precisará desses endereços para interagir com os contratos implantados.
Trate erros e finalize o script:
catch (error: any) {
if (error.code === 'INSUFFICIENT_FUNDS') {
console.error(
'Erro: Fundos insuficientes para o deployment. Por favor, verifique se sua carteira tem fundos suficientes para cobrir as taxas de gas.'
);
} else {
console.error('Ocorreu um erro inesperado:', error.message);
}
process.exit(1);
}
A estrutura try-catch envolve a lógica de implantação para capturar quaisquer erros que possam ocorrer.
Se o erro for devido a fundos insuficientes, ele registra uma mensagem clara sobre a necessidade de mais taxas de gas.
Para outros erros, registra a mensagem de erro específica para ajudar no processo de depuração.
O process.exit(1) garante que o script seja finalizado com um código de status de falha caso ocorra algum erro.
Você pode encontrar o código completo do arquivo deploy.ts abaixo:
import { BytesLike, ethers } from 'ethers';
import * as fs from 'fs';
import * as path from 'path';
import * as dotenv from 'dotenv';
import readlineSync from 'readline-sync';
dotenv.config();
interface ChainConfig {
description: string;
chainId: number;
rpc: string;
tokenBridge: string;
wormholeRelayer: string;
wormhole: string;
}
interface DeployedContracts {
[chainId: number]: {
networkName: string;
CrossChainSender?: string;
CrossChainReceiver?: string;
deployedAt: string;
};
}
function loadConfig(): ChainConfig[] {
const configPath = path.resolve(__dirname, '../deploy-config/config.json');
return JSON.parse(fs.readFileSync(configPath, 'utf8')).chains;
}
function selectChain(
chains: ChainConfig[],
role: 'source' | 'target'
): ChainConfig {
console.log(`\nSelect the ${role.toUpperCase()} chain:`);
chains.forEach((chain, index) => {
console.log(`${index + 1}: ${chain.description}`);
});
const chainIndex =
readlineSync.questionInt(
`\nEnter the number for the ${role.toUpperCase()} chain: `
) - 1;
return chains[chainIndex];
}
async function main() {
const chains = loadConfig();
const sourceChain = selectChain(chains, 'source');
const targetChain = selectChain(chains, 'target');
const sourceProvider = new ethers.JsonRpcProvider(sourceChain.rpc);
const targetProvider = new ethers.JsonRpcProvider(targetChain.rpc);
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY!, sourceProvider);
const senderJson = JSON.parse(
fs.readFileSync(
path.resolve(
__dirname,
'../out/CrossChainSender.sol/CrossChainSender.json'
),
'utf8'
)
);
const abi = senderJson.abi;
const bytecode = senderJson.bytecode;
const CrossChainSenderFactory = new ethers.ContractFactory(
abi,
bytecode,
wallet
);
try {
const senderContract = await CrossChainSenderFactory.deploy(
sourceChain.wormholeRelayer,
sourceChain.tokenBridge,
sourceChain.wormhole
);
await senderContract.waitForDeployment();
// Acessando com segurança o endereço do contrato implantado
const senderAddress = (senderContract as ethers.Contract).target;
console.log(
`CrossChainSender na ${sourceChain.description}: ${senderAddress}`
);
const targetWallet = new ethers.Wallet(
process.env.PRIVATE_KEY!,
targetProvider
);
const receiverJson = JSON.parse(
fs.readFileSync(
path.resolve(
__dirname,
'../out/CrossChainReceiver.sol/CrossChainReceiver.json'
),
'utf8'
)
);
const CrossChainReceiverFactory = new ethers.ContractFactory(
receiverJson.abi,
receiverJson.bytecode,
targetWallet
);
const receiverContract = await CrossChainReceiverFactory.deploy(
targetChain.wormholeRelayer,
targetChain.tokenBridge,
targetChain.wormhole
);
await receiverContract.waitForDeployment();
// Acessando com segurança o endereço do contrato implantado
const receiverAddress = (receiverContract as ethers.Contract).target;
console.log(
`CrossChainReceiver na ${targetChain.description}: ${receiverAddress}`
);
// Registrando o contrato de envio no contrato receptor
console.log(
`Registrando CrossChainSender (${senderAddress}) como um remetente válido no CrossChainReceiver (${receiverAddress})...`
);
const CrossChainReceiverContract = new ethers.Contract(
receiverAddress,
receiverJson.abi,
targetWallet
);
const tx = await CrossChainReceiverContract.setRegisteredSender(
sourceChain.chainId,
ethers.zeroPadValue(senderAddress as BytesLike, 32)
);
await tx.wait();
console.log(
`CrossChainSender registrado como remetente válido na ${targetChain.description}`
);
// Carregar os endereços dos contratos implantados existentes de contracts.json
const deployedContractsPath = path.resolve(
__dirname,
'../deploy-config/contracts.json'
);
let deployedContracts: DeployedContracts = {};
if (fs.existsSync(deployedContractsPath)) {
deployedContracts = JSON.parse(
fs.readFileSync(deployedContractsPath, 'utf8')
);
}
// Atualizar o arquivo contracts.json:
// Se um contrato já existe em uma cadeia, atualize seu endereço; caso contrário, adicione uma nova entrada.
if (!deployedContracts[sourceChain.chainId]) {
deployedContracts[sourceChain.chainId] = {
networkName: sourceChain.description,
deployedAt: new Date().toISOString(),
};
}
deployedContracts[sourceChain.chainId].CrossChainSender =
senderAddress.toString();
deployedContracts[sourceChain.chainId].deployedAt =
new Date().toISOString();
if (!deployedContracts[targetChain.chainId]) {
deployedContracts[targetChain.chainId] = {
networkName: targetChain.description,
deployedAt: new Date().toISOString(),
};
}
deployedContracts[targetChain.chainId].CrossChainReceiver =
receiverAddress.toString();
deployedContracts[targetChain.chainId].deployedAt =
new Date().toISOString();
// Salvar o arquivo contracts.json atualizado
fs.writeFileSync(
deployedContractsPath,
JSON.stringify(deployedContracts, null, 2)
);
} catch (error: any) {
if (error.code === 'INSUFFICIENT_FUNDS') {
console.error(
'Erro: Fundos insuficientes para a implantação. Verifique se sua carteira possui fundos suficientes para cobrir as taxas de gas.'
);
} else {
console.error('Ocorreu um erro inesperado:', error.message);
}
process.exit(1);
}
}
main().catch((error) => {
console.error(error);
process.exit(1);
});
Adicione sua chave privada - você precisará fornecer sua chave privada. Ela permite que o seu script de implantação assine as transações que implantam os contratos inteligentes na blockchain. Sem ela, o script não será capaz de interagir com a blockchain em seu nome.
Crie um arquivo .env na raiz do projeto e adicione sua chave privada:
bashCopiar códigotouch .env
Dentro do arquivo .env, adicione sua chave privada no seguinte formato:
I - Abra um terminal e execute o seguinte comando:
bashCopiar códigonpx ts-node script/deploy.ts
Isso executará o script de implantação, implantando ambos os contratos nas cadeias selecionadas.
II - Verifique a saída da implantação:
Você verá os endereços dos contratos implantados impressos no terminal, caso tenha sido bem-sucedido. O arquivo contracts.json será atualizado com esses endereços.
Se você encontrar um erro, o script fornecerá um feedback, como fundos insuficientes para pagar o gas.
Se você seguiu a lógica fornecida no código completo acima, a saída do seu terminal deve ser algo assim:
Transferir Tokens Entre Cadeias
Resumo Rápido
Até este ponto, você configurou um novo projeto Solidity usando o Foundry, desenvolveu dois contratos principais (CrossChainSender e CrossChainReceiver) e criou um script de implantação para implantar esses contratos em diferentes redes blockchain. O script de implantação também salva os endereços dos contratos para fácil referência. Com tudo pronto, agora é hora de transferir tokens usando os contratos implantados.
Neste passo, você escreverá um script para transferir tokens entre cadeias utilizando os contratos CrossChainSender e CrossChainReceiver que você implantou anteriormente. Este script irá interagir com os contratos e facilitar a transferência de tokens entre cadeias.
Script de Transferência
Configuração do Script de Transferência
Crie um novo arquivo chamado transfer.ts no diretório /script:
bashCopiar códigotouch script/transfer.ts
Abra o arquivo. Comece com as importações necessárias, interfaces e configurações:
import { ethers } from 'ethers';
import * as fs from 'fs';
import * as path from 'path';
import * as dotenv from 'dotenv';
import readlineSync from 'readline-sync';
dotenv.config();
interface ChainConfig {
description: string;
chainId: number;
rpc: string;
tokenBridge: string;
wormholeRelayer: string;
wormhole: string;
}
interface DeployedContracts {
[chainId: number]: {
networkName: string;
CrossChainSender?: string;
CrossChainReceiver?: string;
deployedAt: string;
};
}
Essas importações incluem as bibliotecas essenciais para interagir com o Ethereum, manipular caminhos de arquivos, carregar variáveis de ambiente e gerenciar entradas do usuário.
Carregar Configuração e Contratos:
function loadConfig(): ChainConfig[] {
const configPath = path.resolve(__dirname, '../deploy-config/config.json');
return JSON.parse(fs.readFileSync(configPath, 'utf8')).chains;
}
function loadDeployedContracts(): DeployedContracts {
const contractsPath = path.resolve(
__dirname,
'../deploy-config/contracts.json'
);
if (
!fs.existsSync(contractsPath) ||
fs.readFileSync(contractsPath, 'utf8').trim() === ''
) {
console.error(
'Nenhum contrato encontrado. Por favor, implante os contratos primeiro antes de transferir tokens.'
);
process.exit(1);
}
return JSON.parse(fs.readFileSync(contractsPath, 'utf8'));
}
Essas funções carregam os detalhes da rede e dos contratos que foram salvos durante a implantação.
Permitir que os usuários selecionem as cadeias de origem e destino:
Considere os contratos implantados e crie a lógica conforme necessário. No nosso exemplo, tornamos esse processo interativo, permitindo que os usuários selecionem as cadeias de origem e destino entre todos os contratos implantados historicamente. Essa abordagem interativa ajuda a garantir que as cadeias corretas sejam selecionadas para a transferência de tokens.
function selectSourceChain(deployedContracts: DeployedContracts): {
chainId: number;
networkName: string;
} {
const sourceOptions = Object.entries(deployedContracts).filter(
([, contracts]) => contracts.CrossChainSender
);
if (sourceOptions.length === 0) {
console.error('Nenhuma cadeia de origem disponível com CrossChainSender implantado.');
process.exit(1);
}
console.log('\nSelecione a cadeia de origem:');
sourceOptions.forEach(([chainId, contracts], index) => {
console.log(`${index + 1}: ${contracts.networkName}`);
});
const selectedIndex =
readlineSync.questionInt(`\nDigite o número da cadeia de origem: `) - 1;
return {
chainId: Number(sourceOptions[selectedIndex][0]),
networkName: sourceOptions[selectedIndex][1].networkName,
};
}
function selectTargetChain(deployedContracts: DeployedContracts): {
chainId: number;
networkName: string;
} {
const targetOptions = Object.entries(deployedContracts).filter(
([, contracts]) => contracts.CrossChainReceiver
);
if (targetOptions.length === 0) {
console.error(
'Nenhuma cadeia de destino disponível com CrossChainReceiver implantado.'
);
process.exit(1);
}
console.log('\nSelecione a cadeia de destino:');
targetOptions.forEach(([chainId, contracts], index) => {
console.log(`${index + 1}: ${contracts.networkName}`);
});
const selectedIndex =
readlineSync.questionInt(`\nDigite o número da cadeia de destino: `) - 1;
return {
chainId: Number(targetOptions[selectedIndex][0]),
networkName: targetOptions[selectedIndex][1].networkName,
};
}
Implementar a Lógica de Transferência de Tokens
Inicie a função principal:
async function main() {
const chains = loadConfig();
const deployedContracts = loadDeployedContracts();
// Selecione a cadeia de origem (apenas exibe cadeias com CrossChainSender implantado)
const { chainId: sourceChainId, networkName: sourceNetworkName } =
selectSourceChain(deployedContracts);
const sourceChain = chains.find((chain) => chain.chainId === sourceChainId)!;
// Selecione a cadeia de destino (apenas exibe cadeias com CrossChainReceiver implantado)
const { chainId: targetChainId, networkName: targetNetworkName } =
selectTargetChain(deployedContracts);
const targetChain = chains.find((chain) => chain.chainId === targetChainId)!;
// Configurar provedores e carteiras
const sourceProvider = new ethers.JsonRpcProvider(sourceChain.rpc);
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY!, sourceProvider);
// Carregar o ABI do arquivo JSON (use o ABI compilado do Forge ou Hardhat)
const CrossChainSenderArtifact = JSON.parse(
fs.readFileSync(
path.resolve(
__dirname,
'../out/CrossChainSender.sol/CrossChainSender.json'
),
'utf8'
)
);
const abi = CrossChainSenderArtifact.abi;
// Criar a instância do contrato usando o ABI completo
const CrossChainSender = new ethers.Contract(
deployedContracts[sourceChainId].CrossChainSender!,
abi,
wallet
);
A função principal é onde a lógica de transferência de tokens residirá. Ela carrega os detalhes da cadeia e dos contratos, configura a carteira e o provedor, e carrega o contrato CrossChainSender.
Solicitar os Detalhes da Transferência de Tokens ao Usuário:
Agora, você pedirá ao usuário o endereço do contrato do token, o endereço do destinatário na cadeia de destino e a quantidade de tokens a transferir.
const tokenAddress = readlineSync.question(
'Digite o endereço do contrato do token: '
);
const recipientAddress = readlineSync.question(
'Digite o endereço do destinatário na cadeia de destino: '
);
// Obter o contrato do token
const tokenContractDecimals = new ethers.Contract(
tokenAddress,
[
'function decimals() view returns (uint8)',
'function approve(address spender, uint256 amount) public returns (bool)',
],
wallet
);
// Obter os decimais do token
const decimals = await tokenContractDecimals.decimals();
// Obter a quantidade do usuário e analisar conforme os decimais do token
const amount = ethers.parseUnits(
readlineSync.question('Digite a quantidade de tokens a transferir: '),
decimals
);
Esta seção do script solicita ao usuário o endereço do contrato do token e o endereço do destinatário, obtém o valor decimal do token e analisa a quantidade de acordo.
Iniciar a Transferência:
Finalmente, inicie a transferência entre as cadeias e registre os detalhes.
const cost = await CrossChainSender.quoteCrossChainDeposit(targetChainId);
// Aprovar o contrato CrossChainSender para transferir tokens em nome do usuário
const tokenContract = new ethers.Contract(
tokenAddress,
['function approve(address spender, uint256 amount) public returns (bool)'],
wallet
);
const approveTx = await tokenContract.approve(
deployedContracts[sourceChainId].CrossChainSender!,
amount
);
await approveTx.wait();
console.log(`Tokens aprovados para transferência entre cadeias.`);
// Iniciar a transferência entre as cadeias
const transferTx = await CrossChainSender.sendCrossChainDeposit(
targetChainId,
deployedContracts[targetChainId].CrossChainReceiver!,
recipientAddress,
amount,
tokenAddress,
{ value: cost } // Anexar a taxa necessária para a transferência entre cadeias
);
await transferTx.wait();
console.log(
`Transferência iniciada de ${sourceNetworkName} para ${targetNetworkName}. Hash da transação: ${transferTx.hash}`
);
Esta parte do script primeiro aprova a transferência de tokens, depois inicia a transferência entre as cadeias usando o contrato CrossChainSender e finalmente registra o hash da transação para o usuário acompanhar.
Esta seção finaliza o script chamando a função principal e tratando quaisquer erros que possam ocorrer durante o processo de transferência de tokens.
Você pode encontrar o código completo para o arquivo transfer.ts abaixo:
import { ethers } from 'ethers';
import * as fs from 'fs';
import * as path from 'path';
import * as dotenv from 'dotenv';
import readlineSync from 'readline-sync';
dotenv.config();
interface ChainConfig {
description: string;
chainId: number;
rpc: string;
tokenBridge: string;
wormholeRelayer: string;
wormhole: string;
}
interface DeployedContracts {
[chainId: number]: {
networkName: string;
CrossChainSender?: string;
CrossChainReceiver?: string;
deployedAt: string;
};
}
function loadConfig(): ChainConfig[] {
const configPath = path.resolve(__dirname, '../deploy-config/config.json');
return JSON.parse(fs.readFileSync(configPath, 'utf8')).chains;
}
function loadDeployedContracts(): DeployedContracts {
const contractsPath = path.resolve(
__dirname,
'../deploy-config/contracts.json'
);
if (
!fs.existsSync(contractsPath) ||
fs.readFileSync(contractsPath, 'utf8').trim() === ''
) {
console.error(
'No contracts found. Please deploy contracts first before transferring tokens.'
);
process.exit(1);
}
return JSON.parse(fs.readFileSync(contractsPath, 'utf8'));
}
function selectSourceChain(deployedContracts: DeployedContracts): {
chainId: number;
networkName: string;
} {
const sourceOptions = Object.entries(deployedContracts).filter(
([, contracts]) => contracts.CrossChainSender
);
if (sourceOptions.length === 0) {
console.error('No source chains available with CrossChainSender deployed.');
process.exit(1);
}
console.log('\nSelect the source chain:');
sourceOptions.forEach(([chainId, contracts], index) => {
console.log(`${index + 1}: ${contracts.networkName}`);
});
const selectedIndex =
readlineSync.questionInt(`\nEnter the number for the source chain: `) - 1;
return {
chainId: Number(sourceOptions[selectedIndex][0]),
networkName: sourceOptions[selectedIndex][1].networkName,
};
}
function selectTargetChain(deployedContracts: DeployedContracts): {
chainId: number;
networkName: string;
} {
const targetOptions = Object.entries(deployedContracts).filter(
([, contracts]) => contracts.CrossChainReceiver
);
if (targetOptions.length === 0) {
console.error(
'No target chains available with CrossChainReceiver deployed.'
);
process.exit(1);
}
console.log('\nSelect the target chain:');
targetOptions.forEach(([chainId, contracts], index) => {
console.log(`${index + 1}: ${contracts.networkName}`);
});
const selectedIndex =
readlineSync.questionInt(`\nEnter the number for the target chain: `) - 1;
return {
chainId: Number(targetOptions[selectedIndex][0]),
networkName: targetOptions[selectedIndex][1].networkName,
};
}
async function main() {
const chains = loadConfig();
const deployedContracts = loadDeployedContracts();
// Select the source chain (only show chains with CrossChainSender deployed)
const { chainId: sourceChainId, networkName: sourceNetworkName } =
selectSourceChain(deployedContracts);
const sourceChain = chains.find((chain) => chain.chainId === sourceChainId)!;
// Select the target chain (only show chains with CrossChainReceiver deployed)
const { chainId: targetChainId, networkName: targetNetworkName } =
selectTargetChain(deployedContracts);
const targetChain = chains.find((chain) => chain.chainId === targetChainId)!;
// Set up providers and wallets
const sourceProvider = new ethers.JsonRpcProvider(sourceChain.rpc);
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY!, sourceProvider);
// Load the ABI from the JSON file (use the compiled ABI from Forge or Hardhat)
const CrossChainSenderArtifact = JSON.parse(
fs.readFileSync(
path.resolve(
__dirname,
'../out/CrossChainSender.sol/CrossChainSender.json'
),
'utf8'
)
);
const abi = CrossChainSenderArtifact.abi;
// Create the contract instance using the full ABI
const CrossChainSender = new ethers.Contract(
deployedContracts[sourceChainId].CrossChainSender!,
abi,
wallet
);
// Display the selected chains
console.log(
`\nInitiating transfer from ${sourceNetworkName} to ${targetNetworkName}.`
);
// Ask the user for token transfer details
const tokenAddress = readlineSync.question(
'Enter the token contract address: '
);
const recipientAddress = readlineSync.question(
'Enter the recipient address on the target chain: '
);
// Get the token contract
const tokenContractDecimals = new ethers.Contract(
tokenAddress,
[
'function decimals() view returns (uint8)',
'function approve(address spender, uint256 amount) public returns (bool)',
],
wallet
);
// Fetch the token decimals
const decimals = await tokenContractDecimals.decimals();
// Get the amount from the user, then parse it according to the token's decimals
const amount = ethers.parseUnits(
readlineSync.question('Enter the amount of tokens to transfer: '),
decimals
);
// Calculate the cross-chain transfer cost
const cost = await CrossChainSender.quoteCrossChainDeposit(targetChainId);
// Approve the CrossChainSender contract to transfer tokens on behalf of the user
const tokenContract = new ethers.Contract(
tokenAddress,
['function approve(address spender, uint256 amount) public returns (bool)'],
wallet
);
const approveTx = await tokenContract.approve(
deployedContracts[sourceChainId].CrossChainSender!,
amount
);
await approveTx.wait();
console.log(`Approved tokens for cross-chain transfer.`);
// Initiate the cross-chain transfer
const transferTx = await CrossChainSender.sendCrossChainDeposit(
targetChainId,
deployedContracts[targetChainId].CrossChainReceiver!,
recipientAddress,
amount,
tokenAddress,
{ value: cost } // Attach the necessary fee for cross-chain transfer
);
await transferTx.wait();
console.log(
`Transfer initiated from ${sourceNetworkName} to ${targetNetworkName}. Transaction Hash: ${transferTx.hash}`
);
}
main().catch((error) => {
console.error(error);
process.exit(1);
});
Transferir Tokens
Agora que seu script de transferência está pronto, é hora de executá-lo e realizar a transferência de tokens entre cadeias.
Executar o script de transferência
Abra o terminal e execute o script de transferência:
npx ts-node script/transfer.ts
Este comando iniciará o script, solicitando que você selecione as cadeias de origem e destino, insira o endereço do token, o endereço do destinatário e a quantidade de tokens a ser transferida.
Siga as instruções - o script irá guiá-lo na seleção das cadeias de origem e destino e na inserção dos detalhes necessários para a transferência de tokens. Assim que você fornecer todas as informações requeridas, o script iniciará a transferência dos tokens.
Verifique a transação - após executar o script, você verá uma mensagem de confirmação com o hash da transação. Você pode usar esse hash de transação para verificar o status da transferência nos exploradores de blockchain respectivos.
Você pode verificar a transação no Wormhole Explorer usando o link fornecido na saída do terminal. Este explorador também oferece a opção de adicionar o token transferido automaticamente à sua carteira MetaMask.
Se você seguiu a lógica fornecida no arquivo transfer.ts acima, a saída do seu terminal deve ser algo assim:
Recursos
Conclusão
Parabéns! Você construiu e implantou com sucesso um sistema de transferência de tokens entre cadeias usando Solidity e o protocolo Wormhole. Você aprendeu como:
Configurar um novo projeto Solidity usando o Foundry
Desenvolver contratos inteligentes para enviar e receber tokens entre cadeias
Escrever scripts de implantação para gerenciar e implantar contratos em diferentes redes
Se você quiser explorar o projeto completo ou precisar de uma referência enquanto segue este tutorial, pode encontrar o código completo no repositório do. O repositório inclui todos os scripts, contratos e configurações necessários para implantar e transferir tokens entre cadeias usando o protocolo Wormhole.