- BrainTools - https://www.braintools.ru -

Ошибка в $5 000 на TON из-за кода, написанного нейронкой

Наконец таки статья о том как я облажался. Точнее — как облажалась команда, но ответственность все равно моя.

TL;DR: Relayer для TON-проекта писался с помощью LLM. Без документации. Без тестов. Без понимания модели угроз. В результате — потеря ~$5 000 из пула ликвидности на STON.fi [1]. Блокчейн не взломан, DEX работает как надо. Проблема была в нашей архитектуре.

Это разбор конкретной ошибки [2], которая стоила реальных денег. И пояснение, почему скептики с Хабра всё равно не правы — но по другой причине, чем они думают.

1. Что вообще за проект

Делали сервис для крипто-трейдеров. Суть простая:

  • Пользователь покупает подписку за TON

  • Получает возможность создавать триггеры на валютные пары

  • Когда триггер срабатывает — получает уведомление

Интересная особенность – уведомления приходят в виде… звонка на телефон! пользователь при регистрации указывает свой номер, а при триггере сервис Twilio – из США поступает звонок. У проекта был свой jetton (токен), пул ликвидности на STON.fi [1], подписочная модель через смарт-контракт.

Сначала мы делали просто приложение — без ончейн-логики. Работало отлично. Потом заказчик решил: “Давайте сделаем по-настоящему, со смарт-контрактами, чтобы всё было прозрачно и децентрализованно”. Идея была такая, что за стоимость подписки покупался токен и тут же сжигался. Таким образом каждая покупка подписки увеличивала ликвидность.

Звучало круто. Мы согласились.

2. Архитектура (упрощённо)

Для понимания — как это всё было устроено:

┌─────────────────┐
│  Пользователь   │
│  (Tonkeeper)    │
└────────┬────────┘
         │ платит TON
         ▼
┌─────────────────┐
│  Subscription   │
│  Smart Contract │
└────────┬────────┘
         │ делегирует swap/burn
         ▼
┌─────────────────┐
│    Relayer      │  ← вот тут была дыра
│  (off-chain)    │
└────────┬────────┘
         │ выполняет on-chain операции
         ▼
┌─────────────────┐
│    STON.fi      │
│      DEX        │
└─────────────────┘
Ошибка в $5 000 на TON из-за кода, написанного нейронкой - 1 [3]

Relayer — это off-chain сервис, который:

  • Принимал HTTP-запросы

  • Выполнял on-chain операции (swap, burn)

  • Подписывал транзакции hot-wallet ключом

  • Работал как “мост” между контрактами и DEX

По сути — доверенный посредник с правами на критичные операции.

3. Что вообще произошло

День 0: Что-то пошло не так (мы ещё не знаем)

В какой-то момент злоумышленник получил возможность инициировать burn LP-токенов. LP-токены — это токены ликвидности, которые подтверждают нашу долю в пуле на STON.fi [1].

Что произошло технически:

  • LP-токены сожжены через стандартный StonfiBurnNotification

  • STON.fi [1] вернул активы (USD₮ и наш jetton) — ровно по правилам протокола

  • Активы выведены

Мы это не заметили, вообще!

День 10: Обнаружение

Спустя примерно 10 дней кто-то заметил, что ликвидность в пуле нулевая – необходимо было часть токенов обменять обратно на TON для тестирования контракта в другом сервисе. Начали разбираться.

Первые мысли (типичные):

  • Утек seed?

  • STON.fi [1] взломали? (пффф, и только нашу ликвидность вывели))

  • Баг в LP-контракте?

Спойлер: нет, нет и нет конечно же.

День ~11-14: Восстановление (и ошибка)

Заказчик попросил сделать то, что кажется логичным:

  • Перевести управляющие токены на его новый кошелек

  • Он решил сразу пополнить ликвидность заново

  • Решили, что это был разовый инцидент

Это была ошибка.

День ~15: Второй удар

Буквально в тот же день или на следующий — злоумышленник увидел, что ликвидность снова появилась.
Но теперь у него не было доступа к LP-токенам (мы же перевели всё на новые кошельки). Поэтому он сделал другое:

  • Наминтил токены напрямую через MINT-контракт

  • Продал их в пул за свежую ликвидность

  • Вывел

Ну красавчег, если честно, молодец.

4. Что точно НЕ было причиной

Прежде чем копать дальше — зафиксирую, что мы исключили:

Версия

Почему не она

STON.fi [1] взломан

DEX работает как задокументировано, никаких аномалий

Баг в LP-контракте

Стандартный контракт, используется тысячами проектов

Утечка seed-фразы

Не было тотального вывода всех активов со всех кошельков

approve/allowance (как в EVM)

В TON другая модель, тут это не применимо

То есть это не классический взлом. Блокчейн сделал ровно то, что должен был сделать.

5. Где начали копать по-настоящему

На меня нахлынуло просветление: relayer.

Почему:

  • У relayer были права на burn

  • Relayer работал со STON.fi [1]

  • Действия в инциденте — именно те, которые relayer умел делать

Я выгрузил всю историю инцидента в ChatGPT — последовательность событий, что обнаружили, когда, какие транзакции. Запротоколировал. Потом передал эту информацию вместе с кодовой базой в Codex.

Пара часов делов и ясность пришла полная.

6. Ключевая ошибка: relayer доверял HTTP, а не блокчейну

Вот как была устроена логика [4]:

1. Приходит HTTP-запрос /process-subscription
2. Relayer НЕ проверяет:
   - что реально был ончейн-платёж
   - что txHash существует
   - что сумма совпадает
   - что вызов одноразовый
3. Relayer СРАЗУ делает:
   - swap
   - burn
   - другие on-chain действия
Ошибка в $5 000 на TON из-за кода, написанного нейронкой - 2 [3]

Любой, кто мог дёрнуть relayer API, мог инициировать критичные on-chain операции.

Да, API был защищён(?…) токеном. в .env, всё как положено. Но этого оказалось недостаточно.

7. Конкретные дыры в коде

7.1. Доверие HTTP-запросу вместо ончейн-события

// relayer/src/controllers/relayer.controller.ts
@Post("process-subscription")
async processSubscription(@Body() data: ProcessSubscriptionDto) {
  return this.relayerService.processSubscription(data);
}
Ошибка в $5 000 на TON из-за кода, написанного нейронкой - 3 [3]
// relayer/src/services/relayer.service.ts
const transaction = this.transactionRepository.create({
  lt: Date.now().toString(),  // ← вот это проблема
  hash: data.txHash,          // ← и это
  userAddress: data.userAddress,
  fromAddress: data.subscriptionContractAddress,
  toAddress: this.config.relayerWalletAddress,
  amountNanotons: (parseFloat(data.amount) * 1_000_000_000).toString(),
  // ...
});
// Далее сразу swap+burn без проверки txHash/платежа
Ошибка в $5 000 на TON из-за кода, написанного нейронкой - 4 [3]

Relayer запускает критичные операции на основании входного HTTP-запроса. Без проверки, что платёж реально был на блокчейне.

7.2. Фейковая идемпотентность

// relayer/src/entities/transaction.entity.ts
@Index(["lt", "hash"], { unique: true })
Ошибка в $5 000 на TON из-за кода, написанного нейронкой - 5 [3]
// relayer/src/services/relayer.service.ts
lt: Date.now().toString(),
hash: data.txHash,
Ошибка в $5 000 на TON из-за кода, написанного нейронкой - 6 [3]

Уникальность транзакций строилась на lt + hash, где lt = Date.now [5]().
Это не привязано к реальному ончейн-идентификатору. Один и тот же txHash можно обработать многократно — просто в разное время.

7.3. Нет связи user → intent → действие

Контракт подписки отправлял на relayer тело сообщения:

// blockchain/contracts/caller.tact
message(MessageParameters{
  to: self.stonRouter,
  value: tonToSwap,
  body: beginCell()
    .storeUint(0x7361_6d70, 32)  // op
    .storeAddress(sender())      // user
    .storeCoins(incoming)        // amount
    .endCell()
});
Ошибка в $5 000 на TON из-за кода, написанного нейронкой - 7 [3]

Но relayer эту информацию не читал и не верифицировал. Он просто выполнял то, что пришло по HTTP.
По сути relayer превратился в универсальный прокси выполнения on-chain действий доверенным ключом.

7.4. Hot-wallet с избыточными правами

// relayer/src/config/relayer.config.ts
relayerPrivateKey: process.env.RELAYER_PRIV_KEY!,
relayerWalletAddress: process.env.RELAYER_WALLET_ADDR!,
Ошибка в $5 000 на TON из-за кода, написанного нейронкой - 8 [3]
// relayer/src/modules/ton/ton.service.ts
await walletContract.sendTransfer({
  seqno,
  secretKey: this.keyPair.secretKey,
  messages: [ internal({ to: destination, value, body }) ],
});
Ошибка в $5 000 на TON из-за кода, написанного нейронкой - 9 [3]

Все операции подписывались одним ключом из env. Этот ключ имел права на:

  • swap

  • burn LP

  • mint токенов

  • owner-level операции

Компрометация relayer-сервиса = компрометация всей trust-модели проекта.

8. Как могла выглядеть атака

Вариант A: Доступ к API

  1. Хацкер получает возможность вызвать relayer HTTP API (утечка токена, открытый порт, SSRF)

  2. Отправляет process-subscription с произвольными данными

  3. Relayer запускает on-chain swap + burn

  4. Профит

Вариант Б: Компрометация сервера/ключей

  1. Доступ к relayer-серверу или env-переменным

  2. Использует hot-wallet ключи для owner/minter операций

  3. Mint токенов, burn LP напрямую

  4. Профит

Оба сценария полностью согласуются с тем, что:

  • LP burn мог быть инициирован только владельцем LP (а relayer имел эти права!)

  • Mint возможен только при наличии owner/minter прав (а relayer имел и их!)

  • Компрометация STON.fi [1] или блокчейна не требуется

Мы до конца не выяснили, какой именно вектор был использован, ноо… это и не так важно — важно, что архитектура это позволяла)

9. Почему это произошло

Теперь самое интересное — почему relayer был написан с такими дырами.
Relayer писался с помощью LLM. Без документации. Без тестов. Без понимания модели угроз.
Человек, который его писал, торопился. Не заказчик торопил — сам торопился. Хотел быстро, срочно и почти офигенно.
И он положился на нейронку полностью. Без нашей обычной методики (о которой писал в прошлых статьях подробно):

  • Не было детального ТЗ на компонент

  • Не было документации архитектуры

  • Не было TDD

  • Не было threat-model

  • Не было даже базовых тестов(!?)

LLM отлично справился с задачей. Код работал. Swap работал. Burn работал. Всё функционировало.
Но LLM не сделал того, о чём его не просили:

  • Не подумал о модели угроз

  • Не проверил trust boundaries

  • Не задавался вопросами (ну а зачем?)

  • Не предложил разделение ключей по ролям

Потому что это должен был сделать разработчик.

10. Главный вывод (и он не про “LLM плохие”)

Ожидаю скептиков: “Вот видите! LLM для серьёзных вещей не годится!”
Увы! Они не правы. Но не потому, что LLM идеальны.

LLM — это усилитель.
Это ключевая мысль, которую я повторяю в каждой статье. И этот инцидент ее только подтверждает.

  • Если LLM усиливает понимающего архитектора — получается boost

  • Если LLM усиливает того, кто не понимает предметную область — усиливаются дыры

В нашем случае:

  • Разработчик не понимал модель угроз для блокчейн-приложений

  • Разработчик не применил нашу методичку (документация + TDD)

  • Разработчик торопился

LLM сделал ровно то, что его просили. Написал работающий код. Код работал — до тех пор, пока кто-то не нашёл дыру.
Проблема была не в инструменте. Проблема была в процессе.

11. Что нужно было сделать

Для истории — как должен был быть устроен безопасный relayer:

11.1. Proof of payment

// Вместо доверия HTTP-запросу
async processSubscription(data: ProcessSubscriptionDto) {
  // 1. Проверить, что txHash существует на блокчейне
  const tx = await this.tonClient.getTransaction(data.txHash);
  if (!tx) throw new Error('Transaction not found');
  
  // 2. Проверить, что транзакция от subscription contract
  if (tx.from !== SUBSCRIPTION_CONTRACT_ADDRESS) {
    throw new Error('Invalid source');
  }
  
  // 3. Проверить сумму
  if (tx.value !== expectedAmount) {
    throw new Error('Invalid amount');
  }
  
  // 4. Только после этого — выполнять действия
  await this.executeSwap(data);
}
Ошибка в $5 000 на TON из-за кода, написанного нейронкой - 10 [3]

11.2. Идемпотентность по реальному tx

// Уникальность по ончейн-идентификатору, а не по Date.now()
@Index(["txHash", "lt"], { unique: true })  // lt из блокчейна, не из Date.now()
Ошибка в $5 000 на TON из-за кода, написанного нейронкой - 11 [3]

11.3. Разделение ключей по ролям

relayer-swap-key     → только swap операции
relayer-burn-key     → только burn (если вообще нужно)
owner-key            → owner операции, НИКОГДА не на сервере
minter-key           → mint операции, НИКОГДА не на сервере
Ошибка в $5 000 на TON из-за кода, написанного нейронкой - 12 [3]

11.4. Domain separation

// Whitelist разрешённых операций
const ALLOWED_OPERATIONS = ['swap', 'callback'];
// burn, mint, owner-calls — НИКОГДА через relayer
Ошибка в $5 000 на TON из-за кода, написанного нейронкой - 13 [3]

12. Как это изменило наш процесс

После инцидента я формализовал несколько правил:

Для любого компонента с доступом к критичным ресурсам:

  1. Threat-model ДО написания кода

    • Кто может вызвать этот компонент?

    • Что произойдёт, если вызов будет поддельным?

    • Какие права минимально необходимы?

  2. Документация как у всех остальных компонентов

    • Детальное ТЗ

    • Архитектура

    • API

    • Ограничения

  3. TDD обязательно

    • Тесты на happy path

    • Тесты на edge cases

    • Тесты на атаки (replay, forge, overflow)

Для блокчейн-проектов конкретно:

  1. Минимальные права для каждого компонента

    • Relayer не должен иметь owner/minter права

    • Hot-wallet только для операционных нужд

    • Критичные ключи — только в cold storage

  2. Verify on-chain, not off-chain

    • Любое действие — проверяем на блокчейне

    • HTTP-запросу доверяем только после верификации

13. Про реакцию заказчика

Отдельно хочу отметить: заказчик отреагировал удивительно спокойно.

$5000 — для него это оказалось “не много”. Он не требовал компенсации, не устраивал скандал. Просто сказал:

Ошибка в $5 000 на TON из-за кода, написанного нейронкой - 14

Это редкость. И это позволило мне спокойно провести расследование, задокументировать всё, сделать выводы.
Не все заказчики такие. Нам повезло.

14. Про мою реакцию

Когда я понял, что это наша ошибка архитектуры, а не хацкеры — было немного грустно. Но больше было интересно.
Включился включился исследовательский режим. Хотелось понять:

  • Как именно это произошло?

  • Что мы упустили?

  • Как сделать так, чтобы не повторилось?

  • Что из этого можно вынести для будущих проектов?

Этот инцидент не заставил меня усомниться в LLM-разработке. Наоборот, он подтвердил то, что я говорю всегда:

  • Документация решает

  • Тесты решают

  • Понимание предметной области решает

  • LLM — усилитель, не замена мышлению [6]

15. Про другие блокчейн-проекты

У нас есть другие проекты со смарт-контрактами. Там нет таких проблем.

Разница:

  • Там была полноценная документация

  • Там были тесты

  • Там контракты изолированы и защищены

  • Там не было компонента типа “relayer с правами на всё”

Этот инцидент — исключение, а не правило. Но исключение, которое стоило денег и из которого нужно было сделать выводы.

16. Мораль (без морализаторства)

Если вы используете LLM для разработки — особенно для:

  • Блокчейн-приложений

  • Компонентов с доступом к деньгам

  • Relayer’ов и bridge’ей

  • Чего угодно с owner/minter/admin правами

Сначала нарисуйте модель угроз.

Задайте себе вопросы:

  • Кто может вызвать этот код?

  • Что произойдёт, если входные данные поддельные?

  • Какие минимальные права нужны?

  • Что будет, если этот компонент скомпрометирован?

И только после этого — просите LLM написать код, с детальным ТЗ, с ограничениями и конечно же тестами.

Код LLM напишет. Ответственность за архитектуру — на вас.

17. Итог

Мы потеряли ~$5 000 не из-за:

  • Багов в STON.fi [1]

  • Плохого блокчейна

  • Хацкеров

  • Ненадежных LLM

Мы потеряли их из-за:

  • Неправильной trust-модели

  • Слишком доверчивого relayer’а

  • Отсутствия документации и тестов

  • Торопливости

  • Иллюзии, что LLM может заменить архитектурное мышление

Я продолжаю использовать LLM для 100% кода. Но теперь ещё строже слежу за процессом — особенно когда дело касается денег и безопасности.

Если интересна методология LLM-разработки, которая работает (когда её применяют) — в прошлой статье [7] разбирал документацию, TDD и управление контекстом.

Вопросы по TON/STON.fi/relayer — в комментарии. Расскажу подробнее, что смогу.

Готов к помидорам. Как показывает практика — это только добавляет просмотров.

Автор: okoloboga

Источник [8]


Сайт-источник BrainTools: https://www.braintools.ru

Путь до страницы источника: https://www.braintools.ru/article/25184

URLs in this post:

[1] STON.fi: http://STON.fi

[2] ошибки: http://www.braintools.ru/article/4192

[3] Image: https://sourcecraft.dev/

[4] логика: http://www.braintools.ru/article/7640

[5] Date.now: http://Date.now

[6] мышлению: http://www.braintools.ru/thinking

[7] в прошлой статье: https://habr.com/ru/articles/971496/

[8] Источник: https://habr.com/ru/articles/992740/?utm_source=habrahabr&utm_medium=rss&utm_campaign=992740

www.BrainTools.ru

Rambler's Top100