NFT智能合约是什么东西?

就是能实现NFT基本功能的在区块链上的代码。

一个NFT智能合约,应该怎么写,应该实现什么功能?

如果你正在学习这方面知识,而且一知半解的样子,本文能让你醍醐灌顶。

本文面向的还是小白观众,尽量不放代码,难度从浅入深,小白适可而止,别把自己难着了。

本文介绍的是符合ERC721标准的NFT智能合约,这是NFT目前最流行的合约标准。

本文示例的交易平台为OpenSea,这是目前最流行的NFT交易网站。

一、智能合约是个啥

智能合约是区块链上的代码。

人们把代码部署到区块链上,执行它,并把执行结果记录在区块链上。

区块链的安全性保证了代码不可被任何人篡改,代码正确执行(有bug的另说),执行结果不可篡改,并可以予以公开透明的展示。

以上4点的结合,是人类历史上从来没有过的。

二、搞NFT为什么要弄合约

因为这样玩更高级。

如果你直接在OpenSea网站上做NFT,也不是不可以,但明显不高级,因为你没有自己的智能合约。

OpenSea的智能合约是它的,不是你的,规则都得听它的。

如果你有自己的合约,NFT的玩法就是按你的来了。

所以,如果要来真的,就自己写代码吧。

三、写NFT合约要实现哪些功能

比如你要发行一套“虎虎生威”NFT,你要怎么写合约呢?

这个“虎虎生威”NFT,是一套老虎头像,有10000个,每个都是一个token。

这个合约要实现至少以下几个功能:

1、“铸造”(mint)功能。

NFT是非同质化代币,也就是一种“币”(token)了,既然是“币”(说是币,其实只是png图片而已),就要mint(铸造)了。执行一次mint,就会产生一个铸造好的token。

根据我前面的NFT科普文章,所谓铸造,就是在区块链上记载了一个token的ID和其拥有者的地址。

在计算机世界的术语里,有很多这种莫名其妙的说法,说铸造吧,也没有炉子,也没有高温,也没有金属,也没有模具,其实就单纯是个比喻,一开始会让人不习惯,时间长了就好了。

像“挖矿”、“铸造”、“销毁”、“桥”、“钱包”、“分叉”、“空投”、“分片”等等,一开始看上去是有点懵圈的,仔细研究一下就知道其实八杆子打不上关系,只是一个概念的借用,为了描述方便和好玩而已。

2、转移功能。

要能让拥有者把一个token转移给另外一个人。

3、查询功能。

要能查询某个token在谁手里,一个人有多少token,等等这种类似功能。

4、元数据功能。

元数据这个术语,在老百姓那里说出来有点装。其实就是描述某事物各种属性的信息,比如一个人的元数据,就是他的姓名、性别、年龄、肤色、身份证号码、职业、民族、照片等信息。

一个NFT的元数据,其实是说每个token的元数据,比如在虎虎生威NFT中,有10000个token,每个token都有其元数据,记录老虎头像各种属性的信息,诸如一个老虎的发型、肤色、性别、年龄、姿态、编号,以及存储这个老虎图像的链接。

由于图片一般比较大,所以图片本身都不放在以太坊上,而是放在web上或者IPFS上,链上只是存储了一个链接信息。

合约有了元数据功能,提供了tokenURI函数,人们就可以通过该函数的调用,获取某个token的元数据链接,然后读取元数据,并最终取得其图像。

OpenSea之所以可以展示你的NFT token,就是因为它调用你合约的tokenURI,获得元数据中的image项,然后读取图像的。

5、合约元数据功能。

如果你想把你的NFT放在OpenSea上作为一个Collection(收藏集)出现,就要让OpenSea能获取关于你Collection的一些基本设置。

合约元数据就是干这事的。

6、其他功能

比如你还想实现团队分账功能(团队成员按一定的比例获取收益)、白名单预售功能(只有白名单里的人才能在预售阶段mint)等等。

四、怎么写合约

自己要写的并不多,一般200~300行就差不多了。

共性的那些内容,尤其是ERC721的实现,可以使用现成的别人写好的代码,比如OpenZeppelin1(以下简称OZ)就提供了很多实用的功能。

用的时候,继承OZ的合约即可,比如:

import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
……
contract MyNFT is Ownable, ERC721Enumerable, PaymentSplitter {
……

1、mint功能实现

虽然可以直接调用OZ的ERC721.sol的_safeMint函数来实现mint,但最好外面再封装一层,写自己的mint函数,对于虎虎生威而言,你可以写一个huhu_mint,里面调用OZ的_safeMint即可。

自己写mint的好处是:至少可以控制铸造NFT的价格,以及每个地址可以mint的数量。

类似的可以考虑销毁(burn)功能,burn就是取消某tokenID和具体地址的绑定,或者理解为把这个tokenID转给地址0。直接用OZ的_burn函数即可。

2、转移功能实现

不用自己写,直接用OZ的ERC721.sol。

3、查询功能实现

不用自己写,用OZ的ERC721.sol及ERC721Enumerable.sol(枚举)即可。

ERC721主要提供的查询是:

  • balanceOf函数,查询某个地址持有的token数量。

  • ownerOf函数,查询某token的持有者地址。

ERC721Enumerable提供了如下3个功能:

  • 注意最重要的是:totalSupply函数,调用它返回目前已经铸造出来的NFT的个数。

  • tokenByIndex函数用来查询第index个token的ID是多少,也就是说通过这个函数和totalSupply函数,就可以遍历所有铸造出来的token。

  • tokenOfOwnerByIndex函数,给它一个地址和一个编号index,可以告诉你该地址拥有的第index个token是啥。结合balanceOf函数,就可以遍历一个地址拥有的所有token的ID。

4、元数据功能实现

OZ提供了IERC721Metadata接口,但功能是在ERC721.sol中实现的。 

主要是实现了name、symbol和tokenURI函数,调用后分别返回NFT名、NFT的缩写符号、token元数据的链接。

尤其注意tokenURI函数,给它一个tokenID,它返回该token元数据所在的URI。

你还需要自己实现一个外部可见的函数,用来设置baseURI(注意使用onlyOwner)。这样,如果原先的存储不可用了,就可以换一个地方存。

然后,重写_baseURI这个ERC721.sol中的内部函数,使之可以返回正确的根目录URI。

function setBaseURI(string memory _newBaseURI) public onlyOwner {
    baseURI = _newBaseURI;
  }
function _baseURI() internal view virtual override returns (string memory) {
    return baseURI;
  }

比如对于BAYC这个NFT,他的baseURI在:

ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/

然后,第23号猿猴的tokenURI就在:

 ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/23

读取其中的内容,就是:

{image:ipfs://QmadJd1GgsSgXn7RtrcL8FePionDyf4eQEsREcvdqh6eQe,attributes:[{trait_type:Mouth,value:Bored Pipe},{trait_type:Background,value:Aquamarine},{trait_type:Fur,value:Trippy},{trait_type:Eyes,value:Bored},{trait_type:Hat,value:Beanie}]}

5、合约元数据功能实现

实现一个contractURI函数2,告诉OpenSea你的NFT collection(收藏集)的元数据,比如收藏集的名字、描述、背景图、外部链接等。

比如可以写成这样:

{
  "name": "虎虎生威",
  "description": "在2022年农历虎年发行的专门逗你玩的NFT",
  "image": "https://weisir.com/huhu.png",
  "external_link": "https://weisir.com/huhu",
  "seller_fee_basis_points": 100, # Indicates a 1% seller fee.
  "fee_recipient": "0xA97F337c39cccE66adfeCB2BF99C1DdC54C2D721" 
}

6、其他功能实现

分账功能可以使用OZ提供的PaymentSplitter.sol。

白名单功能可以自己写,比较简单。

7、细节注意

a、每个符合ERC721的智能合约必须同时符合ERC721和ERC165,ERC165告诉外部自己支持哪些接口,外界通过调用supportsInterface 函数获悉一个合约是否支持ERC721。

b、如果你的合约被设计能够接受NFT转账,则需要实现ERC721TokenReceiver接口。

五、其他

1、安全考虑

最主要是防止重入攻击,所以要加上非重入保护,实现很简单,就是加锁。

OZ有个nonReentrant修饰符专门解决这个问题,对于涉及资金交易的函数,加上此修饰符即可。

这个修饰符是在ReentrancyGuard.sol中实现的,直接使用即可。

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

2、铸造入口

如前所述,OpenSea上展现的NFT都是铸造好的。

对于一个有自己智能合约的NFT,铸造过程并不是在OpenSea上完成的,而是通过自有途径完成。

通常,你需要自己做一个网页,通过web3.js,让用户自己来mint(花用户的gas费)。

当然,你也可以自己mint所有的token,这就不用做网页了,调用合约接口就可以。不过,这需要花自己的gas费,现在的人,都舍不得花gas费,千方百计让用户来花,挺有意思的。

六、更底层的细节

这里简单说一下ERC721,给有一定基础的同学观看,详细的内容可以自行搜索。

小白不用看这些,说实话,我都懒得看。

  • balanceOf函数,参数owner,它返回由owner持有的token的数量。

  • ownerOf函数,参数tokenId,它返回该token的持有者地址。

  • transferFrom函数,3个参数:from、to、tokenID,主人或被授权人调用后,把第tokenID号token从from转给to。

  • safeTransferFrom主要是实现可靠的转移,尤其是当to为一个合约时,调用该合约的onERC721Received方法,并且检查其返回值,如果该合约没有这个方法或返回值不对,则回退,避免token丢失。

  • approve函数,两个参数:地址to和tokenID。tokenID的主人调用此函数,授权to可以转移此token。比如张三approve了一个token给李四,李四就可以用transferFrom函数转走该token,from填李四的地址就行。(如果主人approve一个token给地址0,就取消了原先的授权。)

  • setApprovalForAll函数,两个参数:地址operator和布尔值approved,通过此函数,张三可以授予李四(operator)获取自己所有NFT的控制权(approved为True),也可以通过为False的approved收回此授权(说实话,这个函数设计得不太好,应该分成两个函数,而不是一个函数干两件事)。

  • getApproved函数,参数tokenID,可以得知主人将token授权给谁了。

  • isApprovedForAll函数,两个参数:owner和oprator,调用此函数,可以查询owner是否把自己所有token都授权给operator了。

上面就是ERC721的接口函数,当然,发行一个NFT,只有上面这些是不够的。

还需要实现我上面说的那些功能。

七、结束语

差不多就这些内容,如果你想做一个,还是要动手试一试。

如果仅仅就是想了解原理,这就够了。

2e33a3ce9aef9836f806ba7ecac485e1.png中国人民银行发行的2022年虎年金币

文|卫剑钒


  1. https://github.com/OpenZeppelin/openzeppelin-contracts/tree/master/contracts 

  2. https://docs.opensea.io/docs/contract-level-metadata

Logo

为所有Web3兴趣爱好者提供学习成长、分享交流、生态实践、资源工具等服务,作为Anome Land原住民可不断优先享受各种福利,共同打造全球最大的Web3 UGC游戏平台。

更多推荐