📚Lecture Notes
- В контрактах виртуальных машин Ethereum вы можете получить доступ и изменить каждый элемент хранилища по отдельности.
- В TON доступ к хранилищу осуществляется через get_data()/set_data() (регистр c4 содержит ссылку в пакете ячеек).
- Это требует, чтобы разработчик "умелял" хранилищем вручную
Typical message handler
Типичный обработчик сообщений в TON следует этому подходу:
() handle_something(...) impure {
(int total_supply, <a lot of vars>) = load_data();
... ;; do something, change data
save_data(total_supply, <a lot of vars>);
}
Unfortunately, we are noticing a trend: <a lot of vars> is a real enumeration of all contract data fields. For example,
Этот подход имеет ряд недостатков.
Problem 1: Hard to update the storage structure
- Добавление еще одного поля требует обновления всего контракта.
Во-первых, если вы решите добавить еще одно поле, скажем, is_paused, то вам нужно обновить операторы load_data()/save_data() на протяжении всего контракта. И это не только трудоемко, но и приводит к трудноуловимым ошибкам.
В недавнем аудите CertiK мы заметили, что разработчик перепутал два аргумента местами и написал:
save_data(total_supply, min_amount, swap_fee, ...
instead of save_data(total_supply, swap_fee, min_amount, ...
Без внешнего аудита, проведенного группой экспертов, найти такую ошибку очень сложно. Функция с ошибкой использовалась редко, и оба запутанных параметра обычно имели нулевое значение. Вы действительно должны знать, что вы ищете, чтобы уловить такую ошибку.
Problem 2: Namespace pollution
- Чтение всех полей хранения в текущем пространстве имен загрязняет его.
Во-вторых, существует "загрязнение пространства имен". Давайте объясним, в чем проблема, на другом примере из аудита. В середине функции входной параметр гласит:
(int total_supply, int swap_fee, int min_amount, <a lot of vars>) = load_data();
...
int min_amount = in_msg_body~load_coins();
...
save_data(total_supply, swap_fee, min_amount, <a lot of vars>);
То есть, существует затенение поля хранения локальной переменной, и в конце функции это замененное значение хранится в хранилище. Нападаюший имел возможность перезаписать состояние контракта.
- FunC Позволяет передекларировать переменные.
Problem 3: Gas cost increased
- Разбирание всего хранилища и упаковка его обратно при каждом вызове каждой функции увеличивает стоимость газа.
Solution 1: Use global variables
На этапе прототипирования, где не совсем очевидно, что будет храниться в контракте, можно использовать глобальные переменные.
Таким образом, если вы обнаружите, что вам нужна другая переменная, вы просто добавляете новую глобальную переменную и изменяете load_data() и save_data(). Никаких изменений на протяжении всего контракта не требуется. Однако,
- Возможно не более 31 глобальной переменной.
- Globals дороже, чем хранение в стеке.
Solution 2: Use "nested" storage
После создания прототипа мы рекомендуем следовать этому подходу к организации хранения:
() handle_something(...) impure {
(slice swap_data, cell liquidity_data, cell mining_data, cell discovery_data) = load_data(); (int total_supply, int swap_fee, int min_amount, int is_stopped) = swap_data.parse_swap_data();
…
swap_data = pack_swap_data(total_supply + lp_amount, swap_fee, min_amount, is_stopped); save_data(swap_data, liquidity_data, mining_data, discovery_data);
}
- Если переменная используется часто (например, is_paused), она немедленно предоставляется load_data().
- Если группа параматеров нужна только в одном сценарии, не распаковывайте ее.
Хранение состоит из блоков связанных данных. Если параметр используется в каждой функции, например, is_paused, то он немедленно предоставляется load_data(). Если группа параметров нужна только в одном сценарии, то ее не нужно распаковывать, ее не нужно будет упаковывать, и она не засорит пространство имен.
- Если добавлена новая переменная, нужно обновлять меньше фрагментов кода.
Если структура хранения требует изменений (обычно добавление нового поля), то придется вносить гораздо меньше правок.
- Вложенные переменные могут быть подвложены.
Более того, этот подход можно повторить. Если в нашем контракте есть 30 полей хранения, то сначала вы можете получить четыре группы, а затем получить пару переменных и еще одну подгруппу из первой группы. Главное - не переусердствовать.
- Ячейка может хранить до 1023 бит и до 4 ссылок. Ты все равно разделишься.
Обратите внимание, что, поскольку ячейка может хранить до 1023 бит данных и до 4 ссылок, вам все равно придется разделить данные на разные ячейки.
Иерархические данные являются одной из главных особенностей TON, давайте использовать их по назначению.
Use end_parse()
- Используйте end_parse() везде, где это возможно, при чтении данных из хранилища и из полезной нагрузки сообщения.
Поскольку TON использует битовые потоки с переменным форматом данных, полезно убедиться, что вы читаете столько, сколько пишете. Это может сэкономить вам час отладки.
Use helper functions and avoid magic numbers
Этот код из реального проекта. Это может напугать даже опытного разработчика из-за большого количества магических чисел
Bug example
Не забывайте обо всех традиционных подводных камнях и потенциальных ошибках, не связанных конкретно с TON. Вот пример из реального проекта.
() handle_transfer(...) impure {
...
(slice from_user_info, int from_flag) = user_info_dict.udict_get?(256, from_addr_hash);
int from_balance = from_user_info~load_coins();
...
(slice to_user_info, int to_flag) = user_info_dict.udict_get?(256, to_addr_hash);
int to_balance = to_user_info~load_coins();
...
;; save decreased from_balance to user_info_dict
;; save increased to_balance to user_info_dict
}
Перевод денег на один и тот же адрес практически удваивает баланс, так как to_balance перезаписывает zeroed from_balance.
Resume
- Используйте "внестную" организацию хранения и глобальные переменные, чтобы поддерживать надежность работы хранилища.
- Напишите обертки и объявляйте константы, чтобы код был выразительным.
- Проверьте свой код.
- Проведите тщательный аудит, чтобы избежать потерь.