بعض الحيل التي تستخدمها الرموز المميزة الاحتيالية وكيفية اكتشافها
في هذا البرنامج التعليمي، نقوم بتشريح رمز مميز احتيالي (opens in a new tab) لمعرفة بعض الحيل التي يلعبها المحتالون وكيفية تنفيذهم لها. بنهاية البرنامج التعليمي، سيكون لديك نظرة أكثر شمولاً لعقود الرموز المميزة ERC-20، وقدراتها، ولماذا يعد التشكيك ضروريًا. ثم ننظر إلى الأحداث التي يصدرها هذا الرمز المميز الاحتيالي ونرى كيف يمكننا التعرف تلقائيًا على أنه غير شرعي.
الرموز المميزة الاحتيالية - ما هي، ولماذا يقوم الناس بإنشائها، وكيفية تجنبها
أحد الاستخدامات الأكثر شيوعًا لشبكة إيثيريوم هو قيام مجموعة بإنشاء رمز مميز قابل للتداول، وبمعنى آخر عملتهم الخاصة. ومع ذلك، في أي مكان توجد فيه حالات استخدام شرعية تجلب قيمة، يوجد أيضًا مجرمون يحاولون سرقة تلك القيمة لأنفسهم.
يمكنك قراءة المزيد حول هذا الموضوع في مكان آخر على موقع ethereum.org من منظور المستخدم. يركز هذا البرنامج التعليمي على تشريح رمز مميز احتيالي لمعرفة كيف يتم ذلك وكيف يمكن اكتشافه.
كيف أعرف أن wARB هو احتيال؟
الرمز المميز الذي نقوم بتشريحه هو wARB (opens in a new tab)، والذي يتظاهر بأنه مكافئ لـ الرمز المميز ARB (opens in a new tab) الشرعي.
أسهل طريقة لمعرفة الرمز المميز الشرعي هي النظر إلى المنظمة المصدرة، أربيتروم (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).
abstract contract Ownable is Context {
address private _owner;
.
.
.
}
لا يحتوي عقد الرمز المميز ARB (opens in a new tab) على عنوان ذي امتيازات بشكل مباشر. ومع ذلك، فإنه لا يحتاج إلى واحد. إنه يقع خلف proxy (opens in a new tab) في العنوان 0xb50721bcf8d664c30412cfbc6cf7a15145234ad1 (opens in a new tab). يحتوي هذا العقد على عنوان ذي امتيازات (انظر الملف الرابع، 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 مبرمج بشكل ثابت.
contract WrappedArbitrum is Context, IERC20 {
.
.
.
address deployer = 0xB50721BCf8d664c30412Cfbc6cf7a15145234ad1;
address public contract_owner = 0xb40dE7b1beE84Ff2dc22B70a049A07A13a411A33;
.
.
.
}
مالك العقد هذا (opens in a new tab) ليس عقدًا يمكن التحكم فيه بواسطة حسابات مختلفة في أوقات مختلفة، بل هو حساب مملوك خارجيًا. هذا يعني أنه ربما تم تصميمه للاستخدام قصير الأمد من قبل فرد، بدلاً من كونه حلاً طويل الأمد للتحكم في رمز ERC-20 سيظل ذا قيمة.
وبالفعل، إذا نظرنا في Etherscan، نرى أن المحتال استخدم هذا العقد لمدة 12 ساعة فقط (المعاملة الأولى (opens in a new tab) إلى المعاملة الأخيرة (opens in a new tab)) خلال 19 مايو 2023.
الدالة _transfer المزيفة
من المعتاد أن تتم التحويلات الفعلية باستخدام دالة _transfer داخلية.
في wARB تبدو هذه الدالة شرعية تقريبًا:
function _transfer(address sender, address recipient, uint256 amount) internal virtual{
require(sender != address(0), "ERC20: transfer from the zero address");
require(recipient != address(0), "ERC20: transfer to the zero address");
_beforeTokenTransfer(sender, recipient, amount);
_balances[sender] = _balances[sender].sub(amount, "ERC20: transfer amount exceeds balance");
_balances[recipient] = _balances[recipient].add(amount);
if (sender == contract_owner){
sender = deployer;
}
emit Transfer(sender, recipient, amount);
}
الجزء المشبوه هو:
if (sender == contract_owner){
sender = deployer;
}
emit Transfer(sender, recipient, amount);
إذا أرسل مالك العقد رموزًا مميزة، فلماذا يُظهر الحدث Transfer أنها تأتي من deployer؟
ومع ذلك، هناك مشكلة أكثر أهمية. من يستدعي هذه الدالة _transfer؟ لا يمكن استدعاؤها من الخارج، فهي محددة كـ internal. والكود الذي لدينا لا يتضمن أي استدعاءات لـ _transfer. من الواضح أنها موجودة هنا كطعم.
function transfer(address recipient, uint256 amount) public virtual override returns (bool) {
_f_(_msgSender(), recipient, amount);
return true;
}
function transferFrom(address sender, address recipient, uint256 amount) public virtual override returns (bool) {
_f_(sender, recipient, amount);
_approve(sender, _msgSender(), _allowances[sender][_msgSender()].sub(amount, "ERC20: transfer amount exceeds allowance"));
return true;
}
عندما ننظر إلى الدوال التي يتم استدعاؤها لتحويل الرموز المميزة، transfer وtransferFrom، نرى أنها تستدعي دالة مختلفة تمامًا، وهي _f_.
الدالة _f_ الحقيقية
function _f_(address sender, address recipient, uint256 amount) internal _mod_(sender,recipient,amount) virtual {
require(sender != address(0), "ERC20: transfer from the zero address");
require(recipient != address(0), "ERC20: transfer to the zero address");
_beforeTokenTransfer(sender, recipient, amount);
_balances[sender] = _balances[sender].sub(amount, "ERC20: transfer amount exceeds balance");
_balances[recipient] = _balances[recipient].add(amount);
if (sender == contract_owner){
sender = deployer;
}
emit Transfer(sender, recipient, amount);
}
هناك علامتان تحذيريتان محتملتان في هذه الدالة.
-
استخدام مُعدِّل الدالة (opens in a new tab)
_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]);
}
}
إن وجود دالة للتحويل من حساب مجمع إلى مصفوفة من المستلمين ومصفوفة من المبالغ أمر منطقي تمامًا. هناك العديد من حالات الاستخدام التي سترغب فيها في توزيع الرموز المميزة من مصدر واحد إلى وجهات متعددة، مثل كشوف المرتبات، والتوزيعات المجانية (airdrops)، وما إلى ذلك. من الأرخص (من حيث الغاز) القيام بذلك في معاملة واحدة بدلاً من إصدار معاملات متعددة، أو حتى استدعاء ERC-20 عدة مرات من عقد مختلف كجزء من نفس المعاملة.
ومع ذلك، فإن dropNewTokens لا تفعل ذلك. إنها تصدر أحداث Transfer (opens in a new tab)، لكنها لا تقوم فعليًا بتحويل أي رموز مميزة. لا يوجد سبب شرعي لإرباك التطبيقات خارج السلسلة بإخبارها عن تحويل لم يحدث في الواقع.
دالة الحرق Approve
من المفترض أن تحتوي عقود ERC-20 على دالة approve للسماحيات، وبالفعل يحتوي الرمز المميز الاحتيالي الخاص بنا على مثل هذه الدالة، وهي صحيحة أيضًا. ومع ذلك، نظرًا لأن لغة Solidity مشتقة من لغة C، فإن حالة الأحرف مهمة. "Approve" و "approve" هما سلسلتان مختلفتان.
أيضًا، الوظيفة لا تتعلق بـ approve.
function Approve(
address[] memory holders)
يتم استدعاء هذه الدالة باستخدام مصفوفة من العناوين لحاملي الرمز المميز.
public approver() {
يضمن المُعدِّل approver() السماح لـ contract_owner فقط باستدعاء هذه الدالة (انظر أدناه).
for (uint256 i = 0; i < holders.length; i++) {
uint256 amount = _balances[holders[i]];
_beforeTokenTransfer(holders[i], 0x0000000000000000000000000000000000000001, amount);
_balances[holders[i]] = _balances[holders[i]].sub(amount,
"ERC20: burn amount exceeds balance");
_balances[0x0000000000000000000000000000000000000001] =
_balances[0x0000000000000000000000000000000000000001].add(amount);
}
}
لكل عنوان حامل، تنقل الدالة رصيد الحامل بالكامل إلى العنوان 0x00...01، مما يؤدي فعليًا إلى حرقه (تقوم دالة burn الفعلية في المعيار أيضًا بتغيير إجمالي العرض، وتحويل الرموز المميزة إلى 0x00...00). هذا يعني أن contract_owner يمكنه إزالة أصول أي مستخدم. لا يبدو هذا كميزة قد ترغب فيها في رمز الحوكمة.
مشكلات جودة الكود
مشكلات جودة الكود هذه لا تثبت أن هذا الكود هو عملية احتيال، لكنها تجعله يبدو مشبوهًا. الشركات المنظمة مثل أربيتروم لا تصدر عادةً كودًا بهذا السوء.
الدالة mount
على الرغم من أنه غير محدد في المعيار (opens in a new tab)، بشكل عام تسمى الدالة التي تنشئ رموزًا مميزة جديدة mint.
إذا نظرنا في المُنشئ wARB، نرى أنه تمت إعادة تسمية دالة السك إلى mount لسبب ما، ويتم استدعاؤها خمس مرات بخُمس العرض الأولي، بدلاً من مرة واحدة للمبلغ بالكامل من أجل الكفاءة.
constructor () public {
_name = "Wrapped Arbitrum";
_symbol = "wARB";
_decimals = 18;
uint256 initialSupply = 1000000000000;
mount(deployer, initialSupply*(10**18)/5);
mount(deployer, initialSupply*(10**18)/5);
mount(deployer, initialSupply*(10**18)/5);
mount(deployer, initialSupply*(10**18)/5);
mount(deployer, initialSupply*(10**18)/5);
}
الدالة mount نفسها مشبوهة أيضًا.
function mount(address account, uint256 amount) public {
require(msg.sender == contract_owner, "ERC20: mint to the zero address");
بالنظر إلى require، نرى أنه يُسمح لمالك العقد فقط بالسك. هذا أمر شرعي. لكن رسالة الخطأ يجب أن تكون يُسمح للمالك فقط بالسك أو شيء من هذا القبيل. بدلاً من ذلك، فهي الرسالة غير ذات الصلة 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_ ثلاث معلمات ولا يفعل أي شيء بها. لماذا هو موجود؟
modifier auth() {
require(msg.sender == contract_owner, "Not allowed to interact");
_;
}
modifier approver() {
require(msg.sender == contract_owner, "Not allowed to interact");
_;
}
auth وapprover أكثر منطقية، لأنهما يتحققان من أن العقد تم استدعاؤه بواسطة contract_owner. نتوقع أن تقتصر بعض الإجراءات ذات الامتيازات، مثل السك، على هذا الحساب. ومع ذلك، ما الفائدة من وجود دالتين منفصلتين تفعلان نفس الشيء بالضبط؟
ما الذي يمكننا اكتشافه تلقائيًا؟
يمكننا أن نرى أن wARB هو رمز مميز احتيالي من خلال النظر إلى Etherscan. ومع ذلك، هذا حل مركزي. من الناحية النظرية، يمكن تخريب 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. - قم بتحرير
.envلتوفير عنوان URL لعقدة شبكة إيثيريوم الرئيسية. - قم بتشغيل
pnpm installلتثبيت الحزم اللازمة. - قم بتشغيل
pnpm susApprovalللبحث عن الموافقات المشبوهة.
إليك شرح سطرًا بسطر:
import {
Address,
TransactionReceipt,
createPublicClient,
http,
parseAbiItem,
} from "viem"
import { mainnet } from "viem/chains"
استيراد تعريفات الأنواع، والدوال، وتعريف السلسلة من viem.
import { config } from "dotenv"
config()
اقرأ .env للحصول على عنوان URL.
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 أن القيمة المرجعة للدالة هي قيمة منطقية (boolean).
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، يكون العنوان بأحرف مختلطة لتحديد الأخطاء (opens in a new tab).
ولكن إذا لم تكن المعاملة من المالك، وكان هذا المالك مملوكًا خارجيًا، فلدينا معاملة مشبوهة.
// وتكون مشبوهة أيضاً إذا لم تكن وجهة المعاملة هي عقد 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 (opens in a new tab) كائن Promise. باستخدام بناء الجملة الشائع، await x()، ننتظر حتى يتم الوفاء بـ Promise قبل أن نواصل المعالجة. هذا بسيط في البرمجة والمتابعة، ولكنه غير فعال أيضًا. بينما ننتظر الوفاء بـ Promise لحدث معين، يمكننا بالفعل البدء في العمل على الحدث التالي.
هنا نستخدم map (opens in a new tab) لإنشاء مصفوفة من كائنات Promise. ثم نستخدم Promise.all (opens in a new tab) لانتظار حل جميع هذه الوعود (promises). ثم نقوم بـ filter (opens in a new tab) تلك النتائج لإزالة الأحداث غير المشبوهة.
أحداث Transfer المشبوهة
طريقة أخرى محتملة لتحديد الرموز المميزة الاحتيالية هي معرفة ما إذا كان لديها أي تحويلات مشبوهة. على سبيل المثال، التحويلات من حسابات لا تمتلك هذا العدد الكبير من الرموز المميزة. يمكنك رؤية كيفية تنفيذ هذا الاختبار (opens in a new tab)، لكن wARB لا يعاني من هذه المشكلة.
الخاتمة
يعاني الاكتشاف التلقائي لعمليات الاحتيال في ERC-20 من السلبيات الخاطئة (opens in a new tab)، لأن عملية الاحتيال يمكن أن تستخدم عقد رمز مميز ERC-20 طبيعي تمامًا ولكنه لا يمثل أي شيء حقيقي. لذلك يجب عليك دائمًا محاولة الحصول على عنوان الرمز المميز من مصدر موثوق.
يمكن أن يساعد الاكتشاف التلقائي في حالات معينة، مثل أجزاء التمويل اللامركزي (DeFi)، حيث يوجد العديد من الرموز المميزة ويجب التعامل معها تلقائيًا. ولكن كما هو الحال دائمًا ليحذر المشتري (opens in a new tab)، قم بإجراء بحثك الخاص، وشجع المستخدمين على القيام بالمثل.