استاندارد ERC721 که با نام ERC-721 نیز شناخته میشود، یکی از پرکاربردترین استانداردهای شبکه اتریوم برای پیادهسازی توکنهای غیرقابل تعویض (NFT) به شمار میرود. اگر بهدنبال یک آموزش استاندارد ERC721 در سالیدیتی هستید، این مقاله به شما کمک میکند با مفاهیم پایه تا نکات پیشرفته آن آشنا شوید. این استاندارد با اختصاص یک عدد منحصربهفرد به هر آدرس، مالکیت آن توکن را بهصورت مستقیم به آن آدرس نسبت میدهد؛ یعنی اگر یک آدرس عدد مشخصی را در اختیار داشته باشد، آن عدد بهعنوان یک NFT شناخته میشود که متعلق به همان آدرس است.
با وجود منابع و آموزشهای فراوان درباره این استاندارد، بسیاری از برنامه نویسان—حتي افراد باتجربه—همچنان درک کاملی از جزئیات فنی و بهویژه نکات امنیتی آن ندارند. به همین دلیل، در این آموزش تصمیم گرفتیم استاندارد ERC721 را بهگونهای مستند کنیم که تمرکز اصلی آن روی بخشهایی باشد که اغلب از دید توسعهدهندگان حرفهای نیز پنهان میمانند.
چرا NFT ها منحصربهفرد هستند؟
هر NFT را با سه مقدار مشخص میشناسیم:
-
شناسه زنجیره (chain ID)
-
آدرس قرارداد (contract address)
-
شناسه توکن (token id)
وقتی شما یک NFT دارید، در واقع مالک عددی از نوع uint256
هستید که یک قرارداد مبتنی بر استاندارد ERC721 آن را روی یکی از زنجیرههای EVM ذخیره کرده است.
در ادامه، توابع اصلی و مکملی را بررسی میکنیم که رفتار استاندارد ERC721 را تعریف میکنند:
-
ownerOf: مپينگ مربوط به مالکیت
-
mint: ایجاد توکن
-
transferFrom: انتقال مالکیت
-
balanceOf: شمارش تعداد توکنها
-
setApprovalForAll و isApprovedForAll: واگذاری حق انتقال به آدرس دیگر
-
approve و getApproved: تاییدیه اختصاصی برای یک توکن خاص
-
safeTransferFrom و _safeMint: انتقال امن توکن
-
burn: حذف یا نابودسازی توکن
مفهوم مالکیت و تابع ownerOf
در ERC721
در استاندارد ERC721، مفهوم مالکیت با یک mapping ساده پیادهسازی میشود: ownerOf(uint256 id)
در واقع، این استاندارد فقط یک ارتباط ساده بین یک شناسه عددی (uint256
) و آدرس مالک ایجاد میکند. با وجود تمام تبلیغاتی که درباره NFTها شنیدهاید، ساختار واقعی آنها چیزی جز یک mapping ساده نیست. وقتی میگوییم شما مالک یک NFT هستید، منظورمان این است که در mapping مربوطه، شناسه آن NFT به آدرس شما نگاشت (mapping) شده است. و تمام.
طبق مشخصات استاندارد، هر قرارداد ERC721 باید تابعی عمومی ارائه دهد که با دریافت شناسه توکن، آدرس مالک آن را برگرداند.
برای سادهسازی، توسعهدهندگان میتوانند بهجای تعریف یک تابع، از یک متغیر عمومی از نوع mapping استفاده کنند. از نگاه بیرونی، هر دو گزینه رفتاری مشابه دارند.
1 2 3 |
contract ERC721 { mapping(uint256 => address) public ownerOf; } |
ownerOf
یک mapping
عمومی است که با دریافت شناسه توکن، آدرس مالک آن را بازمیگرداند.
ساخت توکن با استفاده از تابع mint
در زبان سالیدیتی، هر mapping
بهصورت پیشفرض مقدار صفر را برای کلیدهایی که مقداردهی نشدهاند نگه میدارد. بنابراین، تا زمانی که توکنی ساخته نشده باشد، شناسه آن به آدرس صفر (address(0)
) نسبت داده میشود. البته این مقدار به معنای مالکیت نیست. وقتی تابع ownerOf
مقدار address(0)
را برمیگرداند، یعنی توکن مورد نظر هنوز ساخته نشده و در واقع وجود ندارد.
فرآیند mint کردن همان سازوکاری است که با استفاده از آن، برای اولین بار یک توکن ساخته میشود و به یک آدرس خاص تعلق میگیرد.
تابع mint
بخشی از استاندارد ERC721 نیست. توسعهدهندگان باید آن را با توجه به نیازهای پروژه خود پیادهسازی کنند. در این استاندارد الزامی وجود ندارد که توکنها به ترتیب (مثل ۰، ۱، ۲ و …) ایجاد شوند. توسعهدهنده میتواند از هر الگوریتمی برای تولید شناسهها استفاده کند، مثلاً با هش کردن شماره بلاک و آدرس گیرنده.
در نمونه پیادهسازی زیر، هر کاربر میتواند یک توکن با شناسه دلخواه mint کند، بهشرط آنکه شناسه مورد نظر قبلاً ساخته نشده باشد:
1 2 3 4 5 6 7 8 9 10 11 12 |
contract ERC721 { mapping(uint256 id => address owner) public ownerOf; event Transfer(address indexed from, address indexed to, uint256 indexed id); function mint(address recipient, uint256 id) public { require(ownerOf[id] == address(0), "already minted"); ownerOf[id] = recipient; emit Transfer(address(0), recipient, id); } } |
Transfer
از آدرس صفر به آدرس گیرنده در ابتدا عجیب به نظر برسد، اما این رفتار دقیقاً مطابق با استاندارد ERC721 تعریف شده است. با این کار، قرارداد اعلام میکند که یک توکن جدید از حالت «عدم وجود» خارج شده و وارد چرخه مالکیت شده است.
انتقال NFT با استفاده از تابع transferFrom
در استاندارد ERC721
در اکثر موارد، کاربران انتظار دارند بتوانند NFTهای خود را به آدرس دیگری منتقل کنند. برای انجام این کار، قرارداد تابع transferFrom
را در اختیار قرار میدهد.
در مثال زیر، نسخهای ساده از تابع transferFrom
پیادهسازی شده تا انتقال مستقیم مالکیت یک توکن را امکانپذیر کند:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
contract ERC721 { mapping(uint256 id => address owner) public ownerOf; event Transfer(address indexed from, address indexed to, uint256 indexed id); // mint در اینجا برای خوانایی حذف شده است function transferFrom(address from, address to, uint256 id) external payable { require(ownerOf[id] == msg.sender, "not allowed to transfer"); ownerOf[id] = to; emit Transfer(from, to, id); } } |
شاید در نگاه اول عجیب باشد که تابع transferFrom
بهصورت payable
تعریف شده، اما این ویژگی دقیقاً در استاندارد ERC721 مشخص شده است. به نظر میرسد این امکان را برای اپلیکیشنهایی فراهم کردهاند که هنگام انتقال یک NFT (که قبلاً ساخته شده)، نیاز به دریافت مبلغی از نوع اتر داشته باشند. البته در عمل، بسیاری از پیادهسازیها این قابلیت را نادیده میگیرند چون در واقعیت بهندرت مورد استفاده قرار میگیرد.
سؤال رایج دیگری که ممکن است ذهن شما را درگیر کند این است که چرا پارامتر from
در این تابع وجود دارد، در حالی که فعلاً تنها زمانی اجازه انتقال داریم که msg.sender
با آدرس مالک برابر باشد؟ پاسخ دقیق این موضوع را در بخش مربوط به مکانیزم تأییدیهها (approvals) بررسی خواهیم کرد. اما فعلاً کافی است بدانید که در این پیادهسازی ساده، فقط مالک توکن میتواند آن را منتقل کند.
درک عملکرد تابع balanceOf
در استاندارد ERC721
استاندارد ERC721 توسعهدهنده را ملزم میکند تا تعداد NFTهایی که هر آدرس در قرارداد مالک آنهاست، پیگیری کند.
برای این کار، قرارداد از یک mapping
با ساختار زیر استفاده میکند:
1 |
mapping(address owner => uint256 balances) balanceOf; |
با افزودن این mapping
، قرارداد اکنون میتواند تعداد توکنهای متعلق به هر آدرس را مشخص کند. تابع balanceOf
این قابلیت را در اختیار کاربران قرار میدهد.
نکته مهم اینجاست که balanceOf
فقط تعداد توکنهای یک آدرس را برمیگرداند و اطلاعاتی درباره شناسه آنها ارائه نمیدهد. برای مثال، اگر آدرسی مالک ۳ توکن باشد، مقدار ۳ را دریافت میکنیم، اما نمیفهمیم کدام شناسهها در اختیار او هستند.
برای اطمینان از صحت این مقدار، باید در تمام توابعی که میتوانند مالکیت توکن را تغییر دهند—یعنی mint
و transferFrom
—مقدار balanceOf
را نیز بهروزرسانی کنیم. (در تصوير زير هايلايت شده اند)
یکی از نکات بسیار مهمی که باید در نظر داشته باشید این است که مالک یک NFT در هر لحظه میتواند توکنهای خود را منتقل کند. به همین دلیل، زمانی که در منطق قرارداد هوشمند به تابع balanceOf
تکیه میکنید، باید با دقت و احتیاط عمل کنید.
تابع balanceOf()
را نباید بهعنوان یک مقدار ایستا یا تغییرناپذیر در نظر گرفت؛ چرا که این مقدار در جریان یک تراکنش میتواند تغییر کند. برای مثال:
-
مالک ممکن است یک NFT را از یکی از آدرسهای خودش به آدرس دیگری که خودش کنترل میکند منتقل کند.
-
یا حتی همان NFT را دوباره به همان آدرس فعلی انتقال دهد.
در هر دو حالت، مقدار برگشتی تابع balanceOf()
تغییر میکند و ممکن است منطق برنامه نویسی قرارداد را دچار خطا یا حتی آسیبپذیری کند.
اگر قصد دارید از balanceOf
برای اعمال شرط یا محدودیت در قرارداد هوشمند استفاده کنید، باید خطرات مربوط به تغییر همزمان مالکیت را در نظر بگیرید و کد را بهگونهای طراحی کنید که امکان سوءاستفاده از این ویژگی وجود نداشته باشد.
تاییدیههای نامحدود با توابع setApprovalForAll
و isApprovedForAll
در ERC721
استاندارد ERC721 این امکان را فراهم کرده است که مالک یک NFT، بدون انتقال مالکیت آن، کنترل توکن را به آدرس دیگری واگذار کند. اولین ابزار برای انجام این کار، تابع setApprovalForAll()
است. همانطور که از نام آن پیداست، این تابع به آدرس دیگر اجازه میدهد که به نمایندگی از مالک، تمام NFTهای او را منتقل کند.
تابع مکمل isApprovedForAll()
بررسی میکند که آیا آدرسی خاص، که در اینجا اپراتور نام دارد، از طرف مالک مجوز دریافت کرده است یا نه.
یک مالک میتواند چندین اپراتور تعریف کند. این قابلیت به بازارهای NFT این امکان را میدهد که یک توکن را بهصورت همزمان در چند پلتفرم برای فروش قرار دهند. زمانی که بازار مورد نظر بهعنوان اپراتور برای مالک تایید شده باشد، میتواند در صورت دریافت مبلغ مناسب از خریدار، توکن را به او منتقل کند.
از این پس، تابع transferFrom
این قابلیت را فراهم میکند که هم خود مالک و هم آدرسی که از طریق isApprovedForAll
تأیید شده است بتوانند توکن را منتقل کنند.
تاییدیه برای یک توکن خاص با استفاده از توابع approve
و getApproved
در ERC721
بهجای آنکه یک آدرس را برای مدیریت همه NFTهای خود تأیید کنید، میتوانید فقط برای یک شناسه خاص (id) این اجازه را صادر کنید. این روش معمولاً ایمنتر است و به شما کنترل دقیقتری میدهد. تاییدیه صادرشده در mapping
عمومی به نام getApproved()
ذخیره میشود.
بر خلاف تابع isApprovedForAll
، در این نوع تاییدیه هیچ ارتباطی با آدرس مالک وجود ندارد و مجوز فقط به شناسه توکن خاص مربوط میشود.
پس از انتقال توکن، معمولاً مالک جدید نمیخواهد که فرد دیگری همچنان اجازه انتقال آن شناسه را داشته باشد. به همین دلیل، تابع transferFrom
باید بهگونهای نوشته شود که تاییدیه مربوط به آن شناسه را پاکسازی کند.
یکی از محدودیتهای تابع approve
این است که تنها میتوان یک آدرس را برای هر شناسه توکن تأیید کرد. اگر بخواهید چندین آدرس را برای یک توکن خاص مجاز کنید، حذف همه آنها هنگام انتقال میتواند از نظر مصرف گس هزینهبر باشد.
همچنین توجه داشته باشید که اگر یک آدرس از طریق setApprovalForAll
بهعنوان اپراتور تأیید شده باشد، میتواند برای توکنهایی که متعلق به آدرس تحت پوشش او هستند، آدرسهای دیگری را هم تأیید کند. این رفتار بخشی از طراحی استاندارد است و عملکرد تابع setApprovalForAll()
تغییری نمیکند.
پس از انتقال مالکیت، تاییدیههای قبلی پاک میشوند، زیرا معمولاً مالک جدید نمیخواهد آدرس قبلی همچنان اجازه دسترسی به آن شناسه را داشته باشد.
در این مرحله، تقریباً تمام توابع اصلی مورد نیاز استاندارد ERC721 را پیادهسازی کردهایم. البته برخی از توابع باقیمانده به مستندات و توضیحات گستردهتری نیاز دارند.
شناسایی NFTهای تحت مالکیت يک آدرس بدون استفاده از افزونه Enumerable
چطور میتوان فهرستی از شناسههای NFT که یک آدرس در اختیار دارد تهیه کرد؟
با استفاده از توابعی که تاکنون بررسی کردهایم، متأسفانه هیچ روش کارآمدی برای شناسایی توکنهای متعلق به یک آدرس خاص وجود ندارد.
تابع balanceOf
فقط تعداد توکنهای یک آدرس را برمیگرداند، و تابع ownerOf
فقط نشان میدهد که مالک یک شناسه خاص چه کسی است. اگر بخواهیم بفهمیم کدام شناسهها به یک آدرس تعلق دارند، باید روی تمام شناسهها حلقه بزنیم و برای هرکدام ownerOf
را صدا بزنیم. این روش بسیار پرهزینه و غیربهینه است.
در واقع، تا زمانی که از افزونه Enumerable استفاده نکنیم، هیچ راه بهینهای برای شناسایی NFTهای متعلق به یک آدرس بهصورت کاملاً درونزنجیرهای (on-chain) در اختیار نداریم.
اما تا زمانی که از این افزونه استفاده نکردهایم، چطور باید چنین اطلاعاتی را در اختیار قرارداد بگذاریم؟
اگر یک قرارداد لازم داشته باشد بررسی کند که آدرس 0xc0ffee...
مالک شناسههای ۵، ۷ و ۲۱ است، بهترین روش این است که خود کاربر این اطلاعات را اعلام کند، سپس قرارداد صحت این ادعا را تأیید کند.
بهعبارت دیگر، باید آرایهای از شناسهها به قرارداد ارسال شود و قرارداد با استفاده از ownerOf
بررسی کند که آن آدرس واقعاً مالک آنهاست.
1 2 3 4 5 6 |
function checkOwnership(uint256[] calldata ids, address claimedOwner) public { for (uint256 i = 0; i < ids.length; i++) { require(nft.ownerOf(ids[i]) == claimedOwner, "not the claimed owner"); } // ادامه منطق قرارداد } |
اما خارج از زنجیره (off-chain) چگونه میتوانیم بهصورت کارآمد تشخیص دهیم که آدرس 0xc0ffee...
مالک شناسههای ۵، ۷ و ۲۱ است؟
یکی از روشهای ساده این است که روی همه شناسهها حلقه بزنیم و برای هر کدام تابع ownerOf()
را صدا بزنیم. اما این روش بسیار پرهزینه است و باعث میشود ارائهدهنده RPC شما، هزینه زیادی بابت این درخواستها دریافت کند.
پردازش رویدادهای ERC721
برای شناسایی مالکیت NFTها، میتوانیم بهجای فراخوانی مستقیم ownerOf()
، رویدادهای Transfer
را اسکن کنیم. این رویدادها هنگام ایجاد، انتقال یا حتی سوزاندن توکنها منتشر میشوند و اطلاعاتی از جمله آدرس فرستنده، گیرنده و شناسه توکن را ثبت میکنند.
در لینک زیر، نمونه کدی با استفاده از web3.js
ارائه شده است که با اسکن این رویدادها، لیست NFTهای تحت مالکیت هر آدرس را استخراج میکند:
https://gist.github.com/RareSkills/5d60ad42cdd81b6e136605a832ba59ee
توجه داشته باشید که این کد تمام بلاکها را از بلاک شماره صفر اسکن میکند، که رویکرد بهینهای نیست. برای بهبود عملکرد، میتوانید نقطه شروع مناسبی بر اساس زمان راهاندازی قرارداد خود انتخاب کنید.
انتقال امن: توابع safeTransferFrom
، _safeMint
و onERC721Received
توابع safeTransferFrom
و _safeMint
بهگونهای طراحی شدهاند که از گیر افتادن توکنها در قراردادهایی که توانایی مدیریت آنها را ندارند جلوگیری کنند. اگر یک NFT به قراردادی منتقل شود که امکان فراخوانی transferFrom
را نداشته باشد، آن توکن عملاً در آن قرارداد قفل میشود و دیگر قابل استفاده نخواهد بود.
برای پیشگیری از این وضعیت، استاندارد ERC721 تنها اجازه میدهد توکن به قراردادهایی منتقل شود که بتوانند آن را دریافت و در صورت لزوم منتقل کنند. یک قرارداد زمانی توانایی مدیریت NFT را دارد که تابع onERC721Received()
را پیادهسازی کرده باشد و مقدار خاص 0x150b7a02
را برگرداند.
این مقدار همان selector تابع onERC721Received()
است؛ شناسه داخلیای که سالیدیتی برای تشخیص توابع از آن استفاده میکند.
ساختار این تابع در قالب رابط زیر تعریف شده است:
1 2 3 4 5 6 7 8 |
interface IERC721Receiver { function onERC721Received( address operator, address from, uint256 tokenId, bytes calldata data ) external returns (bytes4); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; contract MinimaExample is IERC721Receiver { function onERC721Received( address operator, address from, uint256 tokenId, bytes calldata data ) external returns (bytes4) { return IERC721Receiver.onERC721Received.selector; // returns 0x150b7a02 } } |
safeTransferFrom
از نظر عملکرد مشابه transferFrom
است؛ اما در پشت صحنه، پس از انتقال توکن، بررسی میکند که گیرنده یک آدرس معمولی (EOA) است یا یک قرارداد هوشمند. برای انجام این تشخیص، از روشی استفاده میشود که در مطلب تشخیص قرارداد هوشمند در سالیدیتی بهطور کامل توضیح دادهایم.
-
اگر گیرنده یک آدرس معمولی باشد، انتقال بدون هیچ تغییری انجام میشود.
-
اگر گیرنده یک قرارداد باشد، تابع
onERC721Received()
با پارامترهای مشخصشده روی قرارداد گیرنده فراخوانی میشود. -
اگر این فراخوانی با خطا مواجه شود یا مقدار بازگشتی برابر با
0x150b7a02
نباشد، تراکنش revert میشود.
چرا بررسی مقدار بازگشتی تابع (function selector
) ضروری است؟
اینکه onERC721Received()
بدون خطا اجرا شود، بهتنهایی کافی نیست تا مطمئن شویم قرارداد گیرنده توانایی مدیریت درست توکن ERC721 را دارد.
فرض کنید یک NFT به قراردادی منتقل شود که فقط یک تابع fallback دارد. در این حالت، اگر مقدار بازگشتی بررسی نشود، ممکن است تراکنش بدون خطا پیش برود، ولی در واقعیت، قرارداد گیرنده هیچ مکانیزمی برای نگهداری یا انتقال مجدد NFT نداشته باشد. صرفاً داشتن یک تابع fallback بهمعنای سازگاری با استاندارد ERC721 نیست.
آرگومانهای تابع onERC721Received
هنگام فراخوانی onERC721Received()
، پارامترهای زیر به آن ارسال میشوند:
1 2 3 4 5 6 7 8 |
interface IERC721Receiver { function onERC721Received( address operator, address from, uint256 tokenId, bytes calldata data ) external returns (bytes4); } |
operator:
این مقدار برابر است با msg.sender
از دید تابع safeTransfer
. این آدرس میتواند خودِ مالک توکن باشد یا آدرسی که مجوز انتقال آن را دارد.
from:
آدرس مالک اصلی NFT پیش از انتقال است. اگر خود مالک تابع transfer
را فراخوانی کرده باشد، مقدار from
با operator
برابر خواهد بود.
tokenId:
شناسه توکن NFT که در حال انتقال است.
data:
اگر هنگام فراخوانی safeTransferFrom
دادهای همراه با آن ارسال شده باشد، این داده بهصورت مستقیم به قرارداد گیرنده منتقل میشود. بررسی دقیقتر پارامتر data
را در بخش بعدی انجام میدهیم.
ملاحظات امنیتی مربوط به onERC721Received
همواره مقدار msg.sender
را بررسی کنید
در حالت پیشفرض، هر آدرسی میتواند تابع onERC721Received()
را با پارامترهای دلخواه صدا بزند و قرارداد شما را فریب دهد تا فکر کند NFTای دریافت کرده که در واقع ندارد. اگر قرارداد شما از این تابع استفاده میکند، باید بررسی کنید که msg.sender
همان قرارداد ERC721 مورد نظر شما باشد.
خطر حمله بازگشتی (reentrancy) در safeTransfer
توابع safeTransfer
و _safeMint
کنترل اجرای برنامه را به قرارداد خارجی منتقل میکنند. هنگام ارسال NFT به یک آدرس دلخواه با استفاده از safeTransfer
، دقت داشته باشید که گیرنده میتواند هرگونه منطق دلخواه را در تابع onERC721Received()
پیادهسازی کند. این موضوع ممکن است منجر به حمله reentrancy شود. اگر قرارداد شما بهدرستی در برابر این نوع حمله ایمنسازی شده باشد، جای نگرانی نیست.
احتمال شکست در safeTransfer
گیرنده مخرب میتواند با ایجاد خطا در داخل تابع onERC721Received()
یا استفاده از یک حلقه سنگین برای مصرف تمام گس موجود، باعث شود که تراکنش برگشت داده شود. بنابراین، نباید فرض کنید که safeTransferFrom
همیشه با موفقیت به پایان میرسد.
safeTransferFrom
همراه با پارامتر data
و دلیل وجود آن – کاربردهای عملی و بهینهسازی
در استاندارد ERC721 دو نسخه از تابع safeTransferFrom
تعریف شده است:
1 2 3 |
function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable; function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes calldata data) external payable; |
نسخه دوم شامل یک پارامتر اضافی به نام data
است. این پارامتر به ما اجازه میدهد هنگام انتقال، اطلاعاتی اضافی مثل دستورالعملها، مقادیر رأیدهی، آدرس نماینده و غیره را نیز به قرارداد گیرنده ارسال کنیم.
در ادامه مثالی ارائه میشود که نحوه استفاده از data
در ترکیب با تابع onERC721Received()
را نشان میدهد.
استیکینگ بهینه از نظر گس، بدون نیاز به تاییدیه (approval)
یکی از الگوهای رایج در توسعه قراردادهای هوشمند، واریز یک NFT به قرارداد با هدف استیکینگ است. البته در واقعیت، NFT “وارد” قرارداد نمیشود. در عوض، مالکیت توکن (یعنی مقدار ownerOf
برای آن شناسه خاص) به قرارداد استیکینگ منتقل میشود و این قرارداد اطلاعات مربوط به مالک اصلی را در خود ذخیره میکند.
روش مرسومی که برای این کار به کار میرود، در قطعه کد زیر آمده است؛ اما این روش از نظر مصرف گس بهینه نیست، زیرا کاربر باید ابتدا توکن مورد نظر را برای قرارداد استیکینگ تأیید (approve) کند و سپس تابع deposit()
را صدا بزند.
در مثال ارائهشده، قابلیت رأیدهی هنگام استیک کردن نیز اضافه شده تا نشان دهد چگونه میتوان پارامترهای اضافی را در حین انتقال توکن منتقل کرد.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
contract Staking { struct Stake { uint8 voteId; address originalOwner; } mapping(uint256 id => Stake stake) public stakes; function deposit(uint256 id, uint8 _voteId) external { stakes[id] = Stake({voteId: _voteId, originalOwner: msg.sender}); // user must approve Staking contract first nft.transferFrom(msg.sender, address(this), id); } function withdraw(uint256 id) external { require(msg.sender == staked[id].originalOwner, "not original owner"); delete stakes[id]; nft.transferFrom(address(this), msg.sender, id); } } |
روش جایگزینی که از نظر مصرف گس بهمراتب بهینهتر عمل میکند، این است که کاربر مستقیماً از تابع safeTransfer
برای انتقال NFT به قرارداد استفاده کند. با این کار، دیگر نیازی به اجرای مرحلهی approve
نیست و در نتیجه، یک تراکنش صرفهجویی میشود.
البته برای جلوگیری از خطاهای احتمالی، باید این فرآیند از طریق رابط کاربری (frontend) بهدرستی هدایت و مدیریت شود تا تجربه کاربری ساده و بدون ابهام باقی بماند.
در این روش، مقدار vote
در قالب آرگومان data
به قرارداد منتقل میشود و قرارداد میتواند هنگام دریافت NFT، این اطلاعات اضافی را رمزگشایی و پردازش کند.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
contract ImprovedStaking is IERC721Receiver { struct Stake { uint8 voteId; address originalOwner; } mapping(uint256 id => Stake stake) public stakes; function onERC721Received(address operator, address from, uint256 id, bytes calldata data) external { // important safety to check only allow calls from our intended NFT require(msg.sender == address(nft), "wrong NFT"); uint8 voteId = abi.decode(data, (uint8)); originalOwners[id] = from; // from is the original owner } function withdraw(uint256 id) external { address originalOwner = stakes[id].originalOwner; require(msg.sender == originalOwner, "not owner"); delete stakes[id]; nft.transferFrom(address(this), msg.sender, id); } } |
بار دیگر تأکید میکنیم: بررسی msg.sender
در تابع onERC721Received
یک الزام امنیتی است
اگر در تابع onERC721Received
بررسی نکنید که مقدار msg.sender
با آدرس قرارداد NFT مورد انتظار برابر است، هر آدرسی میتواند این تابع را با پارامترهای دلخواه و حتی دادههای مخرب فراخواني کند. این موضوع میتواند امنیت قرارداد شما را بهشدت تهدید کند. به همین دلیل، بررسی دقیق msg.sender
در این تابع کاملاً ضروری است.
مثالی که پیشتر دیدید بهخوبی نشان میدهد که پارامتر data
تا چه اندازه میتواند مفید باشد. نوع دادهای bytes calldata data
این امکان را فراهم میکند که هرگونه اطلاعات دلخواه را در قالب باینری فشردهسازی کرده و هنگام انتقال توکن به قرارداد مقصد ارسال کنیم.
در مثال فعلی، تنها یک مقدار از نوع uint8
بهنام voteId
رمزگشایی شده است. اما میتوانیم پارامترهای بیشتری مانند intendedDuration
و delegate
نیز ارسال کنیم. برای رمزگشایی آنها از کدی مشابه زیر استفاده میشود:
1 |
(voteId, intendedDuration, delegate) = abi.decode(data, (uint8, uint256, address)); |
مقایسه مصرف گس: safeTransferFrom
و _safeMint
در برابر transferFrom
و _mint
اگر مطمئن هستید که گیرنده توکن یک حساب معمولی (EOA) است و نه یک قرارداد هوشمند، بهتر است از توابع transferFrom
یا _mint
استفاده کنید. توابع safeTransferFrom
و _safeMint
ابتدا بررسی میکنند که آیا گیرنده یک قرارداد است یا نه، و این بررسی اضافی در شرایطی که نیازی به آن نیست، تنها باعث اتلاف گس خواهد شد.
تابع burn
و حذف NFT
برای حذف یک NFT، میتوانید آن را به آدرس صفر (address(0)
) منتقل کنید؛ این کار معادل با سوزاندن (burn) توکن محسوب میشود. البته این قابلیت بهصورت رسمی در استاندارد ERC721 گنجانده نشده، بنابراین قراردادها الزاماً نیازی به پشتیبانی از این عملیات ندارند. اگر قصد دارید در پروژه خود امکان حذف توکن را فراهم کنید، باید آن را بهطور جداگانه پیادهسازی کنید.
پیادهسازیهای ERC721
کتابخانهای که OpenZeppelin ارائه داده، یکی از بهترین گزینهها برای توسعهدهندگان تازهکار بهشمار میرود؛ بهویژه زمانی که با مجموعه قراردادهای قابل ارتقا (Upgradeable Contracts) استفاده شود. این کتابخانه امنیت، خوانایی و قابلیت اطمینان بالایی دارد.
در مقابل، توسعهدهندگان حرفهایتر که به دنبال مصرف گس کمتر و عملکرد بالاتر هستند، میتوانند از پیادهسازی Solady ERC721 استفاده کنند. این نسخه بهینهسازیهای پیشرفتهای دارد که در پروژههای بزرگ و پرترافیک بسیار مؤثر است.
راستی! برای دریافت مطالب جدید در کانال تلگرام یا پیج اینستاگرام سورس باران عضو شوید.
- انتشار: ۲۴ اردیبهشت ۱۴۰۴
دسته بندی موضوعات
- آموزش ارز دیجیتال
- آموزش برنامه نویسی
- آموزش متنی برنامه نویسی
- اطلاعیه و سایر مطالب
- پروژه برنامه نویسی
- دوره های تخصصی برنامه نویسی
- رپورتاژ
- فیلم های آموزشی
- ++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
- اچ تی ام ال
- بانک اطلاعاتی
- برنامه نویسی سوکت
- برنامه نویسی موبایل
- پاسکال
- پایان نامه
- پایتون
- جاوا
- جاوا اسکریپت
- جی کوئری
- داده کاوی
- دلفی
- رباتیک
- سئو
- سایر کتاب ها
- سخت افزار
- سی اس اس
- سی پلاس پلاس
- سی شارپ
- طراحی الگوریتم
- فتوشاپ
- مقاله
- مهندسی نرم افزار
- هک و امنیت
- هوش مصنوعی
- ویژوال بیسیک
- نرم افزار و ابزار برنامه نویسی
- وردپرس