Passer au contenu principal

Présentation du contrat de pont standard d'Optimism

Solidity
pont
couche 2
Intermédiaire
Ori Pomerantz
30 mars 2022
35 minutes de lecture

Optimismopens in a new tab est un rollup optimiste. Les rollups optimistes 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, au lieu de chaque nœud sur le réseau. En même temps, toutes les données sont écrites sur la 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 de L1 sur Optimism (ou toute autre L2), les actifs doivent être pontés. Une façon d'y parvenir est pour les utilisateurs de verrouiller des actifs (l'ETH et les jetons ERC-20 sont les plus courants) sur la L1, et de recevoir des actifs équivalents à utiliser sur la L2. Finalement, quiconque se retrouve avec ces actifs pourrait vouloir les ponter à nouveau vers la L1. Ce faisant, les actifs sont brûlés sur la L2, puis restitués à l'utilisateur sur la L1.

C'est ainsi que fonctionne le pont standard d'Optimismopens in a new tab. Dans cet article, nous passons en revue le code source de ce pont pour voir comment il fonctionne et l'étudier comme un exemple de code Solidity bien écrit.

Flux de contrôle

Le pont a deux flux principaux :

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

Flux de dépôt

Couche 1

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

Couche 2

  1. Le pont L2 vérifie que l'appel à finalizeDeposit est légitime :
    • Provenant du contrat de message inter-domaines
    • Était initialement du pont sur la L1
  2. Le pont L2 vérifie si le contrat de jeton ERC-20 sur la L2 est le bon :
    • Le contrat L2 signale que son homologue L1 est le même que celui d'où provenaient les jetons sur la L1
    • Le contrat L2 signale qu'il prend en charge l'interface correcte (en utilisant l'ERC-165opens in a new tab).
  3. Si le contrat L2 est le bon, appelez-le pour frapper le nombre de jetons approprié à l'adresse appropriée. Sinon, démarrez un processus de retrait pour permettre à l'utilisateur de réclamer les jetons sur la L1.

Flux de retrait

Couche 2

  1. Le retireur appelle le pont L2 (withdraw ou withdrawTo)
  2. Le pont L2 brûle le nombre approprié de jetons appartenant à msg.sender
  3. Le pont L2 utilise le mécanisme de message inter-domaines pour appeler finalizeETHWithdrawal ou finalizeERC20Withdrawal sur le pont L1

Couche 1

  1. Le pont L1 vérifie que l'appel à finalizeETHWithdrawal ou finalizeERC20Withdrawal est légitime :
    • Provenant du mécanisme de message inter-domaines
    • Était initialement du pont sur la L2
  2. Le pont 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 la L1, le réseau principal Ethereum.

IL1ERC20Bridge

Cette interface est définie iciopens in a new tab. Elle inclut les fonctions et les définitions nécessaires au pontage des jetons ERC-20.

1// SPDX-License-Identifier: MIT

La plupart du code d'Optimism est publié sous la licence MITopens in a new tab.

1pragma solidity >0.5.0 <0.9.0;

Au moment de la rédaction, la dernière version de Solidity est la 0.8.12. Tant que la version 0.9.0 n'est pas sortie, nous ne savons pas si ce code est compatible avec elle ou non.

1/**
2 * @title IL1ERC20Bridge
3 */
4interface IL1ERC20Bridge {
5 /**********
6 * Événements *
7 **********/
8
9 event ERC20DepositInitiated(
Afficher tout

Dans la terminologie des ponts Optimism, deposit (dépôt) signifie un transfert de la L1 vers la L2, et withdrawal (retrait) signifie un transfert de la L2 vers la L1.

1 address indexed _l1Token,
2 address indexed _l2Token,

Dans la plupart des cas, l'adresse d'un ERC-20 sur la L1 n'est pas la même que l'adresse de l'ERC-20 équivalent sur la L2. Vous pouvez voir la liste des adresses de jetons iciopens in a new tab. L'adresse avec chainId 1 est sur la L1 (réseau principal) et l'adresse avec chainId 10 est sur la L2 (Optimism). Les deux autres valeurs de 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 );

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

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

1
2 /********************
3 * Fonctions publiques *
4 ********************/
5
6 /**
7 * @dev obtient l'adresse du contrat de pont L2 correspondant.
8 * @return Adresse du contrat de pont L2 correspondant.
9 */
10 function l2TokenBridge() external returns (address);
Afficher tout

Cette fonction n'est pas vraiment nécessaire, car sur la L2, c'est un contrat prédéployé, donc il est toujours à l'adresse 0x4200000000000000000000000000000000000010. Elle est là pour la symétrie avec le pont L2, car l'adresse du pont L1 n'est pas triviale à connaître.

1 /**
2 * @dev dépose un montant d'ERC20 sur le solde de l'appelant sur la L2.
3 * @param _l1Token Adresse de l'ERC20 de L1 que nous déposons
4 * @param _l2Token Adresse de l'ERC20 respectif de L2
5 * @param _amount Montant de l'ERC20 à déposer
6 * @param _l2Gas Limite de gaz requise pour finaliser le dépôt sur la L2.
7 * @param _data Données facultatives à transmettre à la L2. Ces données sont fournies
8 * uniquement pour la commodité des contrats externes. En dehors de l'application d'une longueur
9 * maximale, ces contrats ne fournissent aucune garantie sur leur contenu.
10 */
11 function depositERC20(
12 address _l1Token,
13 address _l2Token,
14 uint256 _amount,
15 uint32 _l2Gas,
16 bytes calldata _data
17 ) external;
Afficher tout

Le paramètre _l2Gas est la quantité de gaz L2 que la transaction est autorisée à dépenser. Jusqu'à une certaine limite (élevée), c'est gratuitopens in a new tab, donc à moins que le contrat ERC-20 ne fasse quelque chose de vraiment étrange lors de la frappe, cela ne devrait pas être un problème. Cette fonction prend en charge le scénario courant, où un utilisateur ponte des actifs vers la même adresse sur une autre blockchain.

1 /**
2 * @dev dépose un montant d'ERC20 sur le solde d'un destinataire sur la L2.
3 * @param _l1Token Adresse de l'ERC20 de L1 que nous déposons
4 * @param _l2Token Adresse de l'ERC20 respectif de L2
5 * @param _to Adresse L2 sur laquelle créditer le retrait.
6 * @param _amount Montant de l'ERC20 à déposer.
7 * @param _l2Gas Limite de gaz requise pour finaliser le dépôt sur la L2.
8 * @param _data Données facultatives à transmettre à la L2. Ces données sont fournies
9 * uniquement pour la commodité des contrats externes. En dehors de l'application d'une longueur
10 * maximale, ces contrats ne fournissent aucune garantie sur leur contenu.
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

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

1 /*************************
2 * Fonctions inter-chaînes *
3 *************************/
4
5 /**
6 * @dev Finalise un retrait de la L2 vers la L1, et crédite les fonds sur le solde du destinataire du
7 * jeton ERC20 de L1.
8 * Cet appel échouera si le retrait initialisé depuis la L2 n'a pas été finalisé.
9 *
10 * @param _l1Token Adresse du jeton L1 pour lequel finaliser le retrait.
11 * @param _l2Token Adresse du jeton L2 où le retrait a été initié.
12 * @param _from Adresse L2 initiant le transfert.
13 * @param _to Adresse L1 sur laquelle créditer le retrait.
14 * @param _amount Montant de l'ERC20 à déposer.
15 * @param _data Données fournies par l'expéditeur sur la L2. Ces données sont fournies
16 * uniquement pour la commodité des contrats externes. En dehors de l'application d'une longueur
17 * maximale, ces contrats ne fournissent aucune garantie sur leur contenu.
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

Les retraits (et autres messages de la L2 à la L1) dans Optimism sont un processus en deux étapes :

  1. Une transaction d'initiation sur la L2.
  2. Une transaction de finalisation ou de réclamation sur la L1. Cette transaction doit avoir lieu après la fin de la période de contestation des erreursopens in a new tab pour la transaction L2.

IL1StandardBridge

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

L'interface du pont est divisée en deux fichiers car certains jetons ERC-20 nécessitent un traitement personnalisé et ne peuvent pas être gérés par le pont standard. De cette façon, le pont personnalisé qui gère un tel jeton peut implémenter IL1ERC20Bridge et ne pas avoir à ponter également l'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 * Événements *
12 **********/
13 event ETHDepositInitiated(
14 address indexed _from,
15 address indexed _to,
16 uint256 _amount,
17 bytes _data
18 );
Afficher tout

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 * Fonctions publiques *
9 ********************/
10
11 /**
12 * @dev Dépose un montant d'ETH sur le solde de l'appelant sur la L2.
13 .
14 .
15 .
16 */
17 function depositETH(uint32 _l2Gas, bytes calldata _data) external payable;
18
19 /**
20 * @dev Dépose un montant d'ETH sur le solde d'un destinataire sur la L2.
21 .
22 .
23 .
24 */
25 function depositETHTo(
26 address _to,
27 uint32 _l2Gas,
28 bytes calldata _data
29 ) external payable;
30
31 /*************************
32 * Fonctions inter-chaînes *
33 *************************/
34
35 /**
36 * @dev Finalise un retrait de la L2 vers la L1, et crédite les fonds sur le solde du destinataire du
37 * jeton ETH de L1. Étant donné que seul le xDomainMessenger peut appeler cette fonction, elle ne sera jamais appelée
38 * avant que le retrait soit finalisé.
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

CrossDomainEnabled

Ce contratopens in a new tab est hérité par les deux ponts (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";

Cette interfaceopens in a new tab indique au contrat comment envoyer des messages à l'autre couche, en utilisant le messager inter-domaines. Ce messager inter-domaines est un tout autre système, et mérite son propre article, que j'espère écrire à l'avenir.

1/**
2 * @title CrossDomainEnabled
3 * @dev Contrat d'aide pour les contrats effectuant des communications inter-domaines
4 *
5 * Compilateur utilisé : défini par le contrat héritier
6 */
7contract CrossDomainEnabled {
8 /*************
9 * Variables *
10 *************/
11
12 // Contrat de messagerie utilisé pour envoyer et recevoir des messages de l'autre domaine.
13 address public messenger;
14
15 /***************
16 * Constructeur *
17 ***************/
18
19 /**
20 * @param _messenger Adresse du CrossDomainMessenger sur la couche actuelle.
21 */
22 constructor(address _messenger) {
23 messenger = _messenger;
24 }
Afficher tout

Le seul paramètre que le contrat doit connaître, l'adresse du messager inter-domaines sur cette couche. Ce paramètre est défini une fois, dans le constructeur, et ne change jamais.

1
2 /**********************
3 * Modificateurs de fonction *
4 **********************/
5
6 /**
7 * Impose que la fonction modifiée ne puisse être appelée que par un compte inter-domaine spécifique.
8 * @param _sourceDomainAccount Le seul compte sur le domaine d'origine qui est
9 * authentifié pour appeler cette fonction.
10 */
11 modifier onlyFromCrossDomainAccount(address _sourceDomainAccount) {
Afficher tout

La messagerie inter-domaines 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 que le pont de chaque côté ne fasse confiance à certains messages que s'ils proviennent du pont de l'autre côté.

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

Seuls les messages provenant du messager inter-domaines approprié (messenger, comme vous le verrez ci-dessous) peuvent être fiables.

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

La façon dont le messager inter-domaines 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 cette information.

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

1
2 _;
3 }
4
5 /**********************
6 * Fonctions internes *
7 **********************/
8
9 /**
10 * Obtient le messager, généralement à partir du stockage. Cette fonction est exposée au cas où un contrat enfant
11 * aurait besoin de la remplacer.
12 * @return L'adresse du contrat de messager inter-domaines qui doit être utilisé.
13 */
14 function getCrossDomainMessenger() internal virtual returns (ICrossDomainMessenger) {
15 return ICrossDomainMessenger(messenger);
16 }
Afficher tout

Cette fonction renvoie le messager inter-domaines. Nous utilisons une fonction plutôt que la variable messenger pour permettre aux contrats qui en héritent d'utiliser un algorithme pour spécifier quel messager inter-domaines utiliser.

1
2 /**
3 * Envoie un message à un compte sur un autre domaine
4 * @param _crossDomainTarget Le destinataire prévu sur le domaine de destination
5 * @param _message Les données à envoyer à la cible (généralement des calldata vers une fonction avec
6 * `onlyFromCrossDomainAccount()`)
7 * @param _gasLimit La limite de gaz pour la réception du message sur le domaine cible.
8 */
9 function sendCrossDomainMessage(
10 address _crossDomainTarget,
11 uint32 _gasLimit,
12 bytes memory _message
Afficher tout

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

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

Slitheropens in a new tab est un analyseur statique qu'Optimism exécute sur chaque contrat pour rechercher des vulnérabilités et d'autres problèmes potentiels. Dans ce cas, la ligne suivante déclenche deux vulnérabilités :

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

Dans ce cas, nous ne nous inquiétons pas de la réentrance car nous savons que getCrossDomainMessenger() renvoie une adresse fiable, même si Slither n'a aucun moyen de le savoir.

Le contrat de pont L1

Le code source de ce contrat est iciopens in a new tab.

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

Les interfaces peuvent faire partie d'autres contrats, elles doivent donc prendre en charge une large gamme de versions de Solidity. Mais le pont lui-même est notre contrat, et nous pouvons être stricts sur la version de Solidity qu'il utilise.

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

IL1ERC20Bridge et IL1StandardBridge sont expliqués ci-dessus.

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

Cette interfaceopens in a new tab nous permet de créer des messages pour contrôler le pont standard sur la L2.

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

Cette interfaceopens in a new tab nous permet de contrôler les contrats ERC-20. Vous pouvez en savoir plus à ce sujet ici.

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

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

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

Lib_PredeployAddresses (https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts/contracts/libraries/constants/Lib_PredeployAddresses.solopens in a new tab) contient les adresses des contrats L2 qui ont toujours la même adresse. Cela inclut le pont standard sur la L2.

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

Utilitaires d'adresse d'OpenZeppelinopens in a new tab. Il est utilisé pour distinguer les adresses de contrat de celles appartenant à des comptes externes (EOA).

Notez que ce n'est pas une solution parfaite, car il n'y a aucun moyen de distinguer les appels directs des appels effectués depuis le constructeur d'un contrat, mais au moins cela nous permet d'identifier et d'éviter certaines erreurs d'utilisateur courantes.

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

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

  1. Annuler
  2. Renvoyer false

Gérer les deux cas rendrait notre code plus compliqué, nous utilisons donc SafeERC20 d'OpenZeppelinopens in a new tab, qui s'assure que tous les échecs entraînent une annulationopens in a new tab.

1/**
2 * @title L1StandardBridge
3 * @dev Le pont L1 pour ETH et ERC20 est un contrat qui stocke les fonds L1 déposés et les jetons
4 * standard qui sont utilisés sur la L2. Il synchronise un pont L2 correspondant, l'informant des dépôts
5 * et l'écoutant pour les nouveaux retraits finalisés.
6 *
7 */
8contract L1StandardBridge is IL1StandardBridge, CrossDomainEnabled {
9 using SafeERC20 for IERC20;
Afficher tout

Cette ligne est la façon dont nous spécifions d'utiliser le wrapper SafeERC20 chaque fois que nous utilisons l'interface IERC20.

1
2 /********************************
3 * Références de contrats externes *
4 ********************************/
5
6 address public l2TokenBridge;

L'adresse de L2StandardBridge.

1
2 // Mappe le jeton L1 au jeton L2 au solde du jeton L1 déposé
3 mapping(address => mapping(address => uint256)) public deposits;

Un double mappingopens in a new tab comme celui-ci est la manière de définir un tableau creux à deux dimensionsopens in a new tab. Les valeurs dans cette structure de données sont identifiées comme deposit[adresse jeton L1][adresse jeton L2]. La valeur par défaut est zéro. Seules les cellules qui sont définies à une valeur différente sont écrites dans le stockage.

1
2 /***************
3 * Constructeur *
4 ***************/
5
6 // Ce contrat vit derrière un proxy, donc les paramètres du constructeur ne seront pas utilisés.
7 constructor() CrossDomainEnabled(address(0)) {}

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

Un effet de ce modèle est que le stockage du contrat qui est l'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 * Initialisation *
3 ******************/
4
5 /**
6 * @param _l1messenger Adresse du messager L1 utilisé pour les communications inter-chaînes.
7 * @param _l2TokenBridge Adresse du pont de jetons L2.
8 */
9 // slither-disable-next-line external-function
Afficher tout

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

1 function initialize(address _l1messenger, address _l2TokenBridge) public {
2 require(messenger == address(0), "Le contrat a déjà été initialisé.");

La fonction initialize ne doit être appelée qu'une seule fois. Si l'adresse du messager inter-domaines L1 ou du pont de jetons L2 change, nous créons un nouveau proxy et un nouveau pont qui l'appelle. Il est peu probable que cela se produise, sauf lorsque l'ensemble du système est mis à niveau, un événement très rare.

Notez que cette fonction ne dispose d'aucun mécanisme qui restreint 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 du pont, puis exécuter une attaque par front-runningopens in a new tab pour atteindre la fonction initialize avant l'utilisateur légitime. Mais il existe deux méthodes pour empêcher cela :

  1. Si les contrats ne sont pas déployés directement par un EOA mais dans une transaction qui fait qu'un autre contrat les créeopens in a new tab, l'ensemble du processus peut être atomique et se terminer avant l'exécution de toute autre transaction.
  2. Si l'appel légitime à initialize échoue, il est toujours possible d'ignorer le proxy et le pont nouvellement créés et d'en créer de nouveaux.
1 messenger = _l1messenger;
2 l2TokenBridge = _l2TokenBridge;
3 }

Ce sont les deux paramètres que le pont doit connaître.

1
2 /**************
3 * Dépôt *
4 **************/
5
6 /** @dev Modificateur exigeant que l'expéditeur soit un EOA. Cette vérification pourrait être contournée par un contrat
7 * malveillant via initcode, mais elle prend en charge l'erreur utilisateur que nous voulons éviter.
8 */
9 modifier onlyEOA() {
10 // Utilisé pour arrêter les dépôts depuis des contrats (éviter la perte accidentelle de jetons)
11 require(!Address.isContract(msg.sender), "Compte non EOA");
12 _;
13 }
Afficher tout

C'est la raison pour laquelle nous avions besoin des utilitaires Address d'OpenZeppelin.

1 /**
2 * @dev Cette fonction peut être appelée sans données
3 * pour déposer un montant d'ETH sur le solde de l'appelant sur la L2.
4 * Comme la fonction de réception ne prend pas de données, un montant
5 * par défaut conservateur est transmis à la L2.
6 */
7 receive() external payable onlyEOA {
8 _initiateETHDeposit(msg.sender, msg.sender, 200_000, bytes(""));
9 }
Afficher tout

Cette fonction existe à des fins de test. Notez qu'elle n'apparaît pas dans les définitions d'interface - elle n'est pas destinée à un usage normal.

1 /**
2 * @inheritdoc IL1StandardBridge
3 */
4 function depositETH(uint32 _l2Gas, bytes calldata _data) external payable onlyEOA {
5 _initiateETHDeposit(msg.sender, msg.sender, _l2Gas, _data);
6 }
7
8 /**
9 * @inheritdoc IL1StandardBridge
10 */
11 function depositETHTo(
12 address _to,
13 uint32 _l2Gas,
14 bytes calldata _data
15 ) external payable {
16 _initiateETHDeposit(msg.sender, _to, _l2Gas, _data);
17 }
Afficher tout

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

1 /**
2 * @dev Exécute la logique pour les dépôts en stockant l'ETH et en informant la passerelle ETH L2
3 * du dépôt.
4 * @param _from Compte à partir duquel retirer le dépôt sur la L1.
5 * @param _to Compte auquel donner le dépôt sur la L2.
6 * @param _l2Gas Limite de gaz requise pour finaliser le dépôt sur la L2.
7 * @param _data Données facultatives à transmettre à la L2. Ces données sont fournies
8 * uniquement pour la commodité des contrats externes. En dehors de l'application d'une longueur
9 * maximale, ces contrats ne fournissent aucune garantie sur leur contenu.
10 */
11 function _initiateETHDeposit(
12 address _from,
13 address _to,
14 uint32 _l2Gas,
15 bytes memory _data
16 ) internal {
17 // Construire les calldata pour l'appel finalizeDeposit
18 bytes memory message = abi.encodeWithSelector(
Afficher tout

La manière dont les messages inter-domaines fonctionnent est que le contrat de destination est appelé avec le message comme calldata. Les contrats Solidity interprètent toujours leurs calldata conformément aux spécifications de l'ABIopens in a new tab. La fonction Solidity abi.encodeWithSelectoropens 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 );

Le message ici est d'appeler la fonction finalizeDepositopens in a new tab avec ces paramètres :

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

Envoyer le message via le messager inter-domaines.

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

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

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

Ces deux fonctions sont des wrappers autour de _initiateERC20Deposit, la fonction qui gère le dépôt réel d'ERC-20.

1 /**
2 * @dev Exécute la logique des dépôts en informant le contrat du jeton déposé L2
3 * du dépôt et en appelant un gestionnaire pour verrouiller les fonds L1. (par exemple, transferFrom)
4 *
5 * @param _l1Token Adresse de l'ERC20 de L1 que nous déposons
6 * @param _l2Token Adresse de l'ERC20 respectif de L2
7 * @param _from Compte à partir duquel retirer le dépôt sur la L1
8 * @param _to Compte auquel donner le dépôt sur la L2
9 * @param _amount Montant de l'ERC20 à déposer.
10 * @param _l2Gas Limite de gaz requise pour finaliser le dépôt sur la L2.
11 * @param _data Données facultatives à transmettre à la L2. Ces données sont fournies
12 * uniquement pour la commodité des contrats externes. En dehors de l'application d'une longueur
13 * maximale, ces contrats ne fournissent aucune garantie sur leur contenu.
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

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 jeton et le montant à transférer comme paramètres. Dans le cas de l'ETH, l'appel au pont inclut déjà le transfert de l'actif au compte du pont (msg.value).

1 // Lorsqu'un dépôt est initié sur la L1, le pont L1 transfère les fonds à lui-même pour de futurs
2 // retraits. safeTransferFrom vérifie également si le contrat a du code, donc cela échouera si
3 // _from est un EOA ou address(0).
4 // slither-disable-next-line reentrancy-events, reentrancy-benign
5 IERC20(_l1Token).safeTransferFrom(_from, address(this), _amount);

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

  1. L'utilisateur (_from) donne une autorisation au pont pour transférer les jetons appropriés.
  2. L'utilisateur appelle le pont avec l'adresse du contrat de jeton, le montant, etc.
  3. Le pont transfère les jetons (à lui-même) dans le cadre du processus de dépôt.

La première étape peut se produire dans une transaction distincte des deux dernières. Cependant, le 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 comme paramètre _from.

1 // Construire les calldata pour _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 // Envoyer les calldata vers la 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

Ajouter le montant de jetons déposé à la structure de données deposits. Il pourrait y avoir plusieurs adresses sur la L2 qui correspondent au même jeton ERC-20 de L1, il n'est donc pas suffisant d'utiliser le solde du pont du jeton ERC-20 de L1 pour suivre les dépôts.

1
2 // slither-disable-next-line reentrancy-events
3 emit ERC20DepositInitiated(_l1Token, _l2Token, _from, _to, _amount, _data);
4 }
5
6 /*************************
7 * Fonctions inter-chaînes *
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

Le pont L2 envoie un message au messager inter-domaines L2, ce qui amène le messager inter-domaines L1 à appeler cette fonction (une fois que la transaction qui finalise le messageopens in a new tab est soumise sur la L1, bien sûr).

1 ) external onlyFromCrossDomainAccount(l2TokenBridge) {

Assurez-vous qu'il s'agit d'un message légitime, provenant du messager inter-domaines et émanant du pont de jetons L2. Cette fonction est utilisée pour retirer de l'ETH du pont, nous devons donc 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));

La manière de transférer de l'ETH est d'appeler le destinataire avec le montant de wei dans msg.value.

1 require(success, "TransferHelper::safeTransferETH: le transfert d'ETH a échoué");
2
3 // slither-disable-next-line reentrancy-events
4 emit ETHWithdrawalFinalized(_from, _to, _amount, _data);

Émettre un événement concernant le 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

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

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

Mettre à jour la structure de données deposits.

1
2 // Lorsqu'un retrait est finalisé sur la L1, le pont L1 transfère les fonds au retireur
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 * Temporaire - Migration d'ETH *
13 *****************************/
14
15 /**
16 * @dev Ajoute un solde d'ETH au compte. Cela vise à permettre à l'ETH
17 * d'être migré d'une ancienne passerelle vers une nouvelle passerelle.
18 * NOTE : Ceci est laissé pour une seule mise à jour afin que nous puissions recevoir l'ETH migré de l'ancien
19 * contrat
20 */
21 function donateETH() external payable {}
22}
Afficher tout

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

Jetons ERC-20 sur la L2

Pour qu'un jeton ERC-20 s'intègre dans le pont standard, il doit permettre au pont standard, et uniquement au pont standard, de frapper des jetons. Ceci est nécessaire car les ponts doivent s'assurer que le nombre de jetons circulant sur Optimism est égal au nombre de jetons verrouillés dans le contrat du pont L1. S'il y a trop de jetons sur la L2, certains utilisateurs ne pourraient pas ponter leurs actifs pour les ramener sur la L1. Au lieu d'un pont de confiance, nous recréerions essentiellement un système bancaire à réserves fractionnairesopens in a new tab. S'il y a trop de jetons sur la L1, certains de ces jetons resteraient verrouillés à l'intérieur du contrat du pont pour toujours, car il n'y a aucun moyen de les libérer sans brûler des jetons L2.

IL2StandardERC20

Chaque jeton ERC-20 sur la L2 qui utilise le pont standard doit fournir cette interfaceopens in a new tab, qui contient les fonctions et les événements dont le pont standard a besoin.

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

L'interface standard ERC-20opens in a new tab n'inclut pas les fonctions mint et burn. Ces méthodes ne sont pas requises par la norme ERC-20opens in a new tab, qui ne spécifie pas les mécanismes de création et de destruction des jetons.

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

L'interface ERC-165opens in a new tab est utilisée pour spécifier les fonctions qu'un contrat fournit. Vous pouvez lire la norme iciopens in a new tab.

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

Cette fonction fournit l'adresse du jeton L1 qui est ponté vers ce contrat. Notez que nous n'avons pas de fonction similaire dans la direction opposée. Nous devons être en mesure de ponter n'importe quel jeton L1, que la prise en charge de la L2 ait été prévue ou non lors de son implémentation.

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}

Fonctions et événements pour frapper (créer) et brûler (détruire) des jetons. Le pont doit être la seule entité pouvant exécuter ces fonctions pour garantir que le nombre de jetons est correct (égal au nombre de jetons verrouillés sur la L1).

L2StandardERC20

Ceci est notre implémentation de l'interface IL2StandardERC20opens in a new tab. À moins que vous n'ayez besoin d'une 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";

Le contrat ERC-20 d'OpenZeppelinopens in a new tab. Optimism ne croit pas à la réinvention de la roue, surtout lorsque la roue est bien auditée et doit être suffisamment fiable pour détenir des actifs.

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

Ce sont les deux paramètres de configuration supplémentaires que nous exigeons et qu'un ERC-20 ne requiert normalement pas.

1
2 /**
3 * @param _l2Bridge Adresse du pont standard L2.
4 * @param _l1Token Adresse du jeton L1 correspondant.
5 * @param _name Nom ERC20.
6 * @param _symbol Symbole ERC20.
7 */
8 constructor(
9 address _l2Bridge,
10 address _l1Token,
11 string memory _name,
12 string memory _symbol
13 ) ERC20(_name, _symbol) {
14 l1Token = _l1Token;
15 l2Bridge = _l2Bridge;
16 }
Afficher tout

Appelez d'abord le constructeur pour le contrat dont nous héritons (ERC20(_name, _symbol)) puis définissez nos propres variables.

1
2 modifier onlyL2Bridge() {
3 require(msg.sender == l2Bridge, "Seul le pont L2 peut frapper et brûler");
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

C'est ainsi que l'ERC-165opens in a new tab fonctionne. Chaque interface est un certain nombre de fonctions prises en charge, et est identifiée comme le ou exclusifopens in a new tab des sélecteurs de fonction ABIopens in a new tab de ces fonctions.

Le pont L2 utilise l'ERC-165 comme une vérification de bon sens pour s'assurer que le contrat ERC-20 auquel il envoie des actifs est un IL2StandardERC20.

Note : Rien n'empêche un contrat malveillant de fournir de fausses réponses à supportsInterface, il s'agit donc d'un mécanisme de vérification de bon sens, pas d'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

Seul le pont L2 est autorisé à frapper et brûler des actifs.

_mint et _burn sont en fait définis dans le contrat ERC-20 d'OpenZeppelin. Ce contrat ne les expose tout simplement pas à l'extérieur, car les conditions pour frapper et brûler des jetons sont aussi variées que le nombre de façons d'utiliser l'ERC-20.

Code du pont L2

C'est le code qui exécute le pont sur Optimism. La source de ce contrat est iciopens 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";

L'interface IL2ERC20Bridgeopens in a new tab est très similaire à l'équivalent L1 que nous avons vu plus haut. Il y a deux différences significatives :

  1. Sur la L1, vous initiez les dépôts et finalisez les retraits. Ici, vous initiez les retraits et finalisez les dépôts.
  2. Sur la L1, il est nécessaire de distinguer l'ETH des jetons ERC-20. Sur la L2, nous pouvons utiliser les mêmes fonctions pour les deux car, en interne, les soldes d'ETH sur Optimism sont gérés comme un jeton ERC-20 avec l'adresse 0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000opens in a new tab.
1/* Library Imports */
2import { ERC165Checker } from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol";
3import { CrossDomainEnabled } from "../../libraries/bridge/CrossDomainEnabled.sol";
4import { Lib_PredeployAddresses } from "../../libraries/constants/Lib_PredeployAddresses.sol";
5
6/* Contract Imports */
7import { IL2StandardERC20 } from "../../standards/IL2StandardERC20.sol";
8
9/**
10 * @title L2StandardBridge
11 * @dev Le pont standard L2 est un contrat qui fonctionne en collaboration avec le pont standard L1 pour
12 * permettre les transitions d'ETH et d'ERC20 entre la L1 et la L2.
13 * Ce contrat agit comme un frappeur de nouveaux jetons lorsqu'il est informé de dépôts dans le pont standard L1.
14 * Ce contrat agit également comme un brûleur de jetons destinés au retrait, informant le pont L1
15 * de libérer les fonds L1.
16 */
17contract L2StandardBridge is IL2ERC20Bridge, CrossDomainEnabled {
18 /********************************
19 * Références de contrats externes *
20 ********************************/
21
22 address public l1TokenBridge;
Afficher tout

Garder une trace de l'adresse du pont L1. Notez que contrairement à l'équivalent L1, ici nous avons besoin de cette variable. L'adresse du pont L1 n'est pas connue à l'avance.

1
2 /***************
3 * Constructeur *
4 ***************/
5
6 /**
7 * @param _l2CrossDomainMessenger Messager inter-domaines utilisé par ce contrat.
8 * @param _l1TokenBridge Adresse du pont L1 déployé sur la chaîne principale.
9 */
10 constructor(address _l2CrossDomainMessenger, address _l1TokenBridge)
11 CrossDomainEnabled(_l2CrossDomainMessenger)
12 {
13 l1TokenBridge = _l1TokenBridge;
14 }
15
16 /***************
17 * Retrait *
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

Ces deux fonctions initient les retraits. Notez qu'il n'est pas nécessaire de spécifier l'adresse du jeton L1. Les jetons L2 sont censés nous indiquer l'adresse de l'équivalent L1.

1
2 /**
3 * @dev Exécute la logique des retraits en brûlant le jeton et en informant
4 * la passerelle de jetons L1 du retrait.
5 * @param _l2Token Adresse du jeton L2 où le retrait est initié.
6 * @param _from Compte à partir duquel retirer le retrait sur la L2.
7 * @param _to Compte auquel donner le retrait sur la L1.
8 * @param _amount Montant du jeton à retirer.
9 * @param _l1Gas Inutilisé, mais inclus pour des considérations de compatibilité ascendante potentielles.
10 * @param _data Données facultatives à transmettre à la L1. Ces données sont fournies
11 * uniquement pour la commodité des contrats externes. En dehors de l'application d'une longueur
12 * maximale, ces contrats ne fournissent aucune garantie sur leur contenu.
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 // Lorsqu'un retrait est initié, nous brûlons les fonds du retireur pour empêcher une utilisation ultérieure sur la L2
23 //
24 // slither-disable-next-line reentrancy-events
25 IL2StandardERC20(_l2Token).burn(msg.sender, _amount);
Afficher tout

Notez que nous ne nous fions pas au paramètre _from mais à msg.sender qui est beaucoup plus difficile à falsifier (impossible, à ma connaissance).

1
2 // Construire les calldata pour 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) {

Sur la L1, il est nécessaire de distinguer l'ETH de l'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 // Envoyer le message au pont L1
21 // slither-disable-next-line reentrancy-events
22 sendCrossDomainMessage(l1TokenBridge, _l1Gas, message);
23
24 // slither-disable-next-line reentrancy-events
25 emit WithdrawalInitiated(l1Token, _l2Token, msg.sender, _to, _amount, _data);
26 }
27
28 /************************************
29 * Fonction inter-chaînes : Dépôt *
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

Cette fonction est appelée par L1StandardBridge.

1 ) external virtual onlyFromCrossDomainAccount(l1TokenBridge) {

Assurez-vous que la source du message est légitime. C'est important car cette fonction appelle _mint et pourrait être utilisée pour donner des jetons qui ne sont pas couverts par des jetons que le pont possède sur la L1.

1 // Vérifier que le jeton cible est conforme et
2 // vérifier que le jeton déposé sur la L1 correspond à la représentation du jeton déposé sur la L2 ici
3 if (
4 // slither-disable-next-line reentrancy-events
5 ERC165Checker.supportsInterface(_l2Token, 0x1d1d8b63) &&
6 _l1Token == IL2StandardERC20(_l2Token).l1Token()

Vérifications de bon sens :

  1. L'interface correcte est prise en charge
  2. L'adresse L1 du contrat ERC-20 L2 correspond à la source L1 des jetons
1 ) {
2 // Lorsqu'un dépôt est finalisé, nous créditons le compte sur la L2 avec le même montant
3 // de jetons.
4 // slither-disable-next-line reentrancy-events
5 IL2StandardERC20(_l2Token).mint(_to, _amount);
6 // slither-disable-next-line reentrancy-events
7 emit DepositFinalized(_l1Token, _l2Token, _from, _to, _amount, _data);

Si les vérifications de bon sens sont concluantes, finalisez le dépôt :

  1. Frapper les jetons
  2. Émettre l'événement approprié
1 } else {
2 // Soit le jeton L2 dans lequel le dépôt est effectué est en désaccord sur l'adresse correcte
3 // de son jeton L1, soit il ne prend pas en charge l'interface correcte.
4 // Cela ne devrait se produire que s'il y a un jeton L2 malveillant, ou si un utilisateur a
5 // spécifié la mauvaise adresse de jeton L2 pour le dépôt.
6 // Dans tous les cas, nous arrêtons le processus ici et construisons un message de retrait
7 // afin que les utilisateurs puissent récupérer leurs fonds dans certains cas.
8 // Il n'y a aucun moyen d'empêcher complètement les contrats de jetons malveillants, mais cela limite
9 // les erreurs des utilisateurs et atténue certaines formes de comportement de contrat malveillant.
Afficher tout

Si un utilisateur a commis une erreur détectable en utilisant la mauvaise adresse de jeton L2, nous voulons annuler le dépôt et retourner les jetons sur la L1. La seule façon de le faire depuis la L2 est d'envoyer un message qui devra attendre la période de contestation des erreurs, mais c'est bien mieux pour l'utilisateur que de perdre les jetons de manière permanente.

1 bytes memory message = abi.encodeWithSelector(
2 IL1ERC20Bridge.finalizeERC20Withdrawal.selector,
3 _l1Token,
4 _l2Token,
5 _to, // a inversé le _to et le _from ici pour renvoyer le dépôt à l'expéditeur
6 _from,
7 _amount,
8 _data
9 );
10
11 // Envoyer le message au pont L1
12 // slither-disable-next-line reentrancy-events
13 sendCrossDomainMessage(l1TokenBridge, 0, message);
14 // slither-disable-next-line reentrancy-events
15 emit DepositFailed(_l1Token, _l2Token, _from, _to, _amount, _data);
16 }
17 }
18}
Afficher tout

Conclusion

Le pont standard est le mécanisme le plus flexible 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. En particulier pour les retraits, la plupart des utilisateurs préfèrent utiliser des ponts tiersopens in a new tab qui n'attendent pas la période de contestation et ne nécessitent pas de preuve de Merkle pour finaliser le retrait.

Ces ponts fonctionnent généralement en ayant des actifs sur la L1, qu'ils fournissent immédiatement moyennant de faibles frais (souvent inférieurs au coût du gaz pour un retrait via un pont standard). Lorsque le pont (ou les personnes qui le gèrent) anticipe un manque d'actifs sur la L1, il transfère des actifs suffisants depuis la L2. Comme il s'agit de retraits très importants, le coût du retrait est amorti sur un montant élevé et représente un pourcentage beaucoup plus faible.

Espérons que cet article vous a aidé à mieux comprendre le fonctionnement de la couche 2, et comment écrire du code Solidity clair et sécurisé.

Voir ici pour plus de mon travailopens in a new tab.

Dernière mise à jour de la page : 22 octobre 2025

Ce tutoriel vous a été utile ?