Ir al contenido principal

Recorrido por el contrato del puente estándar de Optimism

Solidity
puente
capa 2
Intermedio
Ori Pomerantz
30 de marzo de 2022
35 minuto leído

Optimismopens in a new tab es un rollup optimista. Los rollups optimistas pueden procesar transacciones por un precio mucho más bajo que la red principal de Ethereum (también conocida como capa 1 o L1) porque las transacciones solo son procesadas por unos pocos nodos, en lugar de cada nodo de la red. Al mismo tiempo, todos los datos se escriben en la L1 para que todo pueda probarse y reconstruirse con todas las garantías de integridad y disponibilidad de la red principal.

Para usar activos de L1 en Optimism (o cualquier otra L2), los activos deben ser transferidos mediante un puente. Una forma de lograr esto es que los usuarios bloqueen activos (ETH y los tokens ERC-20 son los más comunes) en L1, y reciban activos equivalentes para usar en L2. Eventualmente, quien termine con ellos podría querer puentearlos de vuelta a L1. Al hacer esto, los activos se queman en L2 y luego se liberan de nuevo al usuario en L1.

Así es como funciona el puente estándar de Optimismopens in a new tab. En este artículo, repasamos el código fuente de ese puente para ver cómo funciona y estudiarlo como ejemplo de código de Solidity bien escrito.

Flujos de control

El puente tiene dos flujos principales:

  • Depósito (de L1 a L2)
  • Retirada (de L2 a L1)

Flujo de depósito

Capa 1

  1. Si se deposita un ERC-20, el depositante le da al puente un permiso para gastar la cantidad que se está depositando
  2. El depositante llama al puente de L1 (depositERC20, depositERC20To, depositETH, o depositETHTo)
  3. El puente L1 toma posesión del activo puenteado
    • ETH: El activo es transferido por el depositante como parte de la llamada
    • ERC-20: El activo es transferido por el puente a sí mismo utilizando el permiso proporcionado por el depositante
  4. El puente L1 utiliza el mecanismo de mensajes entre dominios para llamar a finalizeDeposit en el puente L2

Capa 2

  1. El puente de L2 verifica que la llamada a finalizeDeposit sea legítima:
    • Procede del contrato de mensajes entre dominios
    • Originalmente era del puente en L1
  2. El puente de L2 comprueba si el contrato de token ERC-20 en L2 es el correcto:
    • El contrato de L2 informa que su contraparte de L1 es la misma de la que provienen los tokens en L1
    • El contrato de L2 informa que admite la interfaz correcta (usando ERC-165opens in a new tab).
  3. Si el contrato L2 es el correcto, llámelo para mintear el número apropiado de tokens a la dirección apropiada. Si no, inicie un proceso de retirada para permitir al usuario reclamar los tokens en L1.

Flujo de retirada

Capa 2

  1. Quien realiza la retirada llama al puente de L2 (withdraw o withdrawTo)
  2. El puente L2 quema el número apropiado de tokens pertenecientes a msg.sender
  3. El puente de L2 utiliza el mecanismo de mensajes entre dominios para llamar a finalizeETHWithdrawal o finalizeERC20Withdrawal en el puente de L1

Capa 1

  1. El puente de L1 verifica que la llamada a finalizeETHWithdrawal o finalizeERC20Withdrawal sea legítima:
    • Procede del mecanismo de mensajes entre dominios
    • Originalmente era del puente en L2
  2. El puente de L1 transfiere el activo apropiado (ETH o ERC-20) a la dirección apropiada

Código de la capa 1

Este es el código que se ejecuta en la L1, la red principal de Ethereum.

IL1ERC20Bridge

Esta interfaz se define aquíopens in a new tab. Incluye funciones y definiciones requeridas para puentear tokens ERC-20.

1// SPDX-License-Identifier: MIT

La mayor parte del código de Optimism se publica bajo la licencia MITopens in a new tab.

1pragma solidity >0.5.0 <0.9.0;

En el momento de escribir este artículo, la última versión de Solidity es la 0.8.12. Hasta que se lance la versión 0.9.0, no sabemos si este código es compatible con ella o no.

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

En la terminología del puente de Optimism, deposit significa una transferencia de L1 a L2, y withdrawal significa una transferencia de L2 a L1.

1 address indexed _l1Token,
2 address indexed _l2Token,

En la mayoría de los casos, la dirección de un ERC-20 en L1 no es la misma que la dirección del ERC-20 equivalente en L2. Puede ver la lista de direcciones de tokens aquíopens in a new tab. La dirección con chainId 1 está en L1 (red principal) y la dirección con chainId 10 está en L2 (Optimism). Los otros dos valores de chainId son para la red de prueba de Kovan (42) y la red de prueba de Optimistic Kovan (69).

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

Es posible añadir notas a las transferencias, en cuyo caso se añaden a los eventos que las notifican.

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

El mismo contrato de puente gestiona las transferencias en ambas direcciones. En el caso del puente de L1, esto significa la inicialización de los depósitos y la finalización de las retiradas.

1 /********************
2 * Funciones públicas *
3 ********************/
4
5 /**
6 * @dev obtiene la dirección del contrato de puente de L2 correspondiente.
7 * @return Dirección del contrato de puente de L2 correspondiente.
8 */
9 function l2TokenBridge() external returns (address);
Mostrar todo

Esta función no es realmente necesaria, porque en L2 es un contrato preimplementado, por lo que siempre está en la dirección 0x4200000000000000000000000000000000000010. Está aquí por simetría con el puente de L2, porque la dirección del puente de L1 no es trivial de conocer.

1 /**
2 * @dev deposita una cantidad de ERC20 en el saldo del llamador en L2.
3 * @param _l1Token Dirección del ERC20 de L1 que estamos depositando
4 * @param _l2Token Dirección del respectivo ERC20 de L2
5 * @param _amount Cantidad del ERC20 a depositar
6 * @param _l2Gas Límite de gas requerido para completar el depósito en L2.
7 * @param _data Datos opcionales para reenviar a L2. Estos datos se proporcionan
8 * únicamente para la comodidad de los contratos externos. Aparte de imponer una longitud máxima,
9 * estos contratos no ofrecen ninguna garantía sobre su contenido.
10 */
11 function depositERC20(
12 address _l1Token,
13 address _l2Token,
14 uint256 _amount,
15 uint32 _l2Gas,
16 bytes calldata _data
17 ) external;
Mostrar todo

El parámetro _l2Gas es la cantidad de gas de L2 que la transacción puede gastar. Hasta un cierto límite (alto), esto es gratuitoopens in a new tab, por lo que, a menos que el contrato ERC-20 haga algo realmente extraño al mintear, no debería ser un problema. Esta función se encarga del escenario común, en el que un usuario transfiere activos mediante un puente a la misma dirección en una cadena de bloques diferente.

1 /**
2 * @dev deposita una cantidad de ERC20 en el saldo de un destinatario en L2.
3 * @param _l1Token Dirección del ERC20 de L1 que estamos depositando
4 * @param _l2Token Dirección del respectivo ERC20 de L2
5 * @param _to Dirección de L2 en la que acreditar el depósito.
6 * @param _amount Cantidad del ERC20 a depositar.
7 * @param _l2Gas Límite de gas requerido para completar el depósito en L2.
8 * @param _data Datos opcionales para reenviar a L2. Estos datos se proporcionan
9 * únicamente para la comodidad de los contratos externos. Aparte de imponer una longitud máxima,
10 * estos contratos no ofrecen ninguna garantía sobre su contenido.
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;
Mostrar todo

Esta función es casi idéntica a depositERC20, pero le permite enviar el ERC-20 a una dirección diferente.

1 /*************************
2 * Funciones entre cadenas *
3 *************************/
4
5 /**
6 * @dev Completa una retirada de L2 a L1 y acredita los fondos al saldo del
7 * token ERC20 de L1 del destinatario.
8 * Esta llamada fallará si la retirada iniciada desde L2 no se ha finalizado.
9 *
10 * @param _l1Token Dirección del token de L1 para finalizeWithdrawal.
11 * @param _l2Token Dirección del token de L2 donde se inició la retirada.
12 * @param _from Dirección de L2 que inicia la transferencia.
13 * @param _to Dirección de L1 en la que acreditar la retirada.
14 * @param _amount Cantidad del ERC20 a depositar.
15 * @param _data Datos proporcionados por el remitente en L2. Estos datos se proporcionan
16 * únicamente para la comodidad de los contratos externos. Aparte de imponer una longitud máxima,
17 * estos contratos no ofrecen ninguna garantía sobre su contenido.
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}
Mostrar todo

Las retiradas (y otros mensajes de L2 a L1) en Optimism son un proceso de dos pasos:

  1. Una transacción iniciadora en L2.
  2. Una transacción de finalización o reclamación en L1. Esta transacción debe ocurrir después de que finalice el período de impugnación de erroresopens in a new tab para la transacción de L2.

IL1StandardBridge

Esta interfaz se define aquíopens in a new tab. Este archivo contiene definiciones de eventos y funciones para ETH. Estas definiciones son muy similares a las definidas en IL1ERC20Bridge anteriormente para ERC-20.

La interfaz de puente está dividida entre dos archivos porque algunos tokens ERC-20 requieren un procesamiento personalizado y no pueden ser manejados por el puente estándar. De esta manera, el puente personalizado que gestiona dicho token puede implementar IL1ERC20Bridge y no tener que puentear también 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 * Eventos *
12 **********/
13 event ETHDepositInitiated(
14 address indexed _from,
15 address indexed _to,
16 uint256 _amount,
17 bytes _data
18 );
Mostrar todo

Este evento es casi idéntico a la versión ERC-20 (ERC20DepositInitiated), excepto que no incluye las direcciones de los tokens de L1 y L2. Lo mismo ocurre con los demás eventos y funciones.

1 event ETHWithdrawalFinalized(
2 .
3 .
4 .
5 );
6
7 /********************
8 * Funciones públicas *
9 ********************/
10
11 /**
12 * @dev Deposita una cantidad de ETH en el saldo del llamador en L2.
13 .
14 .
15 .
16 */
17 function depositETH(uint32 _l2Gas, bytes calldata _data) external payable;
18
19 /**
20 * @dev Deposita una cantidad de ETH en el saldo de un destinatario en L2.
21 .
22 .
23 .
24 */
25 function depositETHTo(
26 address _to,
27 uint32 _l2Gas,
28 bytes calldata _data
29 ) external payable;
30
31 /*************************
32 * Funciones entre cadenas *
33 *************************/
34
35 /**
36 * @dev Completa una retirada de L2 a L1 y acredita los fondos en el saldo del
37 * token ETH de L1 del destinatario. Dado que solo el xDomainMessenger puede llamar a esta función, nunca se llamará
38 * antes de que se finalice la retirada.
39 .
40 .
41 .
42 */
43 function finalizeETHWithdrawal(
44 address _from,
45 address _to,
46 uint256 _amount,
47 bytes calldata _data
48 ) external;
49}
Mostrar todo

CrossDomainEnabled

Este contratoopens in a new tab es heredado por ambos puentes (L1 y L2) para enviar mensajes a la otra capa.

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

Esta interfazopens in a new tab le indica al contrato cómo enviar mensajes a la otra capa, utilizando el mensajero entre dominios. Este mensajero entre dominios es un sistema completamente diferente y merece su propio artículo, que espero escribir en el futuro.

1/**
2 * @title CrossDomainEnabled
3 * @dev Contrato auxiliar para contratos que realizan comunicaciones entre dominios
4 *
5 * Compilador utilizado: definido por el contrato que hereda
6 */
7contract CrossDomainEnabled {
8 /*************
9 * Variables *
10 *************/
11
12 // Contrato de mensajero utilizado para enviar y recibir mensajes desde el otro dominio.
13 address public messenger;
14
15 /***************
16 * Constructor *
17 ***************/
18
19 /**
20 * @param _messenger Dirección del CrossDomainMessenger en la capa actual.
21 */
22 constructor(address _messenger) {
23 messenger = _messenger;
24 }
Mostrar todo

El único parámetro que el contrato necesita saber es la dirección del mensajero entre dominios en esta capa. Este parámetro se establece una vez, en el constructor, y nunca cambia.

1
2 /**********************
3 * Modificadores de función *
4 **********************/
5
6 /**
7 * @dev Exige que la función modificada solo pueda ser llamada por una cuenta específica entre dominios.
8 * @param _sourceDomainAccount La única cuenta en el dominio de origen que está
9 * autenticada para llamar a esta función.
10 */
11 modifier onlyFromCrossDomainAccount(address _sourceDomainAccount) {
Mostrar todo

La mensajería entre dominios es accesible para cualquier contrato en la cadena de bloques donde se esté ejecutando (ya sea la red principal de Ethereum u Optimism). Pero necesitamos que el puente de cada lado confíe únicamente en ciertos mensajes si provienen del puente del otro lado.

1 require(
2 msg.sender == address(getCrossDomainMessenger()),
3 "OVM_XCHAIN: contrato de mensajero no autenticado"
4 );

Solo se puede confiar en los mensajes del mensajero entre dominios apropiado (messenger, como verá a continuación).

1
2 require(
3 getCrossDomainMessenger().xDomainMessageSender() == _sourceDomainAccount,
4 "OVM_XCHAIN: remitente incorrecto del mensaje entre dominios"
5 );

La forma en que el mensajero entre dominios proporciona la dirección que envió un mensaje desde la otra capa es la función .xDomainMessageSender()opens in a new tab. Siempre que se llame en la transacción que fue iniciada por el mensaje, puede proporcionar esta información.

Tenemos que asegurarnos de que el mensaje que recibimos provenga del otro puente.

1
2 _;
3 }
4
5 /**********************
6 * Funciones internas *
7 **********************/
8
9 /**
10 * @dev Obtiene el mensajero, normalmente del almacenamiento. Esta función se expone en caso de que un contrato secundario
11 * necesite anularla.
12 * @return La dirección del contrato de mensajero entre dominios que se debe utilizar.
13 */
14 function getCrossDomainMessenger() internal virtual returns (ICrossDomainMessenger) {
15 return ICrossDomainMessenger(messenger);
16 }
Mostrar todo

Esta función devuelve el mensajero entre dominios. Utilizamos una función en lugar de la variable messenger para permitir que los contratos que heredan de este usen un algoritmo para especificar qué mensajero entre dominios usar.

1
2 /**
3 * @dev Envía un mensaje a una cuenta en otro dominio
4 * @param _crossDomainTarget El destinatario previsto en el dominio de destino
5 * @param _message Los datos para enviar al objetivo (normalmente datos de llamada a una función con
6 * `onlyFromCrossDomainAccount()`)
7 * @param _gasLimit El límite de gas para la recepción del mensaje en el dominio de destino.
8 */
9 function sendCrossDomainMessage(
10 address _crossDomainTarget,
11 uint32 _gasLimit,
12 bytes memory _message
Mostrar todo

Finalmente, la función que envía un mensaje a la otra capa.

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

Slitheropens in a new tab es un analizador estático que Optimism ejecuta en cada contrato para buscar vulnerabilidades y otros problemas potenciales. En este caso, la siguiente línea activa dos vulnerabilidades:

  1. Eventos de reentradaopens in a new tab
  2. Reentrada benignaopens in a new tab
1 getCrossDomainMessenger().sendMessage(_crossDomainTarget, _message, _gasLimit);
2 }
3}

En este caso, no nos preocupa la reentrada, ya que sabemos que getCrossDomainMessenger() devuelve una dirección confiable, incluso si Slither no tiene forma de saberlo.

El contrato del puente de L1

El código fuente de este contrato está aquíopens in a new tab.

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

Las interfaces pueden ser parte de otros contratos, por lo que tienen que admitir una amplia gama de versiones de Solidity. Pero el puente en sí es nuestro contrato, y podemos ser estrictos sobre qué versión de Solidity utiliza.

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

IL1ERC20Bridge e IL1StandardBridge se explican más arriba.

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

Esta interfazopens in a new tab nos permite crear mensajes para controlar el puente estándar en L2.

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

Esta interfazopens in a new tab nos permite controlar los contratos ERC-20. Puede leer más al respecto aquí.

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

Como se explicó anteriormente, este contrato se utiliza para la mensajería entre capas.

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

Lib_PredeployAddressesopens in a new tab tiene las direcciones de los contratos de L2 que siempre tienen la misma dirección. Esto incluye el puente estándar en L2.

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

Utilidades de dirección de OpenZeppelinopens in a new tab. Se utiliza para distinguir entre las direcciones de contrato y las que pertenecen a cuentas de propiedad externa (EOA).

Tenga en cuenta que esta no es una solución perfecta, porque no hay forma de distinguir entre llamadas directas y llamadas realizadas desde el constructor de un contrato, pero al menos esto nos permite identificar y prevenir algunos errores comunes de los usuarios.

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

El estándar ERC-20opens in a new tab admite dos formas para que un contrato informe de un fallo:

  1. Revertir
  2. Devolver false

Manejar ambos casos haría nuestro código más complicado, así que en su lugar usamos SafeERC20 de OpenZeppelinopens in a new tab, que se asegura de que todos los fallos resulten en una reversiónopens in a new tab.

1/**
2 * @title L1StandardBridge
3 * @dev El puente ERC20 y ETH de L1 es un contrato que almacena los fondos de L1 depositados y los tokens
4 * estándar que están en uso en L2. Sincroniza un puente de L2 correspondiente, informándole de los depósitos
5 * y escuchándolo para las retiradas recién finalizadas.
6 *
7 */
8contract L1StandardBridge is IL1StandardBridge, CrossDomainEnabled {
9 using SafeERC20 for IERC20;
Mostrar todo

Esta línea es la forma en que especificamos que se use el contenedor SafeERC20 cada vez que usamos la interfaz IERC20.

1
2 /********************************
3 * Referencias a contratos externos *
4 ********************************/
5
6 address public l2TokenBridge;

La dirección de L2StandardBridge.

1
2 // Asigna el token de L1 al token de L2 al saldo del token de L1 depositado
3 mapping(address => mapping(address => uint256)) public deposits;

Un mapeoopens in a new tab doble como este es la forma en que se define una matriz dispersa bidimensionalopens in a new tab. Los valores en esta estructura de datos se identifican como deposit[dirección de token L1][dirección de token L2]. El valor por defecto es cero. Solo las celdas que se establecen en un valor diferente se escriben en el almacenamiento.

1
2 /***************
3 * Constructor *
4 ***************/
5
6 // Este contrato vive detrás de un proxy, por lo que los parámetros del constructor no se utilizarán.
7 constructor() CrossDomainEnabled(address(0)) {}

Queremos poder actualizar este contrato sin tener que copiar todas las variables en el almacenamiento. Para ello, utilizamos un Proxyopens in a new tab, un contrato que utiliza delegatecallopens in a new tab para transferir llamadas a un contrato separado cuya dirección es almacenada por el contrato proxy (cuando se actualiza, se le dice al proxy que cambie esa dirección). Cuando se utiliza delegatecall, el almacenamiento sigue siendo el almacenamiento del contrato que llama, por lo que los valores de todas las variables de estado del contrato no se ven afectados.

Un efecto de este patrón es que el almacenamiento del contrato que es el llamado de delegatecall no se utiliza y, por lo tanto, los valores del constructor que se le pasan no importan. Esta es la razón por la que podemos proporcionar un valor sin sentido al constructor CrossDomainEnabled. También es la razón por la que la inicialización a continuación está separada del constructor.

1 /******************
2 * Inicialización *
3 ******************/
4
5 /**
6 * @param _l1messenger Dirección del mensajero de L1 que se utiliza para las comunicaciones entre cadenas.
7 * @param _l2TokenBridge Dirección del puente estándar de L2.
8 */
9 // slither-disable-next-line external-function
Mostrar todo

Esta prueba de Slitheropens in a new tab identifica las funciones que no se llaman desde el código del contrato y que, por lo tanto, podrían declararse como external en lugar de public. El coste de gas de las funciones external puede ser menor, porque se les pueden proporcionar parámetros en los datos de llamada. Las funciones declaradas como public deben ser accesibles desde dentro del contrato. Los contratos no pueden modificar sus propios datos de llamada, por lo que los parámetros deben estar en memoria. Cuando se llama a una función de este tipo externamente, es necesario copiar los datos de llamada a la memoria, lo que cuesta gas. En este caso, la función solo se llama una vez, por lo que la ineficiencia no nos importa.

1 function initialize(address _l1messenger, address _l2TokenBridge) public {
2 require(messenger == address(0), "El contrato ya ha sido inicializado.");

La función initialize solo debe llamarse una vez. Si la dirección del mensajero entre dominios de L1 o del puente de tokens de L2 cambia, creamos un nuevo proxy y un nuevo puente que lo llama. Es poco probable que esto ocurra, excepto cuando se actualiza todo el sistema, lo que es muy raro.

Tenga en cuenta que esta función no tiene ningún mecanismo que restrinja quién puede llamarla. Esto significa que, en teoría, un atacante podría esperar hasta que implementemos el proxy y la primera versión del puente y luego hacer front-runningopens in a new tab para llegar a la función initialize antes de que lo haga el usuario legítimo. Pero hay dos métodos para evitarlo:

  1. Si los contratos no son implementados directamente por una EOA, sino en una transacción que hace que otro contrato los creeopens in a new tab, todo el proceso puede ser atómico y terminar antes de que se ejecute cualquier otra transacción.
  2. Si la llamada legítima a initialize falla, siempre es posible ignorar el proxy y el puente recién creados y crear otros nuevos.
1 messenger = _l1messenger;
2 l2TokenBridge = _l2TokenBridge;
3 }

Estos son los dos parámetros que el puente necesita conocer.

1
2 /**************
3 * Depósito *
4 **************/
5
6 /** @dev Modificador que requiere que el remitente sea una EOA. Esta comprobación podría ser
7 * eludida por un contrato malicioso a través de initcode, pero se encarga del error de usuario que queremos evitar.
8 */
9 modifier onlyEOA() {
10 // Se utiliza para detener los depósitos de los contratos (evitar la pérdida accidental de tokens)
11 require(!Address.isContract(msg.sender), "Cuenta no es EOA");
12 _;
13 }
Mostrar todo

Esta es la razón por la que necesitábamos las utilidades de Address de OpenZeppelin.

1 /**
2 * @dev Se puede llamar a esta función sin datos
3 * para depositar una cantidad de ETH en el saldo del llamador en L2.
4 * Como la función de recepción no toma datos, se reenvía una
5 * cantidad conservadora por defecto a L2.
6 */
7 receive() external payable onlyEOA {
8 _initiateETHDeposit(msg.sender, msg.sender, 200_000, bytes(""));
9 }
Mostrar todo

Esta función existe para fines de prueba. Tenga en cuenta que no aparece en las definiciones de la interfaz; no es para uso normal.

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 }
Mostrar todo

Estas dos funciones son envoltorios de _initiateETHDeposit, la función que gestiona el depósito real de ETH.

1 /**
2 * @dev Realiza la lógica de los depósitos almacenando el ETH e informando a la puerta de enlace de ETH de L2
3 * del depósito.
4 * @param _from Cuenta de la que se retira el depósito en L1.
5 * @param _to Cuenta a la que se entrega el depósito en L2.
6 * @param _l2Gas Límite de gas requerido para completar el depósito en L2.
7 * @param _data Datos opcionales para reenviar a L2. Estos datos se proporcionan
8 * únicamente para la comodidad de los contratos externos. Aparte de imponer una longitud máxima,
9 * estos contratos no ofrecen ninguna garantía sobre su contenido.
10 */
11 function _initiateETHDeposit(
12 address _from,
13 address _to,
14 uint32 _l2Gas,
15 bytes memory _data
16 ) internal {
17 // Construir datos de llamada para la llamada a finalizeDeposit
18 bytes memory message = abi.encodeWithSelector(
Mostrar todo

La forma en que funcionan los mensajes entre dominios es que se llama al contrato de destino con el mensaje como sus datos de llamada. Los contratos de Solidity siempre interpretan sus datos de llamada de acuerdo con las especificaciones de la ABIopens in a new tab. La función de Solidity abi.encodeWithSelectoropens in a new tab crea esos datos de llamada.

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

El mensaje aquí es para llamar a la función finalizeDepositopens in a new tab con estos parámetros:

ParámetroValorSignificado
_l1Tokenaddress(0)Valor especial para representar ETH (que no es un token ERC-20) en L1
_l2TokenLib_PredeployAddresses.OVM_ETHEl contrato de L2 que gestiona ETH en Optimism, 0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000 (este contrato es solo para uso interno de Optimism)
_from_fromLa dirección en L1 que envía el ETH
_to_toLa dirección en L2 que recibe el ETH
cantidadmsg.valueCantidad de wei enviados (que ya han sido enviados al puente)
_data_dataDatos adicionales para adjuntar al depósito
1 // Enviar datos de llamada a L2
2 // slither-disable-next-line reentrancy-events
3 sendCrossDomainMessage(l2TokenBridge, _l2Gas, message);

Enviar el mensaje a través del mensajero entre dominios.

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

Emitir un evento para informar de esta transferencia a cualquier aplicación descentralizada que escuche.

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 }
Mostrar todo

Estas dos funciones son envoltorios de _initiateERC20Deposit, la función que gestiona el depósito real de ERC-20.

1 /**
2 * @dev Realiza la lógica de los depósitos informando al contrato de token
3 * depositado en L2 del depósito y llamando a un manejador para bloquear los fondos de L1. (p. ej., transferFrom)
4 *
5 * @param _l1Token Dirección del ERC20 de L1 que estamos depositando
6 * @param _l2Token Dirección del respectivo ERC20 de L2
7 * @param _from Cuenta de la que se retira el depósito en L1
8 * @param _to Cuenta a la que se entrega el depósito en L2
9 * @param _amount Cantidad del ERC20 a depositar.
10 * @param _l2Gas Límite de gas requerido para completar el depósito en L2.
11 * @param _data Datos opcionales para reenviar a L2. Estos datos se proporcionan
12 * únicamente para la comodidad de los contratos externos. Aparte de imponer una longitud máxima,
13 * estos contratos no ofrecen ninguna garantía sobre su contenido.
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 {
Mostrar todo

Esta función es similar a _initiateETHDeposit anterior, con algunas diferencias importantes. La primera diferencia es que esta función recibe las direcciones de los tokens y la cantidad a transferir como parámetros. En el caso de ETH, la llamada al puente ya incluye la transferencia del activo a la cuenta del puente (msg.value).

1 // Cuando se inicia un depósito en L1, el puente de L1 transfiere los fondos a sí mismo para futuras
2 // retiradas. safeTransferFrom también comprueba si el contrato tiene código, por lo que esto fallará si
3 // _from es una EOA o address(0).
4 // slither-disable-next-line reentrancy-events, reentrancy-benign
5 IERC20(_l1Token).safeTransferFrom(_from, address(this), _amount);

Las transferencias de tokens ERC-20 siguen un proceso diferente al de ETH:

  1. El usuario (_from) otorga un permiso al puente para transferir los tokens apropiados.
  2. El usuario llama al puente con la dirección del contrato del token, la cantidad, etc.
  3. El puente transfiere los tokens (a sí mismo) como parte del proceso de depósito.

El primer paso puede ocurrir en una transacción separada de los dos últimos. Sin embargo, el front-running no es un problema porque las dos funciones que llaman a _initiateERC20Deposit (depositERC20 y depositERC20To) solo llaman a esta función con msg.sender como el parámetro _from.

1 // Construir datos de llamada para _l2Token.finalizeDeposit(_to, _amount)
2 bytes memory message = abi.encodeWithSelector(
3 IL2ERC20Bridge.finalizeDeposit.selector,
4 _l1Token,
5 _l2Token,
6 _from,
7 _to,
8 _amount,
9 _data
10 );
11
12 // Enviar datos de llamada a L2
13 // slither-disable-next-line reentrancy-events, reentrancy-benign
14 sendCrossDomainMessage(l2TokenBridge, _l2Gas, message);
15
16 // slither-disable-next-line reentrancy-benign
17 deposits[_l1Token][_l2Token] = deposits[_l1Token][_l2Token] + _amount;
Mostrar todo

Añada la cantidad de tokens depositados a la estructura de datos deposits. Podría haber varias direcciones en L2 que correspondan al mismo token ERC-20 de L1, por lo que no es suficiente usar el saldo del puente del token ERC-20 de L1 para hacer un seguimiento de los depósitos.

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

El puente de L2 envía un mensaje al mensajero entre dominios de L2, lo que hace que el mensajero entre dominios de L1 llame a esta función (una vez que se envíe en L1 la transacción que finaliza el mensajeopens in a new tab, por supuesto).

1 ) external onlyFromCrossDomainAccount(l2TokenBridge) {

Asegúrese de que este sea un mensaje legítimo, proveniente del mensajero entre dominios y originado en el puente de tokens de L2. Esta función se utiliza para retirar ETH del puente, por lo que tenemos que asegurarnos de que solo sea llamada por el llamador autorizado.

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

La forma de transferir ETH es llamar al destinatario con la cantidad de wei en el msg.value.

1 require(success, "TransferHelper::safeTransferETH: Falló la transferencia de ETH");
2
3 // slither-disable-next-line reentrancy-events
4 emit ETHWithdrawalFinalized(_from, _to, _amount, _data);

Emitir un evento sobre la retirada.

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

Esta función es similar a finalizeETHWithdrawal anterior, con los cambios necesarios para los tokens ERC-20.

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

Actualizar la estructura de datos deposits.

1
2 // Cuando se finaliza una retirada en L1, el puente de L1 transfiere los fondos al retirante
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 * Temporal - Migración de ETH *
13 *****************************/
14
15 /**
16 * @dev Añade saldo de ETH a la cuenta. Esto está destinado a permitir que el ETH
17 * se migre de una puerta de enlace antigua a una nueva.
18 * NOTA: Esto se deja solo para una actualización para que podamos recibir el ETH migrado desde el
19 * contrato antiguo
20 */
21 function donateETH() external payable {}
22}
Mostrar todo

Había una implementación anterior del puente. Cuando pasamos de esa implementación a esta, tuvimos que mover todos los activos. Los tokens ERC-20 simplemente se pueden mover. Sin embargo, para transferir ETH a un contrato se necesita la aprobación de ese contrato, que es lo que nos proporciona donateETH.

Tokens ERC-20 en L2

Para que un token ERC-20 encaje en el puente estándar, necesita permitir que el puente estándar, y solo el puente estándar, mintee tokens. Esto es necesario porque los puentes deben garantizar que el número de tokens que circulan en Optimism sea igual al número de tokens bloqueados dentro del contrato de puente de L1. Si hay demasiados tokens en L2, algunos usuarios no podrían puentear sus activos de vuelta a L1. En lugar de un puente de confianza, estaríamos esencialmente recreando la banca de reserva fraccionariaopens in a new tab. Si hay demasiados tokens en L1, algunos de esos tokens permanecerían bloqueados dentro del contrato de puente para siempre, porque no hay forma de liberarlos sin quemar tokens de L2.

IL2StandardERC20

Cada token ERC-20 en L2 que utiliza el puente estándar necesita proporcionar esta interfazopens in a new tab, que tiene las funciones y eventos que el puente estándar necesita.

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

La interfaz estándar ERC-20opens in a new tab no incluye las funciones mint y burn. Esos métodos no son requeridos por el estándar ERC-20opens in a new tab, que deja sin especificar los mecanismos para crear y destruir tokens.

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

La interfaz ERC-165opens in a new tab se utiliza para especificar qué funciones proporciona un contrato. Puede leer el estándar aquíopens in a new tab.

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

Esta función proporciona la dirección del token de L1 que se transfiere a este contrato mediante un puente. Tenga en cuenta que no tenemos una función similar en la dirección opuesta. Necesitamos poder puentear cualquier token de L1, independientemente de si el soporte de L2 fue planeado cuando se implementó o no.

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}

Funciones y eventos para mintear (crear) y quemar (destruir) tokens. El puente debe ser la única entidad que pueda ejecutar estas funciones para garantizar que el número de tokens sea correcto (igual al número de tokens bloqueados en L1).

L2StandardERC20

Esta es nuestra implementación de la interfaz IL2StandardERC20opens in a new tab. A menos que necesite algún tipo de lógica personalizada, debería usar esta.

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

El contrato ERC-20 de OpenZeppelinopens in a new tab. Optimism no cree en reinventar la rueda, especialmente cuando la rueda está bien auditada y necesita ser lo suficientemente confiable como para albergar activos.

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

Estos son los dos parámetros de configuración adicionales que requerimos y que ERC-20 normalmente no tiene.

1
2 /**
3 * @param _l2Bridge Dirección del puente estándar de L2.
4 * @param _l1Token Dirección del token de L1 correspondiente.
5 * @param _name Nombre del ERC20.
6 * @param _symbol Símbolo del ERC20.
7 */
8 constructor(
9 address _l2Bridge,
10 address _l1Token,
11 string memory _name,
12 string memory _symbol
13 ) ERC20(_name, _symbol) {
14 l1Token = _l1Token;
15 l2Bridge = _l2Bridge;
16 }
Mostrar todo

Primero, llame al constructor del contrato del que heredamos (ERC20(_name, _symbol)) y luego establezca nuestras propias variables.

1
2 modifier onlyL2Bridge() {
3 require(msg.sender == l2Bridge, "Solo el puente L2 puede mintear y quemar");
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 }
Mostrar todo

Así es como funciona ERC-165opens in a new tab. Cada interfaz es un número de funciones admitidas, y se identifica como el o exclusivoopens in a new tab de los selectores de función de ABIopens in a new tab de esas funciones.

El puente de L2 utiliza ERC-165 como una comprobación de sanidad para asegurarse de que el contrato ERC-20 al que envía activos es un IL2StandardERC20.

Nota: No hay nada que impida que un contrato malicioso proporcione respuestas falsas a supportsInterface, por lo que este es un mecanismo de comprobación de sanidad, no un mecanismo de seguridad.

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}
Mostrar todo

Solo el puente de L2 puede mintear y quemar activos.

_mint y _burn se definen en realidad en el contrato ERC-20 de OpenZeppelin. Ese contrato simplemente no los expone externamente, porque las condiciones para mintear y quemar tokens son tan variadas como el número de formas de usar ERC-20.

Código del puente de L2

Este es el código que ejecuta el puente en Optimism. El código fuente de este contrato está aquí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";

La interfaz IL2ERC20Bridgeopens in a new tab es muy similar al equivalente de L1 que vimos anteriormente. Existen dos diferencias significativas:

  1. En L1 se inician los depósitos y se finalizan las retiradas. Aquí se inician las retiradas y se finalizan los depósitos.
  2. En L1 es necesario distinguir entre ETH y tokens ERC-20. En L2 podemos usar las mismas funciones para ambos porque, internamente, los saldos de ETH en Optimism se gestionan como un token ERC-20 con la dirección 0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000opens 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 El puente estándar de L2 es un contrato que funciona junto con el puente estándar de L1 para
12 * permitir las transiciones de ETH y ERC20 entre L1 y L2.
13 * Este contrato actúa como un minteador de nuevos tokens cuando recibe información sobre depósitos en el puente
14 * estándar de L1.
15 * Este contrato también actúa como un quemador de los tokens destinados a la retirada, informando al puente de L1
16 * para que libere los fondos de L1.
17 */
18contract L2StandardBridge is IL2ERC20Bridge, CrossDomainEnabled {
19 /********************************
20 * Referencias a contratos externos *
21 ********************************/
22
23 address public l1TokenBridge;
Mostrar todo

Mantener un registro de la dirección del puente de L1. Tenga en cuenta que, a diferencia del equivalente de L1, aquí necesitamos esta variable. La dirección del puente de L1 no se conoce de antemano.

1
2 /***************
3 * Constructor *
4 ***************/
5
6 /**
7 * @param _l2CrossDomainMessenger Mensajero entre dominios utilizado por este contrato.
8 * @param _l1TokenBridge Dirección del puente de L1 implementado en la cadena principal.
9 */
10 constructor(address _l2CrossDomainMessenger, address _l1TokenBridge)
11 CrossDomainEnabled(_l2CrossDomainMessenger)
12 {
13 l1TokenBridge = _l1TokenBridge;
14 }
15
16 /***************
17 * Retirada *
18 ***************/
19
20 /**
21 * @inheritdoc IL2ERC20Bridge
22 */
23 function withdraw(
24 address _l2Token,
25 uint256 _amount,
26 uint32 _l1Gas,
27 bytes calldata _data
28 ) external virtual {
29 _initiateWithdrawal(_l2Token, msg.sender, msg.sender, _amount, _l1Gas, _data);
30 }
31
32 /**
33 * @inheritdoc IL2ERC20Bridge
34 */
35 function withdrawTo(
36 address _l2Token,
37 address _to,
38 uint256 _amount,
39 uint32 _l1Gas,
40 bytes calldata _data
41 ) external virtual {
42 _initiateWithdrawal(_l2Token, msg.sender, _to, _amount, _l1Gas, _data);
43 }
Mostrar todo

Estas dos funciones inician las retiradas. Tenga en cuenta que no es necesario especificar la dirección del token de L1. Se espera que los tokens de L2 nos indiquen la dirección equivalente en L1.

1
2 /**
3 * @dev Realiza la lógica de las retiradas quemando el token e informando
4 * a la puerta de enlace de tokens de L1 de la retirada.
5 * @param _l2Token Dirección del token de L2 donde se inicia la retirada.
6 * @param _from Cuenta de la que se retira el token en L2.
7 * @param _to Cuenta a la que se entrega la retirada en L1.
8 * @param _amount Cantidad del token a retirar.
9 * @param _l1Gas No se utiliza, pero se incluye para posibles consideraciones de compatibilidad futura.
10 * @param _data Datos opcionales para reenviar a L1. Estos datos se proporcionan
11 * únicamente para la comodidad de los contratos externos. Aparte de imponer una longitud máxima,
12 * estos contratos no ofrecen ninguna garantía sobre su contenido.
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 // Cuando se inicia una retirada, quemamos los fondos del retirante para evitar un uso posterior en L2
23 // slither-disable-next-line reentrancy-events
24 IL2StandardERC20(_l2Token).burn(msg.sender, _amount);
Mostrar todo

Tenga en cuenta que no estamos confiando en el parámetro _from, sino en msg.sender, que es mucho más difícil de falsificar (imposible, que yo sepa).

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

En L1 es necesario distinguir entre ETH y 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 // Enviar mensaje al puente de L1
21 // slither-disable-next-line reentrancy-events
22 sendCrossDomainMessage(l1TokenBridge, _l1Gas, message);
23
24 // slither-disable-next-line reentrancy-events
25 emit WithdrawalInitiated(l1Token, _l2Token, msg.sender, _to, _amount, _data);
26 }
27
28 /************************************
29 * Función entre cadenas: Depósito *
30 ************************************/
31
32 /**
33 * @inheritdoc IL2ERC20Bridge
34 */
35 function finalizeDeposit(
36 address _l1Token,
37 address _l2Token,
38 address _from,
39 address _to,
40 uint256 _amount,
41 bytes calldata _data
Mostrar todo

Esta función es llamada por L1StandardBridge.

1 ) external virtual onlyFromCrossDomainAccount(l1TokenBridge) {

Asegúrese de que el origen del mensaje sea legítimo. Esto es importante porque esta función llama a _mint y podría usarse para dar tokens que no están cubiertos por los tokens que el puente posee en L1.

1 // Comprobar que el token de destino es compatible y
2 // verificar que el token depositado en L1 coincide con la representación del token depositado en L2 aquí
3 if (
4 // slither-disable-next-line reentrancy-events
5 ERC165Checker.supportsInterface(_l2Token, 0x1d1d8b63) &&
6 _l1Token == IL2StandardERC20(_l2Token).l1Token()

Comprobaciones de sanidad:

  1. La interfaz correcta es compatible
  2. La dirección de L1 del contrato ERC-20 de L2 coincide con el origen de los tokens en L1
1 ) {
2 // Cuando se finaliza un depósito, acreditamos la cuenta en L2 con la misma cantidad de
3 // tokens.
4 // slither-disable-next-line reentrancy-events
5 IL2StandardERC20(_l2Token).mint(_to, _amount);
6 // slither-disable-next-line reentrancy-events
7 emit DepositFinalized(_l1Token, _l2Token, _from, _to, _amount, _data);

Si las comprobaciones de sanidad pasan, finalice el depósito:

  1. Mintear los tokens
  2. Emitir el evento apropiado
1 } else {
2 // O bien el token de L2 en el que se está depositando no está de acuerdo con la dirección correcta
3 // de su token de L1, o no admite la interfaz correcta.
4 // Esto solo debería ocurrir si hay un token de L2 malicioso, o si un usuario de alguna manera
5 // especificó la dirección de token de L2 incorrecta para depositar.
6 // En cualquier caso, detenemos el proceso aquí y construimos un mensaje de retirada
7 // para que los usuarios puedan recuperar sus fondos en algunos casos.
8 // No hay forma de evitar por completo los contratos de tokens maliciosos, pero esto limita
9 // los errores del usuario y mitiga algunas formas de comportamiento malicioso de los contratos.
Mostrar todo

Si un usuario cometió un error detectable al usar la dirección de token de L2 incorrecta, queremos cancelar el depósito y devolver los tokens en L1. La única forma en que podemos hacer esto desde L2 es enviar un mensaje que tendrá que esperar el período de impugnación de errores, pero eso es mucho mejor para el usuario que perder los tokens permanentemente.

1 bytes memory message = abi.encodeWithSelector(
2 IL1ERC20Bridge.finalizeERC20Withdrawal.selector,
3 _l1Token,
4 _l2Token,
5 _to, // se cambiaron _to y _from aquí para devolver el depósito al remitente
6 _from,
7 _amount,
8 _data
9 );
10
11 // Enviar mensaje al puente de L1
12 // slither-disable-next-line reentrancy-events
13 sendCrossDomainMessage(l1TokenBridge, 0, message);
14 // slither-disable-next-line reentrancy-events
15 emit DepositFailed(_l1Token, _l2Token, _from, _to, _amount, _data);
16 }
17 }
18}
Mostrar todo

Conclusión

El puente estándar es el mecanismo más flexible para las transferencias de activos. Sin embargo, debido a que es tan genérico, no siempre es el mecanismo más fácil de usar. Especialmente para las retiradas, la mayoría de los usuarios prefieren usar puentes de tercerosopens in a new tab que no esperan el período de impugnación y no requieren una prueba de Merkle para finalizar la retirada.

Estos puentes suelen funcionar teniendo activos en L1, que proporcionan inmediatamente por una pequeña tarifa (a menudo menor que el coste de gas para una retirada de puente estándar). Cuando el puente (o las personas que lo gestionan) prevé que se quedará sin activos en L1, transfiere suficientes activos desde L2. Como se trata de retiradas muy grandes, el coste de la retirada se amortiza sobre una gran cantidad y es un porcentaje mucho menor.

Esperamos que este artículo le haya ayudado a entender más sobre cómo funciona la capa 2 y cómo escribir código de Solidity que sea claro y seguro.

Vea aquí más de mi trabajoopens in a new tab.

Última actualización de la página: 22 de octubre de 2025

¿Le ha resultado útil este tutorial?