From d91182637ce7e8b9550ba19f530e29158cdcb846 Mon Sep 17 00:00:00 2001
From: Andres Aiello <aaiello@gmail.com>
Date: Thu, 17 Oct 2024 14:28:21 -0300
Subject: [PATCH] feat: implement the governance smart-contract

---
 .../contracts/xp-nft/ZetaXPGov.sol            | 119 +++++++++++++
 .../contracts/xp-nft/xpNFT_V2.sol             | 161 +++++++++++++++++-
 2 files changed, 277 insertions(+), 3 deletions(-)
 create mode 100644 packages/zevm-app-contracts/contracts/xp-nft/ZetaXPGov.sol

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;
+    }
 }