NFT مانند بمب صدا کرده است و هنوز هم در حال صدا کردن است! (تا زمان نگارش این مقاله در شهریور 1401). Etherscan یک ابزار جستجوی مفید دارد که در کنار ویژگیهای کاربردی verification و decompiling ، به شما امکان میدهد کد بسیاری از ERC721 ها را برای مقایسه، بررسی کنید. در کنار بسیاری از قراردادهایی که به خوبی طراحی شدهاند، میتوانیم اشتباهات تکراری زیادی را هم مشاهده کنیم. در این مقاله، نظر خود را درباره چهار مورد از رایجترین شکستهای طراحی یا Anti Pattern های NFT ، که معمولاً هنگام مشاده قراردادهای NFT در Etherscan متوجه میشوم، ارائه خواهم کرد.
توجه داشته باشید که این مقاله عمدتاً با در نظر گرفتن بلاک چینهای EVM-compatible نوشته شده است. اما بسیاری از نکات در شبکههای دیگر نیز قابل اجرا هستند یا معادل و مشابهی در آنها دارند.
لطفاً اگر برخی از کارهایی که من به عنوان شکست طراحی نام بردهام را انجام میدهید، ناراحت نشوید. این نظر من است. و علاوه بر این، من به عنوان یک توسعهدهنده نیاز به صرفهجویی در هزینه کارمزد در شبکه شلوغ NFT با این کارمزد گران را درک میکنم. دیدگاه من را به عنوان یک مشاور فریلنس درنظر بگیرید. آن مشتری که میتواند هزاران دلار برای کمک به توسعه (development) هزینه کند، قطعاً میتواند صدها دلار بیشتر برای استقرار (deployment) در جهت درست اختصاص دهد.
البته این در مورد زنجیره اتریوم صدق میکند، که تا این لحظه گرانترین است. Polygon ارزانتر است و زنجیرههای دیگر مانند Solana (و زنجیرههای non-EVM ) حتی ارزانتر هم هستند. حرف من این است که اگر مشکل مالی در میان نباشد، مزایای پیادهسازی (implementation) با کیفیت بالاتر ممکن است ارزش هزینه اضافی را داشته باشد.
Anti Pattern شماره 1 : لحاظ کردن اطلاعات قیمت/فروش (Price/Sale) و منطق (Logic) در خود قرارداد
این Anti Pattern در طراحی قراردادهای NFT بسیار رایج است، اما وقتی به آن برخورد میکنم، قرارداد برایم آماتور به نظر میرسد. اگر منصف باشیم، انگیزههای صحیح و قابل درکی وجود دارد. اولاً این که استقرار و مدیریت قراردادها در بسیاری از شبکهها خیلی گران شده است و این اتفاق برای صرفهجویی در هزینهها رخ داده است. و برای سادهسازی این موضوع ممکن است فکر کنیم که چرا منطق minting و فروش را در خود قرارداد قرار ندهیم؟
اما این واقعاً ایده خوبی نیست. قرارداد خود باید مرکز تغییرناپذیر یک شبکه منطقی (logic) باشد، اما هرگز نباید مدیریت مستقیم پول را در دست بگیرد. این شامل فروش، زمانهای فروش، ساخت لیست سفید (whitelisting) و غیره میشود که مستقیماً در همان کد قرارداد به عنوان ERC721 پیادهسازی میشوند. منطق فروش (sale logic) و منطق اصلی (core logic) به شدت به هم مرتبط هستند.

چرا چنین قراردادی تنظیم میشود؟
در حالی که صرفهجویی در کارمزد ممکن است بهترین و قابل درکترین دلیل برای گنجاندن همه منطقها در طراحی یک قرارداد باشد، به نظر من و با در نظر گرفتن همه موارد، دلایل بسیار بهتری برای عدم اجرای این میانبر در طراحی وجود دارد. منطق اصلی قرارداد شما باید تنها چیزی باشد که سنگبنا (stone) قرار داده میشود؛ و در بیشتر موارد استاندارد را به شیوهای بسیار خوب و استاندارد اجرا میکند. بسیاری از کلونها (clones) تقریباً شبیه یکدگیر هستند (یا میتوانند باشند). استراتژی minting شما و قیمتگذاری (اگر mint ها را میفروشید) چیزهایی هستند که باید از منطق اصلی جدا شوند. این کار به قرارداد شما اجازه میدهد تا آنقدر انعطافپذیر باشد که به اعتماد کاربر آسیب نرساند. طراحی جداسازیشده (Decoupled) و اصل تک مسئولیتی کردن (single-responsibility) مواردی هستند که برای جلوگیری از تبدیل شدن Pattern به Anti Pattern باید لحاظ شوند.
نکته: فکر میکنم که محدود کردن عرضه (یعنی حداکثر عرضه) در خود قرارداد ERC721 ، تا زمانی که بتواند توسط شخصی با نقش ادمین تغییر پیدا کند، منطقی است.


Anti Pattern شماره 2 : پیادهسازی نکردن امنیت Role-Based
یک قرارداد token به نوعی کنترل دسترسی نیاز دارد، زیرا توابعی (مانند minting یا انجام هر کاری برای پارامترهای عرضه (supply) ) وجود دارند که باید فقط برای آدرسهای مجاز در دسترس باشند. سادهترین راه برای انجام این کار، استفاده از یک مدل Ownable است. (معمولاً از قرارداد Ownable موسوم به OpenZeppelin استفاده میشود، زیرا برای چنین نیاز اولیهای باید همه چیز را از اول بسازید). اما به دلایل زیر، قویاً توصیه میکنم بجای آن از یک کنترل دسترسی Role-Based (مبتنی بر نقش) استفاده کنید. انگیزهای که پشت استفاده از Ownable (یا چیزی مشابه آن) وجوود دارد، احتمالاً ساده بودن و صرفهجویی در هزینههای کارمزد است که ظاهراً خوب به نظر میرسد. اما همین منجر به شکلگیری یک Anti Pattern میشود.
همچنین ممکن است «بدانید» که شما (یا مشتری شما) «همیشه» تنها کسی هستید که قرارداد را مدیریت میکند. هنگامی که هزینه کم باشد، Future-proofing ترجیح داده میشود. و اگر صادقانه بگویم، امنیت Role-Based (مثلاً کنترل دسترسی OpenZeppelin ) در مقایسه با مدل Ownable کمی پیچیدهتر (و گرانتر) است. اگر هزینه کامزد همچنان مشکل شماست، میتوانید کد امنیتی Role-Based (میتواند OpenZeppelin باشد یا خودتان) را فقط به آنچه نیاز دارید تخصیص دهید.
اما دلیل مهمتر برای استفاده از Role-Based این است که به شما امکان میدهد تا عملکرد (مانند مورد قبلی، اطلاعات فروش و قیمت) را از خود قرارداد ERC721 جدا کنید. این کار به شما امکان میدهد با اختصاص دادن نقش minter به آن، قرارداد جداگانهای را به عنوان minter تعیین کنید، بدون این که مجوز مدیریت کامل به آن داده شود. و این در حالی است که ادمین (یا ادمینها، که احتمالاً انسان هستند نه قرارداد) هنوز اختیارات بالاتری دارد (مانند حذف و اضافه مجوزها).
لغو حقوق Minting
هنگامی که minter (برای مثال) دیگر نیازهای شما را برآورده نمیکند، به سادگی با لغو حقوق minting آن، و واگذاری حقوق minting به یک قرارداد جدید که استراتژی minting جدید را پیادهسازی میکند، بازنشسته میشود. این کار نظاممند، راحت و امن است. بر اساس موارد استفاده خاص در پروژه، سایر فعالیتها به جز minting نیز میتوانند به همین ترتیب انجام شوند.


Anti Pattern شماره 3 : عدم اجرای کامل ERC-165 (درونفکنی)
بسیاری از توکنها (یا به طور کلی قراردادها) یا ERC-165 را پیادهسازی نمیکنند یا آن را به طور کامل پیادهسازی نمیکنند. به نظر من، کاربرد ERC-165 در قابلیت همکنش پذیری (interoperability) است. ERC-165 قرارداد شما را با آینده سازگار میکند و ممکن است صرافیها برای ساختار حق امتیاز NFT شما آن را فراخوانی کنند. این کار اغلب انجام نمیشود، یا به صورت نصفه و نیمه انجام میشود.
در اینجا چند قانون کلی برای اجرای صحیح آن و جلوگیری از شکل گیری Anti Pattern وجود دارد:
هر class والد که ERC-165 را پیادهسازی میکند، باید در لیست override باشد. سپس هنگام فراخوانی super.supportsInterface ، به طور خودکار آنها را فراخوانی میکند.
هر رابط پیادهسازی شده دیگری که در class های والد نمایش داده نمیشود، میتواند با یک clause اضافه شود. مانند این:
|| type(ISomeInterface).interfaceId == _interfaceId
مثال:
function supportsInterface(bytes4 _interfaceId)
public
view
override(ERC721, ERC721Enumerable)
returns (bool)
{
return super.supportsInterface(_interfaceId) ||
_interfaceId == type(IERC2981).interfaceId;
}
اگر کد شما فاقد class های والد است که ERC-165 را پیادهسازی کنند، فقط نوع دوم باید نمایش داده شود.
مانند:
function supportsInterface(bytes4 _interfaceId)
public
view
override
returns (bool)
{
return _interfaceId == type(IERC721).interfaceId ||
_interfaceId == type(IERC2981).interfaceId ||
_interfaceId == type(IAccessControl).interfaceId;
}
اگر کد شما به جز interface هایی که توسط پیادهسازی class های والد ERC-165 مدیریت میشوند، هیچ interface دیگری را پیادهسازی نمیکند، پس نوع دوم مورد نیاز نیست.
مانند:
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 احتمالاً با گذشت زمان و بالغشدن فضا بیشتر مورد استفاده قرار میگیرد.
Anti Pattern شماره 4 : تست نکردن قبل از استقرار (Deploying)
توکن ERC721 شما ممکن است بسیار استاندارد باشد و از تمام class ها و کتابخانههای والد شخص ثالث با شخصیسازی بسیار کمی استفاده کند و ممکن است بدانید که آن کد شخص ثالث به خوبی تست شده و امن است. اما در هر صورت، باید کد خود را به طور کامل آزمایش کنید، زیرا تنها یک فرصت دارید که آن را درست قبل از استقرار در شبکه اصلی دریافت کنید. Anti Pattern
البته در مرتبه اول، تست واحد (unit testing) قرار دارد. به نظر من اینکه از چه چارچوب تستی استفاده میکنید مهم نیست. من از hardhat با ethers و mocha استفاده میکنم. تنها بخش مهم آن برای من این است که coverage تست و coverage موارد happy-path ، موارد استثنایی و موارد لبه (edge) گسترده و عمیق باشد. حتی اگر کدی را تست کنم (مانند OpenZeppelin ) که قبلاً به خوبی تست شده است، (الف) این کد سفارشی ممکن است برخی از این موارد شکسته باشد، بنابراین باید دوباره تست شود، و (ب) OpenZeppelin قبلاً باگهایی داشته است، و آنها ممکن است دوباره در آینده سرباز کنند.
برای صرفهجویی در زمان، ممکن است بخواهید مجموعهای استاندارد از تستها برای همه توکنهای ERC721 ، همه توکنهای ERC20 ، همه توکنهای ERC1155 و غیره داشته باشید که میتوانید در پروژههای مختلف از آنها استفاده کنید. این خوب است. سپس میتوانید برای هر پروژه مواردی را اضافه کنید تا هرگونه شخصیسازی را تا حالت استاندارد پوشش دهد. این باعث صرفهجویی در زمان میشود. (unit test باید کنترل دسترسی، عملکردهای اساسی (مانند minting و transferring ) ، قابلیت توقف (pausability) (اگر قرارداد شما این قابلیت را داشته باشد)، اجرای استاندارد ERC165 و موارد دیگر را پوشش دهد. میتوانید پوشش خود را با استفاده از solidity-coverage (یک بسته nodejs ) آزمایش کنید.
ابزارهای مفید
در نهایت، ابزارهای خودکار میتوانند در تست کردن به شما کمک زیادی بکنند. Slither، Manticore و Mythril استانداردهای این صنعت هستند که معمولاً توسط نامهای اصلی در auditing امنیتی مانند Consensys و Certik مورد استفاده قرار میگیرند. Solidity-coverage (یک بسته nodejs ) درصدهای تخمینی coverage مربوط به unit test هایتان را به شما میگوید. (به عنوان یک قانون کلی بسیار مفید). Solgraph ابزاری است که میتواند به شما کمک کند روابط و اتصالات را در کد قرارداد مشاهده کنید و در برنامهریزی تست مهم است. Echidna نیز به عنوان یک ابزار تست fuzzing مفید است. در صورت امکان، من شخصاً از روش اول تست کردن استفاده میکنم. این روش test coverage خوبی را تضمین میکند و مجموعه آزمایشی شبیه به مشخصات پروژه میشود. در مجموعه من test coverage های خوب را دوست دارم.
> pip3 install slither-analyzer
> pip3 install mythril
> npm install solidity-coverage
بنابراین به طور خلاصه موارد زیر الزامی است:
- تست واحد عمیق و کامل که تا حد امکان موارد happy-path ، استثنایی و edge را دریافت کند.
- تست و معاینه دستی.
- استفاده از ابزارهای خودکار مانند slither ، manticore ، mythril ، echidna و solidity-coverage .
نکته: قرارداد خود را در Etherscan تأیید و Verify کنید.
Verification کد قرارداد در etherscan یک ویژگی عالی است. مشاهده تیک سبزرنگ در تب Contract و دیدن خود کد با تگ “exact match” ، قابل اعتماد بودن و مسئولیتپذیری شما را ثابت میکند. وقتی کسی برای اولین بار قرارداد شما را مشاهده میکند و سعی میکند خطرات نسبی استفاده یا سرمایهگذاری در آن را ارزیابی کند، این موارد میتوانند در طراحی قرارداد کمککننده باشند. و این به طور کلی برای طراحی همه قراردادها از همه نوع است، نه صرفاً NFT ها.
منبع: HackerNoon نویسنده: جان آر.کوزینسکی
Leave feedback about this