Snapshot در ERC20 چیست

مکانیزم Snapshot در ERC20 برای جلوگیری از رأی دادن چندباره با یک دارایی طراحی شده است.

در سیستم‌هایی که وزن رأی بر اساس تعداد توکن های ERC20 تعیین می‌شود، یک مهاجم می‌تواند ابتدا با توکن های خود رأی بدهد. سپس همان توکن ها را به آدرسی دیگر منتقل کرده و دوباره رأی بدهد. اگر برای هر رأی از یک قرارداد هوشمند جدید استفاده کند، می‌تواند تمام این رأی‌ها را در یک تراکنش انجام دهد. شکل دیگر این حمله استفاده از وام آنی (flash loan) است. مهاجم در این روش تعداد زیادی توکن حاکمیتی قرض می‌گیرد، رأی می‌دهد و در همان تراکنش وام را بازمی‌گرداند.

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

مکانیزم Snapshot به توسعه دهنده ها کمک می‌کند تا جلوی انتقال توکن و استفاده دوباره از مزایای آن در همان تراکنش را بگیرند.

در ابتدا، پیاده سازی این سیستم غیرممکن به نظر می‌رسد. راهکار ساده این است که کل آدرس های موجود در balances را پیمایش کنیم و موجودی هرکدام را در مپ جدیدی کپی کنیم. اما از آنجایی که در استاندارد ERC20 امکان پیمایش مستقیم روی mapping وجود ندارد، توسعه‌دهنده باید از مپ شمارش‌پذیر (Enumerable Map) استفاده کند که آرایه‌ای از کلیدها را ذخیره می‌کند.

اما این عملیات از نوع O(n) است و مصرف گس بالایی دارد.

در دنیای برنامه‌نویسی، جمله‌ای معروف می‌گوید:

“با یک لایه از غیرمستقیم‌سازی می‌توان هر مشکلی را حل کرد.”

ERC20 Snapshot نیز دقیقاً با تکیه بر همین اصل این مشکل را برطرف می‌کند.

راهکار ساده اما ناکارآمد

برای درک بهتر، بیایید ساختار balances در قرارداد ERC20 را بررسی کنیم.
در اینجا یک راهکار اولیه و ساده وجود دارد که البته باگ دارد، اما ما را به مسیر درست هدایت می‌کند:

در این روش، متغیر snapshotNumber یک شمارنده است که از صفر شروع می‌شود و هر بار که یک اسنپ شات (snapshot) گرفته می‌شود، عدد آن یکی افزایش می‌یابد.

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

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

اما این راهکار یک ایراد جدی دارد!

هر بار که اسنپ شات جدیدی ایجاد می‌کنیم، موجودی همه کاربران را به صفر تغییر می‌دهیم!

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

راهکار OpenZeppelin

OpenZeppelin برای پیاده سازی Snapshot در ERC20 از ساختاری مؤثر و بهینه استفاده می‌کند. در این روش، هر موجودی به‌جای یک عدد ساده، در قالب یک ساختار (struct) ذخیره می‌شود:

در این پیاده سازی، برای هر کاربر ساختاری به نام Snapshots تعریف می‌شود که شامل دو آرایه است: یکی برای ids که شماره های اسنپ شات را نگه می‌دارد، و دیگری برای values که موجودی مربوط به همان اسنپ شات ها را ثبت می‌کند.

آرایه ids به‌صورت افزایشی و مرتب ذخیره می‌شود و نشان‌دهنده ترتیب زمانی اسنپ شات هاست. به‌عبارت دیگر، هر id زمانی فعال می‌شود که یک اسنپ شات گرفته می‌شود، و در همان لحظه، موجودی کاربر در آرایه values ثبت می‌شود.

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

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

گرفتن snapshot در ERC20

در اینجا تابع snapshot را می‌بینیم. این تابع تنها با افزایش شناسه فعلی اسنپ شات(snapshotId)، یک اسنپ شات جدید ایجاد می‌کند.

هنگامی که کاربر در یک اسنپ شات جدید توکن منتقل می‌کند، هوک _beforeTokenTransfer اجرا می‌شود که کد زیر را در خود دارد.

در این فرآیند، هم فرستنده و هم گیرنده با فراخوانی تابع _updateAccountSnapshot وضعیت اسنپ شات خود را به‌روزرسانی می‌کنند.

این تابع _updateAccountSnapshot است که در جریان تراکنش فراخوانی می‌شود.
این تابع به نوبه خود تابع _updateSnapshot را فراخوانی می‌کند. در ادامه، این تابع را تعریف کرده‌ایم:

از آن‌جایی که مقدار currentId به‌تازگی افزایش یافته است، شرط موجود در دستور if برقرار خواهد بود. در این حالت، مقدار موجودی فعلی به آرایه snapshots اضافه می‌شود. از آن‌جا که این عملیات در هوک _beforeTokenTransfer انجام می‌شود، موجودی که ثبت می‌شود، دقیقاً همان موجودی قبل از انجام تراکنش است.

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

اما اگر یک آدرس بین دو اسنپ شات هیچ تراکنشی انجام ندهد، سیستم نمی‌تواند شناسه‌های ذخیره شده برای آن حساب را به‌صورت پیوسته ثبت کند.

به همین دلیل، برای دسترسی به موجودی یک حساب در یک اسنپ شات خاص، نمی‌توانیم به سادگی از ساختاری مثل ids[snapshotId] استفاده کنیم. در عوض، باید با استفاده از جست‌وجوی دودویی (binary search) شناسه مورد نظر را پیدا کنیم. اگر آن شناسه در آرایه موجود نباشد، از مقدار ثبت‌شده در نزدیک‌ترین اسنپ شات قبلی استفاده می‌کنیم.

برای مثال، اگر بخواهیم موجودی یک کاربر را در اسنپ شات شماره ۵ بدانیم، اما او در اسنپ شات‌های ۳ و ۴ تراکنشی انجام نداده باشد، باید به مقدار ثبت‌شده در اسنپ شات شماره ۲ رجوع کنیم.

پیگیری عرضه کل به همان شیوه انجام می‌شود

ممکن است هنگام بررسی ساختار Snapshots این سوال پیش بیاید که چرا از نام‌هایی عمومی مانند ids و values استفاده شده است؟ چرا به جای آن، متغیرها به شکل دقیق‌تری مثل “balance” نام‌گذاری نشده‌اند؟

دلیل این انتخاب آن است که ERC20 Snapshot از همین ساختار Snapshots نه‌تنها برای ثبت موجودی کاربران، بلکه برای پیگیری عرضه کل (total supply) نیز استفاده می‌کند. بنابراین، نام متغیرها باید به‌اندازه‌ای کلی باشند که بتوانند هر دو کاربرد را پوشش دهند.

در سیستم ERC20، تنها عملیات ایجاد توکن (mint) و سوزاندن توکن (burn) باعث تغییر عرضه کل می‌شوند. به همین دلیل، زمانی که کد یکی از این توابع را اجرا می‌کند، ابتدا بررسی می‌کند آیا اسنپ شات جدیدی وجود دارد یا نه، و سپس مقدار مربوط به عرضه کل را به‌روزرسانی می‌کند. اگر اسنپ شات جدیدی وجود داشته باشد، مقدار قبلی در ساختار مربوط به عرضه کل ذخیره می‌شود.

نکته مهم اینکه مقادیر تاریخی مجوز انتقال (allowance) در اسنپ شات ذخیره نمی‌شوند.

هزینه گس اضافه شده

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

اضافه کردن شناسه جدید به آرایه ids و مقدار جدید به آرایه values باعث می‌شود دو عملیات ذخیره سازی اضافی (SSTORE) انجام شود. به همین دلیل، زمانی که یک اسنپ شات جدید ثبت می‌شود، اولین تراکنش ارسال یا دریافت توکن برای هر آدرس، گران‌تر از حد معمول خواهد بود.

با این حال، از تراکنش دوم به بعد — تا قبل از اسنپ شات بعدی — هزینه انتقال تقریباً برابر با یک انتقال معمولی در استاندارد ERC20 خواهد بود.

احتمال حمله و دستکاری

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

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

محاسبه رأی ها

محاسبه رأی دهی بسیار ساده است:
در یک اسنپ شات مشخص، موجودی یک آدرس تقسیم بر عرضه کل، قدرت رأی آن آدرس را مشخص می‌کند.

درک صحیح از مکانیزم Snapshot در ERC20 می‌تواند نقطه شروع مناسبی برای ورود به مباحث پیشرفته در آموزش برنامه نویسی باشد.

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

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

آموزش گام به گام برنامه نویسی اندروید با B4A (پروژه محور)
  • انتشار: ۲۱ تیر ۱۴۰۴

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

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

مشاهده همه

نظرات

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