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

Огляд контракту 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,

ID токенів 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-бітними адресами. Ці дві змінні відображають ID токенів на їхніх власників і тих, кому схвалено їх переказ (максимум по одному для кожного). В Етеріумі неініціалізовані дані завжди дорівнюють нулю, тому якщо немає власника або схваленого відправника, значення для цього токена дорівнює нулю.

# @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 інтерфейсів ERC165
SUPPORTED_INTERFACES: constant(bytes4[2]) = [
    # ID інтерфейсу ERC165 для ERC165
    0x01ffc9a7,
    # ID інтерфейсу ERC165 для ERC721
    0x80ac58cd,
]

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

Функції

Це функції, які фактично реалізують ERC-721.

Конструктор

@deploy
def __init__():

У Vyper, як і в Python, функція-конструктор називається __init__. Вона позначена декоратором @deploy, що означає, що вона виконується один раз під час розгортання контракту.

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

У Python і у Vyper ви також можете створити коментар, вказавши багаторядковий рядок (який починається і закінчується на """), і ніяк його не використовувати. Ці коментарі також можуть містити NatSpec (відкривається в новій вкладці).

    self.minter = msg.sender

Для доступу до змінних стану ви використовуєте self.<variable name> (знову ж таки, як і в Python). Конструктор записує акаунт, який розгорнув контракт, як minter.

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

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

@view
@external

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

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

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

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

Повертає True, якщо interface_id є одним з 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.

Більше моїх робіт дивіться тут (відкривається в новій вкладці).