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,只有上面这些是不够的。
还需要实现我上面说的那些功能。
七、结束语
差不多就这些内容,如果你想做一个,还是要动手试一试。
如果仅仅就是想了解原理,这就够了。
中国人民银行发行的2022年虎年金币
文|卫剑钒
-
https://github.com/OpenZeppelin/openzeppelin-contracts/tree/master/contracts
-
https://docs.opensea.io/docs/contract-level-metadata
原文始发于微信公众号(卫sir说):揭秘:NFT智能合约到底都干了什么?