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

Огляд контракту ERC-721 на Vyper

Vyper
erc-721
Python
Для початківців
Орі Померанц
1 квітня 2021 р.
17 хвилин на читання

Вступ

Стандарт 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 інтерфейсу на логічне значення щодо того, чи підтримується він
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

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

  • @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__).

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

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

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

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

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

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

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

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


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

@view
@internal

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

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

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

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

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

Змінюйте значення лише за необхідності. Змінні стану живуть у сховищі. Запис у сховище є однією з найдорожчих операцій, які виконує 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).