diff --git a/packages/zevm-app-contracts/contracts/xp-nft/xpNFT.sol b/packages/zevm-app-contracts/contracts/xp-nft/xpNFT.sol index 78adff7..04db04e 100644 --- a/packages/zevm-app-contracts/contracts/xp-nft/xpNFT.sol +++ b/packages/zevm-app-contracts/contracts/xp-nft/xpNFT.sol @@ -153,7 +153,7 @@ contract ZetaXP is ERC721Upgradeable, Ownable2StepUpgradeable, EIP712Upgradeable emit NFTUpdated(owner, tokenId, updateData.tag); } - function _transfer(address from, address to, uint256 tokenId) internal override { + function _transfer(address from, address to, uint256 tokenId) internal virtual override { revert TransferNotAllowed(); } } diff --git a/packages/zevm-app-contracts/contracts/xp-nft/xpNFT_V2.sol b/packages/zevm-app-contracts/contracts/xp-nft/xpNFT_V2.sol index c2c2ce5..81628af 100644 --- a/packages/zevm-app-contracts/contracts/xp-nft/xpNFT_V2.sol +++ b/packages/zevm-app-contracts/contracts/xp-nft/xpNFT_V2.sol @@ -20,7 +20,7 @@ contract ZetaXP_V2 is ZetaXP { // Event for Level Set event LevelSet(address indexed sender, uint256 indexed tokenId, uint256 level); - function version() public pure override returns (string memory) { + function version() public pure virtual override returns (string memory) { return "2.0.0"; } diff --git a/packages/zevm-app-contracts/contracts/xp-nft/xpNFT_V3.sol b/packages/zevm-app-contracts/contracts/xp-nft/xpNFT_V3.sol new file mode 100644 index 0000000..2f5c6a9 --- /dev/null +++ b/packages/zevm-app-contracts/contracts/xp-nft/xpNFT_V3.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "./xpNFT_V2.sol"; + +contract ZetaXP_V3 is ZetaXP_V2 { + event TagUpdated(address indexed sender, uint256 indexed tokenId, bytes32 tag); + + function version() public pure override returns (string memory) { + return "3.0.0"; + } + + function _transfer(address from, address to, uint256 tokenId) internal override { + bytes32 tag = tagByTokenId[tokenId]; + if (tag != 0) { + tokenByUserTag[from][tag] = 0; + } + if (tokenByUserTag[to][tag] == 0) { + tokenByUserTag[to][tag] = tokenId; + } + ERC721Upgradeable._transfer(from, to, tokenId); + } + + function moveTagToToken(uint256 tokenId, bytes32 tag) external { + uint256 currentTokenId = tokenByUserTag[msg.sender][tag]; + address owner = ownerOf(tokenId); + if (owner != msg.sender) { + revert TransferNotAllowed(); + } + if (currentTokenId == tokenId) { + return; + } + if (currentTokenId != 0) { + tagByTokenId[currentTokenId] = 0; + } + + tagByTokenId[tokenId] = tag; + tokenByUserTag[msg.sender][tag] = tokenId; + emit TagUpdated(msg.sender, tokenId, tag); + } +} diff --git a/packages/zevm-app-contracts/test/xp-nft/xp-nft-v3.ts b/packages/zevm-app-contracts/test/xp-nft/xp-nft-v3.ts new file mode 100644 index 0000000..1b59d5b --- /dev/null +++ b/packages/zevm-app-contracts/test/xp-nft/xp-nft-v3.ts @@ -0,0 +1,114 @@ +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { expect } from "chai"; +import { ethers, upgrades } from "hardhat"; + +import { ZetaXP_V3 } from "../../typechain-types"; +import { getSignature, NFT, NFTSigned } from "./test.helpers"; + +const ZETA_BASE_URL = "https://api.zetachain.io/nft/"; +const HARDHAT_CHAIN_ID = 1337; + +const encodeTag = (tag: string) => ethers.utils.keccak256(ethers.utils.defaultAbiCoder.encode(["string"], [tag])); + +const getTokenIdFromRecipient = (receipt: any): number => { + //@ts-ignore + return receipt.events[0].args?.tokenId; +}; + +describe("XP NFT V3 Contract test", () => { + let zetaXP: ZetaXP_V3, signer: SignerWithAddress, user: SignerWithAddress, addrs: SignerWithAddress[]; + let sampleNFT: NFT; + + beforeEach(async () => { + [signer, user, ...addrs] = await ethers.getSigners(); + const zetaXPFactory = await ethers.getContractFactory("ZetaXP"); + + zetaXP = await upgrades.deployProxy(zetaXPFactory, [ + "ZETA NFT", + "ZNFT", + ZETA_BASE_URL, + signer.address, + signer.address, + ]); + + await zetaXP.deployed(); + + const ZetaXPFactory = await ethers.getContractFactory("ZetaXP_V3"); + zetaXP = await upgrades.upgradeProxy(zetaXP.address, ZetaXPFactory); + + const tag = encodeTag("XP_NFT"); + + sampleNFT = { + tag, + to: user.address, + tokenId: undefined, + }; + }); + + const mintNFT = async (nft: NFT) => { + const currentBlock = await ethers.provider.getBlock("latest"); + const sigTimestamp = currentBlock.timestamp; + const signatureExpiration = sigTimestamp + 1000; + + const signature = await getSignature( + HARDHAT_CHAIN_ID, + zetaXP.address, + signer, + signatureExpiration, + sigTimestamp, + nft.to, + nft + ); + + const nftParams: NFTSigned = { + ...nft, + sigTimestamp, + signature, + signatureExpiration, + } as NFTSigned; + + const tx = await zetaXP.mintNFT(nftParams); + const receipt = await tx.wait(); + return getTokenIdFromRecipient(receipt); + }; + + it("Should transfer successfully", async () => { + const user2 = addrs[0]; + const sampleNFT2 = { ...sampleNFT, to: user2.address }; + + const tokenId = await mintNFT(sampleNFT); + const tokenId2 = await mintNFT(sampleNFT2); + { + const owner1 = await zetaXP.ownerOf(tokenId); + const owner2 = await zetaXP.ownerOf(tokenId2); + expect(owner1).to.equal(user.address); + expect(owner2).to.equal(user2.address); + } + { + const token1 = await zetaXP.tokenByUserTag(user.address, sampleNFT.tag); + const token2 = await zetaXP.tokenByUserTag(user2.address, sampleNFT.tag); + expect(token1).to.equal(tokenId); + expect(token2).to.equal(tokenId2); + } + await zetaXP.connect(user).transferFrom(user.address, user2.address, tokenId); + { + const owner1 = await zetaXP.ownerOf(tokenId); + const owner2 = await zetaXP.ownerOf(tokenId2); + expect(owner1).to.equal(user2.address); + expect(owner2).to.equal(user2.address); + } + { + const token1 = await zetaXP.tokenByUserTag(user.address, sampleNFT.tag); + const token2 = await zetaXP.tokenByUserTag(user2.address, sampleNFT.tag); + expect(token1).to.equal(0); + expect(token2).to.equal(tokenId2); + } + await zetaXP.connect(user2).moveTagToToken(tokenId, sampleNFT.tag); + { + const token1 = await zetaXP.tokenByUserTag(user.address, sampleNFT.tag); + const token2 = await zetaXP.tokenByUserTag(user2.address, sampleNFT.tag); + expect(token1).to.equal(0); + expect(token2).to.equal(tokenId); + } + }); +});