代码中学习NFT智能合约
NFT 或不可替代的代币是 DeFi 之后的下一个趋势。就炒作程度和参与人数而言,NFT 远远领先于之前在给定文章中介绍的竞争对手。
在谷歌请求的数量方面,NFT 一词已经赶上了 2017 年“ICO”一词的指标。从 2021 年 2 月 4 日到 3 月 4 日,非同质代币在 12 个最大平台上的交易量达到创纪录的 4.8 亿美元。除了加密货币行业的代表外,许多企业家、政治家和创意精英都对这一趋势表示了兴趣。百事可乐在2021年12月投放了其创世的NFT集合 Pepsi Mic Drop,截止到今天,该项目的交易量为 2.8k
,项目地址。本文来仔细学习 Pepsi NFT
合约,了解是如何实施这个项目的。
基础知识
如果了解 ERC-721
并有编码经验,则可以跳过本节。
-
Solidity
:到目前为止,大多数 NFT 合约都使用 Solidity,无需了解 Solidity 即可理解该概念。但是如果以后想自己实现一个 NFT 合约,一些编码经验会很有帮助。要学习 Solidity,强烈建议阅读官方文档。 -
ERC-721
:Pepsi NFT 建立在Open Zeppelin
实施的ERC-721
智能合约之上。简单来说,ERC-721
标准提供了 NFT 的基本功能,包括转账、查询账户余额、铸造等。
智能合约在哪里?
区块链中许多项目都是开源的,开源链、开源合约等,开源透明是区块链行业的主要特征之一。
百事可乐在官网公布了其 NFT 合约地址。可以简单地复制该地址并在 Etherscan 中找到,这是查看以太坊区块链信息的首选网站,点击查看 Pepsi Mic Drop。
通过单击按钮 Contract
,将看到 14
个文件。不过只需要查看第一个文件 PepsiMicDrop.sol
。文件包含 Pepsi NFT
合约,其余的是Open Zeppelin
的 ERC-721 合约。
智能合约中的 NFT
在深入学习代码之前,先了解智能合约是如何定义 NFT 的。在底层,NFT 有两个主要信息:Id
和 URI
。
在 ERC-721
标准下,每个 NFT 都是独一无二的。因此,需要合约为每个 NFT 分配一个唯一的 Id
,从而实现唯一性。在基于 ERC-721
的合约中,不可能存在两个相同 ID
的不同 NFT。
接下来是图像在哪里(也可以是视频、文档等)?这就是 URI 发挥作用的地方。URI 代表通用资源标识符,更加详细的介绍参阅《图解URL、URI和URN 区别》。将其视为一个 URL,通过它可以获取与 NFT 相关的所有元数据。元数据可以是图像 URL、描述、属性等。当前最流行的 NFT 市场 OpenSea 有其元数据标准,在这里可以找到完整的元数据列表。
URI 和 Id 能保证 NFT 的唯一性吗?
理想情况下,基于 ERC-721
的 NFT 将拥有其独特的图像或元数据,这就是它们不可替代的原因。但是,理论上是可以将相同的 URI 分配给不同的 Id,从而创建两个从表面上看相同的 NFT。然而,这不是 ERC-721
的希望的,有另一个标准 ERC-1155
,它支持半可替代 NFT,ERC-1155 文档。
为什么元数据没有存储在合约中?
因为在区块链上存储数据非常昂贵,尤其是图像或视频。但是,仍然可以在链上存储元数据并支付高昂的 gas
费。一些项目使用 SVG 格式的图像,这将大大减少了数据大小,从而降低了 gas
费用。
智能合约结构
智能合约初看可能觉得很复杂,但它的结构设计的很好,下图为智能合约的结构,来看看每个组件。
- SPDX许可证表达式(
SPDX License Identifier
):SPDX许可证信息,告诉其他人可以如何使用此代码。 (第 48 行) - 智能合约版本:让编译器正确编译代码,然后 EVM 才能理解。 (第 49 行)
- 导入 ERC-721 合约:百事NFT合约将使用
ERC-721
合约作为其蓝图。 PepsiMicDrop
:将在下一节进一步讨论的 NFT 合约,它包含三个部分:(1)状态变量(2)构造函数(3)函数。(第56行)- 状态变量:是其值永久存储在合约存储中的变量。
- 构造函数:是一个特殊函数,仅在合约创建时执行,可以运行合约初始化代码。
- 函数:大多数函数用于设置或获取状态值。
PepsiMicDrop 合约
现在来看看 PepsiMicDrop 合约,通过它的功能来解释合约,每个功能都与一些函数和状态变量相关。
构造函数
constructor() ERC721("Pepsi Mic Drop", "PEPSIMICDROP") {
reserveMicDropsId = 1; // item 1-50
micDropsId = 51; // item 51-1893
}
构造函数定义了 2 个输入:
Pepsi Mic Drop
是 NFT 代币名称PEPSIMICDROP
是 token symbol
分配发生在 ERC721 合约代码中,该合约在第 51 行导入。这就是为什么找不到与此构造函数主体相关的代码逻辑的原因,这是继承来完成的不需要重做
在构造函数内部,可以看到 2 个状态变量 reserveMicDropsId
和 micDropsId
获得了新值。为什么需要它们?在这次 NFT 中,百事将前 50 个 NFT 保留给自己,因此公开可用的 NFT 从 id 51
开始。
NFT 不是图像吗?为什么在这里使用数字来表示 NFT?如果有这样的问题就需要研究什么是 NFT。
铸造
现在来看看铸造函数:
function mint(bytes32[] memory proof, bytes32 leaf) public returns (uint256) {
// merkle tree
if (merkleEnabled) {
require(keccak256(abi.encodePacked(msg.sender)) == leaf, "This leaf does not belong to the sender");
require(proof.verify(merkleRoot, leaf), "You are not in the list");
}
require(saleStarted == true, "The sale is paused");
require(msg.sender != address(0x0), "Public address is not correct");
require(alreadyMinted[msg.sender] == false, "Address already used");
require(micDropsId <= maxMint, "Mint limit reached");
_safeMint(msg.sender, micDropsId++);
alreadyMinted[msg.sender] = true;
return micDropsId;
}
第一部分:Merkle Proof
mint
函数有两个输入,证明 proof
和 leaf
,它们用于第 3 行到第 7 行的 Merkle Proof
。本质上,它检查用户是否有资格铸造 NFT,有一个概念叫白名单,通常白名单账号,才能在以后铸造 NFT。
第二部分:Prerequisites
第 8 到 11 行,定义了 mint
的 4 个先决条件,下面来逐一解释:
require(saleStarted == true, "The sale is paused"); // 第 8 行
require(msg.sender != address(0x0), "Public address is not correct"); // 第 9 行
require(alreadyMinted[msg.sender] == false, "Address already used"); // 第 10 行
require(micDropsId <= maxMint, "Mint limit reached"); // 第 11 行
- 在 NFT 开始销售之前,状态变量
saleStarted
的值为false
,所以它不会执行第 8 行。当销售开始时,合约所有者可以调用函数startSale()
来更改saleStarted
的值。 - 第 9 行:检查确保用户地址不是
0x0
,0x0
是以太坊创世地址,没有用户会使用它。 - 第 10 行:在
Pepsi Drop
中,每个地址只能铸造一个 NFT。alreadyMinted
是 Map 类型的状态变量,跟踪所有铸造 NFT 的地址。 - 第 11 行:铸造数量有限,最多
1983
个 NFT,检查是否所有 NFT 都已被认领,micDropsId
是上面讨论的唯一令牌 ID。
第三部分:真正的铸造
这是铸造实际发生的地方,实际编码不需要自己实现它,因为 ERC-721 已经做好了。
_safeMint(msg.sender, micDropsId++);
为了铸造,使用用户的钱包地址和唯一的代币 ID 调用函数 _safemint()
,在底层,有一个状态变量(如字典)来跟踪每个令牌的所有权。通过调用 _safemint()
函数来更新这个状态变量,分配或更改相应令牌的所有权。状态变量 micDropId
表示令牌 ID,并且在每次铸造后立即递增 1
。
第四部分:更新铸造地址
如第二部分所述,每个地址只能铸造一个 NFT,下面这行代码确保了成功铸造 NFT 的地址都会被记录下来。
alreadyMinted[msg.sender] = true;
返回
铸造成功后,返回 token id
。
更新URI
现在讨论的最后一部分是更新 URI 的函数。
function setBaseURI(string memory _baseUri) public onlyOwner {
baseURI = _baseUri;
}
每个 NFT 都有其 URI,这是用于更改基本 URI 的函数。基础 URI 是每个 NFT 的 URI 之间的组成部分。默认情况下,URI 是 baseURI/tokenId
,之所以设置 baseURI
是为了节省 gas
费用。
可以通过覆盖 ERC-721 合约中定义的函数 tokenURI
来更改 baseURI
和 tokenId
的组合方式。例如,如果 URI 是这样格式化的,可以执行 baseURI/tokenId + '.json'
之类的操作。
那么什么是 public
和 onlyOwner
,这些是函数修饰符,它们定义了函数运行的条件。public
意味着公开的,所有人都可以调用。部署后,通过网站(更具体地说,JavaScript)与合约进行交互,交互方法或者变量就是使用 public
来定义。onlyOwner
表示只有合约所有者才能调用该函数,只希望合约所有者具有更改令牌 URI 的能力。
总结
上面对百事可乐 NFT 智能合约进行解读,其实大部分的 NFT 智能合约的结构都差不多。