Recorrido por el contrato del puente estándar de Optimism
Optimismopens in a new tab es un rollup optimista. Los rollups optimistas pueden procesar transacciones por un precio mucho más bajo que la red principal de Ethereum (también conocida como capa 1 o L1) porque las transacciones solo son procesadas por unos pocos nodos, en lugar de cada nodo de la red. Al mismo tiempo, todos los datos se escriben en la L1 para que todo pueda probarse y reconstruirse con todas las garantías de integridad y disponibilidad de la red principal.
Para usar activos de L1 en Optimism (o cualquier otra L2), los activos deben ser transferidos mediante un puente. Una forma de lograr esto es que los usuarios bloqueen activos (ETH y los tokens ERC-20 son los más comunes) en L1, y reciban activos equivalentes para usar en L2. Eventualmente, quien termine con ellos podría querer puentearlos de vuelta a L1. Al hacer esto, los activos se queman en L2 y luego se liberan de nuevo al usuario en L1.
Así es como funciona el puente estándar de Optimismopens in a new tab. En este artículo, repasamos el código fuente de ese puente para ver cómo funciona y estudiarlo como ejemplo de código de Solidity bien escrito.
Flujos de control
El puente tiene dos flujos principales:
- Depósito (de L1 a L2)
- Retirada (de L2 a L1)
Flujo de depósito
Capa 1
- Si se deposita un ERC-20, el depositante le da al puente un permiso para gastar la cantidad que se está depositando
- El depositante llama al puente de L1 (
depositERC20,depositERC20To,depositETH, odepositETHTo) - El puente L1 toma posesión del activo puenteado
- ETH: El activo es transferido por el depositante como parte de la llamada
- ERC-20: El activo es transferido por el puente a sí mismo utilizando el permiso proporcionado por el depositante
- El puente L1 utiliza el mecanismo de mensajes entre dominios para llamar a
finalizeDepositen el puente L2
Capa 2
- El puente de L2 verifica que la llamada a
finalizeDepositsea legítima:- Procede del contrato de mensajes entre dominios
- Originalmente era del puente en L1
- El puente de L2 comprueba si el contrato de token ERC-20 en L2 es el correcto:
- El contrato de L2 informa que su contraparte de L1 es la misma de la que provienen los tokens en L1
- El contrato de L2 informa que admite la interfaz correcta (usando ERC-165opens in a new tab).
- Si el contrato L2 es el correcto, llámelo para mintear el número apropiado de tokens a la dirección apropiada. Si no, inicie un proceso de retirada para permitir al usuario reclamar los tokens en L1.
Flujo de retirada
Capa 2
- Quien realiza la retirada llama al puente de L2 (
withdrawowithdrawTo) - El puente L2 quema el número apropiado de tokens pertenecientes a
msg.sender - El puente de L2 utiliza el mecanismo de mensajes entre dominios para llamar a
finalizeETHWithdrawalofinalizeERC20Withdrawalen el puente de L1
Capa 1
- El puente de L1 verifica que la llamada a
finalizeETHWithdrawalofinalizeERC20Withdrawalsea legítima:- Procede del mecanismo de mensajes entre dominios
- Originalmente era del puente en L2
- El puente de L1 transfiere el activo apropiado (ETH o ERC-20) a la dirección apropiada
Código de la capa 1
Este es el código que se ejecuta en la L1, la red principal de Ethereum.
IL1ERC20Bridge
Esta interfaz se define aquíopens in a new tab. Incluye funciones y definiciones requeridas para puentear tokens ERC-20.
1// SPDX-License-Identifier: MITLa mayor parte del código de Optimism se publica bajo la licencia MITopens in a new tab.
1pragma solidity >0.5.0 <0.9.0;En el momento de escribir este artículo, la última versión de Solidity es la 0.8.12. Hasta que se lance la versión 0.9.0, no sabemos si este código es compatible con ella o no.
1/**2 * @title IL1ERC20Bridge3 */4interface IL1ERC20Bridge {5 /**********6 * Eventos *7 **********/89 event ERC20DepositInitiated(Mostrar todoEn la terminología del puente de Optimism, deposit significa una transferencia de L1 a L2, y withdrawal significa una transferencia de L2 a L1.
1 address indexed _l1Token,2 address indexed _l2Token,En la mayoría de los casos, la dirección de un ERC-20 en L1 no es la misma que la dirección del ERC-20 equivalente en L2.
Puede ver la lista de direcciones de tokens aquíopens in a new tab.
La dirección con chainId 1 está en L1 (red principal) y la dirección con chainId 10 está en L2 (Optimism).
Los otros dos valores de chainId son para la red de prueba de Kovan (42) y la red de prueba de Optimistic Kovan (69).
1 address indexed _from,2 address _to,3 uint256 _amount,4 bytes _data5 );Es posible añadir notas a las transferencias, en cuyo caso se añaden a los eventos que las notifican.
1 event ERC20WithdrawalFinalized(2 address indexed _l1Token,3 address indexed _l2Token,4 address indexed _from,5 address _to,6 uint256 _amount,7 bytes _data8 );El mismo contrato de puente gestiona las transferencias en ambas direcciones. En el caso del puente de L1, esto significa la inicialización de los depósitos y la finalización de las retiradas.
1 /********************2 * Funciones públicas *3 ********************/45 /**6 * @dev obtiene la dirección del contrato de puente de L2 correspondiente.7 * @return Dirección del contrato de puente de L2 correspondiente.8 */9 function l2TokenBridge() external returns (address);Mostrar todoEsta función no es realmente necesaria, porque en L2 es un contrato preimplementado, por lo que siempre está en la dirección 0x4200000000000000000000000000000000000010.
Está aquí por simetría con el puente de L2, porque la dirección del puente de L1 no es trivial de conocer.
1 /**2 * @dev deposita una cantidad de ERC20 en el saldo del llamador en L2.3 * @param _l1Token Dirección del ERC20 de L1 que estamos depositando4 * @param _l2Token Dirección del respectivo ERC20 de L25 * @param _amount Cantidad del ERC20 a depositar6 * @param _l2Gas Límite de gas requerido para completar el depósito en L2.7 * @param _data Datos opcionales para reenviar a L2. Estos datos se proporcionan8 * únicamente para la comodidad de los contratos externos. Aparte de imponer una longitud máxima,9 * estos contratos no ofrecen ninguna garantía sobre su contenido.10 */11 function depositERC20(12 address _l1Token,13 address _l2Token,14 uint256 _amount,15 uint32 _l2Gas,16 bytes calldata _data17 ) external;Mostrar todoEl parámetro _l2Gas es la cantidad de gas de L2 que la transacción puede gastar.
Hasta un cierto límite (alto), esto es gratuitoopens in a new tab, por lo que, a menos que el contrato ERC-20 haga algo realmente extraño al mintear, no debería ser un problema.
Esta función se encarga del escenario común, en el que un usuario transfiere activos mediante un puente a la misma dirección en una cadena de bloques diferente.
1 /**2 * @dev deposita una cantidad de ERC20 en el saldo de un destinatario en L2.3 * @param _l1Token Dirección del ERC20 de L1 que estamos depositando4 * @param _l2Token Dirección del respectivo ERC20 de L25 * @param _to Dirección de L2 en la que acreditar el depósito.6 * @param _amount Cantidad del ERC20 a depositar.7 * @param _l2Gas Límite de gas requerido para completar el depósito en L2.8 * @param _data Datos opcionales para reenviar a L2. Estos datos se proporcionan9 * únicamente para la comodidad de los contratos externos. Aparte de imponer una longitud máxima,10 * estos contratos no ofrecen ninguna garantía sobre su contenido.11 */12 function depositERC20To(13 address _l1Token,14 address _l2Token,15 address _to,16 uint256 _amount,17 uint32 _l2Gas,18 bytes calldata _data19 ) external;Mostrar todoEsta función es casi idéntica a depositERC20, pero le permite enviar el ERC-20 a una dirección diferente.
1 /*************************2 * Funciones entre cadenas *3 *************************/45 /**6 * @dev Completa una retirada de L2 a L1 y acredita los fondos al saldo del7 * token ERC20 de L1 del destinatario.8 * Esta llamada fallará si la retirada iniciada desde L2 no se ha finalizado.9 *10 * @param _l1Token Dirección del token de L1 para finalizeWithdrawal.11 * @param _l2Token Dirección del token de L2 donde se inició la retirada.12 * @param _from Dirección de L2 que inicia la transferencia.13 * @param _to Dirección de L1 en la que acreditar la retirada.14 * @param _amount Cantidad del ERC20 a depositar.15 * @param _data Datos proporcionados por el remitente en L2. Estos datos se proporcionan16 * únicamente para la comodidad de los contratos externos. Aparte de imponer una longitud máxima,17 * estos contratos no ofrecen ninguna garantía sobre su contenido.18 */19 function finalizeERC20Withdrawal(20 address _l1Token,21 address _l2Token,22 address _from,23 address _to,24 uint256 _amount,25 bytes calldata _data26 ) external;27}Mostrar todoLas retiradas (y otros mensajes de L2 a L1) en Optimism son un proceso de dos pasos:
- Una transacción iniciadora en L2.
- Una transacción de finalización o reclamación en L1. Esta transacción debe ocurrir después de que finalice el período de impugnación de erroresopens in a new tab para la transacción de L2.
IL1StandardBridge
Esta interfaz se define aquíopens in a new tab.
Este archivo contiene definiciones de eventos y funciones para ETH.
Estas definiciones son muy similares a las definidas en IL1ERC20Bridge anteriormente para ERC-20.
La interfaz de puente está dividida entre dos archivos porque algunos tokens ERC-20 requieren un procesamiento personalizado y no pueden ser manejados por el puente estándar.
De esta manera, el puente personalizado que gestiona dicho token puede implementar IL1ERC20Bridge y no tener que puentear también ETH.
1// SPDX-License-Identifier: MIT2pragma solidity >0.5.0 <0.9.0;34import "./IL1ERC20Bridge.sol";56/**7 * @title IL1StandardBridge8 */9interface IL1StandardBridge is IL1ERC20Bridge {10 /**********11 * Eventos *12 **********/13 event ETHDepositInitiated(14 address indexed _from,15 address indexed _to,16 uint256 _amount,17 bytes _data18 );Mostrar todoEste evento es casi idéntico a la versión ERC-20 (ERC20DepositInitiated), excepto que no incluye las direcciones de los tokens de L1 y L2.
Lo mismo ocurre con los demás eventos y funciones.
1 event ETHWithdrawalFinalized(2 .3 .4 .5 );67 /********************8 * Funciones públicas *9 ********************/1011 /**12 * @dev Deposita una cantidad de ETH en el saldo del llamador en L2.13 .14 .15 .16 */17 function depositETH(uint32 _l2Gas, bytes calldata _data) external payable;1819 /**20 * @dev Deposita una cantidad de ETH en el saldo de un destinatario en L2.21 .22 .23 .24 */25 function depositETHTo(26 address _to,27 uint32 _l2Gas,28 bytes calldata _data29 ) external payable;3031 /*************************32 * Funciones entre cadenas *33 *************************/3435 /**36 * @dev Completa una retirada de L2 a L1 y acredita los fondos en el saldo del37 * token ETH de L1 del destinatario. Dado que solo el xDomainMessenger puede llamar a esta función, nunca se llamará38 * antes de que se finalice la retirada.39 .40 .41 .42 */43 function finalizeETHWithdrawal(44 address _from,45 address _to,46 uint256 _amount,47 bytes calldata _data48 ) external;49}Mostrar todoCrossDomainEnabled
Este contratoopens in a new tab es heredado por ambos puentes (L1 y L2) para enviar mensajes a la otra capa.
1// SPDX-License-Identifier: MIT2pragma solidity >0.5.0 <0.9.0;34/* Interface Imports */5import { ICrossDomainMessenger } from "./ICrossDomainMessenger.sol";Esta interfazopens in a new tab le indica al contrato cómo enviar mensajes a la otra capa, utilizando el mensajero entre dominios. Este mensajero entre dominios es un sistema completamente diferente y merece su propio artículo, que espero escribir en el futuro.
1/**2 * @title CrossDomainEnabled3 * @dev Contrato auxiliar para contratos que realizan comunicaciones entre dominios4 *5 * Compilador utilizado: definido por el contrato que hereda6 */7contract CrossDomainEnabled {8 /*************9 * Variables *10 *************/1112 // Contrato de mensajero utilizado para enviar y recibir mensajes desde el otro dominio.13 address public messenger;1415 /***************16 * Constructor *17 ***************/1819 /**20 * @param _messenger Dirección del CrossDomainMessenger en la capa actual.21 */22 constructor(address _messenger) {23 messenger = _messenger;24 }Mostrar todoEl único parámetro que el contrato necesita saber es la dirección del mensajero entre dominios en esta capa. Este parámetro se establece una vez, en el constructor, y nunca cambia.
12 /**********************3 * Modificadores de función *4 **********************/56 /**7 * @dev Exige que la función modificada solo pueda ser llamada por una cuenta específica entre dominios.8 * @param _sourceDomainAccount La única cuenta en el dominio de origen que está9 * autenticada para llamar a esta función.10 */11 modifier onlyFromCrossDomainAccount(address _sourceDomainAccount) {Mostrar todoLa mensajería entre dominios es accesible para cualquier contrato en la cadena de bloques donde se esté ejecutando (ya sea la red principal de Ethereum u Optimism). Pero necesitamos que el puente de cada lado confíe únicamente en ciertos mensajes si provienen del puente del otro lado.
1 require(2 msg.sender == address(getCrossDomainMessenger()),3 "OVM_XCHAIN: contrato de mensajero no autenticado"4 );Solo se puede confiar en los mensajes del mensajero entre dominios apropiado (messenger, como verá a continuación).
12 require(3 getCrossDomainMessenger().xDomainMessageSender() == _sourceDomainAccount,4 "OVM_XCHAIN: remitente incorrecto del mensaje entre dominios"5 );La forma en que el mensajero entre dominios proporciona la dirección que envió un mensaje desde la otra capa es la función .xDomainMessageSender()opens in a new tab.
Siempre que se llame en la transacción que fue iniciada por el mensaje, puede proporcionar esta información.
Tenemos que asegurarnos de que el mensaje que recibimos provenga del otro puente.
12 _;3 }45 /**********************6 * Funciones internas *7 **********************/89 /**10 * @dev Obtiene el mensajero, normalmente del almacenamiento. Esta función se expone en caso de que un contrato secundario11 * necesite anularla.12 * @return La dirección del contrato de mensajero entre dominios que se debe utilizar.13 */14 function getCrossDomainMessenger() internal virtual returns (ICrossDomainMessenger) {15 return ICrossDomainMessenger(messenger);16 }Mostrar todoEsta función devuelve el mensajero entre dominios.
Utilizamos una función en lugar de la variable messenger para permitir que los contratos que heredan de este usen un algoritmo para especificar qué mensajero entre dominios usar.
12 /**3 * @dev Envía un mensaje a una cuenta en otro dominio4 * @param _crossDomainTarget El destinatario previsto en el dominio de destino5 * @param _message Los datos para enviar al objetivo (normalmente datos de llamada a una función con6 * `onlyFromCrossDomainAccount()`)7 * @param _gasLimit El límite de gas para la recepción del mensaje en el dominio de destino.8 */9 function sendCrossDomainMessage(10 address _crossDomainTarget,11 uint32 _gasLimit,12 bytes memory _messageMostrar todoFinalmente, la función que envía un mensaje a la otra capa.
1 ) internal {2 // slither-disable-next-line reentrancy-events, reentrancy-benignSlitheropens in a new tab es un analizador estático que Optimism ejecuta en cada contrato para buscar vulnerabilidades y otros problemas potenciales. En este caso, la siguiente línea activa dos vulnerabilidades:
1 getCrossDomainMessenger().sendMessage(_crossDomainTarget, _message, _gasLimit);2 }3}En este caso, no nos preocupa la reentrada, ya que sabemos que getCrossDomainMessenger() devuelve una dirección confiable, incluso si Slither no tiene forma de saberlo.
El contrato del puente de L1
El código fuente de este contrato está aquíopens in a new tab.
1// SPDX-License-Identifier: MIT2pragma solidity ^0.8.9;Las interfaces pueden ser parte de otros contratos, por lo que tienen que admitir una amplia gama de versiones de Solidity. Pero el puente en sí es nuestro contrato, y podemos ser estrictos sobre qué versión de Solidity utiliza.
1/* Interface Imports */2import { IL1StandardBridge } from "./IL1StandardBridge.sol";3import { IL1ERC20Bridge } from "./IL1ERC20Bridge.sol";IL1ERC20Bridge e IL1StandardBridge se explican más arriba.
1import { IL2ERC20Bridge } from "../../L2/messaging/IL2ERC20Bridge.sol";Esta interfazopens in a new tab nos permite crear mensajes para controlar el puente estándar en L2.
1import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";Esta interfazopens in a new tab nos permite controlar los contratos ERC-20. Puede leer más al respecto aquí.
1/* Library Imports */2import { CrossDomainEnabled } from "../../libraries/bridge/CrossDomainEnabled.sol";Como se explicó anteriormente, este contrato se utiliza para la mensajería entre capas.
1import { Lib_PredeployAddresses } from "../../libraries/constants/Lib_PredeployAddresses.sol";Lib_PredeployAddressesopens in a new tab tiene las direcciones de los contratos de L2 que siempre tienen la misma dirección. Esto incluye el puente estándar en L2.
1import { Address } from "@openzeppelin/contracts/utils/Address.sol";Utilidades de dirección de OpenZeppelinopens in a new tab. Se utiliza para distinguir entre las direcciones de contrato y las que pertenecen a cuentas de propiedad externa (EOA).
Tenga en cuenta que esta no es una solución perfecta, porque no hay forma de distinguir entre llamadas directas y llamadas realizadas desde el constructor de un contrato, pero al menos esto nos permite identificar y prevenir algunos errores comunes de los usuarios.
1import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";El estándar ERC-20opens in a new tab admite dos formas para que un contrato informe de un fallo:
- Revertir
- Devolver
false
Manejar ambos casos haría nuestro código más complicado, así que en su lugar usamos SafeERC20 de OpenZeppelinopens in a new tab, que se asegura de que todos los fallos resulten en una reversiónopens in a new tab.
1/**2 * @title L1StandardBridge3 * @dev El puente ERC20 y ETH de L1 es un contrato que almacena los fondos de L1 depositados y los tokens4 * estándar que están en uso en L2. Sincroniza un puente de L2 correspondiente, informándole de los depósitos5 * y escuchándolo para las retiradas recién finalizadas.6 *7 */8contract L1StandardBridge is IL1StandardBridge, CrossDomainEnabled {9 using SafeERC20 for IERC20;Mostrar todoEsta línea es la forma en que especificamos que se use el contenedor SafeERC20 cada vez que usamos la interfaz IERC20.
12 /********************************3 * Referencias a contratos externos *4 ********************************/56 address public l2TokenBridge;La dirección de L2StandardBridge.
12 // Asigna el token de L1 al token de L2 al saldo del token de L1 depositado3 mapping(address => mapping(address => uint256)) public deposits;Un mapeoopens in a new tab doble como este es la forma en que se define una matriz dispersa bidimensionalopens in a new tab.
Los valores en esta estructura de datos se identifican como deposit[dirección de token L1][dirección de token L2].
El valor por defecto es cero.
Solo las celdas que se establecen en un valor diferente se escriben en el almacenamiento.
12 /***************3 * Constructor *4 ***************/56 // Este contrato vive detrás de un proxy, por lo que los parámetros del constructor no se utilizarán.7 constructor() CrossDomainEnabled(address(0)) {}Queremos poder actualizar este contrato sin tener que copiar todas las variables en el almacenamiento.
Para ello, utilizamos un Proxyopens in a new tab, un contrato que utiliza delegatecallopens in a new tab para transferir llamadas a un contrato separado cuya dirección es almacenada por el contrato proxy (cuando se actualiza, se le dice al proxy que cambie esa dirección).
Cuando se utiliza delegatecall, el almacenamiento sigue siendo el almacenamiento del contrato que llama, por lo que los valores de todas las variables de estado del contrato no se ven afectados.
Un efecto de este patrón es que el almacenamiento del contrato que es el llamado de delegatecall no se utiliza y, por lo tanto, los valores del constructor que se le pasan no importan.
Esta es la razón por la que podemos proporcionar un valor sin sentido al constructor CrossDomainEnabled.
También es la razón por la que la inicialización a continuación está separada del constructor.
1 /******************2 * Inicialización *3 ******************/45 /**6 * @param _l1messenger Dirección del mensajero de L1 que se utiliza para las comunicaciones entre cadenas.7 * @param _l2TokenBridge Dirección del puente estándar de L2.8 */9 // slither-disable-next-line external-functionMostrar todoEsta prueba de Slitheropens in a new tab identifica las funciones que no se llaman desde el código del contrato y que, por lo tanto, podrían declararse como external en lugar de public.
El coste de gas de las funciones external puede ser menor, porque se les pueden proporcionar parámetros en los datos de llamada.
Las funciones declaradas como public deben ser accesibles desde dentro del contrato.
Los contratos no pueden modificar sus propios datos de llamada, por lo que los parámetros deben estar en memoria.
Cuando se llama a una función de este tipo externamente, es necesario copiar los datos de llamada a la memoria, lo que cuesta gas.
En este caso, la función solo se llama una vez, por lo que la ineficiencia no nos importa.
1 function initialize(address _l1messenger, address _l2TokenBridge) public {2 require(messenger == address(0), "El contrato ya ha sido inicializado.");La función initialize solo debe llamarse una vez.
Si la dirección del mensajero entre dominios de L1 o del puente de tokens de L2 cambia, creamos un nuevo proxy y un nuevo puente que lo llama.
Es poco probable que esto ocurra, excepto cuando se actualiza todo el sistema, lo que es muy raro.
Tenga en cuenta que esta función no tiene ningún mecanismo que restrinja quién puede llamarla.
Esto significa que, en teoría, un atacante podría esperar hasta que implementemos el proxy y la primera versión del puente y luego hacer front-runningopens in a new tab para llegar a la función initialize antes de que lo haga el usuario legítimo. Pero hay dos métodos para evitarlo:
- Si los contratos no son implementados directamente por una EOA, sino en una transacción que hace que otro contrato los creeopens in a new tab, todo el proceso puede ser atómico y terminar antes de que se ejecute cualquier otra transacción.
- Si la llamada legítima a
initializefalla, siempre es posible ignorar el proxy y el puente recién creados y crear otros nuevos.
1 messenger = _l1messenger;2 l2TokenBridge = _l2TokenBridge;3 }Estos son los dos parámetros que el puente necesita conocer.
12 /**************3 * Depósito *4 **************/56 /** @dev Modificador que requiere que el remitente sea una EOA. Esta comprobación podría ser7 * eludida por un contrato malicioso a través de initcode, pero se encarga del error de usuario que queremos evitar.8 */9 modifier onlyEOA() {10 // Se utiliza para detener los depósitos de los contratos (evitar la pérdida accidental de tokens)11 require(!Address.isContract(msg.sender), "Cuenta no es EOA");12 _;13 }Mostrar todoEsta es la razón por la que necesitábamos las utilidades de Address de OpenZeppelin.
1 /**2 * @dev Se puede llamar a esta función sin datos3 * para depositar una cantidad de ETH en el saldo del llamador en L2.4 * Como la función de recepción no toma datos, se reenvía una5 * cantidad conservadora por defecto a L2.6 */7 receive() external payable onlyEOA {8 _initiateETHDeposit(msg.sender, msg.sender, 200_000, bytes(""));9 }Mostrar todoEsta función existe para fines de prueba. Tenga en cuenta que no aparece en las definiciones de la interfaz; no es para uso normal.
1 /**2 * @inheritdoc IL1StandardBridge3 */4 function depositETH(uint32 _l2Gas, bytes calldata _data) external payable onlyEOA {5 _initiateETHDeposit(msg.sender, msg.sender, _l2Gas, _data);6 }78 /**9 * @inheritdoc IL1StandardBridge10 */11 function depositETHTo(12 address _to,13 uint32 _l2Gas,14 bytes calldata _data15 ) external payable {16 _initiateETHDeposit(msg.sender, _to, _l2Gas, _data);17 }Mostrar todoEstas dos funciones son envoltorios de _initiateETHDeposit, la función que gestiona el depósito real de ETH.
1 /**2 * @dev Realiza la lógica de los depósitos almacenando el ETH e informando a la puerta de enlace de ETH de L23 * del depósito.4 * @param _from Cuenta de la que se retira el depósito en L1.5 * @param _to Cuenta a la que se entrega el depósito en L2.6 * @param _l2Gas Límite de gas requerido para completar el depósito en L2.7 * @param _data Datos opcionales para reenviar a L2. Estos datos se proporcionan8 * únicamente para la comodidad de los contratos externos. Aparte de imponer una longitud máxima,9 * estos contratos no ofrecen ninguna garantía sobre su contenido.10 */11 function _initiateETHDeposit(12 address _from,13 address _to,14 uint32 _l2Gas,15 bytes memory _data16 ) internal {17 // Construir datos de llamada para la llamada a finalizeDeposit18 bytes memory message = abi.encodeWithSelector(Mostrar todoLa forma en que funcionan los mensajes entre dominios es que se llama al contrato de destino con el mensaje como sus datos de llamada.
Los contratos de Solidity siempre interpretan sus datos de llamada de acuerdo con
las especificaciones de la ABIopens in a new tab.
La función de Solidity abi.encodeWithSelectoropens in a new tab crea esos datos de llamada.
1 IL2ERC20Bridge.finalizeDeposit.selector,2 address(0),3 Lib_PredeployAddresses.OVM_ETH,4 _from,5 _to,6 msg.value,7 _data8 );El mensaje aquí es para llamar a la función finalizeDepositopens in a new tab con estos parámetros:
| Parámetro | Valor | Significado |
|---|---|---|
| _l1Token | address(0) | Valor especial para representar ETH (que no es un token ERC-20) en L1 |
| _l2Token | Lib_PredeployAddresses.OVM_ETH | El contrato de L2 que gestiona ETH en Optimism, 0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000 (este contrato es solo para uso interno de Optimism) |
| _from | _from | La dirección en L1 que envía el ETH |
| _to | _to | La dirección en L2 que recibe el ETH |
| cantidad | msg.value | Cantidad de wei enviados (que ya han sido enviados al puente) |
| _data | _data | Datos adicionales para adjuntar al depósito |
1 // Enviar datos de llamada a L22 // slither-disable-next-line reentrancy-events3 sendCrossDomainMessage(l2TokenBridge, _l2Gas, message);Enviar el mensaje a través del mensajero entre dominios.
1 // slither-disable-next-line reentrancy-events2 emit ETHDepositInitiated(_from, _to, msg.value, _data);3 }Emitir un evento para informar de esta transferencia a cualquier aplicación descentralizada que escuche.
1 /**2 * @inheritdoc IL1ERC20Bridge3 */4 function depositERC20(5 .6 .7 .8 ) external virtual onlyEOA {9 _initiateERC20Deposit(_l1Token, _l2Token, msg.sender, msg.sender, _amount, _l2Gas, _data);10 }1112 /**13 * @inheritdoc IL1ERC20Bridge14 */15 function depositERC20To(16 .17 .18 .19 ) external virtual {20 _initiateERC20Deposit(_l1Token, _l2Token, msg.sender, _to, _amount, _l2Gas, _data);21 }Mostrar todoEstas dos funciones son envoltorios de _initiateERC20Deposit, la función que gestiona el depósito real de ERC-20.
1 /**2 * @dev Realiza la lógica de los depósitos informando al contrato de token3 * depositado en L2 del depósito y llamando a un manejador para bloquear los fondos de L1. (p. ej., transferFrom)4 *5 * @param _l1Token Dirección del ERC20 de L1 que estamos depositando6 * @param _l2Token Dirección del respectivo ERC20 de L27 * @param _from Cuenta de la que se retira el depósito en L18 * @param _to Cuenta a la que se entrega el depósito en L29 * @param _amount Cantidad del ERC20 a depositar.10 * @param _l2Gas Límite de gas requerido para completar el depósito en L2.11 * @param _data Datos opcionales para reenviar a L2. Estos datos se proporcionan12 * únicamente para la comodidad de los contratos externos. Aparte de imponer una longitud máxima,13 * estos contratos no ofrecen ninguna garantía sobre su contenido.14 */15 function _initiateERC20Deposit(16 address _l1Token,17 address _l2Token,18 address _from,19 address _to,20 uint256 _amount,21 uint32 _l2Gas,22 bytes calldata _data23 ) internal {Mostrar todoEsta función es similar a _initiateETHDeposit anterior, con algunas diferencias importantes.
La primera diferencia es que esta función recibe las direcciones de los tokens y la cantidad a transferir como parámetros.
En el caso de ETH, la llamada al puente ya incluye la transferencia del activo a la cuenta del puente (msg.value).
1 // Cuando se inicia un depósito en L1, el puente de L1 transfiere los fondos a sí mismo para futuras2 // retiradas. safeTransferFrom también comprueba si el contrato tiene código, por lo que esto fallará si3 // _from es una EOA o address(0).4 // slither-disable-next-line reentrancy-events, reentrancy-benign5 IERC20(_l1Token).safeTransferFrom(_from, address(this), _amount);Las transferencias de tokens ERC-20 siguen un proceso diferente al de ETH:
- El usuario (
_from) otorga un permiso al puente para transferir los tokens apropiados. - El usuario llama al puente con la dirección del contrato del token, la cantidad, etc.
- El puente transfiere los tokens (a sí mismo) como parte del proceso de depósito.
El primer paso puede ocurrir en una transacción separada de los dos últimos.
Sin embargo, el front-running no es un problema porque las dos funciones que llaman a _initiateERC20Deposit (depositERC20 y depositERC20To) solo llaman a esta función con msg.sender como el parámetro _from.
1 // Construir datos de llamada para _l2Token.finalizeDeposit(_to, _amount)2 bytes memory message = abi.encodeWithSelector(3 IL2ERC20Bridge.finalizeDeposit.selector,4 _l1Token,5 _l2Token,6 _from,7 _to,8 _amount,9 _data10 );1112 // Enviar datos de llamada a L213 // slither-disable-next-line reentrancy-events, reentrancy-benign14 sendCrossDomainMessage(l2TokenBridge, _l2Gas, message);1516 // slither-disable-next-line reentrancy-benign17 deposits[_l1Token][_l2Token] = deposits[_l1Token][_l2Token] + _amount;Mostrar todoAñada la cantidad de tokens depositados a la estructura de datos deposits.
Podría haber varias direcciones en L2 que correspondan al mismo token ERC-20 de L1, por lo que no es suficiente usar el saldo del puente del token ERC-20 de L1 para hacer un seguimiento de los depósitos.
12 // slither-disable-next-line reentrancy-events3 emit ERC20DepositInitiated(_l1Token, _l2Token, _from, _to, _amount, _data);4 }56 /*************************7 * Funciones entre cadenas *8 *************************/910 /**11 * @inheritdoc IL1StandardBridge12 */13 function finalizeETHWithdrawal(14 address _from,15 address _to,16 uint256 _amount,17 bytes calldata _dataMostrar todoEl puente de L2 envía un mensaje al mensajero entre dominios de L2, lo que hace que el mensajero entre dominios de L1 llame a esta función (una vez que se envíe en L1 la transacción que finaliza el mensajeopens in a new tab, por supuesto).
1 ) external onlyFromCrossDomainAccount(l2TokenBridge) {Asegúrese de que este sea un mensaje legítimo, proveniente del mensajero entre dominios y originado en el puente de tokens de L2. Esta función se utiliza para retirar ETH del puente, por lo que tenemos que asegurarnos de que solo sea llamada por el llamador autorizado.
1 // slither-disable-next-line reentrancy-events2 (bool success, ) = _to.call{ value: _amount }(new bytes(0));La forma de transferir ETH es llamar al destinatario con la cantidad de wei en el msg.value.
1 require(success, "TransferHelper::safeTransferETH: Falló la transferencia de ETH");23 // slither-disable-next-line reentrancy-events4 emit ETHWithdrawalFinalized(_from, _to, _amount, _data);Emitir un evento sobre la retirada.
1 }23 /**4 * @inheritdoc IL1ERC20Bridge5 */6 function finalizeERC20Withdrawal(7 address _l1Token,8 address _l2Token,9 address _from,10 address _to,11 uint256 _amount,12 bytes calldata _data13 ) external onlyFromCrossDomainAccount(l2TokenBridge) {Mostrar todoEsta función es similar a finalizeETHWithdrawal anterior, con los cambios necesarios para los tokens ERC-20.
1 deposits[_l1Token][_l2Token] = deposits[_l1Token][_l2Token] - _amount;Actualizar la estructura de datos deposits.
12 // Cuando se finaliza una retirada en L1, el puente de L1 transfiere los fondos al retirante3 // slither-disable-next-line reentrancy-events4 IERC20(_l1Token).safeTransfer(_to, _amount);56 // slither-disable-next-line reentrancy-events7 emit ERC20WithdrawalFinalized(_l1Token, _l2Token, _from, _to, _amount, _data);8 }91011 /*****************************12 * Temporal - Migración de ETH *13 *****************************/1415 /**16 * @dev Añade saldo de ETH a la cuenta. Esto está destinado a permitir que el ETH17 * se migre de una puerta de enlace antigua a una nueva.18 * NOTA: Esto se deja solo para una actualización para que podamos recibir el ETH migrado desde el19 * contrato antiguo20 */21 function donateETH() external payable {}22}Mostrar todoHabía una implementación anterior del puente.
Cuando pasamos de esa implementación a esta, tuvimos que mover todos los activos.
Los tokens ERC-20 simplemente se pueden mover.
Sin embargo, para transferir ETH a un contrato se necesita la aprobación de ese contrato, que es lo que nos proporciona donateETH.
Tokens ERC-20 en L2
Para que un token ERC-20 encaje en el puente estándar, necesita permitir que el puente estándar, y solo el puente estándar, mintee tokens. Esto es necesario porque los puentes deben garantizar que el número de tokens que circulan en Optimism sea igual al número de tokens bloqueados dentro del contrato de puente de L1. Si hay demasiados tokens en L2, algunos usuarios no podrían puentear sus activos de vuelta a L1. En lugar de un puente de confianza, estaríamos esencialmente recreando la banca de reserva fraccionariaopens in a new tab. Si hay demasiados tokens en L1, algunos de esos tokens permanecerían bloqueados dentro del contrato de puente para siempre, porque no hay forma de liberarlos sin quemar tokens de L2.
IL2StandardERC20
Cada token ERC-20 en L2 que utiliza el puente estándar necesita proporcionar esta interfazopens in a new tab, que tiene las funciones y eventos que el puente estándar necesita.
1// SPDX-License-Identifier: MIT2pragma solidity ^0.8.9;34import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";La interfaz estándar ERC-20opens in a new tab no incluye las funciones mint y burn.
Esos métodos no son requeridos por el estándar ERC-20opens in a new tab, que deja sin especificar los mecanismos para crear y destruir tokens.
1import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol";La interfaz ERC-165opens in a new tab se utiliza para especificar qué funciones proporciona un contrato. Puede leer el estándar aquíopens in a new tab.
1interface IL2StandardERC20 is IERC20, IERC165 {2 function l1Token() external returns (address);Esta función proporciona la dirección del token de L1 que se transfiere a este contrato mediante un puente. Tenga en cuenta que no tenemos una función similar en la dirección opuesta. Necesitamos poder puentear cualquier token de L1, independientemente de si el soporte de L2 fue planeado cuando se implementó o no.
12 function mint(address _to, uint256 _amount) external;34 function burn(address _from, uint256 _amount) external;56 event Mint(address indexed _account, uint256 _amount);7 event Burn(address indexed _account, uint256 _amount);8}Funciones y eventos para mintear (crear) y quemar (destruir) tokens. El puente debe ser la única entidad que pueda ejecutar estas funciones para garantizar que el número de tokens sea correcto (igual al número de tokens bloqueados en L1).
L2StandardERC20
Esta es nuestra implementación de la interfaz IL2StandardERC20opens in a new tab.
A menos que necesite algún tipo de lógica personalizada, debería usar esta.
1// SPDX-License-Identifier: MIT2pragma solidity ^0.8.9;34import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";El contrato ERC-20 de OpenZeppelinopens in a new tab. Optimism no cree en reinventar la rueda, especialmente cuando la rueda está bien auditada y necesita ser lo suficientemente confiable como para albergar activos.
1import "./IL2StandardERC20.sol";23contract L2StandardERC20 is IL2StandardERC20, ERC20 {4 address public l1Token;5 address public l2Bridge;Estos son los dos parámetros de configuración adicionales que requerimos y que ERC-20 normalmente no tiene.
12 /**3 * @param _l2Bridge Dirección del puente estándar de L2.4 * @param _l1Token Dirección del token de L1 correspondiente.5 * @param _name Nombre del ERC20.6 * @param _symbol Símbolo del ERC20.7 */8 constructor(9 address _l2Bridge,10 address _l1Token,11 string memory _name,12 string memory _symbol13 ) ERC20(_name, _symbol) {14 l1Token = _l1Token;15 l2Bridge = _l2Bridge;16 }Mostrar todoPrimero, llame al constructor del contrato del que heredamos (ERC20(_name, _symbol)) y luego establezca nuestras propias variables.
12 modifier onlyL2Bridge() {3 require(msg.sender == l2Bridge, "Solo el puente L2 puede mintear y quemar");4 _;5 }678 // slither-disable-next-line external-function9 function supportsInterface(bytes4 _interfaceId) public pure returns (bool) {10 bytes4 firstSupportedInterface = bytes4(keccak256("supportsInterface(bytes4)")); // ERC16511 bytes4 secondSupportedInterface = IL2StandardERC20.l1Token.selector ^12 IL2StandardERC20.mint.selector ^13 IL2StandardERC20.burn.selector;14 return _interfaceId == firstSupportedInterface || _interfaceId == secondSupportedInterface;15 }Mostrar todoAsí es como funciona ERC-165opens in a new tab. Cada interfaz es un número de funciones admitidas, y se identifica como el o exclusivoopens in a new tab de los selectores de función de ABIopens in a new tab de esas funciones.
El puente de L2 utiliza ERC-165 como una comprobación de sanidad para asegurarse de que el contrato ERC-20 al que envía activos es un IL2StandardERC20.
Nota: No hay nada que impida que un contrato malicioso proporcione respuestas falsas a supportsInterface, por lo que este es un mecanismo de comprobación de sanidad, no un mecanismo de seguridad.
1 // slither-disable-next-line external-function2 function mint(address _to, uint256 _amount) public virtual onlyL2Bridge {3 _mint(_to, _amount);45 emit Mint(_to, _amount);6 }78 // slither-disable-next-line external-function9 function burn(address _from, uint256 _amount) public virtual onlyL2Bridge {10 _burn(_from, _amount);1112 emit Burn(_from, _amount);13 }14}Mostrar todoSolo el puente de L2 puede mintear y quemar activos.
_mint y _burn se definen en realidad en el contrato ERC-20 de OpenZeppelin.
Ese contrato simplemente no los expone externamente, porque las condiciones para mintear y quemar tokens son tan variadas como el número de formas de usar ERC-20.
Código del puente de L2
Este es el código que ejecuta el puente en Optimism. El código fuente de este contrato está aquíopens in a new tab.
1// SPDX-License-Identifier: MIT2pragma solidity ^0.8.9;34/* Interface Imports */5import { IL1StandardBridge } from "../../L1/messaging/IL1StandardBridge.sol";6import { IL1ERC20Bridge } from "../../L1/messaging/IL1ERC20Bridge.sol";7import { IL2ERC20Bridge } from "./IL2ERC20Bridge.sol";La interfaz IL2ERC20Bridgeopens in a new tab es muy similar al equivalente de L1 que vimos anteriormente. Existen dos diferencias significativas:
- En L1 se inician los depósitos y se finalizan las retiradas. Aquí se inician las retiradas y se finalizan los depósitos.
- En L1 es necesario distinguir entre ETH y tokens ERC-20. En L2 podemos usar las mismas funciones para ambos porque, internamente, los saldos de ETH en Optimism se gestionan como un token ERC-20 con la dirección 0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000opens in a new tab.
1/* Library Imports */2import { ERC165Checker } from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol";3import { CrossDomainEnabled } from "../../libraries/bridge/CrossDomainEnabled.sol";4import { Lib_PredeployAddresses } from "../../libraries/constants/Lib_PredeployAddresses.sol";56/* Contract Imports */7import { IL2StandardERC20 } from "../../standards/IL2StandardERC20.sol";89/**10 * @title L2StandardBridge11 * @dev El puente estándar de L2 es un contrato que funciona junto con el puente estándar de L1 para12 * permitir las transiciones de ETH y ERC20 entre L1 y L2.13 * Este contrato actúa como un minteador de nuevos tokens cuando recibe información sobre depósitos en el puente14 * estándar de L1.15 * Este contrato también actúa como un quemador de los tokens destinados a la retirada, informando al puente de L116 * para que libere los fondos de L1.17 */18contract L2StandardBridge is IL2ERC20Bridge, CrossDomainEnabled {19 /********************************20 * Referencias a contratos externos *21 ********************************/2223 address public l1TokenBridge;Mostrar todoMantener un registro de la dirección del puente de L1. Tenga en cuenta que, a diferencia del equivalente de L1, aquí necesitamos esta variable. La dirección del puente de L1 no se conoce de antemano.
12 /***************3 * Constructor *4 ***************/56 /**7 * @param _l2CrossDomainMessenger Mensajero entre dominios utilizado por este contrato.8 * @param _l1TokenBridge Dirección del puente de L1 implementado en la cadena principal.9 */10 constructor(address _l2CrossDomainMessenger, address _l1TokenBridge)11 CrossDomainEnabled(_l2CrossDomainMessenger)12 {13 l1TokenBridge = _l1TokenBridge;14 }1516 /***************17 * Retirada *18 ***************/1920 /**21 * @inheritdoc IL2ERC20Bridge22 */23 function withdraw(24 address _l2Token,25 uint256 _amount,26 uint32 _l1Gas,27 bytes calldata _data28 ) external virtual {29 _initiateWithdrawal(_l2Token, msg.sender, msg.sender, _amount, _l1Gas, _data);30 }3132 /**33 * @inheritdoc IL2ERC20Bridge34 */35 function withdrawTo(36 address _l2Token,37 address _to,38 uint256 _amount,39 uint32 _l1Gas,40 bytes calldata _data41 ) external virtual {42 _initiateWithdrawal(_l2Token, msg.sender, _to, _amount, _l1Gas, _data);43 }Mostrar todoEstas dos funciones inician las retiradas. Tenga en cuenta que no es necesario especificar la dirección del token de L1. Se espera que los tokens de L2 nos indiquen la dirección equivalente en L1.
12 /**3 * @dev Realiza la lógica de las retiradas quemando el token e informando4 * a la puerta de enlace de tokens de L1 de la retirada.5 * @param _l2Token Dirección del token de L2 donde se inicia la retirada.6 * @param _from Cuenta de la que se retira el token en L2.7 * @param _to Cuenta a la que se entrega la retirada en L1.8 * @param _amount Cantidad del token a retirar.9 * @param _l1Gas No se utiliza, pero se incluye para posibles consideraciones de compatibilidad futura.10 * @param _data Datos opcionales para reenviar a L1. Estos datos se proporcionan11 * únicamente para la comodidad de los contratos externos. Aparte de imponer una longitud máxima,12 * estos contratos no ofrecen ninguna garantía sobre su contenido.13 */14 function _initiateWithdrawal(15 address _l2Token,16 address _from,17 address _to,18 uint256 _amount,19 uint32 _l1Gas,20 bytes calldata _data21 ) internal {22 // Cuando se inicia una retirada, quemamos los fondos del retirante para evitar un uso posterior en L223 // slither-disable-next-line reentrancy-events24 IL2StandardERC20(_l2Token).burn(msg.sender, _amount);Mostrar todoTenga en cuenta que no estamos confiando en el parámetro _from, sino en msg.sender, que es mucho más difícil de falsificar (imposible, que yo sepa).
12 // Construir datos de llamada para l1TokenBridge.finalizeERC20Withdrawal(_to, _amount)3 // slither-disable-next-line reentrancy-events4 address l1Token = IL2StandardERC20(_l2Token).l1Token();5 bytes memory message;67 if (_l2Token == Lib_PredeployAddresses.OVM_ETH) {En L1 es necesario distinguir entre ETH y ERC-20.
1 message = abi.encodeWithSelector(2 IL1StandardBridge.finalizeETHWithdrawal.selector,3 _from,4 _to,5 _amount,6 _data7 );8 } else {9 message = abi.encodeWithSelector(10 IL1ERC20Bridge.finalizeERC20Withdrawal.selector,11 l1Token,12 _l2Token,13 _from,14 _to,15 _amount,16 _data17 );18 }1920 // Enviar mensaje al puente de L121 // slither-disable-next-line reentrancy-events22 sendCrossDomainMessage(l1TokenBridge, _l1Gas, message);2324 // slither-disable-next-line reentrancy-events25 emit WithdrawalInitiated(l1Token, _l2Token, msg.sender, _to, _amount, _data);26 }2728 /************************************29 * Función entre cadenas: Depósito *30 ************************************/3132 /**33 * @inheritdoc IL2ERC20Bridge34 */35 function finalizeDeposit(36 address _l1Token,37 address _l2Token,38 address _from,39 address _to,40 uint256 _amount,41 bytes calldata _dataMostrar todoEsta función es llamada por L1StandardBridge.
1 ) external virtual onlyFromCrossDomainAccount(l1TokenBridge) {Asegúrese de que el origen del mensaje sea legítimo.
Esto es importante porque esta función llama a _mint y podría usarse para dar tokens que no están cubiertos por los tokens que el puente posee en L1.
1 // Comprobar que el token de destino es compatible y2 // verificar que el token depositado en L1 coincide con la representación del token depositado en L2 aquí3 if (4 // slither-disable-next-line reentrancy-events5 ERC165Checker.supportsInterface(_l2Token, 0x1d1d8b63) &&6 _l1Token == IL2StandardERC20(_l2Token).l1Token()Comprobaciones de sanidad:
- La interfaz correcta es compatible
- La dirección de L1 del contrato ERC-20 de L2 coincide con el origen de los tokens en L1
1 ) {2 // Cuando se finaliza un depósito, acreditamos la cuenta en L2 con la misma cantidad de3 // tokens.4 // slither-disable-next-line reentrancy-events5 IL2StandardERC20(_l2Token).mint(_to, _amount);6 // slither-disable-next-line reentrancy-events7 emit DepositFinalized(_l1Token, _l2Token, _from, _to, _amount, _data);Si las comprobaciones de sanidad pasan, finalice el depósito:
- Mintear los tokens
- Emitir el evento apropiado
1 } else {2 // O bien el token de L2 en el que se está depositando no está de acuerdo con la dirección correcta3 // de su token de L1, o no admite la interfaz correcta.4 // Esto solo debería ocurrir si hay un token de L2 malicioso, o si un usuario de alguna manera5 // especificó la dirección de token de L2 incorrecta para depositar.6 // En cualquier caso, detenemos el proceso aquí y construimos un mensaje de retirada7 // para que los usuarios puedan recuperar sus fondos en algunos casos.8 // No hay forma de evitar por completo los contratos de tokens maliciosos, pero esto limita9 // los errores del usuario y mitiga algunas formas de comportamiento malicioso de los contratos.Mostrar todoSi un usuario cometió un error detectable al usar la dirección de token de L2 incorrecta, queremos cancelar el depósito y devolver los tokens en L1. La única forma en que podemos hacer esto desde L2 es enviar un mensaje que tendrá que esperar el período de impugnación de errores, pero eso es mucho mejor para el usuario que perder los tokens permanentemente.
1 bytes memory message = abi.encodeWithSelector(2 IL1ERC20Bridge.finalizeERC20Withdrawal.selector,3 _l1Token,4 _l2Token,5 _to, // se cambiaron _to y _from aquí para devolver el depósito al remitente6 _from,7 _amount,8 _data9 );1011 // Enviar mensaje al puente de L112 // slither-disable-next-line reentrancy-events13 sendCrossDomainMessage(l1TokenBridge, 0, message);14 // slither-disable-next-line reentrancy-events15 emit DepositFailed(_l1Token, _l2Token, _from, _to, _amount, _data);16 }17 }18}Mostrar todoConclusión
El puente estándar es el mecanismo más flexible para las transferencias de activos. Sin embargo, debido a que es tan genérico, no siempre es el mecanismo más fácil de usar. Especialmente para las retiradas, la mayoría de los usuarios prefieren usar puentes de tercerosopens in a new tab que no esperan el período de impugnación y no requieren una prueba de Merkle para finalizar la retirada.
Estos puentes suelen funcionar teniendo activos en L1, que proporcionan inmediatamente por una pequeña tarifa (a menudo menor que el coste de gas para una retirada de puente estándar). Cuando el puente (o las personas que lo gestionan) prevé que se quedará sin activos en L1, transfiere suficientes activos desde L2. Como se trata de retiradas muy grandes, el coste de la retirada se amortiza sobre una gran cantidad y es un porcentaje mucho menor.
Esperamos que este artículo le haya ayudado a entender más sobre cómo funciona la capa 2 y cómo escribir código de Solidity que sea claro y seguro.
Vea aquí más de mi trabajoopens in a new tab.
Última actualización de la página: 22 de octubre de 2025