/// @title IERC7439 Prevent Ticket Touting Interface
// / @title IERC7439 防止倒卖门票接口
interfaceIERC7439/* is ERC721 */{/// @dev TokenStatus represent the token current status, only specific role can change status
// / @dev TokenStatus 表示 token 当前状态,只有特定角色可以更改状态
enumTokenStatus{Sold,// 0
Resell,// 1
Void,// 2
Redeemed// 3
}/// @param signature Data signed by user's private key or agent's private key
// / @param signature 由用户的私钥或代理的私钥签名的数据
/// @param tokenStatus Token status changing to
// / @param tokenStatus 要更改为的 token 状态
/// @param expireTime Event due time
// / @param expireTime 活动到期时间
structTokenInfo{bytessignature;TokenStatustokenStatus;uint256expireTime;}/// @notice Used to notify listeners that the token with the specified ID has been changed status
// / @notice 用于通知监听器,具有指定 ID 的 token 状态已更改
/// @param tokenId The token has been changed status
// / @param tokenId 已更改状态的 token
/// @param tokenStatus Token status has been changed to
// / @param tokenStatus 已更改为的 token 状态
/// @param signature Data signed by user's private key or agent's private key
// / @param signature 由用户的私钥或代理的私钥签名的数据
eventTokenStatusChanged(uint256indexedtokenId,TokenStatusindexedtokenStatus,bytessignature);/// @notice Used to mint token with token status
// / @notice 用于使用 token 状态来铸造 token
/// @dev MUST emit the `TokenStatusChanged` event if the token status is changed.
// / @dev 如果 token 状态已更改,则必须发出 `TokenStatusChanged` 事件。
/// @param to The receiptent of token
// / @param to token 的接收者
/// @param signature Data signed by user's private key or agent's private key
// / @param signature 由用户的私钥或代理的私钥签名的数据
functionsafeMint(addressto,bytesmemorysignature)external;/// @notice Used to change token status and can only be invoked by a specific role
// / @notice 用于更改 token 状态,并且只能由特定角色调用
/// @dev MUST emit the `TokenStatusChanged` event if the token status is changed.
// / @dev 如果 token 状态已更改,则必须发出 `TokenStatusChanged` 事件。
/// @param tokenId The token need to change status
// / @param tokenId 需要更改状态的 token
/// @param signature Data signed by user's private key or agent's private key
// / @param signature 由用户的私钥或代理的私钥签名的数据
/// @param tokenStatus Token status changing to
// / @param tokenStatus 要更改为的 token 状态
/// @param newExpireTime New event due time
// / @param newExpireTime 新的活动到期时间
functionchangeState(uint256tokenId,bytesmemorysignature,TokenStatustokenStatus,uint256newExpireTime)external;}
const{expectRevert}=require("@openzeppelin/test-helpers");const{expect}=require("chai");constERC7439=artifacts.require("ERC7439");contract("ERC7439",(accounts)=>{const[deployer,partner,userA,userB]=accounts;constexpireTime=19999999;consttokenId=0;constsignature="0x993dab3dd91f5c6dc28e17439be475478f5635c92a56e17e82349d3fb2f166196f466c0b4e0c146f285204f0dcb13e5ae67bc33f4b888ec32dfe0a063e8f3f781b"constzeroHash="0x";beforeEach(async()=>{this.erc7439=awaitERC7439.new({from:deployer,});awaitthis.erc7439.mint(userA,signature,{from:deployer});});it("Should mint a token",async()=>{consttokenInfo=awaitthis.erc7439.tokenInfo(tokenId);expect(awaitthis.erc7439.ownerOf(tokenId)).to.equal(userA);expect(tokenInfo.signature).equal(signature);expect(tokenInfo.status).equal("0");// Soldexpect(tokenInfo.expireTime).equal(expireTime);});it("should ordinary users cannot transfer successfully",async()=>{expectRevert(awaitthis.erc7439.transferFrom(userA,userB,tokenId,{from:userA}),"ERC7439: You cannot transfer this NFT!");});it("should partner can transfer successfully and chage the token info to resell status",async()=>{consttokenStatus=1;// Resellawaitthis.erc7439.changeState(tokenId,zeroHash,tokenStatus,{from:partner});awaitthis.erc7439.transferFrom(userA,partner,tokenId,{from:partner});expect(tokenInfo.tokenHash).equal(zeroHash);expect(tokenInfo.status).equal(tokenStatus);// Resellexpect(awaitthis.erc7439.ownerOf(tokenId)).to.equal(partner);});it("should partner can change the token status to void",async()=>{consttokenStatus=2;// Voidawaitthis.erc7439.changeState(tokenId,zeroHash,tokenStatus,{from:partner});expect(tokenInfo.tokenHash).equal(zeroHash);expect(tokenInfo.status).equal(tokenStatus);// Void});it("should partner can change the token status to redeemed",async()=>{consttokenStatus=3;// Redeemedawaitthis.erc7439.changeState(tokenId,zeroHash,tokenStatus,{from:partner});expect(tokenInfo.tokenHash).equal(zeroHash);expect(tokenInfo.status).equal(tokenStatus);// Redeemed});it("should partner can resell the token and change status from resell to sold",async()=>{lettokenStatus=1;// Resellawaitthis.erc7439.changeState(tokenId,zeroHash,tokenStatus,{from:partner});awaitthis.erc7439.transferFrom(userA,partner,tokenId,{from:partner});expect(tokenInfo.status).equal(tokenStatus);// Resellexpect(tokenInfo.tokenHash).equal(zeroHash);tokenStatus=0;// SoldconstnewSignature="0x113hqb3ff45f5c6ec28e17439be475478f5635c92a56e17e82349d3fb2f166196f466c0b4e0c146f285204f0dcb13e5ae67bc33f4b888ec32dfe0a063w7h2f742f";awaitthis.erc7439.changeState(tokenId,newSignature,tokenStatus,{from:partner});awaitthis.erc7439.transferFrom(partner,userB,tokenId,{from:partner});expect(tokenInfo.status).equal(tokenStatus);// Soldexpect(tokenInfo.tokenHash).equal(newSignature);});});
参考实现
// SPDX-License-Identifier: CC0-1.0
pragmasolidity0.8.19;import"@openzeppelin/contracts/token/ERC721/ERC721.sol";// If you need additional metadata, you can import ERC721URIStorage
// 如果你需要额外的元数据,你可以导入 ERC721URIStorage
// import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import"@openzeppelin/contracts/access/AccessControl.sol";import"@openzeppelin/contracts/utils/Counters.sol";import"./IERC7439.sol";contractERC7439isERC721,AccessControl,IERC7439{usingCountersforCounters.Counter;bytes32publicconstantPARTNER_ROLE=keccak256("PARTNER_ROLE");Counters.Counterprivate_tokenIdCounter;uint256publicexpireTime;mapping(uint256=>TokenInfo)publictokenInfo;constructor(uint256_expireTime)ERC721("MyToken","MTK"){_grantRole(DEFAULT_ADMIN_ROLE,msg.sender);_grantRole(PARTNER_ROLE,msg.sender);expireTime=_expireTime;}functionsafeMint(addressto,bytesmemorysignature)public{uint256tokenId=_tokenIdCounter.current();_tokenIdCounter.increment();_safeMint(to,tokenId);tokenInfo[tokenId]=TokenInfo(signature,TokenStatus.Sold,expireTime);emitTokenStatusChanged(tokenId,TokenStatus.Sold,signature);}functionchangeState(uint256tokenId,bytesmemorysignature,TokenStatustokenStatus,uint256newExpireTime)publiconlyRole(PARTNER_ROLE){tokenInfo[tokenId]=TokenInfo(signature,tokenStatus,newExpireTime);emitTokenStatusChanged(tokenId,tokenStatus,signature);}function_burn(uint256tokenId)internalvirtualoverride(ERC721){super._burn(tokenId);if(_exists(tokenId)){deletetokenInfo[tokenId];// If you import ERC721URIStorage
// 如果你导入 ERC721URIStorage
// delete _tokenURIs[tokenId];
}}functionsupportsInterface(bytes4interfaceId)publicviewvirtualoverride(AccessControl,ERC721)returns(bool){returninterfaceId==type(IERC7439).interfaceId||super.supportsInterface(interfaceId);}function_beforeTokenTransfer(addressfrom,addressto,uint256tokenId)internalvirtualoverride(ERC721){if(!hasRole(PARTNER_ROLE,_msgSender())){require(from==address(0)||to==address(0),"ERC7439: You cannot transfer this NFT!");}super._beforeTokenTransfer(from,to,tokenId);}}