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

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

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

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

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

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

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

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

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

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

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

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

نمط الملكية

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

عمليات التدقيق

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

ترقيات العقد

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

إعادة الدخول

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

تجاوز السعة للأعداد الصحيحة (Underflows and Overflows)

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

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

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

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

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

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

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

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

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

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

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

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

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

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