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) {
Это изменение было частью более крупного пересмотра семантики обработки сообщений. Ранее оператор 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) {