前言本文首先梳理ERC7231标准的核心内容,涵盖核心定位、核心能力、解决的行业痛点及典型应用场景;随后基于OpenZeppelin合约库,实现一个符合ERC-7231标准的智能合约,并依托HardhatV3完成该合约从开发、测试到部署的全流程实践。概述ERC7231给
本文首先梳理 ERC7231 标准的核心内容,涵盖核心定位、核心能力、解决的行业痛点及典型应用场景;随后基于 OpenZeppelin 合约库,实现一个符合 ERC-7231 标准的智能合约,并依托 Hardhat V3 完成该合约从开发、测试到部署的全流程实践。
概述
ERC7231 给 ERC721 NFT 补上 “身份 + 权限” 的核心能力,大幅提升安全性和实用性,主打需要身份验证的场景。
1. 核心定位
ERC7231 是 ERC721 的扩展标准,全称 NFT 的身份与访问管理,核心是给 NFT 加 身份验证 和 权限管控 功能,让 NFT 从 “单纯数字资产” 变成 “带身份的数字凭证 / 钥匙”。
grantAccess/revokeAccess 等),控制谁能铸币、转移、销毁 NFT 或使用其权益。| 原有痛点 | ERC7231 解决方案 |
|---|---|
| 普通 NFT 谁拿到都能用,无身份验证 | 操作前必须验证身份,盗号者无对应身份无法使用 |
| 各项目权限接口不统一,开发集成成本高 | 定义标准化权限接口,所有遵循者通用 |
| 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.tsimport 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 实现了合约从测试到部署的全流程操作,形成了一套理论与实践相结合的完整技术方案。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!