در این مقاله نگاهی به طرز کار ماشین مجازی اتریوم میاندازیم و الگوهایی را که باید در طراحی و توسعه یک قرارداد هوشمند اتریوم (Ethereum Smart Contract) رعایت کرد بررسی میکنیم. این مقاله بیشتر مناسب توسعهدهندگان اتریوم با سطح متوسط است. توصیه میشود افرادی که به تازگی وارد این عرصه شدهاند، ابتدا مسائل سطح پایینتر را مطالعه کنند تا بتوانند به درک مناسبی از موضوع توسعه قرارداد هوشمند اتریوم برسند.
یک توسعهدهنده آگاه اتریوم همیشه باید این پنج اصل را رعایت کند:
- آمادگی برای شکست
- دقت در راهاندازی
- حفظ سادگی قراردادها
- به روز بودن
- آگاهی از ویژگیهای ماشینهای مجازی اتریوم
بحث اول، تماسها یا درخواستهای خارجی در قرارداد هوشمند اتریوم هستند که در زیر به توضیح حالتهای مختلف آن میپردازیم.
در قرارداد هوشمند اتریوم ، همیشه هنگام ایجاد یک تماس خارجی احتیاط کنید
تماسهای خارجی با یک قرارداد هوشمند اتریوم همیشه ممکن است خطرات یا ریسکهای پیشبینی نشدهای به همراه داشته باشد.
این تماسها میتوانند کدهای مخربی را روی قرارداد هوشمند اتریوم ما و همچنین قراردادهای مرتبط با آن اجرا کنند. بنابراین هر تماس خارجی را به عنوان یک خطر امنیتی احتمالی در نظر بگیرید.
زمانی که مجبورید از تماسهای خارجی استفاده کنید، از توصیههایی که در ادامه اشاره میکنیم استفاده کنید تا احتمال خطر را کاهش دهید.
۱. قراردادهای نامطمئن را علامتگذاری کنید
هنگام تعامل با قراردادهای خارجی، متغیرها و متدها و رابطهای قرارداد را طوری نامگذاری کنید که کاملا مشخص باشد که ارتباط با آنها نامطمئن است.
۲. بعد از تماسهای خارجی تغییر وضعیت ایجاد نکنید
هم در زمان استفاده از raw calls با فرمت ()someAddress.call و هم در زمان استفاده از contract calls با فرمت ()ExternalContract.someMethod، حواستان به کدهای مخرب باشد. حتی اگر ExternalContract کد مخرب نباشد، هر قراردادی که توسط آن call میشود، میتواند یک کد مخرب را اجرا کند.
خطر دیگری که وجود دارد این است که یک کد مخرب میتواند کنترل جریان را در دست بگیرد و با هدایت آن به سمت ورود مجدد، باعث آسیبپذیری سیستم شود.
اگر میخواهید با یک قرارداد خارجی نامطمئن تماس برقرار کنید، از تغییر وضعیت سیستم بعد از آن پرهیز کنید. به این مساله، “الگوی چک کردن تاثیر تعاملات” هم گفته میشود.
۳. از ()transfer و ()send استفاده نکنید
این دو دستور دقیقا ۲۳۰۰ گس را به گیرنده ارسال میکنند. هدف تخصیص این مقدار گس، جلوگیری از آسیبپذیری در مقابل ورود مجدد است و تنها زمانی منطقی است که هزینه گس کاملا ثابت باشد. EIP 1884 که بخشی از هارد فورک استانبول بود، گس مربوط به عملیات SLOAD را افزایش داد و باعث شد عملیات برگشت قرارداد بیش از ۲۳۰۰ گس هزینه داشته باشد. پیشنهاد ما استفاده از ()call به جای ()transfer و ()send است.
یادتان باشد که ()call هیچ ربطی به کاهش حملات ورود مجدد ندارد، پس اقدامات پیشگیرانه دیگری را باید در نظر بگیرید. برای جلوگیری از چنین حملاتی از الگوی چک کردن تاثیر تعاملات استفاده کنید.
۴. اشکالات تماسهای خارجی را برطرف کنید
Solidity متدهای تماس سطح پایین را برای کار با آدرسهای خام ارائه میدهد؛ مثل: ()address.call و ()address.callcode و ()address.delegatecall و ()address.send.
این متدها هیچوقت استثنا قائل نمیشوند؛ اما اگر تماس با یک استثنا مواجه شود، غلط (false) میشوند. از طرف دیگر contract call ها به طور خودکار عمل میکنند. (برای مثال ()ExternalContract.doSomething در صورت عمل کردن ()doSomething بطور خودکار عمل خواهد کرد.)
اگر میخواهید از متدهای تماس سطح پایین استفاده کنید، با چک کردن مقدار بازگشت داده شده مطمئن شوید که برای زمان fail شدن تماس، راهحلی داشته باشید.
۵. بیشتر توجه به سمت تماسهای خروجی است تا ورودی
تماسهای خروجی ممکن است به صورت سهوی یا به صورت عمدی fail شوند. برای به حداقل رساندن آسیبها در زمان رخ دادن این اتفاق، بیشتر مواقع بهتر است که هر تماس را در تراکنش مربوطه ایزوله کنیم و این کار میتواند توسط گیرنده تماس شروع شود.
این روش بیشتر به پرداختها مربوط میشود، یعنی بهتر است اجازه دهیم کاربران خودشان درخواست برداشت را انجام دهند تا این که به صورت خودکار برایشان انجام شود. ( این روش همچنین مشکلات احتمالی محدودیت گس را هم کاهش میدهد.) همچنین چند تراکنش اتر را در یک تراکنش با هم ترکیب نکنید.
۶. هیچ تماسی را به کد نامطمئن واگذار نکنید
واگذاری تماس یا همان delegatecall، عملیاتی است که تماسها را از قراردادهای مرتبط دیگر فراخوانی میکند. بنابراین تماس فراخوانده شده شاید وضعیت آدرس فراخوانی را تغییر دهد. اینجای کار ممکن است امن نباشد. مثال زیر نشان میدهد که چگونه یک delegatecall می تواند منجر به تخریب و از بین رفتن تمام موجودی شود.
اگر ()Worker.doWork با یک آدرس مخرب فراخوانی شود، قرارداد Worker خودش را از بین خواهد برد. عملیات واگذاری را فقط به قراردادهای قابل اطمینان انجام دهید و هیچوقت از آدرسهای عرضه شده استفاده نکنید.
هشدار
موجودی قراردادهای جدید را همیشه صفر در نظر نگیرید. یک هکر میتواند قبل از ایجاد یک قرارداد هوشمند اتریوم ، مقداری اتر به آن بفرستد. به همین علت مقدار اولیه قراردادها نباید به طور پیش فرض صفر حساب شوند.
در این قسمت از مقاله میخواهیم راجع به این صحبت کنیم که ممکن است مقداری اتر به زور به یک حساب فرستاده شود.
۷. یادتان باشد که یک اسکریپت ثابت بنویسید که همیشه موجودی یک قرارداد را چک کند
یک هکر میتولند به هر حسابی به زور اتر ارسال کند و نمیتوان جلوی این موضوع را گرفت. (حتی با عملیات fallback که ()revert را انجام دهد هم نمیشود از این اتفاق جلوگیری کرد.)
این هکر عزیز میتواند قراردادی با موجودی خیلی کم ایجاد کرده و بعد شروع به فراخوانی self-destruct کند (victimAddress). هیچ کدی در victimAddress فراخوانی نمیشود پس قابل پیشگیری هم نخواهد بود. این مساله در مورد پاداش بلاک هم صدق میکند که به آدرس ماینر ارسال میشود و این آدرس، میتواند هر آدرس دلخواهی باشد.
همچنین از آنجایی که آدرس هر قرارداد هوشمند اتریوم را میتوان از قبل محاسبه کرد، دیگران میتوانند قبل از این که قرارداد پیاده سازی شود، به آن ارسال داشته باشند.
۸. یادتان باشد که دادههای روی زنجیره کاملا عمومی هستند
خیلی از برنامهها به دادههای تایید شده نیاز دارند تا بتوانند در زمان مشخص به طور خصوصی کار کنند. دو دستهبندی مهم از این برنامهها، بازیها (برای مثال سنگ کاغذ قیچی روی شبکه) و مکانیزمهای مزایدهای (مثل مزایدههای Vickrey با قیمت پیشنهادی مشخص) هستند.
اگر در حال ساخت اپلیکیشنی هستید که حریم خصوصی در آن اهمیت دارد، مراقب باشید که زود از کاربران نخواهید که اطلاعاتشان را برای عموم به اشتراک بگذارند. بهترین راهکار استفاده از طرحهای متعهدسازی در مراحل جداگانه است: ابتدا تعهد به استفاده از اطلاعات هش شده و در مرحله بعدی منتشر کردن اطلاعات.
چند مثال:
- در بازی سنگ کاغذ قیچی، ابتدا لازم است هر دو بازیکن هش مربوط به حرکت مورد نظر خود را ثبت کنند. اگر حرکت انجام شده با هش مطابقت نداشته باشد، دور ریخته میشود.
- در یک مزایده، در مرحله اولیه، دو بازیکن باید هش مربوط به درخواست خرید خود را ثبت کنند (و این که موجودی حسابشان از درخواست خریدشان بیشتر باشد) و در مرحله دوم درخواست خرید را در مزایده ثبت کنند.
- برای توسعه یک اپلیکیشن که به تولید اعداد تصادفی وابسته است، همیشه باید از این ترتیب استفاده شود.
۱. بازیکنان حرکت را ثبت میکنند.
۲. عدد تصادفی تولید میشود.
۳. بازیکنها پرداخت میکنند.
خیلی از مردم درباره تولیدکنندههای اعداد تصادفی تحقیق میکنند. بهترین گزینههای موجود برای این کار در حال حاضر Bitcoin block headers (تایید شده از طریق وب سایت btcrelay.org) و hash-commit-reveal schemes (که ابتدا یک عدد تولید کرده و هش آن را منتشر میکند و ارزش آن را بعدا نمایش میدهد) و RANDAO هستند.
از آنجا که اتریوم یک پروتکل قطعی است، نمیتوانید از هیچ متغیری به عنوان عدد تصادفی غیر قابل پیشبینی در این پروتکل استفاده کنید. همچنین دقت کنید که ماینرها هم بخشی از کنترل block.blockhash() value* را در اختیار دارند.
۹. توجه کنید که ممکن است برخی کاربران ناگهان آفلاین شوند و مجدد باز نگردند
فرآیندهای بازپرداخت و یا درخواست را بدون اینکه راه دیگری برای برداشت سرمایهها وجود داشته باشد، به یک عضو با عملکرد خاص وابسته نکنید. مثلا در بازی سنگ کاغذ قیچی، یکی از اشتباهات رایج این است که تا هر دو بازیکن حرکت خود را ثبت نکرده باشند، پرداختی صورت نگیرد؛ اگرچه یک بازیکن مخرب میتواند هیچوقت حرکت خود را ثبت نکند و همیشه حریف را عذاب دهد.
در حقیقت اگر یک بازیکن، بتواند حرکت حریف را ببیند و متوجه شود که خواهد باخت، اصلا دلیلی ندارد که حرکت خود را ثبت کند. این مساله در بحث توافق کانال وضعیت هم رخ میدهد. در چنین مواقعی میتوانید:
۱. بازیکنهایی که در بازی شرکت نمیکنند را به نوعی گیر بیندازید، مثلا با یک محدودیت زمانی.
۲. یک انگیزه اقتصادی دیگر برای شرکتکنندهها برای ثبت اطلاعات در تمام شرایط در نظر بگیرید.
۱۰. مراقب منفیترین عدد علامتدار باشید
در زبان سالیدیتی به روشهای مختلفی میتوان با اعداد صحیح علامتدار کار کرد. مثل بیشتر زبانهای برنامهنویسی، یک عدد صحیح علامتدار میتواند نشاندهنده اعداد در بازه -2^(N-1) to 2^(N-1)-1 باشد. یعنی هیچ مقدار مثبتی برای MIN_INT وجود ندارد.
منفیسازی، از طریق پیداکردن مکمل دوِ یک عدد پیادهسازی میشود. پس منفیِ منفیترین عدد باید با خود آن عدد برابر باشد. این مطلب در مورد تمام اعداد صحیح در سالیدیتی صدق میکند.
یکی از راههای انجام این کار، چک کردن ارزش یک متغییر قبل از منفیسازی است و اگر با MIN_INT برابر شد، باید دور ریخته شود. راه دیگر این است که ظرفیت را طوری در نظر بگیریم که مطمئن باشیم هیچوقت منفیترین عدد به دست نخواهد آمد.
چنین مشکل مشابهی هم در اعداد صحیح زمانی که MIN_INT در ۱- ضرب یا به آن تقسیم شود، رخ خواهد داشت.
امیدواریم با مطالعه نکات گفته شده در این مقاله، دید بهتری نسبت به برخی مشکلات رایج در توسعه یک قرارداد هوشمند اتریوم پیدا کرده باشید و بتوانید هر چه بیشتر و سریعتر در این حوزه جدید فناوری بلاک چین بدرخشید.