किसी AMM में liquidity जोड़ने का अर्थ है AMM pool में टोकन जमा करना। Liquidity providers ऐसा इस उम्मीद में करते हैं कि वे उन यूज़र्स से fees कमा सकें जो उस pool के साथ swap करते हैं।
Uniswap v2 में, जब कोई LP liquidity जोड़ता है, तो उन्हें Liquidity Provider tokens (LP tokens) के रूप में pool के शेयर प्राप्त होते हैं, जो pool में उन टोकन्स के प्रतिशत को दर्शाते हैं जिनके वे हक़दार हैं, जिसमें fees भी शामिल है। ये LP tokens fungible ERC-20 tokens होते हैं।
Uniswap v3 में, यह तरीका काम नहीं करता है क्योंकि LP उस रेंज (range) को चुनता है जहाँ वह liquidity जमा करना चाहता है— lower और upper ticks। इसलिए, प्रोटोकॉल को प्रत्येक डिपॉज़िट को व्यक्तिगत रूप से एक non-fungible तरीके से ट्रैक करने की आवश्यकता होती है। यह हमें positions के कॉन्सेप्ट पर लाता है।
जब कोई LP किसी रेंज में liquidity जोड़ता है, तो हम कहते हैं कि वे एक position को open या modify करते हैं। यह तब opened (खोली गई) मानी जाती है जब position पहले से मौजूद नहीं होती है, और जब यह मौजूद होती है तब इसे modified (संशोधित) कहा जाता है।
एक रेंज में दो ticks होते हैं — एक lower और एक upper tick। किसी रेंज में liquidity जमा करने का अर्थ है उस रेंज के भीतर real reserves को बढ़ाना। यह UniswapV3Pool.sol में mint function का उपयोग करके प्राप्त किया जाता है, जिसका इंटरफ़ेस नीचे दिखाया गया है।
// UniswapV3Pool.sol
function mint(
address recipient, // the owner of the position
int24 tickLower,
int24 tickUpper,
uint128 amount, // amount in liquidity
bytes calldata data // will be explained in a future chapter, it is not necessary for our discussion
) external override lock returns (uint256 amount0, uint256 amount1) {
इस function का नाम mint Uniswap v2 की याद दिलाता है, जहाँ liquidity जोड़ने पर liquidity provider के लिए ERC-20 tokens मिंट (mint) किए जाते थे। हालाँकि v3 में अब ऐसा नहीं होता है, फिर भी नाम को बरकरार रखा गया है - इस बार एक position को मिंट करने के संदर्भ में।
इस अध्याय का लक्ष्य यह जाँचना है कि प्रोटोकॉल इन positions के बारे में जानकारी कैसे और कहाँ स्टोर करता है।
positions मैपिंग
Positions को positions नामक एक मैपिंग (mapping) में स्टोर किया जाता है, जो UniswapV3Pool कॉन्ट्रैक्ट में स्थित है, जैसा कि नीचे दी गई इमेज में दिखाया गया है।

किसी position की पहचान करने वाली key, position के मालिक (owner) के एड्रेस, lower tick और upper tick के Keccak हैश से बनती है।
इस प्रकार, यदि owner 0xA के लिए ticks -10 और 10 के बीच पहली बार liquidity जमा की जाती है, तो keccak(0xA, -10, 10) key के साथ एक position बनाई जाएगी।
नीचे एक Solidity कोड दिया गया है जिसका उपयोग किसी position के लिए key जनरेट करने के लिए किया जाता है।
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Position {
function getKey(
address owner,
int24 tickLower,
int24 tickUpper)
public pure returns (bytes32 key) {
key = keccak256(abi.encodePacked(owner, tickLower, tickUpper));
}
}
एक बार बन जाने के बाद, यदि owner 0xA के लिए ticks -10 और 10 के बीच अधिक liquidity जमा की जाती है (या निकाली जाती है), तो position को modify किया जाएगा, क्योंकि यह पहले से मौजूद है।
मैपिंग की वैल्यू Position.Info प्रकार की होती है, जो Info नामक एक struct है और Position.sol कॉन्ट्रैक्ट के भीतर Position लाइब्रेरी में स्थित है।
यह struct, जिसे नीचे दिखाया गया है, position की liquidity (नारंगी बॉक्स) के साथ-साथ fees और position के स्वामित्व वाले टोकन्स से संबंधित चार अन्य फ़ील्ड्स (हरा बॉक्स) को स्टोर करता है। हम इन अन्य फ़ील्ड्स की चर्चा को तब तक के लिए टाल देंगे जब तक कि हम fees और position से liquidity निकालने को कवर न कर लें।

अभी के लिए, हम इसे इस तरह से समझ सकते हैं: यदि किसी दिए गए owner द्वारा एक रेंज के भीतर liquidity जोड़ी जाती है, तो एक position बन जाती है। उसके बाद, liquidity को जोड़कर या हटाकर इस position को modify किया जा सकता है।
Positions non-fungible होती हैं
Uniswap v2 में, जब कोई LP liquidity प्रदान करता है, तो LP के लिए ERC-20 LP tokens मिंट किए जाते हैं। ये LP tokens fungible होते हैं और pool के एसेट्स (assets) के एक हिस्से को दर्शाते हैं। इसका मतलब यह है कि, यदि दो LPs में से प्रत्येक के पास 1000 LP tokens हैं और वे pool में अपने शेयर को रिडीम (redeem) करने के लिए अपने LP tokens का उपयोग करते हैं, तो उन्हें मिलने वाले asset tokens समान होंगे।
Uniswap v3 में, positions non-fungible होती हैं। इसका मतलब यह है कि यदि दो LPs के पास अलग-अलग positions हैं और वे pool में अपने शेयरों को रिडीम करने के लिए इन positions का उपयोग करते हैं, तो उन्हें शायद एसेट्स की समान मात्रा प्राप्त नहीं होगी, क्योंकि वे संभवतः ticks की समान रेंज या उन ticks के बीच LPs द्वारा योगदान किए गए real reserves का प्रतिनिधित्व नहीं करते हैं।
एक peripheral contract के माध्यम से इन non-fungible positions को ERC-721 non-fungible tokens के रूप में हैंडल करना संभव है, लेकिन कोर (core) कॉन्ट्रैक्ट स्वयं ऐसा नहीं करता है—यह एक ERC-721 कॉन्ट्रैक्ट नहीं है और third parties को positions ट्रांसफर करने की अनुमति नहीं देता है। कोर कॉन्ट्रैक्ट केवल positions को open करने और modify करने की अनुमति देता है।
Uniswap v3 में Fees
Uniswap v3 में Fees भी Uniswap v2 की तुलना में बहुत अलग तरीके से काम करती हैं। Uniswap v2 में, जब भी कोई swap होता है, तो fees को pool में जोड़ दिया जाता है, जिससे प्रत्येक शेयर के हकदार टोकन की मात्रा बढ़ जाती है।
Uniswap v3 में, एक position केवल उन swaps से fees की हकदार होती है जो इसकी tick रेंज के भीतर—आंशिक या पूर्ण रूप से—हुए हों। इसलिए, यदि कोई LP ऐसी position खोलता है जो कभी किसी swap में शामिल नहीं होती है, तो वह position कोई fees नहीं कमाती है। यह LPs को उन क्षेत्रों में positions खोलने के लिए प्रोत्साहित करता है जहाँ swaps होने की संभावना होती है, जिससे उस जगह liquidity बढ़ती है जहाँ इसकी सबसे अधिक आवश्यकता होती है।
चूंकि fees अब सभी LPs के बीच आनुपातिक रूप से (proportionally) शेयर नहीं की जाती हैं, बल्कि व्यक्तिगत रूप से प्रत्येक position को दी जाती हैं, इसलिए उन्हें अलग से ट्रैक किया जाना चाहिए।
प्रोटोकॉल यह कैसे कैलकुलेट करता है कि एक position कितनी fees की हकदार है, यह सीधा (straightforward) नहीं है और इसमें Position के Info struct में मौजूद feeGrowthInside0LastX128 और feeGrowthInside1LastX128 वेरिएबल्स सहित कई वेरिएबल्स का संयोजन शामिल है।
एक हाई-लेवल व्याख्या के रूप में, प्रोटोकॉल अपने निर्माण के बाद से pool में जमा हुई fees को ट्रैक करता है, साथ ही यह भी ट्रैक करता है कि प्रत्येक tick के नीचे और ऊपर कितनी fees एकत्र की गई थी जो एक position की सीमा (boundary) के रूप में कार्य करती है।
यह प्रोटोकॉल को यह कैलकुलेट करने की अनुमति देता है कि एकत्रित की गई fees का कितना हिस्सा किसी position के lower और upper ticks के बीच एकत्र किया गया था और position के feeGrowthInside0LastX128 और feeGrowthInside1LastX128 वेरिएबल्स का उपयोग यह निर्धारित करने के लिए करता है कि इन fees का कितना हिस्सा उस विशेष position का है।
प्रोटोकॉल इसे कैसे प्राप्त करता है, इसका विवरण भविष्य के अध्यायों में कवर किया जाएगा।
एक pool कई positions से बना होता है
हम कहते आ रहे हैं कि एक pool में सेगमेंट्स (segments) होते हैं, इसलिए हमें सेगमेंट्स और positions के विचार को जोड़ने की आवश्यकता है। वे समान नहीं हैं, क्योंकि positions ओवरलैप (overlap) कर सकती हैं। आइए इसे एक उदाहरण के माध्यम से समझते हैं।
केवल दो positions पर विचार करें:
- Ticks -10 और 5 के बीच 200 की liquidity के साथ, जिसे नीचे लाल बॉक्स में दिखाया गया है।
- Ticks 0 और 10 के बीच 100 की liquidity के साथ, जिसे नीचे नीले बॉक्स में दर्शाया गया है।

- Ticks -10 और 0 के बीच, 200 की liquidity के साथ केवल एक position है, इसलिए इस सेगमेंट के लिए liquidity 200 होगी।
- Ticks 0 और 5 के बीच, दो positions ओवरलैप करती हैं: एक 200 की liquidity के साथ और दूसरी 100 की liquidity के साथ, इसलिए इस सेगमेंट के लिए liquidity 300 होगी।
- Ticks 5 और 15 के बीच, 100 की liquidity के साथ केवल एक position है, इसलिए इस सेगमेंट के लिए liquidity 100 होगी।
सेगमेंट्स को ऊपर दी गई आकृति में दाईं ओर दिखाया गया है।
नीचे एक इंटरैक्टिव टूल है जहाँ पाठक कई positions बना सकता है, और यह टूल इन positions के आधार पर सेगमेंट्स की ऑटोमैटिकली गणना (calculate) करता है। current price को बदलना और उस सेगमेंट की liquidity को देखना भी संभव है जिसमें current price स्थित है।
हालाँकि, प्रोटोकॉल positions के आधार पर सभी सेगमेंट्स की गणना नहीं करता है, जैसा कि हमने अभी किया है। यह अत्यधिक अकुशल (inefficient) होगा, क्योंकि प्रोटोकॉल को ऐसी ग्लोबल पिक्चर (global picture) की आवश्यकता नहीं होती है।
बाद के अध्याय में, हम चर्चा करेंगे कि प्रोटोकॉल सेगमेंट्स की गणना करने के लिए positions को कुशलतापूर्वक कैसे हैंडल करता है। अभी के लिए, हम अस्थायी रूप से उस गणना को एक ब्लैक बॉक्स (black box) के रूप में मानते हैं।
अगले भाग में, हम mint function पर करीब से नज़र डालेंगे और यह भी देखेंगे कि एक position खोलने के लिए LP को क्या जमा करना होगा।
mint function
एक pool में liquidity जोड़ने के लिए, टोकन जमा करना आवश्यक है। जैसा कि हमने देखा है, यह mint function के माध्यम से किया जाता है, जिसका इंटरफ़ेस नीचे फिर से दिखाया गया है।
function mint(
address recipient, // the owner of the position
int24 tickLower,
int24 tickUpper,
uint128 amount, // amount in liquidity
bytes calldata data // will be explained in a future chapter, it is not necessary for our discussion
) external override lock returns (uint256 amount0, uint256 amount1) {
यह function पाँच पैरामीटर्स (parameters) की अपेक्षा करता है:
- Position के owner का एड्रेस (
recipient), lower tick (tickLower), और position का upper tick (tickUpper)। dataपैरामीटर, जोbytesप्रकार का है, उसे बाद में समझाया जाएगा और यह वर्तमान चर्चा के लिए महत्वपूर्ण नहीं है।amountपैरामीटर, जो liquidity की वह मात्रा (amount) है जिसे LP lower और upper tick के बीच pool में जोड़ना चाहता है।
ध्यान दें कि amount uint128 प्रकार का है, जिसका अर्थ है कि यह ऋणात्मक (negative) नहीं हो सकता। mint function का उपयोग केवल liquidity जोड़ने के लिए किया जाता है, इसे हटाने के लिए नहीं—हटाने (removals) का काम burn function द्वारा हैंडल किया जाता है, जिस पर बाद में चर्चा की जाएगी।
lower और upper ticks के बीच liquidity की यह amount जोड़ने के लिए जमा किए जाने वाले टोकन्स की मात्रा की गणना फिर mint function द्वारा की जानी चाहिए।
यह वही है जो हम आगे देखेंगे।
एक position खोलने के लिए आवश्यक टोकन
एक lower और upper tick के बीच liquidity के अनुरूप टोकन की मात्रा उस position के अनुरूप सेगमेंट के real reserves होती है, यानी, lower और upper tick के बीच liquidity वाला एक सेगमेंट।
हम पहले ही सीख चुके हैं कि किसी सेगमेंट के real reserves न केवल tick सीमाओं (boundaries) और liquidity पर निर्भर करते हैं, बल्कि current price पर भी निर्भर करते हैं। नियम इस प्रकार है:
- जब current price upper tick पर या उससे ऊपर होता है, तो सेगमेंट के पास real reserves केवल tokens Y में होते हैं।
- जब current price lower tick पर या उससे नीचे होता है, तो सेगमेंट के पास real reserves केवल tokens X में होते हैं।
- जब current price lower और upper ticks के बीच होता है, तो सेगमेंट के पास real reserves tokens X और Y दोनों में होते हैं।
इन तीन परिदृश्यों (scenarios) को नीचे दिए गए चित्र में दर्शाया गया है, जहाँ लाल किरण current price को दर्शाती है, lower tick को दर्शाता है और upper tick को दर्शाता है।

यदि LP lower tick और upper tick के बीच liquidity जोड़ना चाहता है, तो प्रोटोकॉल ऊपर वर्णित तीन परिदृश्यों के अनुसार (tokens X में real reserves) और (tokens Y में real reserves) की गणना करता है।
- परिदृश्य 1 के लिए, tokens Y में real reserves हैं
- परिदृश्य 2 के लिए, tokens X में real reserves हैं
- परिदृश्य 3 के लिए, tokens X और Y में real reserves हैं
Position में liquidity जोड़ने के लिए आवश्यक इन टोकन मात्राओं की गणना mint function द्वारा की जाती है और वापस (return) की जाती है, जैसा कि नीचे लाल बॉक्स में दिखाया गया है।

हालाँकि, एंड यूज़र (end user) के लिए, liquidity एक अत्यधिक अमूर्त (abstract) कॉन्सेप्ट हो सकता है। एंड यूज़र के लिए liquidity के बजाय टोकन के संदर्भ में सोचना बहुत आम बात है—उदाहरण के लिए, कोई यूज़र 100 tokens X जमा करके liquidity प्रदान करना चाह सकता है, बिना इस बात का अंदाज़ा लगाए कि 100 tokens X कितनी liquidity का प्रतिनिधित्व करते हैं।
टोकन मात्राओं को चुनकर liquidity जोड़ने की सुविधा एक intermediary contract के माध्यम से प्रदान की जा सकती है जो एंड यूज़र और कोर कॉन्ट्रैक्ट के बीच एक ब्रिज (bridge) के रूप में कार्य करता है और टोकन मात्राओं और liquidity के बीच रूपांतरण (conversion) की गणना करता है।
Position Manager के माध्यम से एक position खोलना
कोर कॉन्ट्रैक्ट में mint function का उद्देश्य किसी अन्य कॉन्ट्रैक्ट द्वारा कॉल किया जाना है, न कि EOAs द्वारा।
जब mint function को कॉल किया जाता है, तो यह उस एड्रेस पर कॉलबैक (call back) करता है जिसने इसे uniswapV3MintCallback function के माध्यम से ट्रिगर किया था, जैसा कि हम नीचे लाल बॉक्स में हाइलाइट किए गए mint function के कोड स्निपेट में देख सकते हैं।

जो एड्रेस mint function को कॉल करता है, उसे uniswapV3MintCallback function लागू (implement) करना चाहिए, क्योंकि इसी समय कॉलर (caller) को position को modify करने के लिए आवश्यक टोकन मात्राओं को ट्रांसफर करना होता है। mint function uniswapV3MintCallback को कॉल करने से ठीक पहले और बाद में pool के बैलेंस की जाँच करता है, और अंतर का हिसाब रखता है।
यही कारण है कि mint function को किसी EOA द्वारा कॉल नहीं किया जा सकता: EOAs आवश्यक टोकन मात्राओं को ट्रांसफर करने के लिए कॉलबैक का जवाब नहीं दे सकते। इस प्रकार, यदि LP को किसी भी टोकन को ट्रांसफर करने की आवश्यकता होती है, तो mint function को किया गया एक EOA कॉल रिवर्ट (revert) हो जाएगा, जो कि हमेशा होता है (शून्य liquidity जोड़कर किसी position को modify करने की अनुमति नहीं है)।
ट्रांज़ेक्शन फ़्लो (transaction flow) को नीचे दर्शाया गया है, यह मानते हुए कि जो कॉन्ट्रैक्ट mint function को कॉल करता है उसका नाम Position Manager है।

कोर कॉन्ट्रैक्ट यूज़र-फ़्रेंडली (user-friendly) नहीं है। सामान्य तौर पर, LP liquidity निर्दिष्ट (specify) करने के बजाय tokens X और/या tokens Y की उस मात्रा को चुनना चाहता है जिसे वह position खोलने के लिए जमा करना चाहता है। Intermediary contract की भूमिका एक यूज़र-फ़्रेंडली इंटरफ़ेस प्रदान करना है, जहाँ यूज़र एक टोकन मात्रा चुनता है और Position Manager इसे उस मात्रा के लिए संबंधित liquidity में परिवर्तित करता है।
इस प्रक्रिया का एक प्रतिनिधित्व नीचे दिखाया गया है। एंड यूज़र (एक EOA) intermediary contract (Position Manager) में mint function को कॉल करता है, और मापदंडों (parameters) में से एक के रूप में उन टोकन की मात्रा को पास करता है जिसे वे liquidity में बदलना चाहते हैं। फिर position manager टोकन की इस मात्रा को liquidity में परिवर्तित करता है और कोर कॉन्ट्रैक्ट में mint function को कॉल करके एंड यूज़र के लिए एक position खोलता है।

हम इस किताब में Position Manager कैसे काम करता है, इसके विवरण में नहीं जाएँगे। Uniswap अपनी periphery library में Position Manager के रूप में काम करने वाला एक कॉन्ट्रैक्ट प्रदान करता है, जिसका नाम NonfungiblePositionManager है, जो core library से अलग रिपॉज़िटरी में स्थित है।
सारांश
- Uniswap v3 में liquidity जोड़ने का मतलब है किसी position को खोलना (open) या संशोधित (modify) करना। Positions को उनके owner के एड्रेस और उनके lower और upper ticks द्वारा परिभाषित किया जाता है।
- Positions को
positionsनामक एक मैपिंग में स्टोर किया जाता है, जिसकी key owner एड्रेस और lower और upper ticks का Keccak हैश होती है, और जिसकी वैल्यू एक struct होती है जोPositionलाइब्रेरी में स्थित होती है। - किसी position को खोलने या उसमें liquidity जोड़ने के लिए,
mintfunction का उपयोग किया जाता है। यह function यूज़र-फ़्रेंडली नहीं है और एक argument के रूप में, उस liquidity की मात्रा प्राप्त करता है जिसे यूज़र lower और upper ticks के बीच जमा करना चाहता है। mintfunction को अन्य कॉन्ट्रैक्ट्स द्वारा कॉल किया जाना चाहिए, न कि EOAs द्वारा। ये intermediary contracts EOAs और कोर कॉन्ट्रैक्ट के बीच कार्य करते हैं, और उनके कार्यों में से एक LP द्वारा परिभाषित टोकन मात्रा को कोर कॉन्ट्रैक्ट केmintfunction द्वारा अपेक्षित (expected) liquidity मात्रा में परिवर्तित करना हो सकता है।- एक lower और एक upper tick के बीच liquidity जमा करने के लिए, इन ticks के बीच liquidity वाले एक सेगमेंट के real reserves के अनुरूप टोकन की मात्रा जमा करना आवश्यक है।