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

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

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 टोकन को ब्रिज करने के लिए आवश्यक फ़ंक्शन और परिभाषाएँ शामिल हैं।

1// SPDX-License-Identifier: MIT

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

1pragma solidity >0.5.0 <0.9.0;

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

1/**
2 * @title IL1ERC20Bridge
3 */
4interface IL1ERC20Bridge {
5 /**********
6 * इवेंट्स *
7 **********/
8
9 event ERC20DepositInitiated(
सभी दिखाएँ

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

1 address indexed _l1Token,
2 address indexed _l2Token,

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

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

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

1 event ERC20WithdrawalFinalized(
2 address indexed _l1Token,
3 address indexed _l2Token,
4 address indexed _from,
5 address _to,
6 uint256 _amount,
7 bytes _data
8 );

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

1
2 /********************
3 * सार्वजनिक फ़ंक्शन *
4 ********************/
5
6 /**
7 * @dev संबंधित L2 ब्रिज अनुबंध का पता प्राप्त करें।
8 * @return संबंधित L2 ब्रिज अनुबंध का पता।
9 */
10 function l2TokenBridge() external returns (address);
सभी दिखाएँ

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

1 /**
2 * @dev L2 पर कॉलर की शेष राशि में ERC20 की राशि जमा करें।
3 * @param _l1Token हम जिस L1 ERC20 को जमा कर रहे हैं उसका पता
4 * @param _l2Token L1 संबंधित L2 ERC20 का पता
5 * @param _amount जमा करने के लिए ERC20 की राशि
6 * @param _l2Gas L2 पर जमा को पूरा करने के लिए आवश्यक गैस सीमा।
7 * @param _data L2 को अग्रेषित करने के लिए वैकल्पिक डेटा। यह डेटा प्रदान किया गया है
8 * पूरी तरह से बाहरी अनुबंधों की सुविधा के रूप में। अधिकतम लागू करने के अलावा
9 * लंबाई, ये अनुबंध इसकी सामग्री के बारे में कोई गारंटी नहीं देते हैं।
10 */
11 function depositERC20(
12 address _l1Token,
13 address _l2Token,
14 uint256 _amount,
15 uint32 _l2Gas,
16 bytes calldata _data
17 ) external;
सभी दिखाएँ

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

1 /**
2 * @dev L2 पर प्राप्तकर्ता की शेष राशि में ERC20 की राशि जमा करें।
3 * @param _l1Token हम जिस L1 ERC20 को जमा कर रहे हैं उसका पता
4 * @param _l2Token L1 संबंधित L2 ERC20 का पता
5 * @param _to L2 पता जिसमें निकासी जमा करनी है।
6 * @param _amount जमा करने के लिए ERC20 की राशि।
7 * @param _l2Gas L2 पर जमा को पूरा करने के लिए आवश्यक गैस सीमा।
8 * @param _data L2 को अग्रेषित करने के लिए वैकल्पिक डेटा। यह डेटा प्रदान किया गया है
9 * पूरी तरह से बाहरी अनुबंधों की सुविधा के रूप में। अधिकतम लागू करने के अलावा
10 * लंबाई, ये अनुबंध इसकी सामग्री के बारे में कोई गारंटी नहीं देते हैं।
11 */
12 function depositERC20To(
13 address _l1Token,
14 address _l2Token,
15 address _to,
16 uint256 _amount,
17 uint32 _l2Gas,
18 bytes calldata _data
19 ) external;
सभी दिखाएँ

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

1 /*************************
2 * क्रॉस-चेन फ़ंक्शन *
3 *************************/
4
5 /**
6 * @dev L2 से L1 में निकासी पूरी करें, और धन को प्राप्तकर्ता की
7 * L1 ERC20 टोकन की शेष राशि में जमा करें।
8 * यदि L2 से आरंभ की गई निकासी को अंतिम रूप नहीं दिया गया है तो यह कॉल विफल हो जाएगा।
9 *
10 * @param _l1Token L1 टोकन का पता जिसके लिए finalizeWithdrawal करना है।
11 * @param _l2Token L2 टोकन का पता जहां निकासी शुरू की गई थी।
12 * @param _from स्थानांतरण शुरू करने वाला L2 पता।
13 * @param _to L1 पता जिसमें निकासी जमा करनी है।
14 * @param _amount जमा करने के लिए ERC20 की राशि।
15 * @param _data L2 पर प्रेषक द्वारा प्रदान किया गया डेटा। यह डेटा प्रदान किया गया है
16 * पूरी तरह से बाहरी अनुबंधों की सुविधा के रूप में। अधिकतम लागू करने के अलावा
17 * लंबाई, ये अनुबंध इसकी सामग्री के बारे में कोई गारंटी नहीं देते हैं।
18 */
19 function finalizeERC20Withdrawal(
20 address _l1Token,
21 address _l2Token,
22 address _from,
23 address _to,
24 uint256 _amount,
25 bytes calldata _data
26 ) external;
27}
सभी दिखाएँ

ऑप्टिमिज्म में निकासी (और 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 को भी ब्रिज करने की आवश्यकता नहीं होती है।

1// SPDX-License-Identifier: MIT
2pragma solidity >0.5.0 <0.9.0;
3
4import "./IL1ERC20Bridge.sol";
5
6/**
7 * @title IL1StandardBridge
8 */
9interface IL1StandardBridge is IL1ERC20Bridge {
10 /**********
11 * इवेंट्स *
12 **********/
13 event ETHDepositInitiated(
14 address indexed _from,
15 address indexed _to,
16 uint256 _amount,
17 bytes _data
18 );
सभी दिखाएँ

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

1 event ETHWithdrawalFinalized(
2 .
3 .
4 .
5 );
6
7 /********************
8 * सार्वजनिक फ़ंक्शन *
9 ********************/
10
11 /**
12 * @dev ETH की एक राशि को L2 पर कॉलर की शेष राशि में जमा करें।
13 .
14 .
15 .
16 */
17 function depositETH(uint32 _l2Gas, bytes calldata _data) external payable;
18
19 /**
20 * @dev L2 पर प्राप्तकर्ता की शेष राशि में ETH की राशि जमा करें।
21 .
22 .
23 .
24 */
25 function depositETHTo(
26 address _to,
27 uint32 _l2Gas,
28 bytes calldata _data
29 ) external payable;
30
31 /*************************
32 * क्रॉस-चेन फ़ंक्शन *
33 *************************/
34
35 /**
36 * @dev L2 से L1 में निकासी पूरी करें, और धन को प्राप्तकर्ता की
37 * L1 ETH टोकन की शेष राशि में जमा करें। चूंकि केवल xDomainMessenger ही इस फ़ंक्शन को कॉल कर सकता है, इसलिए इसे कभी भी
38 * निकासी को अंतिम रूप दिए जाने से पहले कॉल नहीं किया जाएगा।
39 .
40 .
41 .
42 */
43 function finalizeETHWithdrawal(
44 address _from,
45 address _to,
46 uint256 _amount,
47 bytes calldata _data
48 ) external;
49}
सभी दिखाएँ

CrossDomainEnabled

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

1// SPDX-License-Identifier: MIT
2pragma solidity >0.5.0 <0.9.0;
3
4/* इंटरफ़ेस इम्पोर्ट */
5import { ICrossDomainMessenger } from "./ICrossDomainMessenger.sol";

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

1/**
2 * @title CrossDomainEnabled
3 * @dev क्रॉस-डोमेन संचार करने वाले अनुबंधों के लिए हेल्पर अनुबंध
4 *
5 * कंपाइलर का उपयोग किया गया: इनहेरिट करने वाले अनुबंध द्वारा परिभाषित
6 */
7contract CrossDomainEnabled {
8 /*************
9 * चर *
10 *************/
11
12 // मैसेंजर अनुबंध का उपयोग दूसरे डोमेन से संदेश भेजने और प्राप्त करने के लिए किया जाता है।
13 address public messenger;
14
15 /***************
16 * कंस्ट्रक्टर *
17 ***************/
18
19 /**
20 * @param _messenger वर्तमान लेयर पर CrossDomainMessenger का पता।
21 */
22 constructor(address _messenger) {
23 messenger = _messenger;
24 }
सभी दिखाएँ

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

1
2 /**********************
3 * फ़ंक्शन संशोधक *
4 **********************/
5
6 /**
7 * यह सुनिश्चित करता है कि संशोधित फ़ंक्शन केवल एक विशिष्ट क्रॉस-डोमेन खाते द्वारा कॉल करने योग्य है।
8 * @param _sourceDomainAccount मूल डोमेन पर एकमात्र खाता जो इस फ़ंक्शन को कॉल करने के लिए
9 * प्रमाणित है।
10 */
11 modifier onlyFromCrossDomainAccount(address _sourceDomainAccount) {
सभी दिखाएँ

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

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

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

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

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

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

1
2 _;
3 }
4
5 /**********************
6 * आंतरिक फ़ंक्शन *
7 **********************/
8
9 /**
10 * मैसेंजर प्राप्त करता है, आमतौर पर स्टोरेज से। यह फ़ंक्शन उस स्थिति में उजागर होता है जब किसी चाइल्ड अनुबंध को
11 * ओवरराइड करने की आवश्यकता होती है।
12 * @return क्रॉस-डोमेन मैसेंजर अनुबंध का पता जिसका उपयोग किया जाना चाहिए।
13 */
14 function getCrossDomainMessenger() internal virtual returns (ICrossDomainMessenger) {
15 return ICrossDomainMessenger(messenger);
16 }
सभी दिखाएँ

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

1
2 /**
3 * दूसरे डोमेन पर एक खाते में एक संदेश भेजता है
4 * @param _crossDomainTarget गंतव्य डोमेन पर इच्छित प्राप्तकर्ता
5 * @param _message लक्ष्य को भेजने के लिए डेटा (आमतौर पर `onlyFromCrossDomainAccount()` के साथ एक फ़ंक्शन के लिए calldata)
6 * @param _gasLimit लक्ष्य डोमेन पर संदेश की रसीद के लिए gasLimit।
7 */
8 function sendCrossDomainMessage(
9 address _crossDomainTarget,
10 uint32 _gasLimit,
11 bytes memory _message
सभी दिखाएँ

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

1 ) internal {
2 // 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)
1 getCrossDomainMessenger().sendMessage(_crossDomainTarget, _message, _gasLimit);
2 }
3}

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

L1 ब्रिज अनुबंध

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

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

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

1/* इंटरफ़ेस इम्पोर्ट */
2import { IL1StandardBridge } from "./IL1StandardBridge.sol";
3import { IL1ERC20Bridge } from "./IL1ERC20Bridge.sol";

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

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

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

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

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

1/* लाइब्रेरी इम्पोर्ट */
2import { CrossDomainEnabled } from "../../libraries/bridge/CrossDomainEnabled.sol";

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

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

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

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

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

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

1import { 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)

1/**
2 * @title L1StandardBridge
3 * @dev L1 ETH और ERC20 ब्रिज एक अनुबंध है जो जमा किए गए L1 फंड और L2 पर उपयोग में आने वाले स्टैंडर्ड टोकन को संग्रहीत करता है।
4 * यह एक संबंधित L2 ब्रिज को सिंक्रनाइज़ करता है, इसे जमा के बारे में सूचित करता है
5 * और नई अंतिम रूप दी गई निकासी के लिए इसे सुनता है।
6 *
7 */
8contract L1StandardBridge is IL1StandardBridge, CrossDomainEnabled {
9 using SafeERC20 for IERC20;
सभी दिखाएँ

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

1
2 /********************************
3 * बाहरी अनुबंध संदर्भ *
4 ********************************/
5
6 address public l2TokenBridge;

L2StandardBridge का पता।

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

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

1
2 /***************
3 * कंस्ट्रक्टर *
4 ***************/
5
6 // यह अनुबंध एक प्रॉक्सी के पीछे रहता है, इसलिए कंस्ट्रक्टर पैरामीटर का उपयोग नहीं किया जाएगा।
7 constructor() CrossDomainEnabled(address(0)) {}

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

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

1 /******************
2 * आरंभीकरण *
3 ******************/
4
5 /**
6 * @param _l1messenger L1 Messenger पता जिसका उपयोग क्रॉस-चेन संचार के लिए किया जा रहा है।
7 * @param _l2TokenBridge L2 स्टैंडर्ड ब्रिज पता।
8 */
9 // slither-disable-next-line external-function
सभी दिखाएँ

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

1 function initialize(address _l1messenger, address _l2TokenBridge) public {
2 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 के लिए वैध कॉल विफल हो जाती है, तो नए बनाए गए प्रॉक्सी और ब्रिज को अनदेखा करना और नए बनाना हमेशा संभव होता है।
1 messenger = _l1messenger;
2 l2TokenBridge = _l2TokenBridge;
3 }

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

1
2 /**************
3 * जमा करना *
4 **************/
5
6 /** @dev प्रेषक को EOA होने की आवश्यकता वाला संशोधक। इस जाँच को एक दुर्भावनापूर्ण
7 * अनुबंध द्वारा initcode के माध्यम से बायपास किया जा सकता है, लेकिन यह उस यूज़र त्रुटि का ध्यान रखता है जिससे हम बचना चाहते हैं।
8 */
9 modifier onlyEOA() {
10 // अनुबंधों से जमा को रोकने के लिए उपयोग किया जाता है (गलती से खोए हुए टोकन से बचें)
11 require(!Address.isContract(msg.sender), "Account not EOA");
12 _;
13 }
सभी दिखाएँ

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

1 /**
2 * @dev इस फ़ंक्शन को बिना किसी डेटा के कॉल किया जा सकता है
3 * L2 पर कॉलर की शेष राशि में ETH की राशि जमा करने के लिए।
4 * चूंकि प्राप्त फ़ंक्शन डेटा नहीं लेता है, एक रूढ़िवादी
5 * डिफ़ॉल्ट राशि L2 को अग्रेषित की जाती है।
6 */
7 receive() external payable onlyEOA {
8 _initiateETHDeposit(msg.sender, msg.sender, 200_000, bytes(""));
9 }
सभी दिखाएँ

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

1 /**
2 * @inheritdoc IL1StandardBridge
3 */
4 function depositETH(uint32 _l2Gas, bytes calldata _data) external payable onlyEOA {
5 _initiateETHDeposit(msg.sender, msg.sender, _l2Gas, _data);
6 }
7
8 /**
9 * @inheritdoc IL1StandardBridge
10 */
11 function depositETHTo(
12 address _to,
13 uint32 _l2Gas,
14 bytes calldata _data
15 ) external payable {
16 _initiateETHDeposit(msg.sender, _to, _l2Gas, _data);
17 }
सभी दिखाएँ

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

1 /**
2 * @dev ETH को संग्रहीत करके और L2 ETH गेटवे को
3 * जमा के बारे में सूचित करके जमा के लिए तर्क करता है।
4 * @param _from L1 पर जमा को खींचने के लिए खाता।
5 * @param _to L2 पर जमा देने के लिए खाता।
6 * @param _l2Gas L2 पर जमा को पूरा करने के लिए आवश्यक गैस सीमा।
7 * @param _data L2 को अग्रेषित करने के लिए वैकल्पिक डेटा। यह डेटा प्रदान किया गया है
8 * पूरी तरह से बाहरी अनुबंधों की सुविधा के रूप में। अधिकतम लागू करने के अलावा
9 * लंबाई, ये अनुबंध इसकी सामग्री के बारे में कोई गारंटी नहीं देते हैं।
10 */
11 function _initiateETHDeposit(
12 address _from,
13 address _to,
14 uint32 _l2Gas,
15 bytes memory _data
16 ) internal {
17 // finalizeDeposit कॉल के लिए calldata का निर्माण करें
18 bytes memory message = abi.encodeWithSelector(
सभी दिखाएँ

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

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

यहां संदेश इन मापदंडों के साथ 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जमा में संलग्न करने के लिए अतिरिक्त डेटा
1 // L2 में calldata भेजें
2 // slither-disable-next-line reentrancy-events
3 sendCrossDomainMessage(l2TokenBridge, _l2Gas, message);

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

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

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

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

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

1 /**
2 * @dev L2 जमा किए गए टोकन को सूचित करके जमा के लिए तर्क करता है
3 * अनुबंध को जमा के बारे में और L1 फंड को लॉक करने के लिए एक हैंडलर को कॉल करता है। (जैसे, transferFrom)
4 *
5 * @param _l1Token हम जिस L1 ERC20 को जमा कर रहे हैं उसका पता
6 * @param _l2Token L1 संबंधित L2 ERC20 का पता
7 * @param _from L1 पर जमा को खींचने के लिए खाता
8 * @param _to L2 पर जमा देने के लिए खाता
9 * @param _amount जमा करने के लिए ERC20 की राशि।
10 * @param _l2Gas L2 पर जमा को पूरा करने के लिए आवश्यक गैस सीमा।
11 * @param _data L2 को अग्रेषित करने के लिए वैकल्पिक डेटा। यह डेटा प्रदान किया गया है
12 * पूरी तरह से बाहरी अनुबंधों की सुविधा के रूप में। अधिकतम लागू करने के अलावा
13 * लंबाई, ये अनुबंध इसकी सामग्री के बारे में कोई गारंटी नहीं देते हैं।
14 */
15 function _initiateERC20Deposit(
16 address _l1Token,
17 address _l2Token,
18 address _from,
19 address _to,
20 uint256 _amount,
21 uint32 _l2Gas,
22 bytes calldata _data
23 ) internal {
सभी दिखाएँ

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

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

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

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

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

1 // _l2Token.finalizeDeposit(_to, _amount) के लिए calldata का निर्माण करें
2 bytes memory message = abi.encodeWithSelector(
3 IL2ERC20Bridge.finalizeDeposit.selector,
4 _l1Token,
5 _l2Token,
6 _from,
7 _to,
8 _amount,
9 _data
10 );
11
12 // L2 में calldata भेजें
13 // slither-disable-next-line reentrancy-events, reentrancy-benign
14 sendCrossDomainMessage(l2TokenBridge, _l2Gas, message);
15
16 // slither-disable-next-line reentrancy-benign
17 deposits[_l1Token][_l2Token] = deposits[_l1Token][_l2Token] + _amount;
सभी दिखाएँ

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

1
2 // slither-disable-next-line reentrancy-events
3 emit ERC20DepositInitiated(_l1Token, _l2Token, _from, _to, _amount, _data);
4 }
5
6 /*************************
7 * क्रॉस-चेन फ़ंक्शन *
8 *************************/
9
10 /**
11 * @inheritdoc IL1StandardBridge
12 */
13 function finalizeETHWithdrawal(
14 address _from,
15 address _to,
16 uint256 _amount,
17 bytes calldata _data
सभी दिखाएँ

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

1 ) external onlyFromCrossDomainAccount(l2TokenBridge) {

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

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

ETH स्थानांतरित करने का तरीका msg.value में wei की राशि के साथ प्राप्तकर्ता को कॉल करना है।

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

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

1 }
2
3 /**
4 * @inheritdoc IL1ERC20Bridge
5 */
6 function finalizeERC20Withdrawal(
7 address _l1Token,
8 address _l2Token,
9 address _from,
10 address _to,
11 uint256 _amount,
12 bytes calldata _data
13 ) external onlyFromCrossDomainAccount(l2TokenBridge) {
सभी दिखाएँ

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

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

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

1
2 // जब L1 पर एक निकासी को अंतिम रूप दिया जाता है, तो L1 ब्रिज फंड को निकासीकर्ता को स्थानांतरित करता है
3 // slither-disable-next-line reentrancy-events
4 IERC20(_l1Token).safeTransfer(_to, _amount);
5
6 // slither-disable-next-line reentrancy-events
7 emit ERC20WithdrawalFinalized(_l1Token, _l2Token, _from, _to, _amount, _data);
8 }
9
10
11 /*****************************
12 * अस्थायी - ETH माइग्रेट करना *
13 *****************************/
14
15 /**
16 * @dev खाते में ETH शेष राशि जोड़ता है। इसका उद्देश्य ETH को
17 * एक पुराने गेटवे से एक नए गेटवे पर माइग्रेट करने की अनुमति देना है।
18 * नोट: यह केवल एक अपग्रेड के लिए छोड़ा गया है ताकि हम पुराने अनुबंध से माइग्रेट किए गए ETH को प्राप्त करने में सक्षम हो सकें
19 *
20 */
21 function donateETH() external payable {}
22}
सभी दिखाएँ

ब्रिज का एक पहले का कार्यान्वयन था। जब हम कार्यान्वयन से इस पर चले गए, तो हमें सभी संपत्तियों को स्थानांतरित करना पड़ा। 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) प्रदान करने की आवश्यकता है, जिसमें स्टैंडर्ड ब्रिज को आवश्यक फ़ंक्शन और इवेंट्स हैं।

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

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

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

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

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

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

1
2 function mint(address _to, uint256 _amount) external;
3
4 function burn(address _from, uint256 _amount) external;
5
6 event Mint(address indexed _account, uint256 _amount);
7 event Burn(address indexed _account, uint256 _amount);
8}

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

L2StandardERC20

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

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

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

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

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

1
2 /**
3 * @param _l2Bridge L2 स्टैंडर्ड ब्रिज का पता।
4 * @param _l1Token संबंधित L1 टोकन का पता।
5 * @param _name ERC20 नाम।
6 * @param _symbol ERC20 प्रतीक।
7 */
8 constructor(
9 address _l2Bridge,
10 address _l1Token,
11 string memory _name,
12 string memory _symbol
13 ) ERC20(_name, _symbol) {
14 l1Token = _l1Token;
15 l2Bridge = _l2Bridge;
16 }
सभी दिखाएँ

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

1
2 modifier onlyL2Bridge() {
3 require(msg.sender == l2Bridge, "Only L2 Bridge can mint and burn");
4 _;
5 }
6
7
8 // slither-disable-next-line external-function
9 function supportsInterface(bytes4 _interfaceId) public pure returns (bool) {
10 bytes4 firstSupportedInterface = bytes4(keccak256("supportsInterface(bytes4)")); // ERC165
11 bytes4 secondSupportedInterface = IL2StandardERC20.l1Token.selector ^
12 IL2StandardERC20.mint.selector ^
13 IL2StandardERC20.burn.selector;
14 return _interfaceId == firstSupportedInterface || _interfaceId == secondSupportedInterface;
15 }
सभी दिखाएँ

यह वह तरीका है जिससे 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 के लिए झूठे उत्तर प्रदान करने से रोकने के लिए कुछ भी नहीं है, इसलिए यह एक सैनिटी चेक तंत्र है, कि एक सुरक्षा तंत्र।

1 // slither-disable-next-line external-function
2 function mint(address _to, uint256 _amount) public virtual onlyL2Bridge {
3 _mint(_to, _amount);
4
5 emit Mint(_to, _amount);
6 }
7
8 // slither-disable-next-line external-function
9 function burn(address _from, uint256 _amount) public virtual onlyL2Bridge {
10 _burn(_from, _amount);
11
12 emit Burn(_from, _amount);
13 }
14}
सभी दिखाएँ

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

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

L2 ब्रिज कोड

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

1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.9;
3
4/* इंटरफ़ेस इम्पोर्ट */
5import { IL1StandardBridge } from "../../L1/messaging/IL1StandardBridge.sol";
6import { IL1ERC20Bridge } from "../../L1/messaging/IL1ERC20Bridge.sol";
7import { 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 टोकन के रूप में संभाला जाता है।
1/* लाइब्रेरी इम्पोर्ट */
2import { ERC165Checker } from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol";
3import { CrossDomainEnabled } from "../../libraries/bridge/CrossDomainEnabled.sol";
4import { Lib_PredeployAddresses } from "../../libraries/constants/Lib_PredeployAddresses.sol";
5
6/* अनुबंध इम्पोर्ट */
7import { IL2StandardERC20 } from "../../standards/IL2StandardERC20.sol";
8
9/**
10 * @title L2StandardBridge
11 * @dev L2 स्टैंडर्ड ब्रिज एक अनुबंध है जो L1 स्टैंडर्ड ब्रिज के साथ मिलकर काम करता है
12 * L1 और L2 के बीच ETH और ERC20 संक्रमण को सक्षम करने के लिए।
13 * यह अनुबंध नए टोकन के लिए एक मिंटर के रूप में कार्य करता है जब यह L1 स्टैंडर्ड में जमा के बारे में सुनता है
14 * ब्रिज।
15 * यह अनुबंध निकासी के लिए इच्छित टोकन के बर्नर के रूप में भी कार्य करता है, L1 को सूचित करता है
16 * L1 फंड जारी करने के लिए ब्रिज।
17 */
18contract L2StandardBridge is IL2ERC20Bridge, CrossDomainEnabled {
19 /********************************
20 * बाहरी अनुबंध संदर्भ *
21 ********************************/
22
23 address public l1TokenBridge;
सभी दिखाएँ

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

1
2 /***************
3 * कंस्ट्रक्टर *
4 ***************/
5
6 /**
7 * @param _l2CrossDomainMessenger इस अनुबंध द्वारा उपयोग किया जाने वाला क्रॉस-डोमेन मैसेंजर।
8 * @param _l1TokenBridge मुख्य चेन पर तैनात L1 ब्रिज का पता।
9 */
10 constructor(address _l2CrossDomainMessenger, address _l1TokenBridge)
11 CrossDomainEnabled(_l2CrossDomainMessenger)
12 {
13 l1TokenBridge = _l1TokenBridge;
14 }
15
16 /***************
17 * निकासी *
18 ***************/
19
20 /**
21 * @inheritdoc IL2ERC20Bridge
22 */
23 function withdraw(
24 address _l2Token,
25 uint256 _amount,
26 uint32 _l1Gas,
27 bytes calldata _data
28 ) external virtual {
29 _initiateWithdrawal(_l2Token, msg.sender, msg.sender, _amount, _l1Gas, _data);
30 }
31
32 /**
33 * @inheritdoc IL2ERC20Bridge
34 */
35 function withdrawTo(
36 address _l2Token,
37 address _to,
38 uint256 _amount,
39 uint32 _l1Gas,
40 bytes calldata _data
41 ) external virtual {
42 _initiateWithdrawal(_l2Token, msg.sender, _to, _amount, _l1Gas, _data);
43 }
सभी दिखाएँ

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

1
2 /**
3 * @dev टोकन को बर्न करके और सूचित करके निकासी के लिए तर्क करता है
4 * निकासी के L1 टोकन गेटवे।
5 * @param _l2Token L2 टोकन का पता जहां निकासी शुरू की गई है।
6 * @param _from L2 पर निकासी को खींचने के लिए खाता।
7 * @param _to L1 पर निकासी देने के लिए खाता।
8 * @param _amount निकालने के लिए टोकन की राशि।
9 * @param _l1Gas अप्रयुक्त, लेकिन संभावित फॉरवर्ड संगतता विचारों के लिए शामिल है।
10 * @param _data L1 को अग्रेषित करने के लिए वैकल्पिक डेटा। यह डेटा प्रदान किया गया है
11 * पूरी तरह से बाहरी अनुबंधों की सुविधा के रूप में। अधिकतम लागू करने के अलावा
12 * लंबाई, ये अनुबंध इसकी सामग्री के बारे में कोई गारंटी नहीं देते हैं।
13 */
14 function _initiateWithdrawal(
15 address _l2Token,
16 address _from,
17 address _to,
18 uint256 _amount,
19 uint32 _l1Gas,
20 bytes calldata _data
21 ) internal {
22 // जब एक निकासी शुरू की जाती है, तो हम बाद के L2 को रोकने के लिए निकासीकर्ता के फंड को बर्न करते हैं
23 // उपयोग
24 // slither-disable-next-line reentrancy-events
25 IL2StandardERC20(_l2Token).burn(msg.sender, _amount);
सभी दिखाएँ

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

1
2 // l1TokenBridge.finalizeERC20Withdrawal(_to, _amount) के लिए calldata का निर्माण करें
3 // slither-disable-next-line reentrancy-events
4 address l1Token = IL2StandardERC20(_l2Token).l1Token();
5 bytes memory message;
6
7 if (_l2Token == Lib_PredeployAddresses.OVM_ETH) {

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

1 message = abi.encodeWithSelector(
2 IL1StandardBridge.finalizeETHWithdrawal.selector,
3 _from,
4 _to,
5 _amount,
6 _data
7 );
8 } else {
9 message = abi.encodeWithSelector(
10 IL1ERC20Bridge.finalizeERC20Withdrawal.selector,
11 l1Token,
12 _l2Token,
13 _from,
14 _to,
15 _amount,
16 _data
17 );
18 }
19
20 // L1 ब्रिज तक संदेश भेजें
21 // slither-disable-next-line reentrancy-events
22 sendCrossDomainMessage(l1TokenBridge, _l1Gas, message);
23
24 // slither-disable-next-line reentrancy-events
25 emit WithdrawalInitiated(l1Token, _l2Token, msg.sender, _to, _amount, _data);
26 }
27
28 /************************************
29 * क्रॉस-चेन फ़ंक्शन: जमा करना *
30 ************************************/
31
32 /**
33 * @inheritdoc IL2ERC20Bridge
34 */
35 function finalizeDeposit(
36 address _l1Token,
37 address _l2Token,
38 address _from,
39 address _to,
40 uint256 _amount,
41 bytes calldata _data
सभी दिखाएँ

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

1 ) external virtual onlyFromCrossDomainAccount(l1TokenBridge) {

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

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

सैनिटी चेक:

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

यदि सैनिटी चेक पास हो जाते हैं, तो जमा को अंतिम रूप दें:

  1. टोकन मिंट करें
  2. उपयुक्त इवेंट उत्सर्जित करें
1 } else {
2 // या तो L2 टोकन जिसमें जमा किया जा रहा है, उसके L1 टोकन के सही पते के बारे में असहमत है,
3 // या सही इंटरफ़ेस का समर्थन नहीं करता है।
4 // यह केवल तभी होना चाहिए जब कोई दुर्भावनापूर्ण L2 टोकन हो, या यदि किसी यूज़र ने किसी तरह
5 // जमा करने के लिए गलत L2 टोकन पता निर्दिष्ट किया हो।
6 // किसी भी मामले में, हम यहां प्रक्रिया को रोकते हैं और एक निकासी
7 // संदेश का निर्माण करते हैं ताकि यूज़र कुछ मामलों में अपना धन निकाल सकें।
8 // दुर्भावनापूर्ण टोकन अनुबंधों को पूरी तरह से रोकने का कोई तरीका नहीं है, लेकिन यह यूज़र त्रुटि को सीमित करता है
9 // और दुर्भावनापूर्ण अनुबंध व्यवहार के कुछ रूपों को कम करता है।
सभी दिखाएँ

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

1 bytes memory message = abi.encodeWithSelector(
2 IL1ERC20Bridge.finalizeERC20Withdrawal.selector,
3 _l1Token,
4 _l2Token,
5 _to, // जमा को प्रेषक को वापस उछालने के लिए यहां _to और _from को स्विच किया गया है
6 _from,
7 _amount,
8 _data
9 );
10
11 // L1 ब्रिज तक संदेश भेजें
12 // slither-disable-next-line reentrancy-events
13 sendCrossDomainMessage(l1TokenBridge, 0, message);
14 // slither-disable-next-line reentrancy-events
15 emit DepositFailed(_l1Token, _l2Token, _from, _to, _amount, _data);
16 }
17 }
18}
सभी दिखाएँ

निष्कर्ष

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

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

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

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

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

क्या यह ट्यूटोरियल सहायक था?