Чеклист аудита
Систематический чеклист аудита — это ваш последний рубеж перед деплоем контракта с реальными деньгами. Подобно тому, как пилот перед взлётом проходит контрольный список, разработчик должен проверить каждый аспект безопасности контракта. Одна пропущенная проверка может стоить миллионы.
В предыдущих уроках мы разобрали отдельные классы уязвимостей. Теперь соберём всё в систематический подход к аудиту смарт-контрактов TON. Этот урок — ваш справочник для проверки любого контракта перед деплоем.
External Messages и accept_message()
Внешние сообщения (external messages) — это сообщения, приходящие извне блокчейна (из кошельков, ботов, офлайн-подписантов). Они обладают особым свойством: газ за их обработку оплачивается из баланса контракта.
Функция accept_message() (в Tact — acceptMessage()) подтверждает, что контракт готов оплатить газ за обработку внешнего сообщения.
Незащищённый 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().
Проверка знаний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?
Send Mode Flags: полный обзор
Неправильный send mode — одна из самых частых ошибок. Вот полная таблица режимов с их влиянием на безопасность:
| Mode | Флаг | Поведение | Риск |
|---|---|---|---|
| 0 | — | Отправить указанный value из баланса контракта | Нет — стандартный режим |
| 1 | SendPayGasSeparately | Forwarding fee оплачивается отдельно от value | Низкий |
| 2 | SendIgnoreErrors | Не выбрасывать ошибку, если отправка не удалась | Средний — можно пропустить ошибку |
| 64 | SendRemainingValue | Переслать весь оставшийся msg_value текущего сообщения | Низкий — стандартный для excess return |
| 128 | SendRemainingBalance | Отправить весь баланс контракта | Критический |
| +32 | SendDestroyIfZero | Уничтожить контракт, если баланс стал нулевым | Высокий — необратимо |
Mode 128: когда допустим
Mode 128 (SendRemainingBalance) допустим только в двух случаях:
- Уничтожение контракта —
mode: SendRemainingBalance + SendDestroyIfZero - Одноразовый контракт (например, 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);
}
}
Частые ошибки
- Проводят аудит поверхностно, проверяя только основные сценарии: злоумышленники атакуют именно edge-cases и неочевидные комбинации.
- Не проверяют accept_message() в обработке external-сообщений: это одна из самых критических уязвимостей, позволяющая опустошить контракт.
- Забывают проверить send_mode во всех отправках сообщений: неправильный mode может привести к потере всего баланса контракта.
- Не аудируют зависимости: контракт может быть безопасен сам по себе, но вызывать уязвимую библиотеку или взаимодействовать с небезопасным контрактом.
Проверяйте, КТО может обновить код
При аудите контракта найдите все вызовы set_code() / nativeSetCode() и убедитесь, что:
- Только admin (или multisig) может вызвать обновление
- Admin — не внешний EOA, а проверенный контракт (governance, multisig)
- Есть 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:
- Bounced messages — обработка ошибок и восстановление состояния
- Gas management — валидация, возврат excess, безопасные send modes
- Integer pitfalls — отрицательные числа, переполнение, типизация
- Race conditions — TOCTOU, шардирование, replay protection
- Production contracts — реальные паттерны Jetton 2.0, Getgems NFT, TON DNS
- Audit checklist — систематический подход к проверке контрактов
Эти знания — фундамент для безопасной разработки на TON. Используйте чеклист при каждом деплое, анализируйте существующие контракты для углубления понимания, и помните: каждый пропущенный bounced-handler — потенциальная потеря средств.
Check Your Understanding
Finished the lesson?
Mark it as complete to track your progress
Войдите чтобы оценить урок