EIP-2981 представляет более простой способ включения роялти для проектов NFT. Узнайте все об этом стандарте роялти и о том, как его реализовать

С завершением работы над стандартом ERC721 невзаимозаменяемые токены (NFT) стали привлекать большое внимание. Эти доказуемо уникальные активы хранятся в блокчейне и представляют собой новый способ сбора и торговли произведениями искусства, музыкой, изображениями профиля (PFP) и многим другим. Летом 2021 года создание и продажа NFT стали быстрым способом накопления богатства из-за бума популярности.

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

Если интерфейс не содержит каких-либо встроенных функций роялти, как создатель, как вы можете создавать NFT, которые позволяют накапливать богатство спустя долгое время после первоначальной продажи? И как торговые платформы, такие как OpenSea, получают свою долю? А что, если у вас есть более сложная ситуация с роялти, например, с разделением роялти?

В этой статье мы рассмотрим несколько аспектов роялти с помощью NFT. Мы рассмотрим способы реализации роялти, включая проприетарные решения, реестры и EIP-2981. Мы также рассмотрим один из способов разделения платежей. Наконец, мы рассмотрим проект и увидим разделение роялти в действии.

Давайте начнем!

Что такое роялти NFT?

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

Настоящая история успеха этих постепенно децентрализованных торговых площадок написана на основе комиссий, которые они берут за сделку. Например, за каждую Bored Ape, торгуемую по 100 Eth, OpenSea стабильно зарабатывает колоссальные 2,5 Eth за выполнение сделки в сети. Стимулом для удержания создателей на своих рыночных платформах было предоставление им возможности получать долгосрочную прибыль от продаж на вторичном рынке. Это обычно называют «роялти» в пространстве NFT.

Заработок для создателей NFT: комиссия за майнинг и роялти

Когда кто-то запускает базовую коллекцию NFT, развернув контракт ERC721, ему сначала придется подумать о чеканке или о том, как появляются новые токены?

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

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

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

Для них лицензионные платежи являются добровольной концепцией, которую каждая торговая площадка реализует индивидуально. Итак, когда контракты NFT заключаются только в том, что реестры и торговые площадки управляют выплатами роялти по отдельности, как владелец коллекции может определить свою схему роялти, чтобы ее поддерживала каждая торговая площадка?

Собственные решения

Самый простой способ для торговой площадки выяснить, сколько лицензионных отчислений нужно собирать и куда их переводить, — это полагаться на собственный проприетарный интерфейс, реализованный коллекцией. Хорошим примером является контракт обмена Rarible, который пытается поддерживать все виды внешних интерфейсов роялти, среди которых два, которые Rarible определили сами, будучи одним из первых игроков в пространстве NFT:

interface RoyaltiesV1 {
    event SecondarySaleFees(uint256 tokenId, address[] recipients, uint[] bps);
    function getFeeRecipients(uint256 id) external view returns (address payable[] memory);
    function getFeeBps(uint256 id) external view returns (uint[] memory);
}
interface RoyaltiesV2 {
    event RoyaltiesSet(uint256 tokenId, LibPart.Part[] royalties);
    function getRaribleV2Royalties(uint256 id) external view returns (LibPart.Part[] memory);
}

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

Обратите внимание, что фактическая цена продажи не является частью интерфейсов методов. Вместо этого они получают долю роялти в виде базисных пунктов (bps) — термин, обычно используемый в схемах распределения роялти и обычно переводимый как 1/10000 — доля в 500 означает, что 5% от стоимости сделки должно быть отправлено в коллекцию. владелец в качестве роялти.

Роялти реестры

Однако проприетарные интерфейсы могут вызывать проблемы. Авторы контрактов NFT не могут знать, какие интерфейсы могут стать обязательными для реализации, поскольку они не могут предсказать, на каких торговых площадках будут продаваться их токены. Хуже того, если они запускают контракт на сбор до публикации соответствующих контрактов на рынке, у них обычно нет простого способа позже добавить соответствующую схему распределения роялти.

Чтобы решить эту проблему, консорциум крупных торговых площадок NFT вокруг manifold.xyz согласился развернуть общеотраслевой контракт реестра, который создатели коллекций могут использовать для подачи сигналов о разделении роялти независимо от своих контрактов на токены. База открытого исходного кода Royalty Registry показывает, что она поддерживает многие из наиболее важных рыночных интерфейсов.

Например, если владелец коллекции NFT реализовал только одну из упомянутых выше схем распределения роялти Rarible, другая торговая площадка, не знающая об этом интерфейсе, может просто вызвать функцию getRoyaltyView общего реестра. Он пытается запросить все известные интерфейсы роялти в контракте токена и переводит любой ответ в обычно используемый результат.

Реестр идет еще дальше. Владельцы коллекций, которые не включили в свой контракт какую-либо схему сигнализации роялти, могут развернуть расширенный контракт «отмены» и зарегистрировать его в общем реестре. Этот метод регистрации гарантирует, что его смогут вызывать только владельцы коллекций (идентифицированные owner общедоступным членом).

EIP-2981: стандарт для сигнализации о роялти NFT на торговых площадках

В 2020 году некоторые амбициозные люди начали определять общий интерфейс, достаточно гибкий, чтобы охватить большинство случаев использования, связанных с роялти, и простой для понимания и реализации: EIP-2981. Он определяет только один метод, который могут реализовать контракты NFT:

function royaltyInfo(uint256 _tokenId,  uint256 _salePrice) 
  external view 
  returns (address receiver, uint256 royaltyAmount);

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

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

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

Чтобы дать вам представление о том, как может выглядеть нетривиальная реализация EIP-2981, вот фрагмент, который вы можете найти в коллекциях 1/1 NFT, который сигнализирует об адресе исходного создателя и его заявлении о выплате гонорара на любом рынке, совместимом со стандартом:

Если вы используете базовые контракты OpenZeppelin ERC721 для создания контрактов NFT, вы, возможно, уже заметили, что они недавно добавили базовый контракт ERC721Royalty, который содержит методы управления и частные члены для упрощения обработки выделенных токенов.

Роялти за отпечатки ERC1155

Торговые площадки — не единственные приложения, которые позволяют своим пользователям получать прибыль от лицензионных схем. Например, компания Treum EulerBeats использует мультитокеновый стандарт ERC1155 в своей коллекции контрактов, которые представляют собой NFT, сочетающие в себе созданные компьютером мелодии и генеративные произведения искусства. После чеканки начального токена пользователи могут получить из него ограниченное количество отпечатков, и цена за каждый отпечаток увеличивается по кривой связывания, определяемой контрактом на токен.

Каждый раз, когда чеканится новый отпечаток семени Enigma, контракт передает 50% роялти от платы за чеканку текущему владельцу семени. Если принимающая сторона реализует интерфейс IEulerBeatsRoyaltyReceiver для конкретной платформы, она может даже реагировать на выплаты лицензионных платежей и выполнять код после того, как отпечаток их семени был отчеканен.

PaymentSplitters: отправка роялти NFT более чем одному получателю.

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

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

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

К счастью, нас снова покрывают базовые контракты OpenZeppelin. Их примитив PaymentSplitter позволяет создавать отдельные разделенные контракты, которые сохраняют средства в безопасности до тех пор, пока их получатели не потребуют их, а их функция получения требует минимального количества газа для работы. Создатели коллекции NFT могут создать встроенный PaymentSplitter, содержащий разыскиваемый список бенефициаров и соответствующие суммы их долей, и позволить своей реализации EIP-2981 получить адрес этого разделенного контракта.

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

Тестирование выплат роялти NFT с помощью локального форка основной сети

Разработка торговых площадок, которые взаимодействуют с произвольным контрактом NFT, — непростая задача, поскольку непредсказуемо, будут ли контракты в действующих сетях вести себя в соответствии с интерфейсами ERC. Однако может быть полезно протестировать наш код на этих контрактах с помощью Ganache. Этот мощный инструмент позволяет нам мгновенно создать форк сети Ethereum на нашем локальном компьютере без создания собственного узла блокчейна. Вместо этого он использует узлы Infura для чтения текущего состояния контрактов и учетных записей, с которыми мы взаимодействуем.

Прежде чем мы запустим наш экземпляр блокчейна, давайте клонируем репозиторий нашего доказательства концепции, перейдем в новый каталог и установим все зависимости:

git clone https://github.com/elmariachi111/royalty-marketplace.git
cd royalty-marketplace
npm i

Чтобы увидеть, что происходит в этом примере торговой площадки NFT, давайте взглянем на код ClosedDesert.sol в папке contracts.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/utils/Address.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/token/common/ERC2981.sol";
import "@manifoldxyz/royalty-registry-solidity/contracts/IRoyaltyEngineV1.sol";
struct Offer {
  IERC721 collection;
  uint256 token_id;
  uint256 priceInWei;
}
/**
 * DO NOT USE IN PRODUCTION!
 * a fixed reserve price marketplace
 */
contract ClosedDesert is ReentrancyGuard {
  mapping(bytes32 => Offer) public offers;
  // https://royaltyregistry.xyz/lookup
  IRoyaltyEngineV1 royaltyEngineMainnet = IRoyaltyEngineV1(0x0385603ab55642cb4Dd5De3aE9e306809991804f);
  event OnSale(bytes32 offerHash, address indexed collection, uint256 token_id, address indexed owner);
  event Bought(address indexed collection, uint256 token_id, address buyer, uint256 price);
  function sellNFT(IERC721 collection, uint256 token_id, uint256 priceInWei) public {
    require(collection.ownerOf(token_id) == msg.sender, "must own the NFT");
    require(collection.getApproved(token_id) == address(this), "must approve the marketplace to sell");
    bytes32 offerHash = keccak256(abi.encodePacked(collection, token_id));
    offers[offerHash] = Offer({
      collection: collection,
      token_id: token_id,
      priceInWei: priceInWei
    });
    emit OnSale(offerHash, address(collection), token_id, msg.sender);
  }
  function buyNft(bytes32 offerHash) public payable nonReentrant {
    Offer memory offer = offers[offerHash];
    require(address(offer.collection) != address(0x0), "no such offer");
    require(msg.value >= offer.priceInWei, "reserve price not met");
    address payable owner = payable(offer.collection.ownerOf(offer.token_id));
    emit Bought(address(offer.collection), offer.token_id, msg.sender, offer.priceInWei);
    // effect: clear offer
    delete offers[offerHash];
    (address payable[] memory recipients, uint256[] memory amounts) =
      royaltyEngineMainnet.getRoyalty(address(offer.collection), offer.token_id, msg.value);
    uint256 payoutToSeller = offer.priceInWei;
    //transfer royalties
    for(uint i = 0; i < recipients.length; i++) {
      payoutToSeller = payoutToSeller - amounts[i];
      Address.sendValue(recipients[i], amounts[i]);
    }
    //transfer remaining sales revenue to seller
    Address.sendValue(owner, payoutToSeller);
    //finally transfer asset
    offer.collection.safeTransferFrom(owner, msg.sender, offer.token_id);
  }
}
}

В нашем примере продавцы могут перечислить свои активы по фиксированной цене продажи после одобрения передачи. Покупатели могут отслеживать OnSale событий и реагировать, выдавая buyNft транзакций и отправляя желаемое значение Eth. Контракт торговой площадки проверяет открытую основную сеть реестр роялти NFT во время транзакции продажи, чтобы увидеть, запрашивают ли владельцы коллекции роялти, а затем выплачивает их соответствующим образом. Как указывалось выше, публичный реестр роялти уже учитывает контракты, совместимые с EIP-2981. Тем не менее, он также поддерживает множество других проприетарных схем распространения.

Далее мы развернем наш локальный экземпляр блокчейна и протестируем наш контракт, используя учетные записи и NFT реальных пользователей.

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

Наконец, найдите учетную запись с достаточным количеством средств основной сети (не менее 1 Eth), чтобы оплатить запрошенную продавцом цену продажи. Имея под рукой эти учетные записи и инструменты, мы можем запустить локальный экземпляр основной сети Ganache, используя следующую команду в новом окне терминала:

npx ganache --fork https://mainnet.infura.io/v3/<infuraid> --unlock <0xseller-account> --unlock <0xbuyer-account>

Обязательно используйте свою собственную конечную точку основной сети Infura для URL-адреса в приведенной выше команде.

Если у вас возникли проблемы с поиском аккаунтов для разблокировки, вот несколько советов:

Адрес продавца:0x27b4582d577d024175ed7ffb7008cc2b1ba7e1c2
Адрес покупателя:0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045

Примечание. Поскольку мы имитируем основную сеть Ethereum в нашем экземпляре Ganache, к тому времени, когда вы будете это читать, продавец может больше не владеть NFT, который мы будем продавать, или у покупателя может уже не хватить Eth для собственно совершить покупку. Поэтому, если эти адреса не работают, вам придется найти те, которые соответствуют вышеуказанным критериям.

Используя примеры адресов выше, наша команда выглядит так:

npx ganache --fork https://mainnet.infura.io/v3/<infuraid> --unlock 0x27b4582d577d024175ed7ffb7008cc2b1ba7e1c2 --unlock 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045

Затем в нашем исходном окне терминала мы скомпилируем и развернем контракт торговой площадки из репозитория и выберем нашего локального провайдера форка основной сети, который можно найти в truffle-config.js:

npx truffle compile
npx truffle migrate --network mainfork

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

Давайте взглянем на скрипт testMarketplace.js (находящийся в папке scripts), который мы будем использовать для взаимодействия с нашим развернутым смарт-контрактом Marketplace:

const ClosedDesert = artifacts.require("ClosedDesert");
const IErc721 = require("../build/contracts/IERC721.json");
//Change these constants:
const collectionAddress = "0xed5af388653567af2f388e6224dc7c4b3241c544"; // Azuki
const tokenId = 9183;
let sellerAddress = "0x27b4582d577d024175ed7ffb7008cc2b1ba7e1c2";
const buyerAddress = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045";
module.exports = async function(callback) {
  try {
    const marketplace = await ClosedDesert.deployed();
    const erc721 = new web3.eth.Contract(IErc721.abi, collectionAddress);
    const salesPrice = web3.utils.toWei("1", "ether");
    //buyerAddress = await web3.utils.toChecksumAddress(buyerAddress);
    // marketplace needs the seller's approval to transfer their tokens
    const approval = await erc721.methods.approve(marketplace.address, tokenId).send({from: sellerAddress});
    const sellReceipt = await marketplace.sellNFT(collectionAddress, tokenId, salesPrice, {
      from: sellerAddress
    });
    const { offerHash } = sellReceipt.logs[0].args;
    const oldOwner = await erc721.methods.ownerOf(tokenId).call();
    console.log(`owner of ${collectionAddress} #${tokenId}`, oldOwner);
    const oldSellerBalance = web3.utils.toBN(await web3.eth.getBalance(sellerAddress));
    console.log("Seller Balance (Eth):", web3.utils.fromWei(oldSellerBalance));
    // buyer buys the item for a sales price of 1 Eth
    const buyReceipt = await marketplace.buyNft(offerHash, {from: buyerAddress, value: salesPrice});
    const newOwner = await erc721.methods.ownerOf(tokenId).call();
    console.log(`owner of ${collectionAddress} #${tokenId}`, newOwner);
    const newSellerBalance = web3.utils.toBN(await web3.eth.getBalance(sellerAddress));
    console.log("Seller Balance (Eth):", web3.utils.fromWei(newSellerBalance));
    console.log("Seller Balance Diff (Eth):", web3.utils.fromWei(newSellerBalance.sub(oldSellerBalance)));
  } catch(e) {
    console.error(e)
  } finally {
    callback();
  }
}

Примечание. Константы collectionAddress, sellerAddress и buyerAddress должны быть законными адресами основной сети, которые соответствуют вышеупомянутым критериям, а sellerAddress и buyerAddress должны быть разблокированы в вашем экземпляре Ganache. Константа tokenId также должна быть фактической tokenId NFT, которой владеет продавец.

В этом вспомогательном скрипте мы настраиваем ссылки на контракты, с которыми будем взаимодействовать. Мы решили получить в коде примера совместимую с EIP-2981 коллекцию Azuki, но это могла быть любая коллекция NFT. Запускаем скрипт с помощью следующей команды:

npx truffle exec scripts/testMarketplace.js --network mainfork

Если все работает правильно, вы должны получить вывод в консоли, подобный следующему:

owner of Azuki 0xed5af388653567af2f388e6224dc7c4b3241c544 #9183 0x27b4582D577d024175ed7FFB7008cC2B1ba7e1C2
Seller Balance (Eth): 0.111864414925655418
owner of Azuki 0xed5af388653567af2f388e6224dc7c4b3241c544 #9183 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045
Seller Balance (Eth): 1.061864414925655418
Seller Balance Diff (Eth): 0.95

Давайте пробежимся по шагам, которые только что произошли, чтобы мы могли понять, как это работает. Во-первых, сценарий требует одобрения продавца на передачу своего NFT после его продажи, что обычно выполняется соответствующими контрактами на рынке. Затем мы создаем предложение о продаже, позвонив по номеру sellNft от имени текущего владельца. Наконец, мы просто повторно используем хэш предложения, содержащийся в событии продажи, и позволяем нашему покупателю вызвать метод buyNft и отправить запрошенную цену продажи в 1 ETH.

Когда вы сравните баланс продавца до и после сделки, вы заметите, что он получил не запрошенную сумму в 1 Eth, а только 0,95. Остальные средства были переведены получателям лицензионных отчислений Azuki, как это было указано в договоре о реестре лицензионных отчислений основной сети.

Заключение

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

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