মূল কন্টেন্টে যান

Uniswap-v2 কন্ট্রাক্ট ওয়াক-থ্রু

Solidity
ডিএ্যাপস
ইন্টারমিডিয়েট
ওরি পোমেরান্টজ
1 মে, 2021
57 মিনিট পড়া

ভূমিকা

Uniswap v2 (opens in a new tab) যেকোনো দুটি ERC-20 টোকেনের মধ্যে একটি এক্সচেঞ্জ মার্কেট তৈরি করতে পারে। এই আর্টিকেলে আমরা এই প্রটোকলটি ইমপ্লিমেন্ট করা কন্ট্রাক্টগুলোর সোর্স কোড নিয়ে আলোচনা করব এবং দেখব কেন সেগুলো এভাবে লেখা হয়েছে।

Uniswap কী করে?

মূলত, দুই ধরনের ব্যবহারকারী রয়েছে: লিকুইডিটি প্রোভাইডার এবং ট্রেডার।

লিকুইডিটি প্রোভাইডাররা পুলটিতে দুটি টোকেন প্রদান করে যা এক্সচেঞ্জ করা যায় (আমরা সেগুলোকে Token0 এবং Token1 বলব)। এর বিনিময়ে, তারা তৃতীয় একটি টোকেন পায় যা পুলের আংশিক মালিকানাকে উপস্থাপন করে, যাকে লিকুইডিটি টোকেন বলা হয়।

ট্রেডাররা পুলে এক ধরনের টোকেন পাঠায় এবং লিকুইডিটি প্রোভাইডারদের দেওয়া পুল থেকে অন্যটি গ্রহণ করে (উদাহরণস্বরূপ, Token0 পাঠায় এবং Token1 গ্রহণ করে)। এক্সচেঞ্জ রেট নির্ধারিত হয় পুলে থাকা Token0 এবং Token1 এর আপেক্ষিক সংখ্যার ওপর ভিত্তি করে। এছাড়া, পুলটি লিকুইডিটি পুলের জন্য রিওয়ার্ড হিসেবে একটি ছোট শতাংশ কেটে নেয়।

লিকুইডিটি প্রোভাইডাররা যখন তাদের সম্পদ ফেরত পেতে চায়, তখন তারা পুল টোকেনগুলো বার্ন করতে পারে এবং রিওয়ার্ডের অংশসহ তাদের টোকেনগুলো ফেরত পেতে পারে।

আরও বিস্তারিত বর্ণনার জন্য এখানে ক্লিক করুন (opens in a new tab)

কেন v2? কেন v3 নয়?

Uniswap v3 (opens in a new tab) হলো একটি আপগ্রেড যা v2 এর চেয়ে অনেক বেশি জটিল। প্রথমে v2 শেখা এবং তারপর v3 তে যাওয়া বেশি সহজ।

কোর কন্ট্রাক্ট বনাম পেরিফেরি কন্ট্রাক্ট

Uniswap v2 দুটি উপাদানে বিভক্ত, একটি কোর এবং একটি পেরিফেরি। এই বিভাজন কোর কন্ট্রাক্টগুলোকে, যা সম্পদ ধারণ করে এবং তাই সেগুলোকে সুরক্ষিত হতেই হবে, আরও সহজ এবং অডিট করার জন্য সুবিধাজনক করে তোলে। ট্রেডারদের জন্য প্রয়োজনীয় সমস্ত অতিরিক্ত কার্যকারিতা এরপর পেরিফেরি কন্ট্রাক্টগুলোর মাধ্যমে প্রদান করা যেতে পারে।

ডাটা এবং কন্ট্রোল ফ্লো

ইউনিসোয়াপের (Uniswap) তিনটি প্রধান কাজ করার সময় ডাটা এবং কন্ট্রোলের ফ্লো যেভাবে কাজ করে:

  1. বিভিন্ন টোকেনের মধ্যে সোয়াপ করা
  2. মার্কেটে লিকুইডিটি যোগ করা এবং পেয়ার এক্সচেঞ্জ ERC-20 লিকুইডিটি টোকেন দিয়ে পুরস্কৃত হওয়া
  3. ERC-20 লিকুইডিটি টোকেন বার্ন করা এবং সেই ERC-20 টোকেনগুলো ফেরত পাওয়া যা পেয়ার এক্সচেঞ্জ ট্রেডারদের এক্সচেঞ্জ করার অনুমতি দেয়

সোয়াপ

এটি সবচেয়ে সাধারণ ফ্লো, যা ট্রেডাররা ব্যবহার করে থাকেন:

কলার

  1. পেরিফেরি একাউন্টকে সোয়াপ করার পরিমাণের একটি এলাউন্স প্রদান করুন।
  2. পেরিফেরি কন্ট্রাক্টের অনেকগুলো সোয়াপ ফাংশনের মধ্যে একটি কল করুন (কোনটি কল করবেন তা নির্ভর করে ETH জড়িত আছে কি না, ট্রেডার জমা দেওয়ার টোকেনের পরিমাণ বা ফেরত পাওয়ার টোকেনের পরিমাণ নির্দিষ্ট করেছেন কি না, ইত্যাদির ওপর)। প্রতিটি সোয়াপ ফাংশন একটি path গ্রহণ করে, যা হলো অতিক্রম করার জন্য এক্সচেঞ্জগুলোর একটি অ্যারে।

পেরিফেরি কন্ট্রাক্টে (UniswapV2Router02.sol)

  1. পাথের প্রতিটি এক্সচেঞ্জে ট্রেড করার পরিমাণগুলো চিহ্নিত করুন।
  2. পাথের ওপর ইটারেট করে। পথের প্রতিটি এক্সচেঞ্জের জন্য এটি ইনপুট টোকেন পাঠায় এবং তারপর এক্সচেঞ্জের swap ফাংশন কল করে। বেশিরভাগ ক্ষেত্রে টোকেনগুলোর গন্তব্য এডড্রেস হলো পাথের পরবর্তী পেয়ার এক্সচেঞ্জ। চূড়ান্ত এক্সচেঞ্জে এটি ট্রেডারের দেওয়া এডড্রেস হয়।

কোর কন্ট্রাক্টে (UniswapV2Pair.sol)

  1. যাচাই করুন যে কোর কন্ট্রাক্ট প্রতারিত হচ্ছে না এবং সোয়াপের পরে পর্যাপ্ত লিকুইডিটি বজায় রাখতে পারে।
  2. দেখুন পরিচিত রিজার্ভের পাশাপাশি আমাদের কাছে কতগুলো অতিরিক্ত টোকেন আছে। সেই পরিমাণটি হলো এক্সচেঞ্জ করার জন্য আমাদের প্রাপ্ত ইনপুট টোকেনের সংখ্যা।
  3. আউটপুট টোকেনগুলো গন্তব্যে পাঠান।
  4. রিজার্ভের পরিমাণ আপডেট করতে _update কল করুন

পেরিফেরি কন্ট্রাক্টে ফিরে আসা (UniswapV2Router02.sol)

  1. প্রয়োজনীয় যেকোনো ক্লিনআপ সম্পন্ন করুন (উদাহরণস্বরূপ, ট্রেডারকে পাঠানোর জন্য ETH ফেরত পেতে WETH টোকেন বার্ন করুন)

লিকুইডিটি যোগ করা

কলার

  1. পেরিফেরি একাউন্টকে লিকুইডিটি পুলে যোগ করার পরিমাণের একটি এলাউন্স প্রদান করুন।
  2. পেরিফেরি কন্ট্রাক্টের যেকোনো একটি addLiquidity ফাংশন কল করুন।

পেরিফেরি কন্ট্রাক্টে (UniswapV2Router02.sol)

  1. প্রয়োজন হলে একটি নতুন পেয়ার এক্সচেঞ্জ তৈরি করুন
  2. যদি কোনো বিদ্যমান পেয়ার এক্সচেঞ্জ থাকে, তবে যোগ করার টোকেনের পরিমাণ হিসাব করুন। এটি উভয় টোকেনের জন্য অভিন্ন ভ্যালু হওয়ার কথা, তাই বিদ্যমান টোকেনের সাথে নতুন টোকেনের অনুপাত একই হবে।
  3. পরিমাণগুলো গ্রহণযোগ্য কি না তা চেক করুন (কলাররা একটি ন্যূনতম পরিমাণ নির্দিষ্ট করতে পারেন যার নিচে তারা লিকুইডিটি যোগ করতে চাইবেন না)
  4. কোর কন্ট্রাক্ট কল করুন।

কোর কন্ট্রাক্টে (UniswapV2Pair.sol)

  1. লিকুইডিটি টোকেন মিন্ট করুন এবং সেগুলো কলারের কাছে পাঠান
  2. রিজার্ভের পরিমাণ আপডেট করতে _update কল করুন

লিকুইডিটি সরানো

কলার

  1. আন্ডারলাইং টোকেনের বিনিময়ে বার্ন করার জন্য পেরিফেরি একাউন্টকে লিকুইডিটি টোকেনের একটি এলাউন্স প্রদান করুন।
  2. পেরিফেরি কন্ট্রাক্টের যেকোনো একটি removeLiquidity ফাংশন কল করুন।

পেরিফেরি কন্ট্রাক্টে (UniswapV2Router02.sol)

  1. পেয়ার এক্সচেঞ্জে লিকুইডিটি টোকেনগুলো পাঠান

কোর কন্ট্রাক্টে (UniswapV2Pair.sol)

  1. বার্ন করা টোকেনের অনুপাতে গন্তব্য এডড্রেসে আন্ডারলাইং টোকেনগুলো পাঠান। উদাহরণস্বরূপ, যদি পুলে 1000 A টোকেন, 500 B টোকেন এবং 90 লিকুইডিটি টোকেন থাকে এবং আমরা বার্ন করার জন্য 9 টোকেন পাই, তবে আমরা 10% লিকুইডিটি টোকেন বার্ন করছি, তাই আমরা ব্যবহারকারীকে 100 A টোকেন এবং 50 B টোকেন ফেরত পাঠাব।
  2. লিকুইডিটি টোকেনগুলো বার্ন করুন
  3. রিজার্ভের পরিমাণ আপডেট করতে _update কল করুন

মূল কন্ট্রাক্টস

এগুলো হলো সুরক্ষিত কন্ট্রাক্ট যা লিকুইডিটি ধরে রাখে।

UniswapV2Pair.sol

এই কন্ট্রাক্টটি (opens in a new tab) আসল পুলটি বাস্তবায়ন করে যা টোকেন এক্সচেঞ্জ করে। এটি Uniswap-এর মূল কার্যকারিতা।

এগুলো হলো সেই সব ইন্টারফেস যা সম্পর্কে কন্ট্রাক্টটির জানা প্রয়োজন, কারণ কন্ট্রাক্টটি এগুলো বাস্তবায়ন করে (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;

পুল কন্ট্রাক্টের অনেক হিসাবের জন্য ভগ্নাংশের প্রয়োজন হয়। তবে, EVM ভগ্নাংশ সমর্থন করে না। Uniswap এর যে সমাধানটি বের করেছে তা হলো 224 বিট মান ব্যবহার করা, যার মধ্যে 112 বিট পূর্ণসংখ্যার জন্য এবং 112 বিট ভগ্নাংশের জন্য। তাই 1.0-কে 2^112 হিসেবে, 1.5-কে 2^112 + 2^111 হিসেবে উপস্থাপন করা হয়, ইত্যাদি।

এই লাইব্রেরি সম্পর্কে আরও বিস্তারিত তথ্য ডকুমেন্টের পরের অংশে পাওয়া যাবে।

ভেরিয়েবলস

    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% ট্রেডিং ফি উপেক্ষা করেছি তাই সংখ্যাগুলো পুরোপুরি নির্ভুল নয়।

ইভেন্টreserve0reserve1reserve0 * reserve1গড় এক্সচেঞ্জ রেট (token1 / token0)
প্রাথমিক সেটআপ1,000.0001,000.0001,000,000
ট্রেডার A 50 token0 সোয়াপ করে 47.619 token1 পায়1,050.000952.3811,000,0000.952
ট্রেডার B 10 token0 সোয়াপ করে 8.984 token1 পায়1,060.000943.3961,000,0000.898
ট্রেডার C 40 token0 সোয়াপ করে 34.305 token1 পায়1,100.000909.0901,000,0000.858
ট্রেডার D 100 token1 সোয়াপ করে 109.01 token0 পায়990.9901,009.0901,000,0000.917
ট্রেডার E 10 token0 সোয়াপ করে 10.079 token1 পায়1,000.990999.0101,000,0001.008

ট্রেডাররা যখন আরও বেশি token0 প্রদান করে, তখন token1-এর আপেক্ষিক মান বৃদ্ধি পায়, এবং এর বিপরীতটিও ঘটে, যা সাপ্লাই এবং ডিমান্ডের ওপর ভিত্তি করে হয়।

লক

    uint private unlocked = 1;

নিরাপত্তা দুর্বলতার একটি শ্রেণী রয়েছে যা রিএন্ট্রান্সি অ্যাবিউজ (opens in a new tab)-এর ওপর ভিত্তি করে তৈরি। Uniswap-এর যেকোনো ERC-20 টোকেন ট্রান্সফার করার প্রয়োজন হয়, যার মানে হলো এমন ERC-20 কন্ট্রাক্টগুলোকে কল করা যা তাদের কল করা Uniswap মার্কেটকে অপব্যবহার করার চেষ্টা করতে পারে। কন্ট্রাক্টের অংশ হিসেবে একটি 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 ট্রান্সফার কল ব্যর্থতা রিপোর্ট করতে পারে:

  1. রিভার্ট। যদি কোনো এক্সটার্নাল কন্ট্রাক্টে কল রিভার্ট হয়, তবে বুলিয়ান রিটার্ন মান হয় false
  2. স্বাভাবিকভাবে শেষ হয় কিন্তু একটি ব্যর্থতা রিপোর্ট করে। সেক্ষেত্রে রিটার্ন মান বাফারের একটি নন-জিরো দৈর্ঘ্য থাকে, এবং যখন একটি বুলিয়ান মান হিসেবে ডিকোড করা হয় তখন এটি 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) {

যদি অতিবাহিত সময় শূন্য না হয়, তবে এর মানে হলো আমরা এই ব্লক-এ প্রথম এক্সচেঞ্জ লেনদেন। সেক্ষেত্রে, আমাদের কস্ট অ্যাকুমুলেটরগুলো আপডেট করতে হবে।

            // * কখনও ওভারফ্লো হয় না, এবং + ওভারফ্লো কাঙ্ক্ষিত
            price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed;
            price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed;
        }

প্রতিটি কস্ট অ্যাকুমুলেটর সর্বশেষ খরচ (অন্য টোকেনের রিজার্ভ/এই টোকেনের রিজার্ভ) গুণ সেকেন্ডে অতিবাহিত সময়ের সাথে আপডেট করা হয়। একটি গড় মূল্য পেতে, আপনি দুটি ভিন্ন সময়ের ক্রমবর্ধমান মূল্য পড়েন এবং তাদের মধ্যকার সময়ের পার্থক্য দিয়ে ভাগ করেন। উদাহরণস্বরূপ, ইভেন্টগুলোর এই ক্রমটি ধরে নিন:

ইভেন্টreserve0reserve1টাইমস্ট্যাম্পপ্রান্তিক এক্সচেঞ্জ রেট (reserve1 / reserve0)price0CumulativeLast
প্রাথমিক সেটআপ1,000.0001,000.0005,0001.0000
ট্রেডার A 50 token0 জমা দেয় এবং 47.619 token1 ফেরত পায়1,050.000952.3815,0200.90720
ট্রেডার B 10 token0 জমা দেয় এবং 8.984 token1 ফেরত পায়1,060.000943.3965,0300.89020+10*0.907 = 29.07
ট্রেডার C 40 token0 জমা দেয় এবং 34.305 token1 ফেরত পায়1,100.000909.0905,1000.82629.07+70*0.890 = 91.37
ট্রেডার D 100 token1 জমা দেয় এবং 109.01 token0 ফেরত পায়990.9901,009.0905,1101.01891.37+10*0.826 = 99.63
ট্রেডার E 10 token0 জমা দেয় এবং 10.079 token1 ফেরত পায়1,000.990999.0105,1500.99899.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) এর বৃদ্ধির ১/৬ অংশের সমতুল্য লিকুইডিটি মিন্ট করে
    function _mintFee(uint112 _reserve0, uint112 _reserve1) private returns (bool feeOn) {

Uniswap 2.0-তে ট্রেডাররা মার্কেট ব্যবহার করার জন্য 0.30% ফি প্রদান করে। সেই ফি-এর বেশিরভাগ অংশ (ট্রেডের 0.25%) সবসময় লিকুইডিটি প্রোভাইডারদের কাছে যায়। বাকি 0.05% লিকুইডিটি প্রোভাইডারদের কাছে অথবা ফ্যাক্টরি দ্বারা প্রটোকল ফি হিসেবে নির্দিষ্ট করা একটি এডড্রেস-এ যেতে পারে, যা Uniswap-কে তাদের ডেভেলপমেন্ট প্রচেষ্টার জন্য অর্থ প্রদান করে।

হিসাব কমানোর জন্য (এবং সেই কারণে গ্যাস খরচ), এই ফি শুধুমাত্র তখনই গণনা করা হয় যখন পুল থেকে লিকুইডিটি যোগ করা বা সরানো হয়, প্রতিটি লেনদেন-এর সময় নয়।

        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)-এর ৫ পৃষ্ঠায় ব্যাখ্যা করা হয়েছে। আমরা জানি যে 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) ছিল যা কন্ট্রাক্টগুলোকে তাদের অপ্রয়োজনীয় স্টোরেজ শূন্য করে ইথিরিয়াম স্টেট-এর সামগ্রিক আকার কমাতে উৎসাহিত করত। এই কোডটি সম্ভব হলে সেই রিফান্ড পায়।

এক্সটার্নালি অ্যাক্সেসযোগ্য ফাংশন

মনে রাখবেন যে যদিও যেকোনো লেনদেন বা কন্ট্রাক্ট এই ফাংশনগুলোকে কল করতে পারে, এগুলো পেরিফেরি কন্ট্রাক্ট থেকে কল করার জন্য ডিজাইন করা হয়েছে। আপনি যদি এগুলোকে সরাসরি কল করেন তবে আপনি পেয়ার এক্সচেঞ্জকে প্রতারণা করতে পারবেন না, তবে আপনি কোনো ভুলের মাধ্যমে মান হারাতে পারেন।

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। খুব বেশি খরচ নয়।

প্রথম জমার সময় আমরা দুটি টোকেনের আপেক্ষিক মান জানি না, তাই আমরা কেবল পরিমাণগুলো গুণ করি এবং একটি স্কয়ার রুট নিই, এই ধরে নিয়ে যে জমাটি আমাদের উভয় টোকেনে সমান মান প্রদান করে।

আমরা এটিকে বিশ্বাস করতে পারি কারণ আরবিট্রেজ-এ মান হারানো এড়াতে সমান মান প্রদান করা আমানতকারীর স্বার্থে। ধরা যাক দুটি টোকেনের মান অভিন্ন, কিন্তু আমাদের আমানতকারী Token0-এর চেয়ে চারগুণ বেশি Token1 জমা দিয়েছেন। একজন ট্রেডার এই সত্যটি ব্যবহার করতে পারে যে পেয়ার এক্সচেঞ্জ মনে করে Token0 বেশি মূল্যবান, এটি থেকে মান বের করে নিতে।

ইভেন্টreserve0reserve1reserve0 * reserve1পুলের মান (reserve0 + reserve1)
প্রাথমিক সেটআপ83225640
ট্রেডার 8টি Token0 টোকেন জমা দেয়, 16টি Token1 ফেরত পায়161625632

যেমনটি আপনি দেখতে পাচ্ছেন, ট্রেডার অতিরিক্ত 8টি টোকেন উপার্জন করেছে, যা পুলের মান হ্রাস থেকে আসে, যা এর মালিক আমানতকারীকে ক্ষতিগ্রস্ত করে।

        } else {
            liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1);

পরবর্তী প্রতিটি জমার সাথে আমরা ইতিমধ্যেই দুটি অ্যাসেটের মধ্যে এক্সচেঞ্জ রেট জানি, এবং আমরা আশা করি লিকুইডিটি প্রোভাইডাররা উভয়ের সমান মান প্রদান করবে। যদি তারা তা না করে, তবে আমরা তাদের শাস্তি হিসেবে তাদের দেওয়া কম মানের ওপর ভিত্তি করে লিকুইডিটি টোকেন দিই।

এটি প্রাথমিক জমা হোক বা পরবর্তী কোনো জমা, আমরা যে সংখ্যক লিকুইডিটি টোকেন প্রদান করি তা reserve0*reserve1-এর পরিবর্তনের স্কয়ার রুটের সমান এবং লিকুইডিটি টোকেনের মান পরিবর্তন হয় না (যদি না আমরা এমন কোনো জমা পাই যার উভয় প্রকারের সমান মান নেই, সেক্ষেত্রে "জরিমানা" বিতরণ করা হয়)। এখানে একই মান থাকা দুটি টোকেনের আরেকটি উদাহরণ দেওয়া হলো, যেখানে তিনটি ভালো জমা এবং একটি খারাপ জমা রয়েছে (শুধুমাত্র এক প্রকারের টোকেন জমা, তাই এটি কোনো লিকুইডিটি টোকেন তৈরি করে না)।

ইভেন্টreserve0reserve1reserve0 * reserve1পুলের মান (reserve0 + reserve1)এই জমার জন্য মিন্ট করা লিকুইডিটি টোকেনমোট লিকুইডিটি টোকেনপ্রতিটি লিকুইডিটি টোকেনের মান
প্রাথমিক সেটআপ8.0008.0006416.000882.000
প্রতিটি প্রকারের চারটি জমা12.00012.00014424.0004122.000
প্রতিটি প্রকারের দুটি জমা14.00014.00019628.0002142.000
অসম মানের জমা18.00014.00025232.000014~2.286
আরবিট্রেজ-এর পর~15.874~15.874252~31.748014~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 ফাংশনটি ওপরের mint ফাংশনের মিরর ইমেজ।

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} এর জন্য স্কোপ, স্ট্যাক টু ডিপ (stack too deep) এরর এড়ায়

লোকাল ভেরিয়েবলগুলো মেমরিতে সংরক্ষণ করা যেতে পারে অথবা, যদি সেগুলো খুব বেশি না হয়, তবে সরাসরি স্ট্যাক-এ সংরক্ষণ করা যেতে পারে। যদি আমরা সংখ্যাটি সীমিত করতে পারি যাতে আমরা স্ট্যাক ব্যবহার করব তবে আমরা কম গ্যাস ব্যবহার করি। আরও বিস্তারিত তথ্যের জন্য ইয়েলো পেপার, আনুষ্ঠানিক ইথিরিয়াম স্পেসিফিকেশন (opens in a new tab), পৃষ্ঠা ২৬, সমীকরণ ২৯৮ দেখুন।

            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); // অপ্টিমিস্টিক্যালি টোকেন ট্রান্সফার করে

এই ট্রান্সফারটি অপ্টিমিস্টিক, কারণ সমস্ত শর্ত পূরণ হয়েছে কিনা তা নিশ্চিত হওয়ার আগেই আমরা ট্রান্সফার করি। ইথিরিয়াম-এ এটি ঠিক আছে কারণ যদি কলের পরে শর্তগুলো পূরণ না হয় তবে আমরা এটি থেকে এবং এটি তৈরি করা যেকোনো পরিবর্তন থেকে রিভার্ট করি।

            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 এর জন্য স্কোপ, স্ট্যাক টু ডিপ (stack too deep) এরর এড়ায়
            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');

এটি একটি স্যানিটি চেক যা নিশ্চিত করে যে আমরা সোয়াপ থেকে হারাব না। এমন কোনো পরিস্থিতি নেই যেখানে একটি সোয়াপ 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 আপডেট করুন, এবং প্রয়োজন হলে প্রাইস অ্যাকুমুলেটর এবং টাইমস্ট্যাম্প আপডেট করুন এবং একটি ইভেন্ট এমিট করুন।

Sync বা Skim

আসল ব্যালেন্সগুলোর পক্ষে পেয়ার এক্সচেঞ্জ-এর মনে করা রিজার্ভের সাথে সিঙ্ক না হওয়া সম্ভব। কন্ট্রাক্টের সম্মতি ছাড়া টোকেন তুলে নেওয়ার কোনো উপায় নেই, তবে জমা দেওয়া একটি ভিন্ন বিষয়। একটি একাউন্ট mint বা swap কল না করেই এক্সচেঞ্জে টোকেন ট্রান্সফার করতে পারে।

সেক্ষেত্রে দুটি সমাধান রয়েছে:

  • sync, বর্তমান ব্যালেন্সগুলোতে রিজার্ভ আপডেট করুন
  • skim, অতিরিক্ত পরিমাণ তুলে নিন। মনে রাখবেন যে যেকোনো একাউন্ট-কে skim কল করার অনুমতি দেওয়া হয়েছে কারণ আমরা জানি না কে টোকেনগুলো জমা দিয়েছে। এই তথ্যটি একটি ইভেন্টে এমিট করা হয়, তবে ইভেন্টগুলো ব্লকচেইন থেকে অ্যাক্সেসযোগ্য নয়।

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), পৃষ্ঠা ৫ দেখুন)। feeTo এডড্রেস প্রটোকল ফি-এর জন্য লিকুইডিটি টোকেনগুলো জমা করে, এবং feeToSetter হলো সেই এডড্রেস যাকে feeTo-কে একটি ভিন্ন এডড্রেস-এ পরিবর্তন করার অনুমতি দেওয়া হয়েছে।

    mapping(address => mapping(address => address)) public getPair;
    address[] public allPairs;

এই ভেরিয়েবলগুলো পেয়ারগুলোর, দুটি টোকেন টাইপের মধ্যে এক্সচেঞ্জগুলোর ট্র্যাক রাখে।

প্রথমটি, getPair, হলো একটি ম্যাপিং যা একটি পেয়ার এক্সচেঞ্জ কন্ট্রাক্টকে এটি যে দুটি ERC-20 টোকেন এক্সচেঞ্জ করে তার ওপর ভিত্তি করে শনাক্ত করে। ERC-20 টোকেনগুলো সেই কন্ট্রাক্টগুলোর এডড্রেস দ্বারা শনাক্ত করা হয় যা সেগুলোকে বাস্তবায়ন করে, তাই কী (keys) এবং মান (value) সবই হলো এডড্রেস। পেয়ার এক্সচেঞ্জের এডড্রেস পেতে যা আপনাকে tokenA থেকে tokenB-তে রূপান্তর করতে দেয়, আপনি getPair[<tokenA address>][<tokenB address>] ব্যবহার করেন (অথবা এর উল্টোটা)।

দ্বিতীয় ভেরিয়েবল, allPairs, হলো একটি অ্যারে যা এই ফ্যাক্টরি দ্বারা তৈরি সমস্ত পেয়ার এক্সচেঞ্জের এডড্রেস অন্তর্ভুক্ত করে। ইথিরিয়াম-এ আপনি একটি ম্যাপিংয়ের কন্টেন্টের ওপর ইটারেট করতে পারবেন না, বা সমস্ত কী-এর একটি তালিকা পেতে পারবেন না, তাই এই ফ্যাক্টরি কোন এক্সচেঞ্জগুলো পরিচালনা করে তা জানার একমাত্র উপায় হলো এই ভেরিয়েবলটি।

দ্রষ্টব্য: আপনি একটি ম্যাপিংয়ের সমস্ত কী-এর ওপর ইটারেট করতে না পারার কারণ হলো কন্ট্রাক্ট ডাটা স্টোরেজ ব্যয়বহুল, তাই আমরা এটি যত কম ব্যবহার করি ততই ভালো, এবং আমরা এটি যত কম পরিবর্তন করি ততই ভালো। আপনি ইটারেশন সমর্থন করে এমন ম্যাপিং (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 টোকেনের মধ্যে একটি পেয়ার এক্সচেঞ্জ তৈরি করা। মনে রাখবেন যে যেকেউ এই ফাংশনটি কল করতে পারে। একটি নতুন পেয়ার এক্সচেঞ্জ তৈরি করতে আপনার Uniswap থেকে অনুমতির প্রয়োজন নেই।

        require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES');
        (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);

আমরা চাই নতুন এক্সচেঞ্জের এডড্রেসটি ডিটারমিনিস্টিক হোক, যাতে এটি আগে থেকেই অফচেইন গণনা করা যায় (এটি লেয়ার ২ লেনদেন-এর জন্য দরকারী হতে পারে)। এটি করার জন্য আমাদের টোকেন এডড্রেসগুলোর একটি সামঞ্জস্যপূর্ণ ক্রম থাকতে হবে, আমরা যে ক্রমেই সেগুলো পেয়ে থাকি না কেন, তাই আমরা সেগুলোকে এখানে সাজাই।

        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);
    }

স্টেট ভেরিয়েবলগুলোতে নতুন পেয়ারের তথ্য সংরক্ষণ করুন এবং বিশ্বকে নতুন পেয়ার এক্সচেঞ্জ সম্পর্কে জানাতে একটি ইভেন্ট এমিট করুন।

এই দুটি ফাংশন feeSetter-কে ফি প্রাপক (যদি থাকে) নিয়ন্ত্রণ করতে এবং feeSetter-কে একটি নতুন এডড্রেস-এ পরিবর্তন করার অনুমতি দেয়।

UniswapV2ERC20.sol

এই কন্ট্রাক্টটি (opens in a new tab) ERC-20 লিকুইডিটি টোকেন বাস্তবায়ন করে। এটি OpenZeppelin 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 নয়।

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) গ্রহণ করে।

        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 অ্যাপ্রুভ (opens in a new tab) হিসেবে বিবেচনা করুন।

পেরিফেরি কন্ট্রাক্টস

পেরিফেরি কন্ট্রাক্টগুলো হলো Uniswap-এর API (এপ্লিকেশন প্রোগ্রাম ইন্টারফেস)। এগুলো অন্যান্য কন্ট্রাক্ট বা ডিসেন্ট্রালাইজড এপ্লিকেশন থেকে এক্সটার্নাল কলের জন্য এভেইলেবল। আপনি চাইলে সরাসরি কোর কন্ট্রাক্টগুলোকে কল করতে পারেন, তবে এটি বেশ জটিল এবং ভুল করলে আপনার ভ্যালু বা ফান্ড হারাতে পারেন। কোর কন্ট্রাক্টগুলোতে শুধুমাত্র এমন টেস্ট থাকে যা নিশ্চিত করে যে তাদের সাথে কোনো প্রতারণা করা হচ্ছে না, অন্য কারো জন্য কোনো স্যানিটি চেক এতে থাকে না। সেগুলো পেরিফেরিতে থাকে যাতে প্রয়োজন অনুযায়ী আপডেট করা যায়।

UniswapV2Router01.sol

এই কন্ট্রাক্টে (opens in a new tab) কিছু সমস্যা রয়েছে এবং এটি আর ব্যবহার করা উচিত নয় (opens in a new tab)। সৌভাগ্যবশত, পেরিফেরি কন্ট্রাক্টগুলো স্টেটলেস এবং এগুলো কোনো অ্যাসেট ধরে রাখে না, তাই এটিকে সহজেই বাতিল করা যায় এবং এর পরিবর্তে মানুষকে UniswapV2Router02 ব্যবহার করার পরামর্শ দেওয়া যায়।

UniswapV2Router02.sol

বেশিরভাগ ক্ষেত্রেই আপনি এই কন্ট্রাক্টের (opens in a new tab) মাধ্যমে Uniswap ব্যবহার করবেন। এটি কীভাবে ব্যবহার করতে হয় তা আপনি এখানে (opens in a new tab) দেখতে পারেন।

এগুলোর বেশিরভাগই আমরা আগে দেখেছি, অথবা বেশ স্পষ্ট। একমাত্র ব্যতিক্রম হলো IWETH.sol। Uniswap v2 যেকোনো জোড়া ERC-20 টোকেনের জন্য এক্সচেঞ্জ করার অনুমতি দেয়, কিন্তু ইথার (ETH) নিজে কোনো ERC-20 টোকেন নয়। এটি স্ট্যান্ডার্ডের আগের এবং ইউনিক মেকানিজমের মাধ্যমে ট্রান্সফার করা হয়। ERC-20 টোকেনের ক্ষেত্রে প্রযোজ্য কন্ট্রাক্টগুলোতে ETH-এর ব্যবহার সক্ষম করতে মানুষ wrapped ether (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

এগুলো হলো জমা করার জন্য সর্বনিম্ন গ্রহণযোগ্য পরিমাণ। যদি এই পরিমাণ বা তার বেশি দিয়ে লেনদেন সম্পন্ন করা না যায়, তবে এটি রিভার্ট হয়ে যাবে। আপনি যদি এই ফিচারটি না চান, তবে শুধু শূন্য (0) উল্লেখ করুন।

লিকুইডিটি প্রোভাইডাররা সাধারণত একটি সর্বনিম্ন পরিমাণ নির্দিষ্ট করে দেন, কারণ তারা লেনদেনটিকে বর্তমান এক্সচেঞ্জ রেটের কাছাকাছি সীমাবদ্ধ রাখতে চান। যদি এক্সচেঞ্জ রেট খুব বেশি ওঠানামা করে, তবে এর মানে হতে পারে এমন কোনো খবর যা আন্ডারলাইং ভ্যালু পরিবর্তন করে দিয়েছে, এবং তারা ম্যানুয়ালি সিদ্ধান্ত নিতে চান যে কী করতে হবে।

উদাহরণস্বরূপ, এমন একটি পরিস্থিতি কল্পনা করুন যেখানে এক্সচেঞ্জ রেট এক-এর বিপরীতে এক (1:1) এবং লিকুইডিটি প্রোভাইডার এই ভ্যালুগুলো নির্দিষ্ট করেছেন:

প্যারামিটারভ্যালু
amountADesired1000
amountBDesired1000
amountAMin900
amountBMin800

যতক্ষণ এক্সচেঞ্জ রেট 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 টোকেন।

Graph

আপনি সরাসরি কোর কন্ট্রাক্টে লিকুইডিটি জমা করতে পারেন (UniswapV2Pair::mint (opens in a new tab) ব্যবহার করে), কিন্তু কোর কন্ট্রাক্ট শুধুমাত্র চেক করে যে তার নিজের সাথে প্রতারণা করা হচ্ছে না, তাই আপনি লেনদেন সাবমিট করা এবং তা এক্সিকিউট হওয়ার মধ্যবর্তী সময়ে এক্সচেঞ্জ রেট পরিবর্তিত হলে ভ্যালু হারানোর ঝুঁকিতে থাকেন। আপনি যদি পেরিফেরি কন্ট্রাক্ট ব্যবহার করেন, তবে এটি আপনার জমা করার পরিমাণ হিসাব করে এবং তা সাথে সাথে জমা করে দেয়, ফলে এক্সচেঞ্জ রেট পরিবর্তন হয় না এবং আপনি কিছুই হারান না।

লিকুইডিটি জমা করার জন্য একটি লেনদেনের মাধ্যমে এই ফাংশনটি কল করা যেতে পারে। বেশিরভাগ প্যারামিটার উপরের _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,

যখন একজন লিকুইডিটি প্রোভাইডার একটি Token/ETH পেয়ার এক্সচেঞ্জে লিকুইডিটি প্রদান করতে চান, তখন কিছু পার্থক্য থাকে। কন্ট্রাক্টটি লিকুইডিটি প্রোভাইডারের জন্য ETH র‍্যাপ করার বিষয়টি পরিচালনা করে। ব্যবহারকারী কত ETH জমা করতে চান তা নির্দিষ্ট করার কোনো প্রয়োজন নেই, কারণ ব্যবহারকারী কেবল লেনদেনের সাথে সেগুলো পাঠিয়ে দেন (পরিমাণটি msg.value-তে এভেইলেবল থাকে)।

ETH জমা করার জন্য কন্ট্রাক্টটি প্রথমে এটিকে WETH-এ র‍্যাপ করে এবং তারপর WETH-কে পেয়ারে ট্রান্সফার করে। লক্ষ্য করুন যে ট্রান্সফারটি একটি assert-এ র‍্যাপ করা আছে। এর মানে হলো যদি ট্রান্সফারটি ব্যর্থ হয় তবে এই কন্ট্রাক্ট কলটিও ব্যর্থ হয়, এবং তাই র‍্যাপিংটি আসলে ঘটে না।

        liquidity = IUniswapV2Pair(pair).mint(to);
        // ডাস্ট (dust) eth থাকলে তা রিফান্ড করুন
        if (msg.value > amountETH) TransferHelper.safeTransferETH(msg.sender, msg.value - amountETH);
    }

ব্যবহারকারী ইতিমধ্যেই আমাদের ETH পাঠিয়েছেন, তাই যদি কোনো অতিরিক্ত অংশ অবশিষ্ট থাকে (কারণ অন্য টোকেনটি ব্যবহারকারীর ধারণার চেয়ে কম মূল্যবান), তবে আমাদের একটি রিফান্ড ইস্যু করতে হবে।

লিকুইডিটি সরানো

এই ফাংশনগুলো লিকুইডিটি সরিয়ে ফেলবে এবং লিকুইডিটি প্রোভাইডারকে অর্থ ফেরত দেবে।

লিকুইডিটি সরানোর সবচেয়ে সহজ ক্ষেত্র। প্রতিটি টোকেনের একটি ন্যূনতম পরিমাণ রয়েছে যা লিকুইডিটি প্রোভাইডার গ্রহণ করতে সম্মত হন, এবং এটি অবশ্যই ডেডলাইনের আগে ঘটতে হবে।

        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');
    }

প্রথমে ট্রান্সফার করা এবং তারপর এটি বৈধ কিনা তা যাচাই করা ঠিক আছে, কারণ যদি এটি বৈধ না হয় তবে আমরা সমস্ত স্টেট পরিবর্তনগুলো রিভার্ট করে দেব।

ETH-এর জন্য লিকুইডিটি সরানো প্রায় একই রকম, শুধু পার্থক্য হলো আমরা WETH টোকেনগুলো গ্রহণ করি এবং তারপর লিকুইডিটি প্রোভাইডারকে ফেরত দেওয়ার জন্য সেগুলোকে ETH-এ রিডিম করি।

এই ফাংশনগুলো মেটা-ট্রানজেকশন রিলে করে যাতে ইথার ছাড়া ব্যবহারকারীরা পারমিট মেকানিজম ব্যবহার করে পুল থেকে উইথড্র করতে পারে।

এই ফাংশনটি এমন টোকেনগুলোর জন্য ব্যবহার করা যেতে পারে যেগুলোর ট্রান্সফার বা স্টোরেজ ফি রয়েছে। যখন কোনো টোকেনের এমন ফি থাকে তখন আমরা কতটা টোকেন ফেরত পাব তা জানার জন্য removeLiquidity ফাংশনের ওপর নির্ভর করতে পারি না, তাই আমাদের প্রথমে উইথড্র করতে হবে এবং তারপর ব্যালেন্স জানতে হবে।

চূড়ান্ত ফাংশনটি মেটা-ট্রানজেকশনের সাথে স্টোরেজ ফি যুক্ত করে।

ট্রেড

    // **** সোয়াপ ****
    // প্রাথমিক পরিমাণটি ইতিমধ্যে প্রথম পেয়ারে পাঠানো হয়েছে তা প্রয়োজন
    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)। এর পরিবর্তে, সোয়াপ ফাংশনগুলো একটি পাথের কনসেপ্ট সমর্থন করে। একজন ট্রেডার A-এর বিনিময়ে B, B-এর বিনিময়ে C, এবং C-এর বিনিময়ে D এক্সচেঞ্জ করতে পারেন, তাই সরাসরি A-D পেয়ার এক্সচেঞ্জের কোনো প্রয়োজন নেই।

এই মার্কেটগুলোতে দামগুলো সাধারণত সিঙ্ক্রোনাইজড থাকে, কারণ যখন সেগুলো সিঙ্কের বাইরে থাকে তখন এটি আর্বিট্রেজের সুযোগ তৈরি করে। উদাহরণস্বরূপ, তিনটি টোকেন A, B এবং C কল্পনা করুন। প্রতিটি পেয়ারের জন্য একটি করে মোট তিনটি পেয়ার এক্সচেঞ্জ রয়েছে।

  1. প্রাথমিক পরিস্থিতি
  2. একজন ট্রেডার 24.695টি A টোকেন বিক্রি করে 25.305টি B টোকেন পান।
  3. ট্রেডার 25.305টি C টোকেনের বিনিময়ে 24.695টি B টোকেন বিক্রি করেন, এবং প্রায় 0.61টি B টোকেন লাভ হিসেবে রাখেন।
  4. তারপর ট্রেডার 25.305টি A টোকেনের বিনিময়ে 24.695টি C টোকেন বিক্রি করেন, এবং প্রায় 0.61টি C টোকেন লাভ হিসেবে রাখেন। ট্রেডারের কাছে 0.61টি অতিরিক্ত A টোকেনও থাকে (ট্রেডারের কাছে শেষে থাকা 25.305টি, বিয়োগ মূল বিনিয়োগ 24.695টি)।
ধাপA-B এক্সচেঞ্জB-C এক্সচেঞ্জA-C এক্সচেঞ্জ
1A:1000 B:1050 A/B=1.05B:1000 C:1050 B/C=1.05A:1050 C:1000 C/A=1.05
2A:1024.695 B:1024.695 A/B=1B:1000 C:1050 B/C=1.05A:1050 C:1000 C/A=1.05
3A:1024.695 B:1024.695 A/B=1B:1024.695 C:1024.695 B/C=1A:1050 C:1000 C/A=1.05
4A:1024.695 B:1024.695 A/B=1B:1024.695 C:1024.695 B/C=1A: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 কন্ট্রাক্টগুলোর এডড্রেস থাকে। উপরে যেমন ব্যাখ্যা করা হয়েছে, এটি একটি অ্যারে কারণ আপনার কাছে থাকা অ্যাসেট থেকে আপনার কাঙ্ক্ষিত অ্যাসেট পেতে আপনাকে বেশ কয়েকটি পেয়ার এক্সচেঞ্জের মধ্য দিয়ে যেতে হতে পারে।

সলিডিটিতে একটি ফাংশন প্যারামিটার memory বা calldata-তে স্টোর করা যেতে পারে। যদি ফাংশনটি কন্ট্রাক্টের একটি এন্ট্রি পয়েন্ট হয়, যা সরাসরি কোনো ব্যবহারকারী (একটি লেনদেন ব্যবহার করে) বা অন্য কোনো কন্ট্রাক্ট থেকে কল করা হয়, তবে প্যারামিটারের ভ্যালু সরাসরি কল ডাটা থেকে নেওয়া যেতে পারে। যদি ফাংশনটি ইন্টারনালি কল করা হয়, যেমন উপরের _swap, তবে প্যারামিটারগুলোকে memory-তে স্টোর করতে হবে। কল করা কন্ট্রাক্টের দৃষ্টিকোণ থেকে calldata হলো রিড অনলি।

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 কল করুন। এই সবকিছু একই লেনদেনে ঘটছে, তাই পেয়ার এক্সচেঞ্জ জানে যে কোনো অপ্রত্যাশিত টোকেন এই ট্রান্সফারেরই অংশ।

আগের ফাংশন, swapTokensForTokens, একজন ট্রেডারকে তিনি ঠিক কতগুলো ইনপুট টোকেন দিতে ইচ্ছুক এবং এর বিনিময়ে তিনি ন্যূনতম কতগুলো আউটপুট টোকেন পেতে ইচ্ছুক তা নির্দিষ্ট করার অনুমতি দেয়। এই ফাংশনটি বিপরীত সোয়াপ করে, এটি একজন ট্রেডারকে তিনি কতগুলো আউটপুট টোকেন চান এবং সেগুলোর জন্য তিনি সর্বোচ্চ কতগুলো ইনপুট টোকেন দিতে ইচ্ছুক তা নির্দিষ্ট করতে দেয়।

উভয় ক্ষেত্রেই, ট্রেডারকে প্রথমে এই পেরিফেরি কন্ট্রাক্টটিকে একটি অ্যালাউন্স দিতে হবে যাতে এটি সেগুলোকে ট্রান্সফার করতে পারে।

এই চারটি ভ্যারিয়েন্টেই ETH এবং টোকেনের মধ্যে ট্রেডিং জড়িত। একমাত্র পার্থক্য হলো আমরা হয় ট্রেডারের কাছ থেকে ETH গ্রহণ করি এবং এটি ব্যবহার করে WETH মিন্ট করি, অথবা আমরা পাথের শেষ এক্সচেঞ্জ থেকে WETH গ্রহণ করি এবং এটি বার্ন করে, ট্রেডারকে এর ফলে প্রাপ্ত ETH ফেরত পাঠাই।

    // **** সোয়াপ (ফি-অন-ট্রান্সফার টোকেন সমর্থন করে) ****
    // প্রাথমিক পরিমাণটি ইতিমধ্যে প্রথম পেয়ারে পাঠানো হয়েছে তা প্রয়োজন
    function _swapSupportingFeeOnTransferTokens(address[] memory path, address _to) internal virtual {

এটি হলো ট্রান্সফার বা স্টোরেজ ফি থাকা টোকেনগুলো সোয়াপ করার ইন্টারনাল ফাংশন যা (এই সমস্যাটি (opens in a new tab)) সমাধান করে।

ট্রান্সফার ফির কারণে আমরা প্রতিটি ট্রান্সফার থেকে কতটা পাব তা জানার জন্য getAmountsOut ফাংশনের ওপর নির্ভর করতে পারি না (যেমনটা আমরা আসল _swap কল করার আগে করি)। এর পরিবর্তে আমাদের প্রথমে ট্রান্সফার করতে হবে এবং তারপর দেখতে হবে আমরা কতগুলো টোকেন ফেরত পেয়েছি।

দ্রষ্টব্য: তাত্ত্বিকভাবে আমরা _swap-এর পরিবর্তে শুধু এই ফাংশনটি ব্যবহার করতে পারতাম, কিন্তু কিছু নির্দিষ্ট ক্ষেত্রে (উদাহরণস্বরূপ, যদি ট্রান্সফারটি শেষে রিভার্ট হয়ে যায় কারণ প্রয়োজনীয় ন্যূনতম পরিমাণ পূরণের জন্য শেষে পর্যাপ্ত পরিমাণ নেই) এতে বেশি গ্যাস খরচ হবে। ট্রান্সফার ফি টোকেনগুলো বেশ বিরল, তাই যদিও আমাদের সেগুলোর ব্যবস্থা করতে হবে, তবে সমস্ত সোয়াপের ক্ষেত্রে ধরে নেওয়ার কোনো প্রয়োজন নেই যে সেগুলো অন্তত একটির মধ্য দিয়ে যাবে।

এগুলো সাধারণ টোকেনের জন্য ব্যবহৃত একই ভ্যারিয়েন্ট, তবে এগুলো এর পরিবর্তে _swapSupportingFeeOnTransferTokens কল করে।

এই ফাংশনগুলো হলো শুধু প্রক্সি যা UniswapV2Library ফাংশনগুলোকে কল করে।

UniswapV2Migrator.sol

এই কন্ট্রাক্টটি পুরনো v1 থেকে v2-তে এক্সচেঞ্জগুলো মাইগ্রেট করতে ব্যবহৃত হয়েছিল। যেহেতু সেগুলো মাইগ্রেট করা হয়ে গেছে, তাই এটি আর প্রাসঙ্গিক নয়।

লাইব্রেরিগুলো

SafeMath লাইব্রেরি (opens in a new tab) খুব ভালোভাবে ডকুমেন্টেড, তাই এখানে এটি ডকুমেন্ট করার কোনো প্রয়োজন নেই।

Math

এই লাইব্রেরিতে কিছু গাণিতিক ফাংশন রয়েছে যা সাধারণত Solidity কোডে প্রয়োজন হয় না, তাই এগুলো এই ভাষার অংশ নয়।

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 হিসেবে এনকোড করে এই কাজটি করে। এটি আমাদের কোনো পরিবর্তন ছাড়াই মূল যোগ এবং বিয়োগ অপকোডগুলো ব্যবহার করতে দেয়।

Q112 হলো একের জন্য এনকোডিং।

    // একটি 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

এই লাইব্রেরিটি শুধুমাত্র পেরিফেরি কন্ট্রাক্টগুলো দ্বারা ব্যবহৃত হয়

এডড্রেস অনুযায়ী দুটি টোকেন সাজান, যাতে আমরা তাদের জন্য পেয়ার এক্সচেঞ্জের এডড্রেস পেতে পারি। এটি প্রয়োজনীয় কারণ অন্যথায় আমাদের দুটি সম্ভাবনা থাকত, একটি A,B প্যারামিটারের জন্য এবং অন্যটি B,A প্যারামিটারের জন্য, যার ফলে একটির পরিবর্তে দুটি এক্সচেঞ্জ তৈরি হতো।

এই ফাংশনটি দুটি টোকেনের জন্য পেয়ার এক্সচেঞ্জের এডড্রেস গণনা করে। এই কন্ট্রাক্টটি 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;
    }

এই ফাংশনটি আপনাকে টোকেন A-এর বিনিময়ে প্রাপ্ত টোকেন B-এর পরিমাণ দেয়, যদি কোনো ফি জড়িত না থাকে। এই গণনাটি বিবেচনা করে যে ট্রান্সফারটি এক্সচেঞ্জ রেট পরিবর্তন করে।

    // একটি অ্যাসেটের ইনপুট পরিমাণ এবং পেয়ার রিজার্ভ দেওয়া হলে, অন্য অ্যাসেটের সর্বোচ্চ আউটপুট পরিমাণ রিটার্ন করে
    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);
    }

এই ফাংশনটি মোটামুটি একই কাজ করে, তবে এটি আউটপুটের পরিমাণ পায় এবং ইনপুট প্রদান করে।

এই দুটি ফাংশন মানগুলো শনাক্ত করার কাজ পরিচালনা করে যখন একাধিক পেয়ার এক্সচেঞ্জের মধ্য দিয়ে যাওয়ার প্রয়োজন হয়।

ট্রান্সফার হেল্পার

এই লাইব্রেরিটি (opens in a new tab) ERC-20 এবং ইথিরিয়াম ট্রান্সফারের চারপাশে সফলতার চেক যোগ করে যাতে একটি রিভার্ট এবং একটি false মান রিটার্নকে একইভাবে বিবেচনা করা যায়।

আমরা দুটি উপায়ের যেকোনো একটিতে ভিন্ন একটি কন্ট্রাক্ট কল করতে পারি:

        require(
            success && (data.length == 0 || abi.decode(data, (bool))),
            'TransferHelper::safeApprove: approve failed'
        );
    }

ERC-20 স্ট্যান্ডার্ডের আগে তৈরি হওয়া টোকেনগুলোর সাথে ব্যাকওয়ার্ড কম্প্যাটিবিলিটির স্বার্থে, একটি ERC-20 কল রিভার্ট হওয়ার মাধ্যমে (যে ক্ষেত্রে success হলো false) অথবা সফল হয়ে একটি false মান রিটার্ন করার মাধ্যমে (যে ক্ষেত্রে আউটপুট ডাটা থাকে, এবং আপনি যদি এটিকে বুলিয়ান হিসেবে ডিকোড করেন তবে আপনি false পাবেন) ব্যর্থ হতে পারে।

এই ফাংশনটি ERC-20-এর ট্রান্সফার কার্যকারিতা (opens in a new tab) বাস্তবায়ন করে, যা একটি একাউন্টকে অন্য একটি একাউন্ট দ্বারা প্রদত্ত অ্যালাউন্স খরচ করার অনুমতি দেয়।

এই ফাংশনটি 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 পৃষ্ঠার একটি দীর্ঘ প্রবন্ধ। আপনি যদি এতদূর এসে থাকেন, তবে আপনাকে অভিনন্দন! আশা করি এতক্ষণে আপনি একটি বাস্তব-জীবনের অ্যাপ্লিকেশন (ছোট নমুনা প্রোগ্রামের বিপরীতে) লেখার বিবেচ্য বিষয়গুলো বুঝতে পেরেছেন এবং আপনার নিজের ব্যবহারের জন্য কন্ট্রাক্ট লিখতে আরও ভালোভাবে সক্ষম হবেন।

এখন যান এবং দরকারী কিছু লিখুন এবং আমাদের চমকে দিন।

আমার আরও কাজের জন্য এখানে দেখুন (opens in a new tab)

পেজ সর্বশেষ আপডেট করা হয়েছে: 3 এপ্রিল, 2026

এই টিউটোরিয়ালটি কি সহায়ক ছিল?