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

В начале сентября протокол 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:
Занимаем флеш-займом крупную сумму
Выкупаем один из токенов пула — соотношение резервов перекашивается, «цена» взлетает
В том же блоке дёргаем протокол-жертву, который читает
getReserves()и видит раздутую ценуЗакладываем подорожавший токен и берём под него настоящий кредит
Возвращаем флеш-займ, уходим с разницей
Вот и всё, одна транзакция — одна атака, миллионы в кармане. У арбитражников нет ни одного блока, чтобы вернуть цену на место, потому что манипуляция и её эксплуатация происходят атомарно. Поэтому в индустрии есть жёсткое правило, что протокол, который берёт спот-цену из пула ликвидности, будет взломан. Не «может быть», а будет.
Разбор: 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
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.