2021年3月11日,佳士得历史上首次拍卖以 NFT 形式展现的纯数字艺术品,这幅底价100美元的作品最终以6934.6万美元成交,一举成为在世艺术家成交作品第三高价。
这一事件,将越来越多的目光吸引向 NFT 的世界。对于区块链行业外的人士而言,NFT 还是一个新鲜事物。但实际上,NFT 很早就已经出现了——即 在2017 年末以太坊上兴起的谜恋猫游戏。“谜恋猫是世界首款区块链游戏。“区块链”是支持类似比特币这样的加密货币的运作技术基础。尽管谜恋猫不是数字货币,但它也能提供同样的安全保障:每一只谜恋猫都是独一无二的,而且100%归您所有。它无法被复制、拿走、或销毁。”当时这款游戏推出后便风靡一时,一度导致了以太坊网络的堵塞。
不过相信大多数人还是对NFT停留在应用上的认知,今天我们来尝试从新的角度来认识它。
NFT 是 Non-Fungible Token 的缩写,中文称为「非同质化代币」,通常是指开发者在以太坊平台上根据 ERC721 标准/协议所发行的代币。
既然NFT被称作「非同质化代币」,那么相应的肯定有「同质化代币」,下图是二者一个形象的举例。
同质化代币的典型代表就是以 ERC20 标准/协议发行的 token。 在 ERC20 标准/协议我们中可以知道,ERC20 的 token 是指定小数位数的,也就是 token 可以任意分割,token 与 token 之间是等价的。简单来说同质化代币就是一种能够相互替换、具有统一性、可被拆分的代币。就像美元之间可以进行交换,即使序号不同,价值却不会有任何不同。
非同质化代币的典型代表就是以 ERC721 标准/协议发行的 token。而在 ERC721标准/协议 中则定义的是一种不可分割的 token ,每个 token 都是独一无二的,我们可以追踪每个 token 的转移和其价值的变动的。可以认识到非同质化代币就是由于包含了记录在其智能合约中的识别信息使得每种代币都具有独一无二的特性,因此不能被另一种代币直接取代。
说了NFT的独特,那我们再来谈谈它是怎么被铸造的,基于什么逻辑实现它的独一无二。
以下非同质化代币的铸造分析,原始代码参考以太坊ERC721 提案。
首先非同质化代币的铸造会执行
_safeMint
函数,该函数存在两个参数,他们分别的作用是:
to
:新铸的代币接受地址
tokenId
:新铸代币的 id 序列
function _safeMint(address to, uint256 tokenId) internal virtual {
_safeMint(to, tokenId, "");//接收以上两个参数加上空字符凑齐3个参数传入同名 _safeMint 函数(重载)
}
可以看到接下来调用三参数同名 _safeMint
函数,
function _safeMint(address to, uint256 tokenId, bytes memory _data) internal virtual {//接收_safeMint 函数传参
_mint(to, tokenId);//将传参 to 和 tokenId 传入 _mint 函数
require(_checkOnERC721Received(address(0), to, tokenId, _data), "ERC721: transfer to non ERC721Receiver implementer");//调用_checkOnERC721Received函数来检测函数接口合法性
}
最后调用 _mint
函数进行铸币:
function _mint(address to, uint256 tokenId) internal virtual {//接收来自_safeMint 函数的传参
require(to != address(0), "ERC721: mint to the zero address");//校验传参地址是否为空(为假继续后续操作,为真终止操作)
require(!_exists(tokenId), "ERC721: token already minted");//接着校验 tokenId 是否已经存在(为假继续后续操作,为真终止操作)
_beforeTokenTransfer(address(0), to, tokenId);//初始化;
_balances[to] += 1;//在传参地址 to 上生成一个 token
_owners[tokenId] = to;//将传参地址 to 与引索 tokenId 进行绑定
emit Transfer(address(0), to, tokenId);//触发 token 生成事件
}
就这样一个独一无二的 NFT 被铸造出来了。总体过程如下:
_safeMint
函数调用铸币函数并检查函数的 ERC721 函数接口合法性,该函数在 ERC721 中有所体现,代码如下: function _checkOnERC721Received(address from, address to, uint256 tokenId, bytes memory _data)
private returns (bool)
{
if (to.isContract()) {
try IERC721Receiver(to).onERC721Received(_msgSender(), from, tokenId, _data) returns (bytes4 retval) {
return retval == IERC721Receiver(to).onERC721Received.selector;
} catch (bytes memory reason) {
if (reason.length == 0) {
revert("ERC721: transfer to non ERC721Receiver implementer");
} else {
// solhint-disable-next-line no-inline-assembly
assembly {
revert(add(32, reason), mload(reason))
}
}
}
} else {
return true;
}
}
_mint
函数中首先检查了地址是否是0地址,还有 tokenId 是否已存在。该 tokenId 在 ERC721 标准中生成规则是一个不可互换的通证。检测通过后为 to 地址铸币(相应的 tokenId ),同时记录余额与该 NFT 艺术品的归属者。 看到这,相信你也了解到什么是 NFT 了,来让我们通过部署一个 ERC721 智能合约来进行铸造演示。
需要用到的工具有remix,metamask和IPFS。
我选择制成 NFT 的是一张可爱的猫猫图片。
在使用这张图片前我们需要对它进行一些处理,这里要用到 IPFS 工具:
打开终端初始化 IPFS repo;
$ ipfs init
再打开一个终端开启 daemon 服务;
$ ipfs daemon
在第一个终端中输入以下命令将图片加入到 IPFS 中,并复制下生成的 hash;
$ ipfs add 2.png
向 IPFS 添加 JOSN 文件,同样复制下生成的 hash。
$ ipfs add nft.json
JSON文件格式
{
"name": "NFT Art",
"description": "This image shows the true nature of NFT.",
"image": "https://ipfs.io/ipfs/QmWdRcjx6Yhjvu97qKjVhCj7Zj8wZpVuFu4aape1MTbxib",
}
我们直接通过模拟合约在 remix 上测试。
NFT 模拟测试合约如下,为了方便和安全,我们使用了 0xcert/ethereum-erc721 合约来创建我们的 NFT 合约。
// SPDX-License-Identifier: MIT
pragma solidity 0.8.0;
import "https://github.com/0xcert/ethereum-erc721/src/contracts/tokens/nf-token-metadata.sol";
import "https://github.com/0xcert/ethereum-erc721/src/contracts/ownership/ownable.sol";
contract NFT is NFTokenMetadata, Ownable {
uint256 public constant NFT_SIGN_BIT = 1 << 255;
uint256 public constant ID_PREFIX_MASK = uint256(~uint152(0)) << 104;
uint256 public totalSupply = 0;
constructor() {
nftName = "MaoMao NFT";
nftSymbol = "MMN";
}
function mint(address _to, uint256 tokenIdPre, string calldata _uri) external onlyOwner {// 铸币
uint256 _tokenId = NFT_SIGN_BIT | (tokenIdPre & ID_PREFIX_MASK) |
(block.timestamp << 64) | uint64(totalSupply + 1);// 生成ID
_mint(_to, _tokenId);//为指定地址铸币
super._setTokenUri(_tokenId, _uri);//使用tokenId 和 JSON 文件的 URI 设置代币 URI
}
function batchMint(address _to, uint256[] memory tokenIdPres, string calldata _uri) external onlyOwner {// 批量铸币
uint256 length = tokenIdPres.length;
for (uint256 i = 0; i != length; ++i) {
uint256 _tokenId = NFT_SIGN_BIT | (tokenIdPres[i] & ID_PREFIX_MASK) |
(block.timestamp << 64) | uint64(totalSupply + 1);// 生成ID
_mint(_to, _tokenId);//为指定地址铸币
super._setTokenUri(_tokenId, _uri);//使用tokenId 和 JSON 文件的 URI 设置代币 URI
}
}
}
使用 Injected Web3 进行部署。编译合约前一定要在 Metamask 上选择测试网络,我选择的是 Kovan 测试网络。
现在进入 Remix 中的 Deployed Contracts 部分,展开部署的合约。你会看到一堆函数/方法。展开 mint 函数并添加以下参数:
接下来在我们的钱包就可以看到一个名叫 MMN 的 NFT 已经生成了。
相信仔细阅读了测试合约代码的小伙伴会发现,我并没有直接使用传参 tokenIdPre 作为代币的标识,而是将它作为依据由算法生成的随机数来作为代币标识。为什么要这样呢?不知道大家有没有玩过盲盒——指消费者不能提前得知具体产品款式的玩具盒子,具有随机属性。很明显 NFT 同样适用于盲盒属性,只要适当的使用随机数就能有各种各样的 NFT 盲盒玩法。
现实玩法如在 BlindBoxes dApp 中,创作者通过主智能合约在 xDai 生成空的收藏品盒,创作者将NFT存入NFT Holder(dApp部署在以太坊上的智能合约);NFT Holder 将其状态通过AMB同步到 xDAI 上的主智能合约;创作者通过主智能合约往收藏品盒中添加一个或多个 NFT,之后可以选择发布。这些发布的 NFT 会通过 RandomAura(基于 RANDAO 链上随机数生成器)引入随机性,确保盲盒的随机性。
但是盲盒也可能不"盲",因为盲盒的随机性是有随机数生成器生成的,一但随机数生成器被破解,如各类预言机被破解事件,足以提醒我们要使用安全的随机数生成器。
以下让我们来分析一些简单的盲盒随机化规则,谈谈其安全性:
function _randomRole(uint index) internal returns (uint8){
require(msg.sender == tx.origin, "human only");//仅允许个人地址调用该函数
uint result = (block.timestamp.mul(index)).mod(TOTAL_ROLE);//抽奖结果
return uint8(result);
}
首先我们可以看到该函数仅允许被个人地址调用,这样可以杜绝黑客使用其它方法来调用该函数。
接着就是最关键的抽奖结果的生成,一共涉及两个变量一个常量:
index
由调用该函数的个人决定block.timestamp
由当前的区块决定TOTAL_ROLE
总量53不会改变 这时问题就出现了,作为该函数的调用者一共能控制变量 index
和常量 TOTAL_ROLE
,那如果黑客能控制最后一个变量,是不是他就能操作抽奖结果?
而这并非不能办到。因为链上的数据都是可以计算和预测的,甚至矿工还可以提前计算结果。而当黑客能预测 block.timestamp
值时,也就相当于他控制了该变量。
所以盲盒真的会不"盲",使用安全的随机数极为重要。
无论什么时代都会有人最求独一无二,数字化的世界里同样如此。NFT 的出现既是对人们追求独一无二欲望的满足,也是数字化时代认证方式的开拓。相信在不断完善的未来世界,NFT 一定能发挥出举足轻重的作用,但同时我们也不能对其安全掉以轻心。