استاندارد ERC 7201 که پیشتر با عنوان EIP-7201 شناخته میشد، روشی استاندارد برای گروه بندی متغیرهای ذخیره سازی تحت یک شناسه مشترک به نام فضای نام (Namespace) ارائه میدهد. این استاندارد همچنین امکان مستندسازی این گروه از متغیرها را از طریق توضیحات NatSpec فراهم میکند.
هدف اصلی این استاندارد، ساده سازی مدیریت متغیرهای ذخیره سازی به ویژه در هنگام بهروزرسانی قراردادهای هوشمند است. با استفاده از این ساختار، توسعه دهندگان میتوانند راحتتر متغیرهای مربوط به یک بخش مشخص از منطق قرارداد را شناسایی، تفکیک و کنترل کنند.
فضای نام (Namespace) چیست؟
فضای نام یکی از مفاهیم رایج در زبان های برنامه نویسی است. برنامه نویسان از فضای نام برای سازماندهی و گروه بندی شناسه های مرتبط مانند متغیرها، توابع، کلاس ها یا ماژول ها استفاده می کنند. این روش جلوی تداخل در نام گذاری را می گیرد و ساختار کد را منظم تر می سازد.
سالیدیتی به صورت ذاتی از فضای نام پشتیبانی نمی کند، اما می توان با تکنیک هایی آن را شبیه سازی کرد. در این زمینه، هدف ما این است که متغیرهای وضعیت یک قرارداد را به شکل منطقی در یک فضای نام دسته بندی کنیم.
البته ایده استفاده از فضای نام در سالیدیتی برای اولین بار توسط ERC-7201 مطرح نشد. پیش از آن، الگوی پراکسی دایموند (ERC-2535) نیز همین مفهوم را پیاده سازی کرده بود.
برای اینکه اهمیت فضای نام را در قراردادهای قابل ارتقا درک کنیم، ابتدا باید بدانیم که ERC-7201 چه مشکلی را هدف گرفته است.
چالش وراثت (Inheritance) در قراردادهای قابل ارتقا
برای درک بهتر مشکل، بیایید ساختار یک قرارداد قابل ارتقا را بررسی کنیم. این قرارداد شامل یک قرارداد پراکسی و یک قرارداد پیاده سازی است که در آن، وراثت بین یک قرارداد والد و یک قرارداد فرزند استفاده شده است.
در بخش پیاده سازی، قرارداد والد و قرارداد فرزند هرکدام یک متغیر وضعیت دارند. این متغیرها در اولین اسلات حافظه خود قرار دارند. زمانی که پراکسی، مثل یک پراکسی شفاف (transparent proxy)، این قرارداد را نمایندگی میکند، باید دقیقاً همین ساختار حافظه را تکرار کند.
برای ساده کردن موضوع، فرض میکنیم هر متغیر دقیقاً یک اسلات حافظه را اشغال میکند. بنابراین فقط از نوعهایی مانند uint256
یا bytes32
استفاده میکنیم تا مدیریت فضای ذخیره سازی ساده تر باشد.
زمانی که توسعه دهنده ساختار متغیرهای وضعیت در قراردادهای پیاده سازی را تغییر میدهد، مشکل اصلی خود را نشان میدهد. تصور کنید در یک ارتقا، نیاز داریم متغیر جدیدی را به قرارداد والد اضافه کنیم. این کار باعث میشود ساختار حافظه قرارداد تغییر کند.
به عنوان مثال، فرض کنید قرارداد پیاده سازی ما یک متغیر مخصوص به خود دارد و همچنین دو متغیر را از قرارداد والد به ارث میبرد. تخصیص اسلات های حافظه به شکل زیر خواهد بود:
حالا مشکل اینجاست: قبلاً متغیر variableB
در یک اسلات مشخص قرار داشت، اما بعد از افزودن متغیر جدید variableA
در قرارداد والد، متغیر variableC
در همان اسلات قرار میگیرد. به این ترتیب، variableC
به جای مقدار صحیح، مقدار قدیمی variableB
را میخواند.
این پدیده که به آن برخورد اسلات (slot collision) میگویند، میتواند عملکرد قرارداد را به شدت مختل کند و در صورت استفاده از داده های اشتباه، منجر به بروز خطاهای جدی شود.
رویکرد Gap در مدیریت حافظه
برای حل مشکل برخورد اسلات، تیم OpenZeppelin راهکاری ارائه داد که در نسخه های قابل ارتقا از کتابخانه خود (تا نسخه ۴) از آن استفاده کرد. در این روش، در انتهای هر قرارداد، فضایی به نام “gap” قرار میگیرد. این gap در واقع مجموعهای از اسلات های خالی است که توسعه دهنده میتواند در ارتقاهای بعدی از آنها برای افزودن متغیرهای جدید استفاده کند؛ بدون اینکه ساختار حافظه متغیرهای قبلی به هم بریزد.
در تصویر زیر، بخشی از کد قرارداد ERC20Upgradeable.sol
نسخه ۴.۹ را میبینید که این رویکرد در آن به کار رفته است. استفاده از gap به توسعه دهندگان این امکان را میدهد که در آینده، بدون ریسک تداخل داده ها، قابلیت های جدیدی به قرارداد اضافه کنند.
محاسبه اندازه متغیر __gap
اندازه آرایه __gap
به گونهای محاسبه میشود که مجموع اسلات های ذخیره سازی مورد استفاده در هر قرارداد همیشه برابر با ۵۰ باشد. بنابراین، اگر یک قرارداد شامل ۵ متغیر وضعیت باشد، آرایه __gap
باید شامل ۴۵ اسلات خالی باشد تا این عدد به ۵۰ برسد.
در تصویر بالا میتوان ساختار قرارداد ERC20Upgradeable.sol
نسخه ۴.۹ را مشاهده کرد که از این الگو پیروی میکند.
حالا بیایید همین مفهوم را در مثال خود اعمال کنیم.
اگر قرارداد والد ۵ متغیر وضعیت داشته باشد و در انتهای خود آرایهای با ۴۵ اسلات خالی به عنوان gap قرار دهد، در این صورت ساختار حافظه قرارداد پیاده سازی – که توسط قرارداد پراکسی نیز تکرار میشود – مشابه تصویر زیر خواهد بود.
این رویکرد تضمین میکند که در آینده بتوان متغیرهای جدید را بدون آسیب زدن به داده های قبلی، در فضای ذخیره سازی قرارداد اضافه کرد.
اکنون ۴۵ اسلات خالی در اختیار قرارداد والد قرار دارد تا در صورت نیاز به ارتقا، از آنها استفاده کند. فرض کنیم در آینده بخواهیم متغیر جدیدی به نام variableN
به قرارداد والد اضافه کنیم. در این حالت، کافی است این متغیر را قبل از آرایه gap قرار دهیم.
با انجام این کار، تنها کاری که نیاز داریم انجام دهیم این است که طول gap را یک واحد کاهش دهیم، چرا که یک اسلات آن را به متغیر جدید اختصاص دادهایم.
همانطور که در انیمیشن زیر نشان داده شده، این فرآیند به صورت ساده و بدون ایجاد اختلال در سایر دادهها انجام میشود و ساختار حافظه موجود را حفظ میکند.
محدودیت های استفاده از gap در قراردادهای قابل ارتقا
آرایه gap به توسعه دهندگان کمک میکند متغیرهای جدید را بدون تغییر در رفتار قبلی قرارداد، به آن اضافه کنند. این فضا مانند یک رزرو حافظه عمل میکند و از بروز برخورد در اسلات های ذخیره سازی جلوگیری میکند. به همین دلیل، هنگام طراحی قراردادهای قابل ارتقا، قراردادن gap در انتهای هر قرارداد پیاده سازی اقدام هوشمندانهای است.
با این حال، این روش نمیتواند تمام سناریوهای پیچیده را پوشش دهد. برای مثال، اگر توسعه دهنده تصمیم بگیرد یک قرارداد والد جدید را قبل از قرارداد والد فعلی در زنجیره وراثت قرار دهد، تمام متغیرهای پایینتر نسبت به قبل در ساختار حافظه جابجا میشوند. در نتیجه، ساختار حافظه قرارداد بههم میریزد.
در چنین شرایطی، gap دیگر نمیتواند از این جابجایی جلوگیری کند. بنابراین، برای جلوگیری از مشکلات ساختاری در قراردادهای قابل ارتقا، نباید فقط به gap اتکا کرد. طراحی دقیق و اصولی ساختار وراثت و حافظه همچنان اهمیت بالایی دارد.
بنابراین، پیدا کردن راهکاری برای تنظیم ساختار حافظه در قراردادهای پیاده سازی، بدون ایجاد تداخل در اسلات ها، کاملاً ضروری است.
بهترین راه حل این است که به هر قرارداد پیاده سازی در زنجیره وراثت، فضای ذخیره سازی اختصاصی خودش را بدهیم. با این کار، متغیرهای هر قرارداد در محدودهای مجزا ذخیره میشوند و دیگر با متغیرهای سایر قراردادها تداخل پیدا نمیکنند.
متأسفانه، سالیدیتی در حال حاضر مکانیزم داخلی برای این کار ندارد. به بیان دیگر، امکان تعریف فضای نام (namespace) برای متغیرهای قرارداد بهصورت بومی در زبان وجود ندارد. بنابراین، برای رسیدن به این هدف، باید با استفاده از قابلیت های موجود در سالیدیتی و زبان سطح پایین YUL، ساختار مورد نظر را شبیه سازی کنیم.
یکی از روش های رایج برای پیاده سازی این ساختار، استفاده از struct
هاست. در ادامه بررسی میکنیم که ساختار حافظه در سالیدیتی چگونه عمل میکند و چطور میتوان یک ساختار ریشهای مبتنی بر فضای نام ایجاد کرد.
ساختار ریشه ای مبتنی بر فضای نام
ساختار حافظه ای که کامپایلر سالیدیتی برای قراردادها تولید میکند، به صورت قابل خلاصهای به شکل زیر قابل توصیف است:
در این ساختار، L
نشاندهنده محل ذخیره سازی (storage location) است، n
یک عدد طبیعی است، و H(k)
یک تابع هش است که روی کلید خاصی به نام k
اعمال میشود. این کلید میتواند، برای مثال، کلید یک mapping
یا ایندکس یک array
باشد.
ساختار کلی حافظه معمولاً با ترکیبی از موقعیت های عددی و هش شده کار میکند. استفاده از فضای نام در این ساختار، به ما اجازه میدهد که با اختصاص دادن هر بخش از حافظه به یک ساختار مشخص، تداخل در اسلات های ذخیره سازی را به طور کامل حذف کنیم.
در ادامه، نحوه پیاده سازی این ایده با استفاده از ساختارهای struct
و مدیریت دستی حافظه را بررسی خواهیم کرد.
فرمول ارائهشده در بالا نشان میدهد که متغیرهای وضعیت را میتوان در سه موقعیت اصلی در ساختار حافظه یافت:
-
در ریشه (Root): که بهصورت پیشفرض در اسلات صفر قرار دارد.
-
در ترکیبی از قواعد نحوی بههمراه یک عدد طبیعی: مانند لیست متغیرها یا ساختارهای داخلی مانند آرایهها و مپها.
-
در مقدار هش شده (keccak) از یک کلید خاص: که بهطور قطعی و قابل پیشبینی از مقدار کلید و محل متغیر نسبت به ریشه محاسبه میشود.
نکته مهم این است که تمام این موقعیت ها در نهایت به ریشه وابسته هستند. سالیدیتی برای هر قرارداد، مقدار 0
را به عنوان ریشه حافظه (root slot) در نظر میگیرد.
اگر بخواهیم محل ذخیره سازی متغیرهای یک قرارداد را بهصورت مستقل تعریف کنیم، باید ریشه را تغییر دهیم؛ اما نه بهصورت فیزیکی، بلکه با استفاده از یک برچسب منحصربهفرد که فقط به آن قرارداد اختصاص دارد. این برچسب همان چیزی است که ما به عنوان فضای نام (namespace) تعریف میکنیم.
ایده اصلی فضای نام در قراردادهای هوشمند این است که ریشه حافظه دیگر در اسلات صفر شروع نشود، بلکه در یک اسلات مشخص قرار گیرد که از طریق فضای نام انتخاب شده تعیین میشود. با این کار، میتوان برای هر قرارداد یا بخش از قرارداد، یک فضای حافظه اختصاصی و بدون تداخل ایجاد کرد.
نمیتوان این ساختار را فقط با استفاده از سالیدیتی پیاده سازی کرد، چون کامپایلر همیشه اسلات صفر را به عنوان ریشه حافظه در نظر میگیرد و به توسعه دهنده اجازه نمیدهد این مقدار را تغییر دهد.
با این حال، اگر از ترکیب struct
ها و کدنویسی سطح پایین با اسمبلی (Yul) استفاده کنیم، میتوانیم راه حلی برای تعریف ریشه حافظه اختصاصی پیدا کنیم. این روش به ما امکان میدهد که فضای ذخیره سازی متغیرها را به طور جداگانه و بدون تداخل سازماندهی کنیم.
پیش از آنکه به پیاده سازی برسیم، باید فرمولی را بررسی کنیم که استاندارد ERC 7201 برای محاسبه ریشه حافظه از روی فضای نام ارائه داده است.
فرمول پیشنهادی برای محاسبه ریشه حافظه بر اساس فضای نام
اگر قصد داشته باشیم ریشه اسلات ذخیره سازی یک قرارداد دارای فضای نام را تغییر دهیم، باید فرمولی برای محاسبه این ریشه جدید تعریف کنیم. استاندارد ERC-7201 فرمول زیر را برای این منظور پیشنهاد داده است:
1 |
keccak256(keccak256(namespace) - 1) & ~0xff |
منطق پشت این فرمول به شرح زیر است:
-
کاهش عدد هششده به اندازه ۱ (یعنی
keccak256(namespace) - 1
) باعث میشود پیشتصویر هش (preimage) قابل پیشبینی نباشد. این کار یک لایه امنیتی اضافه ایجاد میکند. -
اجرای تابع
keccak256
برای بار دوم احتمال تداخل با اسلات هایی که سالیدیتی بهطور داخلی تولید میکند را کاهش میدهد. زیرا محل ذخیره سازی متغیرهایی با اندازه پویا (مثلmapping
وarray
) نیز با استفاده ازkeccak256
تعیین میشود. -
عملیات AND با مکمل
0xff
(& ~0xff
) بایت سمت راست موقعیت حافظه را به00
تبدیل میکند. این کار آمادگی لازم را برای تغییر ساختار ذخیره سازی در آینده (مانند جایگزینی درختهای Merkle با درختهای Verkle در اتریوم) فراهم میکند. در آن ساختار جدید، میتوان ۲۵۶ اسلات مجاور را به صورت همزمان فعال (warmed) کرد.
فرمولی که در بالا معرفی شد، یک ویژگی حیاتی را برای ریشه جدید تضمین میکند: اینکه این ریشه با هیچکدام از موقعیتهای حافظهای که کامپایلر سالیدیتی ممکن است بهصورت پیشفرض به متغیرها اختصاص دهد، تداخل نخواهد داشت.
برای درک بهتر، میتوان این فرمول را به کمک یک قرارداد سالیدیتی پیاده سازی کرد تا مقدار ریشه حافظه (storage root) را از یک فضای نام مشخص محاسبه کنیم. در مثال زیر، تابع getStorageAddress
این محاسبه را انجام میدهد:
1 2 3 4 5 6 7 8 9 10 11 12 |
pragma solidity ^0.8.20; contract Erc7201 { function getStorageAddress( string calldata namespace ) public pure returns (bytes32) { return keccak256( abi.encode(uint256(keccak256(abi.encodePacked(namespace))) - 1) ) & ~bytes32(uint256(0xff)); } } |
"openzeppelin.storage.ERC20"
را به عنوان فضای نام وارد کنیم، مقدار هش نهایی به صورت زیر خواهد بود:
1 2 |
// keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.ERC20")) - 1)) & ~bytes32(uint256(0xff)) bytes32 private constant ERC20StorageLocation = 0x52C63247Ef47d19d5ce046630c49f7C67dcaEcfb71ba98eedaab2ebca6e0; |
ERC20Upgradeable
نسخه ۵ استفاده کرده است — موردی که در بخش بعدی بهطور دقیق بررسی خواهیم کرد.
استفاده از فیلدهای Struct به عنوان متغیرهای وضعیت
در بخش قبلی، نحوه محاسبه ریشه حافظه قرارداد بر اساس فضای نام را بررسی کردیم. حالا برای ادامه مسیر، باید بتوانیم متغیرهای ذخیره سازی را بهگونهای گروه بندی کنیم که از همان ریشه جدید شروع شوند. با این حال، نمیتوانیم این متغیرها را بهصورت مستقیم به عنوان state variable تعریف کنیم، چون در این صورت، کامپایلر سالیدیتی دوباره تخصیص حافظه را از اسلات صفر آغاز میکند — و این دقیقاً چیزی است که میخواهیم از آن اجتناب کنیم.
برای حل این مشکل، باید از struct
استفاده کنیم. وقتی متغیرها را داخل یک struct
تعریف میکنیم، ترتیب قرارگیری آنها در حافظه مطابق با همان ترتیب عادی اسلاتها خواهد بود، اما این بار میتوانیم نقطه شروع آن را کنترل کنیم.
به عنوان نمونه، قرارداد زیر این مفهوم را به خوبی نشان میدهد:
1 2 3 4 5 6 7 8 9 10 |
contract StructStorage { // **ERC-7201 uses a struct to group variables together, but the struct is never // actually declared, nor any other state variable.** struct MyStruct { uint256 fieldA; uint256 fieldB; mapping(address => uint256) fieldC; } // Contract functions... } |
بهصورت فرضی، اگر این struct
را به عنوان اولین متغیر وضعیت قرارداد تعریف کنیم (کاری که استاندارد ERC-7201 انجام نمیدهد)، ترتیب قرارگیری فیلدها در حافظه به شکل زیر خواهد بود:
-
fieldA
در اسلات شماره ۰ قرار میگیرد -
fieldB
در اسلات شماره ۱ -
پایه (base) مربوط به
mapping
تحت عنوانfieldC
در اسلات شماره ۲ ذخیره میشود -
و به همین ترتیب ادامه پیدا میکند
برای آنکه بتوانیم محل دقیق ذخیره سازی هر فیلد داخل یک struct
را مشخص کنیم، باید از یک فرمول ساده استفاده کنیم. در این فرمول، base به اسلاتی اشاره دارد که struct از آن نقطه به بعد فضای حافظه را اشغال میکند:
توجه داشته باشید که این همان فرمول قبلی مربوط به ساختار حافظه است؛ تنها تفاوت اینجاست که به جای ریشه (root)، از پایه struct استفاده کردهایم. به عبارت دیگر، struct از طریق فیلدهایش همان الگوی ساختار حافظه را حفظ میکند. این یعنی میتوانیم پایه struct را به عنوان ریشه جدید در نظر بگیریم.
در مثال بالا، پایه struct در اسلات صفر قرار دارد، اما ما میتوانیم اسلات دیگری را به عنوان پایه struct انتخاب کنیم. برای انجام این کار، میتوانیم از زبان سطح پایین YUL استفاده کنیم؛ همانطور که در مثال زیر نشان داده شده است.
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 |
contract StructOnStorage { // NO STATE VARIABLES struct MyStruct{ uint256 fieldA; mapping(uint => uint) fieldB; } function setMyStruct() public { MyStruct storage myStruct; // Grab a struct assembly { myStruct.slot := 0x02 // Change its base slot } myStruct.fieldA = 100; // FieldA will be in the first slot from the base at 0x02, which is 0x02 itself myStruct.fieldB[10] = 101; // The storage address of this mapping item will be calculated below } function getMyStruct() public view returns (uint256 fieldA, uint256 fielbBSingleValue) { // keccak256(abi.encode(key, struct base + location inside the struct) // The mapping is located in the second slot inside the struct, so struct base + 1 bytes32 locationSingleValue = keccak256(abi.encode(0x0a, 0x02 + 1)); assembly { fieldA := sload(0x02) // Read storage at 0x02 fielbBSingleValue := sload(locationSingleValue) } } } |
زمانی که از دستور myStruct.slot := 0x02
استفاده میکنیم، بهطور صریح پایه struct را تغییر میدهیم و در واقع ساختار حافظهای را شبیه سازی میکنیم که در آن، ریشه دیگر در اسلات صفر قرار ندارد. در این حالت، باید تمام متغیرهایی که در حالت عادی بهصورت متغیر وضعیت تعریف میشدند را بهعنوان فیلدهای struct داخل آن قرار دهیم.
پایه struct به عنوان ریشه جدید برای فیلدهای آن عمل میکند، که دقیقاً همان چیزی است که قصد داشتیم به آن برسیم.
یکی از معایب این روش این است که باید هر بار که میخواهیم فیلدی را ذخیره یا بازیابی کنیم، بهصورت صریح پایه struct را مشخص کنیم.
از آنجا که همواره نیاز داریم به پایه struct ارجاع دهیم، بهتر است برای این کار یک تابع کمکی (utility function) ایجاد کنیم. در پیاده سازی قراردادهای قابل ارتقای OpenZeppelin، یک تابع خصوصی برای همین منظور طراحی شده است تا بهراحتی به struct و پایه آن اشاره کنیم. برای نمونه، در فایل ERC20Upgradeable.sol میتوان این رویکرد را مشاهده کرد:
در ادامه، میبینیم که تمام متغیرهایی که در حالت عادی به عنوان متغیر وضعیت (state variables) تعریف میشدند، باید به عنوان فیلدهایی درون یک struct
تعریف شوند.
1 2 3 4 5 6 7 8 9 10 11 12 |
abstract contract ERC20Upgradeable is Initializable, ContextUpgradeable, IERC20, IERC20Metadata, IERC20Errors { /// @custom:storage-location crc7201:openzeppelin.storage.ERC20 struct ERC20Storage { mapping(address account => uint256) _balances; mapping(address account => mapping(address spender => uint256)) _allowances; uint256 _totalSupply; string _name; string _symbol; } |
ERC20Upgradeable.sol
.
1 2 3 4 5 6 7 |
/** * @dev Returns the name of the token. */ function name() public view virtual returns (string memory) { ERC20Storage storage $ = _getERC20Storage(); return $._name; } |
همانطور که در بالا مشاهده میکنید، زمانی که میخواهیم به متغیرهای ذخیره سازی دسترسی پیدا کنیم، کافی است تابع _getERC20StorageLocation()
را فراخوانی کنیم. این تابع، ریشه فضای ذخیره سازی مرتبط با فضای نام را به صورت مقدار bytes32
باز میگرداند.
همین رویه در زمان بهروزرسانی فیلدها نیز کاربرد دارد. اشارهگر $
در پایه struct قرار دارد؛ بنابراین میتوانیم با استفاده از نگارش $.[نام فیلد]
به فیلدهای struct دسترسی پیدا کنیم یا آنها را بهروزرسانی کنیم.
در تصویر زیر، بخشی از کد تابع _update
در قرارداد ERC20Upgradeable.sol
را مشاهده میکنید و اینکه چگونه از این ساختار برای بهروزرسانی موجودی حسابها هنگام انتقال توکن استفاده میشود.
خلاصه ای از نحوه پیاده سازی ساختار حافظه با ریشه مبتنی بر فضای نام
برای پیاده سازی این الگو، کافی است مراحل زیر را دنبال کنید:
-
از تعریف مستقیم متغیرهای وضعیت خودداری کنید.
-
تمام متغیرهایی که در حالت عادی بهعنوان متغیر وضعیت تعریف میشدند، باید به صورت فیلدهایی در یک struct تعریف شوند.
-
برای هر قرارداد، یک فضای نام (namespace) منحصربهفرد انتخاب کنید.
-
با استفاده از تابع پیشنهادی در ERC 7201، ریشه جدید (root) قرارداد را بر اساس فضای نام محاسبه کنید.
-
یک تابع کمکی ایجاد کنید که ارجاعی به پایه struct بازگرداند. در این تابع، با استفاده از اسمبلی (assembly) بهصورت صریح مشخص کنید که struct باید در اسلاتی قرار گیرد که از فضای نام محاسبه شده است.
-
هر بار که میخواهید فیلدی از struct را بخوانید یا بهروزرسانی کنید، ابتدا از تابع کمکی برای اشاره به پایه struct استفاده کنید.
در بخش بعدی، بررسی خواهیم کرد که چگونه میتوان استفاده از فضای نامها را بهصورت رسمی درون یک قرارداد مستند کرد.
مستندسازی موقعیت ذخیره سازی سفارشی با استفاده از NatSpec
فرمت مستندسازی NatSpec (مخفف Ethereum Natural Language Specification Format) روشی استاندارد برای نوشتن توضیحات داخل قراردادهای هوشمند است. این توضیحات هم برای توسعه دهندگان و هم برای ابزارهای تحلیل کد قابل استفاده هستند. بهعنوان نمونه، یک کامنت NatSpec برای مستندسازی یک تابع به شکل زیر نوشته میشود:
1 2 3 |
/** * @dev Returns the name of the token. */ |
1 |
@custom:storage-location <FORMULA_ID>:<NAMESPACE_ID> |
FormulaID
نشان دهنده فرمولی است که برای محاسبه ریشه ذخیره سازی بر اساس فضای نام استفاده شده است، در حالی که namespaceId
به فضای نام خاصی اشاره دارد که در این ساختار مدنظر قرار گرفته است. این توضیح (annotation) مربوط به خود struct
است، بنابراین باید دقیقاً در بالای تعریف struct نوشته شود.
فرمول پیشنهادی در این استاندارد با برچسب erc7201
مشخص شده است، بنابراین اگر بخواهیم در NatSpec از این فرمول استفاده کنیم، باید از قالب زیر پیروی کنیم:
1 |
@custom:storage-location erc7201:<NAMESPACE_ID> |
برای مثال، در قرارداد ERC20Upgradeable
، فضای نام انتخاب شده عبارت است از openzeppelin.storage.ERC20
. در نتیجه، annotation مربوطه باید به این صورت نوشته شود:
1 2 3 4 |
/// @custom:storage-location erc7201:openzeppelin.storage.ERC20 struct ERC20Storage { ... } |
راستی! برای دریافت مطالب جدید در کانال تلگرام یا پیج اینستاگرام سورس باران عضو شوید.
- انتشار: ۱۲ تیر ۱۴۰۴
دسته بندی موضوعات
- آموزش ارز دیجیتال
- آموزش برنامه نویسی
- آموزش متنی برنامه نویسی
- اطلاعیه و سایر مطالب
- پروژه برنامه نویسی
- دوره های تخصصی برنامه نویسی
- رپورتاژ
- فیلم های آموزشی
- ++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
- اچ تی ام ال
- بانک اطلاعاتی
- برنامه نویسی سوکت
- برنامه نویسی موبایل
- پاسکال
- پایان نامه
- پایتون
- جاوا
- جاوا اسکریپت
- جی کوئری
- داده کاوی
- دلفی
- رباتیک
- سئو
- سایر کتاب ها
- سخت افزار
- سی اس اس
- سی پلاس پلاس
- سی شارپ
- طراحی الگوریتم
- فتوشاپ
- مقاله
- مهندسی نرم افزار
- هک و امنیت
- هوش مصنوعی
- ویژوال بیسیک
- نرم افزار و ابزار برنامه نویسی
- وردپرس