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

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

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

توسعه دهنده از این متادیتا برای ارسال آرگومان به توابع قرارداد پیاده سازی شده استفاده می کند تا رفتار هر کلون را به صورت مستقل تنظیم کند.

از آنجا که بایت کد MetaProxy ساختار ثابتی دارد، ابزارهایی مثل Etherscan می توانند آن را به راحتی شناسایی کنند. این ابزارها همچنین بررسی می کنند که پراکسی به کدام قرارداد پیاده سازی متصل شده و چه متادیتایی به آن اضافه شده است.

بایت کد MetaProxy بدون متادیتا:

این بایت کد دقیقاً ۶۵ بایت است. بخش اول آن ۱۱ بایت دارد که مخصوص راه اندازی اولیه است. بخش دوم نیز ۵۴ بایت دارد که کد اجرایی را شامل می شود.

اگرچه ساختار این بایت کد شبیه به پراکسی مینیمال است، اما تفاوت‌هایی هم دارد. مثلاً بخشی از آن شامل دستورهای اضافه است. در تحلیل های فنی، این بخش ها را معمولاً با رنگ سبز مشخص می کنند. بعداً درباره این تفاوت ها بیشتر توضیح می دهیم.

بایت کد متاپروکسی ERC-3448 با بخش‌های مرتبط هایلایت شده

آدرس زیر به صورت پیش‌فرض در بایت کد وجود دارد:

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

ایجاد یک قرارداد ERC20 با استفاده از استاندارد MetaProxy

در این بخش، قصد داریم یک کلون از قرارداد ERC20 با استفاده از استاندارد MetaProxy ایجاد کنیم. بیایید ببینیم این کار چگونه انجام می شود و متوجه شویم که متادیتا چطور به این کلون اضافه می شود.

برای پیاده سازی قرارداد ERC20، از نسخه قابل ارتقای OpenZeppelin یعنی ERC20Upgradeable استفاده می کنیم. این نسخه به جای استفاده از سازنده (constructor)، تابعی به نام ERC20_init دارد که متغیرهای حالت (مثل نام و نماد توکن) را مقداردهی اولیه می کند. دلیل این کار آن است که در الگوهای پراکسی، از جمله MetaProxy، امکان استفاده از سازنده وجود ندارد.

علت این محدودیت آن است که سازنده تنها در زمان استقرار قرارداد اصلی اجرا می شود. اگر از سازنده استفاده کنیم، متغیرهای حالت مانند name و symbol فقط در قرارداد پیاده سازی مقداردهی می شوند و نه در کلون. در نتیجه، کلون ERC20 که از طریق MetaProxy ساخته می شود، این مقادیر را دریافت نمی کند؛ زیرا سازنده در واقع اطلاعات را در فضای ذخیره سازی قرارداد اصلی قرار می دهد، نه در کلون.

با این حال، در این پروژه حتی از تابع مقداردهی اولیه نیز استفاده نمی کنیم. چون می توانیم name، symbol و totalSupply را مستقیماً به صورت متادیتا به بایت کد کلون اضافه کنیم و سپس هنگام نیاز، آن ها را از همان بایت کد استخراج کنیم.

قرارداد پیاده سازی ERC20

دریافت متادیتا

در قرارداد پیاده سازی، از تابع getMetadata برای بازگرداندن متادیتای کلون استفاده می کنیم. از آنجا که در استاندارد MetaProxy، هر بار که تابعی از کلون فراخوانی می شود، متادیتا به صورت خودکار بارگذاری می شود (این رفتار بخشی از طراحی این استاندارد است که در ادامه مقاله به آن خواهیم پرداخت)، تابع getMetadata به ما کمک می کند تا متادیتا را از ورودی فراخوانی استخراج کنیم و به صورت یک تاپل (Tuple) برگردانیم.

این تابع در توابع name، symbol و totalSupply قرارداد ERC20 نیز کاربرد دارد. به این صورت که هر کدام از این توابع، بخشی از متادیتا را از طریق getMetadata دریافت می کنند؛ برای مثال، نام و نماد به صورت رشته متنی (string) استخراج می شوند و مقدار عرضه کل (totalSupply) به صورت عدد صحیح بدون علامت (uint256).

ما این تابع را بر اساس پیاده سازی نمونه ای که در اینجا ارائه شده، توسعه داده و متناسب با نیازهای خود در قرارداد ERC20 تغییر داده ایم.

قرارداد Factory

در مستندات اصلی EIP، لینکی برای پیاده سازی قرارداد MetaProxyFactory نیز وجود دارد. ما این قرارداد را در پروژه خود import می کنیم و از آن ارث بری (inherit) می کنیم.

قرارداد MetaProxyFactory شامل منطق مربوط به ایجاد کلون های جدید از نوع MetaProxy است. این کد مشخص می کند که چگونه می توان به صورت برنامه نویسی، کلون های جدیدی ساخت که دارای متادیتای مخصوص به خود هستند.

ایجاد کلون – توضیح قرارداد Factory

قرارداد ERC20MetaProxyFactory همان کارخانه ساخت کلون‌ در پروژه ما است. از طریق این قرارداد می‌توان نمونه‌های جدیدی از کلون ها را ایجاد کرد. برای انجام این کار، از تابع _metaProxyFromBytes استفاده می کنیم که از قرارداد پایه MetaProxyFactory به ارث رسیده است.

تابع _metaProxyFromBytes دو ورودی دریافت می کند:

  1. آدرس قرارداد پیاده سازی (Implementation Contract): به همین دلیل، ابتدا یک قرارداد ERC20Implementation جدید ایجاد می کنیم (با استفاده از کلمه کلیدی new) و آدرس آن را به عنوان ورودی می فرستیم.

  2. متادیتا: اطلاعاتی که قصد داریم در بایت کد کلون قرار دهیم.

از آنجا که بایت کد قراردادهای هوشمند در قالب هگزادسیمال (hex) نوشته می شود، قبل از الحاق متادیتا به بایت کد، باید آن را با استفاده از ABI رمزگذاری (abi encode) کنیم. به همین دلیل، ابتدا آرگومان های تابع createClone را رمزگذاری می کنیم و سپس نتیجه را به عنوان متادیتا به تابع _metaProxyFromBytes می دهیم.

این تابع کلون جدید را ایجاد می کند و آدرس آن را به عنوان خروجی بازمی گرداند.

در ادامه، امضای (signature) تابع _metaProxyFromBytes را مشاهده می کنید:

این تابع، منطق اصلی ایجاد کلون ها را اجرا می کند و نقش کلیدی در پیاده سازی الگوی MetaProxy دارد.

استقرار کلون (Deploying the Clone)

در این بخش، یک اسکریپت Hardhat ارائه شده که وظیفه دارد قراردادها را مستقر کند و با یک کلون مستقرشده در شبکه Sepolia تعامل برقرار کند:

پس از اجرای این اسکریپت، کنسول خروجی زیر را نمایش داده است:

همچنین ما قراردادهای خود را در شبکه Sepolia مستقر کرده‌ایم و در ادامه، مشخصات سه قرارداد کلیدی را مشاهده می‌کنید:

  1. قرارداد کارخانه MetaProxy

  2. قرارداد پیاده سازی ERC20

  3. قرارداد ERC20 مبتنی بر MetaProxy

نکته مهم اینجاست که Etherscan در صفحه مربوط به قرارداد ERC20 مبتنی بر MetaProxy، دکمه های “Read” و “Write as Proxy” را نمایش می دهد. با این کار، Etherscan نشان می دهد که این قرارداد را نه به عنوان یک قرارداد معمولی، بلکه به عنوان یک پراکسی واقعی شناسایی کرده است.

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

مدیریت خطاهای Revert

همان طور که در مقدمه اشاره شد، اگر در زمان فراخوانی یک تابع توسط کلون پراکسی، خطایی در قرارداد پیاده سازی رخ دهد، پیام خطا (revert payload) به کلون بازمی‌گردد و به کاربر نمایش داده می‌شود.

بیایید این رفتار را در عمل بررسی کنیم و ببینیم آیا همان‌طور که انتظار داریم کار می‌کند یا نه.

در همان مثال قبلی قرارداد ERC20، تابع transferFrom را بدون تنظیم allowance (مقدار مجاز انتقال) فراخوانی می‌کنیم تا ببینیم آیا تراکنش موفق می‌شود یا پیام خطا به ما بازگردانده می‌شود.

برای این آزمایش از اسکریپت Hardhat زیر استفاده می‌کنیم:

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

توضیح بایت کد کلون ERC20 مستقر شده

قبلاً اشاره کردیم که متادیتای کلون در انتهای بایت کد آن قرار می‌گیرد. در این بخش، قصد داریم ساختار دقیق بایت کد نهایی کلون MetaProxy را بررسی کنیم تا بفهمیم هر بخش از آن چه نقشی دارد.

در تمام کلون‌ها، بایت کد اولیه با ساختار استاندارد MetaProxy آغاز می‌شود. تنها تفاوت این است که در انتهای هر کلون، متادیتای مخصوص آن اضافه شده است.

اکنون بیایید نگاهی به بایت کد کلون ERC20 بیندازیم:

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

بایت کد نهایی این کلون، در مجموع ۳۱۰ بایت طول دارد.

در ادامه، هر بخش از این بایت کد را جداگانه بررسی می‌کنیم و دقیقاً توضیح می‌دهیم که کدام قسمت به کدام متغیر مربوط می‌شود.

رمزگذاری متادیتا شامل آدرس‌های حافظه (offsets) برای محل ذخیره مقادیر، طول رشته‌های رمزگذاری‌شده، خود مقادیر، و اشاره‌گر به حافظه آزاد (free memory pointer) است. در این بخش، به صورت دقیق ساختار این متادیتا را بررسی می کنیم.

بر اساس استاندارد ABI، قرارداد ابتدا سه بخش ۳۲ بایتی را در متادیتا قرار می دهد. اگر مقدار مورد نظر نوع ثابتی داشته باشد، مستقیماً مقدار را در همان اسلات می نویسد. اما اگر نوع آن دینامیک باشد، فقط آدرس حافظه را در اسلات قرار می دهد و مقدار واقعی را در بخش های بعدی متادیتا ذخیره می کند.

در مثال ما، سه مقدار داریم: name، symbol و totalSupply. name و symbol از نوع رشته هستند و نوع دینامیک دارند. بنابراین، قرارداد برای آن ها آدرس محل داده ها را در متادیتا قرار می دهد. اما برای totalSupply که نوع عدد صحیح بدون علامت دارد، مقدار را مستقیماً در همان بخش متادیتا می نویسد. به این ترتیب، قرارداد می تواند هنگام اجرا، مقادیر دینامیک را از حافظه بخواند و مقادیر ثابت را مستقیماً استفاده کند.

همان‌طور که پیش‌تر اشاره کردیم، کد اجرایی (runtime code) در استاندارد MetaProxy برابر با ۵۴ بایت است. اگر بایت کد کلون ERC20 را به دو بخش تقسیم کنیم و ۵۴ بایت اول را که مربوط به کد اجرایی است جدا کنیم، بخش باقی‌مانده شامل دو قسمت می‌شود:

  1. متادیتای رمزگذاری شده با ABI به طول ۲۲۴ بایت

  2. مقدار طول متادیتا که در انتهای بایت کد و در قالب یک کلمه ۳۲ بایتی (word) ذخیره شده است

مطابق با استاندارد:

«…تمام داده هایی که بعد از بایت کد MetaProxy می‌آیند، می‌توانند شامل هر نوع متادیتایی باشند. اما آخرین ۳۲ بایت از بایت کد باید طول دقیق متادیتا را به بایت نشان دهند.»

در مثال ما، متادیتا دقیقاً ۲۲۴ بایت طول دارد و مقدار آن در ۳۲ بایت نهایی ذخیره شده است:

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

در واقع، این روش به توسعه دهنده اجازه می‌دهد با نوشتن چند خط کد ساده، متادیتا را از قرارداد استخراج کرده و آن را تحلیل کند؛ بدون نیاز به پیاده سازی ساختارهای پیچیده برای ردیابی داده ها.

اگر متادیتای موجود در این بخش را رمزگشایی (decode) کنیم، در واقع به داده‌ های مقداردهی اولیه (initialization data) کلون دست پیدا می کنیم.

رمزگشایی توکن پروکسی

بیایید گام به گام از طریق mnemonic های بایت کد پیش برویم

این توضیح، یک مرور کلی از نحوه عملکرد بایت کد کلون است. به طور خلاصه، این بایت‌کد ابتدا داده‌های ورودی تراکنش (calldata) را کپی می‌کند و سپس با استفاده از دستور delegatecall، آن‌ها را همراه با متادیتا به قرارداد پیاده‌سازی ارسال می‌کند.

نکته مهم این است که چون متادیتا در تمام فراخوانی ها به صورت خودکار ارسال می‌شود، حتی در توابعی که به آن نیاز ندارند نیز همراه درخواست خواهد بود. برای مثال، تابع balanceOf() در استاندارد ERC20 هیچ ارتباطی با متادیتا ندارد، اما همچنان متادیتا در هنگام فراخوانی این تابع ارسال می‌شود. این موضوع بخشی از ساختار کلی استاندارد MetaProxy است که همیشه متادیتا را به delegatecall ضمیمه می‌کند.

جمع بندی

توسعه دهندگان، استاندارد MetaProxy در EIP-3448 را به‌عنوان نسخه ای پیشرفته تر از استاندارد EIP-1167 (Minimal Proxy) معرفی کرده‌اند. این استاندارد امکان می‌دهد تا متادیتای تغییرناپذیر به بایت کد اجرایی هر کلون اضافه شود.

با استفاده از MetaProxy، توسعه دهنده می تواند مقدارهای دلخواه را مستقیماً در بایت کد کلون تعریف کند، به جای آن که آن‌ها را در حافظه ذخیره کند. این کار باعث می‌شود نیاز به استفاده از storage کاهش یابد و در نتیجه هزینه گس نیز کمتر شود.

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

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

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

پکیج آموزش پیشرفته ASP.NET Core + طراحی فروشگاه اینترنتی
  • انتشار: ۱۹ تیر ۱۴۰۴

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

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

مشاهده همه

نظرات

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