تمام کاربران درباره تراکنشهای بدون کارمزد گس اتریوم صحبت میکنند زیرا هیچکس پرداخت کارمزد گس را دوست ندارد. اما شبکه اتریوم به دلیل تراکنشهای دارای کارمزد به طور دقیق و بدون مشکل اجرا میشود. پس چطور میتوان تراکنشهای بدون کارمزد گس داشته باشیم؟ چه رازی در این موضوع نهفته است؟
در این مقاله به این موضوع خواهیم پرداخت که چگونه از الگوهای پشت پرده تراکنشهای بدون کارمزد گس استفاده کنیم. در این مقاله متوجه خواهید شد اگرچه هیچ چیزی در اتریوم به صورت رایگان نیست، اما میتوانید هزینه گس را به سمت موضوعات جالبتری هدایت کنید.
با بهکارگیری موضوعات مطرحشده در این مقاله، کاربران میتوانند در پرداخت گس صرفهجویی کنند، از تجربه کاربری بهتری بهرهمند شوند و حتی الگوهای وکالتی مختص به خود را ایجاد کنند و در قراردادهای هوشمند قرار دهند.
تراکنش بدون کارمزد گس در شبکه اتریوم
موضوعی که مانع مهمی برای شما به حساب نمیآید این است که حتی اگر دانش اندکی درباره رمزنگاری داشته باشید باز هم میتوانید تراکنشهای بدون کارمزد گس را پیادهسازی کنید.
از کلید خصوصی برای امضای تراکنشهای ارسالی به اتریوم استفاده میشود و از بعضی از ترفندهای رمزنگاری برای شناسایی ارسالکننده پیام استفاده میشود. این موضوع، اساس تمام دسترسیها و کنترلها در اتریوم است.
راز نهفته در تراکنشهای بدون کارمزد گس این است که میتوان با استفاده از کلید خصوصی و قرارداد هوشمند مدنظر خود، امضا ایجاد کنیم.
راز نهفته در تراکنشهای بدون کارمزد گس این است که میتوان با استفاده از کلید خصوصی و قرارداد هوشمند مدنظر خود، امضا ایجاد کنیم.
این امضا به صورت برون زنجیرهای و بدون صرف کارمزد گس تولید خواهد شد. سپس میتوانیم این امضا را به شخص دیگری بدهیم تا از جانب ما و با گس خود، تراکنش را اجرا کنند.
تابع این امضا یک تابع معمولی خواهد بود، اما با پارامترهای اضافی گسترش یافته است. برای مثال، در dai.sol تابع تایید زیر را داریم:
function approve(address usr, uint wad) external returns (bool)
همچنین تابع permit (اجازه) را داریم که عملکرد مشابهی با تابع approve (تایید) دارد اما امضا را به عنوان یک پارامتر در نظر میگیرد:
function permit(address holder, address spender, uint256 nonce, uint256 expiry, bool allowed, uint8 v, bytes32 r, bytes32 s) external
نگران پارامترهای اضافی نباشید. آنها را نیز بررسی خواهیم کرد. نکتهای که باید به آن توجه کنید تاثیر این دو تابع بر مپینگ allowance است:
function approve(address usr, uint wad) external returns (bool)
{
allowance[msg.sender][usr] = wad;
…
}
function permit(
address holder, address spender,
uint256 nonce, uint256 expiry, bool allowed,
uint8 v, bytes32 r, bytes32 s
) external {
…
allowance[holder][spender] = wad;
…
}
اگر از تابع approve استفاده کنید، به spender (خرجکننده) امکان میدهید تا با wad، توکنهای شما را خرج کند.
اگر امضای معتبر به کسی بدهید، آن فرد میتواند تابع permit را فراخوانی کند و به spender امکان دهد تا از توکنهای شما استفاده کند.
بنابراین، الگوی موجود در تراکنشهای بدون کارمزد گس، ایجاد امضایی است که بتوانید به فرد دیگری بدهید تا آنها بتوانند به طور ایمن، تراکنش مخصوصی را اجرا کنند. این موضوع همانند اجازه دادن به کسی برای اجرای تابع است. این نوع تراکنش، یک الگوی وکالتی است.
استانداردهای لازم برای استفاده از الگو
اولین کاری که باید انجام دهید، بررسی دقیق کد است.
// — — EIP712 niceties — -
پروپوزال EIP712 توضیح میدهد که چگونه امضاهایی برای توابع ایجاد کنیم. سایر EIP ها توضیح میدهند که چگونه EIP712 را برای موارد خاص اعمال کنیم. برای مثال، EIP2612 توضیح میدهد که چگونه از امضاهای EIP712 برای تابع permit استفاده کنیم. این تابع عملکرد یکسانی با تابع approve در توکنهای ERC-20 دارد.
اگر صرفا میخواهید تابع امضایی اجرا کنید که قبلاً انجام شده است، برای مثال اگر میخواهید تاییدیه امضا به MetaCoin خود اضافه کنید، میتوانید EIP2612 را مطالعه کنید و روش این کار را متوجه شوید.
در این مقاله پیادهسازی تراکنشهای بدون کارمزد گس را پر dai.sol بررسی خواهیم کرد. dai.sol قبل از EIP2612 ایجاد شده و اندکی متفاوت است.
محتویات امضا
نسخههای اولیه امضاهای EIP712 را میتوان در dai.sol مشاهده کرد. این امضاها به دارندگان استیبل کوین دای (dai) امکان میدهند تا با محاسبه امضای برون زنجیرهای و ارائه آن به spender، به جای فراخوانی تابع approve، انتقال تراکنش را تایید کنند.
این امضاها شامل چهار مولفه است:
۱- یک DOMAIN_SEPARATOR
۲- یک PERMIT_TYPEHASH
۳- یک متغیر nonces
۴- یک تابع Permit
کد زیر، DOMAIN_SEPARATOR با متغیرهای مربوطه است:
string public constant name = "Dai Stablecoin";
string public constant version = "1";
bytes32 public DOMAIN_SEPARATOR;
constructor(uint256 chainId_) public {
...
DOMAIN_SEPARATOR = keccak256(abi.encode(
keccak256(
"EIP712Domain(string name,string version," +
"uint256 chainId,address verifyingContract)"
),
keccak256(bytes(name)),
keccak256(bytes(version)),
chainId_,
address(this)
));
}
گفتنی است DOMAIN_SEPARATOR صرفا یک هش است که قرارداد هوشمند را شناسایی میکند و از رشتهای تشکیل شده است که شامل دامنه EIP712، اسم قرارداد توکن، نسخه، آیدی زنجیره و آدرسی است که قرارداد در آن اجرا میشود.
تمام این اطلاعات بر روی کانستراکتور داخل متغیر DOMAIN_SEPARATOR هش میشود که باید توسط دارنده هنگام ایجادکردن امضا استفاده شود و هنگام اجرای permit باید بررسی شود. این موضوع برای آن است که امضا فقط برای یک قرارداد معتبر است.
همچنین PERMIT_TYPEHASH به صورت زیر است:
گفتنی است PERMIT_TYPEHASH هش تابع به همین است و شامل تمام پارامترها نظیر نوع و اسم تابع است. هدف آن، شناسایی تابعی است که امضا برای آن ایجاد شده است.
این امضا در تابع permit پردازش خواهد شد و اگر PERMIT_TYPEHASH استفادهشده برای تابع موردنظر نباشد، برگردانده خواهد شد. این موضوع تضمین میکند که امضا فقط برای تابع موردنظر استفاده شده است.
در آخر مپینگ nonces وجود دارد که به صورت زیر است:
mapping (address => uint) public nonces;
این مپینگ تعداد امضاهای استفادهشده برای دارنده موردنظر را ثبت میکند. هنگام ایجادکردن امضا، باید مقدار nonces هم در امضا وجود داشته باشد. هنگام اجرا کردن تابع permit، باید nonces موجود در آن دقیقا مطابق با تعداد امضاهایی باشد که برای آن دارنده مورد استفاده قرار گرفته است. این موضوع تضمین میکند که امضا فقط یک بار استفاده شده است.
این سه شرط تضمین میکنند که هر امضا فقط یک بار برای قرارداد و تابع موردنظر استفاده شده است.
تابع permit
تابع permit یک تابع dai.sol است که استفاده از امضاها برای اصلاح تابع allowance از جانب holder برای spender را امکانپذیر میسازد.
// --- Approve by signature ---
function permit(
address holder, address spender,
uint256 nonce, uint256 expiry, bool allowed,
uint8 v, bytes32 r, bytes32 s
) external;
همانطور که مشاهده میکنید، پارامترهای بسیار زیادی وجود دارد. این پارامترها برای محاسبه امضا و v، r و s نیاز هستند.
شاید نیاز به پارامترهایی که برای ایجاد امضا استفاده شدهاند بیهوده به نظر برسد، اما وجود این پارامترها نیاز است. تنها چیزی که میتوانید از امضا بازیابی کنید، آدرسی است که امضا را ایجاد کرده است. ما از همه پارامترها و آدرس بازیابیشده برای اطمینان از معتبر بودن امضا استفاده خواهیم کرد.
ابتدا با استفاده از تمام پارامترهایی که برای تضمین امنیت نیاز خواهیم داشت، digest را محاسبه میکنیم. holder باید به عنوان بخشی از ایجادکردن امضا، دقیقا همان digest را به صورت برون زنجیرهای محاسبه کند:
bytes32 digest =
keccak256(abi.encodePacked(
"\x19\x01",
DOMAIN_SEPARATOR,
keccak256(abi.encode(
PERMIT_TYPEHASH,
holder,
spender,
nonce,
expiry,
allowed
))
));
با استفاده از ecrecover و امضای v,r,s میتوانیم آدرس را بازیابی کنیم. اگر امضای بازیابیشده همان آدرس holder باشد مطمئن میشویم که تمام پارامترها یکسان هستند. اگر هرکدام از پارامترها یکسان نباشند، امضا مورد قبول واقع نمیشود:
require(holder == ecrecover(digest, v, r, s), "Dai/invalid-permit");
ذکر یک نکته در اینجا لازم است. پارامترهای زیادی وجود دارند که وارد امضا میشوند. بعضی از این پارامترها نظیر آیدی زنجیره (chainId) مبهم هستند. هرکدام از این پارامترها مطابقت نداشته باشند، امضا مورد قبول واقع نمیشود. این موضوع تضمین میکند که دیباگ کردن برون زنجیرهای امضاها بسیار دشوار خواهد بود.
اکنون میدانیم که holder فراخوانی تابع را تایید کرده است. سپس تایید میکنیم که از امضا سوءاستفاده نمیشود. در این خصوص، بررسی میکنیم که زمان کنونی قبل از زمان انقضا (expiry) باشد. این موضوع باعث میشود که permit ها برای زمان مشخصی باقی بمانند.
require(expiry == 0 || now <= expiry, "Dai/permit-expired");
همچنین بررسی میکنیم که امضا با nonce موردنظر تاکنون استفاده نشده باشد تا هر امضا فقط یک بار استفاده شود.
require(nonce == nonces[holder]++, "Dai/invalid-nonce");
کار تمام است. dai.sol اجازه کامل holder برای spender را ارائه میدهد و این رویداد را حذف میکند.
uint wad = allowed ? uint(-1) : 0;
allowance[holder][spender] = wad;
emit Approval(holder, spender, wad);
ایجاد امضای برون زنجیرهای
با اندکی صبر و ممارست میتوان در زمینه ایجادکردن امضا به مهارت رسید. ما عملکرد قرارداد هوشمند در permit را در سه مرحله توضیح خواهیم داد:
۱- تولید DOMAIN_SEPARATOR
۲- تولید digest
۳- ایجاد امضای قرارداد
تابع زیر، DOMAIN_SEPARATOR را ایجاد خواهد کرد. این همان کدی است که در کانستراکتور dai.sol و هم چنین در جاوااسکریپت وجود دارد و keccak256, defaultAbiCoder و toUtfBytes از ethers.js استفاده میکند. این تابع به اسم و آدرس اجرا و آیدی زنجیره نیاز دارد و نسخه توکن را به طور پیش فرض بر روی 1 در نظر میگیرد.
تابع زیر، برای فراخوانی permit خاص، digest ایجاد خواهد کرد. توجه داشته باشید از holder و spender و expiry صرفنظر شده و به صورت پیشفرض در حالت true در نظر گرفته شدهاند، در غیر اینصورت امضا مورد قبول واقع نخواهد شد.
پس از دستیابی به digest، امضای آن نسبتا آسان است. فقط کافی است پس از حذف پیشوند 0x از digest، از ecsign موجود در ethereumjs-util استفاده کنیم. برای انجام این کار به کلید خصوصی کاربر نیاز داریم.
در این کد، توابع را به صورت زیر فراخوانی خواهیم کرد:
به این موضوع توجه کنید که فراخوانی permit چگونه از تمام پارامترهایی که در ایجاد digest استفاده شده بود مجددا استفاده میکند. فقط در این صورت، امضا معتبر خواهد بود.
به این موضوع نیز توجه کنید که فقط دو تراکنش در این اسنیپت (snippet) توسط user2 فراخوانی میشود. user1 همان holder و فردی است که digest را ایجاد و آن را امضا میکند. هرچند، user1 گس خرج نمیکند. user1 امضا را به user2 میدهد که آن را برای اجرای تابع permit و transferFrom استفاده میکند. از دید user1 این تراکنش بدون کارمزد گس است.
نتیجهگیری
در این مقاله توضیح دادیم که چگونه از تراکنش بدون کارمزد گس استفاده کنیم و بیان کردیم که تراکنش بدون کارمزد گس در واقع محول کردن پرداخت گس به فرد دیگری است. برای انجام این کار به تابعی در قرارداد هوشمند نیاز داریم که آماده سروکار داشتن با تراکنشهای از پیش امضاشده و مقدار زیادی تغییرات اطلاعات برای ایمن ساختن این فرآیند است.
هرچند، استفاده از این الگو مزایای بسیار زیادی دارد و به همین دلیل به طور گسترده از آن استفاده میشود. امضاها، هزینه گس را از کاربر به ارائهدهنده خدمات محول میکند و در بسیاری از موارد، موانع قابل توجهی را از بین میبرد. همچنین پیادهسازی الگوهای وکالتی پیچیدهتر را اغلب با بهبودهای قابل توجهی در تجربه کاربری امکانپذیر میسازد.