برنامه نویسی سالیدیتی : استاندارد ERC721 (بخش 3)

تا به حال در این مجموعه آموزشی که توسط Andrew Parker از medium.com تهیه شده، درباره اساس موارد غیرقابل‌‌تعویض (Non-Fungibles) و ERC721 صحبت کرده‌‌ایم و سپس نگاهی به رابط استاندارد و برخی از نیازهای آن داشته‌‌ایم. در این مقاله، تصمیماتی برای طراحی قرارداد ERC721 گرفته و نوشتن آن را آغاز می‌‌کنیم.

0 125

گزینه‌‌های طراحی، مقیاس پذیری و امنیت

همانطور که در بخش 1 این مجموعه بحث کردیم، استاندارد ERC721 برای قراردادهایی است که دارایی‌‌های غیرقابل‌‌تعویض (Non-Fungibles) در بلاکچین‌‌ اتریوم را مدیریت می‌‌کند. دارایی‌‌هایی که توکن‌‌های ERC721 شما بیانگر آن‌‌ها هستند، روی برخی از گزینه‌‌های طراحی برای نحوه عملکرد قرارداد شما تاثیر خواهد گذاشت که بیشترین آن بر روی نحوه ساخت توکن‌‌ها خواهد بود.

به عنوان مثال در بازی Cryptokitties، بازیکنان می‌‌توانند بچه‌‌گربه‌‌های خود را به دنیا بیاورند که این کار کیتی‌‌ها (توکن‌‌های) جدیدی ایجاد می‌‌کند. هرچند اگر توکن‌‌های ERC721 شما بیانگر موارد قابل‌‌تعویض بیشتری مثل بلیط‌‌های کنسرت باشند، شاید نخواهید که دارندگان توکن بتوانند توکن‌‌های بیشتری تولید کنند. در برخی موارد، شاید حتی بخواهید دارندگان توکن، توکن‌‌های خود را بسوزانند و آن‌‌ها را نابود کنند!

استاندارد ERC721 هیچ محدودیتی ندارد و مهم نیست که توکن‌‌ها چگونه به وجود می‌‌آیند یا سوزانده می‌‌شوند یا اینکه چه کسی می‌‌تواند آن‌‌ها را بسازد، پس می‌‌توانید این تصمیمات را بر اساس نیازهای خود بگیرید.

تصمیمات خود را بگیرید

در قرارداد من، برای مقیاس پذیری و سادگی، توکن‌‌ها به دو روش ساخته خواهند شد:

1- عرضه اولیه توکن‌‌ها طی ساخت توکن تعریف خواهد شد که تمام موارد از ابتدا به سازنده قرارداد تعلق خواهد داشت.

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

خواهید دید که این مسئله چگونه به موضوع مقیاس پذیری کمک خواهد کرد. همانند سوزاندن، عملکردی وجود خواهد داشت که توکن مورد نظر را بسوزاند و این عملکرد فقط توسط سازنده (یا اپراتور دارای مجوز) قابل اجرا خواهد بود.

مقیاس پذیری

از آنجایی که هر توکن، یک مالک دارد، برای پیگیری مالکیت توکن، یک mapping خواهم داشت:

 

استاندارد می‌‌گوید که توکن‌‌ها نمی‌‌توانند به آدرس صفر تعلق داشته باشند (0x0)، بنابراین در mapping مالکان، می‌‌توانیم عقب‌‌نشینی 0x0 را به سازنده قرارداد واگذار کنیم. هنگامی که درباره عملکرد ownerOf می‌‌نویسیم، جزییات بیشتری را خواهم گفت، اما این به آن معنا است که اگر ما 10 توکن توزیع کنیم، مجبور نیستیم صریحا مالکیت تک تک سازندگان قرارداد را تنظیم کنیم.

برای ساده کردن مسائل، چون که توکن‌‌ها به عقب‌‌نشینی 0x0 سازنده قرارداد تعلق دارند، من اجازه نخواهم داد تا قرارداد توکن به مالک دیگری منتقل شود.

امنیت

آخرین چیزی که پیش از شروع باید به آن بپردازیم و شدیدا هم مهم است، محافظت در مقابل سرریز است. اگر نمی‌‌دانید سرریز چیست باید بگویم که سرریز دارای عدد صحیحی با حداکثر و حداقل ارزش درون آن می‌‌باشد. در مورد uint256، ارزش حداقل آن 0 و حداکثر آن 115792089237316195423570985008687907853269984665640564039457584007913129639935 خواهد بود.

اگر یک uint256 دارید که ارزش 0 دارد و 1 از آن کم می‌‌کنید، ارزش آن همین عدد دیوانه‌‌کننده خواهد شد. و اگر به آن 1 اضافه کنید، بار دیگر 0 خواهد شد. اساسا uint را نمی‌‌توان بالاتر از حدی شمرد و بار دیگر از پایین شروع می‌‌شود. چنین رفتاری را هنگام ضرب دو عدد که نتیجه‌‌ای بالاتر از ارزش حداکثری یک متغیر دارند، مشاهده می‌‌کنید.

اگر مراقب نباشید، افراد مخرب (یا نادان) می‌‌توانند با استفاده از کد شما، این رفتار را برای تمام کارهای ناشایست خود انجام دهند. هفته پیش تعدادی از توکن‌‌های ERC20 به واسطه ضعف در قراردادهای توکنی مرتبط با بحث سرریز، از خرید و فروش در تعدادی از صرافی‌‌ها، حذف شدند. یک فرد باهوش راهی پیدا کرد تا برای خود تعداد بسیار زیادی توکن ارسال کند و سپس هم این توکن‌‌ها را فروخت! افرادی که درون ماجرا بودند، بی‌‌نظمی عجیبی را تجربه کردند.

اما ناامید نشوید، افراد مهربان در OpenZeppelin  کتابچه‌‌ای نوشته‌‌اند تا از چنین حملاتی جلوگیری کنند. SafeMath.sol برای بررسی دوباره نسبت به عدم رخداد سرریز حین محاسبات مهم مورد استفاده قرار می‌‌گیرد. با اعلام استفاده از SafeMath برای uint256 بر روی قراردادتان، چنین محاسباتی خواهید داشت:

c = a + b

که با وجود خطر سرریز:

;(c = a.add(b

که در صورت سرریز، خارج خواهد شد.

این مسئله به نسبت افزودن عادی، کمی گران‌‌تر است و باید اعمال SafeMath را برای هنگامی که احتمال سرریز وجود دارد نگه داریم (از ورودی غیرقابل‌‌پیش‌‌بینی کاربر شامل قرارداد مالک یا صاحب!).

نکته: تنها باری که من از SafeMath در این قرارداد استفاده می‌‌کنم، برای توکن‌‌های اضافی است. پس اگر قرارداد شما به گونه‌‌ای متفاوت طراحی شده، شاید نتوانید از این کتابچه استفاده کنید؛ هرچند داشتن آن هنوز هم یک گزینه مفید در جعبه ابزارتان خواهد بود.

قرارداد

بالاخره آماده نوشتن قراردادمان هستیم. ما از سالیدیتی 0.4.22 استفاده خواهیم کرد و من موارد وابسته آن را فهرست خواهم کرد:

 

یک نکته سریع درباره ERC721.sol (واسطه استاندارد ERC721)، من کمی تغییرات به قابلیت تغییر و قابلیت دید برخی از عملکردها داده‌‌ام. اما همانطور که در مقاله قبلی توضیح دادم، این مسئله توسط استاندارد مجاز است. تغییرات بعدی را توضیح خواهم داد و تمام فایل‌‌ها در GitHub من موجود است.

سپس قراردادمان را اعلام می‌‌کنیم، در ذهن داشته باشید که رابط ERC721 و بکارگیری ERC165 از جمله uint256 را گسترش دهیم.

 

متغیرها

اعلام متغیرهای قرارداد در بالای قراردادتان، کار بسیار خوبی است، زیرا خواندن آن را آسان‌‌تر می‌‌کند. من تمام آن‌‌ها را به طور خلاصه توضیح خواهم داد، اما هنگامی که از آن‌‌ها استفاده کنیم، توضیحات دقیق‌‌تری ارائه خواهم کرد.

نکته: در حال حاضر از قابلیت‌‌دید داخلی به جای خصوصی استفاده می‌‌کنم تا قرارداد توکنی‌‌مان گسترش یابد (مثل متغیرهای Metadata و قابل‌‌شمارش) و به آن‌‌ها دسترسی داشته باشیم.

 

سازنده (constructor)

حالا سازنده را تعریف خواهیم کرد. بسته به کاری که توکن شما باید انجام دهد، سازنده شما با سازنده من متفاوت خواهد بود. اما باز هم باید ERC165 را قرار دهید. سپس توضیح مختصری درباره آن خواهم داد.

 

ممکن است maxId = _initialSupply را دیده‌‌باشید و فورا با خود فکر کرده‌‌ باشید که آیا این یک اشتباه چاپی است؟ یا آیا tokenIds با 1 شروع می‌‌شود و چرا با 0 شروع نشود؟ کدام دیوانه‌‌ای با 1 شروع می‌‌کند؟!

نه، این اشتباه تایپی نیست. دلیل اینکه tokenIds را از 1 آغاز می‌‌کنم این است که 0 هنگام اضافه کردن لایه‌‌های اضافی به قراردادتان، کمی راحت‌‌تر است (فراتر از محدوده استاندارد ERC721). از آنجایی که پیش‌‌فرض uints 0 است و استفاده از عمل حذف در یک متغیر، سبب بازگشت گس می‌‌شود، یک روش کوتاه برای نامعتبر بودن توکن خواهد بود. و در واقع tokenId یک شاخص نیست. استاندارد فقط نیاز دارد که هر توکن، uint منحصر به فرد خود را داشته باشد که هرگز تغییر نکند و برای آن مهم نیست که شما چگونه آن را تعیین می‌‌کنید. نقل قول این قسمت از استاندارد:

با اینکه برخی از قراردادهای هوشمند ERC-721 نسبت به شروع با ID 0 و برای هر NFT جدید علاقه‌‌مند هستند، فراخوان‌‌دهندگان نباید فرض کنند که شماره‌‌های ID الگوی مشخصی برای آن‌‌ها دارند و MUST با ID به عنوان یک جعبه سیاه رفتار می‌‌کند.

ممکن است متوجه شده باشید که برای interfaceId من از الگوی (bytes4(keccak256 برای عملکرد امضاها استفاده کردم. این عمل به این خاطر است که واسطه استاندارد ERC721، بحث safeTransferFrom را سرریز می‌‌کند و فراخواندن این .safeTransfeFrom.selector سبب TypeError خواهد شد.

توکن‌‌های معتبر

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

 

این مسئله هنگامی کاذب می‌‌شود که _tokenId صفر باشد، بالاتر از maxId باشد یا به یک توکن سوخته پاسخ دهد. اگر توکن شما اجازه سوزاندن نمی‌‌دهد، می‌‌توانید بخش [burned[_tokenId!   && را حذف کنید.

تراز و مالکین

حالا بیایید دو عملکرد اساسی دیگر را بررسی کنیم، balanceOf و ownerOf. آن‌‌ها شدیدا ساده هستند؛ یکی تراز آدرس مشخص و دیگر مالک یک توکن مشخص را باز می‌‌گرداند.

balanceOf ساده است و فقط ارزش mapping تراز ما را می‌‌خواند:

 

OwnerOf کمی پیچیده‌‌تر است:

 

پیش از اینکه بررسی کنیم که آیا توکن معتبر داریم، ابتدا از عملکرد isValidToken خود استفاده می‌‌کنیم. از آنجایی که من توکن‌‌ها را با مالک0x0  و برعکس سازنده قرارداد، ایجاد می‌‌کنم، باید این مسئله را هم بررسی کنم. اما بسته به طراحی قرارداد شما، می‌‌توانید این بررسی را حذف کنید و فقط توکن‌‌آی‌‌دی مالک را باز گردانید.

توجه کنید که من قابلیت‌‌دید اینجا و واسطه را از خارجی به عمومی تبدیل کرده‌‌ام. این مسئله به این خاطر است که عملکرد ownerOf سبب فراخوانی عادلانه در قراردادمان می‌‌شود و حتی اگر خارجی هم نباشد، ارزان‌‌تر خواهد بود.

توزیع و سوزاندن

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

 

توجه کنید که در هر دو مورد اضافه‌‌کردن، من از عملکرد اضافه از SafeMath استفاده کرده‌‌ام. در غیر اینصورت، مقادیر بزرگی برای _extraTokens می‌‌تواند سبب هر نوع خرابی دیگری هم بشود.

 

زیاد نگران قانون یا مجوز نباشید. این مطلب را در مقاله بعدی توضیح خواهم داد.

جمع‌‌بندی

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

در مقاله بعدی، با افزودن تاییدیه‌‌ها، اپراتورها و متغیرهای متعدد عملکرد انتقال، قراردادمان را به پایان خواهیم رساند.

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

ارسال پاسخ

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