यूनिस्वैप-v2 अनुबंध वॉक-थ्रू
परिचय
यूनिस्वैप v2 (opens in a new tab) किन्हीं भी दो ERC-20 टोकनों के बीच एक एक्सचेंज मार्केट बना सकता है। इस लेख में हम उन अनुबंधों के स्रोत कोड (source code) पर नज़र डालेंगे जो इस प्रोटोकॉल को लागू करते हैं और देखेंगे कि उन्हें इस तरह क्यों लिखा गया है।
यूनिस्वैप क्या करता है?
मूल रूप से, दो प्रकार के उपयोगकर्ता होते हैं: तरलता प्रदाता और ट्रेडर।
तरलता प्रदाता पूल को दो टोकन प्रदान करते हैं जिनका विनिमय (exchange) किया जा सकता है (हम उन्हें Token0 और Token1 कहेंगे)। इसके बदले में, उन्हें एक तीसरा टोकन मिलता है जो पूल के आंशिक स्वामित्व को दर्शाता है, जिसे तरलता टोकन कहा जाता है।
ट्रेडर पूल में एक प्रकार का टोकन भेजते हैं और तरलता प्रदाताओं द्वारा प्रदान किए गए पूल से दूसरा टोकन प्राप्त करते हैं (उदाहरण के लिए, Token0 भेजें और Token1 प्राप्त करें)। विनिमय दर (exchange rate) पूल में मौजूद Token0 और Token1 की सापेक्ष संख्या द्वारा निर्धारित की जाती है। इसके अलावा, पूल तरलता पूल के लिए पुरस्कार के रूप में एक छोटा प्रतिशत लेता है।
जब तरलता प्रदाता अपनी संपत्ति वापस चाहते हैं, तो वे पूल टोकनों को बर्न कर सकते हैं और पुरस्कारों में अपने हिस्से सहित अपने टोकन वापस प्राप्त कर सकते हैं।
अधिक विस्तृत विवरण के लिए यहाँ क्लिक करें (opens in a new tab)।
v2 क्यों? v3 क्यों नहीं?
यूनिस्वैप v3 (opens in a new tab) एक अपग्रेड है जो v2 की तुलना में बहुत अधिक जटिल है। पहले v2 सीखना और फिर v3 पर जाना आसान है।
कोर अनुबंध बनाम परिधि (Periphery) अनुबंध
यूनिस्वैप v2 को दो घटकों में विभाजित किया गया है, एक कोर (core) और एक परिधि (periphery)। यह विभाजन कोर अनुबंधों को, जो संपत्ति रखते हैं और इसलिए उनका सुरक्षित होना आवश्यक है, सरल और ऑडिट करने में आसान बनाता है। ट्रेडरों द्वारा आवश्यक सभी अतिरिक्त कार्यक्षमता फिर परिधि अनुबंधों द्वारा प्रदान की जा सकती है।
डेटा और नियंत्रण प्रवाह
यह डेटा और नियंत्रण का प्रवाह है जो तब होता है जब आप यूनिस्वैप के तीन मुख्य कार्य करते हैं:
- विभिन्न टोकनों के बीच स्वैप करना
- बाज़ार में तरलता जोड़ना और पेयर एक्सचेंज ERC-20 तरलता टोकन के साथ पुरस्कार प्राप्त करना
- ERC-20 तरलता टोकन को बर्न करना और उन ERC-20 टोकनों को वापस पाना जिन्हें पेयर एक्सचेंज ट्रेडर्स को एक्सचेंज करने की अनुमति देता है
स्वैप
यह सबसे आम प्रवाह है, जिसका उपयोग ट्रेडर्स द्वारा किया जाता है:
कॉलर
- स्वैप की जाने वाली राशि में परिधि (periphery) खाते को एक व्यय सीमा प्रदान करें।
- परिधि अनुबंध के कई स्वैप फ़ंक्शन्स में से किसी एक को कॉल करें (कौन सा कॉल करना है यह इस बात पर निर्भर करता है कि ETH शामिल है या नहीं, क्या ट्रेडर जमा करने वाले टोकन की राशि या वापस पाने वाले टोकन की राशि निर्दिष्ट करता है, आदि)।
प्रत्येक स्वैप फ़ंक्शन एक
pathस्वीकार करता है, जो उन एक्सचेंजों का एक ऐरे (array) है जिनसे होकर गुज़रना है।
परिधि अनुबंध में (UniswapV2Router02.sol)
- उन राशियों की पहचान करें जिनका पथ के साथ प्रत्येक एक्सचेंज पर ट्रेड किया जाना है।
- पथ पर पुनरावृत्ति (iterate) करता है। रास्ते में प्रत्येक एक्सचेंज के लिए यह इनपुट टोकन भेजता है और फिर एक्सचेंज के
swapफ़ंक्शन को कॉल करता है। ज़्यादातर मामलों में टोकनों के लिए गंतव्य पता पथ में अगला पेयर एक्सचेंज होता है। अंतिम एक्सचेंज में यह ट्रेडर द्वारा प्रदान किया गया पता होता है।
कोर अनुबंध में (UniswapV2Pair.sol)
- सत्यापित करें कि कोर अनुबंध के साथ कोई धोखाधड़ी नहीं हो रही है और स्वैप के बाद पर्याप्त तरलता बनाए रख सकता है।
- देखें कि ज्ञात रिज़र्व के अलावा हमारे पास कितने अतिरिक्त टोकन हैं। वह राशि उन इनपुट टोकनों की संख्या है जो हमें एक्सचेंज करने के लिए प्राप्त हुए हैं।
- आउटपुट टोकनों को गंतव्य पर भेजें।
- रिज़र्व राशियों को अपडेट करने के लिए
_updateको कॉल करें
वापस परिधि अनुबंध में (UniswapV2Router02.sol)
- कोई भी आवश्यक क्लीनअप करें (उदाहरण के लिए, ट्रेडर को भेजने के लिए ETH वापस पाने के लिए WETH टोकनों को बर्न करें)
तरलता जोड़ें
कॉलर
- तरलता पूल में जोड़ी जाने वाली राशियों में परिधि खाते को एक व्यय सीमा प्रदान करें।
- परिधि अनुबंध के
addLiquidityफ़ंक्शन्स में से किसी एक को कॉल करें।
परिधि अनुबंध में (UniswapV2Router02.sol)
- यदि आवश्यक हो तो एक नया पेयर एक्सचेंज बनाएँ
- यदि कोई मौजूदा पेयर एक्सचेंज है, तो जोड़े जाने वाले टोकनों की राशि की गणना करें। यह दोनों टोकनों के लिए समान मूल्य होना चाहिए, इसलिए नए टोकनों का मौजूदा टोकनों के साथ समान अनुपात होना चाहिए।
- जाँचें कि क्या राशियाँ स्वीकार्य हैं (कॉलर्स एक न्यूनतम राशि निर्दिष्ट कर सकते हैं जिसके नीचे वे तरलता नहीं जोड़ना चाहेंगे)
- कोर अनुबंध को कॉल करें।
कोर अनुबंध में (UniswapV2Pair.sol)
- तरलता टोकन मिंट करें और उन्हें कॉलर को भेजें
- रिज़र्व राशियों को अपडेट करने के लिए
_updateको कॉल करें
तरलता हटाएँ
कॉलर
- अंतर्निहित टोकनों के बदले में बर्न किए जाने वाले तरलता टोकनों की एक व्यय सीमा परिधि खाते को प्रदान करें।
- परिधि अनुबंध के
removeLiquidityफ़ंक्शन्स में से किसी एक को कॉल करें।
परिधि अनुबंध में (UniswapV2Router02.sol)
- तरलता टोकनों को पेयर एक्सचेंज में भेजें
कोर अनुबंध में (UniswapV2Pair.sol)
- गंतव्य पते पर बर्न किए गए टोकनों के अनुपात में अंतर्निहित टोकन भेजें। उदाहरण के लिए यदि पूल में 1000 A टोकन, 500 B टोकन और 90 तरलता टोकन हैं, और हमें बर्न करने के लिए 9 टोकन प्राप्त होते हैं, तो हम 10% तरलता टोकन बर्न कर रहे हैं इसलिए हम उपयोगकर्ता को 100 A टोकन और 50 B टोकन वापस भेजते हैं।
- तरलता टोकनों को बर्न करें
- रिज़र्व राशियों को अपडेट करने के लिए
_updateको कॉल करें
कोर अनुबंध
ये वे सुरक्षित अनुबंध हैं जो तरलता रखते हैं।
UniswapV2Pair.sol
यह अनुबंध (opens in a new tab) उस वास्तविक पूल को लागू करता है जो टोकनों का आदान-प्रदान करता है। यह मुख्य यूनिस्वैप कार्यक्षमता है।
pragma solidity =0.5.16;
import './interfaces/IUniswapV2Pair.sol';
import './UniswapV2ERC20.sol';
import './libraries/Math.sol';
import './libraries/UQ112x112.sol';
import './interfaces/IERC20.sol';
import './interfaces/IUniswapV2Factory.sol';
import './interfaces/IUniswapV2Callee.sol';
ये वे सभी इंटरफ़ेस हैं जिनके बारे में अनुबंध को जानने की आवश्यकता है, या तो इसलिए क्योंकि अनुबंध उन्हें लागू करता है (IUniswapV2Pair और UniswapV2ERC20) या इसलिए क्योंकि यह उन अनुबंधों को कॉल करता है जो उन्हें लागू करते हैं।
contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 {
यह अनुबंध UniswapV2ERC20 से इनहेरिट करता है, जो तरलता टोकनों के लिए ERC-20 फ़ंक्शन प्रदान करता है।
using SafeMath for uint;
ओवरफ़्लो और अंडरफ़्लो से बचने के लिए SafeMath लाइब्रेरी (opens in a new tab) का उपयोग किया जाता है। यह महत्वपूर्ण है क्योंकि अन्यथा हम ऐसी स्थिति में आ सकते हैं जहाँ एक मान -1 होना चाहिए, लेकिन इसके बजाय 2^256-1 हो जाता है।
using UQ112x112 for uint224;
पूल अनुबंध में कई गणनाओं के लिए भिन्नों (fractions) की आवश्यकता होती है। हालाँकि, EVM द्वारा भिन्नों का समर्थन नहीं किया जाता है।
यूनिस्वैप ने जो समाधान खोजा है वह 224 बिट मानों का उपयोग करना है, जिसमें पूर्णांक भाग के लिए 112 बिट्स और भिन्न के लिए 112 बिट्स हैं। इसलिए 1.0 को 2^112 के रूप में दर्शाया गया है, 1.5 को 2^112 + 2^111 के रूप में दर्शाया गया है, आदि।
इस लाइब्रेरी के बारे में अधिक विवरण दस्तावेज़ में आगे उपलब्ध हैं।
चर (Variables)
uint public constant MINIMUM_LIQUIDITY = 10**3;
शून्य से विभाजन के मामलों से बचने के लिए, तरलता टोकनों की एक न्यूनतम संख्या होती है जो हमेशा मौजूद रहती है (लेकिन खाता शून्य के स्वामित्व में होती है)। वह संख्या MINIMUM_LIQUIDITY है, जो एक हज़ार है।
bytes4 private constant SELECTOR = bytes4(keccak256(bytes('transfer(address,uint256)')));
यह ERC-20 ट्रांसफर फ़ंक्शन के लिए ABI चयनकर्ता है। इसका उपयोग दो टोकन खातों में ERC-20 टोकन ट्रांसफर करने के लिए किया जाता है।
address public factory;
यह वह फ़ैक्टरी अनुबंध है जिसने इस पूल को बनाया है। प्रत्येक पूल दो ERC-20 टोकनों के बीच एक विनिमय है, फ़ैक्टरी एक केंद्रीय बिंदु है जो इन सभी पूलों को जोड़ता है।
address public token0;
address public token1;
यहाँ उन दो प्रकार के ERC-20 टोकनों के अनुबंधों के पते हैं जिनका इस पूल द्वारा आदान-प्रदान किया जा सकता है।
uint112 private reserve0; // एकल स्टोरेज स्लॉट का उपयोग करता है, getReserves के माध्यम से सुलभ
uint112 private reserve1; // एकल स्टोरेज स्लॉट का उपयोग करता है, getReserves के माध्यम से सुलभ
प्रत्येक टोकन प्रकार के लिए पूल के पास जो रिज़र्व हैं। हम मान लेते हैं कि दोनों समान मूल्य का प्रतिनिधित्व करते हैं, और इसलिए प्रत्येक token0 का मूल्य reserve1/reserve0 token1 के बराबर है।
uint32 private blockTimestampLast; // एकल स्टोरेज स्लॉट का उपयोग करता है, getReserves के माध्यम से सुलभ
अंतिम ब्लॉक का टाइमस्टैम्प जिसमें विनिमय हुआ था, जिसका उपयोग समय के साथ विनिमय दरों को ट्रैक करने के लिए किया जाता है।
इथेरियम अनुबंधों के सबसे बड़े गैस खर्चों में से एक स्टोरेज है, जो अनुबंध की एक कॉल से दूसरी कॉल तक बना रहता है। प्रत्येक स्टोरेज सेल 256 बिट लंबा होता है। इसलिए तीन चर, reserve0, reserve1, और blockTimestampLast, इस तरह से आवंटित किए जाते हैं कि एक एकल स्टोरेज मान उन तीनों को शामिल कर सके (112+112+32=256)।
uint public price0CumulativeLast;
uint public price1CumulativeLast;
ये चर प्रत्येक टोकन के लिए संचयी लागत (एक दूसरे के संदर्भ में) रखते हैं। उनका उपयोग समय की अवधि में औसत विनिमय दर की गणना करने के लिए किया जा सकता है।
uint public kLast; // reserve0 * reserve1, सबसे हालिया तरलता घटना के तुरंत बाद की स्थिति के अनुसार
पेयर एक्सचेंज token0 और token1 के बीच विनिमय दर तय करने का जो तरीका अपनाता है, वह ट्रेडों के दौरान दो रिज़र्व के गुणनफल को स्थिर रखना है। kLast यह मान है। यह तब बदलता है जब कोई तरलता प्रदाता टोकन जमा करता है या निकालता है, और यह 0.3% बाज़ार शुल्क के कारण थोड़ा बढ़ जाता है।
यहाँ एक सरल उदाहरण है। ध्यान दें कि सरलता के लिए तालिका में दशमलव बिंदु के बाद केवल तीन अंक हैं, और हम 0.3% ट्रेडिंग शुल्क को अनदेखा करते हैं इसलिए संख्याएँ सटीक नहीं हैं।
| घटना | reserve0 | reserve1 | reserve0 * reserve1 | औसत विनिमय दर (token1 / token0) |
|---|---|---|---|---|
| प्रारंभिक सेटअप | 1,000.000 | 1,000.000 | 1,000,000 | |
| ट्रेडर A 47.619 token1 के लिए 50 token0 स्वैप करता है | 1,050.000 | 952.381 | 1,000,000 | 0.952 |
| ट्रेडर B 8.984 token1 के लिए 10 token0 स्वैप करता है | 1,060.000 | 943.396 | 1,000,000 | 0.898 |
| ट्रेडर C 34.305 token1 के लिए 40 token0 स्वैप करता है | 1,100.000 | 909.090 | 1,000,000 | 0.858 |
| ट्रेडर D 109.01 token0 के लिए 100 token1 स्वैप करता है | 990.990 | 1,009.090 | 1,000,000 | 0.917 |
| ट्रेडर E 10.079 token1 के लिए 10 token0 स्वैप करता है | 1,000.990 | 999.010 | 1,000,000 | 1.008 |
जैसे-जैसे ट्रेडर अधिक token0 प्रदान करते हैं, आपूर्ति और मांग के आधार पर token1 का सापेक्ष मूल्य बढ़ता है, और इसके विपरीत।
लॉक
uint private unlocked = 1;
सुरक्षा कमजोरियों का एक वर्ग है जो पुन:प्रवेश (reentrancy) के दुरुपयोग (opens in a new tab) पर आधारित है। यूनिस्वैप को मनमाने ERC-20 टोकन ट्रांसफर करने की आवश्यकता होती है, जिसका अर्थ है उन ERC-20 अनुबंधों को कॉल करना जो उन्हें कॉल करने वाले यूनिस्वैप बाज़ार का दुरुपयोग करने का प्रयास कर सकते हैं।
अनुबंध के हिस्से के रूप में एक unlocked चर रखकर, हम फ़ंक्शनों को उनके चलने के दौरान (उसी लेन-देन के भीतर) कॉल किए जाने से रोक सकते हैं।
modifier lock() {
यह फ़ंक्शन एक मॉडिफ़ायर (opens in a new tab) है, एक ऐसा फ़ंक्शन जो किसी सामान्य फ़ंक्शन के व्यवहार को किसी तरह से बदलने के लिए उसके चारों ओर रैप होता है।
require(unlocked == 1, 'UniswapV2: LOCKED');
unlocked = 0;
यदि unlocked एक के बराबर है, तो इसे शून्य पर सेट करें। यदि यह पहले से ही शून्य है तो कॉल को रिवर्ट करें, इसे विफल करें।
_;
एक मॉडिफ़ायर में _; मूल फ़ंक्शन कॉल (सभी मापदंडों के साथ) है। यहाँ इसका मतलब है कि फ़ंक्शन कॉल केवल तभी होता है जब कॉल किए जाने पर unlocked एक था, और जब यह चल रहा होता है तो unlocked का मान शून्य होता है।
unlocked = 1;
}
मुख्य फ़ंक्शन के वापस आने के बाद, लॉक को रिलीज़ करें।
विविध फ़ंक्शन
function getReserves() public view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast) {
_reserve0 = reserve0;
_reserve1 = reserve1;
_blockTimestampLast = blockTimestampLast;
}
यह फ़ंक्शन कॉल करने वालों को एक्सचेंज की वर्तमान स्थिति प्रदान करता है। ध्यान दें कि Solidity फ़ंक्शन कई मान लौटा सकते हैं (opens in a new tab)।
function _safeTransfer(address token, address to, uint value) private {
(bool success, bytes memory data) = token.call(abi.encodeWithSelector(SELECTOR, to, value));
यह आंतरिक फ़ंक्शन एक्सचेंज से किसी और को ERC20 टोकन की एक राशि ट्रांसफर करता है। SELECTOR निर्दिष्ट करता है कि हम जिस फ़ंक्शन को कॉल कर रहे हैं वह transfer(address,uint) है (ऊपर परिभाषा देखें)।
टोकन फ़ंक्शन के लिए एक इंटरफ़ेस आयात करने से बचने के लिए, हम ABI फ़ंक्शनों (opens in a new tab) में से एक का उपयोग करके "मैन्युअल रूप से" कॉल बनाते हैं।
require(success && (data.length == 0 || abi.decode(data, (bool))), 'UniswapV2: TRANSFER_FAILED');
}
दो तरीके हैं जिनसे एक ERC-20 ट्रांसफर कॉल विफलता की रिपोर्ट कर सकता है:
- रिवर्ट। यदि किसी बाहरी अनुबंध की कॉल रिवर्ट होती है, तो बूलियन रिटर्न मान
falseहोता है - सामान्य रूप से समाप्त हो लेकिन विफलता की रिपोर्ट करे। उस स्थिति में रिटर्न मान बफ़र की लंबाई गैर-शून्य होती है, और जब इसे बूलियन मान के रूप में डिकोड किया जाता है तो यह
falseहोता है
यदि इनमें से कोई भी स्थिति होती है, तो रिवर्ट करें।
घटनाएँ
event Mint(address indexed sender, uint amount0, uint amount1);
event Burn(address indexed sender, uint amount0, uint amount1, address indexed to);
ये दो घटनाएँ तब उत्सर्जित होती हैं जब कोई तरलता प्रदाता या तो तरलता जमा करता है (Mint) या उसे निकालता है (Burn)। किसी भी मामले में, जमा या निकाले गए token0 और token1 की मात्रा घटना का हिस्सा होती है, साथ ही उस खाते की पहचान भी जिसने हमें कॉल किया था (sender)। निकासी के मामले में, घटना में वह लक्ष्य भी शामिल होता है जिसने टोकन प्राप्त किए (to), जो प्रेषक के समान नहीं हो सकता है।
event Swap(
address indexed sender,
uint amount0In,
uint amount1In,
uint amount0Out,
uint amount1Out,
address indexed to
);
यह घटना तब उत्सर्जित होती है जब कोई ट्रेडर एक टोकन को दूसरे के लिए स्वैप करता है। फिर से, प्रेषक और गंतव्य समान नहीं हो सकते हैं। प्रत्येक टोकन या तो एक्सचेंज को भेजा जा सकता है, या उससे प्राप्त किया जा सकता है।
event Sync(uint112 reserve0, uint112 reserve1);
अंत में, नवीनतम रिज़र्व जानकारी (और इसलिए विनिमय दर) प्रदान करने के लिए, कारण की परवाह किए बिना, हर बार टोकन जोड़े या निकाले जाने पर Sync उत्सर्जित होता है।
सेटअप फ़ंक्शन
इन फ़ंक्शनों को एक बार कॉल किया जाना चाहिए जब नया पेयर एक्सचेंज सेट किया जाता है।
constructor() public {
factory = msg.sender;
}
कंस्ट्रक्टर यह सुनिश्चित करता है कि हम उस फ़ैक्टरी के पते का ट्रैक रखेंगे जिसने पेयर बनाया है। यह जानकारी initialize और फ़ैक्टरी शुल्क (यदि कोई मौजूद है) के लिए आवश्यक है
// डिप्लॉयमेंट के समय फैक्ट्री द्वारा एक बार कॉल किया गया
function initialize(address _token0, address _token1) external {
require(msg.sender == factory, 'UniswapV2: FORBIDDEN'); // पर्याप्त जाँच
token0 = _token0;
token1 = _token1;
}
यह फ़ंक्शन फ़ैक्टरी (और केवल फ़ैक्टरी) को उन दो ERC-20 टोकनों को निर्दिष्ट करने की अनुमति देता है जिनका यह पेयर आदान-प्रदान करेगा।
आंतरिक अपडेट फ़ंक्शन
_update
// रिज़र्व को अपडेट करें और, प्रति ब्लॉक पहली कॉल पर, मूल्य संचायकों को
function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private {
हर बार टोकन जमा करने या निकालने पर इस फ़ंक्शन को कॉल किया जाता है।
require(balance0 <= uint112(-1) && balance1 <= uint112(-1), 'UniswapV2: OVERFLOW');
यदि balance0 या balance1 (uint256) uint112(-1) (=2^112-1) से अधिक है (ताकि यह ओवरफ़्लो हो जाए और uint112 में परिवर्तित होने पर वापस 0 पर रैप हो जाए) तो ओवरफ़्लो को रोकने के लिए _update को जारी रखने से मना कर दें। एक सामान्य टोकन के साथ जिसे 10^18 इकाइयों में उप-विभाजित किया जा सकता है, इसका मतलब है कि प्रत्येक एक्सचेंज प्रत्येक टोकन के लगभग 5.1*10^15 तक सीमित है। अब तक यह कोई समस्या नहीं रही है।
uint32 blockTimestamp = uint32(block.timestamp % 2**32);
uint32 timeElapsed = blockTimestamp - blockTimestampLast; // ओवरफ्लो वांछित है
if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) {
यदि बीता हुआ समय शून्य नहीं है, तो इसका मतलब है कि हम इस ब्लॉक पर पहला एक्सचेंज लेन-देन हैं। उस स्थिति में, हमें लागत संचायकों (cost accumulators) को अपडेट करने की आवश्यकता है।
// * कभी ओवरफ्लो नहीं होता है, और + ओवरफ्लो वांछित है
price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed;
price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed;
}
प्रत्येक लागत संचायक को नवीनतम लागत (दूसरे टोकन का रिज़र्व/इस टोकन का रिज़र्व) गुणा सेकंड में बीते हुए समय के साथ अपडेट किया जाता है। औसत मूल्य प्राप्त करने के लिए, आप समय के दो बिंदुओं में संचयी मूल्य पढ़ते हैं और उनके बीच के समय के अंतर से विभाजित करते हैं। उदाहरण के लिए, घटनाओं के इस क्रम को मान लें:
| घटना | reserve0 | reserve1 | टाइमस्टैम्प | सीमांत विनिमय दर (reserve1 / reserve0) | price0CumulativeLast |
|---|---|---|---|---|---|
| प्रारंभिक सेटअप | 1,000.000 | 1,000.000 | 5,000 | 1.000 | 0 |
| ट्रेडर A 50 token0 जमा करता है और 47.619 token1 वापस पाता है | 1,050.000 | 952.381 | 5,020 | 0.907 | 20 |
| ट्रेडर B 10 token0 जमा करता है और 8.984 token1 वापस पाता है | 1,060.000 | 943.396 | 5,030 | 0.890 | 20+10*0.907 = 29.07 |
| ट्रेडर C 40 token0 जमा करता है और 34.305 token1 वापस पाता है | 1,100.000 | 909.090 | 5,100 | 0.826 | 29.07+70*0.890 = 91.37 |
| ट्रेडर D 100 token1 जमा करता है और 109.01 token0 वापस पाता है | 990.990 | 1,009.090 | 5,110 | 1.018 | 91.37+10*0.826 = 99.63 |
| ट्रेडर E 10 token0 जमा करता है और 10.079 token1 वापस पाता है | 1,000.990 | 999.010 | 5,150 | 0.998 | 99.63+40*1.1018 = 143.702 |
मान लें कि हम टाइमस्टैम्प 5,030 और 5,150 के बीच Token0 के औसत मूल्य की गणना करना चाहते हैं। price0Cumulative के मान में अंतर 143.702-29.07=114.632 है। यह दो मिनट (120 सेकंड) का औसत है। इसलिए औसत मूल्य 114.632/120 = 0.955 है।
यह मूल्य गणना ही वह कारण है जिसकी वजह से हमें पुराने रिज़र्व आकारों को जानने की आवश्यकता है।
reserve0 = uint112(balance0);
reserve1 = uint112(balance1);
blockTimestampLast = blockTimestamp;
emit Sync(reserve0, reserve1);
}
अंत में, वैश्विक चरों को अपडेट करें और एक Sync घटना उत्सर्जित करें।
_mintFee
// यदि शुल्क चालू है, तो sqrt(k) में वृद्धि के 1/6 के बराबर तरलता मिंट करें
function _mintFee(uint112 _reserve0, uint112 _reserve1) private returns (bool feeOn) {
यूनिस्वैप 2.0 में ट्रेडर बाज़ार का उपयोग करने के लिए 0.30% शुल्क का भुगतान करते हैं। उस शुल्क का अधिकांश हिस्सा (ट्रेड का 0.25%) हमेशा तरलता प्रदाताओं को जाता है। शेष 0.05% या तो तरलता प्रदाताओं को जा सकता है या फ़ैक्टरी द्वारा प्रोटोकॉल शुल्क के रूप में निर्दिष्ट पते पर जा सकता है, जो यूनिस्वैप को उनके विकास प्रयासों के लिए भुगतान करता है।
गणनाओं (और इसलिए गैस लागतों) को कम करने के लिए, इस शुल्क की गणना केवल तभी की जाती है जब पूल में तरलता जोड़ी या हटाई जाती है, न कि प्रत्येक लेन-देन पर।
address feeTo = IUniswapV2Factory(factory).feeTo();
feeOn = feeTo != address(0);
फ़ैक्टरी के शुल्क गंतव्य को पढ़ें। यदि यह शून्य है तो कोई प्रोटोकॉल शुल्क नहीं है और उस शुल्क की गणना करने की कोई आवश्यकता नहीं है।
uint _kLast = kLast; // गैस की बचत
kLast स्थिति चर स्टोरेज में स्थित है, इसलिए अनुबंध की विभिन्न कॉलों के बीच इसका एक मान होगा।
स्टोरेज तक पहुंच उस अस्थिर मेमोरी तक पहुंच की तुलना में बहुत अधिक महंगी है जो अनुबंध के लिए फ़ंक्शन कॉल समाप्त होने पर रिलीज़ हो जाती है, इसलिए हम गैस बचाने के लिए एक आंतरिक चर का उपयोग करते हैं।
if (feeOn) {
if (_kLast != 0) {
तरलता प्रदाताओं को अपना हिस्सा केवल उनके तरलता टोकनों की वृद्धि से मिलता है। लेकिन प्रोटोकॉल शुल्क के लिए नए तरलता टोकन मिंट करने और feeTo पते पर प्रदान करने की आवश्यकता होती है।
uint rootK = Math.sqrt(uint(_reserve0).mul(_reserve1));
uint rootKLast = Math.sqrt(_kLast);
if (rootK > rootKLast) {
यदि कोई नई तरलता है जिस पर प्रोटोकॉल शुल्क एकत्र किया जाना है। आप वर्गमूल फ़ंक्शन को इस लेख में आगे देख सकते हैं
uint numerator = totalSupply.mul(rootK.sub(rootKLast));
uint denominator = rootK.mul(5).add(rootKLast);
uint liquidity = numerator / denominator;
शुल्क की यह जटिल गणना श्वेतपत्र (opens in a new tab) के पृष्ठ 5 पर समझाई गई है। हम जानते हैं कि जिस समय kLast की गणना की गई थी और वर्तमान समय के बीच कोई तरलता नहीं जोड़ी या हटाई गई थी (क्योंकि हम हर बार तरलता जोड़ने या हटाने पर यह गणना चलाते हैं, इसके वास्तव में बदलने से पहले), इसलिए reserve0 * reserve1 में कोई भी परिवर्तन लेन-देन शुल्क से आना चाहिए (उनके बिना हम reserve0 * reserve1 को स्थिर रखेंगे)।
if (liquidity > 0) _mint(feeTo, liquidity);
}
}
वास्तव में अतिरिक्त तरलता टोकन बनाने और उन्हें feeTo को असाइन करने के लिए UniswapV2ERC20._mint फ़ंक्शन का उपयोग करें।
} else if (_kLast != 0) {
kLast = 0;
}
}
यदि कोई शुल्क नहीं है तो kLast को शून्य पर सेट करें (यदि यह पहले से ऐसा नहीं है)। जब यह अनुबंध लिखा गया था तब एक गैस रिफंड सुविधा (opens in a new tab) थी जो अनुबंधों को उस स्टोरेज को शून्य करके इथेरियम स्थिति के समग्र आकार को कम करने के लिए प्रोत्साहित करती थी जिसकी उन्हें आवश्यकता नहीं थी।
यह कोड संभव होने पर वह रिफंड प्राप्त करता है।
बाह्य रूप से सुलभ फ़ंक्शन
ध्यान दें कि हालांकि कोई भी लेन-देन या अनुबंध इन फ़ंक्शनों को कॉल कर सकता है, उन्हें परिधि (periphery) अनुबंध से कॉल किए जाने के लिए डिज़ाइन किया गया है। यदि आप उन्हें सीधे कॉल करते हैं तो आप पेयर एक्सचेंज को धोखा नहीं दे पाएंगे, लेकिन आप किसी गलती के कारण मूल्य खो सकते हैं।
mint
// इस लो-लेवल फ़ंक्शन को एक अनुबंध से कॉल किया जाना चाहिए जो महत्वपूर्ण सुरक्षा जाँच करता है
function mint(address to) external lock returns (uint liquidity) {
यह फ़ंक्शन तब कॉल किया जाता है जब कोई तरलता प्रदाता पूल में तरलता जोड़ता है। यह पुरस्कार के रूप में अतिरिक्त तरलता टोकन मिंट करता है। इसे एक परिधि अनुबंध से कॉल किया जाना चाहिए जो उसी लेन-देन में तरलता जोड़ने के बाद इसे कॉल करता है (ताकि कोई और ऐसा लेन-देन सबमिट न कर सके जो वैध मालिक से पहले नई तरलता का दावा करता हो)।
(uint112 _reserve0, uint112 _reserve1,) = getReserves(); // गैस की बचत
यह एक Solidity फ़ंक्शन के परिणामों को पढ़ने का तरीका है जो कई मान लौटाता है। हम अंतिम लौटाए गए मानों, ब्लॉक टाइमस्टैम्प को छोड़ देते हैं, क्योंकि हमें इसकी आवश्यकता नहीं है।
uint balance0 = IERC20(token0).balanceOf(address(this));
uint balance1 = IERC20(token1).balanceOf(address(this));
uint amount0 = balance0.sub(_reserve0);
uint amount1 = balance1.sub(_reserve1);
वर्तमान शेष राशि प्राप्त करें और देखें कि प्रत्येक टोकन प्रकार का कितना जोड़ा गया था।
bool feeOn = _mintFee(_reserve0, _reserve1);
एकत्र किए जाने वाले प्रोटोकॉल शुल्क की गणना करें, यदि कोई हो, और तदनुसार तरलता टोकन मिंट करें। क्योंकि _mintFee के पैरामीटर पुराने रिज़र्व मान हैं, शुल्क की गणना केवल शुल्क के कारण पूल में होने वाले परिवर्तनों के आधार पर सटीक रूप से की जाती है।
uint _totalSupply = totalSupply; // गैस की बचत, इसे यहाँ परिभाषित किया जाना चाहिए क्योंकि totalSupply _mintFee में अपडेट हो सकता है
if (_totalSupply == 0) {
liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);
_mint(address(0), MINIMUM_LIQUIDITY); // पहले MINIMUM_LIQUIDITY टोकन को स्थायी रूप से लॉक करें
यदि यह पहली जमा राशि है, तो MINIMUM_LIQUIDITY टोकन बनाएँ और उन्हें लॉक करने के लिए शून्य पते पर भेजें। उन्हें कभी भी भुनाया नहीं जा सकता है, जिसका अर्थ है कि पूल कभी भी पूरी तरह से खाली नहीं होगा (यह हमें कुछ स्थानों पर शून्य से विभाजन से बचाता है)। MINIMUM_LIQUIDITY का मान एक हज़ार है, जो यह देखते हुए कि अधिकांश ERC-20 को एक टोकन के 10^-18वें हिस्से की इकाइयों में उप-विभाजित किया जाता है, जैसे ETH को Wei में विभाजित किया जाता है, एक एकल टोकन के मूल्य का 10^-15 है। यह कोई उच्च लागत नहीं है।
पहली जमा राशि के समय हम दोनों टोकनों का सापेक्ष मूल्य नहीं जानते हैं, इसलिए हम केवल राशियों को गुणा करते हैं और एक वर्गमूल लेते हैं, यह मानते हुए कि जमा राशि हमें दोनों टोकनों में समान मूल्य प्रदान करती है।
हम इस पर भरोसा कर सकते हैं क्योंकि समान मूल्य प्रदान करना जमाकर्ता के हित में है, ताकि मध्यस्थता (arbitrage) में मूल्य खोने से बचा जा सके। मान लें कि दोनों टोकनों का मूल्य समान है, लेकिन हमारे जमाकर्ता ने Token0 की तुलना में Token1 को चार गुना अधिक जमा किया है। एक ट्रेडर इस तथ्य का उपयोग कर सकता है कि पेयर एक्सचेंज सोचता है कि Token0 अधिक मूल्यवान है, ताकि इससे मूल्य निकाला जा सके।
| घटना | reserve0 | reserve1 | reserve0 * reserve1 | पूल का मूल्य (reserve0 + reserve1) |
|---|---|---|---|---|
| प्रारंभिक सेटअप | 8 | 32 | 256 | 40 |
| ट्रेडर 8 Token0 टोकन जमा करता है, 16 Token1 वापस पाता है | 16 | 16 | 256 | 32 |
जैसा कि आप देख सकते हैं, ट्रेडर ने अतिरिक्त 8 टोकन कमाए, जो पूल के मूल्य में कमी से आते हैं, जिससे उस जमाकर्ता को नुकसान होता है जो इसका मालिक है।
} else {
liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1);
प्रत्येक बाद की जमा राशि के साथ हम पहले से ही दो संपत्तियों के बीच विनिमय दर जानते हैं, और हम उम्मीद करते हैं कि तरलता प्रदाता दोनों में समान मूल्य प्रदान करेंगे। यदि वे ऐसा नहीं करते हैं, तो हम उन्हें दंड के रूप में उनके द्वारा प्रदान किए गए कम मूल्य के आधार पर तरलता टोकन देते हैं।
चाहे वह प्रारंभिक जमा हो या बाद की जमा, हमारे द्वारा प्रदान किए जाने वाले तरलता टोकनों की संख्या reserve0*reserve1 में परिवर्तन के वर्गमूल के बराबर होती है और तरलता टोकन का मूल्य नहीं बदलता है (जब तक कि हमें ऐसी जमा राशि न मिले जिसमें दोनों प्रकार के समान मूल्य न हों, जिस स्थिति में "जुर्माना" वितरित हो जाता है)। यहाँ समान मूल्य वाले दो टोकनों के साथ एक और उदाहरण दिया गया है, जिसमें तीन अच्छी जमा राशियाँ और एक खराब जमा राशि है (केवल एक टोकन प्रकार की जमा राशि, इसलिए यह कोई तरलता टोकन उत्पन्न नहीं करती है)।
| घटना | reserve0 | reserve1 | reserve0 * reserve1 | पूल मूल्य (reserve0 + reserve1) | इस जमा के लिए मिंट किए गए तरलता टोकन | कुल तरलता टोकन | प्रत्येक तरलता टोकन का मूल्य |
|---|---|---|---|---|---|---|---|
| प्रारंभिक सेटअप | 8.000 | 8.000 | 64 | 16.000 | 8 | 8 | 2.000 |
| प्रत्येक प्रकार के चार जमा करें | 12.000 | 12.000 | 144 | 24.000 | 4 | 12 | 2.000 |
| प्रत्येक प्रकार के दो जमा करें | 14.000 | 14.000 | 196 | 28.000 | 2 | 14 | 2.000 |
| असमान मूल्य जमा | 18.000 | 14.000 | 252 | 32.000 | 0 | 14 | ~2.286 |
| मध्यस्थता (arbitrage) के बाद | ~15.874 | ~15.874 | 252 | ~31.748 | 0 | 14 | ~2.267 |
}
require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED');
_mint(to, liquidity);
वास्तव में अतिरिक्त तरलता टोकन बनाने और उन्हें सही खाते में देने के लिए UniswapV2ERC20._mint फ़ंक्शन का उपयोग करें।
_update(balance0, balance1, _reserve0, _reserve1);
if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 और reserve1 अप-टू-डेट हैं
emit Mint(msg.sender, amount0, amount1);
}
स्थिति चरों (reserve0, reserve1, और यदि आवश्यक हो तो kLast) को अपडेट करें और उचित घटना उत्सर्जित करें।
burn
// इस लो-लेवल फ़ंक्शन को एक अनुबंध से कॉल किया जाना चाहिए जो महत्वपूर्ण सुरक्षा जाँच करता है
function burn(address to) external lock returns (uint amount0, uint amount1) {
यह फ़ंक्शन तब कॉल किया जाता है जब तरलता निकाली जाती है और उचित तरलता टोकनों को बर्न करने की आवश्यकता होती है। इसे भी एक परिधि खाते से कॉल किया जाना चाहिए।
(uint112 _reserve0, uint112 _reserve1,) = getReserves(); // गैस की बचत
address _token0 = token0; // गैस की बचत
address _token1 = token1; // गैस की बचत
uint balance0 = IERC20(_token0).balanceOf(address(this));
uint balance1 = IERC20(_token1).balanceOf(address(this));
uint liquidity = balanceOf[address(this)];
परिधि अनुबंध ने कॉल से पहले बर्न की जाने वाली तरलता को इस अनुबंध में ट्रांसफर कर दिया। इस तरह हम जानते हैं कि कितनी तरलता बर्न करनी है, और हम यह सुनिश्चित कर सकते हैं कि यह बर्न हो जाए।
bool feeOn = _mintFee(_reserve0, _reserve1);
uint _totalSupply = totalSupply; // गैस की बचत, इसे यहाँ परिभाषित किया जाना चाहिए क्योंकि totalSupply _mintFee में अपडेट हो सकता है
amount0 = liquidity.mul(balance0) / _totalSupply; // बैलेंस का उपयोग यथानुपात (pro-rata) वितरण सुनिश्चित करता है
amount1 = liquidity.mul(balance1) / _totalSupply; // बैलेंस का उपयोग यथानुपात (pro-rata) वितरण सुनिश्चित करता है
require(amount0 > 0 && amount1 > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_BURNED');
तरलता प्रदाता को दोनों टोकनों का समान मूल्य प्राप्त होता है। इस तरह हम विनिमय दर को नहीं बदलते हैं।
_burn(address(this), liquidity);
_safeTransfer(_token0, to, amount0);
_safeTransfer(_token1, to, amount1);
balance0 = IERC20(_token0).balanceOf(address(this));
balance1 = IERC20(_token1).balanceOf(address(this));
_update(balance0, balance1, _reserve0, _reserve1);
if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 और reserve1 अप-टू-डेट हैं
emit Burn(msg.sender, amount0, amount1, to);
}
बाकी burn फ़ंक्शन ऊपर दिए गए mint फ़ंक्शन की दर्पण छवि (mirror image) है।
swap
// इस लो-लेवल फ़ंक्शन को एक अनुबंध से कॉल किया जाना चाहिए जो महत्वपूर्ण सुरक्षा जाँच करता है
function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock {
इस फ़ंक्शन को भी एक परिधि अनुबंध से कॉल किया जाना चाहिए।
require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT');
(uint112 _reserve0, uint112 _reserve1,) = getReserves(); // गैस की बचत
require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');
uint balance0;
uint balance1;
{ // _token{0,1} के लिए स्कोप, स्टैक टू डीप त्रुटियों से बचाता है
स्थानीय चरों को या तो मेमोरी में संग्रहीत किया जा सकता है या, यदि वे बहुत अधिक नहीं हैं, तो सीधे स्टैक पर। यदि हम संख्या को सीमित कर सकते हैं ताकि हम स्टैक का उपयोग करें तो हम कम गैस का उपयोग करते हैं। अधिक विवरण के लिए येलो पेपर, औपचारिक इथेरियम विनिर्देश (opens in a new tab), पृष्ठ 26, समीकरण 298 देखें।
address _token0 = token0;
address _token1 = token1;
require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO');
if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // आशावादी रूप से टोकन ट्रांसफर करें
if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // आशावादी रूप से टोकन ट्रांसफर करें
यह ट्रांसफर आशावादी (optimistic) है, क्योंकि हम यह सुनिश्चित होने से पहले ट्रांसफर करते हैं कि सभी शर्तें पूरी हो गई हैं। इथेरियम में यह ठीक है क्योंकि यदि कॉल में बाद में शर्तें पूरी नहीं होती हैं तो हम इसे रिवर्ट कर देते हैं और इसके द्वारा बनाए गए किसी भी परिवर्तन को रद्द कर देते हैं।
if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);
यदि अनुरोध किया गया हो तो रिसीवर को स्वैप के बारे में सूचित करें।
balance0 = IERC20(_token0).balanceOf(address(this));
balance1 = IERC20(_token1).balanceOf(address(this));
}
वर्तमान शेष राशि प्राप्त करें। परिधि अनुबंध स्वैप के लिए हमें कॉल करने से पहले हमें टोकन भेजता है। इससे अनुबंध के लिए यह जाँचना आसान हो जाता है कि उसे धोखा नहीं दिया जा रहा है, एक ऐसी जाँच जो कोर अनुबंध में होनी ही चाहिए (क्योंकि हमें हमारे परिधि अनुबंध के अलावा अन्य संस्थाओं द्वारा भी कॉल किया जा सकता है)।
uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;
uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;
require(amount0In > 0 || amount1In > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT');
{ // reserve{0,1}Adjusted के लिए स्कोप, स्टैक टू डीप त्रुटियों से बचाता है
uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3));
uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3));
require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K');
यह सुनिश्चित करने के लिए एक विवेक जाँच (sanity check) है कि हम स्वैप से नुकसान न उठाएँ। ऐसी कोई परिस्थिति नहीं है जिसमें स्वैप को reserve0*reserve1 कम करना चाहिए। यह वह जगह भी है जहाँ हम सुनिश्चित करते हैं कि स्वैप पर 0.3% का शुल्क भेजा जा रहा है; K के मान की विवेक जाँच करने से पहले, हम दोनों शेष राशियों को 1000 से गुणा करते हैं और उसमें से 3 से गुणा की गई राशियों को घटाते हैं, इसका मतलब है कि इसके K मान की वर्तमान रिज़र्व K मान के साथ तुलना करने से पहले शेष राशि से 0.3% (3/1000 = 0.003 = 0.3%) काटा जा रहा है।
}
_update(balance0, balance1, _reserve0, _reserve1);
emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);
}
reserve0 और reserve1 को अपडेट करें, और यदि आवश्यक हो तो मूल्य संचायकों और टाइमस्टैम्प को अपडेट करें और एक घटना उत्सर्जित करें।
सिंकिंग या स्किम
यह संभव है कि वास्तविक शेष राशि उन रिज़र्व के साथ सिंक से बाहर हो जाए जो पेयर एक्सचेंज सोचता है कि उसके पास हैं।
अनुबंध की सहमति के बिना टोकन निकालने का कोई तरीका नहीं है, लेकिन जमा एक अलग मामला है। कोई खाता mint या swap को कॉल किए बिना एक्सचेंज में टोकन ट्रांसफर कर सकता है।
उस स्थिति में दो समाधान हैं:
sync, रिज़र्व को वर्तमान शेष राशि में अपडेट करेंskim, अतिरिक्त राशि निकालें। ध्यान दें कि किसी भी खाते कोskimकॉल करने की अनुमति है क्योंकि हम नहीं जानते कि टोकन किसने जमा किए हैं। यह जानकारी एक घटना में उत्सर्जित होती है, लेकिन घटनाएँ ब्लॉकचेन से सुलभ नहीं होती हैं।
// बैलेंस को रिज़र्व से मेल खाने के लिए बाध्य करें
function skim(address to) external lock {
address _token0 = token0; // गैस की बचत
address _token1 = token1; // गैस की बचत
_safeTransfer(_token0, to, IERC20(_token0).balanceOf(address(this)).sub(reserve0));
_safeTransfer(_token1, to, IERC20(_token1).balanceOf(address(this)).sub(reserve1));
}
// रिज़र्व को बैलेंस से मेल खाने के लिए बाध्य करें
function sync() external lock {
_update(IERC20(token0).balanceOf(address(this)), IERC20(token1).balanceOf(address(this)), reserve0, reserve1);
}
}
UniswapV2Factory.sol
यह अनुबंध (opens in a new tab) पेयर एक्सचेंज बनाता है।
pragma solidity =0.5.16;
import './interfaces/IUniswapV2Factory.sol';
import './UniswapV2Pair.sol';
contract UniswapV2Factory is IUniswapV2Factory {
address public feeTo;
address public feeToSetter;
प्रोटोकॉल शुल्क लागू करने के लिए ये स्थिति चर आवश्यक हैं (श्वेतपत्र (opens in a new tab), पृष्ठ 5 देखें)।
feeTo पता प्रोटोकॉल शुल्क के लिए तरलता टोकन जमा करता है, और feeToSetter वह पता है जिसे feeTo को एक अलग पते में बदलने की अनुमति है।
mapping(address => mapping(address => address)) public getPair;
address[] public allPairs;
ये चर पेयर, दो टोकन प्रकारों के बीच एक्सचेंजों का ट्रैक रखते हैं।
पहला, getPair, एक मैपिंग है जो एक पेयर एक्सचेंज अनुबंध की पहचान उन दो ERC-20 टोकनों के आधार पर करता है जिनका यह आदान-प्रदान करता है। ERC-20 टोकनों की पहचान उन अनुबंधों के पतों से की जाती है जो उन्हें लागू करते हैं, इसलिए कुंजियाँ (keys) और मान सभी पते हैं। उस पेयर एक्सचेंज का पता प्राप्त करने के लिए जो आपको tokenA से tokenB में बदलने देता है, आप getPair[<tokenA address>][<tokenB address>] का उपयोग करते हैं (या इसके विपरीत)।
दूसरा चर, allPairs, एक ऐरे है जिसमें इस फ़ैक्टरी द्वारा बनाए गए पेयर एक्सचेंजों के सभी पते शामिल हैं। इथेरियम में आप किसी मैपिंग की सामग्री पर पुनरावृति (iterate) नहीं कर सकते हैं, या सभी कुंजियों की सूची प्राप्त नहीं कर सकते हैं, इसलिए यह चर यह जानने का एकमात्र तरीका है कि यह फ़ैक्टरी किन एक्सचेंजों का प्रबंधन करती है।
नोट: आप किसी मैपिंग की सभी कुंजियों पर पुनरावृति क्यों नहीं कर सकते, इसका कारण यह है कि अनुबंध डेटा स्टोरेज महंगा है, इसलिए हम इसका जितना कम उपयोग करेंगे उतना अच्छा है, और हम इसे जितनी कम बार बदलेंगे उतना अच्छा है। आप ऐसी मैपिंग बना सकते हैं जो पुनरावृत्ति का समर्थन करती हैं (opens in a new tab), लेकिन उन्हें कुंजियों की सूची के लिए अतिरिक्त स्टोरेज की आवश्यकता होती है। अधिकांश अनुप्रयोगों में आपको इसकी आवश्यकता नहीं होती है।
event PairCreated(address indexed token0, address indexed token1, address pair, uint);
यह घटना तब उत्सर्जित होती है जब एक नया पेयर एक्सचेंज बनाया जाता है। इसमें टोकनों के पते, पेयर एक्सचेंज का पता और फ़ैक्टरी द्वारा प्रबंधित एक्सचेंजों की कुल संख्या शामिल होती है।
constructor(address _feeToSetter) public {
feeToSetter = _feeToSetter;
}
कंस्ट्रक्टर केवल feeToSetter निर्दिष्ट करता है। फ़ैक्टरियाँ बिना किसी शुल्क के शुरू होती हैं, और केवल feeSetter ही इसे बदल सकता है।
function allPairsLength() external view returns (uint) {
return allPairs.length;
}
यह फ़ंक्शन एक्सचेंज पेयर की संख्या लौटाता है।
function createPair(address tokenA, address tokenB) external returns (address pair) {
यह फ़ैक्टरी का मुख्य फ़ंक्शन है, दो ERC-20 टोकनों के बीच एक पेयर एक्सचेंज बनाना। ध्यान दें कि कोई भी इस फ़ंक्शन को कॉल कर सकता है। नया पेयर एक्सचेंज बनाने के लिए आपको यूनिस्वैप से अनुमति की आवश्यकता नहीं है।
require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES');
(address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
हम चाहते हैं कि नए एक्सचेंज का पता नियतात्मक (deterministic) हो, ताकि इसकी गणना पहले से ही ऑफचेन की जा सके (यह लेयर 2 (l2) लेन-देन के लिए उपयोगी हो सकता है)। ऐसा करने के लिए हमें टोकन पतों का एक सुसंगत क्रम होना चाहिए, चाहे हमने उन्हें किसी भी क्रम में प्राप्त किया हो, इसलिए हम उन्हें यहाँ सॉर्ट करते हैं।
require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS');
require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS'); // एकल जाँच पर्याप्त है
बड़े तरलता पूल छोटे पूलों की तुलना में बेहतर होते हैं, क्योंकि उनकी कीमतें अधिक स्थिर होती हैं। हम प्रति टोकन पेयर एक से अधिक तरलता पूल नहीं चाहते हैं। यदि पहले से ही कोई एक्सचेंज है, तो उसी पेयर के लिए दूसरा बनाने की कोई आवश्यकता नहीं है।
bytes memory bytecode = type(UniswapV2Pair).creationCode;
एक नया अनुबंध बनाने के लिए हमें उस कोड की आवश्यकता होती है जो इसे बनाता है (कंस्ट्रक्टर फ़ंक्शन और वह कोड दोनों जो वास्तविक अनुबंध के EVM बाइटकोड को मेमोरी में लिखता है)। सामान्यतः Solidity में हम केवल addr = new <name of contract>(<constructor parameters>) का उपयोग करते हैं और कंपाइलर हमारे लिए सब कुछ संभाल लेता है, लेकिन एक नियतात्मक अनुबंध पता रखने के लिए हमें CREATE2 ऑपकोड (opens in a new tab) का उपयोग करने की आवश्यकता है।
जब यह कोड लिखा गया था तब उस ऑपकोड को अभी तक Solidity द्वारा समर्थित नहीं किया गया था, इसलिए मैन्युअल रूप से कोड प्राप्त करना आवश्यक था। यह अब कोई समस्या नहीं है, क्योंकि Solidity अब CREATE2 का समर्थन करता है (opens in a new tab)।
bytes32 salt = keccak256(abi.encodePacked(token0, token1));
assembly {
pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
}
जब कोई ऑपकोड अभी तक Solidity द्वारा समर्थित नहीं है तो हम इसे इनलाइन असेंबली (opens in a new tab) का उपयोग करके कॉल कर सकते हैं।
IUniswapV2Pair(pair).initialize(token0, token1);
नए एक्सचेंज को यह बताने के लिए initialize फ़ंक्शन को कॉल करें कि यह किन दो टोकनों का आदान-प्रदान करता है।
getPair[token0][token1] = pair;
getPair[token1][token0] = pair; // विपरीत दिशा में मैपिंग भरें
allPairs.push(pair);
emit PairCreated(token0, token1, pair, allPairs.length);
}
नए पेयर की जानकारी को स्थिति चरों में सहेजें और दुनिया को नए पेयर एक्सचेंज के बारे में सूचित करने के लिए एक घटना उत्सर्जित करें।
function setFeeTo(address _feeTo) external {
require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');
feeTo = _feeTo;
}
function setFeeToSetter(address _feeToSetter) external {
require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');
feeToSetter = _feeToSetter;
}
}
ये दो फ़ंक्शन feeSetter को शुल्क प्राप्तकर्ता (यदि कोई हो) को नियंत्रित करने और feeSetter को एक नए पते में बदलने की अनुमति देते हैं।
UniswapV2ERC20.sol
यह अनुबंध (opens in a new tab) ERC-20 तरलता टोकन लागू करता है। यह ओपनजेपेलिन ERC-20 अनुबंध के समान है, इसलिए मैं केवल उस हिस्से की व्याख्या करूँगा जो अलग है, permit कार्यक्षमता।
इथेरियम पर लेन-देन में ईथर (ETH) खर्च होता है, जो वास्तविक धन के बराबर है। यदि आपके पास ERC-20 टोकन हैं लेकिन ETH नहीं है, तो आप लेन-देन नहीं भेज सकते हैं, इसलिए आप उनके साथ कुछ नहीं कर सकते। इस समस्या से बचने का एक समाधान मेटा-लेन-देन (opens in a new tab) है। टोकन का मालिक एक लेन-देन पर हस्ताक्षर करता है जो किसी और को ऑफचेन टोकन निकालने की अनुमति देता है और इसे इंटरनेट का उपयोग करके प्राप्तकर्ता को भेजता है। प्राप्तकर्ता, जिसके पास ETH है, फिर मालिक की ओर से परमिट सबमिट करता है।
bytes32 public DOMAIN_SEPARATOR;
// keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
bytes32 public constant PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9;
यह हैश लेन-देन प्रकार के लिए पहचानकर्ता (opens in a new tab) है। यहाँ हम केवल इन मापदंडों के साथ Permit का समर्थन करते हैं।
mapping(address => uint) public nonces;
किसी प्राप्तकर्ता के लिए डिजिटल हस्ताक्षर को जाली बनाना संभव नहीं है। हालाँकि, एक ही लेन-देन को दो बार भेजना बहुत आसान है (यह रीप्ले हमले (opens in a new tab) का एक रूप है)। इसे रोकने के लिए, हम एक नॉन्स (opens in a new tab) का उपयोग करते हैं। यदि किसी नए Permit का नॉन्स उपयोग किए गए अंतिम नॉन्स से एक अधिक नहीं है, तो हम मान लेते हैं कि यह अमान्य है।
constructor() public {
uint chainId;
assembly {
chainId := chainid
}
यह चेन पहचानकर्ता (opens in a new tab) प्राप्त करने का कोड है। यह Yul (opens in a new tab) नामक EVM असेंबली बोली का उपयोग करता है। ध्यान दें कि Yul के वर्तमान संस्करण में आपको chainid नहीं, बल्कि chainid() का उपयोग करना होगा।
DOMAIN_SEPARATOR = keccak256(
abi.encode(
keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'),
keccak256(bytes(name)),
keccak256(bytes('1')),
chainId,
address(this)
)
);
}
EIP-712 के लिए डोमेन सेपरेटर (opens in a new tab) की गणना करें।
function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external {
यह वह फ़ंक्शन है जो अनुमतियों को लागू करता है। यह प्रासंगिक फ़ील्ड और हस्ताक्षर (opens in a new tab) (v, r, और s) के लिए तीन अदिश (scalar) मानों को मापदंडों के रूप में प्राप्त करता है।
require(deadline >= block.timestamp, 'UniswapV2: EXPIRED');
समय सीमा के बाद लेन-देन स्वीकार न करें।
bytes32 digest = keccak256(
abi.encodePacked(
'\x19\x01',
DOMAIN_SEPARATOR,
keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline))
)
);
abi.encodePacked(...) वह संदेश है जो हमें मिलने की उम्मीद है। हम जानते हैं कि नॉन्स क्या होना चाहिए, इसलिए हमें इसे पैरामीटर के रूप में प्राप्त करने की कोई आवश्यकता नहीं है।
इथेरियम हस्ताक्षर एल्गोरिदम को हस्ताक्षर करने के लिए 256 बिट्स प्राप्त होने की उम्मीद है, इसलिए हम keccak256 हैश फ़ंक्शन का उपयोग करते हैं।
address recoveredAddress = ecrecover(digest, v, r, s);
डाइजेस्ट और हस्ताक्षर से हम ecrecover (opens in a new tab) का उपयोग करके उस पते को प्राप्त कर सकते हैं जिसने इस पर हस्ताक्षर किए हैं।
require(recoveredAddress != address(0) && recoveredAddress == owner, 'UniswapV2: INVALID_SIGNATURE');
_approve(owner, spender, value);
}
यदि सब कुछ ठीक है, तो इसे एक ERC-20 स्वीकृति (approve) (opens in a new tab) के रूप में मानें।
परिधि अनुबंध
परिधि अनुबंध यूनिस्वैप (Uniswap) के लिए API (एप्लिकेशन प्रोग्राम इंटरफ़ेस) हैं। वे अन्य अनुबंधों या विकेंद्रीकृत एप्लिकेशन (dapps) से बाहरी कॉल के लिए उपलब्ध हैं। आप कोर अनुबंधों को सीधे कॉल कर सकते हैं, लेकिन यह अधिक जटिल है और यदि आप कोई गलती करते हैं तो आप मूल्य खो सकते हैं। कोर अनुबंधों में केवल यह सुनिश्चित करने के लिए परीक्षण होते हैं कि उनके साथ धोखा न हो, किसी और के लिए कोई विवेकपूर्ण जाँच (sanity checks) नहीं होती है। वे परिधि में होते हैं ताकि आवश्यकतानुसार उन्हें अपडेट किया जा सके।
UniswapV2Router01.sol
इस अनुबंध (opens in a new tab) में समस्याएँ हैं, और अब इसका उपयोग नहीं किया जाना चाहिए (opens in a new tab)। सौभाग्य से, परिधि अनुबंध स्थिति-रहित (stateless) होते हैं और कोई संपत्ति नहीं रखते हैं, इसलिए इसे हटाना और लोगों को इसके बजाय प्रतिस्थापन, UniswapV2Router02 का उपयोग करने का सुझाव देना आसान है।
UniswapV2Router02.sol
ज्यादातर मामलों में आप इस अनुबंध (opens in a new tab) के माध्यम से यूनिस्वैप का उपयोग करेंगे। आप देख सकते हैं कि इसका उपयोग यहाँ (opens in a new tab) कैसे करें।
pragma solidity =0.6.6;
import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Factory.sol';
import '@uniswap/lib/contracts/libraries/TransferHelper.sol';
import './interfaces/IUniswapV2Router02.sol';
import './libraries/UniswapV2Library.sol';
import './libraries/SafeMath.sol';
import './interfaces/IERC20.sol';
import './interfaces/IWETH.sol';
इनमें से अधिकांश का हमने या तो पहले सामना किया है, या वे काफी स्पष्ट हैं। एकमात्र अपवाद IWETH.sol है। यूनिस्वैप v2 किसी भी ERC-20 टोकन के जोड़े के लिए विनिमय की अनुमति देता है, लेकिन ईथर (ETH) स्वयं एक ERC-20 टोकन नहीं है। यह मानक से पहले का है और इसे अद्वितीय तंत्र द्वारा ट्रांसफर किया जाता है। ERC-20 टोकनों पर लागू होने वाले अनुबंधों में ETH के उपयोग को सक्षम करने के लिए लोग रैप्ड ईथर (WETH) (opens in a new tab) अनुबंध लेकर आए। आप इस अनुबंध को ETH भेजते हैं, और यह आपको समान मात्रा में WETH मिंट करता है। या आप WETH को बर्न कर सकते हैं, और ETH वापस प्राप्त कर सकते हैं।
contract UniswapV2Router02 is IUniswapV2Router02 {
using SafeMath for uint;
address public immutable override factory;
address public immutable override WETH;
राउटर को यह जानने की आवश्यकता होती है कि किस फ़ैक्टरी का उपयोग करना है, और जिन लेन-देन के लिए WETH की आवश्यकता होती है, उनके लिए किस WETH अनुबंध का उपयोग करना है। ये मान अपरिवर्तनीय (opens in a new tab) हैं, जिसका अर्थ है कि उन्हें केवल कंस्ट्रक्टर में सेट किया जा सकता है। यह उपयोगकर्ताओं को यह विश्वास दिलाता है कि कोई भी उन्हें कम ईमानदार अनुबंधों की ओर इंगित करने के लिए बदल नहीं पाएगा।
modifier ensure(uint deadline) {
require(deadline >= block.timestamp, 'UniswapV2Router: EXPIRED');
_;
}
यह संशोधक यह सुनिश्चित करता है कि समय-सीमित लेन-देन ("यदि आप कर सकते हैं तो समय Y से पहले X करें") उनकी समय सीमा के बाद न हों।
constructor(address _factory, address _WETH) public {
factory = _factory;
WETH = _WETH;
}
कंस्ट्रक्टर केवल अपरिवर्तनीय स्थिति चर सेट करता है।
receive() external payable {
assert(msg.sender == WETH); // केवल WETH अनुबंध से फ़ॉलबैक के माध्यम से ETH स्वीकार करें
}
इस फ़ंक्शन को तब कॉल किया जाता है जब हम WETH अनुबंध से टोकनों को वापस ETH में भुनाते हैं। केवल हम जिस WETH अनुबंध का उपयोग करते हैं, वह ऐसा करने के लिए अधिकृत है।
तरलता जोड़ें
ये फ़ंक्शन पेयर एक्सचेंज में टोकन जोड़ते हैं, जिससे तरलता पूल बढ़ता है।
// **** तरलता जोड़ें ****
function _addLiquidity(
इस फ़ंक्शन का उपयोग A और B टोकनों की मात्रा की गणना करने के लिए किया जाता है जिन्हें पेयर एक्सचेंज में जमा किया जाना चाहिए।
address tokenA,
address tokenB,
ये ERC-20 टोकन अनुबंधों के पते हैं।
uint amountADesired,
uint amountBDesired,
ये वे राशियाँ हैं जिन्हें तरलता प्रदाता जमा करना चाहता है। ये जमा किए जाने वाले A और B की अधिकतम राशियाँ भी हैं।
uint amountAMin,
uint amountBMin
ये जमा करने के लिए न्यूनतम स्वीकार्य राशियाँ हैं। यदि लेन-देन इन राशियों या उससे अधिक के साथ नहीं हो सकता है, तो इसे रिवर्ट कर दें। यदि आप यह सुविधा नहीं चाहते हैं, तो बस शून्य निर्दिष्ट करें।
तरलता प्रदाता आमतौर पर एक न्यूनतम निर्दिष्ट करते हैं, क्योंकि वे लेन-देन को उस विनिमय दर तक सीमित करना चाहते हैं जो वर्तमान दर के करीब हो। यदि विनिमय दर में बहुत अधिक उतार-चढ़ाव होता है, तो इसका मतलब ऐसी खबर हो सकती है जो अंतर्निहित मूल्यों को बदल देती है, और वे मैन्युअल रूप से यह तय करना चाहते हैं कि क्या करना है।
उदाहरण के लिए, एक ऐसे मामले की कल्पना करें जहाँ विनिमय दर एक-से-एक है और तरलता प्रदाता इन मानों को निर्दिष्ट करता है:
| Parameter | Value |
|---|---|
| amountADesired | 1000 |
| amountBDesired | 1000 |
| amountAMin | 900 |
| amountBMin | 800 |
जब तक विनिमय दर 0.9 और 1.25 के बीच रहती है, लेन-देन होता है। यदि विनिमय दर उस सीमा से बाहर हो जाती है, तो लेन-देन रद्द हो जाता है।
इस सावधानी का कारण यह है कि लेन-देन तत्काल नहीं होते हैं, आप उन्हें सबमिट करते हैं और अंततः एक सत्यापक उन्हें एक ब्लॉक में शामिल करने जा रहा है (जब तक कि आपका गैस मूल्य बहुत कम न हो, जिस स्थिति में आपको इसे अधिलेखित करने के लिए उसी नॉन्स और उच्च गैस मूल्य के साथ एक और लेन-देन सबमिट करने की आवश्यकता होगी)। आप यह नियंत्रित नहीं कर सकते कि सबमिशन और समावेशन के बीच के अंतराल के दौरान क्या होता है।
) internal virtual returns (uint amountA, uint amountB) {
फ़ंक्शन उन राशियों को लौटाता है जिन्हें तरलता प्रदाता को जमा करना चाहिए ताकि रिज़र्व के बीच वर्तमान अनुपात के बराबर अनुपात हो।
// यदि यह अभी तक मौजूद नहीं है तो पेयर बनाएँ
if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) {
IUniswapV2Factory(factory).createPair(tokenA, tokenB);
}
यदि इस टोकन जोड़े के लिए अभी तक कोई एक्सचेंज नहीं है, तो इसे बनाएँ।
(uint reserveA, uint reserveB) = UniswapV2Library.getReserves(factory, tokenA, tokenB);
जोड़े में वर्तमान रिज़र्व प्राप्त करें।
if (reserveA == 0 && reserveB == 0) {
(amountA, amountB) = (amountADesired, amountBDesired);
यदि वर्तमान रिज़र्व खाली हैं तो यह एक नया पेयर एक्सचेंज है। जमा की जाने वाली राशियाँ बिल्कुल वैसी ही होनी चाहिए जैसी तरलता प्रदाता प्रदान करना चाहता है।
} else {
uint amountBOptimal = UniswapV2Library.quote(amountADesired, reserveA, reserveB);
यदि हमें यह देखने की आवश्यकता है कि राशियाँ क्या होंगी, तो हम इस फ़ंक्शन (opens in a new tab) का उपयोग करके इष्टतम राशि प्राप्त करते हैं। हम वर्तमान रिज़र्व के समान अनुपात चाहते हैं।
if (amountBOptimal <= amountBDesired) {
require(amountBOptimal >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
(amountA, amountB) = (amountADesired, amountBOptimal);
यदि amountBOptimal उस राशि से छोटा है जिसे तरलता प्रदाता जमा करना चाहता है, तो इसका मतलब है कि टोकन B वर्तमान में तरलता जमाकर्ता की सोच से अधिक मूल्यवान है, इसलिए कम राशि की आवश्यकता है।
} else {
uint amountAOptimal = UniswapV2Library.quote(amountBDesired, reserveB, reserveA);
assert(amountAOptimal <= amountADesired);
require(amountAOptimal >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');
(amountA, amountB) = (amountAOptimal, amountBDesired);
यदि इष्टतम B राशि वांछित B राशि से अधिक है, तो इसका मतलब है कि B टोकन वर्तमान में तरलता जमाकर्ता की सोच से कम मूल्यवान हैं, इसलिए अधिक राशि की आवश्यकता है। हालाँकि, वांछित राशि अधिकतम है, इसलिए हम ऐसा नहीं कर सकते। इसके बजाय हम B टोकनों की वांछित मात्रा के लिए A टोकनों की इष्टतम संख्या की गणना करते हैं।
इन सबको एक साथ रखने पर हमें यह ग्राफ़ मिलता है। मान लें कि आप एक हज़ार A टोकन (नीली रेखा) और एक हज़ार B टोकन (लाल रेखा) जमा करने का प्रयास कर रहे हैं। x अक्ष विनिमय दर, A/B है। यदि x=1 है, तो वे मूल्य में समान हैं और आप प्रत्येक के एक हज़ार जमा करते हैं। यदि x=2 है, तो A का मूल्य B से दोगुना है (आपको प्रत्येक A टोकन के लिए दो B टोकन मिलते हैं) इसलिए आप एक हज़ार B टोकन जमा करते हैं, लेकिन केवल 500 A टोकन। यदि x=0.5 है, तो स्थिति उलट जाती है, एक हज़ार A टोकन और पाँच सौ B टोकन।
आप सीधे कोर अनुबंध में तरलता जमा कर सकते हैं (UniswapV2Pair::mint (opens in a new tab) का उपयोग करके), लेकिन कोर अनुबंध केवल यह जाँचता है कि उसके साथ स्वयं धोखा तो नहीं हो रहा है, इसलिए यदि आपके लेन-देन सबमिट करने और उसके निष्पादित होने के समय के बीच विनिमय दर बदलती है, तो आपको मूल्य खोने का जोखिम रहता है। यदि आप परिधि अनुबंध का उपयोग करते हैं, तो यह उस राशि का पता लगाता है जिसे आपको जमा करना चाहिए और इसे तुरंत जमा कर देता है, इसलिए विनिमय दर नहीं बदलती है और आप कुछ भी नहीं खोते हैं।
function addLiquidity(
address tokenA,
address tokenB,
uint amountADesired,
uint amountBDesired,
uint amountAMin,
uint amountBMin,
address to,
uint deadline
तरलता जमा करने के लिए इस फ़ंक्शन को लेन-देन द्वारा कॉल किया जा सकता है। अधिकांश पैरामीटर ऊपर दिए गए _addLiquidity के समान हैं, दो अपवादों के साथ:
. to वह पता है जिसे पूल के तरलता प्रदाता के हिस्से को दिखाने के लिए नए तरलता टोकन मिंट किए जाते हैं
. deadline लेन-देन पर एक समय सीमा है
) external virtual override ensure(deadline) returns (uint amountA, uint amountB, uint liquidity) {
(amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin);
address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
हम वास्तव में जमा की जाने वाली राशियों की गणना करते हैं और फिर तरलता पूल का पता खोजते हैं। गैस बचाने के लिए हम फ़ैक्टरी से पूछकर ऐसा नहीं करते हैं, बल्कि लाइब्रेरी फ़ंक्शन pairFor का उपयोग करते हैं (नीचे लाइब्रेरी में देखें)
TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA);
TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB);
उपयोगकर्ता से पेयर एक्सचेंज में टोकनों की सही मात्रा ट्रांसफर करें।
liquidity = IUniswapV2Pair(pair).mint(to);
}
बदले में पूल के आंशिक स्वामित्व के लिए to पते को तरलता टोकन दें। कोर अनुबंध का mint फ़ंक्शन देखता है कि उसके पास कितने अतिरिक्त टोकन हैं (पिछली बार तरलता बदलने की तुलना में) और तदनुसार तरलता मिंट करता है।
function addLiquidityETH(
address token,
uint amountTokenDesired,
जब कोई तरलता प्रदाता टोकन/ETH पेयर एक्सचेंज को तरलता प्रदान करना चाहता है, तो कुछ अंतर होते हैं। अनुबंध तरलता प्रदाता के लिए ETH को रैप करने का काम संभालता है। यह निर्दिष्ट करने की कोई आवश्यकता नहीं है कि उपयोगकर्ता कितने ETH जमा करना चाहता है, क्योंकि उपयोगकर्ता उन्हें केवल लेन-देन के साथ भेजता है (राशि msg.value में उपलब्ध है)।
uint amountTokenMin,
uint amountETHMin,
address to,
uint deadline
) external virtual override payable ensure(deadline) returns (uint amountToken, uint amountETH, uint liquidity) {
(amountToken, amountETH) = _addLiquidity(
token,
WETH,
amountTokenDesired,
msg.value,
amountTokenMin,
amountETHMin
);
address pair = UniswapV2Library.pairFor(factory, token, WETH);
TransferHelper.safeTransferFrom(token, msg.sender, pair, amountToken);
IWETH(WETH).deposit{value: amountETH}();
assert(IWETH(WETH).transfer(pair, amountETH));
ETH जमा करने के लिए अनुबंध पहले इसे WETH में रैप करता है और फिर WETH को जोड़े में ट्रांसफर करता है। ध्यान दें कि ट्रांसफर एक assert में लिपटा हुआ है। इसका मतलब है कि यदि ट्रांसफर विफल हो जाता है तो यह अनुबंध कॉल भी विफल हो जाती है, और इसलिए रैपिंग वास्तव में नहीं होती है।
liquidity = IUniswapV2Pair(pair).mint(to);
// डस्ट ईथर वापस करें, यदि कोई हो
if (msg.value > amountETH) TransferHelper.safeTransferETH(msg.sender, msg.value - amountETH);
}
उपयोगकर्ता ने हमें पहले ही ETH भेज दिया है, इसलिए यदि कोई अतिरिक्त बचा है (क्योंकि दूसरा टोकन उपयोगकर्ता की सोच से कम मूल्यवान है), तो हमें रिफंड जारी करने की आवश्यकता है।
तरलता हटाएँ
ये फ़ंक्शन तरलता को हटा देंगे और तरलता प्रदाता को वापस भुगतान करेंगे।
// **** तरलता हटाएँ ****
function removeLiquidity(
address tokenA,
address tokenB,
uint liquidity,
uint amountAMin,
uint amountBMin,
address to,
uint deadline
) public virtual override ensure(deadline) returns (uint amountA, uint amountB) {
तरलता हटाने का सबसे सरल मामला। प्रत्येक टोकन की एक न्यूनतम राशि होती है जिसे तरलता प्रदाता स्वीकार करने के लिए सहमत होता है, और यह समय सीमा से पहले होना चाहिए।
address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
IUniswapV2Pair(pair).transferFrom(msg.sender, pair, liquidity); // पेयर को तरलता भेजें
(uint amount0, uint amount1) = IUniswapV2Pair(pair).burn(to);
कोर अनुबंध का burn फ़ंक्शन उपयोगकर्ता को टोकन वापस भुगतान करने का काम संभालता है।
(address token0,) = UniswapV2Library.sortTokens(tokenA, tokenB);
जब कोई फ़ंक्शन कई मान लौटाता है, लेकिन हम उनमें से केवल कुछ में रुचि रखते हैं, तो इस तरह हम केवल वे मान प्राप्त करते हैं। गैस के संदर्भ में यह किसी मान को पढ़ने और उसका कभी उपयोग न करने की तुलना में कुछ सस्ता है।
(amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0);
राशियों को उस तरीके से अनुवादित करें जिस तरह से कोर अनुबंध उन्हें लौटाता है (निचला पता टोकन पहले) उस तरीके में जिसकी उपयोगकर्ता उनसे अपेक्षा करता है (tokenA और tokenB के अनुरूप)।
require(amountA >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');
require(amountB >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
}
पहले ट्रांसफर करना और फिर यह सत्यापित करना ठीक है कि यह वैध है, क्योंकि यदि ऐसा नहीं है तो हम सभी स्थिति परिवर्तनों को रिवर्ट कर देंगे।
function removeLiquidityETH(
address token,
uint liquidity,
uint amountTokenMin,
uint amountETHMin,
address to,
uint deadline
) public virtual override ensure(deadline) returns (uint amountToken, uint amountETH) {
(amountToken, amountETH) = removeLiquidity(
token,
WETH,
liquidity,
amountTokenMin,
amountETHMin,
address(this),
deadline
);
TransferHelper.safeTransfer(token, to, amountToken);
IWETH(WETH).withdraw(amountETH);
TransferHelper.safeTransferETH(to, amountETH);
}
ETH के लिए तरलता हटाना लगभग समान है, सिवाय इसके कि हम WETH टोकन प्राप्त करते हैं और फिर तरलता प्रदाता को वापस देने के लिए उन्हें ETH के लिए भुनाते हैं।
function removeLiquidityWithPermit(
address tokenA,
address tokenB,
uint liquidity,
uint amountAMin,
uint amountBMin,
address to,
uint deadline,
bool approveMax, uint8 v, bytes32 r, bytes32 s
) external virtual override returns (uint amountA, uint amountB) {
address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
uint value = approveMax ? uint(-1) : liquidity;
IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);
(amountA, amountB) = removeLiquidity(tokenA, tokenB, liquidity, amountAMin, amountBMin, to, deadline);
}
function removeLiquidityETHWithPermit(
address token,
uint liquidity,
uint amountTokenMin,
uint amountETHMin,
address to,
uint deadline,
bool approveMax, uint8 v, bytes32 r, bytes32 s
) external virtual override returns (uint amountToken, uint amountETH) {
address pair = UniswapV2Library.pairFor(factory, token, WETH);
uint value = approveMax ? uint(-1) : liquidity;
IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);
(amountToken, amountETH) = removeLiquidityETH(token, liquidity, amountTokenMin, amountETHMin, to, deadline);
}
ये फ़ंक्शन परमिट तंत्र का उपयोग करके, बिना ईथर वाले उपयोगकर्ताओं को पूल से निकासी करने की अनुमति देने के लिए मेटा-लेन-देन को रिले करते हैं।
// **** तरलता हटाएँ (फी-ऑन-ट्रांसफर टोकन का समर्थन करते हुए) ****
function removeLiquidityETHSupportingFeeOnTransferTokens(
address token,
uint liquidity,
uint amountTokenMin,
uint amountETHMin,
address to,
uint deadline
) public virtual override ensure(deadline) returns (uint amountETH) {
(, amountETH) = removeLiquidity(
token,
WETH,
liquidity,
amountTokenMin,
amountETHMin,
address(this),
deadline
);
TransferHelper.safeTransfer(token, to, IERC20(token).balanceOf(address(this)));
IWETH(WETH).withdraw(amountETH);
TransferHelper.safeTransferETH(to, amountETH);
}
इस फ़ंक्शन का उपयोग उन टोकनों के लिए किया जा सकता है जिनमें ट्रांसफर या स्टोरेज शुल्क होता है। जब किसी टोकन में ऐसा शुल्क होता है तो हम यह बताने के लिए removeLiquidity फ़ंक्शन पर भरोसा नहीं कर सकते कि हमें कितना टोकन वापस मिलता है, इसलिए हमें पहले निकासी करनी होगी और फिर बैलेंस प्राप्त करना होगा।
function removeLiquidityETHWithPermitSupportingFeeOnTransferTokens(
address token,
uint liquidity,
uint amountTokenMin,
uint amountETHMin,
address to,
uint deadline,
bool approveMax, uint8 v, bytes32 r, bytes32 s
) external virtual override returns (uint amountETH) {
address pair = UniswapV2Library.pairFor(factory, token, WETH);
uint value = approveMax ? uint(-1) : liquidity;
IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);
amountETH = removeLiquidityETHSupportingFeeOnTransferTokens(
token, liquidity, amountTokenMin, amountETHMin, to, deadline
);
}
अंतिम फ़ंक्शन स्टोरेज शुल्क को मेटा-लेन-देन के साथ जोड़ता है।
व्यापार
// **** स्वैप ****
// इसके लिए आवश्यक है कि प्रारंभिक राशि पहले ही पहले पेयर को भेजी जा चुकी हो
function _swap(uint[] memory amounts, address[] memory path, address _to) internal virtual {
यह फ़ंक्शन आंतरिक प्रसंस्करण करता है जो उन फ़ंक्शनों के लिए आवश्यक है जो व्यापारियों के सामने आते हैं।
for (uint i; i < path.length - 1; i++) {
जब मैं यह लिख रहा हूँ तब 388,160 ERC-20 टोकन (opens in a new tab) हैं। यदि प्रत्येक टोकन जोड़े के लिए एक पेयर एक्सचेंज होता, तो यह 150 बिलियन से अधिक पेयर एक्सचेंज होते। पूरी चेन में, इस समय, खातों की संख्या का केवल 0.1% है (opens in a new tab)। इसके बजाय, स्वैप फ़ंक्शन एक पथ (path) की अवधारणा का समर्थन करते हैं। एक व्यापारी A को B के लिए, B को C के लिए, और C को D के लिए विनिमय कर सकता है, इसलिए प्रत्यक्ष A-D पेयर एक्सचेंज की कोई आवश्यकता नहीं है।
इन बाज़ारों में कीमतें सिंक्रनाइज़ (synchronized) होती हैं, क्योंकि जब वे सिंक से बाहर होती हैं तो यह मध्यस्थता (arbitrage) का अवसर पैदा करता है। उदाहरण के लिए, तीन टोकन, A, B, और C की कल्पना करें। तीन पेयर एक्सचेंज हैं, प्रत्येक जोड़े के लिए एक।
- प्रारंभिक स्थिति
- एक व्यापारी 24.695 A टोकन बेचता है और 25.305 B टोकन प्राप्त करता है।
- व्यापारी 25.305 C टोकन के लिए 24.695 B टोकन बेचता है, और लगभग 0.61 B टोकन लाभ के रूप में रखता है।
- फिर व्यापारी 25.305 A टोकन के लिए 24.695 C टोकन बेचता है, और लगभग 0.61 C टोकन लाभ के रूप में रखता है। व्यापारी के पास 0.61 अतिरिक्त A टोकन भी हैं (25.305 जिसके साथ व्यापारी समाप्त होता है, माइनस 24.695 का मूल निवेश)।
| चरण | A-B Exchange | B-C Exchange | A-C Exchange |
|---|---|---|---|
| 1 | A:1000 B:1050 A/B=1.05 | B:1000 C:1050 B/C=1.05 | A:1050 C:1000 C/A=1.05 |
| 2 | A:1024.695 B:1024.695 A/B=1 | B:1000 C:1050 B/C=1.05 | A:1050 C:1000 C/A=1.05 |
| 3 | A:1024.695 B:1024.695 A/B=1 | B:1024.695 C:1024.695 B/C=1 | A:1050 C:1000 C/A=1.05 |
| 4 | A:1024.695 B:1024.695 A/B=1 | B:1024.695 C:1024.695 B/C=1 | A:1024.695 C:1024.695 C/A=1 |
(address input, address output) = (path[i], path[i + 1]);
(address token0,) = UniswapV2Library.sortTokens(input, output);
uint amountOut = amounts[i + 1];
वह जोड़ा प्राप्त करें जिसे हम वर्तमान में संभाल रहे हैं, इसे सॉर्ट करें (जोड़े के साथ उपयोग के लिए) और अपेक्षित आउटपुट राशि प्राप्त करें।
(uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOut) : (amountOut, uint(0));
अपेक्षित आउटपुट राशियाँ प्राप्त करें, जिन्हें उस तरीके से सॉर्ट किया गया है जिस तरह से पेयर एक्सचेंज उनसे अपेक्षा करता है।
address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to;
क्या यह अंतिम एक्सचेंज है? यदि हाँ, तो व्यापार के लिए प्राप्त टोकनों को गंतव्य पर भेजें। यदि नहीं, तो इसे अगले पेयर एक्सचेंज में भेजें।
IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output)).swap(
amount0Out, amount1Out, to, new bytes(0)
);
}
}
टोकनों को स्वैप करने के लिए वास्तव में पेयर एक्सचेंज को कॉल करें। हमें एक्सचेंज के बारे में बताने के लिए कॉलबैक की आवश्यकता नहीं है, इसलिए हम उस फ़ील्ड में कोई बाइट नहीं भेजते हैं।
function swapExactTokensForTokens(
इस फ़ंक्शन का उपयोग व्यापारियों द्वारा सीधे एक टोकन को दूसरे के लिए स्वैप करने के लिए किया जाता है।
uint amountIn,
uint amountOutMin,
address[] calldata path,
इस पैरामीटर में ERC-20 अनुबंधों के पते होते हैं। जैसा कि ऊपर बताया गया है, यह एक ऐरे (array) है क्योंकि आपके पास जो संपत्ति है उससे अपनी मनचाही संपत्ति प्राप्त करने के लिए आपको कई पेयर एक्सचेंजों से गुज़रना पड़ सकता है।
Solidity में एक फ़ंक्शन पैरामीटर को memory या calldata में संग्रहीत किया जा सकता है। यदि फ़ंक्शन अनुबंध का एक प्रवेश बिंदु (entry point) है, जिसे सीधे उपयोगकर्ता (लेन-देन का उपयोग करके) या किसी भिन्न अनुबंध से कॉल किया जाता है, तो पैरामीटर का मान सीधे कॉल डेटा से लिया जा सकता है। यदि फ़ंक्शन को आंतरिक रूप से कॉल किया जाता है, जैसा कि ऊपर _swap है, तो पैरामीटर को memory में संग्रहीत किया जाना चाहिए। कॉल किए गए अनुबंध के दृष्टिकोण से calldata केवल पढ़ने योग्य (read only) है।
uint या address जैसे स्केलर प्रकारों के साथ कंपाइलर हमारे लिए स्टोरेज का विकल्प संभालता है, लेकिन ऐरे के साथ, जो लंबे और अधिक महंगे होते हैं, हम उपयोग किए जाने वाले स्टोरेज का प्रकार निर्दिष्ट करते हैं।
address to,
uint deadline
) external virtual override ensure(deadline) returns (uint[] memory amounts) {
रिटर्न मान हमेशा मेमोरी में लौटाए जाते हैं।
amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);
require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
प्रत्येक स्वैप में खरीदी जाने वाली राशि की गणना करें। यदि परिणाम उस न्यूनतम से कम है जिसे व्यापारी स्वीकार करने को तैयार है, तो लेन-देन को रिवर्ट कर दें।
TransferHelper.safeTransferFrom(
path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
);
_swap(amounts, path, to);
}
अंत में, पहले पेयर एक्सचेंज के लिए प्रारंभिक ERC-20 टोकन को खाते में ट्रांसफर करें और _swap को कॉल करें। यह सब एक ही लेन-देन में हो रहा है, इसलिए पेयर एक्सचेंज जानता है कि कोई भी अप्रत्याशित टोकन इस ट्रांसफर का हिस्सा हैं।
function swapTokensForExactTokens(
uint amountOut,
uint amountInMax,
address[] calldata path,
address to,
uint deadline
) external virtual override ensure(deadline) returns (uint[] memory amounts) {
amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);
require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');
TransferHelper.safeTransferFrom(
path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
);
_swap(amounts, path, to);
}
पिछला फ़ंक्शन, swapTokensForTokens, एक व्यापारी को इनपुट टोकनों की सटीक संख्या निर्दिष्ट करने की अनुमति देता है जो वह देने को तैयार है और आउटपुट टोकनों की न्यूनतम संख्या जो वह बदले में प्राप्त करने को तैयार है। यह फ़ंक्शन रिवर्स स्वैप करता है, यह एक व्यापारी को उन आउटपुट टोकनों की संख्या निर्दिष्ट करने देता है जो वह चाहता है, और इनपुट टोकनों की अधिकतम संख्या जो वह उनके लिए भुगतान करने को तैयार है।
दोनों ही मामलों में, व्यापारी को पहले इस परिधि अनुबंध को एक व्यय सीमा (allowance) देनी होगी ताकि वह उन्हें ट्रांसफर कर सके।
function swapExactETHForTokens(uint amountOutMin, address[] calldata path, address to, uint deadline)
external
virtual
override
payable
ensure(deadline)
returns (uint[] memory amounts)
{
require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');
amounts = UniswapV2Library.getAmountsOut(factory, msg.value, path);
require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
IWETH(WETH).deposit{value: amounts[0]}();
assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]));
_swap(amounts, path, to);
}
function swapTokensForExactETH(uint amountOut, uint amountInMax, address[] calldata path, address to, uint deadline)
external
virtual
override
ensure(deadline)
returns (uint[] memory amounts)
{
require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH');
amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);
require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');
TransferHelper.safeTransferFrom(
path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
);
_swap(amounts, path, address(this));
IWETH(WETH).withdraw(amounts[amounts.length - 1]);
TransferHelper.safeTransferETH(to, amounts[amounts.length - 1]);
}
function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline)
external
virtual
override
ensure(deadline)
returns (uint[] memory amounts)
{
require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH');
amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);
require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
TransferHelper.safeTransferFrom(
path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
);
_swap(amounts, path, address(this));
IWETH(WETH).withdraw(amounts[amounts.length - 1]);
TransferHelper.safeTransferETH(to, amounts[amounts.length - 1]);
}
function swapETHForExactTokens(uint amountOut, address[] calldata path, address to, uint deadline)
external
virtual
override
payable
ensure(deadline)
returns (uint[] memory amounts)
{
require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');
amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);
require(amounts[0] <= msg.value, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');
IWETH(WETH).deposit{value: amounts[0]}();
assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]));
_swap(amounts, path, to);
// डस्ट ईथर वापस करें, यदि कोई हो
if (msg.value > amounts[0]) TransferHelper.safeTransferETH(msg.sender, msg.value - amounts[0]);
}
इन चारों प्रकारों में ETH और टोकनों के बीच व्यापार शामिल है। एकमात्र अंतर यह है कि हम या तो व्यापारी से ETH प्राप्त करते हैं और इसका उपयोग WETH मिंट करने के लिए करते हैं, या हम पथ में अंतिम एक्सचेंज से WETH प्राप्त करते हैं और इसे बर्न करते हैं, जिससे व्यापारी को परिणामी ETH वापस मिल जाता है।
// **** स्वैप (फी-ऑन-ट्रांसफर टोकन का समर्थन करते हुए) ****
// इसके लिए आवश्यक है कि प्रारंभिक राशि पहले ही पहले पेयर को भेजी जा चुकी हो
function _swapSupportingFeeOnTransferTokens(address[] memory path, address _to) internal virtual {
यह उन टोकनों को स्वैप करने के लिए आंतरिक फ़ंक्शन है जिनमें (इस समस्या (opens in a new tab)) को हल करने के लिए ट्रांसफर या स्टोरेज शुल्क होता है।
for (uint i; i < path.length - 1; i++) {
(address input, address output) = (path[i], path[i + 1]);
(address token0,) = UniswapV2Library.sortTokens(input, output);
IUniswapV2Pair pair = IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output));
uint amountInput;
uint amountOutput;
{ // स्टैक टू डीप त्रुटियों से बचने के लिए स्कोप
(uint reserve0, uint reserve1,) = pair.getReserves();
(uint reserveInput, uint reserveOutput) = input == token0 ? (reserve0, reserve1) : (reserve1, reserve0);
amountInput = IERC20(input).balanceOf(address(pair)).sub(reserveInput);
amountOutput = UniswapV2Library.getAmountOut(amountInput, reserveInput, reserveOutput);
ट्रांसफर शुल्क के कारण हम यह बताने के लिए getAmountsOut फ़ंक्शन पर भरोसा नहीं कर सकते कि हमें प्रत्येक ट्रांसफर से कितना मिलता है (जिस तरह से हम मूल _swap को कॉल करने से पहले करते हैं)। इसके बजाय हमें पहले ट्रांसफर करना होगा और फिर देखना होगा कि हमें कितने टोकन वापस मिले।
नोट: सिद्धांत रूप में हम _swap के बजाय केवल इस फ़ंक्शन का उपयोग कर सकते हैं, लेकिन कुछ मामलों में (उदाहरण के लिए, यदि ट्रांसफर अंततः रिवर्ट हो जाता है क्योंकि आवश्यक न्यूनतम को पूरा करने के लिए अंत में पर्याप्त नहीं है) तो इसमें अधिक गैस खर्च होगी। ट्रांसफर शुल्क टोकन काफी दुर्लभ हैं, इसलिए यद्यपि हमें उन्हें समायोजित करने की आवश्यकता है, लेकिन सभी स्वैप के लिए यह मानने की कोई आवश्यकता नहीं है कि वे उनमें से कम से कम एक से गुज़रते हैं।
}
(uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOutput) : (amountOutput, uint(0));
address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to;
pair.swap(amount0Out, amount1Out, to, new bytes(0));
}
}
function swapExactTokensForTokensSupportingFeeOnTransferTokens(
uint amountIn,
uint amountOutMin,
address[] calldata path,
address to,
uint deadline
) external virtual override ensure(deadline) {
TransferHelper.safeTransferFrom(
path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn
);
uint balanceBefore = IERC20(path[path.length - 1]).balanceOf(to);
_swapSupportingFeeOnTransferTokens(path, to);
require(
IERC20(path[path.length - 1]).balanceOf(to).sub(balanceBefore) >= amountOutMin,
'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'
);
}
function swapExactETHForTokensSupportingFeeOnTransferTokens(
uint amountOutMin,
address[] calldata path,
address to,
uint deadline
)
external
virtual
override
payable
ensure(deadline)
{
require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');
uint amountIn = msg.value;
IWETH(WETH).deposit{value: amountIn}();
assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn));
uint balanceBefore = IERC20(path[path.length - 1]).balanceOf(to);
_swapSupportingFeeOnTransferTokens(path, to);
require(
IERC20(path[path.length - 1]).balanceOf(to).sub(balanceBefore) >= amountOutMin,
'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'
);
}
function swapExactTokensForETHSupportingFeeOnTransferTokens(
uint amountIn,
uint amountOutMin,
address[] calldata path,
address to,
uint deadline
)
external
virtual
override
ensure(deadline)
{
require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH');
TransferHelper.safeTransferFrom(
path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn
);
_swapSupportingFeeOnTransferTokens(path, address(this));
uint amountOut = IERC20(WETH).balanceOf(address(this));
require(amountOut >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
IWETH(WETH).withdraw(amountOut);
TransferHelper.safeTransferETH(to, amountOut);
}
ये सामान्य टोकनों के लिए उपयोग किए जाने वाले समान प्रकार हैं, लेकिन वे इसके बजाय _swapSupportingFeeOnTransferTokens को कॉल करते हैं।
// **** लाइब्रेरी फ़ंक्शंस ****
function quote(uint amountA, uint reserveA, uint reserveB) public pure virtual override returns (uint amountB) {
return UniswapV2Library.quote(amountA, reserveA, reserveB);
}
function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut)
public
pure
virtual
override
returns (uint amountOut)
{
return UniswapV2Library.getAmountOut(amountIn, reserveIn, reserveOut);
}
function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut)
public
pure
virtual
override
returns (uint amountIn)
{
return UniswapV2Library.getAmountIn(amountOut, reserveIn, reserveOut);
}
function getAmountsOut(uint amountIn, address[] memory path)
public
view
virtual
override
returns (uint[] memory amounts)
{
return UniswapV2Library.getAmountsOut(factory, amountIn, path);
}
function getAmountsIn(uint amountOut, address[] memory path)
public
view
virtual
override
returns (uint[] memory amounts)
{
return UniswapV2Library.getAmountsIn(factory, amountOut, path);
}
}
ये फ़ंक्शन केवल प्रॉक्सी हैं जो UniswapV2Library फ़ंक्शनों को कॉल करते हैं।
UniswapV2Migrator.sol
इस अनुबंध का उपयोग पुराने v1 से v2 में एक्सचेंजों को माइग्रेट करने के लिए किया गया था। अब जब उन्हें माइग्रेट कर दिया गया है, तो यह अब प्रासंगिक नहीं है।
लाइब्रेरीज़
SafeMath लाइब्रेरी (opens in a new tab) अच्छी तरह से प्रलेखित है, इसलिए इसे यहाँ प्रलेखित करने की कोई आवश्यकता नहीं है।
Math
इस लाइब्रेरी में कुछ गणितीय फ़ंक्शन शामिल हैं जिनकी सामान्यतः Solidity कोड में आवश्यकता नहीं होती है, इसलिए वे भाषा का हिस्सा नहीं हैं।
pragma solidity =0.5.16;
// विभिन्न गणितीय संचालन करने के लिए एक लाइब्रेरी
library Math {
function min(uint x, uint y) internal pure returns (uint z) {
z = x < y ? x : y;
}
// बेबीलोनियन विधि (https://wikipedia.org/wiki/Methods_of_computing_square_roots#Babylonian_method)
function sqrt(uint y) internal pure returns (uint z) {
if (y > 3) {
z = y;
uint x = y / 2 + 1;
x को एक अनुमान के रूप में शुरू करें जो वर्गमूल से अधिक हो (यही कारण है कि हमें 1-3 को विशेष मामलों के रूप में मानने की आवश्यकता है)।
while (x < z) {
z = x;
x = (y / x + x) / 2;
एक नज़दीकी अनुमान प्राप्त करें, जो पिछले अनुमान और उस संख्या (जिसका वर्गमूल हम खोजने का प्रयास कर रहे हैं) को पिछले अनुमान से विभाजित करने पर प्राप्त औसत हो। तब तक दोहराएं जब तक कि नया अनुमान मौजूदा अनुमान से कम न हो जाए। अधिक जानकारी के लिए, यहाँ देखें (opens in a new tab)।
}
} else if (y != 0) {
z = 1;
हमें कभी भी शून्य के वर्गमूल की आवश्यकता नहीं होनी चाहिए। एक, दो और तीन के वर्गमूल लगभग एक होते हैं (हम पूर्णांकों का उपयोग करते हैं, इसलिए हम भिन्न को अनदेखा करते हैं)।
}
}
}
फिक्स्ड पॉइंट फ्रैक्शंस (UQ112x112)
यह लाइब्रेरी भिन्नों को संभालती है, जो सामान्यतः इथेरियम अंकगणित का हिस्सा नहीं होते हैं। यह संख्या x को x*2^112 के रूप में एन्कोड करके ऐसा करता है। यह हमें बिना किसी बदलाव के मूल जोड़ और घटाव ऑपकोड का उपयोग करने देता है।
pragma solidity =0.5.16;
// बाइनरी फिक्स्ड पॉइंट नंबरों को संभालने के लिए एक लाइब्रेरी (https://wikipedia.org/wiki/Q_(number_format))
// रेंज: [0, 2**112 - 1]
// रिज़ॉल्यूशन: 1 / 2**112
library UQ112x112 {
uint224 constant Q112 = 2**112;
Q112 एक (1) के लिए एन्कोडिंग है।
// uint112 को UQ112x112 के रूप में एनकोड करें
function encode(uint112 y) internal pure returns (uint224 z) {
z = uint224(y) * Q112; // कभी ओवरफ्लो नहीं होता है
}
चूंकि y uint112 है, इसलिए यह अधिकतम 2^112-1 हो सकता है। उस संख्या को अभी भी UQ112x112 के रूप में एन्कोड किया जा सकता है।
// UQ112x112 को uint112 से विभाजित करें, UQ112x112 लौटाते हुए
function uqdiv(uint224 x, uint112 y) internal pure returns (uint224 z) {
z = x / uint224(y);
}
}
यदि हम दो UQ112x112 मानों को विभाजित करते हैं, तो परिणाम अब 2^112 से गुणा नहीं होता है। इसलिए इसके बजाय हम हर के लिए एक पूर्णांक लेते हैं। हमें गुणा करने के लिए भी इसी तरह की तरकीब का उपयोग करने की आवश्यकता होती, लेकिन हमें UQ112x112 मानों का गुणा करने की आवश्यकता नहीं है।
UniswapV2Library
इस लाइब्रेरी का उपयोग केवल परिधि (periphery) अनुबंधों द्वारा किया जाता है
pragma solidity >=0.5.0;
import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol';
import "./SafeMath.sol";
library UniswapV2Library {
using SafeMath for uint;
// सॉर्ट किए गए टोकन पते लौटाता है, इस क्रम में सॉर्ट किए गए पेयर से रिटर्न वैल्यू को संभालने के लिए उपयोग किया जाता है
function sortTokens(address tokenA, address tokenB) internal pure returns (address token0, address token1) {
require(tokenA != tokenB, 'UniswapV2Library: IDENTICAL_ADDRESSES');
(token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
require(token0 != address(0), 'UniswapV2Library: ZERO_ADDRESS');
}
दोनों टोकनों को पते के अनुसार क्रमबद्ध करें, ताकि हम उनके लिए पेयर एक्सचेंज का पता प्राप्त कर सकें। यह आवश्यक है क्योंकि अन्यथा हमारे पास दो संभावनाएं होंगी, एक पैरामीटर A,B के लिए और दूसरी पैरामीटर B,A के लिए, जिससे एक के बजाय दो एक्सचेंज बन जाएंगे।
// बिना कोई बाहरी कॉल किए पेयर के लिए CREATE2 पते की गणना करता है
function pairFor(address factory, address tokenA, address tokenB) internal pure returns (address pair) {
(address token0, address token1) = sortTokens(tokenA, tokenB);
pair = address(uint(keccak256(abi.encodePacked(
hex'ff',
factory,
keccak256(abi.encodePacked(token0, token1)),
hex'96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f' // इनिट कोड हैश
))));
}
यह फ़ंक्शन दो टोकनों के लिए पेयर एक्सचेंज के पते की गणना करता है। यह अनुबंध CREATE2 ऑपकोड (opens in a new tab) का उपयोग करके बनाया गया है, इसलिए यदि हम इसके द्वारा उपयोग किए जाने वाले मापदंडों को जानते हैं तो हम उसी एल्गोरिदम का उपयोग करके पते की गणना कर सकते हैं। यह फ़ैक्टरी से पूछने की तुलना में बहुत सस्ता है, और
// पेयर के लिए रिज़र्व प्राप्त करता है और सॉर्ट करता है
function getReserves(address factory, address tokenA, address tokenB) internal view returns (uint reserveA, uint reserveB) {
(address token0,) = sortTokens(tokenA, tokenB);
(uint reserve0, uint reserve1,) = IUniswapV2Pair(pairFor(factory, tokenA, tokenB)).getReserves();
(reserveA, reserveB) = tokenA == token0 ? (reserve0, reserve1) : (reserve1, reserve0);
}
यह फ़ंक्शन उन दो टोकनों के रिज़र्व लौटाता है जो पेयर एक्सचेंज के पास हैं। ध्यान दें कि यह टोकनों को किसी भी क्रम में प्राप्त कर सकता है, और उन्हें आंतरिक उपयोग के लिए क्रमबद्ध करता है।
// किसी एसेट की कुछ मात्रा और पेयर रिज़र्व दिए जाने पर, दूसरे एसेट की समतुल्य मात्रा लौटाता है
function quote(uint amountA, uint reserveA, uint reserveB) internal pure returns (uint amountB) {
require(amountA > 0, 'UniswapV2Library: INSUFFICIENT_AMOUNT');
require(reserveA > 0 && reserveB > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
amountB = amountA.mul(reserveB) / reserveA;
}
यह फ़ंक्शन आपको टोकन B की वह मात्रा देता है जो आपको टोकन A के बदले में मिलेगी यदि इसमें कोई शुल्क शामिल नहीं है। यह गणना इस बात को ध्यान में रखती है कि ट्रांसफर विनिमय दर को बदल देता है।
// किसी एसेट की इनपुट मात्रा और पेयर रिज़र्व दिए जाने पर, दूसरे एसेट की अधिकतम आउटपुट मात्रा लौटाता है
function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) internal pure returns (uint amountOut) {
ऊपर दिया गया quote फ़ंक्शन बहुत अच्छा काम करता है यदि पेयर एक्सचेंज का उपयोग करने के लिए कोई शुल्क नहीं है। हालाँकि, यदि 0.3% एक्सचेंज शुल्क है, तो आपको वास्तव में मिलने वाली राशि कम होती है। यह फ़ंक्शन एक्सचेंज शुल्क के बाद की राशि की गणना करता है।
require(amountIn > 0, 'UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT');
require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
uint amountInWithFee = amountIn.mul(997);
uint numerator = amountInWithFee.mul(reserveOut);
uint denominator = reserveIn.mul(1000).add(amountInWithFee);
amountOut = numerator / denominator;
}
Solidity मूल रूप से भिन्नों को नहीं संभालता है, इसलिए हम केवल राशि को 0.997 से गुणा नहीं कर सकते हैं। इसके बजाय, हम अंश को 997 से और हर को 1000 से गुणा करते हैं, जिससे समान प्रभाव प्राप्त होता है।
// किसी एसेट की आउटपुट मात्रा और पेयर रिज़र्व दिए जाने पर, दूसरे एसेट की आवश्यक इनपुट मात्रा लौटाता है
function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) internal pure returns (uint amountIn) {
require(amountOut > 0, 'UniswapV2Library: INSUFFICIENT_OUTPUT_AMOUNT');
require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
uint numerator = reserveIn.mul(amountOut).mul(1000);
uint denominator = reserveOut.sub(amountOut).mul(997);
amountIn = (numerator / denominator).add(1);
}
यह फ़ंक्शन लगभग वही काम करता है, लेकिन यह आउटपुट राशि प्राप्त करता है और इनपुट प्रदान करता है।
// किसी भी संख्या में पेयर पर चेन्ड getAmountOut गणनाएँ करता है
function getAmountsOut(address factory, uint amountIn, address[] memory path) internal view returns (uint[] memory amounts) {
require(path.length >= 2, 'UniswapV2Library: INVALID_PATH');
amounts = new uint[](path.length);
amounts[0] = amountIn;
for (uint i; i < path.length - 1; i++) {
(uint reserveIn, uint reserveOut) = getReserves(factory, path[i], path[i + 1]);
amounts[i + 1] = getAmountOut(amounts[i], reserveIn, reserveOut);
}
}
// किसी भी संख्या में पेयर पर चेन्ड getAmountIn गणनाएँ करता है
function getAmountsIn(address factory, uint amountOut, address[] memory path) internal view returns (uint[] memory amounts) {
require(path.length >= 2, 'UniswapV2Library: INVALID_PATH');
amounts = new uint[](path.length);
amounts[amounts.length - 1] = amountOut;
for (uint i = path.length - 1; i > 0; i--) {
(uint reserveIn, uint reserveOut) = getReserves(factory, path[i - 1], path[i]);
amounts[i - 1] = getAmountIn(amounts[i], reserveIn, reserveOut);
}
}
}
ये दो फ़ंक्शन उन मानों की पहचान करने का काम करते हैं जब कई पेयर एक्सचेंजों से गुज़रना आवश्यक होता है।
ट्रांसफर हेल्पर
यह लाइब्रेरी (opens in a new tab) ERC-20 और इथेरियम ट्रांसफर के आसपास सफलता की जाँच जोड़ती है ताकि एक रिवर्ट और एक false मान वापसी को एक ही तरह से माना जा सके।
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity >=0.6.0;
// ERC-20 टोकन के साथ इंटरैक्ट करने और ETH भेजने के लिए हेल्पर विधियाँ जो लगातार true/false नहीं लौटाती हैं
library TransferHelper {
function safeApprove(
address token,
address to,
uint256 value
) internal {
// bytes4(keccak256(bytes('approve(address,uint256)')));
(bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x095ea7b3, to, value));
हम किसी भिन्न अनुबंध को दो में से किसी एक तरीके से कॉल कर सकते हैं:
- फ़ंक्शन कॉल बनाने के लिए इंटरफ़ेस परिभाषा का उपयोग करें
- कॉल बनाने के लिए "मैन्युअल रूप से" एप्लिकेशन बाइनरी इंटरफ़ेस (ABI) (opens in a new tab) का उपयोग करें। कोड के लेखक ने यही करने का निर्णय लिया है।
require(
success && (data.length == 0 || abi.decode(data, (bool))),
'TransferHelper::safeApprove: approve failed'
);
}
ERC-20 मानक से पहले बनाए गए टोकन के साथ बैकवर्ड संगतता के लिए, एक ERC-20 कॉल या तो रिवर्ट होकर विफल हो सकती है (जिस स्थिति में success false होता है) या सफल होकर false मान लौटा सकती है (जिस स्थिति में आउटपुट डेटा होता है, और यदि आप इसे बूलियन के रूप में डिकोड करते हैं तो आपको false मिलता है)।
function safeTransfer(
address token,
address to,
uint256 value
) internal {
// bytes4(keccak256(bytes('transfer(address,uint256)')));
(bool success, bytes memory data) = token.call(abi.encodeWithSelector(0xa9059cbb, to, value));
require(
success && (data.length == 0 || abi.decode(data, (bool))),
'TransferHelper::safeTransfer: transfer failed'
);
}
यह फ़ंक्शन ERC-20 की ट्रांसफर कार्यक्षमता (opens in a new tab) को लागू करता है, जो एक खाते को किसी भिन्न खाते द्वारा प्रदान की गई व्यय सीमा को खर्च करने की अनुमति देता है।
function safeTransferFrom(
address token,
address from,
address to,
uint256 value
) internal {
// bytes4(keccak256(bytes('transferFrom(address,address,uint256)')));
(bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x23b872dd, from, to, value));
require(
success && (data.length == 0 || abi.decode(data, (bool))),
'TransferHelper::transferFrom: transferFrom failed'
);
}
यह फ़ंक्शन ERC-20 की transferFrom कार्यक्षमता (opens in a new tab) को लागू करता है, जो एक खाते को किसी भिन्न खाते द्वारा प्रदान की गई व्यय सीमा को खर्च करने की अनुमति देता है।
function safeTransferETH(address to, uint256 value) internal {
(bool success, ) = to.call{value: value}(new bytes(0));
require(success, 'TransferHelper::safeTransferETH: ETH transfer failed');
}
}
यह फ़ंक्शन किसी खाते में ईथर ट्रांसफर करता है। किसी भिन्न अनुबंध पर कोई भी कॉल ईथर भेजने का प्रयास कर सकती है। क्योंकि हमें वास्तव में किसी फ़ंक्शन को कॉल करने की आवश्यकता नहीं है, इसलिए हम कॉल के साथ कोई डेटा नहीं भेजते हैं।
निष्कर्ष
यह लगभग 50 पृष्ठों का एक लंबा लेख है। यदि आप यहाँ तक पहुँच गए हैं, तो बधाई हो! उम्मीद है कि अब तक आप एक वास्तविक एप्लिकेशन (छोटे नमूना प्रोग्रामों के विपरीत) लिखने से जुड़ी बातों को समझ गए होंगे और अपने स्वयं के उपयोग के मामलों के लिए अनुबंध लिखने में अधिक सक्षम होंगे।
अब जाएँ और कुछ उपयोगी लिखें और हमें आश्चर्यचकित करें।
