3、ERC721元数据标准:链上还是链下?这是个问题

好,咱们进入元数据这块。说实话,ERC721 的元数据扩展接口,是我在 NFT 开发中打交道最多的部分。你想想看,一个 NFT 除了有个 tokenId,总得有点“面子”吧?名字、符号、还有那个指向具体资源的 URI,这些就是元数据的核心。

3.1 元数据扩展接口:name、symbol、tokenURI

ERC721 的元数据扩展,说白了就是给每个 NFT 加了个“身份证”。接口定义很清晰,就三个函数:

/// @title ERC-721 Non-Fungible Token Standard, optional metadata extension
interface IERC721Metadata {
    function name() external view returns (string memory);
    function symbol() external view returns (string memory);
    function tokenURI(uint256 _tokenId) external view returns (string memory);
}

name()symbol() 是集合级别的。比如你的 NFT 项目叫“蓝海探险家”,symbol 就是“BLUE”。这两个值一般不会变,我习惯在构造函数里一次性写死。

tokenURI() 才是重头戏。它返回一个 URI,指向一个 JSON 文件。这个 JSON 文件里描述了该 token 的具体信息。嗯,这里要注意:tokenURI 返回的是字符串,可以是 HTTP 链接,也可以是 IPFS 的 CID,甚至可以是 data URI。

核心要点: tokenURI 返回的是元数据的位置,不是元数据本身。合约只存“指针”,不存“内容”。

3.2 元数据 JSON Schema:标准长什么样?

OpenSea 等平台约定了一套 JSON Schema,虽然不是 ERC721 强制要求的,但大家都这么用。我见过不少项目因为格式不对,导致 NFT 在市场上显示异常。

{
  "title": "Asset Metadata",
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "description": "标识该资产的名称"
    },
    "description": {
      "type": "string",
      "description": "资产的详细描述"
    },
    "image": {
      "type": "string",
      "description": "指向资产图片的 URI"
    },
    "attributes": {
      "type": "array",
      "description": "资产的属性列表",
      "items": {
        "type": "object",
        "properties": {
          "trait_type": { "type": "string" },
          "value": { "type": "string" }
        }
      }
    }
  }
}

这个 JSON 里,image 字段指向图片,attributes 是属性数组。我在项目中遇到过一个问题:有人把 trait_type 拼写成了 traitType,结果 OpenSea 直接不显示属性。这种坑,踩一次就记住了。

我的建议: 属性值尽量用字符串,别用数字。有些平台对数字类型的解析有 bug,字符串最稳妥。

3.3 链上元数据 vs 链下元数据

这是个老生常谈的话题,但每次讲都有人问。我直接给你对比一下:

对比维度 链上元数据 链下元数据
存储位置 直接存在合约里(如 base64 编码的 JSON) 存在 IPFS、Arweave 或中心化服务器
不可篡改性 完全不可篡改,写入即永恒 取决于存储方式。IPFS 有固定 CID,中心化服务器随时可改
Gas 成本 极高。存一个 JSON 字符串可能消耗 0.01 ETH 以上 极低。只需存一个 URI 字符串
灵活性 差。一旦部署,元数据无法更新 好。可以随时更新 URI 指向的内容(中心化方案)
可访问性 永远在线,只要以太坊网络存在 依赖外部存储的可用性

链下元数据:主流选择

绝大多数项目都用链下元数据。为什么?便宜啊!你想想看,一个 NFT 的图片可能几 MB,存到链上得花多少钱?我做过一个项目,5000 个 NFT,如果全存链上,光部署费就得几十万人民币。

链下元数据的典型流程:

  1. 把图片和 JSON 文件上传到 IPFS
  2. 在合约的 tokenURI() 里返回 IPFS 的 CID
  3. 用户访问时,通过 IPFS 网关获取 JSON,再解析图片

警告: 如果你用中心化服务器存元数据,一旦服务器挂了,你的 NFT 就变成“盲盒”了。我见过一个项目,项目方跑路后服务器关停,所有 NFT 的图片都看不到了。这就是中心化的风险。

链上元数据:极客的选择

链上元数据,说白了就是把整个 JSON 用 base64 编码,直接塞进 tokenURI() 的返回值里。这样返回的 URI 以 data:application/json;base64, 开头。

什么时候用链上?我个人觉得,只有对“永恒性”有极致要求的项目才适合。比如 CryptoPunks 这种,元数据一旦上链,谁也改不了。但代价就是贵,而且图片也得是链上生成的(比如用 SVG)。

// 链上元数据示例(伪代码)
function tokenURI(uint256 tokenId) public view override returns (string memory) {
    string memory json = string(abi.encodePacked(
        '{',
        '"name": "蓝海探险家 #', Strings.toString(tokenId), '",',
        '"description": "一个完全链上的 NFT 项目",',
        '"image": "data:image/svg+xml;base64,', _getSVGBase64(tokenId), '"',
        '}'
    ));
    return string(abi.encodePacked(
        'data:application/json;base64,',
        Base64.encode(bytes(json))
    ));
}

这段代码里,我把 SVG 图片也 base64 编码了,整个元数据都在链上。嗯,这样做的好处是“永不丢失”,但每次调用 tokenURI() 都要消耗 gas,因为合约要动态拼接字符串。

3.4 我的选择建议

如果你问我怎么选,我的答案很简单:

  • 99% 的项目用链下 + IPFS:便宜、够用、去中心化程度也还行
  • 1% 的项目用链上:比如艺术实验、对永恒性有执念的项目
  • 千万别用纯中心化服务器:除非你打算跑路,否则别坑用户

我曾经帮一个项目做迁移,他们之前把元数据存在 AWS 上,结果 AWS 账单没付,所有图片都 404 了。用户骂声一片,项目方只能一个个重新上传到 IPFS,然后改合约里的 URI。这种教训,一次就够了。

避坑指南: 如果你用 IPFS,记得用 IPFS 固定服务(如 Pinata、Filebase)。否则你的文件可能因为没人 pin 而被垃圾回收。我习惯在部署脚本里自动 pin 所有元数据,省心。

好了,元数据这块就聊到这儿。下一章咱们讲 ERC1155 的多代币标准,那个更有意思。