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

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

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

Введение

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

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

Контракт

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

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

from vyper.interfaces import ERC721

implements: ERC721

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

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

#pragma version >0.3.10
#pragma version >0.3.10

Интерфейс 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 байт пользовательских данных.

        ) -> bytes4: nonpayable

Чтобы предотвратить случаи, когда контракт случайно принимает перевод, возвращаемое значение является не логическим, а конкретным четырехбайтовым значением — селектором функции onERC721Received. Функция является nonpayable, поскольку принимающий контракт может изменить свое собственное состояние при принятии токена.

События

События генерируются для информирования пользователей и серверов за пределами блокчейна о происходящем. Обратите внимание, что содержимое событий недоступно контрактам в блокчейне. Три события ERC-721 определены в импортированном нами интерфейсе IERC721, поэтому данный контракт не объявляет их сам; он генерирует их с помощью log IERC721.<Event>(...), как мы увидим в функциях перевода ниже.

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

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

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

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

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

# @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 Статический список поддерживаемых идентификаторов интерфейсов ERC165
SUPPORTED_INTERFACES: constant(bytes4[2]) = [
    # Идентификатор интерфейса ERC165 для ERC165
    0x01ffc9a7,
    # Идентификатор интерфейса ERC165 для ERC721
    0x80ac58cd,
]

ERC-165 (открывается в новой вкладке) определяет механизм, с помощью которого контракт может раскрывать, как приложения могут взаимодействовать с ним и каким стандартам ERC он соответствует. SUPPORTED_INTERFACES — это константный список из двух четырехбайтовых идентификаторов интерфейсов, которым соответствует этот контракт: самого ERC-165 и ERC-721.

Функции

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

Конструктор

@deploy
def __init__():

В Vyper, как и в Python, функция-конструктор называется __init__. Она помечена декоратором @deploy, что означает, что она запускается один раз при развертывании контракта.

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

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

    self.minter = msg.sender

Для доступа к переменным состояния вы используете self.<имя переменной> (опять же, как в Python). Конструктор записывает аккаунт, развернувший контракт, как minter (чеканщика).

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

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

@view
@external

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

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

В отличие от Python, Vyper является языком со статической типизацией (открывается в новой вкладке). Вы не можете объявить переменную или параметр функции без указания типа данных (открывается в новой вкладке). В данном случае входным параметром является bytes4, четырехбайтовое значение, а выходным — логическое значение.

    """
    @dev Идентификация интерфейса указана в ERC-165.
    @param interface_id Идентификатор интерфейса
    """
    return interface_id in SUPPORTED_INTERFACES

Возвращает True, если interface_id является одним из идентификаторов интерфейсов в списке SUPPORTED_INTERFACES.

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

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

Эта строка утверждает (открывается в новой вкладке), что _owner не является нулевым адресом, записанным как empty(address). Если это так, возникает ошибка, и операция откатывается.

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

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

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

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

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


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

@view
@internal

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

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

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

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

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

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

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

Для генерации события в Vyper вы используете оператор log (подробнее см. здесь (открывается в новой вкладке)). Поскольку события принадлежат импортированному интерфейсу, мы ссылаемся на них как на IERC721.Transfer и передаем их поля по ключевому слову.

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

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

Декоратор @payable находится здесь, потому что интерфейс IERC721 объявляет transferFrom, safeTransferFrom и approve как payable, поэтому контракт, реализующий интерфейс, должен соответствовать этим сигнатурам.

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

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

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

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

Вызовите целевой контракт, чтобы узнать, может ли он получать токены ERC-721. Vyper 0.4 требует, чтобы вызовы других контрактов были помечены, поэтому вызову предшествует extcall.

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

Если местом назначения является контракт, но он не принимает токены 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, вы берете этот контракт (открывается в новой вкладке) и модифицируете его для реализации нужной вам бизнес-логики.

Заключение

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

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

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

Смотрите здесь больше моих работ (открывается в новой вкладке).