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

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

Solidity
جسر
طبقة 2
متوسط
أوري بوميرانتس
30 مارس 2022
32 دقيقة للقراءة

أوبتيميزم (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)

  1. في حالة إيداع ERC-20، يمنح المودع الجسر سماحية لإنفاق المبلغ المودع
  2. يستدعي المودع جسر طبقة 1 (L1) (depositERC20، أو depositERC20To، أو depositETH، أو depositETHTo)
  3. يستحوذ جسر طبقة 1 (L1) على الأصل المنقول عبر الجسر
    • ETH: يتم تحويل الأصل بواسطة المودع كجزء من الاستدعاء
    • ERC-20: يتم تحويل الأصل بواسطة الجسر إلى نفسه باستخدام السماحية المقدمة من المودع
  4. يستخدم جسر طبقة 1 (L1) آلية الرسائل عبر النطاقات لاستدعاء finalizeDeposit على جسر طبقة 2 (L2)

طبقة 2 (L2)

  1. يتحقق جسر طبقة 2 (L2) من أن الاستدعاء إلى finalizeDeposit شرعي:
    • جاء من عقد الرسائل عبر النطاقات
    • كان في الأصل من الجسر على طبقة 1 (L1)
  2. يتحقق جسر طبقة 2 (L2) مما إذا كان عقد الرمز المميز ERC-20 على طبقة 2 (L2) هو العقد الصحيح:
    • يُبلغ عقد طبقة 2 (L2) أن نظيره في طبقة 1 (L1) هو نفسه الذي جاءت منه الرموز المميزة على طبقة 1 (L1)
    • يُبلغ عقد طبقة 2 (L2) أنه يدعم الواجهة الصحيحة (باستخدام ERC-165 (opens in a new tab)).
  3. إذا كان عقد طبقة 2 (L2) هو العقد الصحيح، فاستدعه لسك العدد المناسب من الرموز المميزة إلى العنوان المناسب. إذا لم يكن كذلك، فابدأ عملية سحب للسماح للمستخدم بالمطالبة بالرموز المميزة على طبقة 1 (L1).

تدفق السحب

طبقة 2 (L2)

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

طبقة 1 (L1)

  1. يتحقق جسر طبقة 1 (L1) من أن الاستدعاء إلى finalizeETHWithdrawal أو finalizeERC20Withdrawal شرعي:
    • جاء من آلية الرسائل عبر النطاقات
    • كان في الأصل من الجسر على طبقة 2 (L2)
  2. يحول جسر طبقة 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، لا نعرف ما إذا كان هذا الكود متوافقًا معه أم لا.

في مصطلحات جسر أوبتيميزم، يعني الإيداع التحويل من طبقة 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)، يعني هذا بدء الإيداعات وإتمام عمليات السحب.

هذه الدالة ليست ضرورية حقًا، لأنها على طبقة 2 (L2) عبارة عن عقد منشور مسبقًا، لذا فهي دائمًا على العنوان 0x4200000000000000000000000000000000000010. إنها هنا من أجل التماثل مع جسر طبقة 2 (L2)، لأن عنوان جسر طبقة 1 (L1) ليس من السهل معرفته.

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

هذه الدالة متطابقة تقريبًا مع depositERC20، لكنها تتيح لك إرسال ERC-20 إلى عنوان مختلف.

عمليات السحب (والرسائل الأخرى من طبقة 2 (L2) إلى طبقة 1 (L1)) في أوبتيميزم هي عملية من خطوتين:

  1. معاملة بدء على طبقة 2 (L2).
  2. معاملة إتمام أو مطالبة على طبقة 1 (L1). يجب أن تحدث هذه المعاملة بعد انتهاء فترة تحدي الخطأ (opens in a new tab) لمعاملة طبقة 2 (L2).

IL1StandardBridge

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

تنقسم واجهة الجسر بين ملفين لأن بعض رموز ERC-20 المميزة تتطلب معالجة مخصصة ولا يمكن التعامل معها بواسطة الجسر القياسي. بهذه الطريقة، يمكن للجسر المخصص الذي يتعامل مع مثل هذا الرمز المميز تنفيذ IL1ERC20Bridge ولا يضطر أيضًا إلى نقل ETH عبر الجسر.

هذا الحدث متطابق تقريبًا مع إصدار ERC-20 (ERC20DepositInitiated)، باستثناء عدم وجود عناوين الرموز المميزة لطبقة 1 (L1) وطبقة 2 (L2). الشيء نفسه ينطبق على الأحداث والدوال الأخرى.

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) العقد بكيفية إرسال رسائل إلى الطبقة الأخرى، باستخدام مرسل الرسائل عبر النطاقات. مرسل الرسائل عبر النطاقات هذا هو نظام آخر بالكامل، ويستحق مقالًا خاصًا به، والذي آمل أن أكتبه في المستقبل.

المعلمة الوحيدة التي يحتاج العقد إلى معرفتها، هي عنوان مرسل الرسائل عبر النطاقات على هذه الطبقة. يتم تعيين هذه المعلمة مرة واحدة، في المُنشئ، ولا تتغير أبدًا.

يمكن الوصول إلى المراسلة عبر النطاقات بواسطة أي عقد على سلسلة الكتل حيث يتم تشغيله (إما شبكة إيثيريوم الرئيسية أو أوبتيميزم). لكننا نحتاج إلى أن يثق الجسر على كل جانب فقط في رسائل معينة إذا كانت قادمة من الجسر على الجانب الآخر.

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

يمكن الوثوق فقط بالرسائل الواردة من مرسل الرسائل عبر النطاقات المناسب (messenger، كما ترى أدناه).


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

الطريقة التي يوفر بها مرسل الرسائل عبر النطاقات العنوان الذي أرسل رسالة مع الطبقة الأخرى هي الدالة .xDomainMessageSender() (opens in a new tab). طالما تم استدعاؤها في المعاملة التي بدأتها الرسالة، فيمكنها توفير هذه المعلومات.

نحتاج إلى التأكد من أن الرسالة التي تلقيناها جاءت من الجسر الآخر.

تُرجع هذه الدالة مرسل الرسائل عبر النطاقات. نستخدم دالة بدلاً من المتغير messenger للسماح للعقود التي ترث من هذا العقد باستخدام خوارزمية لتحديد مرسل الرسائل عبر النطاقات الذي يجب استخدامه.

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

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

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

  1. أحداث إعادة الدخول (opens in a new tab)
  2. إعادة الدخول الحميدة (opens in a new tab)
        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) طريقتين للعقد للإبلاغ عن الفشل:

  1. التراجع
  2. إرجاع false

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

هذا السطر هو كيفية تحديد استخدام الغلاف 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. وهو أيضًا السبب في أن التهيئة أدناه منفصلة عن المُنشئ.

يحدد اختبار سليذر (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 قبل المستخدم الشرعي. ولكن هناك طريقتان لمنع ذلك:

  1. إذا لم يتم نشر العقود مباشرة بواسطة حساب مملوك خارجيًا (EOA) ولكن في معاملة تجعل عقدًا آخر ينشئها (opens in a new tab)، فيمكن أن تكون العملية بأكملها ذرية، وتنتهي قبل تنفيذ أي معاملة أخرى.
  2. إذا فشل الاستدعاء الشرعي لـ initialize، فمن الممكن دائمًا تجاهل الوكيل والجسر المنشأين حديثًا وإنشاء وكيل وجسر جديدين.
        messenger = _l1messenger;
        l2TokenBridge = _l2TokenBridge;
    }

هاتان هما المعلمتان اللتان يحتاج الجسر إلى معرفتهما.

هذا هو السبب في أننا احتجنا إلى أدوات Address المساعدة من أوبن زبلن.

هذه الدالة موجودة لأغراض الاختبار. لاحظ أنها لا تظهر في تعريفات الواجهة - فهي ليست للاستخدام العادي.

هاتان الدالتان عبارة عن أغلفة حول _initiateETHDeposit، وهي الدالة التي تتعامل مع إيداع ETH الفعلي.

الطريقة التي تعمل بها الرسائل عبر النطاقات هي أنه يتم استدعاء عقد الوجهة مع الرسالة كبيانات الاستدعاء الخاصة به. تفسر عقود Solidity دائمًا بيانات الاستدعاء الخاصة بها وفقًا لـ مواصفات ABI (opens in a new tab). تنشئ دالة Solidity abi.encodeWithSelector (opens in a new tab) بيانات الاستدعاء تلك.

            IL2ERC20Bridge.finalizeDeposit.selector,
            address(0),
            Lib_PredeployAddresses.OVM_ETH,
            _from,
            _to,
            msg.value,
            _data
        );

الرسالة هنا هي استدعاء الدالة finalizeDeposit (opens in a new tab) بهذه المعلمات:

المعلمةالقيمةالمعنى
_l1Tokenaddress(0)قيمة خاصة لتمثيل ETH (والذي ليس رمز ERC-20 مميز) على طبقة 1 (L1)
_l2TokenLib_PredeployAddresses.OVM_ETHعقد طبقة 2 (L2) الذي يدير ETH على أوبتيميزم، 0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000 (هذا العقد للاستخدام الداخلي في أوبتيميزم فقط)
_from_fromالعنوان على طبقة 1 (L1) الذي يرسل ETH
_to_toالعنوان على طبقة 2 (L2) الذي يتلقى ETH
amountmsg.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) يستمع إلى هذا التحويل.

هاتان الدالتان عبارة عن أغلفة حول _initiateERC20Deposit، وهي الدالة التي تتعامل مع إيداع ERC-20 الفعلي.

هذه الدالة مشابهة لـ _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:

  1. يمنح المستخدم (_from) سماحية للجسر لتحويل الرموز المميزة المناسبة.
  2. يستدعي المستخدم الجسر بعنوان عقد الرمز المميز، والمبلغ، وما إلى ذلك.
  3. يحول الجسر الرموز المميزة (إلى نفسه) كجزء من عملية الإيداع.

قد تحدث الخطوة الأولى في معاملة منفصلة عن الخطوتين الأخيرتين. ومع ذلك، فإن الاستباق لا يمثل مشكلة لأن الدالتين اللتين تستدعيان _initiateERC20Deposit (depositERC20 وdepositERC20To) تستدعيان هذه الدالة فقط مع msg.sender كمعلمة _from.

إضافة المبلغ المودع من الرموز المميزة إلى بنية بيانات deposits. يمكن أن يكون هناك عناوين متعددة على طبقة 2 (L2) تتوافق مع نفس الرمز المميز ERC-20 على طبقة 1 (L1)، لذلك لا يكفي استخدام رصيد الجسر من الرمز المميز ERC-20 على طبقة 1 (L1) لتتبع الإيداعات.

يرسل جسر طبقة 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);

إصدار حدث حول عملية السحب.

هذه الدالة مشابهة لـ finalizeETHWithdrawal أعلاه، مع التغييرات اللازمة لرموز ERC-20 المميزة.

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

تحديث بنية بيانات deposits.

كان هناك تنفيذ سابق للجسر. عندما انتقلنا من ذلك التنفيذ إلى هذا التنفيذ، كان علينا نقل جميع الأصول. يمكن ببساطة نقل رموز 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 عادةً.

أولاً استدعِ المُنشئ للعقد الذي نرث منه (ERC20(_name, _symbol)) ثم قم بتعيين متغيراتنا الخاصة.

هذه هي الطريقة التي يعمل بها 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، لذا فهذه آلية فحص سلامة، و_ليست_ آلية أمان.

يُسمح فقط لجسر طبقة 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. على طبقة 1 (L1) تبدأ الإيداعات وتتمم عمليات السحب. هنا تبدأ عمليات السحب وتتمم الإيداعات.
  2. على طبقة 1 (L1) من الضروري التمييز بين ETH ورموز ERC-20 المميزة. على طبقة 2 (L2) يمكننا استخدام نفس الدوال لكليهما لأنه داخليًا يتم التعامل مع أرصدة ETH على أوبتيميزم كرمز ERC-20 مميز بالعنوان 0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000 (opens in a new tab).

تتبع عنوان جسر طبقة 1 (L1). لاحظ أنه على عكس ما يعادله في طبقة 1 (L1)، فإننا هنا نحتاج إلى هذا المتغير. عنوان جسر طبقة 1 (L1) غير معروف مسبقًا.

هاتان الدالتان تبدآن عمليات السحب. لاحظ أنه ليست هناك حاجة لتحديد عنوان الرمز المميز لطبقة 1 (L1). من المتوقع أن تخبرنا رموز طبقة 2 (L2) المميزة بعنوان ما يعادلها في طبقة 1 (L1).

لاحظ أننا لا نعتمد على المعلمة _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.

يتم استدعاء هذه الدالة بواسطة 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. الواجهة الصحيحة مدعومة
  2. يتطابق عنوان طبقة 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);

إذا نجحت فحوصات السلامة، فقم بإتمام الإيداع:

  1. سك الرموز المميزة
  2. إصدار الحدث المناسب

إذا ارتكب المستخدم خطأً يمكن اكتشافه باستخدام عنوان الرمز المميز الخاطئ لطبقة 2 (L2)، فإننا نريد إلغاء الإيداع وإرجاع الرموز المميزة على طبقة 1 (L1). الطريقة الوحيدة التي يمكننا من خلالها القيام بذلك من طبقة 2 (L2) هي إرسال رسالة سيتعين عليها انتظار فترة تحدي الخطأ، ولكن هذا أفضل بكثير للمستخدم من فقدان الرموز المميزة بشكل دائم.

الخاتمة

الجسر القياسي هو الآلية الأكثر مرونة لتحويلات الأصول. ومع ذلك، نظرًا لأنه عام جدًا، فإنه ليس دائمًا الآلية الأسهل في الاستخدام. خاصة بالنسبة لعمليات السحب، يفضل معظم المستخدمين استخدام جسور تابعة لجهات خارجية (opens in a new tab) لا تنتظر فترة التحدي ولا تتطلب إثبات ميركل لإتمام عملية السحب.

تعمل هذه الجسور عادةً من خلال امتلاك أصول على طبقة 1 (L1)، والتي توفرها على الفور مقابل رسوم رمزية (غالبًا ما تكون أقل من تكلفة الغاز لعملية سحب عبر الجسر القياسي). عندما يتوقع الجسر (أو الأشخاص الذين يديرونه) نقصًا في أصول طبقة 1 (L1)، فإنه يحول أصولاً كافية من طبقة 2 (L2). نظرًا لأن هذه عمليات سحب كبيرة جدًا، يتم إطفاء تكلفة السحب على مبلغ كبير وتكون نسبة مئوية أصغر بكثير.

نأمل أن يكون هذا المقال قد ساعدك على فهم المزيد حول كيفية عمل طبقة 2 (L2)، وكيفية كتابة كود Solidity واضح وآمن.

انظر هنا للمزيد من أعمالي (opens in a new tab).