教程区块链区块链技术ch1818.2 基础 ERC-721 智能合约:所有权、铸造与转移

本页目录

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 实现
超额支付退款多余 ETHmsg.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

评论加载中…

发表评论

0/2000