Перейти к основному контенту

Разбор контракта ERC-721 на Vyper

Vyper
erc-721
Python
Для начинающих
Ори Померанц
1 апреля 2021 г.
18 минут на чтение

Введение

Стандарт ERC-721 используется для хранения прав собственности на невзаимозаменяемые токены (NFT). Токены ERC-20 ведут себя как товар, поскольку между отдельными токенами нет разницы. В отличие от них, токены ERC-721 предназначены для активов, которые похожи, но не идентичны, таких как различные мультяшные коты (opens in a new tab) или права собственности на различные объекты недвижимости.

В этой статье мы проанализируем контракт ERC-721 Рюи Накамуры (opens in a new tab). Этот контракт написан на Vyper (opens in a new tab), Python-подобном языке контрактов, разработанном таким образом, чтобы писать небезопасный код было сложнее, чем на Solidity.

Контракт

# @dev Реализация стандарта невзаимозаменяемого токена ERC-721.
# @author Ryuya Nakamura (@nrryuya)
# Изменено из: https://github.com/vyperlang/vyper/blob/de74722bf2d8718cca46902be165f9fe0e3641dd/examples/tokens/ERC721.vy

Комментарии в Vyper, как и в Python, начинаются с символа хеша (#) и продолжаются до конца строки. Комментарии, включающие @<keyword>, используются NatSpec (opens in a new tab) для создания удобочитаемой документации.

from vyper.interfaces import ERC721

implements: ERC721

Интерфейс ERC-721 встроен в язык Vyper. Вы можете посмотреть определение кода здесь (opens in a new tab). Определение интерфейса написано на Python, а не на Vyper, поскольку интерфейсы используются не только внутри блокчейна, но и при отправке транзакции в блокчейн от внешнего клиента, который может быть написан на Python.

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

Интерфейс ERC721Receiver

# Интерфейс для контракта, вызываемого safeTransferFrom()
interface ERC721Receiver:
    def onERC721Received(

ERC-721 поддерживает два типа перевода:

  • transferFrom, который позволяет отправителю указать любой адрес назначения и возлагает ответственность за перевод на отправителя. Это означает, что вы можете сделать перевод на недействительный адрес, и в этом случае NFT будет утерян навсегда.
  • safeTransferFrom, который проверяет, является ли адрес назначения контрактом. Если да, контракт ERC-721 спрашивает принимающий контракт, хочет ли он получить NFT.

Чтобы отвечать на запросы safeTransferFrom, принимающий контракт должен реализовать ERC721Receiver.

            _operator: address,
            _from: address,

Адрес _from — это текущий владелец токена. Адрес _operator — это тот, кто запросил перевод (эти два адреса могут не совпадать из-за разрешений).

            _tokenId: uint256,

Идентификаторы токенов ERC-721 имеют размер 256 бит. Обычно они создаются путем хеширования описания того, что представляет собой токен.

            _data: Bytes[1024]

Запрос может содержать до 1024 байт пользовательских данных.

        ) -> bytes32: view

Чтобы предотвратить случаи, когда контракт случайно принимает перевод, возвращаемое значение является не логическим (boolean), а 256-битным с определенным значением.

Эта функция является view, что означает, что она может читать состояние блокчейна, но не изменять его.

События

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

Это похоже на событие перевода (Transfer) ERC-20, за исключением того, что мы сообщаем tokenId вместо суммы. Никто не владеет нулевым адресом, поэтому по соглашению мы используем его для сообщения о создании и уничтожении токенов.

Одобрение (approval) ERC-721 похоже на разрешение (allowance) ERC-20. Определенному адресу разрешается переводить определенный токен. Это дает контрактам механизм реагирования при принятии токена. Контракты не могут прослушивать события, поэтому, если вы просто переведете им токен, они об этом не «узнают». Таким образом, владелец сначала отправляет одобрение, а затем отправляет запрос контракту: «Я одобрил для вас перевод токена X, пожалуйста, сделайте...».

Это архитектурное решение, чтобы сделать стандарт ERC-721 похожим на стандарт ERC-20. Поскольку токены ERC-721 не являются взаимозаменяемыми, контракт также может определить, что он получил определенный токен, посмотрев на принадлежность токена.

Иногда полезно иметь оператора, который может управлять всеми токенами аккаунта определенного типа (теми, которые управляются определенным контрактом), подобно доверенности. Например, я могу захотеть дать такие полномочия контракту, который проверяет, не связывался ли я с ним в течение шести месяцев, и если да, распределяет мои активы между моими наследниками (если один из них попросит об этом, контракты ничего не могут сделать без вызова транзакцией). В ERC-20 мы можем просто дать большое разрешение контракту наследования, но это не работает для ERC-721, потому что токены не являются взаимозаменяемыми. Это эквивалент.

Значение approved сообщает нам, предназначено ли событие для одобрения или для отзыва одобрения.

Переменные состояния

Эти переменные содержат текущее состояние токенов: какие из них доступны и кому они принадлежат. Большинство из них являются объектами HashMap, однонаправленными сопоставлениями, существующими между двумя типами (opens in a new tab).

# @dev Отображение ID NFT на адрес, который им владеет.
idToOwner: HashMap[uint256, address]

# @dev Отображение ID NFT на одобренный адрес.
idToApprovals: HashMap[uint256, address]

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

# @dev Отображение адреса владельца на количество его токенов.
ownerToNFTokenCount: HashMap[address, uint256]

Эта переменная хранит количество токенов для каждого владельца. Не существует сопоставления от владельцев к токенам, поэтому единственный способ определить токены, которыми владеет конкретный владелец, — это заглянуть в историю событий блокчейна и найти соответствующие события Transfer. Мы можем использовать эту переменную, чтобы узнать, когда у нас есть все NFT, и нам не нужно искать еще дальше во времени.

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

# @dev Отображение адреса владельца на отображение адресов операторов.
ownerToOperators: HashMap[address, HashMap[address, bool]]

У аккаунта может быть более одного оператора. Простого HashMap недостаточно для их отслеживания, потому что каждый ключ ведет к одному значению. Вместо этого вы можете использовать HashMap[address, bool] в качестве значения. По умолчанию значение для каждого адреса равно False, что означает, что он не является оператором. Вы можете устанавливать значения на True по мере необходимости.

# @dev Адрес чеканщика, который может чеканить токен
minter: address

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

# @dev Отображение id интерфейса на bool о том, поддерживается ли он
supportedInterfaces: HashMap[bytes32, bool]

# @dev ID интерфейса ERC-165 для ERC-165
ERC165_INTERFACE_ID: constant(bytes32) = 0x0000000000000000000000000000000000000000000000000000000001ffc9a7

# @dev ID интерфейса ERC-165 для ERC-721
ERC721_INTERFACE_ID: constant(bytes32) = 0x0000000000000000000000000000000000000000000000000000000080ac58cd

ERC-165 (opens in a new tab) определяет механизм, с помощью которого контракт может раскрывать, как приложения могут взаимодействовать с ним, и каким стандартам ERC он соответствует. В данном случае контракт соответствует ERC-165 и ERC-721.

Функции

Это функции, которые фактически реализуют ERC-721.

Конструктор

@external
def __init__():

В Vyper, как и в Python, функция-конструктор называется __init__.

    """
    @dev Конструктор контракта.
    """

В Python и в Vyper вы также можете создать комментарий, указав многострочную строку (которая начинается и заканчивается на """), и никак ее не использовать. Эти комментарии также могут включать NatSpec (opens in a new tab).

    self.supportedInterfaces[ERC165_INTERFACE_ID] = True
    self.supportedInterfaces[ERC721_INTERFACE_ID] = True
    self.minter = msg.sender

Для доступа к переменным состояния вы используете self.<variable name> (опять же, как в Python).

Функции просмотра (View)

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

@view
@external

Эти ключевые слова перед определением функции, которые начинаются со знака «собачка» (@), называются декораторами. Они определяют обстоятельства, при которых может быть вызвана функция.

  • @view указывает, что эта функция является функцией просмотра (view).
  • @external указывает, что эта конкретная функция может быть вызвана транзакциями и другими контрактами.
def supportsInterface(_interfaceID: bytes32) -> bool:

В отличие от Python, Vyper является языком со статической типизацией (opens in a new tab). Вы не можете объявить переменную или параметр функции без указания типа данных (opens in a new tab). В данном случае входным параметром является bytes32, 256-битное значение (256 бит — это собственный размер слова виртуальной машины Эфириума). На выходе получается логическое значение. По соглашению имена параметров функции начинаются с подчеркивания (_).

    """
    @dev Идентификация интерфейса указана в ERC-165.
    @param _interfaceID Id интерфейса
    """
    return self.supportedInterfaces[_interfaceID]

Возвращает значение из HashMap self.supportedInterfaces, которое устанавливается в конструкторе (__init__).

### ФУНКЦИИ ПРОСМОТРА ###

Это функции просмотра, которые делают информацию о токенах доступной для пользователей и других контрактов.

Эта строка утверждает (asserts) (opens in a new tab), что _owner не равен нулю. Если это так, возникает ошибка, и происходит откат операции.

В виртуальной машине Эфириума (EVM) любое хранилище, в котором не сохранено значение, равно нулю. Если по адресу _tokenId нет токена, то значение self.idToOwner[_tokenId] равно нулю. В этом случае происходит откат функции.

Обратите внимание, что getApproved может возвращать ноль. Если токен действителен, он возвращает self.idToApprovals[_tokenId]. Если нет одобряющего лица, это значение равно нулю.

Эта функция проверяет, разрешено ли _operator управлять всеми токенами _owner в этом контракте. Поскольку операторов может быть несколько, это двухуровневый HashMap.

Вспомогательные функции перевода

Эти функции реализуют операции, которые являются частью перевода токенов или управления ими.


### ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ПЕРЕВОДА ###

@view
@internal

Этот декоратор, @internal, означает, что функция доступна только из других функций в рамках того же контракта. По соглашению имена этих функций также начинаются с подчеркивания (_).

Существует три способа, с помощью которых адресу может быть разрешено перевести токен:

  1. Адрес является владельцем токена
  2. Адресу одобрено расходование этого токена
  3. Адрес является оператором для владельца токена

Приведенная выше функция может быть функцией просмотра (view), потому что она не изменяет состояние. Чтобы снизить эксплуатационные расходы, любая функция, которая может быть функцией просмотра, должна ею быть.

Когда возникает проблема с переводом, мы откатываем вызов.

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

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

Для генерации события в Vyper вы используете оператор log (подробнее см. здесь (opens in a new tab)).

Функции перевода

Эта функция позволяет вам сделать перевод на произвольный адрес. Если адрес не является пользователем или контрактом, который знает, как переводить токены, любой переведенный вами токен застрянет на этом адресе и станет бесполезным.

Сначала выполнить перевод — это нормально, потому что, если возникнет проблема, мы все равно сделаем откат, поэтому все, что было сделано в вызове, будет отменено.

    if _to.is_contract: # проверить, является ли `_to` адресом контракта

Сначала проверьте, является ли адрес контрактом (есть ли у него код). Если нет, предположите, что это адрес пользователя, и пользователь сможет использовать токен или перевести его. Но пусть это не усыпляет вашу бдительность ложным чувством безопасности. Вы можете потерять токены, даже с safeTransferFrom, если переведете их на адрес, для которого никто не знает приватный ключ.

        returnValue: bytes32 = ERC721Receiver(_to).onERC721Received(msg.sender, _from, _tokenId, _data)

Вызовите целевой контракт, чтобы узнать, может ли он получать токены ERC-721.

        # Вызывает откат, если место назначения перевода — контракт, который не реализует 'onERC721Received'
        assert returnValue == method_id("onERC721Received(address,address,uint256,bytes)", output_type=bytes32)

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

По соглашению, если вы хотите, чтобы у вас не было одобряющего лица, вы назначаете нулевой адрес, а не себя.

    # Проверить требования
    senderIsOwner: bool = self.idToOwner[_tokenId] == msg.sender
    senderIsApprovedForAll: bool = (self.ownerToOperators[owner])[msg.sender]
    assert (senderIsOwner or senderIsApprovedForAll)

Чтобы установить одобрение, вы можете быть либо владельцем, либо оператором, уполномоченным владельцем.

Чеканка новых токенов и уничтожение существующих

Аккаунт, создавший контракт, является minter — суперпользователем, который уполномочен чеканить новые NFT. Однако даже ему не разрешено сжигать существующие токены. Это может сделать только владелец или сущность, уполномоченная владельцем.

### ФУНКЦИИ ЧЕКАНКИ И СЖИГАНИЯ ###

@external
def mint(_to: address, _tokenId: uint256) -> bool:

Эта функция всегда возвращает True, потому что в случае сбоя операции происходит откат.

Только чеканщик (аккаунт, создавший контракт ERC-721) может чеканить новые токены. Это может стать проблемой в будущем, если мы захотим изменить личность чеканщика. В рабочем контракте вам, вероятно, понадобится функция, которая позволяет чеканщику передавать привилегии чеканщика кому-то другому.

    # Вызывает откат, если `_to` — нулевой адрес
    assert _to != ZERO_ADDRESS
    # Добавить NFT. Вызывает откат, если `_tokenId` принадлежит кому-либо
    self._addTokenTo(_to, _tokenId)
    log Transfer(ZERO_ADDRESS, _to, _tokenId)
    return True

По соглашению чеканка новых токенов считается переводом с нулевого адреса.

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

Использование этого контракта

В отличие от Solidity, в Vyper нет наследования. Это осознанное архитектурное решение, чтобы сделать код более понятным и, следовательно, более простым для обеспечения безопасности. Поэтому, чтобы создать свой собственный контракт ERC-721 на Vyper, вы берете этот контракт (opens in a new tab) и модифицируете его для реализации нужной вам бизнес-логики.

Заключение

Для повторения, вот некоторые из наиболее важных идей в этом контракте:

  • Чтобы получать токены ERC-721 с помощью безопасного перевода, контракты должны реализовывать интерфейс ERC721Receiver.
  • Даже если вы используете безопасный перевод, токены все равно могут застрять, если вы отправите их на адрес, приватный ключ которого неизвестен.
  • Когда возникает проблема с операцией, хорошей идеей будет revert вызов, а не просто возвращать значение ошибки.
  • Токены ERC-721 существуют, когда у них есть владелец.
  • Существует три способа получить разрешение на перевод NFT. Вы можете быть владельцем, получить одобрение для определенного токена, или быть оператором для всех токенов владельца.
  • Прошлые события видны только за пределами блокчейна. Код, работающий внутри блокчейна, не может их просматривать.

Теперь идите и реализуйте безопасные контракты на Vyper.

Смотрите здесь больше моих работ (opens in a new tab).