جولة في عقد الجسر القياسي لشبكة أوبتيميزم
أوبتيميزم (opens in a new tab) هي رول أب متفائل. يمكن للتجميعات المتفائلة معالجة المعاملات بسعر أقل بكثير من شبكة إيثيريوم الرئيسية (المعروفة أيضًا باسم طبقة 1 (L1)) لأن المعاملات تتم معالجتها بواسطة عدد قليل من العقد، بدلاً من كل عقدة على الشبكة. في الوقت نفسه، تتم كتابة جميع البيانات على طبقة 1 (L1) بحيث يمكن إثبات كل شيء وإعادة بنائه مع جميع ضمانات النزاهة والتوافر الخاصة بالشبكة الرئيسية.
لاستخدام أصول طبقة 1 (L1) على أوبتيميزم (أو أي طبقة 2 (L2) أخرى)، يجب نقل الأصول عبر جسر. إحدى الطرق لتحقيق ذلك هي أن يقوم المستخدمون بقفل الأصول (ETH ورموز ERC-20 المميزة هي الأكثر شيوعًا) على طبقة 1 (L1)، وتلقي أصول معادلة لاستخدامها على طبقة 2 (L2). في النهاية، قد يرغب من ينتهي به الأمر بامتلاكها في إعادتها عبر الجسر إلى طبقة 1 (L1). عند القيام بذلك، يتم حرق الأصول على طبقة 2 (L2) ثم يتم تحريرها مرة أخرى للمستخدم على طبقة 1 (L1).
هذه هي الطريقة التي يعمل بها الجسر القياسي لشبكة أوبتيميزم (opens in a new tab). في هذه المقالة، نراجع الكود المصدري لذلك الجسر لنرى كيف يعمل وندرسه كمثال على كود Solidity مكتوب بشكل جيد.
تدفقات التحكم
يحتوي الجسر على تدفقين رئيسيين:
- الإيداع (من طبقة 1 (L1) إلى طبقة 2 (L2))
- السحب (من طبقة 2 (L2) إلى طبقة 1 (L1))
تدفق الإيداع
طبقة 1 (L1)
- في حالة إيداع ERC-20، يمنح المودع الجسر سماحية لإنفاق المبلغ المودع
- يستدعي المودع جسر طبقة 1 (L1) (
depositERC20، أوdepositERC20To، أوdepositETH، أوdepositETHTo) - يستحوذ جسر طبقة 1 (L1) على الأصل المنقول عبر الجسر
- ETH: يتم تحويل الأصل بواسطة المودع كجزء من الاستدعاء
- ERC-20: يتم تحويل الأصل بواسطة الجسر إلى نفسه باستخدام السماحية المقدمة من المودع
- يستخدم جسر طبقة 1 (L1) آلية الرسائل عبر النطاقات لاستدعاء
finalizeDepositعلى جسر طبقة 2 (L2)
طبقة 2 (L2)
- يتحقق جسر طبقة 2 (L2) من أن الاستدعاء إلى
finalizeDepositشرعي:- جاء من عقد الرسائل عبر النطاقات
- كان في الأصل من الجسر على طبقة 1 (L1)
- يتحقق جسر طبقة 2 (L2) مما إذا كان عقد الرمز المميز ERC-20 على طبقة 2 (L2) هو العقد الصحيح:
- يُبلغ عقد طبقة 2 (L2) أن نظيره في طبقة 1 (L1) هو نفسه الذي جاءت منه الرموز المميزة على طبقة 1 (L1)
- يُبلغ عقد طبقة 2 (L2) أنه يدعم الواجهة الصحيحة (باستخدام ERC-165 (opens in a new tab)).
- إذا كان عقد طبقة 2 (L2) هو العقد الصحيح، فاستدعه لسك العدد المناسب من الرموز المميزة إلى العنوان المناسب. إذا لم يكن كذلك، فابدأ عملية سحب للسماح للمستخدم بالمطالبة بالرموز المميزة على طبقة 1 (L1).
تدفق السحب
طبقة 2 (L2)
- يستدعي الساحب جسر طبقة 2 (L2) (
withdrawأوwithdrawTo) - يحرق جسر طبقة 2 (L2) العدد المناسب من الرموز المميزة التي تنتمي إلى
msg.sender - يستخدم جسر طبقة 2 (L2) آلية الرسائل عبر النطاقات لاستدعاء
finalizeETHWithdrawalأوfinalizeERC20Withdrawalعلى جسر طبقة 1 (L1)
طبقة 1 (L1)
- يتحقق جسر طبقة 1 (L1) من أن الاستدعاء إلى
finalizeETHWithdrawalأوfinalizeERC20Withdrawalشرعي:- جاء من آلية الرسائل عبر النطاقات
- كان في الأصل من الجسر على طبقة 2 (L2)
- يحول جسر طبقة 1 (L1) الأصل المناسب (ETH أو ERC-20) إلى العنوان المناسب
كود طبقة 1 (L1)
هذا هو الكود الذي يعمل على طبقة 1 (L1)، شبكة إيثيريوم الرئيسية.
IL1ERC20Bridge
تم تعريف هذه الواجهة هنا (opens in a new tab). تتضمن دوال وتعريفات مطلوبة لنقل رموز ERC-20 المميزة عبر الجسر.
// SPDX-License-Identifier: MIT
يتم إصدار معظم كود أوبتيميزم بموجب ترخيص MIT (opens in a new tab).
pragma solidity >0.5.0 <0.9.0;
وقت كتابة هذا المقال، أحدث إصدار من Solidity هو 0.8.12. حتى يتم إصدار الإصدار 0.9.0، لا نعرف ما إذا كان هذا الكود متوافقًا معه أم لا.
/**
* @title IL1ERC20Bridge
*/
interface IL1ERC20Bridge {
/**********
* أحداث *
**********/
event ERC20DepositInitiated(
في مصطلحات جسر أوبتيميزم، يعني الإيداع التحويل من طبقة 1 (L1) إلى طبقة 2 (L2)، ويعني السحب التحويل من طبقة 2 (L2) إلى طبقة 1 (L1).
address indexed _l1Token,
address indexed _l2Token,
في معظم الحالات، لا يكون عنوان ERC-20 على طبقة 1 (L1) هو نفس عنوان ERC-20 المعادل على طبقة 2 (L2).
يمكنك رؤية قائمة عناوين الرموز المميزة هنا (opens in a new tab).
العنوان الذي يحتوي على chainId 1 موجود على طبقة 1 (L1) (الشبكة الرئيسية) والعنوان الذي يحتوي على chainId 10 موجود على طبقة 2 (L2) (أوبتيميزم).
القيمتان الأخريان لـ chainId هما لشبكة اختبار Kovan (42) وشبكة اختبار Optimistic Kovan (69).
address indexed _from,
address _to,
uint256 _amount,
bytes _data
);
من الممكن إضافة ملاحظات إلى التحويلات، وفي هذه الحالة تتم إضافتها إلى الأحداث التي تبلغ عنها.
event ERC20WithdrawalFinalized(
address indexed _l1Token,
address indexed _l2Token,
address indexed _from,
address _to,
uint256 _amount,
bytes _data
);
يتعامل عقد الجسر نفسه مع التحويلات في كلا الاتجاهين. في حالة جسر طبقة 1 (L1)، يعني هذا بدء الإيداعات وإتمام عمليات السحب.
/********************
* دوال عامة *
********************/
/**
* @dev الحصول على عنوان عقد جسر طبقة 2 (L2) المقابل.
* @return عنوان عقد جسر طبقة 2 (L2) المقابل.
*/
function l2TokenBridge() external returns (address);
هذه الدالة ليست ضرورية حقًا، لأنها على طبقة 2 (L2) عبارة عن عقد منشور مسبقًا، لذا فهي دائمًا على العنوان 0x4200000000000000000000000000000000000010.
إنها هنا من أجل التماثل مع جسر طبقة 2 (L2)، لأن عنوان جسر طبقة 1 (L1) ليس من السهل معرفته.
/**
* @dev إيداع مبلغ من ERC-20 في رصيد المتصل على طبقة 2 (L2).
* @param _l1Token عنوان ERC-20 على طبقة 1 (L1) الذي نقوم بإيداعه
* @param _l2Token عنوان ERC-20 على طبقة 2 (L2) المقابل لـ طبقة 1 (L1)
* @param _amount مبلغ ERC-20 المراد إيداعه
* @param _l2Gas حد الغاز المطلوب لإكمال الإيداع على طبقة 2 (L2).
* @param _data بيانات اختيارية لإرسالها إلى طبقة 2 (L2). يتم توفير هذه البيانات
* فقط لراحة العقود الخارجية. بصرف النظر عن فرض حد أقصى
* للطول، لا تقدم هذه العقود أي ضمانات حول محتواها.
*/
function depositERC20(
address _l1Token,
address _l2Token,
uint256 _amount,
uint32 _l2Gas,
bytes calldata _data
) external;
المعلمة _l2Gas هي كمية غاز طبقة 2 (L2) المسموح للمعاملة بإنفاقها.
حتى حد (مرتفع) معين، يكون هذا مجانيًا (opens in a new tab)، لذلك ما لم يقم عقد ERC-20 بشيء غريب حقًا عند السك، فلا ينبغي أن يمثل ذلك مشكلة.
تتولى هذه الدالة السيناريو الشائع، حيث ينقل المستخدم الأصول عبر الجسر إلى نفس العنوان على سلسلة كتل مختلفة.
/**
* @dev إيداع مبلغ من ERC-20 في رصيد المستلم على طبقة 2 (L2).
* @param _l1Token عنوان ERC-20 على طبقة 1 (L1) الذي نقوم بإيداعه
* @param _l2Token عنوان ERC-20 على طبقة 2 (L2) المقابل لـ طبقة 1 (L1)
* @param _to عنوان طبقة 2 (L2) لإضافة السحب إليه.
* @param _amount مبلغ ERC-20 المراد إيداعه.
* @param _l2Gas حد الغاز المطلوب لإكمال الإيداع على طبقة 2 (L2).
* @param _data بيانات اختيارية لإرسالها إلى طبقة 2 (L2). يتم توفير هذه البيانات
* فقط لراحة العقود الخارجية. بصرف النظر عن فرض حد أقصى
* للطول، لا تقدم هذه العقود أي ضمانات حول محتواها.
*/
function depositERC20To(
address _l1Token,
address _l2Token,
address _to,
uint256 _amount,
uint32 _l2Gas,
bytes calldata _data
) external;
هذه الدالة متطابقة تقريبًا مع depositERC20، لكنها تتيح لك إرسال ERC-20 إلى عنوان مختلف.
/*************************
* دوال عبر السلاسل *
*************************/
/**
* @dev إكمال سحب من طبقة 2 (L2) إلى طبقة 1 (L1)، وإضافة الأموال إلى رصيد المستلم من
* الرمز المميز ERC-20 على طبقة 1 (L1).
* سيفشل هذا الاستدعاء إذا لم يتم الانتهاء من السحب المهيأ من طبقة 2 (L2).
*
* @param _l1Token عنوان الرمز المميز على طبقة 1 (L1) لإنهاء السحب (finalizeWithdrawal) له.
* @param _l2Token عنوان الرمز المميز على طبقة 2 (L2) حيث تم بدء السحب.
* @param _from عنوان طبقة 2 (L2) الذي بدأ التحويل.
* @param _to عنوان طبقة 1 (L1) لإضافة السحب إليه.
* @param _amount مبلغ ERC-20 المراد إيداعه.
* @param _data البيانات المقدمة من المرسل على طبقة 2 (L2). يتم توفير هذه البيانات
* فقط لراحة العقود الخارجية. بصرف النظر عن فرض حد أقصى
* للطول، لا تقدم هذه العقود أي ضمانات حول محتواها.
*/
function finalizeERC20Withdrawal(
address _l1Token,
address _l2Token,
address _from,
address _to,
uint256 _amount,
bytes calldata _data
) external;
}
عمليات السحب (والرسائل الأخرى من طبقة 2 (L2) إلى طبقة 1 (L1)) في أوبتيميزم هي عملية من خطوتين:
- معاملة بدء على طبقة 2 (L2).
- معاملة إتمام أو مطالبة على طبقة 1 (L1). يجب أن تحدث هذه المعاملة بعد انتهاء فترة تحدي الخطأ (opens in a new tab) لمعاملة طبقة 2 (L2).
IL1StandardBridge
تم تعريف هذه الواجهة هنا (opens in a new tab).
يحتوي هذا الملف على تعريفات الأحداث والدوال الخاصة بـ ETH.
هذه التعريفات مشابهة جدًا لتلك المحددة في IL1ERC20Bridge أعلاه لـ ERC-20.
تنقسم واجهة الجسر بين ملفين لأن بعض رموز ERC-20 المميزة تتطلب معالجة مخصصة ولا يمكن التعامل معها بواسطة الجسر القياسي.
بهذه الطريقة، يمكن للجسر المخصص الذي يتعامل مع مثل هذا الرمز المميز تنفيذ IL1ERC20Bridge ولا يضطر أيضًا إلى نقل ETH عبر الجسر.
// SPDX-License-Identifier: MIT
pragma solidity >0.5.0 <0.9.0;
import "./IL1ERC20Bridge.sol";
/**
* @title IL1StandardBridge
*/
interface IL1StandardBridge is IL1ERC20Bridge {
/**********
* أحداث *
**********/
event ETHDepositInitiated(
address indexed _from,
address indexed _to,
uint256 _amount,
bytes _data
);
هذا الحدث متطابق تقريبًا مع إصدار ERC-20 (ERC20DepositInitiated)، باستثناء عدم وجود عناوين الرموز المميزة لطبقة 1 (L1) وطبقة 2 (L2).
الشيء نفسه ينطبق على الأحداث والدوال الأخرى.
event ETHWithdrawalFinalized(
.
.
.
);
/********************
* دوال عامة *
********************/
/**
* @dev إيداع مبلغ من ETH في رصيد المتصل على طبقة 2 (L2).
.
.
.
*/
function depositETH(uint32 _l2Gas, bytes calldata _data) external payable;
/**
* @dev إيداع مبلغ من ETH في رصيد المستلم على طبقة 2 (L2).
.
.
.
*/
function depositETHTo(
address _to,
uint32 _l2Gas,
bytes calldata _data
) external payable;
/*************************
* دوال عبر السلاسل *
*************************/
/**
* @dev إكمال سحب من طبقة 2 (L2) إلى طبقة 1 (L1)، وإضافة الأموال إلى رصيد المستلم من
* الرمز المميز ETH على طبقة 1 (L1). نظرًا لأن xDomainMessenger فقط يمكنه استدعاء هذه الدالة، فلن يتم استدعاؤها أبدًا
* قبل الانتهاء من السحب.
.
.
.
*/
function finalizeETHWithdrawal(
address _from,
address _to,
uint256 _amount,
bytes calldata _data
) external;
}
CrossDomainEnabled
يتم توريث هذا العقد (opens in a new tab) بواسطة كلا الجسرين (طبقة 1 (L1) وطبقة 2 (L2)) لإرسال رسائل إلى الطبقة الأخرى.
// SPDX-License-Identifier: MIT
pragma solidity >0.5.0 <0.9.0;
/* استيرادات الواجهة */
import { ICrossDomainMessenger } from "./ICrossDomainMessenger.sol";
تخبر هذه الواجهة (opens in a new tab) العقد بكيفية إرسال رسائل إلى الطبقة الأخرى، باستخدام مرسل الرسائل عبر النطاقات. مرسل الرسائل عبر النطاقات هذا هو نظام آخر بالكامل، ويستحق مقالًا خاصًا به، والذي آمل أن أكتبه في المستقبل.
/**
* @title CrossDomainEnabled
* @dev عقد مساعد للعقود التي تقوم باتصالات عبر النطاقات
*
* المترجم المستخدم: محدد بواسطة العقد الوارث
*/
contract CrossDomainEnabled {
/*************
* متغيرات *
*************/
// عقد المراسل المستخدم لإرسال واستقبال الرسائل من النطاق الآخر.
address public messenger;
/***************
* مُنشئ *
***************/
/**
* @param _messenger عنوان CrossDomainMessenger على الطبقة الحالية.
*/
constructor(address _messenger) {
messenger = _messenger;
}
المعلمة الوحيدة التي يحتاج العقد إلى معرفتها، هي عنوان مرسل الرسائل عبر النطاقات على هذه الطبقة. يتم تعيين هذه المعلمة مرة واحدة، في المُنشئ، ولا تتغير أبدًا.
/**********************
* معدلات الدوال *
**********************/
/**
* يفرض أن الدالة المعدلة لا يمكن استدعاؤها إلا بواسطة حساب محدد عبر النطاقات.
* @param _sourceDomainAccount الحساب الوحيد على النطاق الأصلي
* المصرح له باستدعاء هذه الدالة.
*/
modifier onlyFromCrossDomainAccount(address _sourceDomainAccount) {
يمكن الوصول إلى المراسلة عبر النطاقات بواسطة أي عقد على سلسلة الكتل حيث يتم تشغيله (إما شبكة إيثيريوم الرئيسية أو أوبتيميزم). لكننا نحتاج إلى أن يثق الجسر على كل جانب فقط في رسائل معينة إذا كانت قادمة من الجسر على الجانب الآخر.
require(
msg.sender == address(getCrossDomainMessenger()),
"OVM_XCHAIN: messenger contract unauthenticated"
);
يمكن الوثوق فقط بالرسائل الواردة من مرسل الرسائل عبر النطاقات المناسب (messenger، كما ترى أدناه).
require(
getCrossDomainMessenger().xDomainMessageSender() == _sourceDomainAccount,
"OVM_XCHAIN: wrong sender of cross-domain message"
);
الطريقة التي يوفر بها مرسل الرسائل عبر النطاقات العنوان الذي أرسل رسالة مع الطبقة الأخرى هي الدالة .xDomainMessageSender() (opens in a new tab).
طالما تم استدعاؤها في المعاملة التي بدأتها الرسالة، فيمكنها توفير هذه المعلومات.
نحتاج إلى التأكد من أن الرسالة التي تلقيناها جاءت من الجسر الآخر.
_;
}
/**********************
* دوال داخلية *
**********************/
/**
* يحصل على المراسل، عادة من التخزين. يتم كشف هذه الدالة في حال احتاج عقد فرعي
* إلى التجاوز.
* @return عنوان عقد المراسل عبر النطاقات الذي يجب استخدامه.
*/
function getCrossDomainMessenger() internal virtual returns (ICrossDomainMessenger) {
return ICrossDomainMessenger(messenger);
}
تُرجع هذه الدالة مرسل الرسائل عبر النطاقات.
نستخدم دالة بدلاً من المتغير messenger للسماح للعقود التي ترث من هذا العقد باستخدام خوارزمية لتحديد مرسل الرسائل عبر النطاقات الذي يجب استخدامه.
/**
* يرسل رسالة إلى حساب على نطاق آخر
* @param _crossDomainTarget المستلم المقصود على النطاق الوجهة
* @param _message البيانات المراد إرسالها إلى الهدف (عادةً بيانات الاستدعاء لدالة تحتوي على
* `onlyFromCrossDomainAccount()`)
* @param _gasLimit حد الغاز (gasLimit) لاستلام الرسالة على النطاق الوجهة.
*/
function sendCrossDomainMessage(
address _crossDomainTarget,
uint32 _gasLimit,
bytes memory _message
أخيرًا، الدالة التي ترسل رسالة إلى الطبقة الأخرى.
) internal {
// slither-disable-next-line reentrancy-events, reentrancy-benign
سليذر (opens in a new tab) هو محلل ثابت تقوم أوبتيميزم بتشغيله على كل عقد للبحث عن نقاط الضعف والمشاكل المحتملة الأخرى. في هذه الحالة، يؤدي السطر التالي إلى إطلاق نقطتي ضعف:
getCrossDomainMessenger().sendMessage(_crossDomainTarget, _message, _gasLimit);
}
}
في هذه الحالة، لسنا قلقين بشأن إعادة الدخول، فنحن نعلم أن getCrossDomainMessenger() تُرجع عنوانًا جديرًا بالثقة، حتى لو لم يكن لدى سليذر طريقة لمعرفة ذلك.
عقد جسر طبقة 1 (L1)
الكود المصدري لهذا العقد موجود هنا (opens in a new tab).
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
يمكن أن تكون الواجهات جزءًا من عقود أخرى، لذا يجب أن تدعم مجموعة واسعة من إصدارات Solidity. لكن الجسر نفسه هو عقدنا، ويمكننا أن نكون صارمين بشأن إصدار Solidity الذي يستخدمه.
/* استيرادات الواجهة */
import { IL1StandardBridge } from "./IL1StandardBridge.sol";
import { IL1ERC20Bridge } from "./IL1ERC20Bridge.sol";
تم شرح IL1ERC20Bridge وIL1StandardBridge أعلاه.
import { IL2ERC20Bridge } from "../../L2/messaging/IL2ERC20Bridge.sol";
تتيح لنا هذه الواجهة (opens in a new tab) إنشاء رسائل للتحكم في الجسر القياسي على طبقة 2 (L2).
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
تتيح لنا هذه الواجهة (opens in a new tab) التحكم في عقود ERC-20. يمكنك قراءة المزيد عنها هنا.
/* استيرادات المكتبة */
import { CrossDomainEnabled } from "../../libraries/bridge/CrossDomainEnabled.sol";
كما هو موضح أعلاه، يُستخدم هذا العقد للمراسلة بين الطبقات.
import { Lib_PredeployAddresses } from "../../libraries/constants/Lib_PredeployAddresses.sol";
يحتوي Lib_PredeployAddresses (opens in a new tab) على عناوين عقود طبقة 2 (L2) التي لها نفس العنوان دائمًا. يتضمن هذا الجسر القياسي على طبقة 2 (L2).
import { Address } from "@openzeppelin/contracts/utils/Address.sol";
أدوات العناوين المساعدة من أوبن زبلن (opens in a new tab). تُستخدم للتمييز بين عناوين العقود وتلك التي تنتمي إلى حسابات مملوكة خارجيًا (EOA).
لاحظ أن هذا ليس حلاً مثاليًا، لأنه لا توجد طريقة للتمييز بين الاستدعاءات المباشرة والاستدعاءات التي يتم إجراؤها من مُنشئ العقد، ولكن على الأقل يتيح لنا هذا تحديد ومنع بعض أخطاء المستخدم الشائعة.
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
يدعم معيار ERC-20 (opens in a new tab) طريقتين للعقد للإبلاغ عن الفشل:
- التراجع
- إرجاع
false
التعامل مع كلتا الحالتين سيجعل الكود الخاص بنا أكثر تعقيدًا، لذا بدلاً من ذلك نستخدم SafeERC20 من أوبن زبلن (opens in a new tab)، والذي يضمن أن تؤدي جميع حالات الفشل إلى تراجع (opens in a new tab).
/**
* @title L1StandardBridge
* @dev جسر ETH و ERC-20 على طبقة 1 (L1) هو عقد يخزن أموال طبقة 1 (L1) المودعة والرموز المميزة
* القياسية المستخدمة على طبقة 2 (L2). يقوم بمزامنة جسر طبقة 2 (L2) المقابل، وإبلاغه بالإيداعات
* والاستماع إليه لمعرفة عمليات السحب المنتهية حديثًا.
*
*/
contract L1StandardBridge is IL1StandardBridge, CrossDomainEnabled {
using SafeERC20 for IERC20;
هذا السطر هو كيفية تحديد استخدام الغلاف SafeERC20 في كل مرة نستخدم فيها الواجهة IERC20.
/********************************
* مراجع العقود الخارجية *
********************************/
address public l2TokenBridge;
عنوان L2StandardBridge.
// يربط الرمز المميز على طبقة 1 (L1) بالرمز المميز على طبقة 2 (L2) برصيد الرمز المميز المودع على طبقة 1 (L1)
mapping(address => mapping(address => uint256)) public deposits;
التعيين (opens in a new tab) المزدوج مثل هذا هو الطريقة التي تحدد بها مصفوفة متفرقة ثنائية الأبعاد (opens in a new tab).
يتم تحديد القيم في بنية البيانات هذه كـ deposit[L1 token addr][L2 token addr].
القيمة الافتراضية هي صفر.
تتم كتابة الخلايا التي تم تعيينها إلى قيمة مختلفة فقط في التخزين.
/***************
* مُنشئ *
***************/
// يعيش هذا العقد خلف وكيل (proxy)، لذلك ستبقى معلمات المُنشئ غير مستخدمة.
constructor() CrossDomainEnabled(address(0)) {}
نريد أن نكون قادرين على ترقية هذا العقد دون الحاجة إلى نسخ جميع المتغيرات في التخزين.
للقيام بذلك، نستخدم Proxy (opens in a new tab)، وهو عقد يستخدم delegatecall (opens in a new tab) لنقل الاستدعاءات إلى عقد منفصل يتم تخزين عنوانه بواسطة عقد وكيل (عند الترقية، تخبر الوكيل بتغيير هذا العنوان).
عند استخدام delegatecall، يظل التخزين هو تخزين العقد المستدعي، لذلك لا تتأثر قيم جميع متغيرات حالة العقد.
أحد تأثيرات هذا النمط هو أن تخزين العقد الذي يتم استدعاؤه بواسطة delegatecall لا يتم استخدامه، وبالتالي فإن قيم المُنشئ التي تم تمريرها إليه لا تهم.
هذا هو السبب في أنه يمكننا توفير قيمة غير منطقية لمُنشئ CrossDomainEnabled.
وهو أيضًا السبب في أن التهيئة أدناه منفصلة عن المُنشئ.
/******************
* تهيئة *
******************/
/**
* @param _l1messenger عنوان مراسل طبقة 1 (L1) المستخدم للاتصالات عبر السلاسل.
* @param _l2TokenBridge عنوان الجسر القياسي على طبقة 2 (L2).
*/
// slither-disable-next-line external-function
يحدد اختبار سليذر (opens in a new tab) هذا الدوال التي لا يتم استدعاؤها من كود العقد وبالتالي يمكن الإعلان عنها كـ external بدلاً من public.
يمكن أن تكون تكلفة الغاز لدوال external أقل، لأنه يمكن تزويدها بمعلمات في بيانات الاستدعاء.
يجب أن تكون الدوال المعلن عنها كـ public قابلة للوصول من داخل العقد.
لا يمكن للعقود تعديل بيانات الاستدعاء الخاصة بها، لذا يجب أن تكون المعلمات في الذاكرة.
عند استدعاء مثل هذه الدالة خارجيًا، من الضروري نسخ بيانات الاستدعاء إلى الذاكرة، مما يكلف غازًا.
في هذه الحالة، يتم استدعاء الدالة مرة واحدة فقط، لذا فإن عدم الكفاءة لا يهمنا.
function initialize(address _l1messenger, address _l2TokenBridge) public {
require(messenger == address(0), "Contract has already been initialized.");
يجب استدعاء الدالة initialize مرة واحدة فقط.
إذا تغير عنوان إما مرسل الرسائل عبر النطاقات لطبقة 1 (L1) أو جسر الرموز المميزة لطبقة 2 (L2)، فإننا ننشئ وكيلًا جديدًا وجسرًا جديدًا يستدعيه.
من غير المرجح أن يحدث هذا إلا عند ترقية النظام بأكمله، وهو حدث نادر جدًا.
لاحظ أن هذه الدالة لا تحتوي على أي آلية تقيد من يمكنه استدعاؤها.
هذا يعني أنه من الناحية النظرية يمكن للمهاجم الانتظار حتى ننشر الوكيل والإصدار الأول من الجسر ثم يقوم بـ الاستباق (opens in a new tab) للوصول إلى الدالة initialize قبل المستخدم الشرعي. ولكن هناك طريقتان لمنع ذلك:
- إذا لم يتم نشر العقود مباشرة بواسطة حساب مملوك خارجيًا (EOA) ولكن في معاملة تجعل عقدًا آخر ينشئها (opens in a new tab)، فيمكن أن تكون العملية بأكملها ذرية، وتنتهي قبل تنفيذ أي معاملة أخرى.
- إذا فشل الاستدعاء الشرعي لـ
initialize، فمن الممكن دائمًا تجاهل الوكيل والجسر المنشأين حديثًا وإنشاء وكيل وجسر جديدين.
messenger = _l1messenger;
l2TokenBridge = _l2TokenBridge;
}
هاتان هما المعلمتان اللتان يحتاج الجسر إلى معرفتهما.
/**************
* إيداع *
**************/
/** @dev مُعدِّل يشترط أن يكون المرسل حسابًا مملوكًا خارجيًا (EOA). يمكن تجاوز هذا التحقق بواسطة عقد
* ضار عبر كود التهيئة (initcode)، لكنه يعالج خطأ المستخدم الذي نريد تجنبه.
*/
modifier onlyEOA() {
// يستخدم لإيقاف الإيداعات من العقود (لتجنب فقدان الرموز المميزة عن طريق الخطأ)
require(!Address.isContract(msg.sender), "Account not EOA");
_;
}
هذا هو السبب في أننا احتجنا إلى أدوات Address المساعدة من أوبن زبلن.
/**
* @dev يمكن استدعاء هذه الدالة بدون بيانات
* لإيداع مبلغ من ETH في رصيد المتصل على طبقة 2 (L2).
* نظرًا لأن دالة الاستلام (receive) لا تأخذ بيانات، يتم إرسال مبلغ
* افتراضي محافظ إلى طبقة 2 (L2).
*/
receive() external payable onlyEOA {
_initiateETHDeposit(msg.sender, msg.sender, 200_000, bytes(""));
}
هذه الدالة موجودة لأغراض الاختبار. لاحظ أنها لا تظهر في تعريفات الواجهة - فهي ليست للاستخدام العادي.
/**
* @inheritdoc IL1StandardBridge
*/
function depositETH(uint32 _l2Gas, bytes calldata _data) external payable onlyEOA {
_initiateETHDeposit(msg.sender, msg.sender, _l2Gas, _data);
}
/**
* @inheritdoc IL1StandardBridge
*/
function depositETHTo(
address _to,
uint32 _l2Gas,
bytes calldata _data
) external payable {
_initiateETHDeposit(msg.sender, _to, _l2Gas, _data);
}
هاتان الدالتان عبارة عن أغلفة حول _initiateETHDeposit، وهي الدالة التي تتعامل مع إيداع ETH الفعلي.
/**
* @dev ينفذ منطق الإيداعات عن طريق تخزين ETH وإبلاغ بوابة ETH على طبقة 2 (L2) بـ
* الإيداع.
* @param _from الحساب الذي سيتم سحب الإيداع منه على طبقة 1 (L1).
* @param _to الحساب الذي سيتم إعطاء الإيداع له على طبقة 2 (L2).
* @param _l2Gas حد الغاز المطلوب لإكمال الإيداع على طبقة 2 (L2).
* @param _data بيانات اختيارية لإرسالها إلى طبقة 2 (L2). يتم توفير هذه البيانات
* فقط لراحة العقود الخارجية. بصرف النظر عن فرض حد أقصى
* للطول، لا تقدم هذه العقود أي ضمانات حول محتواها.
*/
function _initiateETHDeposit(
address _from,
address _to,
uint32 _l2Gas,
bytes memory _data
) internal {
// بناء بيانات الاستدعاء لاستدعاء finalizeDeposit
bytes memory message = abi.encodeWithSelector(
الطريقة التي تعمل بها الرسائل عبر النطاقات هي أنه يتم استدعاء عقد الوجهة مع الرسالة كبيانات الاستدعاء الخاصة به.
تفسر عقود Solidity دائمًا بيانات الاستدعاء الخاصة بها وفقًا لـ مواصفات ABI (opens in a new tab).
تنشئ دالة Solidity abi.encodeWithSelector (opens in a new tab) بيانات الاستدعاء تلك.
IL2ERC20Bridge.finalizeDeposit.selector,
address(0),
Lib_PredeployAddresses.OVM_ETH,
_from,
_to,
msg.value,
_data
);
الرسالة هنا هي استدعاء الدالة finalizeDeposit (opens in a new tab) بهذه المعلمات:
| المعلمة | القيمة | المعنى |
|---|---|---|
| _l1Token | address(0) | قيمة خاصة لتمثيل ETH (والذي ليس رمز ERC-20 مميز) على طبقة 1 (L1) |
| _l2Token | Lib_PredeployAddresses.OVM_ETH | عقد طبقة 2 (L2) الذي يدير ETH على أوبتيميزم، 0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000 (هذا العقد للاستخدام الداخلي في أوبتيميزم فقط) |
| _from | _from | العنوان على طبقة 1 (L1) الذي يرسل ETH |
| _to | _to | العنوان على طبقة 2 (L2) الذي يتلقى ETH |
| amount | msg.value | كمية Wei المرسلة (والتي تم إرسالها بالفعل إلى الجسر) |
| _data | _data | بيانات إضافية لإرفاقها بالإيداع |
// إرسال بيانات الاستدعاء إلى طبقة 2 (L2)
// slither-disable-next-line reentrancy-events
sendCrossDomainMessage(l2TokenBridge, _l2Gas, message);
إرسال الرسالة من خلال مرسل الرسائل عبر النطاقات.
// slither-disable-next-line reentrancy-events
emit ETHDepositInitiated(_from, _to, msg.value, _data);
}
إصدار حدث لإبلاغ أي تطبيق لامركزي (dapp) يستمع إلى هذا التحويل.
/**
* @inheritdoc IL1ERC20Bridge
*/
function depositERC20(
.
.
.
) external virtual onlyEOA {
_initiateERC20Deposit(_l1Token, _l2Token, msg.sender, msg.sender, _amount, _l2Gas, _data);
}
/**
* @inheritdoc IL1ERC20Bridge
*/
function depositERC20To(
.
.
.
) external virtual {
_initiateERC20Deposit(_l1Token, _l2Token, msg.sender, _to, _amount, _l2Gas, _data);
}
هاتان الدالتان عبارة عن أغلفة حول _initiateERC20Deposit، وهي الدالة التي تتعامل مع إيداع ERC-20 الفعلي.
/**
* @dev ينفذ منطق الإيداعات عن طريق إبلاغ عقد الرمز المميز المودع على طبقة 2 (L2)
* بالإيداع واستدعاء معالج لقفل أموال طبقة 1 (L1). (مثل transferFrom)
*
* @param _l1Token عنوان ERC-20 على طبقة 1 (L1) الذي نقوم بإيداعه
* @param _l2Token عنوان ERC-20 على طبقة 2 (L2) المقابل لـ طبقة 1 (L1)
* @param _from الحساب الذي سيتم سحب الإيداع منه على طبقة 1 (L1)
* @param _to الحساب الذي سيتم إعطاء الإيداع له على طبقة 2 (L2)
* @param _amount مبلغ ERC-20 المراد إيداعه.
* @param _l2Gas حد الغاز المطلوب لإكمال الإيداع على طبقة 2 (L2).
* @param _data بيانات اختيارية لإرسالها إلى طبقة 2 (L2). يتم توفير هذه البيانات
* فقط لراحة العقود الخارجية. بصرف النظر عن فرض حد أقصى
* للطول، لا تقدم هذه العقود أي ضمانات حول محتواها.
*/
function _initiateERC20Deposit(
address _l1Token,
address _l2Token,
address _from,
address _to,
uint256 _amount,
uint32 _l2Gas,
bytes calldata _data
) internal {
هذه الدالة مشابهة لـ _initiateETHDeposit أعلاه، مع بعض الاختلافات المهمة.
الاختلاف الأول هو أن هذه الدالة تتلقى عناوين الرموز المميزة والمبلغ المراد تحويله كمعلمات.
في حالة ETH، يتضمن الاستدعاء إلى الجسر بالفعل تحويل الأصل إلى حساب الجسر (msg.value).
// عند بدء إيداع على طبقة 1 (L1)، يقوم جسر طبقة 1 (L1) بتحويل الأموال إلى نفسه من أجل
// عمليات السحب المستقبلية. يتحقق safeTransferFrom أيضًا مما إذا كان العقد يحتوي على كود، لذلك سيفشل هذا إذا كان
// _from حسابًا مملوكًا خارجيًا (EOA) أو العنوان صفر (address(0)).
// slither-disable-next-line reentrancy-events, reentrancy-benign
IERC20(_l1Token).safeTransferFrom(_from, address(this), _amount);
تتبع تحويلات الرموز المميزة ERC-20 عملية مختلفة عن ETH:
- يمنح المستخدم (
_from) سماحية للجسر لتحويل الرموز المميزة المناسبة. - يستدعي المستخدم الجسر بعنوان عقد الرمز المميز، والمبلغ، وما إلى ذلك.
- يحول الجسر الرموز المميزة (إلى نفسه) كجزء من عملية الإيداع.
قد تحدث الخطوة الأولى في معاملة منفصلة عن الخطوتين الأخيرتين.
ومع ذلك، فإن الاستباق لا يمثل مشكلة لأن الدالتين اللتين تستدعيان _initiateERC20Deposit (depositERC20 وdepositERC20To) تستدعيان هذه الدالة فقط مع msg.sender كمعلمة _from.
// بناء بيانات الاستدعاء لـ _l2Token.finalizeDeposit(_to, _amount)
bytes memory message = abi.encodeWithSelector(
IL2ERC20Bridge.finalizeDeposit.selector,
_l1Token,
_l2Token,
_from,
_to,
_amount,
_data
);
// إرسال بيانات الاستدعاء إلى طبقة 2 (L2)
// slither-disable-next-line reentrancy-events, reentrancy-benign
sendCrossDomainMessage(l2TokenBridge, _l2Gas, message);
// slither-disable-next-line reentrancy-benign
deposits[_l1Token][_l2Token] = deposits[_l1Token][_l2Token] + _amount;
إضافة المبلغ المودع من الرموز المميزة إلى بنية بيانات deposits.
يمكن أن يكون هناك عناوين متعددة على طبقة 2 (L2) تتوافق مع نفس الرمز المميز ERC-20 على طبقة 1 (L1)، لذلك لا يكفي استخدام رصيد الجسر من الرمز المميز ERC-20 على طبقة 1 (L1) لتتبع الإيداعات.
// slither-disable-next-line reentrancy-events
emit ERC20DepositInitiated(_l1Token, _l2Token, _from, _to, _amount, _data);
}
/*************************
* دوال عبر السلاسل *
*************************/
/**
* @inheritdoc IL1StandardBridge
*/
function finalizeETHWithdrawal(
address _from,
address _to,
uint256 _amount,
bytes calldata _data
يرسل جسر طبقة 2 (L2) رسالة إلى مرسل الرسائل عبر النطاقات لطبقة 2 (L2) مما يتسبب في قيام مرسل الرسائل عبر النطاقات لطبقة 1 (L1) باستدعاء هذه الدالة (بمجرد إرسال المعاملة التي تتمم الرسالة (opens in a new tab) على طبقة 1 (L1)، بالطبع).
) external onlyFromCrossDomainAccount(l2TokenBridge) {
تأكد من أن هذه رسالة شرعية، قادمة من مرسل الرسائل عبر النطاقات وتنشأ من جسر الرموز المميزة لطبقة 2 (L2). تُستخدم هذه الدالة لسحب ETH من الجسر، لذا يتعين علينا التأكد من استدعائها فقط بواسطة المستدعي المصرح له.
// slither-disable-next-line reentrancy-events
(bool success, ) = _to.call{ value: _amount }(new bytes(0));
طريقة تحويل ETH هي استدعاء المستلم مع كمية Wei في msg.value.
require(success, "TransferHelper::safeTransferETH: ETH transfer failed");
// slither-disable-next-line reentrancy-events
emit ETHWithdrawalFinalized(_from, _to, _amount, _data);
إصدار حدث حول عملية السحب.
}
/**
* @inheritdoc IL1ERC20Bridge
*/
function finalizeERC20Withdrawal(
address _l1Token,
address _l2Token,
address _from,
address _to,
uint256 _amount,
bytes calldata _data
) external onlyFromCrossDomainAccount(l2TokenBridge) {
هذه الدالة مشابهة لـ finalizeETHWithdrawal أعلاه، مع التغييرات اللازمة لرموز ERC-20 المميزة.
deposits[_l1Token][_l2Token] = deposits[_l1Token][_l2Token] - _amount;
تحديث بنية بيانات deposits.
// عند الانتهاء من سحب على طبقة 1 (L1)، يقوم جسر طبقة 1 (L1) بتحويل الأموال إلى الساحب
// slither-disable-next-line reentrancy-events
IERC20(_l1Token).safeTransfer(_to, _amount);
// slither-disable-next-line reentrancy-events
emit ERC20WithdrawalFinalized(_l1Token, _l2Token, _from, _to, _amount, _data);
}
/*****************************
* مؤقت - ترحيل ETH *
*****************************/
/**
* @dev يضيف رصيد ETH إلى الحساب. يُقصد بهذا السماح بترحيل ETH
* من بوابة قديمة إلى بوابة جديدة.
* ملاحظة: يُترك هذا لترقية واحدة فقط حتى نتمكن من استلام ETH المُرحل من
* العقد القديم
*/
function donateETH() external payable {}
}
كان هناك تنفيذ سابق للجسر.
عندما انتقلنا من ذلك التنفيذ إلى هذا التنفيذ، كان علينا نقل جميع الأصول.
يمكن ببساطة نقل رموز ERC-20 المميزة.
ومع ذلك، لتحويل ETH إلى عقد، فإنك تحتاج إلى موافقة ذلك العقد، وهو ما يوفره لنا donateETH.
رموز ERC-20 المميزة على طبقة 2 (L2)
لكي يتناسب رمز ERC-20 المميز مع الجسر القياسي، يجب أن يسمح للجسر القياسي، والجسر القياسي فقط، بسك الرمز المميز. هذا ضروري لأن الجسور تحتاج إلى التأكد من أن عدد الرموز المميزة المتداولة على أوبتيميزم يساوي عدد الرموز المميزة المقفلة داخل عقد جسر طبقة 1 (L1). إذا كان هناك عدد كبير جدًا من الرموز المميزة على طبقة 2 (L2)، فلن يتمكن بعض المستخدمين من إعادة أصولهم عبر الجسر إلى طبقة 1 (L1). بدلاً من جسر موثوق به، سنقوم بشكل أساسي بإعادة إنشاء الخدمات المصرفية الاحتياطية الجزئية (opens in a new tab). إذا كان هناك عدد كبير جدًا من الرموز المميزة على طبقة 1 (L1)، فستبقى بعض هذه الرموز المميزة مقفلة داخل عقد الجسر إلى الأبد لأنه لا توجد طريقة لتحريرها دون حرق رموز طبقة 2 (L2) المميزة.
IL2StandardERC20
يجب أن يوفر كل رمز ERC-20 مميز على طبقة 2 (L2) يستخدم الجسر القياسي هذه الواجهة (opens in a new tab)، والتي تحتوي على الدوال والأحداث التي يحتاجها الجسر القياسي.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
لا تتضمن واجهة ERC-20 القياسية (opens in a new tab) الدالتين mint وburn.
هذه الطرق غير مطلوبة بواسطة معيار ERC-20 (opens in a new tab)، والذي يترك آليات إنشاء وتدمير الرموز المميزة غير محددة.
import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
تُستخدم واجهة ERC-165 (opens in a new tab) لتحديد الدوال التي يوفرها العقد. يمكنك قراءة المعيار هنا (opens in a new tab).
interface IL2StandardERC20 is IERC20, IERC165 {
function l1Token() external returns (address);
توفر هذه الدالة عنوان الرمز المميز لطبقة 1 (L1) الذي يتم نقله عبر الجسر إلى هذا العقد. لاحظ أنه ليس لدينا دالة مماثلة في الاتجاه المعاكس. نحتاج إلى أن نكون قادرين على نقل أي رمز مميز لطبقة 1 (L1) عبر الجسر، بغض النظر عما إذا كان دعم طبقة 2 (L2) مخططًا له عند تنفيذه أم لا.
function mint(address _to, uint256 _amount) external;
function burn(address _from, uint256 _amount) external;
event Mint(address indexed _account, uint256 _amount);
event Burn(address indexed _account, uint256 _amount);
}
دوال وأحداث لسك (إنشاء) وحرق (تدمير) الرموز المميزة. يجب أن يكون الجسر هو الكيان الوحيد الذي يمكنه تشغيل هذه الدوال لضمان صحة عدد الرموز المميزة (يساوي عدد الرموز المميزة المقفلة على طبقة 1 (L1)).
L2StandardERC20
هذا هو تنفيذنا لواجهة IL2StandardERC20 (opens in a new tab).
ما لم تكن بحاجة إلى نوع من المنطق المخصص، يجب عليك استخدام هذا التنفيذ.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
عقد ERC-20 من أوبن زبلن (opens in a new tab). لا تؤمن أوبتيميزم بإعادة اختراع العجلة، خاصة عندما تكون العجلة مدققة جيدًا وتحتاج إلى أن تكون جديرة بالثقة بما يكفي للاحتفاظ بالأصول.
import "./IL2StandardERC20.sol";
contract L2StandardERC20 is IL2StandardERC20, ERC20 {
address public l1Token;
address public l2Bridge;
هاتان هما معلمتا التكوين الإضافيتان اللتان نطلبهما ولا يطلبهما ERC-20 عادةً.
/**
* @param _l2Bridge عنوان الجسر القياسي على طبقة 2 (L2).
* @param _l1Token عنوان الرمز المميز المقابل على طبقة 1 (L1).
* @param _name اسم ERC-20.
* @param _symbol رمز ERC-20.
*/
constructor(
address _l2Bridge,
address _l1Token,
string memory _name,
string memory _symbol
) ERC20(_name, _symbol) {
l1Token = _l1Token;
l2Bridge = _l2Bridge;
}
أولاً استدعِ المُنشئ للعقد الذي نرث منه (ERC20(_name, _symbol)) ثم قم بتعيين متغيراتنا الخاصة.
modifier onlyL2Bridge() {
require(msg.sender == l2Bridge, "Only L2 Bridge can mint and burn");
_;
}
// slither-disable-next-line external-function
function supportsInterface(bytes4 _interfaceId) public pure returns (bool) {
bytes4 firstSupportedInterface = bytes4(keccak256("supportsInterface(bytes4)")); // ERC165
bytes4 secondSupportedInterface = IL2StandardERC20.l1Token.selector ^
IL2StandardERC20.mint.selector ^
IL2StandardERC20.burn.selector;
return _interfaceId == firstSupportedInterface || _interfaceId == secondSupportedInterface;
}
هذه هي الطريقة التي يعمل بها ERC-165 (opens in a new tab). كل واجهة عبارة عن عدد من الدوال المدعومة، ويتم تحديدها على أنها أو الحصرية (XOR) (opens in a new tab) لـ محددات دوال ABI (opens in a new tab) لتلك الدوال.
يستخدم جسر طبقة 2 (L2) ERC-165 كفحص سلامة للتأكد من أن عقد ERC-20 الذي يرسل إليه الأصول هو IL2StandardERC20.
ملاحظة: لا يوجد ما يمنع العقد المارق من تقديم إجابات خاطئة لـ supportsInterface، لذا فهذه آلية فحص سلامة، و_ليست_ آلية أمان.
// slither-disable-next-line external-function
function mint(address _to, uint256 _amount) public virtual onlyL2Bridge {
_mint(_to, _amount);
emit Mint(_to, _amount);
}
// slither-disable-next-line external-function
function burn(address _from, uint256 _amount) public virtual onlyL2Bridge {
_burn(_from, _amount);
emit Burn(_from, _amount);
}
}
يُسمح فقط لجسر طبقة 2 (L2) بسك وحرق الأصول.
تم تعريف _mint و_burn فعليًا في عقد ERC-20 من أوبن زبلن.
هذا العقد لا يعرضها خارجيًا فحسب، لأن شروط سك وحرق الرموز المميزة متنوعة مثل عدد طرق استخدام ERC-20.
كود جسر طبقة 2 (L2)
هذا هو الكود الذي يشغل الجسر على أوبتيميزم. المصدر لهذا العقد موجود هنا (opens in a new tab).
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
/* استيرادات الواجهة */
import { IL1StandardBridge } from "../../L1/messaging/IL1StandardBridge.sol";
import { IL1ERC20Bridge } from "../../L1/messaging/IL1ERC20Bridge.sol";
import { IL2ERC20Bridge } from "./IL2ERC20Bridge.sol";
واجهة IL2ERC20Bridge (opens in a new tab) مشابهة جدًا لـ ما يعادلها في طبقة 1 (L1) التي رأيناها أعلاه. هناك اختلافان مهمان:
- على طبقة 1 (L1) تبدأ الإيداعات وتتمم عمليات السحب. هنا تبدأ عمليات السحب وتتمم الإيداعات.
- على طبقة 1 (L1) من الضروري التمييز بين ETH ورموز ERC-20 المميزة. على طبقة 2 (L2) يمكننا استخدام نفس الدوال لكليهما لأنه داخليًا يتم التعامل مع أرصدة ETH على أوبتيميزم كرمز ERC-20 مميز بالعنوان 0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000 (opens in a new tab).
/* استيرادات المكتبة */
import { ERC165Checker } from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol";
import { CrossDomainEnabled } from "../../libraries/bridge/CrossDomainEnabled.sol";
import { Lib_PredeployAddresses } from "../../libraries/constants/Lib_PredeployAddresses.sol";
/* استيرادات العقد */
import { IL2StandardERC20 } from "../../standards/IL2StandardERC20.sol";
/**
* @title L2StandardBridge
* @dev الجسر القياسي على طبقة 2 (L2) هو عقد يعمل مع الجسر القياسي على طبقة 1 (L1) لـ
* تمكين انتقالات ETH و ERC-20 بين طبقة 1 (L1) و طبقة 2 (L2).
* يعمل هذا العقد كأداة سك للرموز المميزة الجديدة عندما يسمع عن إيداعات في الجسر القياسي
* على طبقة 1 (L1).
* يعمل هذا العقد أيضًا كأداة حرق للرموز المميزة المخصصة لعمليات السحب، لإبلاغ جسر
* طبقة 1 (L1) بتحرير أموال طبقة 1 (L1).
*/
contract L2StandardBridge is IL2ERC20Bridge, CrossDomainEnabled {
/********************************
* مراجع العقود الخارجية *
********************************/
address public l1TokenBridge;
تتبع عنوان جسر طبقة 1 (L1). لاحظ أنه على عكس ما يعادله في طبقة 1 (L1)، فإننا هنا نحتاج إلى هذا المتغير. عنوان جسر طبقة 1 (L1) غير معروف مسبقًا.
/***************
* مُنشئ *
***************/
/**
* @param _l2CrossDomainMessenger المراسل عبر النطاقات المستخدم بواسطة هذا العقد.
* @param _l1TokenBridge عنوان جسر طبقة 1 (L1) المنشور على السلسلة الرئيسية.
*/
constructor(address _l2CrossDomainMessenger, address _l1TokenBridge)
CrossDomainEnabled(_l2CrossDomainMessenger)
{
l1TokenBridge = _l1TokenBridge;
}
/***************
* سحب *
***************/
/**
* @inheritdoc IL2ERC20Bridge
*/
function withdraw(
address _l2Token,
uint256 _amount,
uint32 _l1Gas,
bytes calldata _data
) external virtual {
_initiateWithdrawal(_l2Token, msg.sender, msg.sender, _amount, _l1Gas, _data);
}
/**
* @inheritdoc IL2ERC20Bridge
*/
function withdrawTo(
address _l2Token,
address _to,
uint256 _amount,
uint32 _l1Gas,
bytes calldata _data
) external virtual {
_initiateWithdrawal(_l2Token, msg.sender, _to, _amount, _l1Gas, _data);
}
هاتان الدالتان تبدآن عمليات السحب. لاحظ أنه ليست هناك حاجة لتحديد عنوان الرمز المميز لطبقة 1 (L1). من المتوقع أن تخبرنا رموز طبقة 2 (L2) المميزة بعنوان ما يعادلها في طبقة 1 (L1).
/**
* @dev ينفذ منطق عمليات السحب عن طريق حرق الرمز المميز وإبلاغ
* بوابة الرمز المميز على طبقة 1 (L1) بـ سحب.
* @param _l2Token عنوان الرمز المميز على طبقة 2 (L2) حيث تم بدء السحب.
* @param _from الحساب الذي سيتم سحب السحب منه على طبقة 2 (L2).
* @param _to الحساب الذي سيتم إعطاء السحب له على طبقة 1 (L1).
* @param _amount مبلغ الرمز المميز المراد سحبه.
* @param _l1Gas غير مستخدم، ولكنه مضمن لاعتبارات التوافق المستقبلي المحتملة.
* @param _data بيانات اختيارية لإرسالها إلى طبقة 1 (L1). يتم توفير هذه البيانات
* فقط لراحة العقود الخارجية. بصرف النظر عن فرض حد أقصى
* للطول، لا تقدم هذه العقود أي ضمانات حول محتواها.
*/
function _initiateWithdrawal(
address _l2Token,
address _from,
address _to,
uint256 _amount,
uint32 _l1Gas,
bytes calldata _data
) internal {
// عند بدء سحب، نقوم بـ حرق أموال الساحب لمنع
// الاستخدام اللاحق على طبقة 2 (L2)
// slither-disable-next-line reentrancy-events
IL2StandardERC20(_l2Token).burn(msg.sender, _amount);
لاحظ أننا لا نعتمد على المعلمة _from ولكن على msg.sender وهو أمر يصعب تزييفه كثيرًا (مستحيل، على حد علمي).
// بناء بيانات الاستدعاء لـ l1TokenBridge.finalizeERC20Withdrawal(_to, _amount)
// slither-disable-next-line reentrancy-events
address l1Token = IL2StandardERC20(_l2Token).l1Token();
bytes memory message;
if (_l2Token == Lib_PredeployAddresses.OVM_ETH) {
على طبقة 1 (L1) من الضروري التمييز بين ETH وERC-20.
message = abi.encodeWithSelector(
IL1StandardBridge.finalizeETHWithdrawal.selector,
_from,
_to,
_amount,
_data
);
} else {
message = abi.encodeWithSelector(
IL1ERC20Bridge.finalizeERC20Withdrawal.selector,
l1Token,
_l2Token,
_from,
_to,
_amount,
_data
);
}
// إرسال رسالة إلى جسر طبقة 1 (L1)
// slither-disable-next-line reentrancy-events
sendCrossDomainMessage(l1TokenBridge, _l1Gas, message);
// slither-disable-next-line reentrancy-events
emit WithdrawalInitiated(l1Token, _l2Token, msg.sender, _to, _amount, _data);
}
/************************************
* دالة عبر السلاسل: إيداع *
************************************/
/**
* @inheritdoc IL2ERC20Bridge
*/
function finalizeDeposit(
address _l1Token,
address _l2Token,
address _from,
address _to,
uint256 _amount,
bytes calldata _data
يتم استدعاء هذه الدالة بواسطة L1StandardBridge.
) external virtual onlyFromCrossDomainAccount(l1TokenBridge) {
تأكد من أن مصدر الرسالة شرعي.
هذا مهم لأن هذه الدالة تستدعي _mint ويمكن استخدامها لإعطاء رموز مميزة غير مغطاة بالرموز المميزة التي يمتلكها الجسر على طبقة 1 (L1).
// التحقق من أن الرمز المميز المستهدف متوافق و
// التحقق من أن الرمز المميز المودع على طبقة 1 (L1) يطابق تمثيل الرمز المميز المودع على طبقة 2 (L2) هنا
if (
// slither-disable-next-line reentrancy-events
ERC165Checker.supportsInterface(_l2Token, 0x1d1d8b63) &&
_l1Token == IL2StandardERC20(_l2Token).l1Token()
فحوصات السلامة:
- الواجهة الصحيحة مدعومة
- يتطابق عنوان طبقة 1 (L1) لعقد ERC-20 على طبقة 2 (L2) مع مصدر طبقة 1 (L1) للرموز المميزة
) {
// عند الانتهاء من إيداع، نقوم بإضافة نفس المبلغ إلى الحساب على طبقة 2 (L2) من
// الرموز المميزة.
// slither-disable-next-line reentrancy-events
IL2StandardERC20(_l2Token).mint(_to, _amount);
// slither-disable-next-line reentrancy-events
emit DepositFinalized(_l1Token, _l2Token, _from, _to, _amount, _data);
إذا نجحت فحوصات السلامة، فقم بإتمام الإيداع:
- سك الرموز المميزة
- إصدار الحدث المناسب
} else {
// إما أن الرمز المميز على طبقة 2 (L2) الذي يتم الإيداع فيه يختلف حول العنوان الصحيح
// للرمز المميز الخاص به على طبقة 1 (L1)، أو لا يدعم الواجهة الصحيحة.
// يجب أن يحدث هذا فقط إذا كان هناك رمز مميز ضار على طبقة 2 (L2)، أو إذا قام مستخدم بطريقة ما
// بتحديد عنوان الرمز المميز الخاطئ على طبقة 2 (L2) للإيداع فيه.
// في كلتا الحالتين، نوقف العملية هنا ونقوم ببناء سحب
// رسالة حتى يتمكن المستخدمون من إخراج أموالهم في بعض الحالات.
// لا توجد طريقة لمنع عقود الرموز المميزة الضارة تمامًا، ولكن هذا يحد من
// خطأ المستخدم ويخفف من بعض أشكال سلوك العقد الضار.
إذا ارتكب المستخدم خطأً يمكن اكتشافه باستخدام عنوان الرمز المميز الخاطئ لطبقة 2 (L2)، فإننا نريد إلغاء الإيداع وإرجاع الرموز المميزة على طبقة 1 (L1). الطريقة الوحيدة التي يمكننا من خلالها القيام بذلك من طبقة 2 (L2) هي إرسال رسالة سيتعين عليها انتظار فترة تحدي الخطأ، ولكن هذا أفضل بكثير للمستخدم من فقدان الرموز المميزة بشكل دائم.
bytes memory message = abi.encodeWithSelector(
IL1ERC20Bridge.finalizeERC20Withdrawal.selector,
_l1Token,
_l2Token,
_to, // تم تبديل _to و _from هنا لإرجاع الإيداع إلى المرسل
_from,
_amount,
_data
);
// إرسال رسالة إلى جسر طبقة 1 (L1)
// slither-disable-next-line reentrancy-events
sendCrossDomainMessage(l1TokenBridge, 0, message);
// slither-disable-next-line reentrancy-events
emit DepositFailed(_l1Token, _l2Token, _from, _to, _amount, _data);
}
}
}
الخاتمة
الجسر القياسي هو الآلية الأكثر مرونة لتحويلات الأصول. ومع ذلك، نظرًا لأنه عام جدًا، فإنه ليس دائمًا الآلية الأسهل في الاستخدام. خاصة بالنسبة لعمليات السحب، يفضل معظم المستخدمين استخدام جسور تابعة لجهات خارجية (opens in a new tab) لا تنتظر فترة التحدي ولا تتطلب إثبات ميركل لإتمام عملية السحب.
تعمل هذه الجسور عادةً من خلال امتلاك أصول على طبقة 1 (L1)، والتي توفرها على الفور مقابل رسوم رمزية (غالبًا ما تكون أقل من تكلفة الغاز لعملية سحب عبر الجسر القياسي). عندما يتوقع الجسر (أو الأشخاص الذين يديرونه) نقصًا في أصول طبقة 1 (L1)، فإنه يحول أصولاً كافية من طبقة 2 (L2). نظرًا لأن هذه عمليات سحب كبيرة جدًا، يتم إطفاء تكلفة السحب على مبلغ كبير وتكون نسبة مئوية أصغر بكثير.
نأمل أن يكون هذا المقال قد ساعدك على فهم المزيد حول كيفية عمل طبقة 2 (L2)، وكيفية كتابة كود Solidity واضح وآمن.