使用Solidity实现的链上NFT智能合约

本文详细介绍了如何使用Solidity从头开始创建一个链上NFT智能合约。该合约将NFT的元数据和艺术作品直接存储在区块链上,通过abi.encodePacked等函数将数字资产编码并转换为字节,实现完全的链上存储,包括合约代码、SVG图像生成以及mint和burn函数的功能。

再次问好,作为我之前关于自定义 ERC-20 代币智能合约实现的帖子的后续,我决定这次实现一个非同质化代币的智能合约。

这些年来,甚至在我进入区块链并最终进入开发领域之前,我就听说了很多关于 NFT 的信息,并且一直在想如何创建一个 NFT。最近,在学习 solidity 时,我了解了代币标准以及它们的工作原理,当我发现我实际上可以从头开始链上创建我自己的 NFT 合约时,我感到非常着迷,在本文中,我将逐步向你展示我是如何完成我的实现的。

NFT(Non-Fungible Tokens,非同质化代币)通过为数字资产提供可验证的稀缺性和来源,彻底改变了数字所有权。这些数字资产的价值是独一无二的,即使它们看起来相同,这与 ERC-20 代币不同,在 ERC-20 代币中,合约中的所有资产必须具有相同的价值。

链上 / 链下存储

本文分解了一个链上 NFT 智能合约的实现,该合约直接在区块链上存储元数据和艺术品,这种实现被称为完全链上的,因为 NFTS 不会被上传到像 IPFS 这样的去中心化文件存储平台等等,而是,数字资产通过特殊的 solidity 函数(例如 abi.encodePacked)进行编码并转换为字节,该函数接受不同的字符串并将它们组合成一个字符串,以及用于将数组转换为字节字符的函数。

实现

## License, Pragma, and Imports
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28;
import '@openzeppelin/contracts/token/ERC721/ERC721.sol';
import '@openzeppelin/contracts/utils/Base64.sol'; //用于将 svg 编码为 base64
import '@openzeppelin/contracts/utils/Strings.sol';
import '@openzeppelin/contracts/access/Ownable.sol';

第一行指定了 UNLICENSED 标识符,表明未经明确许可,不得授予使用此代码的许可。

第二行定义了要使用的 Solidity 编译器版本。`⁰.8.28` 符号表示该合约应使用 0.8.28 版本或 0.8.x 范围内的任何兼容的较新版本进行编译。

import 语句从 OpenZeppelin 引入库,OpenZeppelin 是一个用于智能合约的开源平台,这些智能合约已经过实战测试,开发人员可以轻松地在其应用程序中引用,从而节省了构建应用程序的时间。在本智能合约的使用案例中,导入了以下库:

ERC721:以太坊上非同质化代币的标准接口

Base64:用于将二进制数据(如 SVG 图像)编码为 Base64 格式的实用程序

Strings:用于字符串操作和转换的实用程序

Ownable:一种授权机制的实现,用于将某些操作限制为合约所有者

合约声明和状态变量

contract KMSNFTOnChain is ERC721, Ownable {
using Strings for uint256;
uint256 private _tokenIdCounter;
error TokenNotFound();

我们的合约名为`KMSNFTOnChain`, 继承自 ERC721 (NFT 标准) 和 Ownable (用于访问控制)。

using Strings for uint256 语句使我们能够直接在整数值上调用字符串转换方法,这对于代币 ID 格式化非常有用。

我们声明一个私有状态变量 _tokenIdCounter 来跟踪最后铸造的代币 ID。

我们还定义了一个自定义错误 TokenNotFound(),它比使用字符串错误消息更节省 gas。

修饰器和构造函数

// Modifiers
modifier onlyOwner() {
require(owner() == msg.sender, 'Caller is not the owner');
_;
}
constructor() ERC721('KemsguyNFT', 'NKFT') Ownable(msg.sender) {}

onlyOwner 修饰器将函数访问限制为仅合约所有者。它检查调用者 ( msg.sender) 是否为所有者,如果不是,则会显示错误消息。_; 表示将执行修改函数的代码的位置。

构造函数使用以下内容初始化我们的 NFT 合约:

1. 代币集合名称:“KemsguyNFT”

2. 代币符号:“NKFT”

3. 将合约部署者设置为所有者(通过`Ownable(msg.sender)`)

这个简单的构造函数不需要任何参数,并建立我们 NFT 收藏的基本标识。

代币元数据生成

function tokenURI(uint256 tokenId) public view override returns (string memory) {
if (ownerOf(tokenId) == address(0)) revert TokenNotFound();
string memory name = string(abi.encodePacked('KemsguyNFT #', tokenId.toString()));
string memory description = 'This is an on-chain NFT';
string memory image = generateBase64Image();
string memory json = string(
abi.encodePacked(
'{"name":"',
name,
'",',
'"description":"',
description,
'",',
'"image":"',
image,
'"}'
)
);
return string(abi.encodePacked('data:application/json;base64,', Base64.encode(bytes(json))));
}

tokenURI 函数是任何 NFT 合约的关键部分,因为它提供了特定代币的元数据。此函数:

1. 首先检查代币是否存在——如果所有者是零地址(对于有效代币来说是不可能的),则会显示 TokenNotFound 错误

2. 通过将 "KemsguyNFT #" 与代币 ID 组合,为代币创建一个名称

3. 设置所有代币的简单描述

4. 调用 `generateBase64Image()` 函数以获取 SVG 图像作为 Base64 编码的数据 URI

5. 将这些组件组合成一个具有 name、description 和 image 字段的 JSON 结构

6. 将整个 JSON 编码为 Base64,并将其作为以 “ data:application/json;base64,” 开头的数据 URI 返回

这种方法将所有代币元数据存储在链上,使其完全去中心化并免受链接失效的影响(即存储在链下的元数据变得无法访问)。

SVG 图像生成

function generateBase64Image() internal pure returns (string memory) {
string memory svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500">'
'<defs>'
'<linearGradient id="bgGradient" x1="0%" y1="0%" x2="100%" y2="100%">'
'<stop offset="0%" stop-color="#1a2a6c"/>'
'<stop offset="50%" stop-color="#b21f1f"/>'
'<stop offset="100%" stop-color="#fdbb2d"/>'
'</linearGradient>'
'<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">'
'<feGaussianBlur stdDeviation="5" result="blur"/>'
'<feComposite in="SourceGraphic" in2="blur" operator="over"/>'
'</filter>'
'</defs>'
'<rect width="100%" height="100%" rx="15" ry="15" fill="url(#bgGradient)"/>'
'<circle cx="250" cy="150" r="40" fill="#000000" stroke="#ffffff" stroke-width="2"/>'
'<path d="M230 190 L220 300 L280 300 L270 190" fill="#000000" stroke="#ffffff" stroke-width="2"/>'
'<path d="M230 200 L180 230 L190 250 L230 220" fill="#000000" stroke="#ffffff" stroke-width="2"/>'
'<path d="M270 200 L330 180 L335 200 L270 220" fill="#000000" stroke="#ffffff" stroke-width="2"/>'
'<path d="M230 300 L210 380 L240 380 L250 300" fill="#000000" stroke="#ffffff" stroke-width="2"/>'
'<path d="M270 300 L290 380 L260 380 L250 300" fill="#000000" stroke="#ffffff" stroke-width="2"/>'
'<circle cx="180" cy="230" r="15" fill="#ff0000" stroke="#ffffff" stroke-width="2"/>'
'<circle cx="335" cy="200" r="15" fill="#ff0000" stroke="#ffffff" stroke-width="2"/>'
'<text x="250" y="430" font-family="Impact, sans-serif" font-size="40" text-anchor="middle" fill="#ffffff" filter="url(#glow)">KEMSGUY FIGHTER</text>'
'<path d="M100 50 L120 70 L100 90 L80 70 Z" fill="#ffcc00" stroke="#ffffff" stroke-width="1"/>'
'<path d="M400 50 L420 70 L400 90 L380 70 Z" fill="#ffcc00" stroke="#ffffff" stroke-width="1"/>'
'<rect x="150" y="50" width="200" height="40" rx="10" ry="10" fill="rgba(255,255,255,0.2)" stroke="#ffffff" stroke-width="1"/>'
'<text x="250" y="77" font-family="Arial, sans-serif" font-size="20" text-anchor="middle" fill="#ffffff">KMS FIGHTER #001</text>'
'</svg>';
return string(abi.encodePacked('data:image/svg+xml;base64,', Base64.encode(bytes(svg))));
}

generateBase64Image 函数创建一个 SVG 图像作为字符串。此 SVG 定义了一个程式化的战士角色,具有:

- 从深蓝色到红色再到黄色的渐变背景

- 带有红色格斗手套的轮廓人物

- 装饰元素和文字

- 带有发光效果的 “KEMSGUY FIGHTER” 品牌

构造 SVG 字符串后,该函数:

1. 使用 OpenZeppelin 的 Base64 库将其编码为 Base64

2. 在前面加上数据 URI 方案 “data:image/svg+xml;base64,”

3. 返回完整的数据 URI

这项技术允许图像完全存储在链上,并在 NFT 市场上和钱包中呈现,就像任何外部托管的图像一样。

铸造函数

function mint() public {
_tokenIdCounter += 1;
_safeMint(msg.sender, _tokenIdCounter);
}

mint 函数允许任何人创建一个新的 NFT。它被故意保持简单:

1. 增加代币 ID 计数器

2. 调用内部 _safeMint 函数(从 ERC721 继承)以创建代币并将其分配给调用者的地址

此实现允许无限的公共铸造,除了交易本身的 gas 费用外,没有任何限制或成本。

销毁函数

function burn(uint256 tokenId) public onlyOwner() {
_burn(tokenId);
}

burn 函数永久销毁代币,将其从流通中移除。此函数:

1. 使用 onlyOwner 修饰符将访问权限限制为仅合约所有者

2. 调用 ERC721 实现中的内部 _burn 函数来销毁代币

此功能可用于从流通中删除伪造或有问题的代币,但由于只有合约所有者可以调用它,因此代币持有者本身无法销毁他们拥有的代币。

此 NFT 的所有数据都直接存储在区块链上。这包括:

- SVG 图像(作为 SVG 标记的字符串)

- 元数据(名称、描述和图像)

这种方法有利有弊:

优点:完全去中心化、免受链接失效的影响、只要区块链存在,就可以保证可用性

缺点:部署和铸造的 गैस 成本更高,复杂艺术品的空间有限

SVG 用于艺术品

该合约使用 SVG(可缩放矢量图形)作为代币艺术品。SVG 是链上存储的理想选择,因为:

1. 它是基于文本的,因此可以直接存储在智能合约代码中

2. 它是可缩放的,因此可以以任何大小显示而不会损失质量

3. 它受到 Web 浏览器和 NFT 市场的广泛支持

Base64 编码

图像和元数据都以 Base64 编码,这会将二进制数据转换为 ASCII 文本。这种编码是必要的,因为:

1. 区块链存储使用文本

2. 数据 URI 需要 Base64 编码

3. 它允许将复杂数据直接嵌入到 tokenURI 中

仅所有者可用的代币销毁实现

通过以下方式将销毁功能限制为合约所有者:

1. onlyOwner 修饰器,它根据合约所有者检查调用者的身份

2. 将此修饰符直接应用于 burn 函数

此限制非常重要,因为销毁会永久性地将代币从流通中移除。通过将此权力限制为合约所有者,该实现可防止代币持有者销毁他们自己的代币,这对于维护收藏的完整性可能很有用。

  • 原文链接: blog.blockmagnates.com/a...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
blockmagnates
blockmagnates
江湖只有他的大名,没有他的介绍。