Pular para o conteúdo principal

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

soliditypontecamada 2
Intermediário
Ori Pomerantz
30 de março de 2022
33 minutos de leitura minute read

Optimism(opens in a new tab) é uma Optimistic Rollup. Optimistic rollups podem processar transações por um preço muito inferior ao Ethereum Mainnet (também conhecido como layer 1 ou L1) porque as transações são processadas apenas por alguns nós, em vez de cada nó na rede. Ao mesmo tempo, os dados são todos escritos em L1, de modo que tudo pode ser provado e reconstruído com todas as garantias de integridade e disponibilidade da rede principal.

Para usar ativos L1 na Optimism (ou qualquer outra L2), os ativos precisam ser enviados pela ponte. Uma maneira de conseguir isso é os usuários bloquearem ativos (ETH e tokens ERC-20 são os mais comuns) na L1, e receber ativos equivalentes para usar na L2. Por fim, quem for que acabe com eles, talvez queira enviá-los de volta para a L1. Ao fazer isso, os ativos são queimados na L2 e, em seguida, liberados para o usuário na L1.

É assim que a ponte padrão Optimism(opens in a new tab) funciona. Neste artigo, passamos pelo código-fonte para essa ponte para ver como ele funciona e estudá-lo como um exemplo de código Solidity bem escrito.

Fluxo de controle

A ponte tem dois fluxos principais:

  • Depósito (de L1 a L2)
  • Saque de (L2 para L1)

Fluxo de depósitos

Camada 1

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

Camada 2

  1. A ponte L2 verifica que a chamada do finalizeDeposit é legítima:
    • Se veio do contrato de mensagem entre domínios
    • Era originalmente da ponte em L1
  2. A ponte L2 checa se o contrato do token ERC-20 na L2 é o correto:
  3. Se o contrato L2 é o correto, chame-o para cunhar o número apropriado de tokens para o endereço apropriado. Se não, comece o processo de retirada para permitir o usuário reclamar os tokens no L1.

Fluxo de retirada

Camada 2

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

Camada 1

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

Código Camada 1

Este é o código que roda na L1, a Rede Principal do Ethereum.

IL1ERC20Bridge

Esta interface é definida aqui(opens in a new tab). Ela inclui funções e definições exigidas para realizar a ponte de tokens ERC-20.

1// SPDX-License-Identifier: MIT
Copiar

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

1pragma solidity >0.5.0 <0.9.0;
Copiar

Neste momento, a última versão do Solidity é 0.8.12. Até versão 0.9.0 ser lançada, nós não sabemos se este código é compatível com ele ou não.

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

Na terminologia de ponte Optimism deposit significa transferência de L2 para L2, e withdrawal significa uma transferência de L2 para L1.

1 address indexed _l1Token,
2 address indexed _l2Token,
Copiar

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

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

É possível adicionar notas para transferências, caso no qual elas são adicionadas para os eventos que as reportam.

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 );
Copiar

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

1
2 /********************
3 * Public Functions *
4 ********************/
5
6 /**
7 * @dev get the address of the corresponding L2 bridge contract.
8 * @return Address of the corresponding L2 bridge contract.
9 */
10 function l2TokenBridge() external returns (address);
Exibir tudo
Copiar

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

1 /**
2 * @dev deposit an amount of the ERC20 to the caller's balance on L2.
3 * @param _l1Token Address of the L1 ERC20 we are depositing
4 * @param _l2Token Address of the L1 respective L2 ERC20
5 * @param _amount Amount of the ERC20 to deposit
6 * @param _l2Gas Gas limit required to complete the deposit on L2.
7 * @param _data Optional data to forward to L2. This data is provided
8 * solely as a convenience for external contracts. Aside from enforcing a maximum
9 * length, these contracts provide no guarantees about its content.
10 */
11 function depositERC20(
12 address _l1Token,
13 address _l2Token,
14 uint256 _amount,
15 uint32 _l2Gas,
16 bytes calldata _data
17 ) external;
Exibir tudo
Copiar

O parâmetro _l2Gas é a quantidade de gas L2 que a transação tem permissão de gastar. Até um certo (alto) limite, isto é grátis(opens in a new tab), portanto a menos que o contrato ERC-20 faça algo realmente estranho quando cunhando, isto não deveria ser um problema. Esta função cuida do cenário comum, onde um usuário faz a ponte dos ativos para o mesmo endereço em uma blockchain diferente.

1 /**
2 * @dev deposit an amount of ERC20 to a recipient's balance on L2.
3 * @param _l1Token Address of the L1 ERC20 we are depositing
4 * @param _l2Token Address of the L1 respective L2 ERC20
5 * @param _to L2 address to credit the withdrawal to.
6 * @param _amount Amount of the ERC20 to deposit.
7 * @param _l2Gas Gas limit required to complete the deposit on L2.
8 * @param _data Optional data to forward to L2. This data is provided
9 * solely as a convenience for external contracts. Aside from enforcing a maximum
10 * length, these contracts provide no guarantees about its content.
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
Copiar

Esta função é quase idêntica a depositERC20, mas ela deixa você enviar o ERC-20 para diferentes endereços.

1 /*************************
2 * Cross-chain Functions *
3 *************************/
4
5 /**
6 * @dev Complete a withdrawal from L2 to L1, and credit funds to the recipient's balance of the
7 * L1 ERC20 token.
8 * This call will fail if the initialized withdrawal from L2 has not been finalized.
9 *
10 * @param _l1Token Address of L1 token to finalizeWithdrawal for.
11 * @param _l2Token Address of L2 token where withdrawal was initiated.
12 * @param _from L2 address initiating the transfer.
13 * @param _to L1 address to credit the withdrawal to.
14 * @param _amount Amount of the ERC20 to deposit.
15 * @param _data Data provided by the sender on L2. This data is provided
16 * solely as a convenience for external contracts. Aside from enforcing a maximum
17 * length, these contracts provide no guarantees about its content.
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
Copiar

Saques (e outras mensagens de L2 para L1) na Optimism é um processo em duas etapas:

  1. Uma transação inicial no L2.
  2. Uma transação de finalização ou de reclamação na L1. Esta transação precisa acontecer depois do período de desafio de falha(opens in a new tab) para a transação L2 terminar.

IL1StandardBridge

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

A ponte interface é dividida entre dois arquivos, porque alguns tokens ERC-20 requerem processamento customizado e não podem ser manipulados pela ponte padrão. Dessa maneira a ponte customizada que manipula este token pode implementar IL1ERC20Bridge e não ter que também fazer a ponte ETH.

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 * Events *
12 **********/
13 event ETHDepositInitiated(
14 address indexed _from,
15 address indexed _to,
16 uint256 _amount,
17 bytes _data
18 );
Exibir tudo
Copiar

Este evento é praticamente idêntico à versão ERC-20 (ERC20DepositInitiated), exceto por não ter os endereços de token L1 e L2. O mesmo é verdade para outros eventos e funções.

1 event ETHWithdrawalFinalized(
2 .
3 .
4 .
5 );
6
7 /********************
8 * Public Functions *
9 ********************/
10
11 /**
12 * @dev Deposit an amount of the ETH to the caller's balance on L2.
13 .
14 .
15 .
16 */
17 function depositETH(uint32 _l2Gas, bytes calldata _data) external payable;
18
19 /**
20 * @dev Deposit an amount of ETH to a recipient's balance on L2.
21 .
22 .
23 .
24 */
25 function depositETHTo(
26 address _to,
27 uint32 _l2Gas,
28 bytes calldata _data
29 ) external payable;
30
31 /*************************
32 * Cross-chain Functions *
33 *************************/
34
35 /**
36 * @dev Complete a withdrawal from L2 to L1, and credit funds to the recipient's balance of the
37 * L1 ETH token. Since only the xDomainMessenger can call this function, it will never be called
38 * before the withdrawal is finalized.
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
Copiar

CrossDomainEnabled

Este contrato(opens in a new tab) é herdado por ambas 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/* Interface Imports */
5import { ICrossDomainMessenger } from "./ICrossDomainMessenger.sol";
Copiar

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

1/**
2 * @title CrossDomainEnabled
3 * @dev Helper contract for contracts performing cross-domain communications
4 *
5 * Compiler used: defined by inheriting contract
6 */
7contract CrossDomainEnabled {
8 /*************
9 * Variables *
10 *************/
11
12 // Messenger contract used to send and receive messages from the other domain.
13 address public messenger;
14
15 /***************
16 * Constructor *
17 ***************/
18
19 /**
20 * @param _messenger Address of the CrossDomainMessenger on the current layer.
21 */
22 constructor(address _messenger) {
23 messenger = _messenger;
24 }
Exibir tudo
Copiar

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

1
2 /**********************
3 * Function Modifiers *
4 **********************/
5
6 /**
7 * Enforces that the modified function is only callable by a specific cross-domain account.
8 * @param _sourceDomainAccount The only account on the originating domain which is
9 * authenticated to call this function.
10 */
11 modifier onlyFromCrossDomainAccount(address _sourceDomainAccount) {
Exibir tudo
Copiar

O mensageiro entre domínios é acessível por qualquer contrato na blockchain onde estiver rodando (seja Ethereum mainnet ou Optimism). Mas nós precisamos da ponte em cada lado para apenas confiar em certas mensagens se eles vierem da ponte do outro lado.

1 require(
2 msg.sender == address(getCrossDomainMessenger()),
3 "OVM_XCHAIN: messenger contract unauthenticated"
4 );
Copiar

Somente mensagens do mensageiro entre domínios apropriado (messenger, como você vê abaixo) pode ser confiado.

1
2 require(
3 getCrossDomainMessenger().xDomainMessageSender() == _sourceDomainAccount,
4 "OVM_XCHAIN: wrong sender of cross-domain message"
5 );
Copiar

A maneira que o mensageiro entre domínios fornece o endereço que enviou uma mensagem com a outra camada é a função .xDomainMessageSender()(opens in a new tab). Enquanto ele for chamado na transação que foi iniciada pela mensagem, ele pode fornecer esta informação.

Nós precisamos nos certificar que a mensagem que nós recebemos veio da outra ponte.

1
2 _;
3 }
4
5 /**********************
6 * Internal Functions *
7 **********************/
8
9 /**
10 * Gets the messenger, usually from storage. This function is exposed in case a child contract
11 * needs to override.
12 * @return The address of the cross-domain messenger contract which should be used.
13 */
14 function getCrossDomainMessenger() internal virtual returns (ICrossDomainMessenger) {
15 return ICrossDomainMessenger(messenger);
16 }
Exibir tudo
Copiar

Esta função retorna o mensageiro entre domínios. Nós usamos uma função ao invés da variável messenger para permitir contratos que herdam deste para usar um algoritmo para especificar qual mensageiro entre domínios usar.

1
2 /**
3 * Sends a message to an account on another domain
4 * @param _crossDomainTarget The intended recipient on the destination domain
5 * @param _message The data to send to the target (usually calldata to a function with
6 * `onlyFromCrossDomainAccount()`)
7 * @param _gasLimit The gasLimit for the receipt of the message on the target domain.
8 */
9 function sendCrossDomainMessage(
10 address _crossDomainTarget,
11 uint32 _gasLimit,
12 bytes memory _message
Exibir tudo
Copiar

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

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

Slither(opens in a new tab)é um analisador estático que a Optimism roda em cada contrato para procurar por vulnerabilidades e outros problemas em potencial. Nesse caso, as seguintes linhas disparam 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}
Copiar

Neste caso nós não estamos preocupados sobre reentrância. Nós sabemos que getCrossDomainMessenger() returna um endereço confiável, mesmo se Slither não tem como saber isso.

O contrato da ponte 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;
Copiar

A interface pode ser parte de outros contratos, então eles têm de suportar uma larga faixa de versões de Solidity. Mas a ponte por ela mesma é o nosso contrato, e nós podemos ser estritos sobre qual versão Solidity ela usa.

1/* Interface Imports */
2import { IL1StandardBridge } from "./IL1StandardBridge.sol";
3import { IL1ERC20Bridge } from "./IL1ERC20Bridge.sol";
Copiar

IL1ERC20Bridge e IL1StandardBridge são explicados acima.

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

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

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

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

1/* Library Imports */
2import { CrossDomainEnabled } from "../../libraries/bridge/CrossDomainEnabled.sol";
Copiar

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

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

Lib_PredeployAddresses(opens in a new tab) tem os endereços dos contratos L2 que sempre tem o mesmo endereço. Isto inclui a ponte padrão em L2.

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

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

Note que isto não é a solução perfeita, porque não há como distinguir entre chamadas diretas e chamadas feitas de um construtor de contrato, mas pelo menos isto nos deixa identificar e evitar alguns erros comuns de usuário.

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

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

  1. Revert
  2. Return false

Gerenciar ambos casos faria nosso código mais complicado, então ao invés disso, usamos OpenZeppelin SafeERC20(opens in a new tab), que garante que todas as falhas resultem num revert(opens in a new tab).

1/**
2 * @title L1StandardBridge
3 * @dev The L1 ETH and ERC20 Bridge is a contract which stores deposited L1 funds and standard
4 * tokens that are in use on L2. It synchronizes a corresponding L2 Bridge, informing it of deposits
5 * and listening to it for newly finalized withdrawals.
6 *
7 */
8contract L1StandardBridge is IL1StandardBridge, CrossDomainEnabled {
9 using SafeERC20 for IERC20;
Exibir tudo
Copiar

Esta linha é como especificamos para usar o wrapper SafeERC20 cada vez que nós usamos a interface IERC20.

1
2 /********************************
3 * External Contract References *
4 ********************************/
5
6 address public l2TokenBridge;
Copiar

O endereço de L2StandardBridge.

1
2 // Maps L1 token to L2 token to balance of the L1 token deposited
3 mapping(address => mapping(address => uint256)) public deposits;
Copiar

Um mapeamento(opens in a new tab) em dobro como este é a maneira de você definir uma array esparsa bi-dimensional(opens in a new tab). Valores nesta estrutura de dados são identificados como deposit[L1 token addr][L2 token addr]. O valor padrão é zero. Somente células que são configuradas para um valor diferente são escritas no storage.

1
2 /***************
3 * Constructor *
4 ***************/
5
6 // This contract lives behind a proxy, so the constructor parameters will go unused.
7 constructor() CrossDomainEnabled(address(0)) {}
Copiar

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

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

1 /******************
2 * Initialization *
3 ******************/
4
5 /**
6 * @param _l1messenger L1 Messenger address being used for cross-chain communications.
7 * @param _l2TokenBridge L2 standard bridge address.
8 */
9 // slither-disable-next-line external-function
Exibir tudo
Copiar

Este teste Slither(opens in a new tab) identifica funções que não são chamadas do código do contrato e poderiam portanto serem declaradas external ao invés de public. As funções de custo de gas external podem ser menores, porque elas podem ser fornecidas com parâmetros no calldata. Funções declaradas public têm de ser acessíveis de dentro do contrato. Contratos não podem modificar seus próprios calldata, então os parâmetros têm que estar na memória. Quando esta função é chamada externamente, é necessário copiar o calldata para a memória, que custa gas. Nesse caso a função é chamada somente 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), "Contract has already been initialized.");
Copiar

A função initialize deve ser chamada só uma vez. Se o endereço do mensageiro entre domínios L1 ou se a ponte do token L2 mudam, nós criamos um novo proxy e uma nova ponte que chama ele. Isto é improvável de acontecer, exceto quando o sistema inteiro é atualizado, uma ocorrência muito rara.

Note que esta função não tem nenhum mecanismo que restringe quem pode chamá-la. Isto significa que em teoria um atacante poderia esperar até que nós implantassemos o proxy e a primeira versão da ponte e então front-run(opens in a new tab)para pegar a função initialize antes que o usuário legítimo o faça. Mas há dois métodos para evitar isso:

  1. Se o contrato for implantado não diretamente por um EOA mas em uma transação que tem outro contrato criando eles,(opens in a new tab) o processo inteiro pode ser atômico, e finalizar antes que qualquer outra transação seja executada.
  2. Se a chamada legítima para initialize falhar, é sempre possível ignorar o proxy recém-criado e fazer a ponte para criar outros novos.
1 messenger = _l1messenger;
2 l2TokenBridge = _l2TokenBridge;
3 }
Copiar

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

1
2 /**************
3 * Depositing *
4 **************/
5
6 /** @dev Modifier requiring sender to be EOA. This check could be bypassed by a malicious
7 * contract via initcode, but it takes care of the user error we want to avoid.
8 */
9 modifier onlyEOA() {
10 // Used to stop deposits from contracts (avoid accidentally lost tokens)
11 require(!Address.isContract(msg.sender), "Account not EOA");
12 _;
13 }
Exibir tudo
Copiar

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

1 /**
2 * @dev This function can be called with no data
3 * to deposit an amount of ETH to the caller's balance on L2.
4 * Since the receive function doesn't take data, a conservative
5 * default amount is forwarded to L2.
6 */
7 receive() external payable onlyEOA {
8 _initiateETHDeposit(msg.sender, msg.sender, 200_000, bytes(""));
9 }
Exibir tudo
Copiar

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
Copiar

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

1 /**
2 * @dev Performs the logic for deposits by storing the ETH and informing the L2 ETH Gateway of
3 * the deposit.
4 * @param _from Account to pull the deposit from on L1.
5 * @param _to Account to give the deposit to on L2.
6 * @param _l2Gas Gas limit required to complete the deposit on L2.
7 * @param _data Optional data to forward to L2. This data is provided
8 * solely as a convenience for external contracts. Aside from enforcing a maximum
9 * length, these contracts provide no guarantees about its content.
10 */
11 function _initiateETHDeposit(
12 address _from,
13 address _to,
14 uint32 _l2Gas,
15 bytes memory _data
16 ) internal {
17 // Construct calldata for finalizeDeposit call
18 bytes memory message = abi.encodeWithSelector(
Exibir tudo
Copiar

A maneira que mensagens entre domínios trabalham é que o contrato de destino é chamado com a mensagem como o seu calldata. Contratos Solidity sempre interpretam seu calldata de acordo com a especificação ABI(opens in a new tab). A função Solidity abi.encodeWithSelector(opens in a new tab) cria este calldata.

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

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

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

Enviar a mensagem através de mensageiro entre domínios.

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

Emitir um evento para informar qualquer aplicação descentralizada 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
Copiar

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 Performs the logic for deposits by informing the L2 Deposited Token
3 * contract of the deposit and calling a handler to lock the L1 funds. (e.g. transferFrom)
4 *
5 * @param _l1Token Address of the L1 ERC20 we are depositing
6 * @param _l2Token Address of the L1 respective L2 ERC20
7 * @param _from Account to pull the deposit from on L1
8 * @param _to Account to give the deposit to on L2
9 * @param _amount Amount of the ERC20 to deposit.
10 * @param _l2Gas Gas limit required to complete the deposit on L2.
11 * @param _data Optional data to forward to L2. This data is provided
12 * solely as a convenience for external contracts. Aside from enforcing a maximum
13 * length, these contracts provide no guarantees about its content.
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
Copiar

Esta função é similiar a _initiateETHDeposit acima, com algumas poucas diferenças importantes. A primeira diferença é que esta função recebe o endereço de token e a quantia a transferir 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 // When a deposit is initiated on L1, the L1 Bridge transfers the funds to itself for future
2 // withdrawals. safeTransferFrom also checks if the contract has code, so this will fail if
3 // _from is an EOA or address(0).
4 // slither-disable-next-line reentrancy-events, reentrancy-benign
5 IERC20(_l1Token).safeTransferFrom(_from, address(this), _amount);
Copiar

Transferências de tokens ERC-20 seguem um processo diferente do ETH:

  1. O usuário (_from) dá uma permissão para a 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 ela mesmo) como parte do processo de depósito.

O primeiro passo pode acontecer em uma transação separada das últimas duas. Entretanto, front-running não é um problema porque as duas funções que chamam _initiateERC20Deposit (depositERC20 e depositERC20To) somente chamam essa função com msg.sender como parâmetro _from.

1 // Construct calldata for _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 // Send calldata into 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
Copiar

Adicione a quantia depositada de tokens para a estrutura de dados deposits. Pode haver múltiplos endereços em L2 que correspondam ao mesmo token ERC-20 L1, portanto não é suficiente usar saldo de ponte de token ERC-20 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 * Cross-chain Functions *
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
Copiar

A ponte L2 envia uma mensagem para o mensageiro entre domínios L2 que causa o mensageiro entre domínios L1 chamar esta função (uma vez que a transação que finaliza a mensagem(opens in a new tab) é submetida no L1, claro).

1 ) external onlyFromCrossDomainAccount(l2TokenBridge) {
Copiar

Certifique-se que isto é uma mensagem legítima, vinda do mensageiro entre domínios e originada com o token da ponte L2. Esta função é usada para retirar ETH da ponte, então nós temos que nos certificar que é somente chamada pelo chamador autorizado.

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

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

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

Emitir um evento sobre o saque.

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
Copiar

Esta função é similar a finalizeETHWithdrawal acima, com as mudanças necessárias para os tokens ERC-20.

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

Atualiza a estrutura de dadosdeposits.

1
2 // When a withdrawal is finalized on L1, the L1 Bridge transfers the funds to the withdrawer
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 * Temporary - Migrating ETH *
13 *****************************/
14
15 /**
16 * @dev Adds ETH balance to the account. This is meant to allow for ETH
17 * to be migrated from an old gateway to a new gateway.
18 * NOTE: This is left for one upgrade only so we are able to receive the migrated ETH from the
19 * old contract
20 */
21 function donateETH() external payable {}
22}
Exibir tudo
Copiar

Houve uma implementação anterior dessa ponte. Quando nos movemos da implementação para esta ponte, tivemos que mover todos os ativos. Tokens ERC-20 podem serem simplesmente movidos. Entretanto, para transferir ETH para um contrato, você precisa da aprovação do contrato, que é o que donateETH nos fornece.

Tokens ERC-20 na L2

Para um token ERC-20 servir na ponte padrão, ele precisa permitir que a ponte padrão, e somente a ponte padrão, cunhe token. Isto é necessário porque as pontes precisam garantir que o número de tokens circulando na Optimism é igual ao número de tokens travados dentro do contrato da ponte L1. Se houver tokens demais na L2, alguns usuários ficarão incapazes de usar a ponte de volta para os seus ativos para a L1. Ao invés de uma ponte confiável, nós iriamos essencialmente recriar reserva fracionária bancária(opens in a new tab). Se houver tokens demais em L1, alguns desses tokens estaria travados dentro do contrato da ponte para sempre, porque não há maneira de liberá-los sem queimar tokens L2.

IL2StandardERC20

Cada 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 que a ponte padrão necessita.

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

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

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

A interface ERC-165 (opens in a new tab)é usada para especificar que 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);
Copiar

Esta função fornece o endereço do token L1 que usa a ponte para este contrato. Note que nós não temos uma função similar na direção oposta. Nós precisamos ser capazes de usar a ponte para qualquer token L1, independente se o suporte L2 foi planejado quando 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}
Copiar

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

L2StandardERC20

Essa é a nossa implementação da interface IL2StandardERC20(opens in a new tab). A não ser que você precise de algum tipo de lógica customizada, você deveria usar esta.

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

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

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

Há dois parâmetros de configuração adicionais que nós precisamos, e um ERC-20 normalmente não precisa.

1
2 /**
3 * @param _l2Bridge Address of the L2 standard bridge.
4 * @param _l1Token Address of the corresponding L1 token.
5 * @param _name ERC20 name.
6 * @param _symbol ERC20 symbol.
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
Copiar

Primeiro chame o construtor do contrato que nós herdamos (ERC20(_name, _symbol)) e então configure suas próprias variáveis.

1
2 modifier onlyL2Bridge() {
3 require(msg.sender == l2Bridge, "Only L2 Bridge can mint and burn");
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
Copiar

Essa é a maneira que oERC-165(opens in a new tab) funciona. Cada interface é um número de funções suportadas, como identificadas no ou exclusivo(opens in a new tab) dos seletores de funções ABI(opens in a new tab) destas funções.

A ponte L2 usa ERC-165 como checagem de sanidade para garantir que o contrato ERC-20 para o qual ela envia ativos é um IL2StandardERC20.

Note: Não há nada para evitar contratos trapaceiros de fornecer falsas respostas para supportsInterface, portanto isto é um mecanismo de checagem 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
Copiar

Somente a ponte L2 pode cunhar e queimar ativos.

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

Código da ponte L2

Este é o código que roda na ponte na Optimism. A fonte deste contrato é aqui(opens in a new tab).

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

A interface IL2ERC20Bridge(opens in a new tab) é muito similar ao equivalente L1 que nós vimos acima. Há duas diferenças significantes:

  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 ETH e tokens ERC-20. Na L2 nós podemos usar as mesmas funções para ambos os casos porque internamente saldos ETH na Optimism são manipulados por um token ERC-20 com o endereço 0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000(opens in a new tab).
1/* Library Imports */
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/* Contract Imports */
7import { IL2StandardERC20 } from "../../standards/IL2StandardERC20.sol";
8
9/**
10 * @title L2StandardBridge
11 * @dev The L2 Standard bridge is a contract which works together with the L1 Standard bridge to
12 * enable ETH and ERC20 transitions between L1 and L2.
13 * This contract acts as a minter for new tokens when it hears about deposits into the L1 Standard
14 * bridge.
15 * This contract also acts as a burner of the tokens intended for withdrawal, informing the L1
16 * bridge to release L1 funds.
17 */
18contract L2StandardBridge is IL2ERC20Bridge, CrossDomainEnabled {
19 /********************************
20 * External Contract References *
21 ********************************/
22
23 address public l1TokenBridge;
Exibir tudo
Copiar

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

1
2 /***************
3 * Constructor *
4 ***************/
5
6 /**
7 * @param _l2CrossDomainMessenger Cross-domain messenger used by this contract.
8 * @param _l1TokenBridge Address of the L1 bridge deployed to the main chain.
9 */
10 constructor(address _l2CrossDomainMessenger, address _l1TokenBridge)
11 CrossDomainEnabled(_l2CrossDomainMessenger)
12 {
13 l1TokenBridge = _l1TokenBridge;
14 }
15
16 /***************
17 * Withdrawing *
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
Copiar

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

1
2 /**
3 * @dev Performs the logic for withdrawals by burning the token and informing
4 * the L1 token Gateway of the withdrawal.
5 * @param _l2Token Address of L2 token where withdrawal is initiated.
6 * @param _from Account to pull the withdrawal from on L2.
7 * @param _to Account to give the withdrawal to on L1.
8 * @param _amount Amount of the token to withdraw.
9 * @param _l1Gas Unused, but included for potential forward compatibility considerations.
10 * @param _data Optional data to forward to L1. This data is provided
11 * solely as a convenience for external contracts. Aside from enforcing a maximum
12 * length, these contracts provide no guarantees about its content.
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 // When a withdrawal is initiated, we burn the withdrawer's funds to prevent subsequent L2
23 // usage
24 // slither-disable-next-line reentrancy-events
25 IL2StandardERC20(_l2Token).burn(msg.sender, _amount);
Exibir tudo
Copiar

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

1
2 // Construct calldata for 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) {
Copiar

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 // Send message up to L1 bridge
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 * Cross-chain Function: Depositing *
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
Copiar

Essa função é chamada pelo L1StandardBridge.

1 ) external virtual onlyFromCrossDomainAccount(l1TokenBridge) {
Copiar

Certifique-se 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 foram cobertos pelos tokens que a ponte tem na L1.

1 // Check the target token is compliant and
2 // verify the deposited token on L1 matches the L2 deposited token representation here
3 if (
4 // slither-disable-next-line reentrancy-events
5 ERC165Checker.supportsInterface(_l2Token, 0x1d1d8b63) &&
6 _l1Token == IL2StandardERC20(_l2Token).l1Token()
Copiar

Verificações de sanidade:

  1. A interface correta é suportada
  2. O endereço L1 do contrato ERC-20 L2 bate com a fonte L1 dos tokens
1 ) {
2 // When a deposit is finalized, we credit the account on L2 with the same amount of
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);
Copiar

Se a checagem de sanidade passar, finalize o depósito:

  1. Cunhe os tokens
  2. Emita o evento apropriado
1 } else {
2 // Either the L2 token which is being deposited-into disagrees about the correct address
3 // of its L1 token, or does not support the correct interface.
4 // This should only happen if there is a malicious L2 token, or if a user somehow
5 // specified the wrong L2 token address to deposit into.
6 // In either case, we stop the process here and construct a withdrawal
7 // message so that users can get their funds out in some cases.
8 // There is no way to prevent malicious token contracts altogether, but this does limit
9 // user error and mitigate some forms of malicious contract behavior.
Exibir tudo
Copiar

Se um usuário fez um erro detectável usando o endereço de token L2 errado, nós queremos cancelar o depósito e retornar os tokens na L1. A única maneira que nós podemos fazer isso de L2 é enviar uma mensagem que irá ter que esperar pelo período de desafio de falha, mas isto é muito melhor para o usuário que perder seus tokens permanentemente.

1 bytes memory message = abi.encodeWithSelector(
2 IL1ERC20Bridge.finalizeERC20Withdrawal.selector,
3 _l1Token,
4 _l2Token,
5 _to, // switched the _to and _from here to bounce back the deposit to the sender
6 _from,
7 _amount,
8 _data
9 );
10
11 // Send message up to L1 bridge
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
Copiar

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) a esperar o período de desafio e também não precisar de 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 gas para uma retirada de uma ponte padrão). Quando a ponte (ou as pessoas que a administram) antecipa a falta de ativos L1, ela transfere ativos suficientes da L2. Como estes são saques muito grandes, o custo do saque é amortizado por uma larga 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.

Última edição: @lukassim(opens in a new tab), 26 de abril de 2024

Este tutorial foi útil?