Программирование безопасных смарт-контрактов
Эта страница переведена сообществом на русский язык, но нуждается в улучшениях. Если вы хотите принять участие в переводе свяжитесь с @alexgton.
В этом разделе мы рассмотрим несколько наиболее интересных особенностей блокчейна TON, а затем пройдемся по списку лучших практик для разработчиков, программирующих смарт-контракты на FunC.
Разделение контрактов
При разработке контрактов для EVM вы обычно разбиваете проект на несколько контрактов для удобства. В некоторых случаях можно реализовать всю функциональность в одном контракте, и даже там, где разделение контрактов было необходимо (например, пары ликвидности в Automated Market Maker), это не привело к каким-либо особым трудностям. Транзакции выполняются в полном объеме: либо всё работает, либо всё возвращается.
В TON настоятельно рекомендуется избегать "неограниченных структур данных" и разделять один логический контракт на небольшие части, каждая из которых управляет небольшим объёмом данных. Основным примером является реализация TON Jettons. Это версия TON стандарта токенов ERC-20 от Ethereum. Вкратце, у нас есть:
- Один
jetton-minter
, который хранитtotal_supply
,minter_address
и пару ссылок: описание токена (метаданные) иjetton_wallet_code
. - И множество jetton-кошельков, по одному на каждого владельца этих жетонов. В каждом таком кошельке хранится только адрес владельца, его баланс, адрес jetton-minter и ссылка на jetton_wallet_code.
Это необходимо для того, чтобы передача жетонов происходила напрямую между кошельками и не затрагивала высоконагруженные адреса, что является основой для параллельной обработки транзакций.
То есть, приготовьтесь к тому, что ваш контракт станет "группой контрактов", и они будут активно взаимодействовать друг с другом.
Возможно частичное выполнение транзакций
В логике вашего контракта появляется новое уникальное свойство: частичное выполнени е транзакций.
Например, рассмотрим поток сообщений стандартного TON Jetton:
Как следует из диаграммы:
- отправитель посылает сообщение
op::transfer
на свой кошелек (sender_wallet
); sender_wallet
уменьшает баланс токенов;sender_wallet
отправляет сообщениеop::internal_transfer
на кошелек получателя (destination_wallet
);destination_wallet
увеличивает свой баланс токенов;destination_wallet
отправляетop::transfer_notification
своему владельцу (destination
);destination_wallet
возвращает избыточный газ с сообщениемop::excesses
наresponse_destination
(обычноsender
).
Обратите внимание, если destination_wallet
не смог обработать сообщение op::internal_transfer
(произошло исключение или закончился газ), то этот этап и все последующие не будут выполнены. Однако первый шаг (уменьшение баланса в sender_wallet
) будет выполнен. Результатом будет частичное выполнение транзакции, что приводит к некорректному состоянию Jetton
и, в данном случае, потеря средств.
В худшем случае все токены могут быть украдены таким образом. Представьте, что вы сначала начисляете бонусы пользователю, а затем отправляете сообщение op::burn
на его кошелёк Jetton, но не можете гарантировать, что сообщение op::burn
будет успешно обработано.
Разработчик смарт-контрактов TON должен контролировать газ
В Solidity газ не представляет особой проблемы для разработчиков контрактов. Если пользователь предоставит слишком мало газа, все будет отменено, как будто ничего не произошло (но газ не будет возвращён). Если же пользователь предоставит достаточно, то фактические расходы будут автоматически рассчитаны и вычтены из его баланса.
В TON ситуация иная:
- Если газа недостаточно, транзакция будет выполнена частично;
- Если газа слишком много, его излишки должны быть возвращ ены. Это входит в обязанности разработчика;
- Если "группа контрактов" обменивается сообщениями, то контроль и расчёты должны производиться в каждом сообщении.
TON не может автоматически рассчитать газ. Полное выполнение транзакции со всеми последствиями может занять много времени, и к моменту завершения у пользователя может не оказаться достаточного количества тонкоинов на кошельке. Здесь снова используется принцип переноса значения (carry-value principle).
Разработчик смарт-контрактов TON должен управлять хранилищем
Типичный обработчик сообщений в 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>);
}