В первой статье этой серии я представил концепцию нефинансового токена (NFT) и потребность в стандарте ERC721 (черновой вариант). В этой статье мы впервые рассмотрим интерфейс стандарта ERC721 и разберем некоторые из его требований. Также будет краткое, но важное отступление, чтобы поговорить о стандарте ERC165.

Интерфейсы и стандарт ERC165

Стандарт ERC721 гласит, что:

«Каждый контракт, соответствующий стандарту ERC-721, должен реализовывать интерфейсы ERC721 и ERC165»

Если вам никогда не приходилось писать контракт на Solidity, который работает с контрактами, написанными другими разработчиками, вы можете задаться вопросом: «Что такое интерфейс?». И независимо от того, вы, вероятно, также задаетесь вопросом: «Что такое интерфейс ERC165?». Итак, давайте ответим на оба эти вопроса.

Интерфейсы

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

Например, если интерфейс определяет функцию balanceOf как

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

тогда вы знаете, что любой контракт, реализующий этот интерфейс, будет иметь функцию с именем balanceOf, которая принимает один аргумент (адрес) и возвращает одно значение ( uint256). Вы также знаете, что изменяемость этой функции - представление, что означает, что функция может считывать состояние контракта, но не изменять его, и что у нее есть модификатор external. , который влияет на потребление газа, и как эта функция должна называться. Если модификаторы функции контракта или возвращаемые значения не соответствуют тем, которые определены в интерфейсе, это приведет к тому, что ваш компилятор выдаст TypeErrors и не сможет компилировать.

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

Но возникает вопрос, не глядя на ваш код контракта, как другие разработчики могут узнать, что вы использовали данный интерфейс? Вся суть этого интерфейса заключалась в том, чтобы мы не должны знать кодовые базы друг друга. Ответ: Стандарт ERC165.

Стандарт ERC165

"Какие?!? Другой стандарт ERC ?? Насколько глубока эта кроличья нора? »

Не волнуйтесь, это еще один, о котором мы будем говорить, и он очень простой - он выполняет только одну функцию! Стандарт ERC165 - это всего лишь способ проверить, совпадают ли отпечатки вашего контракта с отпечатком любого данного интерфейса. Давайте посмотрим на стандартный интерфейс ERC165 полностью :

interface ERC165 {
 /// @notice Query if a contract implements an interface
 /// @param interfaceID The interface identifier, as 
 ///  specified in ERC-165
 /// @dev Interface identification is specified in 
 ///  ERC-165. This function uses less than 30,000 gas.
 /// @return `true` if the contract implements `interfaceID` 
 ///  and `interfaceID` is not 0xffffffff, `false` otherwise
 function supportsInterface(bytes4 interfaceID) external view returns (bool);
}

Таким образом, ваш контракт должен иметь функцию supportsInterface, которая принимает единственный аргумент, представляющий interfaceID (bytes4) интерфейса, и возвращает true ( bool), если этот интерфейс поддерживается, где interfaceID определен в стандарте ERC165 как «XOR всех селекторов функций в интерфейсе».

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

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

function balanceOf(address _owner) external view returns (uint256){
   //...
};

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

this.balanceOf.selector

или вручную с помощью

bytes4(keccak256("balanceOf(address)"))

Оба вернут 0x70a08231, и хотя первый, на мой взгляд, выглядит чище, иногда нам нужен второй, если интерфейс использует перегруженные функции. Вы заметите, что селектор функций не заботится об именах параметров, модификаторах, изменчивости, возвратах или содержимом функции. Просто имя функции и типы параметров. Вот почему я сказал, что не имеет значения, реализована функция или нет.

Но нам нужен был «XOR всех селекторов функций в интерфейсе» для interfaceID . Предположим, что интерфейс состоит из трех функций: function1(), function2() и function3().

interfaceID это просто:

interfaceID = this.function1.selector ^ this.function2.selector ^ this.function3.selector;

Довольно просто! Теперь помните, причина, по которой мы начали это обсуждение ERC165, заключалась в том, что наш контракт ERC721 должен реализовывать интерфейс ERC165, поэтому давайте напишем нашу реализацию ERC165 (которую унаследует наш контракт ERC721), чтобы завершить это.

Что касается Solidity, мы хотим минимизировать количество используемого газа. Выполнение ненужных вычислений стоит пользователям денег и расходует ресурсы сети. Стандарт ERC165 фактически требует, чтобы supportsInterface функция «использовала менее 30 000 газа». Поэтому вместо того, чтобы повторно вычислять идентификаторы интерфейса каждый раз, когда кто-то вызывает supportsInterface, давайте сохраним наши поддерживаемые идентификаторы интерфейса в сопоставлении .

Начните наш контракт, CheckERC165, примерно так:

contract CheckERC165 is ERC165 {

    mapping (bytes4 => bool) internal supportedInterfaces;

Таким образом, функция supportsInterface просто должна возвращать значение из сопоставления, вот и вся эта функция реализована:

function supportsInterface(bytes4 interfaceID) external view returns (bool){
    return supportedInterfaces[interfaceID];
}

Большой! В нашем контракте теперь реализованы все функции интерфейса ERC165 (он только один). Давайте добавим ERC165 interfaceID к supportedInterfaces и завершим круг.

Недавно была выпущена версия Solidity 0.4.22, которая дала нам более простое constructor имя функции для нашего конструктора. Итак, давайте создадим конструктор и добавим в него interfaceID интерфейса ERC165.

constructor() public {
    supportedInterfaces[this.supportsInterface.selector] = true;
}

Теперь, если кто-нибудь вызовет supportsInterface с interfaceID стандартного интерфейса (0x01ffc9a7) ERC165, он вернет true.

На этом все по ERC165! Полный контракт доступен на моем GitHub. Мы будем использовать это позже с нашей реализацией ERC721, и это также удобно, когда мы имеем дело с интерфейсами в целом.

Интерфейсы ERC721

Теперь, когда мы знаем об интерфейсах, вернемся к ERC721. Вы заметите, что ERC721 Standard на самом деле содержит четыре разных интерфейса. Один основной для общего контракта ERC721, один для контрактов, которые могут получать токены ERC721, и два для дополнительных расширений, которые добавляют дополнительные функции. Мы пока проигнорируем расширения, а просто взглянем на основной интерфейс и получатель.

Платные функции и изменчивость

Что сразу бросилось в глаза, так это то, что четыре функции в интерфейсе ERC721 имеют модификатор payable. А именно две safeTransferFrom функции, transferFrom и approve. На самом деле не имело смысла, чтобы контракт ERC721 всегда принимал оплату каждый раз, когда передается токен или предоставляется контроль над токеном.

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

Из раздела предостережений стандарта ERC721:

«Гарантии изменяемости бывают в порядке от слабых до сильных: payable, неявно неоплачиваемые, view и pure».

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

Есть два способа справиться с этим, вы можете либо:

  1. сохраните эти функции как подлежащие оплате и просто верните любой эфир, отправленный с транзакциями (или сохраните его, если это ваш дизайн), или
  2. удалите модификатор payable, но вам придется сделать то же самое в вашей копии ERC721 интерфейса, чтобы избежать выдачи TypeError.

Помните, как ранее было сказано, что на селекторы функций не влияют модификаторы, поэтому это не повлияет на interfaceID. То же самое и с функциями, помеченными external, вы можете изменить их на public по мере необходимости.

ERC721TokenReceiver

Прежде чем закончить, я хочу быстро осветить ERC721TokenReceiver интерфейс. Это не то, что наш токен-контракт должен унаследовать. Как следует из названия, это интерфейс для контрактов, который может получать токены ERC721.

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

Для наших целей единственный важный бит информации - это то, что действительный ERC721TokenReceiver будет реализовывать функцию

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

и вернуться

bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))

Недопустимый либо не реализует эту функцию, либо вернет буквально что-нибудь еще. Итак, давайте определим наши два приемника следующим образом:

contract ValidReceiver is ERC721TokenReceiver {
    function onERC721Received(address _operator, address _from, uint256 _tokenId, bytes _data) external returns(bytes4){
        return bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"));
    } 
}

а также

contract InvalidReceiver is ERC721TokenReceiver {
    function onERC721Received(address _operator, address _from, uint256 _tokenId, bytes _data) external returns(bytes4){
        return bytes4(keccak256("some invalid return data"));
    } 
}

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

Подведение итогов

Итак, после всего этого вы должны быть знакомы с интерфейсами и иметь реализацию ERC165, которую вы можете использовать для указания, какие интерфейсы реализованы в вашем контракте. Мы также рассмотрели гарантии изменчивости в стандарте ERC721 и быстро написали пару фиктивных контрактов на кошелек для тестирования позже.

В следующем посте мы воспользуемся всем, что уже рассмотрели, и начнем писать фактический контракт на токен ERC721.

Далее: Переход к твердости - стандарт ERC721 (часть 3)