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

স্ক্যাম টোকেনগুলোর ব্যবহৃত কিছু কৌশল এবং কীভাবে সেগুলো শনাক্ত করা যায়

স্ক্যাম
Solidity
erc-20
JavaScript
TypeScript
মধ্যবর্তী
ওরি পোমেরান্টজ
15 সেপ্টেম্বর, 2023
14 মিনিট পড়ার সময়

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

স্ক্যাম টোকেন - এগুলো কী, মানুষ কেন এগুলো তৈরি করে এবং কীভাবে এগুলো এড়ানো যায়

ইথেরিয়াম-এর অন্যতম সাধারণ ব্যবহার হলো কোনো গোষ্ঠীর জন্য একটি ট্রেডযোগ্য টোকেন তৈরি করা, যা এক অর্থে তাদের নিজস্ব মুদ্রা। তবে, যেখানেই বৈধ ব্যবহারের মাধ্যমে মূল্য তৈরি হয়, সেখানেই এমন অপরাধীরা থাকে যারা সেই মূল্য নিজেদের জন্য চুরি করার চেষ্টা করে।

ব্যবহারকারীর দৃষ্টিকোণ থেকে আপনি এই বিষয়ে আরও পড়তে পারেন ethereum.org-এর অন্য কোথাও। এই টিউটোরিয়ালটি মূলত একটি স্ক্যাম টোকেন বিশ্লেষণ করে এটি কীভাবে কাজ করে এবং কীভাবে এটি শনাক্ত করা যায় তার ওপর ফোকাস করে।

আমি কীভাবে বুঝব যে wARB একটি স্ক্যাম?

আমরা যে টোকেনটি বিশ্লেষণ করছি তা হলো wARB (নতুন ট্যাবে খোলে), যা বৈধ ARB টোকেন (নতুন ট্যাবে খোলে)-এর সমতুল্য হওয়ার ভান করে।

কোনটি বৈধ টোকেন তা জানার সবচেয়ে সহজ উপায় হলো এর মূল সংস্থা, আরবিট্রাম (নতুন ট্যাবে খোলে)-এর দিকে নজর দেওয়া। বৈধ ঠিকানাগুলো তাদের ডকুমেন্টেশনে (নতুন ট্যাবে খোলে) উল্লেখ করা আছে।

সোর্স কোড কেন উন্মুক্ত থাকে?

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

তবে, বৈধ টোকেনগুলো সাধারণত তাদের সোর্স কোড প্রকাশ করে, তাই বৈধ দেখানোর জন্য স্ক্যাম টোকেনের নির্মাতারাও মাঝে মাঝে একই কাজ করে। wARB (নতুন ট্যাবে খোলে) হলো এমন একটি টোকেন যার সোর্স কোড উন্মুক্ত, যা এটিকে বোঝা সহজ করে তোলে।

যদিও কন্ট্রাক্ট ডিপ্লয়াররা সোর্স কোড প্রকাশ করবেন কি না তা বেছে নিতে পারেন, তবে তারা ভুল সোর্স কোড প্রকাশ করতে পারেন না। ব্লক এক্সপ্লোরার স্বাধীনভাবে প্রদত্ত সোর্স কোড কম্পাইল করে, এবং যদি এটি হুবহু একই বাইটকোড না পায়, তবে এটি সেই সোর্স কোড প্রত্যাখ্যান করে। আপনি Etherscan সাইটে এই সম্পর্কে আরও পড়তে পারেন (নতুন ট্যাবে খোলে)।

বৈধ ERC-20 টোকেনগুলোর সাথে তুলনা

আমরা এই টোকেনটিকে বৈধ ERC-20 টোকেনগুলোর সাথে তুলনা করতে যাচ্ছি। বৈধ ERC-20 টোকেনগুলো সাধারণত কীভাবে লেখা হয় সে সম্পর্কে আপনার ধারণা না থাকলে, এই টিউটোরিয়ালটি দেখুন।

বিশেষ সুবিধাপ্রাপ্ত ঠিকানাগুলোর জন্য ধ্রুবক (Constants)

কন্ট্রাক্টগুলোর মাঝে মাঝে বিশেষ সুবিধাপ্রাপ্ত ঠিকানা প্রয়োজন হয়। দীর্ঘমেয়াদী ব্যবহারের জন্য ডিজাইন করা কন্ট্রাক্টগুলো কিছু বিশেষ সুবিধাপ্রাপ্ত ঠিকানাকে সেই ঠিকানাগুলো পরিবর্তন করার অনুমতি দেয়, উদাহরণস্বরূপ একটি নতুন মাল্টিসিগ কন্ট্রাক্ট ব্যবহার সক্ষম করতে। এটি করার বেশ কয়েকটি উপায় রয়েছে।

HOP টোকেন কন্ট্রাক্ট (নতুন ট্যাবে খোলে) Ownable (নতুন ট্যাবে খোলে) প্যাটার্ন ব্যবহার করে। বিশেষ সুবিধাপ্রাপ্ত ঠিকানাটি স্টোরেজে _owner নামক একটি ফিল্ডে রাখা হয় (তৃতীয় ফাইলটি দেখুন, Ownable.sol)।

abstract contract Ownable is Context {
    address private _owner;
    .
    .
    .
}

ARB টোকেন কন্ট্রাক্ট (নতুন ট্যাবে খোলে)-এ সরাসরি কোনো বিশেষ সুবিধাপ্রাপ্ত ঠিকানা নেই। তবে, এর কোনো প্রয়োজনও নেই। এটি ঠিকানা 0xb50721bcf8d664c30412cfbc6cf7a15145234ad1 (নতুন ট্যাবে খোলে)-এ একটি proxy (নতুন ট্যাবে খোলে)-এর পেছনে থাকে। সেই কন্ট্রাক্টটিতে একটি বিশেষ সুবিধাপ্রাপ্ত ঠিকানা রয়েছে (চতুর্থ ফাইলটি দেখুন, ERC1967Upgrade.sol) যা আপগ্রেডের জন্য ব্যবহার করা যেতে পারে।

    /**
     * @dev EIP1967 অ্যাডমিন স্লটে একটি নতুন ঠিকানা সংরক্ষণ করে।
     */
    function _setAdmin(address newAdmin) private {
        require(newAdmin != address(0), "ERC1967: new admin is the zero address");
        StorageSlot.getAddressSlot(_ADMIN_SLOT).value = newAdmin;
    }

এর বিপরীতে, wARB কন্ট্রাক্টে একটি হার্ড কোডেড contract_owner রয়েছে।

এই কন্ট্রাক্ট মালিক (নতুন ট্যাবে খোলে) এমন কোনো কন্ট্রাক্ট নয় যা বিভিন্ন সময়ে বিভিন্ন অ্যাকাউন্ট দ্বারা নিয়ন্ত্রিত হতে পারে, বরং এটি একটি এক্সটার্নালি ওনড অ্যাকাউন্ট। এর মানে হলো এটি সম্ভবত কোনো ব্যক্তির স্বল্পমেয়াদী ব্যবহারের জন্য ডিজাইন করা হয়েছে, এমন কোনো দীর্ঘমেয়াদী সমাধান হিসেবে নয় যা একটি মূল্যবান ERC-20 নিয়ন্ত্রণ করবে।

এবং সত্যিই, আমরা যদি Etherscan-এ দেখি তবে দেখতে পাব যে স্ক্যামার এই কন্ট্রাক্টটি 19 মে, 2023 তারিখে মাত্র 12 ঘণ্টার জন্য ব্যবহার করেছিল (প্রথম ট্রানজ্যাকশন (নতুন ট্যাবে খোলে) থেকে শেষ ট্রানজ্যাকশন (নতুন ট্যাবে খোলে))।

ভুয়া _transfer ফাংশন

প্রকৃত হস্তান্তরগুলো একটি অভ্যন্তরীণ _transfer ফাংশন ব্যবহার করে সম্পন্ন হওয়াটাই স্ট্যান্ডার্ড।

wARB-এ এই ফাংশনটি প্রায় বৈধ বলে মনে হয়:

সন্দেহজনক অংশটি হলো:

        if (sender == contract_owner){
            sender = deployer;
        }
        emit Transfer(sender, recipient, amount);

যদি কন্ট্রাক্ট মালিক টোকেন পাঠায়, তবে Transfer ইভেন্ট কেন দেখায় যে সেগুলো deployer থেকে এসেছে?

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

যখন আমরা টোকেন হস্তান্তর করার জন্য কল করা ফাংশনগুলো, transfer এবং transferFrom দেখি, তখন আমরা দেখতে পাই যে তারা সম্পূর্ণ ভিন্ন একটি ফাংশন, _f_-কে কল করে।

আসল _f_ ফাংশন

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

  • ফাংশন মডিফায়ার (নতুন ট্যাবে খোলে) _mod_-এর ব্যবহার। তবে, যখন আমরা সোর্স কোডটি দেখি তখন দেখতে পাই যে _mod_ আসলে ক্ষতিকারক নয়।

    modifier _mod_(address sender, address recipient, uint256 amount){
      _;
    }
    
  • একই সমস্যা যা আমরা _transfer-এ দেখেছি, তা হলো যখন contract_owner টোকেন পাঠায় তখন মনে হয় সেগুলো deployer থেকে এসেছে।

ভুয়া ইভেন্ট ফাংশন dropNewTokens

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

function dropNewTokens(address uPool,
                       address[] memory eReceiver,
                       uint256[] memory eAmounts) public auth()

এই ফাংশনে auth() মডিফায়ার রয়েছে, যার মানে এটি শুধুমাত্র কন্ট্রাক্ট মালিক দ্বারা কল করা যেতে পারে।

modifier auth() {
    require(msg.sender == contract_owner, "Not allowed to interact");
    _;
}

এই বিধিনিষেধটি সম্পূর্ণ যৌক্তিক, কারণ আমরা চাইব না যে কোনো র্যান্ডম অ্যাকাউন্ট টোকেন বিতরণ করুক। তবে, ফাংশনের বাকি অংশটি সন্দেহজনক।

{
    for (uint256 i = 0; i < eReceiver.length; i++) {
        emit Transfer(uPool, eReceiver[i], eAmounts[i]);
    }
}

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

তবে, dropNewTokens তা করে না। এটি Transfer ইভেন্ট (নতুন ট্যাবে খোলে) নির্গত করে, কিন্তু আসলে কোনো টোকেন হস্তান্তর করে না। অফচেইন অ্যাপ্লিকেশনগুলোকে এমন একটি হস্তান্তরের কথা বলে বিভ্রান্ত করার কোনো বৈধ কারণ নেই যা বাস্তবে ঘটেনি।

পোড়ানোর (burning) Approve ফাংশন

ERC-20 কন্ট্রাক্টগুলোতে অ্যালাউন্স-এর জন্য একটি approve ফাংশন থাকার কথা, এবং সত্যিই আমাদের স্ক্যাম টোকেনে এমন একটি ফাংশন রয়েছে, এবং এটি সঠিকও। তবে, যেহেতু Solidity C থেকে এসেছে তাই এটি কেস সেনসিটিভ। "Approve" এবং "approve" হলো ভিন্ন স্ট্রিং।

এছাড়াও, এর কার্যকারিতা approve-এর সাথে সম্পর্কিত নয়।

    function Approve(
        address[] memory holders)

এই ফাংশনটি টোকেন হোল্ডারদের ঠিকানাগুলোর একটি অ্যারে দিয়ে কল করা হয়।

    public approver() {

approver() মডিফায়ার নিশ্চিত করে যে শুধুমাত্র contract_owner এই ফাংশনটি কল করার অনুমতিপ্রাপ্ত (নিচে দেখুন)।

প্রতিটি হোল্ডার ঠিকানার জন্য ফাংশনটি হোল্ডারের সম্পূর্ণ ব্যালেন্স 0x00...01 ঠিকানায় সরিয়ে নেয়, যা কার্যকরভাবে এটিকে পুড়িয়ে ফেলে (স্ট্যান্ডার্ডের আসল burn মোট সরবরাহও পরিবর্তন করে এবং টোকেনগুলোকে 0x00...00-এ হস্তান্তর করে)। এর মানে হলো contract_owner যেকোনো ব্যবহারকারীর সম্পদ সরিয়ে ফেলতে পারে। এটি এমন কোনো বৈশিষ্ট্য বলে মনে হয় না যা আপনি একটি গভর্নেন্স টোকেনে চাইবেন।

কোডের মানের সমস্যা

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

mount ফাংশন

যদিও এটি স্ট্যান্ডার্ডে (নতুন ট্যাবে খোলে) নির্দিষ্ট করা নেই, তবে সাধারণভাবে বলতে গেলে যে ফাংশনটি নতুন টোকেন তৈরি করে তাকে mint বলা হয়।

আমরা যদি wARB কনস্ট্রাক্টর-এ দেখি, তবে দেখতে পাব যে কোনো কারণে মিন্ট ফাংশনটির নাম পরিবর্তন করে mount রাখা হয়েছে, এবং দক্ষতার জন্য সম্পূর্ণ পরিমাণের জন্য একবার কল করার পরিবর্তে প্রাথমিক সরবরাহের এক-পঞ্চমাংশ দিয়ে পাঁচবার কল করা হয়েছে।

mount ফাংশনটিও সন্দেহজনক।

    function mount(address account, uint256 amount) public {
        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>"), যা কন্ট্রাক্টটি কখনোই চেক করার প্রয়োজন মনে করে না।

        _totalSupply = _totalSupply.add(amount);
        _balances[contract_owner] = _balances[contract_owner].add(amount);
        emit Transfer(address(0), account, amount);
    }

মিন্টিং-এর সাথে সরাসরি সম্পর্কিত আরও দুটি সন্দেহজনক বিষয় রয়েছে:

  • একটি account প্যারামিটার রয়েছে, যা সম্ভবত সেই অ্যাকাউন্ট হওয়া উচিত যা মিন্ট করা পরিমাণ গ্রহণ করবে। কিন্তু যে ব্যালেন্সটি বৃদ্ধি পায় তা আসলে contract_owner-এর।

  • যদিও বৃদ্ধিপ্রাপ্ত ব্যালেন্সটি contract_owner-এর, নির্গত ইভেন্টটি account-এ একটি হস্তান্তর দেখায়।

কেন auth এবং approver উভয়ই? কেন mod যা কিছুই করে না?

এই কন্ট্রাক্টে তিনটি মডিফায়ার রয়েছে: _mod_, auth, এবং approver।

    modifier _mod_(address sender, address recipient, uint256 amount){
        _;
    }

_mod_ তিনটি প্যারামিটার নেয় এবং সেগুলো দিয়ে কিছুই করে না। এটি কেন রাখা হয়েছে?

auth এবং approver বেশি যৌক্তিক, কারণ তারা চেক করে যে কন্ট্রাক্টটি contract_owner দ্বারা কল করা হয়েছিল কি না। আমরা আশা করব যে কিছু বিশেষ সুবিধাপ্রাপ্ত কাজ, যেমন মিন্টিং, সেই অ্যাকাউন্টের মধ্যে সীমাবদ্ধ থাকবে। তবে, হুবহু একই কাজ করে এমন দুটি আলাদা ফাংশন থাকার মানে কী?

আমরা স্বয়ংক্রিয়ভাবে কী শনাক্ত করতে পারি?

আমরা Etherscan-এর দিকে তাকিয়ে দেখতে পারি যে wARB একটি স্ক্যাম টোকেন। তবে, এটি একটি কেন্দ্রীভূত সমাধান। তাত্ত্বিকভাবে, Etherscan-কে ধ্বংস বা হ্যাক করা যেতে পারে। একটি টোকেন বৈধ কি না তা স্বাধীনভাবে বের করতে পারাটাই ভালো।

কিছু কৌশল রয়েছে যা ব্যবহার করে আমরা শনাক্ত করতে পারি যে একটি ERC-20 টোকেন সন্দেহজনক (হয় একটি স্ক্যাম বা খুব খারাপভাবে লেখা), তাদের নির্গত ইভেন্টগুলোর দিকে তাকিয়ে।

সন্দেহজনক Approval ইভেন্ট

Approval ইভেন্টগুলো (নতুন ট্যাবে খোলে) শুধুমাত্র একটি সরাসরি অনুরোধের মাধ্যমে ঘটা উচিত (এর বিপরীতে Transfer ইভেন্টগুলো (নতুন ট্যাবে খোলে) একটি অ্যালাউন্স-এর ফলস্বরূপ ঘটতে পারে)। এই সমস্যাটির বিস্তারিত ব্যাখ্যা এবং কেন অনুরোধগুলো কোনো কন্ট্রাক্ট দ্বারা মধ্যস্থতা করার পরিবর্তে সরাসরি হওয়া প্রয়োজন তা জানতে Solidity ডক্স দেখুন (নতুন ট্যাবে খোলে)।

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

এখানে এমন একটি প্রোগ্রাম রয়েছে যা এই ধরনের ইভেন্ট শনাক্ত করে (নতুন ট্যাবে খোলে), যা Viem (নতুন ট্যাবে খোলে) এবং TypeScript (নতুন ট্যাবে খোলে) (টাইপ সেফটি সহ একটি JavaScript ভ্যারিয়েন্ট) ব্যবহার করে। এটি রান করতে:

  1. .env.example-কে .env-এ কপি করুন।
  2. একটি ইথেরিয়াম মেইননেট নোড-এর URL প্রদান করতে .env সম্পাদনা করুন।
  3. প্রয়োজনীয় প্যাকেজগুলো ইনস্টল করতে pnpm install রান করুন।
  4. সন্দেহজনক অনুমোদনগুলো খুঁজতে pnpm susApproval রান করুন।

নিচে লাইন বাই লাইন ব্যাখ্যা দেওয়া হলো:

import {
  Address,
  TransactionReceipt,
  createPublicClient,
  http,
  parseAbiItem,
} from "viem"
import { mainnet } from "viem/chains"

viem থেকে টাইপ ডেফিনিশন, ফাংশন এবং চেইন ডেফিনিশন ইমপোর্ট করুন।

import { config } from "dotenv"
config()

URL পেতে .env পড়ুন।

const client = createPublicClient({
  chain: mainnet,
  transport: http(process.env.URL),
})

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

const testedAddress = "0xb047c8032b99841713b8e3872f06cf32beb27b82"
const fromBlock = 16859812n
const toBlock = 16873372n

সন্দেহজনক ERC-20 কন্ট্রাক্টের ঠিকানা, এবং যে ব্লকগুলোর মধ্যে আমরা ইভেন্টগুলো খুঁজব। নোড প্রোভাইডাররা সাধারণত ইভেন্ট পড়ার ক্ষেত্রে আমাদের সক্ষমতা সীমিত করে কারণ ব্যান্ডউইথ ব্যয়বহুল হতে পারে। সৌভাগ্যবশত wARB আঠারো ঘণ্টার জন্য ব্যবহৃত হয়নি, তাই আমরা সমস্ত ইভেন্ট খুঁজতে পারি (মোট মাত্র 13টি ছিল)।

const approvalEvents = await client.getLogs({
  address: testedAddress,
  fromBlock,
  toBlock,
  event: parseAbiItem(
    "event Approval(address indexed _owner, address indexed _spender, uint256 _value)"
  ),
})

এটি হলো Viem-এর কাছে ইভেন্টের তথ্য চাওয়ার উপায়। যখন আমরা এটিকে ফিল্ডের নামসহ সঠিক ইভেন্ট স্বাক্ষর প্রদান করি, তখন এটি আমাদের জন্য ইভেন্টটি পার্স করে।

const isContract = async (addr: Address): boolean =>
  await client.getBytecode({ address: addr })

আমাদের অ্যালগরিদম শুধুমাত্র এক্সটার্নালি ওনড অ্যাকাউন্টগুলোর জন্য প্রযোজ্য। যদি client.getBytecode দ্বারা কোনো বাইটকোড রিটার্ন করা হয় তবে এর মানে হলো এটি একটি কন্ট্রাক্ট এবং আমাদের এটি এড়িয়ে যাওয়া উচিত।

আপনি যদি আগে TypeScript ব্যবহার না করে থাকেন, তবে ফাংশন ডেফিনিশনটি কিছুটা অদ্ভুত লাগতে পারে। আমরা শুধু এটিকে বলি না যে প্রথম (এবং একমাত্র) প্যারামিটারটিকে addr বলা হয়, বরং এটিও বলি যে এটি Address টাইপের। একইভাবে, : boolean অংশটি TypeScript-কে বলে যে ফাংশনটির রিটার্ন ভ্যালু হলো একটি বুলিয়ান।

const getEventTxn = async (ev: Event): TransactionReceipt =>
  await client.getTransactionReceipt({ hash: ev.transactionHash })

এই ফাংশনটি একটি ইভেন্ট থেকে ট্রানজ্যাকশন রসিদ পায়। ট্রানজ্যাকশনের গন্তব্য কী ছিল তা নিশ্চিতভাবে জানতে আমাদের রসিদটি প্রয়োজন।

const suspiciousApprovalEvent = async (ev : Event) : (Event | null) => {

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

const owner = ev.args._owner

Viem-এর কাছে ফিল্ডের নামগুলো রয়েছে, তাই এটি আমাদের জন্য ইভেন্টটি পার্স করেছে। _owner হলো খরচ করা টোকেনগুলোর মালিক।

// কন্ট্রাক্ট দ্বারা অনুমোদন সন্দেহজনক নয়
if (await isContract(owner)) return null

যদি মালিক একটি কন্ট্রাক্ট হয়, তবে ধরে নিন এই অনুমোদনটি সন্দেহজনক নয়। কোনো কন্ট্রাক্টের অনুমোদন সন্দেহজনক কি না তা পরীক্ষা করার জন্য আমাদের ট্রানজ্যাকশনের সম্পূর্ণ এক্সিকিউশন ট্রেস করতে হবে যাতে দেখা যায় এটি কখনো মালিক কন্ট্রাক্টে পৌঁছেছিল কি না, এবং সেই কন্ট্রাক্টটি সরাসরি ERC-20 কন্ট্রাক্টকে কল করেছিল কি না। এটি আমরা যতটা করতে চাই তার চেয়ে অনেক বেশি রিসোর্স ব্যয়বহুল।

const txn = await getEventTxn(ev)

যদি অনুমোদনটি একটি এক্সটার্নালি ওনড অ্যাকাউন্ট থেকে আসে, তবে যে ট্রানজ্যাকশনের কারণে এটি ঘটেছে তা পান।

// অনুমোদনটি সন্দেহজনক যদি এটি এমন একজন EOA মালিকের কাছ থেকে আসে যে ট্রানজ্যাকশনের `from` নয়
if (owner.toLowerCase() != txn.from.toLowerCase()) return ev

আমরা শুধু স্ট্রিংয়ের সমতা পরীক্ষা করতে পারি না কারণ ঠিকানাগুলো হেক্সাডেসিমাল, তাই সেগুলোতে অক্ষর থাকে। কখনো কখনো, উদাহরণস্বরূপ txn.from-এ, সেই অক্ষরগুলো সবই ছোট হাতের হয়। অন্যান্য ক্ষেত্রে, যেমন ev.args._owner-এ, ঠিকানাটি ত্রুটি শনাক্তকরণের জন্য মিক্সড-কেস (নতুন ট্যাবে খোলে)-এ থাকে।

কিন্তু যদি ট্রানজ্যাকশনটি মালিকের কাছ থেকে না হয়, এবং সেই মালিক এক্সটার্নালি ওনড হয়, তবে আমাদের কাছে একটি সন্দেহজনক ট্রানজ্যাকশন রয়েছে।

// এটিও সন্দেহজনক যদি ট্রানজ্যাকশনের গন্তব্য সেই ERC-20 কন্ট্রাক্ট না হয় যা আমরা
// তদন্ত করছি
if (txn.to.toLowerCase() != testedAddress) return ev

একইভাবে, যদি ট্রানজ্যাকশনের to ঠিকানা, অর্থাৎ কল করা প্রথম কন্ট্রাক্টটি, তদন্তাধীন ERC-20 কন্ট্রাক্ট না হয় তবে এটি সন্দেহজনক।

    // যদি সন্দেহ করার কোনো কারণ না থাকে, তবে null রিটার্ন করুন।
    return null
}

যদি কোনো শর্তই সত্য না হয় তবে Approval ইভেন্টটি সন্দেহজনক নয়।

const testPromises = approvalEvents.map((ev) => suspiciousApprovalEvent(ev))
const testResults = (await Promise.all(testPromises)).filter((x) => x != null)

console.log(testResults)

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

এখানে আমরা Promise অবজেক্টের একটি অ্যারে তৈরি করতে map (নতুন ট্যাবে খোলে) ব্যবহার করি। তারপর আমরা সেই সমস্ত প্রমিসগুলো সমাধান হওয়ার জন্য অপেক্ষা করতে Promise.all (নতুন ট্যাবে খোলে) ব্যবহার করি। এরপর আমরা সন্দেহজনক নয় এমন ইভেন্টগুলো সরাতে সেই ফলাফলগুলোকে filter (নতুন ট্যাবে খোলে) করি।

সন্দেহজনক Transfer ইভেন্ট

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

উপসংহার

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

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

আমার আরও কাজের জন্য এখানে দেখুন (নতুন ট্যাবে খোলে)।