Перейти к основному содержанию

Пошаговый разбор контракта Vyper ERC-721

Vyper
erc-721
Python
Beginner
Ori Pomerantz
1 апреля 2021 г.
18 минута прочтения

Введение

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

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

Контракт

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

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

1from vyper.interfaces import ERC721
2
3implements: ERC721

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

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

Интерфейс ERC721Receiver

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

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

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

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

1 _operator: address,
2 _from: address,

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

1 _tokenId: uint256,

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

1 _data: Bytes[1024]

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

1 ) -> bytes32: view

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

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

События

События (opens in a new tab) создаются для информирования пользователей и серверов за пределами блокчейна о событиях. Обратите внимание, что содержимое событий недоступно контрактам в блокчейне.

1# @dev Создается, когда право собственности на любой NFT изменяется любым механизмом. Это событие создается при создании NFT (`from` == 0) и их уничтожении (`to` == 0). Исключение: во время создания контракта любое
2# количество NFT может быть создано и назначено без создания события Transfer. Во время любого
3# перевода одобренный адрес для этого NFT (если он есть) сбрасывается.
4# @param _from Отправитель NFT (если адрес нулевой, это указывает на создание токена).
5# @param _to Получатель NFT (если адрес нулевой, это указывает на уничтожение токена).
6# @param _tokenId Переданный NFT.
7event Transfer:
8 sender: indexed(address)
9 receiver: indexed(address)
10 tokenId: indexed(uint256)
Показать все

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

1# @dev Создается, когда одобренный адрес для NFT изменяется или подтверждается. Нулевой
2# адрес указывает на отсутствие одобренного адреса. Когда создается событие Transfer, это также
3# указывает, что одобренный адрес для этого NFT (если таковой имеется) сбрасывается.
4# @param _owner Владелец NFT.
5# @param _approved Адрес, который мы одобряем.
6# @param _tokenId NFT, который мы одобряем.
7event Approval:
8 owner: indexed(address)
9 approved: indexed(address)
10 tokenId: indexed(uint256)
Показать все

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

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

1# @dev Это событие создается, когда оператор включается или отключается для владельца. Оператор может управлять
2# всеми NFT владельца.
3# @param _owner Владелец NFT.
4# @param _operator Адрес, которому мы устанавливаем права оператора.
5# @param _approved Статус прав оператора (true, если права оператора предоставлены, и false, если
6# они отозваны).
7event ApprovalForAll:
8 owner: indexed(address)
9 operator: indexed(address)
10 approved: bool
Показать все

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

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

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

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

1# @dev Сопоставление идентификатора NFT с адресом его владельца.
2idToOwner: HashMap[uint256, address]
3
4# @dev Сопоставление идентификатора NFT с одобренным адресом.
5idToApprovals: HashMap[uint256, address]

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

1# @dev Сопоставление адреса владельца с количеством его токенов.
2ownerToNFTokenCount: HashMap[address, uint256]

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

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

1# @dev Сопоставление адреса владельца с сопоставлением адресов операторов.
2ownerToOperators: HashMap[address, HashMap[address, bool]]

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

1# @dev Адрес минтера, который может создавать токен
2minter: address

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

1# @dev Сопоставление идентификатора интерфейса с логическим значением о том, поддерживается он или нет
2supportedInterfaces: HashMap[bytes32, bool]
3
4# @dev Идентификатор интерфейса ERC165 для ERC165
5ERC165_INTERFACE_ID: constant(bytes32) = 0x0000000000000000000000000000000000000000000000000000000001ffc9a7
6
7# @dev Идентификатор интерфейса ERC165 для ERC721
8ERC721_INTERFACE_ID: constant(bytes32) = 0x0000000000000000000000000000000000000000000000000000000080ac58cd

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

Функции

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

Конструктор

1@external
2def __init__():

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

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

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

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

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

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

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

1@view
2@external

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

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

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

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

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

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

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

1@view
2@external
3def balanceOf(_owner: address) -> uint256:
4 """
5 @dev Возвращает количество NFT, принадлежащих `_owner`.
6 Вызывает исключение, если `_owner` является нулевым адресом. NFT, назначенные нулевому адресу, считаются недействительными.
7 @param _owner Адрес, для которого запрашивается баланс.
8 """
9 assert _owner != ZERO_ADDRESS
Показать все

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

1 return self.ownerToNFTokenCount[_owner]
2
3@view
4@external
5def ownerOf(_tokenId: uint256) -> address:
6 """
7 @dev Возвращает адрес владельца NFT.
8 Вызывает исключение, если `_tokenId` не является действительным NFT.
9 @param _tokenId Идентификатор для NFT.
10 """
11 owner: address = self.idToOwner[_tokenId]
12 # Вызывает исключение, если `_tokenId` не является действительным NFT
13 assert owner != ZERO_ADDRESS
14 return owner
Показать все

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

1@view
2@external
3def getApproved(_tokenId: uint256) -> address:
4 """
5 @dev Получает одобренный адрес для одного NFT.
6 Вызывает исключение, если `_tokenId` не является действительным NFT.
7 @param _tokenId Идентификатор NFT для запроса его одобрения.
8 """
9 # Вызывает исключение, если `_tokenId` не является действительным NFT
10 assert self.idToOwner[_tokenId] != ZERO_ADDRESS
11 return self.idToApprovals[_tokenId]
Показать все

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

1@view
2@external
3def isApprovedForAll(_owner: address, _operator: address) -> bool:
4 """
5 @dev Проверяет, является ли `_operator` одобренным оператором для `_owner`.
6 @param _owner Адрес, которому принадлежат NFT.
7 @param _operator Адрес, который действует от имени владельца.
8 """
9 return (self.ownerToOperators[_owner])[_operator]
Показать все

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

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

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

1
2### ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ПЕРЕВОДА ###
3
4@view
5@internal

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

1def _isApprovedOrOwner(_spender: address, _tokenId: uint256) -> bool:
2 """
3 @dev Возвращает, может ли данный тратящий перевести данный идентификатор токена
4 @param spender адрес тратящего для запроса
5 @param tokenId uint256 ID токена для перевода
6 @return bool, является ли msg.sender одобренным для данного ID токена,
7 является ли оператором владельца, или является владельцем токена
8 """
9 owner: address = self.idToOwner[_tokenId]
10 spenderIsOwner: bool = owner == _spender
11 spenderIsApproved: bool = _spender == self.idToApprovals[_tokenId]
12 spenderIsApprovedForAll: bool = (self.ownerToOperators[owner])[_spender]
13 return (spenderIsOwner or spenderIsApproved) or spenderIsApprovedForAll
Показать все

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

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

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

1@internal
2def _addTokenTo(_to: address, _tokenId: uint256):
3 """
4 @dev Добавляет NFT на данный адрес
5 Вызывает исключение, если `_tokenId` принадлежит кому-то.
6 """
7 # Вызывает исключение, если `_tokenId` принадлежит кому-то
8 assert self.idToOwner[_tokenId] == ZERO_ADDRESS
9 # Изменение владельца
10 self.idToOwner[_tokenId] = _to
11 # Изменение отслеживания количества
12 self.ownerToNFTokenCount[_to] += 1
13
14
15@internal
16def _removeTokenFrom(_from: address, _tokenId: uint256):
17 """
18 @dev Удаляет NFT с данного адреса
19 Вызывает исключение, если `_from` не является текущим владельцем.
20 """
21 # Вызывает исключение, если `_from` не является текущим владельцем
22 assert self.idToOwner[_tokenId] == _from
23 # Изменение владельца
24 self.idToOwner[_tokenId] = ZERO_ADDRESS
25 # Изменение отслеживания количества
26 self.ownerToNFTokenCount[_from] -= 1
Показать все

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

1@internal
2def _clearApproval(_owner: address, _tokenId: uint256):
3 """
4 @dev Очищает одобрение для данного адреса
5 Вызывает исключение, если `_owner` не является текущим владельцем.
6 """
7 # Вызывает исключение, если `_owner` не является текущим владельцем
8 assert self.idToOwner[_tokenId] == _owner
9 if self.idToApprovals[_tokenId] != ZERO_ADDRESS:
10 # Сброс одобрений
11 self.idToApprovals[_tokenId] = ZERO_ADDRESS
Показать все

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

1@internal
2def _transferFrom(_from: address, _to: address, _tokenId: uint256, _sender: address):
3 """
4 @dev Выполнение перевода NFT.
5 Вызывает исключение, если `msg.sender` не является текущим владельцем, авторизованным оператором или одобренным
6 адресом для этого NFT. (ПРИМЕЧАНИЕ: `msg.sender` не разрешен в приватной функции, поэтому передайте `_sender`.)
7 Вызывает исключение, если `_to` является нулевым адресом.
8 Вызывает исключение, если `_from` не является текущим владельцем.
9 Вызывает исключение, если `_tokenId` не является действительным NFT.
10 """
Показать все

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

1 # Проверка требований
2 assert self._isApprovedOrOwner(_sender, _tokenId)
3 # Вызывает исключение, если `_to` является нулевым адресом
4 assert _to != ZERO_ADDRESS
5 # Очистка одобрения. Вызывает исключение, если `_from` не является текущим владельцем
6 self._clearApproval(_from, _tokenId)
7 # Удаление NFT. Вызывает исключение, если `_tokenId` не является действительным NFT
8 self._removeTokenFrom(_from, _tokenId)
9 # Добавление NFT
10 self._addTokenTo(_to, _tokenId)
11 # Логирование перевода
12 log Transfer(_from, _to, _tokenId)
Показать все

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

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

1
2### ФУНКЦИИ ПЕРЕВОДА ###
3
4@external
5def transferFrom(_from: address, _to: address, _tokenId: uint256):
6 """
7 @dev Вызывает исключение, если `msg.sender` не является текущим владельцем, авторизованным оператором или одобренным
8 адресом для этого NFT.
9 Вызывает исключение, если `_from` не является текущим владельцем.
10 Вызывает исключение, если `_to` является нулевым адресом.
11 Вызывает исключение, если `_tokenId` не является действительным NFT.
12 @notice Вызывающий несет ответственность за подтверждение того, что `_to` способен получать NFT, иначе
13 они могут быть навсегда потеряны.
14 @param _from Текущий владелец NFT.
15 @param _to Новый владелец.
16 @param _tokenId NFT для перевода.
17 """
18 self._transferFrom(_from, _to, _tokenId, msg.sender)
Показать все

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

1@external
2def safeTransferFrom(
3 _from: address,
4 _to: address,
5 _tokenId: uint256,
6 _data: Bytes[1024]=b""
7 ):
8 """
9 @dev Передает право собственности на NFT с одного адреса на другой.
10 Вызывает исключение, если `msg.sender` не является текущим владельцем, авторизованным оператором или
11 одобренным адресом для этого NFT.
12 Вызывает исключение, если `_from` не является текущим владельцем.
13 Вызывает исключение, если `_to` — нулевой адрес.
14 Вызывает исключение, если `_tokenId` не является действительным NFT.
15 Если `_to` — это смарт-контракт, он вызывает `onERC721Received` для `_to` и вызывает исключение, если
16 возвращаемое значение не равно `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`.
17 ПРИМЕЧАНИЕ: bytes4 представлен как bytes32 с дополнением
18 @param _from Текущий владелец NFT.
19 @param _to Новый владелец.
20 @param _tokenId NFT для перевода.
21 @param _data Дополнительные данные без определенного формата, отправляемые при вызове `_to`.
22 """
23 self._transferFrom(_from, _to, _tokenId, msg.sender)
Показать все

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

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

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

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

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

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

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

1@external
2def approve(_approved: address, _tokenId: uint256):
3 """
4 @dev Устанавливает или подтверждает одобренный адрес для NFT. Нулевой адрес указывает на отсутствие одобренного адреса.
5 Вызывает исключение, если `msg.sender` не является текущим владельцем NFT или авторизованным оператором текущего владельца.
6 Вызывает исключение, если `_tokenId` не является действительным NFT. (ПРИМЕЧАНИЕ: этого нет в EIP)
7 Вызывает исключение, если `_approved` является текущим владельцем. (ПРИМЕЧАНИЕ: этого нет в EIP)
8 @param _approved Адрес, который должен быть одобрен для данного идентификатора NFT.
9 @param _tokenId Идентификатор токена, который должен быть одобрен.
10 """
11 owner: address = self.idToOwner[_tokenId]
12 # Вызывает исключение, если `_tokenId` не является действительным NFT
13 assert owner != ZERO_ADDRESS
14 # Вызывает исключение, если `_approved` является текущим владельцем
15 assert _approved != owner
Показать все

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

1 # Проверка требований
2 senderIsOwner: bool = self.idToOwner[_tokenId] == msg.sender
3 senderIsApprovedForAll: bool = (self.ownerToOperators[owner])[msg.sender]
4 assert (senderIsOwner or senderIsApprovedForAll)

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

1 # Установка одобрения
2 self.idToApprovals[_tokenId] = _approved
3 log Approval(owner, _approved, _tokenId)
4
5
6@external
7def setApprovalForAll(_operator: address, _approved: bool):
8 """
9 @dev Включает или отключает одобрение для третьей стороны ("оператора") для управления всеми
10 активами `msg.sender`. Также генерирует событие ApprovalForAll.
11 Вызывает исключение, если `_operator` является `msg.sender`. (ПРИМЕЧАНИЕ: этого нет в EIP)
12 @notice Это работает, даже если отправитель не владеет никакими токенами в данный момент.
13 @param _operator Адрес для добавления в набор авторизованных операторов.
14 @param _approved True, если оператор одобрен, false — для отзыва одобрения.
15 """
16 # Вызывает исключение, если `_operator` является `msg.sender`
17 assert _operator != msg.sender
18 self.ownerToOperators[msg.sender][_operator] = _approved
19 log ApprovalForAll(msg.sender, _operator, _approved)
Показать все

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

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

1### ФУНКЦИИ СОЗДАНИЯ И СЖИГАНИЯ ###
2
3@external
4def mint(_to: address, _tokenId: uint256) -> bool:

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

1 """
2 @dev Функция для создания токенов
3 Вызывает исключение, если `msg.sender` не является минтером.
4 Вызывает исключение, если `_to` является нулевым адресом.
5 Вызывает исключение, если `_tokenId` уже принадлежит кому-то.
6 @param _to Адрес, который получит созданные токены.
7 @param _tokenId Идентификатор токена для создания.
8 @return Логическое значение, которое указывает, была ли операция успешной.
9 """
10 # Вызывает исключение, если `msg.sender` не является минтером
11 assert msg.sender == self.minter
Показать все

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

1 # Вызывает исключение, если `_to` является нулевым адресом
2 assert _to != ZERO_ADDRESS
3 # Добавление NFT. Вызывает исключение, если `_tokenId` уже принадлежит кому-то
4 self._addTokenTo(_to, _tokenId)
5 log Transfer(ZERO_ADDRESS, _to, _tokenId)
6 return True

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

1
2@external
3def burn(_tokenId: uint256):
4 """
5 @dev Сжигает определенный токен ERC721.
6 Вызывает исключение, если `msg.sender` не является текущим владельцем, авторизованным оператором или одобренным
7 адресом для этого NFT.
8 Вызывает исключение, если `_tokenId` не является действительным NFT.
9 @param _tokenId uint256 id токена ERC721 для сжигания.
10 """
11 # Проверка требований
12 assert self._isApprovedOrOwner(msg.sender, _tokenId)
13 owner: address = self.idToOwner[_tokenId]
14 # Вызывает исключение, если `_tokenId` не является действительным NFT
15 assert owner != ZERO_ADDRESS
16 self._clearApproval(owner, _tokenId)
17 self._removeTokenFrom(owner, _tokenId)
18 log Transfer(owner, ZERO_ADDRESS, _tokenId)
Показать все

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

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

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

Заключение

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

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

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

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

Последнее обновление страницы: 22 августа 2025 г.

Было ли это руководство полезным?