Panoramica del contratto del ponte standard di Optimism
Optimism (opens in a new tab) è un rollup ottimistico. I rollup ottimistici possono elaborare le transazioni a un prezzo molto inferiore rispetto alla rete principale di Ethereum (nota anche come livello 1 o L1) perché le transazioni vengono elaborate solo da pochi nodi, invece che da ogni nodo della rete. Allo stesso tempo, i dati vengono tutti scritti su L1, in modo che tutto possa essere provato e ricostruito con tutte le garanzie di integrità e disponibilità della rete principale.
Per utilizzare gli asset di L1 su Optimism (o su qualsiasi altro L2), gli asset devono essere trasferiti tramite un ponte. Un modo per ottenere questo risultato è che gli utenti blocchino gli asset (ETH e i token ERC-20 sono i più comuni) su L1 e ricevano asset equivalenti da utilizzare su L2. Alla fine, chiunque ne entri in possesso potrebbe volerli riportare su L1 tramite il ponte. Quando si fa questo, gli asset vengono bruciati su L2 e poi rilasciati nuovamente all'utente su L1.
Questo è il modo in cui funziona il ponte standard di Optimism (opens in a new tab). In questo articolo esaminiamo il codice sorgente di quel ponte per vedere come funziona e studiarlo come esempio di codice Solidity ben scritto.
Flussi di controllo
Il ponte ha due flussi principali:
- Deposito (da L1 a L2)
- Prelievo (da L2 a L1)
Flusso di deposito
Livello 1
- Se si deposita un ERC-20, il depositante concede al ponte un'indennità (allowance) per spendere l'importo depositato
- Il depositante chiama il ponte di L1 (
depositERC20,depositERC20To,depositETHodepositETHTo) - Il ponte di L1 prende possesso dell'asset trasferito
- ETH: L'asset viene trasferito dal depositante come parte della chiamata
- ERC-20: L'asset viene trasferito dal ponte a se stesso utilizzando l'indennità fornita dal depositante
- Il ponte di L1 utilizza il meccanismo di messaggistica tra domini per chiamare
finalizeDepositsul ponte di L2
Livello 2
- Il ponte di L2 verifica che la chiamata a
finalizeDepositsia legittima:- Proviene dal contratto di messaggistica tra domini
- Proviene originariamente dal ponte su L1
- Il ponte di L2 controlla se il contratto del token ERC-20 su L2 è quello corretto:
- Il contratto di L2 segnala che la sua controparte di L1 è la stessa da cui provengono i token su L1
- Il contratto di L2 segnala che supporta l'interfaccia corretta (utilizzando ERC-165 (opens in a new tab)).
- Se il contratto di L2 è quello corretto, lo chiama per coniare il numero appropriato di token all'indirizzo appropriato. In caso contrario, avvia un processo di prelievo per consentire all'utente di reclamare i token su L1.
Flusso di prelievo
Livello 2
- Chi preleva chiama il ponte di L2 (
withdrawowithdrawTo) - Il ponte di L2 brucia il numero appropriato di token appartenenti a
msg.sender - Il ponte di L2 utilizza il meccanismo di messaggistica tra domini per chiamare
finalizeETHWithdrawalofinalizeERC20Withdrawalsul ponte di L1
Livello 1
- Il ponte di L1 verifica che la chiamata a
finalizeETHWithdrawalofinalizeERC20Withdrawalsia legittima:- Proviene dal meccanismo di messaggistica tra domini
- Proviene originariamente dal ponte su L2
- Il ponte di L1 trasferisce l'asset appropriato (ETH o ERC-20) all'indirizzo appropriato
Codice di Livello 1
Questo è il codice che viene eseguito su L1, la rete principale di Ethereum.
IL1ERC20Bridge
Questa interfaccia è definita qui (opens in a new tab). Include funzioni e definizioni necessarie per trasferire tramite ponte i token ERC-20.
1// SPDX-License-Identifier: MITLa maggior parte del codice di Optimism è rilasciata sotto licenza MIT (opens in a new tab).
1pragma solidity >0.5.0 <0.9.0;Al momento della stesura, l'ultima versione di Solidity è la 0.8.12. Fino al rilascio della versione 0.9.0, non sappiamo se questo codice sarà compatibile o meno.
1/* *2 * @title IL1ERC20Bridge */3
4
5
6interface IL1ERC20Bridge {7 /* *********8 * Eventi *9 ********* */10 11
12
13
14 event ERC20DepositInitiated(Nella terminologia del ponte di Optimism, deposito significa trasferimento da L1 a L2, e prelievo significa un trasferimento da L2 a L1.
1 address indexed _l1Token,2 address indexed _l2Token,Nella maggior parte dei casi l'indirizzo di un ERC-20 su L1 non è lo stesso dell'indirizzo dell'ERC-20 equivalente su L2.
Puoi vedere l'elenco degli indirizzi dei token qui (opens in a new tab).
L'indirizzo con chainId 1 è su L1 (rete principale) e l'indirizzo con chainId 10 è su L2 (Optimism).
Gli altri due valori di chainId sono per la rete di test Kovan (42) e la rete di test Optimistic Kovan (69).
1 address indexed _from,2 address _to,3 uint256 _amount,4 bytes _data5 );È possibile aggiungere note ai trasferimenti, nel qual caso vengono aggiunte agli eventi che li segnalano.
1 event ERC20WithdrawalFinalized(2 address indexed _l1Token,3 address indexed _l2Token,4 address indexed _from,5 address _to,6 uint256 _amount,7 bytes _data8 );Lo stesso contratto del ponte gestisce i trasferimenti in entrambe le direzioni. Nel caso del ponte di L1, questo significa l'inizializzazione dei depositi e la finalizzazione dei prelievi.
1
2 /* *******************3 * Funzioni Pubbliche *4 ******************* */5 6
7
8
9 /* *10 * @dev ottiene l'indirizzo del contratto ponte L2 corrispondente.11 * @return Indirizzo del contratto ponte L2 corrispondente. */12 13
14
15
16 function l2TokenBridge() external returns (address);Questa funzione non è realmente necessaria, perché su L2 è un contratto pre-distribuito, quindi si trova sempre all'indirizzo 0x4200000000000000000000000000000000000010.
È qui per simmetria con il ponte di L2, perché l'indirizzo del ponte di L1 non è banale da conoscere.
1 /* *2 * @dev deposita un importo di ERC20 sul saldo del chiamante su L2.3 * @param _l1Token Indirizzo dell'ERC20 L1 che stiamo depositando4 * @param _l2Token Indirizzo del rispettivo ERC20 L2 di L15 * @param _amount Importo di ERC20 da depositare6 * @param _l2Gas Limite del gas richiesto per completare il deposito su L2.7 * @param _data Dati opzionali da inoltrare a L2. Questi dati sono forniti8 * esclusivamente per comodità dei contratti esterni. A parte l'imposizione di una9 * lunghezza massima, questi contratti non forniscono alcuna garanzia sul loro contenuto. */10 11
12
13
14
15
16
17
18
19
20 function depositERC20(21 address _l1Token,22 address _l2Token,23 uint256 _amount,24 uint32 _l2Gas,25 bytes calldata _data26 ) external;Il parametro _l2Gas è la quantità di gas di L2 che la transazione è autorizzata a spendere.
Fino a un certo limite (elevato), questo è gratuito (opens in a new tab), quindi a meno che il contratto ERC-20 non faccia qualcosa di veramente strano durante il conio, non dovrebbe essere un problema.
Questa funzione si occupa dello scenario comune, in cui un utente trasferisce tramite ponte gli asset allo stesso indirizzo su una blockchain diversa.
1 /* *2 * @dev deposita un importo di ERC20 sul saldo di un destinatario su L2.3 * @param _l1Token Indirizzo dell'ERC20 L1 che stiamo depositando4 * @param _l2Token Indirizzo del rispettivo ERC20 L2 di L15 * @param _to Indirizzo L2 a cui accreditare il prelievo.6 * @param _amount Importo di ERC20 da depositare.7 * @param _l2Gas Limite del gas richiesto per completare il deposito su L2.8 * @param _data Dati opzionali da inoltrare a L2. Questi dati sono forniti9 * esclusivamente per comodità dei contratti esterni. A parte l'imposizione di una10 * lunghezza massima, questi contratti non forniscono alcuna garanzia sul loro contenuto. */11 12
13
14
15
16
17
18
19
20
21
22 function depositERC20To(23 address _l1Token,24 address _l2Token,25 address _to,26 uint256 _amount,27 uint32 _l2Gas,28 bytes calldata _data29 ) external;Questa funzione è quasi identica a depositERC20, ma ti consente di inviare l'ERC-20 a un indirizzo diverso.
1 /* ************************2 * Funzioni Cross-chain *3 ************************ */4 5
6
7
8 /* *9 * @dev Completa un prelievo da L2 a L1 e accredita i fondi sul saldo del destinatario del10 * token ERC20 L1.11 * Questa chiamata fallirà se il prelievo inizializzato da L2 non è stato finalizzato.12 *13 * @param _l1Token Indirizzo del token L1 per cui eseguire finalizeWithdrawal.14 * @param _l2Token Indirizzo del token L2 in cui è stato avviato il prelievo.15 * @param _from Indirizzo L2 che avvia il trasferimento.16 * @param _to Indirizzo L1 a cui accreditare il prelievo.17 * @param _amount Importo di ERC20 da depositare.18 * @param _data Dati forniti dal mittente su L2. Questi dati sono forniti19 * esclusivamente per comodità dei contratti esterni. A parte l'imposizione di una20 * lunghezza massima, questi contratti non forniscono alcuna garanzia sul loro contenuto. */21 22
23
24
25
26
27
28
29
30
31
32
33
34
35 function finalizeERC20Withdrawal(36 address _l1Token,37 address _l2Token,38 address _from,39 address _to,40 uint256 _amount,41 bytes calldata _data42 ) external;43}I prelievi (e altri messaggi da L2 a L1) in Optimism sono un processo in due fasi:
- Una transazione di avvio su L2.
- Una transazione di finalizzazione o reclamo su L1. Questa transazione deve avvenire dopo la fine del periodo di contestazione dei guasti (opens in a new tab) per la transazione di L2.
IL1StandardBridge
Questa interfaccia è definita qui (opens in a new tab).
Questo file contiene le definizioni di eventi e funzioni per gli ETH.
Queste definizioni sono molto simili a quelle definite in IL1ERC20Bridge sopra per gli ERC-20.
L'interfaccia del ponte è divisa in due file perché alcuni token ERC-20 richiedono un'elaborazione personalizzata e non possono essere gestiti dal ponte standard.
In questo modo il ponte personalizzato che gestisce tale token può implementare IL1ERC20Bridge e non dover trasferire anche gli ETH.
1// SPDX-License-Identifier: MIT2pragma solidity >0.5.0 <0.9.0;3
4import "./IL1ERC20Bridge.sol";5
6/* *7 * @title IL1StandardBridge */8
9
10
11interface IL1StandardBridge is IL1ERC20Bridge {12 /* *********13 * Eventi *14 ********* */15 16
17
18 event ETHDepositInitiated(19 address indexed _from,20 address indexed _to,21 uint256 _amount,22 bytes _data23 );Questo evento è quasi identico alla versione ERC-20 (ERC20DepositInitiated), tranne per l'assenza degli indirizzi dei token di L1 e L2.
Lo stesso vale per gli altri eventi e le funzioni.
1 event ETHWithdrawalFinalized(2 .3 .4 .5 );6
7 /* *******************8 * Funzioni Pubbliche *9 ******************* */10 11
12
13
14 /* *15 * @dev Deposita un importo di ETH sul saldo del chiamante su L2.16 .17 .18 . */19 20
21
22
23
24
25 function depositETH(uint32 _l2Gas, bytes calldata _data) external payable;26
27 /* *28 * @dev Deposita un importo di ETH sul saldo di un destinatario su L2.29 .30 .31 . */32 33
34
35
36
37
38 function depositETHTo(39 address _to,40 uint32 _l2Gas,41 bytes calldata _data42 ) external payable;43
44 /* ************************45 * Funzioni Cross-chain *46 ************************ */47 48
49
50
51 /* *52 * @dev Completa un prelievo da L2 a L1 e accredita i fondi sul saldo del destinatario del53 * token ETH L1. Poiché solo xDomainMessenger può chiamare questa funzione, non verrà mai chiamata54 * prima che il prelievo sia finalizzato.55 .56 .57 . */58 59
60
61
62
63
64
65
66 function finalizeETHWithdrawal(67 address _from,68 address _to,69 uint256 _amount,70 bytes calldata _data71 ) external;72}CrossDomainEnabled
Questo contratto (opens in a new tab) viene ereditato da entrambi i ponti (L1 e L2) per inviare messaggi all'altro livello.
1// SPDX-License-Identifier: MIT2pragma solidity >0.5.0 <0.9.0;3
4/* Importazioni di Interfacce */5/* Interface Imports */6import { ICrossDomainMessenger } from "./ICrossDomainMessenger.sol";Questa interfaccia (opens in a new tab) indica al contratto come inviare messaggi all'altro livello, utilizzando il messaggero tra domini. Questo messaggero tra domini è un sistema completamente diverso e merita un articolo a sé, che spero di scrivere in futuro.
1/* *2 * @title CrossDomainEnabled3 * @dev Contratto di supporto per i contratti che eseguono comunicazioni cross-domain4 *5 * Compilatore utilizzato: definito dal contratto che eredita */6
7
8
9
10
11
12contract CrossDomainEnabled {13 /* ************14 * Variabili *15 ************ */16 17
18
19
20 // Contratto Messenger utilizzato per inviare e ricevere messaggi dall'altro dominio.21 address public messenger;22
23 /* **************24 * Costruttore *25 ************** */26 27
28
29
30 /* *31 * @param _messenger Indirizzo del CrossDomainMessenger sul livello corrente. */32 33
34
35 constructor(address _messenger) {36 messenger = _messenger;37 }L'unico parametro che il contratto deve conoscere è l'indirizzo del messaggero tra domini su questo livello. Questo parametro viene impostato una volta, nel costruttore, e non cambia mai.
1
2 /* *********************3 * Modificatori di Funzione *4 ********************* */5 6
7
8
9 /* *10 * Impone che la funzione modificata sia chiamabile solo da uno specifico account cross-domain.11 * @param _sourceDomainAccount L'unico account sul dominio di origine che è12 * autenticato per chiamare questa funzione. */13 14
15
16
17
18 modifier onlyFromCrossDomainAccount(address _sourceDomainAccount) {La messaggistica tra domini è accessibile da qualsiasi contratto sulla blockchain in cui è in esecuzione (sia la rete principale di Ethereum che Optimism). Ma abbiamo bisogno che il ponte su ciascun lato si fidi solo di determinati messaggi se provengono dal ponte sull'altro lato.
1 require(2 msg.sender == address(getCrossDomainMessenger()),3 "OVM_XCHAIN: messenger contract unauthenticated"4 );Ci si può fidare solo dei messaggi provenienti dal messaggero tra domini appropriato (messenger, come vedi di seguito).
1
2 require(3 getCrossDomainMessenger().xDomainMessageSender() == _sourceDomainAccount,4 "OVM_XCHAIN: wrong sender of cross-domain message"5 );Il modo in cui il messaggero tra domini fornisce l'indirizzo che ha inviato un messaggio con l'altro livello è la funzione .xDomainMessageSender() (opens in a new tab).
Finché viene chiamata nella transazione avviata dal messaggio, può fornire queste informazioni.
Dobbiamo assicurarci che il messaggio ricevuto provenga dall'altro ponte.
1
2 _;3 }4
5 /* *********************6 * Funzioni Interne *7 ********************* */8 9
10
11
12 /* *13 * Ottiene il messenger, di solito dallo storage. Questa funzione è esposta nel caso in cui un contratto figlio14 * debba sovrascriverla.15 * @return L'indirizzo del contratto messenger cross-domain che dovrebbe essere utilizzato. */16 17
18
19
20
21 function getCrossDomainMessenger() internal virtual returns (ICrossDomainMessenger) {22 return ICrossDomainMessenger(messenger);23 }Questa funzione restituisce il messaggero tra domini.
Utilizziamo una funzione anziché la variabile messenger per consentire ai contratti che ereditano da questo di utilizzare un algoritmo per specificare quale messaggero tra domini utilizzare.
1
2 /* *3 * Invia un messaggio a un account su un altro dominio4 * @param _crossDomainTarget Il destinatario previsto sul dominio di destinazione5 * @param _message I dati da inviare al target (di solito calldata per una funzione con6 * `onlyFromCrossDomainAccount()`)7 * @param _gasLimit Il limite del gas per la ricezione del messaggio sul dominio di destinazione. */8 9
10
11
12
13
14
15 function sendCrossDomainMessage(16 address _crossDomainTarget,17 uint32 _gasLimit,18 bytes memory _messageInfine, la funzione che invia un messaggio all'altro livello.
1 ) internal {2 // slither-disable-next-line reentrancy-events, reentrancy-benignSlither (opens in a new tab) è un analizzatore statico che Optimism esegue su ogni contratto per cercare vulnerabilità e altri potenziali problemi. In questo caso, la riga seguente innesca due vulnerabilità:
1 getCrossDomainMessenger().sendMessage(_crossDomainTarget, _message, _gasLimit);2 }3}In questo caso non ci preoccupiamo della rientranza, sappiamo che getCrossDomainMessenger() restituisce un indirizzo affidabile, anche se Slither non ha modo di saperlo.
Il contratto del ponte di L1
Il codice sorgente per questo contratto è qui (opens in a new tab).
1// SPDX-License-Identifier: MIT2pragma solidity ^0.8.9;Le interfacce possono far parte di altri contratti, quindi devono supportare un'ampia gamma di versioni di Solidity. Ma il ponte stesso è il nostro contratto e possiamo essere rigorosi su quale versione di Solidity utilizza.
1/* Importazioni di Interfacce */2/* Interface Imports */3import { IL1StandardBridge } from "./IL1StandardBridge.sol";4import { IL1ERC20Bridge } from "./IL1ERC20Bridge.sol";IL1ERC20Bridge e IL1StandardBridge sono spiegati sopra.
1import { IL2ERC20Bridge } from "../../L2/messaging/IL2ERC20Bridge.sol";Questa interfaccia (opens in a new tab) ci consente di creare messaggi per controllare il ponte standard su L2.
1import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";Questa interfaccia (opens in a new tab) ci consente di controllare i contratti ERC-20. Puoi leggere di più a riguardo qui.
1/* Importazioni di Librerie */2/* Library Imports */3import { CrossDomainEnabled } from "../../libraries/bridge/CrossDomainEnabled.sol";Come spiegato sopra, questo contratto viene utilizzato per la messaggistica tra livelli.
1import { Lib_PredeployAddresses } from "../../libraries/constants/Lib_PredeployAddresses.sol";Lib_PredeployAddresses (opens in a new tab) contiene gli indirizzi per i contratti di L2 che hanno sempre lo stesso indirizzo. Questo include il ponte standard su L2.
1import { Address } from "@openzeppelin/contracts/utils/Address.sol";Le utilità Address di OpenZeppelin (opens in a new tab). Vengono utilizzate per distinguere tra gli indirizzi dei contratti e quelli appartenenti agli account controllati esternamente (EOA).
Nota che questa non è una soluzione perfetta, perché non c'è modo di distinguere tra chiamate dirette e chiamate effettuate dal costruttore di un contratto, ma almeno questo ci consente di identificare e prevenire alcuni errori comuni degli utenti.
1import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";Lo standard ERC-20 (opens in a new tab) supporta due modi per un contratto di segnalare un fallimento:
- Revert (Annullamento)
- Restituire
false
Gestire entrambi i casi renderebbe il nostro codice più complicato, quindi utilizziamo invece SafeERC20 di OpenZeppelin (opens in a new tab), che si assicura che tutti i fallimenti si traducano in un annullamento (opens in a new tab).
1/* *2 * @title L1StandardBridge3 * @dev Il ponte L1 ETH e ERC20 è un contratto che memorizza i fondi L1 depositati e i token4 * standard che sono in uso su L2. Sincronizza un ponte L2 corrispondente, informandolo dei depositi5 * e ascoltandolo per i prelievi appena finalizzati.6 * */7
8
9
10
11
12
13
14contract L1StandardBridge is IL1StandardBridge, CrossDomainEnabled {15 using SafeERC20 for IERC20;Questa riga è il modo in cui specifichiamo di utilizzare il wrapper SafeERC20 ogni volta che utilizziamo l'interfaccia IERC20.
1
2 /* *******************************3 * Riferimenti a Contratti Esterni *4 ******************************* */5 6
7
8
9 address public l2TokenBridge;L'indirizzo di L2StandardBridge.
1
2 // Mappa il token L1 al token L2 al saldo del token L1 depositato3 mapping(address => mapping(address => uint256)) public deposits;Una doppia mappatura (opens in a new tab) come questa è il modo in cui si definisce un array sparso bidimensionale (opens in a new tab).
I valori in questa struttura dati sono identificati come deposit[L1 token addr][L2 token addr].
Il valore predefinito è zero.
Solo le celle impostate su un valore diverso vengono scritte nell'archiviazione.
1
2 /* **************3 * Costruttore *4 ************** */5 6
7
8
9 // Questo contratto vive dietro un proxy, quindi i parametri del costruttore rimarranno inutilizzati.10 constructor() CrossDomainEnabled(address(0)) {}Vogliamo poter aggiornare questo contratto senza dover copiare tutte le variabili nell'archiviazione.
Per farlo utilizziamo un Proxy (opens in a new tab), un contratto che utilizza delegatecall (opens in a new tab) per trasferire le chiamate a un contratto separato il cui indirizzo è memorizzato dal contratto proxy (quando si esegue l'aggiornamento si dice al proxy di cambiare quell'indirizzo).
Quando si utilizza delegatecall l'archiviazione rimane quella del contratto chiamante, quindi i valori di tutte le variabili di stato del contratto non vengono influenzati.
Un effetto di questo modello è che l'archiviazione del contratto che è il chiamato di delegatecall non viene utilizzata e pertanto i valori del costruttore passati ad esso non hanno importanza.
Questo è il motivo per cui possiamo fornire un valore senza senso al costruttore CrossDomainEnabled.
È anche il motivo per cui l'inizializzazione di seguito è separata dal costruttore.
1 /* *****************2 * Inizializzazione *3 ***************** */4 5
6
7
8 /* *9 * @param _l1messenger Indirizzo del Messenger L1 utilizzato per le comunicazioni cross-chain.10 * @param _l2TokenBridge Indirizzo del ponte standard L2. */11 12
13
14
15 // slither-disable-next-line external-functionQuesto test di Slither (opens in a new tab) identifica le funzioni che non vengono chiamate dal codice del contratto e potrebbero quindi essere dichiarate external invece di public.
Il costo del gas delle funzioni external può essere inferiore, perché possono essere fornite con parametri nei calldata.
Le funzioni dichiarate public devono essere accessibili dall'interno del contratto.
I contratti non possono modificare i propri calldata, quindi i parametri devono essere in memoria.
Quando una tale funzione viene chiamata esternamente, è necessario copiare i calldata in memoria, il che costa gas.
In questo caso la funzione viene chiamata solo una volta, quindi l'inefficienza non ci importa.
1 function initialize(address _l1messenger, address _l2TokenBridge) public {2 require(messenger == address(0), "Contract has already been initialized.");La funzione initialize dovrebbe essere chiamata solo una volta.
Se l'indirizzo del messaggero tra domini di L1 o del ponte dei token di L2 cambia, creiamo un nuovo proxy e un nuovo ponte che lo chiama.
È improbabile che ciò accada, tranne quando l'intero sistema viene aggiornato, un evento molto raro.
Nota che questa funzione non ha alcun meccanismo che limiti chi può chiamarla.
Ciò significa che in teoria un utente malintenzionato potrebbe aspettare fino a quando non distribuiamo il proxy e la prima versione del ponte e poi eseguire un front-running (opens in a new tab) per arrivare alla funzione initialize prima dell'utente legittimo. Ma ci sono due metodi per impedirlo:
- Se i contratti non vengono distribuiti direttamente da un EOA ma in una transazione in cui un altro contratto li crea (opens in a new tab), l'intero processo può essere atomico e terminare prima che venga eseguita qualsiasi altra transazione.
- Se la chiamata legittima a
initializefallisce, è sempre possibile ignorare il proxy e il ponte appena creati e crearne di nuovi.
1 messenger = _l1messenger;2 l2TokenBridge = _l2TokenBridge;3 }Questi sono i due parametri che il ponte deve conoscere.
1
2 /* *************3 * Deposito *4 ************* */5 6
7
8
9 /* * @dev Modificatore che richiede che il mittente sia un EOA. Questo controllo potrebbe essere aggirato da un contratto10 * malevolo tramite initcode, ma si occupa dell'errore dell'utente che vogliamo evitare. */11 12
13
14 modifier onlyEOA() {15 // Utilizzato per fermare i depositi dai contratti (evitare token persi accidentalmente)16 require(!Address.isContract(msg.sender), "Account not EOA");17 _;18 }Questo è il motivo per cui avevamo bisogno delle utilità Address di OpenZeppelin.
1 /* *2 * @dev Questa funzione può essere chiamata senza dati3 * per depositare un importo di ETH sul saldo del chiamante su L2.4 * Poiché la funzione receive non accetta dati, un importo5 * predefinito conservativo viene inoltrato a L2. */6 7
8
9
10
11
12 receive() external payable onlyEOA {13 _initiateETHDeposit(msg.sender, msg.sender, 200_000, bytes(""));14 }Questa funzione esiste a scopo di test. Nota che non appare nelle definizioni dell'interfaccia: non è per un uso normale.
1 /* *2 * @inheritdoc IL1StandardBridge */3 4
5
6 function depositETH(uint32 _l2Gas, bytes calldata _data) external payable onlyEOA {7 _initiateETHDeposit(msg.sender, msg.sender, _l2Gas, _data);8 }9
10 /* *11 * @inheritdoc IL1StandardBridge */12 13
14
15 function depositETHTo(16 address _to,17 uint32 _l2Gas,18 bytes calldata _data19 ) external payable {20 _initiateETHDeposit(msg.sender, _to, _l2Gas, _data);21 }Queste due funzioni sono wrapper attorno a _initiateETHDeposit, la funzione che gestisce l'effettivo deposito di ETH.
1 /* *2 * @dev Esegue la logica per i depositi memorizzando gli ETH e informando il Gateway ETH L2 del3 * deposito.4 * @param _from Account da cui prelevare il deposito su L1.5 * @param _to Account a cui dare il deposito su L2.6 * @param _l2Gas Limite del gas richiesto per completare il deposito su L2.7 * @param _data Dati opzionali da inoltrare a L2. Questi dati sono forniti8 * esclusivamente per comodità dei contratti esterni. A parte l'imposizione di una9 * lunghezza massima, questi contratti non forniscono alcuna garanzia sul loro contenuto. */10 11
12
13
14
15
16
17
18
19
20 function _initiateETHDeposit(21 address _from,22 address _to,23 uint32 _l2Gas,24 bytes memory _data25 ) internal {26 // Costruisce i calldata per la chiamata finalizeDeposit27 bytes memory message = abi.encodeWithSelector(Il modo in cui funzionano i messaggi tra domini è che il contratto di destinazione viene chiamato con il messaggio come suoi calldata.
I contratti Solidity interpretano sempre i propri calldata in conformità con le specifiche ABI (opens in a new tab).
La funzione Solidity abi.encodeWithSelector (opens in a new tab) crea quei calldata.
1 IL2ERC20Bridge.finalizeDeposit.selector,2 address(0),3 Lib_PredeployAddresses.OVM_ETH,4 _from,5 _to,6 msg.value,7 _data8 );Il messaggio qui è di chiamare la funzione finalizeDeposit (opens in a new tab) con questi parametri:
| Parametro | Valore | Significato |
|---|---|---|
| _l1Token | address(0) | Valore speciale per rappresentare gli ETH (che non sono un token ERC-20) su L1 |
| _l2Token | Lib_PredeployAddresses.OVM_ETH | Il contratto di L2 che gestisce gli ETH su Optimism, 0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000 (questo contratto è solo per uso interno di Optimism) |
| _from | _from | L'indirizzo su L1 che invia gli ETH |
| _to | _to | L'indirizzo su L2 che riceve gli ETH |
| amount | msg.value | Quantità di wei inviati (che sono già stati inviati al ponte) |
| _data | _data | Dati aggiuntivi da allegare al deposito |
1 // Invia i calldata in L22 // slither-disable-next-line reentrancy-events3 sendCrossDomainMessage(l2TokenBridge, _l2Gas, message);Invia il messaggio tramite il messaggero tra domini.
1 // slither-disable-next-line reentrancy-events2 emit ETHDepositInitiated(_from, _to, msg.value, _data);3 }Emette un evento per informare qualsiasi applicazione decentralizzata in ascolto di questo trasferimento.
1 /* *2 * @inheritdoc IL1ERC20Bridge */3 4
5
6 function depositERC20(7 .8 .9 .10 ) external virtual onlyEOA {11 _initiateERC20Deposit(_l1Token, _l2Token, msg.sender, msg.sender, _amount, _l2Gas, _data);12 }13
14 /* *15 * @inheritdoc IL1ERC20Bridge */16 17
18
19 function depositERC20To(20 .21 .22 .23 ) external virtual {24 _initiateERC20Deposit(_l1Token, _l2Token, msg.sender, _to, _amount, _l2Gas, _data);25 }Queste due funzioni sono wrapper attorno a _initiateERC20Deposit, la funzione che gestisce l'effettivo deposito di ERC-20.
1 /* *2 * @dev Esegue la logica per i depositi informando il contratto L2 Deposited Token3 * del deposito e chiamando un gestore per bloccare i fondi L1. (es., transferFrom)4 *5 * @param _l1Token Indirizzo dell'ERC20 L1 che stiamo depositando6 * @param _l2Token Indirizzo del rispettivo ERC20 L2 di L17 * @param _from Account da cui prelevare il deposito su L18 * @param _to Account a cui dare il deposito su L29 * @param _amount Importo di ERC20 da depositare.10 * @param _l2Gas Limite del gas richiesto per completare il deposito su L2.11 * @param _data Dati opzionali da inoltrare a L2. Questi dati sono forniti12 * esclusivamente per comodità dei contratti esterni. A parte l'imposizione di una13 * lunghezza massima, questi contratti non forniscono alcuna garanzia sul loro contenuto. */14 15
16
17
18
19
20
21
22
23
24
25
26
27
28 function _initiateERC20Deposit(29 address _l1Token,30 address _l2Token,31 address _from,32 address _to,33 uint256 _amount,34 uint32 _l2Gas,35 bytes calldata _data36 ) internal {Questa funzione è simile a _initiateETHDeposit sopra, con alcune importanti differenze.
La prima differenza è che questa funzione riceve gli indirizzi dei token e l'importo da trasferire come parametri.
Nel caso degli ETH, la chiamata al ponte include già il trasferimento dell'asset all'account del ponte (msg.value).
1 // Quando un deposito viene avviato su L1, il ponte L1 trasferisce i fondi a se stesso per futuri2 // prelievi. safeTransferFrom controlla anche se il contratto ha del codice, quindi questo fallirà se3 // _from è un EOA o address(0).4 // slither-disable-next-line reentrancy-events, reentrancy-benign5 IERC20(_l1Token).safeTransferFrom(_from, address(this), _amount);I trasferimenti di token ERC-20 seguono un processo diverso rispetto agli ETH:
- L'utente (
_from) concede un'indennità al ponte per trasferire i token appropriati. - L'utente chiama il ponte con l'indirizzo del contratto del token, l'importo, ecc.
- Il ponte trasferisce i token (a se stesso) come parte del processo di deposito.
Il primo passaggio può avvenire in una transazione separata rispetto agli ultimi due.
Tuttavia, il front-running non è un problema perché le due funzioni che chiamano _initiateERC20Deposit (depositERC20 e depositERC20To) chiamano questa funzione solo con msg.sender come parametro _from.
1 // Costruisce i calldata per _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 );11
12 // Invia i calldata in L213 // slither-disable-next-line reentrancy-events, reentrancy-benign14 sendCrossDomainMessage(l2TokenBridge, _l2Gas, message);15
16 // slither-disable-next-line reentrancy-benign17 deposits[_l1Token][_l2Token] = deposits[_l1Token][_l2Token] + _amount;Aggiunge l'importo depositato di token alla struttura dati deposits.
Potrebbero esserci più indirizzi su L2 che corrispondono allo stesso token ERC-20 di L1, quindi non è sufficiente utilizzare il saldo del ponte del token ERC-20 di L1 per tenere traccia dei depositi.
1
2 // slither-disable-next-line reentrancy-events3 emit ERC20DepositInitiated(_l1Token, _l2Token, _from, _to, _amount, _data);4 }5
6 /* ************************7 * Funzioni Cross-chain *8 ************************ */9 10
11
12
13 /* *14 * @inheritdoc IL1StandardBridge */15 16
17
18 function finalizeETHWithdrawal(19 address _from,20 address _to,21 uint256 _amount,22 bytes calldata _dataIl ponte di L2 invia un messaggio al messaggero tra domini di L2 che fa sì che il messaggero tra domini di L1 chiami questa funzione (una volta che la transazione che finalizza il messaggio (opens in a new tab) viene inviata su L1, ovviamente).
1 ) external onlyFromCrossDomainAccount(l2TokenBridge) {Si assicura che questo sia un messaggio legittimo, proveniente dal messaggero tra domini e originato dal ponte dei token di L2. Questa funzione viene utilizzata per prelevare ETH dal ponte, quindi dobbiamo assicurarci che venga chiamata solo dal chiamante autorizzato.
1 // slither-disable-next-line reentrancy-events2 (bool success, ) = _to.call{ value: _amount }(new bytes(0));Il modo per trasferire ETH è chiamare il destinatario con la quantità di wei in msg.value.
1 require(success, "TransferHelper::safeTransferETH: ETH transfer failed");2
3 // slither-disable-next-line reentrancy-events4 emit ETHWithdrawalFinalized(_from, _to, _amount, _data);Emette un evento relativo al prelievo.
1 }2
3 /* *4 * @inheritdoc IL1ERC20Bridge */5 6
7
8 function finalizeERC20Withdrawal(9 address _l1Token,10 address _l2Token,11 address _from,12 address _to,13 uint256 _amount,14 bytes calldata _data15 ) external onlyFromCrossDomainAccount(l2TokenBridge) {Questa funzione è simile a finalizeETHWithdrawal sopra, con le modifiche necessarie per i token ERC-20.
1 deposits[_l1Token][_l2Token] = deposits[_l1Token][_l2Token] - _amount;Aggiorna la struttura dati deposits.
1
2 // Quando un prelievo viene finalizzato su L1, il ponte L1 trasferisce i fondi a chi effettua il prelievo3 // slither-disable-next-line reentrancy-events4 IERC20(_l1Token).safeTransfer(_to, _amount);5
6 // slither-disable-next-line reentrancy-events7 emit ERC20WithdrawalFinalized(_l1Token, _l2Token, _from, _to, _amount, _data);8 }9
10
11 /* ****************************12 * Temporaneo - Migrazione ETH *13 **************************** */14 15
16
17
18 /* *19 * @dev Aggiunge saldo ETH all'account. Questo ha lo scopo di consentire la migrazione20 * degli ETH da un vecchio gateway a un nuovo gateway.21 * NOTA: Questo viene lasciato solo per un aggiornamento in modo da poter ricevere gli ETH migrati dal22 * vecchio contratto */23 24
25
26
27
28
29 function donateETH() external payable {}30}C'era un'implementazione precedente del ponte.
Quando siamo passati da quell'implementazione a questa, abbiamo dovuto spostare tutti gli asset.
I token ERC-20 possono semplicemente essere spostati.
Tuttavia, per trasferire ETH a un contratto è necessaria l'approvazione di quel contratto, che è ciò che ci fornisce donateETH.
Token ERC-20 su L2
Affinché un token ERC-20 si adatti al ponte standard, deve consentire al ponte standard, e solo al ponte standard, di coniare token. Questo è necessario perché i ponti devono garantire che il numero di token in circolazione su Optimism sia uguale al numero di token bloccati all'interno del contratto del ponte di L1. Se ci sono troppi token su L2, alcuni utenti non sarebbero in grado di riportare i propri asset su L1 tramite il ponte. Invece di un ponte affidabile, ricreeremmo essenzialmente il sistema bancario a riserva frazionaria (opens in a new tab). Se ci sono troppi token su L1, alcuni di quei token rimarrebbero bloccati per sempre all'interno del contratto del ponte perché non c'è modo di rilasciarli senza bruciare i token di L2.
IL2StandardERC20
Ogni token ERC-20 su L2 che utilizza il ponte standard deve fornire questa interfaccia (opens in a new tab), che ha le funzioni e gli eventi di cui il ponte standard ha bisogno.
1// SPDX-License-Identifier: MIT2pragma solidity ^0.8.9;3
4import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";L'interfaccia ERC-20 standard (opens in a new tab) non include le funzioni mint e burn.
Questi metodi non sono richiesti dallo standard ERC-20 (opens in a new tab), che lascia non specificati i meccanismi per creare e distruggere i token.
1import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol";L'interfaccia ERC-165 (opens in a new tab) viene utilizzata per specificare quali funzioni fornisce un contratto. Puoi leggere lo standard qui (opens in a new tab).
1interface IL2StandardERC20 is IERC20, IERC165 {2 function l1Token() external returns (address);Questa funzione fornisce l'indirizzo del token di L1 che è collegato tramite ponte a questo contratto. Nota che non abbiamo una funzione simile nella direzione opposta. Dobbiamo essere in grado di trasferire tramite ponte qualsiasi token di L1, indipendentemente dal fatto che il supporto per L2 fosse previsto o meno al momento della sua implementazione.
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}Funzioni ed eventi per coniare (creare) e bruciare (distruggere) i token. Il ponte dovrebbe essere l'unica entità in grado di eseguire queste funzioni per garantire che il numero di token sia corretto (uguale al numero di token bloccati su L1).
L2StandardERC20
Questa è la nostra implementazione dell'interfaccia IL2StandardERC20 (opens in a new tab).
A meno che tu non abbia bisogno di qualche tipo di logica personalizzata, dovresti usare questa.
1// SPDX-License-Identifier: MIT2pragma solidity ^0.8.9;3
4import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";Il contratto ERC-20 di OpenZeppelin (opens in a new tab). Optimism non crede nel reinventare la ruota, specialmente quando la ruota è ben verificata e deve essere abbastanza affidabile da contenere asset.
1import "./IL2StandardERC20.sol";2
3contract L2StandardERC20 is IL2StandardERC20, ERC20 {4 address public l1Token;5 address public l2Bridge;Questi sono i due parametri di configurazione aggiuntivi che richiediamo e che l'ERC-20 normalmente non richiede.
1
2 /* *3 * @param _l2Bridge Indirizzo del ponte standard L2.4 * @param _l1Token Indirizzo del token L1 corrispondente.5 * @param _name Nome dell'ERC20.6 * @param _symbol Simbolo dell'ERC20. */7 8
9
10
11
12
13 constructor(14 address _l2Bridge,15 address _l1Token,16 string memory _name,17 string memory _symbol18 ) ERC20(_name, _symbol) {19 l1Token = _l1Token;20 l2Bridge = _l2Bridge;21 }Prima chiama il costruttore per il contratto da cui ereditiamo (ERC20(_name, _symbol)) e poi imposta le nostre variabili.
1
2 modifier onlyL2Bridge() {3 require(msg.sender == l2Bridge, "Only L2 Bridge can mint and burn");4 _;5 }6
7
8 // slither-disable-next-line external-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 }Questo è il modo in cui funziona ERC-165 (opens in a new tab). Ogni interfaccia è un numero di funzioni supportate ed è identificata come l'or esclusivo (opens in a new tab) dei selettori di funzione ABI (opens in a new tab) di quelle funzioni.
Il ponte di L2 utilizza ERC-165 come controllo di integrità per assicurarsi che il contratto ERC-20 a cui invia gli asset sia un IL2StandardERC20.
Nota: Non c'è nulla che impedisca a un contratto malevolo di fornire risposte false a supportsInterface, quindi questo è un meccanismo di controllo dell'integrità, non un meccanismo di sicurezza.
1 // slither-disable-next-line external-function2 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-function9 function burn(address _from, uint256 _amount) public virtual onlyL2Bridge {10 _burn(_from, _amount);11
12 emit Burn(_from, _amount);13 }14}Solo il ponte di L2 è autorizzato a coniare e bruciare asset.
_mint e _burn sono in realtà definiti nel contratto ERC-20 di OpenZeppelin.
Quel contratto semplicemente non li espone esternamente, perché le condizioni per coniare e bruciare token sono tanto varie quanto il numero di modi per utilizzare l'ERC-20.
Codice del ponte di L2
Questo è il codice che esegue il ponte su Optimism. Il sorgente per questo contratto è qui (opens in a new tab).
1// SPDX-License-Identifier: MIT2pragma solidity ^0.8.9;3
4/* Importazioni di Interfacce */5/* Interface Imports */6import { IL1StandardBridge } from "../../L1/messaging/IL1StandardBridge.sol";7import { IL1ERC20Bridge } from "../../L1/messaging/IL1ERC20Bridge.sol";8import { IL2ERC20Bridge } from "./IL2ERC20Bridge.sol";L'interfaccia IL2ERC20Bridge (opens in a new tab) è molto simile all'equivalente di L1 che abbiamo visto sopra. Ci sono due differenze significative:
- Su L1 si avviano i depositi e si finalizzano i prelievi. Qui si avviano i prelievi e si finalizzano i depositi.
- Su L1 è necessario distinguere tra ETH e token ERC-20. Su L2 possiamo utilizzare le stesse funzioni per entrambi perché internamente i saldi di ETH su Optimism sono gestiti come un token ERC-20 con l'indirizzo 0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000 (opens in a new tab).
1/* Importazioni di Librerie */2/* Library Imports */3import { ERC165Checker } from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol";4import { CrossDomainEnabled } from "../../libraries/bridge/CrossDomainEnabled.sol";5import { Lib_PredeployAddresses } from "../../libraries/constants/Lib_PredeployAddresses.sol";6
7/* Importazioni di Contratti */8/* Contract Imports */9import { IL2StandardERC20 } from "../../standards/IL2StandardERC20.sol";10
11/* *12 * @title L2StandardBridge13 * @dev Il ponte Standard L2 è un contratto che lavora insieme al ponte Standard L1 per14 * abilitare le transizioni di ETH ed ERC20 tra L1 e L2.15 * Questo contratto agisce per coniare nuovi token quando viene a conoscenza di depositi nel ponte16 * Standard L1.17 * Questo contratto agisce anche come bruciatore dei token destinati al prelievo, informando il ponte18 * L1 di rilasciare i fondi L1. */19
20
21
22
23
24
25
26
27
28contract L2StandardBridge is IL2ERC20Bridge, CrossDomainEnabled {29 /* *******************************30 * Riferimenti a Contratti Esterni *31 ******************************* */32 33
34
35
36 address public l1TokenBridge;Tiene traccia dell'indirizzo del ponte di L1. Nota che, a differenza dell'equivalente di L1, qui abbiamo bisogno di questa variabile. L'indirizzo del ponte di L1 non è noto in anticipo.
1
2 /* **************3 * Costruttore *4 ************** */5 6
7
8
9 /* *10 * @param _l2CrossDomainMessenger Messenger cross-domain utilizzato da questo contratto.11 * @param _l1TokenBridge Indirizzo del ponte L1 distribuito sulla catena principale. */12 13
14
15
16 constructor(address _l2CrossDomainMessenger, address _l1TokenBridge)17 CrossDomainEnabled(_l2CrossDomainMessenger)18 {19 l1TokenBridge = _l1TokenBridge;20 }21
22 /* **************23 * Prelievo *24 ************** */25 26
27
28
29 /* *30 * @inheritdoc IL2ERC20Bridge */31 32
33
34 function withdraw(35 address _l2Token,36 uint256 _amount,37 uint32 _l1Gas,38 bytes calldata _data39 ) external virtual {40 _initiateWithdrawal(_l2Token, msg.sender, msg.sender, _amount, _l1Gas, _data);41 }42
43 /* *44 * @inheritdoc IL2ERC20Bridge */45 46
47
48 function withdrawTo(49 address _l2Token,50 address _to,51 uint256 _amount,52 uint32 _l1Gas,53 bytes calldata _data54 ) external virtual {55 _initiateWithdrawal(_l2Token, msg.sender, _to, _amount, _l1Gas, _data);56 }Queste due funzioni avviano i prelievi. Nota che non è necessario specificare l'indirizzo del token di L1. Ci si aspetta che i token di L2 ci dicano l'indirizzo dell'equivalente di L1.
1
2 /* *3 * @dev Esegue la logica per i prelievi bruciando il token e informando4 * il Gateway del token L1 del prelievo.5 * @param _l2Token Indirizzo del token L2 in cui è stato avviato il prelievo.6 * @param _from Account da cui prelevare il prelievo su L2.7 * @param _to Account a cui dare il prelievo su L1.8 * @param _amount Importo del token da prelevare.9 * @param _l1Gas Inutilizzato, ma incluso per potenziali considerazioni di compatibilità futura.10 * @param _data Dati opzionali da inoltrare a L1. Questi dati sono forniti11 * esclusivamente per comodità dei contratti esterni. A parte l'imposizione di una12 * lunghezza massima, questi contratti non forniscono alcuna garanzia sul loro contenuto. */13 14
15
16
17
18
19
20
21
22
23
24
25 function _initiateWithdrawal(26 address _l2Token,27 address _from,28 address _to,29 uint256 _amount,30 uint32 _l1Gas,31 bytes calldata _data32 ) internal {33 // Quando viene avviato un prelievo, bruciamo i fondi di chi effettua il prelievo per prevenire un successivo34 // utilizzo su L235 // slither-disable-next-line reentrancy-events36 IL2StandardERC20(_l2Token).burn(msg.sender, _amount);Nota che non facciamo affidamento sul parametro _from ma su msg.sender che è molto più difficile da falsificare (impossibile, per quanto ne so).
1
2 // Costruisce i calldata per l1TokenBridge.finalizeERC20Withdrawal(_to, _amount)3 // slither-disable-next-line reentrancy-events4 address l1Token = IL2StandardERC20(_l2Token).l1Token();5 bytes memory message;6
7 if (_l2Token == Lib_PredeployAddresses.OVM_ETH) {Su L1 è necessario distinguere tra ETH ed 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 }19
20 // Invia il messaggio al ponte L121 // slither-disable-next-line reentrancy-events22 sendCrossDomainMessage(l1TokenBridge, _l1Gas, message);23
24 // slither-disable-next-line reentrancy-events25 emit WithdrawalInitiated(l1Token, _l2Token, msg.sender, _to, _amount, _data);26 }27
28 /* ***********************************29 * Funzione Cross-chain: Deposito *30 *********************************** */31 32
33
34
35 /* *36 * @inheritdoc IL2ERC20Bridge */37 38
39
40 function finalizeDeposit(41 address _l1Token,42 address _l2Token,43 address _from,44 address _to,45 uint256 _amount,46 bytes calldata _dataQuesta funzione è chiamata da L1StandardBridge.
1 ) external virtual onlyFromCrossDomainAccount(l1TokenBridge) {Si assicura che la fonte del messaggio sia legittima.
Questo è importante perché questa funzione chiama _mint e potrebbe essere utilizzata per fornire token che non sono coperti dai token che il ponte possiede su L1.
1 // Controlla che il token di destinazione sia conforme e2 // verifica che il token depositato su L1 corrisponda alla rappresentazione del token depositato su L2 qui3 if (4 // slither-disable-next-line reentrancy-events5 ERC165Checker.supportsInterface(_l2Token, 0x1d1d8b63) &&6 _l1Token == IL2StandardERC20(_l2Token).l1Token()Controlli di integrità:
- L'interfaccia corretta è supportata
- L'indirizzo di L1 del contratto ERC-20 di L2 corrisponde alla fonte di L1 dei token
1 ) {2 // Quando un deposito viene finalizzato, accreditiamo sull'account su L2 la stessa quantità di3 // token.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);Se i controlli di integrità vengono superati, finalizza il deposito:
- Conia i token
- Emette l'evento appropriato
1 } else {2 // O il token L2 in cui si sta depositando non concorda sull'indirizzo corretto3 // del suo token L1, oppure non supporta l'interfaccia corretta.4 // Questo dovrebbe accadere solo se c'è un token L2 malevolo, o se un utente in qualche modo5 // ha specificato l'indirizzo del token L2 sbagliato in cui depositare.6 // In entrambi i casi, interrompiamo il processo qui e costruiamo un7 // messaggio di prelievo in modo che gli utenti possano recuperare i propri fondi in alcuni casi.8 // Non c'è modo di prevenire del tutto i contratti di token malevoli, ma questo limita9 // l'errore dell'utente e mitiga alcune forme di comportamento malevolo del contratto.Se un utente ha commesso un errore rilevabile utilizzando l'indirizzo del token di L2 sbagliato, vogliamo annullare il deposito e restituire i token su L1. L'unico modo in cui possiamo farlo da L2 è inviare un messaggio che dovrà attendere il periodo di contestazione dei guasti, ma questo è molto meglio per l'utente rispetto alla perdita permanente dei token.
1 bytes memory message = abi.encodeWithSelector(2 IL1ERC20Bridge.finalizeERC20Withdrawal.selector,3 _l1Token,4 _l2Token,5 _to, // scambiati _to e _from qui per rimbalzare il deposito al mittente6 _from,7 _amount,8 _data9 );10
11 // Invia il messaggio al ponte 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}Conclusione
Il ponte standard è il meccanismo più flessibile per i trasferimenti di asset. Tuttavia, essendo così generico, non è sempre il meccanismo più facile da usare. Soprattutto per i prelievi, la maggior parte degli utenti preferisce utilizzare ponti di terze parti (opens in a new tab) che non attendono il periodo di contestazione e non richiedono una prova di Merkle per finalizzare il prelievo.
Questi ponti in genere funzionano avendo asset su L1, che forniscono immediatamente per una piccola commissione (spesso inferiore al costo del gas per un prelievo dal ponte standard). Quando il ponte (o le persone che lo gestiscono) prevede di essere a corto di asset su L1, trasferisce asset sufficienti da L2. Poiché si tratta di prelievi molto grandi, il costo del prelievo viene ammortizzato su un importo elevato e rappresenta una percentuale molto più piccola.
Speriamo che questo articolo ti abbia aiutato a capire meglio come funziona il livello 2 e come scrivere codice Solidity chiaro e sicuro.
Vedi qui per altri miei lavori (opens in a new tab).
Ultimo aggiornamento della pagina: 3 marzo 2026