Présentation du contrat de pont standard d'Optimism
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
- 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é
- Le déposant appelle le pont L1 (
depositERC20,depositERC20To,depositETHoudepositETHTo) - 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
- Le pont L1 utilise le mécanisme de message inter-domaines pour appeler
finalizeDepositsur le pont L2
Couche 2
- Le pont L2 vérifie que l'appel à
finalizeDepositest légitime :- Provenant du contrat de message inter-domaines
- Était initialement du pont sur la L1
- 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).
- 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
- Le retireur appelle le pont L2 (
withdrawouwithdrawTo) - Le pont L2 brûle le nombre approprié de jetons appartenant à
msg.sender - Le pont L2 utilise le mécanisme de message inter-domaines pour appeler
finalizeETHWithdrawaloufinalizeERC20Withdrawalsur le pont L1
Couche 1
- Le pont L1 vérifie que l'appel à
finalizeETHWithdrawaloufinalizeERC20Withdrawalest légitime :- Provenant du mécanisme de message inter-domaines
- Était initialement du pont sur la L2
- 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: MITLa 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 IL1ERC20Bridge3 */4interface IL1ERC20Bridge {5 /**********6 * Événements *7 **********/89 event ERC20DepositInitiated(Afficher toutDans 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 _data5 );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 _data8 );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.
12 /********************3 * Fonctions publiques *4 ********************/56 /**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 toutCette 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éposons4 * @param _l2Token Adresse de l'ERC20 respectif de L25 * @param _amount Montant de l'ERC20 à déposer6 * @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 fournies8 * uniquement pour la commodité des contrats externes. En dehors de l'application d'une longueur9 * 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 _data17 ) external;Afficher toutLe 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éposons4 * @param _l2Token Adresse de l'ERC20 respectif de L25 * @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 fournies9 * uniquement pour la commodité des contrats externes. En dehors de l'application d'une longueur10 * 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 _data19 ) external;Afficher toutCette 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 *************************/45 /**6 * @dev Finalise un retrait de la L2 vers la L1, et crédite les fonds sur le solde du destinataire du7 * 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 fournies16 * uniquement pour la commodité des contrats externes. En dehors de l'application d'une longueur17 * 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 _data26 ) external;27}Afficher toutLes retraits (et autres messages de la L2 à la L1) dans Optimism sont un processus en deux étapes :
- Une transaction d'initiation sur la L2.
- 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: MIT2pragma solidity >0.5.0 <0.9.0;34import "./IL1ERC20Bridge.sol";56/**7 * @title IL1StandardBridge8 */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 _data18 );Afficher toutCet é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 );67 /********************8 * Fonctions publiques *9 ********************/1011 /**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;1819 /**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 _data29 ) external payable;3031 /*************************32 * Fonctions inter-chaînes *33 *************************/3435 /**36 * @dev Finalise un retrait de la L2 vers la L1, et crédite les fonds sur le solde du destinataire du37 * jeton ETH de L1. Étant donné que seul le xDomainMessenger peut appeler cette fonction, elle ne sera jamais appelée38 * 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 _data48 ) external;49}Afficher toutCrossDomainEnabled
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: MIT2pragma solidity >0.5.0 <0.9.0;34/* 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 CrossDomainEnabled3 * @dev Contrat d'aide pour les contrats effectuant des communications inter-domaines4 *5 * Compilateur utilisé : défini par le contrat héritier6 */7contract CrossDomainEnabled {8 /*************9 * Variables *10 *************/1112 // Contrat de messagerie utilisé pour envoyer et recevoir des messages de l'autre domaine.13 address public messenger;1415 /***************16 * Constructeur *17 ***************/1819 /**20 * @param _messenger Adresse du CrossDomainMessenger sur la couche actuelle.21 */22 constructor(address _messenger) {23 messenger = _messenger;24 }Afficher toutLe 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.
12 /**********************3 * Modificateurs de fonction *4 **********************/56 /**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 est9 * authentifié pour appeler cette fonction.10 */11 modifier onlyFromCrossDomainAccount(address _sourceDomainAccount) {Afficher toutLa 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.
12 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.
12 _;3 }45 /**********************6 * Fonctions internes *7 **********************/89 /**10 * Obtient le messager, généralement à partir du stockage. Cette fonction est exposée au cas où un contrat enfant11 * 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 toutCette 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.
12 /**3 * Envoie un message à un compte sur un autre domaine4 * @param _crossDomainTarget Le destinataire prévu sur le domaine de destination5 * @param _message Les données à envoyer à la cible (généralement des calldata vers une fonction avec6 * `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 _messageAfficher toutEnfin, la fonction qui envoie un message à l'autre couche.
1 ) internal {2 // slither-disable-next-line reentrancy-events, reentrancy-benignSlitheropens 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 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: MIT2pragma 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 :
- Annuler
- 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 L1StandardBridge3 * @dev Le pont L1 pour ETH et ERC20 est un contrat qui stocke les fonds L1 déposés et les jetons4 * standard qui sont utilisés sur la L2. Il synchronise un pont L2 correspondant, l'informant des dépôts5 * et l'écoutant pour les nouveaux retraits finalisés.6 *7 */8contract L1StandardBridge is IL1StandardBridge, CrossDomainEnabled {9 using SafeERC20 for IERC20;Afficher toutCette ligne est la façon dont nous spécifions d'utiliser le wrapper SafeERC20 chaque fois que nous utilisons l'interface IERC20.
12 /********************************3 * Références de contrats externes *4 ********************************/56 address public l2TokenBridge;L'adresse de L2StandardBridge.
12 // 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.
12 /***************3 * Constructeur *4 ***************/56 // 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 ******************/45 /**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-functionAfficher toutCe 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 :
- 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.
- 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.
12 /**************3 * Dépôt *4 **************/56 /** @dev Modificateur exigeant que l'expéditeur soit un EOA. Cette vérification pourrait être contournée par un contrat7 * 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 toutC'est la raison pour laquelle nous avions besoin des utilitaires Address d'OpenZeppelin.
1 /**2 * @dev Cette fonction peut être appelée sans données3 * 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 montant5 * 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 toutCette 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 IL1StandardBridge3 */4 function depositETH(uint32 _l2Gas, bytes calldata _data) external payable onlyEOA {5 _initiateETHDeposit(msg.sender, msg.sender, _l2Gas, _data);6 }78 /**9 * @inheritdoc IL1StandardBridge10 */11 function depositETHTo(12 address _to,13 uint32 _l2Gas,14 bytes calldata _data15 ) external payable {16 _initiateETHDeposit(msg.sender, _to, _l2Gas, _data);17 }Afficher toutCes 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 L23 * 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 fournies8 * uniquement pour la commodité des contrats externes. En dehors de l'application d'une longueur9 * 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 _data16 ) internal {17 // Construire les calldata pour l'appel finalizeDeposit18 bytes memory message = abi.encodeWithSelector(Afficher toutLa 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 _data8 );Le message ici est d'appeler la fonction finalizeDepositopens in a new tab avec ces paramètres :
| Paramètre | Valeur | Signification |
|---|---|---|
| _l1Token | adresse(0) | Valeur spéciale pour représenter l'ETH (qui n'est pas un jeton ERC-20) sur la L1 |
| _l2Token | Lib_PredeployAddresses.OVM_ETH | Le contrat L2 qui gère l'ETH sur Optimism, 0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000 (ce contrat est destiné à un usage interne à Optimism uniquement) |
| _from | _from | L'adresse sur la L1 qui envoie l'ETH |
| _to | _to | L'adresse sur la L2 qui reçoit l'ETH |
| montant | msg.value | Montant de wei envoyé (qui a déjà été envoyé au pont) |
| _data | _data | Données supplémentaires à joindre au dépôt |
1 // Envoyer les calldata vers la L22 // slither-disable-next-line reentrancy-events3 sendCrossDomainMessage(l2TokenBridge, _l2Gas, message);Envoyer le message via le messager inter-domaines.
1 // slither-disable-next-line reentrancy-events2 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 IL1ERC20Bridge3 */4 function depositERC20(5 .6 .7 .8 ) external virtual onlyEOA {9 _initiateERC20Deposit(_l1Token, _l2Token, msg.sender, msg.sender, _amount, _l2Gas, _data);10 }1112 /**13 * @inheritdoc IL1ERC20Bridge14 */15 function depositERC20To(16 .17 .18 .19 ) external virtual {20 _initiateERC20Deposit(_l1Token, _l2Token, msg.sender, _to, _amount, _l2Gas, _data);21 }Afficher toutCes 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é L23 * 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éposons6 * @param _l2Token Adresse de l'ERC20 respectif de L27 * @param _from Compte à partir duquel retirer le dépôt sur la L18 * @param _to Compte auquel donner le dépôt sur la L29 * @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 fournies12 * uniquement pour la commodité des contrats externes. En dehors de l'application d'une longueur13 * 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 _data23 ) internal {Afficher toutCette 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 futurs2 // retraits. safeTransferFrom vérifie également si le contrat a du code, donc cela échouera si3 // _from est un EOA ou address(0).4 // slither-disable-next-line reentrancy-events, reentrancy-benign5 IERC20(_l1Token).safeTransferFrom(_from, address(this), _amount);Les transferts de jetons ERC-20 suivent un processus différent de celui de l'ETH :
- L'utilisateur (
_from) donne une autorisation au pont pour transférer les jetons appropriés. - L'utilisateur appelle le pont avec l'adresse du contrat de jeton, le montant, etc.
- 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 _data10 );1112 // Envoyer les calldata vers la L213 // slither-disable-next-line reentrancy-events, reentrancy-benign14 sendCrossDomainMessage(l2TokenBridge, _l2Gas, message);1516 // slither-disable-next-line reentrancy-benign17 deposits[_l1Token][_l2Token] = deposits[_l1Token][_l2Token] + _amount;Afficher toutAjouter 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.
12 // slither-disable-next-line reentrancy-events3 emit ERC20DepositInitiated(_l1Token, _l2Token, _from, _to, _amount, _data);4 }56 /*************************7 * Fonctions inter-chaînes *8 *************************/910 /**11 * @inheritdoc IL1StandardBridge12 */13 function finalizeETHWithdrawal(14 address _from,15 address _to,16 uint256 _amount,17 bytes calldata _dataAfficher toutLe 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-events2 (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é");23 // slither-disable-next-line reentrancy-events4 emit ETHWithdrawalFinalized(_from, _to, _amount, _data);Émettre un événement concernant le retrait.
1 }23 /**4 * @inheritdoc IL1ERC20Bridge5 */6 function finalizeERC20Withdrawal(7 address _l1Token,8 address _l2Token,9 address _from,10 address _to,11 uint256 _amount,12 bytes calldata _data13 ) external onlyFromCrossDomainAccount(l2TokenBridge) {Afficher toutCette 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.
12 // Lorsqu'un retrait est finalisé sur la L1, le pont L1 transfère les fonds au retireur3 // slither-disable-next-line reentrancy-events4 IERC20(_l1Token).safeTransfer(_to, _amount);56 // slither-disable-next-line reentrancy-events7 emit ERC20WithdrawalFinalized(_l1Token, _l2Token, _from, _to, _amount, _data);8 }91011 /*****************************12 * Temporaire - Migration d'ETH *13 *****************************/1415 /**16 * @dev Ajoute un solde d'ETH au compte. Cela vise à permettre à l'ETH17 * 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'ancien19 * contrat20 */21 function donateETH() external payable {}22}Afficher toutIl 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: MIT2pragma solidity ^0.8.9;34import { 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.
12 function mint(address _to, uint256 _amount) external;34 function burn(address _from, uint256 _amount) external;56 event Mint(address indexed _account, uint256 _amount);7 event Burn(address indexed _account, uint256 _amount);8}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: MIT2pragma solidity ^0.8.9;34import { 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";23contract 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.
12 /**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 _symbol13 ) ERC20(_name, _symbol) {14 l1Token = _l1Token;15 l2Bridge = _l2Bridge;16 }Afficher toutAppelez d'abord le constructeur pour le contrat dont nous héritons (ERC20(_name, _symbol)) puis définissez nos propres variables.
12 modifier onlyL2Bridge() {3 require(msg.sender == l2Bridge, "Seul le pont L2 peut frapper et brûler");4 _;5 }678 // slither-disable-next-line external-function9 function supportsInterface(bytes4 _interfaceId) public pure returns (bool) {10 bytes4 firstSupportedInterface = bytes4(keccak256("supportsInterface(bytes4)")); // ERC16511 bytes4 secondSupportedInterface = IL2StandardERC20.l1Token.selector ^12 IL2StandardERC20.mint.selector ^13 IL2StandardERC20.burn.selector;14 return _interfaceId == firstSupportedInterface || _interfaceId == secondSupportedInterface;15 }Afficher toutC'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-function2 function mint(address _to, uint256 _amount) public virtual onlyL2Bridge {3 _mint(_to, _amount);45 emit Mint(_to, _amount);6 }78 // slither-disable-next-line external-function9 function burn(address _from, uint256 _amount) public virtual onlyL2Bridge {10 _burn(_from, _amount);1112 emit Burn(_from, _amount);13 }14}Afficher toutSeul 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: MIT2pragma solidity ^0.8.9;34/* Interface Imports */5import { IL1StandardBridge } from "../../L1/messaging/IL1StandardBridge.sol";6import { IL1ERC20Bridge } from "../../L1/messaging/IL1ERC20Bridge.sol";7import { IL2ERC20Bridge } from "./IL2ERC20Bridge.sol";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 :
- Sur la L1, vous initiez les dépôts et finalisez les retraits. Ici, vous initiez les retraits et finalisez les dépôts.
- 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";56/* Contract Imports */7import { IL2StandardERC20 } from "../../standards/IL2StandardERC20.sol";89/**10 * @title L2StandardBridge11 * @dev Le pont standard L2 est un contrat qui fonctionne en collaboration avec le pont standard L1 pour12 * 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 L115 * de libérer les fonds L1.16 */17contract L2StandardBridge is IL2ERC20Bridge, CrossDomainEnabled {18 /********************************19 * Références de contrats externes *20 ********************************/2122 address public l1TokenBridge;Afficher toutGarder 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.
12 /***************3 * Constructeur *4 ***************/56 /**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 }1516 /***************17 * Retrait *18 ***************/1920 /**21 * @inheritdoc IL2ERC20Bridge22 */23 function withdraw(24 address _l2Token,25 uint256 _amount,26 uint32 _l1Gas,27 bytes calldata _data28 ) external virtual {29 _initiateWithdrawal(_l2Token, msg.sender, msg.sender, _amount, _l1Gas, _data);30 }3132 /**33 * @inheritdoc IL2ERC20Bridge34 */35 function withdrawTo(36 address _l2Token,37 address _to,38 uint256 _amount,39 uint32 _l1Gas,40 bytes calldata _data41 ) external virtual {42 _initiateWithdrawal(_l2Token, msg.sender, _to, _amount, _l1Gas, _data);43 }Afficher toutCes 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.
12 /**3 * @dev Exécute la logique des retraits en brûlant le jeton et en informant4 * 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 fournies11 * uniquement pour la commodité des contrats externes. En dehors de l'application d'une longueur12 * 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 _data21 ) internal {22 // Lorsqu'un retrait est initié, nous brûlons les fonds du retireur pour empêcher une utilisation ultérieure sur la L223 //24 // slither-disable-next-line reentrancy-events25 IL2StandardERC20(_l2Token).burn(msg.sender, _amount);Afficher toutNotez que nous ne nous fions pas au paramètre _from mais à msg.sender qui est beaucoup plus difficile à falsifier (impossible, à ma connaissance).
12 // Construire les calldata pour l1TokenBridge.finalizeERC20Withdrawal(_to, _amount)3 // slither-disable-next-line reentrancy-events4 address l1Token = IL2StandardERC20(_l2Token).l1Token();5 bytes memory message;67 if (_l2Token == Lib_PredeployAddresses.OVM_ETH) {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 _data7 );8 } else {9 message = abi.encodeWithSelector(10 IL1ERC20Bridge.finalizeERC20Withdrawal.selector,11 l1Token,12 _l2Token,13 _from,14 _to,15 _amount,16 _data17 );18 }1920 // Envoyer le message au pont L121 // slither-disable-next-line reentrancy-events22 sendCrossDomainMessage(l1TokenBridge, _l1Gas, message);2324 // slither-disable-next-line reentrancy-events25 emit WithdrawalInitiated(l1Token, _l2Token, msg.sender, _to, _amount, _data);26 }2728 /************************************29 * Fonction inter-chaînes : Dépôt *30 ************************************/3132 /**33 * @inheritdoc IL2ERC20Bridge34 */35 function finalizeDeposit(36 address _l1Token,37 address _l2Token,38 address _from,39 address _to,40 uint256 _amount,41 bytes calldata _dataAfficher toutCette 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 et2 // vérifier que le jeton déposé sur la L1 correspond à la représentation du jeton déposé sur la L2 ici3 if (4 // slither-disable-next-line reentrancy-events5 ERC165Checker.supportsInterface(_l2Token, 0x1d1d8b63) &&6 _l1Token == IL2StandardERC20(_l2Token).l1Token()Vérifications de bon sens :
- L'interface correcte est prise en charge
- 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 montant3 // de jetons.4 // slither-disable-next-line reentrancy-events5 IL2StandardERC20(_l2Token).mint(_to, _amount);6 // slither-disable-next-line reentrancy-events7 emit DepositFinalized(_l1Token, _l2Token, _from, _to, _amount, _data);Si les vérifications de bon sens sont concluantes, finalisez le dépôt :
- Frapper les jetons
- É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 correcte3 // 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 a5 // 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 retrait7 // 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 limite9 // les erreurs des utilisateurs et atténue certaines formes de comportement de contrat malveillant.Afficher toutSi 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éditeur6 _from,7 _amount,8 _data9 );1011 // Envoyer le message au pont L112 // slither-disable-next-line reentrancy-events13 sendCrossDomainMessage(l1TokenBridge, 0, message);14 // slither-disable-next-line reentrancy-events15 emit DepositFailed(_l1Token, _l2Token, _from, _to, _amount, _data);16 }17 }18}Afficher toutConclusion
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