Русский
English
RU
Русский
English
RU
Сетка BlockFirst
Картинка BlockFirst
Ссылка
Иконка копировать
Иконка скопированно

Nomad Bridge Hack: Анализ коренных причин

В этой статье рассматривается корневая причина взлома кроссчейн-моста Nomad. Описывается, как ошибка в контракте Replica позволила обходить проверку подлинности сообщений, что сделало возможной подделку транзакций. Подробно объясняется, как работает архитектура Nomad.

12.07.2025
Перевод
Начинающий уровень
Автор BlockFirst
Поделиться
Иконка поделиться
Ссылка
Иконка копировать
Иконка скопировано
ARTICLE BELOW
Картинка
Превью BlockFirst

if (_time == 0) {
return false;
}

uint256 _time = confirmAt[_root];

return false;

}
return block.timestamp >= _time;

uint256 _time = confirmAt[_root];
if (_time == 0) {

__NomadBase_initialize(_updater);
// set storage variables
entered = 1;
remoteDomain = _remoteDomain;
committedRoot = _committedRoot;
// pre-approve the committed root.
confirmAt[_committedRoot] = 1;
_setOptimisticTimeout(_optimisticSeconds);

uint32 _remoteDomain,
address _updater,
bytes32 _committedRoot,
uint256 _optimisticSeconds

}

) public initializer {

function initialize(

// ...

// ...
require(messages[_messageHash] == MessageStatus.Proven, "!proven");

}

function process(bytes memory _message) public returns (bool _success) {

// ...
require(acceptableRoot(messages[_messageHash]), "!proven");
// ...

}

function process(bytes memory _message) public returns (bool _success) {

Превью BlockFirst

Это изменение было частью более крупного пересмотра семантики обработки сообщений. Ранее оператор require навсегда блокировал обработку допустимых сообщений, если им сопутствовала недействительная аттестация. Новый оператор require предотвращает возможность мошенническому обновляющему (updater) блокировать доставку валидных сообщений, предоставляя неверную аттестацию. Для реализации этого был введён механизм отображения сообщения на корень, описанный в разделе «Допустимый корень» выше.

Ранее оператор require требовал, чтобы в отображении сообщений (messages mapping) было значение, отличное от 0. Это значение могло быть установлено только через предварительный вызов функции prove. После изменения из отображения могло быть загружено значение 0 и передано в функцию acceptableRoot.

Ранее оператор require в функции process был записан следующим образом:

Nomad также стимулировала публичный обзор кода через программу вознаграждений ImmuneFi, запущенную с 9 июня. Однако эта проблема не была выявлена в рамках программы вознаграждений.

Контракты Replica были исключены из Nomad Bridge и Nomad Governance. Это предотвращает обработку любых сообщений мостом и снижает последствия уязвимости в функции process. При этом новые транзакции моста могут быть инициированы, но не обработаны. Графический интерфейс NomadBridge был отключён. Рекомендуется пользователям воздержаться от использования моста до полного исправления и перезапуска системы.

Nomad Watchers следят за компрометацией ключа Updater и реагируют на неё, но пока не отслеживают подозрительную активность, связанную с ошибками в смарт-контрактах. Поскольку уязвимость находилась внутри функции process контракта, сообщения не требовали поддельной подписи Updater, и поэтому не вызывали срабатывание Watchers. Таким образом, ни один Watcher не предпринял действий.

Каково текущее состояние системы?

Почему Nomad Watchers не отреагировали на эту ситуацию?

Обновления в продуктивных средах были проведены 21 июня 2022 года в следующих транзакциях:

Когда это изменение было внесено в сеть?

Это обновление системы Nomad было проверено компанией Quantstamp в мае и начале июня 2022 года. Финальный отчёт был получен 9 июня. Изменение было внесено 26 мая, в период аудита, в рамках исправлений и улучшений, связанных с аудитом (в частности, касающихся вопросов QSP-2 и QSP-34), и вошло в коммит с указанным хешем, использованным для повторного аудита после устранения замечаний.

Каковы детали аудита обновления контракта Replica, проведённого Quantstamp?

Соответствующий код был внедрён в обновлении смарт-контракта 21 июня 2022 года.

Когда был введён этот код?

Поскольку _time равно 1, этот блок пропускается.

Это позволяет неподтверждённым сообщениям пройти следующую проверку в функции process. В свою очередь, это даёт возможность обрабатывать сообщения без предварительного подтверждения.

_time равно 1, поэтому любое действительное значение времени блока будет больше или равно 1. В результате функция acceptableRoot(bytes32(0)) всегда возвращает true для таких контрактов Replica.

Когда сообщение отправляется в функцию process, протокол извлекает корень из этого отображения и проверяет, возвращает ли функция acceptableRoot значение true. Эта функция должна возвращать true только если истёк период ожидания для допустимого корня. Она также учитывает специальные устаревшие значения корней (из предыдущей версии системы), а также корни, которые не были подтверждены (то есть имеют временную метку 0 в отображении корней). Функция проверяет, истёк ли таймер, сравнивая его с текущим временем блока: return block.timestamp >= _time;.

После того как корень передан на другую цепочку, включение сообщения в дерево подтверждается с помощью доказательства Merkle. Корень, под которым было доказано сообщение, сохраняется в отображении mapping(bytes32 => bytes32) в контракте Replica. Это отображение связывает хеш сообщения с корнем, под которым оно было подтверждено. В данном случае значение по умолчанию для bytes32 — это bytes32(0). Следовательно, любое сообщение, которое не было подтверждено, будет иметь в этом отображении корень bytes32(0).

Контракт Replica отслеживает корни из других цепочек с помощью отображения mapping(bytes32 => uint256). Это отображение сопоставляет корни с отметками времени, указывающими, когда они становятся допустимыми. Обработка сообщений запрещена до тех пор, пока не истечёт оптимистическое время ожидания для соответствующего корня. При чтении из отображения, если значение ещё не установлено, возвращается значение по умолчанию (так называемое нулевое значение). Для типа uint256 это значение по умолчанию равно 0. Следовательно, любой корень, который не был подтверждён, будет иметь отметку времени 0 в этом отображении.

Однако, если Replica развёртывается одновременно с соответствующим контрактом Home — как это происходит при первоначальном развёртывании — дерево Меркла Home не содержит сообщений. В реализации Nomad дерево Меркла без листьев имеет корень bytes32(0). Поэтому Replica, развёрнутый одновременно с контрактом Home, будет инициализирован с корнем bytes32(0), и confirmAt[bytes32(0)] будет установлен в 1.

Во время инициализации значение confirmAt[_committedRoot] устанавливается в 1. Это гарантирует, что сообщения, включённые в исходный корень, могут быть обработаны. Таким образом, недавно инициализированный Replica может принимать сообщения, которые были созданы до его развёртывания.

Сначала значение confirmAt[_root] загружается в переменную с именем _time. Для неизвестных сообщений (включая поддельные) этот _root равен bytes32(0). В любом контракте Replica, инициализированном с _committedRoot, установленным в bytes32(0), значение confirmAt[bytes32(0)] было установлено при инициализации равным 1.

Вот соответствующие строки функции acceptableRoot.

Построчно:

Вместе с тем

Когда контракт Replica развёртывается после связанного с ним контракта Home, Replica инициализируется в определённом состоянии. Это позволяет новым развёртываниям не повторять все предыдущие обновления (Updates) из удалённого Home для обработки сообщений. При развёртывании можно передать параметр _committedRoot, с которого начинается история дерева сообщений, получающего обновления.

Инициализация

Nomad фиксирует межсетевые сообщения в виде дерева Меркла (называемого «деревом сообщений»). Корень этого дерева распространяется на удалённые цепочки с помощью оптимистического механизма.

Подробности

Ошибка в реализации привела к тому, что контракт Replica не смог корректно проверять подлинность сообщений. Это позволило подделывать любые сообщения, при условии, что они ещё не были обработаны. В результате контракты, полагавшиеся на Replica для аутентификации входящих сообщений, столкнулись с нарушением безопасности. Эта ошибка аутентификации привела к передаче поддельных сообщений в контракт Nomad BridgeRouter.

Проблема на высоком уровне

}
return block.timestamp >= _time;

uint256 _time = confirmAt[_root];
if (_time == 0) {
return false;

// this is backwards-compatibility for messages proven/processed
// under previous versions
if (_root == LEGACY_STATUS_PROVEN) return true;
if (_root == LEGACY_STATUS_PROCESSED) return false;

return block.timestamp >= _time;

function acceptableRoot(bytes32 _root) public view returns (bool) {

(Перевод)
Виталий Дорожко
Автор
Поделиться
Иконка поделиться
Ссылка
Иконка копировать
Иконка скопированно
Назад в блог
Кнопка назад
Оригинал статьи
кнопка вперед
Поделиться
Иконка поделиться
Ссылка
Иконка копировать
Иконка скопировано
Назад в блог
Кнопка назад
Оригинал статьи
кнопка вперед
сетка BlockFirst
сетка BlockFirst
сетка BlockFirst

Для запросов от пользователей

hello@blockfirst.io

Icon mail

Для бизнес запросов

business@blockfirst.io

Icon mail

Телеграм для быстрых ответов

Icon mail

компания

Сообщество

медиа

Подписываясь на рассылку, вы можете быть уверены, что мы не будем спамить Вам :)

Новости. Скидки. Анонсы

В начало
© 2025-2026 BlockFirst. Все права защищены.
Сетка BlockFirst
hello@blockfirst.io
Для коммерческих предложений
Компания
Телеграм для быстрых ответов
Кнопка копировать
Скопировано