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

ऑप्टिमिज्म स्टैंडर्ड ब्रिज कॉन्ट्रैक्ट वॉकथ्रू

Solidity
ब्रिज
परत 2
माध्यमिक
ओरी पोमेरेंट्ज़
30 मार्च 2022
38 मिनट का पठन

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

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

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

कंट्रोल फ्लो

ब्रिज में दो मुख्य फ्लो हैं:

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

जमा फ्लो

लेयर 1

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

लेयर 2

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

निकासी फ्लो

लेयर 2

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

लेयर 1

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

लेयर 1 कोड

यह वह कोड है जो 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;

लिखने के समय सॉलिडिटी का नवीनतम संस्करण 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 मान कोवन टेस्ट नेटवर्क (42) और ऑप्टिमिस्टिक कोवन टेस्ट नेटवर्क (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 पर यह एक पूर्व-तैनात अनुबंध है, इसलिए यह हमेशा पते 0x4200000000000000000000000000000000000010 पर होता है। यह यहां L2 ब्रिज के साथ समरूपता के लिए है, क्योंकि L1 ब्रिज का पता जानना मामूली नहीं है।

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

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

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

  1. L2 पर एक प्रारंभिक ट्रांज़ैक्शन।
  2. L1 पर एक अंतिम या दावा करने वाला ट्रांज़ैक्शन। यह ट्रांज़ैक्शन L2 ट्रांज़ैक्शन के लिए फॉल्ट चैलेंज अवधि (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) एक स्टैटिक एनालाइज़र है जिसे ऑप्टिमिज्म हर अनुबंध पर चलाता है ताकि कमजोरियों और अन्य संभावित समस्याओं की तलाश की जा सके। इस मामले में, निम्नलिखित लाइन दो कमजोरियों को ट्रिगर करती है:

  1. रीएंट्रेंसी इवेंट्स (opens in a new tab)
  2. सौम्य रीएंट्रेंसी (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;

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

/* इंटरफ़ेस इम्पोर्ट */
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";

ओपनज़ेपेलिन की पता उपयोगिताएँ (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 का पता।


    // जमा किए गए L1 टोकन की शेष राशि के लिए L1 टोकन को L2 टोकन से मैप करता है
    mapping(address => mapping(address => uint256)) public deposits;

इस तरह की एक डबल मैपिंग (opens in a new tab) दो-आयामी स्पार्स ऐरे (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 का उपयोग करते हैं तो स्टोरेज कॉलिंग अनुबंध का स्टोरेज बना रहता है, इसलिए सभी अनुबंध स्थिति चर के मान अप्रभावित रहते हैं।

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

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

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

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

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

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

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

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

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

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

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

            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 प्राप्त करता है
राशिmsg.valueभेजे गए wei की राशि (जो पहले ही ब्रिज को भेजी जा चुकी है)
_data_dataजमा में संलग्न करने के लिए अतिरिक्त डेटा
        // L2 में calldata भेजें
        // slither-disable-next-line reentrancy-events
        sendCrossDomainMessage(l2TokenBridge, _l2Gas, message);

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

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

इस स्थानांतरण के बारे में सुनने वाले किसी भी विकेंद्रीकृत अनुप्रयोग को सूचित करने के लिए एक इवेंट उत्सर्जित करें।

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

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

        // जब L1 पर एक जमा शुरू किया जाता है, तो L1 ब्रिज भविष्य के लिए फंड को खुद को स्थानांतरित करता है
        // निकासी। safeTransferFrom यह भी जांचता है कि अनुबंध में कोड है या नहीं, इसलिए यह विफल हो जाएगा यदि
        // _from एक EOA या address(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) को कॉल करने वाले दो फ़ंक्शन केवल msg.sender को _from पैरामीटर के रूप में इस फ़ंक्शन को कॉल करते हैं।

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 पर वापस ब्रिज नहीं कर पाएंगे। एक विश्वसनीय ब्रिज के बजाय, हम अनिवार्य रूप से फ्रैक्शनल रिजर्व बैंकिंग (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) के एक्सक्लूसिव या (opens in a new tab) के रूप में पहचाना जाता है।

L2 ब्रिज ERC-165 का उपयोग एक सैनिटी चेक के रूप में करता है ताकि यह सुनिश्चित हो सके कि वह 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) के लिए calldata का निर्माण करें
        // 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 पर ब्रिज के स्वामित्व वाले टोकन द्वारा कवर नहीं किए गए हैं।

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

सैनिटी चेक:

  1. सही इंटरफ़ेस समर्थित है
  2. L2 ERC-20 अनुबंध का L1 पता टोकन के L1 स्रोत से मेल खाता है
        ) {
            // जब एक जमा को अंतिम रूप दिया जाता है, तो हम 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 से ऐसा करने का एकमात्र तरीका एक संदेश भेजना है जिसे फॉल्ट चैलेंज अवधि का इंतजार करना होगा, लेकिन यह यूज़र के लिए स्थायी रूप से टोकन खोने से कहीं बेहतर है।

निष्कर्ष

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

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

उम्मीद है कि इस लेख ने आपको लेयर 2 कैसे काम करता है, और स्पष्ट और सुरक्षित सॉलिडिटी कोड कैसे लिखना है, के बारे में अधिक समझने में मदद की होगी।

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

पेज का अंतिम अपडेट: 3 अप्रैल 2026

क्या यह ट्यूटोरियल उपयोगी था?