Узнайте больше о том, как разрешения могут защитить ваши смарт-контракты Tezos

Одна из самых интересных особенностей смарт-контрактов заключается в том, что они могут предоставлять сложные права доступа своим пользователям. Разработчики смарт-контрактов могут позволить любому использовать свои контракты или установить условия относительно того, кто имеет доступ к определенным функциям контрактов. Один из самых простых способов сделать это — жестко закодировать условия, которые должны быть выполнены, чтобы позволить конкретному пользователю вызывать определенную функцию контракта, например, с помощью инструкции Майкельсона IF .

Однако вы также можете предоставить своим пользователям возможность разрешить или отклонить определенную транзакцию, сохранив ее в большой карте в контракте для дальнейшего использования. Это то, что делают разрешения, изложенные в стандарте ТЗИП-17. Вкратце, разрешения утверждают транзакции заранее и фиксируют эти утверждения в контракте для последующей обработки третьими сторонами.

Хотя разрешения могут быть немного сложными в реализации, они являются мощным инструментом для динамической авторизации транзакций заранее. В этой статье вы узнаете больше о стандарте и о том, как вы можете реализовать разрешения на уровне контракта с Ligo, а также о том, как вы можете создать разрешение с Taquito.

Примечание: первая часть этой статьи представляет собой чрезмерное упрощение того, как работает ТЗИП-17, чтобы сделать эту информацию доступной для более широкой аудитории. Если вас интересуют тонкости разрешений на Tezos, вам следует ознакомиться с официальной документацией.
Вторая и третья части требуют базовых знаний Ligo и TypeScript, чтобы понять реализацию разрешений в этих двух языки.

Стандарт ТЗИП-17

Простой пример

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

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

  1. Алиса покупает 100 xTokens за 100 долларов США у стороннего сервиса (TPS)
  2. Алиса хочет передать 50 xTokens Бобу, она подделывает транзакцию передачи, подписывает ее и предоставляет в качестве разрешения TPS.
  3. TPS записывает разрешение в контракт xToken.
  4. Затем TPS вводит операцию, оплачивает соответствующую комиссию XTZ, и 50 xToken переводятся Бобу со счета Алисы.

Как видите, Алисе не нужны никакие XTZ, так как она не взаимодействует напрямую с блокчейном Tezos и делегирует внедрение и оплату транзакции третьей стороне. Это вид взаимодействия, который разрешает разрешить.

В отличие от разрешений в ТЗИП-7 или операторов в ТЗИП-12, в данном конкретном случае пользователь не разрешает передавать определенное количество токенов, а целая сделка. В случае с токеном FA1.2 Алиса могла установить сторонний сервис в качестве оператора и разрешить передачу 50 xToken Бобу, но мошенническая третья сторона могла перевести это количество токенов себе, а не Бобу. Ситуация усугубляется для токенов FA2, где третьим сторонам разрешено переводить любую сумму, которую они хотят. Разрешения гарантируют, что пользователи разрешают всю транзакцию со всеми ее параметрами, а не один параметр более сложной транзакции.

Характеристики стандарта

Разрешение — это не что иное, как несколько параметров, упакованных в байты, связанные с адресом, который их разрешил, и записанные в контракте. Использование разрешения обычно состоит из нескольких шагов:

  1. Параметры точки входа, использующей разрешения, упаковываются и хешируются отправителем разрешения.
  2. Выходной хэш увязывается с 3-мя другими параметрами для обеспечения сохранности разрешения: идентификатор цепочки, адрес контракта и счетчик в хранилище контрактов.
  3. Затем эти 4 значения упаковываются и подписываются пользователем перед отправкой в ​​контракт.
  4. Контракт подтверждает, что предоставленный адрес tz действительно подписал разрешение, и записывает разрешение в большую карту с соответствующим адресом.
  5. Когда контракт получает транзакцию к точке входа, реализующей разрешения, он упаковывает и хеширует параметры и ищет соответствующее разрешение. Если он найден, транзакция авторизуется.

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

Примеры в цепочке

В настоящее время в основной сети очень мало контрактов, реализующих TZIP-17. Один из них — контракт Quartz FA2 от Ubisoft. Вы также можете ознакомиться с Контрактом FA2 от Rarible.

В ханчжоунете вы можете найти контракт стейблкоина, созданный TQ Tezos в качестве демонстрации TZIP-17.

Реализация в Лиго

Для реализации в Ligo требуется как минимум 2 шага: во-первых, специальная точка входа должна взять разрешение и другие необходимые данные для проверки разрешения и записи его в договоре. Затем одна или несколько точек входа контракта упаковывают свои предоставленные аргументы при отправке транзакции, чтобы проверить, существует ли разрешение для этой конкретной транзакции и является ли разрешение все еще действительным.

Стандарт ТЗИП-17 позволяет разработчикам подавать разрешение и разрешенные параметры за один или два шага. Для краткости мы будем использовать единую точку входа для предоставления как разрешения, так и параметров авторизованной транзакции.

Согласно стандарту, точка входа, получающая новые разрешения, должна называться permit или permit_, за которой следует имя по вашему выбору. Мы также можем предоставить одно разрешение или список разрешений. Мы сделаем последнее в этом уроке.

Вот подпись нашей точки входа permit:

Точка входа permit принимает список разрешений. Каждое разрешение представлено вложенной парой с ключом пользователя слева и парой справа, содержащей четыре обязательных параметра, хешированных и подписанных слева, и хэш параметров транзакции справа.

Поскольку мы предоставляем список в качестве параметра точки входа, первое, что нужно сделать, это перебрать элементы списка и соответствующим образом обновить хранилище:

Мы можем использовать List.fold для просмотра списка и записи каждого разрешения. Функция принимает 3 параметра:

  1. Функция для запуска для каждого элемента списка
  2. Список для использования
  3. Аккумулятор, то есть значение, которое будет изменено на текущей итерации цикла и возвращено для использования в следующей итерации.

Свернутая функция (первый параметр) принимает 2 параметра:

  1. Аккумулятор
  2. Текущий элемент списка

Теперь давайте напишем свернутую функцию! Его цель состоит в том, чтобы сначала проверить правильность разрешения, а затем убедиться, что разрешение еще не существует, прежде чем сохранить его в permits bigmap.

Чтобы проверить разрешение, мы должны сами упаковать четыре обязательных параметра, чтобы сверить их с параметрами, предоставленными пользователем:

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

Далее упорядочиваем данные разрешения в виде пары из двух пар: левая пара содержит идентификатор цепочки и адрес контракта, правая пара содержит текущий счетчик и хеш параметров авторизованной транзакции. Эта пара затем упаковывается с использованием метода Bytes.pack. Теперь, когда у нас есть этот хэш, мы можем убедиться, что адрес, который подписал предоставленный хэш, совпадает с адресом, указанным в параметрах:

Сейчас могут произойти две вещи:

  • Crypto.check возвращает true и мы точно знаем, что адрес, связанный с разрешением, также является адресом, который его подписал, так что все в порядке
  • Crypto.check возвращает false, что означает, что разрешение подписано другим адресом. Если это так, стандарт TZIP-17 указывает, что мы должны вернуть пользователю конкретное сообщение, пару с кодом MISSIGNED слева и ожидаемые байты справа:

Примечание: на момент написания в Ligo невозможно вернуть пару после failwith. Однако можно использовать код Майкельсона непосредственно в Ligo, что имеет место здесь, чтобы следовать стандарту и возвращать требуемую пару.

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

Во-первых, поскольку мы храним разрешение в большой карте, ключи которой представляют собой пары типа address * bytes, мы должны получить адрес из ключа, предоставленного вместе с разрешением. Это может быть немного сложно сделать с Ligo:

Мы начинаем с хеширования ключа, чтобы получить значение key_hash. Tezos.implicit_account превращает хэш ключа в значение типа unit contract. Когда оно у нас есть, мы можем использовать Tezos.address, чтобы преобразовать его в значение типа address.

Как только это будет сделано, мы можем искать разрешение на большой карте permits:

Если мы не находим разрешения, мы можем записать разрешение в бигмап permits и увеличить счетчик в контракте на 1.

Если в бигмапе уже записано разрешение, мы хотим проверить, не истек ли срок действия разрешения. Стандарт TZIP-17 позволяет пользователям устанавливать собственную дату истечения срока действия для всех своих разрешений или для одного разрешения в 2 разных больших картах. Чтобы упростить проверку даты истечения срока действия, мы можем написать небольшую функцию, которая ищет разрешение в этих двух разных больших картах:

Функция get_expiry просматривает большую карту permit_expiries, чтобы проверить, есть ли у пользователя настраиваемая дата истечения срока действия этого разрешения. Если он есть, он возвращается. Если нет, функция просматривает большую карту user_expiries и проверяет, установил ли пользователь пользовательскую дату истечения срока действия для всех своих разрешений. Если он есть, он возвращается, в противном случае возвращается дата истечения срока действия по умолчанию.

Если срок действия ранее сохраненного в контракте разрешения еще не истек, мы возвращаем сообщение об ошибке DUP_PERMIT и делаем контракт недействительным. Если предыдущий дубликат разрешения истек, мы заменяем существующее разрешение новым, чтобы продлить его действие.

И все, мы успешно сохранили новое разрешение в контракте 🥳
Теперь посмотрим, что происходит, когда транзакция, соответствующая разрешению, отправляется в контракт.

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

Теперь мы можем использовать значение, возвращенное функцией и сохраненное в is_transfer_authorized, чтобы разрешить или запретить транзакцию.

Вот что делает функция transfer_presigned:

Сначала функция хэширует параметры точки входа перевода с помощью алгоритма Blake2B, чтобы получить хэш, который она использует в следующей строке для поиска разрешения в permits bigmap.

Затем он проверяет отметку времени, записанную с разрешением. Если разрешение не найдено, возвращается временная метка со значением 0, поэтому мы проверяем значение временной метки. Если есть отметка времени, не равная 0, мы проверяем, что срок действия разрешения не истек, и сначала смотрим на permit_expiries, прежде чем искать user_expiries, если в первой большой карте не установлен срок действия.

Если дата истечения срока действия вообще не найдена, операция завершается с ошибкой NO_EXPIRY_FOUND. Если он найден, функция возвращает true, и разрешение расходуется, т. е. удаляется из большой карты permits.

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

Реализация на TypeScript с Taquito

Разрешения — это функция смарт-контрактов на Tezos, поэтому их невозможно использовать в JavaScript или TypeScript, но вы можете создать и отформатировать разрешение на внешнем интерфейсе, прежде чем отправлять его в контракт, который реализует TZIP-17. Тем не менее, правильное форматирование разрешения перед его отправкой в ​​контракт для регистрации может быть немного сложным.

Пока команда Taquito работает над пакетом TZIP-17, чтобы упростить этот процесс, вы можете создать разрешение в TypeScript с помощью Taquito, прежде чем включать его в транзакцию.

Примечание: следующий код предполагает, что вы настроили экземпляр Taquito TezosToolkit для взаимодействия с выбранной вами сетью.

Во-первых, нам нужно создать абстракцию контракта для контракта, который получит нашу транзакцию с разрешением:

После этого нам нужно значение и тип для параметров перевода:

В этом примере methodName равно "transfer”, но вы можете заменить его именем точки входа, на которую нацелена операция.

Далее нам нужно упаковать параметры вызова точки входа, используя метод packData, доступный в методе rpc класса TezosToolkit:

После упаковки нам нужно хешировать упакованные параметры с помощью алгоритма Blake2B:

Этот шаг требует использования пакета blake2b, а также 2 методов из пакета @taquito/utils: buf2hex и hex2buf.

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

Мы предоставляем параметры в паре и их тип методу packDataBytes из пакета @taquito/michel-codec. Это возвращает байты, которые вы затем можете подписать с помощью подписавшего по вашему выбору (InMemorySigner или кошелек).

Теперь у вас есть все данные, необходимые для оформления сделки и отправки разрешения на контракт!

Заключение

TZIP-17 допускает более сложные меры безопасности на уровне контракта, что, в свою очередь, позволяет более сложное взаимодействие со смарт-контрактами на блокчейне Tezos. Параметры, включенные в данные разрешения (идентификатор цепочки, адрес контракта и счетчик, присутствующий в контракте), обеспечивают безопасность разрешений и предотвращают, среди прочего, дублирование разрешений или использование уже использованных разрешений.

Основное преимущество разрешений по сравнению с другими функциями, реализованными в различных стандартах, таких как TZIP-7 или TZIP-12, заключается в том, что разрешения позволяют пользователям авторизовать полные транзакции, а не отдельные параметры транзакций. Разрешения также могут быть добавлены к любой точке входа контракта, чтобы разрешить отправку определенных транзакций в эту точку входа.

Хотя TZIP-17 включает строгие правила создания разрешений, он сохраняет определенную гибкость в отношении того, какую функцию разрешает разрешение и на какой срок. Пользователи разрешений могут устанавливать даты истечения срока действия для определенных разрешений или всех разрешений, которые они выдают. Это также делает разрешения функцией безопасности, жесткой в ​​отношении потенциальных злоумышленников, но гибкой для тех, кто выдает разрешения.