تخطٍ إلى المحتوى الرئيسي

شرح تفصيلي لعقد جسر أوبتيميزم القياسي

Solidity
جسر
الطبقة الثانية
المستوى المتوسط
أوري بوميرانتز
30 مارس 2022
29 دقيقة قراءة

أوبتيميزم (opens in a new tab) هو رول أب تفاؤلي. يمكن لعمليات الرول أب التفاؤلية معالجة المعاملات بسعر أقل بكثير من شبكة إيثريوم الرئيسية (المعروفة أيضًا باسم الطبقة 1 أو L1) لأن المعاملات تتم معالجتها فقط بواسطة عدد قليل من العُقد، بدلاً من كل عقدة على الشبكة. في الوقت نفسه، تُكتب جميع البيانات على L1 حتى يمكن إثبات كل شيء وإعادة بنائه مع جميع ضمانات السلامة والتوافر الخاصة بالشبكة الرئيسية.

لاستخدام أصول L1 على أوبتيميزم (أو أي L2 آخر)، يجب نقل الأصول عبر جسر. إحدى طرق تحقيق ذلك هي أن يقوم المستخدمون بقفل الأصول (عملات ETH ورموز ERC-20 هي الأكثر شيوعًا) على L1، والحصول على أصول معادلة لاستخدامها على 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 آلية الرسائل عبر النطاقات لاستدعاء finalizeDeposit على جسر L2

الطبقة 2

  1. يتحقق جسر L2 من أن استدعاء finalizeDeposit شرعي:
    • أنه أتى من عقد الرسائل عبر النطاقات
    • كان في الأصل من الجسر على L1
  2. يتحقق جسر L2 مما إذا كان عقد رمز ERC-20 على L2 هو الصحيح:
  3. إذا كان عقد L2 هو الصحيح، يتم استدعاؤه لسك العدد المناسب من الرموز إلى العنوان المناسب. إذا لم يكن كذلك، تبدأ عملية سحب للسماح للمستخدم بالمطالبة بالرموز على L1.

تدفق السحب

الطبقة 2

  1. يستدعي الساحب جسر L2 (withdraw أو withdrawTo)
  2. يحرق جسر L2 العدد المناسب من الرموز التابعة لـ msg.sender
  3. يستخدم جسر L2 آلية الرسائل عبر النطاقات لاستدعاء finalizeETHWithdrawal أو finalizeERC20Withdrawal على جسر L1

الطبقة 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(
إظهار الكل

في مصطلحات جسر أوبتيميزم، يعني الإيداع النقل من L1 إلى L2، ويعني السحب النقل من L2 إلى L1.

1 address indexed _l1Token,
2 address indexed _l2Token,

في معظم الحالات، لا يكون عنوان ERC-20 على L1 هو نفسه عنوان ERC-20 المكافئ على L2. يمكنك رؤية قائمة عناوين الرموز هنا (opens in a new tab). العنوان ذو chainId 1 موجود على L1 (الشبكة الرئيسية) والعنوان ذو chainId 10 موجود على L2 (أوبتيميزم). قيمتا chainId الأخريان هما لشبكة اختبار Kovan (42) وشبكة اختبار Optimistic Kovan (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 إيداع مبلغ من ERC20 في رصيد المتصل على L2.
3 * @param _l1Token عنوان ERC20 للطبقة 1 الذي نودعه
4 * @param _l2Token عنوان ERC20 للطبقة 2 المقابل للطبقة 1
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 إيداع مبلغ من ERC20 في رصيد المستلم على L2.
3 * @param _l1Token عنوان ERC20 للطبقة 1 الذي نودعه
4 * @param _l2Token عنوان ERC20 للطبقة 2 المقابل للطبقة 1
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 * رمز ERC20 على L1.
8 * سيفشل هذا الاستدعاء إذا لم يتم إنهاء السحب المبدئي من L2.
9 *
10 * @param _l1Token عنوان رمز L1 لإنهاء السحب له.
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. يجب أن تتم هذه المعاملة بعد انتهاء فترة الطعن في الخطأ (opens in a new tab) لمعاملة L2.

IL1StandardBridge

هذه الواجهة معرّفة هنا (opens in a new tab). يحتوي هذا الملف على تعريفات الأحداث والوظائف لـ ETH. هذه التعريفات مشابهة جدًا لتلك المعرفة في IL1ERC20Bridge أعلاه لـ ERC-20.

تنقسم واجهة الجسر بين ملفين لأن بعض رموز 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 إيداع مبلغ من ETH في رصيد المستلم على L2.
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، وإيداع الأموال في رصيد المستلم لرمز ETH على L1.
37 * نظرًا لأن 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/* Interface Imports */
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 عنوان مرسل الرسائل عبر النطاقات على الطبقة الحالية.
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 البيانات المراد إرسالها إلى الهدف (عادةً ما تكون calldata لوظيفة مع
6 * `onlyFromCrossDomainAccount()`)
7 * @param _gasLimit حد الغاز لاستلام الرسالة في نطاق الهدف.
8 */
9 function sendCrossDomainMessage(
10 address _crossDomainTarget,
11 uint32 _gasLimit,
12 bytes memory _message
إظهار الكل

أخيرًا، الوظيفة التي ترسل رسالة إلى الطبقة الأخرى.

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

سليذر (opens in a new tab) هو محلل ثابت يقوم أوبتيميزم بتشغيله على كل عقد للبحث عن الثغرات والمشاكل المحتملة الأخرى. في هذه الحالة، يثير السطر التالي ثغرتين أمنيتين:

  1. ثغرات إعادة الدخول (Reentrancy) (opens in a new tab)
  2. إعادة الدخول الحميدة (Benign reentrancy) (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/* Interface Imports */
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/* Library Imports */
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. Revert (التراجع)
  2. إرجاع false (خطأ)

إن التعامل مع كلتا الحالتين سيجعل الكود الخاص بنا أكثر تعقيدًا، لذلك بدلاً من ذلك نستخدم SafeERC20 من أوبن زبلين (opens in a new tab)، والذي يضمن أن تؤدي جميع حالات الفشل إلى تراجع (opens in a new tab).

1/**
2 * @title L1StandardBridge
3 * @dev جسر ETH و ERC20 للطبقة 1 هو عقد يخزن أموال L1 المودعة والرموز القياسية
4 * المستخدمة على L2. يقوم بمزامنة جسر L2 المقابل، وإعلامه بالإيداعات
5 * والاستماع إليه لمعرفة عمليات السحب التي تم إنهاؤها حديثًا.
6 *
7 */
8contract L1StandardBridge is IL1StandardBridge, CrossDomainEnabled {
9 using SafeERC20 for IERC20;
إظهار الكل

هذا هو السطر الذي نحدد به استخدام غلاف SafeERC20 في كل مرة نستخدم فيها واجهة IERC20.

1
2 /********************************
3 * مراجع العقود الخارجية *
4 ********************************/
5
6 address public l2TokenBridge;

عنوان L2StandardBridge.

1
2 // يربط رمز L1 برمز L2 برصيد رمز L1 المودع
3 mapping(address => mapping(address => uint256)) public deposits;

تخطيط (opens in a new tab) مزدوج مثل هذا هو الطريقة التي تُعرّف بها مصفوفة متناثرة ثنائية الأبعاد (opens in a new tab). يتم تحديد القيم في بنية البيانات هذه على أنها deposit[عنوان رمز L1][عنوان رمز L2]. القيمة الافتراضية هي صفر. يتم كتابة الخلايا التي تم تعيينها إلى قيمة مختلفة فقط في التخزين.

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 المستخدم في الاتصالات عبر السلسلة.
7 * @param _l2TokenBridge عنوان جسر L2 القياسي.
8 */
9 // slither-disable-next-line external-function
إظهار الكل

يحدد اختبار سليذر (opens in a new tab) الوظائف التي لا يتم استدعاؤها من كود العقد وبالتالي يمكن إعلانها external بدلاً من public. يمكن أن تكون تكلفة الغاز للوظائف 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، فإننا ننشئ وكيلًا جديدًا وجسرًا جديدًا يستدعيه. من غير المرجح أن يحدث هذا إلا عند ترقية النظام بأكمله، وهو حدث نادر جدًا.

لاحظ أن هذه الوظيفة لا تحتوي على أي آلية تقيد من يمكنه استدعاؤها. هذا يعني أنه من الناحية النظرية يمكن للمهاجم الانتظار حتى ننشر الوكيل والإصدار الأول من الجسر ثم يقوم بتشغيل أمامي (opens in a new tab) للوصول إلى وظيفة initialize قبل المستخدم الشرعي. ولكن هناك طريقتان لمنع هذا:

  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 * لإيداع مبلغ من ETH في رصيد المتصل على L2.
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 وإبلاغ بوابة ETH L2
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 // إنشاء calldata لاستدعاء finalizeDeposit
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) بهذه المعلمات:

ParameterValueالمعنى
_l1Tokenaddress(0)قيمة خاصة لتمثيل ETH (والذي ليس رمز ERC-20) على L1
_l2TokenLib_PredeployAddresses.OVM_ETHعقد L2 الذي يدير ETH على أوبتيميزم، 0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000 (هذا العقد للاستخدام الداخلي لـ أوبتيميزم فقط)
_from_fromالعنوان على L1 الذي يرسل ETH
_to_toالعنوان على L2 الذي يستقبل ETH
المبلغmsg.valueكمية الـ wei المرسلة (والتي تم إرسالها بالفعل إلى الجسر)
_data_dataبيانات إضافية لإرفاقها بالإيداع
1 // إرسال calldata إلى L2
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 عنوان ERC20 للطبقة 1 الذي نودعه
6 * @param _l2Token عنوان ERC20 للطبقة 2 المقابل للطبقة 1
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 // إنشاء calldata لـ _l2Token.finalizeDeposit(_to, _amount)
2 bytes memory message = abi.encodeWithSelector(
3 IL2ERC20Bridge.finalizeDeposit.selector,
4 _l1Token,
5 _l2Token,
6 _from,
7 _to,
8 _amount,
9 _data
10 );
11
12 // إرسال calldata إلى L2
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 هي استدعاء المستلم بكمية wei في msg.value.

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 * مؤقت - ترحيل ETH *
12 *****************************/
13
14 /**
15 * @dev يضيف رصيد ETH إلى الحساب. يهدف هذا إلى السماح بترحيل ETH
16 * من بوابة قديمة إلى بوابة جديدة.
17 * ملاحظة: يتم ترك هذا لترقية واحدة فقط حتى نتمكن من تلقي ETH المرحّل من
18 * العقد القديم
19 */
20 function donateETH() external payable {}
21}
إظهار الكل

كان هناك تطبيق سابق للجسر. عندما انتقلنا من التطبيق إلى هذا التطبيق، كان علينا نقل جميع الأصول. يمكن نقل رموز ERC-20 بسهولة. ومع ذلك، لنقل ETH إلى عقد ما، تحتاج إلى موافقة هذا العقد، وهو ما توفره لنا donateETH.

رموز ERC-20 على L2

لكي يتناسب رمز ERC-20 مع الجسر القياسي، يجب أن يسمح للجسر القياسي، و فقط الجسر القياسي، بسك الرمز. هذا ضروري لأن الجسور تحتاج إلى التأكد من أن عدد الرموز المتداولة على أوبتيميزم يساوي عدد الرموز المقفلة داخل عقد جسر L1. إذا كان هناك عدد كبير جدًا من الرموز على L2، فلن يتمكن بعض المستخدمين من إعادة أصولهم إلى L1 عبر الجسر. بدلاً من جسر موثوق به، سنعيد إنشاء الخدمات المصرفية الاحتياطية الجزئية (opens in a new tab) بشكل أساسي. إذا كان هناك عدد كبير جدًا من الرموز على L1، فستبقى بعض هذه الرموز مقفلة داخل عقد الجسر إلى الأبد لأنه لا توجد طريقة لإطلاقها دون حرق رموز L2.

IL2StandardERC20

يجب على كل رمز ERC-20 على L2 يستخدم الجسر القياسي توفير هذه الواجهة (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 // slither-disable-next-line external-function
8 function supportsInterface(bytes4 _interfaceId) public pure returns (bool) {
9 bytes4 firstSupportedInterface = bytes4(keccak256("supportsInterface(bytes4)")); // ERC165
10 bytes4 secondSupportedInterface = IL2StandardERC20.l1Token.selector ^
11 IL2StandardERC20.mint.selector ^
12 IL2StandardERC20.burn.selector;
13 return _interfaceId == firstSupportedInterface || _interfaceId == secondSupportedInterface;
14 }
إظهار الكل

هذه هي طريقة عمل ERC-165 (opens in a new tab). كل واجهة هي عدد من الوظائف المدعومة، ويتم تحديدها على أنها XOR (opens in a new tab) لـ مُحدِّدات وظائف ABI (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/* Interface Imports */
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 على أوبتيميزم كرمز ERC-20 بالعنوان 0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000 (opens in a new tab).
1/* Library Imports */
2import { ERC165Checker } from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol";
3import { CrossDomainEnabled } from "../../libraries/bridge/CrossDomainEnabled.sol";
4import { Lib_PredeployAddresses } from "../../libraries/constants/Lib_PredeployAddresses.sol";
5
6/* Contract Imports */
7import { IL2StandardERC20 } from "../../standards/IL2StandardERC20.sol";
8
9/**
10 * @title L2StandardBridge
11 * @dev جسر L2 القياسي هو عقد يعمل مع جسر L1 القياسي
12 * لتمكين انتقالات ETH و ERC20 بين L1 و L2.
13 * يعمل هذا العقد كأداة لسك رموز جديدة عندما يسمع عن إيداعات في جسر L1 القياسي.
14 * يعمل هذا العقد أيضًا كأداة لحرق الرموز المخصصة للسحب، ويبلغ جسر L1
15 * لإطلاق أموال L1.
16 */
17contract L2StandardBridge is IL2ERC20Bridge, CrossDomainEnabled {
18 /********************************
19 * مراجع العقود الخارجية *
20 ********************************/
21
22 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 // slither-disable-next-line reentrancy-events
24 IL2StandardERC20(_l2Token).burn(msg.sender, _amount);
إظهار الكل

لاحظ أننا لا نعتمد على معلمة _from بل على msg.sender وهو أصعب بكثير في التزييف (مستحيل، على حد علمي).

1
2 // إنشاء calldata لـ l1TokenBridge.finalizeERC20Withdrawal(_to, _amount)
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. عنوان L1 لعقد L2 ERC-20 يطابق مصدر L1 للرموز
1 ) {
2 // عند إنهاء الإيداع، نقوم بإيداع نفس المبلغ من
3 // الرموز في حساب L2.
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 الذي يتم الإيداع فيه لا يتفق مع العنوان الصحيح
3 // لرمز L1 الخاص به، أو أنه لا يدعم الواجهة الصحيحة.
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).

آخر تحديث للصفحة: 22 أكتوبر 2025

هل كانت تعليمات الاستخدام هذه مفيدة؟