阅读 142

NFT 这么火,你知道 ERC721 么

如果对币圈稍微有些关注的人,这几个月一定被 NFT 刷屏了。面对一张张卖出天价的 NFT,绝大多数人都无法理解,其实对于 NFT,贵的不是那张图,而是大家的共识。

这篇文章暂时不讨论 NFT 的价值问题,而是来起底一下 NFT 的技术支撑。

目前绝大多数的 NFT 资产都在以太坊发行,占了主流资产的 90% 以上。目前 NFT 的发行标准最主流的是 ERC721,这已经成为事实上的标准。

ERC 是什么

在开始说 ERC721 之前,需要先说明一下 ERC,以太坊还在不断的发展,包括协议和各类标准,就像互联网行业的 RFC 一样,每个人都可以都对以太坊的发展提出自己的意见。每个意见都被称之为 EIP(Ethereum Improvement Proposals),直接在这个 GitHub 仓库(github.com/ethereum/EI…)中提交。

EIP 的完整流程如下:

  • Draft:由作者提交的建议,还在做主要的修改
  • Review:建议已经基本完成,可以进行 EIP 评审
  • Last Call:评审完成,这个建议有可能成为最终版
  • Accepted:这个建议在等待以太坊开发者的实现或者部署
  • Final:EIP 会成为一个以太坊的标准

大多数的 EIP 都会止步于 Review 阶段。

ERC(Ethereum Request for Comment) 用于记录以太坊上的各种开发标准和协议,部分 EIP 会成为 ERC。 ERC 都是 EIP,反之则不对。

ERC 721 也是通过这个流程提出来的。

ERC 721 的全称是非同质化代币标准(Non-Fungible Token Standard)。以太币和在以太坊网络上流通的一些代币称之为同质化代币。这些同质化代币发行的标准是 ERC 20 标准。如果代币不实现这些标准,那么就无法在以太坊网络中流通。

从流通的角度来说,ERC 721 和 ERC 20 的实现都是以太坊网络中的代币。它们最大的区别在于,ERC 721 的每一个代币都是独一无二的,有着自己的属性,相互之间是不等价的。而 ERC 20 的每一枚代币都是相同的,是等价的。

ERC 721 协议详解

ERC 721 其实是定义了一系列的接口,如果写过 Java 的人会发现,这个接口的形式与 Java 的非常类似。下面接口定义使用的以太坊的智能合约语言 Solidity:

pragma solidity ^0.4.20;

interface ERC721 /* is ERC165 */ {
    
    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);
}

interface ERC165 {
    function supportsInterface(bytes4 interfaceID) external view returns (bool);
}

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

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);
}

interface ERC721Enumerable /* is ERC721 */ {
    
    function totalSupply() external view returns (uint256);

    function tokenByIndex(uint256 _index) external view returns (uint256);

    function tokenOfOwnerByIndex(address _owner, uint256 _index) external view returns (uint256);
}
复制代码

balanceOf 方法用来判断一个地址下有多少个 NFT,ownerOf 方法用来判断一个 NFT 是不是属于一个地址。transferFrom 和 safeTransferFrom 都是用于 NFT 的转账,但是 safeTransferFrom 在把 NFT 转到零地址时会报错。零地址就是以太坊中的黑洞,任何转入其中的资产都无法取出。

approve 类的地址就是把自己的 NFT 托管给其他管理,这点要注意,千万随意在陌生的网站上执行力 approve 操作,特别是 setApprovalForAll 方法,否则自己的 NFT 别人就尅随意操控了。

如果要你要发行自己的 NFT,使用 solidity 实现之后,发布到以太坊上,就发行成功了。在 ERC 721 中,推荐把上面的所有接口都实现。

在 ERC 721 中,基本包含了 ERC 20 的所有的接口,非同质化代币虽然是独一无二的,但也需要能够转账等代币的基础特性。

相比于 ERC 20,ERC 721 最大的不同是 有了 ERC721Metadata 这个接口,这个接口可以用来标识每一个非同质化代币的属性,也就是 NFT 的元数据。每个非同质化代币的具体属性都通过 tokenURI 这个方法来返回。我们在 OpenSea 上看到的那些 NFT,都是通过调用这个方法获取 NFT 的详情。

实现 ERC721 时,必须实现 ERC165 协议,这个用来检测当前的合约的是否实现了某个接口,在以太坊中,每一个 interface 都有自己的 interfaceId,比如 ERC165 的是 0x01ffc9a7,ERC 721 的是 0x80ac58cd,ERC721Metadata 的是 0x780e9d63,ERC721Enumerable 的是 0x5b5e139f。

ERC721TokenReceiver 用于在调用 transfer 方法之后的回调,如果传的值对不上,就会导致这次 transfer 失败。

ERC721Enumerable 接口中则实现了 NFT 的一些不变的属性,比如总供应量,通过代币的序号来获取 NFT,列举某个地址下的所有 NFT。

ERC 721 实例

一起来看一下 Meebits 的智能合约,合约的地址可以在 etherscan.io/address/0x7… 这里看到。这就是以太坊上应用的神奇之处,所有的代码都是公开的。

Meebits 是 Larva Labs 发行的一组 NFT,总数2 万个。

代码的总行数只有 679 行,逻辑都还是比较简单的。比如我们上面说到标识 NFT 元数据的 tokenURI 方法,Meebits 是这样实现的:

function tokenURI(uint256 _tokenId) external view validNFToken(_tokenId) returns (string memory) {
    return string(abi.encodePacked("https://meebits.larvalabs.com/meebit/", toString(_tokenId)));
}
复制代码

实际上是指向了另外一个地址,我我们在这个 url 后面随机输入一个编号,就可以得到一个 Meebits 的元数据:

// https://meebits.larvalabs.com/meebit/1
{
  "name": "Meebit #1",
  "description": "Meebit #1",
  "image": "http://meebits.larvalabs.com/meebitimages/characterimage?index\u003d1\u0026type\u003dfull\u0026imageType\u003djpg",
  "attributes": [
    {
      "trait_type": "Type",
      "value": "Human"
    },
    {
      "trait_type": "Hair Style",
      "value": "Bald"
    },
    {
      "trait_type": "Hat",
      "value": "Backwards Cap"
    },
    {
      "trait_type": "Hat Color",
      "value": "Gray"
    },
    {
      "trait_type": "Shirt",
      "value": "Skull Tee"
    },
    {
      "trait_type": "Overshirt",
      "value": "Athletic Jacket"
    },
    {
      "trait_type": "Overshirt Color",
      "value": "Red"
    },
    {
      "trait_type": "Pants",
      "value": "Cargo Pants"
    },
    {
      "trait_type": "Pants Color",
      "value": "Camo"
    },
    {
      "trait_type": "Shoes",
      "value": "Workboots"
    }
  ]
}
复制代码

这里我们重点来看几个上面的没有讲到的地方,上面的 ERC 721 中的方法是我们全都都要实现的,但是只实现上面的那些代码却不够,因为在一个 NFT 项目中,发行出来之后, NFT 是要拿出去售卖的,而不是靠 transfer 来手动转出。meebits 公开售卖的方法如下:

function mint() external payable reentrancyGuard returns (uint) {
    require(publicSale, "Sale not started.");
    require(!marketPaused);
    require(numSales < SALE_LIMIT, "Sale limit reached.");
    uint salePrice = getPrice();
    require(msg.value >= salePrice, "Insufficient funds to purchase.");
    if (msg.value > salePrice) {
        msg.sender.transfer(msg.value.sub(salePrice));
    }
    beneficiary.transfer(salePrice);
    numSales++;
    return _mint(msg.sender, 0);
}
复制代码

在这个方法中,限制了开售的时间和每次购买的个数,如果还未开售,或者超过了限购次数就会购买失败。当然,如果你账户中的钱不够,也会购买失败。购买收到的钱都会转入到当前的合约账户下。

售卖 NFT 的钱自然不能永远放在合约账户中,而是要提出来。所以还需要一个提款的方法:

function withdraw(uint amount) external reentrancyGuard {
    require(amount <= ethBalance[msg.sender]);
    ethBalance[msg.sender] = ethBalance[msg.sender].sub(amount);
    (bool success, ) = msg.sender.call{value:amount}("");
    require(success);
    emit Withdraw(msg.sender, amount);
}
复制代码

这个方法也很简单,也许有人会有疑问,这个方法谁都可以调用,那岂不是谁都可以来把钱提走了其实并不是,如果是当前合约之外的账号过来提钱,那么都只能提到 0,因为 ethBalance 数组中并没有其他账号的钱,所以只能是当前合约账号才能把钱提出来,这点设计也很巧妙。

ERC 721 不是终点

ERC 721 虽然现在很受欢迎,但满足不了所有的场景。比如在游戏场景中,很多的装备一次可能会发行多个,多达几千种装备类型,如果为每种装备发行一个 NFT,那就要发行几千个合约,这个代价很大,也会对以太坊的资源造成很大的浪费。

ERC 1155 的目标就是解决这个问题,可以在一个合约中定义多种 NFT,也可以为一种 NFT 定义多个数量,后续我们再详细展开 ERC 1155。

文 / Rayjun

[1] eips.ethereum.org/EIPS/eip-20

[2] eips.ethereum.org/EIPS/eip-72…

[3] eips.ethereum.org/EIPS/eip-11…

[4]eips.ethereum.org/EIPS/eip-16…

文章分类
后端