एक fixed-point नंबर एक ऐसा integer है जो किसी भिन्न (fraction) के केवल अंश (numerator) को स्टोर करता है — जबकि हर (denominator) अंतर्निहित (implied) होता है।
अधिकांश प्रोग्रामिंग भाषाओं में इस प्रकार के arithmetic की आवश्यकता नहीं होती है क्योंकि उनमें floating point नंबर्स होते हैं। Solidity में इसकी आवश्यकता होती है क्योंकि Solidity में केवल integers होते हैं, और हमें अक्सर fractional नंबर्स के साथ ऑपरेशन्स करने की आवश्यकता होती है।
Fixed point नंबर्स अधिकांश DeFi स्मार्ट कॉन्ट्रैक्ट्स में पाए जाते हैं, इसलिए इन्हें समझना बहुत ज़रूरी है।
उदाहरण के लिए, यदि “implied denominator” 100 है, तो “10” को होल्ड करने वाले एक fixed-point नंबर को 0.1 के रूप में इंटरप्रेट किया जाता है।
Solidity में सबसे आम fixed point नंबर है: “decimals” की वह संख्या जो Ethereum और अधिकांश ERC-20 टोकन्स में होती है। जब हम किसी Ethereum एड्रेस का बैलेंस पढ़ते हैं, तो हम उनके Ether अमाउंट को निर्धारित करने के लिए नंबर को से implicitly डिवाइड करते हैं। उदाहरण के लिए, एक एड्रेस जिसका बैलेंस है, उसे 10 Ether के रूप में इंटरप्रेट किया जाता है — क्योंकि से डिवीज़न (विभाजन) implied है।
के denominator वाला fixed point नंबर इतना आम है कि Solidity कम्युनिटी में इंजीनियर्स इसे “Wad” कहते हैं (यह नाम पहली बार MakerDAO द्वारा पेश किया गया था)। कभी-कभी, एक 18-डिजिट fixed-point नंबर को इस तरह इंटरप्रेट किया जाता है कि इसके सबसे दाईं ओर के 18 डिजिट्स decimals के लिए आवंटित होते हैं, उदाहरण के लिए, नंबर “10” नीचे दिखाया गया है:
हालाँकि, हमने पाया है कि यह मेंटल मॉडल fixed-point arithmetic को समझना कठिन बना देता है, इसलिए यह लेख इस मेंटल मॉडल का उपयोग करेगा जहाँ fixed-point नंबर numerator को होल्ड करता है, और denominator implied होता है।
इस लेख में हम सीखेंगे कि fixed point नंबर्स के साथ arithmetic कैसे करें और यह समझाएंगे कि लोकप्रिय fixed point लाइब्रेरियाँ कैसे काम करती हैं।
एक integer को fixed point नंबर में कंवर्ट करना
एक integer को fixed point नंबर में कंवर्ट करने के लिए, integer को implied denominator से गुणा (multiply) करें। उदाहरण के लिए, “2 ether” होता है, इसलिए integer 2 को “2 ether” में कंवर्ट करने के लिए हम इसे से गुणा करते हैं। का implied denominator को कैंसिल कर देता है।
Fixed Point नंबर्स को गुणा करना (Multiplying)
दो fixed-point नंबर्स को एक साथ गुणा करने के लिए, हम fractions को गुणा करने के नियमों का पालन करते हैं:
- numerators को एक साथ गुणा करें
- denominators को एक साथ गुणा करें
- परिणाम को सरल (simplify) करें।
उदाहरण के लिए:
हालाँकि, व्यवहार में हम इस कैलकुलेशन को ऑप्टिमाइज़ कर सकते हैं क्योंकि fixed point नंबर्स के साथ denominator हमेशा समान होता है।
अब चलिए उन fractions के एक अलग सेट पर विचार करते हैं जिनका denominator कॉमन है:
हालाँकि, हम के implied denominator के साथ परिणाम वापस (return) नहीं करना चाहते हैं क्योंकि यह हमारे द्वारा चुने गए implied denominator के साथ कम्पैटिबल नहीं होगा। इसलिए, हमें अपनी पसंद के denominator के अनुरूप एक fixed point नंबर वापस करने के लिए numerator और denominator को से भाग (divide) देना होगा।
इस प्रकार, यदि और ऐसे fixed point नंबर्स हैं जिनका implied denominator है, तो हम उनके प्रोडक्ट को के रूप में कंप्यूट कर सकते हैं।
Fixed points को गुणा करने का कोड उदाहरण
Solady लाइब्रेरी में दो fixed point नंबर्स को एक implied Wad denominator () के साथ गुणा करने के लिए एक mulWad मैथ ऑपरेशन होता है। नीचे, हम कोड दिखाते हैं और फिर समझाते हैं कि यह हमारी पिछली चर्चा से कैसे संबंधित है:

कोर एल्गोरिदम स्क्रीनशॉट के निचले भाग में (हरे बॉक्स के अंदर) है। हम वहाँ कंप्यूट करते हैं जहाँ , WAD या है (जैसा कि स्क्रीनशॉट के शीर्ष पर दिखाया गया है जहाँ WAD डिक्लेयर किया गया है)।
रियल-वर्ल्ड उदाहरण
मान लीजिए कि एक यूज़र के पास 1 DAI (जिसमें 18 decimals होते हैं) है और हम उनके बैलेंस को यह मानते हुए कंप्यूट करना चाहते हैं कि उनके डिपॉज़िट पर 15% ब्याज (interest) मिला है। यह fixed-point arithmetic की आवश्यकता का एक स्पष्ट उदाहरण है, क्योंकि हम Solidity में किसी नंबर को सीधे 1.15 से गुणा नहीं कर सकते हैं।

1e18 से भाग देने के बाद आउटपुट 1.15 आता है। बेशक, हम वास्तव में 1e18 से भाग नहीं दे सकते क्योंकि इससे decimals मिट जाएँगे। हमें एक fixed-point रिप्रेज़ेंटेशन की आवश्यकता होती है क्योंकि 1.15 को एक integer के रूप में रिप्रेज़ेंट नहीं किया जा सकता है। उपरोक्त कोड को यहाँ Remix पर टेस्ट किया जा सकता है।
एक Fixed-Point नंबर को Integer से गुणा करना
एक fraction को एक integer से गुणा करना, को से गुणा करने के समान है:
इस प्रकार, जब हम एक fixed-point नंबर को एक integer से गुणा करते हैं, तो हमें किसी अतिरिक्त कदम की आवश्यकता नहीं होती है। हम बस रिटर्न वैल्यू को बिना बदले हुए denominator के साथ एक fixed-point नंबर के रूप में इंटरप्रेट करते हैं।
Fixed Point नंबर्स को डिवाइड करना
Fractions को डिवाइड करने के लिए, हम दूसरे fraction को “पलट” (flip) देते हैं और उन्हें एक साथ गुणा करते हैं। उदाहरण के लिए:
अब आइए एक ऐसे उदाहरण पर विचार करें जहाँ उनका denominator समान है:
ध्यान दें कि 10 का कॉमन denominator कैंसिल हो गया। यदि हम 2 को 10 के implied denominator के साथ रिप्रेज़ेंट करना चाहते हैं (यानी 10 denominator वाले fixed point नंबर के रूप में), तो हमें इसे फिर से 10 से गुणा करना होगा:
इस प्रकार, एक कॉमन denominator वाले सामान्य और के लिए, यदि हम आउटपुट को के implied denominator के साथ एक्सप्रेस करना चाहते हैं, तो हमें निम्नलिखित करना होगा:
इस प्रकार, यदि और ऐसे fixed point नंबर्स हैं जिनका implied denominator है, तो हम उनके quotient (भागफल) को के रूप में कंप्यूट कर सकते हैं।
/// @dev Equivalent to `(x * WAD) / y` rounded down.
function divWad(uint256 x, uint256 y) internal pure returns (uint256 z) {
/// @solidity memory-safe-assembly
assembly {
// Equivalent to `require((y != 0 && (WAD == 0 || x <= type(uint256).max / WAD))`.
if iszero(mul(y, iszero(mul(WAD, gt(x, div(not(0), WAD)))))) {
mstore(0x00, 0x7c5f487d) // `DivWadFailed()`.
revert(0x1c, 0x04)
}
z := div(mul(x, WAD), y)
}
}
यदि हम mulWad() और divWad() को अगल-बगल रखें, तो हम देख सकते हैं कि उनके बीच एकमात्र अंतर (कैलकुलेशन स्टेप पर, न कि ओवरफ्लो चेक पर) यह है कि div के मामले में एक उल्टे fraction (inverted fraction) से गुणा किया जा रहा है।

एक Fixed Point नंबर को Integer से डिवाइड करना
मान लीजिए कि हम 2.5 को 2 से (या सामान्यतः किसी fraction को एक integer से) डिवाइड करना चाहते हैं। 2 को के माध्यम से एक fixed-point नंबर में कंवर्ट करना आवश्यक नहीं है।
एक fraction को एक integer से डिवाइड करना, के numerator को से डिवाइड करने के समान है।
ध्यान दें कि होता है, 11.666 नहीं, क्योंकि हम integer division का उपयोग कर रहे हैं, न कि floating points का। हम बस fixed-point नंबर को integer से डिवाइड करते हैं और परिणाम को एक fixed-point नंबर के रूप में इंटरप्रेट करते हैं। एक fixed-point नंबर को integer से गुणा करने की तरह ही, यहाँ भी denominator समान रहता है।
Fixed Point नंबर्स को जोड़ना (Adding) और घटाना (Subtracting)
समान denominator वाले fractions को जोड़ने और घटाने के लिए उन्हें बस एक साथ जोड़ दिया जाता है और denominator को इग्नोर कर दिया जाता है। हम योग (sum) को उसी implied denominator वाले एक fixed-point नंबर के रूप में इंटरप्रेट करते हैं जो जोड़े जाने वाले नंबर्स (summands) का था। उदाहरण के लिए,
इसलिए, समान denominator वाले fixed point नंबर्स को जोड़ते समय, हम बस नंबर्स को एक साथ वैसे ही जोड़ते हैं जैसे हम रेगुलर integers के साथ करते हैं।
100 के implied denominator वाले इस उदाहरण पर विचार करें:
इसे कंप्यूट करने के लिए, हम बस करते हैं, हमें अपने कैलकुलेशन में 100 को शामिल करने की आवश्यकता नहीं है।
Binary बनाम Decimal Fixed Point नंबर
एक binary fixed point नंबर वह fixed point नंबर होता है जहाँ denominator को के रूप में एक्सप्रेस किया जा सकता है। Binary fixed point नंबर्स को आमतौर पर Q नोटेशन (Q notation) के साथ दर्शाया जाता है। उदाहरण के लिए, UQ112x112 denominator के रूप में का उपयोग करता है। U का अर्थ “unsigned” है। UQ112x112 को होल्ड करने के लिए उपयोग किया जाने वाला डेटाटाइप 224 होगा। इसे इंटरप्रेट करने का एक और तरीका यह है कि “दशमलव के बाद वाले fractional भाग” को सबसे दाईं ओर के 112 बिट्स में होल्ड किया जाता है और “होल नंबर वाले भाग” को बाईं ओर के 112 बिट्स में होल्ड किया जाता है।
एक अन्य उदाहरण के रूप में, UQ64x64 (या UQ64.64) एक uint128 है जो “fractional भाग” को least significant 64 बिट्स में और “होल नंबर” को most significant बिट्स में होल्ड करता है। इसे अभी भी के implied denominator के रूप में इंटरप्रेट किया जा सकता है, जैसा कि हम आगे देखेंगे।
Binary fixed point नंबर का फायदा यह है कि हम denominator से गुणा करने (किसी integer को fixed point नंबर में कंवर्ट करते समय), या डिवाइड करते समय right bit shift करने के बजाय गैस-एफिशिएंट (gas-efficient) left bit shift का उपयोग कर सकते हैं।
एक बुनियादी उदाहरण के रूप में, इस पर विचार करें:
(1) 2 का बाइनरी (binary) रिप्रेज़ेंटेशन 10 होता है
(2) 16 का बाइनरी रिप्रेज़ेंटेशन 10000 होता है
(3)
(4) binary(100) = binary(10) << 3
ध्यान दें कि 3, (3) में exponent है और वह अमाउंट है जिससे हम (4) में बिट्स को left shift करते हैं।
e अमाउंट से बिटशिफ्ट (bitshift) करने और से गुणा करने के बीच का यह संबंध सामान्यतः लागू होता है। निम्नलिखित ऑपरेशन्स समान (equivalent) हैं:
// x * 2¹¹² equals x left bitshifted by 112 bits
x * 2 ** 112 == x << 112
// x / 2¹¹² equals x right bitshifted by 112 bits
x / 2 ** 112 == x >> 112
x कोई भी आर्बिट्रेरी (arbitrary) नंबर हो सकता है जब तक कि यह unsigned integer में फिट बैठता है।
ABDK लाइब्रेरी निम्नलिखित फंक्शन का उपयोग करके unsigned integers को fixed point नंबर्स (एक implied denominator के साथ) में कंवर्ट करती है:

require स्टेटमेंट यह सुनिश्चित करता है कि x, type(int64).max से कम है, क्योंकि ABDK लाइब्रेरी signed fixed point नंबर्स का उपयोग करती है। 64 से left shift करना से गुणा करने के बराबर है।
इसी तरह, जब ABDK गुणा करता है, तो x और y के गुणनफल को से भाग देने के बजाय, यह 64 बिट्स से एक right shift करता है:

Uniswap V2 Fixed Point लाइब्रेरी
Uniswap V2 की fixed point लाइब्रेरी काफी सरल है क्योंकि Uniswap V2 fixed point नंबर्स के साथ जो एकमात्र ऑपरेशन्स करता है, वे हैं addition (जोड़) और एक fixed point नंबर को एक integer से डिवाइड करना।
pragma solidity =0.5.16;
// A library for handling binary fixed point numbers (https://en.wikipedia.org/wiki/Q_(number_format))
// range: [0, 2**112 - 1]
// resolution: 1 / 2**112
library UQ112x112 {
uint224 constant Q112 = 2**112;
// encode a uint112 as a UQ112x112
function encode(uint112 y) internal pure returns (uint224 z) {
z = uint224(y) * Q112; // never overflows
}
// divide a UQ112x112 by a uint112, returning a UQ112x112
function uqdiv(uint224 x, uint112 y) internal pure returns (uint224 z) {
z = x / uint224(y);
}
}
encode() फंक्शन एक uint112 को uint224 में स्टोर किए गए fixed point नंबर में कंवर्ट करता है। Uniswap V2 2**112 के implied denominator का उपयोग करता है। यदि यह गुणा करने के बजाय bitshift का उपयोग करता तो यह अधिक गैस-एफिशिएंट हो सकता था (संभवतः यह Uniswap डेवलपर्स की एक गलती है)।
Fixed point नंबर एक uint224 में स्टोर किया जाता है जो उस uint112 के आकार का दोगुना है जिसके साथ यह इंटरैक्ट करेगा। Encode ऑपरेशन के दौरान, uint112 नंबर के बिट्स प्रभावी रूप से uint224 के most significant 112 बिट्स में शिफ्ट हो जाते हैं।
छोटे uint साइज़ के साथ इस “encode” ऑपरेशन को विज़ुअलाइज़ करना आसान है। आइए denominator वाले एक काल्पनिक fixed-point नंबर का उपयोग करें। नीचे, हम दिखाते हैं कि क्या होता है जब हम किसी uint8 को denominator वाले fixed point नंबर में encode करते हैं:
नंबर 125 से शुरू करते हुए जिसका बाइनरी रिप्रेज़ेंटेशन 01111101 है, यदि हम इसे से गुणा करते हैं, तो गुणनफल 32000 होता है, जिसे 16 बिट uint में स्टोर करने पर 0111110100000000 के रूप में दर्शाया जाता है। ध्यान दें कि 125 को से गुणा करने का प्रभाव वही होता है जो 8 बिट्स से left shift करने का होता है।
uqdiv() फंक्शन बस एक fixed point नंबर को एक integer से डिवाइड करता है, जिसके लिए किसी अतिरिक्त कदम की आवश्यकता नहीं होती है।
Uniswap नीचे दिए गए TWAP ओरेकल (oracle) के लिए कीमतों को एक्युमुलेट (accumulate) करने हेतु इस लाइब्रेरी का उपयोग करता है। हर बार जब कोई अपडेट होता है तो TWAP नवीनतम कीमत को एक एक्युमुलेटर (accumulator) में जोड़ता है (जिसका उपयोग अतिरिक्त स्टेप्स के साथ औसत कीमत की गणना करने के लिए किया जाता है, जो कि इस लेख के दायरे से बाहर है)। चूँकि कीमतों को fractions के रूप में दर्शाया जाता है, इसलिए fixed point नंबर्स उन्हें दर्शाने का एक आदर्श तरीका है।
वेरिएबल्स _reserve0 और _reserve1 पूल के सबसे हालिया टोकन बैलेंस को होल्ड करते हैं और ये uint112 हैं। price0CumulativeLast और price1CumulativeLast UQ112x112 हैं ( के implied denominator वाले fixed point नंबर्स)। नीचे दिया गया Uniswap V2 का कोड numerator को एक fixed point नंबर (UQ112x112) में कंवर्ट करता है और इसे एक integer से डिवाइड करता है (denominator को UQ112x112 में कंवर्ट नहीं किया जाता है)। परिणाम एक fixed point नंबर होता है।

Rounding up बनाम Rounding down (राउंडिंग अप बनाम राउंडिंग डाउन)
Fixed-point लाइब्रेरियों में आमतौर पर डिवाइड करते समय round up करने का विकल्प होता है। उदाहरण के लिए, Solady में है:
mulWadUp— दो fixed point नंबर्स को गुणा करें, लेकिन d से डिवाइड करते समय, round up करें। याद करें, दो fixed point नंबर्स को गुणा करने का सूत्र है।mulDivUp— दो fixed point नंबर्स को डिवाइड करें, लेकिन डिवाइड करते समय round up करें
Solidity डिवीज़न हमेशा round down करता है, उदाहरण के लिए । हालाँकि, अगर हम round up करते हैं, तो , 4 के बराबर होगा। क्रेडिट या कीमतों की गणना करते समय, हमेशा प्रोटोकॉल के पक्ष में और यूज़र के खिलाफ round करना चाहिए। उदाहरण के लिए, यदि हम यह कंप्यूट कर रहे हैं कि किसी यूज़र को किसी अन्य एसेट की निश्चित मात्रा के लिए कितना भुगतान करना चाहिए, तो हमें कीमत को round up करना चाहिए।
उदाहरण के लिए:
- rounding down 3.3333 होता है
- rounding up 3.3334 होता है (यह हमारे denominator के साइज़ पर निर्भर करता है)
Rounding up का सीधा सा अर्थ है परिणाम में 1 जोड़ना यदि remainder (शेषफल) नॉन-ज़ीरो है। उदाहरण के लिए, सटीक रूप से होता है, इसलिए हमें 4 रिटर्न नहीं करना चाहिए। हालाँकि, और का remainder क्रमशः 1 और 2 होता है, इसलिए हमें डिवीज़न के परिणाम में 1 जोड़ना चाहिए।
यहाँ बताया गया है कि Solmate लाइब्रेरी इसे कैसे करती है:

हरी अंडरलाइन में, कोड यह जांचता है कि क्या modulo (मोडुलो) शून्य से अधिक है। यदि है, तो परिणाम में 1 जोड़ा जाता है (rounding up), अन्यथा 0 जोड़ा जाता है (कोई round up नहीं)।
मूल रूप से 10 जून को प्रकाशित