Uniswap V2 का swap फ़ंक्शन बहुत चतुराई से डिज़ाइन किया गया है, लेकिन कई devs को पहली बार इसे देखने पर इसका लॉजिक उल्टा (counterintuitive) लगता है। यह लेख गहराई से समझाता है कि यह कैसे काम करता है।
नीचे कोड प्रस्तुत किया गया है:

माना कि यह कोड की एक बहुत बड़ी दीवार है, लेकिन आइए इसे छोटे हिस्सों में समझते हैं।
-
लाइन 170-171 पर (yellow box से दर्शाया गया है), फ़ंक्शन सीधे उन tokens की मात्रा को बाहर भेज (transfer out) देता है जिसका अनुरोध ट्रेडर ने फ़ंक्शन आर्गुमेंट्स में किया था। फ़ंक्शन के अंदर ऐसी कोई जगह नहीं है जहाँ tokens अंदर भेजे (transferred in) जाते हों। कोड को स्कैन करें और देखें कि क्या आप वह जगह खोज सकते हैं जहाँ tokens अंदर भेजे जाते हैं, यह मौजूद ही नहीं है। लेकिन इसका मतलब यह नहीं है कि हम केवल swap को कॉल कर सकते हैं और जितने चाहें उतने tokens निकाल सकते हैं!
-
हम तुरंत tokens इसलिए निकाल सकते हैं ताकि हम flash loans कर सकें। बेशक, लाइन 182 पर require स्टेटमेंट (orange arrow) हमें ब्याज के साथ flash loan वापस चुकाने के लिए बाध्य करेगा।
-
फ़ंक्शन के शीर्ष पर, एक कमेंट है जो कहता है कि फ़ंक्शन को किसी अन्य स्मार्ट कॉन्ट्रैक्ट से कॉल किया जाना चाहिए जो महत्वपूर्ण सुरक्षा जाँचों (safety checks) को लागू करता है। इसका मतलब है कि विशेष रूप से इस फ़ंक्शन में सुरक्षा जाँचों की कमी है (लाल रेखांकित)। हम यह निर्धारित करना चाहेंगे कि वे क्या हैं।
-
variables _reserve0 और _reserve1 (blue underline) को लाइन्स 161, 176-177, और 182 पर पढ़ा (read) जाता है, लेकिन उन्हें इस फ़ंक्शन में लिखा (written to) नहीं जाता है।
-
लाइन 182 (orange arrow) सख्ती से यह जाँच नहीं करती कि X × Y = K है या नहीं। यह जाँचता है कि balance1Adjusted × balance2Adjusted ≥ K है या नहीं। यह एकमात्र require स्टेटमेंट है जो कुछ “रोचक” करता है। अन्य require स्टेटमेंट्स यह जाँचते हैं कि मान शून्य नहीं हैं या आप tokens को उनके स्वयं के कॉन्ट्रैक्ट एड्रेस पर नहीं भेज रहे हैं।
-
balance0 और balance1 को सीधे pair कॉन्ट्रैक्ट के वास्तविक बैलेंस से ERC20 balanceOf का उपयोग करके पढ़ा जाता है।
-
लाइन 172 (yellow box के नीचे) केवल तभी निष्पादित (executed) होती है जब data खाली (non-empty) न हो, अन्यथा यह निष्पादित नहीं होती है।
इन अवलोकनों (observations) का उपयोग करते हुए, हम इस फ़ंक्शन को एक-एक फीचर करके समझेंगे।
Flash Borrowing
यूज़र्स को tokens ट्रेड करने के लिए swap फ़ंक्शन का उपयोग करने की आवश्यकता नहीं है, इसका उपयोग पूरी तरह से एक flash loan के रूप में किया जा सकता है।

बरोइंग कॉन्ट्रैक्ट (borrowing contract) केवल उन tokens की मात्रा का अनुरोध करता है जिन्हें वे बिना किसी कोलैटरल (collateral) के उधार लेना चाहते हैं (A) और उन्हें कॉन्ट्रैक्ट में ट्रांसफर कर दिया जाएगा (B)।
फ़ंक्शन कॉल के साथ जो data प्रदान किया जाना चाहिए, उसे एक फ़ंक्शन आर्गुमेंट (C) के रूप में पास किया जाता है, और इसे उस फ़ंक्शन में पास किया जाएगा जो इम्प्लीमेंट करता है
IUniswapV2Callee। फ़ंक्शन uniswapV2Call को flash loan और साथ में शुल्क (fee) का भुगतान करना होगा, अन्यथा ट्रांज़ैक्शन रिवर्ट (revert) हो जाएगा।
Swap के लिए स्मार्ट कॉन्ट्रैक्ट का उपयोग करना आवश्यक है
यदि flash loan का उपयोग नहीं किया जाता है, तो आने वाले (incoming) tokens को swap फ़ंक्शन को कॉल करने के हिस्से के रूप में भेजा जाना चाहिए।
यह स्पष्ट होना चाहिए कि केवल एक स्मार्ट कॉन्ट्रैक्ट ही swap फ़ंक्शन के साथ इंटरैक्ट करने में सक्षम है, क्योंकि एक EOA किसी अन्य स्मार्ट कॉन्ट्रैक्ट की सहायता के बिना एक ही ट्रांज़ैक्शन में एक साथ आने वाले ERC20 tokens नहीं भेज सकता और swap को कॉल नहीं कर सकता।
आने वाले (incoming) tokens की मात्रा मापना
जिस तरह से Uniswap V2 अंदर भेजे गए tokens की मात्रा को “मापता” है, वह लाइन 176 और 177 पर किया जाता है, जिसे नीचे yellow box से चिह्नित किया गया है।

याद रखें, _reserve0 और _reserve1 इस फ़ंक्शन के अंदर अपडेट नहीं होते हैं। वे swap के हिस्से के रूप में नए tokens के सेट को अंदर भेजे जाने से पहले कॉन्ट्रैक्ट के बैलेंस को दर्शाते हैं।
Pair में दोनों tokens में से प्रत्येक के लिए दो में से एक चीज़ हो सकती है:
-
पूल में किसी विशेष token की मात्रा में शुद्ध वृद्धि (net increase) हुई।
-
पूल में किसी विशेष token की मात्रा में शुद्ध कमी (net decrease) (या कोई बदलाव नहीं) हुई।
कोड निम्नलिखित लॉजिक के साथ यह निर्धारित करता है कि कौन सी स्थिति हुई:
currentContractbalanceX > _reserveX - _amountXOut
// alternatively
currentContractBalanceX > previousContractBalanceX - _amountXOut
यदि यह शुद्ध कमी (net decrease) मापता है, तो टर्नरी ऑपरेटर (ternary operator) शून्य लौटाता है, अन्यथा यह अंदर आए tokens का शुद्ध लाभ (net gain) मापेगा।
amountXIn = balanceX - (_reserveX - amountXOut)
यह हमेशा होता है कि _reserveX > amountXOut क्योंकि लाइन 162 पर require स्टेटमेंट मौजूद है।

कुछ उदाहरण।
-
मान लीजिए कि हमारा पिछला बैलेंस 10 था, amountOut शून्य है, और currentBalance 12 है। इसका मतलब है कि यूज़र ने 2 tokens जमा किए। amountXIn 2 होगा।
-
मान लीजिए कि हमारा पिछला बैलेंस 10 था, amountOut 7 है, और currentBalance 3 है। amountXIn 0 होगा।
-
मान लीजिए कि हमारा पिछला बैलेंस 10 था, amountOut 7 है, और currentBalance 2 है। amountXIn अभी भी शून्य होगा, -1 नहीं। यह सच है कि पूल में 8 tokens का शुद्ध नुकसान (net loss) हुआ, लेकिन amountXIn नेगेटिव नहीं हो सकता।
-
मान लीजिए कि हमारा पिछला बैलेंस 10 था, और amountOut 6 है। यदि currentBalance 18 है, तो यूज़र ने 6 tokens “उधार (borrowed)” लिए लेकिन 8 tokens वापस चुका दिए।
निष्कर्ष: amount0In और amount1In शुद्ध लाभ (net gain) को दर्शाएंगे यदि token के लिए कोई शुद्ध लाभ हुआ था, और वे शून्य होंगे यदि उस token का कोई शुद्ध नुकसान (net loss) हुआ था।
XY = K को संतुलित करना
अब जब हम जानते हैं कि यूज़र ने कितने tokens अंदर भेजे हैं, तो आइए देखें कि XY = K को कैसे लागू (enforce) किया जाए।
कोड फिर से है

Uniswap V2 प्रति स्वैप (swap) एक हार्डकोडेड 0.3% शुल्क लेता है, यही कारण है कि हम यहाँ 1000 और 3 नंबरों का खेल देखते हैं, लेकिन आइए इसे उस स्थिति में बदलकर सरल बनाएं जहाँ Uniswap V2 कोई शुल्क नहीं लेता है। इसका मतलब है कि हम .sub(amountXIn.mul(3)) टर्म को हटा सकते हैं और लाइन्स 180 से 181 पर 1000 से या लाइन 182 पर 1000**2 से गुणा नहीं करेंगे।
नया कोड होगा
require(balance0 * balance1 >= reserve0 * reserve1, "K");
यह कह रहा है
K वास्तव में स्थिर (constant) नहीं है
यह कहना थोड़ा भ्रामक है कि “K स्थिर रहता है” भले ही AMM फॉर्मूला को कभी-कभी “constant product formula” कहा जाता है।
इसे इस तरह सोचें, यदि किसी ने पूल में tokens दान कर दिए और K का मान बदल दिया, तो हम उन्हें रोकना नहीं चाहेंगे क्योंकि उन्होंने हम liquidity providers को अधिक अमीर बना दिया है, सही?
Uniswap V2 आपको “बहुत अधिक भुगतान करने” यानी स्वैप के दौरान बहुत अधिक tokens अंदर ट्रांसफर करने से नहीं रोकता है (यह सुरक्षा जाँचों में से एक से संबंधित है, जिस पर हम बाद में चर्चा करेंगे)।
हम परेशान होंगे यदि पूल में कोई शुद्ध नुकसान (net loss) होता है, और यही वह चीज़ है जिसकी जाँच require स्टेटमेंट कर रहा है। यदि K बड़ा हो जाता है, तो इसका मतलब है कि पूल बड़ा हो गया है, और liquidity providers के रूप में, हम यही चाहते हैं।
फीस (fees) का हिसाब रखना
लेकिन हम केवल यही नहीं चाहते कि K बड़ा हो, हम चाहते हैं कि यह कम से कम उस राशि से बड़ा हो जो 0.3% शुल्क लागू (enforces) करती है।
विशेष रूप से, 0.3% शुल्क हमारे ट्रेड के आकार पर लागू होता है, न कि पूल के आकार पर। यह केवल उन tokens पर लागू होता है जो अंदर जाते हैं, न कि बाहर जाने वाले tokens पर। कुछ उदाहरण:
-
मान लीजिए हम token0 के 1000 डालते हैं और token1 के 1000 निकालते हैं। हमें token0 पर 3 का शुल्क देना होगा और token1 पर कोई शुल्क नहीं।
-
मान लीजिए हम token0 के 1000 उधार लेते हैं और token1 उधार नहीं लेते। हमें token0 के 1000 वापस अंदर डालने होंगे, और हमें उस पर 0.3% शुल्क देना होगा — token0 का 3।
ध्यान दें कि यदि हम किसी एक token को flash borrow करते हैं, तो इसका परिणाम उसी मात्रा के लिए उस token को स्वैप करने के समान शुल्क होता है। आप अंदर आने वाले (in) tokens पर शुल्क का भुगतान करते हैं, बाहर जाने वाले (out) tokens पर नहीं। लेकिन यदि आप tokens अंदर नहीं डालते हैं, तो आपके लिए उधार लेने या स्वैप करने का कोई तरीका नहीं है।
याद रखें, reserve0 और reserve1 पुराने बैलेंस को दर्शाते हैं, और balance0 और balance1 अपडेट किए गए बैलेंस को दर्शाते हैं।
इसे ध्यान में रखते हुए, आइए नीचे दिया गया कोड देखें जिसे समझना आसान होना चाहिए। 1000 और 3 से गुणा करना केवल “भिन्नात्मक (fractional)” गुणा को पूरा करने के लिए है क्योंकि यह अंत में कट (cancels out) जाता है।

कोड निम्नलिखित फ़ॉर्मूला को पूरा कर रहा है:
यानी, नया बैलेंस अंदर आई मात्रा (amount in) के 0.3% से बढ़ना चाहिए। कोड में, फ़ॉर्मूले को प्रत्येक टर्म को 1,000 से गुणा करके स्केल (scaled) किया गया है क्योंकि Solidity में फ्लोटिंग पॉइंट (floating point) नंबर नहीं होते हैं, लेकिन गणित का फ़ॉर्मूला दिखाता है कि कोड क्या हासिल करने की कोशिश कर रहा है।
रिज़र्व्स (Reserves) को अपडेट करना
अब जब ट्रेड पूरा हो गया है, तो “पिछले बैलेंस” को वर्तमान बैलेंस से बदल दिया जाना चाहिए। यह swap() के अंत में _update() फ़ंक्शन को कॉल करने पर होता है।

_update() फ़ंक्शन

यहाँ TWAP ओरेकल (oracle) को संभालने के लिए बहुत सारे लॉजिक हैं, लेकिन अभी हमें केवल लाइन्स 82 और 83 की परवाह है जहाँ बदले हुए बैलेंस को दर्शाने के लिए स्टोरेज वेरिएबल्स reserve0 और reserve1 को अपडेट किया जाता है। आर्गुमेंट्स _reserve0 और _reserve1 का उपयोग ओरेकल को अपडेट करने के लिए किया जाता है, लेकिन उन्हें स्टोर नहीं किया जाता है।
सुरक्षा जाँच (Safety Checks)
दो चीजें हैं जो गलत हो सकती हैं:
-
amountIn को अनुकूल (optimal) होने के लिए लागू (enforce) नहीं किया गया है, इसलिए यूज़र स्वैप के लिए अधिक भुगतान (overpay) कर सकता है।
-
AmountOut में कोई लचीलापन (flexibility) नहीं है क्योंकि इसे पैरामीटर आर्गुमेंट के रूप में सप्लाई किया जाता है। यदि amountIn, amountOut के सापेक्ष पर्याप्त नहीं होता है, तो ट्रांज़ैक्शन रिवर्ट हो जाएगा और गैस (gas) बर्बाद हो जाएगी।
ये परिस्थितियाँ तब हो सकती हैं जब कोई किसी ट्रांज़ैक्शन को फ्रंटरन (frontruns) करता है (जानबूझकर या नहीं) और पूल में एसेट्स (assets) के अनुपात को एक अवांछनीय दिशा में बदल देता है।
RareSkills के साथ और जानें
यह लेख हमारे एडवांस Solidity Bootcamp का हिस्सा है। अधिक जानने के लिए कृपया पाठ्यक्रम (curriculum) देखें।
मूल रूप से 28 अक्टूबर, 2023 को प्रकाशित