آموزش gasleft در سالیدیتی

هدف این مقاله، بررسی رفتار تابع gasleft() در زبان برنامه نویسی سالیدیتی و کاربردهای آن است.

تابع gasleft() یکی از توابع داخلی (built-in) در سالیدیتی است که برای بررسی میزان گاز باقی‌مانده در حین اجرای یک تماس قراردادی (contract call) به کار می‌رود. این تابع جزو متغیرها و توابع ویژه‌ای است که همیشه در فضای نام سراسری (global namespace) در دسترس هستند، بنابراین نیازی به ایمپورت کردن آن وجود ندارد.
پیش از نسخه 0.4.21 سالیدیتی، از msg.gas به جای gasleft() استفاده می‌شد.

اهمیت تابع gasleft() در سالیدیتی

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

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

مثال جلوگیری از خطای پایان گاز

جلوگیری از تمام شدن گاز هنگام توزیع اتر

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

اگر گاز موجود برای اجرای این عملیات کافی نباشد، تابع با همان خطای «out of gas» که پیش‌تر ذکر شد متوقف می‌شود.

اما با استفاده از تابع gasleft() می‌توان بررسی کرد که آیا میزان گاز باقی‌مانده برای انتقال بعدی کافی هست یا نه، و در صورت ناکافی بودن گاز، از ادامه اجرا جلوگیری کرد.

کد زیر این موضوع را به خوبی نشان می‌دهد:

تابع distributeEther در قرارداد بالا، یک آرایه از آدرس ها دریافت می‌کند، با استفاده از حلقه for در آن پیمایش می‌کند و به هر آدرس از طریق تابع transfer مقدار یک اتر ارسال می‌کند.

در این میان، یک شرط if بررسی می‌کند که آیا گاز باقی‌مانده پس از هر انتقال اتر، برای انجام انتقال بعدی کافی هست یا نه. این بررسی با مقایسه مقدار گاز باقی‌مانده پس از هر انتقال با عدد ۱۰٬۰۰۰ انجام می‌شود (۹٬۰۰۰ برای عملیات انتقال اتر و ۱٬۰۰۰ برای دستورات جانبی دیگر). اگر مقدار باقی‌مانده کمتر از این مقدار باشد، حلقه متوقف می‌شود و تابع اجرا را پایان می‌دهد، بدون این‌که انتقال‌های قبلی بازگردانده شوند یا تراکنش revert شود.

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

کد بنچمارک (اندازه‌گیری عملکرد)

استفاده از تابع gasleft() برای محاسبه هزینه اجرای کد

یک کاربرد دیگر تابع gasleft() این است که می‌توان با استفاده از آن، میزان گازی را که یک بخش خاص از کد مصرف کرده اندازه‌گیری کرد.

در ادامه، مثالی از این کاربرد در محیط توسعه Remix آورده شده است:

کد سالیدیت بنچمارک با gasleft

در این مثال، تابع gasleft() برای بررسی میزان گاز مصرف‌شده هنگام افزودن یک عدد به آرایه numArr از طریق تابع updateArray استفاده شده است.
نکته مهم این است که مقدار به‌دست‌آمده، نشان‌دهنده کل گاز مصرفی تابع نیست، بلکه صرفاً تفاوت گاز قبل و بعد از اجرای خط numArr.push(_num) در خط ۳۰ کد را اندازه‌گیری می‌کند.

توضیح اعداد به‌دست‌آمده در خروجی

برای مشاهده آسان مقدار مصرف گاز، متغیر gasUsed به‌صورت عمومی (public) تعریف شده است. با این کار، بعد از اجرای تابع می‌توان به‌راحتی مقدار آن را مشاهده کرد. برای آزمایش، قرارداد GasCalc را مستقر کرده و سپس تابع updateArray را صدا می‌زنیم.

خروجی این تابع به‌صورت یک tuple باز می‌گردد. مقدار اول، یعنی initialGas، برابر با 80٬348 است.

مقدار دوم، یعنی finalGas، برابر با 35٬923 به‌دست آمده که این عدد شامل هزینه اجرای خود تابع gasleft() نیز هست.

با کم کردن این دو عدد از یکدیگر، متوجه می‌شویم که اجرای خط ۳۰ در کد، معادل 44٬425 واحد گاز مصرف کرده است.

کد دستورالعمل (opcode) پشت تابع gasleft فقط ۲ واحد گاز مصرف می‌کند

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

کد ماشین (opcode) مربوط به تابع gasleft() با نام GAS شناخته می‌شود و بایت‌کد آن 0x5A است. طبق مستندات رسمی اتریوم، اجرای این دستور تنها ۲ واحد گاز مصرف می‌کند.

کاربردهای واقعی تابع gasleft() در سالیدیتی

استفاده در پراکسی OpenZeppelin – انتقال کامل گاز به قرارداد پیاده‌سازی

تابع gasleft() را می‌توان در زبان سالیدیتی از طریق Yul (اسمبلی داخلی) نیز استفاده کرد، که در این حالت با دستور gas() شناخته می‌شود.

یکی از نمونه‌های برجسته استفاده از این قابلیت، در قرارداد پراکسی OpenZeppelin مشاهده می‌شود. این قرارداد در تابع delegatecall از gas() استفاده می‌کند تا تمام گاز در دسترس را به قرارداد پیاده‌سازی (Implementation Contract) منتقل کند. این کار باعث می‌شود عملیات delegatecall بدون محدودیت در مصرف گاز اجرا شود.

ارسال تمام سوخت در فراخوانی نماینده با سالیدیتی gasleft

در اینجا، استفاده از gasleft() یا معادل آن در Yul یعنی gas()، ساده‌ترین و قابل‌اعتمادترین راه برای انتقال کل گاز موجود به قرارداد پیاده‌سازی است.

مشاهده قرارداد پراکسی OpenZeppelin

استفاده در OpenZeppelin Minimal Forwarder – بررسی صحت گاز ارسال‌ شده توسط Relayer

در برخی موارد، موجودیتی خارج از زنجیره به نام Relayer هزینه گاز تراکنش کاربران دیگر را پرداخت می‌کند. کاربر، تراکنش را امضا کرده و همراه با مقدار گاز مورد نیاز به relayer ارسال می‌کند. Relayer سپس این تراکنش را به قرارداد forwarder ارسال می‌کند تا اجرا شود.

مشکل زمانی پیش می‌آید که relayer، برخلاف ادعای کاربر، مقدار کمتری گاز اختصاص دهد. این نوع حمله، به نام gas griefing شناخته می‌شود و در رجیستری SWC با عنوان SWC-126 ثبت شده است.

در این سناریو، اگر تماس اولیه موفق باشد اما تماس داخلی (subcall) به دلیل تمام شدن گاز شکست بخورد، relayer می‌تواند وانمود کند که خطا از سمت کاربر بوده است. اما واقعیت این است که relayer به اندازه کافی گاز اختصاص نداده و باعث شکست تراکنش شده است.

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

در اینجا از gasleft() برای تشخیص علت شکست استفاده می‌شود.

طبق استاندارد EIP-150، هنگام اجرای تابع call، تنها 63/64 از گاز موجود به تماس داخلی منتقل می‌شود. پس از پایان تماس داخلی، باید حداقل 1/64 از گاز اصلی باقی مانده باشد.

اگر گاز باقی‌مانده کمتر از این مقدار باشد، مشخص است که relayer به اندازه کافی گاز ارسال نکرده است. در قرارداد OpenZeppelin Minimal Forwarder، بررسی می‌شود که حداقل 1/63 گاز باقی مانده باشد تا حاشیه ایمنی در نظر گرفته شود.

در این پیاده‌سازی، از دستور invalid استفاده می‌شود تا تراکنش relayer به طور واضح شکست بخورد و مشخص شود که عامل خطا relayer بوده، نه تماس داخلی. اطلاعات بیشتر درباره حمله gas griefing را می‌توانید در مستندات مربوطه مطالعه کنید.

جلوگیری از نشت گاز رله با استفاده از gasleft solidity

مشاهده کد قرارداد OpenZeppelin Minimal Forwarder

قرارداد EthBalance Monitor در Chainlink – جلوگیری از خطای پایان گاز هنگام توزیع اتر

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

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

chainlink با استفاده از gasleft

قرارداد Chainlink VRFCoordinatorV2 – محاسبه میزان گاز مصرفی برای Fulfillment درخواست های تصادفی

قرارداد هوشمند VRFCoordinatorV2 در شبکه Chainlink، نقش هماهنگ‌کننده برای تولید اعداد تصادفی قابل راستی آزمایی (Verifiable Random Function) را دارد. این قرارداد به عنوان اوراکل بین کاربران و گره‌های Chainlink عمل می‌کند تا درخواست تولید عدد تصادفی را مدیریت کند. (مشاهده: VRFCoordinatorV2)

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

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

chainlink از gasleft برای محاسبه کارمزد اوراکل استفاده می‌کند

مشاهده کد قرارداد Chainlink VRFCoordinatorV2

نتیجه گیری

در این مقاله، با کاربردهای متنوع تابع gasleft() در سالیدیتی آشنا شدیم. این کاربردها شامل موارد زیر هستند:

  • جلوگیری از خطای out-of-gas در اجرای توابع سنگین مانند توزیع اتر در حلقه‌ها

  • بنچمارک‌گیری از کد سالیدیتی برای تحلیل میزان مصرف گاز در بخش‌های مختلف برنامه

  • انتقال کامل گاز به قراردادهای پیاده‌سازی در معماری پراکسی‌ها

  • جلوگیری از حملات DoS توسط relayer ها در تراکنش‌های متا

تابع gasleft() ابزاری ساده اما حیاتی برای نوشتن قراردادهای هوشمند بهینه، ایمن و قابل اعتماد است.

5/5 - (1 امتیاز)

راستی! برای دریافت مطالب جدید در کانال تلگرام یا پیج اینستاگرام سورس باران عضو شوید.

دوره صفر تا صد آموزش بین المللی لینوکس
  • انتشار: ۷ خرداد ۱۴۰۴

دسته بندی موضوعات

آخرین محصولات فروشگاه

مشاهده همه

نظرات

بازخوردهای خود را برای ما ارسال کنید