Ir al contenido principal

Recorrido por contrato de puente estándar de Optimism

soliditypuentecapa 2
Intermedio
Ori Pomerantz
30 de marzo de 2022
34 minuto leído minute read

Optimism(opens in a new tab) es un Optimistic rollup. Los Optimistic rollups 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, los datos se escriben en L1 para que todo pueda ser probado y reconstruido con todas las garantías de integridad y disponibilidad de la red principal.

Para utilizar activos de L1 en Optimism (o cualquier otra L2), los activos deben "puentearse". Una manera de lograr esto es que los usuarios bloqueen activos (ETH y los tokens ERC-20 son los más comunes) en L1 y recibir activos equivalentes para usar en L2. Eventualmente, quien acabe poseyéndolos puede querer puentearlos de vuelta a la L1. Al hacer esto, los activos se queman en L2 y luego se liberan nuevamente al usuario en L1.

Así es como funciona el puente estándar de Optimism(opens in a new tab). En este artículo analizaremos el código fuente de ese puente para ver cómo funciona y lo estudiaremos como ejemplo de código de Solidity bien escrito.

Flujos de control

El puente tiene dos flujos principales:

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

Flujo de depósito

Capa 1

  1. Si se deposita un ERC-20, el depositante le da al puente una asignación para gastar la cantidad depositada.
  2. El depositante llama al puente 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 la asignación proporcionada por el depositante.
  4. El puente L1 utiliza el mecanismo de mensajes de dominio cruzado para llamar a finalizeDeposit en el puente L2

Capa 2

  1. El puente L2 verifica que la llamada a finalizeDeposit sea legítima:
    • Procede del contrato de mensajes de dominio cruzado
    • Era originalmente del puente en L1
  2. El puente de L2 comprueba si el contrato de token ERC-20 en L2 es el correcto:
    • El contrato L2 informa de que su contraparte en L1 es la misma de la que provienen los tokens de L1
    • El contrato de L2 informa que soporta la interfaz correcta (usando ERC-165(opens 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 retiro para permitir al usuario reclamar los tokens en L1.

Flujo de retiro

Capa 2

  1. El que hace el retiro 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 L2 utiliza el mecanismo de mensajes entre dominios para llamar a finalizeETHWithdrawal o finalizeERC20Withdrawal en el puente L1

Capa 1

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

Código de capa 1

Este es el código que se ejecuta en 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
Copiar

La mayor parte del código de Optimism se libera bajo la licencia MIT(opens in a new tab).

1pragma solidity >0.5.0 <0.9.0;
Copiar

Al momento de escribir este artículo, la última versión de Solidity es 0.8.12. Hasta que la versión 0.9.0 sea liberada, no sabemos si este código será compatible.

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

En la terminología de puentes de Optimism, deposit significa transferir de L1 a L2, y withdrawal significa transferir de L2 a L1.

1 address indexed _l1Token,
2 address indexed _l2Token,
Copiar

En la mayoría de los casos, la dirección de un ERC-20 en L1 no es la misma 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 chainId son para la red de pruebas Kovan (42) y la red de pruebas Optimistic Kovan (69).

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

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

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

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

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

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

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

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

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

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

1 /*************************
2 * Cross-chain Functions *
3 *************************/
4
5 /**
6 * @dev Complete a withdrawal from L2 to L1, and credit funds to the recipient's balance of the
7 * L1 ERC20 token.
8 * This call will fail if the initialized withdrawal from L2 has not been finalized.
9 *
10 * @param _l1Token Address of L1 token to finalizeWithdrawal for.
11 * @param _l2Token Address of L2 token where withdrawal was initiated.
12 * @param _from L2 address initiating the transfer.
13 * @param _to L1 address to credit the withdrawal to.
14 * @param _amount Amount of the ERC20 to deposit.
15 * @param _data Data provided by the sender on L2. This data is provided
16 * solely as a convenience for external contracts. Aside from enforcing a maximum
17 * length, these contracts provide no guarantees about its content.
18 */
19 function finalizeERC20Withdrawal(
20 address _l1Token,
21 address _l2Token,
22 address _from,
23 address _to,
24 uint256 _amount,
25 bytes calldata _data
26 ) external;
27}
Mostrar todo
Copiar

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

  1. Una transacción iniciante 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 periodo de desafío (o reclamo) por falta(opens in a new tab) para la transacción 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 arriba 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 maneja tal 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 * Events *
12 **********/
13 event ETHDepositInitiated(
14 address indexed _from,
15 address indexed _to,
16 uint256 _amount,
17 bytes _data
18 );
Mostrar todo
Copiar

Este evento es casi idéntico a la versión ERC-20 (ERC20DepositInitiated), excepto que no incluye las direcciones del token en L1 y L2. Lo mismo es válido para otros eventos y las funciones.

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

CrossDomainEnabled

Este contrato(opens 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";
Copiar

Esta interfaz(opens in a new tab) le dice al contrato cómo enviar mensajes a la otra capa usando el mensajero de dominio cruzado. Este mensajero de dominio cruzado es un sistema completamente diferente y merece su propio artículo, que espero escribir en el futuro.

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

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

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

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 solo confíe en ciertos mensajes si provienen del puente del otro lado.

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

Solo se puede confiar en los mensajes del mensajero de dominio cruzado apropiado (messenger, como ve a continuación).

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

La forma en que el mensajero de dominio cruzado proporciona la dirección que envió un mensaje con la otra capa es la función .xDomainMessageSender()(opens in a new tab). Siempre y cuando se la llame en la transacción iniciada por el mensaje, puede proporcionar esta información.

Tenemos que asegurarnos de que el mensaje que recibimos haya venido del otro puente.

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

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

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

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

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

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

  1. Eventos de reentrada(opens in a new tab)
  2. Reentrada benigna(opens in a new tab)
1 getCrossDomainMessenger().sendMessage(_crossDomainTarget, _message, _gasLimit);
2 }
3}
Copiar

En este caso no estamos preocupados sobre reentradas, ya que sabemos que getCrossDomainMessenger() devuelve una dirección confiable, incluso si Slither no tiene manera de saberlo.

El contrato de puente L1

El códgo fuente para este contrato está aquí(opens in a new tab).

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

Las interfaces pueden ser parte de otros contratos, por lo que tienen que admitir un amplio rango de versiones de Solidity. Pero el puente en sí es nuestro contrato, y podemos ser estrictos en cuanto a la versión de Solidity utilizada.

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

IL1ERC20Bridge y IL1StandardBridge se explican arriba.

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

Esta interfaz(opens 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";
Copiar

Esta interfaz(opens in a new tab) nos permite controlar contratos ERC-20. Puede leer más al respecto aquí.

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

Como se explicó más arriba, este contrato se utiliza para mensajes entre capas.

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

Lib_PredeployAddresses(opens in a new tab) tiene las direcciones para los contratos en L2 que siempre tienen la misma dirección. Esto incluye el puente estándar en L2.

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

Utilidades de dirección de OpenZeppelin(opens in a new tab). Se utiliza para distinguir entre las direcciones del 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 hechas desde el constructor de un contrato, pero al menos esto nos permite identificar y prevenir algunos errores de usuario comunes.

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

El estándar ERC-20(opens in a new tab) permite dos formas para que un contrato reporte fallas:

  1. Revertir
  2. Devolver false

Manejar ambos casos complicaría nuestro código, así que en su lugar utilizamos SafeERC20 de OpenZeppelin(opens in a new tab), el cual asegura que todas las fallas resulten en una reversión(opens in a new tab).

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

Esta línea es cómo especificamos usar el wrapper SafeERC20 cada vez que usamos la interfaz IERC20.

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

La dirección de L2StandardBridge.

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

Un mapeo(opens in a new tab) doble como este es la forma en que se define una sparse array bidimensional(opens in a new tab). Los valores de esta estructura de datos se identifican como deposit[L1 token addr][L2 token addr]. El valor por defecto es cero. Solo las celdas configuradas con un valor diferente se escriben en el almacenamiento.

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

Para poder actualizar este contrato sin tener que copiar todas las variables en el almacenamiento. Para ello usamos un Proxy(opens in a new tab), un contrato que usa delegatecall(opens in a new tab) para transferir llamadas a un contacto separado cuya dirección se almacena en el contrato proxy (cuando actualiza le dice al proxy que cambie esa dirección). Cuando usa delegatecall el almacenamiento sigue siendo el almacenamiento del contrato invocante, así que los valores de todas las variables de estado del contrato no se vean afectados.

Un efecto de este patrón es que el almacenamiento del contrato invocado de delegatecall no se utiliza y, por tanto, los valores del constructor que le son pasados 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 siguiente es independiente del constructor.

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

Esta prueba de Slither(opens in a new tab) identifica funciones que no son llamadas desde el código del contrato y, por lo tanto, podrían declararse como external en lugar de public. El costo de gas de las funciones external puede ser menor, porque pueden ser proporcionadas con parámetros en los datos de llamada. Las funciones declaradas como public deben ser accesibles desde el 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 tal función externamente, es necesario copiar los datos de llamada en la memoria, lo que cuesta gas. En este caso se invoca la función solo una vez, por lo que la ineficiencia no nos importa.

1 function initialize(address _l1messenger, address _l2TokenBridge) public {
2 require(messenger == address(0), "Contract has already been initialized.");
Copiar

La función initialize debe ser invocada una única vez. Si la dirección del mensajero de dominio cruzado L1 o el puente del token L2 cambia, creamos un nuevo proxy y un nuevo puente que lo llame. Es poco probable que esto ocurra, excepto cuando se actualiza todo el sistema, algo muy raro.

Tenga en cuenta que esta función no tiene ningún mecanismo que restrinja quién puede llamarlo. 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-run(opens in a new tab) para llegar a la función initialize antes de que el usuario legítimo lo haga. Pero hay dos métodos para prevenir esto:

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

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

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

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

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

Esta función existe con 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
Copiar

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

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

La forma en que funcionan los mensajes entre dominios es que el contrato de destino es llamado con el mensaje como sus datos de llamada. Los contratos de Solidity interpretan siempre sus datos de llamada de acuerdo con las especificaciones de la ABI(opens in a new tab). La función de Solidity abi.encodeWithSelector(opens 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 );
Copiar

El mensaje aquí es llamar a la función finalizeDeposit(opens 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 L2 que administra ETH en Optimism, 0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000 (este contrato es solo para uso interno en Optimism)
_from_fromLa dirección en L1 que envía el ETH
_to_toLa dirección en L2 que recibe el ETH
amountmsg.valueCantidad de wei enviado (que ya ha sido enviado al puente)
_data_dataFecha adicional a adjuntar al depósito
1 // Send calldata into L2
2 // slither-disable-next-line reentrancy-events
3 sendCrossDomainMessage(l2TokenBridge, _l2Gas, message);
Copiar

Enviar el mensaje a través del mensajero de dominio cruzado.

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

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

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
Copiar

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

1 /**
2 * @dev Performs the logic for deposits by informing the L2 Deposited Token
3 * contract of the deposit and calling a handler to lock the L1 funds. (e.g. transferFrom)
4 *
5 * @param _l1Token Address of the L1 ERC20 we are depositing
6 * @param _l2Token Address of the L1 respective L2 ERC20
7 * @param _from Account to pull the deposit from on L1
8 * @param _to Account to give the deposit to on L2
9 * @param _amount Amount of the ERC20 to deposit.
10 * @param _l2Gas Gas limit required to complete the deposit on L2.
11 * @param _data Optional data to forward to L2. This data is provided
12 * solely as a convenience for external contracts. Aside from enforcing a maximum
13 * length, these contracts provide no guarantees about its content.
14 */
15 function _initiateERC20Deposit(
16 address _l1Token,
17 address _l2Token,
18 address _from,
19 address _to,
20 uint256 _amount,
21 uint32 _l2Gas,
22 bytes calldata _data
23 ) internal {
Mostrar todo
Copiar

Esta función es similar a la función _initiateETHDeposit anterior, con algunas diferencias importantes. La primera diferencia es que esta función recibe las direcciones del token 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 // When a deposit is initiated on L1, the L1 Bridge transfers the funds to itself for future
2 // withdrawals. safeTransferFrom also checks if the contract has code, so this will fail if
3 // _from is an EOA or address(0).
4 // slither-disable-next-line reentrancy-events, reentrancy-benign
5 IERC20(_l1Token).safeTransferFrom(_from, address(this), _amount);
Copiar

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

  1. El usuario (_from) le da una autorización al puente para transferir los tokens apropiados.
  2. El usuario llama al puente con la dirección del contrato de 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, hacer 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 // Construct calldata for _l2Token.finalizeDeposit(_to, _amount)
2 bytes memory message = abi.encodeWithSelector(
3 IL2ERC20Bridge.finalizeDeposit.selector,
4 _l1Token,
5 _l2Token,
6 _from,
7 _to,
8 _amount,
9 _data
10 );
11
12 // Send calldata into L2
13 // slither-disable-next-line reentrancy-events, reentrancy-benign
14 sendCrossDomainMessage(l2TokenBridge, _l2Gas, message);
15
16 // slither-disable-next-line reentrancy-benign
17 deposits[_l1Token][_l2Token] = deposits[_l1Token][_l2Token] + _amount;
Mostrar todo
Copiar

Añada la cantidad de tokens depositados a la estructura de datos de deposits. Podría haber varias direcciones en L2 que correspondan al mismo token ERC-20 L1, por lo que no es suficiente usar el saldo del puente del token ERC-20 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 * Cross-chain Functions *
8 *************************/
9
10 /**
11 * @inheritdoc IL1StandardBridge
12 */
13 function finalizeETHWithdrawal(
14 address _from,
15 address _to,
16 uint256 _amount,
17 bytes calldata _data
Mostrar todo
Copiar

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

1 ) external onlyFromCrossDomainAccount(l2TokenBridge) {
Copiar

Asegúrese de que este sea un mensaje legítimo, proveniente del mensajero de dominio cruzado y que se origine con el puente de token L2. Esta función se utiliza para retirar ETH del puente, así que tenemos que asegurarnos de que solo sea invocada por el llamador autorizado.

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

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

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

Emita un evento sobre el retiro.

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
Copiar

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

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

Actualizar la estructura de datos de deposits.

1
2 // When a withdrawal is finalized on L1, the L1 Bridge transfers the funds to the withdrawer
3 // slither-disable-next-line reentrancy-events
4 IERC20(_l1Token).safeTransfer(_to, _amount);
5
6 // slither-disable-next-line reentrancy-events
7 emit ERC20WithdrawalFinalized(_l1Token, _l2Token, _from, _to, _amount, _data);
8 }
9
10
11 /*****************************
12 * Temporary - Migrating ETH *
13 *****************************/
14
15 /**
16 * @dev Adds ETH balance to the account. This is meant to allow for ETH
17 * to be migrated from an old gateway to a new gateway.
18 * NOTE: This is left for one upgrade only so we are able to receive the migrated ETH from the
19 * old contract
20 */
21 function donateETH() external payable {}
22}
Mostrar todo
Copiar

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

Tokens ERC-20 en L2

Para que un token ERC-20 se ajuste al 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 que se encuentran bloqueados dentro del contrato de puente 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, esencialmente recrearíamos banca de reserva fraccionaria(opens 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 los tokens de L2.

IL2StandardERC20

Todos los tokens ERC-20 en L2 que utilicen el puente estándar deben proporcionar esta interfaz(opens 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";
Copiar

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

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

La interfaz ERC-165(opens 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);
Copiar

Esta función proporciona la dirección del token L1 puenteado a este contrato. Tenga en cuenta que no tenemos una función similar en la dirección opuesta. Tenemos que ser capaces de puentear cualquier token L1, independientemente de que el soporte a L2 se haya planificado o no cuando se implementó.

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

Funciones y eventos para mintear (cear) y quemar (destruir) tokens. El puente debe ser la única entidad que puede ejecutar estas funciones para asegurar 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 IL2StandardERC20(opens in a new tab). A menos que necesite algún tipo de lógica personalizada, debería utilizar esta.

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

El contrato OpenZeppelin ERC-20(opens in a new tab). Optimism no cree en reinventar la rueda, especialmente cuando la rueda está bien auditada y necesita ser lo suficientemente fiable como para mantener los activos.

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

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

1
2 /**
3 * @param _l2Bridge Address of the L2 standard bridge.
4 * @param _l1Token Address of the corresponding L1 token.
5 * @param _name ERC20 name.
6 * @param _symbol ERC20 symbol.
7 */
8 constructor(
9 address _l2Bridge,
10 address _l1Token,
11 string memory _name,
12 string memory _symbol
13 ) ERC20(_name, _symbol) {
14 l1Token = _l1Token;
15 l2Bridge = _l2Bridge;
16 }
Mostrar todo
Copiar

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

1
2 modifier onlyL2Bridge() {
3 require(msg.sender == l2Bridge, "Only L2 Bridge can mint and burn");
4 _;
5 }
6
7
8 // slither-disable-next-line external-function
9 function supportsInterface(bytes4 _interfaceId) public pure returns (bool) {
10 bytes4 firstSupportedInterface = bytes4(keccak256("supportsInterface(bytes4)")); // ERC165
11 bytes4 secondSupportedInterface = IL2StandardERC20.l1Token.selector ^
12 IL2StandardERC20.mint.selector ^
13 IL2StandardERC20.burn.selector;
14 return _interfaceId == firstSupportedInterface || _interfaceId == secondSupportedInterface;
15 }
Mostrar todo
Copiar

Esta es la manera en que funciona ERC-165(opens in a new tab). Cada interfaz es un número de funciones soportadas, y se identifica como la exclusiva o(opens in a new tab) de los selectores de funciones ABI(opens in a new tab) de esas funciones.

El puente L2 utiliza ERC-165 como sanity check (comprobación de cordura) para asegurarse de que el contrato ERC-20 al que envía activos sea un IL2StandardERC20.

Nota: No hay nada que impida que un contrato deshonesto proporcione respuestas falsas a supportsInterface, por lo que esto es un mecanismo de comprobación de cordura, 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
Copiar

Solo el puente L2 puede mintear y quemar activos.

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

Código del puente L2

Este es el código que ejecuta el puente en Optimism. La 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";
Copiar

La interfaz IL2ERC20Bridge(opens in a new tab) es muy similar a la equivalente en L1 que vimos arriba. Hay dos diferencias significativas:

  1. En L1 usted inicia depósitos y finaliza retiros. Aquí usted inicia retiros y finaliza 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 son manejados como un token ERC-20 con la dirección 0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD000(opens in a new tab).
1/* Library Imports */
2import { ERC165Checker } from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol";
3import { CrossDomainEnabled } from "../../libraries/bridge/CrossDomainEnabled.sol";
4import { Lib_PredeployAddresses } from "../../libraries/constants/Lib_PredeployAddresses.sol";
5
6/* Contract Imports */
7import { IL2StandardERC20 } from "../../standards/IL2StandardERC20.sol";
8
9/**
10 * @title L2StandardBridge
11 * @dev The L2 Standard bridge is a contract which works together with the L1 Standard bridge to
12 * enable ETH and ERC20 transitions between L1 and L2.
13 * This contract acts as a minter for new tokens when it hears about deposits into the L1 Standard
14 * bridge.
15 * This contract also acts as a burner of the tokens intended for withdrawal, informing the L1
16 * bridge to release L1 funds.
17 */
18contract L2StandardBridge is IL2ERC20Bridge, CrossDomainEnabled {
19 /********************************
20 * External Contract References *
21 ********************************/
22
23 address public l1TokenBridge;
Mostrar todo
Copiar

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

1
2 /***************
3 * Constructor *
4 ***************/
5
6 /**
7 * @param _l2CrossDomainMessenger Cross-domain messenger used by this contract.
8 * @param _l1TokenBridge Address of the L1 bridge deployed to the main chain.
9 */
10 constructor(address _l2CrossDomainMessenger, address _l1TokenBridge)
11 CrossDomainEnabled(_l2CrossDomainMessenger)
12 {
13 l1TokenBridge = _l1TokenBridge;
14 }
15
16 /***************
17 * Withdrawing *
18 ***************/
19
20 /**
21 * @inheritdoc IL2ERC20Bridge
22 */
23 function withdraw(
24 address _l2Token,
25 uint256 _amount,
26 uint32 _l1Gas,
27 bytes calldata _data
28 ) external virtual {
29 _initiateWithdrawal(_l2Token, msg.sender, msg.sender, _amount, _l1Gas, _data);
30 }
31
32 /**
33 * @inheritdoc IL2ERC20Bridge
34 */
35 function withdrawTo(
36 address _l2Token,
37 address _to,
38 uint256 _amount,
39 uint32 _l1Gas,
40 bytes calldata _data
41 ) external virtual {
42 _initiateWithdrawal(_l2Token, msg.sender, _to, _amount, _l1Gas, _data);
43 }
Mostrar todo
Copiar

Estas dos funciones inician retiros. Tenga en cuenta que no hay necesidad de especificar la dirección del token L1. Se espera que los tokens L2 nos indiquen la dirección equivalente en L1.

1
2 /**
3 * @dev Performs the logic for withdrawals by burning the token and informing
4 * the L1 token Gateway of the withdrawal.
5 * @param _l2Token Address of L2 token where withdrawal is initiated.
6 * @param _from Account to pull the withdrawal from on L2.
7 * @param _to Account to give the withdrawal to on L1.
8 * @param _amount Amount of the token to withdraw.
9 * @param _l1Gas Unused, but included for potential forward compatibility considerations.
10 * @param _data Optional data to forward to L1. This data is provided
11 * solely as a convenience for external contracts. Aside from enforcing a maximum
12 * length, these contracts provide no guarantees about its content.
13 */
14 function _initiateWithdrawal(
15 address _l2Token,
16 address _from,
17 address _to,
18 uint256 _amount,
19 uint32 _l1Gas,
20 bytes calldata _data
21 ) internal {
22 // When a withdrawal is initiated, we burn the withdrawer's funds to prevent subsequent L2
23 // usage
24 // slither-disable-next-line reentrancy-events
25 IL2StandardERC20(_l2Token).burn(msg.sender, _amount);
Mostrar todo
Copiar

Tenga en cuenta que no usamos el parámetro _from, sino msg.sender, que es mucho más difícil de falsificar (imposible, por lo que sé).

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

En L1 es necesario distinguir entre ETH y tokens ERC-20.

1 message = abi.encodeWithSelector(
2 IL1StandardBridge.finalizeETHWithdrawal.selector,
3 _from,
4 _to,
5 _amount,
6 _data
7 );
8 } else {
9 message = abi.encodeWithSelector(
10 IL1ERC20Bridge.finalizeERC20Withdrawal.selector,
11 l1Token,
12 _l2Token,
13 _from,
14 _to,
15 _amount,
16 _data
17 );
18 }
19
20 // Send message up to L1 bridge
21 // slither-disable-next-line reentrancy-events
22 sendCrossDomainMessage(l1TokenBridge, _l1Gas, message);
23
24 // slither-disable-next-line reentrancy-events
25 emit WithdrawalInitiated(l1Token, _l2Token, msg.sender, _to, _amount, _data);
26 }
27
28 /************************************
29 * Cross-chain Function: Depositing *
30 ************************************/
31
32 /**
33 * @inheritdoc IL2ERC20Bridge
34 */
35 function finalizeDeposit(
36 address _l1Token,
37 address _l2Token,
38 address _from,
39 address _to,
40 uint256 _amount,
41 bytes calldata _data
Mostrar todo
Copiar

Esta función es llamada por L1StandardBridge.

1 ) external virtual onlyFromCrossDomainAccount(l1TokenBridge) {
Copiar

Asegúrese de que la fuente del mensaje es legítima. Esto es importante porque esta función llama a _mint y podría ser usada para entregar tokens que no estén cubiertos por los tokens que el puente posee en L1.

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

Pruebas de cordura (sanity checks):

  1. Se admite la interfaz correcta.
  2. La dirección L1 del contrato ERC-20 L2 coincide con la fuente L1 de los tokens.
1 ) {
2 // When a deposit is finalized, we credit the account on L2 with the same amount of
3 // tokens.
4 // slither-disable-next-line reentrancy-events
5 IL2StandardERC20(_l2Token).mint(_to, _amount);
6 // slither-disable-next-line reentrancy-events
7 emit DepositFinalized(_l1Token, _l2Token, _from, _to, _amount, _data);
Copiar

Si las pruebas de cordura son satisfactorias, finalice el depósito:

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

Si un usuario realizó un error detectable mediante el uso de la dirección de token L2 incorrecta, queremos cancelar el depósito y devolver los tokens en L1. La única forma de hacerlo desde L2 es enviar un mensaje que tenga que esperar el período de desafío por falta, 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, // switched the _to and _from here to bounce back the deposit to the sender
6 _from,
7 _amount,
8 _data
9 );
10
11 // Send message up to L1 bridge
12 // slither-disable-next-line reentrancy-events
13 sendCrossDomainMessage(l1TokenBridge, 0, message);
14 // slither-disable-next-line reentrancy-events
15 emit DepositFailed(_l1Token, _l2Token, _from, _to, _amount, _data);
16 }
17 }
18}
Mostrar todo
Copiar

Conclusión

El puente estándar es el mecanismo más flexible para las transferencias de activos. Sin embargo, debido a que es muy genérico, no siempre es el mecanismo más fácil de utilizar. Especialmente para los retiros, la mayoría de los usuarios prefieren usar puentes de terceros(opens in a new tab) que no esperen el periodo de desafío y no requieran una prueba de Merkle para finalizar el retiro.

Estos puentes normalmente funcionan teniendo activos en L1, que proporcionan inmediatamente por una pequeña tarifa (a menudo menor que el costo del gas para un retiro de puente estándar). Cuando el puente (o la gente que lo ejecuta) anticipa quedarse con pocos activos en L1, transfiere suficientes activos de L2. Como se trata de retiros muy grandes, el costo de la retirada se amortiza en grandes cantidades y resulta en un porcentaje mucho menor.

Esperemos 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 de manera clara y segura.

Última edición: @lukassim(opens in a new tab), 1 de mayo de 2024

¿Le ha resultado útil este tutorial?