graph TD
部署者[部署者] --> 合约[ERC-721 合约]
合约 --> 铸造[safeMint→所有者]
合约 --> 转移[transferFrom→]
铸造 --> 所有者[所有者]
转移 --> 新所有者[新所有者]
合约 --> 元数据[TokenURI 链下镜像]
所有者 --> 授权[approve→被授权者]
授权 --> 新所有者
style 合约 fill:#c8e6c9
style 元数据 fill:#e3f2fd
style 新所有者 fill:#c8e6c9
NFT 智能合约的核心:记录谁拥有哪个 tokenId,以及如何把 tokenId 转移给新地址。
18.2.1 最小 ERC-721 实现
solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract MyNFT is ERC721, ERC721Enumerable, Ownable {
using Strings for uint256;
// === 状态变量 ===
string private _baseTokenURI; // 元数据基础 URL: https://ipfs.io/ipfs/Qm.../
uint256 public maxSupply = 10000; // 最大供应量
uint256 public totalMinted = 0; // 已铸造量
uint256 public mintPrice = 0.01 ether; // 铸造价格
bool public mintActive = false; // 铸造开关
// === 事件 ===
event Minted(address indexed to, uint256 indexed tokenId, uint256 price);
event BaseURIChanged(string newURI);
constructor(
string memory name,
string memory symbol,
string memory baseURI
) ERC721(name, symbol) Ownable(msg.sender) {
_baseTokenURI = baseURI;
}
// === 铸造功能 ===
function mint(uint256 quantity) external payable {
require(mintActive, "Minting is not active");
require(quantity > 0 && quantity <= 10, "Max 10 per tx");
require(totalMinted + quantity <= maxSupply, "Exceeds max supply");
require(msg.value >= mintPrice * quantity, "Insufficient payment");
for (uint256 i = 0; i < quantity; i++) {
uint256 tokenId = totalMinted;
_safeMint(msg.sender, tokenId);
totalMinted++;
}
emit Minted(msg.sender, tokenId, msg.value);
// 退款多余 ETH
if (msg.value > mintPrice * quantity) {
payable(msg.sender).transfer(msg.value - mintPrice * quantity);
}
}
// === 查询 ===
function tokenURI(uint256 tokenId) public view override returns (string memory) {
_requireOwned(tokenId);
return string(abi.encodePacked(_baseTokenURI, tokenId.toString(), ".json"));
}
function walletOfOwner(address owner) public view returns (uint256[] memory) {
uint256 balance = balanceOf(owner);
uint256[] memory tokenIds = new uint256[](balance);
for (uint256 i = 0; i < balance; i++) {
tokenIds[i] = tokenOfOwnerByIndex(owner, i);
}
return tokenIds;
}
// === 仅所有者 ===
function setBaseURI(string memory newURI) external onlyOwner {
_baseTokenURI = newURI;
emit BaseURIChanged(newURI);
}
function toggleMint() external onlyOwner {
mintActive = !mintActive;
}
function withdraw() external onlyOwner {
(bool success, ) = payable(owner()).call{value: address(this).balance}("");
require(success, "Withdraw failed");
}
// === 必需覆盖 ===
function supportsInterface(bytes4 interfaceId)
public view override(ERC721, ERC721Enumerable)
returns (bool)
{
return super.supportsInterface(interfaceId);
}
}18.2.2 安全要点
| 问题 | 防御 | 代码体现 |
|---|---|---|
| 重入 | 使用 _safeMint(内部检查) | OpenZeppelin 实现 |
| 超额支付 | 退款多余 ETH | msg.value - mintPrice * quantity |
| 无限铸造 | 上限检查 | totalMinted + quantity <= maxSupply |
| 权限控制 | 仅所有者 | onlyOwner modifier |
| 批量限制 | 单次上限 | quantity <= 10 |
18.2.3 Gas 优化
solidity
// 未优化: 每次循环都检查 supply
for (uint i = 0; i < quantity; i++) {
require(totalMinted < maxSupply, "..."); // 多余
// ...
}
// 已优化: 外部统一检查后批量铸造
require(totalMinted + quantity <= maxSupply, "..."); // 一次检查
for (uint i = 0; i < quantity; i++) {
// ...
}, 前往 → 18.3 铸造页 |*
评论
0评论加载中…