پرش به محتوای اصلی
Change page

امنیت قرارداد هوشمند

قراردادهای هوشمند بسیار انعطاف‌پذیر هستند و می توانند مقادیر زیادی از ارزش و داده را کنترل کنند، در حالی که منطق تغییرناپذیر مبتنی بر کد مستقر در بلاک چین را اجرا می کنند. این یک اکوسیستم پر جنب و جوش از برنامه های کاربردی بی نیاز از اعتماد و غیرمتمرکز ایجاد کرده است که مزایای زیادی را نسبت به سیستم های قدیمی ارائه می دهد. آنها همچنین فرصت‌هایی را برای مهاجمانی که به دنبال سودجویی از طریق سوءاستفاده از آسیب‌پذیری‌ها در قراردادهای هوشمند هستند، نشان می‌دهند.

بلاک چین های عمومی، مانند اتریوم، مسئله ایمن‌سازی قراردادهای هوشمند را پیچیده‌تر و سخت‌تر می کند. معمولا پس از استقرار کد قرارداد در شبکه، نمی‌توان آن را به منظور رفع نقص های امنیتی را تغییر داد، در حالی که ردیابی دارایی های دزدیده شده از قراردادهای هوشمند بسیار دشوار است و عمدتاً به دلیل تغییر ناپذیری قابل بازیابی نیستند.

اگرچه اعداد و ارقام متفاوت است، تخمین زده می شود که کل ارزش سرقت شده یا از دست رفته به دلیل نقص امنیتی در قراردادهای هوشمند به راحتی بیش از یک میلیارد دلار است. این شامل حوادث پرمخاطب، مانند هک DAO(opens in a new tab) (3.6M اتریوم دزدیده شده، به ارزش بیش از 1 میلیارد دلار در قیمت های امروزی)، هک کیف پول چند علامتی Parity(opens in a new tab) (30 میلیون دلار از دست هکرها) و مشکل کیف پول منجمد شده(opens in a new tab) (بیش از 300 میلیون دلار ETH برای همیشه قفل شده است).

مسائل ذکر شده، توسعه دهندگان را مجبور می‌کند تا تلاش کنند قراردادهای هوشمند ایمن، نبوغ آمیز و منعطف بسازند. امنیت قراردادهای هوشمند یک تجارت جدی است و هر توسعه‌دهنده‌ای باید آن را یاد بگیرد. این راهنما ملاحظات امنیتی برای توسعه دهندگان اتریوم را پوشش می دهد و منابعی را برای بهبود امنیت قراردادهای هوشمند بررسی می کند.

پیش‌نیازها

قبل از پرداختن به امنیت، مطمئن شوید که با مبانی توسعه قرارداد هوشمند آشنا هستید.

دستورالعمل هایی برای ساخت قراردادهای هوشمند ایمن در اتریوم

1. کنترل های دسترسی طراحی مناسب

در قراردادهای هوشمند، عملکردهایی که public یا external علامت‌گذاری شده‌اند، می‌توانند توسط هر حساب تحت مالکیت خارجی (EOA) یا حساب قراردادی فراخوانی شوند. اگر می‌خواهید دیگران با قرارداد شما در تعامل باشند، مشخص کردن نمای عمومی برای عملکردها ضروری است. با این حال، عملکردهایی که با private علامت‌گذاری شده‌اند، فقط توسط توابع داخل قرارداد هوشمند فراخوانی می‌شوند و در حساب‌های خارجی مورد استفاده قرار نمی گیرند. دادن دسترسی به هر یک از شرکت‌کنندگان شبکه به توابع قرارداد می‌تواند باعث ایجاد مشکلاتی شود، به‌ویژه اگر به این معنی باشد که هر کسی بتواند عملیات حساس را انجام دهد (به عنوان مثال، استخراج توکن‌های جدید).

برای جلوگیری از استفاده غیرمجاز از توابع قرارداد هوشمند، لازم است کنترل های دسترسی ایمن را اجرا کنید. مکانیسم‌های کنترل دسترسی، توانایی استفاده از عملکردهای خاص در یک قرارداد هوشمند را به نهادهای تایید شده، مانند حساب‌های مسئول مدیریت قرارداد، محدود می‌کند. الگوی مالکیت و کنترل مبتنی بر نقش دو الگوی مفید برای اجرای کنترل دسترسی در قراردادهای هوشمند هستند:

الگوی قابل مالکیت

در الگویل قابل مالکیت، یک آدرس به عنوان "مالک" قرارداد در طول فرآیند ایجاد قرارداد تنظیم می شود. به توابع محافظت شده یک اصلاح‌کننده OnlyOwner اختصاص داده می‌شود، که تضمین می‌کند قرارداد قبل از اجرای تابع، هویت آدرس تماس را تأیید می‌کند. تماس‌های توابع محافظت‌شده از آدرس‌های دیگر به غیر از مالک قرارداد، همیشه برمی‌گردند و از دسترسی ناخواسته جلوگیری می‌کنند.

کنترل دسترسی مبتنی بر نقش

ثبت یک آدرس واحد به‌عنوان Owner در یک قرارداد هوشمند، خطر تمرکز را معرفی می‌کند و نشان‌دهنده یک نقطه شکست واحد است. اگر کلیدهای حساب مالک به خطر بیفتد، مهاجمان می توانند به قرارداد مالکیت حمله کنند. به همین دلیل است که استفاده از الگوی کنترل دسترسی مبتنی بر نقش با چندین حساب اداری ممکن است گزینه بهتری باشد.

در کنترل دسترسی مبتنی بر نقش، دسترسی به عملکردهای حساس بین مجموعه ای از شرکت کنندگان قابل اعتماد توزیع می شود. به عنوان مثال، یک حساب ممکن است مسئول ضرب توکن ها باشد، در حالی که حساب دیگری ارتقاء داده یا قرارداد را متوقف می کند. غیرمتمرکز کردن کنترل دسترسی به این روش، نقاط منفرد شکست را از بین می برد و مفروضات اعتماد را برای کاربران کاهش می دهد.

استفاده از کیف پول‌های چند امضایی

روش دیگر برای اجرای کنترل دسترسی ایمن استفاده از حساب چند امضایی برای مدیریت قرارداد است. برخلاف یک EOA معمولی، حساب‌های چند امضایی متعلق به چندین نهاد هستند و برای انجام تراکنش‌ها به امضای حداقل تعداد حساب‌ها (مثلاً 3 از 5) نیاز دارند.

استفاده از مالتی سیگ برای کنترل دسترسی، یک لایه امنیتی اضافی را معرفی می‌کند زیرا اقدامات روی قرارداد هدف مستلزم رضایت چندین طرف است. این مورد به ویژه در صورتی مفید است که استفاده از الگوی اونبل (Ownable) ضروری باشد، زیرا دستکاری عملکردهای حساس قرارداد برای اهداف مخرب را برای مهاجم دشوارتر می‌کند.

2. برای محافظت از عملیات قرارداد از عبارات require() و assert() و revert() استفاده کنید

همانطور که گفته شد، هر کسی می‌تواند توابع عمومی را در قرارداد هوشمند شما پس از استقرار در بلاک‌چین فراخوانی کند. از آنجایی که نمی‌توانید از قبل بدانید حساب‌های خارجی چگونه با یک قرارداد تعامل خواهند داشت، بهتراست که قبل از استقرار، حفاظت‌های داخلی در برابر عملیات مشکل‌ساز را اجرا کنید. می‌توانید با استفاده از دستورات require()، assert() و revert() رفتار صحیح را در قراردادهای هوشمند برای راه‌اندازی استثناها و برگرداندن تغییرات حالت اعمال کنید، در صورتی که اجرا نتواند الزامات خاصی را برآورده کند.

require(): دستورها require در شروع توابع تعریف می‌شوند و اطمینان می‌دهند که شرایط از پیش تعریف شده قبل از اجرای تابع فراخوانی شده برآورده می‌شوند. یک عبارت require را می‌توان برای اعتبارسنجی ورودی های کاربر، بررسی متغیرهای حالت، یا احراز هویت حساب فراخوان قبل از پیشرفت با یک تابع استفاده کرد.

assert(): دستور assert() برای شناسایی خطاهای داخلی و بررسی نقض "invariants" در کد شما استفاده می شود. یک invariant یک ادعای منطقی در مورد وضعیت قرارداد است که باید برای اجرای همه توابع صادق باشد. یک مثال ثابت، حداکثر عرضه یا موجودی یک قرارداد توکن است. استفاده از assert() تضمین می‌کند که قرارداد شما هرگز به یک وضعیت آسیب‌پذیر نمی‌رسد، و در صورت رسیدن، همه تغییرات در متغیرهای حالت برگردانده می‌شوند.

revert(): دستور revert() را می توان در یک عبارت if-else استفاده کرد که در صورت عدم رعایت شرایط مورد نیاز، یک استثنا ایجاد می کند. قرارداد نمونه زیر از revert() برای محافظت از اجرای توابع استفاده می کند:

1pragma solidity ^0.8.4;
2
3contract VendingMachine {
4 address owner;
5 error Unauthorized();
6 function buy(uint amount) public payable {
7 if (amount > msg.value / 2 ether)
8 revert("Not enough Ether provided.");
9 // Perform the purchase.
10 }
11 function withdraw() public {
12 if (msg.sender != owner)
13 revert Unauthorized();
14
15 payable(msg.sender).transfer(address(this).balance);
16 }
17}
نمایش همه

3. قراردادهای هوشمند را تست کنید و صحت کد را تأیید کنید

تغییرناپذیری کدهای در حال اجرا در ماشین مجازی اتریوم به این معنی است که قراردادهای هوشمند سطح بالاتری از ارزیابی کیفیت را در مرحله توسعه می طلبد. تست قرارداد خود به طور گسترده و مشاهده آن برای نتایج غیرمنتظره امنیت را تا حد زیادی بهبود می‌بخشد و در دراز مدت از کاربران شما محافظت می‌کند.

روش معمول نوشتن تست‌های واحد کوچک با استفاده از داده‌های ساختگی است که انتظار می‌رود قرارداد را از کاربران دریافت کند. آزمایش Unit برای آزمایش عملکرد عملکردهای خاص و اطمینان از اینکه قرارداد هوشمند مطابق انتظار عمل می کند خوب است.

متأسفانه، تست واحد برای بهبود امنیت قراردادهای هوشمند زمانی که به صورت مجزا مورد استفاده قرار می‌گیرد، حداقل مؤثر است. یک تست واحد ممکن است ثابت کند که یک تابع برای داده‌های ساختگی به درستی اجرا می‌شود، اما تست‌های واحد فقط به اندازه تست‌های نوشته شده مؤثر هستند. این امر تشخیص موارد لبه از دست رفته و آسیب پذیری هایی را که می تواند ایمنی قرارداد هوشمند شما را به هم بزند، دشوار می کند.

یک رویکرد بهتر ترکیب آزمایش واحد با آزمایش مبتنی بر ویژگی است که با استفاده از تحلیل استاتیک و پویا انجام می‌شود. تجزیه و تحلیل استاتیک بر نمایش‌های سطح پایین، مانند گراف‌های جریان کنترل(opens in a new tab) ودرخت‌های نحو انتزاعی(opens in a new tab) برای تجزیه و تحلیل وضعیت‌های قابل دسترسی برنامه و مسیرهای اجرا. در همین حال، تکنیک‌های تحلیل پویا، مانند فازی قرارداد هوشمند(opens in a new tab)، قرارداد را اجرا می‌کنند. کد با مقادیر ورودی تصادفی برای شناسایی عملیاتی که ویژگی‌های امنیتی را نقض می‌کند.

تأیید رسمی تکنیک دیگری برای تأیید ویژگی‌های امنیتی در قراردادهای هوشمند است. برخلاف تست‌های معمولی، تأیید رسمی می‌تواند به طور قطعی عدم وجود خطا در یک قرارداد هوشمند را ثابت کند. این امر با ایجاد یک مشخصات رسمی که ویژگی‌های امنیتی مورد نظر را نشان می‌دهد و اثبات اینکه مدل رسمی قراردادها به این مشخصات پایبند است، به دست می‌آید.

4. درخواست بررسی مستقل کد خود را داشته باشید

پس از تست قرارداد خود، بهتر است از دیگران بخواهید که کد منبع را برای هرگونه مشکل امنیتی بررسی کنند. تست تمام ایرادات یک قرارداد هوشمند را آشکار نمی‌کند، اما دریافت یک بررسی مستقل امکان شناسایی آسیب پذیری‌ها را افزایش می‌دهد.

حسابرسی‌های امنیتی

راه‌اندازی آدیت قرارداد هوشمند یکی از راه‌های انجام بررسی مستقل کد است. حسابرسان یا آدیتورها نقش مهمی در حصول اطمینان از ایمن بودن قراردادهای هوشمند و عاری از نقص کیفی و خطاهای طراحی دارند.

با وجود همه‌ی این موارد، شما نباید با حسابرسی امنیتی مانند پاسخی برای تمام مشکلات برخورد کنید. آدیت قراردادهای هوشمند هر اشکالی را شناسایی نمی‌کند و عمدتاً برای ارائه یک سری بررسی اضافی طراحی شده است که می‌تواند به شناسایی مشکلاتی که توسعه دهندگان در طول توسعه و تست اولیه از قلم انداخته‌اند کمک کند. همچنین باید بهترین روش‌ها را برای کار با حسابرسان، مانند مستندسازی کد به درستی و افزودن نظرات درون خطی، دنبال کنید تا از مزایای حسابرسی قرارداد هوشمند به حداکثر برسانید.

پاداش‌های باگ

راه اندازی یک برنامه باگ بانتی روش دیگری برای اجرای بررسی کدهای خار‌جی است. جایزه باگ یک پاداش مالی است که به افرادی (معمولاً هکرهای کلاه سفید) که آسیب‌پذیری‌های یک برنامه را کشف می‌کنند، داده می‌شود.

هنگامی که به درستی استفاده می‌شود، پاداش‌های باگ به اعضای جامعه هکر انگیزه می‌دهد تا کد شما را از نظر نقص‌های مهم بررسی کنند. یک مثال واقعی "باگ پول بی‌نهایت" است که به مهاجم اجازه می‌دهد مقدار نامحدودی اتر را در آپتیمیزم(opens in a new tab) ایجاد کند، یک < یک پروتکل لایه که روی اتریوم اجرا می‌شود. خوشبختانه، یک هکر whitehat این نقص را کشف کرد(opens in a new tab) و به تیم اطلاع داد، کسب پرداختی بزرگ در این فرآیند انجام شد(opens in a new tab) بحرانی-اشکال-.

یک استراتژی مفید این است که پرداخت برنامه پاداش اشکال را متناسب با مقدار وجوه مورد نظر تنظیم کنید. این رویکرد به‌عنوان «اشکال مقیاس‌گذاری(opens in a new tab) توصیف می‌شود. انگیزه‌های مالی برای افراد برای افشای مسئولانه آسیب پذیری‌ها به جای سوء استفاده از آنها را ایجاد می‌کند.

5. در هنگام توسعه قراردادهای هوشمند بهترین رویه های موجود را دنبال کنید

وجود آدیت و پاداش باگ مسئولیت شما را برای نوشتن کد با کیفیت بالا توجیه نمی‌کند. امنیت قرارداد هوشمند خوب با فرآیندهای طراحی و توسعه مناسب زیر شروع می‌شود:

6. اجرای طرح‌های قوی بازیابی حوادث

طراحی کنترل‌های دسترسی ایمن، اجرای اصلاح‌کننده‌های عملکرد و سایر پیشنهادها می‌تواند امنیت قرارداد هوشمند را بهبود بخشد، اما نمی‌تواند احتمال سوء استفاده‌های مخرب را رد کند. ایجاد قراردادهای هوشمند ایمن مستلزم «آماده شدن برای شکست» و داشتن یک برنامه بازگشتی برای پاسخگویی مؤثر به حملات است. یک طرح مناسب برای بازیابی حوادث شامل برخی یا همه اجزای زیر است:

ارتقاهای قرارداد

در حالی که قراردادهای هوشمند اتریوم به طور پیش فرض تغییر ناپذیر هستند، می‌توان با استفاده از الگوهای ارتقا به درجاتی از تغییرپذیری دست یافت. به روز رسانی قراردادها در مواردی ضروری است که یک نقص مهم قرارداد قدیمی شما را غیرقابل استفاده می‌کند و به کارگیری منطق جدید امکان پذیرترین گزینه است.

مکانیسم‌های ارتقای قرارداد متفاوت عمل می‌کنند، اما «الگوی پروکسی» یکی از محبوب‌ترین رویکردها برای ارتقای قراردادهای هوشمند است. الگوهای پراکسی(opens in a new tab) منطق اجرائی و فضای ذخیره‌سازی داده‌ها را بین دو قرارداد تقسیم می‌کند. قرارداد اول (که "قرارداد پراکسی" نامیده می‌شود) متغیرهای حالت را ذخیره می‌کند (به عنوان مثال، موجودی کاربر)، در حالی که قرارداد دوم (که "منطق قرارداد" نامیده می‌شود) کد اجرای توابع قرارداد را نگه می‌دارد.

حساب‌ها با قرارداد پروکسی تعامل دارند، که همه فراخوانی‌های تابع را با استفاده از delegatecall()(opens in a new tab) به قرارداد منطقی ارسال می‌کند. تماس سطح پایین. برخلاف یک تماس پیامی معمولی، delegatecall() تضمین می‌کند که کد در حال اجرا در آدرس قرارداد منطقی در متن قرارداد فراخوانی اجرا می‌شود. یک تماس پیامی معمولی، delegatecall() تضمین می‌کند که کد در حال اجرا در آدرس قرارداد منطقی در متن قرارداد فراخوانی اجرا می‌شود.

واگذاری تماس‌ها به قرارداد منطقی مستلزم ذخیره آدرس آن در فضای ذخیره‌سازی قرارداد پروکسی است. از این رو، ارتقاء منطق قرارداد فقط به استقرار یک قرارداد منطقی دیگر و ذخیره آدرس جدید در قرارداد پروکسی بستگی دارد. از آنجایی که فراخوانی یا تماس‌های بعدی به قرارداد پروکسی به طور خودکار به قرارداد منطقی جدید هدایت می‌شوند، می‌توانید قرارداد را بدون تغییر واقعی کد «ارتقاء» می‌دادید.

اطلاعات بیشتر در مورد ارتقاء قراردادها.

توقف‌های اضطراری

همانطور که گفته شد، آدیت و آزمایش گسترده نمی‌تواند تمام اشکالات یک قرارداد هوشمند را کشف کند. اگر پس از استقرار یک آسیب‌پذیری در کد شما ظاهر شد، اصلاح آن غیرممکن است، زیرا نمی‌توانید کد در حال اجرا در آدرس قرارداد را تغییر دهید. همچنین، مکانیسم‌های ارتقا (مثلاً الگوهای پروکسی) ممکن است برای پیاده‌سازی زمان ببرد (اغلب به تأیید طرف‌های مختلف نیاز دارند)، که تنها به مهاجمان زمان بیشتری برای ایجاد آسیب بیشتر می‌دهد.

گزینه هسته‌ای اجرای یک تابع "توقف اضطراری" است که تماس‌های عملکردهای آسیب پذیر را در یک قرارداد مسدود می‌کند. توقف‌های اضطراری معمولاً شامل اجزای زیر است:

  1. یک متغیر جهانی بولی (Boolean) که نشان می‌دهد قرارداد هوشمند در حالت توقف است یا خیر. این متغیر هنگام تنظیم قرارداد روی false تنظیم می‌شود، اما پس از توقف قرارداد به true برمی‌گردد.

  2. توابعی که در اجرای خود به متغیر بولی (Boolean) اشاره می‌کنند. زمانی که قرارداد هوشمند متوقف نشده باشد، چنین عملکردهایی قابل دسترسی هستند و با فعال شدن ویژگی توقف اضطراری، غیرقابل دسترس می‌شوند.

  3. موجودی که به تابع توقف اضطراری دسترسی دارد، که متغیر Boolean را روی true تنظیم می‌کند. برای جلوگیری از اعمال مخرب، تماس‌های این تابع را می‌توان به یک آدرس مطمئن محدود کرد (به عنوان مثال، مالک قرارداد).

هنگامی که قرارداد توقف اضطراری را فعال کرد، عملکردهای خاصی قابل فراخوانی نخواهند بود. این مورد با قرار دادن توابع انتخابی در یک اصلاح کننده که به متغیر سراسری ارجاع می‌دهد، به دست می‌آید. در زیر نمونه‌ای(opens in a new tab) وجود دارد که اجرای این الگو را در قراردادها شرح می‌دهد:

1// This code has not been professionally audited and makes no promises about safety or correctness. Use at your own risk.
2
3contract EmergencyStop {
4
5 bool isStopped = false;
6
7 modifier stoppedInEmergency {
8 require(!isStopped);
9 _;
10 }
11
12 modifier onlyWhenStopped {
13 require(isStopped);
14 _;
15 }
16
17 modifier onlyAuthorized {
18 // Check for authorization of msg.sender here
19 _;
20 }
21
22 function stopContract() public onlyAuthorized {
23 isStopped = true;
24 }
25
26 function resumeContract() public onlyAuthorized {
27 isStopped = false;
28 }
29
30 function deposit() public payable stoppedInEmergency {
31 // Deposit logic happening here
32 }
33
34 function emergencyWithdraw() public onlyWhenStopped {
35 // Emergency withdraw happening here
36 }
37}
نمایش همه
کپی

این مثال ویژگی‌های اساسی توقف‌های اضطراری را نشان می‌دهد:

  • 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)، مکانیسم‌های رای‌گیری مختلف در DAOها(opens in a new tab)، و بردارهای رایج حمله DAO با استفاده از دیفای(opens in a new tab) در لینک‌های مشترک.

8. کاهش پیچیدگی کد به حداقل

توسعه دهندگان نرم‌افزار سنتی با اصل KISS ("ساده نگهش دار، احمقانه") آشنا هستند، که توصیه می کند از وارد کردن پیچیدگی های غیر ضروری در طراحی نرم‌افزار خودداری کنید. این امر متعاقب این تفکر دیرینه است که "سیستم های پیچیده به روش های پیچیده شکست می خورند" و بیشتر مستعد خطاهای پرهزینه هستند.

با توجه به اینکه قراردادهای هوشمند به طور بالقوه مقادیر زیادی از ارزش را کنترل می کنند، ساده نگه داشتن چیزها هنگام نوشتن قراردادهای هوشمند از اهمیت ویژه ای برخوردار است. نکته ای برای دستیابی به سادگی هنگام نوشتن قراردادهای هوشمند، استفاده مجدد از کتابخانه های موجود، مانند قراردادهای OpenZeppelin(opens in a new tab)، در صورت امکان است. از آنجایی که این کتابخانه ها به طور گسترده توسط توسعه دهندگان ممیزی و آزمایش شده اند، استفاده از آنها با نوشتن عملکردهای جدید از ابتدا، شانس معرفی اشکالات را کاهش می دهد.

توصیه رایج دیگر نوشتن توابع کوچک و مدولار نگه داشتن قراردادها با تقسیم منطق تجاری در چندین قرارداد است. نه تنها نوشتن کد ساده‌تر سطح حمله را در یک قرارداد هوشمند کاهش می‌دهد، بلکه استدلال درباره درستی سیستم کلی و تشخیص زودهنگام خطاهای احتمالی طراحی را آسان‌تر می‌کند.

9. دفاع در برابر آسیب‌پذیری‌های رایج قرارداد هوشمند

ورود دوباره

EVM اجازه همزمانی را نمی دهد، به این معنی که دو قرارداد درگیر در یک تماس پیام نمی توانند به طور همزمان اجرا شوند. یک فراخوانی خارجی، اجرای قرارداد و حافظه فراخوان را تا زمانی که تماس برگردد، متوقف می‌کند، در این مرحله اجرا به طور معمول ادامه می‌یابد. این فرآیند را می توان به طور رسمی به عنوان انتقال جریان کنترل(opens in a new tab) به قرارداد دیگری توصیف کرد.

اگرچه اغلب بی ضرر هستند، اما انتقال جریان کنترل به قراردادهای غیرقابل اعتماد می تواند مشکلاتی مانند ورود دوباره ایجاد کند. یک حمله ورود دوباره زمانی اتفاق می‌افتد که یک قرارداد مخرب قبل از تکمیل فراخوانی عملکرد اصلی، یک قرارداد آسیب‌پذیر را دوباره فراخوانی کند. این نوع حمله به بهترین شکل با یک مثال توضیح داده می شود.

یک قرارداد هوشمند ساده ("قربانی") را در نظر بگیرید که به هر کسی اجازه می دهد اتر را واریز و برداشت کند:

1// This contract is vulnerable. Do not use in production
2
3contract Victim {
4 mapping (address => uint256) public balances;
5
6 function deposit() external payable {
7 balances[msg.sender] += msg.value;
8 }
9
10 function withdraw() external {
11 uint256 amount = balances[msg.sender];
12 (bool success, ) = msg.sender.call.value(amount)("");
13 require(success);
14 balances[msg.sender] = 0;
15 }
16}
نمایش همه
کپی

این قرارداد یک تابع withdraw() را نشان می‌دهد تا به کاربران امکان می‌دهد ETH را که قبلاً در قرارداد سپرده شده برداشت کنند. هنگام پردازش یک برداشت، قرارداد عملیات زیر را انجام می‌دهد:

  1. تعادل اتر کاربر را بررسی می‌کند
  2. وجوه را به آدرس تماس ارسال می‌کند
  3. موجودی آنها را به 0 بازنشانی می‌کند و از برداشت اضافی از کاربر جلوگیری می‌کند

تابع withdraw() در قرارداد قربانی از الگوی "بررسی-تعامل-اثرات" پیروی می کند. که بررسی می‌کند آیا شرایط لازم برای اجرا برآورده شده است (یعنی کاربر دارای موجودی ETH مثبت است) و قبل از اعمال اثرات تراکنش (یعنی کاهش موجودی کاربر) تعامل را با ارسال ETH به آدرس تماس‌گیرنده انجام می‌دهد.

اگر withdraw() از یک حساب تحت مالکیت خارجی (EOA) فراخوانی شود، تابع همانطور که انتظار می رود اجرا می شود: msg.sender.call.value() ETH را برای تماس گیرنده ارسال می کند. با این حال، اگر msg.sender یک حساب قرارداد هوشمند باشد، withdraw() را فراخوانی می‌کند، ارسال وجوه با استفاده از msg.sender.call.value() انجام می‌شود. همچنین کدهای ذخیره شده در آن آدرس را برای اجرا راه اندازی کنید.

تصور کنید این کدی است که در آدرس قرارداد مستقر شده است:

1 contract Attacker {
2 function beginAttack() external payable {
3 Victim(victim_address).deposit.value(1 ether)();
4 Victim(victim_address).withdraw();
5 }
6
7 function() external payable {
8 if (gasleft() > 40000) {
9 Victim(victim_address).withdraw();
10 }
11 }
12}
نمایش همه
کپی

این قرارداد برای انجام سه کار طراحی شده است:

  1. پذیرش سپرده از حساب دیگری (احتمالاً EOA مهاجم)
  2. واریز یک سکه ETH به قرارداد قربانی
  3. برداشت یک سکه ETH ذخیره شده در قرارداد هوشمند

هیچ مشکلی در اینجا وجود ندارد، به جز اینکه مهاجم تابع دیگری دارد که اگر گس باقی مانده از msg.sender.call.value ورودی بیش از 40،000 باشد.ده باشد، تابع دیگری دارد که withdraw() را در قربانی دوباره فراخوانی می‌کند. این به مهاجم این امکان را می‌دهد تا قربانی را دوباره وارد کرده و وجوه بیشتری را قبل از تکمیل اولین فراخوان خروج برداشت کند. چرخه به این صورت است:

1- Attacker's EOA calls `Attacker.beginAttack()` with 1 ETH
2- `Attacker.beginAttack()` deposits 1 ETH into `Victim`
3- `Attacker` calls `withdraw() in `Victim`
4- `Victim` checks `Attacker`’s balance (1 ETH)
5- `Victim` sends 1 ETH to `Attacker` (which triggers the default function)
6- `Attacker` calls `Victim.withdraw()` again (note that `Victim` hasn’t reduced `Attacker`’s balance from the first withdrawal)
7- `Victim` checks `Attacker`’s balance (which is still 1 ETH because it hasn’t applied the effects of the first call)
8- `Victim` sends 1 ETH to `Attacker` (which triggers the default function and allows `Attacker` to reenter the `withdraw` function)
9- The process repeats until `Attacker` runs out of gas, at which point `msg.sender.call.value` returns without triggering additional withdrawals
10- `Victim` finally applies the results of the first transaction (and subsequent ones) to its state, so `Attacker`’s balance is set to 0
نمایش همه
کپی

خلاصه موضوع این است که چون موجودی تماس‌گیرنده تا زمانی که اجرای تابع کامل نشود روی 0 تنظیم نمی‌شود، فراخوانی‌های بعدی موفق خواهند شد و به تماس‌گیرنده اجازه می‌دهند تا موجودی خود را چندین بار برداشت کند. از این نوع حمله می توان برای تخلیه یک قرارداد هوشمند از وجوه آن استفاده کرد، مانند آنچه در هک DAO سال 2016(opens in a new tab) اتفاق افتاد. همانطور که فهرست‌های عمومی اکسپلویت‌های reentrancy(opens in a new tab) نشان می‌دهند، حملات reentrancy امروزه همچنان یک موضوع حیاتی برای قراردادهای هوشمند است.

چگونه از حملات بازگشت مجدد جلوگیری کنیم

یک رویکرد برای مقابله با reentrancy، پیروی از الگوی بررسی-اثرات-تعامل(opens in a new tab) است. این الگو دستور اجرای توابع را می‌دهد به گونه‌ای که کدی که بررسی‌های لازم را قبل از پیشرفت در اجرا انجام می‌دهد، ابتدا می‌آید، به دنبال آن کدی که وضعیت قرارداد را دستکاری می‌کند، کدی که با قراردادهای دیگر تعامل دارد یا EOA‌ها در آخر می‌آیند.

الگوی بررسی-اثرات-تعامل در نسخه اصلاح شده قرارداد قربانی که در زیر نشان داده شده است استفاده می شود:

1contract NoLongerAVictim {
2 function withdraw() external {
3 uint256 amount = balances[msg.sender];
4 balances[msg.sender] = 0;
5 (bool success, ) = msg.sender.call.value(amount)("");
6 require(success);
7 }
8}
کپی

این قرارداد یک بررسی در موجودی کاربر انجام می دهد، اثرات تابع withdraw() را اعمال می کند (با تنظیم مجدد موجودی کاربر به 0)، و به انجام تعامل (ارسال ETH به آدرس کاربر) ادامه می دهد. این مورد تضمین می‌کند که قرارداد قبل از تماس خارجی، فضای ذخیره‌سازی خود را به‌روزرسانی می‌کند و شرایط ورود مجدد را که اولین حمله را فعال می‌کرد، حذف می‌کند. قرارداد مهاجم همچنان می‌تواند به NoLongerAVictim برگردد، اما از آنجایی که balances[msg.sender] روی 0 تنظیم شده است، برداشت‌های اضافی با خطا مواجه می‌شوند.

گزینه دیگر استفاده از یک قفل محرومیت متقابل (که معمولاً به عنوان "mutex" توصیف می شود) است که بخشی از وضعیت قرارداد را تا زمانی که فراخوانی عملکرد کامل شود قفل می کند. این امر با استفاده از یک متغیر بولین که قبل از اجرای تابع روی true تنظیم شده است و پس از انجام فراخوانی به false برمی‌گردد پیاده‌سازی می‌شود. همانطور که در مثال زیر مشاهده می‌شود، استفاده از میوتکس از یک تابع در برابر تماس‌های بازگشتی محافظت می‌کند در حالی که فراخوان اصلی هنوز در حال پردازش است، و به طور مؤثر ورود مجدد را متوقف می‌کند.

1pragma solidity ^0.7.0;
2
3contract MutexPattern {
4 bool locked = false;
5 mapping(address => uint256) public balances;
6
7 modifier noReentrancy() {
8 require(!locked, "Blocked from reentrancy.");
9 locked = true;
10 _;
11 locked = false;
12 }
13 // This function is protected by a mutex, so reentrant calls from within `msg.sender.call` cannot call `withdraw` again.
14 // The `return` statement evaluates to `true` but still evaluates the `locked = false` statement in the modifier
15 function withdraw(uint _amount) public payable noReentrancy returns(bool) {
16 require(balances[msg.sender] >= _amount, "No balance to withdraw.");
17
18 balances[msg.sender] -= _amount;
19 bool (success, ) = msg.sender.call{value: _amount}("");
20 require(success);
21
22 return true;
23 }
24}
نمایش همه
کپی

همچنین می‌توانید به جای سیستم «پرداخت فشاری» که وجوه را به حساب‌ها ارسال می‌کند، از سیستم برگشت پرداخت‌ها(opens in a new tab) استفاده کنید که کاربران را ملزم به برداشت وجه از قراردادهای هوشمند می‌کند. با این کار امکان راه‌اندازی ناخواسته کد در آدرس‌های ناشناس حذف می‌شود (و همچنین می‌تواند از برخی حملات انکار سرویس جلوگیری کند).

پاریز و سرریز اعداد صحیح

سرریز یا اورفلو اعداد صحیح زمانی اتفاق می‌افتد که نتایج یک عملیات حسابی خارج از محدوده قابل قبول مقادیر قرار می‌گیرد و باعث می‌شود که آن را به پایین‌ترین مقدار قابل نمایش تبدیل کند. برای مثال، یک uint8 فقط می‌تواند مقادیر تا 2^8-1=255 را ذخیره کند. عملیات حسابی که به مقادیر بالاتر از 255 منجر می‌شود، سرریز یا اورفلو می‌شوند و uint را به 0 بازنشانی می‌کنند، مشابه اینکه کیلومترشمار ماشین بعد از به حداکثر رسیدن مسافت پیموده شده (999999) به 0 بازنشانی شود.

جریان‌های آندرفلو صحیح به دلایل مشابهی اتفاق می‌افتد: نتایج یک عملیات حسابی کمتر از محدوده قابل قبول است. فرض کنید سعی کرده‌اید 0 را در uint8 کاهش دهید، نتیجه به سادگی به حداکثر مقدار قابل نمایش (255) می‌رسد.

هم اورفلو و هم آندرفلو اعداد صحیح می‌تواند منجر به تغییرات غیرمنتظره در متغیرهای حالت قرارداد شود و منجر به اجرای برنامه ریزی نشده شود. در زیر مثالی وجود دارد که نشان می‌دهد چگونه یک مهاجم می‌تواند از سرریز حسابی در یک قرارداد هوشمند برای انجام یک عملیات نامعتبر سوء استفاده کند:

1pragma solidity ^0.7.6;
2
3// This contract is designed to act as a time vault.
4// User can deposit into this contract but cannot withdraw for at least a week.
5// User can also extend the wait time beyond the 1 week waiting period.
6
7/*
81. Deploy TimeLock
92. Deploy Attack with address of TimeLock
103. Call Attack.attack sending 1 ether. You will immediately be able to
11 withdraw your ether.
12
13What happened?
14Attack caused the TimeLock.lockTime to overflow and was able to withdraw
15before the 1 week waiting period.
16*/
17
18contract TimeLock {
19 mapping(address => uint) public balances;
20 mapping(address => uint) public lockTime;
21
22 function deposit() external payable {
23 balances[msg.sender] += msg.value;
24 lockTime[msg.sender] = block.timestamp + 1 weeks;
25 }
26
27 function increaseLockTime(uint _secondsToIncrease) public {
28 lockTime[msg.sender] += _secondsToIncrease;
29 }
30
31 function withdraw() public {
32 require(balances[msg.sender] > 0, "Insufficient funds");
33 require(block.timestamp > lockTime[msg.sender], "Lock time not expired");
34
35 uint amount = balances[msg.sender];
36 balances[msg.sender] = 0;
37
38 (bool sent, ) = msg.sender.call{value: amount}("");
39 require(sent, "Failed to send Ether");
40 }
41}
42
43contract Attack {
44 TimeLock timeLock;
45
46 constructor(TimeLock _timeLock) {
47 timeLock = TimeLock(_timeLock);
48 }
49
50 fallback() external payable {}
51
52 function attack() public payable {
53 timeLock.deposit{value: msg.value}();
54 /*
55 if t = current lock time then we need to find x such that
56 x + t = 2**256 = 0
57 so x = -t
58 2**256 = type(uint).max + 1
59 so x = type(uint).max + 1 - t
60 */
61 timeLock.increaseLockTime(
62 type(uint).max + 1 - timeLock.lockTime(address(this))
63 );
64 timeLock.withdraw();
65 }
66}
نمایش همه
چگونه از سرریز و آندرفلو اعداد صحیح جلوگیری کنیم

از نسخه 0.8.0، کامپایلر سالیدیتی کدهایی را که منجر به سرریز و زیر جریان یا همان آندر فلو اعداد صحیح می‌شود، رد می‌کند. با این حال، قراردادهایی که با یک نسخه کامپایلر پایین‌تر کامپایل می‌شوند باید یا باید توابع مربوط به عملیات حسابی را بررسی یا از یک کتابخانه استفاده کنند (به عنوان مثال، SafeMath(opens in a new tab)) که اورفلو یا آندرفلو را بررسی می‌کند.

دستکاری اوراکل

اوراکل‌ها اطلاعات خارج از زنجیره را منبع قرار می‌دهند و آن‌ها را به صورت زنجیره‌ای برای استفاده از قراردادهای هوشمند ارسال می‌کند. با اوراکل‌ها، می‌توانید قراردادهای هوشمندی را طراحی کنید که با سیستم‌های خارج از زنجیره، مانند بازارهای سرمایه، همکاری می‌کنند و کاربرد آن‌ها را تا حد زیادی گسترش می‌دهند.

اما اگر اوراکل خراب شود و اطلاعات نادرست را روی زنجیره ارسال کند، قراردادهای هوشمند بر اساس ورودی‌های اشتباه اجرا می‌شوند که می‌تواند مشکلاتی را ایجاد کند. این اساس "مشکل اوراکل" است که به وظیفه اطمینان از دقیق، به روز و به موقع بودن اطلاعات یک اوراکل بلاک چین مربوط می‌شود.

یک نگرانی امنیتی مرتبط، استفاده از یک اوراکل زنجیره‌ای، مانند یک صرافی غیرمتمرکز، برای دریافت قیمت ‌ای یک دارایی است. پلتفرم‌های وام‌دهی در صنعت مالی غیرمتمرکز (DeFi) اغلب این کار را برای تعیین ارزش وثیقه کاربر انجام می‌دهند تا تعیین کنند چقدر می‌توانند وام بگیرند.

قیمت‌های صرافی‌های غیرمتمرکز اغلب دقیق هستند، که عمدتاً به دلیل بازیابی برابری توسط آربیتراژها در بازارها است. با این حال، آنها در معرض دستکاری هستند، به ویژه اگر اوراکل روی زنجیره قیمت دارایی‌ها را بر اساس الگوهای معاملاتی تاریخی محاسبه کند (همانطور که معمولاً اتفاق می‌افتد).

به عنوان مثال، یک مهاجم می‌تواند به‌طور مصنوعی قیمت نقدی یک دارایی را با گرفتن وام فوری یا همان فلش لون درست قبل از تعامل با قرارداد وام شما، افزایش دهد. پرس و جو از دکس برای قیمت دارایی، ارزشی بالاتر از حد معمول را به دست می‌آورد (به دلیل تقاضای انحرافی «سفارش خرید» مهاجم برای دارایی)، به آنها اجازه می‌دهد بیشتر از آنچه باید وام بگیرند. چنین "حملات وام فلش یا همان فلش لون" برای بهره‌برداری از اتکا به اوراکل‌های قیمت در میان برنامه‌های کاربردی دیفای استفاده شده است که میلیون‌ها وجوه از دست رفته پروتکل‌ها ایجاد کرده است.

چگونه از دستکاری اوراکل جلوگیری کنیم؟

حداقل نیاز برای جلوگیری از دستکاری اوراکل(opens in a new tab) استفاده از یک شبکه اوراکل غیرمتمرکز است که پرس و جو یا اطلاعات از چندین منبع برای جلوگیری از نقاط شکست کوئری می‌کند. در بیشتر موارد، اوراکل‌های غیرمتمرکز دارای انگیزه‌های اقتصادی رمزارزی شده‌اند تا نود یا گره‌های اوراکل را تشویق کرده تا اطلاعات صحیح را گزارش کنند و آنها را از اوراکل‌های متمرکز ایمن‌تر می‌کند.

اگر قصد دارید از یک اوراکل زنجیره‌ای یا آنچین برای قیمت دارایی‌ها پرس و جو کنید، از یکی استفاده کنید که مکانیزم قیمت میانگین وزن شده با زمان (TWAP) را پیاده‌سازی می‌کند. یک اوراکل TWAP(opens in a new tab) قیمت یک دارایی را در دو مقطع زمانی مختلف (که شما می‌توانید اصلاح کنید) و قیمت لحظه‌ای را بر اساس میانگین به دست آمده محاسبه می‌کند. انتخاب دوره‌های زمانی طولانی‌تر از پروتکل شما در برابر دستکاری قیمت محافظت می‌کند، زیرا سفارش‌های بزرگی که اخیراً اجرا شده‌اند نمی‌توانند بر قیمت دارایی تأثیر بگذارند.

منابع امنیتی قرارداد هوشمند برای توسعه‌دهندگان

ابزارهایی برای تجزیه و تحلیل قراردادهای هوشمند و تأیید صحت کد

ابزارهای نظارت بر قراردادهای هوشمند

ابزارهایی برای مدیریت امن قراردادهای هوشمند

خدمات حسابرسی قرارداد هوشمند

پلتفرم‌های باگ‌بانتی

رسانه های آسیب پذیری ها و اکسپلویت های شناخته شده قرارداد هوشمند

چالش‌های یادگیری امنیت قراردادهای هوشمند

بهترین روش ها برای ایمن سازی قراردادهای هوشمند

آموزش امنیت قراردادهای هوشمند

  • نحوه نوشتن قراردادهای هوشمند امن

  • نحوه استفاده از Slither برای یافتن اشکالات قرارداد هوشمند

  • نحوه استفاده از Manticore برای یافتن اشکالات قرارداد هوشمند

  • راهنمای امنیتی قراردادهای هوشمند

  • چگونه به طور ایمن قرارداد توکن خود را با توکن‌های دلخواه ادغام کنیم

  • Cyfrin Updraft - امنیت قراردادهای هوشمند و دوره کامل ممیزی(opens in a new tab)

آیا این مقاله مفید بود؟