آموزش استاندارد ERC 7201 در سالیدیتی

استاندارد 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 به توسعه دهندگان این امکان را می‌دهد که در آینده، بدون ریسک تداخل داده ها، قابلیت های جدیدی به قرارداد اضافه کنند.

قطعه کد برای متغیر private __gap از نوع uint256[45]

محاسبه اندازه متغیر __gap

اندازه آرایه __gap به گونه‌ای محاسبه می‌شود که مجموع اسلات های ذخیره سازی مورد استفاده در هر قرارداد همیشه برابر با ۵۰ باشد. بنابراین، اگر یک قرارداد شامل ۵ متغیر وضعیت باشد، آرایه __gap باید شامل ۴۵ اسلات خالی باشد تا این عدد به ۵۰ برسد.

در تصویر بالا می‌توان ساختار قرارداد ERC20Upgradeable.sol نسخه ۴.۹ را مشاهده کرد که از این الگو پیروی می‌کند.

حالا بیایید همین مفهوم را در مثال خود اعمال کنیم.

اگر قرارداد والد ۵ متغیر وضعیت داشته باشد و در انتهای خود آرایه‌ای با ۴۵ اسلات خالی به عنوان gap قرار دهد، در این صورت ساختار حافظه قرارداد پیاده سازی – که توسط قرارداد پراکسی نیز تکرار می‌شود – مشابه تصویر زیر خواهد بود.

این رویکرد تضمین می‌کند که در آینده بتوان متغیرهای جدید را بدون آسیب زدن به داده های قبلی، در فضای ذخیره سازی قرارداد اضافه کرد.

تخصیص جایگاه ذخیره سازی برای یک قرارداد پیاده سازی که از یک قرارداد والد که شامل متغیر private gap است، ارث بری می‌کند.

اکنون ۴۵ اسلات خالی در اختیار قرارداد والد قرار دارد تا در صورت نیاز به ارتقا، از آن‌ها استفاده کند. فرض کنیم در آینده بخواهیم متغیر جدیدی به نام variableN به قرارداد والد اضافه کنیم. در این حالت، کافی است این متغیر را قبل از آرایه gap قرار دهیم.

با انجام این کار، تنها کاری که نیاز داریم انجام دهیم این است که طول gap را یک واحد کاهش دهیم، چرا که یک اسلات آن را به متغیر جدید اختصاص داده‌ایم.

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

انیمیشن نحوه استفاده از متغیر private gap هنگام اعلام متغیرهای state در قرارداد پیاده‌ سازی

نمودار شکاف ذخیره‌سازی یک قرارداد پیاده‌سازی که از قرارداد والد که از متغیر private gap استفاده می‌کند، ارث‌بری می‌کند.

محدودیت های استفاده از gap در قراردادهای قابل ارتقا

آرایه gap به توسعه دهندگان کمک می‌کند متغیرهای جدید را بدون تغییر در رفتار قبلی قرارداد، به آن اضافه کنند. این فضا مانند یک رزرو حافظه عمل می‌کند و از بروز برخورد در اسلات های ذخیره سازی جلوگیری می‌کند. به همین دلیل، هنگام طراحی قراردادهای قابل ارتقا، قراردادن gap در انتهای هر قرارداد پیاده سازی اقدام هوشمندانه‌ای است.

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

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

مثال برخورد اسلات در ارث بری از قرارداد پدربزرگ

بنابراین، پیدا کردن راهکاری برای تنظیم ساختار حافظه در قراردادهای پیاده سازی، بدون ایجاد تداخل در اسلات ها، کاملاً ضروری است.

بهترین راه حل این است که به هر قرارداد پیاده سازی در زنجیره وراثت، فضای ذخیره سازی اختصاصی خودش را بدهیم. با این کار، متغیرهای هر قرارداد در محدوده‌ای مجزا ذخیره می‌شوند و دیگر با متغیرهای سایر قراردادها تداخل پیدا نمی‌کنند.

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

یکی از روش های رایج برای پیاده سازی این ساختار، استفاده از struct هاست. در ادامه بررسی می‌کنیم که ساختار حافظه در سالیدیتی چگونه عمل می‌کند و چطور می‌توان یک ساختار ریشه‌ای مبتنی بر فضای نام ایجاد کرد.

ساختار ریشه ای مبتنی بر فضای نام

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

در این ساختار، L نشان‌دهنده محل ذخیره سازی (storage location) است، n یک عدد طبیعی است، و H(k) یک تابع هش است که روی کلید خاصی به نام k اعمال می‌شود. این کلید می‌تواند، برای مثال، کلید یک mapping یا ایندکس یک array باشد.

ساختار کلی حافظه معمولاً با ترکیبی از موقعیت های عددی و هش شده کار می‌کند. استفاده از فضای نام در این ساختار، به ما اجازه می‌دهد که با اختصاص دادن هر بخش از حافظه به یک ساختار مشخص، تداخل در اسلات های ذخیره سازی را به طور کامل حذف کنیم.

در ادامه، نحوه پیاده سازی این ایده با استفاده از ساختارهای struct و مدیریت دستی حافظه را بررسی خواهیم کرد.

فرمول ارائه‌شده در بالا نشان می‌دهد که متغیرهای وضعیت را می‌توان در سه موقعیت اصلی در ساختار حافظه یافت:

  1. در ریشه (Root): که به‌صورت پیش‌فرض در اسلات صفر قرار دارد.

  2. در ترکیبی از قواعد نحوی به‌همراه یک عدد طبیعی: مانند لیست متغیرها یا ساختارهای داخلی مانند آرایه‌ها و مپ‌ها.

  3. در مقدار هش شده (keccak) از یک کلید خاص: که به‌طور قطعی و قابل پیش‌بینی از مقدار کلید و محل متغیر نسبت به ریشه محاسبه می‌شود.

نکته مهم این است که تمام این موقعیت ها در نهایت به ریشه وابسته هستند. سالیدیتی برای هر قرارداد، مقدار 0 را به عنوان ریشه حافظه (root slot) در نظر می‌گیرد.

اگر بخواهیم محل ذخیره سازی متغیرهای یک قرارداد را به‌صورت مستقل تعریف کنیم، باید ریشه را تغییر دهیم؛ اما نه به‌صورت فیزیکی، بلکه با استفاده از یک برچسب منحصربه‌فرد که فقط به آن قرارداد اختصاص دارد. این برچسب همان چیزی است که ما به عنوان فضای نام (namespace) تعریف می‌کنیم.

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

نمودار سه نمونه فضای نام

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

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

پیش از آنکه به پیاده سازی برسیم، باید فرمولی را بررسی کنیم که استاندارد ERC 7201 برای محاسبه ریشه حافظه از روی فضای نام ارائه داده است.

فرمول پیشنهادی برای محاسبه ریشه حافظه بر اساس فضای نام

اگر قصد داشته باشیم ریشه اسلات ذخیره سازی یک قرارداد دارای فضای نام را تغییر دهیم، باید فرمولی برای محاسبه این ریشه جدید تعریف کنیم. استاندارد ERC-7201 فرمول زیر را برای این منظور پیشنهاد داده است:

منطق پشت این فرمول به شرح زیر است:

  • کاهش عدد هش‌شده به اندازه ۱ (یعنی keccak256(namespace) - 1) باعث می‌شود پیش‌تصویر هش (preimage) قابل پیش‌بینی نباشد. این کار یک لایه امنیتی اضافه ایجاد می‌کند.

  • اجرای تابع keccak256 برای بار دوم احتمال تداخل با اسلات هایی که سالیدیتی به‌طور داخلی تولید می‌کند را کاهش می‌دهد. زیرا محل ذخیره سازی متغیرهایی با اندازه پویا (مثل mapping و array) نیز با استفاده از keccak256 تعیین می‌شود.

  • عملیات AND با مکمل 0xff (& ~0xff) بایت سمت راست موقعیت حافظه را به 00 تبدیل می‌کند. این کار آمادگی لازم را برای تغییر ساختار ذخیره سازی در آینده (مانند جایگزینی درخت‌های Merkle با درخت‌های Verkle در اتریوم) فراهم می‌کند. در آن ساختار جدید، می‌توان ۲۵۶ اسلات مجاور را به صورت همزمان فعال (warmed) کرد.

فرمولی که در بالا معرفی شد، یک ویژگی حیاتی را برای ریشه جدید تضمین می‌کند: اینکه این ریشه با هیچ‌کدام از موقعیت‌های حافظه‌ای که کامپایلر سالیدیتی ممکن است به‌صورت پیش‌فرض به متغیرها اختصاص دهد، تداخل نخواهد داشت.

برای درک بهتر، می‌توان این فرمول را به کمک یک قرارداد سالیدیتی پیاده سازی کرد تا مقدار ریشه حافظه (storage root) را از یک فضای نام مشخص محاسبه کنیم. در مثال زیر، تابع getStorageAddress این محاسبه را انجام می‌دهد:

اگر عبارت "openzeppelin.storage.ERC20" را به عنوان فضای نام وارد کنیم، مقدار هش نهایی به صورت زیر خواهد بود:
در واقع OpenZeppelin دقیقاً از همین روش برای تعیین ریشه حافظه در قرارداد ERC20Upgradeable نسخه ۵ استفاده کرده است — موردی که در بخش بعدی به‌طور دقیق بررسی خواهیم کرد.

استفاده از فیلدهای Struct به عنوان متغیرهای وضعیت

در بخش قبلی، نحوه محاسبه ریشه حافظه قرارداد بر اساس فضای نام را بررسی کردیم. حالا برای ادامه مسیر، باید بتوانیم متغیرهای ذخیره سازی را به‌گونه‌ای گروه بندی کنیم که از همان ریشه جدید شروع شوند. با این حال، نمی‌توانیم این متغیرها را به‌صورت مستقیم به عنوان state variable تعریف کنیم، چون در این صورت، کامپایلر سالیدیتی دوباره تخصیص حافظه را از اسلات صفر آغاز می‌کند — و این دقیقاً چیزی است که می‌خواهیم از آن اجتناب کنیم.

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

به عنوان نمونه، قرارداد زیر این مفهوم را به خوبی نشان می‌دهد:

به‌صورت فرضی، اگر این struct را به عنوان اولین متغیر وضعیت قرارداد تعریف کنیم (کاری که استاندارد ERC-7201 انجام نمی‌دهد)، ترتیب قرارگیری فیلدها در حافظه به شکل زیر خواهد بود:

  • fieldA در اسلات شماره ۰ قرار می‌گیرد

  • fieldB در اسلات شماره ۱

  • پایه‌ (base) مربوط به mapping تحت عنوان fieldC در اسلات شماره ۲ ذخیره می‌شود

  • و به همین ترتیب ادامه پیدا می‌کند

برای آنکه بتوانیم محل دقیق ذخیره سازی هر فیلد داخل یک struct را مشخص کنیم، باید از یک فرمول ساده استفاده کنیم. در این فرمول، base به اسلاتی اشاره دارد که struct از آن نقطه به بعد فضای حافظه را اشغال می‌کند:

توجه داشته باشید که این همان فرمول قبلی مربوط به ساختار حافظه است؛ تنها تفاوت اینجاست که به جای ریشه (root)، از پایه struct استفاده کرده‌ایم. به عبارت دیگر، struct از طریق فیلدهایش همان الگوی ساختار حافظه را حفظ می‌کند. این یعنی می‌توانیم پایه struct را به عنوان ریشه جدید در نظر بگیریم.

در مثال بالا، پایه struct در اسلات صفر قرار دارد، اما ما می‌توانیم اسلات دیگری را به عنوان پایه struct انتخاب کنیم. برای انجام این کار، می‌توانیم از زبان سطح پایین YUL استفاده کنیم؛ همان‌طور که در مثال زیر نشان داده شده است.

زمانی که از دستور myStruct.slot := 0x02 استفاده می‌کنیم، به‌طور صریح پایه struct را تغییر می‌دهیم و در واقع ساختار حافظه‌ای را شبیه سازی می‌کنیم که در آن، ریشه دیگر در اسلات صفر قرار ندارد. در این حالت، باید تمام متغیرهایی که در حالت عادی به‌صورت متغیر وضعیت تعریف می‌شدند را به‌عنوان فیلدهای struct داخل آن قرار دهیم.

پایه struct به عنوان ریشه جدید برای فیلدهای آن عمل می‌کند، که دقیقاً همان چیزی است که قصد داشتیم به آن برسیم.

یکی از معایب این روش این است که باید هر بار که می‌خواهیم فیلدی را ذخیره یا بازیابی کنیم، به‌صورت صریح پایه struct را مشخص کنیم.

از آنجا که همواره نیاز داریم به پایه struct ارجاع دهیم، بهتر است برای این کار یک تابع کمکی (utility function) ایجاد کنیم. در پیاده سازی قراردادهای قابل ارتقای OpenZeppelin، یک تابع خصوصی برای همین منظور طراحی شده است تا به‌راحتی به struct و پایه آن اشاره کنیم. برای نمونه، در فایل ERC20Upgradeable.sol می‌توان این رویکرد را مشاهده کرد:

قطعه کد تابع برای تنظیم پایه ساختار؛ _getERC20Storage()

در ادامه، می‌بینیم که تمام متغیرهایی که در حالت عادی به عنوان متغیر وضعیت (state variables) تعریف می‌شدند، باید به عنوان فیلدهایی درون یک struct تعریف شوند.

بیایید مثالی ببینیم که در آن از تابع کمکی (utility function) برای دسترسی به یکی از فیلدهای struct استفاده می‌کنیم؛ برای نمونه، نام توکن در قرارداد ERC20Upgradeable.sol.

همان‌طور که در بالا مشاهده می‌کنید، زمانی که می‌خواهیم به متغیرهای ذخیره سازی دسترسی پیدا کنیم، کافی است تابع _getERC20StorageLocation() را فراخوانی کنیم. این تابع، ریشه فضای ذخیره سازی مرتبط با فضای نام را به صورت مقدار bytes32 باز می‌گرداند.

همین رویه در زمان به‌روزرسانی فیلدها نیز کاربرد دارد. اشاره‌گر $ در پایه struct قرار دارد؛ بنابراین می‌توانیم با استفاده از نگارش $.[نام فیلد] به فیلدهای struct دسترسی پیدا کنیم یا آن‌ها را به‌روزرسانی کنیم.

در تصویر زیر، بخشی از کد تابع _update در قرارداد ERC20Upgradeable.sol را مشاهده می‌کنید و اینکه چگونه از این ساختار برای به‌روزرسانی موجودی حساب‌ها هنگام انتقال توکن استفاده می‌شود.

قطعه کد تابع erc20upgradeable _update

خلاصه ای از نحوه پیاده سازی ساختار حافظه با ریشه مبتنی بر فضای نام

برای پیاده سازی این الگو، کافی است مراحل زیر را دنبال کنید:

  1. از تعریف مستقیم متغیرهای وضعیت خودداری کنید.

  2. تمام متغیرهایی که در حالت عادی به‌عنوان متغیر وضعیت تعریف می‌شدند، باید به صورت فیلدهایی در یک struct تعریف شوند.

  3. برای هر قرارداد، یک فضای نام (namespace) منحصربه‌فرد انتخاب کنید.

  4. با استفاده از تابع پیشنهادی در ERC 7201، ریشه جدید (root) قرارداد را بر اساس فضای نام محاسبه کنید.

  5. یک تابع کمکی ایجاد کنید که ارجاعی به پایه struct بازگرداند. در این تابع، با استفاده از اسمبلی (assembly) به‌صورت صریح مشخص کنید که struct باید در اسلاتی قرار گیرد که از فضای نام محاسبه شده است.

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

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

مستندسازی موقعیت ذخیره سازی سفارشی با استفاده از NatSpec

فرمت مستندسازی NatSpec (مخفف Ethereum Natural Language Specification Format) روشی استاندارد برای نوشتن توضیحات داخل قراردادهای هوشمند است. این توضیحات هم برای توسعه دهندگان و هم برای ابزارهای تحلیل کد قابل استفاده هستند. به‌عنوان نمونه، یک کامنت NatSpec برای مستندسازی یک تابع به شکل زیر نوشته می‌شود:

یکی از اهداف استاندارد ERC 7201 این است که استفاده از فضای نام‌ها را از طریق NatSpec به‌صورت دقیق مستند کند. برای این کار، از دستور خاصی با قالب زیر استفاده می‌شود:
مقدار FormulaID نشان دهنده فرمولی است که برای محاسبه ریشه ذخیره سازی بر اساس فضای نام استفاده شده است، در حالی که namespaceId به فضای نام خاصی اشاره دارد که در این ساختار مدنظر قرار گرفته است. این توضیح (annotation) مربوط به خود struct است، بنابراین باید دقیقاً در بالای تعریف struct نوشته شود.

فرمول پیشنهادی در این استاندارد با برچسب erc7201 مشخص شده است، بنابراین اگر بخواهیم در NatSpec از این فرمول استفاده کنیم، باید از قالب زیر پیروی کنیم:

برای مثال، در قرارداد ERC20Upgradeable، فضای نام انتخاب شده عبارت است از openzeppelin.storage.ERC20. در نتیجه، annotation مربوطه باید به این صورت نوشته شود:

 

5/5 - (1 امتیاز)

راستی! برای دریافت مطالب جدید در کانال تلگرام یا پیج اینستاگرام سورس باران عضو شوید.

دوره آموزش طراحی فروشگاه اینترنتی بدون کد نویسی در 8 ساعت
  • انتشار: ۱۲ تیر ۱۴۰۴

دسته بندی موضوعات

آخرین محصولات فروشگاه

مشاهده همه

نظرات

بازخوردهای خود را برای ما ارسال کنید