সেফটি রেল সহ ERC-20
ভূমিকা
ইথেরিয়াম এর অন্যতম সেরা দিক হলো এখানে এমন কোনো কেন্দ্রীয় কর্তৃপক্ষ নেই যা আপনার ট্রানজ্যাকশন পরিবর্তন বা বাতিল করতে পারে। ইথেরিয়াম এর অন্যতম বড় সমস্যা হলো এখানে ব্যবহারকারীর ভুল বা অবৈধ ট্রানজ্যাকশন বাতিল করার ক্ষমতাসম্পন্ন কোনো কেন্দ্রীয় কর্তৃপক্ষ নেই। এই নিবন্ধে আপনি ERC-20 টোকেন ব্যবহার করার সময় ব্যবহারকারীদের করা কিছু সাধারণ ভুল সম্পর্কে জানবেন, পাশাপাশি কীভাবে এমন ERC-20 কন্ট্রাক্ট তৈরি করতে হয় যা ব্যবহারকারীদের সেই ভুলগুলো এড়াতে সাহায্য করে, অথবা কোনো কেন্দ্রীয় কর্তৃপক্ষকে কিছু ক্ষমতা দেয় (উদাহরণস্বরূপ অ্যাকাউন্ট ফ্রিজ করার জন্য) তা শিখবেন।
মনে রাখবেন যে আমরা ওপেনজেপেলিন ERC-20 টোকেন কন্ট্রাক্ট (একটি নতুন ট্যাবে খোলে) ব্যবহার করলেও, এই নিবন্ধে এটি বিস্তারিতভাবে ব্যাখ্যা করা হয়নি। আপনি এই তথ্য এখানে পেতে পারেন।
আপনি যদি সম্পূর্ণ সোর্স কোড দেখতে চান:
- Remix IDE (একটি নতুন ট্যাবে খোলে) খুলুন।
- ক্লোন GitHub আইকনে ক্লিক করুন (
)। - GitHub রিপোজিটরি
https://github.com/qbzzt/20220815-erc20-safety-railsক্লোন করুন। - contracts > erc20-safety-rails.sol খুলুন।
একটি ERC-20 কন্ট্রাক্ট তৈরি করা
সেফটি রেল কার্যকারিতা যোগ করার আগে আমাদের একটি ERC-20 কন্ট্রাক্ট প্রয়োজন। এই নিবন্ধে আমরা ওপেনজেপেলিন Contracts Wizard (একটি নতুন ট্যাবে খোলে) ব্যবহার করব। এটি অন্য একটি ব্রাউজারে খুলুন এবং এই নির্দেশাবলী অনুসরণ করুন:
-
ERC20 নির্বাচন করুন।
-
এই সেটিংসগুলো লিখুন:
প্যারামিটার মান Name SafetyRailsToken Symbol SAFE Premint 1000 Features None Access Control Ownable Upgradability None -
উপরে স্ক্রোল করুন এবং Open in Remix (Remix এর জন্য) বা অন্য কোনো পরিবেশ ব্যবহার করতে Download এ ক্লিক করুন। আমি ধরে নিচ্ছি আপনি Remix ব্যবহার করছেন, যদি আপনি অন্য কিছু ব্যবহার করেন তবে শুধু উপযুক্ত পরিবর্তনগুলো করে নিন।
-
এখন আমাদের কাছে একটি সম্পূর্ণ কার্যকরী ERC-20 কন্ট্রাক্ট আছে। ইমপোর্ট করা কোড দেখতে আপনি
.deps>npmপ্রসারিত করতে পারেন। -
এটি একটি ERC-20 কন্ট্রাক্ট হিসেবে কাজ করে কিনা তা দেখতে কন্ট্রাক্টটি কম্পাইল, ডিপ্লয় করা এবং পরীক্ষা করে দেখুন। আপনার যদি Remix ব্যবহার করা শিখতে হয়, তবে এই টিউটোরিয়ালটি ব্যবহার করুন (একটি নতুন ট্যাবে খোলে)।
সাধারণ ভুলগুলো
ভুলগুলো
ব্যবহারকারীরা মাঝে মাঝে ভুল ঠিকানায় টোকেন পাঠায়। যদিও তারা কী করতে চেয়েছিল তা জানার জন্য আমরা তাদের মন পড়তে পারি না, তবে দুটি ধরনের ত্রুটি রয়েছে যা প্রচুর ঘটে এবং সহজেই শনাক্ত করা যায়:
-
কন্ট্রাক্ট এর নিজস্ব ঠিকানায় টোকেন পাঠানো। উদাহরণস্বরূপ, অপটিমিজম এর OP টোকেন (একটি নতুন ট্যাবে খোলে) দুই মাসেরও কম সময়ে 120,000 এর বেশি (একটি নতুন ট্যাবে খোলে) OP টোকেন জমা করতে সক্ষম হয়েছিল। এটি একটি উল্লেখযোগ্য পরিমাণ সম্পদের প্রতিনিধিত্ব করে যা সম্ভবত মানুষ হারিয়েছে।
-
একটি খালি ঠিকানায় টোকেন পাঠানো, যা কোনো এক্সটার্নালি ওনড অ্যাকাউন্ট (EOA) বা স্মার্ট কন্ট্রাক্ট এর সাথে মিলে না। যদিও এটি কত ঘন ঘন ঘটে তার কোনো পরিসংখ্যান আমার কাছে নেই, তবে একটি ঘটনায় 20,000,000 টোকেন খরচ হতে পারত (একটি নতুন ট্যাবে খোলে)।
হস্তান্তর প্রতিরোধ করা
ওপেনজেপেলিন ERC-20 কন্ট্রাক্ট এ একটি হুক, _beforeTokenTransfer (একটি নতুন ট্যাবে খোলে) অন্তর্ভুক্ত রয়েছে, যা একটি টোকেন হস্তান্তর করার আগে কল করা হয়। ডিফল্টরূপে এই হুকটি কিছুই করে না, তবে আমরা এর উপর আমাদের নিজস্ব কার্যকারিতা যুক্ত করতে পারি, যেমন কোনো সমস্যা থাকলে রিভার্ট করার জন্য চেক করা।
হুকটি ব্যবহার করতে, কনস্ট্রাক্টর এর পরে এই ফাংশনটি যোগ করুন:
function _beforeTokenTransfer(address from, address to, uint256 amount)
internal virtual
override(ERC20)
{
super._beforeTokenTransfer(from, to, amount);
}
আপনি যদি Solidity এর সাথে খুব বেশি পরিচিত না হন তবে এই ফাংশনের কিছু অংশ নতুন হতে পারে:
internal virtual
virtual কীওয়ার্ডটির অর্থ হলো, ঠিক যেমন আমরা ERC20 থেকে কার্যকারিতা ইনহেরিট করেছি এবং এই ফাংশনটিকে ওভাররাইড করেছি, তেমনি অন্যান্য কন্ট্রাক্ট আমাদের থেকে ইনহেরিট করতে পারে এবং এই ফাংশনটিকে ওভাররাইড করতে পারে।
override(ERC20)
আমাদের স্পষ্টভাবে উল্লেখ করতে হবে যে আমরা _beforeTokenTransfer এর ERC20 টোকেন সংজ্ঞাকে ওভাররাইড (একটি নতুন ট্যাবে খোলে) করছি। সাধারণভাবে, নিরাপত্তার দৃষ্টিকোণ থেকে, অন্তর্নিহিত সংজ্ঞার চেয়ে স্পষ্ট সংজ্ঞা অনেক ভালো - আপনি যদি কোনো কিছু আপনার চোখের সামনে থাকে তবে আপনি ভুলে যেতে পারবেন না যে আপনি কিছু করেছেন। এটিও একটি কারণ যে আমাদের উল্লেখ করতে হবে আমরা কোন সুপারক্লাসের _beforeTokenTransfer ওভাররাইড করছি।
super._beforeTokenTransfer(from, to, amount);
এই লাইনটি সেই কন্ট্রাক্ট বা কন্ট্রাক্টগুলোর _beforeTokenTransfer ফাংশনকে কল করে যেখান থেকে আমরা এটি ইনহেরিট করেছি। এই ক্ষেত্রে, এটি শুধুমাত্র ERC20, Ownable এর এই হুকটি নেই। যদিও বর্তমানে ERC20._beforeTokenTransfer কিছুই করে না, আমরা এটি কল করি যদি ভবিষ্যতে কার্যকারিতা যোগ করা হয় (এবং আমরা তখন কন্ট্রাক্টটি পুনরায় ডিপ্লয় করা এর সিদ্ধান্ত নিই, কারণ ডিপ্লয়মেন্ট এর পরে কন্ট্রাক্টগুলো পরিবর্তিত হয় না)।
প্রয়োজনীয়তাগুলো কোডিং করা
আমরা ফাংশনে এই প্রয়োজনীয়তাগুলো যোগ করতে চাই:
toঠিকানাটিaddress(this)এর সমান হতে পারে না, যা ERC-20 কন্ট্রাক্ট এর নিজস্ব ঠিকানা।toঠিকানাটি খালি হতে পারে না, এটি হতে হবে হয়:- একটি এক্সটার্নালি ওনড অ্যাকাউন্ট (EOA)। আমরা সরাসরি কোনো ঠিকানা EOA কিনা তা পরীক্ষা করতে পারি না, তবে আমরা একটি ঠিকানার ETH ব্যালেন্স পরীক্ষা করতে পারি। EOA-গুলোর প্রায় সবসময়ই একটি ব্যালেন্স থাকে, এমনকি যদি সেগুলো আর ব্যবহার করা না হয় - সেগুলোকে শেষ Wei পর্যন্ত খালি করা কঠিন।
- একটি স্মার্ট কন্ট্রাক্ট। কোনো ঠিকানা স্মার্ট কন্ট্রাক্ট কিনা তা পরীক্ষা করা একটু কঠিন। একটি অপকোড রয়েছে যা এক্সটার্নাল কোডের দৈর্ঘ্য পরীক্ষা করে, যাকে
EXTCODESIZE(একটি নতুন ট্যাবে খোলে) বলা হয়, তবে এটি সরাসরি Solidity তে উপলব্ধ নয়। এর জন্য আমাদের Yul (একটি নতুন ট্যাবে খোলে) ব্যবহার করতে হবে, যা হলো EVM অ্যাসেম্বলি। আমরা Solidity থেকে অন্যান্য মান ব্যবহার করতে পারি (<address>.codeএবং<address>.codehash(একটি নতুন ট্যাবে খোলে)), তবে সেগুলোর খরচ বেশি।
চলুন নতুন কোডটি লাইন বাই লাইন দেখে নিই:
require(to != address(this), "Can't send tokens to the contract address");
এটি প্রথম প্রয়োজনীয়তা, পরীক্ষা করুন যে to এবং this(address) একই জিনিস নয়।
bool isToContract;
assembly {
isToContract := gt(extcodesize(to), 0)
}
এভাবেই আমরা পরীক্ষা করি যে কোনো ঠিকানা একটি কন্ট্রাক্ট কিনা। আমরা সরাসরি Yul থেকে আউটপুট গ্রহণ করতে পারি না, তাই এর পরিবর্তে আমরা ফলাফল ধরে রাখার জন্য একটি ভেরিয়েবল সংজ্ঞায়িত করি (এই ক্ষেত্রে isToContract)। Yul যেভাবে কাজ করে তা হলো প্রতিটি অপকোড কে একটি ফাংশন হিসেবে বিবেচনা করা হয়। তাই প্রথমে আমরা কন্ট্রাক্ট এর আকার পেতে EXTCODESIZE (একটি নতুন ট্যাবে খোলে) কল করি, এবং তারপর এটি শূন্য নয় তা পরীক্ষা করতে GT (একটি নতুন ট্যাবে খোলে) ব্যবহার করি (আমরা আনসাইনড ইন্টিজার নিয়ে কাজ করছি, তাই অবশ্যই এটি নেতিবাচক হতে পারে না)। এরপর আমরা ফলাফলটি isToContract এ লিখি।
require(to.balance != 0 || isToContract, "Can't send tokens to an empty address");
এবং পরিশেষে, আমাদের কাছে খালি ঠিকানাগুলোর জন্য প্রকৃত চেক রয়েছে।
প্রশাসনিক অ্যাক্সেস
মাঝে মাঝে এমন একজন অ্যাডমিনিস্ট্রেটর থাকা দরকারী যিনি ভুলগুলো বাতিল করতে পারেন। অপব্যবহারের সম্ভাবনা কমাতে, এই অ্যাডমিনিস্ট্রেটর একটি মাল্টিসিগ (একটি নতুন ট্যাবে খোলে) হতে পারে যাতে একাধিক ব্যক্তিকে কোনো পদক্ষেপে একমত হতে হয়। এই নিবন্ধে আমাদের দুটি প্রশাসনিক বৈশিষ্ট্য থাকবে:
-
অ্যাকাউন্ট ফ্রিজ এবং আনফ্রিজ করা। এটি দরকারী হতে পারে, উদাহরণস্বরূপ, যখন কোনো অ্যাকাউন্ট আপস করা হতে পারে।
-
সম্পদ পরিষ্কার করা।
মাঝে মাঝে প্রতারকরা বৈধতা অর্জনের জন্য আসল টোকেনের কন্ট্রাক্ট এ প্রতারণামূলক টোকেন পাঠায়। উদাহরণস্বরূপ, এখানে দেখুন (একটি নতুন ট্যাবে খোলে)। বৈধ ERC-20 কন্ট্রাক্ট হলো 0x4200....0042 (একটি নতুন ট্যাবে খোলে)। যে স্ক্যামটি এর ভান করে তা হলো 0x234....bbe (একটি নতুন ট্যাবে খোলে)।
এমনও হতে পারে যে লোকেরা ভুল করে আমাদের কন্ট্রাক্ট এ বৈধ ERC-20 টোকেন পাঠায়, যা সেগুলোকে বের করার একটি উপায় চাওয়ার আরেকটি কারণ।
ওপেনজেপেলিন প্রশাসনিক অ্যাক্সেস সক্ষম করার জন্য দুটি মেকানিজম প্রদান করে:
Ownable(একটি নতুন ট্যাবে খোলে) কন্ট্রাক্টগুলোর একজন একক মালিক থাকে। যে ফাংশনগুলোতেonlyOwnerমডিফায়ার (একটি নতুন ট্যাবে খোলে) রয়েছে সেগুলো শুধুমাত্র সেই মালিক কল করতে পারেন। মালিকরা অন্য কারো কাছে মালিকানা হস্তান্তর করতে পারেন বা এটি সম্পূর্ণভাবে ত্যাগ করতে পারেন। অন্যান্য সমস্ত অ্যাকাউন্টের অধিকার সাধারণত অভিন্ন হয়।AccessControl(একটি নতুন ট্যাবে খোলে) কন্ট্রাক্টগুলোতে রোল বেসড অ্যাক্সেস কন্ট্রোল (RBAC) (একটি নতুন ট্যাবে খোলে) রয়েছে।
সরলতার খাতিরে, এই নিবন্ধে আমরা Ownable ব্যবহার করি।
কন্ট্রাক্ট ফ্রিজ এবং আনফ্রিজ করা
কন্ট্রাক্ট ফ্রিজ এবং আনফ্রিজ করার জন্য বেশ কয়েকটি পরিবর্তনের প্রয়োজন:
-
কোন ঠিকানাগুলো ফ্রিজ করা হয়েছে তার ট্র্যাক রাখতে ঠিকানা থেকে বুলিয়ান (একটি নতুন ট্যাবে খোলে) এ একটি ম্যাপিং (একটি নতুন ট্যাবে খোলে)। সমস্ত মান প্রাথমিকভাবে শূন্য থাকে, যা বুলিয়ান মানের জন্য ফলস (false) হিসেবে ব্যাখ্যা করা হয়। আমরা এটাই চাই কারণ ডিফল্টরূপে অ্যাকাউন্টগুলো ফ্রিজ করা থাকে না।
mapping(address => bool) public frozenAccounts; -
যখন কোনো অ্যাকাউন্ট ফ্রিজ বা আনফ্রিজ করা হয় তখন আগ্রহী যে কাউকে জানানোর জন্য ইভেন্ট (একটি নতুন ট্যাবে খোলে)। প্রযুক্তিগতভাবে বলতে গেলে এই কাজগুলোর জন্য ইভেন্ট এর প্রয়োজন নেই, তবে এটি অফচেইন কোডকে এই ইভেন্টগুলো শুনতে এবং কী ঘটছে তা জানতে সাহায্য করে। যখন অন্য কারো জন্য প্রাসঙ্গিক হতে পারে এমন কিছু ঘটে তখন একটি স্মার্ট কন্ট্রাক্ট এর জন্য সেগুলো এমিট (emit) করা ভালো শিষ্টাচার হিসেবে বিবেচিত হয়।
ইভেন্টগুলো ইনডেক্স করা হয় যাতে কোনো অ্যাকাউন্ট কতবার ফ্রিজ বা আনফ্রিজ করা হয়েছে তা অনুসন্ধান করা সম্ভব হয়।
// যখন অ্যাকাউন্টগুলো ফ্রিজ বা আনফ্রিজ করা হয় event AccountFrozen(address indexed _addr); event AccountThawed(address indexed _addr); -
অ্যাকাউন্ট ফ্রিজ এবং আনফ্রিজ করার ফাংশন। এই দুটি ফাংশন প্রায় অভিন্ন, তাই আমরা শুধুমাত্র ফ্রিজ ফাংশনটি নিয়ে আলোচনা করব।
function freezeAccount(address addr) public onlyOwnerpublic(একটি নতুন ট্যাবে খোলে) চিহ্নিত ফাংশনগুলো অন্যান্য স্মার্ট কন্ট্রাক্ট থেকে বা সরাসরি কোনো ট্রানজ্যাকশন দ্বারা কল করা যেতে পারে।{ require(!frozenAccounts[addr], "Account already frozen"); frozenAccounts[addr] = true; emit AccountFrozen(addr); } // freezeAccountযদি অ্যাকাউন্টটি ইতিমধ্যে ফ্রিজ করা থাকে, তবে রিভার্ট করুন। অন্যথায়, এটি ফ্রিজ করুন এবং একটি ইভেন্ট
emitকরুন। -
একটি ফ্রিজ করা অ্যাকাউন্ট থেকে অর্থ সরানো রোধ করতে
_beforeTokenTransferপরিবর্তন করুন। মনে রাখবেন যে ফ্রিজ করা অ্যাকাউন্টে এখনও অর্থ হস্তান্তর করা যেতে পারে।require(!frozenAccounts[from], "The account is frozen");
সম্পদ পরিষ্কার করা
এই কন্ট্রাক্ট দ্বারা ধারণ করা ERC-20 টোকেনগুলো রিলিজ করার জন্য আমাদের সেই টোকেন কন্ট্রাক্ট এর একটি ফাংশন কল করতে হবে যার অন্তর্গত সেগুলো, হয় transfer (একটি নতুন ট্যাবে খোলে) অথবা approve (একটি নতুন ট্যাবে খোলে)। এই ক্ষেত্রে অ্যালাউন্স এর উপর গ্যাস নষ্ট করার কোনো মানে নেই, আমরা সরাসরি হস্তান্তর করতে পারি।
function cleanupERC20(
address erc20,
address dest
)
public
onlyOwner
{
IERC20 token = IERC20(erc20);
যখন আমরা ঠিকানা পাই তখন একটি কন্ট্রাক্ট এর জন্য একটি অবজেক্ট তৈরি করার সিনট্যাক্স এটি। আমরা এটি করতে পারি কারণ সোর্স কোডের অংশ হিসেবে আমাদের কাছে ERC20 টোকেনের সংজ্ঞা রয়েছে (লাইন 4 দেখুন), এবং সেই ফাইলে IERC20 এর সংজ্ঞা (একটি নতুন ট্যাবে খোলে) অন্তর্ভুক্ত রয়েছে, যা একটি ওপেনজেপেলিন ERC-20 কন্ট্রাক্ট এর ইন্টারফেস।
uint balance = token.balanceOf(address(this));
token.transfer(dest, balance);
}
এটি একটি ক্লিনআপ ফাংশন, তাই সম্ভবত আমরা কোনো টোকেন রেখে যেতে চাই না। ব্যবহারকারীর কাছ থেকে ম্যানুয়ালি ব্যালেন্স পাওয়ার পরিবর্তে, আমরা প্রক্রিয়াটি স্বয়ংক্রিয় করতে পারি।
উপসংহার
এটি কোনো নিখুঁত সমাধান নয় - "ব্যবহারকারী ভুল করেছে" সমস্যার কোনো নিখুঁত সমাধান নেই। তবে, এই ধরনের চেক ব্যবহার করে অন্তত কিছু ভুল প্রতিরোধ করা যেতে পারে। অ্যাকাউন্ট ফ্রিজ করার ক্ষমতা, যদিও বিপজ্জনক, হ্যাকারকে চুরি করা তহবিল থেকে বঞ্চিত করে নির্দিষ্ট হ্যাকের ক্ষতি সীমিত করতে ব্যবহার করা যেতে পারে।