স্ক্যাম টোকেনগুলোর ব্যবহৃত কিছু কৌশল এবং কীভাবে সেগুলো শনাক্ত করা যায়
এই টিউটোরিয়ালে আমরা একটি স্ক্যাম টোকেন (opens in a new tab) বিশ্লেষণ করে দেখব স্ক্যামাররা কী ধরনের কৌশল ব্যবহার করে এবং কীভাবে সেগুলো প্রয়োগ করে। টিউটোরিয়ালটির শেষে আপনি ERC-20 টোকেন কন্ট্রাক্ট, এগুলোর সক্ষমতা এবং কেন সন্দেহপ্রবণ হওয়া প্রয়োজন সে সম্পর্কে আরও বিশদ ধারণা পাবেন। এরপর আমরা সেই স্ক্যাম টোকেন থেকে নির্গত ইভেন্টগুলো দেখব এবং কীভাবে স্বয়ংক্রিয়ভাবে শনাক্ত করা যায় যে এটি বৈধ নয় তা জানব।
স্ক্যাম টোকেন - এগুলো কী, মানুষ কেন এগুলো তৈরি করে এবং কীভাবে এগুলো এড়িয়ে চলবেন
Ethereum-এর অন্যতম সাধারণ ব্যবহার হলো কোনো গোষ্ঠীর জন্য একটি ট্রেডযোগ্য টোকেন তৈরি করা, যা এক অর্থে তাদের নিজস্ব মুদ্রা। তবে, যেখানেই বৈধ ব্যবহারের মাধ্যমে ভ্যালু তৈরি হয়, সেখানেই এমন অপরাধীরা থাকে যারা সেই ভ্যালু নিজেদের জন্য চুরি করার চেষ্টা করে।
একজন ব্যবহারকারীর দৃষ্টিকোণ থেকে আপনি এই বিষয়ে ethereum.org-এর অন্য জায়গায় আরও পড়তে পারেন। এই টিউটোরিয়ালটি মূলত একটি স্ক্যাম টোকেন বিশ্লেষণ করে কীভাবে এটি করা হয় এবং কীভাবে এটি শনাক্ত করা যায় তার ওপর ফোকাস করে।
আমি কীভাবে বুঝব যে wARB একটি স্ক্যাম?
আমরা যে টোকেনটি বিশ্লেষণ করব তা হলো wARB (opens in a new tab), যা বৈধ ARB টোকেন (opens in a new tab)-এর সমতুল্য হওয়ার ভান করে।
কোনটি বৈধ টোকেন তা জানার সবচেয়ে সহজ উপায় হলো এর মূল সংস্থা, Arbitrum (opens in a new tab)-এর দিকে নজর দেওয়া। বৈধ এডড্রেসগুলো তাদের ডকুমেন্টেশনে (opens in a new tab) উল্লেখ করা আছে।
সোর্স কোড কেন পাওয়া যাচ্ছে?
সাধারণত আমরা আশা করি যারা অন্যদের স্ক্যাম করার চেষ্টা করে তারা গোপনীয়তা বজায় রাখবে, এবং সত্যিই অনেক স্ক্যাম টোকেনের কোড পাওয়া যায় না (উদাহরণস্বরূপ, এটি (opens in a new tab) এবং এটি (opens in a new tab))।
তবে, বৈধ টোকেনগুলো সাধারণত তাদের সোর্স কোড প্রকাশ করে, তাই নিজেদের বৈধ হিসেবে উপস্থাপন করতে স্ক্যাম টোকেনের নির্মাতারাও মাঝে মাঝে একই কাজ করে। wARB (opens in a new tab) হলো এমন একটি টোকেন যার সোর্স কোড পাওয়া যায়, যা এটিকে বুঝতে সহজ করে তোলে।
যদিও কন্ট্রাক্ট ডিপ্লয়াররা সোর্স কোড প্রকাশ করবে কি না তা বেছে নিতে পারে, তবে তারা ভুল সোর্স কোড প্রকাশ করতে পারে না। ব্লক এক্সপ্লোরার স্বাধীনভাবে প্রদত্ত সোর্স কোড কম্পাইল করে, এবং যদি এটি হুবহু একই বাইটকোড না পায়, তবে এটি সেই সোর্স কোডটি প্রত্যাখ্যান করে। আপনি Etherscan সাইটে এই সম্পর্কে আরও পড়তে পারেন (opens in a new tab)।
বৈধ ERC-20 টোকেনগুলোর সাথে তুলনা
আমরা এই টোকেনটিকে বৈধ ERC-20 টোকেনগুলোর সাথে তুলনা করতে যাচ্ছি। বৈধ ERC-20 টোকেনগুলো সাধারণত কীভাবে লেখা হয় সে সম্পর্কে আপনার যদি ধারণা না থাকে, তবে এই টিউটোরিয়ালটি দেখুন।
প্রিভিলেজড এডড্রেসগুলোর জন্য কনস্ট্যান্ট
কন্ট্রাক্টগুলোর মাঝে মাঝে প্রিভিলেজড এডড্রেস প্রয়োজন হয়। দীর্ঘমেয়াদী ব্যবহারের জন্য ডিজাইন করা কন্ট্রাক্টগুলো কিছু প্রিভিলেজড এডড্রেসকে সেই এডড্রেসগুলো পরিবর্তন করার অনুমতি দেয়, উদাহরণস্বরূপ একটি নতুন মাল্টিসিগ কন্ট্রাক্ট ব্যবহার সক্ষম করতে। এটি করার বেশ কয়েকটি উপায় রয়েছে।
HOP টোকেন কন্ট্রাক্ট (opens in a new tab) Ownable (opens in a new tab) প্যাটার্ন ব্যবহার করে। প্রিভিলেজড এডড্রেসটি স্টোরেজে _owner নামক একটি ফিল্ডে রাখা হয় (তৃতীয় ফাইল, Ownable.sol দেখুন)।
1abstract contract Ownable is Context {2 address private _owner;3 .4 .5 .6}ARB টোকেন কন্ট্রাক্ট (opens in a new tab)-এ সরাসরি কোনো প্রিভিলেজড এডড্রেস নেই। তবে, এর কোনো প্রয়োজনও নেই। এটি এডড্রেস 0xb50721bcf8d664c30412cfbc6cf7a15145234ad1 (opens in a new tab)-এ একটি proxy (opens in a new tab)-র পেছনে থাকে। সেই কন্ট্রাক্টটিতে একটি প্রিভিলেজড এডড্রেস রয়েছে (চতুর্থ ফাইল, ERC1967Upgrade.sol দেখুন) যা আপগ্রেডের জন্য ব্যবহার করা যেতে পারে।
1 /* *2 * @dev EIP1967 অ্যাডমিন স্লটে একটি নতুন ঠিকানা সংরক্ষণ করে। */3 function _setAdmin(address newAdmin) private {4 require(newAdmin != address(0), "ERC1967: new admin is the zero address");5 StorageSlot.getAddressSlot(_ADMIN_SLOT).value = newAdmin;6 }এর বিপরীতে, wARB কন্ট্রাক্টে একটি হার্ড কোডেড contract_owner রয়েছে।
1contract WrappedArbitrum is Context, IERC20 {2 .3 .4 .5 address deployer = 0xB50721BCf8d664c30412Cfbc6cf7a15145234ad1;6 address public contract_owner = 0xb40dE7b1beE84Ff2dc22B70a049A07A13a411A33;7 .8 .9 .10}সব দেখানএই কন্ট্রাক্ট ওনার (opens in a new tab) এমন কোনো কন্ট্রাক্ট নয় যা বিভিন্ন সময়ে বিভিন্ন একাউন্ট দ্বারা নিয়ন্ত্রিত হতে পারে, বরং এটি একটি এক্সটার্নালি ওনড একাউন্ট। এর মানে হলো এটি সম্ভবত কোনো ব্যক্তির স্বল্পমেয়াদী ব্যবহারের জন্য ডিজাইন করা হয়েছে, এমন কোনো দীর্ঘমেয়াদী সমাধান হিসেবে নয় যা একটি মূল্যবান ERC-20 নিয়ন্ত্রণ করবে।
এবং সত্যিই, আমরা যদি Etherscan-এ দেখি তবে দেখতে পাব যে স্ক্যামার এই কন্ট্রাক্টটি ১৯ মে, ২০২৩ তারিখে মাত্র ১২ ঘণ্টার জন্য ব্যবহার করেছিল (প্রথম লেনদেন (opens in a new tab) থেকে শেষ লেনদেন (opens in a new tab))।
ভুয়া _transfer ফাংশন
একটি ইন্টারনাল _transfer ফাংশন ব্যবহার করে প্রকৃত লেনদেন সম্পন্ন হওয়াটাই স্ট্যান্ডার্ড।
wARB-এ এই ফাংশনটি প্রায় বৈধ বলেই মনে হয়:
1 function _transfer(address sender, address recipient, uint256 amount) internal virtual{2 require(sender != address(0), "ERC20: transfer from the zero address");3 require(recipient != address(0), "ERC20: transfer to the zero address");45 _beforeTokenTransfer(sender, recipient, amount);67 _balances[sender] = _balances[sender].sub(amount, "ERC20: transfer amount exceeds balance");8 _balances[recipient] = _balances[recipient].add(amount);9 if (sender == contract_owner){10 sender = deployer;11 }12 emit Transfer(sender, recipient, amount);13 }সব দেখানসন্দেহজনক অংশটি হলো:
1 if (sender == contract_owner){2 sender = deployer;3 }4 emit Transfer(sender, recipient, amount);যদি কন্ট্রাক্ট ওনার টোকেন পাঠায়, তবে Transfer ইভেন্ট কেন দেখায় যে সেগুলো deployer থেকে এসেছে?
তবে, এর চেয়েও গুরুত্বপূর্ণ একটি বিষয় রয়েছে। এই _transfer ফাংশনটি কে কল করে? এটি বাইরে থেকে কল করা যায় না, এটি internal হিসেবে চিহ্নিত। এবং আমাদের কাছে থাকা কোডে _transfer-এর কোনো কল অন্তর্ভুক্ত নেই। স্পষ্টতই, এটি এখানে একটি ফাঁদ হিসেবে রয়েছে।
1 function transfer(address recipient, uint256 amount) public virtual override returns (bool) {2 _f_(_msgSender(), recipient, amount);3 return true;4 }56 function transferFrom(address sender, address recipient, uint256 amount) public virtual override returns (bool) {7 _f_(sender, recipient, amount);8 _approve(sender, _msgSender(), _allowances[sender][_msgSender()].sub(amount, "ERC20: transfer amount exceeds allowance"));9 return true;10 }সব দেখানযখন আমরা টোকেন ট্রান্সফার করার জন্য কল করা ফাংশনগুলো, transfer এবং transferFrom দেখি, তখন আমরা দেখতে পাই যে তারা সম্পূর্ণ ভিন্ন একটি ফাংশন, _f_-কে কল করে।
আসল _f_ ফাংশন
1 function _f_(address sender, address recipient, uint256 amount) internal _mod_(sender,recipient,amount) virtual {2 require(sender != address(0), "ERC20: transfer from the zero address");3 require(recipient != address(0), "ERC20: transfer to the zero address");45 _beforeTokenTransfer(sender, recipient, amount);67 _balances[sender] = _balances[sender].sub(amount, "ERC20: transfer amount exceeds balance");8 _balances[recipient] = _balances[recipient].add(amount);9 if (sender == contract_owner){1011 sender = deployer;12 }13 emit Transfer(sender, recipient, amount);14 }সব দেখানএই ফাংশনে দুটি সম্ভাব্য রেড ফ্ল্যাগ বা সতর্ক সংকেত রয়েছে।
-
ফাংশন মডিফায়ার (opens in a new tab)
_mod_-এর ব্যবহার। তবে, যখন আমরা সোর্স কোডটি দেখি তখন বুঝতে পারি যে_mod_আসলে ক্ষতিকারক নয়।1modifier _mod_(address sender, address recipient, uint256 amount){2 _;3} -
একই সমস্যা যা আমরা
_transfer-এ দেখেছি, তা হলো যখনcontract_ownerটোকেন পাঠায় তখন মনে হয় সেগুলোdeployerথেকে এসেছে।
ভুয়া ইভেন্ট ফাংশন dropNewTokens
এখন আমরা এমন কিছুতে আসি যা দেখতে একটি আসল স্ক্যামের মতো। পড়ার সুবিধার জন্য আমি ফাংশনটি কিছুটা এডিট করেছি, তবে এটি কার্যকারিতার দিক থেকে একই।
1function dropNewTokens(address uPool,2 address[] memory eReceiver,3 uint256[] memory eAmounts) public auth()এই ফাংশনে auth() মডিফায়ার রয়েছে, যার মানে এটি শুধুমাত্র কন্ট্রাক্ট ওনার দ্বারা কল করা যেতে পারে।
1modifier auth() {2 require(msg.sender == contract_owner, "Not allowed to interact");3 _;4}এই বিধিনিষেধটি পুরোপুরি যৌক্তিক, কারণ আমরা চাইব না যে কোনো র্যান্ডম একাউন্ট টোকেন বিতরণ করুক। তবে, ফাংশনের বাকি অংশটি সন্দেহজনক।
1{2 for (uint256 i = 0; i < eReceiver.length; i++) {3 emit Transfer(uPool, eReceiver[i], eAmounts[i]);4 }5}একটি পুল একাউন্ট থেকে রিসিভারদের একটি অ্যারেতে অ্যামাউন্টের একটি অ্যারে ট্রান্সফার করার ফাংশনটি পুরোপুরি যৌক্তিক। এমন অনেক ব্যবহার রয়েছে যেখানে আপনি একটি একক উৎস থেকে একাধিক গন্তব্যে টোকেন বিতরণ করতে চাইবেন, যেমন পেরোল, এয়ারড্রপ ইত্যাদি। একাধিক লেনদেন ইস্যু করার পরিবর্তে, বা এমনকি একই লেনদেনের অংশ হিসেবে একটি ভিন্ন কন্ট্রাক্ট থেকে ERC-20-কে একাধিকবার কল করার চেয়ে একটি একক লেনদেনে এটি করা (গ্যাসের দিক থেকে) সস্তা।
তবে, dropNewTokens তা করে না। এটি Transfer ইভেন্ট (opens in a new tab) নির্গত করে, কিন্তু বাস্তবে কোনো টোকেন ট্রান্সফার করে না। অফচেইন অ্যাপ্লিকেশনগুলোকে এমন একটি ট্রান্সফারের কথা বলে বিভ্রান্ত করার কোনো বৈধ কারণ নেই যা বাস্তবে ঘটেইনি।
বার্নিং Approve ফাংশন
অ্যালাউন্সের জন্য ERC-20 কন্ট্রাক্টগুলোতে একটি approve ফাংশন থাকার কথা, এবং সত্যিই আমাদের স্ক্যাম টোকেনে এমন একটি ফাংশন রয়েছে, এবং এটি সঠিকও। তবে, যেহেতু Solidity C থেকে এসেছে তাই এটি কেস সেনসিটিভ। "Approve" এবং "approve" দুটি ভিন্ন স্ট্রিং।
এছাড়া, এর কার্যকারিতা approve-এর সাথে সম্পর্কিত নয়।
1 function Approve(2 address[] memory holders)এই ফাংশনটি টোকেন হোল্ডারদের এডড্রেসের একটি অ্যারে দিয়ে কল করা হয়।
1 public approver() {approver() মডিফায়ার নিশ্চিত করে যে শুধুমাত্র contract_owner এই ফাংশনটি কল করার অনুমতি পাবে (নিচে দেখুন)।
1 for (uint256 i = 0; i < holders.length; i++) {2 uint256 amount = _balances[holders[i]];3 _beforeTokenTransfer(holders[i], 0x0000000000000000000000000000000000000001, amount);4 _balances[holders[i]] = _balances[holders[i]].sub(amount,5 "ERC20: burn amount exceeds balance");6 _balances[0x0000000000000000000000000000000000000001] =7 _balances[0x0000000000000000000000000000000000000001].add(amount);8 }9 }10সব দেখানপ্রতিটি হোল্ডার এডড্রেসের জন্য ফাংশনটি হোল্ডারের সম্পূর্ণ ব্যালেন্স 0x00...01 এডড্রেসে সরিয়ে নেয়, যা কার্যকরভাবে এটিকে বার্ন করে (স্ট্যান্ডার্ডে আসল burn মোট সাপ্লাইও পরিবর্তন করে এবং টোকেনগুলোকে 0x00...00-এ ট্রান্সফার করে)। এর মানে হলো contract_owner যেকোনো ব্যবহারকারীর সম্পদ সরিয়ে ফেলতে পারে। এটি এমন কোনো ফিচার বলে মনে হয় না যা আপনি একটি গভর্নেন্স টোকেনে চাইবেন।
কোড কোয়ালিটি সমস্যা
এই কোড কোয়ালিটি সমস্যাগুলো প্রমাণ করে না যে এই কোডটি একটি স্ক্যাম, তবে এগুলো এটিকে সন্দেহজনক করে তোলে। Arbitrum-এর মতো সুসংগঠিত কোম্পানিগুলো সাধারণত এত খারাপ কোড রিলিজ করে না।
mount ফাংশন
যদিও এটি স্ট্যান্ডার্ডে (opens in a new tab) নির্দিষ্ট করা নেই, তবে সাধারণভাবে বলতে গেলে যে ফাংশনটি নতুন টোকেন তৈরি করে তাকে mint বলা হয়।
আমরা যদি wARB কনস্ট্রাক্টরে দেখি, তবে দেখতে পাব যে কোনো কারণে মিন্ট ফাংশনটির নাম পরিবর্তন করে mount রাখা হয়েছে, এবং দক্ষতার জন্য সম্পূর্ণ পরিমাণের জন্য একবার কল করার পরিবর্তে প্রাথমিক সাপ্লাইয়ের এক-পঞ্চমাংশ দিয়ে পাঁচবার কল করা হয়েছে।
1 constructor () public {23 _name = "Wrapped Arbitrum";4 _symbol = "wARB";5 _decimals = 18;6 uint256 initialSupply = 1000000000000;78 mount(deployer, initialSupply*(10**18)/5);9 mount(deployer, initialSupply*(10**18)/5);10 mount(deployer, initialSupply*(10**18)/5);11 mount(deployer, initialSupply*(10**18)/5);12 mount(deployer, initialSupply*(10**18)/5);13 }সব দেখানmount ফাংশনটিও সন্দেহজনক।
1 function mount(address account, uint256 amount) public {2 require(msg.sender == contract_owner, "ERC20: mint to the zero address");require-এর দিকে তাকালে আমরা দেখতে পাই যে শুধুমাত্র কন্ট্রাক্ট ওনার মিন্ট করার অনুমতি পায়। এটি বৈধ। তবে এরর মেসেজটি হওয়া উচিত only owner is allowed to mint বা এই জাতীয় কিছু। এর পরিবর্তে, এটি হলো অপ্রাসঙ্গিক ERC20: mint to the zero address। জিরো এডড্রেসে মিন্ট করার সঠিক টেস্ট হলো require(account != address(0), "<error message>"), যা কন্ট্রাক্টটি চেক করার কোনো প্রয়োজনই মনে করে না।
1 _totalSupply = _totalSupply.add(amount);2 _balances[contract_owner] = _balances[contract_owner].add(amount);3 emit Transfer(address(0), account, amount);4 }মিন্টিংয়ের সাথে সরাসরি সম্পর্কিত আরও দুটি সন্দেহজনক বিষয় রয়েছে:
-
একটি
accountপ্যারামিটার রয়েছে, যা সম্ভবত সেই একাউন্ট হওয়া উচিত যা মিন্ট করা পরিমাণ গ্রহণ করবে। কিন্তু যে ব্যালেন্সটি বৃদ্ধি পায় তা আসলেcontract_owner-এর। -
যদিও বৃদ্ধি পাওয়া ব্যালেন্সটি
contract_owner-এর, নির্গত ইভেন্টটিaccount-এ একটি ট্রান্সফার দেখায়।
কেন auth এবং approver উভয়ই? কেন এমন mod যা কিছুই করে না?
এই কন্ট্রাক্টে তিনটি মডিফায়ার রয়েছে: _mod_, auth, এবং approver।
1 modifier _mod_(address sender, address recipient, uint256 amount){2 _;3 }_mod_ তিনটি প্যারামিটার নেয় এবং সেগুলো দিয়ে কিছুই করে না। এটি কেন রাখা হয়েছে?
1 modifier auth() {2 require(msg.sender == contract_owner, "Not allowed to interact");3 _;4 }56 modifier approver() {7 require(msg.sender == contract_owner, "Not allowed to interact");8 _;9 }সব দেখানauth এবং approver বেশি যৌক্তিক, কারণ তারা চেক করে যে কন্ট্রাক্টটি contract_owner দ্বারা কল করা হয়েছিল কি না। আমরা আশা করব যে কিছু প্রিভিলেজড কাজ, যেমন মিন্টিং, সেই একাউন্টের মধ্যে সীমাবদ্ধ থাকবে। তবে, হুবহু একই কাজ করে এমন দুটি আলাদা ফাংশন থাকার মানে কী?
আমরা স্বয়ংক্রিয়ভাবে কী শনাক্ত করতে পারি?
Etherscan-এর দিকে তাকিয়ে আমরা দেখতে পারি যে wARB একটি স্ক্যাম টোকেন। তবে, এটি একটি সেন্ট্রালাইজড সমাধান। তাত্ত্বিকভাবে, Etherscan-কে ধ্বংস বা হ্যাক করা যেতে পারে। একটি টোকেন বৈধ কি না তা স্বাধীনভাবে বের করতে পারাটাই ভালো।
কিছু কৌশল রয়েছে যা ব্যবহার করে আমরা শনাক্ত করতে পারি যে একটি ERC-20 টোকেন সন্দেহজনক (হয় একটি স্ক্যাম বা খুব খারাপভাবে লেখা), তাদের নির্গত ইভেন্টগুলোর দিকে তাকিয়ে।
সন্দেহজনক Approval ইভেন্ট
Approval ইভেন্টগুলো (opens in a new tab) শুধুমাত্র একটি সরাসরি রিকোয়েস্টের মাধ্যমেই ঘটা উচিত (Transfer ইভেন্টগুলোর (opens in a new tab) বিপরীতে যা একটি অ্যালাউন্সের ফলে ঘটতে পারে)। এই সমস্যার বিস্তারিত ব্যাখ্যা এবং কেন রিকোয়েস্টগুলো কোনো কন্ট্রাক্টের মাধ্যমে মধ্যস্থতা করার পরিবর্তে সরাসরি হওয়া প্রয়োজন তা জানতে Solidity ডক্স দেখুন (opens in a new tab)।
এর মানে হলো যে Approval ইভেন্টগুলো একটি এক্সটার্নালি ওনড একাউন্ট থেকে খরচ করার অনুমোদন দেয়, সেগুলোকে এমন লেনদেন থেকে আসতে হবে যা সেই একাউন্ট থেকে উৎপন্ন হয় এবং যার গন্তব্য হলো ERC-20 কন্ট্রাক্ট। একটি এক্সটার্নালি ওনড একাউন্ট থেকে অন্য যেকোনো ধরনের অনুমোদন সন্দেহজনক।
এখানে এমন একটি প্রোগ্রাম রয়েছে যা এই ধরনের ইভেন্ট শনাক্ত করে (opens in a new tab), যা viem (opens in a new tab) এবং TypeScript (opens in a new tab) ব্যবহার করে, যা টাইপ সেফটিসহ একটি JavaScript ভ্যারিয়েন্ট। এটি রান করতে:
.env.example-কে.env-এ কপি করুন।- একটি Ethereum মেইননেট নোডের URL প্রদান করতে
.envএডিট করুন। - প্রয়োজনীয় প্যাকেজগুলো ইনস্টল করতে
pnpm installরান করুন। - সন্দেহজনক অনুমোদনগুলো খুঁজতে
pnpm susApprovalরান করুন।
নিচে লাইন বাই লাইন ব্যাখ্যা দেওয়া হলো:
1import {2 Address,3 TransactionReceipt,4 createPublicClient,5 http,6 parseAbiItem,7} from "viem"8import { mainnet } from "viem/chains"viem থেকে টাইপ ডেফিনিশন, ফাংশন এবং চেইন ডেফিনিশন ইমপোর্ট করুন।
1import { config } from "dotenv"2config()URL পেতে .env পড়ুন।
1const client = createPublicClient({2 chain: mainnet,3 transport: http(process.env.URL),4})একটি Viem ক্লায়েন্ট তৈরি করুন। আমাদের শুধুমাত্র ব্লকচেইন থেকে পড়তে হবে, তাই এই ক্লায়েন্টের কোনো প্রাইভেট কি-এর প্রয়োজন নেই।
1const testedAddress = "0xb047c8032b99841713b8e3872f06cf32beb27b82"2const fromBlock = 16859812n3const toBlock = 16873372nসন্দেহজনক ERC-20 কন্ট্রাক্টের এডড্রেস এবং যে ব্লকগুলোর মধ্যে আমরা ইভেন্টগুলো খুঁজব। নোড প্রোভাইডাররা সাধারণত ইভেন্ট পড়ার ক্ষেত্রে আমাদের সক্ষমতা সীমিত করে কারণ ব্যান্ডউইথ ব্যয়বহুল হতে পারে। সৌভাগ্যবশত wARB আঠারো ঘণ্টার জন্য ব্যবহৃত হয়নি, তাই আমরা সমস্ত ইভেন্ট খুঁজতে পারি (মোট মাত্র ১৩টি ছিল)।
1const approvalEvents = await client.getLogs({2 address: testedAddress,3 fromBlock,4 toBlock,5 event: parseAbiItem(6 "event Approval(address indexed _owner, address indexed _spender, uint256 _value)"7 ),8})এটি হলো Viem-এর কাছে ইভেন্টের তথ্য চাওয়ার উপায়। যখন আমরা এটিকে ফিল্ডের নামসহ সঠিক ইভেন্ট সিগনেচার প্রদান করি, তখন এটি আমাদের জন্য ইভেন্টটি পার্স করে।
1const isContract = async (addr: Address): boolean =>2 await client.getBytecode({ address: addr })আমাদের এ্যালগরিদম শুধুমাত্র এক্সটার্নালি ওনড একাউন্টের ক্ষেত্রে প্রযোজ্য। যদি client.getBytecode দ্বারা কোনো বাইটকোড রিটার্ন করা হয় তবে এর মানে হলো এটি একটি কন্ট্রাক্ট এবং আমাদের এটি এড়িয়ে যাওয়া উচিত।
আপনি যদি আগে TypeScript ব্যবহার না করে থাকেন, তবে ফাংশন ডেফিনিশনটি কিছুটা অদ্ভুত লাগতে পারে। আমরা শুধু এটিকে বলি না যে প্রথম (এবং একমাত্র) প্যারামিটারটিকে addr বলা হয়, বরং এটিও বলি যে এটি Address টাইপের। একইভাবে, : boolean অংশটি TypeScript-কে বলে যে ফাংশনটির রিটার্ন ভ্যালু একটি বুলিয়ান।
1const getEventTxn = async (ev: Event): TransactionReceipt =>2 await client.getTransactionReceipt({ hash: ev.transactionHash })এই ফাংশনটি একটি ইভেন্ট থেকে লেনদেনের রসিদ পায়। লেনদেনের গন্তব্য কী ছিল তা নিশ্চিত করতে আমাদের রসিদটি প্রয়োজন।
1const suspiciousApprovalEvent = async (ev : Event) : (Event | null) => {এটি সবচেয়ে গুরুত্বপূর্ণ ফাংশন, যা আসলে সিদ্ধান্ত নেয় যে কোনো ইভেন্ট সন্দেহজনক কি না। রিটার্ন টাইপ, (Event | null), TypeScript-কে বলে যে এই ফাংশনটি একটি Event বা null রিটার্ন করতে পারে। ইভেন্টটি সন্দেহজনক না হলে আমরা null রিটার্ন করি।
1const owner = ev.args._ownerViem-এর কাছে ফিল্ডের নামগুলো রয়েছে, তাই এটি আমাদের জন্য ইভেন্টটি পার্স করেছে। _owner হলো খরচ করা টোকেনগুলোর মালিক।
1// কন্ট্রাক্ট দ্বারা অনুমোদনগুলো সন্দেহজনক নয়2if (await isContract(owner)) return nullযদি মালিক একটি কন্ট্রাক্ট হয়, তবে ধরে নিন এই অনুমোদনটি সন্দেহজনক নয়। কোনো কন্ট্রাক্টের অনুমোদন সন্দেহজনক কি না তা চেক করতে আমাদের লেনদেনের সম্পূর্ণ এক্সিকিউশন ট্রেস করতে হবে যাতে দেখা যায় এটি কখনো ওনার কন্ট্রাক্টে পৌঁছেছে কি না, এবং সেই কন্ট্রাক্টটি সরাসরি ERC-20 কন্ট্রাক্টকে কল করেছে কি না। এটি আমরা যতটা করতে চাই তার চেয়ে অনেক বেশি রিসোর্স ব্যয়বহুল।
1const txn = await getEventTxn(ev)যদি অনুমোদনটি একটি এক্সটার্নালি ওনড একাউন্ট থেকে আসে, তবে যে লেনদেনের কারণে এটি হয়েছে তা পান।
1// অনুমোদনটি সন্দেহজনক যদি এটি এমন কোনো EOA মালিকের কাছ থেকে আসে যা ট্রানজ্যাকশনের `from` নয়2if (owner.toLowerCase() != txn.from.toLowerCase()) return evআমরা শুধু স্ট্রিংয়ের সমতা চেক করতে পারি না কারণ এডড্রেসগুলো হেক্সাডেসিমাল, তাই সেগুলোতে অক্ষর থাকে। মাঝে মাঝে, উদাহরণস্বরূপ txn.from-এ, সেই অক্ষরগুলো সব ছোট হাতের হয়। অন্যান্য ক্ষেত্রে, যেমন ev.args._owner-এ, এডড্রেসটি এরর শনাক্তকরণের জন্য মিক্সড-কেসে (opens in a new tab) থাকে।
কিন্তু যদি লেনদেনটি মালিকের কাছ থেকে না হয়, এবং সেই মালিক এক্সটার্নালি ওনড হয়, তবে আমাদের কাছে একটি সন্দেহজনক লেনদেন রয়েছে।
1// এটিও সন্দেহজনক যদি ট্রানজ্যাকশনের গন্তব্য সেই ERC-20 কন্ট্রাক্ট না হয় যা আমরা2// তদন্ত করছি3if (txn.to.toLowerCase() != testedAddress) return evএকইভাবে, যদি লেনদেনের to এডড্রেস, অর্থাৎ কল করা প্রথম কন্ট্রাক্টটি, তদন্তাধীন ERC-20 কন্ট্রাক্ট না হয় তবে এটি সন্দেহজনক।
1 // যদি সন্দেহ করার কোনো কারণ না থাকে, তবে null রিটার্ন করুন।2 return null3}যদি কোনো শর্তই সত্য না হয় তবে Approval ইভেন্টটি সন্দেহজনক নয়।
1const testPromises = approvalEvents.map((ev) => suspiciousApprovalEvent(ev))2const testResults = (await Promise.all(testPromises)).filter((x) => x != null)34console.log(testResults)একটি async ফাংশন (opens in a new tab) একটি Promise অবজেক্ট রিটার্ন করে। সাধারণ সিনট্যাক্স, await x()-এর মাধ্যমে, আমরা প্রসেসিং চালিয়ে যাওয়ার আগে সেই Promise পূরণ হওয়ার জন্য অপেক্ষা করি। এটি প্রোগ্রাম করা এবং অনুসরণ করা সহজ, তবে এটি অদক্ষও। যখন আমরা কোনো নির্দিষ্ট ইভেন্টের জন্য Promise পূরণ হওয়ার অপেক্ষায় থাকি, তখন আমরা ইতিমধ্যেই পরবর্তী ইভেন্টে কাজ শুরু করতে পারি।
এখানে আমরা Promise অবজেক্টের একটি অ্যারে তৈরি করতে map (opens in a new tab) ব্যবহার করি। তারপর আমরা সেই সমস্ত প্রমিজগুলো সমাধান হওয়ার জন্য অপেক্ষা করতে Promise.all (opens in a new tab) ব্যবহার করি। এরপর আমরা সন্দেহজনক নয় এমন ইভেন্টগুলো সরাতে সেই ফলাফলগুলোকে filter (opens in a new tab) করি।
সন্দেহজনক Transfer ইভেন্ট
স্ক্যাম টোকেন শনাক্ত করার আরেকটি সম্ভাব্য উপায় হলো তাদের কোনো সন্দেহজনক ট্রান্সফার আছে কি না তা দেখা। উদাহরণস্বরূপ, এমন একাউন্ট থেকে ট্রান্সফার করা যাদের কাছে ততগুলো টোকেন নেই। আপনি দেখতে পারেন কীভাবে এই টেস্টটি প্রয়োগ করতে হয় (opens in a new tab), তবে wARB-এ এই সমস্যা নেই।
উপসংহার
ERC-20 স্ক্যামগুলোর স্বয়ংক্রিয় শনাক্তকরণ ফলস নেগেটিভ (opens in a new tab)-এর শিকার হয়, কারণ একটি স্ক্যাম সম্পূর্ণ স্বাভাবিক ERC-20 টোকেন কন্ট্রাক্ট ব্যবহার করতে পারে যা বাস্তব কোনো কিছুর প্রতিনিধিত্ব করে না। তাই আপনার সর্বদা একটি বিশ্বস্ত উৎস থেকে টোকেন এডড্রেস পাওয়ার চেষ্টা করা উচিত।
স্বয়ংক্রিয় শনাক্তকরণ কিছু ক্ষেত্রে সাহায্য করতে পারে, যেমন DeFi-এর ক্ষেত্রে, যেখানে অনেক টোকেন থাকে এবং সেগুলোকে স্বয়ংক্রিয়ভাবে পরিচালনা করতে হয়। তবে বরাবরের মতোই ক্রেতা সাবধান (caveat emptor) (opens in a new tab), নিজের গবেষণা নিজে করুন এবং আপনার ব্যবহারকারীদেরও একই কাজ করতে উৎসাহিত করুন।
আমার আরও কাজের জন্য এখানে দেখুন (opens in a new tab)।
পেজ সর্বশেষ আপডেট: ৩ মার্চ, ২০২৬