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

⁦ERC-20⁩ مع حواجز الأمان

erc-20
مبتدئ
أوري بوميرانتس
15 أغسطس 2022
8 دقيقة للقراءة

المقدمة

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

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

إذا كنت ترغب في رؤية الكود المصدري الكامل:

  1. افتح بيئة التطوير المتكاملة Remix (opens in a new tab).
  2. انقر على أيقونة استنساخ GitHub (clone github icon).
  3. استنسخ مستودع GitHub https://github.com/qbzzt/20220815-erc20-safety-rails.
  4. افتح contracts > erc20-safety-rails.sol.

إنشاء عقد ERC-20

قبل أن نتمكن من إضافة وظيفة حواجز الأمان، نحتاج إلى عقد ERC-20. في هذه المقالة، سنستخدم معالج عقود أوبن زبلن (opens in a new tab). افتحه في متصفح آخر واتبع هذه التعليمات:

  1. حدد ERC20.

  2. أدخل هذه الإعدادات:

    المعلمةالقيمة
    الاسمSafetyRailsToken
    الرمزSAFE
    السك المسبق1000
    الميزاتلا شيء
    التحكم في الوصولOwnable
    قابلية الترقيةلا شيء
  3. قم بالتمرير لأعلى وانقر على Open in Remix (لـ Remix) أو Download لاستخدام بيئة مختلفة. سأفترض أنك تستخدم Remix، وإذا كنت تستخدم شيئًا آخر، فما عليك سوى إجراء التغييرات المناسبة.

  4. لدينا الآن عقد ERC-20 يعمل بكامل طاقته. يمكنك توسيع .deps > npm لرؤية الكود المستورد.

  5. قم بتجميع ونشر وتجربة العقد للتأكد من أنه يعمل كعقد ERC-20. إذا كنت بحاجة إلى تعلم كيفية استخدام Remix، استخدم هذا البرنامج التعليمي (opens in a new tab).

الأخطاء الشائعة

الأخطاء

يرسل المستخدمون أحيانًا الرموز المميزة إلى العنوان الخطأ. على الرغم من أننا لا نستطيع قراءة أفكارهم لمعرفة ما كانوا يقصدون القيام به، إلا أن هناك نوعين من الأخطاء التي تحدث كثيرًا ويسهل اكتشافها:

  1. إرسال الرموز المميزة إلى عنوان العقد نفسه. على سبيل المثال، تمكن الرمز المميز OP الخاص بـ أوبتيميزم (opens in a new tab) من تجميع أكثر من 120,000 (opens in a new tab) رمز OP في أقل من شهرين. يمثل هذا قدرًا كبيرًا من الثروة التي يُفترض أن الناس فقدوها للتو.

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

منع التحويلات

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

لاستخدام الخطاف، أضف هذه الدالة بعد المُنشئ:

    function _beforeTokenTransfer(address from, address to, uint256 amount)
        internal virtual
        override(ERC20)
    {
        super._beforeTokenTransfer(from, to, amount);
    }

قد تكون بعض أجزاء هذه الدالة جديدة إذا لم تكن على دراية جيدة بـ Solidity:

        internal virtual

تعني الكلمة الرئيسية virtual أنه تمامًا كما ورثنا الوظائف من ERC20 وتجاوزنا هذه الدالة، يمكن للعقود الأخرى أن ترث منا وتتجاوز هذه الدالة.

        override(ERC20)

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

        super._beforeTokenTransfer(from, to, amount);

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

برمجة المتطلبات

نريد إضافة هذه المتطلبات إلى الدالة:

  • لا يمكن أن يساوي العنوان to العنوان address(this)، وهو عنوان عقد ERC-20 نفسه.
  • لا يمكن أن يكون العنوان to فارغًا، يجب أن يكون إما:
    • حساب مملوك خارجيًا (EOA). لا يمكننا التحقق مما إذا كان العنوان هو EOA بشكل مباشر، ولكن يمكننا التحقق من رصيد ETH للعنوان. تمتلك حسابات EOA دائمًا رصيدًا تقريبًا، حتى لو لم تعد مستخدمة - فمن الصعب تصفيتها حتى آخر Wei.
    • عقد ذكي. يعد اختبار ما إذا كان العنوان عقدًا ذكيًا أصعب قليلاً. يوجد رمز التشغيل الذي يتحقق من طول الكود الخارجي، ويسمى EXTCODESIZE (opens in a new tab)، ولكنه غير متوفر مباشرة في Solidity. علينا استخدام Yul (opens in a new tab)، وهي لغة التجميع الخاصة بآلة إيثيريوم الافتراضية (EVM)، للقيام بذلك. هناك قيم أخرى يمكننا استخدامها من Solidity (<address>.code و <address>.codehash (opens in a new tab))، لكنها تكلف أكثر.

دعنا نراجع الكود الجديد سطرًا بسطر:

        require(to != address(this), "Can't send tokens to the contract address");

هذا هو المطلب الأول، تحقق من أن to و this(address) ليسا نفس الشيء.

        bool isToContract;
        assembly {
           isToContract := gt(extcodesize(to), 0)
        }

هذه هي الطريقة التي نتحقق بها مما إذا كان العنوان عقدًا. لا يمكننا تلقي المخرجات مباشرة من Yul، لذلك نحدد بدلاً من ذلك متغيرًا للاحتفاظ بالنتيجة (isToContract في هذه الحالة). الطريقة التي تعمل بها Yul هي أن كل رمز التشغيل يعتبر دالة. لذلك نستدعي أولاً EXTCODESIZE (opens in a new tab) للحصول على حجم العقد، ثم نستخدم GT (opens in a new tab) للتحقق من أنه ليس صفرًا (نحن نتعامل مع أعداد صحيحة غير سالبة، لذلك بالطبع لا يمكن أن تكون سالبة). ثم نكتب النتيجة في isToContract.

        require(to.balance != 0 || isToContract, "Can't send tokens to an empty address");

وأخيرًا، لدينا الفحص الفعلي للعناوين الفارغة.

الوصول الإداري

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

  1. تجميد وإلغاء تجميد الحسابات. يمكن أن يكون هذا مفيدًا، على سبيل المثال، عندما يكون الحساب مخترقًا.

  2. تنظيف الأصول.

    في بعض الأحيان يرسل المحتالون رموزًا مميزة احتيالية إلى عقد الرمز المميز الحقيقي لاكتساب الشرعية. على سبيل المثال، انظر هنا (opens in a new tab). عقد ERC-20 الشرعي هو 0x4200....0042 (opens in a new tab). عملية الاحتيال التي تتظاهر بأنها هي 0x234....bbe (opens in a new tab).

    من الممكن أيضًا أن يرسل الأشخاص رموز ERC-20 شرعية إلى عقدنا عن طريق الخطأ، وهو سبب آخر للرغبة في إيجاد طريقة لإخراجها.

توفر أوبن زبلن آليتين لتمكين الوصول الإداري:

من أجل البساطة، نستخدم في هذه المقالة Ownable.

تجميد وإلغاء تجميد العقود

يتطلب تجميد وإلغاء تجميد العقود عدة تغييرات:

  • تخطيط (mapping) (opens in a new tab) من العناوين إلى القيم المنطقية (booleans) (opens in a new tab) لتتبع العناوين المجمدة. جميع القيم تكون صفرًا في البداية، والتي تُفسر للقيم المنطقية على أنها خطأ (false). هذا ما نريده لأنه افتراضيًا لا يتم تجميد الحسابات.

        mapping(address => bool) public frozenAccounts;
    
  • أحداث (opens in a new tab) لإبلاغ أي شخص مهتم عند تجميد حساب أو إلغاء تجميده. من الناحية الفنية، الأحداث ليست مطلوبة لهذه الإجراءات، ولكنها تساعد الكود خارج السلسلة على التمكن من الاستماع إلى هذه الأحداث ومعرفة ما يحدث. يعتبر من الممارسات الجيدة للعقد الذكي أن يصدرها عندما يحدث شيء قد يكون ذا صلة بشخص آخر.

    تتم فهرسة الأحداث بحيث يكون من الممكن البحث عن جميع الأوقات التي تم فيها تجميد حساب أو إلغاء تجميده.

      // عندما يتم تجميد الحسابات أو إلغاء تجميدها
      event AccountFrozen(address indexed _addr);
      event AccountThawed(address indexed _addr);
    
  • دوال لتجميد وإلغاء تجميد الحسابات. هاتان الدالتان متطابقتان تقريبًا، لذلك سنراجع دالة التجميد فقط.

        function freezeAccount(address addr)
          public
          onlyOwner
    

    يمكن استدعاء الدوال المميزة بـ public (opens in a new tab) من عقود ذكية أخرى أو مباشرة عن طريق معاملة.

      {
          require(!frozenAccounts[addr], "Account already frozen");
          frozenAccounts[addr] = true;
          emit AccountFrozen(addr);
      }  // freezeAccount
    

    إذا كان الحساب مجمدًا بالفعل، تراجع. بخلاف ذلك، قم بتجميده و emit حدثًا.

  • قم بتغيير _beforeTokenTransfer لمنع نقل الأموال من حساب مجمد. لاحظ أنه لا يزال من الممكن تحويل الأموال إلى الحساب المجمد.

         require(!frozenAccounts[from], "The account is frozen");
    

تنظيف الأصول

لتحرير الرموز المميزة من نوع ERC-20 التي يحتفظ بها هذا العقد، نحتاج إلى استدعاء دالة في عقد الرمز المميز الذي تنتمي إليه، إما transfer (opens in a new tab) أو approve (opens in a new tab). لا جدوى من إهدار غاز في هذه الحالة على المخصصات (allowances)، قد نقوم بالتحويل مباشرة.

    function cleanupERC20(
        address erc20,
        address dest
    )
        public
        onlyOwner
    {
        IERC20 token = IERC20(erc20);

هذه هي الصيغة لإنشاء كائن لعقد عندما نتلقى العنوان. يمكننا القيام بذلك لأن لدينا تعريف الرموز المميزة ERC20 كجزء من الكود المصدري (انظر السطر 4)، ويتضمن هذا الملف تعريف IERC20 (opens in a new tab)، وهي الواجهة لعقد ERC-20 من أوبن زبلن.

        uint balance = token.balanceOf(address(this));
        token.transfer(dest, balance);
    }

هذه دالة تنظيف، لذلك من المفترض أننا لا نريد ترك أي رموز مميزة. بدلاً من الحصول على الرصيد من المستخدم يدويًا، قد نقوم أيضًا بأتمتة العملية.

الخاتمة

هذا ليس حلاً مثاليًا - لا يوجد حل مثالي لمشكلة "المستخدم ارتكب خطأ". ومع ذلك، فإن استخدام هذه الأنواع من عمليات التحقق يمكن أن يمنع على الأقل بعض الأخطاء. يمكن استخدام القدرة على تجميد الحسابات، على الرغم من خطورتها، للحد من أضرار بعض الاختراقات عن طريق حرمان المخترق من الأموال المسروقة.

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