Uniswap में “price” (कीमत) वास्तव में क्या है?
मान लीजिए कि हमारे पास एक पूल में 1 Ether और 2,000 USDC हैं। इसका मतलब है कि Ether की कीमत 2,000 USDC है। विशेष रूप से, Ether की कीमत 2,000 USDC / 1 Ether है (decimals को अनदेखा करते हुए)।
अधिक सामान्य तौर पर, पेयर (pair) में दूसरी एसेट की कीमत के संदर्भ में किसी एसेट की कीमत एक अनुपात (ratio) होती है, जहां “जिस एसेट की आप परवाह करते हैं” वह डिनॉमिनेटर (denominator) में होती है।
ऊपर दिए गए उदाहरण में, यह कह रहा है कि “एक foo प्राप्त करने के लिए आपको कितने bars का भुगतान करना होगा” (fees को अनदेखा करते हुए)।
Price एक अनुपात है
चूंकि कीमत एक अनुपात है, इसलिए उन्हें एक ऐसे डेटा टाइप के साथ स्टोर करने की आवश्यकता होती है जिसमें दशमलव बिंदु (decimal points) हों (जो कि डिफ़ॉल्ट रूप से Solidity टाइप्स में नहीं होते हैं)।
यानी, हम कहते हैं कि Ethereum 2000 है और USDC (Ethereum की कीमत में) 0.0005 है (यह दोनों एसेट्स के decimals को अनदेखा कर रहा है)।
Uniswap दशमलव के दोनों ओर 112 बिट्स की सटीकता (precision) के साथ एक fixed point number का उपयोग करता है, यह कुल 224 बिट्स लेता है, और जब इसे 32 बिट नंबर के साथ पैक किया जाता है, तो यह एक सिंगल स्लॉट का उपयोग करता है।
Oracle की परिभाषा
कंप्यूटर विज्ञान के संदर्भ में एक oracle “सत्य का स्रोत” (source of truth) है। एक price oracle कीमतों का स्रोत होता है। जब दो एसेट्स को होल्ड किया जाता है तो Uniswap की एक निहित कीमत (implied price) होती है, और अन्य स्मार्ट कॉन्ट्रैक्ट्स इसे price oracle के रूप में उपयोग कर सकते हैं।
Oracle के इच्छित उपयोगकर्ता अन्य स्मार्ट कॉन्ट्रैक्ट्स हैं, क्योंकि कीमत निर्धारित करने के लिए अन्य स्मार्ट कॉन्ट्रैक्ट्स आसानी से Uniswap के साथ संचार कर सकते हैं, लेकिन किसी ऑफ-चेन एक्सचेंज से कीमत का डेटा प्राप्त करना बहुत कठिन होगा।
हालाँकि, वर्तमान कीमत प्राप्त करने के लिए केवल बैलेंस का अनुपात लेना सुरक्षित नहीं है।
TWAP के पीछे की प्रेरणा
पूल में एसेट्स के तात्कालिक स्नैपशॉट (instantaneous snapshot) को मापने से flash loan हमलों का अवसर मिलता है। यानी, कोई व्यक्ति फ्लैश लोन का उपयोग करके एक बहुत बड़ा ट्रेड कर सकता है जिससे कीमत में अस्थायी रूप से भारी बदलाव आ सकता है, और फिर किसी अन्य स्मार्ट कॉन्ट्रैक्ट का लाभ उठा सकता है जो निर्णय लेने के लिए इस कीमत का उपयोग करता है।
Uniswap V2 oracle दो तरीकों से इसका बचाव करता है:
- यह कीमत के उपभोक्ताओं (आमतौर पर स्मार्ट कॉन्ट्रैक्ट्स) के लिए पिछले समय की अवधि (उपयोगकर्ता द्वारा तय की गई) का औसत लेने के लिए एक तंत्र प्रदान करता है। इसका मतलब यह है कि हमलावर को कई ब्लॉक्स तक लगातार कीमत में हेरफेर करना पड़ता है, जो कि फ्लैश लोन का उपयोग करने की तुलना में बहुत अधिक महंगा है।
- यह वर्तमान बैलेंस को oracle की गणना में शामिल नहीं करता है।
इससे यह आभास नहीं होना चाहिए कि मूविंग एवरेज का उपयोग करने वाले oracles कीमत में हेरफेर के हमलों से सुरक्षित हैं। यदि एसेट में पर्याप्त लिक्विडिटी नहीं है, या औसत लेने की समय अवधि (time window) पर्याप्त रूप से बड़ी नहीं है, तो एक अच्छी तरह से संसाधन संपन्न हमलावर अभी भी माप के समय औसत कीमत में हेरफेर करने के लिए कीमत को पर्याप्त समय तक बढ़ा (या गिरा) सकता है।
TWAP कैसे काम करता है
एक TWAP (Time Weighted Average Price) एक साधारण मूविंग एवरेज की तरह ही होता है, सिवाय इसके कि जिस समय कीमत अधिक समय तक “समान रहती” है, उसे अधिक वेट (weight) मिलता है — एक TWAP कीमत को इस आधार पर वेटेज देता है कि कीमत कितने समय तक एक निश्चित स्तर पर रहती है।
- पिछले दिन, पहले 12 घंटों के लिए किसी एसेट की कीमत $10 थी और दूसरे 12 घंटों के लिए $11 थी। औसत कीमत time weighted average price के समान ही है: $10.5।
- पिछले दिन, पहले 23 घंटों के लिए एसेट की कीमत $10 थी और सबसे हाल के एक घंटे के लिए $11 थी। अपेक्षित औसत कीमत $11 की तुलना में $10 के करीब होनी चाहिए, लेकिन यह अभी भी उन मूल्यों के बीच होगी। विशेष रूप से, यह ($10 * 23 + $11 * 1) / 24 = $10.0417 होगी।
- पिछले दिन, पहले घंटे के लिए एसेट की कीमत $10 थी, और सबसे हाल के 23 घंटों के लिए $11 थी। हम उम्मीद करते हैं कि TWAP 10 की तुलना में $11 के करीब होगा। विशेष रूप से, यह ($10 * 1 + $11 * 23) / 24 = $10.9583 होगा।
सामान्य तौर पर, TWAP का फॉर्मूला है:
यहाँ T एक अवधि (duration) है, कोई टाइमस्टैम्प नहीं। यानी, कीमत उस स्तर पर कितने समय तक बनी रही।
Uniswap V2 लुकबैक (lookback) या डिनॉमिनेटर (denominator) को स्टोर नहीं करता है
ऊपर दिए गए हमारे उदाहरण में, हमने केवल पिछले 24 घंटों की कीमतों को देखा, लेकिन क्या होगा यदि आप पिछले एक घंटे, सप्ताह, या किसी अन्य अंतराल (interval) की कीमतों की परवाह करते हैं? Uniswap निश्चित रूप से हर उस लुकबैक को स्टोर नहीं कर सकता जिसमें किसी की रुचि हो सकती है, और कीमत का लगातार स्नैपशॉट लेने का कोई अच्छा तरीका भी नहीं है क्योंकि किसी को इसके लिए गैस (gas) का भुगतान करना होगा।
इसका समाधान यह है कि Uniswap केवल वैल्यूज के न्यूमरेटर (numerator) को स्टोर करता है — हर बार जब लिक्विडिटी अनुपात में कोई बदलाव होता है (mint, burn, swap, या sync कॉल किए जाते हैं), तो यह नई कीमत दर्ज करता है और यह भी कि पिछली कीमत कितने समय तक रही।

वेरिएबल्स price0Cumulativelast और price1CumulativeLast पब्लिक हैं, इसलिए एक इच्छुक पार्टी को उनका स्नैपशॉट लेने की आवश्यकता होती है।
लेकिन यह एक महत्वपूर्ण बिंदु है जिसे आपको हमेशा याद रखना चाहिए कि price0CumulativeLast और price1CumulativeLast केवल ऊपर दिए गए कोड में लाइन 79 और 80 पर अपडेट किए जाते हैं (orange circle), और वे केवल तब तक बढ़ सकते हैं जब तक कि वे ओवरफ़्लो (overflow) न हो जाएँ। उन्हें “कम होने” का कोई तंत्र (mechanism) नहीं है। वे _update के हर कॉल के साथ हमेशा बढ़ते हैं। इसका मतलब है कि वे पूल के लॉन्च होने के समय से ही कीमतों को जमा (accumulate) करते हैं, जो बहुत लंबा समय हो सकता है।
लुकबैक विंडो को सीमित करना
स्पष्ट रूप से, हम आम तौर पर पूल के अस्तित्व में आने के बाद से औसत कीमत में दिलचस्पी नहीं रखते हैं। हम केवल एक निश्चित समय (1 घंटा, 1 दिन, आदि) पीछे देखना चाहते हैं।
यहाँ फिर से TWAP का फॉर्मूला दिया गया है।
यदि हम केवल T4 के बाद की कीमतों में रुचि रखते हैं, तो हम निम्नलिखित करना चाहते हैं:
हम इसे कोड के साथ कैसे पूरा करते हैं? चूँकि price0Cumulativelast रिकॉर्डिंग करता रहता है:
हमें उन हिस्सों को अलग करने के एक तरीके की आवश्यकता है जिनकी हम परवाह करते हैं। निम्नलिखित पर विचार करें:
यदि हम के अंत में कीमत का स्नैपशॉट लेते हैं, तो हमें UpToTime3 वैल्यू मिलती है। यदि हम के समाप्त होने तक प्रतीक्षा करते हैं, फिर हम price0Cumulativelast - UpToTime3then करते हैं तो हमें केवल हाल की विंडो की संचयी कीमतें (cumulative prices) मिलेंगी। यदि हम इसे RecentWindow की अवधि से विभाजित करते हैं, तो हमें हाल की विंडो की TWAP कीमत मिल जाती है।
रेखांकन (Graphically) रूप से, यह वही है जो हम प्राइस एक्युमुलेटर (price accumulator) के साथ कर रहे हैं।

Solidity में केवल पिछले 1 घंटे के TWAP की गणना करना
यदि हम 1 घंटे का TWAP चाहते हैं, तो हमें यह पूर्वानुमान (anticipate) लगाना होगा कि हमें अब से एक घंटे बाद एक्युमुलेटर के एक स्नैपशॉट की आवश्यकता होगी। इसलिए हमें अंतिम अपडेट समय प्राप्त करने के लिए पब्लिक वेरिएबल price0CumulativeLast और पब्लिक फंक्शन getReserves() को एक्सेस करने की आवश्यकता है, और उन वैल्यूज का स्नैपशॉट लेना होगा। (नीचे दिया गया snapshot() फ़ंक्शन देखें)।
कम से कम 1 घंटा बीत जाने के बाद, हम getOneHourPrice() को कॉल कर सकते हैं और हम Uniswap V2 से price0CumulativeLast के नवीनतम वैल्यू को एक्सेस करेंगे।
जब से हमने पुरानी कीमत का स्नैपशॉट लिया है, Uniswap एक्युमुलेटर को अपडेट कर रहा है:
निम्नलिखित कोड को चित्रण के उद्देश्यों (illustration purposes) के लिए यथासंभव सरल बनाया गया है, इसके प्रोडक्शन उपयोग की सलाह नहीं दी जाती है।
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol";
import "@uniswap/lib/contracts/libraries/UQ112x112.sol";
contract OneHourOracle {
using UQ112x112 for uint224;
IUniswapV2Pair public uniswapV2Pair;
uint256 public snapshotPrice0Cumulative;
uint32 public lastSnapshotTime;
constructor(address _uniswapV2Pair) {
uniswapV2Pair = IUniswapV2Pair(_uniswapV2Pair);
}
function getTimeElapsed() internal view returns (uint32 t) {
unchecked {
t = uint32(block.timestamp % 2**32) - lastSnapshotTime;
}
}
function snapshot() public {
require(getTimeElapsed() >= 1 hours, "snapshot is not stale");
// we don't use the reserves, just need the last timestamp update
(, , lastSnapshotTime) = uniswapV2Pair.getReserves();
snapshotPrice0Cumulative = uniswapV2Pair.price0CumulativeLast();
}
function getOneHourPrice() public view returns (uint224 price) {
require(getTimeElapsed() >= 1 hours, "snapshot not old enough");
require(getTimeElapsed() < 3 hours, "price is too stale");
uint256 recentPriceCumulative = uniswapV2Pair.price0CumulativeLast();
uint32 timeElapsed = getTimeElapsed();
unchecked {
price = uint224((recentPriceCumulative - snapshotPrice0Cumulative) / timeElapsed);
}
}
}
क्या होगा यदि अंतिम स्नैपशॉट तीन घंटे से अधिक पुराना हो?
चतुर पाठक ध्यान दे सकते हैं कि उपरोक्त कॉन्ट्रैक्ट स्नैपशॉट लेने में सक्षम नहीं होगा यदि वह जिस पेयर (pair) के साथ इंटरैक्ट कर रहा है, उसका पिछले तीन घंटों में कोई इंटरैक्शन नहीं हुआ है। Uniswap V2 का फ़ंक्शन _update, mint, burn, और swap के दौरान कॉल किया जाता है, लेकिन यदि इनमें से कोई भी इंटरैक्शन नहीं होता है, तो lastSnapshotTime कुछ समय पहले का समय रिकॉर्ड करेगा। इसका समाधान यह है कि जब oracle स्नैपशॉट लेता है तो उसी समय वह sync फ़ंक्शन को कॉल करे, क्योंकि वह आंतरिक रूप से _update को कॉल करेगा।
sync फ़ंक्शन का स्क्रीनशॉट नीचे दिया गया है।

TWAP को दो अनुपातों (ratios) को क्यों ट्रैक करना चाहिए
B के संबंध में A की कीमत केवल A/B है और इसके विपरीत। उदाहरण के लिए, यदि हमारे पास पूल में 2000 USDC (decimals को अनदेखा करते हुए) और 1 Ether है, तो 1 Ether की कीमत केवल 2000 USDC / 1 ETH है।
ETH में दर्शाई गई USDC की कीमत, बस न्यूमरेटर और डिनॉमिनेटर को उलट कर मिलने वाली वही संख्या है।
हालाँकि, जब हम कीमतों को जमा (accumulate) कर रहे होते हैं तो हम केवल एक कीमत को “उलट कर” दूसरी कीमत प्राप्त नहीं कर सकते। निम्नलिखित पर विचार करें। यदि हमारा प्राइस एक्युमुलेटर 2 से शुरू होता है और 3 जोड़ता है, तो हम केवल एक्युमुलेटर का वन-ओवर (one-over) नहीं कर सकते:
हालाँकि, कीमतें अभी भी “कुछ हद तक सममित (symmetric)” हैं, इसलिए फिक्स्ड पॉइंट अरिथमेटिक (fixed point arithmetic) प्रतिनिधित्व के विकल्प में पूर्णांक (integers) और दशमलव (decimals) के लिए समान क्षमता होनी चाहिए। यदि Eth एक USDC की तुलना में 1,000 गुना अधिक “मूल्यवान” है, तो USDC, ETH की तुलना में 1,000 गुना “कम मूल्यवान” है। इसे सटीक रूप से स्टोर करने के लिए, फिक्स्ड पॉइंट नंबर का आकार दशमलव के दोनों ओर समान होना चाहिए, इसलिए Uniswap ने u112x112 को चुना है।
PriceCumulativeLast हमेशा तब तक बढ़ता है जब तक कि यह ओवरफ्लो न हो जाए, फिर यह चलता रहता है
Uniswap V2 को Solidity 0.8.0 से पहले बनाया गया था, इस प्रकार अरिथमेटिक (arithmetic) डिफ़ॉल्ट रूप से ओवरफ्लो (overflow) और अंडरफ्लो (underflow) होते थे। प्राइस oracle के सही आधुनिक कार्यान्वयन (implementations) को यह सुनिश्चित करने के लिए unchecked ब्लॉक का उपयोग करने की आवश्यकता है कि सब कुछ अपेक्षानुसार ओवरफ्लो हो।
अंततः, priceAccumulators और ब्लॉक टाइमस्टैम्प ओवरफ्लो हो जाएंगे। उस स्थिति में, पिछला रिज़र्व (reserve) नए रिज़र्व से अधिक होगा। जब oracle कीमत में बदलाव की गणना करेगा, तो उन्हें एक नेगेटिव वैल्यू मिलेगा। हालाँकि, मॉड्यूलर अरिथमेटिक (modular arithmetic) के नियमों के कारण इससे कोई फर्क नहीं पड़ेगा।
चीजों को सरल बनाने के लिए आइए एक काल्पनिक अनसाइंड इंटीजर्स (unsigned integers) का उपयोग करें जो 100 पर ओवरफ्लो होते हैं।
हम 80 पर priceAccumulator का स्नैपशॉट लेते हैं और कुछ लेनदेन/ब्लॉक्स के बाद priceAccumulator 110 तक चला जाता है, लेकिन यह ओवरफ़्लो होकर 10 हो जाता है। हम 10 में से 80 घटाते हैं, जो -70 देता है। लेकिन वैल्यू को अनसाइंड इंटीजर के रूप में स्टोर किया जाता है, इसलिए यह -70 mod(100) देता है जो कि 30 है। यह वही परिणाम है जिसकी हम उम्मीद करते हैं यदि यह ओवरफ़्लो नहीं होता (110-80=30)।
यह सभी ओवरफ़्लो सीमाओं के लिए सच है, न कि केवल हमारे उदाहरण में 100 के लिए। timestamp या priceAccumulator का ओवरफ़्लो होना कोई समस्या पैदा नहीं करता है क्योंकि मॉड्यूलर अरिथमेटिक इसी तरह से काम करता है।
टाइमस्टैम्प का ओवरफ्लो होना (Overflowing the timestamp)
यही बात तब भी होती है जब हम timestamp को ओवरफ़्लो करते हैं। चूँकि हम इसे दर्शाने के लिए uint32 का उपयोग कर रहे हैं, इसलिए इसमें कोई भी नेगेटिव संख्या नहीं होगी। फिर से, सरलता के लिए मान लें कि हम 100 पर ओवरफ़्लो करते हैं। यदि हम समय 98 पर स्नैपशॉट लेते हैं और समय 4 पर प्राइस oracle से परामर्श (consult) करते हैं, तो 6 सेकंड बीत चुके हैं। 4 - 98 % 100 = 6, जैसा कि अपेक्षित है।
RareSkills के साथ और जानें
यह सामग्री हमारे उन्नत (advanced) Solidity Bootcamp का हिस्सा है। अधिक जानने के लिए कृपया प्रोग्राम देखें।
मूल रूप से 3 नवंबर, 2023 को प्रकाशित