你有没有想过——
你花了几万块买的一个“NFT头像”,本质上只是一串代码里的一行记录。
不是你想象中的那张图片。
那张图片,可能存某个服务器上。哪天服务器一关,图片没了,但你的“所有权”还在链上写着。
所以问题来了:你买的到底是那张图,还是链上的那行字?
说白了,这行代码,才是你真正的资产证明。
先给你看个数据
2023年8月,全球NFT月交易额冲到过49.5亿美元。
但同一时期,研究人员扫描了87,839个ERC-721合约,发现了:
44,863,255个“脆弱的NFT绑定”缺陷
1,373个“不符合规范”的实现
105个“植入后门”
什么概念?
近9万个NFT合约里,藏着超过4485万个潜在问题。
你的NFT,可能就踩中了其中一个。
先搞清楚:NFT到底怎么“确权”的?
一张图看懂
text
┌─────────────────────────────────────────────────────────────┐ │ 你的NFT = 三样东西 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ 🏷️ 唯一Token ID → 就像房产证上的编号 │ │ 📝 合约地址 → 就像“这个房产归哪个区管” │ │ 🔗 元数据链接 → 指向那张图片的“地址” │ │ │ │ 缺了任何一个,你的“所有权”都不完整 │ │ │ └─────────────────────────────────────────────────────────────┘
NFT通过ERC-721智能合约来执行确权规则。核心接口就这几个:
| 函数 | 干什么的 |
|---|---|
ownerOf(tokenId) | 查这个NFT现在归谁 |
balanceOf(address) | 查这个人有多少个NFT |
transferFrom() | 把NFT转给别人 |
tokenURI(tokenId) | 返回一个链接,指向这个NFT的“说明书” |
每个ERC-721代币都有一个唯一编号(tokenId),它和所有权、元数据绑定在一起。
你买的不是那张图,你买的是这组绑定关系。
但是——这组绑定关系,可以被“篡改”
你猜怎么着?很多NFT项目的tokenURI是可以改的。
项目方今天把tokenURI(1)指向ipfs://QmXXX/1.json,明天可以改成指向另一张图。
你手里的“所有权证明”,指向的东西变了。
你买的是一只猴子,项目方改完之后,你手里可能变成了一块石头。
这不是危言耸听。这是元数据篡改(Metadata Tampering)——NFT领域最常见的风险之一。
来,看一段真实的NFT合约代码
这是一个简化版的NFT合约:
solidity
// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; contract MyNFT is ERC721, Ownable { string public baseURI; constructor(string memory _baseURI) ERC721("MyNFT", "MNFT") { baseURI = _baseURI; } // ⭐ 关键:这个函数决定了你的NFT指向什么 function tokenURI(uint256 tokenId) public view override returns (string memory) { require(_exists(tokenId), "Token does not exist"); return string(abi.encodePacked(baseURI, Strings.toString(tokenId), ".json")); } // ⚠️ 危险:只有owner可以改baseURI // 但如果owner是一个钱包,而不是多签……风险就来了 function setBaseURI(string memory _newBaseURI) public onlyOwner { baseURI = _newBaseURI; } // 批量铸造 function batchMint(address to, uint256 amount) public onlyOwner { require(amount > 0, "Amount must be greater than zero"); for (uint256 i = 0; i < amount; i++) { _mint(to, totalSupply() + 1); } } }这段代码看起来没问题,对吧?
但审计的时候,你要问三个问题:
onlyOwner——这个owner是谁?一个人?一个多签?还是一个黑洞地址?setBaseURI——改完之后,所有NFT的tokenURI都变了,用户知情吗?batchMint——owner可以无限增发,你的NFT会贬值吗?
NFT合约的四大“杀手级”漏洞
🚨 漏洞一:重入攻击
ERC-721有个安全机制叫ERC721Receiver,用来防止NFT被转到不支持NFT的合约里。
但这个机制会触发外部调用,攻击者可以利用这个外部调用“重入”合约。
真实场景:
一个叫TheMasks的NFT合约规定:每笔交易最多铸造20个NFT,总量有上限。
攻击者调用mintNFT(20),在_safeMint触发的外部回调里,再次调用mintNFT(20)。
因为totalSupply()还没更新完,合约以为还没到上限,结果——
攻击者一单铸造了110个NFT,远超20个的限制。
2025年,多个Pepe NFT项目因重入和拒绝服务漏洞被攻击,损失超过100万美元。
🚨 漏洞二:无限制铸造(Unrestricted Minting)
典型问题:mint函数没有任何限制,任何人都可以无限铸造。
solidity
// ❌ 危险 function mint(address to) public { _mint(to, totalSupply() + 1); } // ✅ 安全:加限制 function mint(address to) public onlyWhitelisted { require(totalSupply() < MAX_SUPPLY, "Sold out"); require(!hasMinted[msg.sender], "Already minted"); _mint(to, totalSupply() + 1); }🚨 漏洞三:签名可重放(Replayable Signatures)
很多项目用签名来验证白名单。但如果签名里没有包含链ID、合约地址、过期时间,攻击者可以把一个项目的签名拿到另一个项目上用。
2025年,Immunefi审计竞赛中就发现了多个签名重放漏洞,攻击者可以利用它跨合约铸造NFT。
🚨 漏洞四:权限过于集中
owner一个人能改元数据、能增发、能提款、能暂停合约。
如果owner的私钥被盗了——整个项目就没了。
这就是为什么顶级项目都用多签钱包+时间锁来管理合约。
NFT审计要查什么?我给你列个清单
| 检查项 | 要查什么 |
|---|---|
| 铸造控制 | 有没有总量上限?有没有白名单限制? |
| 元数据治理 | tokenURI能不能被改?改之前有没有时间锁? |
| 签名机制 | 签名里有没有链ID、nonce、过期时间? |
| 权限设计 | owner是谁?多签还是单点?有没有时间锁? |
| 重入防护 | 有没有ReentrancyGuard?外部调用前后状态更新了吗? |
| 升级能力 | 合约能不能升级?谁有升级权限? |
| 支付与提款 | 用的是pull模式还是push模式? |
真实案例:Gondi NFT平台被黑
2025年7月,NFT借贷平台Gondi遭遇合约漏洞攻击。
损失金额:23万美元。
攻击者利用了合约中的某个逻辑缺陷,绕过了权限检查。
23万美元,就从一个“不起眼”的漏洞里流走了。
作为普通用户,你怎么保护自己?
✅ 买NFT之前,做这四件事:
查合约有没有审计报告——没审计的,默认有风险
看owner是谁——如果是单钱包,跑;如果是多签+时间锁,相对安全
问三个问题:能无限铸造吗?能改图吗?能暂停转账吗?
用工具扫一下——比如
Honeypot & Rug Detector或NFTGuard
✅ 交易时,做这件事:
模拟签名——用Pocket Universe这类工具,在签名前预览一下这笔交易到底会转走什么资产。
别盲签。盲签是丢NFT最快的方式。
最后给你总结一下
NFT不是那张图。
那张图只是存在某个地方的图片文件。
真正的资产,是链上那行代码里的一串记录——ownerOf(tokenId) = 你的地址。
但前提是:
这行代码写得对
这行代码没被篡改
这行代码的权限没落在坏人手里
所以,别光看图片好不好看。去看代码。
那行代码,才是你真正的资产证明。