मुख्य आशयावर जा

ऑप्टिमिझम् स्टँडर्ड सेतू कॉन्ट्रॅक्ट वॉकथ्रू

solidity
सेतू
स्तर २ (l2)
मध्यम
ओरी पोमेरँट्झ
30 मार्च, 2022
31 मिनिटांचे वाचन

ऑप्टिमिझम् (opens in a new tab) हा एक ऑप्टिमिस्टिक रोलअप आहे. ऑप्टिमिस्टिक रोलअप्स इथरियम मेननेट (ज्याला स्तर १ (l1) असेही म्हणतात) पेक्षा खूप कमी किमतीत व्यवहारांवर प्रक्रिया करू शकतात कारण नेटवर्कवरील प्रत्येक नोडऐवजी केवळ काही नोड्सद्वारे व्यवहारांवर प्रक्रिया केली जाते. त्याच वेळी, सर्व डेटा L1 वर लिहिला जातो जेणेकरून मेननेटच्या सर्व अखंडता आणि उपलब्धतेच्या हमीसह सर्वकाही सिद्ध आणि पुनर्रचित केले जाऊ शकते.

ऑप्टिमिझम् (किंवा इतर कोणत्याही L2) वर L1 मालमत्ता वापरण्यासाठी, मालमत्तांना सेतू करणे आवश्यक आहे. हे साध्य करण्याचा एक मार्ग म्हणजे वापरकर्त्यांनी L1 वर मालमत्ता (ETH आणि ERC-20 टोकन सर्वात सामान्य आहेत) लॉक करणे आणि L2 वर वापरण्यासाठी समतुल्य मालमत्ता प्राप्त करणे. अखेरीस, ज्याच्याकडे ते असतील त्यांना ते परत L1 वर सेतू करण्याची इच्छा असू शकते. असे करताना, मालमत्ता L2 वर जाळल्या जातात आणि नंतर L1 वर वापरकर्त्याला परत दिल्या जातात.

ऑप्टिमिझम् स्टँडर्ड सेतू (opens in a new tab) याच प्रकारे काम करतो. या लेखात आपण तो सेतू कसा काम करतो हे पाहण्यासाठी त्याच्या सोर्स कोडचा आढावा घेऊ आणि चांगल्या प्रकारे लिहिलेल्या Solidity कोडचे उदाहरण म्हणून त्याचा अभ्यास करू.

नियंत्रण प्रवाह

सेतूचे दोन मुख्य प्रवाह आहेत:

  • जमा करणे (L1 कडून L2 कडे)
  • रक्कम काढणे (L2 कडून L1 कडे)

जमा करण्याचा प्रवाह

स्तर १ (l1)

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

स्तर २ (l2)

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

रक्कम काढण्याचा प्रवाह

स्तर २ (l2)

  1. रक्कम काढणारा L2 सेतूला कॉल करतो (withdraw किंवा withdrawTo)
  2. L2 सेतू msg.sender च्या मालकीचे योग्य संख्येने टोकन जाळतो
  3. L2 सेतू L1 सेतूवरील finalizeETHWithdrawal किंवा finalizeERC20Withdrawal ला कॉल करण्यासाठी क्रॉस-डोमेन संदेश यंत्रणेचा वापर करतो

स्तर १ (l1)

  1. L1 सेतू finalizeETHWithdrawal किंवा finalizeERC20Withdrawal ला केलेला कॉल कायदेशीर असल्याची पडताळणी करतो:
    • क्रॉस डोमेन संदेश यंत्रणेतून आला आहे
    • मूळतः L2 वरील सेतूमधून आला होता
  2. L1 सेतू योग्य मालमत्तेचे (ETH किंवा ERC-20) योग्य पत्त्यावर हस्तांतरण करतो

स्तर १ (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) आणि ऑप्टिमिस्टिक कोव्हान चाचणी नेटवर्क (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 व्यवहारासाठी दोष आव्हान कालावधी (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 {
        // स्लिदर-disable-next-line पुनर्प्रवेश-घटना, पुनर्प्रवेश-benign

स्लिदर (opens in a new tab) हा एक स्टॅटिक ॲनालायझर आहे जो ऑप्टिमिझम् प्रत्येक कॉन्ट्रॅक्टवर असुरक्षा आणि इतर संभाव्य समस्या शोधण्यासाठी चालवतो. या प्रकरणात, खालील ओळ दोन असुरक्षा ट्रिगर करते:

  1. पुनर्प्रवेश घटना (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";

ओपनझेपलिनच्या ॲड्रेस युटिलिटीज (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) याची खात्री करते.

/**
 * @title L1StandardBridge
 * @dev स्तर १ (l1) ETH आणि ERC-20 सेतू हे एक कॉन्ट्रॅक्ट आहे जे जमा केलेले स्तर १ (l1) निधी आणि स्तर २ (l2) वर वापरात असलेले मानक टोकन संचयित करते. हे संबंधित स्तर २ (l2) सेतू समक्रमित करते, त्याला ठेवींची माहिती देते आणि नव्याने अंतिम झालेल्या रक्कम काढणे यासाठी ऐकते.
 *
 */
contract L1StandardBridge is IL1StandardBridge, CrossDomainEnabled {
    using SafeERC20 for IERC20;

जेव्हा आपण IERC20 इंटरफेस वापरतो तेव्हा प्रत्येक वेळी SafeERC20 रॅपर वापरण्यासाठी आपण या ओळीद्वारे निर्दिष्ट करतो.


    /********************************
     * बाह्य कॉन्ट्रॅक्ट संदर्भ *
     ********************************/

    address public l2TokenBridge;

L2StandardBridge चा पत्ता.


    // जमा केलेल्या स्तर १ (l1) टोकनच्या शिल्लकीसाठी स्तर १ (l1) टोकनला स्तर २ (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 फंक्शन्सची गॅस किंमत कमी असू शकते, कारण त्यांना कॉल डेटामध्ये पॅरामीटर्स प्रदान केले जाऊ शकतात. public घोषित केलेली फंक्शन्स कॉन्ट्रॅक्टच्या आतून ॲक्सेस करण्यायोग्य असणे आवश्यक आहे. कॉन्ट्रॅक्ट्स त्यांचा स्वतःचा कॉल डेटा सुधारू शकत नाहीत, त्यामुळे पॅरामीटर्स मेमरीमध्ये असणे आवश्यक आहे. जेव्हा अशा फंक्शनला बाहेरून कॉल केले जाते, तेव्हा कॉल डेटा मेमरीमध्ये कॉपी करणे आवश्यक असते, ज्यासाठी गॅस खर्च होतो. या प्रकरणात फंक्शनला फक्त एकदाच कॉल केले जाते, त्यामुळे अकार्यक्षमतेचा आपल्याला काही फरक पडत नाही.

    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 जमा करणे हाताळते.

क्रॉस डोमेन संदेश ज्या प्रकारे काम करतात ते म्हणजे गंतव्य कॉन्ट्रॅक्टला संदेश त्याचा कॉल डेटा म्हणून कॉल केला जातो. 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_ETHऑप्टिमिझम् वर ETH व्यवस्थापित करणारा L2 कॉन्ट्रॅक्ट, 0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000 (हा कॉन्ट्रॅक्ट केवळ अंतर्गत ऑप्टिमिझम् वापरासाठी आहे)
_from_fromL1 वरील पत्ता जो ETH पाठवतो
_to_toL2 वरील पत्ता जो ETH प्राप्त करतो
amountmsg.valueपाठवलेल्या Wei ची रक्कम (जी आधीच सेतूकडे पाठवली गेली आहे)
_data_dataजमा करण्यासोबत जोडण्यासाठी अतिरिक्त डेटा
        // स्तर २ (l2) मध्ये कॉल डेटा पाठवा
        // स्लिदर-disable-next-line पुनर्प्रवेश-घटना
        sendCrossDomainMessage(l2TokenBridge, _l2Gas, message);

क्रॉस डोमेन मेसेंजरद्वारे संदेश पाठवा.

        // स्लिदर-disable-next-line पुनर्प्रवेश-घटना
        emit ETHDepositInitiated(_from, _to, msg.value, _data);
    }

या हस्तांतरणाबद्दल ऐकणाऱ्या कोणत्याही विकेंद्रित ॲप्लिकेशनला (dapp) माहिती देण्यासाठी एक घटना उत्सर्जित करा.

ही दोन फंक्शन्स _initiateERC20Deposit च्या भोवती रॅपर्स आहेत, जे फंक्शन प्रत्यक्ष ERC-20 जमा करणे हाताळते.

हे फंक्शन वरील _initiateETHDeposit सारखेच आहे, ज्यामध्ये काही महत्त्वाचे फरक आहेत. पहिला फरक असा आहे की हे फंक्शन टोकन पत्ते आणि हस्तांतरित करायची रक्कम पॅरामीटर्स म्हणून प्राप्त करते. ETH च्या बाबतीत सेतूला केलेल्या कॉलमध्ये आधीच सेतू खात्यात मालमत्तेचे हस्तांतरण समाविष्ट असते (msg.value).

        // जेव्हा स्तर १ (l1) वर ठेव सुरू केली जाते, तेव्हा स्तर १ (l1) सेतू भविष्यातील
        // रक्कम काढणे यासाठी निधी स्वतःकडे हस्तांतरित करतो. safeTransferFrom कॉन्ट्रॅक्टमध्ये कोड आहे की नाही हे देखील तपासते, त्यामुळे जर
        // _from EOA किंवा पत्ता(0) असेल तर हे अयशस्वी होईल.
        // स्लिदर-disable-next-line पुनर्प्रवेश-घटना, पुनर्प्रवेश-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 काढण्यासाठी वापरले जाते, त्यामुळे आपल्याला हे सुनिश्चित करावे लागेल की ते केवळ अधिकृत कॉलरद्वारे कॉल केले जाते.

        // स्लिदर-disable-next-line पुनर्प्रवेश-घटना
        (bool success, ) = _to.call{ value: _amount }(new bytes(0));

ETH हस्तांतरित करण्याचा मार्ग म्हणजे प्राप्तकर्त्याला msg.value मधील Wei च्या रकमेसह कॉल करणे.

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

        // स्लिदर-disable-next-line पुनर्प्रवेश-घटना
        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 टोकनचा पत्ता प्रदान करते जो या कॉन्ट्रॅक्टशी सेतू केलेला आहे. लक्षात घ्या की आपल्याकडे विरुद्ध दिशेने असे कोणतेही फंक्शन नाही. जेव्हा त्याची अंमलबजावणी केली गेली तेव्हा L2 समर्थनाचे नियोजन केले होते की नाही याची पर्वा न करता, आपण कोणतेही L1 टोकन सेतू करण्यास सक्षम असणे आवश्यक आहे.


    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-20 कॉन्ट्रॅक्टला मालमत्ता पाठवतो तो IL2StandardERC20 आहे याची खात्री करण्यासाठी सॅनिटी चेक म्हणून ERC-165 वापरतो.

टीप: दुष्ट कॉन्ट्रॅक्टला 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) साठी कॉल डेटा तयार करा
        // स्लिदर-disable-next-line पुनर्प्रवेश-घटना
        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 (
            // स्लिदर-disable-next-line पुनर्प्रवेश-घटना
            ERC165Checker.supportsInterface(_l2Token, 0x1d1d8b63) &&
            _l1Token == IL2StandardERC20(_l2Token).l1Token()

सॅनिटी चेक्स:

  1. योग्य इंटरफेस समर्थित आहे
  2. L2 ERC-20 कॉन्ट्रॅक्टचा L1 पत्ता टोकनच्या L1 स्रोताशी जुळतो
        ) {
            // जेव्हा ठेव अंतिम होते, तेव्हा आम्ही स्तर २ (l2) वरील खात्यात तेवढ्याच रकमेचे
            // टोकन जमा करतो.
            // स्लिदर-disable-next-line पुनर्प्रवेश-घटना
            IL2StandardERC20(_l2Token).mint(_to, _amount);
            // स्लिदर-disable-next-line पुनर्प्रवेश-घटना
            emit DepositFinalized(_l1Token, _l2Token, _from, _to, _amount, _data);

जर सॅनिटी चेक्स पास झाले, तर जमा करण्याची अंतिम प्रक्रिया करा:

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

जर वापरकर्त्याने चुकीचा L2 टोकन पत्ता वापरून शोधण्यायोग्य चूक केली असेल, तर आपल्याला जमा करणे रद्द करायचे आहे आणि L1 वर टोकन परत करायचे आहेत. L2 वरून आपण हे करू शकण्याचा एकमेव मार्ग म्हणजे एक संदेश पाठवणे ज्याला दोष आव्हान कालावधीची (fault challenge period) प्रतीक्षा करावी लागेल, परंतु वापरकर्त्यासाठी कायमचे टोकन गमावण्यापेक्षा हे खूप चांगले आहे.

निष्कर्ष

मालमत्ता हस्तांतरणासाठी स्टँडर्ड सेतू ही सर्वात लवचिक यंत्रणा आहे. तथापि, ती खूप सामान्य असल्यामुळे ती वापरण्यासाठी नेहमीच सर्वात सोपी यंत्रणा नसते. विशेषतः रक्कम काढण्यासाठी, बहुतेक वापरकर्ते थर्ड पार्टी सेतू (opens in a new tab) वापरणे पसंत करतात जे आव्हान कालावधीची प्रतीक्षा करत नाहीत आणि रक्कम काढण्याची अंतिम प्रक्रिया करण्यासाठी मर्केल पुरावा आवश्यक नसतो.

हे सेतू सामान्यतः L1 वर मालमत्ता ठेवून काम करतात, जे ते एका छोट्या शुल्कासाठी (बहुतेकदा स्टँडर्ड सेतूमधून रक्कम काढण्यासाठी लागणाऱ्या गॅसच्या किमतीपेक्षा कमी) त्वरित प्रदान करतात. जेव्हा सेतूला (किंवा तो चालवणाऱ्या लोकांना) L1 मालमत्तेची कमतरता भासण्याची शक्यता असते तेव्हा तो L2 मधून पुरेशी मालमत्ता हस्तांतरित करतो. हे खूप मोठे रक्कम काढण्याचे व्यवहार असल्याने, रक्कम काढण्याचा खर्च मोठ्या रकमेवर विभागला जातो आणि त्याची टक्केवारी खूपच कमी असते.

आशा आहे की या लेखामुळे तुम्हाला स्तर २ (l2) कसे काम करते आणि स्पष्ट आणि सुरक्षित Solidity कोड कसा लिहायचा याबद्दल अधिक समजण्यास मदत झाली असेल.

माझ्या अधिक कामासाठी येथे पहा (opens in a new tab).