آموزش جامع روتر در Uniswap V2

قراردادهای روتر در Uniswap V2 یک رابط کاربری برای کاربران فراهم می‌کنند تا بتوانند از طریق آن با قراردادهای هوشمند تعامل داشته باشند. این قراردادها امکان انجام عملیات زیر را به صورت ایمن فراهم می‌کنند:

  • ایجاد (mint) و سوزاندن (burn) امن توکن های نقدینگی (LP tokens) برای افزودن و حذف نقدینگی

  • مبادله ایمن توکن های جفت شده

همچنین این قراردادها قابلیت‌های زیر را اضافه می‌کنند:

  • امکان مبادله اتر (Ether) از طریق ادغام با قرارداد WETH (نسخه رپ‌شده اتر با استاندارد ERC20)

  • بررسی‌های ایمنی مرتبط با لغزش قیمت (slippage) که در قرارداد اصلی وجود ندارند

  • پشتیبانی از توکن هایی که هنگام انتقال، کارمزد دریافت می‌کنند (fee on transfer tokens)

Router02 تمام قابلیت های Router01 را دارد و از توکن های دارای کارمزد انتقال نیز پشتیبانی می‌کند

وقتی پوشه contracts را در مخزن جانبی (periphery) یونی سواپ باز می‌کنیم، با سه قرارداد مواجه می‌شویم:

تصویر اسکرین‌شات مربوط به GitHub یونی سواپ v2 روتِر

Router02 همان عملکردهای Router01 را ارائه می‌دهد و علاوه بر آن، توابع جدیدی برای پشتیبانی از توکن های دارای کارمزد انتقال (fee on transfer tokens) در اختیار توسعه دهنده قرار می‌دهد. اگر رابط Router02 را بررسی کنیم، متوجه می‌شویم که این قرارداد از Router01 ارث بری می‌کند (کادر قرمز) و به همین دلیل تمام توابع آن را درون خود دارد.

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

ساختار ارث‌بری در روتر یونی سواپ V2

توابع swapExactTokensForTokens و swapTokensForExactTokens

بیایید با توابع مربوط به مبادله توکن ها در قرارداد Router شروع کنیم. در Uniswap V2، دو تابع اصلی برای انجام این کار وجود دارد (در تصویر با رنگ سبز مشخص شده‌اند):

تصویر مربوط به توابع swap در روتر یونی سواپ V2

تفاوت اصلی این دو تابع در نام آن‌ها به این شکل تعریف می‌شود:

  • در تابع swapExactTokensForTokens، واژه‌ “Exact” به توکن اول اشاره دارد. یعنی کاربر مقدار دقیقی از توکن ورودی را برای مبادله مشخص می‌کند.

  • در تابع swapTokensForExactTokens، واژه‌ “Exact” مربوط به توکن دوم است. یعنی کاربر مقدار دقیقی از توکن خروجی که می‌خواهد دریافت کند را مشخص می‌کند.

اگر کاربر تنها قصد مبادله بین دو توکن را داشته باشد، باید آرایه‌ path را به‌صورت [address(tokenIn), address(tokenOut)] به این توابع بدهد (در تصویر با رنگ آبی مشخص شده است). اما اگر قصد داشته باشد از چند استخر عبور کند (مثلاً در حالتی که استخر مستقیم بین دو توکن وجود ندارد)، مسیر را به‌صورت [address(tokenIn), address(intermediateToken), …, address(tokenOut)] تعریف می‌کند.

تابع swapExactTokensForTokens

در تابع swapExactTokensForTokens، کاربر مقدار دقیق توکن ورودی را مشخص می‌کند و همچنین حداقل مقداری از توکن خروجی را تعیین می‌نماید که حاضر است دریافت کند.

برای مثال، فرض کنید می‌خواهیم ۲۵ عدد از token0 را با ۵۰ عدد از token1 مبادله کنیم. اگر قیمت لحظه‌ای دقیقاً همین نسبت را نشان دهد، این معامله هیچ گونه حاشیه‌ امنی در برابر تغییرات قیمت نخواهد داشت. در نتیجه، اگر قبل از تأیید تراکنش قیمت کمی نوسان کند، عملیات بازگردانده می‌شود (revert می‌شود).

برای جلوگیری از این مشکل، معمولاً کاربران مقدار حداقل خروجی را کمی کمتر از مقدار مورد انتظار قرار می‌دهند. مثلاً اگر انتظار دارند ۵۰ عدد token1 دریافت کنند، مقدار حداقل خروجی را برابر با ۴۹.۵ قرار می‌دهند تا به صورت ضمنی تحمل نوسان قیمتی ۱ درصدی را در تراکنش لحاظ کنند. اگر با نحوه عملکرد این توابع در قراردادهای هوشمند آشنا نیستید، پیشنهاد می‌کنیم ابتدا با مفاهیم پایه در آموزش برنامه نویسی آشنا شوید.

تابع swapTokensForExactTokens

در این حالت، کاربر مشخص می‌کند که دقیقاً می‌خواهد ۵۰ عدد از token1 دریافت کند، اما حاضر است حداکثر تا ۲۵.۵ عدد از token0 را برای به‌دست آوردن آن خرج کند.

کدام تابع swap را انتخاب کنیم؟

اغلب کاربران معمولی که با حساب شخصی (EOA) کار می‌کنند، ترجیح می‌دهند از تابعی استفاده کنند که ورودی دقیق را دریافت می‌کند (swapExactTokensForTokens). دلیل این انتخاب، نیاز به مرحله‌ تأیید (approval) است؛ چون اگر تراکنش به مقدار بیشتری از توکن نیاز داشته باشد ولی کاربر فقط مقدار محدودی را تأیید کرده باشد، عملیات با خطا مواجه می‌شود. در مقابل، زمانی که کاربر مقدار ورودی را دقیق مشخص می‌کند، می‌تواند همان مقدار را به طور دقیق تأیید کند و احتمال خطا کاهش می‌یابد.

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

نحوه عملکرد swap در یونی سواپ

وقتی کاربر از تابع swapExactTokensForTokens استفاده می‌کند، تابع ابتدا مقدار خروجی مورد انتظار را پیش‌بینی می‌کند؛ این پیش‌بینی می‌تواند برای یک جفت توکن یا زنجیره‌ای از مبادلات انجام شود. اگر مقدار خروجی نهایی کمتر از میزانی باشد که کاربر تعیین کرده، تابع تراکنش را لغو می‌کند (revert).

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

پس از انجام این بررسی‌ها، هر دو تابع، توکن های کاربر را به قرارداد جفت (Pair) منتقل می‌کنند. این مرحله ضروری است، چون در Uniswap V2، باید توکن ها قبل از فراخوانی تابع swap() به قرارداد جفت ارسال شده باشند.

در نهایت، هر دو تابع، تابع داخلی _swap() را اجرا می‌کنند که در ادامه مورد بررسی قرار می‌گیرد.

مبادله با مقدار ورودی دقیق در روتر Uniswap V2

تابع داخلی _swap()

در پشت‌صحنه، تمام توابع عمومی که نام آن‌ها شامل swap() است، در نهایت تابع داخلی _swap() را فراخوانی می‌کنند. این تابع مسئول انجام واقعی مبادله بین توکن ها است.

به یاد داشته باشید که در امضای تابع swap() مربوط به قرارداد جفت (Pair) در Uniswap V2، مقادیر خروجی (amountOut) برای هر دو توکن مشخص می‌شود. در مقابل، مقدار ورودی (amountIn) به‌طور مستقیم به تابع داده نمی‌شود؛ بلکه از مقدار توکنی که قبل از فراخوانی تابع به قرارداد ارسال شده، استنباط می‌شود.

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

عملکرد سواپ داخلی روتر uniswap v2

تابع داخلی _addLiquidity

بررسی‌های ایمنی برای افزودن نقدینگی را به خاطر دارید؟ اگر کاربر دو توکن را دقیقاً با همان نسبتی که الان داخل استخر وجود دارد واریز نکند، سیستم مقدار توکن‌های نقدینگی (LP tokens) را بر اساس ضعیف‌ترین نسبت محاسبه می‌کند. یعنی اگر یکی از توکن‌ها کمتر از نسبت واقعی باشد، فقط همان مقدار در نظر گرفته می‌شود و بقیه رد می‌شود. مشکل اینجاست که ممکن است این نسبت بین زمانی که کاربر تراکنش را ارسال می‌کند تا لحظه‌ای که تأیید می‌شود تغییر کند.

برای همین، یونی سواپ از کاربر می‌خواهد که علاوه بر مقادیر مورد نظرش برای واریز (amountADesired و amountBDesired)، یک مقدار حداقل قابل قبول هم مشخص کند (amountAMin و amountBMin). اگر نسبت استخر طوری تغییر کرده باشد که حداقل‌های مشخص‌شده رعایت نشوند، تراکنش به‌طور کامل لغو می‌شود.

تابع _addLiquidity اول با amountADesired شروع می‌کند و نسبت مناسب برای tokenB را محاسبه می‌کند تا با استخر هماهنگ باشد. اگر مقدار محاسبه‌شده از amountBDesired بیشتر شد، این بار با amountBDesired شروع می‌کند و مقدار مناسب tokenA را به‌دست می‌آورد. این منطق در خود تابع پیاده‌سازی شده. اگر برای این جفت توکن هنوز استخر ساخته نشده باشد، یونی سواپ به‌صورت خودکار یک قرارداد جفت جدید می‌سازد.

روتر uniswap v2، عملکرد داخلی نقدینگی را اضافه می‌کند

مثال ساده

فرض کنید نسبت فعلی در یک استخر، ۱۰۰ عدد token0 و ۳۰۰ عدد token1 است. شما می‌خواهید ۲۰ عدد token0 و ۶۰ عدد token1 به این استخر اضافه کنید. اما چون ممکن است در زمان ثبت تراکنش نسبت‌ها کمی تغییر کند، به یونی سواپ اجازه می‌دهید تا حداکثر ۲۱ عدد token0 و ۶۳ عدد token1 از شما بگیرد، ولی شرط می‌گذارید که حداقل مقدار باید همون ۲۰ و ۶۰ باشد. حالا اگر نسبت استخر طوری تغییر کند که مقدار مناسب token0 مثلاً ۱۹.۹ شود، چون از حداقل کمتر شده، تراکنش رد می‌شود.

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

توابع addLiquidity() و addLiquidityEth()

این توابع عملکرد مشخص و قابل درکی دارند. ابتدا با استفاده از تابع داخلی _addLiquidity، نسبت بهینه بین دو توکن را محاسبه می‌کنند. بعد از آن، توکن ها را به قرارداد جفت (Pair) منتقل می‌کنند و در نهایت تابع mint را روی همان قرارداد صدا می‌زنند تا توکن های نقدینگی (LP tokens) صادر شوند. تنها تفاوت بین این دو تابع این است که در addLiquidityEth، مقدار اتر (ETH) ارسالی ابتدا به نسخه wrap یعنی WETH تبدیل می‌شود، چون یونی سواپ فقط با توکن های استاندارد ERC20 کار می‌کند.

نقدینگی uniswap با اتریوم و weth افزایش می‌یابد

برداشتن نقدینگی (Removing Liquidity)

تابع removeLiquidity از تابع burn استفاده می‌کند اما دو پارامتر مهم amountAMin و amountBMin (در تصویر با رنگ قرمز مشخص شده‌اند) را هم به‌عنوان بررسی ایمنی در نظر می‌گیرد. این پارامترها تضمین می‌کنند که تأمین‌کننده نقدینگی هنگام برداشت، مقدار قابل قبولی از هر دو توکن A و B را دریافت کند. اگر نسبت بین توکن ها تا قبل از سوزاندن توکن های نقدینگی به شکل شدیدی تغییر کند، ممکن است کاربر نتواند مقدار مورد انتظار از هر دو توکن را دریافت کند و در این حالت تراکنش لغو می‌شود.

تابع removeLiquidityEth نیز در ابتدا تابع removeLiquidity را صدا می‌زند (در تصویر با رنگ سبز مشخص شده است)، اما تفاوت آن در نحوه مدیریت خروجی است. در این تابع، توکن های خارج‌شده به روتر فرستاده می‌شوند. سپس، توکن ERC20 عادی مستقیماً به تأمین‌کننده نقدینگی بازگردانده می‌شود و توکن WETH ابتدا به ETH تبدیل می‌شود و پس از آن به حساب کاربر ارسال می‌گردد.

این ساختار باعث می‌شود کاربران بتوانند به‌سادگی با ETH در فرآیند برداشت نقدینگی تعامل داشته باشند، بدون آنکه خودشان مستقیماً با WETH سر و کار داشته باشند.

روتر uniswap v2 نقدینگی را حذف می‌کند

توابع removeLiquidityWithPermit() و removeLiquidityETHWithPermit()

در خط ۱۰۹ از فایل بالا با کامنت خاکستری send liquidity to pair به این نکته اشاره دارد که قرارداد جفت (Pair) باید از طرف تأمین‌کننده‌ نقدینگی مجوز داشته باشد تا بتواند توکن های LP را دریافت و آن‌ها را بسوزاند. به عبارت دیگر، برای سوزاندن توکن های نقدینگی، ابتدا باید کاربر به قرارداد Pair اجازه انتقال این توکن ها را بدهد.

اما این مرحله را می‌توان با استفاده از تابع permit() نادیده گرفت، چون توکن های LP در یونی سواپ V2 از نوع ERC20 دارای قابلیت Permit هستند. تابع removeLiquidityWithPermit() این مجوز را از طریق امضا دریافت می‌کند و در همان تراکنش هم مجوز را اعمال می‌کند و هم توکن ها را می‌سوزاند. در نتیجه، دیگر نیازی به اجرای جداگانه تابع approve نیست.

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

Router02: پشتیبانی از توکن های دارای کارمزد انتقال (fee on transfer tokens)

برای پشتیبانی از توکن هایی که هنگام انتقال درصدی از مبلغ را به‌عنوان کارمزد کسر می‌کنند، روتر نمی‌تواند محاسبات خود را مستقیماً بر اساس پارامترهایی مانند amountIn() (در مبادله توکن) یا liquidity() (در برداشت نقدینگی) انجام دهد؛ چون این مقادیر ممکن است به دلیل کارمزد انتقال، به‌درستی منعکس نشوند.

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

روتر uniswap نسخه ۲ از کارمزد توکن‌های انتقال پشتیبانی می‌کند

کارمزد پشتیبانی یونی‌سواپ نسخه ۲ برای انتقال وجه جهت حذف نقدینگی

توابع پوششی برای UniswapV2Library

بقیه توابع موجود در کتابخانه Router در واقع نقش «پوششی» (Wrapper) دارند و تنها وظیفه‌شان این است که توابع اصلی موجود در کتابخانه UniswapV2 را فراخوانی کنند. ساختار این توابع در کد زیر قابل مشاهده است:

پارامتر deadline در روتر Uniswap V2

در تمام توابع عمومی موجود در روتر Uniswap V2، پارامتر deadline وجود دارد. این پارامتر مشخص می‌کند که تراکنش تا چه زمانی معتبر است. وقتی شما یک معامله را در یونی سواپ اجرا می‌کنید، در واقع انتظار دارید که معامله با قیمت‌های فعلی انجام شود، نه با قیمتی که ده دقیقه یا یک ساعت بعد ممکن است تغییر کند.

اگر در حال نوشتن یک قرارداد هوشمند هستید که با یونی سواپ تعامل دارد، هرگز مقدار deadline را برابر با block.timestamp یا block.timestamp + عدد ثابت قرار ندهید.

قرارداد شما باید به‌طور جداگانه بررسی کند که آیا تراکنش کاربر بیش از حد قدیمی شده یا نه. برای این کار، بهتر است پارامتر deadline را از کاربر دریافت کرده، به یونی سواپ منتقل کنید و در صورت گذشت زمان، تراکنش را لغو کنید (یعنی اگر block.timestamp > deadline، عملیات را متوقف کنید).

چگونه می‌توان از تراکنش‌های قدیمی سوءاستفاده کرد؟

یک استخراج‌کننده‌ مخرب (block builder) می‌تواند تراکنش swap کاربر را نگه دارد و آن را با تأخیر اجرا کند، درست زمانی که این تراکنش برای دستکاری قیمت یا فروختن توکن ها به کاربر با نرخ بدتر مفید باشد. وجود پارامتر deadline این بازه زمانی خطرناک را محدود می‌کند. مقدار deadline باید آن‌قدر جلوتر از زمان فعلی باشد که در شرایط شلوغی شبکه هم تراکنش فرصت اجرا داشته باشد، ولی نه آن‌قدر زیاد که فرصت سوءاستفاده ایجاد کند. معمولاً زمان مناسب برای deadline، چند دقیقه بعد از زمان امضای تراکنش است.

اما اگر قرارداد هوشمند شما اصلاً از deadline استفاده نکند، یا آن را نادیده بگیرد و خودش block.timestamp را به یونی سواپ بدهد، کاربر هیچ محافظتی در برابر حملات نخواهد داشت.

هیچ‌وقت مقدار amountMin را صفر یا amountMax را برابر با type(uint).max قرار ندهید

یکی از اشتباهات رایج این است که مقدار amountMin (حداقل مقدار خروجی مورد قبول) را صفر تنظیم می‌کنند، یا amountMax (حداکثر مقدار ورودی قابل قبول) را روی مقدار بسیار بزرگی مانند type(uint).max می‌گذارند. این کار باعث از بین رفتن کامل محافظت در برابر لغزش قیمت (price slippage) و حملات ساندویچی (sandwich attacks) می‌شود و امنیت کاربر را به‌شدت کاهش می‌دهد.

جمع‌بندی

قراردادهای Router در یونی سواپ نقش رابط کاربر را ایفا می‌کنند و امکان مبادله توکن‌ ها را با محافظت در برابر لغزش قیمت (slippage) فراهم می‌کنند؛ حتی اگر این مبادله از چند استخر مختلف عبور کند. در نسخه Router02، پشتیبانی از مبادله با اتر (ETH) و همچنین توکن‌های دارای کارمزد انتقال (fee-on-transfer tokens) نیز به این امکانات اضافه شده است.

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

فرآیند برداشت نقدینگی می‌تواند به‌سادگی با انتقال توکن‌های LP به روتر و سوزاندن آن‌ها انجام شود، یا در صورت نیاز، شامل تبدیل WETH به ETH و دریافت توکن‌های دارای کارمزد انتقال نیز باشد.

همچنین، این سیستم از تأیید بدون گس (gas-free approvals) از طریق استاندارد ERC20 Permit پشتیبانی می‌کند.

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

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

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

پکیج آموزش صفر تا صد فتوشاپ به زبان فارسی – حرفه ای شوید
  • انتشار: ۲۳ خرداد ۱۴۰۴

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

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

مشاهده همه

نظرات

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