در این مقاله با مفهوم TWAP در Uniswap v2 و نحوه عملکرد آن در قالب اوراکل قیمت آشنا میشویم. TWAP که مخفف Time Weighted Average Price است، یکی از روشهای متداول برای محاسبه میانگین قیمت دارایی ها در بازههای زمانی مشخص محسوب میشود و نقش مهمی در مقابله با دستکاری قیمت در قراردادهای هوشمند دارد.
منظور از “قیمت” در Uniswap چیست؟
فرض کنید در یک استخر، ۱ اتر (Ether) و ۲۰۰۰ واحد USDC قرار دارد. این ترکیب نشان میدهد که قیمت هر اتر ۲۰۰۰ USDC است. به بیان ساده، نسبت ۲۰۰۰ USDC به ۱ اتر برابر با قیمت اتر است. (در اینجا اعشار را در نظر نمیگیریم.)
بهطور کلی، وقتی میخواهیم قیمت یک دارایی را نسبت به دارایی دیگر محاسبه کنیم، آن را بهصورت یک نسبت مینویسیم. در این نسبت، دارایی که برای ما اهمیت دارد در مخرج قرار میگیرد.
مثلاً در همین مثال، داریم: “برای بهدست آوردن یک واحد از Foo، چند واحد Bar لازم است؟” (هزینه کارمزدها را فعلاً نادیده میگیریم.)
قیمت یک نسبت است
از آنجا که قیمت به صورت یک نسبت تعریف میشود، برای ذخیرهسازی آن نیاز به نوع دادهای داریم که قابلیت نمایش اعداد اعشاری را داشته باشد. اما در سالیدیتی، بهطور پیشفرض چنین نوع دادهای وجود ندارد.
برای مثال، فرض کنید قیمت اتریوم ۲۰۰۰ باشد و معادل آن در USDC چیزی در حدود ۰.۰۰۰۵ در نظر گرفته شود (در اینجا اعشار دقیق هر دو دارایی نادیده گرفته شده است).
یونی سواپ از قالب عدد اعشاری ثابت استفاده میکند که در آن، ۱۱۲ بیت برای بخش صحیح و ۱۱۲ بیت برای بخش اعشاری اختصاص یافته است. این ساختار در مجموع ۲۲۴ بیت فضا اشغال میکند. اگر همراه با یک عدد ۳۲ بیتی ذخیره شود، فقط یک اسلات حافظه را در قرارداد هوشمند مصرف خواهد کرد.
تعریف اوراکل
در علوم کامپیوتر و برنامه نویسی، اوراکل به منبعی گفته میشود که اطلاعات قطعی و قابل اعتماد ارائه میدهد. اوراکل قیمت، منبعی برای ارائه قیمت داراییها است. در یونی سواپ، وقتی دو دارایی در یک استخر وجود دارند، میتوان از نسبت آنها یک قیمت ضمنی به دست آورد. سایر قراردادهای هوشمند قادرند این قیمت را به عنوان اوراکل استفاده کنند.
هدف اصلی از وجود اوراکل در یونی سواپ، فراهم کردن دسترسی آسان برای قراردادهای دیگر است. این قراردادها میتوانند بهسادگی با یونی سواپ تعامل داشته باشند و قیمتها را مستقیماً به دست آورند. در مقابل، دریافت قیمت از یک صرافی خارج از زنجیره، بهمراتب سختتر و پرهزینهتر است.
با این حال، تکیه صرف بر نسبت موجودی داراییها برای تعیین قیمت، روش قابل اعتمادی نیست. این کار میتواند در برابر حملاتی مانند دستکاری قیمت آسیبپذیر باشد و تصویری نادرست از شرایط واقعی بازار ارائه دهد.
دلیل استفاده از TWAP در Uniswap v2
اگر قیمت داراییها را صرفاً بر اساس یک لحظه از وضعیت استخر اندازهگیری کنیم، فرصت حمله برای وامهای آنی (Flash Loan) فراهم میشود. در چنین حملهای، فرد مهاجم با استفاده از یک وام آنی، معاملهای بزرگ انجام میدهد تا قیمت را بهطور موقت بهشدت تغییر دهد. سپس از این قیمت دستکاریشده برای فریب یک قرارداد هوشمند دیگر استفاده میکند.
اوراکل یونی سواپ نسخه ۲ برای مقابله با این مشکل از دو راهکار استفاده میکند:
-
امکانی را فراهم کرده است تا مصرفکنندههای قیمت (که معمولاً قراردادهای هوشمند هستند) بتوانند میانگین قیمت یک بازه زمانی گذشته را محاسبه کنند. این بازه زمانی توسط کاربر تعیین میشود. در نتیجه، مهاجم مجبور میشود برای چند بلاک متوالی قیمت را دستکاری کند، که این کار بسیار پرهزینهتر از یک حمله لحظهای با وام آنی است.
-
موجودی فعلی استخر در محاسبه قیمت اوراکل در نظر گرفته نمیشود.
البته این به آن معنا نیست که اوراکلهایی که از میانگین متحرک استفاده میکنند، بهطور کامل در برابر دستکاری قیمت ایمن هستند. اگر یک دارایی نقدشوندگی کافی نداشته باشد یا بازه زمانی میانگینگیری کوتاه باشد، مهاجم توانمند میتواند برای مدت کافی قیمت را بالا یا پایین نگه دارد و میانگین را به نفع خود تغییر دهد.
TWAP در Uniswap v2 چگونه کار میکند؟
TWAP یا میانگین قیمت وزنی بر اساس زمان مشابه میانگین متحرک ساده است، با این تفاوت که در آن، قیمتهایی که برای مدت طولانیتری ثابت ماندهاند، وزن بیشتری پیدا میکنند. در واقع، در TWAP قیمتها بر اساس مدت زمانی که در یک سطح باقی ماندهاند وزنگذاری میشوند.
برای مثال:
-
در ۲۴ ساعت گذشته، قیمت یک دارایی برای ۱۲ ساعت اول ۱۰ دلار بوده و برای ۱۲ ساعت دوم ۱۱ دلار. در این حالت، میانگین قیمت با TWAP برابر است: ۱۰.۵ دلار.
-
در ۲۴ ساعت گذشته، قیمت یک دارایی برای ۲۳ ساعت اول ۱۰ دلار بوده و فقط در یک ساعت پایانی به ۱۱ دلار رسیده. در این شرایط، میانگین مورد انتظار باید به ۱۰ نزدیکتر باشد، اما همچنان بین ۱۰ و ۱۱ قرار میگیرد. بهطور دقیق:
(۱۰ × ۲۳ + ۱۱ × ۱) ÷ ۲۴ = ۱۰.۰۴۱۷ دلار -
در ۲۴ ساعت گذشته، قیمت یک دارایی برای ساعت اول ۱۰ دلار بوده و در ۲۳ ساعت باقیمانده ۱۱ دلار. در این حالت، انتظار داریم TWAP به ۱۱ نزدیکتر باشد. بهطور دقیق:
(۱۰ × ۱ + ۱۱ × ۲۳) ÷ ۲۴ = ۱۰.۹۵۸۳ دلار
در حالت کلی، فرمول TWAP در Uniswap v2 به شکل زیر است:
در این فرمول، T نشاندهنده مدت زمان است، نه یک زمان خاص (Timestamp). یعنی مشخص میکند که یک قیمت خاص چه مدت زمانی در آن سطح باقی مانده است.
Uniswap V2 مقدار بازه زمانی یا مخرج را ذخیره نمیکند
در مثال بالا فقط قیمتهای ۲۴ ساعت گذشته را بررسی کردیم. اما اگر بخواهیم قیمتها را در بازهای مثل یک ساعت، یک هفته یا هر بازه زمانی دیگری بررسی کنیم چه؟ واضح است که یونی سواپ نمیتواند تمام بازههای زمانی مورد نظر کاربران را ذخیره کند. علاوه بر این، راه مناسبی هم برای ثبت مداوم اسنپشات از قیمت وجود ندارد، چون انجام این کار نیاز به پرداخت گس دارد.
راهحل چیست؟ یونی سواپ فقط صورت کسر (numerator) را ذخیره میکند — هر بار که نسبت نقدینگی تغییر میکند (یعنی وقتی دستوراتی مثل mint، burn، swap یا sync اجرا میشوند)، سیستم قیمت جدید و مدت زمان پایداری قیمت قبلی را ثبت میکند.
متغیرهای price0CumulativeLast
و price1CumulativeLast
به صورت عمومی (public) تعریف شدهاند. بنابراین، هر کسی که به این مقادیر علاقه دارد، باید خودش در زمان دلخواه از آنها اسنپشات بگیرد.
اما نکته مهمی وجود دارد که باید همیشه به خاطر داشته باشید: مقادیر price0CumulativeLast
و price1CumulativeLast
فقط در خطوط ۷۹ و ۸۰ کد (دایره نارنجی) بهروزرسانی میشوند. این مقادیر فقط افزایش پیدا میکنند تا زمانی که سرریز (overflow) شوند. هیچ مکانیزمی وجود ندارد که باعث کاهش آنها شود. با هر بار اجرای تابع _update
، این مقادیر افزایش مییابند.
در نتیجه، این مقادیر از زمان راهاندازی استخر بهصورت تجمعی (accumulated) ذخیره شدهاند و ممکن است این بازه زمانی بسیار طولانی باشد.
محدود کردن بازه زمانی نگاه به گذشته
بدیهی است که در حالت معمول، میانگین قیمت از زمان ایجاد استخر برای ما اهمیتی ندارد. ما فقط میخواهیم به قیمتها در یک بازه مشخص نگاه کنیم؛ مثلاً یک ساعت گذشته، یا یک روز گذشته.
در اینجا، فرمول TWAP دوباره آورده شده است:
اگر فقط به قیمتها از زمان T4 به بعد علاقه داشته باشیم، در این صورت باید محاسبه زیر را انجام دهیم:
چگونه این کار را در کد انجام دهیم؟ از آنجا که price0CumulativeLast
بهصورت مداوم به ثبت مقادیر ادامه میدهد، باید راهی برای محاسبه صحیح بازه مورد نظر پیدا کنیم.
ما به روشی نیاز داریم تا فقط بخشهایی را که برای ما اهمیت دارند جدا کنیم. به مثال زیر توجه کنید:
اگر در پایان زمان T3 از قیمت اسنپ شات بگیریم، مقدار حاصل برابر با UpToTime3 خواهد بود. و اگر صبر کنیم تا زمان T6 به پایان برسد، سپس عبارت price0CumulativeLast - UpToTime3
را محاسبه کنیم، در این صورت فقط قیمتهای تجمعی مربوط به بازه اخیر را به دست میآوریم.
اگر این مقدار را بر مدت زمان بازه اخیر (T4+T5+T6) تقسیم کنیم، قیمت میانگین وزنی بر اساس زمان (TWAP) برای همین بازه زمانی به دست میآید.
بهصورت گرافیکی، این دقیقاً همان فرآیندی است که با استفاده از تجمعکننده قیمت (Price Accumulator) انجام میدهیم.
محاسبه TWAP یک ساعته در سالیدیتی
اگر بخواهیم TWAP یک ساعته را محاسبه کنیم، باید از قبل بدانیم که به یک اسنپشات از تجمعکننده قیمت (Accumulator) در یک ساعت بعد نیاز خواهیم داشت. برای این کار، باید متغیر عمومی price0CumulativeLast
و تابع عمومی getReserves()
را فراخوانی کنیم تا زمان آخرین بهروزرسانی را بهدست آوریم و از آنها اسنپشات بگیریم. (به تابع snapshot()
در پایین مراجعه کنید.)
پس از گذشت حداقل یک ساعت، میتوانیم تابع getOneHourPrice()
را صدا بزنیم. این تابع به مقدار جدید price0CumulativeLast
از یونی سواپ نسخه ۲ دسترسی پیدا میکند.
در این مدت، از زمانی که قیمت قبلی را اسنپشات گرفتیم، یونی سواپ بهطور مداوم مقدار تجمعی قیمت را بهروزرسانی کرده است.
کد زیر صرفاً برای درک بهتر، به سادهترین شکل ممکن نوشته شده است. استفاده از آن در محیط واقعی (Production) توصیه نمیشود.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
contract OneHourOracle { using UQ112x112 for uint224; // requires importing UQ112x112 IUniswapV2Pair uniswapV2pair; UQ112x112 snapshotPrice0Cumulative; uint32 lastSnapshotTime; function getTimeElapsed() internal view returns (uint32 t) { unchecked { t = uint32(block.timestamp % 2**32) - lastSnapshotTime; } } function snapshot() public returns (UQ112x112 twapPrice) { require(getTimeElapsed() >= 1 hours, "snapshot is not stale"); // we don't use the reserves, just need the last timestamp update ( , , lastSnapshotTime) = uniswapV2pair.getReserves(); snapshotPrice0Cumulative = uniswapV2pair.price0CumulativeLast; } function getOneHourPrice() public view returns (UQ112x112 price) { require(getTimeElapsed() >= 1 hours, "snapshot not old enough"); require(getTimeElapsed() < 3 hours, "price is too stale"); uint256 recentPriceCumul = uniswapV2pair.price0CumulativeLast; unchecked { twapPrice = (recentPriceCumul - snapshotPrice0Cumulative) / timeElapsed; } } } |
اگر آخرین اسنپ شات بیش از سه ساعت قبل ثبت شده باشد چه؟
خوانندگان بادقت ممکن است متوجه شوند که قرارداد بالا نمیتواند اسنپشات بگیرد اگر جفتارزی (pair) که با آن در تعامل است، در سه ساعت گذشته هیچ تعاملی نداشته باشد. در یونی سواپ نسخه ۲، تابع _update
فقط زمانی اجرا میشود که عملیاتهایی مانند mint
، burn
یا swap
انجام شوند. اگر هیچکدام از این تعاملات رخ ندهد، مقدار lastSnapshotTime
به زمان دوری اشاره خواهد کرد.
راهحل این است که اوراکل هنگام گرفتن اسنپشات، تابع sync
را نیز فراخوانی کند. این تابع بهصورت داخلی، باعث اجرای _update
میشود.
تصویری از تابع sync
در پایین آورده شده است.
چرا TWAP باید دو نسبت قیمتی را جداگانه دنبال کند؟
قیمت دارایی A نسبت به B بهسادگی برابر است با A تقسیم بر B. بالعکس، قیمت B نسبت به A نیز برابر است با B تقسیم بر A. برای مثال، اگر در یک استخر ۲۰۰۰ واحد USDC و ۱ واحد اتریوم داشته باشیم (بدون در نظر گرفتن اعشار)، قیمت هر واحد اتریوم برابر است با: ۲۰۰۰ USDC ÷ ۱ ETH.
قیمت USDC بر حسب ETH نیز فقط نسبت معکوس همین عدد است. یعنی صورت و مخرج جایگزین میشوند.
اما هنگام محاسبه تجمعی قیمتها نمیتوانیم صرفاً یکی از این نسبتها را معکوس کنیم تا به نسبت دیگر برسیم. برای درک بهتر این موضوع، به مثال زیر توجه کنید:
فرض کنید مقدار اولیه تجمعکننده قیمت برابر ۲ باشد و سپس مقدار ۳ به آن اضافه شود. در این حالت، نمیتوانیم صرفاً عدد نهایی را معکوس کنیم تا نسبت معکوس به دست آید.
با این حال، قیمتها همچنان «تا حدی متقارن» هستند. به همین دلیل، در نمایش آنها با محاسبات اعشاری ثابت (Fixed Point Arithmetic)، باید ظرفیت بخش صحیح و بخش اعشاری عدد برابر در نظر گرفته شود.
اگر ETH هزار برابر «ارزشمندتر» از USDC باشد، در مقابل، USDC نیز هزار برابر «کمارزشتر» از ETH خواهد بود. برای ذخیره دقیق این نسبتهای متقارن، عدد اعشاری ثابت باید اندازهای برابر در هر دو طرف ممیز داشته باشد.
به همین دلیل است که یونی سواپ از قالب عددی u112x112 استفاده کرده است.
PriceCumulativeLast همیشه افزایش پیدا میکند تا زمانی که سرریز شود، سپس دوباره از صفر ادامه میدهد
یونی سواپ نسخه ۲ قبل از انتشار سالیدیتی ۰.۸.۰ ساخته شده است؛ در نتیجه، عملیاتهای ریاضی بهصورت پیشفرض دچار سرریز (overflow) و کمریز (underflow) میشدند. در پیادهسازیهای مدرن اوراکل قیمت، باید از بلوک unchecked
استفاده شود تا این سرریزها مطابق انتظار انجام شوند.
و در نهایت، مقادیر priceAccumulator
و زمان بلاک (block timestamp) هر دو دچار سرریز خواهند شد. در چنین حالتی، مقدار ذخیرهشده قبلی (reserve قبلی) بیشتر از مقدار جدید به نظر میرسد. هنگام محاسبه تغییر قیمت توسط اوراکل، مقدار منفی به دست میآید. اما این مسئله مشکلی ایجاد نمیکند، چون قوانین حساب مدولار (modular arithmetic) این اختلاف را بهدرستی مدیریت میکنند.
برای سادهسازی، فرض کنیم از یک عدد صحیح بدون علامت (unsigned integer) استفاده میکنیم که در مقدار ۱۰۰ دچار سرریز میشود.
اگر در مقدار ۸۰ از priceAccumulator
اسنپشات بگیریم، و پس از چند تراکنش یا بلاک، مقدار آن به ۱۱۰ برسد، مقدار ذخیرهشده دچار سرریز شده و به ۱۰ تبدیل میشود. وقتی ۸۰ را از ۱۰ کم میکنیم، حاصل برابر با ۷۰- میشود. اما چون عدد بهصورت unsigned ذخیره شده، نتیجه محاسبه برابر خواهد بود با ۷۰- mod(100) که برابر با ۳۰ است. این همان مقداری است که در صورت نبود سرریز به دست میآمد (۱۱۰ – ۸۰ = ۳۰).
این رفتار نهفقط در مثال ما با مرز ۱۰۰، بلکه برای تمام مقادیر سرریز صدق میکند. سرریز شدن timestamp
یا priceAccumulator
مشکلی ایجاد نمیکند، چون محاسبات طبق قواعد حساب مدولار انجام میشود.
سرریز شدن timestamp
همین اتفاق در مورد سرریز شدن timestamp نیز رخ میدهد. چون برای نمایش آن از نوع داده uint32
استفاده میکنیم، امکان ایجاد عدد منفی وجود ندارد. برای سادهسازی، فرض کنیم سرریز در عدد ۱۰۰ اتفاق میافتد. اگر در زمان ۹۸ اسنپشات بگیریم و در زمان ۴ از اوراکل قیمت استفاده کنیم، یعنی ۶ ثانیه گذشته است.
۴ – ۹۸ mod ۱۰۰ = ۶، که دقیقاً همان مقداری است که انتظار داریم.
راستی! برای دریافت مطالب جدید در کانال تلگرام یا پیج اینستاگرام سورس باران عضو شوید.
- انتشار: ۲۱ خرداد ۱۴۰۴
دسته بندی موضوعات
- آموزش ارز دیجیتال
- آموزش برنامه نویسی
- آموزش متنی برنامه نویسی
- اطلاعیه و سایر مطالب
- پروژه برنامه نویسی
- دوره های تخصصی برنامه نویسی
- رپورتاژ
- فیلم های آموزشی
- ++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
- اچ تی ام ال
- بانک اطلاعاتی
- برنامه نویسی سوکت
- برنامه نویسی موبایل
- پاسکال
- پایان نامه
- پایتون
- جاوا
- جاوا اسکریپت
- جی کوئری
- داده کاوی
- دلفی
- رباتیک
- سئو
- سایر کتاب ها
- سخت افزار
- سی اس اس
- سی پلاس پلاس
- سی شارپ
- طراحی الگوریتم
- فتوشاپ
- مقاله
- مهندسی نرم افزار
- هک و امنیت
- هوش مصنوعی
- ویژوال بیسیک
- نرم افزار و ابزار برنامه نویسی
- وردپرس