Tutorial·8 min का पठन·Solingo द्वारा

Uniswap V2 स्टाइल का AMM शुरू से बनाएं

एक minimal Uniswap V2 Pair contract बनाकर समझें कि automated market maker असल में कैसे काम करता है: x*y=k invariant, LP token minting, 0.3 percent swap fee, और getAmountOut का गणित।

# Uniswap V2 स्टाइल का AMM शुरू से बनाएं

Automated market makers ने order book को एक सरल समीकरण से बदल दिया। खरीदारों और विक्रेताओं को मिलाने के बजाय, एक Uniswap V2 pool दो token reserves रखता है और किसी को भी उनके खिलाफ उस price पर trade करने देता है जो गणित अपने आप तय करता है। इस tutorial में हम एक minimal Pair contract शुरू से दोबारा बनाएंगे ताकि वह मशहूर invariant, fee और liquidity accounting जादू न रह जाएं।

Constant product invariant

एक pool दो reserves रखता है, उन्हें x और y कहते हैं। मुख्य नियम है:

x * y = k

एक swap के दौरान product k कभी घटना नहीं चाहिए। जब कोई trader pool में token X डालता है, तो X का reserve बढ़ता है, इसलिए product को कम से कम k के बराबर रखने के लिए Y का reserve घटना चाहिए। वही घटता हुआ Y बिल्कुल वह है जो trader को मिलता है।

इस एक समीकरण से दो परिणाम निकलते हैं:

  • Price बस एक ratio है। Y के संदर्भ में X का marginal price y / x है। जैसे-जैसे आप Y खरीदते हैं, x बढ़ता है और y घटता है, इसलिए Y की हर unit पिछली से महंगी पड़ती है। यही slippage है, और यह curve में अंदर से बना हुआ है, बाहर से जोड़ा नहीं गया।
  • Pool कभी पूरी तरह खाली नहीं हो सकता। जैसे-जैसे x अनंत की ओर जाता है, Y का price भी अनंत की ओर जाता है, इसलिए आप आखिरी unit कभी नहीं निकाल सकते। Reserves asymptotic होते हैं।

Liquidity जोड़ना और LP tokens mint करना

Liquidity providers दोनों tokens जमा करते हैं और बदले में LP tokens पाते हैं जो pool में उनके हिस्से को दर्शाते हैं। Contract को ये LP tokens अनुपात के हिसाब से mint करने चाहिए ताकि कोई provider किसी दूसरे को dilute न कर सके।

Uniswap V2 जो नियम इस्तेमाल करता है:

  • पहले deposit के लिए, LP supply sqrt(amount0 * amount1) होती है, घटाकर एक छोटी सी हमेशा के लिए locked मात्रा (MINIMUM_LIQUIDITY, 1000 wei)। इस धूल को lock करना एक attacker को share price हेरफेर करने से रोकता है जब supply शून्य के करीब हो।
  • बाद के deposits के लिए, contract min(amount0 * totalSupply / reserve0, amount1 * totalSupply / reserve1) mint करता है। Minimum इस्तेमाल करना depositors को मौजूदा ratio में tokens जोड़ने पर मजबूर करता है। Ratio से ज्यादा जो भी आप जोड़ते हैं वह मौजूदा holders को तोहफा है।

पहले mint के लिए geometric mean (sqrt) शुरुआती LP मात्रा को tokens के absolute decimals से स्वतंत्र बनाता है और सस्ती inflation के प्रति प्रतिरोधी बनाता है।

0.3 percent fee के साथ swap

हर swap एक 0.3 percent fee लेता है जो pool में ही रहता है, और इसी से liquidity providers कमाते हैं। Uniswap V2 इसे सुंदर तरीके से व्यक्त करता है: input amount से fee घटाने के बाद invariant की जांच करके।

Reserves reserveIn और reserveOut के खिलाफ input amountIn के लिए canonical getAmountOut formula:

amountInWithFee = amountIn * 997

numerator = amountInWithFee * reserveOut

denominator = reserveIn * 1000 + amountInWithFee

amountOut = numerator / denominator

997 / 1000 factor ही 0.3 percent fee है। ध्यान दें कि fee input पर लगाई जाती है, फिर fee-adjusted amount पर constant product का गणित चलता है। चूंकि सब कुछ integer arithmetic है, truncation से बचने के लिए भाग देने से पहले गुणा करें, और operations का क्रम कभी न बदलें।

एक minimal Pair contract

यहां एक compact लेकिन सही Pair है। यह जरूरी हिस्से (reserves, mint, burn, swap, fee check) रखता है और LP token के लिए एक सरल ERC20 base इस्तेमाल करता है। इसने जानबूझकर price oracle और flash swap hooks छोड़ दिए हैं ताकि core पठनीय रहे।

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

interface IERC20 {

function balanceOf(address) external view returns (uint256);

function transfer(address to, uint256 value) external returns (bool);

function transferFrom(address from, address to, uint256 value) external returns (bool);

}

function sqrt(uint256 y) pure returns (uint256 z) {

if (y > 3) {

z = y;

uint256 x = y / 2 + 1;

while (x < z) {

z = x;

x = (y / x + x) / 2;

}

} else if (y != 0) {

z = 1;

}

}

contract MiniPair {

uint256 public constant MINIMUM_LIQUIDITY = 1000;

address public immutable token0;

address public immutable token1;

uint112 private reserve0;

uint112 private reserve1;

uint256 public totalSupply;

mapping(address => uint256) public balanceOf;

uint256 private unlocked = 1;

modifier lock() {

require(unlocked == 1, "LOCKED");

unlocked = 0;

_;

unlocked = 1;

}

constructor(address _token0, address _token1) {

token0 = _token0;

token1 = _token1;

}

function getReserves() public view returns (uint112, uint112) {

return (reserve0, reserve1);

}

function _mint(address to, uint256 value) private {

totalSupply += value;

balanceOf[to] += value;

}

function _burn(address from, uint256 value) private {

balanceOf[from] -= value;

totalSupply -= value;

}

function _update(uint256 bal0, uint256 bal1) private {

require(bal0 <= type(uint112).max && bal1 <= type(uint112).max, "OVERFLOW");

reserve0 = uint112(bal0);

reserve1 = uint112(bal1);

}

// mint कॉल करने से पहले caller को tokens इस contract में भेजने होंगे।

function mint(address to) external lock returns (uint256 liquidity) {

(uint112 r0, uint112 r1) = getReserves();

uint256 bal0 = IERC20(token0).balanceOf(address(this));

uint256 bal1 = IERC20(token1).balanceOf(address(this));

uint256 amount0 = bal0 - r0;

uint256 amount1 = bal1 - r1;

if (totalSupply == 0) {

liquidity = sqrt(amount0 * amount1) - MINIMUM_LIQUIDITY;

_mint(address(0), MINIMUM_LIQUIDITY); // हमेशा के लिए locked

} else {

uint256 l0 = (amount0 * totalSupply) / r0;

uint256 l1 = (amount1 * totalSupply) / r1;

liquidity = l0 < l1 ? l0 : l1;

}

require(liquidity > 0, "INSUFFICIENT_LIQUIDITY_MINTED");

_mint(to, liquidity);

_update(bal0, bal1);

}

// burn कॉल करने से पहले caller को LP tokens इस contract में भेजने होंगे।

function burn(address to) external lock returns (uint256 amount0, uint256 amount1) {

uint256 bal0 = IERC20(token0).balanceOf(address(this));

uint256 bal1 = IERC20(token1).balanceOf(address(this));

uint256 liquidity = balanceOf[address(this)];

amount0 = (liquidity * bal0) / totalSupply;

amount1 = (liquidity * bal1) / totalSupply;

require(amount0 > 0 && amount1 > 0, "INSUFFICIENT_LIQUIDITY_BURNED");

_burn(address(this), liquidity);

require(IERC20(token0).transfer(to, amount0), "T0_FAIL");

require(IERC20(token1).transfer(to, amount1), "T1_FAIL");

_update(

IERC20(token0).balanceOf(address(this)),

IERC20(token1).balanceOf(address(this))

);

}

// Caller पहले input tokens भेजता है, फिर amountOut मांगता है।

function swap(uint256 amount0Out, uint256 amount1Out, address to) external lock {

require(amount0Out > 0 || amount1Out > 0, "INSUFFICIENT_OUTPUT");

(uint112 r0, uint112 r1) = getReserves();

require(amount0Out < r0 && amount1Out < r1, "INSUFFICIENT_LIQUIDITY");

if (amount0Out > 0) require(IERC20(token0).transfer(to, amount0Out), "T0_FAIL");

if (amount1Out > 0) require(IERC20(token1).transfer(to, amount1Out), "T1_FAIL");

uint256 bal0 = IERC20(token0).balanceOf(address(this));

uint256 bal1 = IERC20(token1).balanceOf(address(this));

uint256 amount0In = bal0 > r0 - amount0Out ? bal0 - (r0 - amount0Out) : 0;

uint256 amount1In = bal1 > r1 - amount1Out ? bal1 - (r1 - amount1Out) : 0;

require(amount0In > 0 || amount1In > 0, "INSUFFICIENT_INPUT");

// 0.3% fee लगाएं, फिर adjusted balances पर x*y=k लागू करें।

uint256 bal0Adj = bal0 * 1000 - amount0In * 3;

uint256 bal1Adj = bal1 * 1000 - amount1In * 3;

require(

bal0Adj * bal1Adj >= uint256(r0) * uint256(r1) * (1000 ** 2),

"K"

);

_update(bal0, bal1);

}

}

इस pattern पर ध्यान दें: contract swap के अंदर कभी transferFrom से tokens नहीं खींचता। Router (या caller) पहले input tokens भेजता है, फिर swap कॉल करता है। Pair सिर्फ यह जांचता है कि अंतिम balances invariant को संतुष्ट करते हैं। यही optimistic transfer design बाद में flash swaps को संभव बनाता है।

Callers के लिए एक helper

ज्यादातर users amount0Out हाथ से नहीं निकालते। एक router गणित को लपेट देता है। Pure quote function ऊपर के formula को दोहराता है:

function getAmountOut(

uint256 amountIn,

uint256 reserveIn,

uint256 reserveOut

) pure returns (uint256 amountOut) {

require(amountIn > 0, "INSUFFICIENT_INPUT");

require(reserveIn > 0 && reserveOut > 0, "INSUFFICIENT_LIQUIDITY");

uint256 amountInWithFee = amountIn * 997;

uint256 numerator = amountInWithFee * reserveOut;

uint256 denominator = reserveIn * 1000 + amountInWithFee;

amountOut = numerator / denominator;

}

Security notes

एक खिलौना AMM आसान है। एक सुरक्षित AMM नहीं। इन बातों पर नजर रखें:

  • Reentrancy. lock modifier हर state बदलने वाले function की रक्षा करता है। इसके बिना, transfer में callback वाला एक malicious token निष्पादन के बीच swap में दोबारा घुसकर invariant check तोड़ सकता है।
  • K check post-transfer balances इस्तेमाल करता है। Output भेजने के बाद और input आने के बाद हमेशा balanceOf पढ़ें। On chain balances के बजाय caller द्वारा दिए गए numbers पर भरोसा करना एक classic चोरी का vector है।
  • MINIMUM_LIQUIDITY मायने रखता है। पहले 1000 wei को jala देना उस inflation attack को रोकता है जहां एक donor पहले depositor के लिए share price हेरफेर करता है। इसे न छोड़ें।
  • Fee on transfer और rebasing tokens गणित तोड़ देते हैं। अगर कोई token transfer पर कटौती लेता है, तो Pair जो balance पढ़ता है वह अपेक्षा से कम होता है और K check fail हो सकता है या, बदतर, accounting बहक जाती है। Pools या तो ऐसे tokens रोकते हैं या जोखिम document करते हैं।
  • Integer truncation. भाग देने से पहले गुणा करें, और fee scaling (* 1000, * 997) बिल्कुल जैसा दिखाया गया है वैसा रखें। क्रम बदलना चुपचाप value को pool से बाहर round कर देता है।
  • Oracle manipulation. getReserves से पढ़ा गया spot price एक ही block के भीतर आसानी से हेरफेर किया जा सकता है। Production code इसके बजाय एक time weighted average price (TWAP) जमा करता है। इस minimal Pair में कोई oracle नहीं है, इसलिए इसके instantaneous price को कभी सच के स्रोत के रूप में इस्तेमाल न करें।
  • इसे hands-on अभ्यास करें

    Invariant पढ़ना एक बात है। Trades push करते हुए k को टिकते देखना, या एक fee on transfer token को reserves बिगाड़ते देखना, सब समझा देता है। आप इस जैसे एक Pair को कदम-दर-कदम बना और तोड़ सकते हैं, app.solingo-blockchain.xyz पर interactive Solidity environment में। Contract deploy करें, liquidity जोड़ें, एक swap चलाएं, और सत्यापित करें कि product कभी k से नीचे नहीं गिरता।

    Practice में लगाने के लिए तैयार हैं?

    Solingo पर interactive exercises के साथ इन concepts को apply करें।

    मुफ्त में शुरू करें