ইউনিসোয়াপ-v2 কন্ট্রাক্ট ওয়াক-থ্রু
ভূমিকা
ইউনিসোয়াপ v2 (opens in a new tab) যেকোনো দুটি ERC-20 টোকেনের মধ্যে একটি এক্সচেঞ্জ মার্কেট তৈরি করতে পারে। এই নিবন্ধে আমরা এই প্রোটোকল বাস্তবায়নকারী কন্ট্রাক্টগুলোর সোর্স কোড নিয়ে আলোচনা করব এবং দেখব কেন সেগুলো এভাবে লেখা হয়েছে।
ইউনিসোয়াপ কী করে?
মূলত, দুই ধরনের ব্যবহারকারী রয়েছে: তারল্য প্রদানকারী এবং ট্রেডার।
তারল্য প্রদানকারী পুলটিতে এমন দুটি টোকেন প্রদান করে যা এক্সচেঞ্জ করা যায় (আমরা সেগুলোকে Token0 এবং Token1 বলব)। এর বিনিময়ে, তারা তৃতীয় একটি টোকেন পায় যা পুলের আংশিক মালিকানার প্রতিনিধিত্ব করে, যাকে তারল্য টোকেন বলা হয়।
ট্রেডাররা পুলে এক ধরনের টোকেন পাঠায় এবং অন্যটি গ্রহণ করে (উদাহরণস্বরূপ, Token0 পাঠায় এবং Token1 গ্রহণ করে) যা তারল্য প্রদানকারীদের দ্বারা সরবরাহকৃত পুল থেকে আসে। এক্সচেঞ্জ রেট নির্ধারিত হয় পুলে থাকা Token0 এবং Token1-এর আপেক্ষিক সংখ্যার ওপর ভিত্তি করে। এছাড়া, পুলটি তারল্য পুলের জন্য পুরস্কার হিসেবে একটি ছোট শতাংশ কেটে নেয়।
যখন তারল্য প্রদানকারীরা তাদের সম্পদ ফেরত পেতে চায়, তখন তারা পুল টোকেনগুলো পোড়াতে পারে এবং তাদের টোকেনগুলো ফেরত পেতে পারে, যার মধ্যে তাদের পুরস্কারের অংশও অন্তর্ভুক্ত থাকে।
আরও বিস্তারিত বিবরণের জন্য এখানে ক্লিক করুন (opens in a new tab)।
কেন v2? কেন v3 নয়?
ইউনিসোয়াপ v3 (opens in a new tab) হলো এমন একটি আপগ্রেড যা v2-এর চেয়ে অনেক বেশি জটিল। প্রথমে v2 শেখা এবং তারপর v3-তে যাওয়া বেশি সহজ।
কোর কন্ট্রাক্ট বনাম পেরিফেরি কন্ট্রাক্ট
ইউনিসোয়াপ v2 দুটি উপাদানে বিভক্ত, একটি কোর এবং একটি পেরিফেরি। এই বিভাজনের ফলে কোর কন্ট্রাক্টগুলো, যা সম্পদ ধারণ করে এবং তাই সেগুলোকে নিরাপদ হতেই হয়, আরও সহজ এবং অডিট করার জন্য সুবিধাজনক হয়। ট্রেডারদের প্রয়োজনীয় সমস্ত অতিরিক্ত কার্যকারিতা এরপর পেরিফেরি কন্ট্রাক্টগুলোর মাধ্যমে প্রদান করা যেতে পারে।
ডেটা এবং কন্ট্রোল ফ্লো
ইউনিসোয়াপ-এর তিনটি প্রধান কাজ করার সময় ডেটা এবং কন্ট্রোলের যে ফ্লো বা প্রবাহ ঘটে তা নিচে দেওয়া হলো:
- বিভিন্ন টোকেনের মধ্যে সোয়াপ করা
- মার্কেটে তারল্য যোগ করা এবং পেয়ার এক্সচেঞ্জ ERC-20 তারল্য টোকেন দিয়ে পুরস্কৃত হওয়া
- ERC-20 তারল্য টোকেন পোড়ানো এবং সেই ERC-20 টোকেনগুলো ফেরত পাওয়া যা পেয়ার এক্সচেঞ্জ ট্রেডারদের এক্সচেঞ্জ করার অনুমতি দেয়
সোয়াপ
এটি সবচেয়ে সাধারণ ফ্লো, যা ট্রেডাররা ব্যবহার করে থাকেন:
কলার
- পেরিফেরি অ্যাকাউন্টকে সোয়াপ করার পরিমাণের সমান একটি অ্যালাউন্স প্রদান করুন।
- পেরিফেরি কন্ট্রাক্টের অনেকগুলো সোয়াপ ফাংশনের মধ্যে যেকোনো একটি কল করুন (কোনটি কল করবেন তা নির্ভর করে ETH জড়িত আছে কি না, ট্রেডার জমা দেওয়ার টোকেনের পরিমাণ বা ফেরত পাওয়ার টোকেনের পরিমাণ নির্দিষ্ট করেছেন কি না ইত্যাদির ওপর)।
প্রতিটি সোয়াপ ফাংশন একটি
pathগ্রহণ করে, যা হলো অতিক্রম করার জন্য এক্সচেঞ্জগুলোর একটি অ্যারে।
পেরিফেরি কন্ট্রাক্টে (UniswapV2Router02.sol)
- পাথের প্রতিটি এক্সচেঞ্জে যে পরিমাণ ট্রেড করা প্রয়োজন তা শনাক্ত করুন।
- পাথের ওপর ইটারেট করে। পথের প্রতিটি এক্সচেঞ্জের জন্য এটি ইনপুট টোকেন পাঠায় এবং তারপর এক্সচেঞ্জের
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;
পুল কন্ট্রাক্টের অনেক গণনায় ভগ্নাংশের প্রয়োজন হয়। তবে, EVM ভগ্নাংশ সমর্থন করে না।
ইউনিসোয়াপ যে সমাধানটি খুঁজে পেয়েছে তা হলো 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% ট্রেডিং ফি উপেক্ষা করেছি তাই সংখ্যাগুলো পুরোপুরি সঠিক নয়।
| ইভেন্ট | 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;
নিরাপত্তা দুর্বলতার একটি শ্রেণি রয়েছে যা রিএন্ট্রান্সি অপব্যবহারের (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) {
যদি অতিবাহিত সময় শূন্য না হয়, তবে এর অর্থ হলো আমরা এই ব্লকের প্রথম এক্সচেঞ্জ ট্রানজ্যাকশন। সেক্ষেত্রে, আমাদের কস্ট অ্যাকুমুলেটরগুলো আপডেট করতে হবে।
// * কখনও ওভারফ্লো হয় না, এবং + ওভারফ্লো কাঙ্ক্ষিত
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) এর বৃদ্ধির ১/৬ অংশের সমতুল্য তারল্য মিন্ট করে
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) ছিল যা কন্ট্রাক্টগুলোকে তাদের অপ্রয়োজনীয় স্টোরেজ শূন্য করে ইথেরিয়াম স্টেটের সামগ্রিক আকার কমাতে উৎসাহিত করত।
এই কোডটি সম্ভব হলে সেই রিফান্ড পায়।
বাহ্যিকভাবে অ্যাক্সেসযোগ্য ফাংশন
মনে রাখবেন যে যদিও যেকোনো ট্রানজ্যাকশন বা কন্ট্রাক্ট এই ফাংশনগুলোকে কল করতে পারে, এগুলো পেরিফেরি কন্ট্রাক্ট থেকে কল করার জন্য ডিজাইন করা হয়েছে। আপনি যদি এগুলোকে সরাসরি কল করেন তবে আপনি পেয়ার এক্সচেঞ্জকে প্রতারণা করতে পারবেন না, তবে আপনি ভুলের মাধ্যমে মান হারাতে পারেন।
মিন্ট
// এই লো-লেভেল ফাংশনটি এমন একটি কন্ট্রাক্ট থেকে কল করা উচিত যা গুরুত্বপূর্ণ নিরাপত্তা যাচাই করে
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 আরও মূল্যবান, এটি থেকে মান বের করতে।
| ইভেন্ট | 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 |
| আরবিট্রেজের পরে | ~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) আপডেট করুন এবং উপযুক্ত ইভেন্ট এমিট করুন।
পোড়ানো
// এই লো-লেভেল ফাংশনটি এমন একটি কন্ট্রাক্ট থেকে কল করা উচিত যা গুরুত্বপূর্ণ নিরাপত্তা যাচাই করে
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; // ব্যালেন্স ব্যবহার করা আনুপাতিক বণ্টন নিশ্চিত করে
amount1 = liquidity.mul(balance1) / _totalSupply; // ব্যালেন্স ব্যবহার করা আনুপাতিক বণ্টন নিশ্চিত করে
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 ফাংশনের মিরর ইমেজ।
সোয়াপ
// এই লো-লেভেল ফাংশনটি এমন একটি কন্ট্রাক্ট থেকে কল করা উচিত যা গুরুত্বপূর্ণ নিরাপত্তা যাচাই করে
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), পৃষ্ঠা 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); // অপটিমিস্টিক্যালি টোকেন হস্তান্তর করে
এই হস্তান্তরটি আশাবাদী, কারণ সমস্ত শর্ত পূরণ হয়েছে কিনা তা নিশ্চিত হওয়ার আগেই আমরা হস্তান্তর করি। ইথেরিয়াম-এ এটি ঠিক আছে কারণ যদি কলের পরে শর্তগুলো পূরণ না হয় তবে আমরা এটি থেকে রিভার্ট করি এবং এটি যে কোনো পরিবর্তন তৈরি করেছে তা বাতিল করি।
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 আপডেট করুন, এবং প্রয়োজন হলে প্রাইস অ্যাকুমুলেটর এবং টাইমস্ট্যাম্প আপডেট করুন এবং একটি ইভেন্ট এমিট করুন।
সিঙ্কিং বা স্কিম
পেয়ার এক্সচেঞ্জ যে রিজার্ভগুলো আছে বলে মনে করে তার সাথে প্রকৃত ব্যালেন্সগুলোর সিঙ্কিং হারিয়ে যাওয়া সম্ভব।
কন্ট্রাক্টের সম্মতি ছাড়া টোকেন উত্তোলন করার কোনো উপায় নেই, তবে জমা দেওয়া একটি ভিন্ন বিষয়। একটি অ্যাকাউন্ট 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 টোকেনগুলো সেই কন্ট্রাক্টগুলোর ঠিকানা দ্বারা শনাক্ত করা হয় যা সেগুলোকে বাস্তবায়ন করে, তাই কী (key) এবং মান (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 টোকেনের মধ্যে একটি পেয়ার এক্সচেঞ্জ তৈরি করা। মনে রাখবেন যে যেকেউ এই ফাংশনটি কল করতে পারে। একটি নতুন পেয়ার এক্সচেঞ্জ তৈরি করতে আপনার ইউনিসোয়াপ থেকে অনুমতির প্রয়োজন নেই।
require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES');
(address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
আমরা চাই নতুন এক্সচেঞ্জের ঠিকানাটি ডিটারমিনিস্টিক হোক, যাতে এটি অফচেইন-এ আগে থেকেই গণনা করা যায় (এটি লেয়ার ২ (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) গ্রহণ করে।
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) হিসেবে বিবেচনা করুন।
পেরিফেরি কন্ট্রাক্ট
পেরিফেরি কন্ট্রাক্টগুলো হলো ইউনিসোয়াপ-এর API (অ্যাপ্লিকেশন প্রোগ্রাম ইন্টারফেস)। এগুলো অন্যান্য কন্ট্রাক্ট বা বিকেন্দ্রীকৃত অ্যাপ্লিকেশন (dapp) থেকে এক্সটার্নাল কলের জন্য উপলব্ধ। আপনি সরাসরি কোর কন্ট্রাক্টগুলোতে কল করতে পারেন, তবে এটি আরও জটিল এবং ভুল করলে আপনার ভ্যালু হারাতে পারে। কোর কন্ট্রাক্টগুলোতে কেবল এমন টেস্ট থাকে যা নিশ্চিত করে যে তাদের সাথে প্রতারণা করা হচ্ছে না, অন্য কারও জন্য কোনো স্যানিটি চেক থাকে না। সেগুলো পেরিফেরিতে থাকে যাতে প্রয়োজন অনুযায়ী আপডেট করা যায়।
UniswapV2Router01.sol
এই কন্ট্রাক্টে (opens in a new tab) সমস্যা রয়েছে এবং এটি আর ব্যবহার করা উচিত নয় (opens in a new tab)। সৌভাগ্যবশত, পেরিফেরি কন্ট্রাক্টগুলো স্টেটলেস এবং কোনো সম্পদ ধারণ করে না, তাই এটিকে বাতিল করা এবং এর পরিবর্তে মানুষকে 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
এগুলো হলো জমা করার জন্য ন্যূনতম গ্রহণযোগ্য পরিমাণ। যদি এই পরিমাণ বা তার বেশি দিয়ে ট্রানজ্যাকশন সম্পন্ন করা না যায়, তবে এটি রিভার্ট করুন। আপনি যদি এই বৈশিষ্ট্যটি না চান, তবে কেবল শূন্য (0) উল্লেখ করুন।
তারল্য প্রদানকারীরা সাধারণত একটি ন্যূনতম পরিমাণ নির্দিষ্ট করেন, কারণ তারা ট্রানজ্যাকশনটিকে বর্তমান বিনিময় হারের কাছাকাছি একটি হারে সীমাবদ্ধ করতে চান। যদি বিনিময় হার খুব বেশি ওঠানামা করে তবে এর অর্থ হতে পারে এমন কোনো খবর যা অন্তর্নিহিত মান পরিবর্তন করে, এবং তারা ম্যানুয়ালি সিদ্ধান্ত নিতে চান কী করতে হবে।
উদাহরণস্বরূপ, এমন একটি পরিস্থিতি কল্পনা করুন যেখানে বিনিময় হার এক-এর বিপরীতে এক এবং তারল্য প্রদানকারী এই মানগুলো নির্দিষ্ট করেন:
| 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);
// ডাস্ট eth থাকলে তা ফেরত দেয়
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)। এর পরিবর্তে, সোয়াপ ফাংশনগুলো একটি পাথের ধারণা সমর্থন করে। একজন ট্রেডার A-কে B-এর জন্য, B-কে C-এর জন্য এবং C-কে D-এর জন্য এক্সচেঞ্জ করতে পারেন, তাই সরাসরি A-D পেয়ার এক্সচেঞ্জের কোনো প্রয়োজন নেই।
এই মার্কেটগুলোতে দামগুলো সিঙ্ক্রোনাইজড থাকে, কারণ যখন সেগুলো সিঙ্কের বাইরে থাকে তখন এটি আর্বিট্রেজের সুযোগ তৈরি করে। উদাহরণস্বরূপ, তিনটি টোকেন 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 এক্সচেঞ্জ | B-C এক্সচেঞ্জ | A-C এক্সচেঞ্জ |
|---|---|---|---|
| 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 কন্ট্রাক্টগুলোর ঠিকানা থাকে। উপরে যেমন ব্যাখ্যা করা হয়েছে, এটি একটি অ্যারে কারণ আপনার কাছে থাকা সম্পদ থেকে আপনার কাঙ্ক্ষিত সম্পদে পৌঁছানোর জন্য আপনাকে বেশ কয়েকটি পেয়ার এক্সচেঞ্জের মধ্য দিয়ে যেতে হতে পারে।
Solidity-তে একটি ফাংশন প্যারামিটার 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 কল করুন। এটি সবই একই ট্রানজ্যাকশনে ঘটছে, তাই পেয়ার এক্সচেঞ্জ জানে যে কোনো অপ্রত্যাশিত টোকেন এই হস্তান্তরের অংশ।
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, একজন ট্রেডারকে তিনি ঠিক কতগুলো ইনপুট টোকেন দিতে ইচ্ছুক এবং বিনিময়ে তিনি ন্যূনতম কতগুলো আউটপুট টোকেন পেতে ইচ্ছুক তা নির্দিষ্ট করার অনুমতি দেয়। এই ফাংশনটি বিপরীত সোয়াপ করে, এটি একজন ট্রেডারকে তিনি কতগুলো আউটপুট টোকেন চান এবং সেগুলোর জন্য তিনি সর্বোচ্চ কতগুলো ইনপুট টোকেন দিতে ইচ্ছুক তা নির্দিষ্ট করতে দেয়।
উভয় ক্ষেত্রেই, ট্রেডারকে প্রথমে এই পেরিফেরি কন্ট্রাক্টকে একটি অ্যালাউন্স দিতে হবে যাতে এটি সেগুলোকে হস্তান্তর করতে পারে।
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);
// ডাস্ট eth থাকলে তা ফেরত দেয়
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;
{ // স্ট্যাক টু ডিপ (stack too deep) এরর এড়াতে স্কোপ
(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 হলো একের জন্য এনকোডিং।
// একটি 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
এই লাইব্রেরিটি শুধুমাত্র পেরিফেরি কন্ট্রাক্টগুলো ব্যবহার করে
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;
}
যদি কোনো ফি জড়িত না থাকে, তবে টোকেন 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);
}
এই ফাংশনটি প্রায় একই কাজ করে, তবে এটি আউটপুটের পরিমাণ পায় এবং ইনপুট প্রদান করে।
// যেকোনো সংখ্যক পেয়ারে চেইনড 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 পৃষ্ঠার একটি দীর্ঘ নিবন্ধ। আপনি যদি এতদূর পড়ে থাকেন, তবে আপনাকে অভিনন্দন! আশা করি এতক্ষণে আপনি বুঝতে পেরেছেন যে একটি বাস্তব-জীবনের অ্যাপ্লিকেশন (ছোট নমুনা প্রোগ্রামের বিপরীতে) লেখার ক্ষেত্রে কী কী বিষয় বিবেচনা করতে হয় এবং আপনার নিজের ব্যবহারের জন্য কন্ট্রাক্ট লিখতে আপনি এখন আরও বেশি সক্ষম।
এখন যান, দরকারী কিছু লিখুন এবং আমাদের চমকে দিন।
