NFT 防盗秘籍:ERC7231 权限合约开发与部署实战

  • 木西
  • 发布于 1天前
  • 阅读 18

前言本文首先梳理ERC7231标准的核心内容,涵盖核心定位、核心能力、解决的行业痛点及典型应用场景;随后基于OpenZeppelin合约库,实现一个符合ERC-7231标准的智能合约,并依托HardhatV3完成该合约从开发、测试到部署的全流程实践。概述ERC7231给

前言

本文首先梳理 ERC7231 标准的核心内容,涵盖核心定位、核心能力、解决的行业痛点及典型应用场景;随后基于 OpenZeppelin 合约库,实现一个符合 ERC-7231 标准的智能合约,并依托 Hardhat V3 完成该合约从开发、测试到部署的全流程实践。

概述

ERC7231 给 ERC721 NFT 补上 “身份 + 权限” 的核心能力,大幅提升安全性和实用性,主打需要身份验证的场景。

1. 核心定位

ERC7231 是 ERC721 的扩展标准,全称 NFT 的身份与访问管理,核心是给 NFT 加 身份验证 和 权限管控 功能,让 NFT 从 “单纯数字资产” 变成 “带身份的数字凭证 / 钥匙”。

2. 核心能力

  1. 身份绑定:NFT 可与用户身份哈希绑定,只有对应身份的用户才能操作 NFT。
  2. 细粒度权限控制:标准化接口(grantAccess/revokeAccess 等),控制谁能铸币、转移、销毁 NFT 或使用其权益。
  3. 标准化验证:兼容 Merkle 树、零知识证明等主流验证方式,无需重复开发。
  4. 权限可追溯 / 撤销:所有权限操作上链可查,NFT 被盗后可撤销非法地址的访问权限。

3. 解决的核心痛点

原有痛点 ERC7231 解决方案
普通 NFT 谁拿到都能用,无身份验证 操作前必须验证身份,盗号者无对应身份无法使用
各项目权限接口不统一,开发集成成本高 定义标准化权限接口,所有遵循者通用
NFT 权益与持有者身份脱节(如转卖后非会员也能用) 权益与身份绑定,仅合法身份可触发权益
项目方重复开发身份验证逻辑,易出漏洞 内置标准化验证接口,直接调用即可

4. 典型应用场景

  • 数字门票 / 通行证:防止盗票、转卖滥用
  • DAO 治理 NFT:只有成员身份才能投票
  • 企业数字资产:员工离职可一键撤销权限
  • 元宇宙身份凭证:防止角色被盗后恶意使用

智能合约开发、部署、测试

智能合约

// 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/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";
import "@openzeppelin/contracts/interfaces/IERC1271.sol";

/**
 * @title ERC-7231: Identity Aggregated NFT
 * @dev 完全兼容 OpenZeppelin 5.4.0 - 移除 isContract 依赖
 */
contract ERC7231 is ERC721, ERC721Enumerable, ERC721URIStorage, Ownable, IERC1271 {
    using ECDSA for bytes32;
    using MessageHashUtils for bytes32;

    mapping(uint256 => bytes32) private _identitiesRoot;
    event IdentitiesRootSet(uint256 indexed tokenId, bytes32 identitiesRoot);

    constructor(string memory name, string memory symbol) 
        ERC721(name, symbol) 
        Ownable(msg.sender) 
    {}

    function setIdentitiesRoot(uint256 tokenId, bytes32 identitiesRoot) external {
        require(_ownerOf(tokenId) != address(0), "ERC7231: token does not exist");
        require(ownerOf(tokenId) == msg.sender, "ERC7231: caller is not owner");
        require(identitiesRoot != bytes32(0), "ERC7231: invalid root");

        _identitiesRoot[tokenId] = identitiesRoot;
        emit IdentitiesRootSet(tokenId, identitiesRoot);
    }

    function getIdentitiesRoot(uint256 tokenId) external view returns (bytes32) {
        require(_ownerOf(tokenId) != address(0), "ERC7231: token does not exist");
        return _identitiesRoot[tokenId];
    }

    function verifyIdentityBinding(
        uint256 tokenId,
        bytes32 identityHash,
        bytes32[] calldata merkleProof
    ) external view returns (bool) {
        require(_ownerOf(tokenId) != address(0), "ERC7231: token does not exist");
        require(_identitiesRoot[tokenId] != bytes32(0), "ERC7231: root not set");

        bytes32 computedHash = identityHash;
        for (uint256 i = 0; i < merkleProof.length; i++) {
            computedHash = _hashPair(computedHash, merkleProof[i]);
        }
        return computedHash == _identitiesRoot[tokenId];
    }

    /**
     * @dev ✅ 简化验证:直接使用 ECDSA,兼容 EOA 和智能钱包
     * 智能钱包通常使用 EIP-1271,但 recover 对它们同样有效
     */
    function verifyIdentitiesBinding(
        uint256 tokenId,
        bytes32 identityHash,
        bytes memory signature
    ) external view returns (bool) {
        require(_ownerOf(tokenId) != address(0), "ERC7231: token does not exist");

        address owner = ownerOf(tokenId);
        bytes32 messageHash = keccak256(abi.encodePacked(tokenId, identityHash));
        bytes32 ethSignedMessageHash = messageHash.toEthSignedMessageHash();

        // 直接验证签名
        return ethSignedMessageHash.recover(signature) == owner;
    }

    function _hashPair(bytes32 a, bytes32 b) private pure returns (bytes32) {
        return a < b ? keccak256(abi.encodePacked(a, b)) : keccak256(abi.encodePacked(b, a));
    }

    function safeMint(
        address to,
        uint256 tokenId,
        string memory uri,
        bytes32 identitiesRoot
    ) external onlyOwner {
        _safeMint(to, tokenId);
        _setTokenURI(tokenId, uri);

        if (identitiesRoot != bytes32(0)) {
            _identitiesRoot[tokenId] = identitiesRoot;
            emit IdentitiesRootSet(tokenId, identitiesRoot);
        }
    }

    // 重写必需函数
    function _update(address to, uint256 tokenId, address auth)
        internal override(ERC721, ERC721Enumerable)
        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 interfaceId == type(IERC1271).interfaceId || super.supportsInterface(interfaceId);
    }

    /**
     * @dev ✅ 修复:明确返回 bytes4 类型
     */
    function isValidSignature(bytes32 hash, bytes memory signature)
        external view override
        returns (bytes4 magicValue)
    {
        address signer = hash.toEthSignedMessageHash().recover(signature);

        if (signer == owner()) {
            magicValue = 0x1626ba7e; // ERC-1271 成功返回值
        } else {
            magicValue = 0xffffffff; // ERC-1271 失败返回值
        }
    }
}

编译指令:npx hardhat compile

智能合约部署

// scripts/deploy.js
import { network, artifacts } from "hardhat";
async function main() {
  // 连接网络
  const { viem } = await network.connect({ network: network.name });//指定网络进行链接

  // 获取客户端
  const [deployer] = await viem.getWalletClients();
  const publicClient = await viem.getPublicClient();

  const deployerAddress = deployer.account.address;
   console.log("部署者的地址:", deployerAddress);
  // 加载合约
  const artifact = await artifacts.readArtifact("ERC7231");
  const hash = await deployer.deployContract({
    abi: artifact.abi,//获取abi
    bytecode: artifact.bytecode,//硬编码
    args: ["MyERC7231NFT","MINFT"],//nft名称,nft符号
  });

  // 等待确认并打印地址
  const receipt = await publicClient.waitForTransactionReceipt({ hash });
  console.log("合约地址:", receipt.contractAddress);
}

main().catch(console.error);

部署指令:npx hardhat run ./scripts/xxx.ts

智能合约测试

import assert from "node:assert/strict";
import { describe, it,beforeEach  } from "node:test";
import { formatEther,parseEther,keccak256,toHex,hexToBytes, bytesToHex,zeroHash,encodePacked } from 'viem'
import { network } from "hardhat";
import { MerkleTree } from 'merkletreejs';
describe("ERC7231智能合约测试", async function () {
    let viem: any;
    let publicClient: any;
    let owner: any, user1: any, user2: any, user3: any;
    let deployerAddress: string;
    let MyERC7231: any;
    // 测试常量
    const ERC1271_MAGIC_VALUE = "0x1626ba7e";
    const ERC1271_FAILURE_VALUE = "0xffffffff";
    const ZERO_HASH = zeroHash;//
    const TOKEN_ID = 1;
    const TOKEN_URI = "https://zygomorphic-magenta-bobolink.myfilebase.com/ipfs/QmQT8VpmWQVhUhoDCEK1mdHXaFaJ3KawkRxHm96GUhrXLB";
   const IDENTITY_HASH = keccak256(new TextEncoder().encode("user1-identity-001"));
   const INVALID_IDENTITY_HASH = keccak256(new TextEncoder().encode("invalid-identity"));
    beforeEach (async function () {
        const { viem } = await network.connect();
         publicClient = await viem.getPublicClient();//创建一个公共客户端实例用于读取链上数据(无需私钥签名)。
         [owner,user1,user2,user3] = await viem.getWalletClients();//获取第一个钱包客户端 写入联合交易
        deployerAddress = owner.account.address;//钱包地址

        MyERC7231 = await viem.deployContract("ERC7231", [
            "My Royalty NFT",
            "MRNFT",
        ]);//部署合约
        console.log("MyRoyaltyNFT合约地址:", MyERC7231.address); 
    });
    it("应该创建一个以这些身份信息作为基础的新NFT", async function () {
        //查询nft名称和符号
       const name= await publicClient.readContract({
            address: MyERC7231.address,
            abi: MyERC7231.abi,
            functionName: "name",
            args: [],
        });
       const symbol= await publicClient.readContract({
            address: MyERC7231.address,
            abi: MyERC7231.abi,
            functionName: "symbol",
            args: [],
        });
       console.log("nftINFO:", name,symbol);
       // 准备Merkle树
      const leaves = [
         IDENTITY_HASH,
         keccak256(new TextEncoder().encode("user1-identity-002")),
         keccak256(new TextEncoder().encode("user1-identity-003"))
         ];
      const viemKeccak256Adapter = (data: Buffer | string) => {
      // 如果是 Buffer(MerkleTree 内部传参),先转 Hex;如果是字符串,直接用
      const hexData = Buffer.isBuffer(data) ? `0x${data.toString("hex")}` : data;
      return Buffer.from(keccak256(hexData).slice(2), "hex"); // 去掉 0x 转 Buffer 返回
    };

    const merkleTree = new MerkleTree(leaves, (value) => keccak256(value as Uint8Array), {
        sortPairs: true,
      });
      const root = bytesToHex(merkleTree.getRoot());
       //nft铸造
       const safeMint=await owner.writeContract({
            address: MyERC7231.address,
            abi: MyERC7231.abi,
            functionName: "safeMint",
            args: [user1.account.address,TOKEN_ID,TOKEN_URI,root],
        });
       console.log(safeMint)
       //查询余额和拥有者
       const balanceOf=await publicClient.readContract({
            address: MyERC7231.address,
            abi: MyERC7231.abi,
            functionName: "balanceOf",
            args: [user1.account.address],
        });
       console.log(balanceOf)
         const ownerOf=await publicClient.readContract({
            address: MyERC7231.address,
            abi: MyERC7231.abi,
            functionName: "ownerOf",
            args: [TOKEN_ID],
        });
       console.log(ownerOf)
       const tokenURI=await publicClient.readContract({
            address: MyERC7231.address,
            abi: MyERC7231.abi,
            functionName: "tokenURI",
            args: [TOKEN_ID],
        });
       console.log("tokenURI:",tokenURI)
       // 验证根值
      const identitiesRoot = await publicClient.readContract({
        address: MyERC7231.address,
        abi: MyERC7231.abi,
        functionName: "getIdentitiesRoot",
        args: [TOKEN_ID],
      });
      console.log(identitiesRoot===root);
    });
    it("Should only allow owner to mint NFTs", async function () {
      // 非所有者尝试铸造应该失败
      await user1.writeContract({
          address: MyERC7231.address,
          abi: MyERC7231.abi,
          functionName: "safeMint",
          args: [user1.account.address, TOKEN_ID, TOKEN_URI, zeroHash],
        }).catch((error: any) => {
          console.log("铸造失败:", "非所有者尝试铸造应该失败");
        });

    });
// 测试设置身份根
describe("设置身份根", function () {
    beforeEach(async function () {
      // 先铸造一个没有根的NFT
      const hash = await owner.writeContract({
        address: MyERC7231.address,
        abi: MyERC7231.abi,
        functionName: "safeMint",
        args: [user1.account.address, TOKEN_ID, TOKEN_URI, zeroHash],
      });
      await publicClient.waitForTransactionReceipt({ hash });
    });

    it("应该允许令牌所有者设置身份根", async function () {
      const newRoot = keccak256(new TextEncoder().encode("new-root-value"));

      // 设置新根
      const hash = await user1.writeContract({
        address: MyERC7231.address,
        abi: MyERC7231.abi,
        functionName: "setIdentitiesRoot",
        args: [TOKEN_ID, newRoot],
      });
      await publicClient.waitForTransactionReceipt({ hash });

      // 验证新根
      const identitiesRoot = await publicClient.readContract({
        address: MyERC7231.address,
        abi: MyERC7231.abi,
        functionName: "getIdentitiesRoot",
        args: [TOKEN_ID],
      });
      console.log("新根:",identitiesRoot==newRoot)
    });
  });
  // 测试3:Merkle验证功能
  describe("Merkle证明验证", function () {
    let merkleTree: MerkleTree;
    let root: `0x${string}`;
    let proof: `0x${string}`[];

    beforeEach(async function () {
      // 创建Merkle树
      const leaves = [
        IDENTITY_HASH,
        keccak256(new TextEncoder().encode("user1-identity-002")),
        keccak256(new TextEncoder().encode("user1-identity-003")),
        keccak256(new TextEncoder().encode("user1-identity-004")),
      ];
      merkleTree = new MerkleTree(leaves, (value) => keccak256(value as Uint8Array), {
        sortPairs: true,
      });
      root = bytesToHex(merkleTree.getRoot());

      // 获取proof并转换格式
      const rawProof = merkleTree.getProof(IDENTITY_HASH);
      proof = rawProof.map((p) => bytesToHex(p.data) as `0x${string}`);

      // 铸造NFT并设置根
      const hash = await owner.writeContract({
        address: MyERC7231.address,
        abi: MyERC7231.abi,
        functionName: "safeMint",
        args: [user1.account.address, TOKEN_ID, TOKEN_URI, root],
      });
      await publicClient.waitForTransactionReceipt({ hash });
    });

    it("应该验证有效的默克尔证明", async function () {
      const isValid = await publicClient.readContract({
        address: MyERC7231.address,
        abi: MyERC7231.abi,
        functionName: "verifyIdentityBinding",
        args: [TOKEN_ID, IDENTITY_HASH, proof],
      });
      console.log(isValid)

    });

    it("应该拒绝无效的默克尔证明", async function () {
      const isValid = await publicClient.readContract({
        address: MyERC7231.address,
        abi: MyERC7231.abi,
        functionName: "verifyIdentityBinding",
        args: [TOKEN_ID, INVALID_IDENTITY_HASH, proof],
      });
      console.log(isValid)

    });

    it("如果未设置 root 应拒绝验证", async function () {
      // 铸造一个没有根的NFT
      const NEW_TOKEN_ID = 2n;
      const hash = await owner.writeContract({
        address: MyERC7231.address,
        abi: MyERC7231.abi,
        functionName: "safeMint",
        args: [user1.account.address, NEW_TOKEN_ID, TOKEN_URI, zeroHash],
      });
      await publicClient.waitForTransactionReceipt({ hash });

      await publicClient.readContract({
          address: MyERC7231.address,
          abi: MyERC7231.abi,
          functionName: "verifyIdentityBinding",
          args: [NEW_TOKEN_ID, IDENTITY_HASH, proof],
        }).catch((error: any) => {
          console.log("验证失败:","ERC7231: root not set");
        });

    });
  });
  // 测试4:签名验证功能
  describe("签名验证", function () {
    let signature: `0x${string}`;
    let invalidSignature: `0x${string}`;

    beforeEach(async function () {
      // 铸造NFT
      const hash = await owner.writeContract({
        address: MyERC7231.address,
        abi: MyERC7231.abi,
        functionName: "safeMint",
        args: [user1.account.address, TOKEN_ID, TOKEN_URI, zeroHash],
      });
      await publicClient.waitForTransactionReceipt({ hash });

      // 创建消息哈希
      const messageHash = keccak256(
        encodePacked(
          ["uint256", "bytes32"],
          [toHex(TOKEN_ID), IDENTITY_HASH]
        )
      );

      // 有效签名(user1签名)
      signature = await user1.signMessage({
        message: { raw: messageHash },
      });

      // 无效签名(user2签名)
      invalidSignature = await user2.signMessage({
        message: { raw: messageHash },
      });
    });

    it("应验证有效签名", async function () {
      const isValid = await publicClient.readContract({
        address: MyERC7231.address,
        abi: MyERC7231.abi,
        functionName: "verifyIdentitiesBinding",
        args: [TOKEN_ID, IDENTITY_HASH, signature],
      });
        console.log(isValid)
    });

    it("应拒绝无效签名", async function () {
      const isValid = await publicClient.readContract({
        address: MyERC7231.address,
        abi: MyERC7231.abi,
        functionName: "verifyIdentitiesBinding",
        args: [TOKEN_ID, IDENTITY_HASH, invalidSignature],
      });
      console.log(isValid)
    });

    it("应拒绝使用错误身份哈希的验证", async function () {
      const isValid = await publicClient.readContract({
        address: MyERC7231.address,
        abi: MyERC7231.abi,
        functionName: "verifyIdentitiesBinding",
        args: [TOKEN_ID, INVALID_IDENTITY_HASH, signature],
      });
        console.log(isValid)
    //   expect(isValid).to.be.false;
    });
  });

  // 测试5:ERC1271 签名验证
  describe("ERC1271签名验证", function () {
    let hash: `0x${string}`;
    let validSignature: `0x${string}`;
    let invalidSignature: `0x${string}`;

    beforeEach(async function () {
      hash = keccak256(new TextEncoder().encode("test message")) as `0x${string}`;

      // 所有者签名
      validSignature = await owner.signMessage({
        message: { raw: hash },
      });

      // 非所有者签名
      invalidSignature = await user1.signMessage({
        message: { raw: hash },
      });
    });

    it("应该返回有效签名的正确魔法值", async function () {
      const magicValue = await publicClient.readContract({
        address: MyERC7231.address,
        abi: MyERC7231.abi,
        functionName: "isValidSignature",
        args: [hash, validSignature],
      });
      console.log(magicValue)
    });

    it("应返回无效签名的失败值", async function () {
      const magicValue = await publicClient.readContract({
        address: MyERC7231.address,
        abi: MyERC7231.abi,
        functionName: "isValidSignature",
        args: [hash, invalidSignature],
      });
        console.log(magicValue)
        console.log(ERC1271_FAILURE_VALUE)
    //   expect(magicValue).to.equal(ERC1271_FAILURE_VALUE);
    });
  });
  // 测试6:接口支持验证
  describe("接口支持", function () {
    it("应该支持ERC1271接口", async function () {
      const ERC1271_INTERFACE_ID = "0x1626ba7e";
      const supports = await publicClient.readContract({
        address: MyERC7231.address,
        abi: MyERC7231.abi,
        functionName: "supportsInterface",
        args: [ERC1271_INTERFACE_ID],
      });
        console.log(supports)
    //   expect(supports).to.be.true;
    });

    it("应支持 ERC721 和 ERC721Enumerable 接口", async function () {
      const ERC721_INTERFACE_ID = "0x80ac58cd";
      const ERC721_ENUMERABLE_INTERFACE_ID = "0x780e9d63";

      const supportsERC721 = await publicClient.readContract({
        address: MyERC7231.address,
        abi: MyERC7231.abi,
        functionName: "supportsInterface",
        args: [ERC721_INTERFACE_ID],
      });

      const supportsEnumerable = await publicClient.readContract({
        address: MyERC7231.address,
        abi: MyERC7231.abi,
        functionName: "supportsInterface",
        args: [ERC721_ENUMERABLE_INTERFACE_ID],
      });
        console.log(supportsERC721)
        console.log(supportsEnumerable)
    //   expect(supportsERC721).to.be.true;
    //   expect(supportsEnumerable).to.be.true;
    });
  });
});

测试指令:npx hardhat test ./test/xxx.ts

总结

至此,本文围绕 ERC7231 标准展开了从理论到实践的完整梳理与落地。理论层面,清晰界定了该标准作为 ERC721 扩展协议的核心定位,拆解了其身份绑定、权限管控等关键能力,分析了它针对 NFT 行业身份验证缺失、权限接口碎片化等痛点的解决方案,并列举了数字门票、DAO 治理等典型应用场景。实践层面,依托 OpenZeppelin 合约库完成了 ERC7231 智能合约的开发,再通过 Hardhat V3 实现了合约从测试到部署的全流程操作,形成了一套理论与实践相结合的完整技术方案。

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

0 条评论

请先 登录 后评论
木西
木西
0x5D5C...2dD7
江湖只有他的大名,没有他的介绍。