Một số thủ thuật được token lừa đảo sử dụng và cách phát hiện chúng
Trong hướng dẫn này, chúng tôi phân tích một token lừa đảoopens in a new tab để xem một số thủ thuật mà những kẻ lừa đảo sử dụng và cách chúng triển khai. Khi kết thúc hướng dẫn, bạn sẽ có cái nhìn toàn diện hơn về các hợp đồng token ERC-20, khả năng của chúng và lý do tại sao sự hoài nghi là cần thiết. Sau đó, chúng tôi xem xét các sự kiện do token lừa đảo đó đưa ra và xem cách chúng ta có thể tự động xác định rằng token đó không hợp pháp.
Token lừa đảo - chúng là gì, tại sao mọi người tạo ra chúng và làm thế nào để tránh chúng
Một trong những ứng dụng cho Ethereum đó là cho một nhóm tạo ra Token có thể giao dịch, nói đơn giản là tiền tệ của riêng họ. Tuy nhiên, bất kỳ nơi nào có các trường hợp sử dụng hợp pháp mang lại giá trị, cũng có những tên tội phạm cố gắng đánh cắp giá trị đó cho riêng họ.
Bạn có thể đọc thêm về chủ đề này ở nơi khác trên ethereum.org từ góc độ người dùng. Hướng dẫn này tập trung vào việc phân tích một token lừa đảo để xem nó được thực hiện như thế nào và cách phát hiện nó.
Làm cách nào để tôi biết wARB là một trò lừa đảo?
Token mà chúng tôi phân tích là wARBopens in a new tab, nó giả vờ tương đương với token ARBopens in a new tab hợp pháp.
Cách dễ nhất để biết đâu là token hợp pháp là xem xét tổ chức ban đầu, Arbitrumopens in a new tab. Các địa chỉ hợp pháp được chỉ định trong tài liệu của họopens in a new tab.
Tại sao mã nguồn lại có sẵn?
Thông thường, chúng tôi mong đợi những người cố gắng lừa đảo người khác sẽ giữ bí mật, và thực tế là nhiều token lừa đảo không có sẵn mã của chúng (ví dụ: token nàyopens in a new tab và token nàyopens in a new tab).
Tuy nhiên, các token hợp pháp thường công bố mã nguồn của chúng, vì vậy để có vẻ hợp pháp, tác giả của các token lừa đảo đôi khi cũng làm như vậy. wARBopens in a new tab là một trong những token có mã nguồn sẵn có, giúp việc tìm hiểu dễ dàng hơn.
Mặc dù người triển khai hợp đồng có thể chọn có công bố mã nguồn hay không, nhưng họ không thể công bố mã nguồn sai. Trình duyệt khối biên dịch mã nguồn được cung cấp một cách độc lập và nếu không nhận được chính xác cùng một chỉ thị biên dịch, nó sẽ từ chối mã nguồn đó. Bạn có thể đọc thêm về điều này trên trang Etherscanopens in a new tab.
So sánh với các token ERC-20 hợp pháp
Chúng tôi sẽ so sánh token này với các token ERC-20 hợp pháp. Nếu bạn không quen với cách các token ERC-20 hợp pháp thường được viết, xem hướng dẫn này.
Hằng số cho các địa chỉ đặc quyền
Các hợp đồng đôi khi cần các địa chỉ đặc quyền. Các hợp đồng được thiết kế để sử dụng lâu dài cho phép một số địa chỉ đặc quyền thay đổi các địa chỉ đó, ví dụ như để cho phép sử dụng một hợp đồng multisig mới. Có một số cách để làm điều này.
Hợp đồng token HOPopens in a new tab sử dụng mẫu Ownableopens in a new tab. Địa chỉ đặc quyền được giữ trong bộ nhớ, trong một trường có tên là _owner (xem tệp thứ ba, Ownable.sol).
1abstract contract Ownable is Context {2 address private _owner;3 .4 .5 .6}Hợp đồng token ARBopens in a new tab không có địa chỉ đặc quyền trực tiếp. Tuy nhiên, nó không cần. Nó nằm sau một proxyopens in a new tab tại địa chỉ 0xb50721bcf8d664c30412cfbc6cf7a15145234ad1opens in a new tab. Hợp đồng đó có một địa chỉ đặc quyền (xem tệp thứ tư, ERC1967Upgrade.sol) có thể được sử dụng để nâng cấp.
1 /**2 * @dev Lưu trữ một địa chỉ mới trong slot quản trị 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 }Ngược lại, hợp đồng wARB có một contract_owner được mã hóa cứng.
1contract WrappedArbitrum is Context, IERC20 {2 .3 .4 .5 address deployer = 0xB50721BCf8d664c30412Cfbc6cf7a15145234ad1;6 address public contract_owner = 0xb40dE7b1beE84Ff2dc22B70a049A07A13a411A33;7 .8 .9 .10}Hiện tất cảChủ sở hữu hợp đồng nàyopens in a new tab không phải là một hợp đồng có thể được kiểm soát bởi các tài khoản khác nhau vào những thời điểm khác nhau, mà là một tài khoản sở hữu ngoại biên. Điều này có nghĩa là nó có thể được thiết kế để một cá nhân sử dụng trong thời gian ngắn, thay vì là một giải pháp lâu dài để kiểm soát một ERC-20 vẫn còn giá trị.
Và thực vậy, nếu chúng ta xem trên Etherscan, chúng ta sẽ thấy rằng kẻ lừa đảo chỉ sử dụng hợp đồng này trong 12 giờ (giao dịch đầu tiênopens in a new tab đến giao dịch cuối cùngopens in a new tab) trong ngày 19 tháng 5 năm 2023.
Hàm _transfer giả mạo
Thông thường, các giao dịch chuyển tiền thực tế sẽ xảy ra bằng cách sử dụng một hàm _transfer nội bộ.
Trong wARB, hàm này trông gần như hợp pháp:
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");45 _beforeTokenTransfer(sender, recipient, amount);67 _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 }Hiện tất cảPhần đáng ngờ là:
1 if (sender == contract_owner){2 sender = deployer;3 }4 emit Transfer(sender, recipient, amount);Nếu chủ sở hữu hợp đồng gửi token, tại sao sự kiện Transfer lại cho thấy chúng đến từ deployer?
Tuy nhiên, có một vấn đề quan trọng hơn. Ai gọi hàm _transfer này? Nó không thể được gọi từ bên ngoài, nó được đánh dấu là internal. Và mã chúng tôi có không bao gồm bất kỳ lệnh gọi nào đến _transfer. Rõ ràng, nó ở đây như một mồi nhử.
1 function transfer(address recipient, uint256 amount) public virtual override returns (bool) {2 _f_(_msgSender(), recipient, amount);3 return true;4 }56 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 }Hiện tất cảKhi chúng ta xem các hàm được gọi để chuyển token, transfer và transferFrom, chúng ta thấy rằng chúng gọi một hàm hoàn toàn khác, _f_.
Hàm _f_ thực sự
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");45 _beforeTokenTransfer(sender, recipient, amount);67 _balances[sender] = _balances[sender].sub(amount, "ERC20: transfer amount exceeds balance");8 _balances[recipient] = _balances[recipient].add(amount);9 if (sender == contract_owner){1011 sender = deployer;12 }13 emit Transfer(sender, recipient, amount);14 }Hiện tất cảCó hai dấu hiệu đáng báo động tiềm ẩn trong hàm này.
-
Việc sử dụng bộ sửa đổi hàmopens in a new tab
_mod_. Tuy nhiên, khi xem xét mã nguồn, chúng tôi thấy rằng_mod_thực sự vô hại.1modifier _mod_(address sender, address recipient, uint256 amount){2 _;3} -
Vấn đề tương tự mà chúng tôi đã thấy trong
_transfer, đó là khicontract_ownergửi token, chúng dường như đến từdeployer.
Hàm sự kiện giả mạo dropNewTokens
Bây giờ chúng ta đến với một cái gì đó trông giống như một trò lừa đảo thực sự. Tôi đã chỉnh sửa hàm một chút để dễ đọc hơn, nhưng về mặt chức năng thì nó tương đương.
1function dropNewTokens(address uPool,2 address[] memory eReceiver,3 uint256[] memory eAmounts) public auth()Hàm này có công cụ sửa đổi auth(), có nghĩa là nó chỉ có thể được gọi bởi chủ sở hữu hợp đồng.
1modifier auth() {2 require(msg.sender == contract_owner, "Not allowed to interact");3 _;4}Hạn chế này hoàn toàn hợp lý, vì chúng tôi không muốn các tài khoản ngẫu nhiên phân phối token. Tuy nhiên, phần còn lại của hàm rất đáng ngờ.
1{2 for (uint256 i = 0; i < eReceiver.length; i++) {3 emit Transfer(uPool, eReceiver[i], eAmounts[i]);4 }5}Một hàm để chuyển từ một tài khoản pool đến một mảng người nhận một mảng số tiền là hoàn toàn hợp lý. Có nhiều trường hợp sử dụng trong đó bạn sẽ muốn phân phối token từ một nguồn duy nhất đến nhiều điểm đến, chẳng hạn như trả lương, airdrop, v.v. Sẽ rẻ hơn (về gas) nếu thực hiện trong một giao dịch duy nhất thay vì phát hành nhiều giao dịch hoặc thậm chí gọi ERC-20 nhiều lần từ một hợp đồng khác như một phần của cùng một giao dịch.
Tuy nhiên, dropNewTokens không làm điều đó. Nó phát ra các sự kiện Transferopens in a new tab, nhưng thực sự không chuyển bất kỳ token nào. Không có lý do chính đáng nào để gây nhầm lẫn cho các ứng dụng ngoài chuỗi bằng cách cho chúng biết về một giao dịch chuyển tiền không thực sự xảy ra.
Hàm Approve đốt token
Các hợp đồng ERC-20 được cho là có hàm approve cho các khoản phụ cấp và thực tế token lừa đảo của chúng tôi có một hàm như vậy, và nó thậm chí còn đúng. Tuy nhiên, vì Solidity có nguồn gốc từ C nên nó phân biệt chữ hoa chữ thường. "Approve" và "approve" là các chuỗi khác nhau.
Ngoài ra, chức năng này không liên quan đến approve.
1 function Approve(2 address[] memory holders)Hàm này được gọi với một mảng địa chỉ cho những người nắm giữ token.
1 public approver() {Công cụ sửa đổi approver() đảm bảo chỉ contract_owner mới được phép gọi hàm này (xem bên dưới).
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 }10Hiện tất cảĐối với mỗi địa chỉ người nắm giữ, hàm sẽ chuyển toàn bộ số dư của người nắm giữ đến địa chỉ 0x00...01, đốt nó một cách hiệu quả (hàm burn thực tế trong tiêu chuẩn cũng thay đổi tổng cung và chuyển token đến 0x00...00). Điều này có nghĩa là contract_owner có thể xóa tài sản của bất kỳ người dùng nào. Đó không phải là một tính năng mà bạn muốn có trong một token quản trị.
Các vấn đề về chất lượng mã
Những vấn đề về chất lượng mã này không chứng minh rằng mã này là một trò lừa đảo, nhưng chúng khiến nó có vẻ đáng ngờ. Các công ty có tổ chức như Arbitrum thường không phát hành mã tệ như vậy.
Hàm mount
Mặc dù nó không được chỉ định trong tiêu chuẩnopens in a new tab, nhưng nói chung, hàm tạo ra các token mới được gọi là mintopens in a new tab.
Nếu chúng ta nhìn vào hàm khởi tạo wARB, chúng ta sẽ thấy hàm mint đã được đổi tên thành mount vì một lý do nào đó và được gọi năm lần với một phần năm nguồn cung ban đầu, thay vì một lần cho toàn bộ số lượng để đạt hiệu quả.
1 constructor () public {23 _name = "Wrapped Arbitrum";4 _symbol = "wARB";5 _decimals = 18;6 uint256 initialSupply = 1000000000000;78 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 }Hiện tất cảBản thân hàm mount cũng đáng ngờ.
1 function mount(address account, uint256 amount) public {2 require(msg.sender == contract_owner, "ERC20: mint to the zero address");Nhìn vào require, chúng ta thấy rằng chỉ chủ sở hữu hợp đồng mới được phép mint. Điều đó là hợp pháp. Nhưng thông báo lỗi phải là chỉ chủ sở hữu mới được phép mint hoặc một cái gì đó tương tự. Thay vào đó, nó là một thông báo không liên quan ERC20: mint vào địa chỉ không. Thử nghiệm chính xác cho việc mint vào địa chỉ không là require(account != address(0), "<error message>"), mà hợp đồng không bao giờ bận tâm kiểm tra.
1 _totalSupply = _totalSupply.add(amount);2 _balances[contract_owner] = _balances[contract_owner].add(amount);3 emit Transfer(address(0), account, amount);4 }Có hai sự kiện đáng ngờ khác, liên quan trực tiếp đến việc mint:
-
Có một tham số
account, có lẽ là tài khoản sẽ nhận số tiền được mint. Nhưng số dư tăng lên thực sự là củacontract_owner. -
Trong khi số dư tăng thuộc về
contract_owner, sự kiện được phát ra cho thấy một giao dịch chuyển tiền đếnaccount.
Tại sao lại có cả auth và approver? Tại sao lại có mod không làm gì cả?
Hợp đồng này chứa ba công cụ sửa đổi: _mod_, auth và approver.
1 modifier _mod_(address sender, address recipient, uint256 amount){2 _;3 }_mod_ nhận ba tham số và không làm gì với chúng. Tại sao lại có nó?
1 modifier auth() {2 require(msg.sender == contract_owner, "Not allowed to interact");3 _;4 }56 modifier approver() {7 require(msg.sender == contract_owner, "Not allowed to interact");8 _;9 }Hiện tất cảauth và approver có ý nghĩa hơn, vì chúng kiểm tra xem hợp đồng có được gọi bởi contract_owner hay không. Chúng tôi mong đợi một số hành động đặc quyền nhất định, chẳng hạn như mint, sẽ bị giới hạn trong tài khoản đó. Tuy nhiên, mục đích của việc có hai hàm riêng biệt thực hiện chính xác cùng một việc là gì?
Chúng ta có thể tự động phát hiện những gì?
Chúng ta có thể thấy wARB là một token lừa đảo bằng cách xem Etherscan. Tuy nhiên, đó là một giải pháp tập trung. Về lý thuyết, Etherscan có thể bị lật đổ hoặc bị tấn công. Tốt hơn là có thể tự mình tìm ra một token có hợp pháp hay không.
Có một số thủ thuật chúng ta có thể sử dụng để xác định rằng một token ERC-20 là đáng ngờ (lừa đảo hoặc được viết rất tệ), bằng cách xem các sự kiện mà chúng phát ra.
Các sự kiện Approval đáng ngờ
Các sự kiện Approvalopens in a new tab chỉ nên xảy ra với một yêu cầu trực tiếp (trái ngược với các sự kiện Transferopens in a new tab có thể xảy ra do phụ cấp). Xem tài liệu Solidityopens in a new tab để được giải thích chi tiết về vấn đề này và tại sao các yêu cầu cần phải trực tiếp, thay vì thông qua một hợp đồng.
Điều này có nghĩa là các sự kiện Approval phê duyệt chi tiêu từ một tài khoản sở hữu ngoại biên phải đến từ các giao dịch bắt nguồn từ tài khoản đó và có đích đến là hợp đồng ERC-20. Bất kỳ loại phê duyệt nào khác từ một tài khoản sở hữu ngoại biên đều đáng ngờ.
Đây là một chương trình xác định loại sự kiện nàyopens in a new tab, sử dụng viemopens in a new tab và TypeScriptopens in a new tab, một biến thể JavaScript với an toàn kiểu. Để chạy nó:
- Sao chép
.env.examplevào.env. - Chỉnh sửa
.envđể cung cấp URL cho một nút mạng chính Ethereum. - Chạy
pnpm installđể cài đặt các gói cần thiết. - Chạy
pnpm susApprovalđể tìm kiếm các phê duyệt đáng ngờ.
Đây là giải thích từng dòng:
1import {2 Address,3 TransactionReceipt,4 createPublicClient,5 http,6 parseAbiItem,7} from "viem"8import { mainnet } from "viem/chains"Nhập định nghĩa kiểu, hàm và định nghĩa chuỗi từ viem.
1import { config } from "dotenv"2config()Đọc .env để lấy URL.
1const client = createPublicClient({2 chain: mainnet,3 transport: http(process.env.URL),4})Tạo một ứng dụng Viem. Chúng ta chỉ cần đọc từ chuỗi khối, vì vậy ứng dụng này không cần khoá riêng tư.
1const testedAddress = "0xb047c8032b99841713b8e3872f06cf32beb27b82"2const fromBlock = 16859812n3const toBlock = 16873372nĐịa chỉ của hợp đồng ERC-20 đáng ngờ và các khối mà chúng tôi sẽ tìm kiếm sự kiện. Các nhà cung cấp nút thường giới hạn khả năng đọc các sự kiện của chúng tôi vì băng thông có thể tốn kém. May mắn là wARB không được sử dụng trong khoảng thời gian mười tám giờ, vì vậy chúng ta có thể tìm kiếm tất cả các sự kiện (chỉ có tổng cộng 13 sự kiện).
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})Đây là cách để yêu cầu Viem cung cấp thông tin sự kiện. Khi chúng tôi cung cấp cho nó chữ ký sự kiện chính xác, bao gồm cả tên trường, nó sẽ phân tích cú pháp sự kiện cho chúng tôi.
1const isContract = async (addr: Address): boolean =>2 await client.getBytecode({ address: addr })Thuật toán của chúng tôi chỉ áp dụng cho các tài khoản sở hữu ngoại biên. Nếu có bất kỳ chỉ thị biên dịch nào được trả về bởi client.getBytecode, điều đó có nghĩa đây là một hợp đồng và chúng ta nên bỏ qua nó.
Nếu bạn chưa từng sử dụng TypeScript trước đây, định nghĩa hàm có thể trông hơi lạ. Chúng tôi không chỉ cho nó biết tham số đầu tiên (và duy nhất) được gọi là addr, mà còn cho biết nó thuộc loại Address. Tương tự, phần : boolean cho TypeScript biết rằng giá trị trả về của hàm là một giá trị boolean.
1const getEventTxn = async (ev: Event): TransactionReceipt =>2 await client.getTransactionReceipt({ hash: ev.transactionHash })Hàm này lấy biên lai giao dịch từ một sự kiện. Chúng tôi cần biên lai để đảm bảo chúng tôi biết đích đến của giao dịch là gì.
1const suspiciousApprovalEvent = async (ev : Event) : (Event | null) => {Đây là hàm quan trọng nhất, hàm thực sự quyết định một sự kiện có đáng ngờ hay không. Kiểu trả về, (Event | null), cho TypeScript biết rằng hàm này có thể trả về một Event hoặc null. Chúng tôi trả về null nếu sự kiện không đáng ngờ.
1const owner = ev.args._ownerViem có tên trường, vì vậy nó đã phân tích cú pháp sự kiện cho chúng tôi. _owner là chủ sở hữu của các token sẽ được chi tiêu.
1// Phê duyệt bởi các hợp đồng không đáng ngờ2if (await isContract(owner)) return nullNếu chủ sở hữu là một hợp đồng, hãy cho rằng sự chấp thuận này không đáng ngờ. Để kiểm tra xem sự chấp thuận của một hợp đồng có đáng ngờ hay không, chúng ta sẽ cần phải theo dõi toàn bộ quá trình thực hiện của giao dịch để xem liệu nó có bao giờ đến được hợp đồng của chủ sở hữu hay không, và liệu hợp đồng đó có gọi trực tiếp hợp đồng ERC-20 hay không. Điều đó tốn nhiều tài nguyên hơn chúng ta muốn làm.
1const txn = await getEventTxn(ev)Nếu sự chấp thuận đến từ một tài khoản sở hữu ngoại biên, hãy lấy giao dịch đã gây ra nó.
1// Phê duyệt là đáng ngờ nếu nó đến từ một chủ sở hữu EOA không phải là `from` của giao dịch2if (owner.toLowerCase() != txn.from.toLowerCase()) return evChúng ta không thể chỉ kiểm tra sự bằng nhau của chuỗi vì địa chỉ là hệ thập lục phân, vì vậy chúng chứa các chữ cái. Đôi khi, ví dụ như trong txn.from, tất cả các chữ cái đó đều là chữ thường. Trong các trường hợp khác, chẳng hạn như ev.args._owner, địa chỉ ở dạng chữ hoa chữ thường hỗn hợp để xác định lỗiopens in a new tab.
Nhưng nếu giao dịch không phải từ chủ sở hữu và chủ sở hữu đó là sở hữu ngoại biên, thì chúng ta có một giao dịch đáng ngờ.
1// Nó cũng đáng ngờ nếu đích đến của giao dịch không phải là hợp đồng ERC-20 mà chúng ta đang2// điều tra3if (txn.to.toLowerCase() != testedAddress) return evTương tự, nếu địa chỉ to của giao dịch, hợp đồng đầu tiên được gọi, không phải là hợp đồng ERC-20 đang được điều tra thì nó đáng ngờ.
1 // Nếu không có lý do gì để nghi ngờ, hãy trả về null.2 return null3}Nếu không có điều kiện nào là đúng thì sự kiện Approval không đáng ngờ.
1const testPromises = approvalEvents.map((ev) => suspiciousApprovalEvent(ev))2const testResults = (await Promise.all(testPromises)).filter((x) => x != null)34console.log(testResults)Một hàm asyncopens in a new tab trả về một đối tượng Promise. Với cú pháp phổ biến, await x(), chúng ta chờ cho Promise đó được thực hiện trước khi tiếp tục xử lý. Điều này đơn giản để lập trình và làm theo, nhưng nó cũng không hiệu quả. Trong khi chúng ta đang chờ Promise cho một sự kiện cụ thể được thực hiện, chúng ta đã có thể bắt đầu xử lý sự kiện tiếp theo.
Ở đây chúng tôi sử dụng mapopens in a new tab để tạo một mảng các đối tượng Promise. Sau đó, chúng tôi sử dụng Promise.allopens in a new tab để chờ tất cả các promise đó được giải quyết. Sau đó, chúng tôi filteropens in a new tab các kết quả đó để loại bỏ các sự kiện không đáng ngờ.
Các sự kiện Transfer đáng ngờ
Một cách khả thi khác để xác định các token lừa đảo là xem chúng có bất kỳ giao dịch chuyển tiền đáng ngờ nào không. Ví dụ: chuyển tiền từ các tài khoản không có nhiều token. Bạn có thể xem cách triển khai thử nghiệm nàyopens in a new tab, nhưng wARB không gặp phải vấn đề này.
Kết luận
Việc phát hiện tự động các trò lừa đảo ERC-20 bị ảnh hưởng bởi kết quả âm tính giảopens in a new tab, bởi vì một trò lừa đảo có thể sử dụng một hợp đồng token ERC-20 hoàn toàn bình thường mà không đại diện cho bất cứ thứ gì thực. Vì vậy, bạn nên luôn cố gắng lấy địa chỉ token từ một nguồn đáng tin cậy.
Phát hiện tự động có thể giúp ích trong một số trường hợp nhất định, chẳng hạn như các phần DeFi, nơi có nhiều token và chúng cần được xử lý tự động. Nhưng như mọi khi caveat emptoropens in a new tab, hãy tự nghiên cứu và khuyến khích người dùng của bạn làm tương tự.
Xem thêm công việc của tôi tại đâyopens in a new tab.
Lần cập nhật trang lần cuối: 25 tháng 2, 2026