ERC-20 مع حواجز أمان
مقدمة
من أروع الأشياء في إيثريوم أنه لا توجد سلطة مركزية يمكنها تعديل معاملاتك أو التراجع عنها. من أكبر المشاكل في إيثريوم أنه لا توجد سلطة مركزية لديها القدرة على التراجع عن أخطاء المستخدم أو المعاملات غير المشروعة. في هذه المقالة، ستتعلم بعض الأخطاء الشائعة التي يرتكبها المستخدمون مع رموز ERC-20، بالإضافة إلى كيفية إنشاء عقود ERC-20 تساعد المستخدمين على تجنب تلك الأخطاء، أو التي تمنح سلطة مركزية بعض الصلاحيات (على سبيل المثال، لتجميد الحسابات).
لاحظ أنه بينما سنستخدم عقد رمز أوبن زبلين ERC-20 (opens in a new tab)، فإن هذه المقالة لا تشرحه بتفصيل كبير. يمكنك العثور على هذه المعلومات هنا.
إذا كنت تريد رؤية الكود المصدري الكامل:
- افتح ريميكس IDE (opens in a new tab).
- انقر على أيقونة استنساخ github (
). - استنسخ مستودع github
https://github.com/qbzzt/20220815-erc20-safety-rails. - افتح contracts > erc20-safety-rails.sol.
إنشاء عقد ERC-20
قبل أن نتمكن من إضافة وظيفة حواجز الأمان، نحتاج إلى عقد ERC-20. في هذه المقالة، سنستخدم معالج عقود أوبن زبلين (opens in a new tab). افتحه في متصفح آخر واتبع هذه التعليمات:
-
حدد ERC20.
-
أدخل هذه الإعدادات:
Parameter Value الاسم SafetyRailsToken الرمز SAFE Premint لا نريد أن يكون لدينا أكثر من مجمع سيولة واحد لكل زوج من الرموز. الميزات لا شيء التحكم في الوصول Ownable قابلية الترقية لا شيء -
مرر لأعلى وانقر على افتح في ريميكس (لـ ريميكس) أو تنزيل لاستخدام بيئة مختلفة. سأفترض أنك تستخدم ريميكس، إذا كنت تستخدم شيئًا آخر، فقم فقط بإجراء التغييرات المناسبة.
-
لدينا الآن عقد ERC-20 يعمل بشكل كامل. يمكنك توسيع
.deps>npmلرؤية الكود المستورد. -
قم بتجميع العقد ونشره والتفاعل معه لترى أنه يعمل كعقد ERC-20. إذا كنت بحاجة إلى تعلم كيفية استخدام ريميكس، استخدم تعليمات الاستخدام هذه (opens in a new tab).
الأخطاء الشائعة
الأخطاء
يرسل المستخدمون أحيانًا الرموز إلى عنوان خاطئ. على الرغم من أننا لا نستطيع قراءة أفكارهم لمعرفة ما كانوا ينوون فعله، إلا أن هناك نوعين من الأخطاء يحدثان كثيرًا ويسهل اكتشافهما:
-
إرسال الرموز إلى عنوان العقد نفسه. على سبيل المثال، تمكن رمز OP الخاص بـ أوبتيميزم (opens in a new tab) من تجميع أكثر من 120,000 (opens in a new tab) رمز OP في أقل من شهرين. يمثل هذا قدرًا كبيرًا من الثروة التي يُفترض أن الناس قد فقدوها.
-
إرسال الرموز إلى عنوان فارغ، وهو عنوان لا يتوافق مع حساب ذي ملكية خارجية أو عقد ذكي. على الرغم من أنه ليس لدي إحصائيات حول مدى تكرار حدوث ذلك، إلا أن حادثة واحدة كان من الممكن أن تكلف 20,000,000 رمز (opens in a new tab).
منع التحويلات
يتضمن عقد أوبن زبلين ERC-20 خطافًا، _beforeTokenTransfer (opens in a new tab)، يتم استدعاؤه قبل تحويل الرمز. بشكل افتراضي، لا يقوم هذا الخطاف بأي شيء، ولكن يمكننا ربط وظائفنا الخاصة به، مثل عمليات التحقق التي تعود إلى الحالة السابقة إذا كانت هناك مشكلة.
لاستخدام الخطاف، أضف هذه الدالة بعد الدالة البانية:
1 function _beforeTokenTransfer(address from, address to, uint256 amount)2 internal virtual3 override(ERC20)4 {5 super._beforeTokenTransfer(from, to, amount);6 }قد تكون بعض أجزاء هذه الدالة جديدة إذا لم تكن على دراية كافية بـ سوليديتي:
1 internal virtualتعني الكلمة المفتاحية virtual أنه مثلما ورثنا الوظيفة من ERC20 وتجاوزنا هذه الدالة، يمكن للعقود الأخرى أن ترث منا وتتجاوز هذه الدالة.
1 override(ERC20)علينا أن نحدد صراحةً أننا نتجاوز (opens in a new tab) تعريف رمز ERC20 للدالة _beforeTokenTransfer. بشكل عام، تعتبر التعريفات الصريحة أفضل بكثير من التعريفات الضمنية من وجهة نظر أمنية - لا يمكنك أن تنسى أنك فعلت شيئًا إذا كان أمامك مباشرة. وهذا هو أيضًا السبب في أننا بحاجة إلى تحديد _beforeTokenTransfer لأي فئة فائقة نتجاوزها.
1 super._beforeTokenTransfer(from, to, amount);يستدعي هذا السطر دالة _beforeTokenTransfer للعقد أو العقود التي ورثنا منها والتي تحتوي عليها. في هذه الحالة، هذا هو ERC20 فقط، حيث لا يحتوي Ownable على هذا الخطاف. على الرغم من أن ERC20._beforeTokenTransfer لا تفعل أي شيء حاليًا، إلا أننا نستدعيها في حالة إضافة وظائف في المستقبل (ثم نقرر إعادة نشر العقد، لأن العقود لا تتغير بعد النشر).
ترميز المتطلبات
نريد إضافة هذه المتطلبات إلى الدالة:
- لا يمكن أن يساوي عنوان
toaddress(this)، وهو عنوان عقد ERC-20 نفسه. - لا يمكن أن يكون عنوان
toفارغًا، يجب أن يكون إما:- حساب مملوك خارجيًا (EOA). لا يمكننا التحقق مما إذا كان العنوان حسابًا مملوكًا خارجيًا (EOA) مباشرةً، ولكن يمكننا التحقق من رصيد ETH للعنوان. دائمًا ما يكون لدى الحسابات المملوكة خارجيًا (EOAs) رصيد، حتى لو لم تعد تُستخدم - فمن الصعب تصفيتها حتى آخر wei.
- عقد ذكي. اختبار ما إذا كان العنوان عقدًا ذكيًا هو أمر أصعب قليلاً. هناك رمز تشغيلي يتحقق من طول الكود الخارجي، يسمى
EXTCODESIZE(opens in a new tab)، لكنه غير متاح مباشرة في سوليديتي. علينا استخدام يول (opens in a new tab)، وهي لغة تجميع EVM، لذلك. هناك قيم أخرى يمكننا استخدامها من سوليديتي (<address>.codeو<address>.codehash(opens in a new tab))، لكنها تكلف أكثر.
دعنا نمر على الكود الجديد سطرًا بسطر:
1 require(to != address(this), "لا يمكن إرسال الرموز إلى عنوان العقد");هذا هو المتطلب الأول، تحقق من أن to و address(this) ليسا نفس الشيء.
1 bool isToContract;2 assembly {3 isToContract := gt(extcodesize(to), 0)4 }هذه هي الطريقة التي نتحقق بها مما إذا كان العنوان عقدًا. لا يمكننا تلقي المخرجات مباشرة من يول، لذلك بدلاً من ذلك نحدد متغيرًا للاحتفاظ بالنتيجة (isToContract في هذه الحالة). الطريقة التي تعمل بها يول هي أن كل رمز تشغيلي يعتبر دالة. لذلك أولاً نستدعي EXTCODESIZE (opens in a new tab) للحصول على حجم العقد، ثم نستخدم GT (opens in a new tab) للتحقق من أنه ليس صفرًا (نحن نتعامل مع أعداد صحيحة غير سالبة، لذلك بالطبع لا يمكن أن يكون سالبًا). ثم نكتب النتيجة إلى isToContract.
1 require(to.balance != 0 || isToContract, "لا يمكن إرسال الرموز إلى عنوان فارغ");وأخيرًا، لدينا التحقق الفعلي من العناوين الفارغة.
الوصول الإداري
في بعض الأحيان يكون من المفيد وجود مسؤول يمكنه التراجع عن الأخطاء. للحد من احتمالية إساءة الاستخدام، يمكن أن يكون هذا المسؤول متعدد التوقيعات (opens in a new tab) بحيث يتعين على عدة أشخاص الموافقة على إجراء ما. في هذه المقالة، سيكون لدينا ميزتان إداريتان:
-
تجميد وإلغاء تجميد الحسابات. يمكن أن يكون هذا مفيدًا، على سبيل المثال، عندما قد يكون الحساب مخترقًا.
-
تنظيف الأصول.
في بعض الأحيان، يرسل المحتالون رموزًا احتيالية إلى عقد الرمز الحقيقي لاكتساب الشرعية. على سبيل المثال، انظر هنا (opens in a new tab). عقد ERC-20 الشرعي هو 0x4200....0042 (opens in a new tab). الاحتيال الذي يتظاهر بأنه هو 0x234....bbe (opens in a new tab).
من الممكن أيضًا أن يرسل الأشخاص رموز ERC-20 شرعية إلى عقدنا عن طريق الخطأ، وهو سبب آخر للرغبة في وجود طريقة لإخراجها.
يوفر أوبن زبلين آليتين لتمكين الوصول الإداري:
- عقود
Ownable(opens in a new tab) لها مالك واحد. الدوال التي تحتوي على معدِّل (opens in a new tab)onlyOwnerلا يمكن استدعاؤها إلا من قبل ذلك المالك. يمكن للمالكين نقل الملكية إلى شخص آخر أو التنازل عنها تمامًا. عادة ما تكون حقوق جميع الحسابات الأخرى متطابقة. - عقود
AccessControl(opens in a new tab) لديها التحكم في الوصول المستند إلى الأدوار (RBAC) (opens in a new tab).
من أجل البساطة، في هذه المقالة نستخدم Ownable.
تجميد وإلغاء تجميد العقود
تجميد وإلغاء تجميد العقود يتطلب عدة تغييرات:
-
ربط (opens in a new tab) من العناوين إلى القيم المنطقية (opens in a new tab) لتتبع العناوين المجمدة. جميع القيم مبدئيًا صفر، والتي تفسر للقيم المنطقية على أنها خطأ (false). هذا ما نريده لأنه بشكل افتراضي، الحسابات ليست مجمدة.
1 mapping(address => bool) public frozenAccounts; -
الأحداث (opens in a new tab) لإبلاغ أي شخص مهتم عند تجميد حساب أو إلغاء تجميده. من الناحية الفنية، الأحداث ليست مطلوبة لهذه الإجراءات، لكنها تساعد الكود خارج السلسلة على أن يكون قادرًا على الاستماع إلى هذه الأحداث ومعرفة ما يحدث. يعتبر من حسن السلوك أن يقوم العقد الذكي بإصدارها عندما يحدث شيء قد يكون ذا صلة بشخص آخر.
الأحداث مفهرسة، لذلك سيكون من الممكن البحث عن كل المرات التي تم فيها تجميد حساب أو إلغاء تجميده.
1 // عندما يتم تجميد الحسابات أو إلغاء تجميدها2 event AccountFrozen(address indexed _addr);3 event AccountThawed(address indexed _addr); -
دوال لتجميد وإلغاء تجميد الحسابات. هاتان الدالتان متطابقتان تقريبًا، لذلك سنتناول فقط دالة التجميد.
1 function freezeAccount(address addr)2 public3 onlyOwnerيمكن استدعاء الدوال المميزة بـ
public(opens in a new tab) من عقود ذكية أخرى أو مباشرة عن طريق معاملة.1 {2 require(!frozenAccounts[addr], "الحساب مجمد بالفعل");3 frozenAccounts[addr] = true;4 emit AccountFrozen(addr);5 } // freezeAccountإذا كان الحساب مجمدًا بالفعل، يتم التراجع. وإلا، قم بتجميده و
إصدارحدث. -
غيّر
_beforeTokenTransferلمنع نقل الأموال من حساب مجمد. لاحظ أنه لا يزال من الممكن تحويل الأموال إلى الحساب المجمد.1 require(!frozenAccounts[from], "الحساب مجمد");
تنظيف الأصول
لتحرير رموز ERC-20 التي يحتفظ بها هذا العقد، نحتاج إلى استدعاء دالة في عقد الرمز الذي تنتمي إليه، إما transfer (opens in a new tab) أو approve (opens in a new tab). لا جدوى من إهدار الغاز في هذه الحالة على المخصصات، يمكننا التحويل مباشرة.
1 function cleanupERC20(2 address erc20,3 address dest4 )5 public6 onlyOwner7 {8 IERC20 token = IERC20(erc20);هذه هي الصيغة لإنشاء كائن لعقد عندما نتلقى العنوان. يمكننا القيام بذلك لأن لدينا تعريف لرموز ERC20 كجزء من الكود المصدري (انظر السطر 4)، وهذا الملف يتضمن تعريف IERC20 (opens in a new tab)، وهي واجهة لعقد أوبن زبلين ERC-20.
1 uint balance = token.balanceOf(address(this));2 token.transfer(dest, balance);3 }هذه دالة تنظيف، لذلك من المفترض أننا لا نريد ترك أي رموز. بدلاً من الحصول على الرصيد من المستخدم يدويًا، يمكننا أتمتة العملية.
الخلاصة
هذا ليس حلاً مثاليًا - لا يوجد حل مثالي لمشكلة "ارتكب المستخدم خطأ". ومع ذلك، فإن استخدام هذه الأنواع من عمليات التحقق يمكن أن يمنع على الأقل بعض الأخطاء. القدرة على تجميد الحسابات، على الرغم من خطورتها، يمكن استخدامها للحد من أضرار بعض الاختراقات عن طريق منع المخترق من الوصول إلى الأموال المسروقة.
انظر هنا لمزيد من أعمالي (opens in a new tab).
آخر تحديث للصفحة: 3 مارس 2026