Passo a passo do contrato da ponte padrão do Optimism
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
- Ao depositar um ERC-20, o depositante concede à ponte uma autorização para gastar o valor que está sendo depositado
- O depositante chama a ponte da L1 (
depositERC20,depositERC20To,depositETHoudepositETHTo) - 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
- A ponte da L1 usa o mecanismo de mensagens entre domínios para chamar
finalizeDepositna ponte da L2
Camada 2
- 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
- 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)).
- 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
- O sacador chama a ponte da L2 (
withdrawouwithdrawTo) - A ponte da L2 queima o número apropriado de tokens pertencentes a
msg.sender - A ponte da L2 usa o mecanismo de mensagens entre domínios para chamar
finalizeETHWithdrawaloufinalizeERC20Withdrawalna ponte da L1
Camada 1
- A ponte da L1 verifica se a chamada para
finalizeETHWithdrawaloufinalizeERC20Withdrawalé legítima:- Veio do mecanismo de mensagens entre domínios
- Foi originada da ponte na L2
- 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: MITA 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 IL1ERC20Bridge3 */4interface IL1ERC20Bridge {5 /**********6 * Eventos *7 **********/89 event ERC20DepositInitiated(Exibir tudoNa 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 _data5 );É 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 _data8 );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.
12 /********************3 * Funções Públicas *4 ********************/56 /**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 tudoEsta 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 depositando4 * @param _l2Token Endereço do respectivo ERC20 da L25 * @param _amount Quantia do ERC20 a ser depositada6 * @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 fornecidos8 * apenas como uma conveniência para contratos externos. Além de impor um comprimento9 * 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 _data17 ) external;Exibir tudoO 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 depositando4 * @param _l2Token Endereço do respectivo ERC20 da L25 * @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 fornecidos9 * apenas como uma conveniência para contratos externos. Além de impor um comprimento10 * 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 _data19 ) external;Exibir tudoEsta 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 *************************/45 /**6 * @dev Completa uma retirada da L2 para a L1 e credita fundos no saldo do destinatário do7 * 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 fornecidos16 * apenas como uma conveniência para contratos externos. Além de impor um comprimento17 * 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 _data26 ) external;27}Exibir tudoAs retiradas (e outras mensagens da L2 para a L1) no Optimism são um processo de duas etapas:
- Uma transação de iniciação na L2.
- 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: MIT2pragma solidity >0.5.0 <0.9.0;34import "./IL1ERC20Bridge.sol";56/**7 * @title IL1StandardBridge8 */9interface IL1StandardBridge is IL1ERC20Bridge {10 /**********11 * Eventos *12 **********/13 event ETHDepositInitiated(14 address indexed _from,15 address indexed _to,16 uint256 _amount,17 bytes _data18 );Exibir tudoEste 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 );67 /********************8 * Funções Públicas *9 ********************/1011 /**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;1819 /**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 _data29 ) external payable;3031 /*************************32 * Funções entre cadeias *33 *************************/3435 /**36 * @dev Completa uma retirada da L2 para a L1 e credita os fundos no saldo do destinatário do37 * token ETH da L1. Como apenas o xDomainMessenger pode chamar esta função, ela nunca será chamada38 * 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 _data48 ) external;49}Exibir tudoCrossDomainEnabled
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: MIT2pragma solidity >0.5.0 <0.9.0;34/* 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 CrossDomainEnabled3 * @dev Contrato auxiliar para contratos que realizam comunicações entre domínios4 *5 * Compilador usado: definido pelo contrato herdado6 */7contract CrossDomainEnabled {8 /*************9 * Variáveis *10 *************/1112 // Contrato de mensageiro usado para enviar e receber mensagens do outro domínio.13 address public messenger;1415 /***************16 * Construtor *17 ***************/1819 /**20 * @param _messenger Endereço do CrossDomainMessenger na camada atual.21 */22 constructor(address _messenger) {23 messenger = _messenger;24 }Exibir tudoO ú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.
12 /**********************3 * Modificadores de Função *4 **********************/56 /**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 tudoO 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.
12 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.
12 _;3 }45 /**********************6 * Funções Internas *7 **********************/89 /**10 * Obtém o mensageiro, geralmente do armazenamento. Esta função é exposta caso um contrato filho11 * 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 tudoEsta 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.
12 /**3 * Envia uma mensagem para uma conta em outro domínio4 * @param _crossDomainTarget O destinatário pretendido no domínio de destino5 * @param _message Os dados a serem enviados para o destino (geralmente calldata para uma função com6 * `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 _messageExibir tudoFinalmente, a função que envia uma mensagem para a outra camada.
1 ) internal {2 // slither-disable-next-line reentrancy-events, reentrancy-benignSlither (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 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: MIT2pragma 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:
- Reverter
- 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 L1StandardBridge3 * @dev A Ponte de ETH e ERC20 da L1 é um contrato que armazena fundos depositados da L1 e tokens padrão4 * que estão em uso na L2. Ela sincroniza uma Ponte da L2 correspondente, informando-a sobre depósitos5 * e escutando-a para novas retiradas finalizadas.6 *7 */8contract L1StandardBridge is IL1StandardBridge, CrossDomainEnabled {9 using SafeERC20 for IERC20;Exibir tudoEsta linha é como especificamos o uso do wrapper SafeERC20 toda vez que usamos a interface IERC20.
12 /********************************3 * Referências de Contratos Externos *4 ********************************/56 address public l2TokenBridge;O endereço do L2StandardBridge.
12 // Mapeia o token da L1 para o token da L2 para o saldo do token da L1 depositado3 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.
12 /***************3 * Construtor *4 ***************/56 // 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 ******************/45 /**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-functionExibir tudoEste 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:
- 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.
- Se a chamada legítima para
initializefalhar, é 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.
12 /**************3 * Depósito *4 **************/56 /** @dev Modificador que exige que o remetente seja uma EOA. Esta verificação poderia ser contornada por um7 * 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 dados3 * para depositar uma quantia de ETH no saldo do chamador na L2.4 * Como a função receive não aceita dados, uma quantia5 * padrão conservadora é encaminhada para a L2.6 */7 receive() external payable onlyEOA {8 _initiateETHDeposit(msg.sender, msg.sender, 200_000, bytes(""));9 }Exibir tudoA 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 IL1StandardBridge3 */4 function depositETH(uint32 _l2Gas, bytes calldata _data) external payable onlyEOA {5 _initiateETHDeposit(msg.sender, msg.sender, _l2Gas, _data);6 }78 /**9 * @inheritdoc IL1StandardBridge10 */11 function depositETHTo(12 address _to,13 uint32 _l2Gas,14 bytes calldata _data15 ) external payable {16 _initiateETHDeposit(msg.sender, _to, _l2Gas, _data);17 }Exibir tudoEstas 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 sobre3 * 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 fornecidos8 * apenas como uma conveniência para contratos externos. Além de impor um comprimento9 * 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 _data16 ) internal {17 // Constrói o calldata para a chamada finalizeDeposit18 bytes memory message = abi.encodeWithSelector(Exibir tudoA 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 _data8 );A mensagem aqui é para chamar a função finalizeDeposit (opens in a new tab) com estes parâmetros:
| Parâmetro | Valor | Significado |
|---|---|---|
| _l1Token | address(0) | Valor especial para representar ETH (que não é um token ERC-20) na L1 |
| _l2Token | Lib_PredeployAddresses.OVM_ETH | O contrato da L2 que gerencia ETH no Optimism, 0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000 (este contrato é para uso interno do Optimism apenas) |
| _from | _from | O endereço na L1 que envia o ETH |
| _to | _to | O endereço na L2 que recebe o ETH |
| quantidade | msg.value | Quantidade de wei enviado (que já foi enviado para a ponte) |
| _data | _data | Dados adicionais para anexar ao depósito |
1 // Envia o calldata para a L22 // slither-disable-next-line reentrancy-events3 sendCrossDomainMessage(l2TokenBridge, _l2Gas, message);Envia a mensagem através do mensageiro entre domínios.
1 // slither-disable-next-line reentrancy-events2 emit ETHDepositInitiated(_from, _to, msg.value, _data);3 }Emite um evento para informar qualquer aplicativo descentralizado que escuta esta transferência.
1 /**2 * @inheritdoc IL1ERC20Bridge3 */4 function depositERC20(5 .6 .7 .8 ) external virtual onlyEOA {9 _initiateERC20Deposit(_l1Token, _l2Token, msg.sender, msg.sender, _amount, _l2Gas, _data);10 }1112 /**13 * @inheritdoc IL1ERC20Bridge14 */15 function depositERC20To(16 .17 .18 .19 ) external virtual {20 _initiateERC20Deposit(_l1Token, _l2Token, msg.sender, _to, _amount, _l2Gas, _data);21 }Exibir tudoEstas 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 L23 * 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 depositando6 * @param _l2Token Endereço do respectivo ERC20 da L27 * @param _from Conta da qual o depósito será retirado na L18 * @param _to Conta para a qual o depósito será creditado na L29 * @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 fornecidos12 * apenas como uma conveniência para contratos externos. Além de impor um comprimento13 * 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 _data23 ) internal {Exibir tudoEsta 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 futuras2 // retiradas. safeTransferFrom também verifica se o contrato tem código, então isso falhará se3 // _from for uma EOA ou address(0).4 // slither-disable-next-line reentrancy-events, reentrancy-benign5 IERC20(_l1Token).safeTransferFrom(_from, address(this), _amount);As transferências de token ERC-20 seguem um processo diferente do ETH:
- O usuário (
_from) concede uma autorização à ponte para transferir os tokens apropriados. - O usuário chama a ponte com o endereço do contrato do token, a quantia, etc.
- 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 _data10 );1112 // Envia o calldata para a L213 // slither-disable-next-line reentrancy-events, reentrancy-benign14 sendCrossDomainMessage(l2TokenBridge, _l2Gas, message);1516 // slither-disable-next-line reentrancy-benign17 deposits[_l1Token][_l2Token] = deposits[_l1Token][_l2Token] + _amount;Exibir tudoAdiciona 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.
12 // slither-disable-next-line reentrancy-events3 emit ERC20DepositInitiated(_l1Token, _l2Token, _from, _to, _amount, _data);4 }56 /*************************7 * Funções entre cadeias *8 *************************/910 /**11 * @inheritdoc IL1StandardBridge12 */13 function finalizeETHWithdrawal(14 address _from,15 address _to,16 uint256 _amount,17 bytes calldata _dataExibir tudoA 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-events2 (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");23 // slither-disable-next-line reentrancy-events4 emit ETHWithdrawalFinalized(_from, _to, _amount, _data);Emite um evento sobre a retirada.
1 }23 /**4 * @inheritdoc IL1ERC20Bridge5 */6 function finalizeERC20Withdrawal(7 address _l1Token,8 address _l2Token,9 address _from,10 address _to,11 uint256 _amount,12 bytes calldata _data13 ) external onlyFromCrossDomainAccount(l2TokenBridge) {Exibir tudoEsta 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.
12 // Quando uma retirada é finalizada na L1, a Ponte da L1 transfere os fundos para o sacador3 // slither-disable-next-line reentrancy-events4 IERC20(_l1Token).safeTransfer(_to, _amount);56 // slither-disable-next-line reentrancy-events7 emit ERC20WithdrawalFinalized(_l1Token, _l2Token, _from, _to, _amount, _data);8 }91011 /*****************************12 * Temporário - Migrando ETH *13 *****************************/1415 /**16 * @dev Adiciona saldo de ETH à conta. Isso serve para permitir que o ETH17 * 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 do19 * contrato antigo20 */21 function donateETH() external payable {}22}Exibir tudoHavia 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: MIT2pragma solidity ^0.8.9;34import { 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.
12 function mint(address _to, uint256 _amount) external;34 function burn(address _from, uint256 _amount) external;56 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: MIT2pragma solidity ^0.8.9;34import { 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";23contract 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.
12 /**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 _symbol13 ) ERC20(_name, _symbol) {14 l1Token = _l1Token;15 l2Bridge = _l2Bridge;16 }Exibir tudoPrimeiro chame o construtor do contrato que herdamos (ERC20(_name, _symbol)) e depois defina nossas próprias variáveis.
12 modifier onlyL2Bridge() {3 require(msg.sender == l2Bridge, "Apenas a Ponte da L2 pode cunhar e queimar");4 _;5 }678 // slither-disable-next-line external-function9 function supportsInterface(bytes4 _interfaceId) public pure returns (bool) {10 bytes4 firstSupportedInterface = bytes4(keccak256("supportsInterface(bytes4)")); // ERC16511 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-function2 function mint(address _to, uint256 _amount) public virtual onlyL2Bridge {3 _mint(_to, _amount);45 emit Mint(_to, _amount);6 }78 // slither-disable-next-line external-function9 function burn(address _from, uint256 _amount) public virtual onlyL2Bridge {10 _burn(_from, _amount);1112 emit Burn(_from, _amount);13 }14}Exibir tudoApenas 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: MIT2pragma solidity ^0.8.9;34/* 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:
- Na L1, você inicia depósitos e finaliza retiradas. Aqui, você inicia retiradas e finaliza depósitos.
- 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";56/* Importações de Contrato */7import { IL2StandardERC20 } from "../../standards/IL2StandardERC20.sol";89/**10 * @title L2StandardBridge11 * @dev A ponte Padrão da L2 é um contrato que funciona em conjunto com a ponte Padrão da L1 para12 * 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ão14 * da L1.15 * Este contrato também atua como um queimador dos tokens destinados à retirada, informando à ponte16 * da L1 para liberar fundos da L1.17 */18contract L2StandardBridge is IL2ERC20Bridge, CrossDomainEnabled {19 /********************************20 * Referências de Contratos Externos *21 ********************************/2223 address public l1TokenBridge;Exibir tudoAcompanhe 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.
12 /***************3 * Construtor *4 ***************/56 /**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 }1516 /***************17 * Retirada *18 ***************/1920 /**21 * @inheritdoc IL2ERC20Bridge22 */23 function withdraw(24 address _l2Token,25 uint256 _amount,26 uint32 _l1Gas,27 bytes calldata _data28 ) external virtual {29 _initiateWithdrawal(_l2Token, msg.sender, msg.sender, _amount, _l1Gas, _data);30 }3132 /**33 * @inheritdoc IL2ERC20Bridge34 */35 function withdrawTo(36 address _l2Token,37 address _to,38 uint256 _amount,39 uint32 _l1Gas,40 bytes calldata _data41 ) external virtual {42 _initiateWithdrawal(_l2Token, msg.sender, _to, _amount, _l1Gas, _data);43 }Exibir tudoEstas 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.
12 /**3 * @dev Executa a lógica para retiradas, queimando o token e informando4 * 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 fornecidos11 * apenas como uma conveniência para contratos externos. Além de impor um comprimento12 * 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 _data21 ) internal {22 // Quando uma retirada é iniciada, nós queimamos os fundos do sacador para evitar o uso subsequente na L223 //24 // slither-disable-next-line reentrancy-events25 IL2StandardERC20(_l2Token).burn(msg.sender, _amount);Exibir tudoObserve 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).
12 // Constrói o calldata para l1TokenBridge.finalizeERC20Withdrawal(_to, _amount)3 // slither-disable-next-line reentrancy-events4 address l1Token = IL2StandardERC20(_l2Token).l1Token();5 bytes memory message;67 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 _data7 );8 } else {9 message = abi.encodeWithSelector(10 IL1ERC20Bridge.finalizeERC20Withdrawal.selector,11 l1Token,12 _l2Token,13 _from,14 _to,15 _amount,16 _data17 );18 }1920 // Envia a mensagem para a ponte da L121 // slither-disable-next-line reentrancy-events22 sendCrossDomainMessage(l1TokenBridge, _l1Gas, message);2324 // slither-disable-next-line reentrancy-events25 emit WithdrawalInitiated(l1Token, _l2Token, msg.sender, _to, _amount, _data);26 }2728 /************************************29 * Função entre cadeias: Depósito *30 ************************************/3132 /**33 * @inheritdoc IL2ERC20Bridge34 */35 function finalizeDeposit(36 address _l1Token,37 address _l2Token,38 address _from,39 address _to,40 uint256 _amount,41 bytes calldata _dataExibir tudoEssa 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 e2 // verifica se o token depositado na L1 corresponde à representação do token depositado na L2 aqui3 if (4 // slither-disable-next-line reentrancy-events5 ERC165Checker.supportsInterface(_l2Token, 0x1d1d8b63) &&6 _l1Token == IL2StandardERC20(_l2Token).l1Token()Verificações de sanidade:
- A interface correta é suportada
- 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 de3 // tokens.4 // slither-disable-next-line reentrancy-events5 IL2StandardERC20(_l2Token).mint(_to, _amount);6 // slither-disable-next-line reentrancy-events7 emit DepositFinalized(_l1Token, _l2Token, _from, _to, _amount, _data);Se a verificação de sanidade passar, finalize o depósito:
- Cunhar os tokens
- Emitir o evento apropriado
1 } else {2 // Ou o token da L2 que está sendo depositado discorda sobre o endereço correto3 // 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 forma5 // 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 retirada7 // 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 limita9 // o erro do usuário e mitiga algumas formas de comportamento de contrato malicioso.Exibir tudoSe 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 remetente6 _from,7 _amount,8 _data9 );1011 // Envia a mensagem para a ponte da L112 // slither-disable-next-line reentrancy-events13 sendCrossDomainMessage(l1TokenBridge, 0, message);14 // slither-disable-next-line reentrancy-events15 emit DepositFailed(_l1Token, _l2Token, _from, _to, _amount, _data);16 }17 }18}Exibir tudoConclusã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