بررسی دقیق تابع Swap در Uniswap V2

تابع Swap در Uniswap V2 طراحی هوشمندانه ای دارد. با این حال، بسیاری از توسعه دهندگان هنگام اولین مواجهه با آن، منطق این تابع را غیر شهودی می دانند. در این مقاله، نحوه عملکرد این تابع را گام به گام و کامل بررسی می کنیم.

در ادامه، این کد تابع را مشاهده می کنید:

تابع 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

در فرآیند وام گیری سریع در Uniswap V2، قرارداد وام گیرنده مقدار توکن مورد نظر خود را بدون نیاز به وثیقه درخواست می کند (مرحله A). سپس این مقدار توکن به قرارداد وام گیرنده منتقل می شود (مرحله B).

همراه با فراخوانی تابع، کاربر یک داده (data) را به عنوان آرگومان ورودی ارسال می کند (مرحله C). تابعی که رابط (interface) IUniswapV2Callee را پیاده سازی می کند، این داده را پردازش می کند.

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

استفاده از تابع Swap در Uniswap V2 نیاز به قرارداد هوشمند دارد

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

این موضوع کاملا روشن است که فقط یک قرارداد هوشمند می تواند با تابع swap تعامل کند. چون یک حساب معمولی (EOA) نمی تواند در یک تراکنش به صورت همزمان توکن های ERC20 ورودی را ارسال کند و تابع swap را هم فراخوانی کند، مگر با کمک یک قرارداد هوشمند دیگر.

محاسبه مقدار توکن های ورودی

نحوه ای که Uniswap V2 مقدار توکن های ورودی را “اندازه گیری” می کند، در خطوط ۱۷۶ و ۱۷۷ انجام می شود. تصویر زیر این بخش را در کادر زرد نشان می دهد.

ذخایر و مانده‌های اندازه‌گیری سوآپ

در اینجا باید به یاد داشته باشیم که متغیرهای _reserve0 و _reserve1 داخل این تابع به روز نمی شوند. این مقادیر در واقع موجودی قرارداد را پیش از ارسال توکن های جدید (به عنوان بخشی از فرآیند swap) نمایش می دهند.

برای هر یک از دو توکن موجود در جفت (pair)، دو حالت ممکن است رخ دهد:

۱. مقدار یک توکن در استخر افزایش خالص داشته باشد.
۲. مقدار یک توکن در استخر کاهش خالص داشته باشد (یا بدون تغییر بماند).

کد با استفاده از منطق زیر مشخص می کند که کدام وضعیت رخ می دهد:

اگر در استخر کاهش خالص رخ دهد، عملگر سه گانه (ternary operator) مقدار صفر را باز می گرداند. در غیر این صورت، مقدار افزایش توکن ها محاسبه می شود.

فرمول محاسبه به این صورت است:

همیشه شرط _reserveX > amountXOut برقرار است، چون در خط ۱۶۲ یک عبارت require وجود دارد که این موضوع را تضمین می کند.

تصویری از عبارت require

مثال های کاربردی

اکنون چند مثال را بررسی می کنیم:

  • فرض کنید موجودی قبلی ما ۱۰ بوده است، مقدار amountOut برابر صفر است، و موجودی فعلی ۱۲ است. این یعنی کاربر ۲ توکن واریز کرده است، در نتیجه amountXIn برابر با ۲ خواهد بود.
  • حال فرض کنید موجودی قبلی ۱۰ بوده، مقدار amountOut برابر ۷ است، و موجودی فعلی ۳ است. در این حالت amountXIn برابر صفر می شود.
  • اگر موجودی قبلی ۱۰ بوده، مقدار amountOut برابر ۷ باشد، و موجودی فعلی ۲ باشد، باز هم amountXIn برابر صفر خواهد بود، نه منفی یک. هرچند استخر به طور خالص ۸ توکن از دست داده است، اما مقدار amountXIn نمی تواند منفی باشد.
  • مثال دیگر: اگر موجودی قبلی ۱۰ بوده، amountOut برابر ۶ باشد، و موجودی فعلی ۱۸ باشد، در این صورت کاربر ۶ توکن “وام گرفته” و در عوض ۸ توکن بازپرداخت کرده است.

جمع بندی: مقادیر amount0In و amount1In در صورتی که توکن مورد نظر افزایش خالص داشته باشد، مقدار افزایش را نشان می دهند. در غیر این صورت، یعنی در صورت کاهش خالص، مقدار آن ها صفر خواهد بود.

متعادل کردن رابطه XY = K

اکنون که متوجه شدیم کاربر چه مقدار توکن به قرارداد ارسال کرده است، می توانیم ببینیم که چگونه باید شرط XY = K را برقرار کنیم.

کد دوباره به شکل زیر است:

Uniswap V2 به ازای هر swap کارمزدی ثابت معادل ۰.۳ درصد دریافت می کند. به همین دلیل، برنامه نویس در کد از اعدادی مثل ۱۰۰۰ و ۳ استفاده می کند. اما برای ساده سازی، فرض می کنیم Uniswap V2 هیچ کارمزدی دریافت نمی کند.

در این حالت می توانیم بخش .sub(amountXIn.mul(3)) را حذف کنیم و دیگر نیازی به ضرب در ۱۰۰۰ در خطوط ۱۸۰ تا ۱۸۱ یا ضرب در ۱۰۰۰ به توان ۲ در خط ۱۸۲ نخواهد بود.

کد جدید به این صورت خواهد بود:

این عبارت بیان می کند که:

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 reserves

تابع _update()

تابع update reserves در _update

در این تابع منطق زیادی برای به روز رسانی اوراکل TWAP (میانگین قیمت وزنی زمان دار) وجود دارد. اما فعلا فقط خطوط ۸۲ و ۸۳ برای ما اهمیت دارند. در این خطوط، متغیرهای ذخیره شده reserve0 و reserve1 به روز رسانی می شوند تا موجودی های جدید را بازتاب دهند.
آرگومان های _reserve0 و _reserve1 برای به روز رسانی اوراکل استفاده می شوند، اما این مقادیر در ذخیره قرارداد (storage) نوشته نمی شوند.

بررسی های ایمنی (Safety Checks)

در اینجا دو مشکل احتمالی وجود دارد:

۱. مقدار amountIn به صورت بهینه تضمین نشده است. در نتیجه ممکن است کاربر بیش از مقدار لازم برای swap پرداخت کند.

۲. مقدار amountOut هیچ انعطافی ندارد، چون به عنوان پارامتر ورودی مشخص شده است. اگر در زمان اجرا مقدار amountIn نسبت به amountOut کافی نباشد، تراکنش باطل (revert) می شود و گس مصرف شده از بین می رود.

این شرایط ممکن است در صورت وقوع frontrunning (یعنی زمانی که یک نفر عمدا یا به صورت غیر عمدی تراکنش را جلوتر از شما اجرا کند) اتفاق بیفتد. این کار می تواند نسبت دارایی ها در استخر را به شکلی نامطلوب تغییر دهد.

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

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

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

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

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

مشاهده همه

نظرات

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