লেখক: জিউজিউ
সম্পাদনা: 77
প্রেক্ষাপট
৪ সেপ্টেম্বর, ২০২৬ তারিখে, প্রখ্যাত ডিসেন্ট্রালাইজড লিন্ডিং প্ল্যাটফর্ম Notional Finance-এ আক্রমণ হয়, যার ফলে প্রায় ১.৭৩ মিলিয়ন মার্কিন ডলারের ক্ষতি হয়। নিচে স্লো ফিউজ সিকিউরিটি টিমের এই আক্রমণের বিশদ বিশ্লেষণ দেওয়া হল:
পূর্ব জ্ঞান
Notional Finance V1-এ, fCash-কে পরিপক্কতার তারিখ সহ একটি নগদ ঋণ হিসাবে বুঝা যায়। পরিপক্কতার সময়, নগদ গ্রহীতা পেমেন্ট পায় এবং নগদ প্রদানকারী পেমেন্ট করে। প্রোটোকলটি উভয় পোজিশনকেই পোর্টফোলিওতে রেকর্ড করে এবং একটি অ্যাকাউন্টের পোজিশন খোলার স্বাস্থ্যকরতা নির্ধারণের জন্য ফ্রি কল্যাণ চেকের মাধ্যমে পরীক্ষা করে।
ERC1155Trade-এর safeTransferFrom এখানে সম্পদ হিসাব রাখার ভূমিকা পালন করে। যখন প্রেরিত সম্পদের ধরন cash receiver হয়, তখন চুক্তিটি Portfolios.mintfCashPair কল করে, এবং payer-এর জন্য একটি দায় এবং receiver-এর জন্য সমান পরিমাণের হক রেকর্ড করে। এই কলটি ERC1155 ট্রান্সফারের মতো দেখায়, কিন্তু ফলাফলটি একটি fCash জোড়া মিন্টিং।
পোর্টফোলিওতে নতুন পজিশন লিখিত হওয়ার পর, ফ্রি কলাটারের গণনা এবং পরীক্ষা করা প্রয়োজন। রিস্কফ্রেমওয়ার্ক কন্ট্রাক্টটি পেয়ারের fCash দায়কে সাইনযুক্ত int256-এ সংকলন করবে, তারপর প্রতিটি ক্রিপ্টোকারেন্সির ব্যালেন্সকে ETH-এ রূপান্তর করে সংকলন করবে। ঋণাত্মক সংখ্যা প্রদানের জন্য প্রয়োজনীয় নগদ টাকা নির্দেশ করে, এবং ধনাত্মক সংখ্যা অ্যাকাউন্টের নগদ টাকা বা দাবি নির্দেশ করে। এই গণনার ফলাফলই নির্ধারণ করে যে লেনদেনটি অনুমোদিত হবে কিনা।
পোর্টফোলিও সময় শেষ হওয়ার পর, পোর্টফোলিও Escrow-এর portfolioSettleCash ফাংশন কল করবে যাতে সময় শেষ হওয়া fCash নগদ ব্যালেন্সে রূপান্তরিত হয়। তারপর অ্যাকাউন্ট Escrow থেকে প্রাসঙ্গিক DAI বা USDC সম্পদ তুলে নেবে।
মূল কারণ
এই আক্রমণের মূল দুর্বলতা হলো Escrow বাস্তবায়ন কন্ট্রাক্টে ব্যবহৃত ExchangeRate._convertToETH, যেখানে একটি কোড সাইনড পূর্ণসংখ্যার পরম মানকে সরাসরি uint128-এ রূপান্তর করে।

আক্রমণকারী প্রথমে একই পেয়ারের দুটি দায়ের অবস্থান তৈরি করে, যার পরিমাণ যথাক্রমে 1 এবং uint128.max। এই দুটি দায় যোগ করলে ঠিক 2^128 পাওয়া যায়। যেহেতু ঝুঁকি গণনায় পেয়ারের পেমেন্ট ঋণাত্মক হিসাবে রেকর্ড করা হয়, তাই Escrow-এর convertBalancesToETH-এ প্রবেশ করানো হওয়া মূল প্যারামিটারটি হল -2^128।
কিন্তু এই স্মার্ট চুক্তিটি যে Solidity সংস্করণ ব্যবহার করে তা হল 0.6.x, তাই 2^128-এর পরম মানকে uint128-এ প্রতিস্থাপন করলে এটি ওভারফ্লো হয়ে 0 হয়ে যায়, ফলে এই দায়টি ETH-এর মূল্যায়নে অন্তর্ভুক্ত হয়নি।
শেষ পর্যন্ত, mintfCashPair ফাংশনে শুধুমাত্র পেয়ারের মুক্ত প্রতিজমি শূন্য বা তার বেশি হওয়ার প্রয়োজন হয়, তাই ফলাফল 0-এ ওভারফ্লো হওয়ার পরেও এই পরীক্ষা পাস করে, যার ফলে যথেষ্ট সম্পদ দিয়ে দায় কভার করা যায়না এমন একটি অ্যাকাউন্ট এখনও পজিশন খুলতে পারে।
আক্রমণ ধাপের বিশ্লেষণ
1. প্রিট্রেড (0xe1589a19…d60a) এ, আক্রমণকারী প্রথমে একাধিক সহায়ক স্মার্ট চুক্তি তৈরি করে এবং সহায়ক স্মার্ট চুক্তিগুলিকে অনুমতি দেওয়ার জন্য ERC1155Trade স্মার্ট চুক্তির setApprovalForAll ফাংশন কল করে; এছাড়াও, তিনি দুটি CashMarket-এর মেয়াদোত্তীর্ণ তারিখ আগে থেকেই পরীক্ষা করেন এবং পরবর্তী অপারেশনগুলিতে প্রবেশ করানোর জন্য তিনটি AssetId প্যারামিটারের মানগুলি গণনা করেন।

2. এরপর, আক্রমণকারী ERC1155Trade স্মার্ট চুক্তির safeTransferFrom ফাংশন কল করে 1 পরিমাণের fCash জোড়া মিন্ট করে, যার বন্ড সম্পদের cashGroupId 2 এবং পরিপক্বতার টাইমস্ট্যাম্প 1788480000 (2026 সালের 4 সেপ্টেম্বর, 8:00 AM)। এতে _upsertAsset অন্তর্নিহিত ফাংশনটি from ঠিকানা (আক্রমণকারী স্মার্ট চুক্তি) এবং to ঠিকানা (গ্রহণকারী স্মার্ট চুক্তি 1) এর জন্য যথাক্রমে দায় এবং প্রত্যাশিত আয় আপডেট করে।

পেয়ার মিন্ট হওয়ার পরে, পোর্টফোলিও তাত্ক্ষণিকভাবে আক্রমণ স্মার্ট কন্ট্রাক্টের মুক্ত প্রতিজামি পরীক্ষা করবে, এবং প্রথম মিন্টের পরিমাণ খুব কম হওয়ায়, বর্তমান রেট এবং সঠিকতার ভিত্তিতে ETH-এ রূপান্তরিত হলে শূন্যে গোলাকার হয়ে যায়, তাই প্রথম পরীক্ষা সফল হয়।
3. আক্রমণকারী তারপর আবার ERC1155Trade চুক্তির safeTransferFrom ফাংশনটি কল করে একটি uint128.max পরিমাণের fCash জোড়া মিন্ট করে, এবং এই ক্ষেত্রে সংশ্লিষ্ট বন্ড সম্পদের cashGroupId 2, কিন্তু পরিপক্বতার টাইমস্ট্যাম্প 1796256000 (৩ ডিসেম্বর, ২০২৬, ৮:০০ AM)।
একটি বিস্তারিত বিষয় হলো, আক্রমণকারী দুটি ভিন্ন ঠিকানায় ভিন্ন মেয়াদোত্তীর্ণ বন্ড সম্পদ তৈরি করেছে। এটি কারণ, from ঠিকানার দায় আপডেট করার সময়, যদি বন্ড সম্পদ একই হয়, তাহলে এটি সরাসরি যোগ করা হয়, যার ফলে যোগফলের ওভারফ্লোর কারণে revert (স্মার্ট চুক্তি safeMath লাইব্রেরি ব্যবহার করে) ঘটে।

৪. লায়েবিলিটি এবং প্রত্যাশিত আয় আপডেট করার পরে, mintfCashPair ফাংশনটি from ঠিকানা (আক্রমণ কন্ট্রাক্ট) এবং to ঠিকানা (রিসিভিং কন্ট্রাক্ট 2) থেকে freeCollateral ফাংশনকে কল করে যাতে from ঠিকানার নেট কল্লাটারাল পজিশন গণনা করা যায় এবং এটি শূন্যের সমান বা তার বেশি হওয়ার পরীক্ষা করা যায়।

যেখানে প্রথমে RiskFramework চুক্তিতে getRequirement ফাংশনে from ঠিকানার দুটি মিন্টের দায়ের যোগফল গণনা করা হবে:

এর চূড়ান্ত ফলাফল হল -2^128, এরপর Escrow স্মার্ট চুক্তির convertBalancesToETH ফাংশনটি এই মানটিকে ETH-এ রূপান্তর করতে কল করবে, এবং convertBalancesToETH ফাংশনটি গণনা প্রক্রিয়াকরণের জন্য ExchangeRate লাইব্রেরির _convertToETH ফাংশনটিকে কল করবে:


_convertToETH ফাংশনে যাওয়া যাক, এটি প্রথমে balance-এর পরম মান নেয়, তারপর uint128 ব্যবহার করে 256-বিটকে 128-বিটে রূপান্তর করে। উপরের from-এর দায়ের মোট পরিমাণ -2^128, যার পরম মান 2^128। এই 2^128-কে uint128() এ কাস্ট করলে সীমা অতিক্রম করে 0-এ ওভারফ্লো হয়ে যায়। এর অর্থ হলো, প্রোটোকলটি from ঠিকানার মুক্ত জামানতের পজিশনকে 0 হিসাবে ভুলভাবে ধরে নেয়, যা mintfCashPair ফাংশনের চূড়ান্ত চেককে পাস করে।

5. এরপর কন্ট্র্যাক্ট 2 এর পোজিশনটি আরও দুটি সহায়ক কন্ট্র্যাক্টে বিভক্ত করা হয়, এই দুটি safeTransferFrom এখনও mintfCashPair ফাংশনে প্রবেশ করবে। তবে এই সময়ে payer হল দ্বিতীয়বার মিন্ট করার সময়ের গ্রহণকারী কন্ট্র্যাক্ট 2, যা আগের ধাপে প্রাপ্ত বিশাল প্রত্যাশিত আয়ের পোজিশন ধারণ করে, যা ঝুঁকি গণনা ধনাত্মক ঋণ হিসাবে বিবেচনা করে এবং এটি প্রতিজামীকরণ পরীক্ষা পার করে, ফলে দুটি সহায়ক কন্ট্র্যাক্ট পরবর্তীতে নগদে রূপান্তরযোগ্য receiver fCash পায়।

৬. আক্রমণকারী পরবর্তীতে একটি দ্বিতীয় আনুষ্ঠানিক লাভ লেনদেন শুরু করে, যেখানে পূর্ববর্তী লেনদেনের দুটি ধারণ পজিশনের জন্য সহায়ক চুক্তির প্রতিটির জন্য সংশ্লিষ্ট দাবি সমাধান করা হয়, সংশ্লিষ্ট সম্পদের নগদ ব্যালেন্স বৃদ্ধি করা হয়, এবং তারপর Escrow চুক্তির withdraw ফাংশনটি কল করে তাদের সম্পদটি উত্তোলন করা হয়।
সারাংশ
এই আক্রমণের মূল বিষয় হল আক্রমণকারী প্রথমে দুটি পৃথক পজিশন ব্যবহার করে পেয়ারের দায়ের যোগফলকে 2^128-এ পৌঁছায়, তারপর Escrow-এর মাধ্যমে এই দায়কে ETH-এ রূপান্তর করে। টাইপ কনভার্সনের ওভারফ্লো দুর্বলতা ব্যবহার করে ফলাফলকে শূন্যে কাটিয়ে ফেলে, যার ফলে ঝুঁকি পরীক্ষা অতিক্রম হয়।
মেড সিকিউরিটি টিম সুপারিশ করে যে প্রকল্প দলকে টাইপ রূপান্তরের আগে সীমা পরীক্ষা করতে হবে, type(uint128).max-এর বেশি ফলাফলকে সরাসরি প্রত্যাখ্যান করতে হবে, এছাড়াও, সমস্ত সাইনড অ্যামাউন্ট, প্রিসিশন স্কেলিং এবং এসেট ফেস ভ্যালুর সীমা পরীক্ষা করা উচিত, বিশেষ করে uint128.max, uint128.max + 1 এবং নেগেটিভ অ্যাবসোলিউট এই তিনটি ইনপুটকে কভার করা উচিত। OpenZeppelin-এর SafeCast লাইব্রেরির সাহায্যে রূপান্তর প্রক্রিয়াটি করা যেতে পারে।
