قراردادهای روتر در Uniswap V2 یک رابط کاربری برای کاربران فراهم میکنند تا بتوانند از طریق آن با قراردادهای هوشمند تعامل داشته باشند. این قراردادها امکان انجام عملیات زیر را به صورت ایمن فراهم میکنند:
-
ایجاد (mint) و سوزاندن (burn) امن توکن های نقدینگی (LP tokens) برای افزودن و حذف نقدینگی
-
مبادله ایمن توکن های جفت شده
همچنین این قراردادها قابلیتهای زیر را اضافه میکنند:
-
امکان مبادله اتر (Ether) از طریق ادغام با قرارداد WETH (نسخه رپشده اتر با استاندارد ERC20)
-
بررسیهای ایمنی مرتبط با لغزش قیمت (slippage) که در قرارداد اصلی وجود ندارند
-
پشتیبانی از توکن هایی که هنگام انتقال، کارمزد دریافت میکنند (fee on transfer tokens)
Router02 تمام قابلیت های Router01 را دارد و از توکن های دارای کارمزد انتقال نیز پشتیبانی میکند
وقتی پوشه contracts
را در مخزن جانبی (periphery) یونی سواپ باز میکنیم، با سه قرارداد مواجه میشویم:
Router02
همان عملکردهای Router01
را ارائه میدهد و علاوه بر آن، توابع جدیدی برای پشتیبانی از توکن های دارای کارمزد انتقال (fee on transfer tokens) در اختیار توسعه دهنده قرار میدهد. اگر رابط Router02
را بررسی کنیم، متوجه میشویم که این قرارداد از Router01
ارث بری میکند (کادر قرمز) و به همین دلیل تمام توابع آن را درون خود دارد.
همچنین توسعهدهندگان میتوانند با استفاده از توابع اضافهشده (مشخصشده با هایلایت زرد)، عملیات مورد نظر خود را روی توکن هایی انجام دهند که هنگام انتقال، بخشی از مبلغ را بهعنوان کارمزد کسر میکنند.
توابع swapExactTokensForTokens و swapTokensForExactTokens
بیایید با توابع مربوط به مبادله توکن ها در قرارداد Router شروع کنیم. در Uniswap 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()
را اجرا میکنند که در ادامه مورد بررسی قرار میگیرد.
تابع داخلی _swap()
در پشتصحنه، تمام توابع عمومی که نام آنها شامل swap()
است، در نهایت تابع داخلی _swap()
را فراخوانی میکنند. این تابع مسئول انجام واقعی مبادله بین توکن ها است.
به یاد داشته باشید که در امضای تابع swap()
مربوط به قرارداد جفت (Pair) در Uniswap V2، مقادیر خروجی (amountOut
) برای هر دو توکن مشخص میشود. در مقابل، مقدار ورودی (amountIn
) بهطور مستقیم به تابع داده نمیشود؛ بلکه از مقدار توکنی که قبل از فراخوانی تابع به قرارداد ارسال شده، استنباط میشود.
این طراحی باعث میشود که امنیت و سادگی بیشتری در تعامل با قرارداد حفظ شود، زیرا تابع بهجای اعتماد به پارامترهای دریافتی، به موجودی واقعی توکن هایی که در قرارداد قرار گرفتهاند اتکا میکند.
تابع داخلی _addLiquidity
بررسیهای ایمنی برای افزودن نقدینگی را به خاطر دارید؟ اگر کاربر دو توکن را دقیقاً با همان نسبتی که الان داخل استخر وجود دارد واریز نکند، سیستم مقدار توکنهای نقدینگی (LP tokens) را بر اساس ضعیفترین نسبت محاسبه میکند. یعنی اگر یکی از توکنها کمتر از نسبت واقعی باشد، فقط همان مقدار در نظر گرفته میشود و بقیه رد میشود. مشکل اینجاست که ممکن است این نسبت بین زمانی که کاربر تراکنش را ارسال میکند تا لحظهای که تأیید میشود تغییر کند.
برای همین، یونی سواپ از کاربر میخواهد که علاوه بر مقادیر مورد نظرش برای واریز (amountADesired
و amountBDesired
)، یک مقدار حداقل قابل قبول هم مشخص کند (amountAMin
و amountBMin
). اگر نسبت استخر طوری تغییر کرده باشد که حداقلهای مشخصشده رعایت نشوند، تراکنش بهطور کامل لغو میشود.
تابع _addLiquidity
اول با amountADesired
شروع میکند و نسبت مناسب برای tokenB
را محاسبه میکند تا با استخر هماهنگ باشد. اگر مقدار محاسبهشده از amountBDesired
بیشتر شد، این بار با amountBDesired
شروع میکند و مقدار مناسب tokenA
را بهدست میآورد. این منطق در خود تابع پیادهسازی شده. اگر برای این جفت توکن هنوز استخر ساخته نشده باشد، یونی سواپ بهصورت خودکار یک قرارداد جفت جدید میسازد.
مثال ساده
فرض کنید نسبت فعلی در یک استخر، ۱۰۰ عدد token0
و ۳۰۰ عدد token1
است. شما میخواهید ۲۰ عدد token0
و ۶۰ عدد token1
به این استخر اضافه کنید. اما چون ممکن است در زمان ثبت تراکنش نسبتها کمی تغییر کند، به یونی سواپ اجازه میدهید تا حداکثر ۲۱ عدد token0
و ۶۳ عدد token1
از شما بگیرد، ولی شرط میگذارید که حداقل مقدار باید همون ۲۰ و ۶۰ باشد. حالا اگر نسبت استخر طوری تغییر کند که مقدار مناسب token0
مثلاً ۱۹.۹ شود، چون از حداقل کمتر شده، تراکنش رد میشود.
در نهایت به این نکته توجه کنید که تابع quote
نباید بهعنوان قیمت لحظهای استفاده شود. قبلاً هم گفتیم که این تابع اوراکل نیست و هنوز هم این نکته برقرار است. اما موقع افزودن نقدینگی، چیزی که مهم است نسبت فعلی توکن ها در استخر است، نه میانگین قیمتهای قبلی؛ چون تأمینکننده نقدینگی باید دقیقاً از همین نسبت لحظهای پیروی کند.
توابع addLiquidity() و addLiquidityEth()
این توابع عملکرد مشخص و قابل درکی دارند. ابتدا با استفاده از تابع داخلی _addLiquidity
، نسبت بهینه بین دو توکن را محاسبه میکنند. بعد از آن، توکن ها را به قرارداد جفت (Pair) منتقل میکنند و در نهایت تابع mint
را روی همان قرارداد صدا میزنند تا توکن های نقدینگی (LP tokens) صادر شوند. تنها تفاوت بین این دو تابع این است که در addLiquidityEth
، مقدار اتر (ETH) ارسالی ابتدا به نسخه wrap یعنی WETH تبدیل میشود، چون یونی سواپ فقط با توکن های استاندارد ERC20 کار میکند.
برداشتن نقدینگی (Removing Liquidity)
تابع removeLiquidity
از تابع burn
استفاده میکند اما دو پارامتر مهم amountAMin
و amountBMin
(در تصویر با رنگ قرمز مشخص شدهاند) را هم بهعنوان بررسی ایمنی در نظر میگیرد. این پارامترها تضمین میکنند که تأمینکننده نقدینگی هنگام برداشت، مقدار قابل قبولی از هر دو توکن A و B را دریافت کند. اگر نسبت بین توکن ها تا قبل از سوزاندن توکن های نقدینگی به شکل شدیدی تغییر کند، ممکن است کاربر نتواند مقدار مورد انتظار از هر دو توکن را دریافت کند و در این حالت تراکنش لغو میشود.
تابع removeLiquidityEth
نیز در ابتدا تابع removeLiquidity
را صدا میزند (در تصویر با رنگ سبز مشخص شده است)، اما تفاوت آن در نحوه مدیریت خروجی است. در این تابع، توکن های خارجشده به روتر فرستاده میشوند. سپس، توکن ERC20 عادی مستقیماً به تأمینکننده نقدینگی بازگردانده میشود و توکن WETH ابتدا به ETH تبدیل میشود و پس از آن به حساب کاربر ارسال میگردد.
این ساختار باعث میشود کاربران بتوانند بهسادگی با ETH در فرآیند برداشت نقدینگی تعامل داشته باشند، بدون آنکه خودشان مستقیماً با WETH سر و کار داشته باشند.
توابع 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()
(در برداشت نقدینگی) انجام دهد؛ چون این مقادیر ممکن است به دلیل کارمزد انتقال، بهدرستی منعکس نشوند.
با این حال، فرآیند افزودن نقدینگی تحت تأثیر این نوع توکن ها قرار نمیگیرد، چون فقط مقدار واقعی که کاربر به قرارداد جفت منتقل میکند در نظر گرفته میشود و همان مقدار ملاک محاسبه قرار میگیرد. بنابراین در این بخش، کارمزد انتقال مشکلی ایجاد نمیکند.
توابع پوششی برای UniswapV2Library
بقیه توابع موجود در کتابخانه Router در واقع نقش «پوششی» (Wrapper) دارند و تنها وظیفهشان این است که توابع اصلی موجود در کتابخانه UniswapV2
را فراخوانی کنند. ساختار این توابع در کد زیر قابل مشاهده است:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
function quote(uint amountA, uint reserveA, uint reserveB) public pure override returns (uint amountB) { return UniswapV2Library.quote(amountA, reserveA, reserveB); } function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) public pure override returns (uint amountOut) { return UniswapV2Library.getAmountOut(amountIn, reserveIn, reserveOut); } function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) public pure override returns (uint amountIn) { return UniswapV2Library.getAmountOut(amountOut, reserveIn, reserveOut); } function getAmountsOut(uint amountIn, address[] memory path) public view override returns (uint[] memory amounts) { return UniswapV2Library.getAmountsOut(factory, amountIn, path); } function getAmountsIn(uint amountOut, address[] memory path) public view override returns (uint[] memory amounts) { return UniswapV2Library.getAmountsIn(factory, amountOut, path); } } |
پارامتر 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 پشتیبانی میکند.
نکته پایانی اینکه اگر قصد دارید یک قرارداد هوشمند بنویسید که با یونی سواپ ادغام شود، نباید هیچکدام از محافظتهای مهم در برابر تأخیر در مبادله یا لغزش قیمت را غیرفعال کنید. این محافظتها بخش مهمی از امنیت تراکنشها را تضمین میکنند.
راستی! برای دریافت مطالب جدید در کانال تلگرام یا پیج اینستاگرام سورس باران عضو شوید.
- انتشار: ۲۳ خرداد ۱۴۰۴
دسته بندی موضوعات
- آموزش ارز دیجیتال
- آموزش برنامه نویسی
- آموزش متنی برنامه نویسی
- اطلاعیه و سایر مطالب
- پروژه برنامه نویسی
- دوره های تخصصی برنامه نویسی
- رپورتاژ
- فیلم های آموزشی
- ++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
- اچ تی ام ال
- بانک اطلاعاتی
- برنامه نویسی سوکت
- برنامه نویسی موبایل
- پاسکال
- پایان نامه
- پایتون
- جاوا
- جاوا اسکریپت
- جی کوئری
- داده کاوی
- دلفی
- رباتیک
- سئو
- سایر کتاب ها
- سخت افزار
- سی اس اس
- سی پلاس پلاس
- سی شارپ
- طراحی الگوریتم
- فتوشاپ
- مقاله
- مهندسی نرم افزار
- هک و امنیت
- هوش مصنوعی
- ویژوال بیسیک
- نرم افزار و ابزار برنامه نویسی
- وردپرس