Learning Platform
Глоссарий
Troubleshooting

Решение проблем System Design для TON Developer

Частые ошибки проектирования TON-приложений — симптомы, причины и пошаговые решения.

Область

Тип ошибки

Показано 20 из 20 ошибок

Симптомы

  • Контракт упирается в лимит газа при росте числа пользователей
  • Невозможно параллельно обрабатывать транзакции — все идут через один аккаунт
  • Стоимость одной операции растёт пропорционально размеру хранилища

Причина

Перенос монолитного паттерна из EVM: вся логика и состояние в одном контракте. В TON это создаёт единую точку сериализации — один аккаунт обрабатывает все транзакции последовательно, теряя преимущества шардирования.

Решение

  1. Декомпозируйте контракт по паттерну master–child: master хранит только глобальную конфигурацию
  2. Каждый пользователь или сущность получает собственный child-контракт с отдельным состоянием
  3. Используйте deterministic addressing (hash(master_address, user_id)) для вычисления адресов child-контрактов
  4. Проверьте, что child-контракты попадают в разные шарды для параллелизма

Связанные уроки:

Симптомы

  • Средства отправлены, но не зачислены получателю — застряли в промежуточном контракте
  • Состояние отправителя обновлено, но получатель не существует или отклонил сообщение
  • Невозможно восстановить средства после неуспешной отправки

Причина

В TON сообщения — асинхронные. Если получатель не существует или выбрасывает исключение, сообщение возвращается (bounce). Без обработки bounce-сообщений контракт теряет средства и остаётся в несогласованном состоянии.

Решение

  1. Реализуйте on_bounce handler в каждом контракте, отправляющем сообщения с TON
  2. В bounce handler откатывайте состояние: верните баланс отправителю, сбросьте флаги
  3. Используйте op-коды для идентификации, какое именно исходящее сообщение вернулось
  4. Тестируйте bounce-сценарии: отправка на несуществующий адрес, отправка с недостаточным gas

Связанные уроки:

Симптомы

  • Race conditions: два сообщения читают одно состояние до того, как первое завершит обновление
  • Контракт ожидает ответ, которого может не быть — зависает в промежуточном состоянии
  • Непредсказуемый порядок обработки сообщений при высокой нагрузке

Причина

Разработчик мыслит в терминах синхронных вызовов (как в Solidity), но в TON каждое сообщение — отдельная транзакция. Между отправкой и получением ответа состояние контракта может измениться другими сообщениями.

Решение

  1. Проектируйте контракты как state machines: каждое состояние явно определяет допустимые переходы
  2. Используйте lock-паттерн: помечайте операцию как in_progress, блокируя конкурентные изменения
  3. Не храните промежуточное состояние, зависящее от ответа — передавайте контекст в payload сообщения
  4. Проектируйте каждую операцию как идемпотентную: повторная отправка даёт тот же результат

Связанные уроки:

Симптомы

  • Контракт начинает выполнение, но завершается с out-of-gas посередине операции
  • Частичное обновление состояния: одна часть данных записана, другая нет
  • Пользователь теряет TON — газ потрачен, но операция не завершена

Причина

Контракт не проверяет msg_value при получении сообщения. Если пользователь отправил недостаточно TON для покрытия всей цепочки вычислений и исходящих сообщений, выполнение обрывается на полпути.

Решение

  1. Добавьте проверку msg_value >= estimated_gas в самом начале recv_internal
  2. Рассчитайте минимальный газ с запасом: вычисления + storage fee + forward fee для исходящих сообщений
  3. При недостаточном газе — немедленно верните средства отправителю вместо начала операции
  4. Документируйте минимальный msg_value для каждого op-кода в контракте

Связанные уроки:

Симптомы

  • Gas-стоимость операций растёт логарифмически с числом записей в Dictionary
  • Один контракт хранит Dictionary на миллионы ключей — storage fee становится неподъёмным
  • Невозможно масштабировать: все операции сериализуются через один аккаунт

Причина

Использование одного большого HashMap/Dictionary в контракте для хранения всех пользовательских данных. В TON каждая операция с Dictionary имеет стоимость O(log n) по глубине дерева Cell, и весь контракт — единая точка сериализации.

Решение

  1. Перенесите per-user данные в отдельные child-контракты (sharding по пользователям)
  2. Используйте Jetton-паттерн: master хранит метаданные, wallet-контракты хранят балансы
  3. Если Dictionary необходим, ограничьте его размер и используйте пагинацию
  4. Оцените storage fee заранее: dict_size × cell_cost × time

Связанные уроки:

Симптомы

  • Аномальные свопы на DEX с мгновенным возвратом ликвидности
  • Oracle-цена манипулирована в рамках одной транзакционной цепочки
  • Потеря средств пула ликвидности за одну транзакцию

Причина

В TON асинхронные сообщения затрудняют классические flash-loan атаки, но аналогичные атаки возможны через цепочки сообщений в одном блоке. Протокол полагается только на текущую цену без TWAP или проверки временного окна.

Решение

  1. Используйте TWAP (Time-Weighted Average Price) вместо spot price для critical операций
  2. Добавьте минимальную задержку между deposit и withdraw в пулах ликвидности
  3. Ограничьте максимальный размер свопа относительно глубины пула
  4. Реализуйте circuit breaker: приостановка при аномальном движении цены

Связанные уроки:

Симптомы

  • После обновления контракта все интеграции ломаются — адрес изменился
  • Невозможно мигрировать на новую версию без координации со всеми клиентами
  • Хардкод адресов в нескольких контрактах создаёт хрупкую связность

Причина

Контракты ссылаются друг на друга через захардкоженные адреса вместо registry-паттерна или deterministic addressing. При обновлении одного контракта вся цепочка зависимостей ломается.

Решение

  1. Используйте registry-контракт: единая точка разрешения адресов по именам/ключам
  2. Применяйте deterministic addressing: адрес вычисляется из init_state, а не хранится
  3. Реализуйте proxy-паттерн: стабильный адрес прокси, обновляемая логика за ним
  4. При необходимости хардкода — выносите адреса в configurable storage, а не в код

Симптомы

  • Контракт обработал 3 из 5 переводов в цикле, потом закончился газ
  • Данные в Dictionary частично обновлены — часть записей в новом формате, часть в старом
  • Пользователь видит inconsistent state после неудачной транзакции

Причина

Операции, требующие итерации по коллекции (batch-переводы, массовые обновления), не учитывают, что газ может закончиться на любой итерации. TVM не откатывает storage-изменения при out-of-gas.

Решение

  1. Проектируйте batch-операции с continuation: обработайте N элементов, отправьте себе сообщение с оставшимися
  2. Храните cursor/offset для возобновления обработки с точки остановки
  3. Рассчитайте газ на одну итерацию и проверяйте остаток газа перед каждой итерацией
  4. Предпочитайте sharded-подход: каждый элемент обрабатывается отдельным контрактом

Симптомы

  • Все переводы токенов проходят через один minter-контракт
  • Gas растёт с числом держателей — Dictionary балансов увеличивается
  • Переводы между пользователями невозможно параллелизировать

Причина

Реализация токена по ERC-20 паттерну: один контракт хранит mapping(address => balance). В TON это уничтожает шардирование — все операции сериализуются через один аккаунт вместо распределения по шардам.

Решение

  1. Следуйте стандарту TEP-74 (Jetton): minter + per-user wallet контракты
  2. Каждый wallet-контракт хранит баланс своего владельца и обрабатывает переводы
  3. Transfer: wallet_A отправляет сообщение wallet_B напрямую — без участия minter
  4. Minter используется только для mint/burn и хранения метаданных токена

Связанные уроки:

Симптомы

  • Одна и та же транзакция может быть отправлена повторно с тем же эффектом
  • Пользователь получает двойное начисление при повторной отправке сообщения
  • Атакующий перехватывает и переигрывает успешные транзакции

Причина

Контракт не использует seqno, query_id или другой механизм для отслеживания уже обработанных сообщений. Каждое входящее сообщение с одинаковым payload обрабатывается как новое.

Решение

  1. Для external messages: используйте seqno (sequence number) с инкрементом при каждой обработке
  2. Для internal messages: используйте уникальный query_id и храните set обработанных ID
  3. Устанавливайте msg_valid_until для ограничения времени жизни сообщения
  4. Регулярно очищайте set обработанных query_id, удаляя записи старше TTL

Симптомы

  • Контракт принимает поддельные сообщения от произвольных адресов
  • Атакующий имитирует child-контракт и вызывает привилегированные операции на master
  • Средства выведены через фальшивый wallet-контракт

Причина

Master-контракт проверяет sender_address, но не верифицирует, что отправитель — настоящий child-контракт с правильным init_state. В TON адрес вычисляется из init_state — проверка адреса без проверки кода недостаточна.

Решение

  1. Вычисляйте ожидаемый адрес отправителя из init_state: hash(state_init) должен совпадать с sender
  2. Храните code hash child-контрактов в master и проверяйте при каждом входящем сообщении
  3. Не доверяйте sender_address без верификации — любой может задеплоить контракт с произвольным адресом
  4. Используйте паттерн из TEP-74: master знает wallet_code и вычисляет адреса детерминистически

Симптомы

  • Storage fee непропорционально высок для объёма хранимых данных
  • Операции чтения/записи тратят больше газа, чем ожидалось
  • Cell overflow: данные не помещаются в 1023 бита одной Cell

Причина

Данные упакованы без учёта структуры Cell tree в TVM. Неправильный порядок полей, избыточные Cell reference, неиспользование bits для маленьких значений — всё увеличивает число Cell и стоимость хранения.

Решение

  1. Группируйте часто читаемые поля в root Cell — минимизируйте traversal depth
  2. Используйте bit-packing: bool = 1 bit, small int = минимальное число бит вместо полных 256
  3. Размещайте Optional-поля в отдельных ref-Cell — не тратьте биты root Cell на Maybe-обёртки
  4. Профилируйте storage fee: подсчитайте cells × bits и сравните с оптимальной упаковкой

Связанные уроки:

Симптомы

  • Проблемы обнаруживаются, когда пользователи жалуются, а не по метрикам
  • Невозможно определить, когда контракт перестал обрабатывать сообщения
  • Нет visibility в баланс газа контрактов — контракт замерзает неожиданно

Причина

Разработчик сфокусирован на функциональности контрактов, но не выстраивает off-chain мониторинг. Без наблюдения за балансами, throughput и error rate проблемы обнаруживаются слишком поздно.

Решение

  1. Настройте индексер (TON Index, custom) для отслеживания транзакций ваших контрактов
  2. Мониторьте баланс контрактов — alert при приближении к rent-due threshold
  3. Отслеживайте bounce rate: рост % bounced сообщений указывает на системную проблему
  4. Настройте дашборд с метриками: TPS, average gas per tx, error rate, message latency

Связанные уроки:

Симптомы

  • Контракт считает операцию успешной, хотя вызванный контракт вернул ошибку
  • Средства списаны, но целевая операция не выполнена
  • Сложно отлаживать: нет информации о причине неудачи в цепочке сообщений

Причина

При получении ответного сообщения контракт не проверяет exit_code или op-код ответа. Успешное получение сообщения не означает успешное выполнение операции на стороне получателя.

Решение

  1. Определите протокол ответных сообщений: отдельные op-коды для success и error
  2. Проверяйте exit_code в bounced messages: code = 0xFFFF означает ошибку выполнения
  3. Логируйте результаты в emit-event для off-chain мониторинга
  4. Реализуйте timeout: если ответ не получен за N секунд, запускайте recovery procedure

Связанные уроки:

Симптомы

  • Сериализация/десериализация съедает значительную часть газа каждой транзакции
  • Новые разработчики не могут разобраться в формате данных контракта
  • Ошибки при обновлении схемы — новые поля ломают парсинг старых данных

Причина

Схема данных спроектирована без учёта частоты доступа к полям и cost модели TVM. Глубокая вложенность Cell, неиспользуемые поля в hot path, отсутствие версионирования.

Решение

  1. Следуйте принципу: hot fields в root Cell, cold fields в глубоких ref
  2. Добавьте version byte в начало root Cell для совместимости при обновлениях
  3. Минимизируйте глубину Cell tree: каждый ref — дополнительный gas за cell load
  4. Используйте TL-B компилятор для генерации сериализаторов вместо ручного побитового парсинга

Связанные уроки:

Симптомы

  • Обнаружен баг в production-контракте — невозможно исправить без миграции всех пользователей
  • Средства заблокированы в контракте с неисправимой ошибкой
  • Обновление требует координации с каждым пользователем для миграции на новый адрес

Причина

Контракт задеплоен без set_code/set_data механизма или governance для обновлений. В отличие от Ethereum proxy pattern, TON позволяет менять код контракта через SETCODE — но эту возможность нужно заложить при проектировании.

Решение

  1. Реализуйте admin-op для set_code с проверкой авторизации (governance multisig)
  2. Предусмотрите data migration: при смене кода новая версия должна уметь парсить старый формат данных
  3. Для decentralized протоколов используйте governance voting перед обновлением
  4. Добавьте timelock: между объявлением обновления и его применением должна быть задержка

Связанные уроки:

Симптомы

  • Последнее сообщение в цепочке не доставляется — недостаточно TON для forward_fee
  • Пользователь отправляет 1 TON, но до конечного получателя доходит 0.01 TON
  • Часть цепочки выполняется, часть нет — inconsistent state между контрактами

Причина

При проектировании цепочки A→B→C→D каждое звено потребляет gas + forward_fee. Разработчик не учёл кумулятивные затраты и не зарезервировал достаточно TON на старте цепочки.

Решение

  1. Рассчитайте total_cost = sum(gas_i + forward_fee_i) для всей цепочки перед отправкой
  2. Используйте RAWRESERVE для гарантии сохранения minimum balance на каждом этапе
  3. Передавайте оставшийся баланс через flag 64 (carry remaining value) где возможно
  4. Добавьте buffer 20-30% к расчётному gas на случай увеличения storage fee

Связанные уроки:

Симптомы

  • DeFi-протокол использует цену из одного источника — при его компрометации все операции под угрозой
  • Oracle не обновлялся 24 часа — протокол работает с устаревшей ценой
  • Манипуляция ценой одного oracle позволяет провести арбитражную атаку

Причина

Протокол полагается на единственный oracle-контракт для ценовых данных. Отсутствие агрегации, проверки freshness и fallback-механизма создаёт single point of failure.

Решение

  1. Агрегируйте данные из нескольких oracle источников: median(price_1, price_2, price_3)
  2. Проверяйте freshness: отклоняйте цену, если timestamp > max_staleness
  3. Установите price deviation threshold: если новая цена отличается от предыдущей более чем на X% — pause protocol
  4. Реализуйте fallback oracle: при недоступности основного — переключение на резервный

Связанные уроки:

Симптомы

  • Приложение показывает белый экран при потере соединения с TON RPC
  • Пользователь нажимает кнопку оплаты, ничего не происходит — нет feedback
  • Crash при таймауте запроса к блокчейну — приложение не обрабатывает network errors

Причина

Mini App разработан для happy path: все запросы к TON API и RPC успешны. Отсутствует обработка таймаутов, retry-логика и UI-индикация ошибок. Telegram Mini App работает в мобильном контексте с нестабильным интернетом.

Решение

  1. Реализуйте retry с exponential backoff для всех blockchain-запросов
  2. Показывайте loading state и error state с кнопкой retry для каждой blockchain-операции
  3. Кэшируйте последнее известное состояние — показывайте stale data с пометкой вместо пустого экрана
  4. Используйте optimistic UI: показывайте ожидаемый результат, откатывайте при ошибке

Связанные уроки:

Симптомы

  • Mint 10 000 NFT требует 10 000 транзакций через один контракт — bottleneck
  • Метаданные всех NFT хранятся в одном Dictionary — storage fee растёт квадратично
  • Невозможно transfer NFT без участия collection-контракта

Причина

Вся коллекция реализована как один контракт с Dictionary of items. Каждая операция (mint, transfer, burn) проходит через единственный аккаунт. TEP-62 NFT Standard предполагает отдельные контракты для каждого item.

Решение

  1. Следуйте TEP-62: collection-контракт создаёт отдельный NFT Item контракт при каждом mint
  2. Каждый NFT Item — независимый контракт со своим owner, content и transfer логикой
  3. Transfer выполняется напрямую между NFT Item и новым owner — collection не участвует
  4. Для batch mint используйте параллельные сообщения из collection к нескольким NFT Items

Связанные уроки: