چگونه با زبان برنامهنویسی سالیدیتی امنیت قرارداد های هوشمند را برقرار کنیم؟! ، به گزارش آکادمی ریبیز Reybiz ، در این مقاله به بیان نکات مهم، از نظارت تا موارد مرتبط با برچسب زمانی، میپردازیم تا اطمینان حاصل کنید که قرارداد هوشمند اتریوم شما به خوبی و مستحکم بنا نهاده شده است.
چگونه با زبان برنامهنویسی سالیدیتی امنیت قرارداد های هوشمند را برقرار کنیم؟
اگر با امنیت قرارداد های هوشمند آشنا باشید و شناخت کافی از سازوکار ماشین مجازی اتریوم (EVM) داشته باشید، وقت آن رسیده است که به بعضی از الگوهای امنیتی دقت کنید که مختص زبان برنامهنویسی سالیدیتی هستند.
در این مقاله به بیان توصیههای توسعه ایمن در سالیدیتی خواهیم پرداخت که میتوانند در زمینه توسعه قرارداد های هوشمند در سایر زبانهای برنامهنویسی آموزنده باشند.
از توابع assert و require میتوان برای بررسی شرایط و ایجاد استثنا در صورت محقق نشدن شرایط استفاده کرد.
تابع assert در سالیدیتی فقط باید برای آزمایش خطاهای داخلی و بررسی مقادیر ثابت استفاده شود.
تابع require باید برای اطمینان از شرایط معتبر نظیر ورودیها یا شرایط قرارداد که متغیرها با آن مواجه میشوند یا برای تایید مقادیر بازگشتی به قراردادها استفاده کرد.
پیروی از این الگو به ابزارهای تحلیل رسمی امکان میدهد تا بررسی کنند که هرگز آپکد (opcode یا همان کدهای عملیاتی) نامعتبر حاصل نشود. این موضوع بدان معنا است که هیچ مقدار ثابتی در کد نقض نشود و کد به صورت رسمی بررسی و تایید شود.
استفاده از مادیفایر (Modifier) فقط برای بررسیهای لازم
کد درون مادیفایر معمولا قبل از متن اصلی تابع اجرا میشود، بنابراین هر تغییر وضعیت یا فراخوانی خارجی باعث اختلال و نقض الگوی تعاملات بررسیها و تاثیرات (Checks-Effects-interactions pattern) خواهد شد. به علاوه، ممکن است توسعهدهنده متوجه این گزارهها نشود، زیرا کد مادیفایر میتواند فاصله زیادی با فرض تابع داشته باشد. برای مثال، یک فراخوانی خارجی در مادیفایر میتواند منجر به حمله reentrancy شود:
در این مورد، قرارداد registry میتواند با فراخوانی ()Election.vote درون ()isVoter، حمله reentrancy ایجاد کند.
نکته: از مادیفایر برای جایگزین کردن بررسیهای شرایط دوگانه در توابع چندمنظوره نظیر ()isOwner استفاده کنید، در غیر این صورت از require یا revert درون تابع استفاده کنید. این موضوع باعث میشود که کد قرارداد هوشمند خوانشپذیرتر و حسابرسی آن آسانتر شود.
مراقب گرد کردن تقسیم عدد صحیح باشید
تمام تقسیمهای اعداد صحیح به سمت نزدیکترین عدد صحیح گرد میشوند. اگر دقت بیشتری نیاز دارید، استفاده از ضریکننده یا ذخیره صورت و مخرج کسر را مدنظر قرار دهید.
استفاده از ضربکنندهای که مانع از گرد کردن به سمت پایین میشود، باید هنگام کار کردن با x مدنظر قرار بگیرد:
ذخیره صورت و مخرج کسر بدان معنا است که میتوانید حاصل صورت بر مخرج را به صورت برون زنجیرهای محاسبه کنید:
مراقب نکات مثبت و منفی قراردادهای ابسترکت (Abstract) و رابط کاربری باشید
رابط کاربری و قرارداد ابسترکت هردو رویکردی قابل شخصیسازی و قابل استفاده مجدد را برای قرارداد های هوشمند ارائه میدهند. رابطهای کاربری (Interface) که در نسخه ۰.۴.۱۱ سالیدیتی معرفی شدند، همانند قراردادهای ابسترکت هستند، با این تفاوت که نمیتوانند هیچگونه تابعی اجرا کنند. رابطهای کاربری همچنین محدودیتهایی نظیر عدم توانایی دسترسی به فضای ذخیرهسازی یا استفاده از سایر رابطهای کاربری دارند. این ویژگیها باعث میشوند که قراردادهای ابسترکت، کاربردیتر شوند. هرچند رابطهای کاربری برای طراحی قراردادها قبل از اجرای آنها مفید هستند. به علاوه، این نکته را باید به خاطر داشته باشیم که اگر قراردادی از قرارداد ابسترکت حاصل شود، باید تمام توابع اجرانشده را از طریق اورراید کردن (Override) اجرا کند، در غیر این صورت آن قرارداد نیز ابسترکت خواهد بود.
به گزارش وبسایت کوین تلگراف، دوج کوین در بازه تاریخ می ۲۰۲۱ تا فوریه ۲۰۲۲ حدود ۸۵ درصد از ارزش خود را از دست داد که این مسئله موجب نگرانی سرمایهگذاران این رمزارز شده بود.
توابع فالبک
حال در اینجا به بررسی توابع فال بک در سالیدیتی میپردازیم
سادهسازی توابع فالبک
توابع فالبک (Fallback function) هنگامی فراخوانی میشوند که یک قرارداد بدون هیچگونه استدلالی، پیام ارسال کند و هنگامی که از ()send. یا ()transfer. فراخوانی میشود فقط به ۲۳۰۰ گس دسترسی دارد. اگر میخواهید بتوانید از ()send. یا ()transfer. اتر دریافت کنید، بیشترین کاری که میتوانید در تابع فالبک انجام دهید، ثبت سابقه رویداد است. اگر به گس بیشتری نیاز دارید از تابع مناسب استفاده کنید.
بررسی طول دیتا در توابع فالبک
از آنجایی که توابع فالبک فقط برای انتقال اتر فراخوانی نمیشوند، اگر قرار است تابع فالبک فقط برای ثبت دریافت اتر استفاده شود باید خالی بودن دیتا را بررسی کنید. در غیر این صورت، فراخوانها (caller) متوجه نخواهند شد که قرارداد شما به درستی استفاده شده است یا خیر و توابعی که وجود ندارند فراخوانی میشوند.
توابع payable و متغیرهای حالت
از نسخه ۰.۴.۰ سالیدیتی ، هر تابعی که اتر دریافت میکند باید از مادیفایر payable استفاده کند، در غیر این صورت اگر mag.value تراکنش بیشتر از صفر باشد، برگشت داده خواهد شد.
نکته: موضوعی که شاید مشخص نباشد این است که مادیفایر payable فقط بر روی فراخوانیها از قراردادهای خارجی اعمال میشوند. اگر تابع پرداختناپذیر در تابع پرداختپذیر در یک قرارداد فراخوانی کنیم، تابع پرداختناپذیر شکست نخواهد خورد، هرچند msg.value همچنان برقرار است.
نحوه نمایش توابع و متغیرهای حالت
توابع را میتوان به صورت خارجی (external)، عمومی (public)، داخلی (internal) و خصوصی (private) دستهبندی کرد. لطفاً تفاوت آنها را بشناسید. برای مثال، ممکن است توابع external کافی باشد و به تابع public نیازی نباشد. برای متغیرهای حالت، شرایط external امکانپذیر نیست. برچسبگذاری نحوه نمایش باعث میشود که فرضهای نادرست درباره اینکه چه کسی میتواند تابع را فراخوانی کند یا به متغیرها دسترسی داشته باشد مشخصتر شود.
توابع external بخشی از رابط کاربری قرارداد است. تابع f که external است نمیتواند به صورت داخلی فراخوانی شود. به عبارت دیگر، ()f صحیح نیست اما ()this.f صحیح است و کار میکند. توابع خارجی گاهی اوقات که طیف وسیعی از اطلاعات و دادهها را دریافت میکنند کارآمدتر هستند.
توابع public بخشی از رابط کاربری قرارداد هستند و میتوانند به صورت داخلی یا از طریق پیامها فراخوانی شوند. در خصوص متغیرهای حالت، تابع دریافتکننده خودکار تولید میشود.
متغیرهای حالت و توابع internal فقط به صورت داخلی و بدون استفاده از this قابل دسترسی هستند.
متغیرهای حالت و توابع private فقط برای قراردادهای تعریف شده قابل مشاهده هستند.
نکته: هرچیزی که داخل قرارداد باشد برای تمام مشاهدهکنندگان خارج از بلاک چین قابل مشاهده است، حتی متغیرهای private.
محدودسازی pragma به نسخه خاصی از کامپایلر
قراردادها باید با نسخه یکسانی از کامپایلر اجرا شوند. محدودسازی pragma تضمین میکند که قراردادها به صورت تصادفی و با استفاده از آخرین نسخه کامپایلر که ممکن است باگهای کشفنشده داشته باشد اجرا نشوند. قراردادها ممکن است توسط سایر افراد اجرا شود و pragma، نسخه موردنظر نویسنده اصلی را نشان میدهد.
استفاده از رویدادها برای نظارت بر فعالیتهای قرارداد
نظارت بر فعالیت قرارداد پس از اجرای آن میتواند سودمند باشد. یکی از روشهای دستیابی به این نظارت، بررسی تمام تراکنشهای قرارداد است. هرچند این مورد میتواند کافی نباشد زیرا پیامهای بین قراردادها در بلاک چین ثبت نمیشوند. به علاوه، این مورد فقط پارامترهای ورودی را نشان میدهد، نه تغییرات واقعی انجامشده بر روی وضعیت قرارداد. همچنین از رویدادها میتوان برای فعال کردن توابع در رابط کاربری استفاده کرد.
در اینجا، قرارداد Game فراخوانی داخلی در ()Charity.donate انجام میدهد. این تراکنش در فهرست تراکنشهای خارجی Charity نشان داده نمیشود، بلکه فقط در تراکنشهای داخلی قابل مشاهده است.
رویداد (event) یک روش آسان برای ثبت مواردی است که در قرارداد رخ داده است. رویدادهایی که حذف میشوند، همراه با سایر اطلاعات قرارداد در بلاک چین باقی میمانند و برای حسابرسیهای آتی در دسترس خواهند بود. مثال زیر، بهبودی از مثال فوق است که از رویدادها برای ارائه تاریخچهای از کمکهای مالی خیریه ارائه میدهد.
در این مورد، تمام تراکنشهایی که به صورت مستقیم یا غیرمستقیم از قرارداد Charity عبور میکنند همراه با مقادیر کمکهای مالی در فهرست رویدادهای آن قرارداد نشان داده خواهند شد.
نکته: بهتر است از ساختارها و نسخههای جدیدتر سالیدیتی استفاده شود. بهتر است از ساختارهایی نظیر selfdestruct به جای suicide و keccak256 به جای sha3 استفاده شود. برای استفاده از ()transfer می.نولن از الگوهایی نظیر require(msg.sender.send(1 ether)) استفاده کرد.
مراقب تکرار Built-in ها باشید
در حال حاضر تکرار شدن Built-in ها در سالیدیتی امکانپذیر است. این موضوع به قراردادها امکان میدهد تا عملکرد Built-in هایی نظیر msg و ()revert تکرار شود. اگرچه این موضوع عمدی است، اما میتواند باعث گمراه شدن کاربران قرارداد در خصوص رفتار واقعی قرارداد شود. کاربران قرارداد باید مراقب کد منبع قرارداد هوشمند کامل باشند.
از tx.origin استفاده نکنید
هرگز از tx.origin برای اجازه دادن استفاده نکنید، زیرا ممکن است قرارداد دیگری از روشی استفاده کند که قرارداد شما را فراخوانی کند. بدین ترتیب، قرارداد شما به آن قرارداد اجاره خواهد داد زیرا آدرس شما در tx.origin قرار دارد.
برای این کار باید از msg.sender استفاده کنید. در این صورت اگر قرارداد دیگری، قرارداد شما را فراخوانی کند، msg.sender آدرس قرارداد خواهد بود، نه آدرس کاربرای که قرارداد را فراخوانی کرده است.
هشدار:
علاوه بر مشکل مطرح شده، احتمال این موضوع نیز وجود دارد که tx.origin در آینده از پروتکل اتریوم حذف شود، بنابراین کدی که از tx.origin استفاده میکند از نسخههای آتی پشتیبانی نخواهد کرد. ویتالیک بوترین نیز در این خصوص گفته است به نظر نمیرسد که tx.origin در ادامه مفید یا معنادار باقی بماند.
این نکته نیز قابل ذکر است که با استفاده از tx.origin، تعاملپذیری بین قراردادها محدود میشود زیرا نمیتوان از قراردادی که از tx.origin استفاده میکند قابل استفاده توسط قرارداد دیگر نیست.
وابستگی به برچسب زمانی
هنگام استفاده از برچسب زمانی برای اجرای یک تابع مهم در قرارداد و به ویژه برای اقداماتی که شامل انتقال سرمایه هستند، ۳ نکته اصلی را باید مدنظر قرار داد.
دستکاری برچسب زمانی
مراقب این موضوع باشید که ماینر میتواند برچسب زمانی بلاک را دستکاری کند. این قرارداد را در نظر بگیرید:
هنگامی که قرارداد از برچسب زمانی برای ایجاد یک عدد تصادفی استفاده میکند، ماینر میتواند طی ۱۵ ثانیه که بلاک در حال تایید شدن است، برچسب زمانی منتشر کند. این موضوع به ماینر امکان میدهد گزینه موردنظر را به نفع خود پیشمحاسبه کند. برچسبهای زمانی تصادفی نیستند و نباید در این موارد استفاده شوند.
قانون ۱۵ ثانیه
یلو پیپر (Yellow Paper یا همان مشخصات فنی مرجع اتریوم) مانعی در خصوص تعداد ایجاد بلاکها در زمان معین را مشخص نمیکند، بلکه بیان میکند هر برچسب زمانی باید از برچسب زمانی قبلی خود بزرگتر باشد. نسخههای محبوب پروتکل اتریوم گث (Geth) و پریتی (Parity) بلاکهایی با برچسب زمانی بیشتر از چند ثانیه را نمیپذیرند. بنابراین، قانون خوب برای ارزیابی کاربرد برچسب زمانی این است که اگر مقیاس رویداد وابسته به زمان شما بتواند در طیف ۱۵ ثانیهای باشد و یکپارچگی خود را حفظ کند، میتوان از block.timestamp استفاده کرد.
از block.number به عنوان برچسب زمانی استفاده نکنید
با استفاده از ویژگی block.number و میانگین زمان بلاک میتوان اختلاف زمانی را تخمین زد، هرچند این مورد در آینده به عنوان یک گواه قابل قبول نخواهد بود زیرا ممکن است زمان بلاک تغییر کند. قانون ۱۵ ثانیه امکان میدهد تا تخمین معتبر و مطمئنتری از زمان به دست آید.
هشدار وراثت چندگانه
هنگام استفاده از وراثت چندگانه (multiple inheritance) در سالیدیتی ، درک و شناخت نحوه ترکیب گرافهای وراثت توسط کامپایلر بسیار مهم است.
هنگامی که قراردادی اجرا شود، کامپایلر وراثت را از راست به چپ به صورت خطی درمیآورد. خطیسازی قرارداد A به صورت زیر است:
پیامد خطیسازی، مقدار fee برابر با ۵ است، زیرا C پراستفادهترین قرارداد است. ممکن است این موضوع مشخص باشد، اما شرایطی را تصور کنید که در آن، C میتواند توابع مهم را تکرار کند، ترتیب عبارتهای بولی (Boolean clauses) را تغییر دهد و باعث شود که توسعهدهنده یک قرارداد اکسپلویتپذیر بنویسد.
استفاده از رابط کاربری به جای آدرس
هنگامی که تابع، آدرس قرارداد را به عنوان فرض در نظر میگیرد، بهتر است از نوع قرارداد یا رابط کاربری به جای address استفاده شود. اگر تابع در جای دیگری در کد منبع فراخوانی شود، کامپایلر امنیت بیشتری ارائه خواهد داد.
در بخش زیر، دو گزینه موجود را مشاهده میکنیم:
مزایای استفاده از TypeSafeAuction در قرارداد فوق را میتوانیم در مثال زیر مشاهده کنیم. اگر ()validate bet با فرض address یا قراردادی به غیر از Validator فراخوانی شود، کامپایلر این خطا را نشان خواهد داد:
برای بررسی حسابهای خارجی از extcodesize استفاده نکنید
اغلب از مادیفایر زیر برای بررسی این موضوع استفاده میشود که آیا فراخوانی موردنظر از حساب خارجی (EOA) انجام شده است یا یک حساب قرارداد:
ایده این موضوع کاملا مشخص است. اگر آدرس حاوی کد باشد، این آدرس EOA نیست بلکه یک حساب قرارداد است. هرچند، یک قرارداد طی فرایند ایجاد خود هیچگونه کد منبع در دسترسی ندارد. این موضوع بدان معنا است که اگرچه کانستراکتور در حال اجرا است، اما میتواند فراخوانیها را برای سایر قراردادها انجام دهد. در بخش زیر مثالی از نحوه بررسی این موضوع بیان شده است:
تحلیل گر و معامله گر بازارهای مالی اعم از بورس، فارکس و به ویژه ارزهای دیجیتال، با سابقه 6 سال فعالیت و فارغ التحصیل رشتهحسابداری از دانشگاه تهران جتوب تهران می باشم.