diff --git a/packages/zevm-app-contracts/contracts/xp-nft/ZetaXPGov.sol b/packages/zevm-app-contracts/contracts/xp-nft/ZetaXPGov.sol new file mode 100644 index 0000000..6677f89 --- /dev/null +++ b/packages/zevm-app-contracts/contracts/xp-nft/ZetaXPGov.sol @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: MIT +// Compatible with OpenZeppelin Contracts ^5.0.0 +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts/governance/Governor.sol"; +import "@openzeppelin/contracts/governance/extensions/GovernorSettings.sol"; +import "@openzeppelin/contracts/governance/extensions/GovernorCountingSimple.sol"; +import "@openzeppelin/contracts/governance/extensions/GovernorTimelockControl.sol"; +import "@openzeppelin/contracts/interfaces/IERC6372.sol"; + +import "./xpNFT_V2.sol"; + +contract ZetaXPGov is Governor, GovernorSettings, GovernorCountingSimple, GovernorTimelockControl { + bytes32 public tagValidToVote; + ZetaXP_V2 public xpNFT; + uint256 public quorumPercentage; // New state to store the quorum percentage + + constructor( + ZetaXP_V2 _xpNFT, + TimelockController _timelock, + uint256 _quorumPercentage // Set the quorum percentage (e.g., 4%) + ) + Governor("ZetaXPGov") + GovernorSettings(7200 /* 1 day */, 50400 /* 1 week */, 0) + GovernorTimelockControl(_timelock) + { + xpNFT = _xpNFT; + quorumPercentage = _quorumPercentage; + } + + function setTagValidToVote(bytes32 _tag) external onlyGovernance { + tagValidToVote = _tag; + } + + // Override the _getVotes function to apply custom weight based on NFT levels + function _getVotes( + address account, + uint256 blockNumber, + bytes memory params + ) internal view override returns (uint256) { + uint256 tokenId = xpNFT.tokenByUserTag(account, tagValidToVote); + uint256 level = xpNFT.getLevel(tokenId); + + // Assign voting weight based on NFT level + if (level == 1) { + return 1; // Rosegold + } else if (level == 2) { + return 2; // Black + } else if (level == 3) { + return 3; // Green + } else { + return 0; // Silver cannot vote + } + } + + // Manually implement the quorum function to define quorum based on the total percentage of votes + function quorum(uint256 blockNumber) public view override returns (uint256) { + uint256 totalSupply = xpNFT.totalSupply(); // Total number of NFTs in circulation + return (totalSupply * quorumPercentage) / 100; // Quorum calculation based on the percentage + } + + // Override the _execute function to resolve the conflict + function _execute( + uint256 proposalId, + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) internal override(Governor, GovernorTimelockControl) { + super._execute(proposalId, targets, values, calldatas, descriptionHash); + } + + // Override the supportsInterface function to resolve the conflict + function supportsInterface( + bytes4 interfaceId + ) public view override(Governor, GovernorTimelockControl) returns (bool) { + return super.supportsInterface(interfaceId); + } + + // Implementation of clock and CLOCK_MODE functions to comply with IERC6372 + function clock() public view override returns (uint48) { + return uint48(block.timestamp); + } + + function CLOCK_MODE() public view override returns (string memory) { + return "mode=timestamp"; + } + + // The rest of the functions required to be overridden by Solidity + + function votingDelay() public view override(IGovernor, GovernorSettings) returns (uint256) { + return super.votingDelay(); + } + + function votingPeriod() public view override(IGovernor, GovernorSettings) returns (uint256) { + return super.votingPeriod(); + } + + function state(uint256 proposalId) public view override(Governor, GovernorTimelockControl) returns (ProposalState) { + return super.state(proposalId); + } + + function proposalThreshold() public view override(Governor, GovernorSettings) returns (uint256) { + return super.proposalThreshold(); + } + + function _cancel( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) internal override(Governor, GovernorTimelockControl) returns (uint256) { + return super._cancel(targets, values, calldatas, descriptionHash); + } + + function _executor() internal view override(Governor, GovernorTimelockControl) returns (address) { + return super._executor(); + } +} 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 979eb31..2ca46af 100644 --- a/packages/zevm-app-contracts/contracts/xp-nft/xpNFT_V2.sol +++ b/packages/zevm-app-contracts/contracts/xp-nft/xpNFT_V2.sol @@ -1,12 +1,38 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; -import "./xpNFT.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/utils/cryptography/EIP712Upgradeable.sol"; +import "@openzeppelin/contracts/utils/Strings.sol"; +import {SignatureChecker} from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol"; + +contract ZetaXP_V2 is ERC721Upgradeable, Ownable2StepUpgradeable, EIP712Upgradeable { + bytes32 private constant MINTORUPDATE_TYPEHASH = + keccak256("MintOrUpdateNFT(address to,uint256 signatureExpiration,uint256 sigTimestamp,bytes32 tag)"); -contract ZetaXP_V2 is ZetaXP { bytes32 private constant SETLEVEL_TYPEHASH = keccak256("SetLevel(uint256 tokenId,uint256 signatureExpiration,uint256 sigTimestamp,uint256 level)"); + struct UpdateData { + address to; + bytes signature; + uint256 signatureExpiration; + uint256 sigTimestamp; + bytes32 tag; + } + + mapping(uint256 => uint256) public lastUpdateTimestampByTokenId; + mapping(uint256 => bytes32) public tagByTokenId; + mapping(address => mapping(bytes32 => uint256)) public tokenByUserTag; + + // Base URL for NFT images + string public baseTokenURI; + address public signerAddress; + + // Counter for the next token ID + uint256 private _currentTokenId; + struct SetLevelData { uint256 tokenId; bytes signature; @@ -16,12 +42,137 @@ contract ZetaXP_V2 is ZetaXP { } mapping(uint256 => uint256) public levelByTokenId; + + // Event for New Mint + event NFTMinted(address indexed sender, uint256 indexed tokenId, bytes32 tag); + // Event for NFT Update + event NFTUpdated(address indexed sender, uint256 indexed tokenId, bytes32 tag); + // Event for Signer Update + event SignerUpdated(address indexed signerAddress); + // Event for Base URI Update + event BaseURIUpdated(string baseURI); + // Event for Level Set event LevelSet(address indexed sender, uint256 indexed tokenId, uint256 level); - function version() public pure override returns (string memory) { + error InvalidSigner(); + error SignatureExpired(); + error InvalidAddress(); + error LengthMismatch(); + error TransferNotAllowed(); + error OutdatedSignature(); + error TagAlreadyHoldByUser(); + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + function initialize( + string memory name, + string memory symbol, + string memory baseTokenURI_, + address signerAddress_, + address owner + ) public initializer { + if (signerAddress_ == address(0)) revert InvalidAddress(); + __EIP712_init("ZetaXP", "1"); + __ERC721_init(name, symbol); + __Ownable_init(); + transferOwnership(owner); + baseTokenURI = baseTokenURI_; + signerAddress = signerAddress_; + _currentTokenId = 1; // Start token IDs from 1 + } + + function version() public pure returns (string memory) { return "2.0.0"; } + // Internal function to set the signer address + function setSignerAddress(address signerAddress_) external onlyOwner { + if (signerAddress_ == address(0)) revert InvalidAddress(); + signerAddress = signerAddress_; + emit SignerUpdated(signerAddress_); + } + + // Set the base URI for tokens + function setBaseURI(string calldata _uri) external onlyOwner { + baseTokenURI = _uri; + emit BaseURIUpdated(_uri); + } + + // The following functions are overrides required by Solidity. + function tokenURI(uint256 tokenId) public view override(ERC721Upgradeable) returns (string memory) { + _requireMinted(tokenId); + + return string(abi.encodePacked(baseTokenURI, Strings.toString(tokenId))); + } + + function supportsInterface(bytes4 interfaceId) public view override(ERC721Upgradeable) returns (bool) { + return super.supportsInterface(interfaceId); + } + + function _verifyUpdateNFTSignature(uint256 tokenId, UpdateData memory updateData) private view { + bytes32 structHash = keccak256( + abi.encode( + MINTORUPDATE_TYPEHASH, + updateData.to, + updateData.signatureExpiration, + updateData.sigTimestamp, + updateData.tag + ) + ); + bytes32 constructedHash = _hashTypedDataV4(structHash); + + if (!SignatureChecker.isValidSignatureNow(signerAddress, constructedHash, updateData.signature)) { + revert InvalidSigner(); + } + + if (block.timestamp > updateData.signatureExpiration) revert SignatureExpired(); + if (updateData.sigTimestamp <= lastUpdateTimestampByTokenId[tokenId]) revert OutdatedSignature(); + } + + function _updateNFT(uint256 tokenId, UpdateData memory updateData) internal { + _verifyUpdateNFTSignature(tokenId, updateData); + lastUpdateTimestampByTokenId[tokenId] = updateData.sigTimestamp; + tagByTokenId[tokenId] = updateData.tag; + tokenByUserTag[updateData.to][updateData.tag] = tokenId; + } + + // External mint function with auto-incremented token ID + function mintNFT(UpdateData memory mintData) external { + 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); + + _currentTokenId++; // Increment the token ID for the next mint + } + + // External update function + function updateNFT(uint256 tokenId, UpdateData memory updateData) external { + address owner = ownerOf(tokenId); + updateData.to = owner; + bool willUpdateTag = tagByTokenId[tokenId] != updateData.tag; + + if (willUpdateTag) { + if (tokenByUserTag[owner][updateData.tag] != 0) revert TagAlreadyHoldByUser(); + tokenByUserTag[owner][tagByTokenId[tokenId]] = 0; + } + + _updateNFT(tokenId, updateData); + + emit NFTUpdated(owner, tokenId, updateData.tag); + } + + function _transfer(address from, address to, uint256 tokenId) internal override { + revert TransferNotAllowed(); + } + + // V2 Methods function _verifySetLevelSignature(SetLevelData memory data) private view { bytes32 structHash = keccak256( abi.encode(SETLEVEL_TYPEHASH, data.tokenId, data.signatureExpiration, data.sigTimestamp, data.level) @@ -47,4 +198,8 @@ contract ZetaXP_V2 is ZetaXP { function getLevel(uint256 tokenId) external view returns (uint256) { return levelByTokenId[tokenId]; } + + function totalSupply() external view returns (uint256) { + return _currentTokenId; + } }