• NFT:使用 EIP-2981 开启 NFT 版税之旅


    NFT:使用 EIP-2981 开启 NFT 版税之旅

    未标题-3

    截屏2022-06-30 上午11.12.38

    随着ERC721标准的最终确定,NFT开始受到大量关注。这些被证明是独一无二的资产存储在区块链,并引入了一种新的方式来收集和交易艺术,音乐,PFP等。2021年夏天,由于NFT的人气飙升,创造和销售NFT成为了积累财富的一种快速方式。

    然而,当阅读底层规范时,我们会注意到在ERC721或ERC1155标准中没有获取和分配版税的功能。这些标准只处理所有权状态跟踪、批准和直接转让。

    如果接口不包含任何原生的版税功能,作为创造者,你如何制作能够在最初销售后长期积累财富的NFT?而像OpenSea这样的交易平台又如何分得一杯羹?如果你有一个更复杂的版税情况,比如分成版税,你要怎么办呢?

    本文将探讨NFT版税的几个方面。我们将研究实现版税的方法,包括专有解决方案、注册和EIP-2981。我们还将研究一种拆分付款的方法。最后,我们将运行一个项目,看看版税分成的实际效果。

    什么是NFT版税?

    在第一批NFT问世几个月后,市场合约就被建立了起来,它们允许持有者给他们的物品贴上价格标签,对它们进行出价和要价,并可以安全地与他人进行交易。许多这样的市场甚至不存储用户在链上的竞价交易;他们的匹配合约收集交易的链下签名,并将其存储在一个中心化的服务器基础设施上。在区块链上拥有一些独特的东西的想法让OpenSea成为世界上最成功的市场之一,一直在耗gas量排行榜上名列第一。

    那些逐步实现去中心化的市场的真正成功故事,是从他们每笔交易收取的费用中写出来的。例如,对于每个以100个ETH交易的Bored Ape,OpenSea通过完成链上交易赚取2.5个ETH。在他们的市场平台上留住创作者的一个目的是,为创造者提供从二级市场销售中长期获利的机会。在NFT领域,这通常被称为“版税”。

    为 NFT 创作者赚钱:铸造费和版税

    当有人通过部署ERC721合约来开始创造一个基本的NFT收藏品时,他们首先必须会考虑铸造,或者新的代币应如何进入生活当中?

    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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    NFT收藏品可以实现这些接口以返回版税金额和接收者。然后,当在市场合约上进行交易时,交易合约检查所涉及的NFT收藏品是否实现了其中一个接口,之后调用它,并使用它的返回值来相应地拆分费用。

    实际销售价格不是方法接口的一部分。相反,他们以基准点(bps)作为版税分成,这是一个在版税分配方案中常用的术语,通常理解为1/10000——500的分成意味着贸易价值的5%应该作为版税发给收藏品所有者。

    版税注册

    然而,专有接口可能会导致一些问题。NFT合约不知道哪些接口可能成为强制实现,因为他们无法预测他们的代币将在哪些市场上交易。更糟糕的是,如果他们在发布相关市场合约之前就启动了收藏品合约,那么他们就很难在之后添加相应的版税分配方案。

    为了解决这个问题,manifold.xyz的一个主要 NFT 市场联盟同意部署一个行业范围的注册合约,收藏品构建者可以使用该合约来独立于他们的代币合约,以此来表示版税分割。版权注册的开放源代码库显示,它支持许多最重要的市场接口。

    例如,如果一个NFT 收藏品所有者只实现了上面提到的Rarible 的一种版税分配方案,那么另一个不知道该接口的市场可以简单地调用公共注册中心的getRoyaltyView函数。它可以查询代币合约上所有已知的版税接口,并将任何响应转换为一个常用的结果。

    没有在合约中加入版税信号方案的收藏品所有者可以部署一个扩展的“覆盖”合约,并将其注册到公共注册中心。这个注册方法将确保只有收藏品所有者(由所有者公共成员标识)可以调用它。

    EIP-2981:一种跨市场发送NFT版税的标准

    在2020年,一些雄心勃勃的人开始定义一个通用接口:EIP-2981,该接口足够灵活,可以覆盖大多数与版税相关的用例,而且易于理解和实现。它只定义了NFT合约可以实现的方法:

    function royaltyInfo(uint256 _tokenId,  uint256 _salePrice) 
      external view 
      returns (address receiver, uint256 royaltyAmount);
    
    • 1
    • 2
    • 3

    它缺少一些特性:它既不关心多方之间的分裂,也不强加任何百分比或基点的概念。对于调用者来说,他们将接收到什么以作为返回值是非常清楚的,对于实现者来说,如何实现这一点也很简单。

    该接口也完全在链下工作,因此在替代基础设施上交易资产的市场仍然可以查询创建者的费用,而不需要知道除了EIP-2981方法的接口签名之外的任何其他东西。

    该接口适用于ETH以及其他任何货币表示的销售金额。实现者只需用_salesprice除以他们的计算基数,然后乘以相同基数的版税百分比。实现者可以运行复杂的逻辑来计算取决于外部因素的动态版税,但最好较少的执行该方法,因为它将在交易双方之间的销售转让交易期间执行,而且他们的gas费用应该是相当低的。

    为了了解EIP-2981实现是什么样的,这里有一个可以在1/1 NFT 收藏品上找到的代码片段,它表示原始创建者的地址和他们对任何与标准兼容的市场的版权声明:

    https://gist.github.com/elmariachi111/4df402dcefa4a86c78545e5e0a44bc6b

    // contracts/Splice.sol
    // SPDX-License-Identifier: MIT
    pragma solidity 0.8.10;import '@openzeppelin/contracts/utils/math/SafeMath.sol';contract OneOnOneNFTMarketPlace {
      using SafeMath for uint256;  struct RoyaltyReceiver {
        address creator;
        uint8 royaltyPercent;
      }  mapping(uint256 => RoyaltyReceiver) royalties;  function mint(
        /*...mint args...*/
        uint8 _royaltyPercent
      ) public {
        //... minting logic ...
        uint256 token_id = 1;
        royalties[token_id] = RoyaltyReceiver({
          creator: msg.sender,
          royaltyPercent: _royaltyPercent
        });
      }  function royaltyInfo(uint256 tokenId, uint256 salePrice)
        public
        view
        returns (address receiver, uint256 royaltyAmount)
      {
        receiver = royalties[tokenId].creator;
        royaltyAmount = (royalties[tokenId].royaltyPercent * salePrice).div(100);
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    如果你正在使用OpenZeppelin的ERC721基础合约来构建NFT合约,那就可能已经注意到他们最近添加了一个ERC721Royalty基础合约,该合约中包含管理方法和私有成员,以简化专用代币版税的处理。

    ERC1155

    市场并不是唯一一个让用户从版税计划中获利的应用程序。例如,Treum的EulerBeats在他们的合约集合中使用了多代币标准ERC1155,这些合约代表了结合计算机生成的音乐和生成艺术作品的 NFT。在铸造了种子代币之后,用户可以从中获得有限数量的print,并且每个print的价格会沿着代币合约定义的绑定曲线增加。

    每铸造 Enigma 种子的新print时,合约就会将铸造费的50%转让给当前种子的拥有者。如果接收方实现了平台特定的IEulerBeatsRoyaltyReceiver接口,它甚至可以对版税支付做出反应,并在他们的种子的print出来后执行代码。

    PaymentSplitters:将NFT版税发送给多个接收者

    EIP-2981无法满足其他方法的开箱即用的用例。它只能向请求方发送一个版税接收地址。因此,在需要将版税分配给几个接收者的情况时,就必须单独实现。

    这可能会带来一些新的问题:首先,调用方/市场不一定要在触发交易的同一笔交易中发送资金,但可以决定稍后这样做,比如从另一个账户的 gas 高效多次调用中。其次,给地址的支付调用可能在gas使用量上受到严格限制。强烈建议在 Solidity 中的任何默认接收函数中使用尽可能少的 gas,因为发送者可能不知道他们正在转移资金到合约。

    最重要的考虑是,直接从合约交互中发送资金会增加陷入重入漏洞的风险。

    幸运的是,OpenZeppelin的PaymentSplitter原语允许设置单独的拆分合约,以保证资金的安全,直到他们的收款人认领了它们,他们的接收功能需要最低限度的gas来运行。NFT收藏品构建者可以创建一个内联PaymentSplitter,其中包含想要的受益人列表和他们各自的份额金额,并让他们的EIP-2981生成拆分合约的地址。

    对于许多用例来说,这种方法的权衡可能是可以忽略的:PaymentSplitter部署相对来说是gas密集型的,一旦拆分器初始化,就不可能替换收款人或份额。

    使用本地主网分叉测试 NFT 版税支出

    设计与任意NFT合约交互的市场并不是一件简单的任务,因为实时网络上的合约是否有根据 ERC 接口运行是不可预测的。然而,使用Ganache在这些合约中测试我们的代码是有帮助的。这个强大的工具允许我们在本地机器上创建以太坊网络的即时分叉,而无需设置自己的区块链节点。相反,它依赖于Infura节点来读取我们正在交互的合约和帐户的当前状态。

    在启动区块链实例之前,让我们克隆概念验证的存储库,更改到新目录,并安装依赖项:

    git clone https://github.com/elmariachi111/royalty-marketplace.git
    cd royalty-marketplace
    npm i
    
    • 1
    • 2
    • 3

    要了解这个 NFT 市场示例中发生了什么,就应该看一下合约文件夹中的ClosedDesert.sol代码。

    // 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);
      }
    }}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    在我们的示例中,卖方可以在批准转让后以固定的销售价格列出其资产。买家可以观察OnSale事件,并通过发布buyNft交易和发送想要的Eth值来做出响应。市场合约在销售交易期间检查开放的主网NFT版税注册,查看收藏品所有者是否要求版税,然后相应地支付版税。如上所述,公共版税注册中心已经将EIP-2981兼容合约考虑在内。还支持许多其他专有分发方案。

    接下来,我们将部署本地区块链实例,并使用真实用户的帐户和NFT测试我们的合约。

    为了测试主网条件下的合约行为,我们首先需要通过请求项目ID并在我们的机器上本地安装Ganache v7来访问Infura主网节点。然后,我们可以使用我们最喜欢的NFT市场来找一个收藏品,并找到NFT持有者帐户,该帐户将在我们的测试中扮演卖方的角色。卖方必须实际拥有我们将出售的 NFT。

    最后,找到一个有足够主网资金(至少1个Eth)的账户来支付卖方要求的销售价格。有了这些帐户和工具在手,我们可以在新的终端窗口中使用以下命令启动本地的Ganache主网实例:

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

    确保在上面的命令中使用自己的Infura主网端点作为URL。

    img

    如果你找不到需要解锁的账户,可以试试以下几个:

    卖方地址:0x27b4582d577d024175ed7ffb7008cc2b1ba7e1c2
    买方地址:0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045

    我们在 Ganache 实例中模拟以太坊主网,所以当你读到这篇文章时,卖方可能不再拥有我们将要出售的NFT,或者买方可能不再有足够的Eth来实际购买。因此,如果这些地址不起作用,你将不得不找到符合上述标准的地址。

    使用上面的示例地址,我们的命令如下所示:

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

    接下来,在我们原来的终端窗口中,我们将从存储库中编译和部署市场合约,并选择我们本地的主网分叉提供商,可以在truffle-config.js中找到:

    npx truffle compile
    npx truffle migrate --network mainfork
    
    • 1
    • 2

    现在我们可以在主网条件下测试我们的版税市场合约,而不用支付一分钱的gas费用。所有即将进行的交易将由本地Ganache链代表真实用户的帐户执行。

    让我们看看testMarketplace.js脚本(在scripts文件夹中),我们将使用它来与我们部署的市场智能合约进行交互:

    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();
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    collectionAddresssellerAddressbuyerAddress常量必须都是符合上述条件的合法主网地址,而sellerAddressbuyerAddress都必须在你的 Ganache 实例中解锁。tokenId常数也必须是tokenId卖方拥有的 NFT 的实际值。

    在这个助手脚本中,我们将设置与我们进行交互的合约的引用。我们决定在示例代码中获取兼容EIP-2981的Azuki收藏品,也可以是任何其他NFT收藏品。我们使用以下命令运行脚本:

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

    如果一切正常运行,你应该在控制台中收到如下输出:

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5

    让我们回顾一下刚刚发生的步骤,这样我们就能理解它是如何工作的。首先,该脚本要求卖方批准在其出售 NFT 后转移其 NFT,这一步骤通常由各自的市场合约处理。然后,我们通过代表当前所有者调用sellNft来创建一个销售报价。最后,我们简单地重用sale事件中包含的offer散列,并让我们的买家调用buyNft方法并发送1个ETH的请求销售价格。

    当你比较卖家交易前后的余额时,你会注意到他们没有收到要求的1个ETH,而只有0.95。剩下的资金已经转移给了Azuki的版税接收者,因为它们是接收到由主网版税注册合约发出的信号。

    结论

    版税是NFT领域成功的主要驱动力。以前是专有市场的附加功能,它们已经演变成不可替代的代币经济的强制性属性。它们是一个很好的经济概念,能够以一种激励原始代码作者或NFT艺术家的方式分配销售收入。

    ERC721没有包含任何经济特征的概念;因此,NFT版税不能由代币合约直接强制执行。相反,市场建设者必须为代币合约提供接口,以表明他们对交易费用的要求以及将它们发送到哪里。EIP-2981版税信号接口是一个简洁而强大的行业标准 。每个新的ERC721合约都应该考虑至少实现一个基本的版税信号接口,这样专有市场工具就可以获取并引用它。

    Source:https://medium.com/@michael-bogan/enabling-nft-royalties-with-eip-2981-1cc7cf4378a9

    关于

    ChinaDeFi - ChinaDeFi.com 是一个研究驱动的DeFi创新组织,同时我们也是区块链开发团队。每天从全球超过500个优质信息源的近900篇内容中,寻找思考更具深度、梳理更为系统的内容,以最快的速度同步到中国市场提供决策辅助材料。

    Layer 2道友 - 欢迎对Layer 2感兴趣的区块链技术爱好者、研究分析人与Gavin(微信: chinadefi)联系,共同探讨Layer 2带来的落地机遇。敬请关注我们的微信公众号 “去中心化金融社区”

    img

  • 相关阅读:
    ALTER TABLE 分区操作-动态增加一级,多级分区,动态删除分区
    宁波大学NBU计算机嵌入式系统期末考试题库(二)
    系统平台关键词怎么补?具体操作思路
    2024042期传足14场胜负前瞻
    设计模式再探——宏观篇
    ffmpeg 中av_rescale_rnd 的含义
    commons-io
    Flutter 3.3 之 SelectionArea 好不好用?用 “Bug” 带你全面了解它
    计算机毕业设计ssm汽车资讯网站wlri7系统+程序+源码+lw+远程部署
    分布式锁:不同实现方式实践测评
  • 原文地址:https://blog.csdn.net/chinadefi/article/details/125543108