The Jerusalem PostStudent opens fire outside school in Turkey, eight pupils wounded, NTV reportsDaily MaverickStudent opens fire outside school in Turkey, eight pupils wounded, NTV reportsוואלהבמהלך חג הסוכות תוגבל כניסה למטיילים במשטחי אימונים בדרוםBollywood HungamaEXCLUSIVE: Ajay Devgn-Rohit Jugraj’s horror thriller titled SuryasparshInquirer EntertainmentJopay Paguia ‘respects’ Rochelle Pangilinan, but stands firm on her work ethicInquirerAgusan solon pushes national soil strategy through SUCsХабрКак в игровых студиях принимаются технические решения, когда на кону денюжкиCollider‘Marvel’s Wolverine’ Officially Changes Controversial Gameplay Feature After Fan BacklashSouth China Morning PostCan China’s grain reserves protect food security against El Nino?The South AfricanAircraft crash reported near Morningstar Airfield on the N7RTL BoulevardRekenkamer: Van Weel zette Kamer op verkeerde been over 10.000 onbehandelde aangiftenGhaflaBoundaries And Public Office: Omanga’s School Attire Trend Sparks Debate Online
The Daily Newsstand · Free, Always
Tuesday, September 22, 2026

Блокчейн цел, деньги пропали: атаки на оракулы и безопасное чтение цены в Solidity

Translate

В начале сентября протокол Tectonic на Cronos потерял около 75 миллионов долларов. Блокчейн при этом работал штатно — консенсус не сломался, приватные ключи не утекли, в коде контракта не было переполнения. Одним словом — тишь да гладь. Атакующий просто сделал так, что контракт поверил в неправильную цену и после этого честно, как будто так и надо, по всем правилам, отдал ему чужие деньги. Это не единичный случай, а тренд года. По данным TRM Labs, за 2026-й на манипуляциях ценой протоколы поймали 32 атаки против 12 за весь 2025-й, а это, на минуточку, почти втрое больше, и год даже ещё не закончился. Доля таких атак среди всех крипто-взломов выросла примерно с одной семнадцатой в 2022-м до одной восьмой сейчас. В списке пострадавших за последние недели — ($8.7млн), More Markets на Flow EVM ($410 тыс.), Cozy Finance на Optimism ($170 тыс.), Full Sail на Sui ($91 тыс.).

Почему у контракта вообще есть слабое место

Смарт-контракт живёт в детерминированной клетке. У него нет fetch(), нет часов, нет источника случайности. Так задумано, иначе ноды не смогли бы ни о чём договориться, исполняя один и тот же код. У кого-то миллисекунды не совпали, кто-то сфетчил данные с сайта центробанка и они разошлись и т.д. и т.п. Это фундаментальное свойство, а не косяк создателей. Но каждый контракт кредитного протокола хочет знать, сколько стоит залог в каждый конкретный промежуток времени, а курс живёт снаружи, в старом мире, в web2. Так вот, этот мостик между мирами и называется Оракул: кто-то приносит данные внутрь обычной транзакцией, и с этого момента они детерминированы для всех. И вот здесь спрятан парадокс, ради которого всё затевалось. Мы построили систему, где не нужно доверять никому, и поставили на входе компонент, которому приходится доверять. Собственно, это и есть «проблема оракула». Вся безопасность протокола на миллионы долларов упирается в один-единственный вопрос. Откуда взялось это число, этот курс, эти данные, и можно ли на них повлиять.

Атака первая: спот-цена из пула

Самый частый способ узнать цену «ончейн, без посредников» — заглянуть в пул ликвидности DEX. Выглядит красиво: данные из самого блокчейна, никаких внешних сервисов.

// ТАК ДЕЛАТЬ НЕЛЬЗЯ
function getPrice() public view returns (uint256) {
    (uint112 reserve0, uint112 reserve1, ) = pair.getReserves();
    return (uint256(reserve1) * 1e18) / uint256(reserve0);
}

Три простых строки, читается как справочник, ничего лишнего, но проблема в том, что это не «цена токена» — это соотношение резервов в конкретном пуле прямо сейчас, а соотношение резервов меняет любой, у кого есть деньги на своп. Дальше вступает механизм, которого нет в web2, так называемый флеш-займ. Можно занять миллионы без залога, но только при одном условии, вернуть нужно в той же транзакции, не раньше и не позже, одна транзакция на получение и возврат средств. Если не вернул, транзакция откатывается целиком, как будто ничего и не было. В этой ситуации кредитор ничем не рискует, поэтому занимать может кто угодно. Теперь складываем 2+2:

  1. Занимаем флеш-займом крупную сумму

  2. Выкупаем один из токенов пула — соотношение резервов перекашивается, «цена» взлетает

  3. В том же блоке дёргаем протокол-жертву, который читает getReserves() и видит раздутую цену

  4. Закладываем подорожавший токен и берём под него настоящий кредит

  5. Возвращаем флеш-займ, уходим с разницей

Вот и всё, одна транзакция — одна атака, миллионы в кармане. У арбитражников нет ни одного блока, чтобы вернуть цену на место, потому что манипуляция и её эксплуатация происходят атомарно. Поэтому в индустрии есть жёсткое правило, что протокол, который берёт спот-цену из пула ликвидности, будет взломан. Не «может быть», а будет.

Разбор: Mango Markets, $110 миллионов за десять минут

Урок истории из октября 2022-го. Аврахам Айзенберг зашёл на Mango Markets с 10 миллионами долларов, которые предусмотрительно разложил на два аккаунта. На одном аккаунте он открыл шорт на 488 миллионов MNGO, на другом просто их скупал. Его задача была не заработать на сделке, а нарисовать цену. MNGO — низколиквидный токен, и объёма хватило, чтобы поднять его цену примерно на 2000% относительно десятидневной средней. Протокол считал стоимость залога по этой нарисованной цене, увидел у аккаунта гигантское обеспечение, недолго думая, выдал под него займов на сумму, которая выкосила протокол. Итог: свыше 110 миллионов долларов. Дальше было самое интересное. Айзенберг публично назвал это «прибыльной торговой стратегией» и предложил вернуть часть, оставив себе десятки миллионов как «баг-баунти». Его аргумент был прост, как лапоть: я не взламывал код, я совершал сделки, разрешённые правилами. CFTC, SEC и Минюст США с такой трактовкой не согласились, и он был признан виновным в мошенничестве и манипулировании рынком. Техническая мораль такова: код отработал ровно так, как написан. Ни одной строчки не было нарушено, потому что уязвимость была не в логике займов, а в том, что источником истины о цене назначили то, на что можно влиять деньгами.

TWAP: помогает, но не спасает

Первый рефлекс, который сразу же срабатывает у среднестатистического технаря — усреднить. Time-Weighted Average Price берёт цену не «сейчас», а среднюю за определённый промежуток времени, например полчаса или час. Тогда однотранзакционный флеш-займ перестаёт работать, потому что для сдвига средней за 30 минут нужно держать перекошенную цену минимум эти 30 минут, отбиваясь от арбитражников и всех тех, кто увидел возможность заработать. Это уже не бесплатно, а дорого и рискованно. Но TWAP выставляет сразу два чека за эту защиту. Первый чек: он по определению отстающий, потому что на реальном обвале рынка TWAP ещё несколько минут показывает вчерашний мир, и протокол в это время принимает решения по устаревшим данным, а это, на минуточку, не сработавшие ликвидации или сработавшие, но не на тех и не там. Второй чек: TWAP может защитить только от дешёвой атаки, а вот дорогая атака ему не по зубам. Если протокол держит сотни миллионов, а атака на низколиквидный пул стоит несколько миллионов, тогда экономика всё ещё на стороне атакующего. Именно поэтому в списке жертв 2026 года так много протоколов на молодых сетях. В них просто тонкая ликвидность, и сдвинуть цену дёшево.

Правильный путь — и вторая ловушка

Промышленный ответ — децентрализованная сеть оракулов. У Chainlink цену приносит не одна нода, а десятки независимых, и каждая берёт данные с нескольких бирж, результат агрегируется и публикуется, когда цена отклонилась больше заданного порога (deviation threshold) или истёк срок (heartbeat). Чтобы это подделать, нужно скомпрометировать большинство нод, а это намного дороже, чем украсть. Читается это так:

(, int256 answer, , , ) = feed.latestRoundData();
uint256 price = uint256(answer);

И вот здесь девять из десяти туториалов заканчиваются — а зря. Потому что так писать тоже нельзя. 13 мая 2022 года цена LUNA летела в пропасть, как электросамокатчик, падающий в ад. У фида Chainlink была зашита нижняя граница minAnswer на уровне примерно$0.10, так сказать, страховка от «мусорных» значений. Когда рынок ушёл ниже, фид упёрся в эту границу и был приостановлен из-за беспрецедентной волатильности. Ну а дальше начинается арифметика. Оракул показывал ~$0.107, в то же время рынок — ~$0.01. Разница в десять раз, и она держалась. Атакующие завели на Venus Protocol 230 миллионов LUNA по цене оракула, а это, примерно, больше 24 миллионов долларов «обеспечения», в то время как реальная цена этого беспредела составляла около 2 миллионов. Взяли под этот залог активов на ~13.5 миллиона и ушли. Venus потерял около 11 миллионов долларов. Blizz Finance, работавший по той же схеме, потерял свыше 8.3 миллиона и закрылся, потому что его выпотрошили в ноль. Оракул не был взломан. Он работал ровно по спецификации. Взломаны были протоколы, которые не проверяли, что им пришло.

Безопасное чтение цены

Собираем всё в один рабочий пример. Это не «красивее», это минимально достаточно.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

interface AggregatorV3Interface {
    function latestRoundData()
        external
        view
        returns (
            uint80 roundId,
            int256 answer,
            uint256 startedAt,
            uint256 updatedAt,
            uint80 answeredInRound
        );
    function decimals() external view returns (uint8);
}

contract PriceReader {
    error BadPrice(int256 answer);
    error StalePrice(uint256 updatedAt, uint256 nowTs);
    error PriceOutOfBounds(uint256 price);

    AggregatorV3Interface public immutable feed;

    /// heartbeat фида + запас. Берётся из документации КОНКРЕТНОГО фида,
    /// а не копируется из чужого проекта: у разных пар она разная.
    uint256 public immutable maxDelay;

    /// границы здравого смысла для этого актива
    uint256 public immutable minPrice;
    uint256 public immutable maxPrice;

    constructor(address feed_, uint256 maxDelay_, uint256 minPrice_, uint256 maxPrice_) {
        feed = AggregatorV3Interface(feed_);
        maxDelay = maxDelay_;
        minPrice = minPrice_;
        maxPrice = maxPrice_;
    }

    function getPrice() public view returns (uint256) {
        (, int256 answer, , uint256 updatedAt, ) = feed.latestRoundData();

        // 1. цена вообще положительная
        if (answer <= 0) revert BadPrice(answer);

        // 2. данные свежие: фид не завис и не приостановлен
        if (updatedAt == 0 || block.timestamp - updatedAt > maxDelay) {
            revert StalePrice(updatedAt, block.timestamp);
        }

        uint256 price = uint256(answer);

        // 3. цена в пределах здравого смысла — страховка от упора
        //    в minAnswer/maxAnswer самого фида (кейс LUNA)
        if (price < minPrice || price > maxPrice) revert PriceOutOfBounds(price);

        return price;
    }
}

Три проверки, каждая закрывает конкретный способ потерять деньги. Проверка на свежесть — против зависшего или приостановленного фида. Величину maxDelay берут из heartbeat конкретной пары: у ETH/USD он один, у экзотики совсем другой, и подставлять сюда круглое число «на глаз» означает либо ложные срабатывания, либо дыру, в которую даже слон пролезет. Границы — тот самый урок LUNA. Если фид упёрся в свой внутренний minAnswer, формально он отдаёт валидное число, и единственный способ это заметить — сравнивать с диапазоном, в котором цена вообще имеет смысл. Тут есть неприятный трейд-офф: слишком узкие границы сами становятся отказом в обслуживании на резком движении рынка, поэтому границы ставят широкие «физически осмысленные значения». Отдельно про L2. Если контракт живёт на Base, Optimism, Arbitrum, тогда обязательно читайте ещё и sequencer uptime feed. Когда секвенсер лежит и снова поднимается, цены прилетают пачкой, и в этот момент ликвидации работают по данным из прошлого. Chainlink отдаёт отдельный фид «жив ли секвенсер и сколько времени прошло с подъёма», очень простое правило, которое спасёт: после восстановления выдержать grace period и только потом принимать решения по цене. Показательно, что два из свежих инцидентов 2026-го случились именно на L2.

Чек-лист: аудит собственного кода

Пройдитесь по своему проекту прямо сейчас.

  • Цена берётся из getReserves() или любого «сейчас в пуле»? Это не оракул, это дыра

  • Результат latestRoundData() используется без проверки answer > 0?

  • Проверяется ли свежесть через updatedAt, и взят ли maxDelay из документации именно этого фида?

  • Есть ли границы на цену на случай упора фида в minAnswer/maxAnswer?

  • Контракт на L2? Читается ли sequencer uptime feed?

  • Один источник цены на весь протокол? Подумайте о втором, независимом, и о расхождении между ними как о стоп-кране

  • Можно ли в одной транзакции сдвинуть цену и тут же ею воспользоваться? Если да, то атака стоит ровно флеш-займ

  • Что происходит, если оракул вернул revert? Протокол встаёт или на всякий случай берёт последнее известное значение? Второе опаснее, чем кажется

Что из этого следует

Забавный парадокс индустрии: мы тратим огромные усилия на то, чтобы сделать вычисления недоверенными, а потом сводим всю безопасность к одному числу, приехавшему снаружи. Блокчейн действительно почти невозможно взломать, поэтому взламывают не его. Тридцать две атаки за неполный год и больше миллиарда суммарных потерь в DeFi за 2026-й говорят, что это не экзотика для аудиторов, а рабочая рутина. И почти каждая из них разбирается на одно предложение. Контракт поверил числу, которое не проверил. Если пишете контракт, который трогает деньги, выпишите на бумажке один вопрос: откуда я узнаю цену и сколько стоит на неё повлиять. Пока у вас нет численного ответа на вторую половину, протокол не готов.

Такие разборы — раз в неделю. Между ними я веду серию «Web3 для JS-разработчиков»: двадцать постов о том, как вся эта кухня устроена изнутри, сегодня вышел шестнадцатый. Если хочешь понимать блокчейн системно, а не от взлома к взлому — начни с него: blockwright Telegram

View the original on Хабр

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.