چگونه از نابودی سرمایه در قراردادهای هوشمند جلوگیری کنیم؟ (بخش اول)

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

0 103

با ظهور ارزهای رمزنگاری شده‌‌ مبتنی بر بلاکچین، قراردادهای هوشمند هم به طرز شگفت‌‌انگیزی محبوب شدند.

google trends

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

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

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

این مقاله بر اساس سخنرانی فوق‌‌العادهLeonid Beder در آکادمی بلاکچین با عنوان “چگونه میلیون‌‌ها دلار را نابود نکنیم” نوشته شده است. من شدیدا توصیه می‌‌کنم که سخنرانی او و باقی محتوای موجود در کانال یوتیوبی اکوسیستم کین را گوش دهید.

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

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

حالا بیایید این تعریف را کلمه به کلمه بشکافیم.

قرارداد

مثالی برای قرارداد مشترک، قرارداد فروش خواهد بود. فروشنده قول می‌‌دهد برای خریدار، کالا مهیا کند. خریدار قول می‌‌دهد تا قیمت مطلوبی پرداخت کند.

قالب دیجیتالی

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

پروتکل‌‌ها

به عنوان مثال، فرض کنید طرفین موافقت کنند که کالای خریداری‌‌شده در قالب بیت کوین پرداخت شود. پروتکل انتخابی واضح در این حالت، پروتکل بیت کوین خواهد بود.

هوشمند

این قسمت بسیار جذاب به نظر می‌‌رسد: بکارگیری زبان برنامه‌‌نویسی قرارداد هوشمند.

اتریوم

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

بیت کوین

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

  • حساب‌‌های چند امضایی
  • کانال‌‌های پرداخت
  • حساب‌‌های امانی
  • تایم لاک‌‌ها
  • خرید و فروش خارج از زنجیره اتمی
  • اوراکل‌‌ها

سالیدیتی (Solidity) به صورت مثال

سالیدیتی یک زبان OOP برای نوشتن قراردادهای هوشمند در اتریوم است. ما با یک مثال ساده از قراردادهای هوشمند با عنوان گریتر (Greeter) آغاز خواهیم کرد که این موارد را انجام خواهد داد:

  • با یک پیام خوشامدگویی (مثل سلام دنیا!) آغاز می‌‌کند.
  • روشی برای خواندن یا پرس و جوی پیام خوشامدگویی ارائه می‌‌کند.
  • روشی برای نوشتن یا تغییر پیام خوشامدگویی ارائه می‌‌کند.
  • برای این مثال‌‌ها ما به وبسایت آنلاین Remix IDE با آدرس @ https://remix.ethereum.org نگاهی خواهیم داشت.

solidity

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

سالیدیتی ترکیب مشابهی با JS دارد. ما قراردادی با عنوان گریتر را تعریف می‌‌کنیم. به علاوه ما یک حالت عمومی متغیر با عنوان گریتینگ و دو روش دیگر داریم (سازنده و setGreeting)

موارد اساسی: مدل اجرایی

  • تمام قراردادهای هوشمند باید پیش از باز شدن در شبکه، به کد بایتی جمع‌‌آوری شوند (کامپایل شوند). باز شدن به معنای ذخیره شدن در شبکه اتریوم است. این مسئله می‌‌تواند تولید، آزمایش یا شبکه خصوصی باشد.
  • تمام تراکنش‌‌ها یا پیام‌‌ها در اتریوم توسط ماشین مجازی اتریوم (EVM) در نودهای اتریوم انجام می‌‌شود (استخراج‌‌کننده یا فقط یک نود کامل).
  • از آنجایی که تمام قراردادهای هوشمند بر روی EVM اجرا می‌‌شوند و تمام عملیات در هر نود شبکه به صورت همزمان انجام می‌‌شود، باید مکانیسمی برای محدود کردند منابع مورد استفاده هر قرارداد وجود داشته باشد. مکانیسمی که در اتریوم منابع را محدود می‌‌کند، gas است.
  • هر عملیات اجرایی، یک هزینه قطعی دارد که با واحد gas اندازه‌‌گیری می‌‌شود. تعیین هزینه gas سخت خواهد بود، زیرا گردش کنترل شرطی ممکن است عملیات‌‌های اجرایی در زمان اجرا را تغییر دهد. این برنامه را در نظر بگیرید:

اگر (شرایط صحیح باشد){

// کاری بسیار آسان و از نظر gas بسیار ارزان انجام دهید

}در غیر این صورت{

کاری بسیار پیچیده و بسیار گران انجام دهید!

}

  • هر واحد gas مصرفی توسط یک تراکنش، باید در قالب اتر پرداخت شود و این مسئله بر اساس قیمت gas به اتر می‌‌باشد که توسط فرستنده تراکنش تعیین می‌‌شود. فرستنده مبلغی که مایل به پرداخت آن می‌‌باشد را تعیین می‌‌کند. این مسئله به زمان یا اهمیت کار شما بستگی دارد، و هر چقدر اهمیت یا اضطرار کار شما بالاتر باشد، باید مبلغ بیشتری پرداخت کنید.
  • تراکنش‌‌ها یک پارامتر محدودکننده gas هم دارند که حداکثر gas یک تراکنش را تعیین می‌‌کند.
  • چرا اصلا به پارامتر gas نیاز داریم؟ ما یک حداکثر b/c داریم که همیشه هزینه اجرای تراکنش‌‌ها را به طور دقیق مشخص نمی‌‌کند و ما می‌‌خواهیم بدانیم نهایت اتری که باید خرج کنیم چقدر است! بیایید قرارداد را به Remix IDE آپلود کنیم و بعد از ساخت آن را گسترش دهیم.

pic

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

وقتی که قرارداد باز شد، ما آن را در بخش قرارداداهی باز شده در کادر می‌‌بینیم (فلش را ببینید!). توجه کنید که ما به سادگی می‌‌توانیم شناسه سازنده را تغییر داده و شبکه را دوباره گسترش دهیم.

solidity

موارد اساسی: عملکردها و متغیرهای اختصاصی

عملکردها و متغیرهای اختصاصی همیشه در فضای نام وجود دارند. اینجا درباره این موارد صحبت خواهیم کرد:

  • msg.sender  (آدرس اتریوم): فرستنده پیام (تماس فعلی). این بخش به تماس گیرنده اصلی تراکنش تعلق دارد. این بخش می‌‌تواند یک حساب کاربری یا یک آدرس قرارداد هوشمند دیگر باشد. مدل امنیتی اتریوم تضمین می‌‌کند که msg.sender نتواند تقلبی باشد.
  • msg.value  (واحد): تعداد wei ارسالی همراه با پیام. Wei کوچکترین واحد اتر است (سنت در برابر دلار را فرض کنید). پس این بخش، میزان اتری را که ما برای خرید، تراکنش و غیره ارسال می‌‌کنیم، مشخص می‌‌کند.

موارد اساسی: عملکردها و حالت قابلیت دید متغیرها

در سالیدیتی، چهار نوع قابلیت دید داریم:

برای عملکردها:

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

برای متغیرهای حالت:

عمومی: در دسترس تمامی افراد قرار دارد. سالیدیتی به صورت اتوماتیک یک گیرنده برای متغیرهای عمومی ایجاد می‌‌کند.

داخلی (پیش‌‌فرض): می‌‌تواند به صورت داخلی و فقط از خود قرارداد قابل دسترسی باشد.

خصوصی: می‌‌تواند به صورت داخلی و فقط از خود قرارداد قابل دسترسی باشد.

موارد اساسی: تعدیل‌‌کننده‌‌های عملکرد

تعدیل‌‌کننده‌‌ها را می‌‌توان برای تعدیل و تغییر رفتار عملکردها به کار برد. این مسئله به ما اجازه می‌‌دهد تا پیش از احضار تابع (invocation of the function)، هنگام یا بعد از آن بتوانیم عملکرد اضافه کنیم (مثلا واقعه‌‌نگاری یا Logging)

تعدیل‌‌کننده‌‌ها در باقی زبان‌‌های برنامه‌‌نویسی به (before/after/around/hooks) بسیار شبیه هستند. به عنوان مثال، بیایید قرارداد هوشمند گریتر را بررسی و تقویت کنیم:

  • یک مالک داشته باشد (در مثال ما، گسترش‌‌دهنده قرارداد هوشمند)
  • مطمئن شوید که فقط مالک می‌‌تواند خوشامدگویی را تغییر دهد.

ما می‌‌خواهیم این الگو را چندین بار، هم در مثال‌‌های پیش رو و هم در دنیای واقعی، مشاهده کنیم. به علاوه، بیایید یک شرط اضافه کنیم که می‌‌گوید فقط مالک می‌‌تواند خوشامدگویی را تغییر دهد.

solidity

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

خط رسم شده، معادل یک سود (بازدهی) است. این یعنی در این مکان باید کد باقی مانده عملکرد را اجرا کرد.

موارد اساسی: عملکرد عقب‌‌نشینی

قرارداد هوشمند می‌‌تواند دقیقا یک عملکرد بدون اسم (بدون منبع) داشته باشد.

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

موارد اساسی: تعدیل‌‌کننده قابل‌‌پرداخت

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

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

اگر چنین عملکردی وجود نداشته باشد، قرارداد نمی‌‌تواند با تراکنش‌‌های عادی، اتر دریافت کند.

مثال رویدادها

این مسئله یک راه تفننی (جالب) برای انجام واقعه‌‌نگاری (logging) در اتریوم است.

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

همچنین ما یک عملکرد با عنوان اهدا داریم که قابل‌‌پرداخت است (ما تعدیل‌‌کننده قابل‌‌پرداخت را اضافه کردیم). همچنین ما عملکردی با عنوان چرخش (troll) داریم که قابل‌‌پرداخت نیست، بنابراین ارسال اتر در این حالت با شکست مواجه می‌‌شود (تعدیل‌‌کننده قابل‌‌پرداخت وجود ندارد).

solidity

بیایید این قرارداد را به رمیکس گسترش دهیم تا بتوانیم نتایج ارسال پول از طریق هر کدام از این روش‌‌ها را تایید کنیم.

solidity

در فایل متحرک بالا، می‌‌توانیم ببینیم که استناد به روش اهدا، سبب ایجاد تراکنشی می‌‌شود که ارزش انتقالی 5 wei دارد و وقتی که به روش چرخش (troll) استناد می‌‌کنیم، هیچ پولی منتقل نمی‌‌شود.

بیایید کمی اتر تخریب کنیم!

solidity

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

آیا کسی با این مسئله دچار مشکل شده؟

توجه داشته باشید که وقتی ما مسیر پاداش را اعلام می‌‌کنیم، منظور ما مسیری از آدرس به سمت عدد صحیح بدون علامت (unsigned integer) است.

solidity

اینجا ما بدون بررسی اینکه آیا سرمایه‌‌های ضروری را در اختیار داریم یا نه، اقدام به تفریق می‌‌کنیم! بنابراین تمام حالاتی که ما در آن به یک مقدار منفی در بخش پاداش می‌‌رسیم، منجر به یک پاریز (underflow) خواهد شد.

خارج از این مسئله: پاریز میزانی منفی را به یک مقدار کاملا مثبت تبدیل خواهد کرد. لینک زیر را برای توضیح مفصل درباره نحوه عمل پاریز، بررسی کنید.

این مثال را در نظر بگیرید. یک پاداش 500 واحدی به دست آمده‌‌است. حالا هکر ما می‌‌خواهد 1000 واحد به جیب بزند. روی کاغذ و به صورت تئوری، این مسئله نباید ممکن باشد، زیرا که ما فقط 500 واحد داریم. بعد از اجرای این تراکنش، مدعی (the claimer) میزان نامحدودی سکه خواهد داشت!

باگ‌‌ها: سرریز / پاریز (Overflow / Underflow)

  • سالیدیتی می‌‌تواند تا اعداد 256 بیتی را مدیریت کند.
  • سرریز هنگامی است که عددی بیشتر از ارزش و میزان حداکثری‌‌اش، افزایش یابد. بنابراین اضافه کردن 1 به 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF نتیجه 0 خواهد داشت.
  • پاریز دقیقا عکس این مسئله است. هنگامی که یک عدد بدون علامت است، کاهش باعث پاریز شدن عدد می‌‌شود. این مسئله به حالت دیگر هم ممکن خواهد بود، جایی که تفریق 1 از 0x00000000000000000000 نتیجه 0xFFFFFFFFFFFFFFFFFFFFF خواهد داشت.

Mitigation 1: صحت را آزمایش کنید

پیش از انجام هر کاری، صحت را آزمایش کنید. یک تغییر یا اصلاح سریع برای این موضوع، استفاده از بررسی و تایید این مسئله است که ادعا (claim یا تلاش) کوچکتر از تراز حساب فعلی است.

solidity

Mitigation 2: ریاضیات مطمئن (SafeMath)

  • همیشه از کتابخانه (برنامه) SafeMath استاندارد (de-facto) استفاده کنید.
  • قراردادهای هوشمند، مناسب و نسبتا پایدار در صندوق Github متعلق به OpenZeppelin بیابید.
  • کاری که کتابخانه (برنامه) می‌‌کند، انجام عملیات محاسباتی ریاضی به گونه‌‌ای ایمن ست تا اگر یک سرریز رخ داد، تراکنش برگشت بخورد. دفاع کردن (Assert) هم شباهت زیادی با بازگشت (revert) دارد و سبب بازگشت تراکنش بد می‌‌شود.
  • در مثال زیر ما SafeMath را برای استفاده روی unit256 وارد کرده‌‌ایم و بنابراین کتابخانه تمام تراکنش‌‌هایی که سبب پاریز در خط 30 می‌‌شود را بازگشت خواهد داد.

solidity

تنظیم قابلیت دید عملکرد

solidity

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

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

داستان هک (حالت هک):

آلیس 1000 wei برای این مثال واریز می‌‌کند. باب سعی می‌‌کند برداشت کند، اما موفق نمی‌‌شود، زیرا که مالک این خیریه نیست. حالا باب درخواست setOwner داده و مالک را به خود تغییر می‌‌دهد. باب تمام سرمایه را بر می‌‌دارد. تمام این مسائل به خاطر این است که شخصی فراموش کرده قابلیت دید را تعریف کند!

Mitigation 3: همیشه قابلیت دید تعریف کنید!

  • همیشه قابلیت دید تعریف کنید.
  • در صورتی که ممکن است، عملکرد قابلیت دید را محدود کنید.

نتیجه‌‌گیری

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

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

اگر به دنبال منابعی برای افزایش اطلاعات خود از دنیای بلاکچین هستید، من شدیدا به شما توصیه می‌‌کنم که کانال یوتیوبی اکوسیستم کین (Kin Ecosystem) را بررسی کنید. در این کانال اطلاعات فوق‌‌العاده‌‌، بحث‌‌های فنی و جلسات آموزشی با کیفیتی وجود دارد. با تشکر فراوان از لنوید بدر (Leonid Beder ) برای تولید این جلسه و آموزش آن در آکادمی بلاکچین!

برای مطالعه بخش دوم اینجا کلیک کنید.

شاید از این مطالب هم خوشتان بیاید.

ارسال پاسخ

آدرس ایمیل شما منتشر نخواهد شد.