بازار توکنهای بیهمتا (NFT) تاکنون بهصورت انفجاری رشد کرده است و همچنان نیز به رشد و تکامل خود ادامه میدهد. پلتفرم اتر اسکن (Etherscan) دارای یک ابزار جستجوی کاربردی است که بهوسیله آن میتوانید علاوهبر استفاده از ویژگیهای تایید و دیکامپایل (Decompile)، کدهای بسیاری از قراردادهای ERC721 را باهم مقایسه کنید. در مقایسه قراردادها میتوان مشاهده کرد که علیرغم طراحی خوب برخی از قراردادها، تعدادی اشتباهات خاص در بسیاری از قراردادها تکرار میشوند. در این مقاله از میهن بلاکچین با ۴ اشتباه رایج در قراردادهای NFT یا بهاصطلاح ضدالگو (Anti-Pattern) که در مقایسه قراردادهای NFT مشاهده شده است، آشنا میشوید.
۴ اشتباه رایج در قراردادهای NFT
در نظر داشته باشید که این مقاله عمدتا درباره بلاکچینهای سازگار با سرویس نام اتریوم (EVM) نوشته شده است، اما نکات آن را میتوان به شبکههای دیگر نیز تعمیم داد. اگر شما نیز مرتکب برخی از این اشتباهات رایج در طراحی شدهاید، این مقاله به شما کمک میکند که از تکرار آن جلوگیری کنید. با توجه به شرایط فعلی و کارمزدهای سرسامآور شبکهها، همه ما اهمیت صرفه جویی در گس (Gas) و کارمزد شبکه را میدانیم.
در حال حاضر اتریوم گرانترین بلاک چین است. پالیگان (Polygon) کمی از اتریوم ارزانتر است و بلاکچینهایی مثل سولانا (فاقد قابلیت EVM) بسیار ارزانتر هستند. اگر سرمایه کافی داشته باشید، پیادهسازی پروژهها با کیفیت بالا مزایایی دارد که به هزینههای اضافی آن میارزد.
ضدالگو یا اشتباه رایج ۱: آوردن اطلاعات مربوط به قیمت، فروش و منطق در قرارداد
این اشتباه بسیار رایج است و قبل از هر چیزی در یک قرارداد به چشم میخورد. البته، انگیزههای منطقی و قابلدرکی در پشت این اشتباه وجود دارد. هزینه استقرار و مدیریت قرارداد در اکثر شبکهها بسیار بالاست و افراد تلاش میکنند تا حد ممکن در این هزینهها صرفهجویی کنند. شاید برای برخی سوال باشد که چرا نباید منطق صدور (ضرب) و فروش را در قرارداد آورد؟
قرار دادن این نوع اطلاعات در قراردادها ایده خوبی نیست. درست است که قرارداد باید مرکز غیرقابلتغییر شبکهای از منطقها باشد، اما نباید بهطور مستقیم پول را مدیریت کند. یعنی اطلاعات فروش، زمانهای فروش، وایت لیست و غیره نباید بهطور مستقیم در کد استقرار ERC721 قرار بگیرد. منطق فروش و منطق هسته بسیار شبیه به هم هستند.
شاید صرفهجویی در هزینه گس، بهترین و منطقیترین دلیل جمعآوری تمام منطقها در یک قرارداد باشد، اما در نگاه کلی دلایل مهمتری برای استفاده نکردن از این روش وجود دارد. منطق قرارداد شما باید ثابت و بدون تغییر باشد و استانداردها را بهخوبی اجرا کند. اکثر کلونها (Clones) تقریبا مثل هم هستند. اطلاعاتی مثل استراتژی صدور و قیمتگذاری باید از منطق اصلی قرارداد جدا شوند. این باعث میشود که قرارداد شما انعطافپذیر باشد و در عین حال به اعتماد کاربر ضربه نزند. باید طراحی مجزا و قاعده تک-وظیفهای (Single-responsibility) داشته باشید. بهتر است عرضه (MaxSupply) در خود قرارداد ERC721 محدود شود و تنها ادمین قابلیت تغییر آن را داشته باشد.
اشتباه رایج ۲: استفاده نکردن از امنیت مبتنیبر نقش
هر قرارداد توکن به نوعی کنترل دسترسی نیاز دارد، چون برخی از عملیاتها (مثل صدور یا تغییر پارامترهای عرضه) فقط باید برای آدرسهای مجاز قابل دسترس باشند. سادهترین راه اجرای این نوع کنترل دسترسی این است که از یک مدل Ownable استفاده کنید (معمولا از قرارداد Ownable OpenZeppelin استفاده میشود چون با یک نیاز ساده روبهروهستیم). اما بنا به دلایلی توصیه میشود که از کنترل دسترسی مبتنی بر نقش (Role-Based Security) استفاده کنید.
از مدل Ownable معمولا بهدلیل سادگی استفاده میشود که در ظاهر تصمیم خوبی است. ممکن است شما یا مشتری شما تنها کسی باشد که برای همیشه قرارداد را مدیریت خواهد کرد. هنگامی که هزینه کم است، افراد پیشبینی و مقابله با حوادث آینده (Future-proofing) را ترجیح میدهند. امنیت نقشمحور (OpenZeppelin) کمی پیچیدهتر و گرانتر از مدل Ownable است. اما اگر هزینه گس زیاد است، میتوانید از کد امنیتی نقشمحور (چه OpenZeppelin و چه مدل خاص شما) فقط برای نیاز موجود استفاده کنید.
دلیل مهمتر برای استفاده از امنیت نقشمحور این است که به شما امکان میدهد تا کاربرد (اطلاعات قیمتگذاری و فروش) را از خود قرارداد ERC721 جدا کنید. این مدل به شما امکان میدهد تا یک قرارداد جداگانهای را بهعنوان صادرکننده (ضربکننده) انتخاب کنید و بدون اینکه مجوز مدیریت کامل را به آن بدهید، نقش «صادرکننده» را به آن اختصاص دهید. در این شرایط، مجوزهای مهم مثل ایجاد تغییر در مجوزها، همچنان در دست ادمینها (که احتمالا انسان هستند) خواهد بود. وقتی که دیگر به صادرکننده نیازی ندارید، میتوانید بهسادگی آن را لغو کنید و نقش صادرکننده آن را بههمراه استراتژی جدید به قرارداد دیگری که ماژولار، مناسبتر و امنتر است، واگذار کنید. عملیات دیگر را نیز میتوان با همین روش و بر اساس کاربردهای پروژه مدیریت کرد.
اشتباه رایج ۳: استفاده نکردن از ERC-165 (Introspection)
اکثر توکنها (یا بهطور کلی قراردادها) یا ERC-165 را بهکار نمیگیرند یا آن را بهطور مناسب اجرا نمیکنند. اهمیت ERC-165 در ایجاد قابلیت همکاری (Interoperability) است. ERC- 165 سازگاری قرارداد شما را افزایش میدهد و احتمالا صرافیها بخواهند ساختار حق امتیاز NFT شما را بدانند. در اکثر قراردادها، ERC- 165 یا اجرا نمیشود یا بهطور نامناسب اجرا میشود. برای اجرای درست این استاندارد به قوانین زیر دقت کنید:
- هر کلاس والدی که ERC-165 را اجرا میکند، باید در فهرست لغو (Override) قرار بگیرد. در این صورت هنگام استفاده از دستور Super.supportsInterface آنها نیز بهطور خودکار فعال خواهند شد.
- هر رابط پیادهسازی دیگری را که در کلاس والد نیست، میتوانید با کلمه an یا با یک جمله به شکل زیر اضافه کنید:
|| type(ISomeInterface).interfaceId == _interfaceId
مثال:
function supportsInterface(bytes4 _interfaceId)
public
view
override(ERC721, ERC721Enumerable)
returns (bool)
{
return super.supportsInterface(_interfaceId) ||
_interfaceId == type(IERC2981).interfaceId;
}
اگر کد شما کلاس والدی ندارد که ERC-165 را اجرا کند، در این صورت باید به نوع دوم اشاره کنید. مثلا:
function supportsInterface(bytes4 _interfaceId)
public
view
override
returns (bool)
{
return _interfaceId == type(IERC721).interfaceId ||
_interfaceId == type(IERC2981).interfaceId ||
_interfaceId == type(IAccessControl).interfaceId;
}
اگر کد شما بهجز ERC-165 اجراشده توسط کلاس والد هیچ رابط دیگری را اجرا نمیکند، نیازی به نوع دوم ندارید. مثلا:
function supportsInterface(bytes4 _interfaceId)
public
view
override(ERC721, ERC721Enumerable) //just make sure this list is complete
returns (bool)
{
return super.supportsInterface(_interfaceId);
}
اجرای صحیح ERC-165 اختیاری است، اما اهمیت زیادی دارد. اگر میخواهید توکن شما با سیستمهای زیادی مثل صرافیها و حتی سیستمهایی که هنوز پیادهسازی نشدهاند سازگار باشد، باید از آن استفاده کنید. بهمرور زمان و با بلوغ این حوزه، استفاده از ERC-165 افزایش خواهد یافت.
اشتباه رایج ۴: تست نکردن کامل قبل از استقرار
آخرین مورد از ۴ اشتباه رایج در قراردادهای NFT، تست نکردن قرارداد است. شاید توکن ERC721 شما بسیار استاندارد باشد و بدون تغییرات خاصی، از تمام کتابخانهها و کلاسهای والد شخص ثالث استفاده کند و شما نیز مطمئن باشید که کد شخص ثالث تستشده و کاملا امن است. با این حال باز هم باید کد خود را بهطور کامل تست کنید؛ چون قبل از استقرار آن بر شبکه اصلی، فقط یکبار فرصت تست و رفع ایراد دارید.
اول از همه، باید تست یونیت (Unit Testing) را انجام دهید. مهم نیس که از چه چهارچوبی برای تست استفاده میکنید. نویسنده مقاله معمولا از Hardhat، Ethers و Mocha استفاده میکند. نکات مهمی که در انتخاب چهارچوب تست باید به آنها دقت کنید پوشش تست، پوشش موارد Happy-path ،پوشش موارد استثنایی و عمق و گستردگی موارد Edge است.
شاید از کدی استفاده میکنید که قبلا بهخوبی تست شده است (OpenZeppelin). اما باید توجه کنید که اولا کد شما ممکن است در موارد ذکر شده بالا دچار مشکل شده باشد و باید دوباره آزمایش شود. دوما OpenZeppelin قبلا باگهایی داشته است و ممکن است در آینده نیز این باگها دوباره ظاهر شوند. برای صرفهجویی در وقت میتوانید مجموعهای از تستها را برای تمام توکنهای ERC721، توکنهای ERC20 و توکنهای ERC1155 و غیره آماده کنید و در هر پروژه از این مجموعه استفاده کنید. بعدها میتوانید موارد بیشتری را به پروژهها اضافه کنید و استاندارد را سفارشیسازی کنید. در این صورت در وقت شما صرفهجویی میشود. تستهای یونیت باید شامل کنترل دسترسی، عملیات پایه (صدور و انتقال)، مکثپذیری (اگر قرارداد شما قابلیت مکث داشته باشد)، پیادهسازی استاندارد ERC165 و غیره باشد. میتوانید پوشش خود را بهوسیله Solidity-Coverage که یکی از پکیجهای پلتفرم Nodejs است، تست کنید.
نهایتا، استفاده از ابزارهای خودکار در تستها نیز کمک بزرگی به شما خواهد کرد. Slither، Manticore و Mythril استانداردهای صنعتی هستند که معمولا توسط شرکتهای بزرگ بازبینی امنیتی مثل Consensys و Certik استفاده میشوند. Solidity-coverage درصد تخمینی پوشش تستهای یونیت شما را نشان میدهد. Solgraph ابزاری است که بهوسیله آن میتوانید روابط و ارتباطات موجود در کد قرارداد را مشاهده کنید و استفاده از آن در برنامهریزی تستها مفید است. Echidna نیز یک ابزار مفید و کمی پیچیده است. بهتر است همیشه با یک روش تستمحور (Test-first) پیش بروید. در این صورت پوشش تست خوبی خواهید داشت و تستهای شما به مشخصات پروژه نزدیک میشود.
> pip3 install slither-analyzer
> pip3 install mythril
> npm install solidity-coverage
پس بهطور خلاصه به موارد زیر دقت کنید:
- انجام یونیت تست کامل و عمیق در تمام موارد
- تست و آزمایش دستی
- استفاده از ابزارهای خودکار مثل Slither، Manticore، Mythril، Echidna و Solidity-coverage
توصیه: قرارداد خود را در اتر اسکن تایید کنید
امکان تایید کد قرارداد در اتر اسکن یک ویژگی بسیار خوب است. تایید قرارداد در سایت اتر اسکن و دریافت تیک سبز از این پلتفرم، باعث افزایش حس اعتماد و مسئولیتپذیری میشود. زمانی که فردی برای اولین بار قرارداد شما را مشاهده می کند و میخواهد ریسکهای سرمایه گذاری در آن را ارزیابی کند، این موارد کمککننده خواهد بود. ضمنا نه تنها قراردادهای NFT، بلکه بهتر است تمام قراردادها در اتر اسکن تایید شوند.
جمعبندی
با مقایسه قراردادهای مختلف NFT، بهویژه قراردادهای ERC721، میتوانیم نکات مثبت و منفی ساختار و طراحی این قراردادها را مشاهده کنیم و از آنها درس بگیریم. تعدادی اشتباه خاص و مشخص وجود دارد که تقریبا در بسیاری از قراردادهای NFT تکرار میشود. در مطلب فوق، ۴ ضد الگو یا ۴ اشتباه رایج در قراردادهای NFT