【源码解读】你买的 NFT 到底是什么?
内容概要
如果你是 WEB3 加密界的新手,面对众多概念无从入手,那么欢迎你,来对地方了!!
本文围绕标准 ERC721 协议,描述了 Mint、 safeMint、 transfer 等是如何实现资产管理的,并通过解读代码来了解它的安全性设计和以太坊数据上链成本构成。
目录大纲
1.所谓 NFT 资产是什么?
2.Mint 和 safeMint 的差别
3.交易时会发生什么?有哪些细节设计
4.NFT 哪些数据也存储在链上?
5.以太坊上存储有多贵?
面向对象
-
Web3 新手,有无技术背景均可:
-
研发——可无障碍阅读,理解精美的合约设计
-
非研发——可能读不懂列举的代码,但能体会标准协议的设计思路
-
正文
1.所谓 NFT 资产是什么?
在 opensea 上,可看到每个 NFT 都有个唯一的编号。
比如 azuki 系列中第 4132 号,在页面的 Details 栏目可以看到其合约地址,ID 编号,部署所在公链等信息,而 Properties 栏目则是其设定的具备各种属性,对应的稀有度(非 azuki 本身携带,而是 opensea 整合计算的)。
1.1 资产在标准 ERC721 协议里是什么?
而咱们回顾到源代码(此处取 ERC721 标准库 openzepplin 代码),会发现程序记录了全局性的两个字典类型的变量,通过 **_owners
中用数字映射地址的方式记录每一个ID
当前对应的所有者,同时也附带用_balances
**记录了当前所有者总计持有的 NFT 数量
mapping(uint256 => address) private _owners;
并且由于 ERC721 创新性的赋予了一个 ID 对应地址的变量 _owners
,从而与 ERC20 仅**_balances
**进行地址与余额的管理,区分出了 FT(同质化)与 NFT(非同质化)的差别。
2.Mint 和 safeMint 的差别
2.1 **Mint
**是如何进行的
**Mint
**意思为铸造,即每个 NFT 的创造过程,例如之前的
爱死机 NFT
十四君,公众号:十四君当奈飞的 NFT 忘记了 web2 的业务安全
**Mint
**获取到该 NFT 的资产证明。
从源代码中可以看到,**Mint
**主要是进行了安全判断:
-
判断 1:确保转入的不是
0x00
地址(黑洞地址无法转出,转入则资产损失) -
判断 2:确保此交易所操作的
NFTID
是不存在的
最终代码执行的操作是:
-
操作 1:将转入地址的
_balances
所持有总数加 1 -
操作 2:将对应
NFTID
的所有者修改为转入的地址 -
操作 3:完成交易则发出
emit
事件,可以让链下监听到这次交易的数据
/**
* @dev Mints `tokenId` and transfers it to `to`.
* Emits a {Transfer} event.
*/
function _mint(address to, uint256 tokenId) internal virtual {
require(to != address(0), "ERC721: mint to the zero address");
require(!_exists(tokenId), "ERC721: token already minted");
_beforeTokenTransfer(address(0), to, tokenId);
_balances[to] += 1;
_owners[tokenId] = to;
emit Transfer(address(0), to, tokenId);
_afterTokenTransfer(address(0), to, tokenId);
}
中间有 **_beforeTokenTransfer
**和 **_afterTokenTransfer
**属于虚函数,作为标准,是让项目方可以在不修改标准协议的情况下增加一些特定的逻辑代码用的。
2.2 为何**safeMint
更安全**
safeMint
意为安全的铸造,从代码实现中可以看到他本身也是调用了Mint
但是他额外增加了 **_checkOnERC721Received
**的判断,这点是属于 **ERC165
**的标准,相当于在完成转入操作后,则判断对方地址,是否是黑洞地址(即无法发起交易 NFT 操作的地址)是防止转入对象为合约地址时候,其合约没有预设置好转出的函数,导致资产在内无法被转走,从而造成永久损失:
/**
* @dev Same as {xref-ERC721-_safeMint-address-uint256-}[`_safeMint`], with an additional `data` parameter which is
* forwarded in {IERC721Receiver-onERC721Received} to contract recipients.
*/
function _safeMint(address to,uint256 tokenId,bytes memory _data)
internal virtual {
_mint(to, tokenId);
require(
_checkOnERC721Received(address(0), to, tokenId, _data),
"ERC721: transfer to non ERC721Receiver implementer"
);
}
2.3 **ERC165
是如何防止资产转入黑洞的?**
https://eips.ethereum.org/EIPS/eip-165___设计初衷可见: _
是让合约接口标准化的提案,在编程语法中**interface
**是接口的意思,在其中定义的函数可以不实现仅仅放上函数名字相关参数,在程序复杂的时候,相当于目录一般告诉别人我都有什么功能。但是接口的写法各有千秋,名字定义参数类型,甚至是否存在都有不同,所以此提案最终形成了 ERC165 标准,规范了接口的识别规则:
interface ERC165 {
/// @notice 查询合约所实现的接口
/// @param interfaceID 参数:接口ID
/// @return true 如果函数实现了 interfaceID (interfaceID 不为 0xffffffff )返回true, 否则为 false
function supportsInterface(bytes4 interfaceID) external view returns (bool);
}
使用流程是:
STEP 1 判断是否存在 **supportsInterface
**函数,并且其符合 165 标准
STEP 2 通过 **supportsInterface
**函数,判断是否具有转出 NFT 的函数
(PS:让合约具备 NFT 接收转出功能,可通过引入 IERC721Receiver.sol
__ 拓展包来实现)
3.交易时会发生什么?有哪些细节?
标准协议设计有两种转移方式,**transfer
**和 ****`transferFrom`**,
**作用于两种场景:
-
transfer
转移:由用户调用,将本消息发送的钱包所持有的 NFTID 转移到指定地址 -
transferFrom
从转移:用某机构调用,需要用户先授权某地址,让其有权可转移
类比一下:
- **
transfer
**就是现金交易,从自己口袋里拿钱支付 transferFrom
就是扫码扣款,由店家申请扣款,受制于用户是否开通小额代扣权限
接下来咱们从代码来看看,其中可能有会意想不到的细节。
3.1 **transfer
是如何进行的**
他会检测当前交易的 **from
**方是否是此NFTID
的持有者,并且限制该 NFT 转入0x00
地址。其次进行 **from
**转出地址和 **to
**转入地址的余额刷新,修改 _balances
全局变量并且重新设置_owners
此 NFTID 的所有者地址修改为to。
这里有个防护的细节会先执行_approve(address(0), tokenId)
**;
清空历史授权,**如果没有这一步,则资产完成了转移,但是其 NFTID 的转移授权依旧在,细思极恐:
function _transfer(address from,address to,uint256 tokenId) internal virtual {
require(ERC721.ownerOf(tokenId) == from, "ERC721: transfer from incorrect owner");
require(to != address(0), "ERC721: transfer to the zero address");
_beforeTokenTransfer(from, to, tokenId);
_approve(address(0), tokenId);// Clear approvals from the previous owner
_balances[from] -= 1;
_balances[to] += 1;
_owners[tokenId] = to;
emit Transfer(from, to, tokenId);
_afterTokenTransfer(from, to, tokenId);
}
3.2 **transferFrom
是如何进行的**
这里的交易本质调用的是 _safeTransfer
所以他的核心逻辑是 require
部分,这的一大细节是:_msgSender()
这是 **openzepplin
**的标准库 **Context.sol
**中的方法。
其实就是获取当前交易的发送者地址,但这里使用了封装版本,而不是直接使用**msg.sender
**是考虑到,可能存在一种交易类型“元交易”
,即交易的付费 gas 方和交易发起方不相同的情况。
所以一些处于中间环节的,类似 library 的合约需要考虑这种特殊情况。其余部分判断是确定是否有授权记录,易于理解,不作赘述:
function safeTransferFrom(address from,address to,uint256 tokenId,bytes memory _data) public virtual override { require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: transfer caller is not owner nor approved"); _safeTransfer(from, to, tokenId, _data);}
4.NFT 哪些数据也存储在链上?
交易的环节也看完后,其实很多新同学也顿感奇怪,原来我买的 NFT 只有一个 ID 的归属地址指向了我,从而达成了唯一性。那就算如此,稀有度信息放在哪里?我的 NFT 图像本身在哪里?
这就是涉及到 ERC721 的元数据拓展**IERC721Metadata.sol。
**要放什么都可以,但是项目方往往在链上只存储最基础的 ID+IPFS 的地址。
咱们可以通过之前**Etherscan
**教程方法来看看一些项目数据有什么,先取Azuki
上合约地址:
0xed5af388653567af2f388e6224dc7c4b3241c544
再通过**Read Contract
**可以查阅到,其元数据只存放了 ipfs 上的指向地址。而近期兴起的Metaverse
项目元宇宙土地 Sandbox
和Decentraland
,以及去年火热的Axie Infinity
,基本链上存储元数据也只是 ID+网址:
像 mirror 那些是专门设计低费用可进行高存储,一个块常规都是 30M 起步,大约是以太坊的 1000 倍。
5.以太坊上存储有多贵?
以下是本文稍难理解的地方。咱们从源码来分析链上存储的成本构成以及金额换算,成本产生将有 2 个方面,按执行流程来看:
- 用户发起一笔交易,将数据作为参数传入,其大小是一笔成本
- 交易执行合约代码,依据修改和使用,EVM 计算消耗的 gas 成本
5.1 交易发起的成本
咱们可以核对下以太坊黄皮书,里对交易数据大小所消耗 gas 有清晰的定义
可以看到交易所附带的参数的价格:
-
每笔交易都有 21000 GAS 需要支付
-
为交易的每个非零字节数据或代码支付 68 GAS
-
为交易的每个零字节数据或代码支付 4 GAS
所以如果是再 Mint
的时候,登记上若干 NFT 属性信息,交易的 data 部分会将 abc 等字符转成 2 个十六进制表示,而每个字符为一个八位二进制,等于一个 byte。所以可以约等于将 data 的长度除以 2 作为 byte 数。
而 1kb 的数据,如果都是非 0 的有信息量的文本信息,则等于增加是 68*1000=6.8W 的 gas 消耗。按20gwei
的 gas 价格和2000
的 eth 兑换美元价格,可以估算出,每上链 1kb 数据在交易发起端就要:
20*(21000+68000)*1e9/1e18 * 2000 =
3.5 美金
5.2 合约存储的成本
由于交易发起后,还有智能合约上存储的逻辑,咱们从以太坊 go 源代码中(EIP1283
),来分析具体的消费量,代码具体在函数内,太长了不全粘来:
func gasSStore(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error)
历史上 GAS 消耗的估算有经过若干迭代,如果是Petersburg
或者 Constantinople
未激活的话,则不按下面逻辑进行计算
gas 消耗计算,依赖 3 个种数据的管理形式(增删改)
- 从零值地址到非零值(NEW VALUE),每个存储槽需消耗 2Wgas
- 从非零值地址到零值地址(DELETE),每个存储槽需消耗 5Kgas,但会有奖励 1.5W gas 退回
- 从非零到非零(CHANGE),每个存储槽需消耗 200 gas
注意,上述每一个存储槽算**32byte
,1kb 存储则是 32 个存储槽。**Mint
的过程是新增存储,所以如果新增 1kb 的数据存储在链上代价将是 64Wgas,换算成金额则是:
20*(640000)*1e9/1e18 * 2000 =
25 美金
真可谓寸土寸金!
评论区