Найти тему

Модель Carry-Value

Оглавление

📚Lecture Notes

При разработке контрактов для виртуальной машины Ethereum вы обычно разбиваете проект на несколько контрактов только для удобства. В некоторых случаях можно реализовать всю функциональность в одном контракте, и даже там, где было необходимо разделение контракта (например, пары ликвидности в Automated Market Maker), это не привело к каким-либо особым трудностям.

  • В EVM транзакции выполняются полностью: либо все получается, либо все возвращается.

В TON настоятельно рекомендуется избегать «неограниченных структур данных» и разделить один логический контракт на небольшие части, каждая из которых управляет небольшим объемом данных. Основным примером является реализация TON Jettons (это версия TON стандарта токенов Ethereum ERC-20).

  • В TON проект разделен на несколько небольших частей.

Contract Sharding

Jetton structure:jetton-minter stores total_supply, minter_address, metadata, jetton_wallet_code
many jetton-wallet store owner's address, their balance, jetton-minter address, jetton_wallet_code

Вкратце, у нас есть:
Один jetton-minter, который хранит total_supply, minter_address и пару ссылок: описание токена (метаданные) и jetton_wallet_code. И много джеттон-кошелька, по одному на каждого владельца этих джеттонов. Каждый такой кошелек хранит только адрес владельца, его баланс, адрес jetton-minter и ссылку на jetton_wallet_code. Это необходимо для того, чтобы передача Jettons происходила непосредственно между кошельками и не влияла на адреса с высокой нагрузкой, что имеет основополагающее значение для параллельной обработки транзакций.

То есть, приготовьтесь к тому, чтобы ваш контракт превратился в "группу контрактов", и они будут активно взаимодействовать друг с другом.

Что из этого следует?

Возможно частичное выполнение транзакций

Новое уникальное свойство появляется в логике вашего контракта: частичное исполнение сделок.

Например, рассмотрим поток сообщений стандартного TON Jetton:

-2

Вот поток сообщений TON Jetton. Что следует из диаграммы:

  1. Отправитель отправляет сообщение op::transfer на свой кошелек (sender_wallet)
  2. Sender_wallet уменьшает баланс токенов
  3. Sender_wallet отправляет сообщение op::internal_transfer на кошелек получателя (destination_wallet)
  4. Destination_wallet увеличивает баланс своих токенов
  5. Destination_wallet отправляет op::transfer_notification своему владельцу (пункт назначения)
  6. Destination_wallet возвращает избыточный газ с сообщением op::excesses на response_destination (обычно отправитель)

Обратите внимание, что если destination_wallet не смог обработать сообщение op::internal_transfer (произошло исключение или газ закончился), то эта часть и последующие шаги не будут выполнены.

Но первый шаг (умениение баланса в sender_wallet) будет завершен. Результатом является частичное исполнение сделки, непоследовательное состояние Jetton и, в данном случае, потеря денег.

В худшем случае все токены могут быть украдены таким образом. Представьте, что вы сначала накапливаете бонусы пользователю, а затем отправляете сообщение op::burn на его кошелек Jetton, но вы не можете гарантировать, что op::burn будет успешно обработан.

Always Draw Message Flow Diagrams

-3

В CertiK мы используем язык DOT для описания и обновления таких диаграмм в ходе аудита. Это то, что мы также рекомендуем разработчикам.

В отличие от смарт-контрактов Ethereum, где внешний вызов должен быть выполнен до продолжения выполнения, в TON внешний вызов - это сообщение, которое будет доставлено и обработано через некоторое время в некоторых новых условиях. Это требует гораздо большего внимания со стороны разработчика.

Avoid Fails and Catch Bounced Messages

  • Определите "точки входа" "группы контрактов".
  • Проверьте там полезную нагрузку, подачу газа, происхождение сообщения, чтобы свести к минимуму риск отказа.
  • В других обработчиках сообщений ("следствия") проверьте источник сообщения. Другие проверки являются "подтвердениями".
  • Не могу позволить себе потерпеть неудачу в "последствиях". Если может потерпеть неудачу - просмотрите поток сообщений.

Используя поток сообщений, сначала определите точку входа. Это сообщение, которое запускает каскад сообщений в вашей группе контрактов ("следствия"). Именно здесь необходимо проверить все (полезная нагрузка, газоснабжение и т. д.), чтобы свести к минимуму вероятность отказа на последующих этапах.

Если вы не уверены, удастся ли выполнить все ваши планы (например, есть ли у пользователя достаточно токенов для завершения сделки), это означает, что поток сообщений, вероятно, построен неправильно.

В последующих сообщениях (следствиях) все
throw_if()/throw_unless() будут играть роль утверждений, а не фактически проверять что-либо.

Processing bounced messages

Многие контракты также обрабатывают отбрасываемые сообщения на всякий случай.

Например, в TON Jetton, если кошелек получателя не может принимать какие-либо токены (это зависит от логики получения), то кошелек отправителя обработает возвращенное сообщение и вернет токены на свой собственный баланс.

-4

В целом, мы рекомендуем обрабатывать возвращенные сообщения, однако они не могут быть использованы в качестве средства полной защиты от неудачной обработки сообщений и неполного выполнения.

Во-первых, требуется газ, чтобы отправить возвращенное сообщение и обработать его, и если отправитель не предоставляет достаточного количества, то нет отскока.

-5

Expect a Man-in-the-Middle of the Message Flow

  • Каскад сообщений может быть обработан во многих блоках.
  • Предположим, что во время одного потока сообщений злоумышленник может инициировать второй параллельно.

То есть, если свойство было проверено в начале (например, достаточно ли у пользователя токенов), не предполагайте, что на третьем этапе того же контракта они все равно удовлетворят это свойство.

Use a Carry-Value Pattern

Мы пришли к самому важному совету: используйте Carry-Value Pattern.

В TON Jetton это демонстрируется:

  • sender_wallet subtracts the balance and sends it with an op::internal_transfer message to destination_wallet,
and it, in turn,
  • receives the balance with the message and adds it to its own balance (or bounces it back).
  • The value is transferred, not the message.

Can't get Jetton balance on-chain

Вы, вероятно, уже заметили, что реализация Jetton не позволяет вам узнать баланс Jetton в цепочке. Это потому, что такой вопрос не соответствует шаблону. К тому времени, когда ответ на сообщение op::get_balance достигнет заявителя, этот баланс уже мог быть израсходован кем-то.

Но давайте реализуем альтернативу:

-6
  1. Мастер отправляет сообщение op::provide_balance на кошелек
  2. Кошелек обнуляет свой баланс и отправляет обратно op::take_balance
  3. Мастер получает деньги, решает, достаточно ли у них, и либо использует их (снимает что-то взамен), либо отправляет их обратно на кошелек

Итак, это похоже на получение баланса.

Return Value Instead of Rejecting

Из модели переноса стоимости следует, что ваша группа контрактов часто будет получать не только запрос, но и запрос вместе со значением. Таким образом, вы не можете просто отказаться от выполнения запроса (через throw_unless()), вы должны отправить Jettons обратно отправителю.

-7

Например, типичный старт потока:

  1. Отправитель отправляет сообщение op:::transfer через sender_wallet на ваш_contract_wallet, указывая forward_ton_amount и forward_payload для вашего контракта.
  2. Sender_wallet отправляет сообщение op::internal_transfer на ваш_contract_wallet.
  3. Your_contract_wallet отправляет сообщение op::transfer_notification на ваш_контракт, доставляя forward_ton_amount, forward_payload, а также sender_address и jetton_amount.

    И здесь, в вашем контракте, в handle_transfer_notification() начинается поток.

Там вам нужно выяснить, какой это был запрос, достаточно ли газа для его выполнения и все ли правильно в полезной нагрузке.

На этом этапе вы не должны использовать throw(), потому что тогда Jettons будут просто потеряны, и запрос не будет выполнен. Стоит использовать утверждения try-catch. Если что-то не соответствует ожиданиям вашего контракта, Jettons должны быть возвращены.

Return jettons to sender_address

But be careful,

  • Отправить возвращающийся op::transfer на sender_address, а не на какие-либо реальные кошельки Jetton.
  • Вы не знаете, получили ли вы op::transfer_notification из реального кошелька или кто-то шутит.
  • Если ваш контракт получает неожиданные или неизвестные Jettons, они также должны быть возвращены.

Resume

  • TON требует от разработчика гораздо больше усилий по проектированию, чтобы избежать «неограниченных структур данных» и разрешить «бесконечную парадигму шардинга».
  • Даже при правильном дизайне и использовании "паттерна стоимости передачи" разработчик TON должен учитывать асинхронный характер сообщений TON.
  • Принятие Jettons требует тщательного анализа полезной нагрузки вручную и проверки условий.
  • Возвращает значение через sender_address, так как сообщения могут быть поддельны.
BitStake NEWS