نکات و ترفندهای سالیدیتی و قراردادهای هوشمند

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

0 116

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

پس سخن کوتاه می‌کنیم؛ این شما و این هم ۱۰ مشکل رایج…نه، صبر کنید، ۶۲ مشکل رایج!

۱- هر چیزی که قرارداد آن را روی بلاکچین، محاسبات، هزینه‌ها و سوخت (Gas) اجرا می‌کند. هر عملی که EVM آن را از طرف قرارداد شما انجام می‌دهد هزینه‌ای دارد. سعی کنید تا جای ممکن محاسبات را به سمت کاربر منتقل نمایید، مثلاً فرض کنید می‌خواهید میانگین مبالغ اهدایی به قراردادی را محاسبه کنید که پول جمع می‌کند و مبالغ اهدایی را می‌شمارد. در این صورت قرارداد فقط باید مجموع تعداد و مبالغ اهدایی را فراهم و بعد میانگین را در سمت کلاینت محاسبه کند.

۲- هر چیزی که قرارداد آن را در فضای ذخیره‌سازی پایدارش ذخیره می‌کند هزینه (Gas) دارد. سعی کنید از فضای ذخیره‌سازی قرارداد فقط برای چیزهایی استفاده نمایید که به منظور اجرای قرارداد ضروری هستند. اطلاعاتی مثل محاسبات مشتقه، حافظه کش، جمع‌ها و غیره باید در سمت کلاینت مدیریت شود.

۳- هرگاه مقدور بود برای اطلاعات پایدار از رویدادها و لاگ‌ها استفاده کنید. لاگ‌ها در دسترس کد قرارداد نبوده و استاتیک نیست، بنابراین نمی‌توانید برای اجرای کد در بلاکچین از آن‌ها استفاده کنید. ولی می‌توانید آن‌ها را از سمت کلاینت بخوانید چون این کار هزینه بسیار کمتری دارد.

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

۵- هر چیزی که قرارداد از آن برای حافظه موقت استفاده می‌کند هزینه (Gas) دارد. قیمت‌گذاری برای بخشی مصرفی حافظه، یعنی همان بخشی که پس از اجرا پاک می‌شود (چیزی شبیه به RAM) نیز خطی نیست، و هزینه‌ی هر واحد پس از استفاده‌ی زیاد از حافظه افزایش می‌یابد. سعی کنید از مصرف غیرمنطقی حافظه پرهیز کنید.

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

۷- هر بار که قراردادی می‌سازید، این کار هزینه (Gas) دارد. بنابراین اگر عادت دارید بعد از هر تغییر کوچک کل پایگاه داده را Push کنید، این کار می‌توانید هزینه‌ی زیادی روی دست شما بگذارد. هر گاه مقدور است، سعی کنید عملکرد برنامه خود را به چند قرارداد متمرکزتر تقسیم نمایید. با این کار وقتی یک بخش را اصلاح کردید، دیگر لازم نیست دوباره بقیه را پیاده‌سازی کنید. از قراردادهای کتابخانه‌ای استفاده نمایید. حذف قرارداد به صورت جزئی قابل بازیابی است، ولی میزان بازیابی آن کمتر از پیاده‌سازی کل پروژه است.

۸- بازیابی هرگز نمی‌تواند بیشتر از سوختی باشد که در تراکنشِ عملیات بازیابی فراهم شده است. در واقع، بازیابی هزینه حداکثر می‌تواند به میزان ۵۰ درصد سوخت تراکنش باشد.

۹- هر گاه صرفا پولی را به یک قرارداد می‌فرستید (تراکنشی همراه با ارزش > ۰)، حتی اگر هیچ تابعی را فراخوانی نکرده باشید، کد قرارداد را اجرا کرده‌اید. قرارداد فرصت فراخوانی سایر توابع را پیدا می‌کند. اتریوم با بیت کوین تفاوت دارد: حتی ساده‌ترین جابجایی پول هم باعث اجرای کد می‌شود.

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

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

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

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

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

۱۵- اگر ABI را از دست دادید، راهی برای فراخوانی قرارداد، مگر با استفاده از تابع بازگشت ندارید. این وضعیت مثل زمانی است که یک کد کامپایل‌شده و قابل اجرا دارید ولی منبع آن در دسترس نیست. وقتی با Populus کامپایل می‌کنید، ABI در یک فایل پروژه ذخیره می‌شود. ABI به EVM می‌گوید که چطور باید به طور صحیح بایت‌کد کامپایل‌شده فراخوانی کند. بدون ABI هیچ راهی برای انجام این کار نیست. پس حواستان باشد که ABI را گم نکنید.

۱۶- البته روش نسبتا پیچیده‌ای وجود دارد که با کمک آن می‌توانید بدون استفاده از ABI قرارداد را فراخوانی کنید. با آدرس متد call می‌توانید تابع بازگشت را فراخوانی نمایید، فقط کافی است آرگومان‌ها را فراهم کنید. علاوه بر این، اگر بازگشت تابع اصلی کار شماست، راه ساده‌ای برای فراخوانی قرارداد وجود دارد. اگر بخش اول آرگومانِ call از نوع byte4 باشد، اولین آرگومان امضای تابع در نظر گرفته می‌شود. به منظور فراخوانی و انتقال تمام سوخت باقی‌مانده در قرارداد از تابع    ()(addr.call.value(xاستفاده کنید.

۱۷- وقتی یک قرارداد برای یک قرارداد دیگر پول می‌فرستد، قرارداد فراخوانی‌شده کنترلِ اجرایی می‌گیرد و می‌تواند پیش از برگشتن فراخوان و قبل از آن که متغیرهای وضعیت را آپدیت کنید، آن را فراخوانی نماید. به این اتفاق حمله‌ی ورود مجدد ( re-entry) می‌گویند. از این رو، بعد از این فراخوانی دوباره، قرارداد شما یک بار دیگر اجرا می‌شود، اما متغیرهای وضعیت پولی که فرستاده شده را بازتاب نمی‌دهند. به عبارت دیگر، قرارداد فراخوانده‌شده پول را می‌گیرد، سپس فرستنده را در حالتی فرا می‌خواند که معلوم نیست پول ارسال شده یا نه. برای جلوگیری از این اتفاق، همیشه پیش از ارسال پول متغیرهای وضعیتی را آپدیت کنید که نگهدارنده‌ی مقدار پولی است که اکانت دوم می‌تواند دریافت کند. اگر تراکنش با شکست مواجه شد، این فرآیند را به صورت معکوس انجام دهید.

۱۸- به‌علاوه، این قرارداد فراخوانی‌شده می‌تواند کد یا بازگشتی را فراخوانی کند که می‌تواند از حداکثر عمق ۱۰۲۴ تایی پشته فراتر رفته و استثنایی را اجرا نماید که در فراخوانی قرارداد شما ظاهر می‌شود.

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

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

۲۱- call و delegatecall سایر قراردادها را فراخوانی می‌کنند، ولی نمی‌توانند در این قراردادها بروز استثنا داشته باشند. اگر فراخوانی دچار حالت استثنا شود، تنها علامتی که دریافت می‌کنید، زمانی است که مقدار false برگردانده می‌شود. این یعنی با استفاده از آدرس قراردادهای ناموجود، از طریق فراخوانی call و delegatecall نمی‌توانید چیزی به دست بیاورید. در این دو حالت، یعنی اجرای موفق و فراخوانی قرارداد ناموجود، مقدار true برمی‌گردد. پیش از فراخوانی، وجود قرارداد را از آدرس آن بررسی کنید.

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

۲۳- فراخوانی در سمت کلاینت راهکاری از پشته وب ۳ است که دقیقا مشابه ارسال تراکنش واقعی عمل می‌کند، ولی وضعیت بلاکچین را تغییر نمی‌دهد. call برای دریافت اطلاعات از وضعیت موجود، و بدون تغییر دادن آن، مفید است. با توجه به این که هیچ اطلاعاتی تغییر نیافته، این تابع می‌تواند روی گره محلی اجرا شده و در نتیجه هزینه‌ کمی داشته باشد.

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

۲۵- محدودیت عمق پشته‌ ماشین مجازی اتریوم ۱۰۲۴ است. برای اجرای عملیات‌های پیچیده و عمیق، بهتر است به جای برگشتی‌ها از گام‌ها و حلقه‌ها استفاده کنید.

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

۲۷- تخصیص متغیر حافظه به متغیر فضای ذخیره‌سازی همیشه باعث به وجود آمدن اشاره‌گری می‌شود که در صورت تغییر متغیر وضعیت پایه‌ای متوجه این تغییر نمی‌شود.

۲۸- در قراردادها از گرد کردن Eth خودداری کنید، چون باعث از دست رفتن پولی می‌شود که گرد شده است. در عوض از خود واحدهای Eth استفاده کنید که برای همین کار طراحی شده‌اند.

۲۹- واحد پیش‌فرض پول، هم در سالیدیتی و هم در وب ۳، مثل msg.value یا دریافت تراز، Wei است.

۳۰- سالیدیتی از نسخه ۰.۴.۱۷ نوع داده‌ای برای کار کردن با اعداد دسیمال ندارد و این اعداد باید به اعداد صحیح تبدیل شود.

۳۱- وقتی اکانت خود را در یک گره در حال اجرا آنلاک کردید، پروسه‌ی اجرایی به پول شما دسترسی کامل پیدا می‌کند. پس مراقب باشید. اکانت خود را فقط در سیستم‌های محلی و محافظت‌شده آنلاک کنید.

۳۲- اگر با RPC از راه دور به یک گره وصل شدید، فقط برای کارهایی مثل خواندن لاگ، اطلاعات یا غیره از این اکانت استفاده کنید. اکانت‌ها را در گره‌های RPC از راه دور آنلاک نکنید، زیرا هر کسی که بتواند از طریق اینترنت به این گره دسترسی یابد، امکان استفاده از سرمایه‌ی اکانت را خواهد داشت.

۳۳- اگر برای پیاده‌سازی قراردادها، ارسال تراکنش‌ها و غیره باید اکانتی را آنلاک کنید، حتما حواستان باشد که حداقل مقدار Eth را وارد این اکانت نمایید.

۳۴- هر کسی که کلید خصوصی را داشته باشد، می‌تواند سرمایه‌های موجود در اکانت را خارج کند. هیچ مانعی در برابر او وجود ندارد.

۳۵- هر کسی که فایل رمزنگاری‌شده‌ کیف پول و رمزعبور آن را در اختیار داشته باشد، می‌تواند سرمایه‌های موجود در اکانت را خارج کند. هیچ مانعی در برابر او وجود ندارد.

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

۳۷- اگر با سایت‌هایی مثل etherscan.io اکانت خود را بررسی کردید و متوجه شدید که در آن پول وجود دارد، ولی وقتی به صورت محلی آن را نگاه می‌کنید پولی دیده نمی‌شود، گره محلی شما سینک نشده است.

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

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

۴۰- اسکوپ (Scope) و پدیداری (visibility) ویژگی‌هایی هستند که در زبان سالیدیتی فقط از نظر اجرای کد معنادارند. وقتی EVM کد قرارداد شما را اجرا می‌کند، به توابع public، external و internal دقت می‌کند. EVM از این کلیدواژه‌ها استفاده نمی‌کند، ولی شاخصه‌ پدیداری در بایت‌کد و رابط آن مشهود است. با این حال، تعریف پدیداریِ حوزه تاثیری بر اطلاعاتی که بلاکچین آن را در اختیار دنیای بیرونی قرار می‌دهد ندارد.

۴۱- اگر به طور واضح مادیفایر payable را در تابع تعریف نکرده‌اید، آن Eth که در تراکنش ارسال شده رد می‌شود. اگر تابع payable نداشته باشید، قرارداد نمی‌تواند اتر را بپذیرد.

۴۲- پاسخ مشکل همین است.

۴۳- در پایتون نمی‌توانید فهرستی از همه‌ی کلیدها یا مقادیر متغیر mapping، مثل ()mydict.keys یا ()mydict.values داشته باشید. اگر لازم باشد، خودتان باید چنین فهرستی را تهیه کنید.

۴۴- سازنده (Constructor) قرارداد تنها زمانی اجرا می‌شود که قرارداد ساخته شده باشد و دیگر نتوانیم آن را فراخوانی کنیم. سازنده یک مؤلفه‌ اختیاری است.

۴۵- سازوکار وراثت در سالیدیتی متفاوت است. معمولاً یک کلاس و زیرکلاس دارید که هر کدام بخشی مستقل است که می‌توانید به آن دسترسی داشته باشید. در سالیدیتی، وراثت بیشتر جنبه‌ی نحوی (Syntatic) دارد. در فرآیند کامپایل نهایی، کامپایلر اعضای کلاس والد را کپی می‌کند تا با اعضای کپی‌شده بایت‌کد قرارداد مشتقه را بسازد. در این حالت، تابع private صرفا نشان می‌دهد که کامپایلر کدام متغیرها و توابع وضعیت را کپی نمی‌کند.

۴۶- حجم خوانش حافظه به طول ۲۵۶ بیت محدود است، در حالی که عرض نوشتار حافظه می‌تواند ۸ تا ۲۵۶ بیت باشد.

۴۷- throw و revert همه‌ی تغییرات وضعیت را لغو کرده و آن‌ها را به تراز اتر برمی‌گرداند. سوخت‌های مصرف‌شده دیگر قابل بازیابی نیست.

۴۸- function یک نوع داده‌ معتبر است و می‌تواند به عنوان آرگومانی به یک تابع دیگر منتقل شود. اگر یک متغیر نوع function آغاز نشده باشد، فراخوانی آن باعث به وجود آمدن حالت استثنا می‌شود.

۴۹- نقشه‌برداری فقط برای متغیرهای وضعیت مجاز است.

۵۰- delete عملاً چیزی را حذف نمی‌کند، بلکه مقادیر اولیه را برمی‌گرداند. این تابع نوع خاصی از تخصیص داده است. حذف یک متغیر محلیvar که به یک متغیر وضعیت اشاره دارد باعث به وجود آمدن حالت استثنا می‌شود، چون متغیر حذف‌شده (اشاره‌گر) هیچ مقدار اولیه‌ای ندارد که به آن برگردد.

۵۱- متغیرهای تعریف‌شده صریحاً در ابتدای تابع به همان مقادیر اولیه‌ خود باز می‌گردند.

۵۲- شما می‌توانید تابعی را به عنوان constant یا با اصطلاح جدید view تعریف کنید تا به‌لحاظ تئوری تابعی ایمن داشته باشید که وضعیت را تغییر نمی‌دهد. با این حال کامپایلر اجباری در انجام این کار اعمال نمی‌کند.

۵۳- توابع internal فقط می‌توانند از طریق خود قرارداد فراخوانی شوند.

۵۴- برای دسترسی پیدا کردن به تابع خارجی f از داخل همان قراردادی که این تابع را داخلش تعریف کردید، از this.f استفاده کنید. البته برخی مواقع به this هم نیازی ندارید.

۵۵- private فقط در صورتی اهمیت دارد که قراردادهای مشتقه داشته باشید، آن‌جا که private متضمن اعضایی است که کامپایلر آن‌ها را به قراردادهای مشتقه کپی نمی‌کند. در غیر این صورت، در داخل قرارداد، private مشابه همان internal است.

۵۶- external فقط برای توابع در دسترس است. public، internal و private در دسترس توابع و متغیرهای وضعیت قرار دارند. رابط قرارداد از اعضای external و public ساخته می‌شود.

۵۷- کامپایلر به طور خودکار برای متغیرهای وضعیت public تابعی برای کسب دسترسی (تابع get) می‌سازد.

۵۸- now برچسب زمانی بلاک کنونی است.

۵۹- واحدهای اتریوم، یعنی wei، finney، Szabo و ether کلمات رزرو شده هستند.

۶۰- واحدهای زمانی مثل seconds, minutes, hours, days, weeks و years کلمات رزرو شده هستند.

۶۱- تبدیل نوع از حالت غیربولی به بولی ممکن نیست.{…} (if (1 از نظر سالیدیتی معتبر نیست.

۶۲- متغیرهای msg، block و tx همیشه در فضای نام عمومی وجود دارند و می‌توانید بدون اعلان یا تخصیص قبلی از آن‌ها یا اعضایشان استفاده کنید.

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

ارسال پاسخ

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