Guida del ponte standard di Optimism per contratti
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
- In caso di deposito di un ERC-20, il depositante concede al ponte un'indennità per spendere l'importo depositato
- Il depositante chiama il ponte L1 (
depositERC20
,depositERC20To
,depositETH
, odepositETHTo
) - 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
- Il ponte L1 usa il meccanismo di messaggio interdominio per chiamare
finalizeDeposit
sul ponte L2
Livello 2
- Il ponte L2 verifica che la chiamata a
finalizeDeposit
sia legittima:- Proviene dal contratto di messaggistica interdominio
- Originariamente proveniva dal ponte su L1
- 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)).
- 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
- Il prelevante chiama il ponte L2 (
withdraw
owithdrawTo
) - Il ponte L2 brucia il giusto numero di token appartenente a
msg.sender
- Il ponte L2 usa il meccanismo di messaggio interdominio per chiamare
finalizeETHWithdrawal
ofinalizeERC20Withdrawal
sul ponte L1
Livello 1
- Il ponte L1 verifica che la chiamata a
finalizeETHWithdrawal
ofinalizeERC20Withdrawal
sia legittima:- Proviene dal meccanismo di messaggistica interdominio
- Originariamente proveniva dal ponte su L2
- 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: MITCopia
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 IL1ERC20Bridge3 */4interface IL1ERC20Bridge {5 /**********6 * Events *7 **********/89 event ERC20DepositInitiated(Mostra tuttoCopia
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 _data5 );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 _data8 );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.
12 /********************3 * Public Functions *4 ********************/56 /**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 tuttoCopia
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 depositing4 * @param _l2Token Address of the L1 respective L2 ERC205 * @param _amount Amount of the ERC20 to deposit6 * @param _l2Gas Gas limit required to complete the deposit on L2.7 * @param _data Optional data to forward to L2. This data is provided8 * solely as a convenience for external contracts. Aside from enforcing a maximum9 * 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 _data17 ) external;Mostra tuttoCopia
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 depositing4 * @param _l2Token Address of the L1 respective L2 ERC205 * @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 provided9 * solely as a convenience for external contracts. Aside from enforcing a maximum10 * 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 _data19 ) external;Mostra tuttoCopia
Questa funzione è quasi identica a depositERC20
, ma ti consente di inviare l'ERC-20 a un altro indirizzo.
1 /*************************2 * Cross-chain Functions *3 *************************/45 /**6 * @dev Complete a withdrawal from L2 to L1, and credit funds to the recipient's balance of the7 * 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 provided16 * solely as a convenience for external contracts. Aside from enforcing a maximum17 * 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 _data26 ) external;27}Mostra tuttoCopia
I prelievi (e altri messaggi da L2 a L1) su Optimism sono processi in due fasi:
- Una transazione di avvio su L2.
- 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: MIT2pragma solidity >0.5.0 <0.9.0;34import "./IL1ERC20Bridge.sol";56/**7 * @title IL1StandardBridge8 */9interface IL1StandardBridge is IL1ERC20Bridge {10 /**********11 * Events *12 **********/13 event ETHDepositInitiated(14 address indexed _from,15 address indexed _to,16 uint256 _amount,17 bytes _data18 );Mostra tuttoCopia
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 );67 /********************8 * Public Functions *9 ********************/1011 /**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;1819 /**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 _data29 ) external payable;3031 /*************************32 * Cross-chain Functions *33 *************************/3435 /**36 * @dev Complete a withdrawal from L2 to L1, and credit funds to the recipient's balance of the37 * L1 ETH token. Since only the xDomainMessenger can call this function, it will never be called38 * before the withdrawal is finalized.39 .40 .41 .42 */43 function finalizeETHWithdrawal(44 address _from,45 address _to,46 uint256 _amount,47 bytes calldata _data48 ) external;49}Mostra tuttoCopia
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: MIT2pragma solidity >0.5.0 <0.9.0;34/* 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 CrossDomainEnabled3 * @dev Helper contract for contracts performing cross-domain communications4 *5 * Compiler used: defined by inheriting contract6 */7contract CrossDomainEnabled {8 /*************9 * Variables *10 *************/1112 // Messenger contract used to send and receive messages from the other domain.13 address public messenger;1415 /***************16 * Constructor *17 ***************/1819 /**20 * @param _messenger Address of the CrossDomainMessenger on the current layer.21 */22 constructor(address _messenger) {23 messenger = _messenger;24 }Mostra tuttoCopia
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.
12 /**********************3 * Function Modifiers *4 **********************/56 /**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 is9 * authenticated to call this function.10 */11 modifier onlyFromCrossDomainAccount(address _sourceDomainAccount) {Mostra tuttoCopia
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.
12 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.
12 _;3 }45 /**********************6 * Internal Functions *7 **********************/89 /**10 * Gets the messenger, usually from storage. This function is exposed in case a child contract11 * 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 tuttoCopia
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.
12 /**3 * Sends a message to an account on another domain4 * @param _crossDomainTarget The intended recipient on the destination domain5 * @param _message The data to send to the target (usually calldata to a function with6 * `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 _messageMostra tuttoCopia
Infine, la funzione che invia un messaggio all'altro livello.
1 ) internal {2 // slither-disable-next-line reentrancy-events, reentrancy-benignCopia
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 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: MIT2pragma 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:
- Ripristino
- 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 L1StandardBridge3 * @dev The L1 ETH and ERC20 Bridge is a contract which stores deposited L1 funds and standard4 * tokens that are in use on L2. It synchronizes a corresponding L2 Bridge, informing it of deposits5 * and listening to it for newly finalized withdrawals.6 *7 */8contract L1StandardBridge is IL1StandardBridge, CrossDomainEnabled {9 using SafeERC20 for IERC20;Mostra tuttoCopia
Questa riga specifica come usare il wrapper di SafeERC20
, ogni volta che usiamo l'interfaccia di IERC20
.
12 /********************************3 * External Contract References *4 ********************************/56 address public l2TokenBridge;Copia
L'indirizzo di L2StandardBridge.
12 // Maps L1 token to L2 token to balance of the L1 token deposited3 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.
12 /***************3 * Constructor *4 ***************/56 // 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 ******************/45 /**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-functionMostra tuttoCopia
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:
- 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.
- 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.
12 /**************3 * Depositing *4 **************/56 /** @dev Modifier requiring sender to be EOA. This check could be bypassed by a malicious7 * 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 tuttoCopia
Per questo abbiamo bisogno delle utility Address
di OpenZeppelin.
1 /**2 * @dev This function can be called with no data3 * to deposit an amount of ETH to the caller's balance on L2.4 * Since the receive function doesn't take data, a conservative5 * default amount is forwarded to L2.6 */7 receive() external payable onlyEOA {8 _initiateETHDeposit(msg.sender, msg.sender, 200_000, bytes(""));9 }Mostra tuttoCopia
Questa funzione serve per scopi di test. Non compare nelle definizioni dell'interfaccia: non è pensata per un uso ordinario.
1 /**2 * @inheritdoc IL1StandardBridge3 */4 function depositETH(uint32 _l2Gas, bytes calldata _data) external payable onlyEOA {5 _initiateETHDeposit(msg.sender, msg.sender, _l2Gas, _data);6 }78 /**9 * @inheritdoc IL1StandardBridge10 */11 function depositETHTo(12 address _to,13 uint32 _l2Gas,14 bytes calldata _data15 ) external payable {16 _initiateETHDeposit(msg.sender, _to, _l2Gas, _data);17 }Mostra tuttoCopia
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 of3 * 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 provided8 * solely as a convenience for external contracts. Aside from enforcing a maximum9 * 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 _data16 ) internal {17 // Construct calldata for finalizeDeposit call18 bytes memory message = abi.encodeWithSelector(Mostra tuttoCopia
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 _data8 );Copia
In questo caso il messaggio chiama la funzione finalizeDeposit
(opens in a new tab) con questi parametri:
Parametro | Valore | Significato |
---|---|---|
_l1Token | address(0) | Valore speciale che sta per ETH (che non è un token ERC-20) su L1 |
_l2Token | Lib_PredeployAddresses.OVM_ETH | Il contratto L2 che gestisce ETH su Optimism, 0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000 (questo contratto è solo per uso interno a Optimism) |
_from | _from | L'indirizzo su L1 che invia gli ETH |
_to | _to | L'indirizzo su L2 che riceve gli ETH |
amount | msg.value | Importo di wei inviato (già inviato al ponte) |
_data | _data | Data aggiuntiva da allegare al deposito |
1 // Send calldata into L22 // slither-disable-next-line reentrancy-events3 sendCrossDomainMessage(l2TokenBridge, _l2Gas, message);Copia
Invia il messaggio tramite la messaggistica interdominio.
1 // slither-disable-next-line reentrancy-events2 emit ETHDepositInitiated(_from, _to, msg.value, _data);3 }Copia
Emette un evento per informare qualsiasi applicazione decentralizzata che ascolta questo trasferimento.
1 /**2 * @inheritdoc IL1ERC20Bridge3 */4 function depositERC20(5 .6 .7 .8 ) external virtual onlyEOA {9 _initiateERC20Deposit(_l1Token, _l2Token, msg.sender, msg.sender, _amount, _l2Gas, _data);10 }1112 /**13 * @inheritdoc IL1ERC20Bridge14 */15 function depositERC20To(16 .17 .18 .19 ) external virtual {20 _initiateERC20Deposit(_l1Token, _l2Token, msg.sender, _to, _amount, _l2Gas, _data);21 }Mostra tuttoCopia
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 Token3 * 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 depositing6 * @param _l2Token Address of the L1 respective L2 ERC207 * @param _from Account to pull the deposit from on L18 * @param _to Account to give the deposit to on L29 * @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 provided12 * solely as a convenience for external contracts. Aside from enforcing a maximum13 * 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 _data23 ) internal {Mostra tuttoCopia
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 future2 // withdrawals. safeTransferFrom also checks if the contract has code, so this will fail if3 // _from is an EOA or address(0).4 // slither-disable-next-line reentrancy-events, reentrancy-benign5 IERC20(_l1Token).safeTransferFrom(_from, address(this), _amount);Copia
I trasferimenti di token ERC-20 seguono un processo differente rispetto agli ETH:
- L'utente (
_from
) dà un indennità al ponte per trasferire i token appropriati. - L'utente chiama il ponte con l'indirizzo del contratto del token, l'importo, etc.
- 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 _data10 );1112 // Send calldata into L213 // slither-disable-next-line reentrancy-events, reentrancy-benign14 sendCrossDomainMessage(l2TokenBridge, _l2Gas, message);1516 // slither-disable-next-line reentrancy-benign17 deposits[_l1Token][_l2Token] = deposits[_l1Token][_l2Token] + _amount;Mostra tuttoCopia
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.
12 // slither-disable-next-line reentrancy-events3 emit ERC20DepositInitiated(_l1Token, _l2Token, _from, _to, _amount, _data);4 }56 /*************************7 * Cross-chain Functions *8 *************************/910 /**11 * @inheritdoc IL1StandardBridge12 */13 function finalizeETHWithdrawal(14 address _from,15 address _to,16 uint256 _amount,17 bytes calldata _dataMostra tuttoCopia
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-events2 (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");23 // slither-disable-next-line reentrancy-events4 emit ETHWithdrawalFinalized(_from, _to, _amount, _data);Copia
Genera un evento riguardante il prelievo.
1 }23 /**4 * @inheritdoc IL1ERC20Bridge5 */6 function finalizeERC20Withdrawal(7 address _l1Token,8 address _l2Token,9 address _from,10 address _to,11 uint256 _amount,12 bytes calldata _data13 ) external onlyFromCrossDomainAccount(l2TokenBridge) {Mostra tuttoCopia
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
.
12 // When a withdrawal is finalized on L1, the L1 Bridge transfers the funds to the withdrawer3 // slither-disable-next-line reentrancy-events4 IERC20(_l1Token).safeTransfer(_to, _amount);56 // slither-disable-next-line reentrancy-events7 emit ERC20WithdrawalFinalized(_l1Token, _l2Token, _from, _to, _amount, _data);8 }91011 /*****************************12 * Temporary - Migrating ETH *13 *****************************/1415 /**16 * @dev Adds ETH balance to the account. This is meant to allow for ETH17 * 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 the19 * old contract20 */21 function donateETH() external payable {}22}Mostra tuttoCopia
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: MIT2pragma solidity ^0.8.9;34import { 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.
12 function mint(address _to, uint256 _amount) external;34 function burn(address _from, uint256 _amount) external;56 event Mint(address indexed _account, uint256 _amount);7 event Burn(address indexed _account, uint256 _amount);8}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: MIT2pragma solidity ^0.8.9;34import { 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";23contract 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.
12 /**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 _symbol13 ) ERC20(_name, _symbol) {14 l1Token = _l1Token;15 l2Bridge = _l2Bridge;16 }Mostra tuttoCopia
Per prima cosa, chiama il costruttore per il contratto da cui ereditiamo (ERC20(_name, _symbol)
) e poi imposta le nostre variabili.
12 modifier onlyL2Bridge() {3 require(msg.sender == l2Bridge, "Only L2 Bridge can mint and burn");4 _;5 }678 // slither-disable-next-line external-function9 function supportsInterface(bytes4 _interfaceId) public pure returns (bool) {10 bytes4 firstSupportedInterface = bytes4(keccak256("supportsInterface(bytes4)")); // ERC16511 bytes4 secondSupportedInterface = IL2StandardERC20.l1Token.selector ^12 IL2StandardERC20.mint.selector ^13 IL2StandardERC20.burn.selector;14 return _interfaceId == firstSupportedInterface || _interfaceId == secondSupportedInterface;15 }Mostra tuttoCopia
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-function2 function mint(address _to, uint256 _amount) public virtual onlyL2Bridge {3 _mint(_to, _amount);45 emit Mint(_to, _amount);6 }78 // slither-disable-next-line external-function9 function burn(address _from, uint256 _amount) public virtual onlyL2Bridge {10 _burn(_from, _amount);1112 emit Burn(_from, _amount);13 }14}Mostra tuttoCopia
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: MIT2pragma solidity ^0.8.9;34/* Interface Imports */5import { IL1StandardBridge } from "../../L1/messaging/IL1StandardBridge.sol";6import { IL1ERC20Bridge } from "../../L1/messaging/IL1ERC20Bridge.sol";7import { IL2ERC20Bridge } from "./IL2ERC20Bridge.sol";Copia
L'interfaccia di IL2ERC20Bridge(opens in a new tab) è molto simile all'equivalente di L1, visto in precedenza. Vi sono due differenze significative:
- Su L1, avvii i depositi e finalizzi i prelievi. Qui, avvii i prelievi e finalizzi i depositi.
- 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";56/* Contract Imports */7import { IL2StandardERC20 } from "../../standards/IL2StandardERC20.sol";89/**10 * @title L2StandardBridge11 * @dev The L2 Standard bridge is a contract which works together with the L1 Standard bridge to12 * 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 Standard14 * bridge.15 * This contract also acts as a burner of the tokens intended for withdrawal, informing the L116 * bridge to release L1 funds.17 */18contract L2StandardBridge is IL2ERC20Bridge, CrossDomainEnabled {19 /********************************20 * External Contract References *21 ********************************/2223 address public l1TokenBridge;Mostra tuttoCopia
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.
12 /***************3 * Constructor *4 ***************/56 /**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 }1516 /***************17 * Withdrawing *18 ***************/1920 /**21 * @inheritdoc IL2ERC20Bridge22 */23 function withdraw(24 address _l2Token,25 uint256 _amount,26 uint32 _l1Gas,27 bytes calldata _data28 ) external virtual {29 _initiateWithdrawal(_l2Token, msg.sender, msg.sender, _amount, _l1Gas, _data);30 }3132 /**33 * @inheritdoc IL2ERC20Bridge34 */35 function withdrawTo(36 address _l2Token,37 address _to,38 uint256 _amount,39 uint32 _l1Gas,40 bytes calldata _data41 ) external virtual {42 _initiateWithdrawal(_l2Token, msg.sender, _to, _amount, _l1Gas, _data);43 }Mostra tuttoCopia
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.
12 /**3 * @dev Performs the logic for withdrawals by burning the token and informing4 * 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 provided11 * solely as a convenience for external contracts. Aside from enforcing a maximum12 * 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 _data21 ) internal {22 // When a withdrawal is initiated, we burn the withdrawer's funds to prevent subsequent L223 // usage24 // slither-disable-next-line reentrancy-events25 IL2StandardERC20(_l2Token).burn(msg.sender, _amount);Mostra tuttoCopia
Nota che non ci stiamo affidando al parametro _from
, ma su msg.sender
, molto più difficile da falsificare (impossibile, per quanto ne so).
12 // Construct calldata for l1TokenBridge.finalizeERC20Withdrawal(_to, _amount)3 // slither-disable-next-line reentrancy-events4 address l1Token = IL2StandardERC20(_l2Token).l1Token();5 bytes memory message;67 if (_l2Token == Lib_PredeployAddresses.OVM_ETH) {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 _data7 );8 } else {9 message = abi.encodeWithSelector(10 IL1ERC20Bridge.finalizeERC20Withdrawal.selector,11 l1Token,12 _l2Token,13 _from,14 _to,15 _amount,16 _data17 );18 }1920 // Send message up to L1 bridge21 // slither-disable-next-line reentrancy-events22 sendCrossDomainMessage(l1TokenBridge, _l1Gas, message);2324 // slither-disable-next-line reentrancy-events25 emit WithdrawalInitiated(l1Token, _l2Token, msg.sender, _to, _amount, _data);26 }2728 /************************************29 * Cross-chain Function: Depositing *30 ************************************/3132 /**33 * @inheritdoc IL2ERC20Bridge34 */35 function finalizeDeposit(36 address _l1Token,37 address _l2Token,38 address _from,39 address _to,40 uint256 _amount,41 bytes calldata _dataMostra tuttoCopia
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 and2 // verify the deposited token on L1 matches the L2 deposited token representation here3 if (4 // slither-disable-next-line reentrancy-events5 ERC165Checker.supportsInterface(_l2Token, 0x1d1d8b63) &&6 _l1Token == IL2StandardERC20(_l2Token).l1Token()Copia
Controlli di integrità:
- L'interfaccia corretta è supportata
- 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 of3 // tokens.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);Copia
Se il controllo di integrità riesce, finalizza il deposito:
- Conia i token
- Genera l'evento appropriato
1 } else {2 // Either the L2 token which is being deposited-into disagrees about the correct address3 // 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 somehow5 // specified the wrong L2 token address to deposit into.6 // In either case, we stop the process here and construct a withdrawal7 // 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 limit9 // user error and mitigate some forms of malicious contract behavior.Mostra tuttoCopia
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 sender6 _from,7 _amount,8 _data9 );1011 // Send message up to L1 bridge12 // 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}Mostra tuttoCopia
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: @lukassim(opens in a new tab), 26 aprile 2024