هدف این مقاله، بررسی رفتار تابع gasleft()
در زبان برنامه نویسی سالیدیتی و کاربردهای آن است.
تابع gasleft()
یکی از توابع داخلی (built-in) در سالیدیتی است که برای بررسی میزان گاز باقیمانده در حین اجرای یک تماس قراردادی (contract call) به کار میرود. این تابع جزو متغیرها و توابع ویژهای است که همیشه در فضای نام سراسری (global namespace) در دسترس هستند، بنابراین نیازی به ایمپورت کردن آن وجود ندارد.
پیش از نسخه 0.4.21 سالیدیتی، از msg.gas
به جای gasleft()
استفاده میشد.
اهمیت تابع gasleft() در سالیدیتی
مقدار گازی که در قراردادهای هوشمند مصرف میشود، به پیچیدگی کد و همچنین حجم دادههایی بستگی دارد که در طول اجرای تراکنش پردازش میشوند.
اگر گاز اختصاصدادهشده به تراکنش کافی نباشد، اجرای آن با خطای «out of gas» (پایان گاز) متوقف میشود. استفاده صحیح از تابع gasleft()
میتواند از وقوع چنین خطاهایی جلوگیری کند. در بخش بعد، یک مثال کاربردی را بررسی میکنیم.
مثال جلوگیری از خطای پایان گاز
جلوگیری از تمام شدن گاز هنگام توزیع اتر
ارسال اتر به چندین آدرس از طریق حلقه ها در قراردادهای هوشمند، خصوصاً زمانی که با آرایه های بزرگ سر و کار داریم، میتواند هزینه زیادی از نظر گاز داشته باشد.
اگر گاز موجود برای اجرای این عملیات کافی نباشد، تابع با همان خطای «out of gas» که پیشتر ذکر شد متوقف میشود.
اما با استفاده از تابع gasleft()
میتوان بررسی کرد که آیا میزان گاز باقیمانده برای انتقال بعدی کافی هست یا نه، و در صورت ناکافی بودن گاز، از ادامه اجرا جلوگیری کرد.
کد زیر این موضوع را به خوبی نشان میدهد:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// SPDX-License-Identifier: MIT pragma solidity ^0.8.7; contract GasConsumer{ uint constant MINIMUM_AMOUNT = 10_000; // this is for illustration purposes only, not production function distributeEther(address[] calldata receivers) external { for (uint i = 0; i < receivers.length; i++) { payable(receivers[i]).transfer(1 ether); if (gasleft() < MINIMUM_AMOUNT) { return; } } } receive() external payable {} } |
تابع distributeEther
در قرارداد بالا، یک آرایه از آدرس ها دریافت میکند، با استفاده از حلقه for
در آن پیمایش میکند و به هر آدرس از طریق تابع transfer
مقدار یک اتر ارسال میکند.
در این میان، یک شرط if
بررسی میکند که آیا گاز باقیمانده پس از هر انتقال اتر، برای انجام انتقال بعدی کافی هست یا نه. این بررسی با مقایسه مقدار گاز باقیمانده پس از هر انتقال با عدد ۱۰٬۰۰۰ انجام میشود (۹٬۰۰۰ برای عملیات انتقال اتر و ۱٬۰۰۰ برای دستورات جانبی دیگر). اگر مقدار باقیمانده کمتر از این مقدار باشد، حلقه متوقف میشود و تابع اجرا را پایان میدهد، بدون اینکه انتقالهای قبلی بازگردانده شوند یا تراکنش revert شود.
(به این نکته مهم توجه کنید که ارسال اتر به آدرسهایی که به آنها اعتماد ندارید، ایده خوبی نیست. چون ممکن است دریافتکننده، آدرس یک قرارداد مخرب را وارد کند که هنگام دریافت اتر، تراکنش را revert کند.)
کد بنچمارک (اندازهگیری عملکرد)
استفاده از تابع gasleft() برای محاسبه هزینه اجرای کد
یک کاربرد دیگر تابع gasleft()
این است که میتوان با استفاده از آن، میزان گازی را که یک بخش خاص از کد مصرف کرده اندازهگیری کرد.
در ادامه، مثالی از این کاربرد در محیط توسعه Remix آورده شده است:
در این مثال، تابع 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()
یا معادل آن در 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 را میتوانید در مستندات مربوطه مطالعه کنید.
مشاهده کد قرارداد OpenZeppelin Minimal Forwarder
قرارداد EthBalance Monitor در Chainlink – جلوگیری از خطای پایان گاز هنگام توزیع اتر
این مورد یک نمونه واقعی از کاربردی است که پیشتر در مقاله به آن پرداختیم: توزیع اتر به چند آدرس با استفاده از حلقه. در این نسخه واقعی که توسط Chainlink پیادهسازی شده، منطق تجاری پیچیدهتری وجود دارد، اما اگر بررسی کنیم، همچنان همان بررسی کلیدی gasleft()
برای خروج زودهنگام از حلقه اعمال شده است.
در واقع، این بررسی همان هدف اولیه را دنبال میکند: جلوگیری از شکست تراکنش به دلیل کمبود گاز هنگام توزیع اتر.
قرارداد Chainlink VRFCoordinatorV2 – محاسبه میزان گاز مصرفی برای Fulfillment درخواست های تصادفی
قرارداد هوشمند VRFCoordinatorV2
در شبکه Chainlink، نقش هماهنگکننده برای تولید اعداد تصادفی قابل راستی آزمایی (Verifiable Random Function) را دارد. این قرارداد به عنوان اوراکل بین کاربران و گرههای Chainlink عمل میکند تا درخواست تولید عدد تصادفی را مدیریت کند. (مشاهده: VRFCoordinatorV2)
در این قرارداد، تابع gasleft()
در درون متد calculatePaymentAmount
استفاده میشود تا کارمزدی متناسب با گاز مصرفشده محاسبه شود. اگر گره (Node) مجبور باشد برای اجرای عملیات تصادفی گاز بیشتری مصرف کند، کاربر باید هزینه بیشتری پرداخت کند.
فرمول محاسبه بر اساس تفاوت بین startGas
و gasleft()
عمل میکند. هرچه گاز باقیمانده کمتر باشد، یعنی گاز بیشتری مصرف شده و در نتیجه، کارمزد نیز افزایش مییابد.
مشاهده کد قرارداد Chainlink VRFCoordinatorV2
نتیجه گیری
در این مقاله، با کاربردهای متنوع تابع gasleft() در سالیدیتی آشنا شدیم. این کاربردها شامل موارد زیر هستند:
-
جلوگیری از خطای out-of-gas در اجرای توابع سنگین مانند توزیع اتر در حلقهها
-
بنچمارکگیری از کد سالیدیتی برای تحلیل میزان مصرف گاز در بخشهای مختلف برنامه
-
انتقال کامل گاز به قراردادهای پیادهسازی در معماری پراکسیها
-
جلوگیری از حملات DoS توسط relayer ها در تراکنشهای متا
تابع gasleft()
ابزاری ساده اما حیاتی برای نوشتن قراردادهای هوشمند بهینه، ایمن و قابل اعتماد است.
راستی! برای دریافت مطالب جدید در کانال تلگرام یا پیج اینستاگرام سورس باران عضو شوید.
- انتشار: ۷ خرداد ۱۴۰۴
دسته بندی موضوعات
- آموزش ارز دیجیتال
- آموزش برنامه نویسی
- آموزش متنی برنامه نویسی
- اطلاعیه و سایر مطالب
- پروژه برنامه نویسی
- دوره های تخصصی برنامه نویسی
- رپورتاژ
- فیلم های آموزشی
- ++C
- ADO.NET
- Adobe Flash
- Ajax
- AngularJS
- apache
- ARM
- Asp.Net
- ASP.NET MVC
- AVR
- Bootstrap
- CCNA
- CCNP
- CMD
- CSS
- Dreameaver
- EntityFramework
- HTML
- IOS
- jquery
- Linq
- Mysql
- Oracle
- PHP
- PHPMyAdmin
- Rational Rose
- silver light
- SQL Server
- Stimulsoft Reports
- Telerik
- UML
- VB.NET&VB6
- WPF
- Xml
- آموزش های پروژه محور
- اتوکد
- الگوریتم تقریبی
- امنیت
- اندروید
- اندروید استودیو
- بک ترک
- بیسیک فور اندروید
- پایتون
- جاوا
- جاوا اسکریپت
- جوملا
- دلفی
- دوره آموزش Go
- دوره های رایگان پیشنهادی
- زامارین
- سئو
- ساخت CMS
- سی شارپ
- شبکه و مجازی سازی
- طراحی الگوریتم
- طراحی بازی
- طراحی وب
- فتوشاپ
- فریم ورک codeigniter
- فلاتر
- کانستراکت
- کریستال ریپورت
- لاراول
- معماری کامپیوتر
- مهندسی اینترنت
- هوش مصنوعی
- یونیتی
- کتاب های آموزشی
- Android
- ASP.NET
- AVR
- LINQ
- php
- Workflow
- اچ تی ام ال
- بانک اطلاعاتی
- برنامه نویسی سوکت
- برنامه نویسی موبایل
- پاسکال
- پایان نامه
- پایتون
- جاوا
- جاوا اسکریپت
- جی کوئری
- داده کاوی
- دلفی
- رباتیک
- سئو
- سایر کتاب ها
- سخت افزار
- سی اس اس
- سی پلاس پلاس
- سی شارپ
- طراحی الگوریتم
- فتوشاپ
- مقاله
- مهندسی نرم افزار
- هک و امنیت
- هوش مصنوعی
- ویژوال بیسیک
- نرم افزار و ابزار برنامه نویسی
- وردپرس