Ошибка в $5 000 на TON из-за кода, написанного нейронкой. ai.. ai. blockchain.. ai. blockchain. crypto.. ai. blockchain. crypto. fail.. ai. blockchain. crypto. fail. llm.. ai. blockchain. crypto. fail. llm. Node.JS.. ai. blockchain. crypto. fail. llm. Node.JS. relayer.. ai. blockchain. crypto. fail. llm. Node.JS. relayer. security.. ai. blockchain. crypto. fail. llm. Node.JS. relayer. security. smartcontract.. ai. blockchain. crypto. fail. llm. Node.JS. relayer. security. smartcontract. token.. ai. blockchain. crypto. fail. llm. Node.JS. relayer. security. smartcontract. token. ton.. ai. blockchain. crypto. fail. llm. Node.JS. relayer. security. smartcontract. token. ton. Децентрализованные сети.. ai. blockchain. crypto. fail. llm. Node.JS. relayer. security. smartcontract. token. ton. Децентрализованные сети. Информационная безопасность.. ai. blockchain. crypto. fail. llm. Node.JS. relayer. security. smartcontract. token. ton. Децентрализованные сети. Информационная безопасность. качество кода.. ai. blockchain. crypto. fail. llm. Node.JS. relayer. security. smartcontract. token. ton. Децентрализованные сети. Информационная безопасность. качество кода. Платежные системы.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  • Утек seed?

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

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

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

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

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

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

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

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

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

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

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

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

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

  • Вывел

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

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

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

Версия

Почему не она

STON.fi взломан

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

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

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

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

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

approve/allowance (как в EVM)

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

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

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

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

Почему:

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

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

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

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

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

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

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

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

Любой, кто мог дёрнуть 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
// 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

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

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

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

Уникальность транзакций строилась на lt + hash, где lt = Date.now().
Это не привязано к реальному ончейн-идентификатору. Один и тот же 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

Но 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
// 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

Все операции подписывались одним ключом из 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 или блокчейна не требуется

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

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

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

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

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

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

11.4. Domain separation

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

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 — усилитель, не замена мышлению

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

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

Разница:

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

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

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

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

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

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

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

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

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

  • Relayer’ов и bridge’ей

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

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

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

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

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

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

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

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

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

17. Итог

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

  • Багов в STON.fi

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

  • Хацкеров

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

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

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

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

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

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

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

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

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

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

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

Автор: okoloboga

Источник

Rambler's Top100