Přeskočit na hlavní obsah

Některé triky používané podvodnými tokeny a jak je odhalit

podvod
solidity
erc-20
javascript
typescript
Středně pokročilý
Ori Pomerantz
15. září 2023
13 minuta čtení

V tomto tutoriálu rozebereme podvodný tokenopens in a new tab, abychom se podívali na některé triky, které podvodníci používají a jak je implementují. Na konci tohoto tutoriálu získáte komplexnější pohled na smlouvy o tokenech ERC-20, jejich možnosti a důvody, proč je nutná skepse. Poté se podíváme na události emitované tímto podvodným tokenem a zjistíme, jak můžeme automaticky identifikovat, že není legitimní.

Podvodné tokeny – co jsou, proč je lidé vytvářejí a jak se jim vyhnout

Jedním z nejčastějších způsobů využití Etherea je vytvoření obchodovatelného tokenu pro skupinu, v podstatě jejich vlastní měny. Kdekoliv, kde existují legitimní případy použití, které přinášejí hodnotu, se také objevují i zločinci, kteří se snaží tuto hodnotu ukrást pro sebe.

Více si o tomto tématu můžete přečíst jinde na ethereum.org z pohledu uživatele. Tento tutoriál se zaměřuje na rozebrání podvodného tokenu, abychom zjistili, jak je vytvořen a jak jej lze odhalit.

Jak poznám, že wARB je podvod?

Token, který rozebíráme, je wARBopens in a new tab, který se tváří jako ekvivalent legitimního tokenu ARBopens in a new tab.

Nejjednodušší způsob, jak zjistit, který token je legitimní, je podívat se na původní organizaci, Arbitrumopens in a new tab. Legitimní adresy jsou uvedeny v jejich dokumentaciopens in a new tab.

Proč je zdrojový kód dostupný?

Obvykle bychom očekávali, že lidé, kteří se snaží ostatní podvést, budou tajnůstkářští, a skutečně mnoho podvodných tokenů nemá svůj kód k dispozici (například tentoopens in a new tab a tentoopens in a new tab).

Legitimní tokeny však obvykle svůj zdrojový kód zveřejňují, takže aby působili legitimně, autoři podvodných tokenů někdy dělají totéž. wARBopens in a new tab je jedním z těch tokenů s dostupným zdrojovým kódem, což usnadňuje jeho pochopení.

I když si tvůrci smluv mohou vybrat, zda zdrojový kód zveřejní, či nikoli, nemohou zveřejnit nesprávný zdrojový kód. Průzkumník bloků nezávisle zkompiluje poskytnutý zdrojový kód, a pokud neobdrží přesně stejný bajtkód, tento zdrojový kód odmítne. Více si o tom můžete přečíst na stránkách Etherscanuopens in a new tab.

Srovnání s legitimními tokeny ERC-20

Tento token porovnáme s legitimními tokeny ERC-20. Pokud nevíte, jak se obvykle píší legitimní tokeny ERC-20, podívejte se na tento tutoriál.

Konstanty pro privilegované adresy

Smlouvy někdy potřebují privilegované adresy. Smlouvy, které jsou navrženy pro dlouhodobé použití, umožňují některé privilegované adrese změnit tyto adresy, například aby bylo možné použít novou smlouvu multisig. Existuje několik způsobů, jak to udělat.

Smlouva tokenu HOPopens in a new tab používá vzor Ownableopens in a new tab. Privilegovaná adresa je uložena v úložišti, v poli nazvaném _owner (viz třetí soubor, Ownable.sol).

1abstract contract Ownable is Context {
2 address private _owner;
3 .
4 .
5 .
6}

Smlouva o tokenu ARBopens in a new tab nemá přímo privilegovanou adresu. Nepotřebuje ji však. Nachází se za proxyopens in a new tab na adrese 0xb50721bcf8d664c30412cfbc6cf7a15145234ad1opens in a new tab. Tato smlouva má privilegovanou adresu (viz čtvrtý soubor, ERC1967Upgrade.sol), kterou lze použít pro upgrady.

1 /**
2 * @dev Uloží novou adresu do slotu správce 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 }

Naproti tomu smlouva wARB má napevno zakódovaného contract_owner.

1contract WrappedArbitrum is Context, IERC20 {
2 .
3 .
4 .
5 address deployer = 0xB50721BCf8d664c30412Cfbc6cf7a15145234ad1;
6 address public contract_owner = 0xb40dE7b1beE84Ff2dc22B70a049A07A13a411A33;
7 .
8 .
9 .
10}
Zobrazit vše

Tento vlastník smlouvyopens in a new tab není smlouva, kterou by mohly v různých časech ovládat různé účty, ale externě vlastněný účet. To znamená, že je pravděpodobně navržen pro krátkodobé použití jednotlivcem, spíše než jako dlouhodobé řešení pro ovládání ERC-20, které si udrží hodnotu.

A skutečně, když se podíváme na Etherscan, vidíme, že podvodník tuto smlouvu používal pouze 12 hodin (první transakceopens in a new tab po poslední transakciopens in a new tab) dne 19. května 2023.

Falešná funkce _transfer

Je standardní, že skutečné převody probíhají pomocí interní funkce _transfer.

Ve wARB tato funkce vypadá téměř legitimně:

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: částka převodu překračuje zůstatek");
8 _balances[recipient] = _balances[recipient].add(amount);
9 if (sender == contract_owner){
10 sender = deployer;
11 }
12 emit Transfer(sender, recipient, amount);
13 }
Zobrazit vše

Podezřelá část je:

1 if (sender == contract_owner){
2 sender = deployer;
3 }
4 emit Transfer(sender, recipient, amount);

Pokud vlastník smlouvy posílá tokeny, proč událost Transfer ukazuje, že pocházejí od deployer?

Je zde však důležitější problém. Kdo volá tuto funkci _transfer? Nelze ji volat zvenčí, je označena jako internal. A kód, který máme, neobsahuje žádná volání _transfer. Je zřejmé, že je zde jako návnada.

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: částka převodu překračuje povolenou částku"));
9 return true;
10 }
Zobrazit vše

Když se podíváme na funkce, které se volají pro převod tokenů, transfer a transferFrom, vidíme, že volají úplně jinou funkci, _f_.

Skutečná funkce _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: částka převodu překračuje zůstatek");
8 _balances[recipient] = _balances[recipient].add(amount);
9 if (sender == contract_owner){
10
11 sender = deployer;
12 }
13 emit Transfer(sender, recipient, amount);
14 }
Zobrazit vše

V této funkci jsou dva potenciální varovné signály.

  • Použití modifikátoru funkceopens in a new tab _mod_. Když se však podíváme do zdrojového kódu, vidíme, že _mod_ je ve skutečnosti neškodný.

    1modifier _mod_(address sender, address recipient, uint256 amount){
    2 _;
    3}
  • Stejný problém, který jsme viděli v _transfer, což je, když contract_owner posílá tokeny, které se zdají pocházet od deployer.

Falešná funkce událostí dropNewTokens

Nyní se dostáváme k něčemu, co vypadá jako skutečný podvod. Funkci jsem pro lepší čitelnost trochu upravil, ale je funkčně ekvivalentní.

1function dropNewTokens(address uPool,
2 address[] memory eReceiver,
3 uint256[] memory eAmounts) public auth()

Tato funkce má modifikátor auth(), což znamená, že ji může volat pouze vlastník smlouvy.

1modifier auth() {
2 require(msg.sender == contract_owner, "Not allowed to interact");
3 _;
4}

Toto omezení dává dokonalý smysl, protože bychom nechtěli, aby náhodné účty distribuovaly tokeny. Zbytek funkce je však podezřelý.

1{
2 for (uint256 i = 0; i < eReceiver.length; i++) {
3 emit Transfer(uPool, eReceiver[i], eAmounts[i]);
4 }
5}

Funkce pro převod z účtu fondu na pole příjemců pole částek dává dokonalý smysl. Existuje mnoho případů použití, kdy budete chtít distribuovat tokeny z jednoho zdroje do více cílů, jako jsou výplaty, airdropy atd. Je levnější (z hlediska paliva) provést to v jedné transakci namísto vydávání více transakcí, nebo dokonce volat ERC-20 vícekrát z jiné smlouvy jako součást stejné transakce.

dropNewTokens to však nedělá. Emituje události Transferopens in a new tab, ale ve skutečnosti žádné tokeny nepřevádí. Neexistuje žádný legitimní důvod k matení offchainových aplikací tím, že jim řeknete o převodu, který se ve skutečnosti nestal.

Pálící funkce Approve

Smlouvy ERC-20 mají mít funkci approve pro povolené částky, a náš podvodný token skutečně takovou funkci má, a je dokonce správná. Protože však Solidity vychází z jazyka C, rozlišuje velikost písmen. "Approve" a "approve" jsou různé řetězce.

Funkčnost také nesouvisí s approve.

1 function Approve(
2 address[] memory holders)

Tato funkce je volána s polem adres držitelů tokenu.

1 public approver() {

Modifikátor approver() zajišťuje, že tuto funkci může volat pouze contract_owner (viz níže).

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: pálená částka překračuje zůstatek");
6 _balances[0x0000000000000000000000000000000000000001] =
7 _balances[0x0000000000000000000000000000000000000001].add(amount);
8 }
9 }
10
Zobrazit vše

Pro každou adresu držitele funkce přesune celý zůstatek držitele na adresu 0x00...01 a tím ho efektivně spálí (skutečné pálení ve standardu také mění celkovou zásobu a převádí tokeny na 0x00...00). To znamená, že contract_owner může odebrat majetek jakéhokoli uživatele. To se nezdá jako funkce, kterou byste chtěli ve správcovském tokenu.

Problémy s kvalitou kódu

Tyto problémy s kvalitou kódu nedokazují, že tento kód je podvod, ale způsobují, že působí podezřele. Organizované společnosti jako Arbitrum obvykle takto špatný kód nevydávají.

Funkce mount

Ačkoli to není uvedeno ve standarduopens in a new tab, obecně se funkce, která vytváří nové tokeny, nazývá mint.

Když se podíváme do konstruktoru wARB, vidíme, že funkce mint byla z nějakého důvodu přejmenována na mount a je volána pětkrát s pětinou počáteční zásoby, místo jednou pro celou částku z důvodu efektivity.

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 }
Zobrazit vše

Samotná funkce mount je také podezřelá.

1 function mount(address account, uint256 amount) public {
2 require(msg.sender == contract_owner, "ERC20: mint to the zero address");

Při pohledu na require vidíme, že pouze vlastník smlouvy má povoleno razit. To je legitimní. Ale chybová zpráva by měla být pouze vlastník má povoleno razit nebo něco podobného. Místo toho je to irelevantní ERC20: ražba na nulovou adresu. Správný test pro ražbu na nulovou adresu je require(account != address(0), "<chybová zpráva>"), což se smlouva nikdy neobtěžuje zkontrolovat.

1 _totalSupply = _totalSupply.add(amount);
2 _balances[contract_owner] = _balances[contract_owner].add(amount);
3 emit Transfer(address(0), account, amount);
4 }

Existují dvě další podezřelé skutečnosti, přímo související s ražbou:

  • Existuje parametr account, což je pravděpodobně účet, který by měl obdržet vyraženou částku. Ale zůstatek, který se zvyšuje, je ve skutečnosti contract_ownera.

  • Zatímco zvýšený zůstatek patří contract_ownerovi, emitovaná událost ukazuje převod na account.

Proč auth i approver? Proč mod, který nic nedělá?

Tato smlouva obsahuje tři modifikátory: _mod_, auth a approver.

1 modifier _mod_(address sender, address recipient, uint256 amount){
2 _;
3 }

_mod_ přijímá tři parametry a nic s nimi nedělá. Proč ho mít?

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 }
Zobrazit vše

auth a approver dávají větší smysl, protože kontrolují, že smlouva byla volána contract_owner. Očekávali bychom, že určité privilegované akce, jako je ražba, budou omezeny na tento účet. Jaký je však smysl mít dvě samostatné funkce, které dělají přesně to samé?

Co můžeme detekovat automaticky?

Při pohledu na Etherscan vidíme, že wARB je podvodný token. Jedná se však o centralizované řešení. Teoreticky by mohl být Etherscan podvržen nebo napaden. Je lepší být schopen nezávisle zjistit, zda je token legitimní, nebo ne.

Existují některé triky, které můžeme použít k identifikaci, že token ERC-20 je podezřelý (buď podvod, nebo velmi špatně napsaný), a to pohledem na události, které emitují.

Podezřelé události Approval

Události Approvalopens in a new tab by se měly dít pouze na přímou žádost (na rozdíl od událostí Transferopens in a new tab, které se mohou stát v důsledku povolené částky). Viz dokumentaci Solidityopens in a new tab pro podrobné vysvětlení tohoto problému a proč musí být žádosti přímé, nikoli zprostředkované smlouvou.

To znamená, že události Approval, které schvalují útratu z externě vlastněného účtu, musí pocházet z transakcí, které vznikly na tomto účtu a jejichž cílem je smlouva ERC-20. Jakýkoli jiný druh schválení z externě vlastněného účtu je podezřelý.

Zde je program, který identifikuje tento druh událostiopens in a new tab, pomocí viemopens in a new tab a TypeScriptopens in a new tab, varianty JavaScriptu s typovou bezpečností. Spustíte ho takto:

  1. Zkopírujte .env.example do .env.
  2. Upravte .env a zadejte URL k uzlu hlavní sítě Ethereum.
  3. Spusťte pnpm install pro instalaci potřebných balíčků.
  4. Spusťte pnpm susApproval pro vyhledání podezřelých schválení.

Zde je vysvětlení řádek po řádku:

1import {
2 Address,
3 TransactionReceipt,
4 createPublicClient,
5 http,
6 parseAbiItem,
7} from "viem"
8import { mainnet } from "viem/chains"

Importujte definice typů, funkce a definici řetězce z viem.

1import { config } from "dotenv"
2config()

Přečtěte si .env pro získání URL.

1const client = createPublicClient({
2 chain: mainnet,
3 transport: http(process.env.URL),
4})

Vytvořte klienta Viem. Potřebujeme pouze číst z blockchainu, takže tento klient nepotřebuje soukromý klíč.

1const testedAddress = "0xb047c8032b99841713b8e3872f06cf32beb27b82"
2const fromBlock = 16859812n
3const toBlock = 16873372n

Adresa podezřelé smlouvy ERC-20 a bloky, ve kterých budeme hledat události. Poskytovatelé uzlů obvykle omezují naši schopnost číst události, protože šířka pásma může být drahá. Naštěstí wARB nebyl používán po dobu osmnácti hodin, takže se můžeme podívat na všechny události (bylo jich celkem jen 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})

Tímto způsobem požádáte Viem o informace o události. Když mu poskytneme přesný podpis události, včetně názvů polí, událost pro nás analyzuje.

1const isContract = async (addr: Address): boolean =>
2 await client.getBytecode({ address: addr })

Náš algoritmus je použitelný pouze na externě vlastněné účty. Pokud client.getBytecode vrátí jakýkoli bajtkód, znamená to, že se jedná o smlouvu a měli bychom ji přeskočit.

Pokud jste TypeScript ještě nepoužívali, definice funkce může vypadat trochu divně. Neříkáme mu jen, že se první (a jediný) parametr jmenuje addr, ale také, že je typu Address. Podobně část : boolean říká TypeScriptu, že návratová hodnota funkce je boolean.

1const getEventTxn = async (ev: Event): TransactionReceipt =>
2 await client.getTransactionReceipt({ hash: ev.transactionHash })

Tato funkce získá potvrzení o transakci z události. Potvrzení potřebujeme, abychom se ujistili, že známe cíl transakce.

1const suspiciousApprovalEvent = async (ev : Event) : (Event | null) => {

Toto je nejdůležitější funkce, která skutečně rozhoduje, zda je událost podezřelá, nebo ne. Návratový typ, (Event | null), říká TypeScriptu, že tato funkce může vrátit buď Event, nebo null. Vrátíme null, pokud událost není podezřelá.

1const owner = ev.args._owner

Viem má názvy polí, takže pro nás analyzoval událost. _owner je vlastníkem tokenů, které mají být utraceny.

1// Schválení smlouvami nejsou podezřelá
2if (await isContract(owner)) return null

Pokud je vlastníkem smlouva, předpokládejme, že toto schválení není podezřelé. Abychom zkontrolovali, zda je schválení smlouvy podezřelé, nebo ne, budeme muset sledovat celé provedení transakce, abychom zjistili, zda se někdy dostala k vlastnické smlouvě a zda tato smlouva volala smlouvu ERC-20 přímo. To je mnohem náročnější na zdroje, než bychom chtěli dělat.

1const txn = await getEventTxn(ev)

Pokud schválení pochází z externě vlastněného účtu, získejte transakci, která ho způsobila.

1// Schválení je podezřelé, pokud pochází od vlastníka EOA, který není `from` transakce
2if (owner.toLowerCase() != txn.from.toLowerCase()) return ev

Nemůžeme jen zkontrolovat rovnost řetězců, protože adresy jsou hexadecimální, takže obsahují písmena. Někdy, například v txn.from, jsou všechna tato písmena malá. V jiných případech, jako je ev.args._owner, je adresa v různých velikostech písmen pro identifikaci chybyopens in a new tab.

Ale pokud transakce nepochází od vlastníka a tento vlastník je externě vlastněn, pak máme podezřelou transakci.

1// Je také podezřelé, pokud cíl transakce není smlouva ERC-20, kterou
2// zkoumáme
3if (txn.to.toLowerCase() != testedAddress) return ev

Podobně, pokud adresa to transakce, první volaná smlouva, není zkoumaná smlouva ERC-20, pak je to podezřelé.

1 // Pokud není důvod k podezření, vrátíme null.
2 return null
3}

Pokud žádná z podmínek není pravdivá, pak událost Approval není podezřelá.

1const testPromises = approvalEvents.map((ev) => suspiciousApprovalEvent(ev))
2const testResults = (await Promise.all(testPromises)).filter((x) => x != null)
3
4console.log(testResults)

Funkce asyncopens in a new tab vrací objekt Promise. S běžnou syntaxí await x() čekáme, až se tento Promise splní, než budeme pokračovat ve zpracování. To se jednoduše programuje a sleduje, ale je to také neefektivní. Zatímco čekáme na splnění Promise pro konkrétní událost, můžeme již začít pracovat na další události.

Zde používáme mapopens in a new tab k vytvoření pole objektů Promise. Poté použijeme Promise.allopens in a new tab k čekání, až se všechny tyto sliby vyřeší. Poté tyto výsledky filtrujemeopens in a new tab, abychom odstranili nepodezřelé události.

Podezřelé události Transfer

Dalším možným způsobem, jak identifikovat podvodné tokeny, je zjistit, zda mají nějaké podezřelé převody. Například převody z účtů, které nemají tolik tokenů. Můžete se podívat, jak implementovat tento testopens in a new tab, ale wARB tento problém nemá.

Závěr

Automatická detekce podvodů ERC-20 trpí falešně negativními výsledkyopens in a new tab, protože podvod může použít naprosto normální smlouvu o tokenu ERC-20, která jen nepředstavuje nic skutečného. Proto byste se měli vždy pokusit získat adresu tokenu z důvěryhodného zdroje.

Automatická detekce může pomoci v určitých případech, jako jsou součásti DeFi, kde je mnoho tokenů a je třeba s nimi zacházet automaticky. Ale jako vždy caveat emptoropens in a new tab, proveďte si vlastní průzkum a povzbuďte své uživatele, aby dělali totéž.

Více z mé práce najdete zdeopens in a new tab.

Stránka naposledy aktualizována: 25. února 2026

Byl tento tutoriál užitečný?