Чтение FunC кода
Умение читать FunC-код — практический навык, который понадобится каждому TON-разработчику. Все стандартные контракты (Jetton, NFT, кошельки) написаны на FunC, и при аудите или интеграции вам придётся разбираться в чужом коде. Это как умение читать чертежи для инженера — вы можете не рисовать их сами, но должны понимать каждую линию.
Умение читать FunC — ключевой навык для работы с экосистемой TON. Большинство системных контрактов (кошельки, governance, Elector) написаны на FunC. В этом уроке мы разберём основные паттерны чтения и пройдёмся по реальному контракту.
Идиомы Cell-операций
FunC использует характерные паттерны для работы с Cell, Slice и Builder. Узнавание этих идиом — первый шаг к пониманию любого контракта.
Чтение данных (Slice)
;; Парсинг ячейки: begin_parse() создаёт Slice для чтения
slice cs = cell_data.begin_parse();
;; Загрузка данных в порядке записи
int flags = cs~load_uint(4); ;; 4 бита без знака
int op = cs~load_uint(32); ;; 32-битный operation code
int query_id = cs~load_uint(64); ;; 64-битный идентификатор запроса
slice addr = cs~load_msg_addr(); ;; адрес (переменная длина)
int amount = cs~load_coins(); ;; количество нанотонов (VarUInt16)
Оператор ~ (тильда) означает модифицирующий вызов: метод изменяет cs, продвигая указатель чтения вперёд.
Запись данных (Builder)
;; Создание ячейки: begin_cell() -> store_... -> end_cell()
cell msg = begin_cell()
.store_uint(0x18, 6) ;; флаги внутреннего сообщения
.store_slice(dest_addr) ;; адрес получателя
.store_coins(amount) ;; сумма в нанотонах
.store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) ;; служебные поля
.store_slice(body) ;; тело сообщения
.end_cell();
При аудите FunC кода обращайте внимание на recv_internal и layout хранилища (порядок store_/load_ полей). Это два самых важных элемента для понимания логики контракта.
Паттерн Operation Code (op)
Почти все контракты TON используют 32-битный operation code в начале тела сообщения:
() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure {
if (in_msg_body.slice_empty?()) {
return (); ;; Пустое сообщение -- просто получение TON
}
int op = in_msg_body~load_uint(32);
int query_id = in_msg_body~load_uint(64);
if (op == 0x7362d09c) { ;; op::transfer_notification
handle_transfer(in_msg_body);
return ();
}
if (op == 0x595f07bc) { ;; op::excesses
return (); ;; Возврат лишних средств
}
throw(0xffff); ;; Неизвестный op
}
Распространённые op-коды в экосистеме TON:
| Op-код | Название | Описание |
|---|---|---|
0x0 | internal_transfer | Перевод внутри jetton |
0xf8a7ea5 | transfer | Запрос на перевод jetton |
0x7362d09c | transfer_notification | Уведомление о получении jetton |
0x595f07bc | excesses | Возврат лишнего газа |
0xd53276db | change_owner | Смена владельца |
Работа со словарями
Dictionary (hashmap) — основная структура для хранения коллекций в TON:
;; Чтение из словаря
(slice value, int found?) = udict_get?(dict, 256, key);
if (found?) {
int amount = value~load_coins();
}
;; Запись в словарь
dict~udict_set(256, key, begin_cell().store_coins(amount).end_cell().begin_parse());
;; Удаление из словаря
(dict, int deleted?) = udict_delete?(dict, 256, key);
;; Проверка, пуст ли словарь
if (dict_empty?(dict)) {
;; Словарь пуст
}
Типы словарей
udict_*— ключи — беззнаковые целые числа (uint)idict_*— ключи — знаковые целые числа (int)dict_*— ключи — произвольные Slice
Словари в TON хранятся как деревья ячеек (Hashmap). Каждая операция поиска потребляет газ пропорционально глубине дерева. Это важно для оптимизации газа.
Обработка ошибок
FunC использует числовые коды ошибок с throw_if и throw_unless:
;; throw_unless(code, condition) -- бросает ошибку, если условие FALSE
throw_unless(401, equal_slices(sender_addr, owner_addr)); ;; Только владелец
;; throw_if(code, condition) -- бросает ошибку, если условие TRUE
throw_if(402, amount <= 0); ;; Сумма должна быть положительной
;; throw(code) -- безусловное исключение
throw(0xffff); ;; Неизвестная операция
Стандартные коды ошибок:
| Диапазон | Назначение |
|---|---|
| 0-255 | Зарезервированы TVM |
| 256-65535 | Пользовательские коды контракта |
| 0xffff | Стандартный “неизвестная операция” |
Разбор: контракт Wallet V4
Рассмотрим ключевые секции стандартного контракта кошелька V4 — одного из самых распространённых контрактов на TON.
Хранилище кошелька
;; Данные кошелька: seqno, subwallet_id, public_key, plugins
(int, int, int, cell) load_data() inline {
slice ds = get_data().begin_parse();
return (
ds~load_uint(32), ;; seqno -- счётчик транзакций (защита от replay)
ds~load_uint(32), ;; subwallet_id -- идентификатор подкошелька
ds~load_uint(256), ;; public_key -- Ed25519 публичный ключ
ds~load_dict() ;; plugins -- словарь установленных плагинов
);
}
Обработка внешнего сообщения
() recv_external(slice in_msg) impure {
var signature = in_msg~load_bits(512); ;; Ed25519 подпись
var cs = in_msg;
var (subwallet_id, valid_until, msg_seqno) = (
cs~load_uint(32), ;; subwallet_id для проверки
cs~load_uint(32), ;; TTL сообщения
cs~load_uint(32) ;; seqno для защиты от replay
);
;; Загружаем данные контракта
var (stored_seqno, stored_subwallet, public_key, plugins) = load_data();
;; Проверки безопасности
throw_unless(35, valid_until > now()); ;; Сообщение не просрочено
throw_unless(33, msg_seqno == stored_seqno); ;; Правильный seqno
throw_unless(34, subwallet_id == stored_subwallet); ;; Правильный subwallet
throw_unless(35, check_signature(
slice_hash(in_msg), signature, public_key ;; Валидная подпись
));
accept_message(); ;; Принимаем и оплачиваем газ из баланса
;; Обрабатываем действия (отправка сообщений)
;; ...
;; Увеличиваем seqno
save_data(stored_seqno + 1, stored_subwallet, public_key, plugins);
}
Ключевые паттерны кошелька
- Seqno — защита от replay-атак. Каждое сообщение увеличивает счётчик.
- valid_until — TTL сообщения. Просроченные сообщения отклоняются.
- check_signature — Ed25519 подпись проверяется до
accept_message(). - accept_message() — контракт соглашается оплатить газ из своего баланса.
В recv_external все проверки безопасности (подпись, seqno, TTL) должны выполняться до accept_message(). Иначе злоумышленник может истощить баланс контракта, отправляя невалидные сообщения.
Частые ошибки
- Пытаются понять весь контракт сразу, вместо того чтобы начать с recv_internal — точки входа для обработки сообщений.
- Не обращают внимание на комментарии ;;, в которых часто содержится важная документация по TL-B схемам и ожидаемым форматам сообщений.
- Путают load_uint и preload_uint: load сдвигает курсор чтения, а preload читает без сдвига, что критично при парсинге сообщений.
- Игнорируют функции-хелперы в начале файла и сразу переходят к основной логике, хотя часто именно в хелперах скрыта критически важная бизнес-логика.
Проверка знанийIn a standard FunC contract, what does the `recv_internal` signature `(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body)` tell you about how to extract the sender's address?
Итоги
- Оператор
~обозначает модифицирующий вызов (продвигает указатель чтения) - Op-коды (32 бита) — стандартный паттерн маршрутизации операций в TON
- Словари (
udict_*,idict_*) — основа для хранения коллекций throw_if/throw_unless— механизм обработки ошибок с числовыми кодами- Стандартный кошелёк V4 демонстрирует все основные паттерны: seqno, подписи, accept_message
Теперь, когда мы умеем читать FunC, перейдём к Tolk — современной замене FunC с более привычным синтаксисом.
Проверьте понимание
Закончили урок?
Отметьте его как пройденный, чтобы отслеживать свой прогресс
Войдите чтобы оценить урок