تابع Swap در Uniswap V2 طراحی هوشمندانه ای دارد. با این حال، بسیاری از توسعه دهندگان هنگام اولین مواجهه با آن، منطق این تابع را غیر شهودی می دانند. در این مقاله، نحوه عملکرد این تابع را گام به گام و کامل بررسی می کنیم.
در ادامه، این کد تابع را مشاهده می کنید:
این کد در نگاه اول حجیم به نظر می رسد. اما اگر آن را به بخش های کوچک تر تقسیم کنیم، درک آن بسیار ساده تر می شود.
آنالیز مرحله به مرحله تابع Swap در Uniswap V2
- تابع در خطوط ۱۷۰ و ۱۷۱ مقدار توکنی را که معامله گر در ورودی درخواست کرده است، مستقیما از قرارداد به آدرس مقصد منتقل می کند.
هیچ بخش از این تابع، انتقال توکن به داخل قرارداد را انجام نمی دهد. با بررسی دقیق کد، می توان دید که چنین انتقالی وجود ندارد. البته این موضوع به معنای آن نیست که می توانیم به راحتی تابع swap را اجرا کنیم و هر مقدار توکن که می خواهیم برداشت کنیم! - این طراحی به ما اجازه می دهد که از وام های سریع (flash loan) استفاده کنیم. البته در خط ۱۸۲ (فلش نارنجی)، عبارت
require
وجود دارد. این بخش ما را موظف می کند وام سریع را به همراه بهره آن بازپرداخت کنیم. - در ابتدای این تابع، توسعه دهنده یک کامنت قرار داده است. این کامنت توضیح می دهد که باید این تابع را از طریق یک قرارداد هوشمند دیگر که شامل بررسی های امنیتی است، فراخوانی کنیم. خود این تابع چنین بررسی هایی انجام نمی دهد (مشخص شده با خط زیر قرمز). در نتیجه لازم است بررسی کنیم که این چک های امنیتی شامل چه مواردی می شوند.
- متغیرهای
_reserve0
و_reserve1
(مشخص شده با خط زیر آبی) در خطوط ۱۶۱، ۱۷۶-۱۷۷ و ۱۸۲ خوانده می شوند. اما در این تابع هیچ تغییری در آن ها ایجاد نمی شود. - در خط ۱۸۲ (فلش نارنجی)، تابع بررسی نمی کند که دقیقاً
X × Y = K
برقرار است.
بلکه بررسی می کند که:balance1Adjusted × balance2Adjusted ≥ K
این تنها عبارتrequire
است که محاسبه خاصی انجام می دهد.
سایر عبارت هایrequire
بررسی می کنند که مقادیر صفر نباشند و یا توکن ها به آدرس خود قرارداد ارسال نشوند. - مقادیر
balance0
وbalance1
از موجودی واقعی قرارداد جفت (pair contract) خوانده می شوند. برای این کار از تابعbalanceOf
در استاندارد ERC20 استفاده می شود. - خط ۱۷۲ (در زیر کادر زرد) تنها زمانی اجرا می شود که مقدار data خالی نباشد. در غیر این صورت، کد این بخش را نادیده می گیرد.
جمع بندی منطق تابع swap
با در نظر گرفتن این نکات، می توانیم منطق این تابع را به خوبی درک کنیم. در ادامه، هر بخش از این تابع را ویژگی به ویژگی تحلیل می کنیم تا تصویر کاملی از عملکرد آن به دست آوریم.
وام گیری سریع در Uniswap V2
کاربران برای دریافت وام سریع (flash loan) نیازی ندارند که تابع swap را صرفا برای معامله توکن ها به کار ببرند. این تابع را می توان تنها برای دریافت وام سریع نیز استفاده کرد.
در فرآیند وام گیری سریع در Uniswap V2، قرارداد وام گیرنده مقدار توکن مورد نظر خود را بدون نیاز به وثیقه درخواست می کند (مرحله A). سپس این مقدار توکن به قرارداد وام گیرنده منتقل می شود (مرحله B).
همراه با فراخوانی تابع، کاربر یک داده (data) را به عنوان آرگومان ورودی ارسال می کند (مرحله C). تابعی که رابط (interface) IUniswapV2Callee را پیاده سازی می کند، این داده را پردازش می کند.
تابع uniswapV2Call مسئول بازپرداخت وام است. این تابع باید مبلغ وام دریافتی را همراه با کارمزد آن به قرارداد بازگرداند. در صورتی که بازپرداخت کامل انجام نشود، کل تراکنش باطل (revert) خواهد شد.
استفاده از تابع Swap در Uniswap V2 نیاز به قرارداد هوشمند دارد
اگر از وام سریع استفاده نشود، توکن های ورودی باید هنگام فراخوانی تابع swap به قرارداد ارسال شوند.
این موضوع کاملا روشن است که فقط یک قرارداد هوشمند می تواند با تابع swap تعامل کند. چون یک حساب معمولی (EOA) نمی تواند در یک تراکنش به صورت همزمان توکن های ERC20 ورودی را ارسال کند و تابع swap را هم فراخوانی کند، مگر با کمک یک قرارداد هوشمند دیگر.
محاسبه مقدار توکن های ورودی
نحوه ای که Uniswap V2 مقدار توکن های ورودی را “اندازه گیری” می کند، در خطوط ۱۷۶ و ۱۷۷ انجام می شود. تصویر زیر این بخش را در کادر زرد نشان می دهد.
در اینجا باید به یاد داشته باشیم که متغیرهای _reserve0
و _reserve1
داخل این تابع به روز نمی شوند. این مقادیر در واقع موجودی قرارداد را پیش از ارسال توکن های جدید (به عنوان بخشی از فرآیند swap) نمایش می دهند.
برای هر یک از دو توکن موجود در جفت (pair)، دو حالت ممکن است رخ دهد:
۱. مقدار یک توکن در استخر افزایش خالص داشته باشد.
۲. مقدار یک توکن در استخر کاهش خالص داشته باشد (یا بدون تغییر بماند).
کد با استفاده از منطق زیر مشخص می کند که کدام وضعیت رخ می دهد:
1 2 3 4 5 |
currentContractbalanceX > _reserveX - _amountXOut // alternatively currentContractBalanceX > previousContractBalanceX - _amountXOut |
اگر در استخر کاهش خالص رخ دهد، عملگر سه گانه (ternary operator) مقدار صفر را باز می گرداند. در غیر این صورت، مقدار افزایش توکن ها محاسبه می شود.
فرمول محاسبه به این صورت است:
1 |
amountXIn = balanceX - (_reserveX - amountXOut) |
_reserveX > amountXOut
برقرار است، چون در خط ۱۶۲ یک عبارت require
وجود دارد که این موضوع را تضمین می کند.
مثال های کاربردی
اکنون چند مثال را بررسی می کنیم:
- فرض کنید موجودی قبلی ما ۱۰ بوده است، مقدار
amountOut
برابر صفر است، و موجودی فعلی ۱۲ است. این یعنی کاربر ۲ توکن واریز کرده است، در نتیجهamountXIn
برابر با ۲ خواهد بود. - حال فرض کنید موجودی قبلی ۱۰ بوده، مقدار
amountOut
برابر ۷ است، و موجودی فعلی ۳ است. در این حالتamountXIn
برابر صفر می شود. - اگر موجودی قبلی ۱۰ بوده، مقدار
amountOut
برابر ۷ باشد، و موجودی فعلی ۲ باشد، باز همamountXIn
برابر صفر خواهد بود، نه منفی یک. هرچند استخر به طور خالص ۸ توکن از دست داده است، اما مقدارamountXIn
نمی تواند منفی باشد. - مثال دیگر: اگر موجودی قبلی ۱۰ بوده،
amountOut
برابر ۶ باشد، و موجودی فعلی ۱۸ باشد، در این صورت کاربر ۶ توکن “وام گرفته” و در عوض ۸ توکن بازپرداخت کرده است.
جمع بندی: مقادیر amount0In
و amount1In
در صورتی که توکن مورد نظر افزایش خالص داشته باشد، مقدار افزایش را نشان می دهند. در غیر این صورت، یعنی در صورت کاهش خالص، مقدار آن ها صفر خواهد بود.
متعادل کردن رابطه XY = K
اکنون که متوجه شدیم کاربر چه مقدار توکن به قرارداد ارسال کرده است، می توانیم ببینیم که چگونه باید شرط XY = K را برقرار کنیم.
کد دوباره به شکل زیر است:
Uniswap V2 به ازای هر swap کارمزدی ثابت معادل ۰.۳ درصد دریافت می کند. به همین دلیل، برنامه نویس در کد از اعدادی مثل ۱۰۰۰ و ۳ استفاده می کند. اما برای ساده سازی، فرض می کنیم Uniswap V2 هیچ کارمزدی دریافت نمی کند.
در این حالت می توانیم بخش .sub(amountXIn.mul(3))
را حذف کنیم و دیگر نیازی به ضرب در ۱۰۰۰ در خطوط ۱۸۰ تا ۱۸۱ یا ضرب در ۱۰۰۰ به توان ۲ در خط ۱۸۲ نخواهد بود.
کد جدید به این صورت خواهد بود:
1 |
require(balance0 * balance1 >= reserve0 * reserve1, "K"); |
K واقعا ثابت نیست
اینکه بگوییم “K ثابت می ماند”، کمی گمراه کننده است، حتی اگر بسیاری از افراد فرمول AMM را “فرمول حاصلضرب ثابت” بنامند.
به این شکل به آن فکر کنید: اگر کسی به استخر توکن اهدا کند و مقدار K را افزایش دهد، ما تمایلی نداریم جلوی او را بگیریم. چون در این صورت، ما به عنوان تأمین کنندگان نقدینگی، ثروتمندتر خواهیم شد، درست است؟
Uniswap V2 مانع از این نمی شود که کاربر “بیش از حد پرداخت کند”، یعنی مقدار زیادی توکن در طول فرآیند swap به قرارداد وارد کند. (این موضوع به یکی از بررسی های امنیتی مربوط می شود که در ادامه به آن خواهیم پرداخت.)
ما تنها زمانی ناراحت می شویم که استخر کاهش خالص داشته باشد. عبارت require همین وضعیت را بررسی می کند. اگر مقدار K افزایش یابد، یعنی حجم استخر بزرگ تر شده است و ما به عنوان تأمین کنندگان نقدینگی، دقیقا همین نتیجه را می خواهیم.
محاسبه کارمزدها
اما ما فقط نمی خواهیم مقدار K بزرگ تر شود، بلکه می خواهیم این افزایش حداقل به اندازه ای باشد که کارمزد ۰.۳ درصد را هم لحاظ کند.
به طور دقیق، قرارداد این کارمزد ۰.۳ درصد را بر مقدار معامله شده اعمال می کند، نه بر کل اندازه استخر. همچنین قرارداد این کارمزد را فقط بر توکن های ورودی محاسبه می کند و برای توکن هایی که از استخر خارج می شوند، کارمزدی در نظر نمی گیرد. درک این منطق برای توسعه دهندگان و افرادی که در زمینه برنامه نویسی قراردادهای هوشمند فعالیت می کنند، اهمیت بالایی دارد. چرا که به آنها کمک می کند تعاملات بهینهتری با پروتکلهای دیفای مانند Uniswap V2 طراحی کنند.
چند مثال برای درک بهتر:
- فرض کنید ۱۰۰۰ واحد از توکن ۰ به استخر وارد می کنیم و در عوض ۱۰۰۰ واحد از توکن ۱ خارج می کنیم. در این حالت باید ۳ واحد کارمزد روی توکن ۰ بپردازیم و هیچ کارمزدی برای توکن ۱ وجود ندارد.
- یا فرض کنید ۱۰۰۰ واحد از توکن ۰ را وام می گیریم و از توکن ۱ هیچ چیزی قرض نمی گیریم. برای بازپرداخت باید همان ۱۰۰۰ واحد توکن ۰ را به استخر برگردانیم و علاوه بر آن، ۳ واحد کارمزد (۰.۳ درصد) روی همین توکن پرداخت کنیم.
تفاوت بین وام سریع و Swap
همانطور که می بینید، اگر یک توکن را از طریق وام سریع دریافت کنیم، کارمزدی که پرداخت می کنیم دقیقا برابر با زمانی است که همان مقدار را به صورت swap معامله می کنیم.
در واقع شما روی توکن های ورودی کارمزد می پردازید، نه روی توکن های خروجی. اما اگر توکنی وارد نکنید، اصلا نمی توانید چیزی وام بگیرید یا swap انجام دهید.
یادآوری: متغیرهای reserve0
و reserve1
نشان دهنده موجودی های قبلی هستند، در حالی که balance0
و balance1
نشان دهنده موجودی های به روز شده می باشند.
با این توضیحات، کدی که در ادامه می آید، ساختاری کاملا روشن و قابل فهم خواهد داشت.
ضرب کردن در اعداد ۱۰۰۰ و ۳ تنها به این دلیل انجام می شود که در Solidity امکان استفاده از اعداد اعشاری وجود ندارد. به همین خاطر برای انجام ضرب کسری (fractional multiplication)، از این مقیاس استفاده می کنیم که در انتها مقدار صحیح محاسبه شود.
در واقع، این کد سعی می کند فرمول زیر را پیاده سازی کند:
موجودی جدید باید به اندازه ۰.۳ درصد از مقدار ورودی افزایش یابد. در کد، این فرمول با ضرب هر جمله در عدد ۱۰۰۰ مقیاس بندی شده، چون در Solidity اعداد اعشاری نداریم. اما فرمول ریاضی نشان می دهد که دقیقا هدف کد چیست.
به روز رسانی مقادیر Reserves
اکنون که تراکنش swap تکمیل شده است، قرارداد موجودی فعلی را جایگزین موجودی قبلی می کند. این کار در انتهای تابع swap()
و با فراخوانی تابع _update()
انجام می شود.
تابع _update()
در این تابع منطق زیادی برای به روز رسانی اوراکل TWAP (میانگین قیمت وزنی زمان دار) وجود دارد. اما فعلا فقط خطوط ۸۲ و ۸۳ برای ما اهمیت دارند. در این خطوط، متغیرهای ذخیره شده reserve0
و reserve1
به روز رسانی می شوند تا موجودی های جدید را بازتاب دهند.
آرگومان های _reserve0
و _reserve1
برای به روز رسانی اوراکل استفاده می شوند، اما این مقادیر در ذخیره قرارداد (storage) نوشته نمی شوند.
بررسی های ایمنی (Safety Checks)
در اینجا دو مشکل احتمالی وجود دارد:
۱. مقدار amountIn
به صورت بهینه تضمین نشده است. در نتیجه ممکن است کاربر بیش از مقدار لازم برای swap پرداخت کند.
۲. مقدار amountOut
هیچ انعطافی ندارد، چون به عنوان پارامتر ورودی مشخص شده است. اگر در زمان اجرا مقدار amountIn
نسبت به amountOut
کافی نباشد، تراکنش باطل (revert) می شود و گس مصرف شده از بین می رود.
این شرایط ممکن است در صورت وقوع frontrunning (یعنی زمانی که یک نفر عمدا یا به صورت غیر عمدی تراکنش را جلوتر از شما اجرا کند) اتفاق بیفتد. این کار می تواند نسبت دارایی ها در استخر را به شکلی نامطلوب تغییر دهد.
راستی! برای دریافت مطالب جدید در کانال تلگرام یا پیج اینستاگرام سورس باران عضو شوید.
- انتشار: ۱۸ خرداد ۱۴۰۴
دسته بندی موضوعات
- آموزش ارز دیجیتال
- آموزش برنامه نویسی
- آموزش متنی برنامه نویسی
- اطلاعیه و سایر مطالب
- پروژه برنامه نویسی
- دوره های تخصصی برنامه نویسی
- رپورتاژ
- فیلم های آموزشی
- ++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
- اچ تی ام ال
- بانک اطلاعاتی
- برنامه نویسی سوکت
- برنامه نویسی موبایل
- پاسکال
- پایان نامه
- پایتون
- جاوا
- جاوا اسکریپت
- جی کوئری
- داده کاوی
- دلفی
- رباتیک
- سئو
- سایر کتاب ها
- سخت افزار
- سی اس اس
- سی پلاس پلاس
- سی شارپ
- طراحی الگوریتم
- فتوشاپ
- مقاله
- مهندسی نرم افزار
- هک و امنیت
- هوش مصنوعی
- ویژوال بیسیک
- نرم افزار و ابزار برنامه نویسی
- وردپرس