Ir al contenido principal

Paseo por el contrato estándar de puente en 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 Rollup Optimista. Los Rollups Optimistas pueden procesar transacciones por un precio mucho más bajo que la red Mainnet de Ethereum (también conocido como capa 1 o L1) porque las transacciones sólo 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 Mainnet.

Para utilizar activos L1 en Optimism (o cualquier otra L2), los activos deben ser puenteados. Una manera de lograr esto es que los usuarios bloqueen activos (ETH y tokens ERC-20 son los más comunes) en L1, y recibir activos equivalentes a 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 al usuario en L1.

Así es como funciona el puente estándar Optimism(opens in a new tab). En este artículo pasamos por el código fuente de ese puente para ver cómo funciona y estudiarlo como un 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 depósito 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 puentado
    • 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 depósito
  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 esté legitimada:
    • 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 L1 es la misma de la que provienen los tokens del L1
    • El contrato 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 acuñar el número apropiado de tokens a la dirección apropiada. Si no, inicie un proceso de retiro para permitir al usuario reclamar las fichas en L1.

Flujo de retiro

Capa 2

  1. Aquél que realiza el retiro llama al puente L2 (withdraw or 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 cruzados 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 Ethereum.

IL1ERC20Bridge

Esta interfaz está definida 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 está liberado bajo la licencia MIT(opens in a new tab).

1pragma solidity >0.5.0 <0.9.0;
Copiar

Al instante de esta escritura 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 es compatible con él o no.

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

En la terminología de puente 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. Puedes 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 predesplegado, 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 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 acuñar, 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 finalización o reclamación de transacción en L1. Esta transacción debe ocurrir después de que finalice el periodo de desafío de falta(opens in a new tab) para la transacción L2.

IL1StandardBridge

Esta interfaz está definida 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 sin las direcciones L1 y L2 del token. 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 puestes (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 mesajes 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 por 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 sólo 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 pueden confiar en los mensajes del mensajero transversal apropiado (messenger, como ves 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 .xDomainMessageSender() función(opens in a new tab). Siempre y cuando 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 vino 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 ésta 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 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 soportar una amplio rango de versiones de Solidity. Pero el puente en sí es nuestro contrato, y podemos ser estrictos con qué versión de Solidity utiliza.

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

IL1ERC20Bridge y IL1StandardBridge están explicados 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. Puedes 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 mensajear 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 del constructor de un contrato, pero al menos 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) soporta dos formas para que un contrato reporte fallido:

  1. Revertir
  2. Devolver false

Manejar ambos casos complicaría nuestro código, así que en su lugar utilizamosSafeERC20de OpenZeppelin(opens in a new tab), el cual aseguraque todos los fallos 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 envoltorio 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 éste es la forma en que se define una matriz bidimensional dispersa(opens in a new tab). Los valores en esta estructura de datos se identifican como deposit[L1 token addr][L2 token addr]. El valor por defecto es cero. Sólo las celdas que están configuradas en 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 actualice se le dice al proxy que cambie esa dirección). Cuando usas delegatecall el almacenamiento sigue siendo el almacenamiento del contrato de llamada, para que los valores de todas las variables del estado del contrato no se vean afectados.

Un efecto de este patrón es que el almacenamiento del contrato que es el called 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 a continuación 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 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 external en lugar de public. El coste de gas de external puede ser menor, porque pueden ser proporcionadas con parámetros en los datos de llamada. Las funciones declaradas 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 tal función se llama externamente, es necesario copiar los datos de llamada a la memoria, lo que cuesta gas. En este caso la función sólo 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), "Contract has already been initialized.");
Copiar

La función initialize debe ser llamada una única vez. Si la dirección del mensajero de dominio cruzado L1 o el token de puente 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 restringe quién puede llamarlo. Esto significa que en teoría un atacante podría esperar hasta que despliegue el proxy y la primera versión del puente y luego 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 se despliegan no directamente por un EOA sino en una transacción que tiene otro contrato que los crea(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 recién creado y el puente, y crear uno nuevo.
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 envolturas alrededor de _initiateETHDeposit, la función que gestiona el depósito ETH actual.

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 que sus datos de llamada están de acuerdo con las especificaciones 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 sólo 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
amountmsg.valueCantidad de wei enviados (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 envolturas alrededor de _initiateERC20Deposit, la función que gestiona el depósito ERC-20 actual.

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, ejecutar 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ñade la cantidad de tokens depositados a la estructura de datos de depósitos. Puede haber varias direcciones en L2 que correspondan con el mismo token ERC-20 L1, por lo que no es suficiente con 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 L2 envía un mensaje al mensajero de dominio cruzado L2 que causa 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ía en L1, por supuesto).

1 ) external onlyFromCrossDomainAccount(l2TokenBridge) {
Copiar

Asegúrate de que este es un mensaje legítimo, proveniente del mensajero de dominio cruzado y que se origina con el token de puente L2. Esta función se utiliza para retirar ETH del puente, así que tenemos que asegurarnos de que sólo es llamada 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 recipiente 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

Emitir 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 de ERC-20.

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

Actualizar la estructura de datos de depósitos.

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

Hubo una implementación anterior del puente. Cuando pasamos de la implementación a ésta, tuvimos que mover todos los activos. Los tokens ERC-20 pueden moverse sin más. Sin embargo, para transferir ETH a un contrato necesitas 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 el puente estándar, y solo el puente estándar, para acuñar el token. 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 de 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 que funciones proporciona un contrato. Puedes 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 que está 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 acuñar (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 es 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 ERC-165(opens in a new tab) funciona. 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 comprobación de la cordura 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 deshonesto proporcione respuestas falsas a supportsInterface, por lo que se trata de un mecanismo de comprobación de salubridad, no de 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

Sólo el puente L2 puede acuñar y quemar activos.

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

Código Puente L2

Este es el código que ejecuta el puente sobre 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 L1 equivalente 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 ETH en Optimism son manejados como un token ERC-20 con la dirección0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD000(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 L1, aquí necesitamos esta variable. La dirección del puente L1 no es conocida 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 digan 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

Ten en cuenta que no dependemos del parámetro _from sino de 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

Comprobaciones de sanidad:

  1. La interfaz correcta está soportada
  2. La dirección del contrato ERC-20 L2 en L1 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 comprobaciones de sanidad se superan, finaliza el depósito:

  1. Acuña los tokens
  2. Emite 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 vía de hacerlo desde L2 es enviar un mensaje que tenga que esperar el período del desafío de 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 tan genérico no siempre es el mecanismo más fácil de utilizar. Especialmente para 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 requieren 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 corto en activos L1 transfiere suficientes activos de L2. Como se trata de retiros muy grandes, el coste de la retirada se amortiza sobre una gran cantidad y es 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 el código de Solidity de manera clara y segura.

Última edición: @CryptoSpace(opens in a new tab), 21 de febrero de 2024

¿Le ha resultado útil este tutorial?