Перейти до основного вмісту

Покроковий опис контракту Vyper ERC-721

Vyper
erc-721
Python
Початківець
Ori Pomerantz
1 квітня 2021 р.
18 читається за хвилину

Вступ

Стандарт ERC-721 використовується для володіння невзаємозамінними токенами (NFT). Токени ERC-20 поводяться як товар, оскільки між окремими токенами немає різниці. На відміну від цього, токени ERC-721 призначені для активів, які є подібними, але не ідентичними, як-от різні котячі мультики або права власності на різні об’єкти нерухомості.

У цій статті ми проаналізуємо контракт ERC-721 від Рюї Накамури (opens in a new tab). Цей контракт написано мовою Vyper (opens in a new tab), контрактною мовою, подібною до Python, яка розроблена, щоб ускладнити написання незахищеного коду, ніж у Solidity.

Контракт

# @dev Реалізація стандарту невзаємозамінних токенів ERC-721.
# @author Рюя Накамура (@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

Щоб запобігти випадкам, коли контракт випадково приймає переказ, значення, що повертається, — це не булеве значення, а 256-бітне значення з певним вмістом.

Ця функція є функцією view, що означає, що вона може читати стан блокчейну, але не змінювати його.

Події

Події (opens in a new tab) генеруються для інформування користувачів і серверів за межами блокчейну про події. Зверніть увагу, що вміст подій недоступний для контрактів у блокчейні.

Це схоже на подію Transfer в ERC-20, за винятком того, що ми повідомляємо tokenId замість суми. Ніхто не є власником нульової адреси, тому за угодою ми використовуємо її для повідомлення про створення й знищення токенів.

Затвердження ERC-721 схоже на дозвіл 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]

Ідентифікатори користувачів і контрактів в Ethereum представлені 160-бітними адресами. Ці дві змінні зіставляють ID токенів з їхніми власниками та тими, хто має дозвіл на їх переказ (максимум один на кожен токен). В Ethereum неініціалізовані дані завжди дорівнюють нулю, тому, якщо немає власника або затвердженого відправника, значення для цього токена дорівнює нулю.

# @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 інтерфейсу з булевим значенням, що вказує на його підтримку
supportedInterfaces: HashMap[bytes32, bool]

# @dev ID інтерфейсу ERC165 для ERC165
ERC165_INTERFACE_ID: constant(bytes32) = 0x0000000000000000000000000000000000000000000000000000000001ffc9a7

# @dev ID інтерфейсу ERC165 для ERC721
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.<ім’я змінної> (знову ж таки, як і в Python).

Функції перегляду

Це функції, які не змінюють стан блокчейну, і тому можуть виконуватися безплатно, якщо вони викликаються ззовні. Якщо функції перегляду викликаються контрактом, вони все одно повинні виконуватися на кожному вузлі, отже, коштуватимуть газ.

@view
@external

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

  • @view вказує, що ця функція є функцією перегляду.
  • @external вказує, що ця конкретна функція може викликатися транзакціями та іншими контрактами.
def supportsInterface(_interfaceID: bytes32) -> bool:

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

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

Повертає значення з HashMap self.supportedInterfaces, яке встановлюється в конструкторі (__init__).

### ФУНКЦІЇ ПЕРЕГЛЯДУ ###

Існують функції перегляду, які роблять інформацію про токени доступною для користувачів та інших контрактів.

Цей рядок стверджує (opens in a new tab), що _owner не є нульовим. Якщо це так, то виникає помилка, і операція скасовується.

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

Зверніть увагу, що getApproved може повертати нуль. Якщо токен дійсний, він повертає self.idToApprovals[_tokenId]. Якщо немає затверджувача, це значення дорівнює нулю.

Ця функція перевіряє, чи дозволено _operator керувати всіма токенами _owner у цьому контракті. Оскільки може бути декілька операторів, це дворівнева HashMap.

Допоміжні функції переказу

Ці функції реалізують операції, які є частиною передачі або управління токенами.


### ДОПОМІЖНІ ФУНКЦІЇ ПЕРЕКАЗУ ###

@view
@internal

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

Існує три способи, за допомогою яких адреса може отримати дозвіл на передачу токена:

  1. Адреса є власником токена
  2. Адреса затверджена для витрачання цього токена
  3. Адреса є оператором для власника токена

Наведена вище функція може бути функцією перегляду, тому що вона не змінює стан. Щоб зменшити операційні витрати, будь-яка функція, яка може бути функцією перегляду, повинна нею бути.

Коли виникає проблема з переказом, ми скасовуємо виклик.

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

Ми маємо цю внутрішню функцію, оскільки існує два способи передачі токенів (звичайний і безпечний), але ми хочемо мати лише одне місце в коді, де ми це робимо, щоб полегшити аудит.

Щоб згенерувати подію у 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 не має успадкування. Це навмисний вибір архітектури, щоб зробити код більш зрозумілим і, отже, більш безпечним. Отже, щоб створити свій власний контракт Vyper ERC-721, ви берете цей контракт і змінюєте його для реалізації потрібної вам бізнес-логіки.

Висновок

Для повторення, ось деякі з найважливіших ідей цього контракту:

  • Щоб отримувати токени ERC-721 за допомогою безпечного переказу, контракти повинні реалізувати інтерфейс ERC721Receiver.
  • Навіть якщо ви використовуєте безпечний переказ, токени все одно можуть застрягти, якщо ви відправите їх на адресу, приватний ключ до якої невідомий.
  • Коли виникає проблема з операцією, краще скасувати виклик, а не просто повертати значення збою.
  • Токени ERC-721 існують, коли у них є власник.
  • Існує три способи отримати дозвіл на передачу NFT. Ви можете бути власником, бути затвердженим для певного токена або бути оператором для всіх токенів власника.
  • Минулі події видно тільки за межами блокчейну. Код, що виконується всередині блокчейну, не може їх переглядати.

Тепер переходьте до реалізації безпечних контрактів Vyper.

Більше моїх робіт дивіться тут (opens in a new tab).

Останнє оновлення сторінки: 28 квітня 2026 р.

Цей посібник був корисним?