شرح تفصيلي لعقد جسر أوبتيميزم القياسي
أوبتيميزم (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
- في حالة إيداع رمز ERC-20، يمنح المودِع الجسر إذنًا لإنفاق المبلغ الذي يتم إيداعه
- يستدعي المودِع جسر L1 (
depositERC20أوdepositERC20ToأوdepositETHأوdepositETHTo) - يستحوذ جسر L1 على الأصل المنقول عبر الجسر
- ETH: يتم تحويل الأصل من قِبل المودِع كجزء من الاستدعاء
- ERC-20: يتم تحويل الأصل بواسطة الجسر إلى نفسه باستخدام الإذن المقدم من المودِع
- يستخدم جسر L1 آلية الرسائل عبر النطاقات لاستدعاء
finalizeDepositعلى جسر L2
الطبقة 2
- يتحقق جسر L2 من أن استدعاء
finalizeDepositشرعي:- أنه أتى من عقد الرسائل عبر النطاقات
- كان في الأصل من الجسر على L1
- يتحقق جسر L2 مما إذا كان عقد رمز ERC-20 على L2 هو الصحيح:
- يُبلغ عقد L2 أن نظيره على L1 هو نفسه الذي أتت منه الرموز على L1
- يُبلغ عقد L2 أنه يدعم الواجهة الصحيحة (باستخدام ERC-165 (opens in a new tab)).
- إذا كان عقد L2 هو الصحيح، يتم استدعاؤه لسك العدد المناسب من الرموز إلى العنوان المناسب. إذا لم يكن كذلك، تبدأ عملية سحب للسماح للمستخدم بالمطالبة بالرموز على L1.
تدفق السحب
الطبقة 2
- يستدعي الساحب جسر L2 (
withdrawأوwithdrawTo) - يحرق جسر L2 العدد المناسب من الرموز التابعة لـ
msg.sender - يستخدم جسر L2 آلية الرسائل عبر النطاقات لاستدعاء
finalizeETHWithdrawalأوfinalizeERC20Withdrawalعلى جسر L1
الطبقة 1
- يتحقق جسر L1 من أن استدعاء
finalizeETHWithdrawalأوfinalizeERC20Withdrawalشرعي:- أنه أتى من آلية الرسائل عبر النطاقات
- كان في الأصل من الجسر على L2
- ينقل جسر 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 IL1ERC20Bridge3 */4interface IL1ERC20Bridge {5 /**********6 * الأحداث *7 **********/89 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 _data5 );من الممكن إضافة ملاحظات إلى التحويلات، وفي هذه الحالة تضاف إلى الأحداث التي تبلغ عنها.
1 event ERC20WithdrawalFinalized(2 address indexed _l1Token,3 address indexed _l2Token,4 address indexed _from,5 address _to,6 uint256 _amount,7 bytes _data8 );نفس عقد الجسر يعالج التحويلات في كلا الاتجاهين. في حالة جسر L1، يعني هذا بدء الإيداعات وإنهاء عمليات السحب.
12 /********************3 * الوظائف العامة *4 ********************/56 /**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 المقابل للطبقة 15 * @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 _data17 ) 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 المقابل للطبقة 15 * @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 _data19 ) external;إظهار الكلهذه الوظيفة مطابقة تقريبًا لـ depositERC20، لكنها تتيح لك إرسال ERC-20 إلى عنوان مختلف.
1 /*************************2 * وظائف عبر السلسلة *3 *************************/45 /**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 _data26 ) external;27}إظهار الكلعمليات السحب (والرسائل الأخرى من L2 إلى L1) في أوبتيميزم هي عملية من خطوتين:
- معاملة بدء على L2.
- معاملة إنهاء أو مطالبة على 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: MIT2pragma solidity >0.5.0 <0.9.0;34import "./IL1ERC20Bridge.sol";56/**7 * @title IL1StandardBridge8 */9interface IL1StandardBridge is IL1ERC20Bridge {10 /**********11 * الأحداث *12 **********/13 event ETHDepositInitiated(14 address indexed _from,15 address indexed _to,16 uint256 _amount,17 bytes _data18 );إظهار الكلهذا الحدث مطابق تقريبًا لإصدار ERC-20 (ERC20DepositInitiated)، باستثناء عدم وجود عناوين رموز L1 و L2.
وينطبق الشيء نفسه على الأحداث والوظائف الأخرى.
1 event ETHWithdrawalFinalized(2 .3 .4 .5 );67 /********************8 * الوظائف العامة *9 ********************/1011 /**12 * @dev إيداع مبلغ من ETH في رصيد المتصل على L2.13 .14 .15 .16 */17 function depositETH(uint32 _l2Gas, bytes calldata _data) external payable;1819 /**20 * @dev إيداع مبلغ من ETH في رصيد المستلم على L2.21 .22 .23 .24 */25 function depositETHTo(26 address _to,27 uint32 _l2Gas,28 bytes calldata _data29 ) external payable;3031 /*************************32 * وظائف عبر السلسلة *33 *************************/3435 /**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 _data48 ) external;49}إظهار الكلCrossDomainEnabled
هذا العقد (opens in a new tab) موروث من قبل كلا الجسرين (L1 و L2) لإرسال رسائل إلى الطبقة الأخرى.
1// SPDX-License-Identifier: MIT2pragma solidity >0.5.0 <0.9.0;34/* Interface Imports */5import { ICrossDomainMessenger } from "./ICrossDomainMessenger.sol";تخبر هذه الواجهة (opens in a new tab) العقد كيفية إرسال رسائل إلى الطبقة الأخرى، باستخدام مرسل الرسائل عبر النطاقات. مرسل الرسائل عبر النطاقات هذا هو نظام آخر تمامًا، ويستحق مقالًا خاصًا به، وآمل أن أكتبه في المستقبل.
1/**2 * @title CrossDomainEnabled3 * @dev عقد مساعد للعقود التي تقوم باتصالات عبر النطاقات4 *5 * المترجم المستخدم: يحدده العقد الوارث6 */7contract CrossDomainEnabled {8 /*************9 * المتغيرات *10 *************/1112 // عقد المرسال المستخدم لإرسال واستقبال الرسائل من النطاق الآخر.13 address public messenger;1415 /***************16 * المُنشئ *17 ***************/1819 /**20 * @param _messenger عنوان مرسل الرسائل عبر النطاقات على الطبقة الحالية.21 */22 constructor(address _messenger) {23 messenger = _messenger;24 }إظهار الكلالمعلمة الوحيدة التي يحتاج العقد إلى معرفتها، هي عنوان مرسل الرسائل عبر النطاقات على هذه الطبقة. يتم تعيين هذه المعلمة مرة واحدة، في المُنشئ، ولا تتغير أبدًا.
12 /**********************3 * مُعدِّلات الوظائف *4 **********************/56 /**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، كما ترى أدناه).
12 require(3 getCrossDomainMessenger().xDomainMessageSender() == _sourceDomainAccount,4 "OVM_XCHAIN: wrong sender of cross-domain message"5 );الطريقة التي يوفر بها مرسل الرسائل عبر النطاقات العنوان الذي أرسل رسالة بالطبقة الأخرى هي وظيفة .xDomainMessageSender() (opens in a new tab).
طالما يتم استدعاؤها في المعاملة التي بدأتها الرسالة، يمكنها توفير هذه المعلومات.
نحتاج إلى التأكد من أن الرسالة التي تلقيناها جاءت من الجسر الآخر.
12 _;3 }45 /**********************6 * الوظائف الداخلية *7 **********************/89 /**10 * يحصل على المرسال، عادة من التخزين. يتم كشف هذه الوظيفة في حالة احتياج عقد فرعي11 * لتجاوزها.12 * @return عنوان عقد مرسل الرسائل عبر النطاقات الذي يجب استخدامه.13 */14 function getCrossDomainMessenger() internal virtual returns (ICrossDomainMessenger) {15 return ICrossDomainMessenger(messenger);16 }إظهار الكلتعيد هذه الوظيفة مرسل الرسائل عبر النطاقات.
نستخدم وظيفة بدلاً من المتغير messenger للسماح للعقود التي ترث من هذا العقد باستخدام خوارزمية لتحديد أي مرسل رسائل عبر النطاقات يجب استخدامه.
12 /**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) هو محلل ثابت يقوم أوبتيميزم بتشغيله على كل عقد للبحث عن الثغرات والمشاكل المحتملة الأخرى. في هذه الحالة، يثير السطر التالي ثغرتين أمنيتين:
- ثغرات إعادة الدخول (Reentrancy) (opens in a new tab)
- إعادة الدخول الحميدة (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: MIT2pragma 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) طريقتين للإبلاغ عن فشل العقد:
- Revert (التراجع)
- إرجاع
false(خطأ)
إن التعامل مع كلتا الحالتين سيجعل الكود الخاص بنا أكثر تعقيدًا، لذلك بدلاً من ذلك نستخدم SafeERC20 من أوبن زبلين (opens in a new tab)، والذي يضمن أن تؤدي جميع حالات الفشل إلى تراجع (opens in a new tab).
1/**2 * @title L1StandardBridge3 * @dev جسر ETH و ERC20 للطبقة 1 هو عقد يخزن أموال L1 المودعة والرموز القياسية4 * المستخدمة على L2. يقوم بمزامنة جسر L2 المقابل، وإعلامه بالإيداعات5 * والاستماع إليه لمعرفة عمليات السحب التي تم إنهاؤها حديثًا.6 *7 */8contract L1StandardBridge is IL1StandardBridge, CrossDomainEnabled {9 using SafeERC20 for IERC20;إظهار الكلهذا هو السطر الذي نحدد به استخدام غلاف SafeERC20 في كل مرة نستخدم فيها واجهة IERC20.
12 /********************************3 * مراجع العقود الخارجية *4 ********************************/56 address public l2TokenBridge;عنوان L2StandardBridge.
12 // يربط رمز L1 برمز L2 برصيد رمز L1 المودع3 mapping(address => mapping(address => uint256)) public deposits;تخطيط (opens in a new tab) مزدوج مثل هذا هو الطريقة التي تُعرّف بها مصفوفة متناثرة ثنائية الأبعاد (opens in a new tab).
يتم تحديد القيم في بنية البيانات هذه على أنها deposit[عنوان رمز L1][عنوان رمز L2].
القيمة الافتراضية هي صفر.
يتم كتابة الخلايا التي تم تعيينها إلى قيمة مختلفة فقط في التخزين.
12 /***************3 * المُنشئ *4 ***************/56 // يعيش هذا العقد خلف وكيل، لذا لن تُستخدم معلمات المُنشئ.7 constructor() CrossDomainEnabled(address(0)) {}لكي نكون قادرين على ترقية هذا العقد دون الحاجة إلى نسخ جميع المتغيرات في التخزين.
للقيام بذلك نستخدم Proxy (opens in a new tab)، وهو عقد يستخدم delegatecall (opens in a new tab) لنقل الاستدعاءات إلى عقد منفصل يتم تخزين عنوانه بواسطة عقد الوكيل (عند الترقية تخبر الوكيل بتغيير هذا العنوان).
عند استخدام delegatecall، يظل التخزين هو تخزين العقد المستدعي، لذا تظل قيم جميع متغيرات حالة العقد غير متأثرة.
أحد تأثيرات هذا النمط هو أن تخزين العقد الذي هو المستدعى لـ delegatecall لا يُستخدم وبالتالي فإن قيم المُنشئ التي يتم تمريرها إليه لا تهم.
هذا هو السبب في أنه يمكننا تقديم قيمة غير منطقية لمُنشئ CrossDomainEnabled.
وهذا هو أيضًا سبب فصل التهيئة أدناه عن المُنشئ.
1 /******************2 * التهيئة *3 ******************/45 /**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 قبل المستخدم الشرعي. ولكن هناك طريقتان لمنع هذا:
- إذا لم يتم نشر العقود مباشرة من قبل EOA ولكن في معاملة بها عقد آخر يقوم بإنشائها (opens in a new tab) يمكن أن تكون العملية بأكملها ذرية، وتنتهي قبل تنفيذ أي معاملة أخرى.
- إذا فشل الاستدعاء الشرعي لـ
initialize، فمن الممكن دائمًا تجاهل الوكيل والجسر المنشأين حديثًا وإنشاء وكيل وجسر جديدين.
1 messenger = _l1messenger;2 l2TokenBridge = _l2TokenBridge;3 }هذه هي المعلمتان اللتان يحتاج الجسر إلى معرفتهما.
12 /**************3 * الإيداع *4 **************/56 /** @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 IL1StandardBridge3 */4 function depositETH(uint32 _l2Gas, bytes calldata _data) external payable onlyEOA {5 _initiateETHDeposit(msg.sender, msg.sender, _l2Gas, _data);6 }78 /**9 * @inheritdoc IL1StandardBridge10 */11 function depositETHTo(12 address _to,13 uint32 _l2Gas,14 bytes calldata _data15 ) external payable {16 _initiateETHDeposit(msg.sender, _to, _l2Gas, _data);17 }إظهار الكلهاتان الوظيفتان هما أغلفة حول _initiateETHDeposit، الوظيفة التي تتعامل مع إيداع ETH الفعلي.
1 /**2 * @dev ينفذ منطق الإيداعات عن طريق تخزين ETH وإبلاغ بوابة ETH L23 * بالإيداع.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 _data16 ) internal {17 // إنشاء calldata لاستدعاء finalizeDeposit18 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 _data8 );الرسالة هنا هي استدعاء وظيفة finalizeDeposit (opens in a new tab) بهذه المعلمات:
| Parameter | Value | المعنى |
|---|---|---|
| _l1Token | address(0) | قيمة خاصة لتمثيل ETH (والذي ليس رمز ERC-20) على L1 |
| _l2Token | Lib_PredeployAddresses.OVM_ETH | عقد L2 الذي يدير ETH على أوبتيميزم، 0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000 (هذا العقد للاستخدام الداخلي لـ أوبتيميزم فقط) |
| _from | _from | العنوان على L1 الذي يرسل ETH |
| _to | _to | العنوان على L2 الذي يستقبل ETH |
| المبلغ | msg.value | كمية الـ wei المرسلة (والتي تم إرسالها بالفعل إلى الجسر) |
| _data | _data | بيانات إضافية لإرفاقها بالإيداع |
1 // إرسال calldata إلى L22 // slither-disable-next-line reentrancy-events3 sendCrossDomainMessage(l2TokenBridge, _l2Gas, message);إرسال الرسالة من خلال مرسل الرسائل عبر النطاقات.
1 // slither-disable-next-line reentrancy-events2 emit ETHDepositInitiated(_from, _to, msg.value, _data);3 }إصدار حدث لإبلاغ أي تطبيق لامركزي يستمع بهذا التحويل.
1 /**2 * @inheritdoc IL1ERC20Bridge3 */4 function depositERC20(5 .6 .7 .8 ) external virtual onlyEOA {9 _initiateERC20Deposit(_l1Token, _l2Token, msg.sender, msg.sender, _amount, _l2Gas, _data);10 }1112 /**13 * @inheritdoc IL1ERC20Bridge14 */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 ينفذ منطق الإيداعات عن طريق إبلاغ عقد رمز الإيداع L23 * بالإيداع واستدعاء معالج لقفل أموال L1. (على سبيل المثال، transferFrom)4 *5 * @param _l1Token عنوان ERC20 للطبقة 1 الذي نودعه6 * @param _l2Token عنوان ERC20 للطبقة 2 المقابل للطبقة 17 * @param _from الحساب الذي سيتم سحب الإيداع منه على L18 * @param _to الحساب الذي سيتم منح الإيداع له على L29 * @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 _data23 ) internal {إظهار الكلهذه الوظيفة مشابهة لـ _initiateETHDeposit أعلاه، مع بعض الاختلافات المهمة.
الاختلاف الأول هو أن هذه الوظيفة تتلقى عناوين الرموز والمبلغ المطلوب تحويله كمعلمات.
في حالة ETH، يتضمن الاستدعاء إلى الجسر بالفعل نقل الأصل إلى حساب الجسر (msg.value).
1 // عند بدء إيداع على L1، ينقل جسر L1 الأموال إلى نفسه لعمليات2 // السحب المستقبلية. يتحقق safeTransferFrom أيضًا مما إذا كان العقد يحتوي على كود، لذلك سيفشل هذا3 // إذا كان _from حسابًا مملوكًا خارجيًا (EOA) أو address(0).4 // slither-disable-next-line reentrancy-events, reentrancy-benign5 IERC20(_l1Token).safeTransferFrom(_from, address(this), _amount);تتبع عمليات نقل رموز ERC-20 عملية مختلفة عن ETH:
- يمنح المستخدم (
_from) إذنًا للجسر لنقل الرموز المناسبة. - يستدعي المستخدم الجسر مع عنوان عقد الرمز، والمبلغ، إلخ.
- ينقل الجسر الرموز (إلى نفسه) كجزء من عملية الإيداع.
قد تحدث الخطوة الأولى في معاملة منفصلة عن الخطوتين الأخيرتين.
ومع ذلك، فإن التشغيل الأمامي ليس مشكلة لأن الوظيفتين اللتين تستدعيان _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 _data10 );1112 // إرسال calldata إلى L213 // slither-disable-next-line reentrancy-events, reentrancy-benign14 sendCrossDomainMessage(l2TokenBridge, _l2Gas, message);1516 // slither-disable-next-line reentrancy-benign17 deposits[_l1Token][_l2Token] = deposits[_l1Token][_l2Token] + _amount;إظهار الكلأضف المبلغ المودع من الرموز إلى بنية بيانات deposits.
قد تكون هناك عناوين متعددة على L2 تتوافق مع نفس رمز L1 ERC-20، لذلك لا يكفي استخدام رصيد الجسر من رمز L1 ERC-20 لتتبع الإيداعات.
12 // slither-disable-next-line reentrancy-events3 emit ERC20DepositInitiated(_l1Token, _l2Token, _from, _to, _amount, _data);4 }56 /*************************7 * وظائف عبر السلسلة *8 *************************/910 /**11 * @inheritdoc IL1StandardBridge12 */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-events2 (bool success, ) = _to.call{ value: _amount }(new bytes(0));طريقة تحويل ETH هي استدعاء المستلم بكمية wei في msg.value.
1 require(success, "TransferHelper::safeTransferETH: ETH transfer failed");23 // slither-disable-next-line reentrancy-events4 emit ETHWithdrawalFinalized(_from, _to, _amount, _data);إصدار حدث حول السحب.
1 }23 /**4 * @inheritdoc IL1ERC20Bridge5 */6 function finalizeERC20Withdrawal(7 address _l1Token,8 address _l2Token,9 address _from,10 address _to,11 uint256 _amount,12 bytes calldata _data13 ) external onlyFromCrossDomainAccount(l2TokenBridge) {إظهار الكلهذه الوظيفة مشابهة لـ finalizeETHWithdrawal أعلاه، مع التغييرات اللازمة لرموز ERC-20.
1 deposits[_l1Token][_l2Token] = deposits[_l1Token][_l2Token] - _amount;تحديث بنية بيانات deposits.
12 // عند إنهاء السحب على L1، ينقل جسر L1 الأموال إلى الساحب3 // slither-disable-next-line reentrancy-events4 IERC20(_l1Token).safeTransfer(_to, _amount);56 // slither-disable-next-line reentrancy-events7 emit ERC20WithdrawalFinalized(_l1Token, _l2Token, _from, _to, _amount, _data);8 }910 /*****************************11 * مؤقت - ترحيل ETH *12 *****************************/1314 /**15 * @dev يضيف رصيد ETH إلى الحساب. يهدف هذا إلى السماح بترحيل ETH16 * من بوابة قديمة إلى بوابة جديدة.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: MIT2pragma solidity ^0.8.9;34import { 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 مخططًا له عند تنفيذه أم لا.
12 function mint(address _to, uint256 _amount) external;34 function burn(address _from, uint256 _amount) external;56 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: MIT2pragma solidity ^0.8.9;34import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";عقد ERC-20 من أوبن زبلين (opens in a new tab). لا تؤمن أوبتيميزم بإعادة اختراع العجلة، خاصة عندما تكون العجلة مدققة جيدًا وتحتاج إلى أن تكون جديرة بالثقة بما يكفي لحمل الأصول.
1import "./IL2StandardERC20.sol";23contract L2StandardERC20 is IL2StandardERC20, ERC20 {4 address public l1Token;5 address public l2Bridge;هاتان هما المعلمتان الإضافيتان للتكوين اللتان نطلبهما ولا يطلبهما ERC-20 عادةً.
12 /**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 _symbol13 ) ERC20(_name, _symbol) {14 l1Token = _l1Token;15 l2Bridge = _l2Bridge;16 }إظهار الكلأولاً، استدعِ مُنشئ العقد الذي نرث منه (ERC20(_name, _symbol)) ثم عيّن متغيراتنا الخاصة.
12 modifier onlyL2Bridge() {3 require(msg.sender == l2Bridge, "Only L2 Bridge can mint and burn");4 _;5 }67 // slither-disable-next-line external-function8 function supportsInterface(bytes4 _interfaceId) public pure returns (bool) {9 bytes4 firstSupportedInterface = bytes4(keccak256("supportsInterface(bytes4)")); // ERC16510 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-function2 function mint(address _to, uint256 _amount) public virtual onlyL2Bridge {3 _mint(_to, _amount);45 emit Mint(_to, _amount);6 }78 // slither-disable-next-line external-function9 function burn(address _from, uint256 _amount) public virtual onlyL2Bridge {10 _burn(_from, _amount);1112 emit Burn(_from, _amount);13 }14}إظهار الكليُسمح فقط لجسر L2 بسك وحرق الأصول.
_mint و _burn مُعرَّفتان فعليًا في عقد ERC-20 من أوبن زبلين.
هذا العقد لا يكشفهما خارجيًا، لأن شروط سك وحرق الرموز متنوعة مثل عدد طرق استخدام ERC-20.
كود جسر L2
هذا هو الكود الذي يشغل الجسر على أوبتيميزم. مصدر هذا العقد موجود هنا (opens in a new tab).
1// SPDX-License-Identifier: MIT2pragma solidity ^0.8.9;34/* 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 الذي رأيناه أعلاه. هناك اختلافان كبيران:
- على L1، تبدأ الإيداعات وتنهي عمليات السحب. هنا تبدأ عمليات السحب وتنهي الإيداعات.
- على 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";56/* Contract Imports */7import { IL2StandardERC20 } from "../../standards/IL2StandardERC20.sol";89/**10 * @title L2StandardBridge11 * @dev جسر L2 القياسي هو عقد يعمل مع جسر L1 القياسي12 * لتمكين انتقالات ETH و ERC20 بين L1 و L2.13 * يعمل هذا العقد كأداة لسك رموز جديدة عندما يسمع عن إيداعات في جسر L1 القياسي.14 * يعمل هذا العقد أيضًا كأداة لحرق الرموز المخصصة للسحب، ويبلغ جسر L115 * لإطلاق أموال L1.16 */17contract L2StandardBridge is IL2ERC20Bridge, CrossDomainEnabled {18 /********************************19 * مراجع العقود الخارجية *20 ********************************/2122 address public l1TokenBridge;إظهار الكلتتبع عنوان جسر L1. لاحظ أنه على عكس مكافئ L1، فإننا نحتاج هنا إلى هذا المتغير. عنوان جسر L1 غير معروف مسبقًا.
12 /***************3 * المُنشئ *4 ***************/56 /**7 * @param _l2CrossDomainMessenger مرسال الرسائل عبر النطاقات المستخدم من قبل هذا العقد.8 * @param _l1TokenBridge عنوان جسر L1 المنشور على السلسلة الرئيسية.9 */10 constructor(address _l2CrossDomainMessenger, address _l1TokenBridge)11 CrossDomainEnabled(_l2CrossDomainMessenger)12 {13 l1TokenBridge = _l1TokenBridge;14 }1516 /***************17 * السحب *18 ***************/1920 /**21 * @inheritdoc IL2ERC20Bridge22 */23 function withdraw(24 address _l2Token,25 uint256 _amount,26 uint32 _l1Gas,27 bytes calldata _data28 ) external virtual {29 _initiateWithdrawal(_l2Token, msg.sender, msg.sender, _amount, _l1Gas, _data);30 }3132 /**33 * @inheritdoc IL2ERC20Bridge34 */35 function withdrawTo(36 address _l2Token,37 address _to,38 uint256 _amount,39 uint32 _l1Gas,40 bytes calldata _data41 ) external virtual {42 _initiateWithdrawal(_l2Token, msg.sender, _to, _amount, _l1Gas, _data);43 }إظهار الكلهاتان الوظيفتان تبدآن عمليات السحب. لاحظ أنه لا توجد حاجة لتحديد عنوان رمز L1. من المتوقع أن تخبرنا رموز L2 بعنوان مكافئ L1.
12 /**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 _data21 ) internal {22 // عند بدء السحب، نحرق أموال الساحب لمنع الاستخدام اللاحق لـ L223 // slither-disable-next-line reentrancy-events24 IL2StandardERC20(_l2Token).burn(msg.sender, _amount);إظهار الكللاحظ أننا لا نعتمد على معلمة _from بل على msg.sender وهو أصعب بكثير في التزييف (مستحيل، على حد علمي).
12 // إنشاء calldata لـ l1TokenBridge.finalizeERC20Withdrawal(_to, _amount)3 // slither-disable-next-line reentrancy-events4 address l1Token = IL2StandardERC20(_l2Token).l1Token();5 bytes memory message;67 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 _data7 );8 } else {9 message = abi.encodeWithSelector(10 IL1ERC20Bridge.finalizeERC20Withdrawal.selector,11 l1Token,12 _l2Token,13 _from,14 _to,15 _amount,16 _data17 );18 }1920 // إرسال رسالة إلى جسر L121 // slither-disable-next-line reentrancy-events22 sendCrossDomainMessage(l1TokenBridge, _l1Gas, message);2324 // slither-disable-next-line reentrancy-events25 emit WithdrawalInitiated(l1Token, _l2Token, msg.sender, _to, _amount, _data);26 }2728 /************************************29 * وظيفة عبر السلسلة: الإيداع *30 ************************************/3132 /**33 * @inheritdoc IL2ERC20Bridge34 */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-events5 ERC165Checker.supportsInterface(_l2Token, 0x1d1d8b63) &&6 _l1Token == IL2StandardERC20(_l2Token).l1Token()فحوصات السلامة:
- الواجهة الصحيحة مدعومة
- عنوان L1 لعقد L2 ERC-20 يطابق مصدر L1 للرموز
1 ) {2 // عند إنهاء الإيداع، نقوم بإيداع نفس المبلغ من3 // الرموز في حساب L2.4 // slither-disable-next-line reentrancy-events5 IL2StandardERC20(_l2Token).mint(_to, _amount);6 // slither-disable-next-line reentrancy-events7 emit DepositFinalized(_l1Token, _l2Token, _from, _to, _amount, _data);إذا نجحت فحوصات السلامة، قم بإنهاء الإيداع:
- سك الرموز
- إصدار الحدث المناسب
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 _data9 );1011 // إرسال رسالة إلى جسر L112 // slither-disable-next-line reentrancy-events13 sendCrossDomainMessage(l1TokenBridge, 0, message);14 // slither-disable-next-line reentrancy-events15 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