أمن العقود الذكية (2): الحماية من ثغرة Reentrancy باستخدام ReentrancyGuard

دقائق القراءة: 5

أمن العقود الذكية (2): الحماية من ثغرة Reentrancy باستخدام ReentrancyGuard

بعد أن شرحنا في مقال أمن العقود الذكية (1): ثغرة إعادة الدخول (Reentrancy Attack) الشهيرة وكيفية استغلالها كيف يمكن لمهاجم أن يستغل ترتيب تنفيذ الأوامر داخل العقد الذكي، ننتقل هنا إلى مرحلة أكثر أهمية: الحماية العملية. في بيئة Ethereum لا يكفي أن تعرف شكل الهجوم، بل يجب أن تبني منطقاً دفاعياً يمنع تكرار الدخول إلى الدالة الحساسة أثناء تنفيذها.

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

ما هو ReentrancyGuard ولماذا نحتاجه؟

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

هذا مهم خصوصاً عندما تعتمد دوال السحب أو التحويل على استدعاءات منخفضة المستوى مثل call. وكما أوضحنا سابقاً في مقال استلام وإرسال الأموال (Ether) برمجياً: فهم الدوال payable و fallback، فإن أي تفاعل خارجي قد يفتح الباب أمام تنفيذ كود إضافي داخل عقد آخر، وهو أصل مشكلة إعادة الدخول.

استخدام ReentrancyGuard لا يلغي الحاجة إلى كتابة منطق صحيح. أفضل حماية دائماً هي الجمع بين نمط Checks-Effects-Interactions وبين الحماية الإضافية عبر nonReentrant.

كيف يعمل المعدّل nonReentrant داخلياً؟

توفّر مكتبة OpenZeppelin عقداً أساسياً يمكن توريثه كما تعلمنا في الوراثة (Inheritance): بناء عقود ذكية متقدمة بالاعتماد على أكواد عقود سابقة. هذا العقد يعرّف معدّلاً اسمه nonReentrant يقوم بثلاث خطوات:

  • يتحقق أولاً أن الدالة ليست قيد التنفيذ.
  • يغيّر الحالة إلى أن التنفيذ بدأ فعلاً.
  • بعد نهاية التنفيذ، يعيد الحالة إلى وضعها الطبيعي.

إذا حاول مهاجم تشغيل نفس الدالة مرة ثانية من خلال fallback أو receive قبل اكتمال الاستدعاء الأول، ينهار هذا المسار فوراً عبر revert. وللتعمق في إدارة أخطاء التنفيذ يمكنك مراجعة التعامل مع الأخطاء وإرجاع الأموال: استخدام require, assert, revert.

مثال عملي: عقد سحب محمي باستخدام ReentrancyGuard

في المثال التالي سنبني عقداً بسيطاً لتخزين أرصدة المستخدمين داخل mapping، ثم نسمح لهم بالسحب بأمان. إذا كنت تحتاج مراجعة بنية التخزين، فمقال القواميس (Mappings): أسرع طريقة لربط عناوين المحافظ بأرصدتها (Key-Value) يشرح هذه الفكرة بالتفصيل.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract SecureVault is ReentrancyGuard {
    mapping(address => uint256) private balances;

    event Deposited(address indexed user, uint256 amount);
    event Withdrawn(address indexed user, uint256 amount);

    function deposit() external payable {
        require(msg.value > 0, "Send some Ether");
        balances[msg.sender] += msg.value;
        emit Deposited(msg.sender, msg.value);
    }

    function withdraw(uint256 amount) external nonReentrant {
        require(amount > 0, "Amount must be greater than zero");
        require(balances[msg.sender] >= amount, "Insufficient balance");

        balances[msg.sender] -= amount;

        (bool success, ) = payable(msg.sender).call{value: amount}("");
        require(success, "Transfer failed");

        emit Withdrawn(msg.sender, amount);
    }

    function getBalance(address user) external view returns (uint256) {
        return balances[user];
    }
}

لماذا هذا المثال أكثر أماناً؟

كيفية استخدام ReentrancyGuard داخل Hardhat أو Remix

إذا كنت تعمل ضمن بيئة احترافية، فغالباً ستستخدم Hardhat مع الحزم الرسمية. أما إن كنت في مرحلة التجريب السريع، فيمكنك الاعتماد على Remix IDE. في الحالتين، الفكرة نفسها: استيراد المكتبة ثم وراثة العقد الأساسي.

خطوات العمل في Hardhat

  1. تثبيت مكتبة OpenZeppelin داخل المشروع.
  2. استيراد ملف ReentrancyGuard.sol.
  3. توريث العقد من ReentrancyGuard.
  4. إضافة المعدّل nonReentrant إلى كل دالة معرضة لإعادة الدخول.
  5. كتابة unit tests للتحقق من رفض الاستدعاءات المتداخلة، كما في اختبار العقود الذكية محلياً: كتابة اختبارات الوحدة (Unit Tests) باستخدام Chai & Mocha.

هل ReentrancyGuard وحده كافٍ؟

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

لهذا السبب يجب دمج الحماية مع مفاهيم أخرى مثل استخدام modifiers مناسبة، وفهم التفاعل بين العقود كما في التفاعل بين العقود الذكية: كيف تجعل عقداً يستدعي دالة من عقد آخر؟، والحرص على مراجعة مسارات التنفيذ التي تتضمن استدعاءات خارجية.

من زاوية Gas Optimization، لا تضف nonReentrant إلى كل دالة دون سبب. طبّقه فقط على الدوال التي تنفذ استدعاءات خارجية أو تتعامل مع تحويل الأموال. الحماية الزائدة في غير موضعها قد ترفع تكلفة التنفيذ بلا فائدة عملية.

أخطاء شائعة عند تطبيق الحماية

  • الاعتماد على ReentrancyGuard مع ترك تحديث الحالة بعد إرسال الأموال.
  • حماية دالة السحب، لكن ترك دوال أخرى حساسة تستدعي عقوداً خارجية دون حماية.
  • نسيان اختبار سيناريو مهاجم فعلي بعقد خبيث يملك fallback قابل لإعادة الدخول.
  • الخلط بين الحماية من إعادة الدخول وبين التحكم في الوصول عبر onlyOwner أو غيره من القيود.

أفضل ممارسة أمنية للمشاريع الحقيقية

في المشاريع الجادة، حماية withdraw ليست النهاية، بل البداية. يجب أن تمر العقود عبر مراجعات يدوية، اختبارات تلقائية، وتحليل ثابت للكود، لأن ثغرات Smart Contracts غالباً لا تمنح فرصة ثانية بعد النشر على الشبكة.

كما أن فهمك الجيد للبنية الأساسية للبلوكتشين، بدءاً من مدخل إلى Web3: ما هو البلوكتشين ولماذا يغير شكل الإنترنت والأنظمة المالية؟، يساعدك على رؤية الجانب التنفيذي والأمني معاً، وليس مجرد كتابة كود يعمل ظاهرياً فقط.

الخلاصة

الحماية من ثغرة إعادة الدخول لا تعتمد على سطر واحد فقط، بل على عقلية تطوير آمنة. مكتبة OpenZeppelin وفّرت لنا ReentrancyGuard كحل عملي ومجرب، لكن قيمته الحقيقية تظهر عندما يُستخدم مع ترتيب منطقي صحيح للعمليات، واختبارات واقعية، ومراجعة واعية لمسارات تحويل الأموال.

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

17 comments

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *