Понимание стандартов токенов в Ethereum, часть II (ERC721)

Понимание стандартов токенов в Ethereum, часть II (ERC721)

2 марта 2023 г.

Стандарт ERC-721 был предложен для NFT (невзаимозаменяемых токенов). NFT относится к уникальным токенам, что означает, что каждый токен внутри смарт-контракта будет отличаться от других. Его можно использовать для идентификации уникальных вещей, например, какого-то изображения, лотерейных билетов, предметов коллекционирования, картин или даже музыки и т. д. Напомню, что ERC-20 предназначался для взаимозаменяемых токенов, если вы не читали статью о ERC-20, то подумайте о том, чтобы сначала прочитать ее. Лучший реальный проект для примера ERC-721 — Cryptokitties. Это игра на основе блокчейна. Попробуйте эту замечательную игру для получения знаний с удовольствием.

Но как это работает? Как эти изображения, музыка и картины хранятся в смарт-контрактах?

Ну, эти вещи не хранятся внутри контракта, вместо этого контракт просто указывает на внешний актив. Существует число tokenId, показывающее право собственности. Основным активом является изображение или некоторые другие данные, прикрепленные к tokenId через URI. Давайте углубимся еще немного.

Мы назначаем URI, который представляет собой текст JSON, идентификатору токена. И этот текст JSON содержит детали и путь к изображению или любому другому ресурсу. И пользователь владеет только tokenid. Например, мой адрес имеет баланс tokenid 4, теперь изображение или любые данные, прикрепленные к этому tokenid, будут моими. Я владею этими данными, потому что мне принадлежит tokenid.

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

Теперь давайте посмотрим на хранилище.

Хранение данных

Есть два способа сохранить изображение и URI. Мы можем разместить его в цепочке (внутри цепочки блоков) или вне цепочки (вне цепочки блоков).

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

Таким образом, у нас есть возможность размещать данные вне блокчейна. Через наш веб-сайт с использованием AWS или любого другого облачного сервиса. Но это убивает основную идею блокчейна — децентрализацию. Что, если сервер выйдет из строя или кто-то его взломает? Так как будет один сервер, на котором будут храниться все данные. Поэтому нам нужно что-то еще, устойчивое к атакам, а также децентрализованное, и это что-то IPFS (межпланетная файловая система), которая представляет собой распределенную систему хранения. IPFS имеет несколько узлов, аналогичных идее блокчейна, в котором хранятся данные. Но вам не нужно платить никаких комиссий. Мы можем разместить наши изображения, а также файл JSON (URI) в IPFS, и это даст уникальный CID (случайная тарабарщина), который напрямую указывает на данные, и мы назначаем конкретный CID конкретному идентификатору токена. Мы поговорим о технической части позже, сначала давайте разберемся с интерфейсом для стандарта ERC-721.

ИНТЕРФЕЙС ERC-721

Посмотрим, как выглядит интерфейс ERC-721.

Этот стандарт имеет следующие функции:

  1. Перенос токенов с одного аккаунта на другой.
  2. Получение текущего баланса токенов аккаунта.
  3. Получение владельца определенного токена.
  4. Общее количество токенов, доступных в сети.
  5. Помимо этого, он также имеет некоторые другие функции, такие как подтверждение того, что количество токенов из учетной записи может быть перемещено сторонней учетной записью.

Теперь давайте посмотрим на интерфейс для ERC-721.

pragma solidity ^0.4.20;

interface ERC721 {

 event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);

 event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId);


 event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved);

 function balanceOf(address _owner) external view returns (uint256);

 function ownerOf(uint256 _tokenId) external view returns (address);

 function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable;

 function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable;

 function transferFrom(address _from, address _to, uint256 _tokenId) external payable; 

 function approve(address _approved, uint256 _tokenId) external payable;

 function setApprovalForAll(address _operator, bool _approved) external;

 function getApproved(uint256 _tokenId) external view returns (address);

 function isApprovedForAll(address _owner, address _operator) external view returns (bool);
}

Break

Разбивка

  1. balanceOf Возвращает количество токенов в аккаунте владельца. Как мы следим за остатками? Ну, это всегда одно и то же, мы определяем сопоставление, которое отслеживает общее количество токенов в кошельке. сопоставление (адрес => uint) государственных балансов; Помните, что он просто суммирует общее количество токенов и возвращает то же самое, это сопоставление не знает о владельцах токена, поскольку оно просто сообщает количество идентификаторов токенов, которые есть у адреса.
function balanceOf(address _owner) external view returns (uint256);

<сильный>2. ownerOf Находит владельца NFT. Теперь эта функция сообщает нам о владельце конкретного NFT. Мы храним эти данные аналогично приведенным выше. сопоставление (uint => адрес) государственных балансов; NFT, назначенные нулевым адресам, считаются недействительными, и запросы о них должны вызывать ошибку.

function ownerOf(uint256 _tokenId) external view returns (address);

<сильный>3. safeTransferFrom Переносит право собственности на NFT с одного адреса на другой. Он должен просто установить владельца tokenId на новый адрес, а также обновить сопоставление balances. Он должен вызывать ошибку в следующих случаях:

* msg.sender — текущий владелец, авторизованный оператор или утвержденный адрес для этого NFT. * _from не является текущим владельцем * __to_ — нулевой адрес. * __tokenId_ не является действительным NFT

Когда передача завершена, эта функция проверяет, является ли __to_ смарт-контрактом (размер кода > 0). Если это так, он вызывает функцию onERC721Received для __to_ и выдает исключение, если возвращаемое значение не равно этому: bytes4(keccak256("onERC721Received(address, address, uint256, байт)")).

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

function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data)      external payable;

<сильный>4. safeTransferFrom: работает так же, как описанная выше функция с дополнительным параметром данных, за исключением того, что эта функция просто устанавливает данные в « ».

function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable;

<цитата>

Почему эти две функции имеют одинаковое имя?

Забавный факт: в отличие от Javascript, Solidity поддерживает перегрузку функций. означает, что мы можем определить две функции с одинаковыми именами, единственным условием является то, что параметры должны различаться, влияя на сигнатуру функции. Не знаете, что такое сигнатуры функций? Ну вот. Ссылка на ответ< /а>

<сильный>5. TransferFrom передает право собственности на NFT. Работает так же, как функция safeTransferFrom, с той лишь разницей, что она не вызывает вызов безопасности (onERC721Received) для получателя. Таким образом, создается риск потери NFT.

function transferFrom(address _from, address _to, uint256 _tokenId) external payable;

<сильный>6. утвердить ту же концепцию, что и функция утверждения ERC20, но с большей функциональностью и логикой. Для работы этой функции мы определяем вложенное сопоставление: mapping(uint =>address) внутреннее разрешение;В этом сопоставлении uint относится к идентификатору токена и адресу, который утвержден для выполнения операций на этот конкретный идентификатор токена. Эта функция подтверждения должна проверять, что msg.sender является текущим владельцем или оператором для владельца токена. Если эта проверка проходит успешно, следует обновить только сопоставление. При чем здесь оператор? См. следующую функцию.

function approve(address _approved, uint256 _tokenId) external payable;

<сильный>7. setApprovalForAll работает так же, как и предыдущая функция утверждения, но вместо утверждения адреса для одного идентификатора токена эта функция должна утверждать и адресовать все токены, принадлежащие определенному адресу. Что это значит? Это означает, что вы можете создать адресный оператор для своих NFT. Этот адрес будет владельцем ваших NFT, пока вы не отзовете право собственности. На этот раз мы снова используем сопоставление: mapping(address => mapping(address=>bool))internal operator;Первый адрес устанавливает логическое значение true или false для второго адреса, чтобы одобрить или отозвать соответственно.

function setApprovalForAll(address _operator, bool _approved) external;

<сильный>8. getApproved аналогична функции разрешений в интерфейсе ERC-20. Возвращает утвержденный адрес для одного NFT. Он проверяет сопоставление, которое мы обновили в функции утверждения выше.

function getApproved(uint256 _tokenId) external view returns (address);

<сильный>9. isApprovedForAll аналогична приведенной выше функции. Он запрашивает, является ли адрес авторизованным оператором для другого адреса. Вместо того, чтобы возвращать одобренный адрес определенного tokenId, эта функция должна возвращать address operator для заданный адрес, который мы обновили в функции setApprovalForAll

function isApprovedForAll(address _owner, address _operator) external view returns (bool);

События для ERC-721

Передача: генерируется, когда право собственности на любой NFT изменяется любым механизмом. Это событие генерируется при создании NFT (от == 0) и уничтожении(до == 0).Исключение: во время создания контракта любой количество NFT может быть создано и назначено без отправки Transfer. Во время любого переноса утвержденный адрес для этого NFT (если есть) сбрасывается до нуля.

event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);

Подтверждение выдается при изменении или повторном подтверждении утвержденного адреса для NFT. Нулевой адрес указывает на отсутствие утвержденного адреса. Когда генерируется событие Transfer, это также указывает на то, что утвержденный адрес для этого NFT (если есть) сбрасывается до нуля.

event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId);

ApproveForAll генерируется, когда оператор включен или отключен для владельца. Оператор может управлять всеми NFT владельца.

event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved);

Это был интерфейс для контрактов ERC 721. Теперь давайте узнаем больше о его реализации и правилах.

TokenReceiver ERC721

Перед реализацией есть еще кое-что. Помните, мы говорили о функции под названием onERC721Received, когда говорили о передаче токенов? Давайте поговорим об этом сейчас. Если получателем токена ERC-721 является контракт, то он должен работать с токенами ERC-721. Потому что что, если в этом контракте получателя нет функциональности для взаимодействия с токенами? Токены будут заблокированы в этом контракте навсегда. Никто не может вывести эти токены на любой адрес, так как там нет функциональности. Чтобы преодолеть эту уязвимость, получатель токена ERC-721 должен реализовать интерфейс получателя, если нет, то ни один из контрактов не может отправить любой токен ERC-721 этому контракту. Давайте посмотрим на интерфейс приемника.

этот интерфейс содержит только одну функцию для реализации, и это onerc721receed, эта функция должна обрабатывать получение NFT. Смарт-контракт ERC-721 вызывает эту функцию на получателе после Transfer . Эта функция может бросить, чтобы вернуть и отклонить передачу. Возврат, кроме магического значения, должна привести к возврату транзакции.

interface ERC721TokenReceiver {
function onERC721Received(address _operator,address _from,uint256 _tokenId,bytes _data) external returns(bytes4);
}

Здесь вы можете подумать, что как бы договоры знали, что приемник реализовал интерфейс erc721tokenReceiver или нет? Поэтому, прежде чем отправлять токены, вы должны сначала проверить эту часть. Для этого давайте посмотрим на openzeppelin реализация ERC-7211. Полем Вот частная функция, которую вы должны позвонить внутри safetrancfer () перед отправкой токенов. Если это возвращает истину, то должна произойти только транзакция.

Что ж, это не гарантирует функциональность извлечения NFT из контракта получателя, поскольку проверяется только реализация функции получателя, но не функциональность передачи NFT из контракта. Так в чем же его значение? Этот метод может сказать нам, что автор контракта-получателя, по крайней мере, знал об этом методе, а это значит, что они должны были реализовать функциональность для передачи NFT из контракта. И, конечно же, никто не хотел бы, чтобы их токены застревали внутри контракта.

Метаданные ERC721 и ERC721Enumerable

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

Это ERC721Metadata и ERC721Enumerable. Давайте рассмотрим их один за другим.

  1. Метаданные ERC721: расширение метаданных используется в основном для запроса контракта на предмет имени токена. Теперь давайте посмотрим на интерфейс метаданных. Это довольно просто понять.
interface ERC721Metadata/* is ERC721 */ {
/// @notice A descriptive name for a collection of NFTs in this
/// contract
function name()external view returns (string _name);
/// @notice An abbreviated name for NFTs in this contract
function symbol()external view returns (string _symbol);
/// @notice A distinct Uniform Resource Identifier (URI) for a given
 /// asset.
/// @dev Throws if `_tokenId` is not a valid NFT. URIs are defined
/// in RFC3986. The URI may point to a JSON file that conforms to 
/// the "ERC721Metadata JSON Schema".
function tokenURI(uint256 _tokenId)external view returns (string);

  1. ERC721Enumerable- Это просто дает данные о NFT.

javascript интерфейс ERC721Enumerable/* является ERC721 */ { /// @notice Количество NFT, отслеживаемых этим контрактом /// @return Количество действительных NFT, отслеживаемых этим контрактом, где /// у каждого из них есть назначенный и запрашиваемый владелец, не равный /// по нулевому адресу функция totalSupply() возвращает внешний вид (uint256); /// @notice Перечислить допустимые NFT /// @dev Выдает, если `_index` >= `totalSupply()`. /// @param _index Счетчик меньше, чем `totalSupply()` /// @return Идентификатор токена для `_index`th NFT, /// (порядок сортировки не указан) функция tokenByIndex(uint256 _index) возвращает внешний вид (uint256); /// @notice Перечислить NFT, назначенные владельцу /// @dev Выдает, если `_index` >= `balanceOf(_owner)` или если /// `_owner` — это нулевой адрес, представляющий недействительные NFT. /// @param _owner Адрес, по которому нас интересуют принадлежащие им NFT /// /// @param _index Счетчик меньше `balanceOf(_owner)` /// @return Идентификатор токена для `_index`го NFT, присвоенного /// `_owner`, /// (порядок сортировки не указан) функция tokenOfOwnerByIndex (адрес _owner, uint256 _index) возвращает внешнее представление (uint256);

ERC165

Как узнать, реализован ли интерфейс по контракту или нет? Для этого нам нужно отойти от темы, т.е. ERC-165, у которой есть функция:

функция supportsInterface(bytes4 interfaceID) возвращает внешнее представление (bool);

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

Давайте узнаем больше об интерфейсе в этой функции type(IERC721).interfaceId ;

interfaceID достигается двумя основными операциями.

1. keccak 256 хеш

2. Операция XOR

keccak256 — это алгоритм, который принимает входные данные и выдает случайную строку в байтах. Теперь давайте узнаем хэш keccak для этой функции.

функция supportsInterface(bytes4 interfaceID) возвращает внешнее представление (bool);

Синтаксис выглядит примерно так.

bytes4(keccak256(’supportsInterface(bytes4)’);

Теперь у нас есть хэш keccak для этой функции. Помните, что вам не нужно передавать всю функцию внутри параметра для keccak256, а только подпись. Под подписью подразумевается только имя и тип параметра, даже имя параметра не включается. Получив хэш keccak256 всех функций, мы выполняем между ними операцию XOR, и в результате получаем интерфейсный идентификатор. Посмотрим, как. Операции XOR принимают входные данные и дают некоторые выходные данные после их сравнения. Он выводит true, если один и только один из входных данных истинен. Если оба ввода ложны или оба верны, вывод будет false.

Вам не нужно понимать математику XOR. Просто помните, что вы берете хеш keccak256 каждой функции и передаете их в вентиль XOR, а полученное значение является идентификатором интерфейса. Таким образом, основная идея состоит в том, чтобы взять хеш-функции keccak и получить их выходные данные XOR. этот вывод является интерфейсным идентификатором, и после этого вы можете проверить контракт, имеет ли он тот же интерфейсный идентификатор. Если да, это означает, что контракт реализует желаемый интерфейс.

Не волнуйтесь, Solidity упростила вам эту задачу. Давайте научимся на примере интерфейса для ERC721Metadata.

interface ERC721Metadata/* is ERC721 */ {
function name()external view returns (string _name);
function symbol()external view returns (string _symbol);
function tokenURI(uint256 _tokenId)external view returns (string);
}

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

// SPDX-License-Identifier: MIT
pragma solidity   ^0.8.16 ;
interface  metadata {
function name()external view returns (string memory _name );
function symbol()external view returns (string memory _symbol);
function tokenURI(uint256 _tokenId)external view returns (string memory a);
}

contract  {
//old and lengthy way by applying the keccak256 algorithm and performing XOR.
   function getHashOldWay ()public pure  returns(bytes4){
return  bytes4(keccak256('name()')) ^  bytes4(keccak256('symbol()'))^ bytes4(keccak256('tokenURI(uint256)')); 
   }
//Improved way
   function getHashNewWay ()public pure  returns(bytes4){
     metadata a;
     return a.name.selector ^  a.symbol.selector ^ a.tokenURI.selector; 
   }
//this is the latest simplest way to get the interfaceId
    function getHashLatestWay ()public pure  returns(bytes4){
    return  type(metadata).interfaceId;
    }
 }

О, мы слишком увлеклись ERC165, что забыли о ERC721, давайте вернемся к нему.

Как OpenSea (или любая другая торговая площадка) извлекает данные из смарт-контракта?

Это подходящее время для понимания URI токенов, которые помогают различным рынкам NFT идентифицировать данные, которые вы назначили с определенным идентификатором токена. Поскольку Opensea — самая известная торговая площадка, мы возьмем ее в качестве примера. Opensea ожидает, что ваш контракт ERC721 вернет tokenURI, вызвав функцию tokenURI(). Давайте посмотрим на контракт openzeppelin для этого.

TokenURI может быть двух типов.

<сильный>1. Один и тот же базовый URI для всех токенов, отличающийся идентификатором токена.

Таким образом мы назначаем базовый URI для всех токенов, а затем объединяем с ним tokenId, получая URI метаданных. Это возможно только в том случае, если вы назначили свою коллекцию таким образом, что ссылка одинакова для каждого файла метаданных JSON, единственное отличие будет в идентификаторе токена. Например,

https://gateway.pinata.cloud/ipfs/QmYLwrqMmzC3k4eZu7qJ4MZJ4SNYMgqbRJFLkyiPtUBZUP/ что это baseURI.

Чтобы получить единые метаданные, нам нужно перейти на https://gateway.pinata.cloud/ipfs/QmYLwrqMmzC3k4eZu7qJ4MZUPbRJFLKyMgg /1.json

Теперь вам нужно вернуть tokenURI из контракта таким образом, чтобы он напрямую относился к метаданным этого конкретного токена, и рынок мог его получить. Мы делаем это, объединяя baseURI с tokenId и кодируя его с помощью abi. Посмотрите на эту функцию ниже.

function tokenURI(uint tokenId) override public view returns(string memory) {
  return (string(abi.encodePacked(
   "https://gateway.pinata.cloud/ipfs/QmYLwrqMmzC3k4eZu7qJ4MZJ4SNYMgqbRJFLkyiPtUBZUP/",Strings.toString(tokenId),".json"))
  );
}

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

<сильный>2. разные URI для каждого токена.

Это еще один способ, с помощью которого мы можем назначать URI, расположенные в разных местах. И ссылка IPFS для отдельного файла выглядит так.https://ipfs.filebase.io/ipfs/Qma65D75em77UTgP5TYXT4ZK5Scwv9ZaNyJPdX9DNCXRWcТаким образом, вы не можете просто объединить tokenId с baseURI, чтобы получить эту ссылку, вместо этого , эта ссылка должна быть напрямую привязана к идентификатору токена в цепочке. Для этого взгляните на приведенный выше контракт ERC721URIStorage, в котором мы определяем частное сопоставление для назначения URI определенному идентификатору токена. А позже также определить функцию, которая заполняет отображение заданным URI. А функция tokenURI переопределяет родительскую функцию, добавляя новую функцию возврата сопоставленного URI.

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

Стандартная схема JSON для ERC721.

{
    "title": "Asset Metadata",
    "type": "object",
    "properties": {
        "name": {
            "type": "string",
            "description": "Identifies the asset to which this NFT represents"
        },
        "description": {
            "type": "string",
            "description": "Describes the asset to which this NFT represents"
        },
        "image": {
            "type": "string",
            "description": "A URI pointing to a resource with mime type image/* representing the asset to which this NFT represents. Consider making any images at a width between 320 and 1080 pixels and aspect ratio between 1.91:1 and 4:5 inclusive."
        }
    }
}

Итак, это были вещи, которые вы должны знать, если вы заинтересованы в разработке смарт-контрактов ERC-721 (NFT).

Проблемы:

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

ERC-1155 решает эти проблемы. Как? Поговорим об этом в отдельном посте. Если вы полностью поняли стандарт ERC721, то понять ERC1155 будет несложно.

Также опубликовано здесь.


Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE