Перейти к содержанию
Learning Platform
Глоссарий Troubleshooting
Урок 11.06 · 20 мин
Продвинутый
SecurityAuditBest PracticesTON

Чеклист аудита

Систематический чеклист аудита — это ваш последний рубеж перед деплоем контракта с реальными деньгами. Подобно тому, как пилот перед взлётом проходит контрольный список, разработчик должен проверить каждый аспект безопасности контракта. Одна пропущенная проверка может стоить миллионы.

В предыдущих уроках мы разобрали отдельные классы уязвимостей. Теперь соберём всё в систематический подход к аудиту смарт-контрактов TON. Этот урок — ваш справочник для проверки любого контракта перед деплоем.


External Messages и accept_message()

Внешние сообщения (external messages) — это сообщения, приходящие извне блокчейна (из кошельков, ботов, офлайн-подписантов). Они обладают особым свойством: газ за их обработку оплачивается из баланса контракта.

Функция accept_message() (в Tact — acceptMessage()) подтверждает, что контракт готов оплатить газ за обработку внешнего сообщения.

DANGER

Незащищённый accept_message() — критическая уязвимость

Если контракт вызывает acceptMessage() без предварительной проверки (подпись, seqno, whitelist), любой может отправить внешнее сообщение и заставить контракт оплатить газ из своего баланса. Повторяя это тысячи раз, атакующий осушит весь баланс контракта.

Уязвимый код

// УЯЗВИМЫЙ КОД -- accept без проверки
contract VulnerableExternal {
    external("process") {
        acceptMessage();  // Любой может вызвать!
        // ... выполняем операцию за счёт контракта ...
    }
}

Исправленный код

contract SecureExternal {
    seqno: Int as uint32;
    publicKey: Int as uint256;

    external(msg: SignedMessage) {
        // 1. СНАЧАЛА проверяем подпись
        require(checkSignature(msg.hash, msg.signature, self.publicKey),
                "Invalid signature");

        // 2. Проверяем seqno (replay protection, урок 04)
        require(msg.seqno == self.seqno, "Invalid seqno");

        // 3. ТОЛЬКО ПОСЛЕ проверок принимаем сообщение
        acceptMessage();

        self.seqno += 1;
        // ... выполняем операцию ...
    }
}

Правило: всегда проверяйте подпись и seqno ДО вызова acceptMessage().

Проверка знанийKnowledge check
You are auditing a TON contract and see that the checklist items span bounced handlers, gas validation, integer safety, async patterns, and accept_message placement. If you could only check three items due to time constraints, which three would catch the most critical vulnerabilities and why?
ОтветAnswer
The three highest-impact items are: (1) acceptMessage() placement -- if called before signature verification, an attacker can drain the entire contract balance through repeated external messages; (2) bounced handlers for state-modifying sends -- missing handlers cause permanent fund loss (token burns); (3) require(amount > 0) before subtraction -- missing checks enable infinite minting via negative amounts. These three cover balance drainage, fund loss, and supply manipulation -- the most financially damaging vulnerability classes.

Send Mode Flags: полный обзор

Неправильный send mode — одна из самых частых ошибок. Вот полная таблица режимов с их влиянием на безопасность:

ModeФлагПоведениеРиск
0Отправить указанный value из баланса контрактаНет — стандартный режим
1SendPayGasSeparatelyForwarding fee оплачивается отдельно от valueНизкий
2SendIgnoreErrorsНе выбрасывать ошибку, если отправка не удаласьСредний — можно пропустить ошибку
64SendRemainingValueПереслать весь оставшийся msg_value текущего сообщенияНизкий — стандартный для excess return
128SendRemainingBalanceОтправить весь баланс контрактаКритический
+32SendDestroyIfZeroУничтожить контракт, если баланс стал нулевымВысокий — необратимо

Mode 128: когда допустим

Mode 128 (SendRemainingBalance) допустим только в двух случаях:

  1. Уничтожение контрактаmode: SendRemainingBalance + SendDestroyIfZero
  2. Одноразовый контракт (например, Sale контракт в Getgems, который существует только для одной сделки)

В любом другом случае mode 128 осушит контракт.

Mode 2: скрытые ошибки

SendIgnoreErrors подавляет ошибки отправки. Если сообщение не доставлено (недостаточно газа, некорректный адрес), контракт не узнает об этом. Используйте только для некритичных уведомлений.


Защита обновления кода

В TON контракт может обновить собственный код через set_code(). Это мощная возможность, но и опасная — если обновление не ограничено, злоумышленник может заменить код контракта:

// Защищённое обновление кода
contract UpgradeableContract {
    admin: Address;

    receive(msg: UpgradeCode) {
        // Только admin может обновить код
        require(sender() == self.admin, "Not admin");

        // Обновляем код контракта
        nativeSetCode(msg.newCode);
    }
}

Частые ошибки

  1. Проводят аудит поверхностно, проверяя только основные сценарии: злоумышленники атакуют именно edge-cases и неочевидные комбинации.
  2. Не проверяют accept_message() в обработке external-сообщений: это одна из самых критических уязвимостей, позволяющая опустошить контракт.
  3. Забывают проверить send_mode во всех отправках сообщений: неправильный mode может привести к потере всего баланса контракта.
  4. Не аудируют зависимости: контракт может быть безопасен сам по себе, но вызывать уязвимую библиотеку или взаимодействовать с небезопасным контрактом.

WARNING

Проверяйте, КТО может обновить код

При аудите контракта найдите все вызовы set_code() / nativeSetCode() и убедитесь, что:

  1. Только admin (или multisig) может вызвать обновление
  2. Admin — не внешний EOA, а проверенный контракт (governance, multisig)
  3. Есть timelock или голосование перед обновлением (для критичных контрактов)

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

Смарт-контракты иногда нуждаются в случайных числах (лотереи, рандомные раздачи). В TON функция random() / nativeRandom() использует seed, зависящий от данных блока.

Проблема: валидаторы видят данные блока до его создания. Это означает, что random() предсказуем для участников сети:

// УЯЗВИМЫЙ КОД -- предсказуемый рандом
contract UnsafeLottery {
    receive("draw") {
        // random() зависит от блока -- валидатор может предсказать
        let winner: Int = nativeRandom() % self.participantCount;
        // Валидатор или MEV-бот может манипулировать результатом
    }
}

Решение: commit-reveal

Для честной случайности используйте двухэтапную схему commit-reveal:

contract FairLottery {
    commitHash: Int?;           // Хеш секретного числа
    commitBlock: Int as uint32; // Блок коммита

    // Этап 1: commit -- участник отправляет хеш секретного числа
    receive(msg: Commit) {
        self.commitHash = msg.hash;
        self.commitBlock = now();
    }

    // Этап 2: reveal -- участник раскрывает число
    // Должно пройти минимум N блоков после commit
    receive(msg: Reveal) {
        require(now() > self.commitBlock + 60, "Too early");
        require(sha256(msg.secret) == self.commitHash, "Hash mismatch");

        // Комбинируем секрет с блоковым рандомом --
        // ни одна сторона не может предсказать результат
        let seed: Int = msg.secret ^ nativeRandom();
        let winner: Int = seed % self.participantCount;
    }
}

Идея: пользователь фиксирует (commit) своё число заранее, а потом раскрывает (reveal) его в будущем блоке. Ни пользователь, ни валидатор не могут манипулировать результатом в одиночку.


Итоговый чеклист аудита

Используйте этот чеклист для проверки каждого контракта перед деплоем в mainnet:

Из урока 01: Bounced Messages

  • Все операции, изменяющие состояние перед отправкой сообщения, имеют bounced-handler
  • Bounced-handler восстанавливает состояние (баланс, владение, статус)
  • Bounced-handler простой — без сложной логики, без отправки сообщений
  • Ключевые данные для восстановления находятся в первых 256 битах тела сообщения

Из урока 02: Gas Management

  • Каждый handler проверяет context().value >= MIN_GAS
  • Каждый handler возвращает excess через Excesses (0xd53276db)
  • Send modes используются корректно: mode 64 для gas return, mode 128 только при уничтожении
  • Газ заложен с запасом для цепочек сообщений (2-3 hop)

Из урока 03: Integer Safety

  • require(amount > 0) перед каждым вычитанием
  • require(balance >= amount) перед каждым вычитанием из баланса
  • Балансы используют as coins (120 бит, беззнаковое)
  • Счётчики используют as uint32 или as uint64

Из урока 04: Async Safety

  • Проверка и действие выполняются в одной транзакции (нет TOCTOU)
  • Состояние шардировано — каждый контракт владеет своими данными
  • External messages защищены seqno (replay protection)
  • Handler-ы идемпотентны — не полагаются на “старое” состояние

Из урока 05: Production Patterns

  • Дочерние контракты аутентифицируются по коду (state_init verification)
  • Архитектура следует паттерну “один контракт — одна единица данных”

Из этого урока: Additional Checks

  • acceptMessage() вызывается только после проверки подписи и seqno
  • Нет SendRemainingBalance (mode 128) в обычных операциях
  • SendIgnoreErrors (mode 2) используется только для некритичных уведомлений
  • Обновление кода (set_code) ограничено admin/multisig
  • Нет использования random() / nativeRandom() без commit-reveal
  • Код контракта обновляется через timelock/governance (для критичных контрактов)

Инструменты для аудита

ИнструментНазначение
Blueprint test suiteЮнит-тесты контрактов в sandbox (из модуля M04)
TON VerifierВерификация исходного кода деплоенного контракта
tonviewer.comПросмотр транзакций, сообщений, состояния контракта
Tact compiler warningsКомпилятор Tact предупреждает о некоторых паттернах

Заключение модуля

Модуль M10 покрыл шесть критических областей безопасности смарт-контрактов TON:

  1. Bounced messages — обработка ошибок и восстановление состояния
  2. Gas management — валидация, возврат excess, безопасные send modes
  3. Integer pitfalls — отрицательные числа, переполнение, типизация
  4. Race conditions — TOCTOU, шардирование, replay protection
  5. Production contracts — реальные паттерны Jetton 2.0, Getgems NFT, TON DNS
  6. Audit checklist — систематический подход к проверке контрактов

Эти знания — фундамент для безопасной разработки на TON. Используйте чеклист при каждом деплое, анализируйте существующие контракты для углубления понимания, и помните: каждый пропущенный bounced-handler — потенциальная потеря средств.

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 6