آموزش استاندارد EIP 1967

استاندارد EIP 1967 مشخص می‌کند که اطلاعات مورد نیاز قراردادهای پراکسی باید در کدام بخش از حافظه ذخیره شوند تا این قراردادها بتوانند به درستی اجرا شوند. این استاندارد هم در الگوی UUPS (استاندارد جهانی پراکسی قابل ارتقا) و هم در الگوی Transparent Upgradeable Proxy (پراکسی قابل ارتقا به‌صورت شفاف) مورد استفاده قرار می‌گیرد.

نکته مهم: EIP-1967 صرفاً محل قرارگیری برخی متغیرهای ذخیره سازی و لاگ هایی که در صورت تغییر آن‌ها منتشر می‌شوند را مشخص می‌کند، نه بیشتر. این استاندارد توضیحی درباره نحوه به‌روزرسانی این متغیرها یا اینکه چه کسی مجاز به مدیریت آن‌هاست ارائه نمی‌دهد. همچنین هیچ تابع عمومی برای پیاده سازی تعریف نمی‌کند. دستورالعمل‌های مربوط به مدیریت و به‌روزرسانی این متغیرها در اسناد مربوط به الگوی Transparent Proxy یا مشخصات UUPS بیان شده‌اند.

برای آنکه یک پراکسی بتواند به‌درستی کار کند، به دو متغیر کلیدی نیاز دارد: آدرس قرارداد پیاده سازی (implementation address) و مدیر (admin). آدرس پیاده سازی همان جایی است که پراکسی فراخوانی ها را به آن منتقل می‌کند (delegate می‌کند). در زمان ارتقا، این آدرس به قرارداد جدید و ارتقا یافته تغییر پیدا می‌کند. فقط مدیر (admin) اجازه دارد این تغییرات را اعمال کند.

پیش نیازها

در این مقاله، ما فرض می‌کنیم که شما با مفاهیم پایه مانند نحوه کار پراکسی ها و دستور delegatecall آشنا هستید. همچنین باید بدانید شیارهای ذخیره سازی (storage slots) چیستند، شناسه توابع (function selectors) چه کاربردی دارند، و تداخل این شناسه ها در قراردادهای پراکسی به چه معناست.

روش نادرست طراحی شیارهای پراکسی

نمونه‌ای که در ادامه می‌بینید، یک طراحی اشتباه برای پراکسی است:

تصویر مربوط به طراحی اشتباه پراکسی

اول از همه باید بدانید که در این طراحی، احتمال تداخل بین شناسه تابع changeAdmin() و یکی از توابع قرارداد پیاده سازی وجود دارد. این احتمال کم نیست و نمی‌توان آن را نادیده گرفت. استاندارد EIP 1967 درباره نحوه مدیریت این تداخل هیچ توضیحی نمی‌دهد. برای پیشگیری از چنین مشکلی باید از الگوی Transparent Upgradeable Proxy یا استاندارد UUPS استفاده کنید. چون فقط آن‌ها نحوه مدیریت این وضعیت را مشخص کرده اند.
توجه داشته باشید که EIP 1967 مسئولیتی در قبال تداخل شناسه توابع ندارد.

هدف اصلی EIP 1967 چیز دیگری است. این استاندارد از تداخل متغیرهای implementation و admin با متغیرهای ذخیره سازی قرارداد پیاده سازی جلوگیری می‌کند. دلیلش این است که این دو متغیر معمولاً از شیارهای 0 و 1 حافظه استفاده می‌کنند. این شیارها همان‌هایی هستند که بسیاری از قراردادهای پیاده سازی نیز به کار می‌برند.

جلوگیری از تداخل

از آنجا که آدرس های admin و implementation ممکن است در طول زمان تغییر کنند، نمی‌توان آن‌ها را به صورت ثابت (immutable) تعریف کرد. بنابراین باید آن‌ها را در متغیرهای ذخیره سازی قرار داد. اما نکته مهم اینجاست: این متغیرها باید در شیارهایی از حافظه قرار بگیرند که با متغیرهای ذخیره سازی قرارداد پیاده سازی تداخلی نداشته باشند.

اینجا یک نکته کلیدی وجود دارد: فضای شیارهای ذخیره سازی به‌قدری وسیع است که عملاً بی‌نهایت به حساب می‌آید؛ دقیقاً برابر با 2**256 – 1.

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

شیارهای ذخیره سازی برای implementation و admin

آدرس قرارداد پیاده سازی (implementation) در شیار حافظه زیر ذخیره می‌شود:

و آدرس مدیر (admin) در این شیار قرار می‌گیرد:
این شیارها به صورت تصادفی شبه امن (pseudorandom) از عبارات زیر به دست آمده اند:
و
اگر این مقادیر را به دسیمال (عدد ده دهی) تبدیل کنیم، مقدار شیار مربوط به implementation برابر است با:
و شیار مربوط به admin برابر است با:

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

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

استخراج شیارهای ذخیره سازی

وقتی از تابع keccak256 برای هش کردن یک رشته استفاده می‌کنیم، خروجی آن عملاً یک عدد تصادفی شبه امن محسوب می‌شود. اگر از این مقدار عدد ۱ را کم کنیم، به عددی می‌رسیم که هیچ پیش‌تصویری (preimage) شناخته‌شده برای آن وجود ندارد. به بیان ساده، هیچ قراردادی نمی‌تواند مقدار خاصی را به keccak256 بدهد تا دقیقاً همان شیار حافظه تولید شود و با این مقادیر تداخل داشته باشد.

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

فرضیات مربوط به استفاده از شیارهای ذخیره سازی

البته، همیشه این احتمال وجود دارد که توسعه دهندگان قرارداد پیاده سازی، عمداً به شیارهای حافظه مربوط به پراکسی بنویسند. برای مثال، می‌توانند با استفاده از کد اسمبلی زیر، مستقیماً در شیار implementation مقداردهی کنند:

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

بنابراین، این ساختار به یک فرض مهم متکی است: توسعه دهندگان نباید چنین کاری انجام دهند.

EIP 1967 تشخیص قراردادهای پراکسی را برای Etherscan آسان می‌کند

برای نمونه، به تصویر قرارداد پراکسی پروژه Compound Finance توجه کنید:

تصویر مربوط به قرارداد پراکسی در Compound Finance

با بررسی مقدار شیارهای مشخص‌شده در EIP 1967، مرورگرهای بلاک (مانند Etherscan) می‌توانند تشخیص دهند که آیا یک قرارداد از نوع پراکسی هست یا خیر. اگر این شیارها دارای مقادیری غیر از صفر باشند، می‌توان نتیجه گرفت که قرارداد مربوطه یک پراکسی است.

در تصویر بالا چند نکته مهم وجود دارد:

  • در دایره بنفش، می‌بینیم که Etherscan تشخیص داده این قرارداد از الگوی EIP 1967 پیروی می‌کند.

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

  • در دایره قرمز، گزینه‌هایی برای خواندن (Read) و نوشتن (Write) در اختیار داریم. این گزینه‌ها برای هر دو قرارداد پراکسی و پیاده سازی فعال هستند. اما به طور کلی، تعامل اصلی با قرارداد پراکسی انجام می‌شود؛ زیرا وضعیت (state) اصلی قرارداد در آن ذخیره می‌شود.

Beacon Slot چیست؟

اگر به نسخه اصلی استاندارد EIP 1967 مراجعه کنید، با مفهومی به نام Beacon Slot مواجه می‌شوید. چون Beaconها در عمل بسیار کم‌کاربرد هستند، بحث درباره آن‌ها معمولاً به بخش‌های پایانی منابع تخصصی واگذار می‌شود.

اگرچه بررسی Beaconها موضوعی جداگانه است، اما به‌طور خلاصه باید گفت: Beacon یک مکانیزم برای به‌روزرسانی همزمان چند قرارداد پراکسی است. به عنوان مثال، می‌توان چندین پراکسی را به یک قرارداد پیاده سازی واحد متصل کرد. از آنجا که وضعیت (state) هر پراکسی به‌طور مستقل در خودش ذخیره می‌شود، هیچ تداخلی میان آن‌ها به وجود نمی‌آید.

قرارداد Beacon ساختار ساده‌ای دارد و فقط آدرس قرارداد پیاده سازی را برمی‌گرداند:

هر یک از پراکسی‌ها قبل از اجرای delegatecall، از Beacon سوال می‌کنند که آدرس فعلی قرارداد پیاده سازی چیست. با تغییر مقدار بازگشتی تابع implementation() در Beacon، می‌توان تمام پراکسی‌های متصل به آن را به‌صورت یکجا به‌روزرسانی کرد.

شیار ذخیره سازی مربوط به Beacon برابر است با:

و این مقدار از عبارت زیر مشتق شده است:
برای پراکسی هایی که از Beacon استفاده نمی‌کنند، می‌توان مقدار address(0) را در این شیار ذخیره کرد یا آن را به کل خالی گذاشت.

پیاده سازی در OpenZeppelin و Solady

در کتابخانه OpenZeppelin، هر دو الگوی Transparent Upgradeable Proxy و UUPS از استاندارد ERC 1967 استفاده می‌کنند تا محل دقیق ذخیره متغیرهایی که در این مقاله توضیح داده شد را مشخص کنند.

همچنین کتابخانه Solady که به‌دلیل بهینه بودن از نظر مصرف گس شناخته می‌شود، پیاده سازی سبک و کارآمدی از پراکسی UUPS ارائه داده که آن هم از ساختار ERC 1967 برای تعیین محل ذخیره متغیرها بهره می‌برد.

جمع بندی

استاندارد ERC 1967 روشی برای تعیین محل ذخیره سه متغیر کلیدی در قراردادهای پراکسی فراهم می‌کند: آدرس قرارداد پیاده سازی (implementation)، آدرس مدیر (admin) و آدرس قرارداد Beacon. این استاندارد باعث می‌شود که مرورگرهای بلاکچین بتوانند به سادگی تشخیص دهند آیا یک قرارداد از نوع پراکسی است یا نه. همچنین با اختصاص شیارهای خاص و ایمن، از تداخل متغیرهای ذخیره سازی بین قرارداد پراکسی و قرارداد پیاده سازی جلوگیری می‌کند. اگر به دنبال درک عمیق‌تری از این مفاهیم هستید، پیشنهاد می‌کنیم از آموزش برنامه نویسی در حوزه سالیدیتی و قراردادهای هوشمند استفاده کنید.

به این مطلب امتیاز دهید

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

دوره آموزش طراحی وب سایت مدرسه با PHP و MySql
  • انتشار: ۱۲ تیر ۱۴۰۴

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

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

مشاهده همه

نظرات

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