بعض الحيل التي تستخدمها الرموز الاحتيالية وكيفية اكتشافها
في هذا البرنامج التعليمي، نحلل رمزًا احتياليًا (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) هو أحد تلك الرموز التي يتوفر لها كود مصدري، مما يسهل فهمه.
بينما يمكن لناشري العقود اختيار نشر الكود المصدري أم لا، فإنهم لا يستطيعون نشر الكود المصدري الخاطئ. يقوم مستكشف الكتل بتجميع الكود المصدري المقدم بشكل مستقل، وإذا لم يحصل على نفس الكود الثانوي (bytecode)، فإنه يرفض هذا الكود المصدري. يمكنك قراءة المزيد عن هذا على موقع إيثرسكان (opens in a new tab).
مقارنة برموز ERC-20 الشرعية
سنقوم بمقارنة هذا الرمز برموز ERC-20 الشرعية. إذا لم تكن على دراية بكيفية كتابة رموز ERC-20 الشرعية عادةً، راجع هذا البرنامج التعليمي.
الثوابت للعناوين ذات الامتيازات
تحتاج العقود أحيانًا إلى عناوين ذات امتيازات. تسمح العقود المصممة للاستخدام طويل الأمد لبعض العناوين ذات الامتيازات بتغيير تلك العناوين، على سبيل المثال لتمكين استخدام عقد جديد متعدد التوقيع (multisig). هناك عدة طرق للقيام بذلك.
يستخدم عقد رمز 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) لا يحتوي على عنوان ذي امتيازات بشكل مباشر. ومع ذلك، فهو لا يحتاج إلى واحد. إنه يقع خلف proxy (opens in a new tab) في العنوان 0xb50721bcf8d664c30412cfbc6cf7a15145234ad1 (opens in a new tab). يحتوي هذا العقد على عنوان ذي امتيازات (انظر الملف الرابع، ERC1967Upgrade.sol) يمكن استخدامه للترقيات.
1 /**2 * @dev يخزن عنوانًا جديدًا في خانة مسؤول EIP1967.3 */4 function _setAdmin(address newAdmin) private {5 require(newAdmin != address(0), "ERC1967: new admin is the zero address");6 StorageSlot.getAddressSlot(_ADMIN_SLOT).value = newAdmin;7 }في المقابل، يحتوي عقد 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 الذي سيظل ذا قيمة.
وبالفعل، إذا نظرنا إلى إيثرسكان، نرى أن المحتال استخدم هذا العقد لمدة 12 ساعة فقط (أول معاملة (opens in a new tab) إلى آخر معاملة (opens in a new tab)) خلال 19 مايو 2023.
دالة _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");4
5 _beforeTokenTransfer(sender, recipient, amount);6
7 _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 }5
6 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");4
5 _beforeTokenTransfer(sender, recipient, amount);6
7 _balances[sender] = _balances[sender].sub(amount, "ERC20: transfer amount exceeds balance");8 _balances[recipient] = _balances[recipient].add(amount);9 if (sender == contract_owner){10
11 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}إن وجود دالة للتحويل من حساب مجمع إلى مصفوفة من المستلمين لمصفوفة من المبالغ أمر منطقي تمامًا. هناك العديد من حالات الاستخدام التي سترغب فيها في توزيع الرموز من مصدر واحد إلى وجهات متعددة، مثل كشوف المرتبات، والإسقاطات الجوية (airdrops)، وما إلى ذلك. من الأرخص (من حيث الغاز) القيام بذلك في معاملة واحدة بدلاً من إصدار معاملات متعددة، أو حتى استدعاء ERC-20 عدة مرات من عقد مختلف كجزء من نفس المعاملة.
ومع ذلك، لا تقوم دالة dropNewTokens بذلك. إنها تُصدر أحداث Transfer (opens in a new tab)، لكنها لا تقوم فعليًا بتحويل أي رموز. لا يوجد سبب شرعي لإرباك التطبيقات خارج السلسلة بإخبارها عن عملية تحويل لم تحدث بالفعل.
دالة Approve الحارقة
من المفترض أن تحتوي عقود ERC-20 على دالة approve للبدلات، وبالفعل يحتوي رمزنا الاحتيالي على مثل هذه الدالة، وهي صحيحة حتى. ومع ذلك، نظرًا لأن لغة سوليديتي مشتقة من لغة 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 يمكنه إزالة أصول أي مستخدم. لا تبدو هذه ميزة قد ترغب بها في رمز حوكمة.
مشكلات جودة الكود
مشكلات جودة الكود هذه لا تثبت أن هذا الكود عملية احتيال، لكنها تجعله يبدو مشبوهًا. الشركات المنظمة مثل أربيتروم لا تصدر عادةً كودًا بهذا السوء.
دالة mount
على الرغم من أنها غير محددة في المعيار (opens in a new tab)، بشكل عام تسمى الدالة التي تنشئ رموزًا جديدة mint (opens in a new tab).
إذا نظرنا إلى مُنشئ wARB، نرى أن دالة mint قد تم تغيير اسمها إلى mount لسبب ما، ويتم استدعاؤها خمس مرات بخُمس العرض الأولي، بدلاً من مرة واحدة للمبلغ بأكمله من أجل الكفاءة.
1 constructor () public {2
3 _name = "Wrapped Arbitrum";4 _symbol = "wARB";5 _decimals = 18;6 uint256 initialSupply = 1000000000000;7
8 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، نرى أن مالك العقد فقط هو المسموح له بالسك (mint). هذا أمر شرعي. لكن رسالة الخطأ يجب أن تكون 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 }5
6 modifier approver() {7 require(msg.sender == contract_owner, "Not allowed to interact");8 _;9 }auth و approver أكثر منطقية، لأنهما يتحققان من أن العقد قد تم استدعاؤه بواسطة contract_owner. نتوقع أن تكون بعض الإجراءات ذات الامتيازات، مثل السك، مقتصرة على ذلك الحساب. ولكن، ما الفائدة من وجود دالتين منفصلتين تفعلان نفس الشيء بالضبط؟
ما الذي يمكننا اكتشافه تلقائيًا؟
يمكننا أن نرى أن wARB هو رمز احتيالي من خلال النظر إلى إيثرسكان. ومع ذلك، هذا حل مركزي. نظريًا، يمكن تخريب إيثرسكان أو اختراقه. من الأفضل أن تكون قادرًا على تحديد ما إذا كان الرمز شرعيًا أم لا بشكل مستقل.
هناك بعض الحيل التي يمكننا استخدامها لتحديد أن رمز ERC-20 مشبوه (إما عملية احتيال أو مكتوب بشكل سيئ للغاية)، من خلال النظر إلى الأحداث التي يصدرها.
أحداث Approval المشبوهة
يجب أن تحدث أحداث Approval (opens in a new tab) فقط بطلب مباشر (على عكس أحداث Transfer (opens in a new tab) التي يمكن أن تحدث نتيجة لبدل). انظر وثائق سوليديتي (opens in a new tab) للحصول على شرح مفصل لهذه المشكلة ولماذا يجب أن تكون الطلبات مباشرة، بدلاً من أن يتوسط فيها عقد.
هذا يعني أن أحداث Approval التي توافق على الإنفاق من حساب مملوك خارجيًا يجب أن تأتي من معاملات تنشأ في ذلك الحساب، ويكون وجهتها عقد ERC-20. أي نوع آخر من الموافقة من حساب مملوك خارجيًا هو أمر مشبوه.
إليك برنامج يحدد هذا النوع من الأحداث (opens in a new tab)، باستخدام viem (opens in a new tab) و تايب سكريبت (opens in a new tab)، وهو متغير من جافا سكريبت مع أمان الأنواع. لتشغيله:
- انسخ
.env.exampleإلى.env. - حرر
.envلتوفير عنوان URL لعقدة شبكة إيثريوم الرئيسية. - شغل
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()اقرأ .env للحصول على عنوان URL.
1const client = createPublicClient({2 chain: mainnet,3 transport: http(process.env.URL),4})إنشاء عميل فيم. نحتاج فقط إلى القراءة من البلوك تشين، لذلك لا يحتاج هذا العميل إلى مفتاح خاص.
1const testedAddress = "0xb047c8032b99841713b8e3872f06cf32beb27b82"2const fromBlock = 16859812n3const toBlock = 16873372nعنوان عقد ERC-20 المشبوه، والكتل التي سنبحث فيها عن الأحداث. عادةً ما يحد مزودو العقد من قدرتنا على قراءة الأحداث لأن النطاق الترددي يمكن أن يصبح مكلفًا. لحسن الحظ، لم يكن wARB قيد الاستخدام لمدة ثماني عشرة ساعة، لذلك يمكننا البحث عن جميع الأحداث (كان هناك 13 حدثًا فقط في المجموع).
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})هذه هي طريقة طلب معلومات الأحداث من فيم. عندما نزوده بتوقيع الحدث الدقيق، بما في ذلك أسماء الحقول، فإنه يحلل الحدث لنا.
1const isContract = async (addr: Address): boolean =>2 await client.getBytecode({ address: addr })خوارزميتنا قابلة للتطبيق فقط على الحسابات المملوكة خارجيًا. إذا كان هناك أي bytecode يتم إرجاعه بواسطة client.getBytecode، فهذا يعني أن هذا عقد ويجب علينا تخطيه.
إذا لم تكن قد استخدمت تايب سكريبت من قبل، فقد يبدو تعريف الدالة غريبًا بعض الشيء. نحن لا نخبره فقط أن المعامل الأول (والوحيد) يسمى addr، ولكن أيضًا أنه من النوع Address. وبالمثل، يخبر الجزء : boolean لغة تايب سكريبت أن القيمة المرجعة للدالة هي قيمة منطقية (boolean).
1const getEventTxn = async (ev: Event): TransactionReceipt =>2 await client.getTransactionReceipt({ hash: ev.transactionHash })تحصل هذه الدالة على إيصال المعاملة من حدث ما. نحن بحاجة إلى الإيصال للتأكد من أننا نعرف ما هي وجهة المعاملة.
1const suspiciousApprovalEvent = async (ev : Event) : (Event | null) => {هذه هي أهم دالة، وهي التي تقرر بالفعل ما إذا كان الحدث مشبوهًا أم لا. نوع الإرجاع، (Event | null)، يخبر تايب سكريبت أن هذه الدالة يمكن أن تعيد إما Event أو null. نعيد null إذا لم يكن الحدث مشبوهًا.
1const owner = ev.args._ownerيحتوي فيم على أسماء الحقول، لذلك قام بتحليل الحدث لنا. _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)3
4console.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) لانتظار حل جميع تلك الوعود. ثم نقوم بـتصفية (opens in a new tab) تلك النتائج لإزالة الأحداث غير المشبوهة.
أحداث Transfer المشبوهة
طريقة أخرى ممكنة لتحديد الرموز الاحتيالية هي معرفة ما إذا كانت لديهم أي تحويلات مشبوهة. على سبيل المثال، تحويلات من حسابات لا تحتوي على هذا العدد الكبير من الرموز. يمكنك رؤية كيفية تنفيذ هذا الاختبار (opens in a new tab)، لكن wARB لا يعاني من هذه المشكلة.
الخلاصة
يعاني الكشف الآلي عن عمليات احتيال ERC-20 من السلبيات الكاذبة (opens in a new tab)، لأن عملية الاحتيال يمكن أن تستخدم عقد رمز ERC-20 عاديًا تمامًا لا يمثل أي شيء حقيقي. لذلك يجب عليك دائمًا محاولة الحصول على عنوان الرمز من مصدر موثوق.
يمكن أن يساعد الكشف الآلي في حالات معينة، مثل أجزاء التمويل اللامركزي (دي فاي)، حيث يوجد العديد من الرموز ويجب التعامل معها تلقائيًا. ولكن كما هو الحال دائمًا على المشتري أن يحذر (opens in a new tab)، قم ببحثك الخاص، وشجع المستخدمين على فعل الشيء نفسه.
انظر هنا لمزيد من أعمالي (opens in a new tab).
آخر تحديث للصفحة: 3 مارس 2026