Vai al contenuto principale

Guida del ponte standard di Optimism per contratti

soliditypontelivello 2
Intermedio
Ori Pomerantz
30 marzo 2022
33 minuti letti minute read

Optimism(opens in a new tab) è un Rollup ottimistico. I rollup ottimistici possono elaborare le transazioni a un prezzo molto più basso di quello della rete principale di Ethereum (nota anche come livello 1 o L1), poiché le transazioni sono elaborate solo da alcuni nodi, invece che da ogni nodo sulla rete. Allo stesso tempo, i dati vengono tutti scritti nel L1, così che tutto possa essere provato e ricostruito con le garanzie d'integrità e disponibilità della rete principale.

Per usare le risorse del L1 su Optimism (o su qualsiasi altro L2), le risorse devono essere collegate con un ponte. Un modo per farlo è che gli utenti blocchino le risorse (ETH e token ERC-20 sono le più comuni) nel L1 e ricevano le risorse equivalenti da usare nel L2. In definitiva, chiunque le riceva potrebbe volerle ricollegare al L1. Così facendo, le risorse sono bruciate nel L2 e poi rilasciate nuovamente all'utente nel 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 per studiarlo come un esempio di codice di 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

  1. In caso di deposito di un ERC-20, il depositante concede al ponte un'indennità per spendere l'importo depositato
  2. Il depositante chiama il ponte L1 (depositERC20, depositERC20To, depositETH, o depositETHTo)
  3. Il ponte L1 prende possesso della risorsa collegata
    • ETH: la risorsa è trasferita dal depositante all'interno della chiamata
    • ERC-20: la risorsa è trasferita dal ponte a sé stessa, usando l'indennità fornita dal depositante
  4. Il ponte L1 usa il meccanismo di messaggio interdominio per chiamare finalizeDeposit sul ponte L2

Livello 2

  1. Il ponte L2 verifica che la chiamata a finalizeDeposit sia legittima:
    • Proviene dal contratto di messaggistica interdominio
    • Originariamente proveniva dal ponte su L1
  2. Il ponte L2 verifica se il contratto del token ERC-20 su L2 è quello corretto:
    • Il contratto L2 segnala che la sua controparte del L1 è uguale a quella da cui i token provenivano su L1
    • Il contratto L2 segnala che supporta l'interfaccia corretta (che usa ERC-165(opens in a new tab)).
  3. Se il contratto L2 è quello corretto, chiamalo per coniare il numero di token appropriato all'indirizzo corretto. Altrimenti, avvia un processo di prelievo per consentire all'utente di rivendicare i token su L1.

Flusso di prelievo

Livello 2

  1. Il prelevante chiama il ponte L2 (withdraw o withdrawTo)
  2. Il ponte L2 brucia il giusto numero di token appartenente a msg.sender
  3. Il ponte L2 usa il meccanismo di messaggio interdominio per chiamare finalizeETHWithdrawal o finalizeERC20Withdrawal sul ponte L1

Livello 1

  1. Il ponte L1 verifica che la chiamata a finalizeETHWithdrawal o finalizeERC20Withdrawal sia legittima:
    • Proviene dal meccanismo di messaggistica interdominio
    • Originariamente proveniva dal ponte su L2
  2. Il ponte L1 trasferisce la risorsa appropriata (ETH o ERC-20) all'indirizzo appropriato

Codice del Livello 1

Questo è il codice eseguito su L1, la rete principale di Ethereum.

IL1ERC20Bridge

Quest'interfaccia è definita qui(opens in a new tab). Include le funzioni e definizioni richieste per collegare i token ERC-20.

1// SPDX-License-Identifier: MIT
Copia

Gran parte del codice di Optimism è rilasciato sotto la licenza MIT(opens in a new tab).

1pragma solidity >0.5.0 <0.9.0;
Copia

Al momento della scrittura, l'ultima versione di Solidity è la 0.8.12. Fino al rilascio della versione 0.9.0, non sappiamo se questo codice è compatibile con esso o meno.

1/**
2 * @title IL1ERC20Bridge
3 */
4interface IL1ERC20Bridge {
5 /**********
6 * Events *
7 **********/
8
9 event ERC20DepositInitiated(
Mostra tutto
Copia

Nella terminologia del ponte di Optimism, deposito indica un trasferimento da L1 a L2, e prelievo indica un trasferimento da L2 a L1.

1 address indexed _l1Token,
2 address indexed _l2Token,
Copia

Nella maggior parte dei casi, l'indirizzo di un ERC-20 su L1 non equivale all'indirizzo dell'ERC-20 equivalente su L2. Puoi visualizzare l'elenco di indirizzi di token qui(opens in a new tab). L'indirizzo con chainId 1 è sul L1 (Mainnet) e l'indirizzo con chainId 10 è sul L2 (Optimism). Gli altri due valori di chainId sono per la rete di prova Kovan (42) e la rete di prova Kovan di Optimistic (69).

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

È possibile aggiungere note ai trasferimenti, nel qual caso sono aggiunti 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 _data
8 );
Copia

Lo stesso contratto del ponte gestisce i trasferimenti in entrambe le direzioni. Nel caso del ponte L1, ciò indica l'inizializzazione dei depositi e la finalizzazione dei prelievi.

1
2 /********************
3 * Public Functions *
4 ********************/
5
6 /**
7 * @dev get the address of the corresponding L2 bridge contract.
8 * @return Address of the corresponding L2 bridge contract.
9 */
10 function l2TokenBridge() external returns (address);
Mostra tutto
Copia

Questa funzione non è davvero necessaria, perché sul L2 è un contratto pre-distribuito, quindi è sempre all'indirizzo 0x4200000000000000000000000000000000000010. Serve per simmetria con il ponte L2, perché non è banale sapere l'indirizzo del ponte L1.

1 /**
2 * @dev deposit an amount of the ERC20 to the caller's balance on L2.
3 * @param _l1Token Address of the L1 ERC20 we are depositing
4 * @param _l2Token Address of the L1 respective L2 ERC20
5 * @param _amount Amount of the ERC20 to deposit
6 * @param _l2Gas Gas limit required to complete the deposit on L2.
7 * @param _data Optional data to forward to L2. This data is provided
8 * solely as a convenience for external contracts. Aside from enforcing a maximum
9 * length, these contracts provide no guarantees about its content.
10 */
11 function depositERC20(
12 address _l1Token,
13 address _l2Token,
14 uint256 _amount,
15 uint32 _l2Gas,
16 bytes calldata _data
17 ) external;
Mostra tutto
Copia

Il parametro _l2Gas è l'importo di gas del L2 che la transazione può spendere. Fino a un certo limite (elevato), è gratuito(opens in a new tab), quindi, a meno che il contratto ERC-20 non faccia qualcosa di davvero strano durante il conio, non dovrebbe essere un problema. Questa funzione si occupa dello scenario comune, in cui un utente collega le risorse allo stesso indirizzo su una blockchain differente.

1 /**
2 * @dev deposit an amount of ERC20 to a recipient's balance on L2.
3 * @param _l1Token Address of the L1 ERC20 we are depositing
4 * @param _l2Token Address of the L1 respective L2 ERC20
5 * @param _to L2 address to credit the withdrawal to.
6 * @param _amount Amount of the ERC20 to deposit.
7 * @param _l2Gas Gas limit required to complete the deposit on L2.
8 * @param _data Optional data to forward to L2. This data is provided
9 * solely as a convenience for external contracts. Aside from enforcing a maximum
10 * length, these contracts provide no guarantees about its content.
11 */
12 function depositERC20To(
13 address _l1Token,
14 address _l2Token,
15 address _to,
16 uint256 _amount,
17 uint32 _l2Gas,
18 bytes calldata _data
19 ) external;
Mostra tutto
Copia

Questa funzione è quasi identica a depositERC20, ma ti consente di inviare l'ERC-20 a un altro indirizzo.

1 /*************************
2 * Cross-chain Functions *
3 *************************/
4
5 /**
6 * @dev Complete a withdrawal from L2 to L1, and credit funds to the recipient's balance of the
7 * L1 ERC20 token.
8 * This call will fail if the initialized withdrawal from L2 has not been finalized.
9 *
10 * @param _l1Token Address of L1 token to finalizeWithdrawal for.
11 * @param _l2Token Address of L2 token where withdrawal was initiated.
12 * @param _from L2 address initiating the transfer.
13 * @param _to L1 address to credit the withdrawal to.
14 * @param _amount Amount of the ERC20 to deposit.
15 * @param _data Data provided by the sender on L2. This data is provided
16 * solely as a convenience for external contracts. Aside from enforcing a maximum
17 * length, these contracts provide no guarantees about its content.
18 */
19 function finalizeERC20Withdrawal(
20 address _l1Token,
21 address _l2Token,
22 address _from,
23 address _to,
24 uint256 _amount,
25 bytes calldata _data
26 ) external;
27}
Mostra tutto
Copia

I prelievi (e altri messaggi da L2 a L1) su Optimism sono processi in due fasi:

  1. Una transazione di avvio su L2.
  2. Una transazione di finalizzazione o rivendicazione su L1. Questa transazione deve verificarsi dopo il periodo di contestazione dell'errore(opens in a new tab) perché la transazione di L2 termini.

IL1StandardBridge

Quest'interfaccia è definita qui(opens in a new tab). Questo file contiene le definizioni dell'evento e la funzione per ETH. Queste definizioni sono molto simili a quelle definite nel precedente IL1ERC20Bridge per ERC-20.

L'interfaccia del ponte è divisa tra due file perché alcuni token ERC-20 richiedono un'elaborazione specifica e non possono esser gestiti dal ponte standard. Il ponte personalizzato che gestisce un token di questo tipo può quindi implementare IL1ERC20Bridge e non dover collegare anche ETH.

1// SPDX-License-Identifier: MIT
2pragma solidity >0.5.0 <0.9.0;
3
4import "./IL1ERC20Bridge.sol";
5
6/**
7 * @title IL1StandardBridge
8 */
9interface IL1StandardBridge is IL1ERC20Bridge {
10 /**********
11 * Events *
12 **********/
13 event ETHDepositInitiated(
14 address indexed _from,
15 address indexed _to,
16 uint256 _amount,
17 bytes _data
18 );
Mostra tutto
Copia

Questo evento è quasi identico alla versione di ERC-20 (ERC20DepositInitiated), salvo che mancano gli indirizzi del token di L1 e L2. Lo stesso vale per gli altri eventi e le altre funzioni.

1 event ETHWithdrawalFinalized(
2 .
3 .
4 .
5 );
6
7 /********************
8 * Public Functions *
9 ********************/
10
11 /**
12 * @dev Deposit an amount of the ETH to the caller's balance on L2.
13 .
14 .
15 .
16 */
17 function depositETH(uint32 _l2Gas, bytes calldata _data) external payable;
18
19 /**
20 * @dev Deposit an amount of ETH to a recipient's balance on L2.
21 .
22 .
23 .
24 */
25 function depositETHTo(
26 address _to,
27 uint32 _l2Gas,
28 bytes calldata _data
29 ) external payable;
30
31 /*************************
32 * Cross-chain Functions *
33 *************************/
34
35 /**
36 * @dev Complete a withdrawal from L2 to L1, and credit funds to the recipient's balance of the
37 * L1 ETH token. Since only the xDomainMessenger can call this function, it will never be called
38 * before the withdrawal is finalized.
39 .
40 .
41 .
42 */
43 function finalizeETHWithdrawal(
44 address _from,
45 address _to,
46 uint256 _amount,
47 bytes calldata _data
48 ) external;
49}
Mostra tutto
Copia

CrossDomainEnabled

Questo contratto(opens in a new tab) è ereditato da entrambi i ponti (L1 e L2) per inviare i messaggi all'altro livello.

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

Quest'interfaccia(opens in a new tab) dice al contratto come inviare i messaggi all'altro livello, usando la messaggistica interdominio. Questa messaggistica interdominio è un sistema totalmente a sé, che merita il proprio articolo, che spero scriverò in futuro.

1/**
2 * @title CrossDomainEnabled
3 * @dev Contratto di supporto per contratti che eseguono comunicazioni tra domini
4 *
5 * Compiler usato: definito dal contratto ereditario
6 */
7contract CrossDomainEnabled {
8 /*************
9 * Variabili *
10 *************/
11
12 // Il contratto di Messaggistica usato per inviare e ricevere messaggi dall'altro dominio.
13 address public messenger;
14
15 /***************
16 * Constructor *
17 ***************/
18
19 /**
20 * @param _messenger Address of the CrossDomainMessenger on the current layer.
21 */
22 constructor(address _messenger) {
23 messenger = _messenger;
24 }
Mostra tutto
Copia

Il parametro che il contratto deve conoscere, l'indirizzo della messaggistica interdominio su questo livello. Questo parametro è impostato una volta, nel costruttore, e non cambia mai.

1
2 /**********************
3 * Function Modifiers *
4 **********************/
5
6 /**
7 * Enforces that the modified function is only callable by a specific cross-domain account.
8 * @param _sourceDomainAccount The only account on the originating domain which is
9 * authenticated to call this function.
10 */
11 modifier onlyFromCrossDomainAccount(address _sourceDomainAccount) {
Mostra tutto
Copia

La messaggistica interdominio è accessibile da qualsiasi contratto sulla blockchain mentre è in esecuzione (sulla mainnet di Ethereum o su Optimism). Ma per fidarsi solo di certi messaggi, se provengono dal ponte dall'altra parte, serve il ponte su entrambi i lati.

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

Solo i messaggi dalla messaggistica interdominio appropriata (messenger, come vedi di seguito) sono affidabili.

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

Il modo in cui la messaggistica interdominio fornisce l'indirizzo che ha inviato un messaggio con l'altro livello è la funzione .xDomainMessageSender()(opens in a new tab). Se è chiamato 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 * Internal Functions *
7 **********************/
8
9 /**
10 * Gets the messenger, usually from storage. This function is exposed in case a child contract
11 * needs to override.
12 * @return The address of the cross-domain messenger contract which should be used.
13 */
14 function getCrossDomainMessenger() internal virtual returns (ICrossDomainMessenger) {
15 return ICrossDomainMessenger(messenger);
16 }
Mostra tutto
Copia

Questa funzione restituisce la messaggistica interdominio. Usiamo una funzione piuttosto che la variabile messenger per consentire ai contratti che ereditano da questa di usare un algoritmo per specificare quale messaggistica interdominio utilizzare.

1
2 /**
3 * Sends a message to an account on another domain
4 * @param _crossDomainTarget The intended recipient on the destination domain
5 * @param _message The data to send to the target (usually calldata to a function with
6 * `onlyFromCrossDomainAccount()`)
7 * @param _gasLimit The gasLimit for the receipt of the message on the target domain.
8 */
9 function sendCrossDomainMessage(
10 address _crossDomainTarget,
11 uint32 _gasLimit,
12 bytes memory _message
Mostra tutto
Copia

Infine, la funzione che invia un messaggio all'altro livello.

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

Slither(opens in a new tab) è un analizzatore statico che Optimism esegue su ogni contratto per cercare le vulnerabilità e altri problemi potenziali. In questo caso, la seguente riga innesca due vulnerabilità:

  1. Eventi di rientranza(opens in a new tab)
  2. Rientranza benigna(opens in a new tab)
1 getCrossDomainMessenger().sendMessage(_crossDomainTarget, _message, _gasLimit);
2 }
3}
Copia

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 di questo contratto è qui(opens in a new tab).

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

Le interfacce possono far parte di altri contratti, quindi devono supportare una vasta gamma di versioni di Solidity. Ma il ponte in sé è il nostro contratto, e possiamo essere rigidi sulla versione di Solidity utilizzata.

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

IL1ERC20Bridge e IL1StandardBridge sono spiegati sopra.

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

Quest'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";
Copia

Quest'interfaccia(opens in a new tab) ci consente di controllare i contratti ERC-20. Puoi approfondire questo argomento qui.

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

Come spiegato sopra, questo contratto è usato per la messaggistica tra livelli.

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

Lib_PredeployAddresses(opens in a new tab) contiene gli indirizzi per i contratti L2 che hanno sempre lo stesso indirizzo. Comprende il ponte standard su L2.

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

Utility per indirizzi di OpenZeppelin(opens in a new tab). Serve a distinguere tra gli indirizzi del contratto e quelli appartenenti a conti posseduti esternamente (EOA).

Non è una soluzione perfetta, perché non esiste modo di distinguere tra chiamate dirette e chiamate effettuate dal costruttore di un contratto ma, quantomeno, ci consente di identificare ed evitare alcuni errori comuni dell'utente.

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

Lo standard ERC-20(opens in a new tab) supporta due metodi di segnalazione del fallimento di un contratto:

  1. Ripristino
  2. Restituzione di false

Gestire entrambi i casi renderebbe il nostro codice più complicato, quindi, invece, usiamo SafeERC20 di OpenZeppelin(opens in a new tab), che si assicura che tutti i fallimenti portino a un ripristino(opens in a new tab).

1/**
2 * @title L1StandardBridge
3 * @dev The L1 ETH and ERC20 Bridge is a contract which stores deposited L1 funds and standard
4 * tokens that are in use on L2. It synchronizes a corresponding L2 Bridge, informing it of deposits
5 * and listening to it for newly finalized withdrawals.
6 *
7 */
8contract L1StandardBridge is IL1StandardBridge, CrossDomainEnabled {
9 using SafeERC20 for IERC20;
Mostra tutto
Copia

Questa riga specifica come usare il wrapper di SafeERC20, ogni volta che usiamo l'interfaccia di IERC20.

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

L'indirizzo di L2StandardBridge.

1
2 // Maps L1 token to L2 token to balance of the L1 token deposited
3 mapping(address => mapping(address => uint256)) public deposits;
Copia

Una doppia mappatura(opens in a new tab) come questa definisce un array sparso bidimensionale(opens in a new tab). I valori in questa struttura di dati sono identificati come deposit[L1 token addr][L2 token addr]. Il valore predefinito è zero. Solo le celle impostate a un valore differente sono scritte in memoria.

1
2 /***************
3 * Constructor *
4 ***************/
5
6 // This contract lives behind a proxy, so the constructor parameters will go unused.
7 constructor() CrossDomainEnabled(address(0)) {}
Copia

Per poter aggiornare questo contratto senza dover copiare tutte le variabili in memoria. Per farlo, usiamo un Proxy(opens in a new tab), un contratto che usa delegatecall(opens in a new tab) per trasferire le chiamate a un contratto distinto, il cui indirizzo è memorizzato dal contratto del proxy (quando aggiorni, dici al proxy di modificare tale indirizzo). Quando usi delegatecall, la memoria rimane quella del contratto chiamante, quindi non sono influenzati i valori di tutte le variabili di stato del contratto.

Un effetto di questo schema è che l'archiviazione del contratto, ovvero la chiamata di delegatecall, non è usata e dunque i valori del costruttore a esso passati non importano. Questo è il motivo per cui possiamo fornire un valore senza senso al costruttore di CrossDomainEnabled. È anche il motivo per cui l'inizializzazione di seguito è separata dal costruttore.

1 /******************
2 * Initialization *
3 ******************/
4
5 /**
6 * @param _l1messenger L1 Messenger address being used for cross-chain communications.
7 * @param _l2TokenBridge L2 standard bridge address.
8 */
9 // slither-disable-next-line external-function
Mostra tutto
Copia

Questo test di Slither(opens in a new tab), identifica le funzioni non chiamate dal codice del contratto e che potrebbero dunque esser dichiarate external invece che public. Il costo del gas delle funzioni external può essere inferiore, perché possono contenere dei parametri nei dati della chiamata. Le funzioni dichiarate come public devono esser accessibili dall'interno del contratto. I contratti non possono modificare i propri dati di chiamata, quindi, i parametri devono essere in memoria. Quando una funzione simile è chiamata esternamente, è necessario copiare i dati della chiamata alla memoria, il che costa gas. In questo caso la funzione è chiamata solo una volta, quindi, non siamo interessati alla sua inefficienza.

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

La funzione initialize dovrebbe esser chiamata solo una volta. Se cambia l'indirizzo della messaggistica interdominio di L1 o del ponte del token L2, creiamo un nuovo proxy e un nuovo ponte che lo chiama. È improbabile che si verifichi, tranne quando viene aggiornato l'intero sistema, un evento molto raro.

Nota che questa funzione non ha alcun meccanismo che limiti chi possa chiamarla. Questo significa che, in teoria, un malintenzionato potrebbe attendere la distribuzione del proxy e della prima versione del ponte e, poi, eseguire un front running(opens in a new tab) per ottenere la funzione initialize, prima dell'utente legittimo. Ma esistono due metodi per impedirlo:

  1. Se i contratti sono distribuiti indirettamente da un conto EOA, ma in una transazione avente un altro contratto che li crea(opens in a new tab), l'intero processo può esser atomico e terminare prima che ogni altra transazione sia eseguita.
  2. Se la chiamata legittima a initialize fallisce, è sempre possibile ignorare il proxy e il ponte appena creato e crearne di nuovi.
1 messenger = _l1messenger;
2 l2TokenBridge = _l2TokenBridge;
3 }
Copia

Questi sono i due parametri che il ponte deve conoscere.

1
2 /**************
3 * Depositing *
4 **************/
5
6 /** @dev Modifier requiring sender to be EOA. This check could be bypassed by a malicious
7 * contract via initcode, but it takes care of the user error we want to avoid.
8 */
9 modifier onlyEOA() {
10 // Used to stop deposits from contracts (avoid accidentally lost tokens)
11 require(!Address.isContract(msg.sender), "Account not EOA");
12 _;
13 }
Mostra tutto
Copia

Per questo abbiamo bisogno delle utility Address di OpenZeppelin.

1 /**
2 * @dev This function can be called with no data
3 * to deposit an amount of ETH to the caller's balance on L2.
4 * Since the receive function doesn't take data, a conservative
5 * default amount is forwarded to L2.
6 */
7 receive() external payable onlyEOA {
8 _initiateETHDeposit(msg.sender, msg.sender, 200_000, bytes(""));
9 }
Mostra tutto
Copia

Questa funzione serve per scopi di test. Non compare nelle definizioni dell'interfaccia: non è pensata per un uso ordinario.

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

Queste due funzioni sono wrapper intorno a _initiateETHDeposit, la funzione che gestisce l'effettivo deposito di ETH.

1 /**
2 * @dev Performs the logic for deposits by storing the ETH and informing the L2 ETH Gateway of
3 * the deposit.
4 * @param _from Account to pull the deposit from on L1.
5 * @param _to Account to give the deposit to on L2.
6 * @param _l2Gas Gas limit required to complete the deposit on L2.
7 * @param _data Optional data to forward to L2. This data is provided
8 * solely as a convenience for external contracts. Aside from enforcing a maximum
9 * length, these contracts provide no guarantees about its content.
10 */
11 function _initiateETHDeposit(
12 address _from,
13 address _to,
14 uint32 _l2Gas,
15 bytes memory _data
16 ) internal {
17 // Construct calldata for finalizeDeposit call
18 bytes memory message = abi.encodeWithSelector(
Mostra tutto
Copia

I messaggi interdominio funzionano chiamando il contratto di destinazione passando il messaggio come dati di chiamata. I contratti in Solidity interpretano sempre i propri dati di chiamata secondo le specifiche ABI(opens in a new tab). La funzione di Solidity abi.encodeWithSelector(opens in a new tab) crea questi dati di chiamata.

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

In questo caso il messaggio chiama la funzione finalizeDeposit(opens in a new tab) con questi parametri:

ParametroValoreSignificato
_l1Tokenaddress(0)Valore speciale che sta per ETH (che non è un token ERC-20) su L1
_l2TokenLib_PredeployAddresses.OVM_ETHIl contratto L2 che gestisce ETH su Optimism, 0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000 (questo contratto è solo per uso interno a Optimism)
_from_fromL'indirizzo su L1 che invia gli ETH
_to_toL'indirizzo su L2 che riceve gli ETH
amountmsg.valueImporto di wei inviato (già inviato al ponte)
_data_dataData aggiuntiva da allegare al deposito
1 // Send calldata into L2
2 // slither-disable-next-line reentrancy-events
3 sendCrossDomainMessage(l2TokenBridge, _l2Gas, message);
Copia

Invia il messaggio tramite la messaggistica interdominio.

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

Emette un evento per informare qualsiasi applicazione decentralizzata che ascolta questo trasferimento.

1 /**
2 * @inheritdoc IL1ERC20Bridge
3 */
4 function depositERC20(
5 .
6 .
7 .
8 ) external virtual onlyEOA {
9 _initiateERC20Deposit(_l1Token, _l2Token, msg.sender, msg.sender, _amount, _l2Gas, _data);
10 }
11
12 /**
13 * @inheritdoc IL1ERC20Bridge
14 */
15 function depositERC20To(
16 .
17 .
18 .
19 ) external virtual {
20 _initiateERC20Deposit(_l1Token, _l2Token, msg.sender, _to, _amount, _l2Gas, _data);
21 }
Mostra tutto
Copia

Queste due funzioni sono wrapper intorno a _initiateERC20Deposit, la funzione che gestisce l'effettivo deposito ERC-20.

1 /**
2 * @dev Performs the logic for deposits by informing the L2 Deposited Token
3 * contract of the deposit and calling a handler to lock the L1 funds. (e.g. transferFrom)
4 *
5 * @param _l1Token Address of the L1 ERC20 we are depositing
6 * @param _l2Token Address of the L1 respective L2 ERC20
7 * @param _from Account to pull the deposit from on L1
8 * @param _to Account to give the deposit to on L2
9 * @param _amount Amount of the ERC20 to deposit.
10 * @param _l2Gas Gas limit required to complete the deposit on L2.
11 * @param _data Optional data to forward to L2. This data is provided
12 * solely as a convenience for external contracts. Aside from enforcing a maximum
13 * length, these contracts provide no guarantees about its content.
14 */
15 function _initiateERC20Deposit(
16 address _l1Token,
17 address _l2Token,
18 address _from,
19 address _to,
20 uint256 _amount,
21 uint32 _l2Gas,
22 bytes calldata _data
23 ) internal {
Mostra tutto
Copia

Questa funzione è simile a _initiateETHDeposit di cui sopra, con alcune importanti differenze. La prima differenza è che questa funzione riceve come parametri gli indirizzi del token e l'importo da trasferire. Nel caso degli ETH, la chiamata al ponte include il trasferimento della risorsa al conto del ponte (msg.value).

1 // When a deposit is initiated on L1, the L1 Bridge transfers the funds to itself for future
2 // withdrawals. safeTransferFrom also checks if the contract has code, so this will fail if
3 // _from is an EOA or address(0).
4 // slither-disable-next-line reentrancy-events, reentrancy-benign
5 IERC20(_l1Token).safeTransferFrom(_from, address(this), _amount);
Copia

I trasferimenti di token ERC-20 seguono un processo differente rispetto agli ETH:

  1. L'utente (_from) dà un indennità al ponte per trasferire i token appropriati.
  2. L'utente chiama il ponte con l'indirizzo del contratto del token, l'importo, etc.
  3. Il ponte trasferisce i token (a sé stesso) nell'ambito del processo di deposito.

Il primo passaggio potrebbe verificarsi in una transazione separata dalle ultime due. Tuttavia, il front running non è un problema perché le due funzioni che chiamano _initiateERC20Deposit (depositERC20 e depositERC20To), chiamano questa funzione con msg.sender come solo parametro _from.

1 // Construct calldata for _l2Token.finalizeDeposit(_to, _amount)
2 bytes memory message = abi.encodeWithSelector(
3 IL2ERC20Bridge.finalizeDeposit.selector,
4 _l1Token,
5 _l2Token,
6 _from,
7 _to,
8 _amount,
9 _data
10 );
11
12 // Send calldata into L2
13 // slither-disable-next-line reentrancy-events, reentrancy-benign
14 sendCrossDomainMessage(l2TokenBridge, _l2Gas, message);
15
16 // slither-disable-next-line reentrancy-benign
17 deposits[_l1Token][_l2Token] = deposits[_l1Token][_l2Token] + _amount;
Mostra tutto
Copia

Aggiungi l'importo di token depositato alla struttura dei dati deposits. Potrebbero esistere diversi indirizzi su L2 corrispondenti allo stesso token L1 ERC-20, quindi non basta usare il saldo del ponte del token L1 ERC-20, per monitorare i depositi.

1
2 // slither-disable-next-line reentrancy-events
3 emit ERC20DepositInitiated(_l1Token, _l2Token, _from, _to, _amount, _data);
4 }
5
6 /*************************
7 * Cross-chain Functions *
8 *************************/
9
10 /**
11 * @inheritdoc IL1StandardBridge
12 */
13 function finalizeETHWithdrawal(
14 address _from,
15 address _to,
16 uint256 _amount,
17 bytes calldata _data
Mostra tutto
Copia

Il ponte L2 invia un messaggio alla messaggistica interdominio del L2, che fa sì che la messaggistica interdominio del L1 chiami questa funzione (una volta che la transazione che finalizza il messaggio(opens in a new tab) è inviata al L1, ovviamente).

1 ) external onlyFromCrossDomainAccount(l2TokenBridge) {
Copia

Assicurati che questo sia un messaggio legittimo, proveniente dalla messaggistica interdominio e proveniente dal ponte del token del L2. Questa funzione è usata per prelevare ETH dal ponte, quindi dobbiamo assicurarci che sia chiamata solo dal chiamante autorizzato.

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

Il metodo per trasferire ETH è chiamare il destinatario indicando l'importo di wei nel msg.value.

1 require(success, "TransferHelper::safeTransferETH: ETH transfer failed");
2
3 // slither-disable-next-line reentrancy-events
4 emit ETHWithdrawalFinalized(_from, _to, _amount, _data);
Copia

Genera un evento riguardante il prelievo.

1 }
2
3 /**
4 * @inheritdoc IL1ERC20Bridge
5 */
6 function finalizeERC20Withdrawal(
7 address _l1Token,
8 address _l2Token,
9 address _from,
10 address _to,
11 uint256 _amount,
12 bytes calldata _data
13 ) external onlyFromCrossDomainAccount(l2TokenBridge) {
Mostra tutto
Copia

Questa funzione è simile al precedente finalizeETHWithdrawal, con le modifiche necessarie per i token ERC-20.

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

Aggiorna la struttura dei dati di deposits.

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

Vi è stata un'implementazione precedente del ponte. Quando ci siamo spostati a questa nuova implementazione, abbiamo dovuto spostare tutte le risorse. I token ERC-20 possono essere semplicemente spostati. Al contrario, per trasferire ETH a un contratto, serve l'approvazione di quel contratto, e proprio questo a cui serve donateETH.

Token ERC-20 sul L2

Perché un token ERC-20 si adatti al ponte standard, deve consentire al ponte standard, e solo al ponte standard, di coniare il token. Questo è necessario perché i ponti devono assicurare che il numero di token in circolazione su Optimism sia pari al numero di token bloccati nel contratto del ponte del L1. Se esistono troppi token su L2, alcuni utenti non potrebbero ricollegare le proprie risorse al L1. Invece di un ponte fidato, ricreeremmo essenzialmente la riserva frazionaria bancaria(opens in a new tab). Se esistono troppi token su L1, alcuni di questi rimarrebbero bloccati nel contratto del ponte per sempre, perché non esiste modo di rilasciarli senza bruciare token del L2.

IL2StandardERC20

Ogni token ERC-20 sul L2 che usa il ponte standard deve presentare quest'interfaccia(opens in a new tab), che ha le funzioni e gli eventi necessari al ponte standard.

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

L'interfaccia standard dell'ERC-20(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 non specifica i meccanismi per creare e distruggere i token.

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

L'interfaccia ERC-165(opens in a new tab) è usata per specificare quali funzioni sono fornite da un contratto. Puoi leggere lo standard qui(opens in a new tab).

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

Questa funzione fornisce l'indirizzo del token L1, collegato a questo contratto. Nota che non esiste una funzione simile nella direzione opposta. Dobbiamo poter collegare qualsiasi token del L1, indipendentemente dal fatto che il supporto a L2 sia stato o meno pianificato alla 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}
Copia

Funzioni ed eventi per coniare (creare) e bruciare (distruggere) i token. Il ponte dovrebbe esser la sola entità capace d'eseguire queste funzioni per assicurare che il numero di token sia corretto (pari al numero di token bloccati su L1).

L2StandardERC20

Questa è la nostra implementazione dell'interfaccia di IL2StandardERC20(opens in a new tab). A meno che tu non necessiti di qualche tipo di logica personalizzata, dovresti usare questa.

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

Il contratto ERC-20 di OpenZeppelin(opens in a new tab). Optimism non vuole reinventare la ruota, specialmente quando questa è ben rodata e deve esser abbastanza affidabile da contenere delle risorse.

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

Questi sono due parametri di configurazione aggiuntivi che noi richiediamo e che invece ERC-20 normalmente non richiede.

1
2 /**
3 * @param _l2Bridge Address of the L2 standard bridge.
4 * @param _l1Token Address of the corresponding L1 token.
5 * @param _name ERC20 name.
6 * @param _symbol ERC20 symbol.
7 */
8 constructor(
9 address _l2Bridge,
10 address _l1Token,
11 string memory _name,
12 string memory _symbol
13 ) ERC20(_name, _symbol) {
14 l1Token = _l1Token;
15 l2Bridge = _l2Bridge;
16 }
Mostra tutto
Copia

Per prima cosa, 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-function
9 function supportsInterface(bytes4 _interfaceId) public pure returns (bool) {
10 bytes4 firstSupportedInterface = bytes4(keccak256("supportsInterface(bytes4)")); // ERC165
11 bytes4 secondSupportedInterface = IL2StandardERC20.l1Token.selector ^
12 IL2StandardERC20.mint.selector ^
13 IL2StandardERC20.burn.selector;
14 return _interfaceId == firstSupportedInterface || _interfaceId == secondSupportedInterface;
15 }
Mostra tutto
Copia

ERC-165(opens in a new tab) funziona così. Ogni interfaccia è un numero di funzioni supportate ed è identificata come l'OR esclusivo(opens in a new tab) dei selettori della funzione ABI(opens in a new tab) di queste funzioni.

Il ponte L2 usa ERC-165 come controllo di integrità per assicurarsi che il contratto ERC-20 a cui invia le risorse sia un IL2StandardERC20.

Nota: Non c'è nulla che impedisca che un contratto malevolo fornisca risposte false a supportsInterface, questo è quindi un meccanismo di controllo dell'integrità, non un meccanismo di sicurezza.

1 // slither-disable-next-line external-function
2 function mint(address _to, uint256 _amount) public virtual onlyL2Bridge {
3 _mint(_to, _amount);
4
5 emit Mint(_to, _amount);
6 }
7
8 // slither-disable-next-line external-function
9 function burn(address _from, uint256 _amount) public virtual onlyL2Bridge {
10 _burn(_from, _amount);
11
12 emit Burn(_from, _amount);
13 }
14}
Mostra tutto
Copia

Solo il ponte L2 può coniare e bruciare le risorse.

_mint e _burn sono in realtà definiti nel contratto ERC-20 di OpenZeppelin. Quel contratto non li espone esternamente, perché le condizioni per coniare e bruciare token sono tanto varie quanto il numero di metodi per usare ERC-20.

Codice del ponte di L2

Questo è il codice che esegue il ponte su Optimism. Il codice sorgente di questo contratto è qui(opens in a new tab).

1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.9;
3
4/* Interface Imports */
5import { IL1StandardBridge } from "../../L1/messaging/IL1StandardBridge.sol";
6import { IL1ERC20Bridge } from "../../L1/messaging/IL1ERC20Bridge.sol";
7import { IL2ERC20Bridge } from "./IL2ERC20Bridge.sol";
Copia

L'interfaccia di IL2ERC20Bridge(opens in a new tab) è molto simile all'equivalente di L1, visto in precedenza. Vi sono due differenze significative:

  1. Su L1, avvii i depositi e finalizzi i prelievi. Qui, avvii i prelievi e finalizzi i depositi.
  2. Su L1 è necessario distinguere tra ETH e token ERC-20. Su L2 possiamo usare 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/* Library Imports */
2import { ERC165Checker } from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol";
3import { CrossDomainEnabled } from "../../libraries/bridge/CrossDomainEnabled.sol";
4import { Lib_PredeployAddresses } from "../../libraries/constants/Lib_PredeployAddresses.sol";
5
6/* Contract Imports */
7import { IL2StandardERC20 } from "../../standards/IL2StandardERC20.sol";
8
9/**
10 * @title L2StandardBridge
11 * @dev The L2 Standard bridge is a contract which works together with the L1 Standard bridge to
12 * enable ETH and ERC20 transitions between L1 and L2.
13 * This contract acts as a minter for new tokens when it hears about deposits into the L1 Standard
14 * bridge.
15 * This contract also acts as a burner of the tokens intended for withdrawal, informing the L1
16 * bridge to release L1 funds.
17 */
18contract L2StandardBridge is IL2ERC20Bridge, CrossDomainEnabled {
19 /********************************
20 * External Contract References *
21 ********************************/
22
23 address public l1TokenBridge;
Mostra tutto
Copia

Tieni traccia dell'indirizzo del ponte L1. Nota che, a differenza dell'equivalente L1, qui serve questa variabile. L'indirizzo del ponte L1 non è noto preventivamente.

1
2 /***************
3 * Constructor *
4 ***************/
5
6 /**
7 * @param _l2CrossDomainMessenger Cross-domain messenger used by this contract.
8 * @param _l1TokenBridge Address of the L1 bridge deployed to the main chain.
9 */
10 constructor(address _l2CrossDomainMessenger, address _l1TokenBridge)
11 CrossDomainEnabled(_l2CrossDomainMessenger)
12 {
13 l1TokenBridge = _l1TokenBridge;
14 }
15
16 /***************
17 * Withdrawing *
18 ***************/
19
20 /**
21 * @inheritdoc IL2ERC20Bridge
22 */
23 function withdraw(
24 address _l2Token,
25 uint256 _amount,
26 uint32 _l1Gas,
27 bytes calldata _data
28 ) external virtual {
29 _initiateWithdrawal(_l2Token, msg.sender, msg.sender, _amount, _l1Gas, _data);
30 }
31
32 /**
33 * @inheritdoc IL2ERC20Bridge
34 */
35 function withdrawTo(
36 address _l2Token,
37 address _to,
38 uint256 _amount,
39 uint32 _l1Gas,
40 bytes calldata _data
41 ) external virtual {
42 _initiateWithdrawal(_l2Token, msg.sender, _to, _amount, _l1Gas, _data);
43 }
Mostra tutto
Copia

Queste due funzioni avviano dei prelievi. Nota che non serve specificare l'indirizzo del token L1. I token L2 dovrebbero dirci l'indirizzo dell'equivalente L1.

1
2 /**
3 * @dev Performs the logic for withdrawals by burning the token and informing
4 * the L1 token Gateway of the withdrawal.
5 * @param _l2Token Address of L2 token where withdrawal is initiated.
6 * @param _from Account to pull the withdrawal from on L2.
7 * @param _to Account to give the withdrawal to on L1.
8 * @param _amount Amount of the token to withdraw.
9 * @param _l1Gas Unused, but included for potential forward compatibility considerations.
10 * @param _data Optional data to forward to L1. This data is provided
11 * solely as a convenience for external contracts. Aside from enforcing a maximum
12 * length, these contracts provide no guarantees about its content.
13 */
14 function _initiateWithdrawal(
15 address _l2Token,
16 address _from,
17 address _to,
18 uint256 _amount,
19 uint32 _l1Gas,
20 bytes calldata _data
21 ) internal {
22 // When a withdrawal is initiated, we burn the withdrawer's funds to prevent subsequent L2
23 // usage
24 // slither-disable-next-line reentrancy-events
25 IL2StandardERC20(_l2Token).burn(msg.sender, _amount);
Mostra tutto
Copia

Nota che non ci stiamo affidando al parametro _from, ma su msg.sender, molto più difficile da falsificare (impossibile, per quanto ne so).

1
2 // Construct calldata for l1TokenBridge.finalizeERC20Withdrawal(_to, _amount)
3 // slither-disable-next-line reentrancy-events
4 address l1Token = IL2StandardERC20(_l2Token).l1Token();
5 bytes memory message;
6
7 if (_l2Token == Lib_PredeployAddresses.OVM_ETH) {
Copia

Su L1 è necessario distinguere tra ETH ed ERC-20.

1 message = abi.encodeWithSelector(
2 IL1StandardBridge.finalizeETHWithdrawal.selector,
3 _from,
4 _to,
5 _amount,
6 _data
7 );
8 } else {
9 message = abi.encodeWithSelector(
10 IL1ERC20Bridge.finalizeERC20Withdrawal.selector,
11 l1Token,
12 _l2Token,
13 _from,
14 _to,
15 _amount,
16 _data
17 );
18 }
19
20 // Send message up to L1 bridge
21 // slither-disable-next-line reentrancy-events
22 sendCrossDomainMessage(l1TokenBridge, _l1Gas, message);
23
24 // slither-disable-next-line reentrancy-events
25 emit WithdrawalInitiated(l1Token, _l2Token, msg.sender, _to, _amount, _data);
26 }
27
28 /************************************
29 * Cross-chain Function: Depositing *
30 ************************************/
31
32 /**
33 * @inheritdoc IL2ERC20Bridge
34 */
35 function finalizeDeposit(
36 address _l1Token,
37 address _l2Token,
38 address _from,
39 address _to,
40 uint256 _amount,
41 bytes calldata _data
Mostra tutto
Copia

Questa funzione è chiamata da L1StandardBridge.

1 ) external virtual onlyFromCrossDomainAccount(l1TokenBridge) {
Copia

Assicurati che la fonte del messaggio sia legittima. Questo è importante perché questa funzione chiama _mint e potrebbe esser usata per dare token non coperti dai token posseduti dal ponte su L1.

1 // Check the target token is compliant and
2 // verify the deposited token on L1 matches the L2 deposited token representation here
3 if (
4 // slither-disable-next-line reentrancy-events
5 ERC165Checker.supportsInterface(_l2Token, 0x1d1d8b63) &&
6 _l1Token == IL2StandardERC20(_l2Token).l1Token()
Copia

Controlli di integrità:

  1. L'interfaccia corretta è supportata
  2. L'indirizzo L1 del contratto ERC-20 del L2 corrisponde alla sorgente L1 dei token
1 ) {
2 // When a deposit is finalized, we credit the account on L2 with the same amount of
3 // tokens.
4 // slither-disable-next-line reentrancy-events
5 IL2StandardERC20(_l2Token).mint(_to, _amount);
6 // slither-disable-next-line reentrancy-events
7 emit DepositFinalized(_l1Token, _l2Token, _from, _to, _amount, _data);
Copia

Se il controllo di integrità riesce, finalizza il deposito:

  1. Conia i token
  2. Genera l'evento appropriato
1 } else {
2 // Either the L2 token which is being deposited-into disagrees about the correct address
3 // of its L1 token, or does not support the correct interface.
4 // This should only happen if there is a malicious L2 token, or if a user somehow
5 // specified the wrong L2 token address to deposit into.
6 // In either case, we stop the process here and construct a withdrawal
7 // message so that users can get their funds out in some cases.
8 // There is no way to prevent malicious token contracts altogether, but this does limit
9 // user error and mitigate some forms of malicious contract behavior.
Mostra tutto
Copia

Se un utente ha commesso un errore rilevabile usando l'indirizzo del token L2 errato, dobbiamo annullare il deposito e restituire i token sul L1. Il solo modo in cui possiamo farlo da L2 è inviare un messaggio che dovrà attendere il periodo di contestazione dell'errore, ma è molto meglio per l'utente rispetto a perdere permanentemente i token.

1 bytes memory message = abi.encodeWithSelector(
2 IL1ERC20Bridge.finalizeERC20Withdrawal.selector,
3 _l1Token,
4 _l2Token,
5 _to, // switched the _to and _from here to bounce back the deposit to the sender
6 _from,
7 _amount,
8 _data
9 );
10
11 // Send message up to L1 bridge
12 // slither-disable-next-line reentrancy-events
13 sendCrossDomainMessage(l1TokenBridge, 0, message);
14 // slither-disable-next-line reentrancy-events
15 emit DepositFailed(_l1Token, _l2Token, _from, _to, _amount, _data);
16 }
17 }
18}
Mostra tutto
Copia

Conclusioni

Il ponte standard è il meccanismo più flessibile per i trasferimenti di risorse. Tuttavia, essendo così generico, non è sempre il metodo più facile da usare. Specialmente per i prelievi, gran parte degli utenti preferisce usare ponti di terze parti(opens in a new tab) che non attendono il periodo di contestazione dell'errore e non richiedono una prova di Merkle per finalizzare il prelievo.

Questi ponti funzionano tipicamente avendo delle risorse sul L1, che forniscono immediatamente per una ridotta commissione (spesso inferiore al costo del gas per un prelievo del ponte standard). Quando il ponte (o le persone che lo gestiscono) prevede di avere poche risorse su L1, trasferisce delle sufficienti risorse da L2. Poiché questi sono prelievi molto grandi, il costo di prelievo è ammortizzato su un grande importo e ha un'incidenza minore.

Spero che questo articolo ti abbia aiutato a comprendere meglio come funziona il livello 2 e come scrivere un codice chiaro e sicuro in Solidity.

Ultima modifica: @Herbie_23(opens in a new tab), 8 dicembre 2023

Questo tutorial è stato utile?