Блокчейн цел, деньги пропали: атаки на оракулы и безопасное чтение цены в Solidity
За 2026 год протоколы потеряли на манипуляциях ценой больше, чем за три предыдущих: 32 атаки против 12 за весь 2025-й. При этом ни один блокчейн не был взломан — ломают не цепочку, а то, откуда контракт узнаёт цену.

В начале сентября протокол Tectonic на Cronos потерял около 75 миллионов долларов. Блокчейн при этом работал штатно — консенсус не сломался, приватные ключи не утекли, в коде контракта не было переполнения. Одним словом — тишь да гладь. Атакующий просто сделал так, что контракт поверил в неправильную цену и после этого честно, как будто так и надо, по всем правилам, отдал ему чужие деньги.
Это не единичный случай, а тренд года. По данным TRM Labs, за 2026-й на манипуляциях ценой протоколы поймали 32 атаки против 12 за весь 2025-й, а это, на минуточку, почти втрое больше, и год даже ещё не закончился. Доля таких атак среди всех крипто-взломов выросла примерно с одной семнадцатой в 2022-м до одной восьмой сейчас. В списке пострадавших за последние недели — Moonwell на Base ($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-й говорят, что это не экзотика для аудиторов, а рабочая рутина. И почти каждая из них разбирается на одно предложение. Контракт поверил числу, которое не проверил.
Если пишете контракт, который трогает деньги, выпишите на бумажке один вопрос: откуда я узнаю цену и сколько стоит на неё повлиять. Пока у вас нет численного ответа на вторую половину, протокол не готов.
Это Райт. Ни бага, ни костыля тебе, друг!