Web3

2025年07月12日更新 6 人订阅
原价: ¥ 10 限时优惠
专栏简介 Web3 学习之私钥保护 ——将私钥导入加密密钥库 Web3实战:使用web3modal SDK实现钱包连接并部署在Vercel React 学习之 createElement Web3 学习之钱包与链上交易速度问题以及与传统交易系统的对比 Web3 学习之GAS 机制与手续费详解 Web3学习之去中心化交易所(DEX) Web3学习之Uniswap Web3学习之Uniswap V2 的手续费计算 Web3学习之 ERC20 Web3学习之使用Foundry开发部署和开源ERC20合约 Foundry 高级实战:实现一个可升级的工厂合约 UpgradeableTokenFactory 升级合约源码分析 OpenZeppelin Foundry Upgrades upgradeProxy 深入解析 Uniswap V2 的手续费计算:公式推导与代码详解 全面指南:构建与部署以太坊多签钱包(MultiSigWallet)智能合约的最佳实践 利用 Chainlink Automation 自动化 Bank 合约:使用 Solidity 实现动态存款管理和自动转账 利用 Chainlink VRF 实现100 Token抽奖:从名单中随机选出幸运得主的完整指南 Op-Stack架构全景图:Layer 2 架构详解 钱包地址生成和作用 浏览器扩展、网页工具 require,revert,和assert的使用场景分别是什么样的? library 在使用上有什么限制 fallback 如何防范 ApproveScam 漏洞 透明代理 vs UUPS:智能合约升级模式全景解析与实用指南 MPC钱包和多签钱包的区别:一文看懂 BIP39和BIP44:你的加密货币钱包安全基石 Qtum 量子链:UTXO 交易的深度解析与实操指南 探索数据库系统:从概念到应用的全景概览 Solidity on Polkadot: Web3 实战开发指南 Web3 实践:在 Polkadot 上用 Solidity 玩转 Delegatecall Web3 新星:Monad 打造 NFT 全解 Ethers.js 实战:带你掌握 Web3 区块链开发 Web3 开发入门:用 Ethers.js 玩转以太坊交易与合约 玩转 Web3:用 Viem 库实现以太坊合约部署与交互 Web3新速度:Monad与BuyEarth DApp重塑虚拟世界 Web3开发必知:Solidity内存布局(Storage、Memory、Stack)解析 以太坊大变革:Vitalik 提议用RISC-V重塑未来! Web3实战:打造属于你的NFT数字资产 Web3 数据索引新利器:用 The Graph 打造 NFT 市场子图全攻略 用 Python 解锁 Web3:以太坊日志解析实战 Web3 数据神器:用 Go 解锁以太坊事件解析 用 Rust 解锁 Web3:以太坊事件解析实战 Web3 实战:解锁 Monad MCP,轻松查询 MON 余额 Web3 开发神器:Arbitrum Stylus 智能合约全攻略 解锁Web3未来:Rust与Solidity智能合约实战 Web3 新体验:Blink 一键解锁 Monad 未来 Alloy 赋能 Web3:Rust 区块链实战 Web3 开发实战:用 Foundry 高效探索以太坊区块链 Web3 金融:Uniswap V2 资金效率深度剖析 Uniswap V3 流动性机制与限价订单解析:资金效率提升之道 用 Rust 打造 Web3 区块链浏览器:从零开始的实战指南 探索Web3新速度:Sonic高性能Layer-1上的BlindAuction智能合约实践 Uniswap V2 合约部署全攻略:Web3 实践指南 重磅!国家级智库为人民币稳定币“出招”,上海香港或将联动!

Web3 新星:Monad 打造 NFT 全解

Web3新星:Monad打造NFT全解Web3浪潮席卷而来,高性能区块链成为开发者的新宠。作为Web3生态的新星,Monad以10,000TPS的超高吞吐量、500毫秒的区块速度和1秒交易确认,重新定义了区块链的可能性。本文将带你走进Monad的世界,通过打造Mo

Web3 新星:Monad 打造 NFT 全解

Web3 浪潮席卷而来,高性能区块链成为开发者的新宠。作为 Web3 生态的新星,Monad 以 10,000 TPS 的超高吞吐量、500 毫秒的区块速度和 1 秒交易确认,重新定义了区块链的可能性。本文将带你走进 Monad 的世界,通过打造 MonaPunk NFT,完整呈现从开发到部署的实战流程。无论你是 Web3 新手还是资深开发者,这篇全解都将为你开启 Monad 的探索之旅!

本文深入剖析了如何在 Web3 新星 Monad 区块链上开发并部署 MonaPunk NFT 智能合约。MonaPunk 基于 ERC721 标准,融合了可枚举、可暂停、可销毁和 URI 存储等功能。我们从 Foundry 项目初始化入手,编写合约代码并进行全面测试,覆盖铸造、转移和销毁等核心功能,随后在 Monad 测试网完成部署与验证。通过 Sourcify 验证合约并在区块链浏览器查看 NFT 元数据,测试覆盖率高达 100%(因 Foundry 显示问题,实际覆盖完整),展现了合约的稳健性。这是一份面向 Web3 开发者的 Monad NFT 实战指南。

Monad is an Ethereum-compatible Layer-1 blockchain with 10,000 tps of throughput, 500ms block frequency, and 1s finality.

实操

创建项目并切换到项目目录

mcd MonadArt # mkdir MonadArt && cd MonadArt

通过模版初始化项目

forge init --template https://github.com/qiaopengjun5162/foundry-template
Warning: This is a nightly build of Foundry. It is recommended to use the latest stable version. Visit https://book.getfoundry.sh/announcements for more information. 
To mute this warning set `FOUNDRY_DISABLE_NIGHTLY_WARNING` in your environment. 

Initializing /Users/qiaopengjun/Code/monad/MonadArt from https://github.com/qiaopengjun5162/foundry-template...
remote: Enumerating objects: 36, done.
remote: Counting objects: 100% (36/36), done.
remote: Compressing objects: 100% (30/30), done.
remote: Total 36 (delta 1), reused 31 (delta 1), pack-reused 0 (from 0)
展开对象中: 100% (36/36), 13.08 KiB | 837.00 KiB/s, 完成.
来自 https://github.com/qiaopengjun5162/foundry-template
 * branch            HEAD       -> FETCH_HEAD
 ... ...

处理 delta 中: 100% (1419/1419), 完成.
    Initialized forge project

设置禁用 Foundry nightly版本警告

export FOUNDRY_DISABLE_NIGHTLY_WARNING=1

查看项目目录

MonadArt on  master [✘?] on 🐳 v27.5.1 (orbstack) via 🅒 base 
➜ tree . -L 6 -I 'target|cache|lib|out|build'

.
├── CHANGELOG.md
├── LICENSE
├── README.md
├── _typos.toml
├── cliff.toml
├── foundry.toml
├── remappings.txt
├── script
│   ├── MonaPunk.s.sol
│   └── deploy.sh
├── slither.config.json
├── src
│   └── MonaPunk.sol
├── style_guide.md
└── test
    └── MonaPunk.t.sol

4 directories, 13 files

MonaPunk.sol 文件

// SPDX-License-Identifier: MIT
// Compatible with OpenZeppelin Contracts ^5.0.0
pragma solidity ^0.8.22;

import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import {ERC721Burnable} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721Burnable.sol";
import {ERC721Enumerable} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import {ERC721Pausable} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721Pausable.sol";
import {ERC721URIStorage} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";

contract MonaPunk is ERC721, ERC721Enumerable, ERC721URIStorage, ERC721Pausable, Ownable, ERC721Burnable {
    uint256 private _nextTokenId;

    constructor(address initialOwner) ERC721("MonaPunk", "MPUNK") Ownable(initialOwner) {}

    function pause() public onlyOwner {
        _pause();
    }

    function unpause() public onlyOwner {
        _unpause();
    }

    function safeMint(address to, string memory uri) public onlyOwner returns (uint256) {
        uint256 tokenId = _nextTokenId++;
        _safeMint(to, tokenId);
        _setTokenURI(tokenId, uri);
        return tokenId;
    }

    // The following functions are overrides required by Solidity.

    function _update(address to, uint256 tokenId, address auth)
        internal
        override(ERC721, ERC721Enumerable, ERC721Pausable)
        returns (address)
    {
        return super._update(to, tokenId, auth);
    }

    function _increaseBalance(address account, uint128 value) internal override(ERC721, ERC721Enumerable) {
        super._increaseBalance(account, value);
    }

    function tokenURI(uint256 tokenId) public view override(ERC721, ERC721URIStorage) returns (string memory) {
        return super.tokenURI(tokenId);
    }

    function supportsInterface(bytes4 interfaceId)
        public
        view
        override(ERC721, ERC721Enumerable, ERC721URIStorage)
        returns (bool)
    {
        return super.supportsInterface(interfaceId);
    }
}

MonaPunk.t.sol 文件


// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;

import {Test, console} from "forge-std/Test.sol";
import {MonaPunk} from "../src/MonaPunk.sol";
import {MonaPunkScript} from "../script/MonaPunk.s.sol";

contract MonaPunkTest is Test {
    MonaPunk public monaPunk;

    Account public owner = makeAccount("owner");
    Account public user1 = makeAccount("user1");
    Account public user2 = makeAccount("user2");

    string tokenURI = "ipfs://QmTestURI123";

    function setUp() public {
        monaPunk = new MonaPunk(owner.addr);
    }

    // 测试部署和基本信息
    function test_Deployment() public view {
        assertEq(monaPunk.name(), "MonaPunk", "name is not correct");
        assertEq(monaPunk.symbol(), "MPUNK", "symbol is not correct");
        assertEq(monaPunk.owner(), owner.addr, "owner is not correct");
    }

    // 测试铸造功能
    function test_SafeMint() public {
        // owner 铸造
        vm.startPrank(owner.addr); // 模拟 owner 调用
        uint256 tokenId = monaPunk.safeMint(user1.addr, tokenURI);
        vm.stopPrank(); // 停止模拟
        assertEq(tokenId, 0, "first tokenId is not 0");
        assertEq(monaPunk.ownerOf(0), user1.addr, "NFTToken owner is not correct");
        assertEq(monaPunk.tokenURI(0), tokenURI, "URI is not correct");
        assertEq(monaPunk.balanceOf(user1.addr), 1, "user1 balance is not correct");

        // 非 owner 铸造应该失败
        vm.prank(user1.addr); // 模拟 user1 调用
        bytes4 errorSelector = bytes4(keccak256("OwnableUnauthorizedAccount(address)"));
        vm.expectRevert(abi.encodeWithSelector(errorSelector, user1.addr));
        monaPunk.safeMint(user2.addr, tokenURI);
    }

    // 测试暂停功能
    function test_Pause() public {
        vm.prank(owner.addr); // 模拟 owner 调用
        monaPunk.pause();
        assertTrue(monaPunk.paused(), "paused is not correct");

        // 非 owner 暂停应该失败
        vm.prank(user1.addr);
        bytes4 errorSelector = bytes4(keccak256("OwnableUnauthorizedAccount(address)"));
        vm.expectRevert(abi.encodeWithSelector(errorSelector, user1.addr));
        monaPunk.pause();
    }

    // 测试暂停时转移失败
    function test_Pause_TransferFails() public {
        vm.prank(owner.addr);
        monaPunk.safeMint(user1.addr, tokenURI);
        vm.prank(owner.addr);
        monaPunk.pause();
        vm.prank(user1.addr);
        bytes4 errorSelector = bytes4(keccak256("EnforcedPause()"));
        vm.expectRevert(abi.encodeWithSelector(errorSelector));
        monaPunk.transferFrom(user1.addr, user2.addr, 0);
    }

    // 测试取消暂停
    function test_Unpause() public {
        vm.startPrank(owner.addr);
        monaPunk.pause();
        monaPunk.unpause();
        assertFalse(monaPunk.paused(), "unpause failed");

        monaPunk.safeMint(user1.addr, tokenURI);
        vm.stopPrank();
        vm.prank(user1.addr);
        monaPunk.transferFrom(user1.addr, user2.addr, 0);
        assertEq(monaPunk.ownerOf(0), user2.addr, "transfer failed after unpause");
    }

    // 测试销毁功能
    function test_Burn() public {
        // 1. Owner 铸造 NFT 给 user1
        vm.prank(owner.addr);
        monaPunk.safeMint(user1.addr, tokenURI);

        // 2. user1 销毁 tokenId=0
        vm.prank(user1.addr);
        monaPunk.burn(0);

        // 3. 查询已销毁的 token 应抛错
        bytes4 errorSelector = bytes4(keccak256("ERC721NonexistentToken(uint256)"));
        vm.expectRevert(abi.encodeWithSelector(errorSelector, 0));
        monaPunk.ownerOf(0);

        // 4. 测试非所有者销毁(应抛权限错误)
        vm.prank(owner.addr);
        monaPunk.safeMint(user1.addr, tokenURI); // 重新铸造 tokenId=1
        vm.prank(user2.addr); // user2 无权限
        errorSelector = bytes4(keccak256("ERC721InsufficientApproval(address,uint256)"));
        vm.expectRevert(abi.encodeWithSelector(errorSelector, user2.addr, 1));
        monaPunk.burn(1);
    }

    // 测试 Enumerable 功能
    function test_Enumerable() public {
        vm.startPrank(owner.addr);
        monaPunk.safeMint(user1.addr, tokenURI);
        monaPunk.safeMint(user1.addr, tokenURI);
        vm.stopPrank();
        assertEq(monaPunk.tokenOfOwnerByIndex(user1.addr, 0), 0, "first NFT ID incorrect");
        assertEq(monaPunk.tokenOfOwnerByIndex(user1.addr, 1), 1, "second NFT ID incorrect");
        assertEq(monaPunk.totalSupply(), 2, "total supply incorrect");
    }

    // 测试 URI 存储
    function test_URIStorage() public {
        vm.prank(owner.addr);
        monaPunk.safeMint(user1.addr, tokenURI);
        assertEq(monaPunk.tokenURI(0), tokenURI, "URI is not correct");

        bytes4 errorSelector = bytes4(keccak256("ERC721NonexistentToken(uint256)"));
        vm.expectRevert(abi.encodeWithSelector(errorSelector, 999));
        monaPunk.tokenURI(999);
    }

    // 测试接口支持
    function test_SupportsInterface() public view {
        assertTrue(monaPunk.supportsInterface(0x80ac58cd), "Not support ERC721");
        assertTrue(monaPunk.supportsInterface(0x780e9d63), "Not support Enumerable");
        assertTrue(monaPunk.supportsInterface(0x5b5e139f), "Not support URIStorage");
    }

    // 测试所有权管理
    function test_Ownership() public {
        vm.prank(owner.addr);
        monaPunk.transferOwnership(user1.addr);
        assertEq(monaPunk.owner(), user1.addr, "ownership transfer failed");

        vm.prank(user2.addr);
        bytes4 errorSelector = bytes4(keccak256("OwnableUnauthorizedAccount(address)"));
        vm.expectRevert(abi.encodeWithSelector(errorSelector, user2.addr));
        monaPunk.transferOwnership(user2.addr);

        vm.prank(user1.addr);
        monaPunk.safeMint(user2.addr, tokenURI);
        assertEq(monaPunk.ownerOf(0), user2.addr, "new owner mint failed");
    }

    // 测试批准后转移,覆盖 _update 和 _increaseBalance
    function test_ApproveAndTransfer() public {
        vm.prank(owner.addr);
        monaPunk.safeMint(user1.addr, tokenURI);
        assertEq(monaPunk.balanceOf(user1.addr), 1, "user1 balance incorrect after mint");

        vm.prank(user1.addr);
        monaPunk.approve(user2.addr, 0); // user1 批准 user2 操作 token 0
        vm.prank(user2.addr);
        monaPunk.transferFrom(user1.addr, user2.addr, 0);
        assertEq(monaPunk.ownerOf(0), user2.addr, "transfer after approve failed");
        assertEq(monaPunk.balanceOf(user1.addr), 0, "user1 balance incorrect");
        assertEq(monaPunk.balanceOf(user2.addr), 1, "user2 balance incorrect");
    }

    // 测试 _update 的失败场景
    function test_Update_InvalidToken() public {
        vm.prank(user1.addr);
        bytes4 errorSelector = bytes4(keccak256("ERC721NonexistentToken(uint256)"));
        vm.expectRevert(abi.encodeWithSelector(errorSelector, 999));
        monaPunk.transferFrom(user1.addr, user2.addr, 999); // 不存在的 tokenId
    }

    function test_Update_Unauthorized() public {
        vm.prank(owner.addr);
        monaPunk.safeMint(user1.addr, tokenURI);
        vm.prank(user2.addr);
        bytes4 errorSelector = bytes4(keccak256("ERC721InsufficientApproval(address,uint256)"));
        vm.expectRevert(abi.encodeWithSelector(errorSelector, user2.addr, 0));
        monaPunk.transferFrom(user1.addr, user2.addr, 0);
    }

    // 测试构造函数的间接覆盖
    function test_Constructor() public {
        MonaPunk newMonaPunk = new MonaPunk(user1.addr); // 部署新实例
        assertEq(newMonaPunk.owner(), user1.addr, "constructor owner incorrect");
        assertEq(newMonaPunk.name(), "MonaPunk", "constructor name incorrect");
        assertEq(newMonaPunk.symbol(), "MPUNK", "constructor symbol incorrect");

        // 额外验证初始状态
        vm.prank(user1.addr);
        newMonaPunk.safeMint(user2.addr, tokenURI);
        assertEq(newMonaPunk.ownerOf(0), user2.addr, "mint after constructor failed");
    }

    function test_Constructor_Hack() public {
        vm.startPrank(owner.addr);
        MonaPunk newMonaPunk = new MonaPunk(owner.addr);
        newMonaPunk.safeMint(user1.addr, tokenURI); // 触发所有初始化
        vm.stopPrank();
        assertEq(newMonaPunk.owner(), owner.addr, "owner incorrect");
        assertEq(newMonaPunk.name(), "MonaPunk", "name incorrect");
        assertEq(newMonaPunk.symbol(), "MPUNK", "symbol incorrect");
    }

    // 测试零地址转账
    function test_ZeroAddressTransfer() public {
        vm.prank(owner.addr);
        monaPunk.safeMint(user1.addr, tokenURI);

        vm.prank(user1.addr);
        bytes4 errorSelector = bytes4(keccak256("ERC721InvalidReceiver(address)"));
        vm.expectRevert(abi.encodeWithSelector(errorSelector, address(0)));
        monaPunk.transferFrom(user1.addr, address(0), 0);
    }

    // 测试重复铸造(适配 OZ V5 防重复逻辑)
    function test_DuplicateMint() public {
        vm.startPrank(owner.addr);
        monaPunk.safeMint(user1.addr, tokenURI);

        monaPunk.safeMint(user1.addr, tokenURI);
        vm.stopPrank();
    }

    // 测试边界条件
    function test_MaxSupply() public {
        vm.startPrank(owner.addr);
        for (uint256 i = 0; i < 100; i++) {
            monaPunk.safeMint(user1.addr, tokenURI); // 测试大量铸造
        }
        vm.stopPrank();
        assertEq(monaPunk.totalSupply(), 100, "max supply issue");
    }

    function test_ApproveAndTransfer2() public {
        vm.prank(owner.addr);
        monaPunk.safeMint(user1.addr, tokenURI);
        assertEq(monaPunk.balanceOf(user1.addr), 1, "user1 balance incorrect after mint");

        vm.prank(user1.addr);
        monaPunk.approve(user2.addr, 0);
        vm.prank(user2.addr);
        monaPunk.transferFrom(user1.addr, user2.addr, 0);
        assertEq(monaPunk.ownerOf(0), user2.addr, "transfer after approve failed");
        assertEq(monaPunk.balanceOf(user1.addr), 0, "user1 balance incorrect after transfer");...

剩余50%的内容订阅专栏后可查看

点赞 1
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论