Skip to content

Commit

Permalink
feat: implement the governance smart-contract
Browse files Browse the repository at this point in the history
  • Loading branch information
andresaiello committed Oct 17, 2024
1 parent 89275a0 commit d911826
Show file tree
Hide file tree
Showing 2 changed files with 277 additions and 3 deletions.
119 changes: 119 additions & 0 deletions packages/zevm-app-contracts/contracts/xp-nft/ZetaXPGov.sol
Original file line number Diff line number Diff line change
@@ -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;

Check warning

Code scanning / Slither

State variables that could be declared immutable Warning

ZetaXPGov.xpNFT should be immutable
uint256 public quorumPercentage; // New state to store the quorum percentage

Check warning

Code scanning / Slither

State variables that could be declared immutable Warning

ZetaXPGov.quorumPercentage should be immutable

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 {

Check warning

Code scanning / Slither

Conformance to Solidity naming conventions Warning

Parameter ZetaXPGov.setTagValidToVote(bytes32)._tag is not in mixedCase
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";
}

Check warning

Code scanning / Slither

Conformance to Solidity naming conventions Warning

Function ZetaXPGov.CLOCK_MODE() is not in mixedCase

// 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();
}
}
161 changes: 158 additions & 3 deletions packages/zevm-app-contracts/contracts/xp-nft/xpNFT_V2.sol
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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 {

Check warning

Code scanning / Slither

Conformance to Solidity naming conventions Warning

Parameter ZetaXP_V2.setBaseURI(string)._uri is not in mixedCase
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)));
}

Check failure

Code scanning / Slither

ABI encodePacked Collision High

ZetaXP_V2.tokenURI(uint256) calls abi.encodePacked() with multiple dynamic arguments:
- 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();
}

Check notice

Code scanning / Slither

Block timestamp Low


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)
Expand All @@ -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;
}
}

Check warning

Code scanning / Slither

Conformance to Solidity naming conventions Warning

Contract ZetaXP_V2 is not in CapWords

0 comments on commit d911826

Please sign in to comment.