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

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");...
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!