Переполнение и типы данных в FunC

Избежишь тихих обрезаний и неверных сумм в FunC: явные битовые ширины, проверки диапазона и безопасная арифметика.

Переполнение в FunC

FunC оперирует целыми произвольной точности на уровне языка, но при записи в ячейку (store_uint, store_coins) и при неверных предположениях о ширине поля значения обрезаются или падают. Ошибки типов дают неверные балансы и обойденные лимиты так же часто, как «логическая» дыра.


Уязвимо: узкий store без проверки

func (fallback highlight)
;; FunC (func 0.4.6)
;; УЯЗВИМО: amount из сообщения кладём в uint64 без верхней границы входных данных

() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure {
  slice cs = in_msg_full.begin_parse();
  cs~load_uint(4);
  slice sender_addr = cs~load_msg_addr();

  int amount = in_msg_body~load_coins();
  ;; дальше бизнес-логика считает, что amount «всегда разумный»
  ;; при сериализации в другой контракт:
  var payload = begin_cell()
    .store_uint(0x123, 32)
    .store_uint(amount, 64) ;; если amount не влезает — throw; если логика выше уже списала — рассинхрон
    .end_cell();
}
tact (fallback highlight)
// @tact-lang/compiler 1.6.13
// Риск рядом: Int без as-аннотации шире, чем слот в message

message Credit {
    // без `as` компилятор выберет представление; для интеропа фиксируй ширину
    amount: Int;
}

Исправлено: диапазон + явная ширина

func (fallback highlight)
;; FunC (func 0.4.6)
;; ИСПРАВЛЕНО

const int MAX_AMOUNT = 1000000000000000000; ;; 10^18 nano

() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure {
  slice cs = in_msg_full.begin_parse();
  cs~load_uint(4);
  slice sender_addr = cs~load_msg_addr();

  int amount = in_msg_body~load_coins();
  throw_unless(402, amount > 0);
  throw_unless(403, amount <= MAX_AMOUNT);
  throw_unless(404, amount <= my_balance);

  cell payload = begin_cell()
    .store_uint(0x123, 32)
    .store_coins(amount) ;; coins — каноничный формат сумм, не «сырой uint64 наугад»
    .end_cell();
}
tact (fallback highlight)
// @tact-lang/compiler 1.6.13
// ИСПРАВЛЕНО: фиксированные типы + require

message Credit {
    amount: Int as coins;
}

contract Ledger(
    total: Int as coins,
) {
    receive(msg: Credit) {
        require(msg.amount > 0, "zero");
        require(msg.amount <= ton("1000000"), "too large");
        self.total = self.total + msg.amount;
        cashback(sender());
    }
}

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

  • Для денег предпочитай coins / store_coins / load_coins, а не произвольный uintN, если нет жёсткого ABI.
  • Перед записью в узкий слот — throw_unless на верхнюю и нижнюю границу.
  • Вычитание баланса: сначала throw_unless(bal >= amount), потом bal - amount; не наоборот в голове «потом проверим».
  • В Tact аннотации as uint32 / as coins задают сериализацию; без них интероп с FunC-контрактами ломается незаметно.

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

Путаешь nano и целые Gram в уме → off-by-10^9 в лимитах.

Ловишь overflow только «надеждой на throw при store» → часть state уже изменена выше по коду.

Складываешь два uint без проверки суммы → результат больше слота при последующей записи.


Что дальше

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