مکانیزم Snapshot در ERC20 برای جلوگیری از رأی دادن چندباره با یک دارایی طراحی شده است.
در سیستمهایی که وزن رأی بر اساس تعداد توکن های ERC20 تعیین میشود، یک مهاجم میتواند ابتدا با توکن های خود رأی بدهد. سپس همان توکن ها را به آدرسی دیگر منتقل کرده و دوباره رأی بدهد. اگر برای هر رأی از یک قرارداد هوشمند جدید استفاده کند، میتواند تمام این رأیها را در یک تراکنش انجام دهد. شکل دیگر این حمله استفاده از وام آنی (flash loan) است. مهاجم در این روش تعداد زیادی توکن حاکمیتی قرض میگیرد، رأی میدهد و در همان تراکنش وام را بازمیگرداند.
مشکل مشابهی در توزیع ایردراپ ها هم دیده میشود. فرد ابتدا با توکن های خود یک ایردراپ دریافت میکند، سپس توکن ها را به آدرسی دیگر منتقل میکند و بار دیگر ایردراپ میگیرد.
مکانیزم Snapshot به توسعه دهنده ها کمک میکند تا جلوی انتقال توکن و استفاده دوباره از مزایای آن در همان تراکنش را بگیرند.
در ابتدا، پیاده سازی این سیستم غیرممکن به نظر میرسد. راهکار ساده این است که کل آدرس های موجود در balances
را پیمایش کنیم و موجودی هرکدام را در مپ جدیدی کپی کنیم. اما از آنجایی که در استاندارد ERC20 امکان پیمایش مستقیم روی mapping
وجود ندارد، توسعهدهنده باید از مپ شمارشپذیر (Enumerable Map) استفاده کند که آرایهای از کلیدها را ذخیره میکند.
اما این عملیات از نوع O(n)
است و مصرف گس بالایی دارد.
در دنیای برنامهنویسی، جملهای معروف میگوید:
“با یک لایه از غیرمستقیمسازی میتوان هر مشکلی را حل کرد.”
ERC20 Snapshot نیز دقیقاً با تکیه بر همین اصل این مشکل را برطرف میکند.
راهکار ساده اما ناکارآمد
برای درک بهتر، بیایید ساختار balances
در قرارداد ERC20 را بررسی کنیم.
در اینجا یک راهکار اولیه و ساده وجود دارد که البته باگ دارد، اما ما را به مسیر درست هدایت میکند:
1 |
balances[snapshotNumber][user] |
در این روش، متغیر snapshotNumber
یک شمارنده است که از صفر شروع میشود و هر بار که یک اسنپ شات (snapshot) گرفته میشود، عدد آن یکی افزایش مییابد.
اگر به مثال رأی گیری بازگردیم، در یک لحظه خاص از زمان، یک اسنپ شات ایجاد میکنیم. سپس به کاربران اجازه میدهیم فعالیت عادی خود را انجام دهند و در زمان مناسب، اسنپ شات بعدی را میگیریم. هنگام رأی گیری، به جای استفاده از وضعیت فعلی، از اسنپ شات قبلی استفاده میکنیم، چون ممکن است وضعیت فعلی هنوز با انتقال توکن تغییر کند.
در این ساختار، توسعه دهنده میتواند با وارد کردن شماره اسنپ شات مورد نظر و آدرس کاربر، موجودی او را بهدست بیاورد. از آنجایی که شماره اسنپ شات فعلی مشخص است، تابع balanceOf
تنها کافیست موجودی را از آخرین اسنپ شات بخواند.
اما این راهکار یک ایراد جدی دارد!
هر بار که اسنپ شات جدیدی ایجاد میکنیم، موجودی همه کاربران را به صفر تغییر میدهیم!
میتوان با کمی حسابداری این مشکل را حل کرد. مثلاً میتوان آخرین شماره اسنپ شاتی را که هر کاربر در آن تراکنش داشته، ذخیره کرد. اما همین موضوع باعث پیچیدگیهای زیاد میشود، چون توسعه دهنده باید همه حالت های خاص و گوشهای (corner cases) را مدیریت کند.
راهکار OpenZeppelin
OpenZeppelin برای پیاده سازی Snapshot در ERC20 از ساختاری مؤثر و بهینه استفاده میکند. در این روش، هر موجودی بهجای یک عدد ساده، در قالب یک ساختار (struct) ذخیره میشود:
1 2 3 4 5 6 7 8 9 10 11 12 |
struct Snapshots { uint256[] ids; uint256[] values; } mapping(address => Snapshots) private _accountBalanceSnapshots; function balanceOfAt(address account, uint256 snapshotId) public view virtual returns (uint256) { (bool snapshotted, uint256 value) = _valueAt(snapshotId, _accountBalanceSnapshots[account]); return snapshotted ? value : balanceOf(account); } |
در این پیاده سازی، برای هر کاربر ساختاری به نام Snapshots
تعریف میشود که شامل دو آرایه است: یکی برای ids
که شماره های اسنپ شات را نگه میدارد، و دیگری برای values
که موجودی مربوط به همان اسنپ شات ها را ثبت میکند.
آرایه ids
بهصورت افزایشی و مرتب ذخیره میشود و نشاندهنده ترتیب زمانی اسنپ شات هاست. بهعبارت دیگر، هر id
زمانی فعال میشود که یک اسنپ شات گرفته میشود، و در همان لحظه، موجودی کاربر در آرایه values
ثبت میشود.
تابع balanceOfAt
با استفاده از تابع کمکی _valueAt
بررسی میکند که آیا برای اسنپ شات خواستهشده، مقدار خاصی ذخیره شده یا خیر. اگر چنین مقداری وجود داشته باشد، همان را بازمیگرداند؛ در غیر این صورت، موجودی فعلی حساب را برمیگرداند.
ما این طراحی را بهگونهای انجام دادهایم که از ثبت مقادیر تکراری و محاسبات سنگین جلوگیری شود و در عین حال، بتوانیم موجودی هر کاربر را در هر اسنپ شات مشخص بهدقت بررسی کنیم.
گرفتن snapshot در ERC20
در اینجا تابع snapshot را میبینیم. این تابع تنها با افزایش شناسه فعلی اسنپ شات(snapshotId
)، یک اسنپ شات جدید ایجاد میکند.
1 2 3 4 5 6 7 |
function _snapshot() internal virtual returns (uint256) { _currentSnapshotId.increment(); uint256 currentId = _getCurrentSnapshotId(); emit Snapshot(currentId); return currentId; } |
هنگامی که کاربر در یک اسنپ شات جدید توکن منتقل میکند، هوک _beforeTokenTransfer
اجرا میشود که کد زیر را در خود دارد.
در این فرآیند، هم فرستنده و هم گیرنده با فراخوانی تابع _updateAccountSnapshot
وضعیت اسنپ شات خود را بهروزرسانی میکنند.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// Update balance and/or total supply snapshots before the values are modified. This is implemented // in the _beforeTokenTransfer hook, which is executed for _mint, _burn, and _transfer operations. function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual override { super._beforeTokenTransfer(from, to, amount); if (from == address(0)) { // mint _updateAccountSnapshot(to); _updateTotalSupplySnapshot(); } else if (to == address(0)) { // burn _updateAccountSnapshot(from); _updateTotalSupplySnapshot(); } else { // transfer _updateAccountSnapshot(from); _updateAccountSnapshot(to); } } |
_updateAccountSnapshot
است که در جریان تراکنش فراخوانی میشود.
1 2 3 |
function _updateAccountSnapshot(address account) private { _updateSnapshot(_accountBalanceSnapshots[account], balanceOf(account)); } |
_updateSnapshot
را فراخوانی میکند. در ادامه، این تابع را تعریف کردهایم:
1 2 3 4 5 6 7 |
function _updateSnapshot(Snapshots storage snapshots, uint256 currentValue) private { uint256 currentId = _getCurrentSnapshotId(); if (_lastSnapshotId(snapshots.ids) < currentId) { snapshots.ids.push(currentId); snapshots.values.push(currentValue); } } |
از آنجایی که مقدار 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 میتواند نقطه شروع مناسبی برای ورود به مباحث پیشرفته در آموزش برنامه نویسی باشد.
راستی! برای دریافت مطالب جدید در کانال تلگرام یا پیج اینستاگرام سورس باران عضو شوید.
- انتشار: ۲۱ تیر ۱۴۰۴
دسته بندی موضوعات
- آموزش ارز دیجیتال
- آموزش برنامه نویسی
- آموزش متنی برنامه نویسی
- اطلاعیه و سایر مطالب
- پروژه برنامه نویسی
- دوره های تخصصی برنامه نویسی
- رپورتاژ
- فیلم های آموزشی
- ++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
- اچ تی ام ال
- بانک اطلاعاتی
- برنامه نویسی سوکت
- برنامه نویسی موبایل
- پاسکال
- پایان نامه
- پایتون
- جاوا
- جاوا اسکریپت
- جی کوئری
- داده کاوی
- دلفی
- رباتیک
- سئو
- سایر کتاب ها
- سخت افزار
- سی اس اس
- سی پلاس پلاس
- سی شارپ
- طراحی الگوریتم
- فتوشاپ
- مقاله
- مهندسی نرم افزار
- هک و امنیت
- هوش مصنوعی
- ویژوال بیسیک
- نرم افزار و ابزار برنامه نویسی
- وردپرس