Một số thủ thuật được sử dụng bởi các token lừa đảo và cách phát hiện chúng
Trong hướng dẫn này, chúng ta sẽ mổ xẻ một token lừa đảo (opens 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. Đến cuối 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 ta sẽ xem xét các sự kiện được phát ra bởi token lừa đảo đó và xem cách chúng ta có thể tự động xác định rằng nó không hợp pháp.
Token lừa đảo - chúng là gì, tại sao mọi người lại tạo ra chúng và cách phòng tránh
Một trong những ứng dụng phổ biến nhất của Ethereum là để một nhóm tạo ra một token có thể giao dịch, theo một nghĩa nào đó là tiền tệ của riêng họ. Tuy nhiên, ở bất cứ 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 kẻ tội phạm cố gắng đánh cắp giá trị đó cho riêng mình.
Bạn có thể đọc thêm về chủ đề này ở những 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 mổ xẻ một token lừa đảo để xem nó được thực hiện như thế nào và cách phát hiện ra nó.
Làm sao tôi biết wARB là lừa đảo?
Token mà chúng ta mổ xẻ là wARB (opens in a new tab), giả mạo là tương đương với token ARB (opens in a new tab) hợp pháp.
Cách dễ nhất để biết đâu là token hợp pháp là nhìn vào tổ chức phát hành, Arbitrum (opens 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 ta cho rằng những kẻ cố gắng lừa đảo người khác sẽ rất kín đáo, và thực tế là nhiều token lừa đảo không công khai mã nguồn của chúng (ví dụ: token này (opens in a new tab) và token này (opens 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 để tỏ ra hợp pháp, các tác giả của token lừa đảo đôi khi cũng làm như vậy. wARB (opens in a new tab) là một trong những token có sẵn mã nguồn, điều này giúp chúng ta dễ dàng hiểu nó hơn.
Mặc dù những người triển khai hợp đồng có thể chọn công bố mã nguồn hay không, nhưng họ không thể công bố sai mã nguồn. Trình khám phá 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 mã byte giống hệt nhau, nó sẽ từ chối mã nguồn đó. Bạn có thể đọc thêm về điều này trên trang web Etherscan (opens in a new tab).
So sánh với các token ERC-20 hợp pháp
Chúng ta 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 thuộc với cách các token ERC-20 hợp pháp thường được viết, hãy xem hướng dẫn này.
Các 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 đa chữ ký mới. Có một vài cách để thực hiện điều này.
Hợp đồng token HOP (opens in a new tab) sử dụng mẫu Ownable (opens 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).
abstract contract Ownable is Context {
address private _owner;
.
.
.
}
Hợp đồng token ARB (opens in a new tab) không có địa chỉ đặc quyền trực tiếp. Tuy nhiên, nó không cần một địa chỉ như vậy. Nó nằm sau một proxy (opens in a new tab) tại địa chỉ 0xb50721bcf8d664c30412cfbc6cf7a15145234ad1 (opens in a new tab). Hợp đồng đó có một địa chỉ đặc quyền (xem tệp thứ tư, ERC1967Upgrade.sol) được sử dụng để nâng cấp.
/**
* @dev Lưu trữ một địa chỉ mới trong slot admin EIP1967.
*/
function _setAdmin(address newAdmin) private {
require(newAdmin != address(0), "ERC1967: new admin is the zero address");
StorageSlot.getAddressSlot(_ADMIN_SLOT).value = newAdmin;
}
Ngược lại, hợp đồng wARB có một contract_owner được mã hóa cứng.
contract WrappedArbitrum is Context, IERC20 {
.
.
.
address deployer = 0xB50721BCf8d664c30412Cfbc6cf7a15145234ad1;
address public contract_owner = 0xb40dE7b1beE84Ff2dc22B70a049A07A13a411A33;
.
.
.
}
Chủ sở hữu hợp đồng này (opens 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 các thời điểm khác nhau, mà là một tài khoản thuộc sở hữu bên ngoài. Điều này có nghĩa là nó có thể được thiết kế để sử dụng ngắn hạn bởi một cá nhân, thay vì là một giải pháp dài hạn để kiểm soát một ERC-20 sẽ duy trì giá trị.
Và thực sự, nếu chúng ta nhìn vào Etherscan, chúng ta thấy rằng kẻ lừa đảo chỉ sử dụng hợp đồng này trong vỏn vẹn 12 giờ (từ giao dịch đầu tiên (opens in a new tab) đến giao dịch cuối cùng (opens in a new tab)) trong ngày 19 tháng 5 năm 2023.
Hàm _transfer giả mạo
Theo tiêu chuẩn, các giao dịch chuyển thực tế sẽ diễn 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:
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);
}
Phần đáng ngờ là:
if (sender == contract_owner){
sender = deployer;
}
emit Transfer(sender, recipient, amount);
Nếu chủ sở hữu hợp đồng chuyển token, tại sao sự kiện Transfer lại hiển thị 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ã mà chúng ta 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ử.
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;
}
Khi chúng ta xem xét 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ự
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);
}
Có hai dấu hiệu cảnh báo tiềm ẩn trong hàm này.
-
Việc sử dụng công cụ sửa đổi hàm (opens in a new tab)
_mod_. Tuy nhiên, khi chúng ta xem xét mã nguồn, chúng ta thấy rằng_mod_thực sự vô hại.modifier _mod_(address sender, address recipient, uint256 amount){ _; } -
Vấn đề tương tự mà chúng ta đã thấy trong
_transfer, đó là khicontract_ownerchuyển 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 thứ 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.
function dropNewTokens(address uPool,
address[] memory eReceiver,
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.
modifier auth() {
require(msg.sender == contract_owner, "Not allowed to interact");
_;
}
Hạn chế này hoàn toàn hợp lý, bởi vì chúng ta 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 lại rất đáng ngờ.
{
for (uint256 i = 0; i < eReceiver.length; i++) {
emit Transfer(uPool, eReceiver[i], eAmounts[i]);
}
}
Một hàm để chuyển từ một tài khoản chung đến một mảng người nhận với một mảng số lượng là hoàn toàn hợp lý. Có nhiều trường hợp sử dụng mà bạn sẽ muốn phân phối token từ một nguồn duy nhất đến nhiều đích, chẳng hạn như trả lương, airdrop, v.v. Việc thực hiện trong một giao dịch duy nhất sẽ rẻ hơn (về Gas) 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 Transfer (opens in a new tab), nhưng không thực sự 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 thông báo cho chúng về một giao dịch chuyển không thực sự xảy ra.
Hàm đốt Approve
Các hợp đồng ERC-20 được cho là có một hàm approve cho các hạn mức, và thực sự token lừa đảo của chúng ta có một hàm như vậy, và nó thậm chí còn chính xác. Tuy nhiên, vì Solidity bắt nguồn 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.
function Approve(
address[] memory holders)
Hàm này được gọi với một mảng các địa chỉ của những người nắm giữ token.
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).
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);
}
}
Đố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, thực chất là đốt nó (hàm burn thực tế trong tiêu chuẩn cũng thay đổi tổng nguồn cung và chuyển các 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. Đó dường như 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à lừa đảo, nhưng chúng làm cho 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ư thế này.
Hàm mount
Mặc dù nó không được chỉ định trong tiêu chuẩn (opens in a new tab), nhưng nói chung hàm tạo ra các token mới được gọi là mint.
Nếu chúng ta nhìn vào hàm khởi tạo wARB, chúng ta thấy hàm đúc thời gian đã đượ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ả.
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);
}
Bản thân hàm mount cũng rất đáng ngờ.
function mount(address account, uint256 amount) public {
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 đúc. Điều đó là hợp pháp. Nhưng thông báo lỗi nên là chỉ chủ sở hữu mới được phép đúc hoặc một cái gì đó tương tự. Thay vào đó, nó lại là thông báo không liên quan ERC20: mint to the zero address. Bài kiểm tra chính xác cho việc đúc đến địa chỉ zero là require(account != address(0), "<error message>"), mà hợp đồng không bao giờ bận tâm kiểm tra.
_totalSupply = _totalSupply.add(amount);
_balances[contract_owner] = _balances[contract_owner].add(amount);
emit Transfer(address(0), account, amount);
}
Có thêm hai sự thật đáng ngờ, liên quan trực tiếp đến việc đúc:
-
Có một tham số
account, có lẽ là tài khoản sẽ nhận được số lượng được đúc. Nhưng số dư tăng lên thực sự lại là củacontract_owner. -
Mặc dù số dư tăng lên thuộc về
contract_owner, sự kiện được phát ra lại hiển thị một giao dịch chuyển đếnaccount.
Tại sao lại có cả auth và approver? Tại sao lại có mod mà không làm gì cả?
Hợp đồng này chứa ba công cụ sửa đổi: _mod_, auth và approver.
modifier _mod_(address sender, address recipient, uint256 amount){
_;
}
_mod_ nhận ba tham số và không làm gì với chúng. Tại sao lại có nó?
modifier auth() {
require(msg.sender == contract_owner, "Not allowed to interact");
_;
}
modifier approver() {
require(msg.sender == contract_owner, "Not allowed to interact");
_;
}
auth và approver có ý nghĩa hơn, bởi vì chúng kiểm tra xem hợp đồng có được gọi bởi contract_owner hay không. Chúng ta mong đợi một số hành động đặc quyền nhất định, chẳng hạn như việc đúc, sẽ bị giới hạn ở tài khoản đó. Tuy nhiên, mục đích của việc có hai hàm riêng biệt làm chính xác cùng một việc là gì?
Chúng ta có thể tự động phát hiện điều gì?
Chúng ta có thể thấy rằng wARB là một token lừa đảo bằng cách nhìn vào Etherscan. Tuy nhiên, đó là một giải pháp tập trung. Về lý thuyết, Etherscan có thể bị phá hoại hoặc bị hack. Tốt hơn là có thể tự độc lập tìm ra liệu một token có hợp pháp hay không.
Có một số thủ thuật mà chúng ta có thể sử dụng để xác định rằng một token ERC-20 là đáng ngờ (có thể là lừa đảo hoặc được viết rất tệ), bằng cách xem xét các sự kiện mà chúng phát ra.
Các sự kiện Approval đáng ngờ
Các sự kiện Approval (opens in a new tab) chỉ nên xảy ra với một yêu cầu trực tiếp (ngược lại với các sự kiện Transfer (opens in a new tab) có thể xảy ra do một hạn mức). Xem tài liệu Solidity (opens in a new tab) để biết giải thích chi tiết về vấn đề này và lý do tại sao các yêu cầu cần phải trực tiếp, thay vì được trung gian bởi một hợp đồng.
Điều này có nghĩa là các sự kiện Approval chấp thuận việc chi tiêu từ một tài khoản thuộc sở hữu bên ngoài 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 chấp thuận nào khác từ một tài khoản thuộc sở hữu bên ngoài đều đáng ngờ.
Dưới đây là một chương trình xác định loại sự kiện này (opens in a new tab), sử dụng Viem (opens in a new tab) và TypeScript (opens in a new tab), một biến thể JavaScript có tính an toàn kiểu. Để chạy nó:
- Sao chép
.env.examplethành.env. - Chỉnh sửa
.envđể cung cấp URL đến 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 chấp thuận đáng ngờ.
Dưới đây là giải thích từng dòng:
import {
Address,
TransactionReceipt,
createPublicClient,
http,
parseAbiItem,
} from "viem"
import { mainnet } from "viem/chains"
Nhập các định nghĩa kiểu, các hàm và định nghĩa chuỗi từ viem.
import { config } from "dotenv"
config()
Đọc .env để lấy URL.
const client = createPublicClient({
chain: mainnet,
transport: http(process.env.URL),
})
Tạo một máy khách Viem. Chúng ta chỉ cần đọc từ chuỗi khối, vì vậy máy khách này không cần khóa riêng tư.
const testedAddress = "0xb047c8032b99841713b8e3872f06cf32beb27b82"
const fromBlock = 16859812n
const toBlock = 16873372n
Địa chỉ của hợp đồng ERC-20 đáng ngờ và các khối mà trong đó chúng ta sẽ tìm kiếm các 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 ta vì băng thông có thể trở nên đắt đỏ. May mắn thay, 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).
const approvalEvents = await client.getLogs({
address: testedAddress,
fromBlock,
toBlock,
event: parseAbiItem(
"event Approval(address indexed _owner, address indexed _spender, uint256 _value)"
),
})
Đây là cách để yêu cầu Viem cung cấp thông tin sự kiện. Khi chúng ta 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 ta.
const isContract = async (addr: Address): boolean =>
await client.getBytecode({ address: addr })
Thuật toán của chúng ta chỉ áp dụng cho các tài khoản thuộc sở hữu bên ngoài. Nếu có bất kỳ mã byte 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 chỉ 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 kỳ lạ. Chúng ta không chỉ nói với nó rằng tham số đầu tiên (và duy nhất) được gọi là addr, mà còn cho biết nó thuộc kiểu Address. Tương tự, phần : boolean cho TypeScript biết rằng giá trị trả về của hàm là một boolean.
const getEventTxn = async (ev: Event): TransactionReceipt =>
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 ta cần biên lai để đảm bảo chúng ta biết đích đến của giao dịch là gì.
const suspiciousApprovalEvent = async (ev : Event) : (Event | null) => {
Đây là hàm quan trọng nhất, hàm thực sự quyết định xem 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 ta trả về null nếu sự kiện không đáng ngờ.
const owner = ev.args._owner
Viem có các tên trường, vì vậy nó đã phân tích cú pháp sự kiện cho chúng ta. _owner là chủ sở hữu của các token sẽ được chi tiêu.
// Sự chấp thuận bởi các hợp đồng là không đáng ngờ
if (await isContract(owner)) return null
Nếu chủ sở hữu là một hợp đồng, hãy giả định 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 theo dõi toàn bộ quá trình thực thi của giao dịch để xem liệu nó có bao giờ đến được hợp đồng 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. Việc đó tốn nhiều tài nguyên hơn nhiều so với những gì chúng ta muốn làm.
const txn = await getEventTxn(ev)
Nếu sự chấp thuận đến từ một tài khoản thuộc sở hữu bên ngoài, hãy lấy giao dịch đã gây ra nó.
// Sự chấp thuận 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ịch
if (owner.toLowerCase() != txn.from.toLowerCase()) return ev
Chúng ta không thể chỉ kiểm tra sự bằng nhau của chuỗi vì các địa chỉ là hệ thập lục phân, do đó chúng chứa các chữ cái. Đôi khi, ví dụ như trong txn.from, những 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 để nhận dạng lỗi (opens 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 đó thuộc sở hữu bên ngoài, thì chúng ta có một giao dịch đáng ngờ.
// 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 đang
// điều tra
if (txn.to.toLowerCase() != testedAddress) return ev
Tươ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ó rất đáng ngờ.
// Nếu không có lý do gì để nghi ngờ, trả về null.
return null
}
Nếu không có điều kiện nào đúng thì sự kiện Approval không đáng ngờ.
const testPromises = approvalEvents.map((ev) => suspiciousApprovalEvent(ev))
const testResults = (await Promise.all(testPromises)).filter((x) => x != null)
console.log(testResults)
Một hàm async (opens 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 đợi Promise đó được hoàn thành trước khi tiếp tục xử lý. Điều này rất đơn giản để lập trình và theo dõi, 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 hoàn thành, chúng ta đã có thể bắt tay vào làm việc với sự kiện tiếp theo.
Ở đây chúng ta sử dụng map (opens in a new tab) để tạo một mảng các đối tượng Promise. Sau đó, chúng ta sử dụng Promise.all (opens in a new tab) để đợi tất cả các promise đó được giải quyết. Sau đó, chúng ta filter (opens 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ác có thể để xác định các token lừa đảo là xem liệu chúng có bất kỳ giao dịch chuyển nào đáng ngờ hay không. Ví dụ: các giao dịch chuyển từ các tài khoản không có nhiều token như vậy. Bạn có thể xem cách triển khai bài kiểm tra này (opens in a new tab), nhưng wARB không gặp vấn đề này.
Kết luận
Việc tự động phát hiện các trò lừa đảo ERC-20 gặp phải â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à chỉ là không đại diện cho bất cứ điều gì thực tế. 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.
Việc tự động phát hiện 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 của tài chính phi tập trung (DeFi), nơi có nhiều token và chúng cần được xử lý tự động. Nhưng như thường lệ, người mua hãy cẩn thận (opens 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 điều tương tự.
Xem tại đây để biết thêm về công việc của tôi (opens in a new tab).