मुख्य सामग्री पर जाएं

ऑप्टिमिज़्म स्टैंडर्ड सेतु अनुबंध वॉकथ्रू

solidity
सेतु
लेयर 2 (l2)
मध्यवर्ती
ओरी पोमेरेंट्ज़
30 मार्च 2022
39 मिनट पढ़ें

ऑप्टिमिज़्म (opens in a new tab) एक ऑप्टिमिस्टिक रोलअप है। ऑप्टिमिस्टिक रोलअप्स इथेरियम मेननेट (जिसे लेयर 1 (l1) के रूप में भी जाना जाता है) की तुलना में बहुत कम कीमत पर लेन-देन को प्रोसेस कर सकते हैं क्योंकि लेन-देन नेटवर्क के हर नोड के बजाय केवल कुछ नोड्स द्वारा प्रोसेस किए जाते हैं। साथ ही, सारा डेटा L1 पर लिखा जाता है ताकि मेननेट की सभी अखंडता और उपलब्धता की गारंटी के साथ सब कुछ साबित और फिर से बनाया जा सके।

ऑप्टिमिज़्म (या किसी अन्य L2) पर L1 संपत्तियों का उपयोग करने के लिए, संपत्तियों को सेतु के माध्यम से ट्रांसफर करने की आवश्यकता होती है। इसे प्राप्त करने का एक तरीका यह है कि उपयोगकर्ता L1 पर संपत्तियों (ETH और ERC-20 टोकन सबसे आम हैं) को लॉक करें, और L2 पर उपयोग करने के लिए समान संपत्तियां प्राप्त करें। अंततः, जिसके पास भी वे संपत्तियां होंगी, वह उन्हें वापस L1 पर सेतु के माध्यम से ट्रांसफर करना चाह सकता है। ऐसा करते समय, संपत्तियों को L2 पर बर्न कर दिया जाता है और फिर L1 पर उपयोगकर्ता को वापस जारी कर दिया जाता है।

इसी तरह से ऑप्टिमिज़्म मानक सेतु (opens in a new tab) काम करता है। इस लेख में हम उस सेतु के स्रोत कोड की समीक्षा करेंगे ताकि यह देखा जा सके कि यह कैसे काम करता है और इसे अच्छी तरह से लिखे गए Solidity कोड के उदाहरण के रूप में अध्ययन करेंगे।

नियंत्रण प्रवाह (Control flows)

सेतु के दो मुख्य प्रवाह हैं:

  • जमा (L1 से L2 तक)
  • निकासी (L2 से L1 तक)

जमा प्रवाह

लेयर 1 (l1)

  1. यदि ERC-20 जमा कर रहे हैं, तो जमाकर्ता सेतु को जमा की जा रही राशि खर्च करने की व्यय सीमा (allowance) देता है
  2. जमाकर्ता L1 सेतु को कॉल करता है (depositERC20, depositERC20To, depositETH, या depositETHTo)
  3. L1 सेतु ब्रिज की गई संपत्ति का कब्ज़ा ले लेता है
    • ETH: संपत्ति को जमाकर्ता द्वारा कॉल के हिस्से के रूप में ट्रांसफर किया जाता है
    • ERC-20: जमाकर्ता द्वारा प्रदान की गई व्यय सीमा का उपयोग करके सेतु द्वारा संपत्ति को स्वयं को ट्रांसफर किया जाता है
  4. L1 सेतु L2 सेतु पर finalizeDeposit को कॉल करने के लिए क्रॉस-डोमेन संदेश तंत्र का उपयोग करता है

लेयर 2 (l2)

  1. L2 सेतु सत्यापित करता है कि finalizeDeposit पर कॉल वैध है:
    • क्रॉस डोमेन संदेश अनुबंध से आया है
    • मूल रूप से L1 पर सेतु से था
  2. L2 सेतु जांचता है कि क्या L2 पर ERC-20 टोकन अनुबंध सही है:
    • L2 अनुबंध रिपोर्ट करता है कि इसका L1 समकक्ष वही है जहां से L1 पर टोकन आए थे
    • L2 अनुबंध रिपोर्ट करता है कि यह सही इंटरफ़ेस का समर्थन करता है (ERC-165 का उपयोग करके (opens in a new tab))।
  3. यदि L2 अनुबंध सही है, तो उचित पते पर उचित संख्या में टोकन मिंट करने के लिए इसे कॉल करें। यदि नहीं, तो उपयोगकर्ता को L1 पर टोकन का दावा करने की अनुमति देने के लिए निकासी प्रक्रिया शुरू करें।

निकासी प्रवाह

लेयर 2 (l2)

  1. निकासी करने वाला L2 सेतु को कॉल करता है (withdraw या withdrawTo)
  2. L2 सेतु msg.sender से संबंधित उचित संख्या में टोकन बर्न करता है
  3. L2 सेतु L1 सेतु पर finalizeETHWithdrawal या finalizeERC20Withdrawal को कॉल करने के लिए क्रॉस-डोमेन संदेश तंत्र का उपयोग करता है

लेयर 1 (l1)

  1. L1 सेतु सत्यापित करता है कि finalizeETHWithdrawal या finalizeERC20Withdrawal पर कॉल वैध है:
    • क्रॉस डोमेन संदेश तंत्र से आया है
    • मूल रूप से L2 पर सेतु से था
  2. L1 सेतु उचित संपत्ति (ETH या ERC-20) को उचित पते पर ट्रांसफर करता है

लेयर 1 (l1) कोड

यह वह कोड है जो L1, इथेरियम मेननेट पर चलता है।

IL1ERC20Bridge

यह इंटरफ़ेस यहाँ परिभाषित किया गया है (opens in a new tab)। इसमें ERC-20 टोकन को सेतु के माध्यम से ट्रांसफर करने के लिए आवश्यक फ़ंक्शन और परिभाषाएँ शामिल हैं।

// SPDX-License-Identifier: MIT

ऑप्टिमिज़्म का अधिकांश कोड MIT लाइसेंस के तहत जारी किया गया है (opens in a new tab)

pragma solidity >0.5.0 <0.9.0;

लिखते समय Solidity का नवीनतम संस्करण 0.8.12 है। जब तक संस्करण 0.9.0 जारी नहीं हो जाता, हम नहीं जानते कि यह कोड इसके साथ संगत है या नहीं।

ऑप्टिमिज़्म सेतु शब्दावली में जमा (deposit) का अर्थ L1 से L2 में ट्रांसफर है, और निकासी (withdrawal) का अर्थ L2 से L1 में ट्रांसफर है।

        address indexed _l1Token,
        address indexed _l2Token,

ज्यादातर मामलों में L1 पर ERC-20 का पता L2 पर समकक्ष ERC-20 के पते के समान नहीं होता है। आप यहाँ टोकन पतों की सूची देख सकते हैं (opens in a new tab)chainId 1 वाला पता L1 (मेननेट) पर है और chainId 10 वाला पता L2 (ऑप्टिमिज़्म) पर है। अन्य दो chainId मान Kovan टेस्ट नेटवर्क (42) और Optimistic Kovan टेस्ट नेटवर्क (69) के लिए हैं।

        address indexed _from,
        address _to,
        uint256 _amount,
        bytes _data
    );

ट्रांसफर में नोट्स जोड़ना संभव है, इस स्थिति में उन्हें उन घटनाओं में जोड़ा जाता है जो उनकी रिपोर्ट करती हैं।

    event ERC20WithdrawalFinalized(
        address indexed _l1Token,
        address indexed _l2Token,
        address indexed _from,
        address _to,
        uint256 _amount,
        bytes _data
    );

वही सेतु अनुबंध दोनों दिशाओं में ट्रांसफर को संभालता है। L1 सेतु के मामले में, इसका मतलब जमा की शुरुआत और निकासी को अंतिम रूप देना है।

इस फ़ंक्शन की वास्तव में आवश्यकता नहीं है, क्योंकि L2 पर यह पहले से तैनात (predeployed) अनुबंध है, इसलिए यह हमेशा 0x4200000000000000000000000000000000000010 पते पर होता है। यह यहाँ L2 सेतु के साथ समरूपता के लिए है, क्योंकि L1 सेतु का पता जानना आसान नहीं है।

_l2Gas पैरामीटर L2 गैस की वह मात्रा है जिसे लेन-देन को खर्च करने की अनुमति है। एक निश्चित (उच्च) सीमा तक, यह मुफ़्त है (opens in a new tab), इसलिए जब तक ERC-20 अनुबंध मिंटिंग करते समय कुछ बहुत अजीब नहीं करता, तब तक यह कोई समस्या नहीं होनी चाहिए। यह फ़ंक्शन उस सामान्य परिदृश्य का ध्यान रखता है, जहाँ कोई उपयोगकर्ता संपत्तियों को एक अलग ब्लॉकचेन पर उसी पते पर सेतु के माध्यम से ट्रांसफर करता है।

यह फ़ंक्शन लगभग depositERC20 के समान है, लेकिन यह आपको ERC-20 को एक अलग पते पर भेजने की अनुमति देता है।

ऑप्टिमिज़्म में निकासी (और L2 से L1 तक अन्य संदेश) एक दो-चरणीय प्रक्रिया है:

  1. L2 पर एक प्रारंभिक लेन-देन।
  2. L1 पर एक अंतिम रूप देने वाला या दावा करने वाला लेन-देन। यह लेन-देन L2 लेन-देन के लिए दोष चुनौती अवधि (fault challenge period) (opens in a new tab) समाप्त होने के बाद होना चाहिए।

IL1StandardBridge

यह इंटरफ़ेस यहाँ परिभाषित किया गया है (opens in a new tab)। इस फ़ाइल में ETH के लिए घटना और फ़ंक्शन परिभाषाएँ शामिल हैं। ये परिभाषाएँ ERC-20 के लिए ऊपर IL1ERC20Bridge में परिभाषित परिभाषाओं के समान हैं।

सेतु इंटरफ़ेस को दो फ़ाइलों के बीच विभाजित किया गया है क्योंकि कुछ ERC-20 टोकन को कस्टम प्रोसेसिंग की आवश्यकता होती है और उन्हें मानक सेतु द्वारा नियंत्रित नहीं किया जा सकता है। इस तरह कस्टम सेतु जो ऐसे टोकन को संभालता है, IL1ERC20Bridge को लागू कर सकता है और उसे ETH को भी सेतु के माध्यम से ट्रांसफर करने की आवश्यकता नहीं होती है।

यह घटना ERC-20 संस्करण (ERC20DepositInitiated) के लगभग समान है, सिवाय इसके कि इसमें L1 और L2 टोकन पते नहीं हैं। यही बात अन्य घटनाओं और फ़ंक्शंस के लिए भी सच है।

CrossDomainEnabled

यह अनुबंध (opens in a new tab) दोनों सेतुओं (L1 और L2) द्वारा दूसरी लेयर पर संदेश भेजने के लिए इनहेरिट किया जाता है।

// SPDX-License-Identifier: MIT
pragma solidity >0.5.0 <0.9.0;

/* इंटरफ़ेस आयात */
import { ICrossDomainMessenger } from "./ICrossDomainMessenger.sol";

यह इंटरफ़ेस (opens in a new tab) अनुबंध को बताता है कि क्रॉस डोमेन मैसेंजर का उपयोग करके दूसरी लेयर पर संदेश कैसे भेजें। यह क्रॉस डोमेन मैसेंजर एक पूरी तरह से अलग प्रणाली है, और इसके लिए एक अलग लेख की आवश्यकता है, जिसे मैं भविष्य में लिखने की उम्मीद करता हूँ।

एक पैरामीटर जो अनुबंध को जानने की आवश्यकता है, वह इस लेयर पर क्रॉस डोमेन मैसेंजर का पता है। यह पैरामीटर कंस्ट्रक्टर में एक बार सेट किया जाता है, और कभी नहीं बदलता है।

क्रॉस डोमेन मैसेजिंग उस ब्लॉकचेन पर किसी भी अनुबंध द्वारा सुलभ है जहां यह चल रहा है (या तो इथेरियम मेननेट या ऑप्टिमिज़्म)। लेकिन हमें प्रत्येक पक्ष के सेतु की आवश्यकता है कि वह केवल कुछ संदेशों पर भरोसा करे यदि वे दूसरे पक्ष के सेतु से आते हैं।

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

केवल उपयुक्त क्रॉस डोमेन मैसेंजर (messenger, जैसा कि आप नीचे देखते हैं) के संदेशों पर भरोसा किया जा सकता है।


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

क्रॉस डोमेन मैसेंजर जिस तरह से वह पता प्रदान करता है जिसने दूसरी लेयर के साथ संदेश भेजा था, वह .xDomainMessageSender() फ़ंक्शन (opens in a new tab) है। जब तक इसे उस लेन-देन में कॉल किया जाता है जो संदेश द्वारा शुरू किया गया था, यह यह जानकारी प्रदान कर सकता है।

हमें यह सुनिश्चित करने की आवश्यकता है कि हमें जो संदेश मिला है वह दूसरे सेतु से आया है।

यह फ़ंक्शन क्रॉस डोमेन मैसेंजर लौटाता है। हम चर messenger के बजाय एक फ़ंक्शन का उपयोग करते हैं ताकि इस अनुबंध से इनहेरिट करने वाले अनुबंधों को यह निर्दिष्ट करने के लिए एक एल्गोरिदम का उपयोग करने की अनुमति मिल सके कि किस क्रॉस डोमेन मैसेंजर का उपयोग करना है।

अंत में, वह फ़ंक्शन जो दूसरी लेयर पर संदेश भेजता है।

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

स्लिथर (opens in a new tab) एक स्थिर विश्लेषक (static analyzer) है जिसे ऑप्टिमिज़्म कमजोरियों और अन्य संभावित समस्याओं की तलाश के लिए हर अनुबंध पर चलाता है। इस मामले में, निम्नलिखित पंक्ति दो कमजोरियों को ट्रिगर करती है:

  1. पुन:प्रवेश (reentrancy) घटनाएँ (opens in a new tab)
  2. सौम्य पुन:प्रवेश (Benign reentrancy) (opens in a new tab)
        getCrossDomainMessenger().sendMessage(_crossDomainTarget, _message, _gasLimit);
    }
}

इस मामले में हम पुन:प्रवेश के बारे में चिंतित नहीं हैं, हम जानते हैं कि getCrossDomainMessenger() एक भरोसेमंद पता लौटाता है, भले ही स्लिथर के पास यह जानने का कोई तरीका न हो।

L1 सेतु अनुबंध

इस अनुबंध का स्रोत कोड यहाँ है (opens in a new tab)

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

इंटरफ़ेस अन्य अनुबंधों का हिस्सा हो सकते हैं, इसलिए उन्हें Solidity संस्करणों की एक विस्तृत श्रृंखला का समर्थन करना होगा। लेकिन सेतु स्वयं हमारा अनुबंध है, और हम इस बारे में सख्त हो सकते हैं कि यह किस Solidity संस्करण का उपयोग करता है।

/* इंटरफ़ेस आयात */
import { IL1StandardBridge } from "./IL1StandardBridge.sol";
import { IL1ERC20Bridge } from "./IL1ERC20Bridge.sol";

IL1ERC20Bridge और IL1StandardBridge को ऊपर समझाया गया है।

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

यह इंटरफ़ेस (opens in a new tab) हमें L2 पर मानक सेतु को नियंत्रित करने के लिए संदेश बनाने देता है।

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

यह इंटरफ़ेस (opens in a new tab) हमें ERC-20 अनुबंधों को नियंत्रित करने देता है। आप इसके बारे में यहाँ और पढ़ सकते हैं

/* लाइब्रेरी आयात */
import { CrossDomainEnabled } from "../../libraries/bridge/CrossDomainEnabled.sol";

जैसा कि ऊपर बताया गया है, इस अनुबंध का उपयोग इंटरलेयर मैसेजिंग के लिए किया जाता है।

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

Lib_PredeployAddresses (opens in a new tab) में L2 अनुबंधों के पते हैं जिनका हमेशा एक ही पता होता है। इसमें L2 पर मानक सेतु शामिल है।

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

ओपनजेपेलिन की Address उपयोगिताएँ (opens in a new tab)। इसका उपयोग अनुबंध पतों और बाह्य रूप से स्वामित्व वाले खातों (EOA) से संबंधित पतों के बीच अंतर करने के लिए किया जाता है।

ध्यान दें कि यह एक सही समाधान नहीं है, क्योंकि प्रत्यक्ष कॉल और अनुबंध के कंस्ट्रक्टर से की गई कॉल के बीच अंतर करने का कोई तरीका नहीं है, लेकिन कम से कम यह हमें कुछ सामान्य उपयोगकर्ता त्रुटियों को पहचानने और रोकने देता है।

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

ERC-20 मानक (opens in a new tab) किसी अनुबंध के लिए विफलता की रिपोर्ट करने के दो तरीकों का समर्थन करता है:

  1. रिवर्ट
  2. false लौटाएं

दोनों मामलों को संभालने से हमारा कोड अधिक जटिल हो जाएगा, इसलिए इसके बजाय हम ओपनजेपेलिन के SafeERC20 (opens in a new tab) का उपयोग करते हैं, जो यह सुनिश्चित करता है कि सभी विफलताओं का परिणाम रिवर्ट हो (opens in a new tab)

यह पंक्ति बताती है कि हम हर बार IERC20 इंटरफ़ेस का उपयोग करते समय SafeERC20 रैपर का उपयोग कैसे निर्दिष्ट करते हैं।


    /********************************
     * बाहरी अनुबंध संदर्भ *
     ********************************/

    address public l2TokenBridge;

L2StandardBridge का पता।


    // जमा किए गए लेयर 1 (l1) टोकन के बैलेंस के लिए लेयर 1 (l1) टोकन को लेयर 2 (l2) टोकन से मैप करता है
    mapping(address => mapping(address => uint256)) public deposits;

इस तरह की दोहरी मैपिंग (opens in a new tab) वह तरीका है जिससे आप दो-आयामी स्पार्स एरे (two-dimensional sparse array) (opens in a new tab) को परिभाषित करते हैं। इस डेटा संरचना में मानों को deposit[L1 token addr][L2 token addr] के रूप में पहचाना जाता है। डिफ़ॉल्ट मान शून्य है। केवल वे सेल जो एक अलग मान पर सेट हैं, स्टोरेज में लिखे जाते हैं।


    /***************
     * कंस्ट्रक्टर *
     ***************/

    // यह अनुबंध एक प्रॉक्सी के पीछे रहता है, इसलिए कंस्ट्रक्टर पैरामीटर अप्रयुक्त रहेंगे।
    constructor() CrossDomainEnabled(address(0)) {}

स्टोरेज में सभी चरों को कॉपी किए बिना इस अनुबंध को अपग्रेड करने में सक्षम होना चाहते हैं। ऐसा करने के लिए हम एक Proxy (opens in a new tab) का उपयोग करते हैं, एक अनुबंध जो कॉल को एक अलग अनुबंध में ट्रांसफर करने के लिए delegatecall (opens in a new tab) का उपयोग करता है जिसका पता प्रॉक्सी कॉन्ट्रैक्ट द्वारा संग्रहीत किया जाता है (जब आप अपग्रेड करते हैं तो आप प्रॉक्सी को उस पते को बदलने के लिए कहते हैं)। जब आप delegatecall का उपयोग करते हैं तो स्टोरेज कॉल करने वाले अनुबंध का स्टोरेज बना रहता है, इसलिए सभी अनुबंध स्थिति (state) चरों के मान अप्रभावित रहते हैं।

इस पैटर्न का एक प्रभाव यह है कि उस अनुबंध का स्टोरेज जो delegatecall का कॉल किया गया है, उपयोग नहीं किया जाता है और इसलिए इसे पास किए गए कंस्ट्रक्टर मान मायने नहीं रखते हैं। यही कारण है कि हम CrossDomainEnabled कंस्ट्रक्टर को एक निरर्थक मान प्रदान कर सकते हैं। यही कारण है कि नीचे दिया गया इनिशियलाइज़ेशन कंस्ट्रक्टर से अलग है।

यह स्लिथर परीक्षण (opens in a new tab) उन फ़ंक्शंस की पहचान करता है जिन्हें अनुबंध कोड से कॉल नहीं किया जाता है और इसलिए उन्हें public के बजाय external घोषित किया जा सकता है। external फ़ंक्शंस की गैस लागत कम हो सकती है, क्योंकि उन्हें कॉल डेटा (calldata) में पैरामीटर प्रदान किए जा सकते हैं। public घोषित फ़ंक्शंस को अनुबंध के भीतर से सुलभ होना चाहिए। अनुबंध अपने स्वयं के कॉल डेटा को संशोधित नहीं कर सकते हैं, इसलिए पैरामीटर मेमोरी में होने चाहिए। जब ऐसे फ़ंक्शन को बाहरी रूप से कॉल किया जाता है, तो कॉल डेटा को मेमोरी में कॉपी करना आवश्यक होता है, जिसमें गैस खर्च होती है। इस मामले में फ़ंक्शन को केवल एक बार कॉल किया जाता है, इसलिए अक्षमता हमारे लिए मायने नहीं रखती है।

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

initialize फ़ंक्शन को केवल एक बार कॉल किया जाना चाहिए। यदि L1 क्रॉस डोमेन मैसेंजर या L2 टोकन सेतु का पता बदलता है, तो हम एक नया प्रॉक्सी और एक नया सेतु बनाते हैं जो इसे कॉल करता है। ऐसा होने की संभावना नहीं है सिवाय इसके कि जब पूरी प्रणाली को अपग्रेड किया जाए, जो एक बहुत ही दुर्लभ घटना है।

ध्यान दें कि इस फ़ंक्शन में ऐसा कोई तंत्र नहीं है जो यह प्रतिबंधित करता हो कि इसे कौन कॉल कर सकता है। इसका मतलब यह है कि सिद्धांत रूप में एक हमलावर तब तक इंतजार कर सकता है जब तक कि हम प्रॉक्सी और सेतु के पहले संस्करण को तैनात (deploy) नहीं कर देते और फिर वैध उपयोगकर्ता से पहले initialize फ़ंक्शन तक पहुंचने के लिए फ्रंट-रनिंग (opens in a new tab) कर सकता है। लेकिन इसे रोकने के दो तरीके हैं:

  1. यदि अनुबंध सीधे EOA द्वारा तैनात नहीं किए जाते हैं बल्कि एक ऐसे लेन-देन में होते हैं जिसमें कोई अन्य अनुबंध उन्हें बनाता है (opens in a new tab) तो पूरी प्रक्रिया परमाणु (atomic) हो सकती है, और किसी भी अन्य लेन-देन के निष्पादित होने से पहले समाप्त हो सकती है।
  2. यदि initialize पर वैध कॉल विफल हो जाती है तो हमेशा नए बनाए गए प्रॉक्सी और सेतु को अनदेखा करना और नए बनाना संभव है।
        messenger = _l1messenger;
        l2TokenBridge = _l2TokenBridge;
    }

ये दो पैरामीटर हैं जिन्हें सेतु को जानने की आवश्यकता है।

यही कारण है कि हमें ओपनजेपेलिन की Address उपयोगिताओं की आवश्यकता थी।

यह फ़ंक्शन परीक्षण उद्देश्यों के लिए मौजूद है। ध्यान दें कि यह इंटरफ़ेस परिभाषाओं में दिखाई नहीं देता है - यह सामान्य उपयोग के लिए नहीं है।

ये दो फ़ंक्शन _initiateETHDeposit के चारों ओर रैपर हैं, वह फ़ंक्शन जो वास्तविक ETH जमा को संभालता है।

क्रॉस डोमेन संदेश जिस तरह से काम करते हैं वह यह है कि गंतव्य अनुबंध को संदेश के साथ उसके कॉल डेटा के रूप में कॉल किया जाता है। Solidity अनुबंध हमेशा अपने कॉल डेटा की व्याख्या इसके अनुसार करते हैं ABI विनिर्देशों (opens in a new tab)। Solidity फ़ंक्शन abi.encodeWithSelector (opens in a new tab) वह कॉल डेटा बनाता है।

            IL2ERC20Bridge.finalizeDeposit.selector,
            address(0),
            Lib_PredeployAddresses.OVM_ETH,
            _from,
            _to,
            msg.value,
            _data
        );

यहाँ संदेश इन मापदंडों के साथ finalizeDeposit फ़ंक्शन (opens in a new tab) को कॉल करना है:

पैरामीटरमानअर्थ
_l1Tokenaddress(0)L1 पर ETH (जो ERC-20 टोकन नहीं है) के लिए विशेष मान
_l2TokenLib_PredeployAddresses.OVM_ETHL2 अनुबंध जो ऑप्टिमिज़्म पर ETH का प्रबंधन करता है, 0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000 (यह अनुबंध केवल आंतरिक ऑप्टिमिज़्म उपयोग के लिए है)
_from_fromL1 पर वह पता जो ETH भेजता है
_to_toL2 पर वह पता जो ETH प्राप्त करता है
amountmsg.valueभेजे गए Wei की मात्रा (जो पहले ही सेतु को भेजी जा चुकी है)
_data_dataजमा के साथ संलग्न करने के लिए अतिरिक्त डेटा
        // लेयर 2 (l2) में कॉल डेटा भेजें
        // slither-disable-next-line reentrancy-events
        sendCrossDomainMessage(l2TokenBridge, _l2Gas, message);

क्रॉस डोमेन मैसेंजर के माध्यम से संदेश भेजें।

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

किसी भी विकेंद्रीकृत एप्लिकेशन (dapp) को सूचित करने के लिए एक घटना (event) उत्सर्जित करें जो इस ट्रांसफर को सुनता है।

ये दो फ़ंक्शन _initiateERC20Deposit के चारों ओर रैपर हैं, वह फ़ंक्शन जो वास्तविक ERC-20 जमा को संभालता है।

यह फ़ंक्शन ऊपर दिए गए _initiateETHDeposit के समान है, जिसमें कुछ महत्वपूर्ण अंतर हैं। पहला अंतर यह है कि यह फ़ंक्शन टोकन पते और ट्रांसफर की जाने वाली राशि को पैरामीटर के रूप में प्राप्त करता है। ETH के मामले में सेतु को कॉल में पहले से ही सेतु खाते (msg.value) में संपत्ति का ट्रांसफर शामिल है।

        // जब लेयर 1 (l1) पर जमा शुरू किया जाता है, तो लेयर 1 (l1) सेतु भविष्य की
        // निकासी के लिए फंड को स्वयं में ट्रांसफर करता है। safeTransferFrom यह भी जांचता है कि क्या अनुबंध में कोड है, इसलिए यह विफल हो जाएगा यदि
        // _from एक EOA या पता(0) है।
        // slither-disable-next-line reentrancy-events, reentrancy-benign
        IERC20(_l1Token).safeTransferFrom(_from, address(this), _amount);

ERC-20 टोकन ट्रांसफर ETH से एक अलग प्रक्रिया का पालन करते हैं:

  1. उपयोगकर्ता (_from) उचित टोकन ट्रांसफर करने के लिए सेतु को व्यय सीमा देता है।
  2. उपयोगकर्ता टोकन अनुबंध के पते, राशि आदि के साथ सेतु को कॉल करता है।
  3. सेतु जमा प्रक्रिया के हिस्से के रूप में टोकन (स्वयं को) ट्रांसफर करता है।

पहला कदम अंतिम दो से एक अलग लेन-देन में हो सकता है। हालाँकि, फ्रंट-रनिंग कोई समस्या नहीं है क्योंकि दो फ़ंक्शन जो _initiateERC20Deposit (depositERC20 और depositERC20To) को कॉल करते हैं, वे केवल _from पैरामीटर के रूप में msg.sender के साथ इस फ़ंक्शन को कॉल करते हैं।

टोकन की जमा राशि को deposits डेटा संरचना में जोड़ें। L2 पर कई पते हो सकते हैं जो एक ही L1 ERC-20 टोकन के अनुरूप हों, इसलिए जमा का ट्रैक रखने के लिए L1 ERC-20 टोकन के सेतु के बैलेंस का उपयोग करना पर्याप्त नहीं है।

L2 सेतु L2 क्रॉस डोमेन मैसेंजर को एक संदेश भेजता है जिसके कारण L1 क्रॉस डोमेन मैसेंजर इस फ़ंक्शन को कॉल करता है (बेशक, एक बार जब संदेश को अंतिम रूप देने वाला लेन-देन (opens in a new tab) L1 पर सबमिट हो जाता है)।

    ) external onlyFromCrossDomainAccount(l2TokenBridge) {

सुनिश्चित करें कि यह एक वैध संदेश है, जो क्रॉस डोमेन मैसेंजर से आ रहा है और L2 टोकन सेतु से उत्पन्न हो रहा है। इस फ़ंक्शन का उपयोग सेतु से ETH निकालने के लिए किया जाता है, इसलिए हमें यह सुनिश्चित करना होगा कि इसे केवल अधिकृत कॉलर द्वारा ही कॉल किया जाए।

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

ETH ट्रांसफर करने का तरीका प्राप्तकर्ता को msg.value में Wei की मात्रा के साथ कॉल करना है।

        require(success, "TransferHelper::safeTransferETH: ETH transfer failed");

        // slither-disable-next-line reentrancy-events
        emit ETHWithdrawalFinalized(_from, _to, _amount, _data);

निकासी के बारे में एक घटना उत्सर्जित करें।

यह फ़ंक्शन ऊपर दिए गए finalizeETHWithdrawal के समान है, जिसमें ERC-20 टोकन के लिए आवश्यक परिवर्तन हैं।

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

deposits डेटा संरचना को अपडेट करें।

सेतु का एक पूर्व कार्यान्वयन था। जब हम उस कार्यान्वयन से इस पर गए, तो हमें सभी संपत्तियों को स्थानांतरित करना पड़ा। ERC-20 टोकन को बस स्थानांतरित किया जा सकता है। हालाँकि, किसी अनुबंध में ETH ट्रांसफर करने के लिए आपको उस अनुबंध की स्वीकृति की आवश्यकता होती है, जो donateETH हमें प्रदान करता है।

L2 पर ERC-20 टोकन

एक ERC-20 टोकन को मानक सेतु में फिट होने के लिए, इसे मानक सेतु को, और केवल मानक सेतु को, टोकन मिंट करने की अनुमति देने की आवश्यकता है। यह आवश्यक है क्योंकि सेतुओं को यह सुनिश्चित करने की आवश्यकता है कि ऑप्टिमिज़्म पर प्रसारित होने वाले टोकन की संख्या L1 सेतु अनुबंध के अंदर लॉक किए गए टोकन की संख्या के बराबर है। यदि L2 पर बहुत अधिक टोकन हैं तो कुछ उपयोगकर्ता अपनी संपत्तियों को वापस L1 पर सेतु के माध्यम से ट्रांसफर करने में असमर्थ होंगे। एक विश्वसनीय सेतु के बजाय, हम अनिवार्य रूप से आंशिक आरक्षित बैंकिंग (fractional reserve banking) (opens in a new tab) को फिर से बनाएंगे। यदि L1 पर बहुत अधिक टोकन हैं, तो उनमें से कुछ टोकन हमेशा के लिए सेतु अनुबंध के अंदर लॉक रहेंगे क्योंकि L2 टोकन को बर्न किए बिना उन्हें जारी करने का कोई तरीका नहीं है।

IL2StandardERC20

L2 पर प्रत्येक ERC-20 टोकन जो मानक सेतु का उपयोग करता है, उसे यह इंटरफ़ेस (opens in a new tab) प्रदान करने की आवश्यकता है, जिसमें वे फ़ंक्शन और घटनाएँ हैं जिनकी मानक सेतु को आवश्यकता है।

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

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

मानक ERC-20 इंटरफ़ेस (opens in a new tab) में mint और burn फ़ंक्शन शामिल नहीं हैं। उन विधियों की ERC-20 मानक (opens in a new tab) द्वारा आवश्यकता नहीं है, जो टोकन बनाने और नष्ट करने के तंत्र को अनिर्दिष्ट छोड़ देता है।

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

ERC-165 इंटरफ़ेस (opens in a new tab) का उपयोग यह निर्दिष्ट करने के लिए किया जाता है कि अनुबंध कौन से फ़ंक्शन प्रदान करता है। आप यहाँ मानक पढ़ सकते हैं (opens in a new tab)

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

यह फ़ंक्शन L1 टोकन का पता प्रदान करता है जिसे इस अनुबंध में सेतु के माध्यम से ट्रांसफर किया गया है। ध्यान दें कि हमारे पास विपरीत दिशा में समान फ़ंक्शन नहीं है। हमें किसी भी L1 टोकन को सेतु के माध्यम से ट्रांसफर करने में सक्षम होने की आवश्यकता है, भले ही इसे लागू करते समय L2 समर्थन की योजना बनाई गई थी या नहीं।


    function mint(address _to, uint256 _amount) external;

    function burn(address _from, uint256 _amount) external;

    event Mint(address indexed _account, uint256 _amount);
    event Burn(address indexed _account, uint256 _amount);
}

टोकन मिंट (बनाने) और बर्न (नष्ट करने) के लिए फ़ंक्शन और घटनाएँ। सेतु एकमात्र इकाई होनी चाहिए जो यह सुनिश्चित करने के लिए इन फ़ंक्शंस को चला सके कि टोकन की संख्या सही है (L1 पर लॉक किए गए टोकन की संख्या के बराबर)।

L2StandardERC20

यह IL2StandardERC20 इंटरफ़ेस का हमारा कार्यान्वयन है (opens in a new tab)। जब तक आपको किसी प्रकार के कस्टम लॉजिक की आवश्यकता न हो, आपको इसका उपयोग करना चाहिए।

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

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

ओपनजेपेलिन ERC-20 अनुबंध (opens in a new tab)। ऑप्टिमिज़्म पहिये का फिर से आविष्कार करने में विश्वास नहीं करता है, खासकर जब पहिया अच्छी तरह से ऑडिट किया गया हो और संपत्तियों को रखने के लिए पर्याप्त भरोसेमंद होने की आवश्यकता हो।

import "./IL2StandardERC20.sol";

contract L2StandardERC20 is IL2StandardERC20, ERC20 {
    address public l1Token;
    address public l2Bridge;

ये दो अतिरिक्त कॉन्फ़िगरेशन पैरामीटर हैं जिनकी हमें आवश्यकता है और ERC-20 को सामान्य रूप से नहीं होती है।

पहले उस अनुबंध के लिए कंस्ट्रक्टर को कॉल करें जिससे हम इनहेरिट करते हैं (ERC20(_name, _symbol)) और फिर अपने स्वयं के चर सेट करें।

इस तरह ERC-165 (opens in a new tab) काम करता है। प्रत्येक इंटरफ़ेस समर्थित फ़ंक्शंस की एक संख्या है, और इसे उन फ़ंक्शंस के ABI फ़ंक्शन चयनकर्ताओं (opens in a new tab) के एक्सक्लूसिव ऑर (exclusive or) (opens in a new tab) के रूप में पहचाना जाता है।

L2 सेतु ERC-165 का उपयोग एक विवेक परीक्षण (sanity check) के रूप में करता है ताकि यह सुनिश्चित हो सके कि जिस ERC-20 अनुबंध को वह संपत्ति भेजता है वह एक IL2StandardERC20 है।

नोट: दुष्ट अनुबंध को supportsInterface के गलत उत्तर प्रदान करने से रोकने के लिए कुछ भी नहीं है, इसलिए यह एक विवेक परीक्षण तंत्र है, सुरक्षा तंत्र नहीं

केवल L2 सेतु को संपत्तियों को मिंट और बर्न करने की अनुमति है।

_mint और _burn वास्तव में ओपनजेपेलिन ERC-20 अनुबंध में परिभाषित हैं। वह अनुबंध बस उन्हें बाहरी रूप से उजागर नहीं करता है, क्योंकि टोकन मिंट और बर्न करने की शर्तें उतनी ही विविध हैं जितने ERC-20 का उपयोग करने के तरीके हैं।

L2 सेतु कोड

यह वह कोड है जो ऑप्टिमिज़्म पर सेतु चलाता है। इस अनुबंध का स्रोत यहाँ है (opens in a new tab)

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

/* इंटरफ़ेस आयात */
import { IL1StandardBridge } from "../../L1/messaging/IL1StandardBridge.sol";
import { IL1ERC20Bridge } from "../../L1/messaging/IL1ERC20Bridge.sol";
import { IL2ERC20Bridge } from "./IL2ERC20Bridge.sol";

IL2ERC20Bridge (opens in a new tab) इंटरफ़ेस ऊपर देखे गए L1 समकक्ष के बहुत समान है। दो महत्वपूर्ण अंतर हैं:

  1. L1 पर आप जमा शुरू करते हैं और निकासी को अंतिम रूप देते हैं। यहाँ आप निकासी शुरू करते हैं और जमा को अंतिम रूप देते हैं।
  2. L1 पर ETH और ERC-20 टोकन के बीच अंतर करना आवश्यक है। L2 पर हम दोनों के लिए समान फ़ंक्शंस का उपयोग कर सकते हैं क्योंकि आंतरिक रूप से ऑप्टिमिज़्म पर ETH बैलेंस को 0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000 (opens in a new tab) पते वाले ERC-20 टोकन के रूप में नियंत्रित किया जाता है।

L1 सेतु के पते का ट्रैक रखें। ध्यान दें कि L1 समकक्ष के विपरीत, यहाँ हमें इस चर की आवश्यकता है। L1 सेतु का पता पहले से ज्ञात नहीं होता है।

ये दो फ़ंक्शन निकासी शुरू करते हैं। ध्यान दें कि L1 टोकन पते को निर्दिष्ट करने की कोई आवश्यकता नहीं है। L2 टोकन से हमें L1 समकक्ष का पता बताने की उम्मीद है।

ध्यान दें कि हम _from पैरामीटर पर निर्भर नहीं हैं बल्कि msg.sender पर निर्भर हैं जिसे नकली बनाना बहुत कठिन है (जहाँ तक मुझे पता है, असंभव है)।


        // l1TokenBridge.finalizeERC20Withdrawal(_to, _amount) के लिए कॉल डेटा बनाएँ
        // slither-disable-next-line reentrancy-events
        address l1Token = IL2StandardERC20(_l2Token).l1Token();
        bytes memory message;

        if (_l2Token == Lib_PredeployAddresses.OVM_ETH) {

L1 पर ETH और ERC-20 के बीच अंतर करना आवश्यक है।

यह फ़ंक्शन L1StandardBridge द्वारा कॉल किया जाता है।

    ) external virtual onlyFromCrossDomainAccount(l1TokenBridge) {

सुनिश्चित करें कि संदेश का स्रोत वैध है। यह महत्वपूर्ण है क्योंकि यह फ़ंक्शन _mint को कॉल करता है और इसका उपयोग उन टोकन को देने के लिए किया जा सकता है जो L1 पर सेतु के स्वामित्व वाले टोकन द्वारा कवर नहीं किए गए हैं।

        // जांचें कि लक्ष्य टोकन अनुपालन करता है और
        // सत्यापित करें कि लेयर 1 (l1) पर जमा किया गया टोकन यहां लेयर 2 (l2) जमा टोकन प्रतिनिधित्व से मेल खाता है
        if (
            // slither-disable-next-line reentrancy-events
            ERC165Checker.supportsInterface(_l2Token, 0x1d1d8b63) &&
            _l1Token == IL2StandardERC20(_l2Token).l1Token()

विवेक परीक्षण (Sanity checks):

  1. सही इंटरफ़ेस समर्थित है
  2. L2 ERC-20 अनुबंध का L1 पता टोकन के L1 स्रोत से मेल खाता है
        ) {
            // जब किसी जमा को अंतिम रूप दिया जाता है, तो हम लेयर 2 (l2) पर खाते में उसी राशि के
            // टोकन क्रेडिट करते हैं।
            // slither-disable-next-line reentrancy-events
            IL2StandardERC20(_l2Token).mint(_to, _amount);
            // slither-disable-next-line reentrancy-events
            emit DepositFinalized(_l1Token, _l2Token, _from, _to, _amount, _data);

यदि विवेक परीक्षण पास हो जाते हैं, तो जमा को अंतिम रूप दें:

  1. टोकन मिंट करें
  2. उचित घटना उत्सर्जित करें

यदि किसी उपयोगकर्ता ने गलत L2 टोकन पते का उपयोग करके कोई पता लगाने योग्य त्रुटि की है, तो हम जमा को रद्द करना चाहते हैं और L1 पर टोकन वापस करना चाहते हैं। L2 से हम ऐसा करने का एकमात्र तरीका एक संदेश भेजना है जिसे दोष चुनौती अवधि (fault challenge period) की प्रतीक्षा करनी होगी, लेकिन यह उपयोगकर्ता के लिए टोकन को स्थायी रूप से खोने से कहीं बेहतर है।

निष्कर्ष

मानक सेतु संपत्ति ट्रांसफर के लिए सबसे लचीला तंत्र है। हालाँकि, क्योंकि यह बहुत सामान्य है, इसलिए इसका उपयोग करना हमेशा सबसे आसान तंत्र नहीं होता है। विशेष रूप से निकासी के लिए, अधिकांश उपयोगकर्ता थर्ड पार्टी सेतुओं (opens in a new tab) का उपयोग करना पसंद करते हैं जो चुनौती अवधि की प्रतीक्षा नहीं करते हैं और निकासी को अंतिम रूप देने के लिए मर्कल प्रमाण (Merkle proof) की आवश्यकता नहीं होती है।

ये सेतु आमतौर पर L1 पर संपत्ति रखकर काम करते हैं, जिसे वे एक छोटे से शुल्क (अक्सर मानक सेतु निकासी के लिए गैस की लागत से कम) के लिए तुरंत प्रदान करते हैं। जब सेतु (या इसे चलाने वाले लोग) को L1 संपत्तियों की कमी होने का अनुमान होता है, तो यह L2 से पर्याप्त संपत्तियों को ट्रांसफर करता है। चूंकि ये बहुत बड़ी निकासी हैं, इसलिए निकासी लागत एक बड़ी राशि पर परिशोधित (amortized) हो जाती है और यह बहुत छोटा प्रतिशत होता है।

उम्मीद है कि इस लेख ने आपको यह समझने में मदद की है कि लेयर 2 (l2) कैसे काम करता है, और स्पष्ट और सुरक्षित Solidity कोड कैसे लिखें।

मेरे और काम के लिए यहाँ देखें (opens in a new tab)