Типичные уязвимости контрактов на FunC/Tact

Узнаешь пять частых классов ошибок в контрактах TON и какие защитные паттерны ставить рядом с каждым.

Типичные уязвимости

Большинство потерь средств на TON — не «экзотика TVM», а пропущенные проверки: кто отправил сообщение, можно ли повторить payload, что делать при bounce, и хватает ли типизации чисел. Ниже — карта классов; детали — в отдельных статьях раздела.


Карта рисков и минимальный фикс

КлассСимптомБазовая защита
Нет access controlЛюбой вызывает admin-методrequire(sender() == owner)
Нет проверки senderПоддельный «ответ» от чужого контрактаСравнивать sender() с доверенным адресом
Replay externalОдин подписанный payload принимается много разseqno / nonce + инкремент до эффекта
Bounce без компенсацииStorage уже изменили, value вернулосьbounced + откат локального состояния
Overflow / узкие типыНеверные суммы, обрезаниеЯвные uintN, проверки диапазона

Пример «всё сразу плохо» vs минимальный каркас:

tact (fallback highlight)
// @tact-lang/compiler 1.6.13
// УЯЗВИМО: нет owner, нет seqno, нет bounce-компенсации

message Withdraw {
    amount: Int as coins;
}

contract VaultBad {
    receive(msg: Withdraw) {
        // любой может вывести; повтор того же external (через wallet) не отсекается здесь
        send(SendParameters{
            to: sender(),
            value: msg.amount,
            mode: SendIgnoreErrors,
            bounce: false,
        });
    }
}
tact (fallback highlight)
// @tact-lang/compiler 1.6.13
// ИСПРАВЛЕНО: owner + проверка суммы + bounce на исходящем

message Withdraw {
    amount: Int as coins;
}

contract VaultOk(
    owner: Address,
    reserved: Int as coins,
) {
    receive() { cashback(sender()) }

    receive(msg: Withdraw) {
        require(sender() == self.owner, "not owner");
        require(msg.amount > 0, "zero");
        require(myBalance() - msg.amount >= self.reserved, "reserve");

        send(SendParameters{
            to: self.owner,
            value: msg.amount,
            mode: SendPayGasSeparately,
            bounce: true,
            body: "withdraw".asComment(),
        });
    }
}

Как это работает

  • Access control режет «кто может». Sender-check режет «от кого доверяем данным».
  • Replay на уровне wallet (seqno) и на уровне своего контракта (свой nonce) — разные слои; для контрактов с external — свой счётчик обязателен.
  • bounce: false + «успех» в storage без компенсации — классический рассинхрон балансов.
  • FunC без жёстких типов сильнее бьёт overflow; в Tact смотри as uintN / coins и явные require.

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

Считаешь, что «раз через мой фронт — значит безопасно» → любой может собрать то же сообщение; проверки только on-chain.

Копируешь jetton/NFT без проверки sender мастер-кошелька → чужой контракт пришлёт «тот же» opcode.

Чинишь один класс и забываешь соседний → owner есть, а bounce/replay — нет; смотри чеклист перед mainnet.


Что дальше

Материалы gramdocs.tech носят образовательный характер и не являются финансовой, юридической или инвестиционной рекомендацией. Работа с блокчейном TON и токеном Gram связана с рисками потери средств. Правовая информация