本文详细介绍了如何使用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 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!