diff --git a/packages/zevm-app-contracts/contracts/xp-nft/xpNFT.sol b/packages/zevm-app-contracts/contracts/xp-nft/xpNFT.sol index 9b1477b..a195535 100644 --- a/packages/zevm-app-contracts/contracts/xp-nft/xpNFT.sol +++ b/packages/zevm-app-contracts/contracts/xp-nft/xpNFT.sol @@ -23,6 +23,7 @@ contract ZetaXP is ERC721Upgradeable, OwnableUpgradeable { mapping(uint256 => uint256) public lastUpdateTimestampByTokenId; mapping(uint256 => uint256) public signedUpByTokenId; mapping(uint256 => bytes32) public tagByTokenId; + mapping(address => mapping(bytes32 => uint256)) public tokenByUserTag; // Base URL for NFT images string public baseTokenURI; @@ -40,6 +41,7 @@ contract ZetaXP is ERC721Upgradeable, OwnableUpgradeable { error LengthMismatch(); error TransferNotAllowed(); error OutdatedSignature(); + error TagAlreadyHoldByUser(); function initialize( string memory name, @@ -122,6 +124,8 @@ contract ZetaXP is ERC721Upgradeable, OwnableUpgradeable { _verify(tokenId, updateData); lastUpdateTimestampByTokenId[tokenId] = updateData.sigTimestamp; signedUpByTokenId[tokenId] = updateData.signedUp; + tagByTokenId[tokenId] = updateData.tag; + tokenByUserTag[updateData.to][updateData.tag] = tokenId; } // External mint function with auto-incremented token ID @@ -129,6 +133,7 @@ contract ZetaXP is ERC721Upgradeable, OwnableUpgradeable { uint256 newTokenId = _currentTokenId; _mint(mintData.to, newTokenId); + if (tokenByUserTag[mintData.to][mintData.tag] != 0) revert TagAlreadyHoldByUser(); _updateNFT(newTokenId, mintData); emit NFTMinted(mintData.to, newTokenId, mintData.tag); @@ -140,6 +145,7 @@ contract ZetaXP is ERC721Upgradeable, OwnableUpgradeable { function updateNFT(uint256 tokenId, UpdateData memory updateData) external { address owner = ownerOf(tokenId); updateData.to = owner; + if (tokenByUserTag[owner][updateData.tag] != tokenId) revert TagAlreadyHoldByUser(); _updateNFT(tokenId, updateData); emit NFTUpdated(owner, tokenId, updateData.tag); diff --git a/packages/zevm-app-contracts/test/xp-nft/xp-nft.ts b/packages/zevm-app-contracts/test/xp-nft/xp-nft.ts index 27fffd7..8cc3b77 100644 --- a/packages/zevm-app-contracts/test/xp-nft/xp-nft.ts +++ b/packages/zevm-app-contracts/test/xp-nft/xp-nft.ts @@ -175,7 +175,11 @@ describe("XP NFT Contract test", () => { await expect(url).to.be.eq(`${ZETA_BASE_URL}v2/`); { - const sampleNFT2 = { ...sampleNFT, tokenId: 2 }; + const sampleNFT2 = { + ...sampleNFT, + tag: ethers.utils.keccak256(ethers.utils.defaultAbiCoder.encode(["string"], ["XP_NFT2"])), + tokenId: 2, + }; const currentBlock = await ethers.provider.getBlock("latest"); const sigTimestamp = currentBlock.timestamp; @@ -250,4 +254,64 @@ describe("XP NFT Contract test", () => { const version2 = await zetaXPV2.version(); await expect(version2).to.be.eq("2.0.0"); }); + + it("Should revert if user already have the tag", async () => { + { + const currentBlock = await ethers.provider.getBlock("latest"); + const sigTimestamp = currentBlock.timestamp; + + const signature = await getSignature(signer, sigTimestamp, sampleNFT.to, sampleNFT); + + const nftParams: UpdateParam = { + ...sampleNFT, + sigTimestamp, + signature, + } as UpdateParam; + + await zetaXP.mintNFT(nftParams); + } + + { + const sampleNFT2 = { + ...sampleNFT, + tokenId: 2, + }; + const currentBlock = await ethers.provider.getBlock("latest"); + const sigTimestamp = currentBlock.timestamp; + + const signature = await getSignature(signer, sigTimestamp, user.address, sampleNFT2); + + const nftParams: UpdateParam = { + ...sampleNFT2, + sigTimestamp, + signature, + } as UpdateParam; + + const tx = zetaXP.mintNFT(nftParams); + await expect(tx).to.be.revertedWith("TagAlreadyHoldByUser"); + } + }); + + it("Should query by tag and by user", async () => { + const currentBlock = await ethers.provider.getBlock("latest"); + const sigTimestamp = currentBlock.timestamp; + + const signature = await getSignature(signer, sigTimestamp, sampleNFT.to, sampleNFT); + + const nftParams: UpdateParam = { + ...sampleNFT, + sigTimestamp, + signature, + } as UpdateParam; + + const tx = await zetaXP.mintNFT(nftParams); + const receipt = await tx.wait(); + const tokenId = getTokenIdFromRecipient(receipt); + + const queriedTokenId = await zetaXP.tokenByUserTag(sampleNFT.to, sampleNFT.tag); + await expect(queriedTokenId).to.be.eq(tokenId); + + const queriedTag = await zetaXP.tagByTokenId(tokenId); + await expect(queriedTag).to.be.eq(sampleNFT.tag); + }); });