Passer au contenu principal

Introduction au contrat de passerelle standard Optimism

soliditybridgeCouche 2
Intermédiaire
Ori Pomerantz
30 mars 2022
34 minutes de lecture minute read

Optimism(opens in a new tab) est un rollup optimiste. Les rollups Optimistics peuvent traiter les transactions à un prix beaucoup plus bas que le réseau principal Ethereum (également connu sous le nom de couche 1 ou L1), car les transactions sont traitées uniquement par quelques nœuds en lieu et place de tous les nœuds du réseau. En même temps, les données sont toutes écrites sur L1 afin que tout puisse être prouvé et reconstruit avec toutes les garanties d'intégrité et de disponibilité du réseau principal.

Pour utiliser les actifs L1 sur Optimism (ou n'importe quel autre L2), les actifs doivent être connectés. Une façon d'y arriver est pour les utilisateurs de verrouiller les actifs (ETH et les jetons ERC-20 sont les plus communs) sur L1 et de recevoir des actifs équivalents à utiliser sur L2. Finalement, celui qui se retrouve avec souhaitera peut-être les ramener en L1. En faisant cela, les actifs sont brûlés sur L2 puis redistribués à l'utilisateur sur L1.

C'est ainsi que fonctionne la passerelle standard Optimism(opens in a new tab). Dans cet article, nous passerons en revue le code source de cette passerelle pour comprendre comment elle fonctionne et l'étudier comme un exemple de code Solidity parfaitement écrit.

Flux de contrôle

La passerelle dispose de deux flux principaux :

  • Dépôt (de L1 vers L2)
  • Retrait (de L2 vers L1)

Flux de dépôt

Couche 1

  1. En cas de dépôt d'un ERC-20, le déposant affecte à la passerelle une provision pour dépenser le montant déposé
  2. Le déposant appelle la passerelle L1 (depositERC20, depositERC20To, depositETH, ou depositETHTo)
  3. La passerelle L1 prend possession de l'actif connecté
    • ETH : l'actif est transféré par le déposant dans le cadre de l'appel
    • ERC-20 : l'actif est transféré par la passerelle à elle-même en utilisant la provision fournie par le déposant
  4. La passerelle de connexion L1 utilise le mécanisme de message inter-domaine pour appeler finalizeDeposit sur la passerelle de connexion L2

Couche 2

  1. La passerelle de connexion L2 vérifie que l'appel finalizeDeposit est légitime :
    • Provient du contrat de message inter-domaine
    • Était à l'origine en provenance de la passerelle de connexion sur L1
  2. La passerelle de connexion L2 vérifie si le contrat de jeton ERC-20 sur L2 est le bon :
    • Le contrat L2 signale que son homologue L1 est identique à celui dont les jetons provenaient sur L1
    • Le contrat L2 signale qu'il prend en charge l'interface correcte (en utilisant ERC-165(opens in a new tab)).
  3. Si le contrat L2 est le bon, appelez-le pour frapper le nombre approprié de jetons à l'adresse appropriée. Sinon, commencez un processus de retrait pour permettre à l'utilisateur de réclamer les jetons sur L1.

Flux de retrait

Couche 2

  1. Le retirant appelle la passerelle de connexion L2 (withdraw ou withdrawTo)
  2. La passerelle de connexion L2 brûle le nombre approprié de jetons appartenant à msg.sender
  3. La passerelle de connexion L2 utilise le mécanisme de message inter-domaine pour appeler finalizeETHWithdrawal ou finalizeERC20Withdrawal de la passerelle L1

Couche 1

  1. La passerelle de connexion L1 vérifie que l'appel à finalizeETHWithal ou à finalizeERC20Withal est légitime :
    • Provient du mécanisme de message inter-domaine
    • Était à l'origine en provenance de la passerelle de connexion sur L2
  2. La passerelle L1 transfère l'actif approprié (ETH ou ERC-20) à l'adresse appropriée

Code de la couche 1

C'est le code qui s'exécute sur L1, le réseau principal Ethereum.

IL1ERC20Bridge

Cette interface est définie ici(opens in a new tab). Elle comprend les fonctions et les définitions requises pour la connexion en passerelle des jetons ERC-20.

1// SPDX-License-Identifier: MIT
Copier

La plupart du code Optimism est publié sous la licence MIT(opens in a new tab).

1pragma solidity >0.5.0 <0.9.0;
Copier

Lors de l'écriture de cet article, la dernière version de Solidity était 0.8.12. Jusqu'à la publication de la version 0.9.0, nous ne saurons pas si ce code est compatible ou non.

1/**
2 * @title IL1ERC20Bridge
3 */
4interface IL1ERC20Bridge {
5 /**********
6 * Events *
7 **********/
8
9 event ERC20DepositInitiated(
Afficher tout
Copier

Dans le terminologique des passerelles pour Optimism deposit signifie transférer de L1 vers L2, et withdrawal signifie un transfert de L2 vers L1.

1 address indexed _l1Token,
2 address indexed _l2Token,
Copier

Dans la plupart des cas, l'adresse d'un ERC-20 sur L1 n'est pas la même que celle de l'équivalent ERC-20 sur L2. Vous pouvez consulter la liste des adresses de jetons ici(opens in a new tab). L'adresse avec chainId 1 est sur L1 (le réseau principal) et l'adresse avec chainId 10 est sur L2 (Optimism). Les deux autres valeurs chainId sont pour le réseau de test Kovan (42) et le réseau de test Optimistic Kovan (69).

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

Il est possible d'ajouter des notes aux transferts, auquel cas elles sont ajoutées aux événements qui les signalent.

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

Le même contrat passerelle gère les transferts dans les deux sens. Dans le cas de la passerelle L1, cela signifie l'initialisation des dépôts et la finalisation des retraits.

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);
Afficher tout
Copier

Cette fonction n'est pas vraiment nécessaire, car sur L2 c'est un contrat prédéployé, donc il sera toujours à l'adresse 0x420000000000000000000000000000000000000000000010. Il est ici pour la symétrie avec la passerelle L2, car l'adresse de la passerelle de connexion L1 n'est pas à connaître.

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;
Afficher tout
Copier

Le paramètre _l2Gas est le montant de gaz L2 que la transaction est autorisée à dépenser. Jusqu'à une certaine limite (haute), c'est gratuit(opens in a new tab), donc à moins que le contrat ERC-20 ne fasse quelque chose de vraiment étrange lors de la frappe, il ne devrait pas y avoir de problème. Cette fonction prend en charge le scénario commun où un utilisateur relie les actifs à la même adresse sur une blockchain différente.

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;
Afficher tout
Copier

Cette fonction est presque identique à depositERC20, mais elle vous permet d'envoyer l'ERC-20 à une adresse différente.

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}
Afficher tout
Copier

Les retraits (et autres messages de L2 vers L1) dans Optimism sont des processus en deux étapes :

  1. Une transaction d'initialisation sur L2.
  2. Une transaction de finalisation ou de réclamation sur L1. Cette transaction doit être réalisée après la période de contestation des défauts(opens in a new tab) pour que la transaction L2 se termine.

IL1StandardBridge

Cette interface est définie ici(opens in a new tab). Ce fichier contient des définitions d'événements et de fonctions pour ETH. Ces définitions sont très similaires à celles définies ci-dessus dans IL1ERC20Bridge pour ERC-20.

L'interface de passerelle est divisée entre deux fichiers puisque certains jetons ERC-20 nécessitent un traitement personnalisé et ne peuvent pas être traités par la passerelle de connexion standard. De cette façon, la passerelle personnalisée de connexion qui gère un tel jeton peut implémenter IL1ERC20Bridge et ne pas nécessiter une passerelle pour 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 );
Afficher tout
Copier

Cet événement est presque identique à la version ERC-20 (ERC20DepositInitiated), mais sans les adresses de jeton L1 et L2. Il en va de même pour les autres événements et les fonctions.

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}
Afficher tout
Copier

CrossDomainEnabled

Ce contrat(opens in a new tab) a hérité des deux passerelles (L1 et L2) pour envoyer des messages à l'autre couche.

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

Cette interface(opens in a new tab) indique au contrat comment envoyer des messages à l'autre couche, en utilisant le messager inter-domaine. Cette messagerie transversale de domaine est un autre système à part entière et mériterait son propre article que j'espère écrire à l'avenir.

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 }
Afficher tout
Copier

Le seul paramètre que le contrat a besoin de connaître est l'adresse du messager de domaines croisés sur cette couche. Ce paramètre est défini une seule fois, dans le constructeur, et ne change jamais.

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) {
Afficher tout
Copier

La messagerie inter-domaine est accessible par n'importe quel contrat sur la blockchain où elle est exécutée (soit le réseau principal Ethereum, soit Optimism). Mais nous avons besoin du pont de chaque côté pour faire confiance uniquement à certains messages qui viennent de la passerelle de l'autre côté.

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

Seuls les messages du messager inter-domaine approprié (messenger, comme indiqué ci-dessous) peuvent être fiables.

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

La façon dont la messagerie inter-domaine fournit l'adresse qui a envoyé un message avec l'autre couche est la fonction .xDomainMessageSender()(opens in a new tab). Tant qu'elle est appelée dans la transaction qui a été initiée par le message, elle peut fournir ces informations.

Nous devons nous assurer que le message que nous avons reçu provient bien de l'autre passerelle.

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 }
Afficher tout
Copier

Cette fonction retourne le messager inter-domaine. Nous utilisons une fonction plutôt que la variable messenger pour permettre aux contrats qui héritent de celui-ci d'utiliser un algorithme pour spécifier quel messager de domaine croisé utiliser.

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
Afficher tout
Copier

Enfin, la fonction qui envoie un message à l'autre couche.

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

Slither(opens in a new tab) est un analyseur statique Optimism qui fonctionne sur chaque contrat pour rechercher des vulnérabilités et d'autres problèmes potentiels. Dans notre cas, la ligne suivante déclenche deux vulnérabilités :

  1. Événements de réentrance(opens in a new tab)
  2. Réentrance Benign(opens in a new tab)
1 getCrossDomainMessenger().sendMessage(_crossDomainTarget, _message, _gasLimit);
2 }
3}
Copier

Dans notre cas, nous ne devons pas nous inquiéter de la réentrance, nous savons que getCrossDomainMessenger() retourne une adresse digne de confiance, même si Slither n'a aucun moyen de le savoir.

Le contrat de passerelle L1

Le code source de ce contrat se trouve ici(opens in a new tab).

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

Les interfaces peuvent faire partie d'autres contrats, elles doivent donc supporter un large éventail de versions Solidity. Mais la passerelle en elle-même est notre contrat, et nous pouvons être stricts quant à la version de Solidity utilisée.

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

IL1ERC20Bridge et IL1StandardBridge sont expliqués ci-dessus.

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

Cette interface(opens in a new tab) nous permet de créer des messages pour contrôler la passerelle de connexion standard sur L2.

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

Cette interface(opens in a new tab) nous permet de piloter les contrats ERC-20. Vous pouvez en savoir plus sur ce sujet ici.

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

Comme expliqué ci-dessus, ce contrat est utilisé pour la messagerie intercouche.

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

Lib_PredeployAdresses(opens in a new tab)dispose des adresses pour les contrats L2 qui ont toujours la même adresse. Cela inclut la passerelle standard sur la L2.

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

Utilitaires d'adresses OpenZeppelin(opens in a new tab). Ils servent à distinguer les adresses contractuelles de celles appartenant à des comptes propriétaires externes (EOA).

Notez que ce n'est pas une solution parfaite, car il n'y a aucun moyen de distinguer les appels directs de ceux réalisés par le constructeur d'un contrat, mais au moins cela nous permet d'identifier et de prévenir certaines erreurs utilisateurs courantes.

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

La norme ERC-20(opens in a new tab) prend en charge deux manières pour un contrat de signaler un échec :

  1. Rétablir
  2. Renvoyer false

La gestion des deux cas rendrait notre code plus compliqué donc à la place nous utilisons le SafeERC20 d'OpenZeppelin(opens in a new tab), qui s'assure que tous les échecs aboutissent à un rétablissement(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;
Afficher tout
Copier

Cette ligne montre comment nous spécifions d'utiliser le wrapper SafeERC20 chaque fois que nous utilisons l'interface IERC20.

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

L'adresse 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;
Copier

Un double mapping(opens in a new tab) comme celui-ci est la façon dont vous définissez un rare tableau bidimensionnel(opens in a new tab). Les valeurs dans cette structure de données sont identifiées comme deposit[L1 token addr][L2 token addr]. La valeur par défaut est zéro. Seules les cellules qui sont définies à une valeur différente sont écrites pour le stockage.

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)) {}
Copier

Pour pouvoir mettre à jour ce contrat sans avoir à copier toutes les variables dans le stockage. Pour cela, nous utilisons un Proxy(opens in a new tab), un contrat qui utilise delegatecall(opens in a new tab) pour transférer des appels à un contact distinct dont l'adresse est stockée par le contrat de proxy (lorsque vous mettez à jour, vous ordonnez au proxy de changer cette adresse). Lorsque vous utilisez delegatecall le stockage reste le stockage du contrat appelant, donc les valeurs de toutes les variables d'état du contrat ne sont pas affectées.

Un effet de cette pratique est que le stockage du contrat qui est appelé de delegatecall n'est pas utilisé et donc les valeurs du constructeur qui lui sont passées n'ont pas d'importance. C'est la raison pour laquelle nous pouvons fournir une valeur absurde au constructeur CrossDomainEnabled. C'est aussi la raison pour laquelle l'initialisation ci-dessous est séparée du constructeur.

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
Afficher tout
Copier

Ce test Slither(opens in a new tab) identifie les fonctions qui ne sont pas appelées à partir du code du contrat et pourrait donc être déclarées external au lieu de public. Le coût en gaz des fonctions external peut être diminué, car elles peuvent être fournies avec des paramètres dans le calldata. Les fonctions déclarées public doivent être accessibles depuis le contrat. Les contrats ne peuvent pas modifier leurs propres calldata ainsi, les paramètres doivent être en mémoire. Lorsqu'une telle fonction est appelée en externe, il est nécessaire de copier le calldata dans la mémoire, ce qui coûte du gaz. Dans ce cas, la fonction n'est appelée qu'une seule fois, donc son inefficacité n'a pas d'importance pour nous.

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

La fonction initialize ne doit être appelée qu'une seule fois. Si l'adresse du messager inter-domaine L1 ou du jeton de connexion L2 changent, nous créons un nouveau proxy et une nouvelle passerelle qui l'appellera. Il est peu probable que cela se produise sauf lorsque le système dans son entier est mis à jour, ce qui est très rare.

Notez que cette fonction ne dispose d'aucun mécanisme qui délimite qui peut l'appeler. Cela signifie qu'en théorie, un attaquant pourrait attendre que nous déployions le proxy et la première version de la passerelle de connexion et donc front-run(opens in a new tab) pour accéder à la fonction initialize avant que l'utilisateur légitime ne le fasse. Il existe deux méthodes pour éviter cela :

  1. Si les contrats ne sont pas déployés directement par un EOA, mais dans une transaction qui contient un autre contrat les créant(opens in a new tab) l'ensemble du processus peut être atomique, et se terminer avant que toute autre transaction soit exécutée.
  2. Si l'appel légitime à initialize échoue, il est toujours possible d'ignorer le proxy et la passerelle de connexion nouvellement créé et d'en créer de nouveaux.
1 messenger = _l1messenger;
2 l2TokenBridge = _l2TokenBridge;
3 }
Copier

Ce sont les deux paramètres que la passerelle a besoin de connaître.

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 }
Afficher tout
Copier

C'est la raison pour laquelle nous avions besoin des utilitaires d'Address d'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 }
Afficher tout
Copier

Cette fonction existe à des fins de test. Notez qu'il n'apparaît pas dans les définitions d'interface - elle n'est pas prévue pour une utilisation normale.

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 }
Afficher tout
Copier

Ces deux fonctions sont enveloppées autour de _initiateETHDeposit, la fonction qui gère le dépôt réel ETH.

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(
Afficher tout
Copier

La façon dont les messages transversaux fonctionnent est que le contrat de destination est appelé avec le message comme ses calldata. Les contrats Solidity interprètent toujours si leurs calldata sont conformes aux spécifications de l'ABI(opens in a new tab). La fonction Solidity abi.encodeWithSelector(opens in a new tab) crée ces calldata.

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

Le message ici est destiné à appeler la fonction finalizeDeposit(opens in a new tab) avec ces paramètres :

ParamètreValeurSignification
_l1Tokenaddress(0)Valeur spéciale pour représenter ETH (qui n'est pas un jeton ERC-20) sur L1
_l2TokenLib_PredeployAddresses.OVM_ETHLe contrat L2 qui gère ETH sur Optimism, 0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000 (ce contrat est destiné à un usage Optimism uniquement interne)
_from_fromL'adresse sur L1 qui envoie l'ETH
_to_toL'adresse sur L2 qui reçoit l'ETH
amountmsg.valueMontant de Wei envoyé (qui a déjà été envoyé sur la passerelle)
_data_dataDate supplémentaire à joindre au dépôt
1 // Send calldata into L2
2 // slither-disable-next-line reentrancy-events
3 sendCrossDomainMessage(l2TokenBridge, _l2Gas, message);
Copier

Envoyez le message à travers le messager inter-domaine.

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

Émettre un événement pour informer toute application décentralisée qui écoute ce transfert.

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 }
Afficher tout
Copier

Ces deux fonctions sont enveloppées autour de _initiateERC20Deposit, la fonction qui gère le dépôt réel ETH.

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 {
Afficher tout
Copier

Cette fonction est similaire à _initiateETHDeposit ci-dessus, avec quelques différences importantes. La première différence est que cette fonction reçoit les adresses de jetons et le montant à transférer en tant que paramètres. Dans le cas d'ETH, l'appel à la passerelle comprend déjà le transfert de l'actif à la passerelle de connexion (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);
Copier

Les transferts de jetons ERC-20 suivent un processus différent de celui pour ETH :

  1. L'utilisateur (_from) apporte une provision à la passerelle de connexion pour transférer les jetons appropriés.
  2. L'utilisateur appelle la passerelle de connexion avec l'adresse du contrat de jeton, le montant, etc.
  3. La passerelle transfère les jetons (à elle-même) dans le cadre du processus de dépôt.

La première étape peut se produire dans une transaction séparée des deux dernières. Cependant, front-running n'est pas un problème car les deux fonctions qui appellent _initiateERC20Deposit (depositERC20 et depositERC20To) n'appellent cette fonction qu'avec msg.sender en tant que paramètre _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;
Afficher tout
Copier

Ajoute le nombre de jetons déposés à la structure de données deposits. Il pourrait y avoir plusieurs adresses sur L2 qui correspondent au même jeton L1 ERC-20, donc il n'est pas suffisant d'utiliser le solde sur la passerelle de jetons L1 ERC-20 pour garder une trace des dépôts.

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
Afficher tout
Copier

La passerelle de connexion L2 envoie un message au messager du domaine transversal L2 qui fait que le messager du domaine transversal L1 appelle cette fonction (lorsque la transaction qui finalise le message(opens in a new tab) est soumise sur L1, bien sûr).

1 ) external onlyFromCrossDomainAccount(l2TokenBridge) {
Copier

Assurez-vous qu'il s'agit d'un message légitime provenant de la messagerie inter-domaine et originaire de la passerelle de jeton L2. Cette fonction est utilisée pour retirer l'ETH de la passerelle de connexion ainsi, nous devons nous assurer qu'elle n'est appelée que par l'appelant autorisé.

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

Le moyen de transférer de l'ETH est d'appeler le destinataire avec le montant de wei dans le 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);
Copier

Émettre un événement à propos du retrait.

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) {
Afficher tout
Copier

Cette fonction est similaire à finalizeETHWithal ci-dessus, avec les changements nécessaires pour les jetons ERC-20.

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

Mettre à jour la structure de données deposits.

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

Il y a eu une implémentation antérieure de la passerelle. Lorsque nous sommes passés de l'implémentation à celle-ci, nous avons dû déplacer tous les actifs. Les jetons ERC-20 peuvent juste être déplacés. Cependant, pour transférer l'ETH à un contrat, vous avez besoin de l'approbation de ce contrat, ce que donateETH nous fournit.

Jetons ERC-20 sur L2

Pour qu'un jeton ERC-20 s'intègre dans la passerelle standard, il doit permettre à la passerelle de connexion standard, et uniquement la passerelle standard, de frapper des jetons. Ceci est nécessaire, car les passerelles doivent s'assurer que le nombre de jetons circulant sur Optimism est égal au nombre de jetons verrouillés à l'intérieur du contrat passerelle L1. S'il existe trop de jetons sur L2, certains utilisateurs seraient incapables de récupérer leurs actifs sur L1. Au lieu d'une passerelle de confiance, nous recréerions en fait une banque de réserve fractionnaire(opens in a new tab). S'il y a trop de jetons sur L1, certains de ces jetons resteraient bloqués à l'intérieur du contrat passerelle pour toujours puisqu'il n'y a aucun moyen de les libérer sans brûler les jetons L2.

IL2StandardERC20

Chaque jeton ERC-20 sur L2 qui utilise la passerelle standard doit fournir cette interface(opens in a new tab), qui possède les fonctions et les événements dont la passerelle standard a besoin.

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

L'interface standard ERC-20(opens in a new tab) ne dispose ni des fonctions mint ni burn. Ces méthodes ne sont pas requises par le standard ERC-20(opens in a new tab), qui laisse les mécanismes non spécifiés pour créer et détruire des jetons.

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

L'interface ERC-165(opens in a new tab) est utilisée pour spécifier quelles fonctions sont proposées par un contrat. Vous pouvez lire le standard ici(opens in a new tab).

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

Cette fonction fournit l'adresse du jeton L1 qui est relié à ce contrat. Notez que nous n'avons pas de fonction similaire dans la direction opposée. Nous devons être en mesure de relier tout jeton L1, que la prise en charge L2 ait été planifiée ou non lorsqu'il a été implémenté.

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}
Copier

Les fonctions et événements à frapper (créer) et brûler (détruire) des jetons. La passerelle de connexion devrait être la seule entité qui puisse exécuter ces fonctions pour s'assurer que le nombre de jetons est correct (égal au nombre de jetons verrouillés sur L1).

L2StandardERC20

Ceci est notre implémentation de l'interface IL2StandardERC20(opens in a new tab). À moins que vous n'ayez besoin d'une sorte de logique personnalisée, vous devriez utiliser celle-ci.

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

Le contrat OpenZeppelin ERC-20(opens in a new tab). Optimism ne souhaite pas réinventer de la roue, surtout lorsque la roue est bien contrôlée et a besoin d'être suffisamment digne de confiance pour détenir des actifs.

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

Ce sont les deux paramètres de configuration supplémentaires dont nous avons besoin et que ERC-20 ne réalise normalement pas.

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 }
Afficher tout
Copier

En premier lieu, appelez le constructeur pour le contrat dont nous héritons de (ERC20(_name, _symbol)) et définissez ensuite vos propres 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 }
Afficher tout
Copier

C'est ainsi que fonctionne ERC-165(opens in a new tab). Chaque interface est un certain nombre de fonctions supportées, et est identifiée comme ABIexclusive ou(opens in a new tab) des sélecteurs de fonctions ABI(opens in a new tab) pour ces fonctions.

La passerelle de connexion L2 utilise ERC-165 comme vérification garantissant que le contrat ERC-20 auquel des actifs sont envoyés est un IL2StandardERC20.

Remarque : Il n'existe rien pour empêcher le contrat dévoyé de fournir de fausses réponses à supportsInterface ainsi, il s'agit donc d'un mécanisme de vérification et non pas un mécanisme de sécurité.

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}
Afficher tout
Copier

Seul la passerelle L2 est autorisée à frapper et à brûler des actifs.

_mint et _burn sont définis dans le contrat OpenZeppelin ERC-20. Ce contrat ne les expose pas en externe, parce que les conditions de frappe et de brûlage des jetons sont aussi variées que le nombre de façons d'utiliser ERC-20.

Code de passerelle L2

Il s'agit du code qui exécute la passerelle de connexion sur Optimism. La source de ce contrat se trouve ici(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";
Copier

L'interface IL2ERC20Bridge(opens in a new tab) est très similaire à l'équivalent L1 que nous avons vu ci-dessus. Il existe deux différences significatives :

  1. Sur L1, vous initiez des dépôts et finalisez des retraits. Ici, vous initiez des retraits et finalisez les dépôts.
  2. Sur L1, il est nécessaire de faire une distinction entre les jetons ETH et ERC-20. Sur L2, nous pouvons utiliser les mêmes fonctions pour les deux, parce que les soldes ETH en interne sur Optimism sont traités comme un jeton ERC-20 avec l'adresse 0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000(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;
Afficher tout
Copier

Conserver la trace de l'adresse de la passerelle de connexion L1. Notez que contrairement à l'équivalent pour L1, ici nous _ avons besoin _de cette variable. L'adresse de la passerelle L1 n'est pas connue à l'avance.

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 }
Afficher tout
Copier

Ces deux fonctions initient les retraits. Notez qu'il n'y a pas besoin de spécifier l'adresse de jeton L1. Les jetons L2 doivent nous indiquer l'adresse de l'équivalent 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);
Afficher tout
Copier

Notez que nous ne sommes pas reliés au paramètre _from mais à msg.sender qui est beaucoup plus difficile à contrefaire (même impossible, à ma connaissance).

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) {
Copier

Sur L1, il est nécessaire de faire une distinction entre les jetons ETH et 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
Afficher tout
Copier

Cette fonction est appelée via L1StandardBridge.

1 ) external virtual onlyFromCrossDomainAccount(l1TokenBridge) {
Copier

Assurez-vous que la source du message est fiable. C'est important car cette fonction appelle _mint et peut être utilisée pour donner des jetons qui ne sont pas couverts par des jetons que la passerelle possède sur 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()
Copier

Contrôle de cohérence :

  1. L'interface correcte est prise en charge
  2. L'adresse du contrat L2 ERC-20 du L1 correspond à la source L1 des jetons
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);
Copier

Si les contrôles de cohérence sont effectués, finalisez le dépôt :

  1. Frapper les jetons
  2. Émettre l'événement approprié
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.
Afficher tout
Copier

Si un utilisateur a fait une erreur détectable en utilisant la mauvaise adresse de jeton L2, nous souhaitons annuler le dépôt et retourner les jetons sur L1. La seule façon de le faire à partir de L2 est d'envoyer un message qui devra attendre la période problématique de défaut mais c'est beaucoup mieux pour l'utilisateur que de perdre les jetons définitivement.

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}
Afficher tout
Copier

Conclusion

La passerelle standard est le mécanisme le plus souple pour les transferts d'actifs. Cependant, parce qu'il est si générique, ce n'est pas toujours le mécanisme le plus facile à utiliser. Spécialement pour les retraits, la plupart des utilisateurs préfèrent utiliser des passerelles tierces(opens in a new tab) qui n'attendent pas la période problématique et ne nécessitent pas de preuve de Merkle pour finaliser le retrait.

Ces passerelles fonctionnent généralement en ayant des actifs sur L1 qu'ils fournissent immédiatement moyennant un petit supplément (souvent inférieur au coût du gaz pour un retrait standard d'une passerelle). Quand la passerelle (ou la personne qui la gère) prévoit d'être en deçà des actifs L1, elle transfère suffisamment d'actifs de L2. Comme il s'agit de retraits très importants, le coût de retrait est amorti sur une somme importante et représente un pourcentage beaucoup plus faible.

Espérons que cet article vous aide à mieux comprendre comment la couche 2 fonctionne, et comment écrire du code Solidity clair et sécurisé.

Dernière modification: @MATsxm(opens in a new tab), 8 décembre 2023

Ce tutoriel vous a été utile ?