Pular para o conteúdo principal

Passo a passo do contrato da ponte padrão do Optimism

Solidity
ponte
camada 2
Intermediário
Ori Pomerantz
30 de março de 2022
34 minutos de leitura

Optimism (opens in a new tab) é um Optimistic Rollup. Os optimistic rollups podem processar transações por um preço muito mais baixo do que a Rede Principal da Ethereum (também conhecida como camada 1 ou L1), porque as transações são processadas apenas por alguns nós, em vez de por todos os nós na rede. Ao mesmo tempo, todos os dados são escritos na L1, para que tudo possa ser provado e reconstruído com todas as garantias de integridade e disponibilidade da Rede Principal.

Para usar ativos da L1 no Optimism (ou em qualquer outra L2), os ativos precisam ser transferidos por ponte. Uma maneira de conseguir isso é os usuários bloquearem ativos (ETH e tokens ERC-20 são os mais comuns) na L1 e receberem ativos equivalentes para usar na L2. Eventualmente, quem quer que acabe com eles pode querer transferi-los por ponte de volta para a L1. Ao fazer isso, os ativos são queimados na L2 e então liberados de volta para o usuário na L1.

É assim que a ponte padrão do Optimism (opens in a new tab) funciona. Neste artigo, analisamos o código-fonte dessa ponte para ver como ela funciona e estudá-la como um exemplo de código Solidity bem escrito.

Fluxos de controle

A ponte tem dois fluxos principais:

  • Depósito (da L1 para a L2)
  • Retirada (da L2 para a L1)

Fluxo de depósito

Camada 1

  1. Ao depositar um ERC-20, o depositante concede à ponte uma autorização para gastar o valor que está sendo depositado
  2. O depositante chama a ponte da L1 (depositERC20, depositERC20To, depositETH ou depositETHTo)
  3. A ponte da L1 toma posse do ativo transferido pela ponte
    • ETH: o ativo é transferido pelo depositante como parte da chamada
    • ERC-20: o ativo é transferido pela ponte para si mesma usando a autorização fornecida pelo depositante
  4. A ponte da L1 usa o mecanismo de mensagens entre domínios para chamar finalizeDeposit na ponte da L2

Camada 2

  1. A ponte da L2 verifica se a chamada para finalizeDeposit é legítima:
    • Veio do contrato de mensagens entre domínios
    • Era originalmente da ponte na L1
  2. A ponte da L2 verifica se o contrato do token ERC-20 na L2 é o correto:
    • O contrato da L2 informa que sua contraparte da L1 é a mesma de onde os tokens vieram na L1
    • O contrato da L2 informa que suporta a interface correta (usando ERC-165 (opens in a new tab)).
  3. Se o contrato da L2 for o correto, chame-o para cunhar o número apropriado de tokens para o endereço apropriado. Caso contrário, inicie um processo de retirada para permitir que o usuário resgate os tokens na L1.

Fluxo de retirada

Camada 2

  1. O sacador chama a ponte da L2 (withdraw ou withdrawTo)
  2. A ponte da L2 queima o número apropriado de tokens pertencentes a msg.sender
  3. A ponte da L2 usa o mecanismo de mensagens entre domínios para chamar finalizeETHWithdrawal ou finalizeERC20Withdrawal na ponte da L1

Camada 1

  1. A ponte da L1 verifica se a chamada para finalizeETHWithdrawal ou finalizeERC20Withdrawal é legítima:
    • Veio do mecanismo de mensagens entre domínios
    • Foi originada da ponte na L2
  2. A ponte da L1 transfere o ativo apropriado (ETH ou ERC-20) para o endereço apropriado

Código da Camada 1

Este é o código que é executado na L1, a Rede Principal da Ethereum.

IL1ERC20Bridge

Esta interface é definida aqui (opens in a new tab). Ela inclui funções e definições necessárias para a transferência de tokens ERC-20 por ponte.

1// SPDX-License-Identifier: MIT

A maior parte do código do Optimism é lançada sob a licença MIT (opens in a new tab).

1pragma solidity >0.5.0 <0.9.0;

No momento em que este artigo foi escrito, a versão mais recente do Solidity era a 0.8.12. Até que a versão 0.9.0 seja lançada, não sabemos se este código é compatível com ela ou não.

1/**
2 * @title IL1ERC20Bridge
3 */
4interface IL1ERC20Bridge {
5 /**********
6 * Eventos *
7 **********/
8
9 event ERC20DepositInitiated(
Exibir tudo

Na terminologia de ponte do Optimism, deposit (depósito) significa transferência da L1 para a L2, e withdrawal (retirada) significa uma transferência da L2 para a L1.

1 address indexed _l1Token,
2 address indexed _l2Token,

Na maioria dos casos, o endereço de um ERC-20 na L1 não é o mesmo que o endereço do ERC-20 equivalente na L2. Você pode ver a lista de endereços de token aqui (opens in a new tab). O endereço com chainId 1 está na L1 (Rede Principal) e o endereço com chainId 10 está na L2 (Optimism). Os outros dois valores de chainId são para a rede de teste Kovan (42) e a rede de teste Optimistic Kovan (69).

1 address indexed _from,
2 address _to,
3 uint256 _amount,
4 bytes _data
5 );

É possível adicionar anotações às transferências, e nesse caso, elas são adicionadas aos eventos que as relatam.

1 event ERC20WithdrawalFinalized(
2 address indexed _l1Token,
3 address indexed _l2Token,
4 address indexed _from,
5 address _to,
6 uint256 _amount,
7 bytes _data
8 );

O mesmo contrato de ponte lida com transferências em ambas as direções. No caso da ponte da L1, isso significa a inicialização de depósitos e a finalização de retiradas.

1
2 /********************
3 * Funções Públicas *
4 ********************/
5
6 /**
7 * @dev obtém o endereço do contrato da ponte da L2 correspondente.
8 * @return Endereço do contrato da ponte da L2 correspondente.
9 */
10 function l2TokenBridge() external returns (address);
Exibir tudo

Esta função não é realmente necessária, porque na L2 ela é um contrato pré-implantado, então está sempre no endereço 0x4200000000000000000000000000000000000010. Ela está aqui por simetria com a ponte da L2, porque o endereço da ponte da L1 não é trivial de saber.

1 /**
2 * @dev deposita uma quantia do ERC20 no saldo do chamador na L2.
3 * @param _l1Token Endereço do ERC20 da L1 que estamos depositando
4 * @param _l2Token Endereço do respectivo ERC20 da L2
5 * @param _amount Quantia do ERC20 a ser depositada
6 * @param _l2Gas Limite de gás necessário para completar o depósito na L2.
7 * @param _data Dados opcionais para encaminhar para a L2. Estes dados são fornecidos
8 * apenas como uma conveniência para contratos externos. Além de impor um comprimento
9 * máximo, estes contratos não fornecem garantias sobre seu conteúdo.
10 */
11 function depositERC20(
12 address _l1Token,
13 address _l2Token,
14 uint256 _amount,
15 uint32 _l2Gas,
16 bytes calldata _data
17 ) external;
Exibir tudo

O parâmetro _l2Gas é a quantidade de gás da L2 que a transação pode gastar. Até um certo limite (alto), isso é gratuito (opens in a new tab), então, a menos que o contrato ERC-20 faça algo muito estranho durante a cunhagem, não deve ser um problema. Esta função cuida do cenário comum, em que um usuário transfere ativos por ponte para o mesmo endereço em uma blockchain diferente.

1 /**
2 * @dev deposita uma quantia de ERC20 no saldo de um destinatário na L2.
3 * @param _l1Token Endereço do ERC20 da L1 que estamos depositando
4 * @param _l2Token Endereço do respectivo ERC20 da L2
5 * @param _to endereço da L2 para creditar a retirada.
6 * @param _amount Quantia do ERC20 a ser depositada.
7 * @param _l2Gas Limite de gás necessário para completar o depósito na L2.
8 * @param _data Dados opcionais para encaminhar para a L2. Estes dados são fornecidos
9 * apenas como uma conveniência para contratos externos. Além de impor um comprimento
10 * máximo, estes contratos não fornecem garantias sobre seu conteúdo.
11 */
12 function depositERC20To(
13 address _l1Token,
14 address _l2Token,
15 address _to,
16 uint256 _amount,
17 uint32 _l2Gas,
18 bytes calldata _data
19 ) external;
Exibir tudo

Esta função é quase idêntica a depositERC20, mas permite que você envie o ERC-20 para um endereço diferente.

1 /*************************
2 * Funções entre cadeias *
3 *************************/
4
5 /**
6 * @dev Completa uma retirada da L2 para a L1 e credita fundos no saldo do destinatário do
7 * token ERC20 da L1.
8 * Esta chamada falhará se a retirada iniciada da L2 não tiver sido finalizada.
9 *
10 * @param _l1Token Endereço do token da L1 para o qual finalizeWithdrawal será executado.
11 * @param _l2Token Endereço do token da L2 onde a retirada foi iniciada.
12 * @param _from Endereço da L2 que iniciou a transferência.
13 * @param _to Endereço da L1 para creditar a retirada.
14 * @param _amount Quantia do ERC20 a ser depositada.
15 * @param _data Dados fornecidos pelo remetente na L2. Estes dados são fornecidos
16 * apenas como uma conveniência para contratos externos. Além de impor um comprimento
17 * máximo, estes contratos não fornecem garantias sobre seu conteúdo.
18 */
19 function finalizeERC20Withdrawal(
20 address _l1Token,
21 address _l2Token,
22 address _from,
23 address _to,
24 uint256 _amount,
25 bytes calldata _data
26 ) external;
27}
Exibir tudo

As retiradas (e outras mensagens da L2 para a L1) no Optimism são um processo de duas etapas:

  1. Uma transação de iniciação na L2.
  2. Uma transação de finalização ou de resgate na L1. Essa transação precisa acontecer após o término do período de contestação de falhas (opens in a new tab) para a transação da L2.

IL1StandardBridge

Esta interface é definida aqui (opens in a new tab). Este arquivo contém definições de evento e função para ETH. Essas definições são muito semelhantes às definidas em IL1ERC20Bridge acima para ERC-20.

A interface da ponte é dividida em dois arquivos porque alguns tokens ERC-20 exigem processamento personalizado e não podem ser tratados pela ponte padrão. Dessa forma, a ponte personalizada que lida com esse token pode implementar IL1ERC20Bridge e não precisar também transferir ETH por ponte.

1// SPDX-License-Identifier: MIT
2pragma solidity >0.5.0 <0.9.0;
3
4import "./IL1ERC20Bridge.sol";
5
6/**
7 * @title IL1StandardBridge
8 */
9interface IL1StandardBridge is IL1ERC20Bridge {
10 /**********
11 * Eventos *
12 **********/
13 event ETHDepositInitiated(
14 address indexed _from,
15 address indexed _to,
16 uint256 _amount,
17 bytes _data
18 );
Exibir tudo

Este evento é quase idêntico à versão ERC-20 (ERC20DepositInitiated), exceto que não tem os endereços de token da L1 e da L2. O mesmo se aplica aos outros eventos e funções.

1 event ETHWithdrawalFinalized(
2 .
3 .
4 .
5 );
6
7 /********************
8 * Funções Públicas *
9 ********************/
10
11 /**
12 * @dev Deposita uma quantia de ETH no saldo do chamador na L2.
13 .
14 .
15 .
16 */
17 function depositETH(uint32 _l2Gas, bytes calldata _data) external payable;
18
19 /**
20 * @dev Deposita uma quantia de ETH no saldo de um destinatário na L2.
21 .
22 .
23 .
24 */
25 function depositETHTo(
26 address _to,
27 uint32 _l2Gas,
28 bytes calldata _data
29 ) external payable;
30
31 /*************************
32 * Funções entre cadeias *
33 *************************/
34
35 /**
36 * @dev Completa uma retirada da L2 para a L1 e credita os fundos no saldo do destinatário do
37 * token ETH da L1. Como apenas o xDomainMessenger pode chamar esta função, ela nunca será chamada
38 * antes da finalização da retirada.
39 .
40 .
41 .
42 */
43 function finalizeETHWithdrawal(
44 address _from,
45 address _to,
46 uint256 _amount,
47 bytes calldata _data
48 ) external;
49}
Exibir tudo

CrossDomainEnabled

Este contrato (opens in a new tab) é herdado por ambas as pontes (L1 e L2) para enviar mensagens para a outra camada.

1// SPDX-License-Identifier: MIT
2pragma solidity >0.5.0 <0.9.0;
3
4/* Importações de Interface */
5import { ICrossDomainMessenger } from "./ICrossDomainMessenger.sol";

Esta interface (opens in a new tab) informa ao contrato como enviar mensagens para a outra camada, usando o mensageiro entre domínios. Este mensageiro entre domínios é um sistema completamente diferente, e merece seu próprio artigo, que espero escrever no futuro.

1/**
2 * @title CrossDomainEnabled
3 * @dev Contrato auxiliar para contratos que realizam comunicações entre domínios
4 *
5 * Compilador usado: definido pelo contrato herdado
6 */
7contract CrossDomainEnabled {
8 /*************
9 * Variáveis *
10 *************/
11
12 // Contrato de mensageiro usado para enviar e receber mensagens do outro domínio.
13 address public messenger;
14
15 /***************
16 * Construtor *
17 ***************/
18
19 /**
20 * @param _messenger Endereço do CrossDomainMessenger na camada atual.
21 */
22 constructor(address _messenger) {
23 messenger = _messenger;
24 }
Exibir tudo

O único parâmetro que o contrato precisa saber é o endereço do mensageiro entre domínios nessa camada. Este parâmetro é definido uma vez, no construtor, e nunca muda.

1
2 /**********************
3 * Modificadores de Função *
4 **********************/
5
6 /**
7 * Garante que a função modificada só pode ser chamada por uma conta específica de outro domínio.
8 * @param _sourceDomainAccount A única conta no domínio de origem que está
9 * autenticada para chamar esta função.
10 */
11 modifier onlyFromCrossDomainAccount(address _sourceDomainAccount) {
Exibir tudo

O sistema de mensagens entre domínios é acessível por qualquer contrato na blockchain em que está sendo executado (seja a Rede Principal da Ethereum ou o Optimism). Mas precisamos que a ponte de cada lado confie apenas em certas mensagens se elas vierem da ponte do outro lado.

1 require(
2 msg.sender == address(getCrossDomainMessenger()),
3 "OVM_XCHAIN: contrato de mensageiro não autenticado"
4 );

Apenas mensagens do mensageiro entre domínios apropriado (messenger, como você verá abaixo) são confiáveis.

1
2 require(
3 getCrossDomainMessenger().xDomainMessageSender() == _sourceDomainAccount,
4 "OVM_XCHAIN: remetente incorreto da mensagem entre domínios"
5 );

A forma como o mensageiro entre domínios fornece o endereço que enviou uma mensagem para a outra camada é através da função .xDomainMessageSender() (opens in a new tab). Desde que seja chamada na transação que foi iniciada pela mensagem, ela pode fornecer esta informação.

Precisamos ter certeza de que a mensagem que recebemos veio da outra ponte.

1
2 _;
3 }
4
5 /**********************
6 * Funções Internas *
7 **********************/
8
9 /**
10 * Obtém o mensageiro, geralmente do armazenamento. Esta função é exposta caso um contrato filho
11 * precise substituí-la.
12 * @return O endereço do contrato do mensageiro entre domínios que deve ser usado.
13 */
14 function getCrossDomainMessenger() internal virtual returns (ICrossDomainMessenger) {
15 return ICrossDomainMessenger(messenger);
16 }
Exibir tudo

Esta função retorna o mensageiro entre domínios. Usamos uma função em vez da variável messenger para permitir que contratos que herdam deste usem um algoritmo para especificar qual mensageiro entre domínios usar.

1
2 /**
3 * Envia uma mensagem para uma conta em outro domínio
4 * @param _crossDomainTarget O destinatário pretendido no domínio de destino
5 * @param _message Os dados a serem enviados para o destino (geralmente calldata para uma função com
6 * `onlyFromCrossDomainAccount()`)
7 * @param _gasLimit O gasLimit para o recebimento da mensagem no domínio de destino.
8 */
9 function sendCrossDomainMessage(
10 address _crossDomainTarget,
11 uint32 _gasLimit,
12 bytes memory _message
Exibir tudo

Finalmente, a função que envia uma mensagem para a outra camada.

1 ) internal {
2 // slither-disable-next-line reentrancy-events, reentrancy-benign

Slither (opens in a new tab) é um analisador estático que o Optimism executa em cada contrato para procurar por vulnerabilidades e outros problemas em potencial. Nesse caso, a linha seguinte aciona duas vulnerabilidades:

  1. Eventos de reentrância (opens in a new tab)
  2. Reentrância benigna (opens in a new tab)
1 getCrossDomainMessenger().sendMessage(_crossDomainTarget, _message, _gasLimit);
2 }
3}

Neste caso, não estamos preocupados com a reentrância, pois sabemos que getCrossDomainMessenger() retorna um endereço confiável, mesmo que o Slither não tenha como saber disso.

O contrato da ponte da L1

O código-fonte para este contrato está aqui (opens in a new tab).

1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.9;

As interfaces podem fazer parte de outros contratos, por isso precisam suportar uma ampla gama de versões do Solidity. Mas a ponte em si é nosso contrato, e podemos ser rigorosos sobre qual versão do Solidity ela usa.

1/* Importações de Interface */
2import { IL1StandardBridge } from "./IL1StandardBridge.sol";
3import { IL1ERC20Bridge } from "./IL1ERC20Bridge.sol";

IL1ERC20Bridge e IL1StandardBridge são explicados acima.

1import { IL2ERC20Bridge } from "../../L2/messaging/IL2ERC20Bridge.sol";

Esta interface (opens in a new tab) nos permite criar mensagens para controlar a ponte padrão na L2.

1import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

Esta interface (opens in a new tab) nos permite controlar contratos ERC-20. Você pode ler mais sobre isso aqui.

1/* Importações de Biblioteca */
2import { CrossDomainEnabled } from "../../libraries/bridge/CrossDomainEnabled.sol";

Como explicado acima, este contrato é usado para mensagens entre camadas.

1import { Lib_PredeployAddresses } from "../../libraries/constants/Lib_PredeployAddresses.sol";

Lib_PredeployAddresses (https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts/contracts/libraries/constants/Lib_PredeployAddresses.sol (opens in a new tab)) tem os endereços para os contratos da L2 que sempre têm o mesmo endereço. Isso inclui a ponte padrão na L2.

1import { Address } from "@openzeppelin/contracts/utils/Address.sol";

Utilitários de Endereço do OpenZeppelin (opens in a new tab). É usado para distinguir entre endereços de contrato e aqueles pertencentes a contas de propriedade externa (EOA).

Note que esta não é uma solução perfeita, porque não há como distinguir entre chamadas diretas e chamadas feitas a partir do construtor de um contrato, mas pelo menos isso nos permite identificar e prevenir alguns erros comuns de usuários.

1import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

O padrão ERC-20 (opens in a new tab) suporta duas maneiras para um contrato relatar falhas:

  1. Reverter
  2. Retornar false

Lidar com ambos os casos tornaria nosso código mais complicado, então, em vez disso, usamos o SafeERC20 do OpenZeppelin (opens in a new tab), que garante que todas as falhas resultem em uma reversão (opens in a new tab).

1/**
2 * @title L1StandardBridge
3 * @dev A Ponte de ETH e ERC20 da L1 é um contrato que armazena fundos depositados da L1 e tokens padrão
4 * que estão em uso na L2. Ela sincroniza uma Ponte da L2 correspondente, informando-a sobre depósitos
5 * e escutando-a para novas retiradas finalizadas.
6 *
7 */
8contract L1StandardBridge is IL1StandardBridge, CrossDomainEnabled {
9 using SafeERC20 for IERC20;
Exibir tudo

Esta linha é como especificamos o uso do wrapper SafeERC20 toda vez que usamos a interface IERC20.

1
2 /********************************
3 * Referências de Contratos Externos *
4 ********************************/
5
6 address public l2TokenBridge;

O endereço do L2StandardBridge.

1
2 // Mapeia o token da L1 para o token da L2 para o saldo do token da L1 depositado
3 mapping(address => mapping(address => uint256)) public deposits;

Um mapeamento (opens in a new tab) duplo como este é a maneira de definir uma matriz esparsa bidimensional (opens in a new tab). Os valores nesta estrutura de dados são identificados como deposit[L1 token addr][L2 token addr]. O valor padrão é zero. Apenas as células que são definidas com um valor diferente são escritas no armazenamento.

1
2 /***************
3 * Construtor *
4 ***************/
5
6 // Este contrato vive por trás de um proxy, então os parâmetros do construtor não serão utilizados.
7 constructor() CrossDomainEnabled(address(0)) {}

Para poder atualizar este contrato sem ter que copiar todas as variáveis no armazenamento. Para fazer isso, usamos um Proxy (opens in a new tab), um contrato que usa delegatecall (opens in a new tab) para transferir chamadas para um contrato separado cujo endereço é armazenado pelo contrato proxy (quando você atualiza, você diz ao proxy para mudar esse endereço). Quando você usa delegatecall, o armazenamento permanece o armazenamento do contrato chamador, então os valores de todas as variáveis de estado do contrato não são afetados.

Um efeito deste padrão é que o armazenamento do contrato que é o chamado de delegatecall não é usado e, portanto, os valores do construtor passados para ele não importam. Esta é a razão pela qual podemos fornecer um valor sem sentido para o construtor CrossDomainEnabled. É também a razão pela qual a inicialização abaixo é separada do construtor.

1 /******************
2 * Inicialização *
3 ******************/
4
5 /**
6 * @param _l1messenger Endereço do Mensageiro da L1 sendo usado para comunicações entre cadeias.
7 * @param _l2TokenBridge Endereço da ponte padrão da L2.
8 */
9 // slither-disable-next-line external-function
Exibir tudo

Este teste do Slither (opens in a new tab) identifica funções que não são chamadas do código do contrato e que, portanto, poderiam ser declaradas external em vez de public. O custo de gás de funções external pode ser menor, porque elas podem receber parâmetros no calldata. Funções declaradas como public precisam ser acessíveis de dentro do contrato. Contratos não podem modificar seu próprio calldata, então os parâmetros precisam estar na memória. Quando tal função é chamada externamente, é necessário copiar o calldata para a memória, o que custa gás. Neste caso, a função é chamada apenas uma vez, então a ineficiência não importa para nós.

1 function initialize(address _l1messenger, address _l2TokenBridge) public {
2 require(messenger == address(0), "O contrato já foi inicializado.");

A função initialize deve ser chamada apenas uma vez. Se o endereço do mensageiro entre domínios da L1 ou da ponte de token da L2 mudar, criamos um novo proxy e uma nova ponte que o chama. Isso é improvável de acontecer, exceto quando todo o sistema é atualizado, uma ocorrência muito rara.

Note que esta função não tem nenhum mecanismo que restringe quem pode chamá-la. Isso significa que, em teoria, um invasor poderia esperar até que implantemos o proxy e a primeira versão da ponte e, em seguida, usar front-run (opens in a new tab) para chegar à função initialize antes que o usuário legítimo o faça. Mas há dois métodos para evitar isso:

  1. Se os contratos forem implantados não diretamente por uma EOA, mas em uma transação que tem outro contrato para criá-los (opens in a new tab), todo o processo pode ser atômico e terminar antes que qualquer outra transação seja executada.
  2. Se a chamada legítima para initialize falhar, é sempre possível ignorar o proxy e a ponte recém-criados e criar novos.
1 messenger = _l1messenger;
2 l2TokenBridge = _l2TokenBridge;
3 }

Estes são os dois parâmetros que a ponte precisa conhecer.

1
2 /**************
3 * Depósito *
4 **************/
5
6 /** @dev Modificador que exige que o remetente seja uma EOA. Esta verificação poderia ser contornada por um
7 * contrato malicioso via initcode, mas cuida do erro de usuário que queremos evitar.
8 */
9 modifier onlyEOA() {
10 // Usado para impedir depósitos de contratos (evita a perda acidental de tokens)
11 require(!Address.isContract(msg.sender), "Conta não é EOA");
12 _;
13 }
Exibir tudo

É por essa razão que precisamos de utilitários de Address do OpenZeppelin.

1 /**
2 * @dev Esta função pode ser chamada sem dados
3 * para depositar uma quantia de ETH no saldo do chamador na L2.
4 * Como a função receive não aceita dados, uma quantia
5 * padrão conservadora é encaminhada para a L2.
6 */
7 receive() external payable onlyEOA {
8 _initiateETHDeposit(msg.sender, msg.sender, 200_000, bytes(""));
9 }
Exibir tudo

A função existe para finalidade de testes. Note que ela não aparece nas definições de interface - não é para uso corrente.

1 /**
2 * @inheritdoc IL1StandardBridge
3 */
4 function depositETH(uint32 _l2Gas, bytes calldata _data) external payable onlyEOA {
5 _initiateETHDeposit(msg.sender, msg.sender, _l2Gas, _data);
6 }
7
8 /**
9 * @inheritdoc IL1StandardBridge
10 */
11 function depositETHTo(
12 address _to,
13 uint32 _l2Gas,
14 bytes calldata _data
15 ) external payable {
16 _initiateETHDeposit(msg.sender, _to, _l2Gas, _data);
17 }
Exibir tudo

Estas duas funções são wrappers em volta do _initiateETHDeposit, a função que lida com o depósito real de ETH.

1 /**
2 * @dev Executa a lógica para depósitos armazenando o ETH e informando o Gateway de ETH da L2 sobre
3 * o depósito.
4 * @param _from Conta da qual o depósito será retirado na L1.
5 * @param _to Conta para a qual o depósito será creditado na L2.
6 * @param _l2Gas Limite de gás necessário para completar o depósito na L2.
7 * @param _data Dados opcionais para encaminhar para a L2. Estes dados são fornecidos
8 * apenas como uma conveniência para contratos externos. Além de impor um comprimento
9 * máximo, estes contratos não fornecem garantias sobre seu conteúdo.
10 */
11 function _initiateETHDeposit(
12 address _from,
13 address _to,
14 uint32 _l2Gas,
15 bytes memory _data
16 ) internal {
17 // Constrói o calldata para a chamada finalizeDeposit
18 bytes memory message = abi.encodeWithSelector(
Exibir tudo

A maneira como as mensagens entre domínios funcionam é que o contrato de destino é chamado com a mensagem como seu calldata. Os contratos Solidity sempre interpretam seu calldata de acordo com as especificações da ABI (opens in a new tab). A função do Solidity abi.encodeWithSelector (opens in a new tab) cria esse calldata.

1 IL2ERC20Bridge.finalizeDeposit.selector,
2 address(0),
3 Lib_PredeployAddresses.OVM_ETH,
4 _from,
5 _to,
6 msg.value,
7 _data
8 );

A mensagem aqui é para chamar a função finalizeDeposit (opens in a new tab) com estes parâmetros:

ParâmetroValorSignificado
_l1Tokenaddress(0)Valor especial para representar ETH (que não é um token ERC-20) na L1
_l2TokenLib_PredeployAddresses.OVM_ETHO contrato da L2 que gerencia ETH no Optimism, 0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000 (este contrato é para uso interno do Optimism apenas)
_from_fromO endereço na L1 que envia o ETH
_to_toO endereço na L2 que recebe o ETH
quantidademsg.valueQuantidade de wei enviado (que já foi enviado para a ponte)
_data_dataDados adicionais para anexar ao depósito
1 // Envia o calldata para a L2
2 // slither-disable-next-line reentrancy-events
3 sendCrossDomainMessage(l2TokenBridge, _l2Gas, message);

Envia a mensagem através do mensageiro entre domínios.

1 // slither-disable-next-line reentrancy-events
2 emit ETHDepositInitiated(_from, _to, msg.value, _data);
3 }

Emite um evento para informar qualquer aplicativo descentralizado que escuta esta transferência.

1 /**
2 * @inheritdoc IL1ERC20Bridge
3 */
4 function depositERC20(
5 .
6 .
7 .
8 ) external virtual onlyEOA {
9 _initiateERC20Deposit(_l1Token, _l2Token, msg.sender, msg.sender, _amount, _l2Gas, _data);
10 }
11
12 /**
13 * @inheritdoc IL1ERC20Bridge
14 */
15 function depositERC20To(
16 .
17 .
18 .
19 ) external virtual {
20 _initiateERC20Deposit(_l1Token, _l2Token, msg.sender, _to, _amount, _l2Gas, _data);
21 }
Exibir tudo

Estas duas funções são wrappers em volta do _initiateERC20Deposit, a função que manipula o depósito real do ERC-20.

1 /**
2 * @dev Executa a lógica para depósitos, informando o contrato do Token Depositado da L2
3 * sobre o depósito e chamando um manipulador para bloquear os fundos da L1. (ex: transferFrom)
4 *
5 * @param _l1Token Endereço do ERC20 da L1 que estamos depositando
6 * @param _l2Token Endereço do respectivo ERC20 da L2
7 * @param _from Conta da qual o depósito será retirado na L1
8 * @param _to Conta para a qual o depósito será creditado na L2
9 * @param _amount Quantia do ERC20 a ser depositada.
10 * @param _l2Gas Limite de gás necessário para completar o depósito na L2.
11 * @param _data Dados opcionais para encaminhar para a L2. Estes dados são fornecidos
12 * apenas como uma conveniência para contratos externos. Além de impor um comprimento
13 * máximo, estes contratos não fornecem garantias sobre seu conteúdo.
14 */
15 function _initiateERC20Deposit(
16 address _l1Token,
17 address _l2Token,
18 address _from,
19 address _to,
20 uint256 _amount,
21 uint32 _l2Gas,
22 bytes calldata _data
23 ) internal {
Exibir tudo

Esta função é semelhante a _initiateETHDeposit acima, com algumas diferenças importantes. A primeira diferença é que esta função recebe os endereços de token e a quantia a ser transferida como parâmetros. No caso do ETH, a chamada para a ponte já inclui a transferência do ativo para a conta da ponte (msg.value).

1 // Quando um depósito é iniciado na L1, a Ponte da L1 transfere os fundos para si mesma para futuras
2 // retiradas. safeTransferFrom também verifica se o contrato tem código, então isso falhará se
3 // _from for uma EOA ou address(0).
4 // slither-disable-next-line reentrancy-events, reentrancy-benign
5 IERC20(_l1Token).safeTransferFrom(_from, address(this), _amount);

As transferências de token ERC-20 seguem um processo diferente do ETH:

  1. O usuário (_from) concede uma autorização à ponte para transferir os tokens apropriados.
  2. O usuário chama a ponte com o endereço do contrato do token, a quantia, etc.
  3. A ponte transfere os tokens (para si mesma) como parte do processo de depósito.

O primeiro passo pode acontecer em uma transação separada dos dois últimos. No entanto, o front-running não é um problema porque as duas funções que chamam _initiateERC20Deposit (depositERC20 e depositERC20To) só chamam esta função com msg.sender como o parâmetro _from.

1 // Constrói o calldata para _l2Token.finalizeDeposit(_to, _amount)
2 bytes memory message = abi.encodeWithSelector(
3 IL2ERC20Bridge.finalizeDeposit.selector,
4 _l1Token,
5 _l2Token,
6 _from,
7 _to,
8 _amount,
9 _data
10 );
11
12 // Envia o calldata para a L2
13 // slither-disable-next-line reentrancy-events, reentrancy-benign
14 sendCrossDomainMessage(l2TokenBridge, _l2Gas, message);
15
16 // slither-disable-next-line reentrancy-benign
17 deposits[_l1Token][_l2Token] = deposits[_l1Token][_l2Token] + _amount;
Exibir tudo

Adiciona a quantia depositada de tokens à estrutura de dados deposits. Pode haver múltiplos endereços na L2 que correspondem ao mesmo token ERC-20 da L1, então não é suficiente usar o saldo da ponte do token ERC-20 da L1 para rastrear os depósitos.

1
2 // slither-disable-next-line reentrancy-events
3 emit ERC20DepositInitiated(_l1Token, _l2Token, _from, _to, _amount, _data);
4 }
5
6 /*************************
7 * Funções entre cadeias *
8 *************************/
9
10 /**
11 * @inheritdoc IL1StandardBridge
12 */
13 function finalizeETHWithdrawal(
14 address _from,
15 address _to,
16 uint256 _amount,
17 bytes calldata _data
Exibir tudo

A ponte da L2 envia uma mensagem para o mensageiro entre domínios da L2, o que faz com que o mensageiro entre domínios da L1 chame esta função (uma vez que a transação que finaliza a mensagem (opens in a new tab) seja enviada na L1, é claro).

1 ) external onlyFromCrossDomainAccount(l2TokenBridge) {

Certifique-se de que esta é uma mensagem legítima, vinda do mensageiro entre domínios e originada na ponte de tokens da L2. Esta função é usada para retirar ETH da ponte, então temos que ter certeza de que ela só é chamada pelo chamador autorizado.

1 // slither-disable-next-line reentrancy-events
2 (bool success, ) = _to.call{ value: _amount }(new bytes(0));

A maneira de transferir ETH é chamar o destinatário com a quantia de wei no msg.value.

1 require(success, "TransferHelper::safeTransferETH: falha na transferência de ETH");
2
3 // slither-disable-next-line reentrancy-events
4 emit ETHWithdrawalFinalized(_from, _to, _amount, _data);

Emite um evento sobre a retirada.

1 }
2
3 /**
4 * @inheritdoc IL1ERC20Bridge
5 */
6 function finalizeERC20Withdrawal(
7 address _l1Token,
8 address _l2Token,
9 address _from,
10 address _to,
11 uint256 _amount,
12 bytes calldata _data
13 ) external onlyFromCrossDomainAccount(l2TokenBridge) {
Exibir tudo

Esta função é semelhante a finalizeETHWithdrawal acima, com as alterações necessárias para tokens ERC-20.

1 deposits[_l1Token][_l2Token] = deposits[_l1Token][_l2Token] - _amount;

Atualiza a estrutura de dados deposits.

1
2 // Quando uma retirada é finalizada na L1, a Ponte da L1 transfere os fundos para o sacador
3 // slither-disable-next-line reentrancy-events
4 IERC20(_l1Token).safeTransfer(_to, _amount);
5
6 // slither-disable-next-line reentrancy-events
7 emit ERC20WithdrawalFinalized(_l1Token, _l2Token, _from, _to, _amount, _data);
8 }
9
10
11 /*****************************
12 * Temporário - Migrando ETH *
13 *****************************/
14
15 /**
16 * @dev Adiciona saldo de ETH à conta. Isso serve para permitir que o ETH
17 * seja migrado de um gateway antigo para um novo gateway.
18 * OBS: Isso é deixado para apenas uma atualização, para que possamos receber o ETH migrado do
19 * contrato antigo
20 */
21 function donateETH() external payable {}
22}
Exibir tudo

Havia uma implementação anterior da ponte. Quando mudamos da implementação anterior para esta, tivemos que mover todos os ativos. Os tokens ERC-20 podem simplesmente ser movidos. No entanto, para transferir ETH para um contrato, você precisa da aprovação desse contrato, que é o que donateETH nos fornece.

Tokens ERC-20 na L2

Para um token ERC-20 se encaixar na ponte padrão, ele precisa permitir que a ponte padrão, e apenas a ponte padrão, cunhe o token. Isso é necessário porque as pontes precisam garantir que o número de tokens circulando no Optimism seja igual ao número de tokens bloqueados dentro do contrato da ponte da L1. Se houver muitos tokens na L2, alguns usuários não conseguirão transferir seus ativos de volta para a L1 por meio da ponte. Em vez de uma ponte confiável, estaríamos essencialmente recriando o sistema de reservas fracionárias (opens in a new tab). Se houver tokens demais na L1, alguns desses tokens ficariam bloqueados dentro do contrato da ponte para sempre, porque não há como liberá-los sem queimar tokens da L2.

IL2StandardERC20

Todo token ERC-20 na L2 que usa a ponte padrão precisa fornecer esta interface (opens in a new tab), que tem as funções e eventos de que a ponte padrão precisa.

1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.9;
3
4import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

A interface padrão ERC-20 (opens in a new tab) não inclui as funções mint e burn. Esses métodos não são exigidos pelo padrão ERC-20 (opens in a new tab), que não especifica os mecanismos para criar e destruir tokens.

1import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol";

A interface ERC-165 (opens in a new tab) é usada para especificar quais funções um contrato fornece. Você pode ler o padrão aqui (opens in a new tab).

1interface IL2StandardERC20 is IERC20, IERC165 {
2 function l1Token() external returns (address);

Esta função fornece o endereço do token da L1 que é transferido por ponte para este contrato. Observe que não temos uma função semelhante na direção oposta. Precisamos ser capazes de transferir por ponte qualquer token da L1, independentemente de o suporte à L2 ter sido planejado quando ele foi implementado ou não.

1
2 function mint(address _to, uint256 _amount) external;
3
4 function burn(address _from, uint256 _amount) external;
5
6 event Mint(address indexed _account, uint256 _amount);
7 event Burn(address indexed _account, uint256 _amount);
8}

Funções e eventos para cunhar (criar) e queimar (destruir) tokens. A ponte deve ser a única entidade que pode executar essas funções para garantir que o número de tokens esteja correto (igual ao número de tokens bloqueados na L1).

L2StandardERC20

Esta é a nossa implementação da interface IL2StandardERC20 (opens in a new tab). A menos que você precise de algum tipo de lógica personalizada, você deve usar esta.

1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.9;
3
4import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";

O contrato ERC-20 do OpenZeppelin (opens in a new tab). O Optimism não acredita em reinventar a roda, especialmente quando a roda é bem auditada e precisa ser confiável o suficiente para guardar ativos.

1import "./IL2StandardERC20.sol";
2
3contract L2StandardERC20 is IL2StandardERC20, ERC20 {
4 address public l1Token;
5 address public l2Bridge;

Estes são os dois parâmetros de configuração adicionais que exigimos e que um ERC-20 normalmente não exige.

1
2 /**
3 * @param _l2Bridge Endereço da ponte padrão da L2.
4 * @param _l1Token Endereço do token correspondente da L1.
5 * @param _name Nome do ERC20.
6 * @param _symbol Símbolo do ERC20.
7 */
8 constructor(
9 address _l2Bridge,
10 address _l1Token,
11 string memory _name,
12 string memory _symbol
13 ) ERC20(_name, _symbol) {
14 l1Token = _l1Token;
15 l2Bridge = _l2Bridge;
16 }
Exibir tudo

Primeiro chame o construtor do contrato que herdamos (ERC20(_name, _symbol)) e depois defina nossas próprias variáveis.

1
2 modifier onlyL2Bridge() {
3 require(msg.sender == l2Bridge, "Apenas a Ponte da L2 pode cunhar e queimar");
4 _;
5 }
6
7
8 // slither-disable-next-line external-function
9 function supportsInterface(bytes4 _interfaceId) public pure returns (bool) {
10 bytes4 firstSupportedInterface = bytes4(keccak256("supportsInterface(bytes4)")); // ERC165
11 bytes4 secondSupportedInterface = IL2StandardERC20.l1Token.selector ^
12 IL2StandardERC20.mint.selector ^
13 IL2StandardERC20.burn.selector;
14 return _interfaceId == firstSupportedInterface || _interfaceId == secondSupportedInterface;
15 }
Exibir tudo

É assim que o ERC-165 (opens in a new tab) funciona. Cada interface é um número de funções suportadas e é identificada como o ou exclusivo (opens in a new tab) dos seletores de função da ABI (opens in a new tab) dessas funções.

A ponte da L2 usa o ERC-165 como uma verificação de sanidade para garantir que o contrato ERC-20 para o qual envia ativos é um IL2StandardERC20.

Observação: Não há nada que impeça um contrato mal-intencionado de fornecer respostas falsas para supportsInterface, portanto, este é um mecanismo de verificação de sanidade, não um mecanismo de segurança.

1 // slither-disable-next-line external-function
2 function mint(address _to, uint256 _amount) public virtual onlyL2Bridge {
3 _mint(_to, _amount);
4
5 emit Mint(_to, _amount);
6 }
7
8 // slither-disable-next-line external-function
9 function burn(address _from, uint256 _amount) public virtual onlyL2Bridge {
10 _burn(_from, _amount);
11
12 emit Burn(_from, _amount);
13 }
14}
Exibir tudo

Apenas a ponte da L2 pode cunhar e queimar ativos.

_mint e _burn são, na verdade, definidos no contrato ERC-20 da OpenZeppelin. Aquele contrato simplesmente não os expõe externamente, porque as condições para cunhar e queimar tokens são tão variadas quanto o número de maneiras de usar o ERC-20.

Código da Ponte da L2

Este é o código que executa a ponte no Optimism. A fonte para este contrato está aqui (opens in a new tab).

1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.9;
3
4/* Importações de Interface */
5import { IL1StandardBridge } from "../../L1/messaging/IL1StandardBridge.sol";
6import { IL1ERC20Bridge } from "../../L1/messaging/IL1ERC20Bridge.sol";
7import { IL2ERC20Bridge } from "./IL2ERC20Bridge.sol";

A interface IL2ERC20Bridge (opens in a new tab) é muito semelhante ao equivalente da L1 que vimos acima. Há duas diferenças significativas:

  1. Na L1, você inicia depósitos e finaliza retiradas. Aqui, você inicia retiradas e finaliza depósitos.
  2. Na L1, é necessário distinguir entre tokens ETH e ERC-20. Na L2, podemos usar as mesmas funções para ambos, porque internamente os saldos de ETH no Optimism são tratados como um token ERC-20 com o endereço 0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000 (opens in a new tab).
1/* Importações de Biblioteca */
2import { ERC165Checker } from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol";
3import { CrossDomainEnabled } from "../../libraries/bridge/CrossDomainEnabled.sol";
4import { Lib_PredeployAddresses } from "../../libraries/constants/Lib_PredeployAddresses.sol";
5
6/* Importações de Contrato */
7import { IL2StandardERC20 } from "../../standards/IL2StandardERC20.sol";
8
9/**
10 * @title L2StandardBridge
11 * @dev A ponte Padrão da L2 é um contrato que funciona em conjunto com a ponte Padrão da L1 para
12 * permitir transições de ETH e ERC20 entre L1 e L2.
13 * Este contrato atua como um cunhador para novos tokens quando ouve sobre depósitos na Ponte Padrão
14 * da L1.
15 * Este contrato também atua como um queimador dos tokens destinados à retirada, informando à ponte
16 * da L1 para liberar fundos da L1.
17 */
18contract L2StandardBridge is IL2ERC20Bridge, CrossDomainEnabled {
19 /********************************
20 * Referências de Contratos Externos *
21 ********************************/
22
23 address public l1TokenBridge;
Exibir tudo

Acompanhe o endereço da ponte da L1. Observe que, em contraste com o equivalente da L1, aqui precisamos desta variável. O endereço da ponte da L1 não é conhecido antecipadamente.

1
2 /***************
3 * Construtor *
4 ***************/
5
6 /**
7 * @param _l2CrossDomainMessenger Mensageiro entre domínios usado por este contrato.
8 * @param _l1TokenBridge Endereço da ponte da L1 implantada na cadeia principal.
9 */
10 constructor(address _l2CrossDomainMessenger, address _l1TokenBridge)
11 CrossDomainEnabled(_l2CrossDomainMessenger)
12 {
13 l1TokenBridge = _l1TokenBridge;
14 }
15
16 /***************
17 * Retirada *
18 ***************/
19
20 /**
21 * @inheritdoc IL2ERC20Bridge
22 */
23 function withdraw(
24 address _l2Token,
25 uint256 _amount,
26 uint32 _l1Gas,
27 bytes calldata _data
28 ) external virtual {
29 _initiateWithdrawal(_l2Token, msg.sender, msg.sender, _amount, _l1Gas, _data);
30 }
31
32 /**
33 * @inheritdoc IL2ERC20Bridge
34 */
35 function withdrawTo(
36 address _l2Token,
37 address _to,
38 uint256 _amount,
39 uint32 _l1Gas,
40 bytes calldata _data
41 ) external virtual {
42 _initiateWithdrawal(_l2Token, msg.sender, _to, _amount, _l1Gas, _data);
43 }
Exibir tudo

Estas duas funções iniciam as retiradas. Observe que não há necessidade de especificar o endereço do token da L1. Espera-se que os tokens da L2 nos digam o endereço do equivalente da L1.

1
2 /**
3 * @dev Executa a lógica para retiradas, queimando o token e informando
4 * o Gateway de Token da L1 sobre a retirada.
5 * @param _l2Token Endereço do token da L2 onde a retirada é iniciada.
6 * @param _from Conta da qual a retirada será retirada na L2.
7 * @param _to Conta para a qual a retirada será creditada na L1.
8 * @param _amount Quantidade do token a ser retirado.
9 * @param _l1Gas Não utilizado, mas incluído para possíveis considerações de compatibilidade futura.
10 * @param _data Dados opcionais para encaminhar para a L1. Estes dados são fornecidos
11 * apenas como uma conveniência para contratos externos. Além de impor um comprimento
12 * máximo, estes contratos não fornecem garantias sobre seu conteúdo.
13 */
14 function _initiateWithdrawal(
15 address _l2Token,
16 address _from,
17 address _to,
18 uint256 _amount,
19 uint32 _l1Gas,
20 bytes calldata _data
21 ) internal {
22 // Quando uma retirada é iniciada, nós queimamos os fundos do sacador para evitar o uso subsequente na L2
23 //
24 // slither-disable-next-line reentrancy-events
25 IL2StandardERC20(_l2Token).burn(msg.sender, _amount);
Exibir tudo

Observe que não estamos confiando no parâmetro _from, mas em msg.sender, que é muito mais difícil de falsificar (impossível, até onde eu sei).

1
2 // Constrói o calldata para l1TokenBridge.finalizeERC20Withdrawal(_to, _amount)
3 // slither-disable-next-line reentrancy-events
4 address l1Token = IL2StandardERC20(_l2Token).l1Token();
5 bytes memory message;
6
7 if (_l2Token == Lib_PredeployAddresses.OVM_ETH) {

Na L1, é necessário distinguir entre ETH e ERC-20.

1 message = abi.encodeWithSelector(
2 IL1StandardBridge.finalizeETHWithdrawal.selector,
3 _from,
4 _to,
5 _amount,
6 _data
7 );
8 } else {
9 message = abi.encodeWithSelector(
10 IL1ERC20Bridge.finalizeERC20Withdrawal.selector,
11 l1Token,
12 _l2Token,
13 _from,
14 _to,
15 _amount,
16 _data
17 );
18 }
19
20 // Envia a mensagem para a ponte da L1
21 // slither-disable-next-line reentrancy-events
22 sendCrossDomainMessage(l1TokenBridge, _l1Gas, message);
23
24 // slither-disable-next-line reentrancy-events
25 emit WithdrawalInitiated(l1Token, _l2Token, msg.sender, _to, _amount, _data);
26 }
27
28 /************************************
29 * Função entre cadeias: Depósito *
30 ************************************/
31
32 /**
33 * @inheritdoc IL2ERC20Bridge
34 */
35 function finalizeDeposit(
36 address _l1Token,
37 address _l2Token,
38 address _from,
39 address _to,
40 uint256 _amount,
41 bytes calldata _data
Exibir tudo

Essa função é chamada pelo L1StandardBridge.

1 ) external virtual onlyFromCrossDomainAccount(l1TokenBridge) {

Certifique-se de que a origem da mensagem é legítima. Isso é importante porque a função chama _mint e poderia ser usada para dar tokens que não são cobertos pelos tokens que a ponte possui na L1.

1 // Verifica se o token de destino é compatível e
2 // verifica se o token depositado na L1 corresponde à representação do token depositado na L2 aqui
3 if (
4 // slither-disable-next-line reentrancy-events
5 ERC165Checker.supportsInterface(_l2Token, 0x1d1d8b63) &&
6 _l1Token == IL2StandardERC20(_l2Token).l1Token()

Verificações de sanidade:

  1. A interface correta é suportada
  2. O endereço L1 do contrato ERC-20 da L2 corresponde à origem dos tokens na L1
1 ) {
2 // Quando um depósito é finalizado, creditamos na conta da L2 a mesma quantidade de
3 // tokens.
4 // slither-disable-next-line reentrancy-events
5 IL2StandardERC20(_l2Token).mint(_to, _amount);
6 // slither-disable-next-line reentrancy-events
7 emit DepositFinalized(_l1Token, _l2Token, _from, _to, _amount, _data);

Se a verificação de sanidade passar, finalize o depósito:

  1. Cunhar os tokens
  2. Emitir o evento apropriado
1 } else {
2 // Ou o token da L2 que está sendo depositado discorda sobre o endereço correto
3 // de seu token da L1, ou não suporta a interface correta.
4 // Isso só deve acontecer se houver um token malicioso na L2, ou se um usuário de alguma forma
5 // especificou o endereço errado do token da L2 para depositar.
6 // Em ambos os casos, paramos o processo aqui e construímos uma mensagem de retirada
7 // para que os usuários possam retirar seus fundos em alguns casos.
8 // Não há como evitar contratos de token maliciosos completamente, mas isso limita
9 // o erro do usuário e mitiga algumas formas de comportamento de contrato malicioso.
Exibir tudo

Se um usuário cometeu um erro detectável usando o endereço de token da L2 errado, queremos cancelar o depósito e retornar os tokens na L1. A única maneira de fazer isso a partir da L2 é enviar uma mensagem que terá que esperar o período de contestação de falhas, mas isso é muito melhor para o usuário do que perder os tokens permanentemente.

1 bytes memory message = abi.encodeWithSelector(
2 IL1ERC20Bridge.finalizeERC20Withdrawal.selector,
3 _l1Token,
4 _l2Token,
5 _to, // trocou o _to e o _from aqui para devolver o depósito ao remetente
6 _from,
7 _amount,
8 _data
9 );
10
11 // Envia a mensagem para a ponte da L1
12 // slither-disable-next-line reentrancy-events
13 sendCrossDomainMessage(l1TokenBridge, 0, message);
14 // slither-disable-next-line reentrancy-events
15 emit DepositFailed(_l1Token, _l2Token, _from, _to, _amount, _data);
16 }
17 }
18}
Exibir tudo

Conclusão

A ponte padrão é o mecanismo mais flexível para transferência de ativos. Porém, por ser genérico não é sempre o mecanismo mais fácil de usar. Especialmente para retiradas, a maioria dos usuários prefere usar pontes de terceiros (opens in a new tab) que não esperam o período de desafio e não exigem uma prova de Merkle para finalizar a retirada.

Estas pontes tipicamente funcionam tendo ativos na L1, que elas fornecem imediatamente por uma taxa pequena (geralmente menor que o custo de gás para uma retirada de uma ponte padrão). Quando a ponte (ou as pessoas que a administram) antecipa a falta de ativos da L1, ela transfere ativos suficientes da L2. Como estes são saques muito grandes, o custo do saque é amortizado por uma grande quantia e é um percentual muito menor.

Esperamos que este artigo tenha ajudado você a entender mais sobre como a camada 2 funciona e como escrever um código Solidity claro e seguro.

Veja aqui mais do meu trabalho (opens in a new tab).

Última atualização da página: 22 de outubro de 2025

Este tutorial foi útil?