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

أمان العقود الذكية

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

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

على الرغم من اختلاف الأرقام، يُقدر أن إجمالي القيمة المسروقة أو المفقودة بسبب العيوب الأمنية في العقود الذكية يتجاوز بسهولة مليار دولار. يشمل ذلك حوادث بارزة، مثل اختراق DAO (يفتح في علامة تبويب جديدة) (سرقة 3.6M ETH، بقيمة تزيد عن $1B بأسعار اليوم)، واختراق محفظة Parity متعددة التوقيعات (يفتح في علامة تبويب جديدة) (خسارة $30M لصالح المتسللين)، ومشكلة محفظة Parity المجمدة (يفتح في علامة تبويب جديدة) (أكثر من $300M من ETH مقفلة للأبد).

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

المتطلبات الأساسية

تأكد من أنك على دراية بـ أساسيات تطوير العقود الذكية قبل التطرق إلى الأمان.

إرشادات لبناء عقود ذكية آمنة على إيثيريوم

1. تصميم ضوابط وصول مناسبة

في العقود الذكية، يمكن استدعاء الدوال المحددة بـ public أو external بواسطة أي حسابات مملوكة خارجياً (EOAs) أو حسابات عقود. تحديد الرؤية العامة (public visibility) للدوال ضروري إذا كنت تريد أن يتفاعل الآخرون مع عقدك. ومع ذلك، فإن الدوال المحددة بـ private لا يمكن استدعاؤها إلا بواسطة دوال داخل العقد الذكي، وليس بواسطة حسابات خارجية. منح كل مشارك في الشبكة حق الوصول إلى دوال العقد يمكن أن يسبب مشاكل، خاصة إذا كان ذلك يعني أن أي شخص يمكنه إجراء عمليات حساسة (مثل عملية سك رموز مميزة جديدة).

لمنع الاستخدام غير المصرح به لدوال العقد الذكي، من الضروري تنفيذ ضوابط وصول آمنة. تقيد آليات التحكم في الوصول القدرة على استخدام دوال معينة في العقد الذكي على الكيانات المعتمدة، مثل الحسابات المسؤولة عن إدارة العقد. يُعد نمط الملكية (Ownable pattern) والتحكم القائم على الأدوار (role-based control) نمطين مفيدين لتنفيذ التحكم في الوصول في العقود الذكية:

نمط الملكية

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

التحكم في الوصول القائم على الأدوار

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

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

استخدام محافظ متعددة التوقيعات

نهج آخر لتنفيذ تحكم آمن في الوصول هو استخدام حساب متعدد التوقيعات لإدارة العقد. على عكس الحساب المملوك خارجياً (EOA) العادي، فإن الحسابات متعددة التوقيعات مملوكة لكيانات متعددة وتتطلب توقيعات من حد أدنى من الحسابات — لنقل 3-of-5 — لتنفيذ المعاملات.

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

2. استخدام عبارات require() و assert() و revert() لحماية عمليات العقد

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

require(): يتم تعريف require في بداية الدوال وتضمن استيفاء الشروط المحددة مسبقًا قبل تنفيذ الدالة المستدعاة. يمكن استخدام عبارة require للتحقق من صحة مدخلات المستخدم، أو التحقق من متغيرات الحالة، أو مصادقة هوية الحساب المستدعي قبل التقدم في الدالة.

assert(): يُستخدم assert() لاكتشاف الأخطاء الداخلية والتحقق من انتهاكات "الثوابت" (invariants) في الكود الخاص بك. الثابت هو تأكيد منطقي حول حالة العقد يجب أن يظل صحيحًا لجميع عمليات تنفيذ الدوال. مثال على الثابت هو الحد الأقصى لإجمالي العرض أو الرصيد لعقد رمز مميز. يضمن استخدام assert() عدم وصول عقدك أبدًا إلى حالة ضعيفة، وإذا حدث ذلك، يتم التراجع عن جميع التغييرات في متغيرات الحالة.

revert(): يمكن استخدام revert() في عبارة if-else التي تطلق استثناءً إذا لم يتم استيفاء الشرط المطلوب. يستخدم العقد النموذجي أدناه revert() لحماية تنفيذ الدوال:

3. اختبار العقود الذكية والتحقق من صحة الكود

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

الطريقة المعتادة هي كتابة اختبارات وحدة صغيرة باستخدام بيانات وهمية يُتوقع أن يتلقاها العقد من المستخدمين. يُعد اختبار الوحدة جيدًا لاختبار وظائف دوال معينة والتأكد من أن العقد الذكي يعمل كما هو متوقع.

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

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

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

4. اطلب مراجعة مستقلة للكود الخاص بك

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

التدقيقات

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

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

مكافآت اكتشاف الأخطاء

يعد إعداد برنامج مكافآت اكتشاف الأخطاء نهجًا آخر لتنفيذ مراجعات الكود الخارجية. مكافأة اكتشاف الأخطاء هي مكافأة مالية تُمنح للأفراد (عادةً قراصنة القبعات البيضاء) الذين يكتشفون نقاط ضعف في تطبيق ما.

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

تتمثل الإستراتيجية المفيدة في تحديد تعويض برنامج مكافآت اكتشاف الأخطاء بما يتناسب مع مقدار الأموال المعرضة للخطر. يوفر هذا النهج، الموصوف بـ "مكافأة اكتشاف الأخطاء المتدرجة (يفتح في علامة تبويب جديدة)"، حوافز مالية للأفراد للكشف عن نقاط الضعف بمسؤولية بدلاً من استغلالها.

5. اتبع أفضل الممارسات أثناء تطوير العقود الذكية

إن وجود التدقيقات ومكافآت اكتشاف الأخطاء لا يعفيك من مسؤوليتك في كتابة كود عالي الجودة. يبدأ أمان العقد الذكي الجيد باتباع عمليات التصميم والتطوير المناسبة:

  • تخزين جميع الأكواد في نظام التحكم في الإصدار، مثل git

  • إجراء جميع تعديلات الكود عبر طلبات السحب (pull requests)

  • التأكد من أن طلبات السحب تحتوي على مراجع مستقل واحد على الأقل — إذا كنت تعمل بمفردك في مشروع، ففكر في العثور على مطورين آخرين وتبادل مراجعات الكود

  • استخدام بيئة تطوير لاختبار وتصريف ونشر العقود الذكية

  • تمرير الكود الخاص بك عبر أدوات تحليل الكود الأساسية، مثل Cyfrin Aderyn (يفتح في علامة تبويب جديدة) و Mythril و سليذر. من الناحية المثالية، يجب عليك القيام بذلك قبل دمج كل طلب سحب ومقارنة الاختلافات في المخرجات

  • التأكد من تصريف الكود الخاص بك دون أخطاء، وأن مُصرّف Solidity لا يصدر أي تحذيرات

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

6. تنفيذ خطط قوية للتعافي من الكوارث

يمكن أن يؤدي تصميم ضوابط وصول آمنة، وتنفيذ مُعدِّلات الدوال، والاقتراحات الأخرى إلى تحسين أمان العقد الذكي، لكنها لا يمكن أن تستبعد إمكانية الاستغلال الخبيث. يتطلب بناء عقود ذكية آمنة "الاستعداد للفشل" ووجود خطة احتياطية للاستجابة بفعالية للهجمات. ستتضمن خطة التعافي من الكوارث المناسبة بعض أو كل المكونات التالية:

ترقيات العقد

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

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

تتفاعل الحسابات مع العقد الوكيل، الذي يرسل جميع استدعاءات الدوال إلى عقد المنطق باستخدام الاستدعاء منخفض المستوى delegatecall() (يفتح في علامة تبويب جديدة). على عكس استدعاء رسالة عادي، يضمن delegatecall() تنفيذ الكود الذي يعمل في عنوان عقد المنطق في سياق العقد المستدعي. هذا يعني أن عقد المنطق سيكتب دائمًا في تخزين الوكيل (بدلاً من تخزينه الخاص) ويتم الحفاظ على القيم الأصلية لـ msg.sender و msg.value.

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

المزيد حول ترقية العقود.

التوقفات الطارئة

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

الخيار الجذري هو تنفيذ دالة "توقف طارئ" تمنع الاستدعاءات للدوال الضعيفة في العقد. تتكون التوقفات الطارئة عادةً من المكونات التالية:

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

  2. الدوال التي تشير إلى المتغير المنطقي في تنفيذها. يمكن الوصول إلى هذه الدوال عندما لا يكون العقد الذكي متوقفًا، وتصبح غير قابلة للوصول عند تشغيل ميزة التوقف الطارئ.

  3. كيان لديه حق الوصول إلى دالة التوقف الطارئ، والذي يعين المتغير المنطقي إلى true. لمنع الإجراءات الخبيثة، يمكن تقييد الاستدعاءات لهذه الدالة على عنوان موثوق به (مثل مالك العقد).

بمجرد أن ينشط العقد التوقف الطارئ، لن تكون بعض الدوال قابلة للاستدعاء. يتم تحقيق ذلك عن طريق تغليف دوال محددة في مُعدِّل يشير إلى المتغير العام. يوجد أدناه مثال (يفتح في علامة تبويب جديدة) يصف تنفيذ هذا النمط في العقود:

يوضح هذا المثال الميزات الأساسية للتوقفات الطارئة:

  • isStopped هو متغير منطقي يتم تقييمه إلى false في البداية و true عندما يدخل العقد في وضع الطوارئ.

  • تتحقق مُعدِّلات الدوال onlyWhenStopped و stoppedInEmergency من المتغير isStopped. يُستخدم stoppedInEmergency للتحكم في الدوال التي يجب أن تكون غير قابلة للوصول عندما يكون العقد ضعيفًا (مثل deposit()). الاستدعاءات لهذه الدوال ستتراجع ببساطة.

يُستخدم onlyWhenStopped للدوال التي يجب أن تكون قابلة للاستدعاء أثناء حالة الطوارئ (مثل emergencyWithdraw()). يمكن أن تساعد هذه الدوال في حل الموقف، ومن هنا جاء استبعادها من قائمة "الدوال المقيدة".

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

مراقبة الأحداث

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

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

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

7. تصميم أنظمة حوكمة آمنة

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

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

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

المزيد حول تصميم أنظمة حوكمة آمنة (يفتح في علامة تبويب جديدة)، وآليات التصويت المختلفة في المنظمات المستقلة اللامركزية (DAOs) (يفتح في علامة تبويب جديدة)، ونواقل هجوم DAO الشائعة التي تستفيد من التمويل اللامركزي (DeFi) (يفتح في علامة تبويب جديدة) في الروابط المشتركة.

8. تقليل التعقيد في الكود إلى الحد الأدنى

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

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

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

9. الدفاع ضد نقاط ضعف العقود الذكية الشائعة

إعادة الدخول

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

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

ضع في اعتبارك عقدًا ذكيًا بسيطًا ("الضحية") يسمح لأي شخص بإيداع وسحب إيثر:

يعرض هذا العقد دالة withdraw() للسماح للمستخدمين بسحب ETH المودع مسبقًا في العقد. عند معالجة السحب، يقوم العقد بالعمليات التالية:

  1. يتحقق من رصيد ETH للمستخدم
  2. يرسل الأموال إلى العنوان المستدعي
  3. يعيد تعيين رصيدهم إلى 0، مما يمنع عمليات السحب الإضافية من المستخدم

تتبع دالة withdraw() في عقد Victim نمط "التحققات-التفاعلات-التأثيرات" (checks-interactions-effects). إنها تتحقق مما إذا كانت الشروط اللازمة للتنفيذ مستوفاة (أي أن المستخدم لديه رصيد ETH إيجابي) وتنفذ التفاعل عن طريق إرسال ETH إلى عنوان المستدعي، قبل تطبيق تأثيرات المعاملة (أي تقليل رصيد المستخدم).

إذا تم استدعاء withdraw() من حساب مملوك خارجياً (EOA)، يتم تنفيذ الدالة كما هو متوقع: يرسل msg.sender.call.value() ETH إلى المستدعي. ومع ذلك، إذا كان msg.sender حساب عقد ذكي يستدعي withdraw()، فإن إرسال الأموال باستخدام msg.sender.call.value() سيؤدي أيضًا إلى تشغيل الكود المخزن في ذلك العنوان.

تخيل أن هذا هو الكود المنشور في عنوان العقد:

تم تصميم هذا العقد للقيام بثلاثة أشياء:

  1. قبول إيداع من حساب آخر (من المحتمل أن يكون الحساب المملوك خارجياً للمهاجم)
  2. إيداع 1 ETH في عقد الضحية
  3. سحب 1 ETH المخزن في العقد الذكي

لا يوجد شيء خاطئ هنا، باستثناء أن Attacker لديه دالة أخرى تستدعي withdraw() في Victim مرة أخرى إذا كان الغاز المتبقي من msg.sender.call.value الوارد أكثر من 40,000. يمنح هذا Attacker القدرة على إعادة الدخول إلى Victim وسحب المزيد من الأموال قبل اكتمال الاستدعاء الأول لـ withdraw. تبدو الدورة هكذا:

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

كيفية منع هجمات إعادة الدخول

أحد الأساليب للتعامل مع إعادة الدخول هو اتباع نمط التحققات-التأثيرات-التفاعلات (يفتح في علامة تبويب جديدة). يرتب هذا النمط تنفيذ الدوال بطريقة تجعل الكود الذي يقوم بالتحققات اللازمة قبل التقدم في التنفيذ يأتي أولاً، يليه الكود الذي يعالج حالة العقد، مع وصول الكود الذي يتفاعل مع العقود الأخرى أو الحسابات المملوكة خارجياً (EOAs) أخيرًا.

يُستخدم نمط التحققات-التأثيرات-التفاعلات في نسخة منقحة من عقد Victim الموضح أدناه:

contract NoLongerAVictim {
    function withdraw() external {
        uint256 amount = balances[msg.sender];
        balances[msg.sender] = 0;
        (bool success, ) = msg.sender.call.value(amount)("");
        require(success);
    }
}

يقوم هذا العقد بـ تحقق من رصيد المستخدم، ويطبق تأثيرات دالة withdraw() (عن طريق إعادة تعيين رصيد المستخدم إلى 0)، ويشرع في تنفيذ التفاعل (إرسال ETH إلى عنوان المستخدم). يضمن هذا قيام العقد بتحديث تخزينه قبل الاستدعاء الخارجي، مما يقضي على شرط إعادة الدخول الذي مكّن الهجوم الأول. لا يزال بإمكان عقد Attacker الاستدعاء مرة أخرى إلى NoLongerAVictim، ولكن نظرًا لأنه تم تعيين balances[msg.sender] إلى 0، فإن عمليات السحب الإضافية ستطرح خطأ.

خيار آخر هو استخدام قفل استبعاد متبادل (يوصف عادة باسم "mutex") يقفل جزءًا من حالة العقد حتى يكتمل استدعاء الدالة. يتم تنفيذ ذلك باستخدام متغير منطقي يتم تعيينه إلى true قبل تنفيذ الدالة ويعود إلى false بعد الانتهاء من الاستدعاء. كما يظهر في المثال أدناه، فإن استخدام mutex يحمي الدالة من الاستدعاءات العودية (recursive calls) بينما لا يزال الاستدعاء الأصلي قيد المعالجة، مما يوقف إعادة الدخول بشكل فعال.

يمكنك أيضًا استخدام نظام مدفوعات السحب (pull payments) (يفتح في علامة تبويب جديدة) الذي يتطلب من المستخدمين سحب الأموال من العقود الذكية، بدلاً من نظام "مدفوعات الدفع" (push payments) الذي يرسل الأموال إلى الحسابات. يزيل هذا إمكانية تشغيل الكود عن غير قصد في عناوين غير معروفة (ويمكنه أيضًا منع بعض هجمات الحرمان من الخدمة).

تجاوز السعة السفلي والعلوي للأعداد الصحيحة

يحدث تجاوز السعة العلوي (overflow) للعدد الصحيح عندما تقع نتائج عملية حسابية خارج النطاق المقبول للقيم، مما يتسبب في "التفافها" إلى أدنى قيمة يمكن تمثيلها. على سبيل المثال، يمكن لـ uint8 تخزين قيم تصل إلى 2^8-1=255 فقط. العمليات الحسابية التي تؤدي إلى قيم أعلى من 255 ستتجاوز السعة وتعيد تعيين uint إلى 0، على غرار كيفية إعادة تعيين عداد المسافات في السيارة إلى 0 بمجرد وصوله إلى الحد الأقصى للمسافة المقطوعة (999999).

يحدث تجاوز السعة السفلي (underflow) للأعداد الصحيحة لأسباب مماثلة: تقع نتائج عملية حسابية أقل من النطاق المقبول. لنفترض أنك حاولت إنقاص 0 في uint8، فإن النتيجة ستلتف ببساطة إلى أقصى قيمة يمكن تمثيلها (255).

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

كيفية منع تجاوز السعة السفلي والعلوي للأعداد الصحيحة

اعتبارًا من الإصدار 0.8.0، يرفض مُصرّف Solidity الكود الذي يؤدي إلى تجاوز السعة السفلي والعلوي للأعداد الصحيحة. ومع ذلك، يجب على العقود المُصرّفة بإصدار مُصرّف أقل إما إجراء فحوصات على الدوال التي تتضمن عمليات حسابية أو استخدام مكتبة (مثل SafeMath (يفتح في علامة تبويب جديدة)) تتحقق من تجاوز السعة السفلي/العلوي.

التلاعب بالأوراكل

تستمد الأوراكل المعلومات خارج السلسلة وترسلها على السلسلة لتستخدمها العقود الذكية. باستخدام الأوراكل، يمكنك تصميم عقود ذكية تتفاعل مع الأنظمة خارج السلسلة، مثل أسواق رأس المال، مما يوسع نطاق تطبيقها بشكل كبير.

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

هناك مصدر قلق أمني ذو صلة وهو استخدام أوراكل على السلسلة، مثل منصة تداول لامركزية (DEX)، للحصول على السعر الفوري لأصل ما. غالبًا ما تقوم منصات الإقراض في صناعة التمويل اللامركزي (DeFi) بذلك لتحديد قيمة ضمان المستخدم لتحديد المبلغ الذي يمكنه اقتراضه.

غالبًا ما تكون أسعار منصات التداول اللامركزية (DEX) دقيقة، ويرجع ذلك إلى حد كبير إلى قيام المراجحين (arbitrageurs) باستعادة التكافؤ في الأسواق. ومع ذلك، فهي مفتوحة للتلاعب، خاصة إذا كان الأوراكل على السلسلة يحسب أسعار الأصول بناءً على أنماط التداول التاريخية (كما هو الحال عادةً).

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

كيفية منع التلاعب بالأوراكل

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

إذا كنت تخطط للاستعلام من أوراكل على السلسلة عن أسعار الأصول، ففكر في استخدام واحد ينفذ آلية متوسط السعر المرجح بالوقت (TWAP). يستعلم أوراكل TWAP (يفتح في علامة تبويب جديدة) عن سعر الأصل في نقطتين زمنيتين مختلفتين (والتي يمكنك تعديلها) ويحسب السعر الفوري بناءً على المتوسط الذي تم الحصول عليه. يحمي اختيار فترات زمنية أطول بروتوكولك من التلاعب بالأسعار نظرًا لأن الطلبات الكبيرة المنفذة مؤخرًا لا يمكن أن تؤثر على أسعار الأصول.

موارد أمان العقود الذكية للمطورين

أدوات لتحليل العقود الذكية والتحقق من صحة الكود

أدوات لمراقبة العقود الذكية

أدوات للإدارة الآمنة للعقود الذكية

خدمات تدقيق العقود الذكية

منصات مكافآت اكتشاف الثغرات

  • Immunefi (يفتح في علامة تبويب جديدة) - منصة مكافآت اكتشاف الثغرات للعقود الذكية ومشاريع التمويل اللامركزي (DeFi)، حيث يقوم الباحثون الأمنيون بمراجعة الكود، والإفصاح عن الثغرات، والحصول على أموال، وجعل الكريبتو أكثر أماناً.

  • HackerOne (يفتح في علامة تبويب جديدة) - منصة لتنسيق الثغرات ومكافآت اكتشافها تربط الشركات بمختبري الاختراق وباحثي الأمن السيبراني.

  • HackenProof (يفتح في علامة تبويب جديدة) - منصة متخصصة في مكافآت اكتشاف الثغرات لمشاريع الكريبتو (التمويل اللامركزي، العقود الذكية، المحافظ، منصات التداول المركزية والمزيد)، حيث يقدم محترفو الأمن خدمات الفرز ويحصل الباحثون على أموال مقابل تقارير الثغرات ذات الصلة والمتحقق منها.

  • Sherlock (يفتح في علامة تبويب جديدة) - ضامن في Web3 لأمن العقود الذكية، مع مدفوعات للمدققين تُدار عبر العقود الذكية لضمان دفع مكافآت الثغرات ذات الصلة بشكل عادل.

  • CodeHawks (يفتح في علامة تبويب جديدة) - منصة تنافسية لمكافآت اكتشاف الثغرات حيث يشارك المدققون في مسابقات وتحديات أمنية، و(قريباً) في عمليات التدقيق الخاصة بهم.

منشورات حول ثغرات العقود الذكية المعروفة وعمليات الاستغلال

تحديات لتعلم أمن العقود الذكية

أفضل الممارسات لتأمين العقود الذكية

دروس تعليمية حول أمن العقود الذكية