За прошедшую неделю (12.10.2026–18.10.2026) отмечены следующие 3 значимых инцидента безопасности с совокупным оценочным ущербом примерно 3,87 млн USD.
| Дата | Инцидент | Тип | Оценочный ущерб |
|---|---|---|---|
| 12.10.2026 | Autheo Network | Компрометация приватного ключа | ~1,76 млн USD |
| 14.10.2026 | UNCX Network | Ошибка учёта | ~807 тыс. USD |
| 18.10.2026 | Qubic Wallet | Генерация слабой сид-фразы | ~1,30 млн USD |
Размеры ущерба оценены на основе фактически реализованной стоимости.
- UNCX Network: Этот инцидент подробно разобран, поскольку ошибка в расчёте совокупного количества долей даёт самый наглядный за неделю урок по учёту в контрактах.
Бесплатная проверка безопасности
Быстрая проверка безопасности с помощью нашего собственного механизма автоматизированного анализа.
Проверить бесплатноГлавный инцидент недели: UNCX Network
14.10.2026 злоумышленники использовали уязвимость контрактов Smart Token Vesting V2 сети UNCX Network в Ethereum, BNB Chain, Base и Robinhood Chain. Инцидент затронул 10 активов в 11 подтверждённых транзакциях вывода средств и привёл к получению или переводу активов стоимостью примерно 807,609 тыс. USD [1]. Первопричиной стала ошибка в расчёте совокупного количества долей: при расчёте смешивались транши вестинга, созданные при разных соотношениях токенов и долей. В результате записи отдельных позиций перестали соответствовать общему количеству долей, что позволило позициям под контролем злоумышленников вывести больше токенов, чем те внесли.
Общая информация
UNCX Network — поставщик инфраструктуры DeFi для нескольких блокчейнов. Его продукт Smart Token Vesting V2 позволяет проектам блокировать токены ERC-20 по графикам вестинга, привязанным ко времени. Каждый график представлен идентификатором вестинга и содержит сведения о токене, создателе, бенефициаре, общей сумме, уже выданной сумме и одном или нескольких траншах разблокировки. В позицию типа soft, допускающую пополнение, после её создания можно добавлять новые транши, а её создатель может отменить вестинг ещё не разблокированной части.
Позиции одного и того же токена используют общий баланс в VestingManager, а протокол определяет принадлежащую каждой позиции часть этого баланса через доли. Поэтому учёт ведётся на двух связанных уровнях:
Global pool, per token
create / topUp: receive tokens and mint shares
release: convert releasable shares into tokens
_totalTokenShares[token]: active shares across all positions
Position record, per vesting ID
_depositedShares[id]: share basis assigned to the position
totalAmount: token amount represented by its tranches
released: shares already paid to the beneficiary
При выдаче менеджер сначала рассчитывает, какая часть суммы по графику прошла вестинг. Затем он переводит соответствующую долю от totalAmount в доли, прошедшие вестинг, с использованием _depositedShares[vestingId], вычитает released и называет результат releasableShares. В приведённой ниже формуле tokenBalance — текущий баланс менеджера в токене ERC-20, указанном в графике; этот баланс является общим для всех позиций по данному токену. При этом _totalTokenShares[token] — соответствующее общее количество активных долей. Количество выдаваемых токенов зависит от их соотношения:
amount = releasableShares * tokenBalance / _totalTokenShares[token]
Внесение средств через функции вестинга увеличивает и баланс токенов, и общее количество долей. Прямой перевод токенов ERC-20 менеджеру изменяет только баланс токенов, а значит, и количество токенов, соответствующее каждой доле. Такая модель долей может поддерживать позиции в общем пуле только при условии, что на каждом этапе жизненного цикла записи позиций согласуются с общим количеством долей.
Анализ уязвимости
В затронутом контракте VestingManager в Ethereum по адресу 0xA98F...99F5 есть ошибка в расчёте совокупного количества долей, которой можно воспользоваться при последовательном вызове release(), topUpVesting() и cancel(). Аналогичные затронутые развёртывания существуют и в трёх других сетях.
Чтобы было проще сопоставлять изменения состояния, в анализе ниже введены сокращённые обозначения D, R и V. В коде контракта этих обозначений нет; таблица сопоставляет каждое из них с соответствующей переменной или выражением контракта. Сопоставленные значения относятся к двум типам величин:
| Обозначение | Переменная / выражение контракта | Тип величины | Значение |
|---|---|---|---|
D |
_depositedShares[vestingId] |
Количество долей | Совокупная база долей, назначенная всем депозитам, учтённым для позиции |
R |
schedule.released |
Количество долей | Совокупное количество долей, уже выплаченных бенефициару |
| - | schedule.totalAmount |
Количество токенов | Общее количество токенов, учтённое по текущим траншам позиции |
| - | vestedAmount |
Количество токенов | Часть totalAmount, прошедшая вестинг по этим траншам к текущему моменту |
V |
vestedShare |
Количество долей | Совокупное количество долей, прошедших вестинг, рассчитанное путём применения единого смешанного коэффициента вестинга токенов к D |
В release() контракт рассчитывает releasableShares, увеличивает schedule.released и вычитает ту же величину из _totalTokenShares[schedule.token]. При этом он не уменьшает _depositedShares[vestingId]. Поэтому после выдачи позиция вносит в общее количество долей лишь D - R действующих долей, хотя сохранённая для неё база остаётся равной D.
function release(uint256 vestingId) external nonReentrant {
...
uint256 releasableShares = _getReleasableShares(vestingId);
uint256 balance = IERC20(schedule.token).balanceOf(address(this));
uint256 amount =
(releasableShares * balance) / _totalTokenShares[schedule.token];
schedule.released += releasableShares;
_totalTokenShares[schedule.token] -= releasableShares;
IERC20(schedule.token).safeTransfer(msg.sender, amount);
}
cancel() применяет единый смешанный коэффициент вестинга к совокупной базе долей позиции. Сначала функция рассчитывает vestedAmount через getVestedAmount(vestingId), затем переводит выраженную в токенах долю, прошедшую вестинг, в доли как V = (vestedAmount * D) / totalAmount. Наконец, функция считает D - V количеством не прошедших вестинг долей позиции, которое можно удалить. V — это совокупная величина, а не остаток долей, прошедших вестинг после предыдущих выплат.
function cancel(uint256 vestingId) external nonReentrant {
...
uint256 vestedAmount = getVestedAmount(vestingId);
uint256 remainingAmount = schedule.totalAmount - vestedAmount;
uint256 vestedShare =
vestedAmount * _depositedShares[vestingId] / schedule.totalAmount;
uint256 sharesToRemove =
_depositedShares[vestingId] - vestedShare;
_totalTokenShares[schedule.token] -= sharesToRemove;
_depositedShares[vestingId] = vestedShare;
schedule.totalAmount = vestedAmount;
schedule.cancelled = true;
IERC20(schedule.token).safeTransfer(schedule.creator, remainingAmount);
}
Ошибка заключается в самом смешанном расчёте. У каждого транша есть собственное количество токенов, прогресс вестинга и количество долей, выпущенных по действовавшему при добавлении этого транша соотношению в пуле. Поэтому доли каждого транша, прошедшие вестинг, следует рассчитывать отдельно, а затем суммировать. Вместо этого контракт сначала объединяет количества токенов и базы долей, а затем применяет единый коэффициент вестинга ко всей позиции, поскольку хранит только одно значение D и одно значение totalAmount. Оба подхода дают одинаковый результат только в том случае, если у траншей одинаковое количество долей на токен или одинаковый прогресс вестинга. Прямой перевод токенов меняет соотношение в пуле до пополнения, поэтому новый транш получает значительно меньше долей на токен, чем первоначальный. Применение единого смешанного коэффициента вестинга к обоим траншам может дать значение V, которое уже не отражает их совокупное количество долей, прошедших вестинг.
Поскольку R уже выплачено из совокупного количества долей позиции, прошедших вестинг, согласованный график должен удовлетворять условию R <= V. Нарушение этого неравенства — прямой признак ошибки смешанного расчёта. Код не ведёт отдельный учёт остатка действующих долей позиции: он вычитает D - V непосредственно из общего значения _totalTokenShares. Предположим, другие позиции вносят 1000 долей, а у рассматриваемой позиции D = 100 и R = 80. До отмены общее количество включает 1,000 + (D - R) = 1,020 долей. Если смешанный расчёт даёт V = 60, cancel() вычитает D - V = 40, оставляя в общем количестве лишь 980 долей — на 20 меньше, чем 1000 долей, внесённых другими позициями. Если бы общее количество включало только 20 действующих долей этой позиции, вычитание 40 привело бы к переполнению при вычитании и откату транзакции; избыточное вычитание проходит только потому, что в общем пуле есть доли, внесённые другими позициями. Затем функция сохраняет _depositedShares[vestingId] = 60, тогда как schedule.released остаётся равным 80, и запись позиции оказывается в невозможном состоянии D < R.
Код не обеспечивает соблюдения R <= V и не ограничивает sharesToRemove количеством действующих долей позиции. Поэтому при отмене могут вычитаться исторические доли, которых уже нет в общем количестве долей. Одновременно функция напрямую переводит totalAmount - vestedAmount в токенах, не пересчитывая возвращаемую сумму по соотношению долей в пуле. Сама отмена не приводит к избыточной выплате: она лишь уменьшает общее количество долей ниже суммы действующих долей, учтённых по позициям, создавая условия для последующего избыточного вывода.
Анализ атаки
В следующем анализе рассмотрена одна показательная транзакция в Ethereum — 0x352065...c23823. В рамках этой транзакции атакующий контракт выполнил одну и ту же основную последовательность действий с пятью токенами, используя для каждого из них флеш-ликвидность.
-
Шаг 1: Злоумышленник создал небольшую позицию
#5058с немедленным вестингом и две значительно более крупные позиции#5059и#5060с немедленным вестингом, типа soft и с возможностью пополнения. Небольшую позицию предполагалось сохранить для заключительного вывода, а крупные позиции обеспечивали исторические записи о долях, необходимые для нарушения общего учёта. -
Шаг 2: Злоумышленник вывел средства из двух крупных позиций. При первоначальном соотношении в пуле, близком к 1:1, из каждой позиции вернулась сумма, приблизительно равная её чистому депозиту. Соответствующие доли были вычтены из
_totalTokenShares, но первоначальные значения сохранились в записях_depositedSharesдля позиций#5059и#5060, а как выданные они были учтены только в отдельном полеreleasedкаждого графика. -
Шаг 3: Злоумышленник напрямую перевёл токены в
VestingManager. Это восстановило баланс токенов, но не количество выведенных долей, поэтому количество токенов, соответствующее каждой оставшейся доле, резко выросло. -
Шаг 4: Злоумышленник пополнил крупные позиции, из которых уже были выведены средства. Поскольку пополнения оценивались по новому, повышенному соотношению токенов на долю, значительные депозиты токенов привели к выпуску лишь небольшого количества новых долей. Теперь позиции сочетали большую устаревшую историческую базу с небольшим вкладом действующих долей.
-
Шаг 5: Злоумышленник отменил две пополненные позиции. При каждой отмене к совокупной истории внесённых долей применялся единый смешанный коэффициент вестинга. Пересчитанное значение
vestedShareоказалось меньше количества уже выданных долей, поэтомуsharesToRemoveпревысило вклад действующих долей позиции. Общее количество долей уменьшилось примерно на5.0e25фиктивных долей на позицию, хотя при пополнении каждая позиция добавила лишь небольшое количество действующих долей. -
Шаг 6: После того как знаменатель оказался сильно занижен, злоумышленник вывел средства из сохранённой небольшой позиции. В рассматриваемом примере с одним токеном чистый депозит примерно в
5.73e22единиц базового номинала позволил вывести примерно1.07e26единиц базового номинала — примерно в 1,873 раз больше внесённого. Злоумышленник повторил циклы создания позиций и вывода средств при искажённом соотношении, а затем вернул флеш-ликвидность.

В официальном отчёте об инциденте такая же схема выявлена в 11 подтверждённых транзакциях вывода средств. Некоторые варианты выполнялись атомарно, а в других заключительный вывод происходил в связанной транзакции. UNCX рассчитала сумму примерно в 807,609 тыс. USD на основе фактически полученных или переведённых средств, исключив собственные средства злоумышленника, временные объёмы флеш-ликвидности и возвращённые активы [1].
Начните работу с Phalcon Explorer
Изучайте транзакции, чтобы принимать взвешенные решения
Попробовать бесплатноЗаключение
Первопричиной была ошибка в учёте совокупного количества долей. Протокол объединял транши, созданные при разных соотношениях токенов и долей, а затем применял единый коэффициент вестинга к их совокупной базе долей. Это приводило к неверному расчёту совокупного количества долей, прошедших вестинг, позволяло вычесть для позиции больше долей, чем она продолжала вносить, и нарушало соответствие записей отдельных позиций общему количеству долей. Злоумышленники воспользовались заниженным общим количеством долей, чтобы вывести из общего баланса токены, которые по-прежнему были необходимы для обеспечения других позиций вестинга.
Модель учёта, допускающая последующие депозиты при изменяющемся соотношении долей, должна сохранять для каждого транша его количество токенов, базу долей и прогресс вестинга, а не объединять их под одним коэффициентом. Она также должна обеспечивать, чтобы совокупные выплаты никогда не превышали совокупное количество долей, прошедших вестинг, и чтобы из позиции нельзя было вычесть больше её вклада действующих долей.
Источники
[1] Отчёт UNCX Network об инциденте со Smart Token Vesting V2
Лучший аудитор безопасности для Web3
Проверьте архитектуру, код и бизнес-логику перед запуском



