From 80a126fdd8fbb6eb97ac2f0db9b15af9666401a2 Mon Sep 17 00:00:00 2001 From: Dan Rusnac Date: Mon, 2 Dec 2024 13:01:41 +0100 Subject: [PATCH 1/2] feat: updated contracts and tests --- CONTRACTS_CHANGELOG.md | 197 ++++- contracts/GalaxyMember.sol | 49 +- contracts/NodeManagement.sol | 168 +++- contracts/VoterRewards.sol | 5 +- contracts/deprecated/V1/NodeManagementV1.sol | 306 +++++++ .../V1/interfaces/INodeManagementV1.sol | 108 +++ contracts/deprecated/V2/GalaxyMemberV2.sol | 833 ++++++++++++++++++ .../V2/interfaces/IGalaxyMemberV2.sol | 354 ++++++++ .../V2/interfaces/IVeBetterPassportV2.sol | 524 +++++++++++ .../ve-better-passport/VeBetterPassportV2.sol | 815 +++++++++++++++++ .../libraries/PassportChecksLogicV2.sol | 143 +++ .../libraries/PassportClockLogicV2.sol | 49 ++ .../libraries/PassportConfiguratorV2.sol | 127 +++ .../libraries/PassportDelegationLogicV2.sol | 501 +++++++++++ .../PassportEIP712SigningLogicV2.sol | 121 +++ .../libraries/PassportEntityLogicV2.sol | 611 +++++++++++++ .../libraries/PassportPersonhoodLogicV2.sol | 191 ++++ .../libraries/PassportPoPScoreLogicV2.sol | 378 ++++++++ .../libraries/PassportSignalingLogicV2.sol | 313 +++++++ .../libraries/PassportStorageTypesV2.sol | 134 +++ .../libraries/PassportTypesV2.sol | 96 ++ .../PassportWhitelistAndBlacklistLogicV2.sol | 344 ++++++++ contracts/deprecated/V3/VoterRewardsV3.sol | 505 +++++++++++ contracts/interfaces/IGalaxyMember.sol | 6 + contracts/interfaces/INodeManagement.sol | 28 + .../ve-better-passport/VeBetterPassport.sol | 2 +- .../libraries/PassportPersonhoodLogic.sol | 14 +- scripts/deploy/deploy.ts | 56 +- scripts/deploy/setup.ts | 4 +- scripts/galaxyMember/getGMowners.ts | 55 ++ scripts/galaxyMember/getGmSelectedTokens.ts | 60 ++ scripts/galaxyMember/index.ts | 1 + scripts/libraries/passportLibraries.ts | 51 ++ test/GalaxyMember.test.ts | 218 ++++- test/NodeManagement.test.ts | 414 ++++++++- test/VeBetterPassport.test.ts | 512 ++++++++++- test/VoterRewards.test.ts | 80 +- test/XApps.test.ts | 146 +-- test/helpers/deploy.ts | 166 +++- 39 files changed, 8459 insertions(+), 226 deletions(-) create mode 100644 contracts/deprecated/V1/NodeManagementV1.sol create mode 100644 contracts/deprecated/V1/interfaces/INodeManagementV1.sol create mode 100644 contracts/deprecated/V2/GalaxyMemberV2.sol create mode 100644 contracts/deprecated/V2/interfaces/IGalaxyMemberV2.sol create mode 100644 contracts/deprecated/V2/interfaces/IVeBetterPassportV2.sol create mode 100644 contracts/deprecated/V2/ve-better-passport/VeBetterPassportV2.sol create mode 100644 contracts/deprecated/V2/ve-better-passport/libraries/PassportChecksLogicV2.sol create mode 100644 contracts/deprecated/V2/ve-better-passport/libraries/PassportClockLogicV2.sol create mode 100644 contracts/deprecated/V2/ve-better-passport/libraries/PassportConfiguratorV2.sol create mode 100644 contracts/deprecated/V2/ve-better-passport/libraries/PassportDelegationLogicV2.sol create mode 100644 contracts/deprecated/V2/ve-better-passport/libraries/PassportEIP712SigningLogicV2.sol create mode 100644 contracts/deprecated/V2/ve-better-passport/libraries/PassportEntityLogicV2.sol create mode 100644 contracts/deprecated/V2/ve-better-passport/libraries/PassportPersonhoodLogicV2.sol create mode 100644 contracts/deprecated/V2/ve-better-passport/libraries/PassportPoPScoreLogicV2.sol create mode 100644 contracts/deprecated/V2/ve-better-passport/libraries/PassportSignalingLogicV2.sol create mode 100644 contracts/deprecated/V2/ve-better-passport/libraries/PassportStorageTypesV2.sol create mode 100644 contracts/deprecated/V2/ve-better-passport/libraries/PassportTypesV2.sol create mode 100644 contracts/deprecated/V2/ve-better-passport/libraries/PassportWhitelistAndBlacklistLogicV2.sol create mode 100644 contracts/deprecated/V3/VoterRewardsV3.sol create mode 100644 scripts/galaxyMember/getGMowners.ts create mode 100644 scripts/galaxyMember/getGmSelectedTokens.ts create mode 100644 scripts/galaxyMember/index.ts diff --git a/CONTRACTS_CHANGELOG.md b/CONTRACTS_CHANGELOG.md index d9bbd98..755bdc1 100644 --- a/CONTRACTS_CHANGELOG.md +++ b/CONTRACTS_CHANGELOG.md @@ -4,18 +4,151 @@ This document provides a detailed log of upgrades to the smart contract suite, e ## Version History -| Date | Contract(s) | Summary | -| ------------------- | --------------------------------------------------------- | ----------------------------------------------------------- | -| 15th November 2024 | `X2EarnApps` version `2` | Added X2Earn Apps Vechain Node Endorsement feature | -| 21th October 2024 | `VeBetterPassport` version `2` | Check if the entity is a delegatee when request is created | -| 11th October 2024 | `XAllocationVoting` version `2` | Check isPerson when casting vote & fixed weight during vote | -| 11th October 2024 | `B3TRGovernor` version `4` | Check isPerson when casting vote | -| 11th October 2024 | `X2EarnRewardsPool` version `3` | Register action in VeBetter Passport contract | -| 27th September 2024 | `Emissions` version `2` | Aligned emissions with the expected schedule | -| 13th September 2024 | `B3TRGovernor` version `3`, `XAllocationPool` version `2` | - Added toggling of quadratic voting and funding | -| 4th September 2024 | `X2EarnRewardsPool` version `2` | - Added impact key management and proof building | -| 31st August 2024 | `VoterRewards` version `2` | - Added quadratic rewarding features | -| 29th August 2024 | `B3TRGovernor` version `2` | Updated access control modifiers | +| Date | Contract(s) | Summary | +| ------------------- | ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | +| 29th November 2024 | `VeBetterPassport` version `3`, `GalaxyMember` version `3`, and `VoterRewards` version 4 | Added GM level as personhood check in VeBetter passport. | +| 28th November 2024 | `NodeManagement` version `2` | Added new functions to check node delegation status and improved node management capabilities. | +| 15th November 2024 | `GalaxyMember` version `2`, `VoterRewards` version `3`, `B3TRGovernor` version `5` | Added Vechain Node Binding with Galaxy Member feature | +| 15th November 2024 | `X2EarnApps` version `2` | Added X2Earn Apps Vechain Node Endorsement feature | +| 21th October 2024 | `VeBetterPassport` version `2` | Check if the entity is a delegatee when request is created | +| 11th October 2024 | `XAllocationVoting` version `2` | Check isPerson when casting vote & fixed weight during vote | +| 11th October 2024 | `B3TRGovernor` version `4` | Check isPerson when casting vote | +| 11th October 2024 | `X2EarnRewardsPool` version `3` | Register action in VeBetter Passport contract | +| 27th September 2024 | `Emissions` version `2` | Aligned emissions with the expected schedule | +| 13th September 2024 | `B3TRGovernor` version `3`, `XAllocationPool` version `2` | - Added toggling of quadratic voting and funding | +| 4th September 2024 | `X2EarnRewardsPool` version `2` | - Added impact key management and proof building | +| 31st August 2024 | `VoterRewards` version `2` | - Added quadratic rewarding features | +| 29th August 2024 | `B3TRGovernor` version `2` | Updated access control modifiers | + +--- + +## Upgrade `VeBetterPassport` to Version 3, and `GalaxyMember` to Version 3 + +Added new personhood check in VeBetter passport, if a user owns a GM with a level greater than 1 they are considered a person. +
+Updated `GalaxyMember` to checkpoint selected GM NFT and allow admin to select token for user for GM levels go live. +
+Updated `VoterRewards` to use version `3` of `GalaxyMember` interface. + +### Changes 🚀 + +- **Upgraded Contract(s):** + - `VeBetterPassport.sol` to version `3` + - `GalaxyMember.sol` to version `3` + - `VoterRewards.sol` to version `4` + +### Storage Changes 📦 + +- **`GalaxyMember`**: + - Added `_selectedTokenIDCheckpoints` to store checkpoints for selected GM token ID of the user. + +### New Features 🚀 + +- **`VeBetterPassport`**: + - Updated `PassportPersonhoodLogic.sol` library's function `_checkPassport()` to include check for GM level. +- **`GalaxyMember`**: + - Added `selectFor()` function to allow the admin to select a token for the user. + - Added `clock()` and `CLOCK_MODE()` functions to allow for custom time tracking. + - Added `getSelectedTokenIdAtBlock()` to get the selected GM token ID for the user at a specific block number. + - Updated Node Management interface to include new getters of Node Management V2 contract. + +### Bug Fixes 🐛 + +- None. + +--- + +## Upgrade `NodeManagement` to Version 2 + +Added new functions to check node delegation status and improved node management capabilities. + +### Changes 🚀 + +- **Upgraded Contract(s):** + - `NodeManagement.sol` to version `2` + +### Storage Changes 📦 + +- None. + +### New Features 🚀 + +- **`NodeManagement`**: + - Added `isNodeDelegated()` to check if a specific node ID is delegated + - Added `isNodeDelegator()` to check if a user has delegated their node + - Added `getDirectNodeOwnership()` to check direct node ownership without delegation + - Added `isNodeHolder()` to check if a user is a node holder (both directly owned and indirectly through delegation) + - Added `getUserNodes()` to get comprehensive node information including: + - Node ID + - Node level + - Owner address + - Node holder status + - Delegation status + - Delegator status + - Delegatee status + - Delegatee address + +### Bug Fixes 🐛 + +- None. + +--- + +## Upgrade `GalaxyMember` to Version 2 + +Introduced a composition pattern to attach and detach Vechain nodes to/from Galaxy Member (GM) NFTs. This feature allows GM NFTs to dynamically acquire or lose levels based on the attached node's capabilities. + +### Changes 🚀 + +- **Upgraded Contract(s):** + - `GalaxyMember.sol` to version `2` + - `VoterRewards.sol` to version `3` + - `B3TRGovernor.sol` to version `5` + +### Storage Changes 📦 + +- **`GalaxyMember.sol`**: + - Added `vechainNodes` to store the address of the Vechain Nodes contract. + - Added `nodeManagement` to store the address of the Node Management contract. + - Added `_nodeToTokenId` to track the XNode tied to the GM token ID. + - Added `_tokenIdToNode` to track the GM token ID tied to the XNode token ID. + - Added `_nodeToFreeUpgradeLevel` to track the GM level that can be upgraded for free for a given Vechain node level. + - Added `_tokenIdToB3TRdonated` to store the mapping from GM Token ID to B3TR donated for upgrading. + - Added `_selectedTokenID` to store the mapping from user address to selected GM token ID. +- **`VoterRewards.sol`**: + - Added `proposalToGalaxyMemberToHasVoted` to keep track of whether a galaxy member has voted in a proposal. + - Added `proposalToNodeToHasVoted` to keep track of whether a vechain node has been used while attached to a galaxy member NFT when voting for a proposal. + +### New Features 🚀 + +- **`GalaxyMember.sol`**: + - Added `attachNode()` function to attach Vechain Node to GM NFT. + - Added `detachNode()` function to detach Vechain Node from GM NFT. + - Added `setVechainNodes()` function to update the Vechain Nodes contract address. + - Added `setNodeToFreeUpgradeLevel()` to set the levelin which a Vechain Node can upgrade to for free. + - Added `levelOf()` to get the level of GM token. + - Added `getB3TRtoUpgradeToLevel()` to get the required B3TR to upgrade GM NFT to certain level. + - Added `getB3TRtoUpgrade()` to get the required B3TR to upgrade GM NFT to the next level. + - Added `getNodeLevelOf()` to get the level of a give Vechain node. + - Added `getLevelAfterAttachingNode()` to get level of GM NFT after attaching particular GM NFT. + - Added `getIdAttachedToNode()` to get GM NFT attached to Vechain node. + - Added `getIdAttachedToNode()` to get Vechain node attached to GM NFT. + - Added `getNodeToFreeLevel()` to get level in which GM NFT can be upgraded to for free if particular Vechain node is attached. + - Added `getB3TRdonated()` to get the B3TR donated by a GM NFT so far to reach ther aquired level. + - Added `getTokenInfoByTokenId()` to get infomation on particular GM NFT. + - Added `getSelectedTokenInfoByOwner()` to get GM NFT user is using for rewards boosts. + - Added `getTokensInfoByOwner()` to get infomation on GM NFTs owned by a particular address. +- **`VoterRewards.sol`**: + - Added `getMultiplier()` to get the reward multiplier for a user in a specific proposal. + - Added `hasNodeVoted()` to check if a Vechain Node has voted on a proposal. + - Added `hasTokenVoted()` to check if a GM NFT has voted on a proposal. +- **`GovernorVotesLogic.sol`**: + - Updated `castVote()` to pass proposalId instead of snapshot to Voter Rewards `registerVote()` function. + +### Bug Fixes 🐛 + +- **`GalaxyMember.sol`**: + - In Version 1, transfers that occur from an approved address are subject to underflow issues when updating the `_ownedLevels` map. This is fixed with Version 2 by also asserting updates are made on the owner of the token ID rather than the auth of the internal `_update` function. --- @@ -31,35 +164,35 @@ Added Vechain Node XApp Endorsement feature. ### Storage Changes 📦 - **`EndorsementUpgradeable.sol`**: - - Added `_unendorsedApps` to store the list of apps pending endorsement. - - Added `_unendorsedAppsIndex` to store mapping from app ID to index in the _unendorsedApps array. - - Added `_appEndorsers` to store the mapping of each app ID to an array of node IDs that have endorsed it. - - Added `_nodeEnodorsmentScore` to score the endorsement score for each node level. - - Added `_appGracePeriodStart` to store the grace period elapsed by the app since endorsed. - - Added `_nodeToEndorsedApp` to store the mapping of a node ID to the app it currently endorses. - - Added `_gracePeriodDuration` to store the grace period threshold for no endorsement in blocks. - - Added `_endorsementScoreThreshold` to store the endorsement score threshold for an app to be eligible for voting. - - Added `_appScores` to store the score of each app. - - Added `_appSecurity` to store the security score of each app. - - Added `_nodeManagementContract` to store the node management contract address. - - Added `_veBetterPassport` to store the VeBetterPassport contract address. -- **`EndorsementUpgradeable.sol`**: - - Added `_creators` to store a mapping of addresses that have a creators NFT and can manage interactions with Node holders for a specifc XApp. - - Added `_creatorApps` to store the number of apps created by a creator. - - Added `_x2EarnCreatorContract` to store the address of the X2Earn Creators contract. - - **`VoteEligibilityUpgradeable.sol`**: - - Added `_blackList` to store a record blacklisted X2Earn appIds. + - Added `_unendorsedApps` to store the list of apps pending endorsement. + - Added `_unendorsedAppsIndex` to store mapping from app ID to index in the \_unendorsedApps array. + - Added `_appEndorsers` to store the mapping of each app ID to an array of node IDs that have endorsed it. + - Added `_nodeEnodorsmentScore` to score the endorsement score for each node level. + - Added `_appGracePeriodStart` to store the grace period elapsed by the app since endorsed. + - Added `_nodeToEndorsedApp` to store the mapping of a node ID to the app it currently endorses. + - Added `_gracePeriodDuration` to store the grace period threshold for no endorsement in blocks. + - Added `_endorsementScoreThreshold` to store the endorsement score threshold for an app to be eligible for voting. + - Added `_appScores` to store the score of each app. + - Added `_appSecurity` to store the security score of each app. + - Added `_nodeManagementContract` to store the node management contract address. + - Added `_veBetterPassport` to store the VeBetterPassport contract address. +- **`EndorsementUpgradeable.sol`**: + - Added `_creators` to store a mapping of addresses that have a creators NFT and can manage interactions with Node holders for a specifc XApp. + - Added `_creatorApps` to store the number of apps created by a creator. + - Added `_x2EarnCreatorContract` to store the address of the X2Earn Creators contract. +- **`VoteEligibilityUpgradeable.sol`**: + - Added `_blackList` to store a record blacklisted X2Earn appIds. ### New Features 🚀 - Added `EndorsementUpgradeable.sol` module which makes up all X2EarnApps endorsement logic and functions (see docs for more info). - Replaced `appApp()` with `submitApp()`. - Added getter `isBlacklisted()` to check if XApp is blacklisted. -- Added `removeAppCreator()`, `appCreators()`, `isAppCreator()` and `creatorApps()` to manage and get info on X2Earn app creators. +- Added `removeAppCreator()`, `appCreators()`, `isAppCreator()` and `creatorApps()` to manage and get info on X2Earn app creators. ### Bug Fixes 🐛 -- - Added libraries `AdministrationUtils.sol`, `EndorsementUtils.sol`, `AppStorageUtils.sol` and `VoteEligibilityUtils.sol` to store some of the logic for the X2EarnApps contracts modules to reduce contract size. +- - Added libraries `AdministrationUtils.sol`, `EndorsementUtils.sol`, `AppStorageUtils.sol` and `VoteEligibilityUtils.sol` to store some of the logic for the X2EarnApps contracts modules to reduce contract size. --- diff --git a/contracts/GalaxyMember.sol b/contracts/GalaxyMember.sol index 030ce71..05c5d5a 100644 --- a/contracts/GalaxyMember.sol +++ b/contracts/GalaxyMember.sol @@ -39,13 +39,14 @@ import { IB3TR } from "./interfaces/IB3TR.sol"; import { ITokenAuction } from "./interfaces/ITokenAuction.sol"; import { INodeManagement } from "./interfaces/INodeManagement.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import { Time } from "@openzeppelin/contracts/utils/types/Time.sol"; /** * @title GalaxyMember * @notice This contract manages the unique assets owned by users within the Galaxy Member ecosystem. * @dev Extends ERC721 Non-Fungible Token Standard basic implementation with upgradeable pattern, burnable, pausable, and access control functionalities. * - * --------------------------------- VERSION --------------------------------- + * --------------------------------- VERSION 2 --------------------------------- * - Added Vechain Nodes contract to attach and detach nodes to tokens * - Added NODES_MANAGER_ROLE to manage Vechain Nodes Contract address and free upgrade levels * - Added free upgrade levels for each Vechain node level @@ -54,6 +55,12 @@ import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; * - Core logic functions are now overridable through inheritance * - B3TRGovernor has been updated to V2 thus pointing to the new interface * - NodeManagement contract has been added to permit attaching and detaching nodes from managed nodes too + * + * --------------------------------- VERSION 3 --------------------------------- + * - Updated Node Management interface to include getters (isNodeDelegator() and isNodeDelegated()) + * - Added `selectFor` function to allow the admin to select a token for the user + * - Added checkpoints for the selected token ID of the user + * - Added `clock()` and `CLOCK_MODE()` functions to allow for custom time tracking */ contract GalaxyMember is ERC721Upgradeable, @@ -64,6 +71,8 @@ contract GalaxyMember is ReentrancyGuardUpgradeable, UUPSUpgradeable { + using Checkpoints for Checkpoints.Trace208; // Checkpoints library for managing checkpoints of the selected token ID of the user + bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE"); bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); @@ -93,7 +102,9 @@ contract GalaxyMember is mapping(uint256 => uint256) _tokenIdToNode; // Mapping from GalaxyMember Token ID to Vechain node ID. Used to track the GM token ID tied to the XNode token ID mapping(uint8 => uint256) _nodeToFreeUpgradeLevel; // Mapping from Vechain node level to GalaxyMember level. Used to track the GM level that can be upgraded for free for a given Vechain node level mapping(uint256 => uint256) _tokenIdToB3TRdonated; // Mapping from GM Token ID to B3TR donated for upgrading - mapping(address => uint256) _selectedTokenID; // Mapping from user address to selected GM token ID + mapping(address => uint256) _selectedTokenID_DEPRECATED; // Mapping from user address to selected GM token ID - DEPRECATED IN FAVOUR OF CHECKPOINTS + // --------------------------- V3 Additions --------------------------- // + mapping(address => Checkpoints.Trace208) _selectedTokenIDCheckpoints; // Checkpoints for selected GM token ID of the user } /// @notice Storage slot for GalaxyMemberStorage @@ -309,6 +320,12 @@ contract GalaxyMember is _select(msg.sender, tokenID); } + /// @notice Allows the admin to select a token for the user + /// @param owner The address of the owner to check + function selectFor(address owner, uint256 tokenID) public virtual onlyRole(DEFAULT_ADMIN_ROLE) { + _select(owner, tokenID); + } + /// @notice selects the specified token for the user /// @param owner The address of the owner to check /// @param tokenId the token ID to select @@ -317,7 +334,7 @@ contract GalaxyMember is GalaxyMemberStorage storage $ = _getGalaxyMemberStorage(); - $._selectedTokenID[owner] = tokenId; + $._selectedTokenIDCheckpoints[owner].push(clock(), SafeCast.toUint208(tokenId)); emit Selected(owner, tokenId); } @@ -566,7 +583,15 @@ contract GalaxyMember is /// @param owner The address of the owner to check function getSelectedTokenId(address owner) public view virtual returns (uint256) { GalaxyMemberStorage storage $ = _getGalaxyMemberStorage(); - return $._selectedTokenID[owner]; + return $._selectedTokenIDCheckpoints[owner].latest(); + } + + /// @notice Gets the selected token ID for the user in a specific block number + /// @param owner The address of the owner to check + /// @param blockNumber The block number to check + function getSelectedTokenIdAtBlock(address owner, uint48 blockNumber) public view virtual returns (uint256) { + GalaxyMemberStorage storage $ = _getGalaxyMemberStorage(); + return $._selectedTokenIDCheckpoints[owner].upperLookupRecent(blockNumber); } /// @notice Gets the level of the token after attaching a node @@ -683,7 +708,7 @@ contract GalaxyMember is /// @dev This function is used to identify the version of the contract and should be updated in each new version /// @return string The version of the contract function version() external pure virtual returns (string memory) { - return "2"; + return "3"; } struct TokenInfo { @@ -769,6 +794,18 @@ contract GalaxyMember is return tokens; } + /// @dev Clock used for flagging checkpoints. + function clock() public view virtual returns (uint48) { + return Time.blockNumber(); + } + + /** + * @dev Returns the mode of the clock. + */ + function CLOCK_MODE() public view virtual returns (string memory) { + return "mode=blocknumber&from=default"; + } + // ---------- Overrides ---------- // /// @notice Performs automatic level updating upon token updates @@ -792,7 +829,7 @@ contract GalaxyMember is // If the owner has no tokens, don't select any token if (_previousOwner != address(0) && balanceOf(_previousOwner) == 0) { - delete _getGalaxyMemberStorage()._selectedTokenID[_previousOwner]; + _getGalaxyMemberStorage()._selectedTokenIDCheckpoints[_previousOwner].push(clock(), 0); } // If the owner transfers out the selected token, select the first token he owns diff --git a/contracts/NodeManagement.sol b/contracts/NodeManagement.sol index 7199497..323eacf 100644 --- a/contracts/NodeManagement.sol +++ b/contracts/NodeManagement.sol @@ -1,5 +1,26 @@ // SPDX-License-Identifier: MIT +// ####### +// ################ +// #################### +// ########### ######### +// ######### ######### +// ####### ######### ######### +// ######### ######### ########## +// ########## ######## #################### +// ########## ######### ######################### +// ################### ############################ +// ################# ########## ######## +// ############## ### ######## +// ############ ######### +// ########## ########## +// ######## ########### +// ### ############ +// ############## +// ################# +// ############## +// ######### + pragma solidity 0.8.20; import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; @@ -9,6 +30,18 @@ import { VechainNodesDataTypes } from "./libraries/VechainNodesDataTypes.sol"; import { ITokenAuction } from "./interfaces/ITokenAuction.sol"; import { INodeManagement } from "./interfaces/INodeManagement.sol"; +/** + * @title NodeManagement + * @notice This contract manages node ownership and delegation within the VeBetter DAO ecosystem. It supports delegation, + * retrieval of managed nodes, and integration with VeChain Nodes and token auction contracts. + * @dev The contract is upgradeable using the UUPS proxy pattern and implements role-based access control for secure upgrades. + * + * ------------------------ Version 2 ------------------------ + * - Add function to check if Node is delegated + * - Add function to check if user is a delegator + * - Add function to get users owned node ID + * - Add function to retrieve detailed information about a user's nodes (both delegated and owned) + */ contract NodeManagement is INodeManagement, AccessControlUpgradeable, UUPSUpgradeable { using EnumerableSet for EnumerableSet.UintSet; @@ -19,6 +52,18 @@ contract NodeManagement is INodeManagement, AccessControlUpgradeable, UUPSUpgrad bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE"); + // Struct used to format return values for getUserNodes + struct NodeInfo { + uint256 nodeId; + VechainNodesDataTypes.NodeStrengthLevel nodeLevel; + address xNodeOwner; + bool isXNodeHolder; + bool isXNodeDelegated; + bool isXNodeDelegator; + bool isXNodeDelegatee; + address delegatee; + } + /// @custom:storage-location erc7201:b3tr.storage.NodeManagement struct NodeManagementStorage { ITokenAuction vechainNodesContract; // The token auction contract @@ -33,11 +78,11 @@ contract NodeManagement is INodeManagement, AccessControlUpgradeable, UUPSUpgrad /** * @notice Retrieve the storage reference for node delegation data. * @dev Internal pure function to get the storage slot for node delegation data using inline assembly. - * @return storageReference The storage reference for node delegation data. + * @return $ The storage reference for node delegation data. */ - function _getNodeManagementStorage() internal pure returns (NodeManagementStorage storage storageReference) { + function _getNodeManagementStorage() internal pure returns (NodeManagementStorage storage $) { assembly { - storageReference.slot := NodeManagementStorageLocation + $.slot := NodeManagementStorageLocation } } @@ -94,7 +139,7 @@ contract NodeManagement is INodeManagement, AccessControlUpgradeable, UUPSUpgrad // Emit event for delegation removal emit NodeDelegated(nodeId, $.nodeIdToDelegatee[nodeId], false); // Remove delegation - $.delegateeToNodeIds[delegatee].remove(nodeId); + $.delegateeToNodeIds[$.nodeIdToDelegatee[nodeId]].remove(nodeId); } // Update mappings for delegation @@ -222,6 +267,109 @@ contract NodeManagement is INodeManagement, AccessControlUpgradeable, UUPSUpgrad return $.vechainNodesContract.idToOwner(nodeId) == user; } + /** + * @notice Check if a node is delegated. + * @param nodeId The node ID to check for. + * @return bool True if the node is delegated. + */ + function isNodeDelegated(uint256 nodeId) public view returns (bool) { + NodeManagementStorage storage $ = _getNodeManagementStorage(); + return $.nodeIdToDelegatee[nodeId] != address(0); + } + + /** + * @notice Check if a user is a delegator. + * @param user The address of the user to check. + * @return bool True if the user is a delegator. + */ + function isNodeDelegator(address user) public view returns (bool) { + // first we do direct call to check if user is node owner + uint256 nodeId = getDirectNodeOwnership(user); + // if it is then we check if node is delegated + if (nodeId != 0) { + // if node is delegated then we return true + return isNodeDelegated(nodeId); + } + + // otherwise we return false + return false; + } + + /** + * @notice Check if a user is a node holder (either directly or through delegation). + * @param user The address of the user to check. + * @return bool True if the user is a node holder. + */ + function isNodeHolder(address user) public view returns (bool) { + NodeManagementStorage storage $ = _getNodeManagementStorage(); + + // Check if the user directly owns a node + if ($.vechainNodesContract.ownerToId(user) != 0) { + return true; + } + + // Check if the user is a delegatee of any node + uint256[] memory nodeIds = getNodeIds(user); + for (uint256 i = 0; i < nodeIds.length; i++) { + if ($.nodeIdToDelegatee[nodeIds[i]] == user) { + return true; + } + } + + return false; + } + + /** + * @notice Retrieves detailed information about all of a user's nodes, including owned and delegated nodes. + * @param user The address of the user to check. + * @return NodeInfo[] Array of node information structures. + */ + function getUserNodes(address user) public view returns (NodeInfo[] memory) { + NodeManagementStorage storage $ = _getNodeManagementStorage(); + + // Get the set of node IDs delegated to the user + EnumerableSet.UintSet storage nodeIdsSet = $.delegateeToNodeIds[user]; + // Calculate the total number of node IDs + uint256 count = nodeIdsSet.length(); + // Create an array to hold the node IDs + uint256[] memory nodeIds = new uint256[](count); + // Populate the array with node IDs from the set + for (uint256 i = 0; i < count; i++) { + nodeIds[i] = nodeIdsSet.at(i); + } + + // Get the node ID directly owned by the user + uint256 ownedNodeId = $.vechainNodesContract.ownerToId(user); + if (ownedNodeId != 0) { + // If the user directly owns a node, add it to the array + nodeIds = _appendToArray(nodeIds, ownedNodeId); + } + + // Create array to store node information + NodeInfo[] memory nodesInfo = new NodeInfo[](nodeIds.length); + + // Populate information for each node + for (uint256 i = 0; i < nodeIds.length; i++) { + uint256 currentNodeId = nodeIds[i]; + address currentNodeOwner = $.vechainNodesContract.idToOwner(currentNodeId); + address currentDelegatee = $.nodeIdToDelegatee[currentNodeId]; + bool isCurrentNodeDelegated = currentDelegatee != address(0); + + nodesInfo[i] = NodeInfo({ + nodeId: currentNodeId, + nodeLevel: getNodeLevel(currentNodeId), + xNodeOwner: currentNodeOwner, + isXNodeHolder: true, // If it's in the nodeIds array, user is either owner or delegatee + isXNodeDelegated: isCurrentNodeDelegated, + isXNodeDelegator: currentNodeId == ownedNodeId && isCurrentNodeDelegated, + isXNodeDelegatee: currentNodeId != ownedNodeId, + delegatee: currentDelegatee + }); + } + + return nodesInfo; + } + /** * @notice Retrieves the node level of a given node ID. * @dev Internal function to get the node level of a token ID. The node level is determined based on the metadata associated with the token ID. @@ -262,6 +410,16 @@ contract NodeManagement is INodeManagement, AccessControlUpgradeable, UUPSUpgrad return nodeLevels; } + /** + * @notice Check if a user directly owns a node (not delegated). + * @param user The address of the user to check. + * @return uint256 The ID of the owned node (0 if none). + */ + function getDirectNodeOwnership(address user) public view returns (uint256) { + NodeManagementStorage storage $ = _getNodeManagementStorage(); + return $.vechainNodesContract.ownerToId(user); + } + /** * @notice Returns the Vechain node contract instance. * @return ITokenAuction The instance of the Vechain node contract. @@ -276,7 +434,7 @@ contract NodeManagement is INodeManagement, AccessControlUpgradeable, UUPSUpgrad * @return string The current version of the contract. */ function version() external pure virtual returns (string memory) { - return "1"; + return "2"; } // ---------- Internal ---------- // diff --git a/contracts/VoterRewards.sol b/contracts/VoterRewards.sol index 30dd681..14428c7 100644 --- a/contracts/VoterRewards.sol +++ b/contracts/VoterRewards.sol @@ -65,6 +65,9 @@ import "@openzeppelin/contracts/utils/types/Time.sol"; * - Added the ability to track if a Vechain node attached to a Galaxy Member NFT has voted in a proposal. * - Proposal Id is now required when registering votes instead of proposal snapshot. * - Core logic functions are now virtual allowing to be overridden through inheritance. + * + * ------------------ Version 4 Changes ------------------ + * - Update the contract to use new Galaxy Member interface. */ contract VoterRewards is AccessControlUpgradeable, ReentrancyGuardUpgradeable, UUPSUpgradeable { using Checkpoints for Checkpoints.Trace208; // Checkpoints library for managing checkpoints of the selected level of the user @@ -495,7 +498,7 @@ contract VoterRewards is AccessControlUpgradeable, ReentrancyGuardUpgradeable, U /// @dev This should be updated every time a new version of implementation is deployed /// @return string The version of the contract function version() external pure virtual returns (string memory) { - return "3"; + return "4"; } /// @dev Clock used for flagging checkpoints. diff --git a/contracts/deprecated/V1/NodeManagementV1.sol b/contracts/deprecated/V1/NodeManagementV1.sol new file mode 100644 index 0000000..fcde94f --- /dev/null +++ b/contracts/deprecated/V1/NodeManagementV1.sol @@ -0,0 +1,306 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.20; + +import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import { VechainNodesDataTypes } from "../../libraries/VechainNodesDataTypes.sol"; +import { ITokenAuction } from "../../interfaces/ITokenAuction.sol"; +import { INodeManagementV1 } from "./interfaces/INodeManagementV1.sol"; + +contract NodeManagementV1 is INodeManagementV1, AccessControlUpgradeable, UUPSUpgradeable { + using EnumerableSet for EnumerableSet.UintSet; + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE"); + + /// @custom:storage-location erc7201:b3tr.storage.NodeManagement + struct NodeManagementStorage { + ITokenAuction vechainNodesContract; // The token auction contract + mapping(address => EnumerableSet.UintSet) delegateeToNodeIds; // Map delegatee address to set of node IDs + mapping(uint256 => address) nodeIdToDelegatee; // Map node ID to delegatee address + } + + // keccak256(abi.encode(uint256(keccak256("b3tr.storage.NodeManagement")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant NodeManagementStorageLocation = + 0x895b04a03424f581b1c6717e3715bbb5ceb9c40a4e5b61a13e84096251cf8f00; + + /** + * @notice Retrieve the storage reference for node delegation data. + * @dev Internal pure function to get the storage slot for node delegation data using inline assembly. + * @return $ The storage reference for node delegation data. + */ + function _getNodeManagementStorage() internal pure returns (NodeManagementStorage storage $) { + assembly { + $.slot := NodeManagementStorageLocation + } + } + + /** + * @notice Initialize the contract with the specified VeChain Nodes contract, admin, and upgrader addresses. + * @dev This function initializes the contract and sets the initial values for the VeChain Nodes contract address and other roles. It should be called only once during deployment. + * @param _vechainNodesContract The address of the VeChain Nodes contract. + * @param _admin The address to be granted the default admin role. + * @param _upgrader The address to be granted the upgrader role. + */ + function initialize(address _vechainNodesContract, address _admin, address _upgrader) external initializer { + __UUPSUpgradeable_init(); + __AccessControl_init(); + + require(_admin != address(0), "NodeManagement: admin address cannot be zero"); + _grantRole(DEFAULT_ADMIN_ROLE, _admin); + _grantRole(UPGRADER_ROLE, _upgrader); + + NodeManagementStorage storage $ = _getNodeManagementStorage(); + $.vechainNodesContract = ITokenAuction(_vechainNodesContract); + emit VechainNodeContractSet(address(0), _vechainNodesContract); + } + + // ---------- Setters ---------- // + + /** + * @notice Delegate a node to another address. + * @dev This function allows a node owner to delegate their node to another address. + * @param delegatee The address to delegate the node to. + */ + function delegateNode(address delegatee) public virtual { + NodeManagementStorage storage $ = _getNodeManagementStorage(); + + // Check if the delegatee address is the zero address + if (delegatee == address(0)) { + revert NodeManagementZeroAddress(); + } + + // Get the node ID of the caller + uint256 nodeId = $.vechainNodesContract.ownerToId(msg.sender); + + // If node ID is equal to zero, user does not own a node + if (nodeId == 0) { + revert NodeManagementNonNodeHolder(); + } + + // Check if the delegatee is the same as the caller, a node owner by defualt is the node manager and cannot delegate to themselves + if (msg.sender == delegatee) { + revert NodeManagementSelfDelegation(); + } + + // Check if node ID is already delegated to another user and if so remove the delegation + if ($.nodeIdToDelegatee[nodeId] != address(0)) { + // Emit event for delegation removal + emit NodeDelegated(nodeId, $.nodeIdToDelegatee[nodeId], false); + // Remove delegation + $.delegateeToNodeIds[delegatee].remove(nodeId); + } + + // Update mappings for delegation + $.delegateeToNodeIds[delegatee].add(nodeId); // Add node ID to delegatee's set + $.nodeIdToDelegatee[nodeId] = delegatee; // Map node ID to delegatee + + // Emit event for delegation + emit NodeDelegated(nodeId, delegatee, true); + } + + /** + * @notice Remove the delegation of a node. + * @dev This function allows a node owner to remove the delegation of their node, effectively revoking the delegatee's access to the node. + */ + function removeNodeDelegation() public virtual { + NodeManagementStorage storage $ = _getNodeManagementStorage(); + + // Get the node ID of the caller + uint256 nodeId = $.vechainNodesContract.ownerToId(msg.sender); + + // If node ID is equal to zero, user does not own a node + if (nodeId == 0) { + revert NodeManagementNonNodeHolder(); + } + + // Check if node is delegated + address delegatee = $.nodeIdToDelegatee[nodeId]; + if (delegatee == address(0)) { + revert NodeManagementNodeNotDelegated(); + } + + // Remove delegation + $.delegateeToNodeIds[delegatee].remove(nodeId); + delete $.nodeIdToDelegatee[nodeId]; + + // Emit event for delegation removal + emit NodeDelegated(nodeId, delegatee, false); + } + + /** + * @notice Set the address of the VeChain Nodes contract. + * @dev This function allows the admin to update the address of the VeChain Nodes contract. + * @param vechainNodesContract The new address of the VeChain Nodes contract. + */ + function setVechainNodesContract(address vechainNodesContract) external onlyRole(DEFAULT_ADMIN_ROLE) { + require(vechainNodesContract != address(0), "NodeManagement: vechainNodesContract cannot be the zero address"); + + NodeManagementStorage storage $ = _getNodeManagementStorage(); + + emit VechainNodeContractSet(address($.vechainNodesContract), vechainNodesContract); + $.vechainNodesContract = ITokenAuction(vechainNodesContract); + } + + // ---------- Getters ---------- // + + /** + * @notice Retrieves the address of the user managing the node ID endorsement either through ownership or delegation. + * @dev If the node is delegated, this function returns the delegatee's address. If the node is not delegated, it returns the owner's address. + * @param nodeId The ID of the node for which the manager address is being retrieved. + * @return The address of the manager of the specified node. + */ + function getNodeManager(uint256 nodeId) public view returns (address) { + NodeManagementStorage storage $ = _getNodeManagementStorage(); + + // Get the address of the delegatee for the given nodeId + address user = $.nodeIdToDelegatee[nodeId]; + + // Return the delegated node ID if it exists, otherwise return the node ID directly owned by the user + return user != address(0) ? user : $.vechainNodesContract.idToOwner(nodeId); + } + + /** + * @notice Retrieve the node IDs associated with a user, either through direct ownership or delegation. + * @param user The address of the user to check. + * @return uint256[] The node IDs associated with the user. + */ + function getNodeIds(address user) public view returns (uint256[] memory) { + NodeManagementStorage storage $ = _getNodeManagementStorage(); + + // Get the set of node IDs delegated to the user + EnumerableSet.UintSet storage nodeIdsSet = $.delegateeToNodeIds[user]; + + // Calculate the total number of node IDs + uint256 count = nodeIdsSet.length(); + + // Create an array to hold the node IDs + uint256[] memory nodeIds = new uint256[](count); + + // Populate the array with node IDs from the set + for (uint256 i = 0; i < count; i++) { + nodeIds[i] = nodeIdsSet.at(i); + } + + // Get the node ID directly owned by the user + uint256 ownedNodeId = $.vechainNodesContract.ownerToId(user); + if (ownedNodeId != 0 && $.nodeIdToDelegatee[ownedNodeId] == address(0)) { + // If the user directly owns a node, add it to the array + nodeIds = _appendToArray(nodeIds, ownedNodeId); + } + + return nodeIds; + } + + /** + * @notice Check if a user is holding a specific node ID either directly or through delegation. + * @param user The address of the user to check. + * @param nodeId The node ID to check for. + * @return bool True if the user is holding the node ID and it is a valid node. + */ + function isNodeManager(address user, uint256 nodeId) public view virtual returns (bool) { + NodeManagementStorage storage $ = _getNodeManagementStorage(); + + // Check if the user has the node ID delegated to them and if it is valid + if ($.nodeIdToDelegatee[nodeId] == user) { + // Return true if the owner of the token ID is not the zero address (valid nodeId) + return $.vechainNodesContract.idToOwner(nodeId) != address(0); + } + + if ($.nodeIdToDelegatee[nodeId] != address(0)) { + // If the node ID is delegated to another user, return false + return false; + } + + // Check if the user owns the node ID + return $.vechainNodesContract.idToOwner(nodeId) == user; + } + + /** + * @notice Retrieves the node level of a given node ID. + * @dev Internal function to get the node level of a token ID. The node level is determined based on the metadata associated with the token ID. + * @param nodeId The token ID of the endorsing node. + * @return The node level of the specified token ID as a VechainNodesDataTypes.NodeStrengthLevel enum. + */ + function getNodeLevel(uint256 nodeId) public view returns (VechainNodesDataTypes.NodeStrengthLevel) { + NodeManagementStorage storage $ = _getNodeManagementStorage(); + + // Retrieve the metadata for the specified node ID + (, uint8 nodeLevel, , , , , ) = $.vechainNodesContract.getMetadata(nodeId); + + // Cast the uint8 node level to VechainNodesDataTypes.NodeStrengthLevel enum and return + return VechainNodesDataTypes.NodeStrengthLevel(nodeLevel); + } + + /** + * @notice Retrieves the node levels of a user's managed nodes. + * @dev This function retrieves the node levels of the nodes managed by the specified user, either through ownership or delegation. + * @param user The address of the user managing the nodes. + * @return VechainNodesDataTypes.NodeStrengthLevel[] The node levels of the nodes managed by the user. + */ + function getUsersNodeLevels(address user) public view returns (VechainNodesDataTypes.NodeStrengthLevel[] memory) { + // Retrieve the node IDs managed by the specified user + uint256[] memory nodeIds = getNodeIds(user); + + // Initialize an array to hold the node levels + VechainNodesDataTypes.NodeStrengthLevel[] memory nodeLevels = new VechainNodesDataTypes.NodeStrengthLevel[]( + nodeIds.length + ); + + // Retrieve the node level for each node ID and store it in the nodeLevels array + for (uint256 i; i < nodeIds.length; i++) { + nodeLevels[i] = getNodeLevel(nodeIds[i]); + } + + // Return the array of node levels + return nodeLevels; + } + + /** + * @notice Returns the Vechain node contract instance. + * @return ITokenAuction The instance of the Vechain node contract. + */ + function getVechainNodesContract() external view returns (ITokenAuction) { + NodeManagementStorage storage $ = _getNodeManagementStorage(); + return $.vechainNodesContract; + } + + /** + * @notice Retrieves the current version of the contract. + * @return string The current version of the contract. + */ + function version() external pure virtual returns (string memory) { + return "1"; + } + + // ---------- Internal ---------- // + + /** + * @notice Appends an element to an array. + * @dev Internal function to append an element to an array. + * @param array The array to append to. + * @param element The element to append. + * @return uint256[] The new array with the appended element. + */ + function _appendToArray(uint256[] memory array, uint256 element) internal pure returns (uint256[] memory) { + uint256[] memory newArray = new uint256[](array.length + 1); + for (uint256 i; i < array.length; i++) { + newArray[i] = array[i]; + } + newArray[array.length] = element; + return newArray; + } + + /** + * @notice Authorize the upgrade to a new implementation. + * @dev Internal function to authorize the upgrade to a new contract implementation. This function is restricted to addresses with the upgrader role. + * @param newImplementation The address of the new contract implementation. + */ + function _authorizeUpgrade(address newImplementation) internal virtual override onlyRole(UPGRADER_ROLE) {} +} diff --git a/contracts/deprecated/V1/interfaces/INodeManagementV1.sol b/contracts/deprecated/V1/interfaces/INodeManagementV1.sol new file mode 100644 index 0000000..011dadc --- /dev/null +++ b/contracts/deprecated/V1/interfaces/INodeManagementV1.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.20; + +import { VechainNodesDataTypes } from "../../../libraries/VechainNodesDataTypes.sol"; + +interface INodeManagementV1 { + /** + * @notice Error indicating that the caller does not own a node. + */ + error NodeManagementNonNodeHolder(); + + /** + * @notice Error indicating that the node is not currently delegated. + */ + error NodeManagementNodeNotDelegated(); + + /** + * @notice Error indicating that an address is being set to the zero address. + */ + error NodeManagementZeroAddress(); + + /** + * @notice Error indicating that an address is being set to the zero address. + */ + error NodeManagementSelfDelegation(); + + /** + * @notice Error indicating that the node is already delegated to another user. + * @param nodeId The ID of the node that is already delegated. + * @param delegatee The address of the current delegatee. + */ + error NodeManagementNodeAlreadyDelegated(uint256 nodeId, address delegatee); + + /** + * @notice Event emitted when a node is delegated or the delegation is removed. + * @param nodeId The ID of the node being delegated or having its delegation removed. + * @param delegatee The address to which the node is delegated or from which the delegation is removed. + * @param delegated A boolean indicating whether the node is delegated (true) or the delegation is removed (false). + */ + event NodeDelegated(uint256 indexed nodeId, address indexed delegatee, bool delegated); + + /** + * @dev Emit when the vechain node contract address is set or updated + */ + event VechainNodeContractSet(address oldContractAddress, address newContractAddress); + + /** + * @notice Initialize the contract with the specified VeChain Nodes contract, admin, and upgrader addresses. + * @param vechainNodesContract The address of the VeChain Nodes contract. + * @param admin The address to be granted the default admin role. + * @param upgrader The address to be granted the upgrader role. + */ + function initialize(address vechainNodesContract, address admin, address upgrader) external; + + /** + * @notice Set the address of the VeChain Nodes contract. + * @param vechainNodesContract The new address of the VeChain Nodes contract. + */ + function setVechainNodesContract(address vechainNodesContract) external; + + /** + * @notice Delegate a node to another address. + * @param delegatee The address to delegate the node to. + */ + function delegateNode(address delegatee) external; + + /** + * @notice Remove the delegation of a node. + */ + function removeNodeDelegation() external; + + /** + * @notice Retrieve the node ID associated with a user, either through direct ownership or delegation. + * @param user The address of the user to check. + * @return uint256 The node ID associated with the user. + */ + function getNodeIds(address user) external view returns (uint256[] memory); + + /** + * @notice Retrieves the address of the user managing the node ID endorsement either through ownership or delegation. + * @param nodeId The ID of the node for which the manager address is being retrieved. + * @return address The address of the manager of the specified node. + */ + function getNodeManager(uint256 nodeId) external view returns (address); + + /** + * @notice Check if a user is holding a specific node ID either directly or through delegation. + * @param user The address of the user to check. + * @param nodeId The node ID to check for. + * @return bool True if the user is holding the node ID and it is a valid node. + */ + function isNodeManager(address user, uint256 nodeId) external view returns (bool); + + /** + * @notice Retrieves the node level of a given node ID. + * @param nodeId The token ID of the endorsing node. + * @return VechainNodesDataTypes.NodeStrengthLevel The node level of the specified token ID. + */ + function getNodeLevel(uint256 nodeId) external view returns (VechainNodesDataTypes.NodeStrengthLevel); + + /** + * @notice Retrieves the node level of a user's managed node. + * @param user The address of the user managing the node. + * @return VechainNodesDataTypes.NodeStrengthLevel The node level of the node managed by the user. + */ + function getUsersNodeLevels(address user) external view returns (VechainNodesDataTypes.NodeStrengthLevel[] memory); +} diff --git a/contracts/deprecated/V2/GalaxyMemberV2.sol b/contracts/deprecated/V2/GalaxyMemberV2.sol new file mode 100644 index 0000000..b5d9266 --- /dev/null +++ b/contracts/deprecated/V2/GalaxyMemberV2.sol @@ -0,0 +1,833 @@ +// SPDX-License-Identifier: MIT + +// ####### +// ################ +// #################### +// ########### ######### +// ######### ######### +// ####### ######### ######### +// ######### ######### ########## +// ########## ######## #################### +// ########## ######### ######################### +// ################### ############################ +// ################# ########## ######## +// ############## ### ######## +// ############ ######### +// ########## ########## +// ######## ########### +// ### ############ +// ############## +// ################# +// ############## +// ######### + +pragma solidity 0.8.20; + +import "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721EnumerableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721URIStorageUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721PausableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721BurnableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import "@openzeppelin/contracts/utils/Strings.sol"; +import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; +import { Checkpoints } from "@openzeppelin/contracts/utils/structs/Checkpoints.sol"; +import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import { IXAllocationVotingGovernor } from "../../interfaces/IXAllocationVotingGovernor.sol"; +import { IB3TRGovernor } from "../../interfaces/IB3TRGovernor.sol"; +import { IB3TR } from "../../interfaces/IB3TR.sol"; +import { ITokenAuction } from "../../interfaces/ITokenAuction.sol"; +import { INodeManagementV1 } from "../V1/interfaces/INodeManagementV1.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; + +/** + * @title GalaxyMember + * @notice This contract manages the unique assets owned by users within the Galaxy Member ecosystem. + * @dev Extends ERC721 Non-Fungible Token Standard basic implementation with upgradeable pattern, burnable, pausable, and access control functionalities. + * + * --------------------------------- VERSION --------------------------------- + * - Added Vechain Nodes contract to attach and detach nodes to tokens + * - Added NODES_MANAGER_ROLE to manage Vechain Nodes Contract address and free upgrade levels + * - Added free upgrade levels for each Vechain node level + * - Removed automatic highest level owned selection + * - Added dynamic level fetching of the token based on the attached Vechain node and B3TR donated for upgrading + * - Core logic functions are now overridable through inheritance + * - B3TRGovernor has been updated to V2 thus pointing to the new interface + * - NodeManagement contract has been added to permit attaching and detaching nodes from managed nodes too + */ +contract GalaxyMemberV2 is + ERC721Upgradeable, + ERC721EnumerableUpgradeable, + ERC721PausableUpgradeable, + ERC721BurnableUpgradeable, + AccessControlUpgradeable, + ReentrancyGuardUpgradeable, + UUPSUpgradeable +{ + bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE"); + bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); + bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); + bytes32 public constant CONTRACTS_ADDRESS_MANAGER_ROLE = keccak256("CONTRACTS_ADDRESS_MANAGER_ROLE"); + bytes32 public constant NODES_MANAGER_ROLE = keccak256("NODES_MANAGER_ROLE"); + + /// @notice Storage structure for GalaxyMember + /// @dev GalaxyMemberStorage structure holds all the state variables in a single location. + /// @custom:storage-location erc7201:b3tr.storage.GalaxyMember + struct GalaxyMemberStorage { + IXAllocationVotingGovernor xAllocationsGovernor; // XAllocationVotingGovernor contract + IB3TRGovernor b3trGovernor; // B3TRGovernor contract + IB3TR b3tr; // B3TR token contract + address treasury; // Treasury contract address + string _baseTokenURI; // Base URI for the Token + uint256 _nextTokenId; // Next Token ID to be minted + uint256 MAX_LEVEL; // Current Maximum level the Token can be minted or upgraded to + mapping(uint256 => uint256) levelOf; // Mapping from token ID to level of the Token + mapping(uint256 => uint256) _b3trToUpgradeToLevel; // Mapping from level to B3TR required to upgrade to that level + mapping(address owner => Checkpoints.Trace208) _selectedLevelCheckpoints; // Checkpoints for selected level of the user + mapping(address => mapping(uint256 => uint256)) _ownedLevels; // Value-Frequency map tracking levels owned by users + bool isPublicMintingPaused; // Flag to pause public minting + // --------------------------- V2 Additions --------------------------- // + ITokenAuction vechainNodes; // Vechain Nodes contract + INodeManagementV1 nodeManagement; // Node Management contract + mapping(uint256 => uint256) _nodeToTokenId; // Mapping from Vechain node ID to GalaxyMember Token ID. Used to track the XNode tied to the GM token ID + mapping(uint256 => uint256) _tokenIdToNode; // Mapping from GalaxyMember Token ID to Vechain node ID. Used to track the GM token ID tied to the XNode token ID + mapping(uint8 => uint256) _nodeToFreeUpgradeLevel; // Mapping from Vechain node level to GalaxyMember level. Used to track the GM level that can be upgraded for free for a given Vechain node level + mapping(uint256 => uint256) _tokenIdToB3TRdonated; // Mapping from GM Token ID to B3TR donated for upgrading + mapping(address => uint256) _selectedTokenID; // Mapping from user address to selected GM token ID + } + + /// @notice Storage slot for GalaxyMemberStorage + /// @dev keccak256(abi.encode(uint256(keccak256("b3tr.storage.GalaxyMember")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant GalaxyMemberStorageLocation = + 0x7a79e46844ed04411e4579c7bc49d053e59b0854fa4e9a8df3d5a0597ce45200; + + /// @dev Retrieves the current state from the GalaxyMemberStorage mapping + function _getGalaxyMemberStorage() private pure returns (GalaxyMemberStorage storage $) { + assembly { + $.slot := GalaxyMemberStorageLocation + } + } + + /// @dev Emitted when an account changes the selected token for voting rewards. + event Selected(address indexed owner, uint256 tokenId); + + /// @dev Emitted when a token is upgraded. + event Upgraded(uint256 indexed tokenId, uint256 oldLevel, uint256 newLevel); + + /// @dev Emitted when the max level is updated. + event MaxLevelUpdated(uint256 oldLevel, uint256 newLevel); + + /// @dev Emitted when XAllocationVotingGovernor contract address is updated + event XAllocationsGovernorAddressUpdated(address indexed newAddress, address indexed oldAddress); + + /// @dev Emitted when B3TRGovernor contract address is updated + event B3trGovernorAddressUpdated(address indexed newAddress, address indexed oldAddress); + + /// @dev Emitted when base URI is updated + event BaseURIUpdated(string indexed newBaseURI, string indexed oldBaseURI); + + /// @dev Emitted when B3TR required to upgrade to each level is updated + event B3TRtoUpgradeToLevelUpdated(uint256[] indexed b3trToUpgradeToLevel); + + /// @dev Emitted when public minting is paused + event PublicMintingPaused(bool isPaused); + + /// @dev Emitted when a node is attached to a token + event NodeAttached(uint256 indexed nodeTokenId, uint256 indexed tokenId); + + /// @dev Emitted when a node is detached from a token + event NodeDetached(uint256 indexed nodeTokenId, uint256 indexed tokenId); + + /// @notice Modifier to check if public minting is not paused + modifier whenPublicMintingNotPaused() { + GalaxyMemberStorage storage $ = _getGalaxyMemberStorage(); + require(!$.isPublicMintingPaused, "Galaxy Member: Public minting is paused"); + _; + } + + /// @notice Ensures only initializer functions are called when deploying a proxy + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + /// @notice Data for initializing the contract + /// @param name Name of the ERC721 token + /// @param symbol Symbol of the ERC721 token + /// @param admin Address to grant the admin role + /// @param upgrader Address to grant the upgrader role + /// @param pauser Address to grant the pauser role + /// @param minter Address to grant the minter role + /// @param contractsAddressManager Address that can update external contracts address + /// @param maxLevel Maximum level tokens can achieve + /// @param baseTokenURI Base URI for computing {tokenURI} + /// @param b3trToUpgradeToLevel Mapping of B3TR requirements per level + /// @param _b3tr B3TR token contract address + /// @param _treasury Address of the treasury + struct InitializationData { + string name; + string symbol; + address admin; + address upgrader; + address pauser; + address minter; + address contractsAddressManager; + uint256 maxLevel; + string baseTokenURI; + uint256[] b3trToUpgradeToLevel; + address b3tr; + address treasury; + } + + /// @notice Initializes a new GalaxyMember contract + /// @dev Sets initial values for all relevant contract properties and state variables. + /// @custom:oz-upgrades-unsafe-allow constructor + function initialize(InitializationData memory data) external initializer { + require(data.maxLevel > 0, "Galaxy Member: Max level must be greater than 0"); + require(bytes(data.baseTokenURI).length > 0, "Galaxy Member: Base URI must be set"); + require(data.b3tr != address(0), "Galaxy Member: B3TR token address cannot be the zero address"); + require(data.treasury != address(0), "Galaxy Member: Treasury address cannot be the zero address"); + require( + data.b3trToUpgradeToLevel.length >= data.maxLevel - 1, + "Galaxy Member: B3TR to upgrade must be set for all unlocked levels" + ); + + __ERC721_init(data.name, data.symbol); + __ERC721Enumerable_init(); + __ERC721Pausable_init(); + __ERC721Burnable_init(); + __AccessControl_init(); + __ReentrancyGuard_init(); + __UUPSUpgradeable_init(); + + GalaxyMemberStorage storage $ = _getGalaxyMemberStorage(); + + $._baseTokenURI = data.baseTokenURI; + + for (uint256 i = 0; i < data.b3trToUpgradeToLevel.length; i++) { + require(data.b3trToUpgradeToLevel[i] > 0, "Galaxy Member: B3TR to upgrade must be greater than 0"); + $._b3trToUpgradeToLevel[i + 2] = data.b3trToUpgradeToLevel[i]; // First Level that requires B3TR is level 2 + } + + $.MAX_LEVEL = data.maxLevel; + + $.b3tr = IB3TR(data.b3tr); + $.treasury = data.treasury; + + require(data.admin != address(0), "Galaxy Member: Admin address cannot be the zero address"); + _grantRole(DEFAULT_ADMIN_ROLE, data.admin); + _grantRole(UPGRADER_ROLE, data.upgrader); + _grantRole(PAUSER_ROLE, data.pauser); + _grantRole(MINTER_ROLE, data.minter); + _grantRole(CONTRACTS_ADDRESS_MANAGER_ROLE, data.contractsAddressManager); + } + + /// @notice Initializes a new GalaxyMember contract + /// @dev Sets initial values for all relevant contract properties and state variables. + /// @custom:oz-upgrades-unsafe-allow constructor + function initializeV2( + address _vechainNodes, + address _nodesMangaement, + address _nodesAdmin, + uint256[] memory _nodeFreeLevels + ) external reinitializer(2) { + require(_nodeFreeLevels.length == 8, "GalaxyMember: invalid node free levels. Must be 7 levels"); + require(_vechainNodes != address(0), "GalaxyMember: _vechainNodes cannot be the zero address"); + require(_nodesMangaement != address(0), "GalaxyMember: _nodesMangaement cannot be the zero address"); + + GalaxyMemberStorage storage $ = _getGalaxyMemberStorage(); + + $.vechainNodes = ITokenAuction(_vechainNodes); + $.nodeManagement = INodeManagementV1(_nodesMangaement); + + $._nextTokenId = $._nextTokenId == 0 ? 1 : $._nextTokenId; + + for (uint8 i; i < _nodeFreeLevels.length; i++) { + require(_nodeFreeLevels[i] >= 1, "GalaxyMember: invalid node free level"); + $._nodeToFreeUpgradeLevel[i] = _nodeFreeLevels[i]; + } + + _grantRole(NODES_MANAGER_ROLE, _nodesAdmin); + } + + /// @notice Internal function to authorize contract upgrades + /// @dev Restricts upgrade authorization to addresses with UPGRADER_ROLE + /// @param newImplementation Address of the new contract implementation + function _authorizeUpgrade(address newImplementation) internal override onlyRole(UPGRADER_ROLE) {} + + /// @notice Pauses the Galaxy Member contract + /// @dev pausing the contract will prevent minting, upgrading, and transferring of tokens + /// @dev Only callable by the pauser role + function pause() public virtual onlyRole(PAUSER_ROLE) { + _pause(); + } + + /// @notice Unpauses the Galaxy Member contract + /// @dev Only callable by the pauser role + function unpause() public virtual onlyRole(PAUSER_ROLE) { + _unpause(); + } + + /// @notice Allows a user to freely mint a token if they have participated in governance + /// @dev Mints a token with level 1 and ensures that the public minting is not paused + function freeMint() public virtual whenPublicMintingNotPaused { + require(participatedInGovernance(msg.sender), "Galaxy Member: User has not participated in governance"); + + safeMint(msg.sender); + } + + /// @notice Upgrades a token to the next level + /// @dev Requires the owner to have enough B3TR tokens and sufficient allowance for the contract to use them + /// @param tokenId Token ID to upgrade + function upgrade(uint256 tokenId) public virtual nonReentrant whenNotPaused { + require(ownerOf(tokenId) == msg.sender, "Galaxy Member: you must own the Token to upgrade it"); + GalaxyMemberStorage storage $ = _getGalaxyMemberStorage(); + + uint256 currentLevel = levelOf(tokenId); + + require(currentLevel < MAX_LEVEL(), "Galaxy Member: Token is already at max level"); + + uint256 b3trRequired = getB3TRtoUpgrade(tokenId); + + require($.b3tr.balanceOf(msg.sender) >= b3trRequired, "Galaxy Member: Insufficient balance to upgrade"); + + require( + $.b3tr.allowance(msg.sender, address(this)) >= b3trRequired, + "Galaxy Member: Insufficient allowance to upgrade" + ); + + $._tokenIdToB3TRdonated[tokenId] += b3trRequired; + + require($.b3tr.transferFrom(msg.sender, $.treasury, b3trRequired), "GalaxyMember: Transfer failed"); + + emit Upgraded(tokenId, currentLevel, levelOf(tokenId)); + } + + /// @notice Allows the user to select a token for voting rewards multiplier + /// @param tokenID Token ID to select + function select(uint256 tokenID) public virtual { + _select(msg.sender, tokenID); + } + + /// @notice selects the specified token for the user + /// @param owner The address of the owner to check + /// @param tokenId the token ID to select + function _select(address owner, uint256 tokenId) internal virtual { + require(ownerOf(tokenId) == owner, "Galaxy Member: caller is not the owner of the token"); + + GalaxyMemberStorage storage $ = _getGalaxyMemberStorage(); + + $._selectedTokenID[owner] = tokenId; + + emit Selected(owner, tokenId); + } + + /// @notice Allows the token owner to burn their token + /// @dev Overrides the ERC721BurnableUpgradeable function to include custom burning logic + /// @param tokenId Token ID to burn + function burn(uint256 tokenId) public virtual override(ERC721BurnableUpgradeable) { + require(ownerOf(tokenId) == msg.sender, "Galaxy Member: caller is not the owner of the token"); + + super.burn(tokenId); + } + + // ------------------------------- VECHAIN NODES FUNCTIONS ------------------------------- // + + function attachNode(uint256 nodeTokenId, uint256 tokenId) public virtual whenNotPaused { + GalaxyMemberStorage storage $ = _getGalaxyMemberStorage(); + + require(ownerOf(tokenId) == msg.sender, "GalaxyMember: token not owned by caller"); + require( + $.nodeManagement.getNodeManager(nodeTokenId) == msg.sender, + "GalaxyMember: vechain node not owned or managed by caller" + ); + require(getIdAttachedToNode(nodeTokenId) == 0, "GalaxyMember: node already attached to a token"); + require(getNodeIdAttached(tokenId) == 0, "GalaxyMember: token already attached to a node"); + + $._nodeToTokenId[nodeTokenId] = tokenId; + $._tokenIdToNode[tokenId] = nodeTokenId; + + emit NodeAttached(nodeTokenId, tokenId); + } + + function detachNode(uint256 nodeTokenId, uint256 tokenId) public virtual whenNotPaused { + GalaxyMemberStorage storage $ = _getGalaxyMemberStorage(); + + require( + ownerOf(tokenId) == msg.sender || + $.nodeManagement.getNodeManager(nodeTokenId) == msg.sender || + $.vechainNodes.idToOwner(nodeTokenId) == msg.sender, + "GalaxyMember: vechain node not owned or managed by caller or token not owned by caller" + ); + require(getIdAttachedToNode(nodeTokenId) == tokenId, "GalaxyMember: node not attached to the token"); + require(getNodeIdAttached(tokenId) == nodeTokenId, "GalaxyMember: token not attached to the node"); + + delete $._nodeToTokenId[nodeTokenId]; + delete $._tokenIdToNode[tokenId]; + + emit NodeDetached(nodeTokenId, tokenId); + } + + // ----------- Internal & Private ----------- // + + /// @notice Internal function to safely mint a token + /// @dev Adds a token to the total supply and assigns it to an address, incrementing the owner's balance + /// @param to Address to mint the token to + function safeMint(address to) internal virtual { + GalaxyMemberStorage storage $ = _getGalaxyMemberStorage(); + + uint256 tokenId = $._nextTokenId++; + _safeMint(to, tokenId); + } + + // ---------- Setters ---------- // + + /// @notice Sets the maximum level that tokens can be minted or upgraded to + /// @dev Only callable by the admin role + function setMaxLevel(uint256 level) public virtual onlyRole(DEFAULT_ADMIN_ROLE) { + GalaxyMemberStorage storage $ = _getGalaxyMemberStorage(); + + require(level > $.MAX_LEVEL, "Galaxy Member: Max level must be greater than the current max level"); + + // First Level that requires B3TR is level 2 + for (uint256 i = 2; i <= level; i++) { + require($._b3trToUpgradeToLevel[i] > 0, "Galaxy Member: B3TR to upgrade must be set for all levels unlocked"); // Require all levels til the new max level to have a B3TR requirement + } + + uint256 oldLevel = $.MAX_LEVEL; + + $.MAX_LEVEL = level; + + emit MaxLevelUpdated(oldLevel, level); + } + + /// @notice Sets the XAllocationVotingGovernor contract address + /// @dev Only callable by the contractsAddressManager role + /// @param _xAllocationsGovernor XAllocationVotingGovernor contract address + function setXAllocationsGovernorAddress( + address _xAllocationsGovernor + ) public virtual onlyRole(CONTRACTS_ADDRESS_MANAGER_ROLE) { + require(_xAllocationsGovernor != address(0), "Galaxy Member: _xAllocationsGovernor cannot be the zero address"); + GalaxyMemberStorage storage $ = _getGalaxyMemberStorage(); + + emit XAllocationsGovernorAddressUpdated(_xAllocationsGovernor, address($.xAllocationsGovernor)); + $.xAllocationsGovernor = IXAllocationVotingGovernor(_xAllocationsGovernor); + } + + /// @notice Sets the B3TRGovernor contract address + /// @dev Only callable by the contractsAddressManager role + /// @param _b3trGovernor B3TRGovernor contract address + function setB3trGovernorAddress(address _b3trGovernor) public virtual onlyRole(CONTRACTS_ADDRESS_MANAGER_ROLE) { + require(_b3trGovernor != address(0), "Galaxy Member: _b3trGovernor cannot be the zero address"); + GalaxyMemberStorage storage $ = _getGalaxyMemberStorage(); + + emit B3trGovernorAddressUpdated(_b3trGovernor, address($.b3trGovernor)); + $.b3trGovernor = IB3TRGovernor(payable(_b3trGovernor)); + } + + /// @notice Sets the base URI for computing the tokenURI + /// @dev Only callable by the admin role + /// @param baseTokenURI Base URI for the Token + function setBaseURI(string memory baseTokenURI) public virtual onlyRole(DEFAULT_ADMIN_ROLE) { + require(bytes(baseTokenURI).length > 0, "Galaxy Member: Base URI must be set"); + GalaxyMemberStorage storage $ = _getGalaxyMemberStorage(); + emit BaseURIUpdated(baseTokenURI, $._baseTokenURI); + $._baseTokenURI = baseTokenURI; + } + + /// @notice Sets the amount of B3TR required to upgrade to each level + /// @dev Only callable by the admin role + /// @param b3trToUpgradeToLevel Mapping of B3TR requirements per level + function setB3TRtoUpgradeToLevel(uint256[] memory b3trToUpgradeToLevel) public virtual onlyRole(DEFAULT_ADMIN_ROLE) { + GalaxyMemberStorage storage $ = _getGalaxyMemberStorage(); + for (uint256 i; i < b3trToUpgradeToLevel.length; i++) { + require(b3trToUpgradeToLevel[i] > 0, "Galaxy Member: B3TR to upgrade must be greater than 0"); + $._b3trToUpgradeToLevel[i + 2] = b3trToUpgradeToLevel[i]; // First Level that requires B3TR is level 2 + } + emit B3TRtoUpgradeToLevelUpdated(b3trToUpgradeToLevel); + } + + /// @notice Pauses public minting + /// @dev Only callable by the admin role + /// @param isPaused Flag to pause or unpause public minting + function setIsPublicMintingPaused(bool isPaused) public virtual onlyRole(DEFAULT_ADMIN_ROLE) { + GalaxyMemberStorage storage $ = _getGalaxyMemberStorage(); + emit PublicMintingPaused(isPaused); + $.isPublicMintingPaused = isPaused; + } + + /// @notice Sets the Vechain Nodes contract address + /// @param _vechainNodes Vechain Nodes contract address + function setVechainNodes(address _vechainNodes) public virtual onlyRole(NODES_MANAGER_ROLE) { + require(_vechainNodes != address(0), "GalaxyMember: _vechainNodes cannot be the zero address"); + GalaxyMemberStorage storage $ = _getGalaxyMemberStorage(); + $.vechainNodes = ITokenAuction(_vechainNodes); + } + + /// @notice Sets the treasury contract address + /// @param nodeLevel Vechain node level (i.e., 1, 2, 3, 4, 5, 6, 7, 8 => Strength, Thunder, Mjolnir, VeThorX, StrengthX, ThunderX, MjolnirX) + /// @param level new free upgrade level + function setNodeToFreeUpgradeLevel(uint8 nodeLevel, uint256 level) public virtual onlyRole(NODES_MANAGER_ROLE) { + require(level >= 1, "GalaxyMember: invalid level"); + require(nodeLevel >= 1, "GalaxyMember: invalid node level"); + + GalaxyMemberStorage storage $ = _getGalaxyMemberStorage(); + + require(level <= $.MAX_LEVEL, "GalaxyMember: level must be less than or equal to MAX_LEVEL"); + + $._nodeToFreeUpgradeLevel[nodeLevel] = level; + } + + // ---------- Getters ---------- // + + /// @notice Gets the level of the GM token + /// @param tokenId Token ID to check + function levelOf(uint256 tokenId) public view virtual returns (uint256) { + uint256 nodeId = getNodeIdAttached(tokenId); + + (uint256 level, ) = _getLevelOfAndB3TRleft(tokenId, nodeId); + + return level; + } + + /// @notice Gets the B3TR required to upgrade to the next level + /// @param tokenId Token ID to check + function getB3TRtoUpgrade(uint256 tokenId) public view virtual returns (uint256) { + if (_ownerOf(tokenId) == address(0)) return 0; + + uint256 nodeId = getNodeIdAttached(tokenId); + + // Get the level of the token and the B3TR donated left to upgrade + (uint256 currentLevel, uint256 b3trDonatedLeft) = _getLevelOfAndB3TRleft(tokenId, nodeId); + + // If the current level is the max level, return 0 + if (currentLevel == MAX_LEVEL()) { + return 0; + } + + // Get the B3TR required to upgrade to the next level by subtracting the B3TR donated left from the B3TR required to upgrade to the next level + return getB3TRtoUpgradeToLevel(currentLevel + 1) - b3trDonatedLeft; + } + + /// @notice Gets the level of the GM token and the B3TR Donated left to upgrade + /// @param tokenId Token ID to check + /// @return level The level of the token + /// @return b3trDonatedLeft The B3TR donated left to upgrade + function _getLevelOfAndB3TRleft(uint256 tokenId, uint256 nodeId) internal view virtual returns (uint256, uint256) { + GalaxyMemberStorage storage $ = _getGalaxyMemberStorage(); + + // Default level is 1 if the token is not attached to a node or the token has not been upgraded + uint256 level = 1; + + // if the token is attached to a node and the node is managed by the caller + if (nodeId != 0 && $.nodeManagement.getNodeManager(nodeId) == ownerOf(tokenId)) { + // Get the level of the node (i.e., Strength, Thunder, Mjolnir, VeThorX, StrengthX, ThunderX, MjolnirX) + uint8 nodeLevel = getNodeLevelOf(nodeId); + + // If the node level is not 0 (i.e., None) + if (nodeLevel != 0) { + // Get the level of the token that can be upgraded for free + uint256 nodeToFreeLevel = getNodeToFreeLevel(nodeLevel); + + // If the free upgrade level is greater than the current max level, set the level to the max level + level = nodeToFreeLevel <= $.MAX_LEVEL ? nodeToFreeLevel : $.MAX_LEVEL; + } + } + + // Initialise the B3TR donated which keeps track of the B3TR left after upgrading the token + uint256 b3trDonatedLeft = $._tokenIdToB3TRdonated[tokenId]; + + // Loop through the levels starting from the current level and check if the B3TR donated is enough to upgrade to the next level + for (uint256 i = level + 1; i <= $.MAX_LEVEL; i++) { + if (b3trDonatedLeft >= $._b3trToUpgradeToLevel[i]) { + level = i; + b3trDonatedLeft -= $._b3trToUpgradeToLevel[i]; + } else { + // If the B3TR donated is not enough to upgrade to the next level, break the loop + break; + } + } + + // Return the level and the B3TR donated left. + return (level, b3trDonatedLeft); + } + + /// @notice Gets the strength level of the Vechain node + /// @param nodeId Vechain Node Token ID + function getNodeLevelOf(uint256 nodeId) public view virtual returns (uint8) { + GalaxyMemberStorage storage $ = _getGalaxyMemberStorage(); + + (, uint8 nodeLevel, , , , , ) = $.vechainNodes.getMetadata(nodeId); + + return nodeLevel; + } + + /// @notice Gets the selected token ID for the user + /// @param owner The address of the owner to check + function getSelectedTokenId(address owner) public view virtual returns (uint256) { + GalaxyMemberStorage storage $ = _getGalaxyMemberStorage(); + return $._selectedTokenID[owner]; + } + + /// @notice Gets the level of the token after attaching a node + /// @param tokenId - GM Token ID + /// @param nodeTokenId - Vechain Node Token ID + function getLevelAfterAttachingNode(uint256 tokenId, uint256 nodeTokenId) public view virtual returns (uint256) { + (uint256 level, ) = _getLevelOfAndB3TRleft(tokenId, nodeTokenId); + + return level; + } + + /// @notice Gets the level of the token after detaching a node + /// @param tokenId - GM Token ID + function getLevelAfterDetachingNode(uint256 tokenId) public view virtual returns (uint256) { + (uint256 level, ) = _getLevelOfAndB3TRleft(tokenId, 0); + + return level; + } + + /// @notice Gets whether the user has participated in governance + /// @param user The address of the user to check + function participatedInGovernance(address user) public view virtual returns (bool) { + GalaxyMemberStorage storage $ = _getGalaxyMemberStorage(); + require( + $.xAllocationsGovernor != IXAllocationVotingGovernor(address(0)), + "Galaxy Member: XAllocationVotingGovernor not set" + ); + require($.b3trGovernor != IB3TRGovernor(payable(address(0))), "Galaxy Member: B3TRGovernor not set"); + + if ($.xAllocationsGovernor.hasVotedOnce(user) || $.b3trGovernor.hasVotedOnce(user)) { + return true; + } + + return false; + } + + /// @notice Gets the base URI for computing the tokenURI + function baseURI() public view virtual returns (string memory) { + return _baseURI(); + } + + /// @notice Gets the B3TR required to upgrade to a specific level + /// @param level Level to upgrade to + function getB3TRtoUpgradeToLevel(uint256 level) public view virtual returns (uint256) { + GalaxyMemberStorage storage $ = _getGalaxyMemberStorage(); + return $._b3trToUpgradeToLevel[level]; + } + + /// @notice gets the token URI for a specific token + /// @dev computes the token URI based on the base URI and the level of the token + /// @param tokenId Token ID to get the URI for + function tokenURI(uint256 tokenId) public view virtual override(ERC721Upgradeable) returns (string memory) { + if (_ownerOf(tokenId) == address(0)) return ""; + + uint256 levelOfToken = levelOf(tokenId); + return levelOfToken > 0 ? string.concat(baseURI(), Strings.toString(levelOfToken), ".json") : ""; + } + + /// @notice Gets the xAllocationsGovernor contract address + function xAllocationsGovernor() external view returns (IXAllocationVotingGovernor) { + GalaxyMemberStorage storage $ = _getGalaxyMemberStorage(); + return $.xAllocationsGovernor; + } + + /// @notice Gets the b3trGovernor contract address + function b3trGovernor() external view returns (IB3TRGovernor) { + GalaxyMemberStorage storage $ = _getGalaxyMemberStorage(); + return $.b3trGovernor; + } + + /// @notice Gets the B3TR token contract address + function b3tr() external view returns (IB3TR) { + GalaxyMemberStorage storage $ = _getGalaxyMemberStorage(); + return $.b3tr; + } + + /// @notice Gets the treasury contract address + function treasury() external view returns (address) { + GalaxyMemberStorage storage $ = _getGalaxyMemberStorage(); + return $.treasury; + } + + /// @notice Gets the maximum level that tokens can be minted or upgraded to + function MAX_LEVEL() public view virtual returns (uint256) { + GalaxyMemberStorage storage $ = _getGalaxyMemberStorage(); + return $.MAX_LEVEL; + } + + /// @notice Get the GM Token ID attached to the Vechain Node Token ID + /// @param nodeId Vechain node Token ID + function getIdAttachedToNode(uint256 nodeId) public view virtual returns (uint256) { + return _getGalaxyMemberStorage()._nodeToTokenId[nodeId]; + } + + /// @notice Get the Vechain Node Token ID attached to the GM Token ID + /// @param tokenId GM Token ID + function getNodeIdAttached(uint256 tokenId) public view virtual returns (uint256) { + return _getGalaxyMemberStorage()._tokenIdToNode[tokenId]; + } + + /// @notice Get the GM level that can be upgraded for free for a given Vechain node level + /// @param nodeLevel Vechain node level + function getNodeToFreeLevel(uint8 nodeLevel) public view virtual returns (uint256) { + return _getGalaxyMemberStorage()._nodeToFreeUpgradeLevel[nodeLevel]; + } + + /// @notice Get the B3TR donated for upgrading a token + /// @param tokenId Token ID to check + function getB3TRdonated(uint256 tokenId) public view virtual returns (uint256) { + return _getGalaxyMemberStorage()._tokenIdToB3TRdonated[tokenId]; + } + + /// @notice Retrieves the current version of the contract + /// @dev This function is used to identify the version of the contract and should be updated in each new version + /// @return string The version of the contract + function version() external pure virtual returns (string memory) { + return "2"; + } + + struct TokenInfo { + uint256 tokenId; + string tokenURI; + uint256 tokenLevel; + uint256 b3trToUpgrade; + } + + /// @notice Gets the token info by token ID + /// @param tokenId Token ID to get the info for + /// @return TokenInfo The token info + function getTokenInfoByTokenId(uint256 tokenId) public view virtual returns (TokenInfo memory) { + // Check if the token ID exists + require(_ownerOf(tokenId) != address(0), "GalaxyMember: tokenId doesn't exist"); + + TokenInfo memory tokenInfo; + tokenInfo.tokenId = tokenId; + tokenInfo.tokenURI = tokenURI(tokenId); + tokenInfo.tokenLevel = levelOf(tokenId); + tokenInfo.b3trToUpgrade = getB3TRtoUpgrade(tokenId); + return tokenInfo; + } + + /// @notice Gets the selected token info for an address + /// @param owner The address of the owner to check + /// @return TokenInfo The selected token info + function getSelectedTokenInfoByOwner(address owner) public view returns (TokenInfo memory) { + uint256 tokenId = getSelectedTokenId(owner); + return getTokenInfoByTokenId(tokenId); + } + + /// @notice Gets the tokens owned by an address + /// @param owner The address of the owner to check + /// @param page The page number to fetch + /// @param size The number of tokens to fetch (cannot exceed 100) + /// @return TokenInfo[] The tokens owned by the address + function getTokensInfoByOwner(address owner, uint256 page, uint256 size) public view returns (TokenInfo[] memory) { + // Ensure size is not 0 + if (size == 0) { + revert("GalaxyMember: Invalid size, cannot be 0"); + } + + // Maximum number of tokens to fetch per page + uint256 MAX_PAGINATION_SIZE = 100; + + // Ensure size is not greater than the maximum allowed value + if (size > MAX_PAGINATION_SIZE) { + revert( + string( + abi.encodePacked("GalaxyMember: Invalid size, cannot be greater than ", Strings.toString(MAX_PAGINATION_SIZE)) + ) + ); + } + + uint256 balance = balanceOf(owner); // Get the number of tokens owned by the address + + // Calculate the starting index for the current page + uint256 start = page * size; + + // If start index is greater than or equal to balance, return an empty array + if (start >= balance) { + return new TokenInfo[](0); + } + + // Calculate the end index (exclusive) + uint256 end = start + size; + if (end > balance) { + end = balance; // Ensure the end index doesn't exceed the owner's balance + } + + // Calculate the number of tokens to return + uint256 numTokens = end - start; + + TokenInfo[] memory tokens = new TokenInfo[](numTokens); + + for (uint256 i = 0; i < numTokens; i++) { + uint256 tokenIndex = start + i; + uint256 tokenId = tokenOfOwnerByIndex(owner, tokenIndex); + tokens[i] = getTokenInfoByTokenId(tokenId); + } + + return tokens; + } + + // ---------- Overrides ---------- // + + /// @notice Performs automatic level updating upon token updates + /// @dev Overrides the _update function to update the highest level owned by the owner + /// @param to The address to transfer the token to + /// @param tokenId The token ID to update + /// @param auth The address of the sender + function _update( + address to, + uint256 tokenId, + address auth + ) + internal + override(ERC721Upgradeable, ERC721EnumerableUpgradeable, ERC721PausableUpgradeable) + whenNotPaused + returns (address) + { + require(getNodeIdAttached(tokenId) == 0, "GalaxyMember: token attached to a node, detach before transfer"); + + address _previousOwner = super._update(to, tokenId, auth); + + // If the owner has no tokens, don't select any token + if (_previousOwner != address(0) && balanceOf(_previousOwner) == 0) { + delete _getGalaxyMemberStorage()._selectedTokenID[_previousOwner]; + } + + // If the owner transfers out the selected token, select the first token he owns + if ( + _previousOwner != address(0) && getSelectedTokenId(_previousOwner) == tokenId && balanceOf(_previousOwner) > 0 + ) { + _select(_previousOwner, tokenOfOwnerByIndex(_previousOwner, 0)); + } + + // If the new owner has only one token, select it + if (to != address(0) && balanceOf(to) == 1) { + _select(to, tokenOfOwnerByIndex(to, 0)); + } + + return _previousOwner; + } + + /// @dev Overrides the _increaseBalance for ERC721Upgradeable and ERC721EnumerableUpgradeable + function _increaseBalance( + address account, + uint128 value + ) internal override(ERC721Upgradeable, ERC721EnumerableUpgradeable) { + super._increaseBalance(account, value); + } + + /// @dev Overrides the supportsInterface for ERC721Upgradeable, ERC721EnumerableUpgradeable, and AccessControlUpgradeable + function supportsInterface( + bytes4 interfaceId + ) public view override(ERC721Upgradeable, ERC721EnumerableUpgradeable, AccessControlUpgradeable) returns (bool) { + return super.supportsInterface(interfaceId); + } + + /// @dev Overrides the _baseURI for ERC721URIStorageUpgradeable + function _baseURI() internal view override returns (string memory) { + GalaxyMemberStorage storage $ = _getGalaxyMemberStorage(); + return $._baseTokenURI; + } +} \ No newline at end of file diff --git a/contracts/deprecated/V2/interfaces/IGalaxyMemberV2.sol b/contracts/deprecated/V2/interfaces/IGalaxyMemberV2.sol new file mode 100644 index 0000000..5f7c577 --- /dev/null +++ b/contracts/deprecated/V2/interfaces/IGalaxyMemberV2.sol @@ -0,0 +1,354 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.20; + +import "@openzeppelin/contracts/utils/structs/Checkpoints.sol"; + +/** + * @title IGalaxyMember + * @notice Interface for the GalaxyMember contract which handles NFT membership and governance functionality + * @dev Implements ERC721 with additional features for level upgrades, node attachments, and governance participation + */ +interface IGalaxyMemberV2 { + // Custom errors + error AccessControlBadConfirmation(); + error AccessControlUnauthorizedAccount(address account, bytes32 neededRole); + error AddressEmptyCode(address target); + error ERC1967InvalidImplementation(address implementation); + error ERC1967NonPayable(); + error ERC5805FutureLookup(uint256 timepoint, uint48 clock); + error ERC6372InconsistentClock(); + error ERC721EnumerableForbiddenBatchMint(); + error ERC721IncorrectOwner(address sender, uint256 tokenId, address owner); + error ERC721InsufficientApproval(address operator, uint256 tokenId); + error ERC721InvalidApprover(address approver); + error ERC721InvalidOperator(address operator); + error ERC721InvalidOwner(address owner); + error ERC721InvalidReceiver(address receiver); + error ERC721InvalidSender(address sender); + error ERC721NonexistentToken(uint256 tokenId); + error ERC721OutOfBoundsIndex(address owner, uint256 index); + error EnforcedPause(); + error ExpectedPause(); + error FailedInnerCall(); + error InvalidInitialization(); + error NotInitializing(); + error ReentrancyGuardReentrantCall(); + error UUPSUnauthorizedCallContext(); + error UUPSUnsupportedProxiableUUID(bytes32 slot); + + // Events + event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId); + event ApprovalForAll(address indexed owner, address indexed operator, bool approved); + event B3TRtoUpgradeToLevelUpdated(uint256[] indexed b3trToUpgradeToLevel); + event B3trGovernorAddressUpdated(address indexed newAddress, address indexed oldAddress); + event BaseURIUpdated(string indexed newBaseURI, string indexed oldBaseURI); + event Initialized(uint64 version); + event MaxLevelUpdated(uint256 oldLevel, uint256 newLevel); + event Paused(address account); + event PublicMintingPaused(bool isPaused); + event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole); + event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender); + event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender); + event Selected(address indexed owner, uint256 tokenId); + event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); + event Unpaused(address account); + event Upgraded(address indexed implementation); + event Upgraded(uint256 indexed tokenId, uint256 oldLevel, uint256 newLevel); + event XAllocationsGovernorAddressUpdated(address indexed newAddress, address indexed oldAddress); + event NodeDetached(uint256 indexed nodeTokenId, uint256 indexed tokenId); + event NodeAttached(uint256 indexed nodeTokenId, uint256 indexed tokenId); + + /// @notice Returns the role identifier for contracts address manager + function CONTRACTS_ADDRESS_MANAGER_ROLE() external view returns (bytes32); + + /// @notice Returns the role identifier for default admin + function DEFAULT_ADMIN_ROLE() external view returns (bytes32); + + /// @notice Returns the maximum level achievable for a token + function MAX_LEVEL() external view returns (uint256); + + /// @notice Returns the role identifier for minter + function MINTER_ROLE() external view returns (bytes32); + + /// @notice Returns the role identifier for nodes manager + function NODES_MANAGER_ROLE() external view returns (bytes32); + + /// @notice Returns the role identifier for pauser + function PAUSER_ROLE() external view returns (bytes32); + + /// @notice Returns the role identifier for upgrader + function UPGRADER_ROLE() external view returns (bytes32); + + /// @notice Returns the interface version string + function UPGRADE_INTERFACE_VERSION() external view returns (string memory); + + /// @notice Approves an address to transfer a specific token + /// @param to Address to be approved + /// @param tokenId ID of the token to be approved + function approve(address to, uint256 tokenId) external; + + /// @notice Attaches a node to a token + /// @param nodeTokenId ID of the node to attach + /// @param tokenId ID of the token to attach to + function attachNode(uint256 nodeTokenId, uint256 tokenId) external; + + /// @notice Returns the B3TR token contract address + function b3tr() external view returns (address); + + /// @notice Returns the B3TR governor contract address + function b3trGovernor() external view returns (address); + + /// @notice Returns the number of tokens owned by an address + /// @param owner Address to query + /// @return Number of tokens owned + function balanceOf(address owner) external view returns (uint256); + + /// @notice Returns the base URI for token metadata + function baseURI() external view returns (string memory); + + /// @notice Burns a specific token + /// @param tokenId ID of the token to burn + function burn(uint256 tokenId) external; + + /// @notice Detaches a node from a token + /// @param nodeTokenId ID of the node to detach + /// @param tokenId ID of the token to detach from + function detachNode(uint256 nodeTokenId, uint256 tokenId) external; + + /// @notice Allows eligible addresses to mint a token for free + function freeMint() external; + + /// @notice Returns the approved address for a token + /// @param tokenId ID of the token to query + /// @return Address approved for the token + function getApproved(uint256 tokenId) external view returns (address); + + /// @notice Returns the amount of B3TR donated for a token + /// @param tokenId ID of the token to query + /// @return Amount of B3TR donated + function getB3TRdonated(uint256 tokenId) external view returns (uint256); + + /// @notice Returns the amount of B3TR needed to upgrade a token + /// @param tokenId ID of the token to query + /// @return Amount of B3TR needed + function getB3TRtoUpgrade(uint256 tokenId) external view returns (uint256); + + /// @notice Returns the amount of B3TR needed to upgrade to a specific level + /// @param level Target level + /// @return Amount of B3TR needed + function getB3TRtoUpgradeToLevel(uint256 level) external view returns (uint256); + + /// @notice Returns the token ID attached to a node + /// @param nodeId ID of the node to query + /// @return Token ID attached to the node + function getIdAttachedToNode(uint256 nodeId) external view returns (uint256); + + /// @notice Calculates the level after attaching a node + /// @param tokenId ID of the token + /// @param nodeTokenId ID of the node to attach + /// @return New level after attachment + function getLevelAfterAttachingNode(uint256 tokenId, uint256 nodeTokenId) external view returns (uint256); + + /// @notice Calculates the level after detaching a node + /// @param tokenId ID of the token + /// @return New level after detachment + function getLevelAfterDetachingNode(uint256 tokenId) external view returns (uint256); + + /// @notice Returns the node ID attached to a token + /// @param tokenId ID of the token to query + /// @return ID of the attached node + function getNodeIdAttached(uint256 tokenId) external view returns (uint256); + + /// @notice Returns the level of a node + /// @param nodeId ID of the node to query + /// @return Level of the node + function getNodeLevelOf(uint256 nodeId) external view returns (uint8); + + /// @notice Returns the free level granted by a node level + /// @param nodeLevel Level of the node + /// @return Free level granted + function getNodeToFreeLevel(uint8 nodeLevel) external view returns (uint256); + + /// @notice Returns the admin role for a given role + /// @param role Role to query + /// @return Admin role + function getRoleAdmin(bytes32 role) external view returns (bytes32); + + /// @notice Returns the selected token ID for an owner + /// @param owner Address to query + /// @return Selected token ID + function getSelectedTokenId(address owner) external view returns (uint256); + + /// @notice Grants a role to an account + /// @param role Role to grant + /// @param account Account to receive the role + function grantRole(bytes32 role, address account) external; + + /// @notice Checks if an account has a role + /// @param role Role to check + /// @param account Account to check + /// @return True if account has the role + function hasRole(bytes32 role, address account) external view returns (bool); + + /// @notice Initializes the contract V2 + /// @param _vechainNodes Address of the VeChain nodes contract + /// @param _nodesAdmin Address of the nodes admin + /// @param _nodeFreeLevels Array of free levels for nodes + function initializeV2(address _vechainNodes, address _nodesAdmin, uint256[] memory _nodeFreeLevels) external; + + /// @notice Checks if an operator is approved for all tokens of an owner + /// @param owner Owner address + /// @param operator Operator address + /// @return True if operator is approved for all + function isApprovedForAll(address owner, address operator) external view returns (bool); + + /// @notice Returns the level of a token + /// @param tokenId ID of the token to query + /// @return Level of the token + function levelOf(uint256 tokenId) external view returns (uint256); + + /// @notice Returns the name of the token collection + function name() external view returns (string memory); + + /// @notice Returns the owner of a token + /// @param tokenId ID of the token to query + /// @return Address of the owner + function ownerOf(uint256 tokenId) external view returns (address); + + /// @notice Checks if a user has participated in governance + /// @param user Address to check + /// @return True if user has participated + function participatedInGovernance(address user) external view returns (bool); + + /// @notice Pauses all token transfers + function pause() external; + + /// @notice Returns the paused status of the contract + /// @return True if contract is paused + function paused() external view returns (bool); + + /// @notice Returns the storage slot for the implementation + function proxiableUUID() external view returns (bytes32); + + /// @notice Allows an account to renounce a role + /// @param role Role to renounce + /// @param callerConfirmation Address of the caller for confirmation + function renounceRole(bytes32 role, address callerConfirmation) external; + + /// @notice Revokes a role from an account + /// @param role Role to revoke + /// @param account Account to revoke from + function revokeRole(bytes32 role, address account) external; + + /// @notice Safely transfers a token + /// @param from Current owner + /// @param to New owner + /// @param tokenId ID of the token to transfer + function safeTransferFrom(address from, address to, uint256 tokenId) external; + + /// @notice Safely transfers a token with additional data + /// @param from Current owner + /// @param to New owner + /// @param tokenId ID of the token to transfer + /// @param data Additional data + function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) external; + + /// @notice Selects a token for an owner + /// @param tokenID ID of the token to select + function select(uint256 tokenID) external; + + /// @notice Sets approval for all tokens + /// @param operator Address to approve + /// @param approved Approval status + function setApprovalForAll(address operator, bool approved) external; + + /// @notice Sets the B3TR amounts needed for level upgrades + /// @param b3trToUpgradeToLevel Array of B3TR amounts + function setB3TRtoUpgradeToLevel(uint256[] memory b3trToUpgradeToLevel) external; + + /// @notice Sets the B3TR governor address + /// @param _b3trGovernor New governor address + function setB3trGovernorAddress(address _b3trGovernor) external; + + /// @notice Sets the base URI for token metadata + /// @param baseTokenURI New base URI + function setBaseURI(string memory baseTokenURI) external; + + /// @notice Sets the public minting pause status + /// @param isPaused New pause status + function setIsPublicMintingPaused(bool isPaused) external; + + /// @notice Sets the maximum level + /// @param level New maximum level + function setMaxLevel(uint256 level) external; + + /// @notice Sets the free upgrade level for a node level + /// @param nodeLevel Level of the node + /// @param level Free upgrade level + function setNodeToFreeUpgradeLevel(uint8 nodeLevel, uint256 level) external; + + /// @notice Sets the VeChain nodes contract address + /// @param _vechainNodes New nodes contract address + function setVechainNodes(address _vechainNodes) external; + + /// @notice Sets the X-Allocations governor address + /// @param _xAllocationsGovernor New governor address + function setXAllocationsGovernorAddress(address _xAllocationsGovernor) external; + + /// @notice Checks if the contract supports an interface + /// @param interfaceId Interface identifier + /// @return True if interface is supported + function supportsInterface(bytes4 interfaceId) external view returns (bool); + + /// @notice Returns the symbol of the token collection + function symbol() external view returns (string memory); + + /// @notice Returns a token by its index + /// @param index Index of the token + /// @return Token ID at the index + function tokenByIndex(uint256 index) external view returns (uint256); + + /// @notice Returns a token owned by an address by index + /// @param owner Owner address + /// @param index Index of the token + /// @return Token ID at the index + function tokenOfOwnerByIndex(address owner, uint256 index) external view returns (uint256); + + /// @notice Returns the URI for a token's metadata + /// @param tokenId ID of the token + /// @return Metadata URI + function tokenURI(uint256 tokenId) external view returns (string memory); + + /// @notice Returns the total supply of tokens + /// @return Total number of tokens + function totalSupply() external view returns (uint256); + + /// @notice Transfers a token + /// @param from Current owner + /// @param to New owner + /// @param tokenId ID of the token to transfer + function transferFrom(address from, address to, uint256 tokenId) external; + + /// @notice Returns the treasury address + function treasury() external view returns (address); + + /// @notice Unpauses all token transfers + function unpause() external; + + /// @notice Upgrades a token's level + /// @param tokenId ID of the token to upgrade + function upgrade(uint256 tokenId) external; + + /// @notice Upgrades the contract implementation + /// @param newImplementation Address of the new implementation + /// @param data Additional data for the upgrade + function upgradeToAndCall(address newImplementation, bytes memory data) external payable; + + /// @notice Returns the contract version + /// @return Version string + function version() external pure returns (string memory); + + /// @notice Returns the X-Allocations governor address + function xAllocationsGovernor() external view returns (address); +} diff --git a/contracts/deprecated/V2/interfaces/IVeBetterPassportV2.sol b/contracts/deprecated/V2/interfaces/IVeBetterPassportV2.sol new file mode 100644 index 0000000..60fc5a2 --- /dev/null +++ b/contracts/deprecated/V2/interfaces/IVeBetterPassportV2.sol @@ -0,0 +1,524 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.20; + +import { PassportTypesV2 } from "../ve-better-passport/libraries/PassportTypesV2.sol"; +import { IX2EarnApps } from "../../../interfaces/IX2EarnApps.sol"; +import { IXAllocationVotingGovernor } from "../../../interfaces/IXAllocationVotingGovernor.sol"; + +interface IVeBetterPassportV2 { + // ---------- Events ---------- // + + /// @notice Emitted when a specific check is toggled. + /// @param checkName The name of the check being toggled. + /// @param enabled True if the check is enabled, false if disabled. + event CheckToggled(string indexed checkName, bool enabled); + + /// @notice Emitted when the minimum galaxy member level is set. + /// @param minimumGalaxyMemberLevel The new minimum galaxy member level. + event MinimumGalaxyMemberLevelSet(uint256 minimumGalaxyMemberLevel); + + /// @notice Emitted when a user delegates personhood to another user. + event LinkCreated(address indexed entity, address indexed passport); + + /// @notice Emitted when a user revokes the delegation of personhood to another user. + event LinkRemoved(address indexed entity, address indexed passport); + + /// @notice Emitted when a user delegates personhood to another user pending acceptance. + event LinkPending(address indexed entity, address indexed passport); + + /// @notice Emitted when a user registers an action + /// @param user - the user that registered the action + /// @param passport - the passport address of the user + /// @param appId - the app id of the action + /// @param round - the round of the action + /// @param actionScore - the score of the action + event RegisteredAction( + address indexed user, + address passport, + bytes32 indexed appId, + uint256 indexed round, + uint256 actionScore + ); + + /// @notice Emitted when a user is signaled. + /// @param user The address of the user that was signaled. + /// @param signaler The address of the user that signaled the user. + /// @param app The app that the user was signaled for. + /// @param reason The reason for signaling the user. + event UserSignaled(address indexed user, address indexed signaler, bytes32 indexed app, string reason); + + /// @notice Emited when an address is associated with an app. + /// @param signaler The address of the signaler. + /// @param app The app that the signaler was associated with. + event SignalerAssignedToApp(address indexed signaler, bytes32 indexed app); + + /// @notice Emitted when an address is removed from an app. + /// @param signaler The address of the signaler. + /// @param app The app that the signaler was removed from. + event SignalerRemovedFromApp(address indexed signaler, bytes32 indexed app); + + /// @notice Emitted when a user's signals are reset. + /// @param user The address of the user that had their signals reset. + /// @param reason The reason for resetting the signals. + event UserSignalsReset(address indexed user, string reason); + + /// @notice Emitted when a user is whitelisted + /// @param user - the user that is whitelisted + /// @param whitelistedBy - the user that whitelisted the user + event UserWhitelisted(address indexed user, address indexed whitelistedBy); + + /// @notice Emitted when a user is removed from the whitelist + /// @param user - the user that is removed from the whitelist + /// @param removedBy - the user that removed the user from the whitelist + event RemovedUserFromWhitelist(address indexed user, address indexed passport, address indexed removedBy); + + /// @notice Emitted when a user is blacklisted + /// @param user - the user that is blacklisted + /// @param blacklistedBy - the user that blacklisted the user + event UserBlacklisted(address indexed user, address indexed blacklistedBy); + + /// @notice Emitted when a user is removed from the blacklist + /// @param user - the user that is removed from the blacklist + /// @param removedBy - the user that removed the user from the blacklist + event RemovedUserFromBlacklist(address indexed user, address indexed removedBy); + + /// @notice Emitted when a user's signals are reset for an app. + /// @param user The address of the user that had their signals reset. + /// @param app The app that the user had their signals reset for. + /// @param reason - The reason for resetting the signals. + event UserSignalsResetForApp(address indexed user, bytes32 indexed app, string reason); + + /// @notice Emitted when a user delegates passport to another user. + event DelegationCreated(address indexed delegator, address indexed delegatee); + + /// @notice Emitted when a user delegates passport to another user pending acceptance. + event DelegationPending(address indexed delegator, address indexed delegatee); + + /// @notice Emitted when a user revokes the delegation of passport to another user. + event DelegationRevoked(address indexed delegator, address indexed delegatee); + + /// @notice Emitted when an an entity is linked to a passport + error AlreadyLinked(address entity); + + // ---------- Errors ---------- // + /// @notice Emitted when a user does not have permission to delegate personhood. + error UnauthorizedUser(address user); + + /// @notice Emitted when a user tries to delegate personhood to a user that has already been delegated to. + error AlreadyDelegated(address entity); + + /// @notice Emitted when a user tries to delegate personhood to themselves. + error CannotLinkToSelf(address user); + + /// @notice Emitted when a user tries to delegate personhood to more than one user. + error OnlyOneLinkAllowed(); + + /// @notice Emitted when a user tries to call a function that they are not authorized to call. + error VeBetterPassportUnauthorizedUser(address user); + + /// @notice Emitted when a user does not have permission to delegate passport. + error PassportDelegationUnauthorizedUser(address user); + + /// @notice Emitted when a user tries to delegate passport to themselves. + error CannotDelegateToSelf(address user); + + /// @notice Emitted when a user tries to revoke a delegation that does not exist. + error NotDelegated(address user); + + /// @notice Emitted when a user tries to delegate passport to more than one user. + error OnlyOneUserAllowed(); + + /// @notice Emiited when a user tries to delegate a passport to another passport or entity. + error PassportDelegationFromEntity(); + + /// @notice Emitted when a user tries to delegate a passport to another entity. + error PassportDelegationToEntity(); + + /// @notice Emitted when a user tries to sign a message with an expired signature + error SignatureExpired(); + + /// @notice Emitted when a user tries to sign a message with an invalid signature + error InvalidSignature(); + + /// @notice Thrown when a user tries to link a entity to a passport that has reached the maximum number of entities. + error MaxEntitiesPerPassportReached(); + + /// @notice Thrown when a user tries to link a entity to a passport that is already linked to another entity. + error NotLinked(address user); + + /// @notice Thrown when a user tries to link a entity to a passport that is already delegated. + error DelegatedEntity(address entity); + + // ---------- Functions ---------- // + /// @notice Initializes the contract with the required data and roles + /// @param data The initialization data for the contract + /// @param roles The roles data for initialization + function initialize( + PassportTypesV2.InitializationData calldata data, + PassportTypesV2.InitializationRoleData calldata roles + ) external; + + /// @notice Checks if a user is a person based on the participation score and other criteria + /// @param user The address of the user to check + /// @return person True if the user is a valid person + /// @return reason Reason why the user is not a person + function isPerson(address user) external view returns (bool person, string memory reason); + + /// @notice Checks if a user is a person + /// @dev Checks if a wallet is a person or not at a specific timepoint based on the participation score, blacklisting, and GM holdings + /// @param user - the user address + /// @param timepoint - the timepoint to query + /// @return person - true if the user is a person + /// @return reason - the reason why the user is not a person + function isPersonAtTimepoint( + address user, + uint48 timepoint + ) external view returns (bool person, string memory reason); + + /// @notice Checks if a user is whitelisted + /// @param _user The user to check + /// @return True if the user is whitelisted + function isWhitelisted(address _user) external view returns (bool); + + /// @notice Checks if a user is blacklisted + /// @param _user The user to check + /// @return True if the user is blacklisted + function isBlacklisted(address _user) external view returns (bool); + + /// @notice Toggles the specified check + function toggleCheck(PassportTypesV2.CheckType check) external; + + /// @notice Returns the passport address for a entity + /// @param entity The entity's address + /// @return The address of the passport + function getPassportForEntity(address entity) external view returns (address); + + /// @notice Returns the pending links for a user (both incoming and outgoing) + /// @param user The address of the user + /// @return incoming The addresss of users that want to link to the user. + /// @return outgoing The address that the user wants to link to. + function getPendingLinkings(address user) external view returns (address[] memory incoming, address outgoing); + + /// @notice Returns the passport address for a entity at a specific timepoint + /// @param entity The entity's address + /// @param timepoint The timepoint to query + function getPassportForEntityAtTimepoint(address entity, uint256 timepoint) external view returns (address); + + /// @notice Returns the entity address for a passport + /// @param passport The passport's address + /// @return The address of the entity + function getEntitiesLinkedToPassport(address passport) external view returns (address[] memory); + + /// @notice Returns if a user is a entity + /// @param user The user address + function isEntity(address user) external view returns (bool); + + /// @notice Returns if a user is a entity at a specific timepoint + /// @param user The user address + /// @param timepoint The timepoint to query + function isEntityInTimepoint(address user, uint256 timepoint) external view returns (bool); + + /// @notice Returns if a user is a passport + /// @param user The user address + function isPassport(address user) external view returns (bool); + + /// @notice Returns if a user is a passport at a specific timepoint + /// @param user The user address + /// @param timepoint The timepoint to query + function isPassportInTimepoint(address user, uint256 timepoint) external view returns (bool); + + /// @notice Gets the cumulative score of a user based on exponential decay for a number of last rounds + /// @param user The user address + /// @param lastRound The round to consider as a starting point for the cumulative score + /// @return The cumulative score of the user + function getCumulativeScoreWithDecay(address user, uint256 lastRound) external view returns (uint256); + + /// @notice Gets the round score of a user + /// @param user The user address + /// @param round The round to check + /// @return The round score of the user + function userRoundScore(address user, uint256 round) external view returns (uint256); + + /// @notice Gets the total score of a user + /// @param user The user address + /// @return The total score of the user + function userTotalScore(address user) external view returns (uint256); + + /// @notice Gets the score of a user for an app in a specific round + /// @param user The user address + /// @param round The round to check + /// @param appId The app ID + /// @return The score of the user for the app in the round + function userRoundScoreApp(address user, uint256 round, bytes32 appId) external view returns (uint256); + + /// @notice Gets the total score of a user for an app + /// @param user The user address + /// @param appId The app ID + /// @return The total score of the user for the app + function userAppTotalScore(address user, bytes32 appId) external view returns (uint256); + + /// @notice Gets the threshold score for a user to be considered a person + /// @return The threshold participation score + function thresholdPoPScore() external view returns (uint256); + + /// @notice Gets the threshold score for a user to be considered a person at a specific timepoint + function thresholdPoPScoreAtTimepoint(uint48 timepoint) external view returns (uint256); + + /// @notice Gets the number of rounds to be considered for the cumulative score + /// @return The number of rounds + function roundsForCumulativeScore() external view returns (uint256); + + /// @notice Gets the security multiplier for an app security + /// @param security The app security level (LOW, MEDIUM, HIGH) + /// @return The security multiplier for the app + function securityMultiplier(PassportTypesV2.APP_SECURITY security) external view returns (uint256); + + /// @notice Gets the security level of an app + /// @param appId The app ID + /// @return The security level of the app + function appSecurity(bytes32 appId) external view returns (PassportTypesV2.APP_SECURITY); + + /// @notice Gets the minimum galaxy member level required + /// @return The minimum galaxy member level + function getMinimumGalaxyMemberLevel() external view returns (uint256); + + /// @notice Returns if the specific check is enabled + function isCheckEnabled(PassportTypesV2.CheckType check) external view returns (bool); + + /// @notice Returns the signaling threshold + /// @return The signaling threshold + function signalingThreshold() external view returns (uint256); + + /// @notice Gets the total number of signals for an app + /// @param app The app ID + /// @return The total number of signals for the app + function appTotalSignalsCounter(bytes32 app) external view returns (uint256); + + /// @notice Returns the domain for EIP-712 signature + function eip712Domain() + external + view + returns ( + bytes1 fields, + string memory name, + string memory signatureVersion, + uint256 chainId, + address verifyingContract, + bytes32 salt, + uint256[] memory extensions + ); + + /// @notice Grants a role to a specific account + /// @param role The role to grant + /// @param account The account to grant the role to + function grantRole(bytes32 role, address account) external; + + /// @notice Revokes a role from a specific account + /// @param role The role to revoke + /// @param account The account to revoke the role from + function revokeRole(bytes32 role, address account) external; + + /// @notice Signals a user + /// @param _user The user to signal + function signalUser(address _user) external; + + /// @notice Signals a user with a reason + /// @param _user The user to signal + /// @param reason The reason for the signal + function signalUserWithReason(address _user, string memory reason) external; + + /// @notice Assigns a signaler to an app + /// @param app The app ID + /// @param user The signaler address + function assignSignalerToApp(bytes32 app, address user) external; + + /// @notice Removes a signaler from an app + /// @param user The signaler address + function removeSignalerFromApp(address user) external; + + /// @notice Resets the signals of a user with a given reason + /// @param user The user address + /// @param reason The reason for resetting the signals + function resetUserSignalsWithReason(address user, string memory reason) external; + + /// @notice Gets the version of the contract + /// @return The version of the contract as a string + function version() external pure returns (string memory); + + /// @notice Returns the current block number + /// @return The current block number + function clock() external view returns (uint48); + + /// @notice Returns the clock mode for the contract + /// @return The clock mode as a string + function CLOCK_MODE() external pure returns (string memory); + + /// @notice Sets the signaling threshold + /// @param threshold The new signaling threshold + function setSignalingThreshold(uint256 threshold) external; + + /// @notice Sets the security multiplier for an app security level + /// @param security The app security level + /// @param multiplier The security multiplier + function setSecurityMultiplier(PassportTypesV2.APP_SECURITY security, uint256 multiplier) external; + + /// @notice Sets the app security level for a specific app + /// @param appId The app ID + /// @param security The security level + function setAppSecurity(bytes32 appId, PassportTypesV2.APP_SECURITY security) external; + + /// @notice Sets the threshold score for a user to be considered a person + /// @param threshold The threshold score + function setThresholdPoPScore(uint208 threshold) external; + + /// @notice Sets the number of rounds to consider for cumulative score calculation + /// @param rounds The number of rounds + function setRoundsForCumulativeScore(uint256 rounds) external; + + /// @notice Sets the decay rate for exponential decay scoring + /// @param decayRate The decay rate + function setDecayRate(uint256 decayRate) external; + + /// @notice Sets the X2EarnApps contract address + /// @param _x2EarnApps The X2EarnApps contract address + function setX2EarnApps(IX2EarnApps _x2EarnApps) external; + + /// @notice Sets the xAllocationVoting contract address + /// @param xAllocationVoting The xAllocationVoting contract address + function setXAllocationVoting(IXAllocationVotingGovernor xAllocationVoting) external; + + /// @notice Link an account (which will become an entity) to a passport (an address that is not an enitity) + /// After linking, the scores of the enitity will be stored to the linked account (passport) + /// Balance is not transferred and the entity will not be able to vote after linking. + /// @param entity The entity's address + /// @param deadline The deadline for the signature + /// @param signature The signature of the delegation + function linkEntityToPassportWithSignature(address entity, uint256 deadline, bytes memory signature) external; + + /// @notice Link an account (which will become an entity) to a passport (an address that is not an enitity) + /// After linking, the scores of the enitity will be stored to the linked account (passport) + /// Balance is not transferred and the entity will not be able to vote after linking. + /// @dev The passport must accept the delegation + function linkEntityToPassport(address passport) external; + + /// @notice Allow the passport to accept the delegation + /// @param entity - the entity address + function acceptEntityLink(address entity) external; + + /// @notice Deny an incoming pending entity link to the sender's passport. + /// @param entity - the entity address + function denyIncomingPendingEntityLink(address entity) external; + + /// @notice Cancel an outgoing pending entity link from the sender. + function cancelOutgoingPendingEntityLink() external; + + /// @notice Remove the linked enitity from the passport + /// @param entity - the entity address + function removeEntityLink(address entity) external; + + /// @notice Registers an action for a user + /// @param user - the user that performed the action + /// @param appId - the app id of the action + function registerAction(address user, bytes32 appId) external; + + /// @notice Registers an action for a user in a round + /// @param user - the user that performed the action + /// @param appId - the app id of the action + /// @param round - the round id of the action + function registerActionForRound(address user, bytes32 appId, uint256 round) external; + + /// @notice Function used to seed the passport with old actions by aggregating them + /// based on (user, appId, round) and summing up the total score offchain + /// @param user - the user that performed the actions + /// @param appId - the app id of the actions + /// @param round - the round id of the actions + /// @param totalScore - the total score of the actions + function registerAggregatedActionsForRound(address user, bytes32 appId, uint256 round, uint256 totalScore) external; + + /// @notice Gets the threshold percentage of blacklisted entities for a passport to be considered blacklisted + function blacklistThreshold() external view returns (uint256); + + // @notice Gets the threshold percentage of whitelisted entities for a passport to be considered whitelisted + function whitelistThreshold() external view returns (uint256); + + /// @notice Returns the maximum number of entities per passport + function maxEntitiesPerPassport() external view returns (uint256); + + /// @notice Gets the decay rate for the cumulative score + function decayRate() external view returns (uint256); + + /// @notice Gets the minimum galaxy member level to be considered a person + function minimumGalaxyMemberLevel() external view returns (uint256); + + /// @notice Sets the threshold percentage of blacklisted entities for a passport to be considered blacklisted + function setBlacklistThreshold(uint256 _threshold) external; + + /// @notice Sets the threshold percentage of whitelisted entities for a passport to be considered whitelisted + function setWhitelistThreshold(uint256 _threshold) external; + + /// @notice Sets the maximum number of entities that can be linked to a passport + /// @param maxEntities - the maximum number of entities + function setMaxEntitiesPerPassport(uint256 maxEntities) external; + + /// @notice Delegate the personhood to another address + /// @param delegatee - the delegatee address + function delegatePassport(address delegatee) external; + + /// @notice Allow the delegatee to accept the delegation + /// @param delegator - the delegator address + function acceptDelegation(address delegator) external; + + /// @notice Revoke the delegation (can be done by the delegator or the delegatee) + function revokeDelegation() external; + + /// @notice Allows a delegator to deny (and remove) an incoming pending delegation. + /// @param delegator - the user who is delegating to me (aka the delegator) + function denyIncomingPendingDelegation(address delegator) external; + + /// @notice Allows a delegator to cancel (and remove) the outgoing pending delegation. + function cancelOutgoingPendingDelegation() external; + + /// @notice Returns the delegatee address for a delegator + /// @param delegator The delegator's address + /// @return The address of the delegatee + function getDelegatee(address delegator) external view returns (address); + + /// @notice Returns the incoming and outgoing pending delegations for a user + /// @param user - the user address + /// @return incoming The address[] memory of users that are delegating to the user. + /// @return outgoing The address that the user is delegating to. + function getPendingDelegations(address user) external view returns (address[] memory incoming, address outgoing); + + /// @notice Returns the delegatee address for a delegator at a specific timepoint + /// @param delegator The delegator's address + /// @param timepoint The timepoint to query + function getDelegateeInTimepoint(address delegator, uint256 timepoint) external view returns (address); + + /// @notice Returns the delegator address for a delegatee + /// @param delegatee The delegatee's address + /// @return The address of the delegator + function getDelegator(address delegatee) external view returns (address); + + /// @notice Returns the delegator address for a delegatee at a specific timepoint + /// @param delegatee The delegatee's address + /// @param timepoint The timepoint to query + function getDelegatorInTimepoint(address delegatee, uint256 timepoint) external view returns (address); + + /// @notice Returns if a user is a delegator + /// @param user The user address + function isDelegator(address user) external view returns (bool); + + /// @notice Returns if a user is a delegator at a specific timepoint + /// @param user The user address + /// @param timepoint The timepoint to query + function isDelegatorInTimepoint(address user, uint256 timepoint) external view returns (bool); + + /// @notice Returns if a user is a delegatee + /// @param user The user address + function isDelegatee(address user) external view returns (bool); + + /// @notice Returns if a user is a delegatee at a specific timepoint + /// @param user The user address + /// @param timepoint The timepoint to query + function isDelegateeInTimepoint(address user, uint256 timepoint) external view returns (bool); +} diff --git a/contracts/deprecated/V2/ve-better-passport/VeBetterPassportV2.sol b/contracts/deprecated/V2/ve-better-passport/VeBetterPassportV2.sol new file mode 100644 index 0000000..95ce31a --- /dev/null +++ b/contracts/deprecated/V2/ve-better-passport/VeBetterPassportV2.sol @@ -0,0 +1,815 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.20; + +import { PassportTypesV2 } from "./libraries/PassportTypesV2.sol"; +import { PassportStorageTypesV2 } from "./libraries/PassportStorageTypesV2.sol"; +import { PassportChecksLogicV2 } from "./libraries/PassportChecksLogicV2.sol"; +import { PassportWhitelistAndBlacklistLogicV2 } from "./libraries/PassportWhitelistAndBlacklistLogicV2.sol"; +import { PassportPoPScoreLogicV2 } from "./libraries/PassportPoPScoreLogicV2.sol"; +import { PassportEntityLogicV2 } from "./libraries/PassportEntityLogicV2.sol"; +import { PassportClockLogicV2 } from "./libraries/PassportClockLogicV2.sol"; +import { PassportDelegationLogicV2 } from "./libraries/PassportDelegationLogicV2.sol"; +import { PassportSignalingLogicV2 } from "./libraries/PassportSignalingLogicV2.sol"; +import { PassportPersonhoodLogicV2 } from "./libraries/PassportPersonhoodLogicV2.sol"; +import { PassportEIP712SigningLogicV2 } from "./libraries/PassportEIP712SigningLogicV2.sol"; +import { PassportConfiguratorV2 } from "./libraries/PassportConfiguratorV2.sol"; +import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import { AccessControlUpgradeable } from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import { IVeBetterPassportV2 } from "../interfaces/IVeBetterPassportV2.sol"; +import { IXAllocationVotingGovernor } from "../../../interfaces/IXAllocationVotingGovernor.sol"; +import { IGalaxyMemberV2 } from "../../V2/interfaces/IGalaxyMemberV2.sol"; +import { IX2EarnApps } from "../../../interfaces/IX2EarnApps.sol"; + +/// @title VeBetterPassportV2 +/// @notice Contract to manage the VeBetterPassport, a system to determine if a wallet is a person or not +/// based on the participation score, blacklisting, GM holdings and much more that can be added in the future. +contract VeBetterPassportV2 is AccessControlUpgradeable, UUPSUpgradeable, IVeBetterPassportV2 { + bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE"); + bytes32 public constant ROLE_GRANTER = keccak256("ROLE_GRANTER"); + bytes32 public constant SETTINGS_MANAGER_ROLE = keccak256("SETTINGS_MANAGER_ROLE"); + bytes32 public constant WHITELISTER_ROLE = keccak256("WHITELISTER_ROLE"); + bytes32 public constant ACTION_REGISTRAR_ROLE = keccak256("ACTION_REGISTRAR_ROLE"); + bytes32 public constant ACTION_SCORE_MANAGER_ROLE = keccak256("ACTION_SCORE_MANAGER_ROLE"); + bytes32 public constant SIGNALER_ROLE = keccak256("SIGNALER_ROLE"); + + // keccak256(abi.encode(uint256(keccak256("PassportStorageLocation")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant PassportStorageLocation = 0x273c9387b78d9b22e6f3371bb3aa3a918f53507e8cacc54e4831933cbb844100; + + /// @dev Internal function to access the passport storage slot. + function getPassportStorage() internal pure returns (PassportStorageTypesV2.PassportStorage storage $) { + assembly { + $.slot := PassportStorageLocation + } + } + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + /// @notice Initializes the contract + function initialize( + PassportTypesV2.InitializationData memory data, + PassportTypesV2.InitializationRoleData memory roles + ) external initializer { + __UUPSUpgradeable_init(); + __AccessControl_init(); + + PassportConfiguratorV2.initializePassportStorage(getPassportStorage(), data); + + // Grant roles + _grantRole(DEFAULT_ADMIN_ROLE, roles.admin); + _grantRole(UPGRADER_ROLE, roles.upgrader); + _grantRole(SIGNALER_ROLE, roles.botSignaler); + _grantRole(ROLE_GRANTER, roles.roleGranter); + _grantRole(SETTINGS_MANAGER_ROLE, roles.settingsManager); + _grantRole(WHITELISTER_ROLE, roles.whitelister); + _grantRole(ACTION_REGISTRAR_ROLE, roles.actionRegistrar); + _grantRole(ACTION_SCORE_MANAGER_ROLE, roles.actionScoreManager); + } + + // ---------- Modifiers ------------ // + + /// @notice Modifier to check if the user has the required role or is the DEFAULT_ADMIN_ROLE + /// @param role - the role to check + modifier onlyRoleOrAdmin(bytes32 role) { + if (!hasRole(role, msg.sender) && !hasRole(DEFAULT_ADMIN_ROLE, msg.sender)) { + revert VeBetterPassportUnauthorizedUser(msg.sender); + } + _; + } + + // ---------- Authorizers ---------- // + + /// @notice Authorizes the upgrade of the contract + /// @param newImplementation - the new implementation address + function _authorizeUpgrade(address newImplementation) internal virtual override onlyRole(UPGRADER_ROLE) {} + + // ---------- Getters ---------- // + + /// @notice Checks if a user is a person + /// @dev Checks if a wallet is a person or not based on the participation score, blacklisting, and GM holdings + /// @param user - the user address + /// @return person - true if the user is a person + /// @return reason - the reason why the user is not a person + function isPerson(address user) external view returns (bool person, string memory reason) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + return PassportPersonhoodLogicV2.isPerson($, user); + } + + /// @notice Checks if a user is a person + /// @dev Checks if a wallet is a person or not at a specific timepoint based on the participation score, blacklisting, and GM holdings + /// @param user - the user address + /// @param timepoint - the timepoint to query + /// @return person - true if the user is a person + /// @return reason - the reason why the user is not a person + function isPersonAtTimepoint( + address user, + uint48 timepoint + ) external view returns (bool person, string memory reason) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + return PassportPersonhoodLogicV2.isPersonAtTimepoint($, user, timepoint); + } + + /// @notice Returns if the specific check is enabled + function isCheckEnabled(PassportTypesV2.CheckType check) external view returns (bool) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + return PassportChecksLogicV2.isCheckEnabled($, check); + } + + /// @notice Returns the minimum galaxy member level + function getMinimumGalaxyMemberLevel() external view returns (uint256) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + return PassportChecksLogicV2.getMinimumGalaxyMemberLevel($); + } + + /// @notice Returns if a user is whitelisted + function isWhitelisted(address _user) external view returns (bool) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + return PassportWhitelistAndBlacklistLogicV2.isWhitelisted($, _user); + } + + /// @notice Returns if a user is blacklisted + function isBlacklisted(address _user) external view returns (bool) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + return PassportWhitelistAndBlacklistLogicV2.isBlacklisted($, _user); + } + + /// @notice Checks if a passport is whitelisted. + /// @dev If passport is an entity, it will check the passport of the entity. + /// @param passport The address of the passport to check. + /// @return True if the passport is whitelisted, false otherwise. + function isPassportWhitelisted(address passport) external view returns (bool) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + return PassportWhitelistAndBlacklistLogicV2.isPassportWhitelisted($, passport); + } + + /// @notice Checks if a passport is blacklisted. + /// @dev If passport is an entity, it will check the passport of the entity. + /// @param passport The address of the passport to check. + /// @return True if the passport is blacklisted, false otherwise. + function isPassportBlacklisted(address passport) external view returns (bool) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + return PassportWhitelistAndBlacklistLogicV2.isPassportBlacklisted($, passport); + } + + /// @notice Gets the threshold percentage of blacklisted entities for a passport to be considered blacklisted + function blacklistThreshold() external view returns (uint256) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + return PassportWhitelistAndBlacklistLogicV2.blacklistThreshold($); + } + + /// @notice Gets the threshold percentage of whitelisted entities for a passport to be considered whitelisted + function whitelistThreshold() external view returns (uint256) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + return PassportWhitelistAndBlacklistLogicV2.whitelistThreshold($); + } + + /// @notice Gets the cumulative score of a user based on exponential decay for a number of last rounds + /// @dev This function calculates the decayed score f(t) = a * (1 - r)^t + /// @param user - the user address + /// @param lastRound - the round to consider as a starting point for the cumulative score + function getCumulativeScoreWithDecay(address user, uint256 lastRound) external view returns (uint256) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + return PassportPoPScoreLogicV2.getCumulativeScoreWithDecay($, user, lastRound); + } + + /// @notice Gets the round score of a user + /// @param user - the user address + /// @param round - the round + function userRoundScore(address user, uint256 round) external view returns (uint256) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + return PassportPoPScoreLogicV2.userRoundScore($, user, round); + } + + /// @notice Gets the total score of a user + /// @param user - the user address + function userTotalScore(address user) external view returns (uint256) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + return PassportPoPScoreLogicV2.userTotalScore($, user); + } + + /// @notice Gets the score of a user for an app in a round + /// @param user - the user address + /// @param round - the round + /// @param appId - the app id + function userRoundScoreApp(address user, uint256 round, bytes32 appId) external view returns (uint256) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + return PassportPoPScoreLogicV2.userRoundScoreApp($, user, round, appId); + } + + /// @notice Gets the total score of a user for an app + /// @param user - the user address + /// @param appId - the app id + function userAppTotalScore(address user, bytes32 appId) external view returns (uint256) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + return PassportPoPScoreLogicV2.userAppTotalScore($, user, appId); + } + + /// @notice Gets the threshold for a user to be considered a person + function thresholdPoPScore() external view returns (uint256) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + return PassportPoPScoreLogicV2.thresholdPoPScore($); + } + + /// @notice Gets the threshold for a user to be considered a person at a specific timepoint (block number) + function thresholdPoPScoreAtTimepoint(uint48 timepoint) external view returns (uint256) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + return PassportPoPScoreLogicV2.thresholdPoPScoreAtTimepoint($, timepoint); + } + + /// @notice Gets the security multiplier for an app security + /// @param security - the app security between LOW, MEDIUM, HIGH + function securityMultiplier(PassportTypesV2.APP_SECURITY security) external view returns (uint256) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + return PassportPoPScoreLogicV2.securityMultiplier($, security); + } + + /// @notice Gets the security level of an app + /// @param appId - the app id + function appSecurity(bytes32 appId) external view returns (PassportTypesV2.APP_SECURITY) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + return PassportPoPScoreLogicV2.appSecurity($, appId); + } + + /// @notice Gets the round threshold for a user to be considered a person + function roundsForCumulativeScore() external view returns (uint256) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + return PassportPoPScoreLogicV2.roundsForCumulativeScore($); + } + + /// @notice Gets the decay rate for the cumulative score + function decayRate() external view returns (uint256) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + return PassportPoPScoreLogicV2.decayRate($); + } + + /// @notice Gets the minimum galaxy member level to be considered a person + function minimumGalaxyMemberLevel() external view returns (uint256) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + return $.minimumGalaxyMemberLevel; + } + + /// @notice Returns the maximum number of entities per passport + function maxEntitiesPerPassport() external view returns (uint256) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + return PassportEntityLogicV2.getMaxEntitiesPerPassport($); + } + + /// @notice Returns the passport address for a entity + /// @param entity - the entity address + function getPassportForEntity(address entity) external view returns (address) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + return PassportEntityLogicV2.getPassportForEntity($, entity); + } + + /// @notice Returns the passport address for a entity at a specific timepoint + /// @param entity - the entity address + /// @param timepoint - the timepoint to query + function getPassportForEntityAtTimepoint(address entity, uint256 timepoint) external view returns (address) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + return PassportEntityLogicV2.getPassportForEntityAtTimepoint($, entity, timepoint); + } + + /// @notice Returns the entity address for a passport + /// @param passport - the passport address + function getEntitiesLinkedToPassport(address passport) external view returns (address[] memory) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + return PassportEntityLogicV2.getEntitiesLinkedToPassport($, passport); + } + + /// @notice Returns if a user is a entity + /// @param user - the user address + function isEntity(address user) external view returns (bool) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + return PassportEntityLogicV2.isEntity($, user); + } + + /// @notice Returns if a user is a entity at a specific timepoint + /// @param user - the user address + /// @param timepoint - the timepoint to query + function isEntityInTimepoint(address user, uint256 timepoint) external view returns (bool) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + return PassportEntityLogicV2.isEntityInTimepoint($, user, timepoint); + } + + /// @notice Returns if a user is a passport + /// @param user - the user address + function isPassport(address user) external view returns (bool) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + return PassportEntityLogicV2.isPassport($, user); + } + + /// @notice Returns if a user is a passport at a specific timepoint + /// @param user - the user address + /// @param timepoint - the timepoint to query + function isPassportInTimepoint(address user, uint256 timepoint) external view returns (bool) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + return PassportEntityLogicV2.isPassportInTimepoint($, user, timepoint); + } + + /// @notice Returns the pending links for a user (both incoming and outgoing) + /// @param user The address of the user + /// @return incoming The addresss of users that want to link to the user. + /// @return outgoing The address that the user wants to link to. + function getPendingLinkings(address user) external view returns (address[] memory incoming, address outgoing) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + return PassportEntityLogicV2.getPendingLinkings($, user); + } + + /// @notice Returns the delegatee address for a delegator + /// @param delegator - the delegator address + function getDelegatee(address delegator) external view returns (address) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + return PassportDelegationLogicV2.getDelegatee($, delegator); + } + + /// @notice Returns the delegatee address for a delegator at a specific timepoint + /// @param delegator - the delegator address + /// @param timepoint - the timepoint to query + function getDelegateeInTimepoint(address delegator, uint256 timepoint) external view returns (address) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + return PassportDelegationLogicV2.getDelegateeInTimepoint($, delegator, timepoint); + } + + /// @notice Returns the delegator address for a delegatee + /// @param delegatee - the delegatee address + function getDelegator(address delegatee) external view returns (address) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + return PassportDelegationLogicV2.getDelegator($, delegatee); + } + + /// @notice Returns the delegator address for a delegatee at a specific timepoint + /// @param delegatee - the delegatee address + /// @param timepoint - the timepoint to query + function getDelegatorInTimepoint(address delegatee, uint256 timepoint) external view returns (address) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + return PassportDelegationLogicV2.getDelegatorInTimepoint($, delegatee, timepoint); + } + + /// @notice Returns if a user is a delegator + /// @param user - the user address + function isDelegator(address user) external view returns (bool) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + return PassportDelegationLogicV2.isDelegator($, user); + } + + /// @notice Returns if a user is a delegator at a specific timepoint + /// @param user - the user address + /// @param timepoint - the timepoint to query + function isDelegatorInTimepoint(address user, uint256 timepoint) external view returns (bool) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + return PassportDelegationLogicV2.isDelegatorInTimepoint($, user, timepoint); + } + + /// @notice Returns if a user is a delegatee + /// @param user - the user address + function isDelegatee(address user) external view returns (bool) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + return PassportDelegationLogicV2.isDelegatee($, user); + } + + /// @notice Returns if a user is a delegatee at a specific timepoint + /// @param user - the user address + /// @param timepoint - the timepoint to query + function isDelegateeInTimepoint(address user, uint256 timepoint) external view returns (bool) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + return PassportDelegationLogicV2.isDelegateeInTimepoint($, user, timepoint); + } + + /// @notice Returns the pending incoming and outgoing delegations for a user + /// @param user - the user address + /// @return incoming The address[] memory of users that are delegating to the user. + /// @return outgoing The address that the user is delegating to. + function getPendingDelegations(address user) external view returns (address[] memory incoming, address outgoing) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + return PassportDelegationLogicV2.getPendingDelegations($, user); + } + + /// @notice Returns the number of times a user has been signaled + function signaledCounter(address _user) external view returns (uint256) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + return PassportSignalingLogicV2.signaledCounter($, _user); + } + + /// @notice Returns the belonging app of a signaler + function appOfSignaler(address _signaler) external view returns (bytes32) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + return PassportSignalingLogicV2.appOfSignaler($, _signaler); + } + + /// @notice Returns the number of times a user has been signaled by an app + function appSignalsCounter(bytes32 _app, address _user) external view returns (uint256) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + return PassportSignalingLogicV2.appSignalsCounter($, _app, _user); + } + + /// @notice Returns the total number of signals for an app + function appTotalSignalsCounter(bytes32 _app) external view returns (uint256) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + return PassportSignalingLogicV2.appTotalSignalsCounter($, _app); + } + + /// @notice Returns the signaling threshold + function signalingThreshold() external view returns (uint256) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + return PassportSignalingLogicV2.signalingThreshold($); + } + + /// @notice Gets the x2EarnApps contract address + function getX2EarnApps() external view returns (IX2EarnApps) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + return PassportConfiguratorV2.getX2EarnApps($); + } + + /// @notice Gets the xAllocationVoting contract address + function getXAllocationVoting() external view returns (IXAllocationVotingGovernor) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + return PassportConfiguratorV2.getXAllocationVoting($); + } + + /// @notice Gets the galaxy member contract address + function getGalaxyMember() external view returns (IGalaxyMemberV2) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + return PassportConfiguratorV2.getGalaxyMember($); + } + + /// @notice Get the current block number + function clock() external view returns (uint48) { + return PassportClockLogicV2.clock(); + } + + /// @notice Get the clock mode + function CLOCK_MODE() external pure returns (string memory) { + return PassportClockLogicV2.CLOCK_MODE(); + } + + ///@dev returns the fields and values that describe the domain separator used by this contract for EIP-712 signature. + function eip712Domain() + external + view + returns ( + bytes1 fields, + string memory name, + string memory signatureVersion, + uint256 chainId, + address verifyingContract, + bytes32 salt, + uint256[] memory extensions + ) + { + return PassportEIP712SigningLogicV2.eip712Domain(); + } + + /// @notice Returns the version of the contract + function version() external pure returns (string memory) { + return "2"; + } + + // ---------- Setters ---------- // + /// @notice Toggles the specified check + function toggleCheck(PassportTypesV2.CheckType check) external onlyRole(SETTINGS_MANAGER_ROLE) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + PassportChecksLogicV2.toggleCheck($, check); + } + + /// @notice user can be whitelisted but the counter will not be reset + function whitelist(address _user) external onlyRoleOrAdmin(WHITELISTER_ROLE) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + PassportWhitelistAndBlacklistLogicV2.whitelist($, _user); + } + + /// @notice Removes a user from the whitelist + function removeFromWhitelist(address _user) external onlyRoleOrAdmin(WHITELISTER_ROLE) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + PassportWhitelistAndBlacklistLogicV2.removeFromWhitelist($, _user); + } + + /// @notice user can be blacklisted but the counter will not be reset + function blacklist(address _user) external onlyRoleOrAdmin(WHITELISTER_ROLE) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + PassportWhitelistAndBlacklistLogicV2.blacklist($, _user); + } + + /// @notice Removes a user from the blacklist + function removeFromBlacklist(address _user) external onlyRoleOrAdmin(WHITELISTER_ROLE) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + PassportWhitelistAndBlacklistLogicV2.removeFromBlacklist($, _user); + } + + /// @notice Sets the threshold percentage of blacklisted entities for a passport to be considered blacklisted + function setBlacklistThreshold(uint256 _threshold) external onlyRoleOrAdmin(SETTINGS_MANAGER_ROLE) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + PassportWhitelistAndBlacklistLogicV2.setBlacklistThreshold($, _threshold); + } + + /// @notice Sets the threshold percentage of whitelisted entities for a passport to be considered whitelisted + function setWhitelistThreshold(uint256 _threshold) external onlyRoleOrAdmin(SETTINGS_MANAGER_ROLE) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + PassportWhitelistAndBlacklistLogicV2.setWhitelistThreshold($, _threshold); + } + + /// @notice Registers an action for a user + /// @param user - the user that performed the action + /// @param appId - the app id of the action + function registerAction(address user, bytes32 appId) external onlyRole(ACTION_REGISTRAR_ROLE) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + PassportPoPScoreLogicV2.registerAction($, user, appId); + } + + /// @notice Registers an action for a user in a round + /// @param user - the user that performed the action + /// @param appId - the app id of the action + /// @param round - the round id of the action + function registerActionForRound(address user, bytes32 appId, uint256 round) external onlyRole(ACTION_REGISTRAR_ROLE) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + PassportPoPScoreLogicV2.registerActionForRound($, user, appId, round); + } + + /// @notice Function used to seed the passport with old actions by aggregating them + /// based on (user, appId, round) and summing up the total score offchain + /// @param user - the user that performed the actions + /// @param appId - the app id of the actions + /// @param round - the round id of the actions + /// @param totalScore - the total score of the actions + function registerAggregatedActionsForRound( + address user, + bytes32 appId, + uint256 round, + uint256 totalScore + ) external onlyRole(ACTION_REGISTRAR_ROLE) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + PassportPoPScoreLogicV2.registerAggregatedActionsForRound($, user, appId, round, totalScore); + } + + /// @notice Sets the threshold for a user to be considered a person + /// @param threshold - the proof of participation score threshold + function setThresholdPoPScore(uint208 threshold) external onlyRoleOrAdmin(ACTION_SCORE_MANAGER_ROLE) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + PassportPoPScoreLogicV2.setThresholdPoPScore($, threshold); + } + + /// @notice Sets the number of rounds to consider for the cumulative score + /// @param rounds - the number of rounds + function setRoundsForCumulativeScore(uint256 rounds) external onlyRoleOrAdmin(ACTION_SCORE_MANAGER_ROLE) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + PassportPoPScoreLogicV2.setRoundsForCumulativeScore($, rounds); + } + + /// @notice Sets the security multiplier + /// @param security - the app security between LOW, MEDIUM, HIGH + /// @param multiplier - the multiplier + function setSecurityMultiplier( + PassportTypesV2.APP_SECURITY security, + uint256 multiplier + ) external onlyRoleOrAdmin(ACTION_SCORE_MANAGER_ROLE) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + PassportPoPScoreLogicV2.setSecurityMultiplier($, security, multiplier); + } + + /// @dev Sets the security level of an app + /// @param appId - the app id + /// @param security - the security level + function setAppSecurity( + bytes32 appId, + PassportTypesV2.APP_SECURITY security + ) external onlyRoleOrAdmin(ACTION_SCORE_MANAGER_ROLE) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + PassportPoPScoreLogicV2.setAppSecurity($, appId, security); + } + + /// @notice Sets the decay rate for the exponential decay + /// @param _decayRate - the decay rate + function setDecayRate(uint256 _decayRate) external onlyRoleOrAdmin(DEFAULT_ADMIN_ROLE) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + PassportPoPScoreLogicV2.setDecayRate($, _decayRate); + } + + /// @notice Link an account (which will become an entity) to a passport (an address that is not an enitity) + /// After linking, the scores of the enitity will be stored to the linked account (passport) + /// Balance is not transferred and the entity will not be able to vote after linking. + /// @param entity - the entity address + /// @param deadline - the deadline for the signature + /// @param signature - the signature of the delegation + function linkEntityToPassportWithSignature(address entity, uint256 deadline, bytes memory signature) external { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + PassportEntityLogicV2.linkEntityToPassportWithSignature($, entity, deadline, signature); + } + + /// @notice Link an account (which will become an entity) to a passport (an address that is not an enitity) + /// After linking, the scores of the enitity will be stored to the linked account (passport) + /// Balance is not transferred and the entity will not be able to vote after linking. + /// @dev The passport must accept the delegation + function linkEntityToPassport(address passport) external { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + PassportEntityLogicV2.linkEntityToPassport($, passport); + } + + /// @notice Allow the passport to accept the delegation + /// @param entity - the entity address + function acceptEntityLink(address entity) external { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + PassportEntityLogicV2.acceptEntityLink($, entity); + } + + /// @notice Revoke the delegation (can be done by the entity or the passport) + /// @param entity - the entity address + function removeEntityLink(address entity) external { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + PassportEntityLogicV2.removeEntityLink($, entity); + } + + /// @notice Deny an incoming pending entity link to the sender's passport. + /// @param entity - the entity address + function denyIncomingPendingEntityLink(address entity) external { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + PassportEntityLogicV2.denyIncomingPendingEntityLink($, entity); + } + + /// @notice Cancel an outgoing pending entity link from the sender. + function cancelOutgoingPendingEntityLink() external { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + PassportEntityLogicV2.cancelOutgoingPendingEntityLink($); + } + + /// @notice Sets the maximum number of entities that can be linked to a passport + /// @param maxEntities - the maximum number of entities + function setMaxEntitiesPerPassport(uint256 maxEntities) external onlyRoleOrAdmin(SETTINGS_MANAGER_ROLE) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + PassportEntityLogicV2.setMaxEntitiesPerPassport($, maxEntities); + } + + /// @notice Delegate the passport to another address + /// The delegator must sign a message where he authorizes the delegatee to request the delegation: + /// this is done to avoid that a malicious user delegates the personhood to another user without his consent. + /// Eg: Alice has a personhood where she is not considered a person, she delegates her personhood to Bob, which + /// is considered a person. Bob now cannot vote because he is not considered a person anymore. + /// @param delegator - the delegator address + /// @param deadline - the deadline for the signature + /// @param signature - the signature of the delegation + function delegateWithSignature(address delegator, uint256 deadline, bytes memory signature) external { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + PassportDelegationLogicV2.delegateWithSignature($, delegator, deadline, signature); + } + + /// @notice Delegate the personhood to another address + /// @dev The delegatee must accept the delegation + /// Eg: Alice has a personhood where she is not considered a person, she delegates her personhood to Bob, which + /// is considered a person. Bob now cannot vote because he is not considered a person anymore. + function delegatePassport(address delegatee) external { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + PassportDelegationLogicV2.delegatePassport($, delegatee); + } + + /// @notice Allow the delegatee to accept the delegation + /// @param delegator - the delegator address + function acceptDelegation(address delegator) external { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + PassportDelegationLogicV2.acceptDelegation($, delegator); + } + + /// @notice Revoke the delegation (can be done by the delegator or the delegatee) + function revokeDelegation() external { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + PassportDelegationLogicV2.revokeDelegation($); + } + + /// @notice Allows a user to deny (and remove) an incoming pending delegation. + /// @param delegator - the user who is delegating to me (aka the delegator) + function denyIncomingPendingDelegation(address delegator) external { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + PassportDelegationLogicV2.denyIncomingPendingDelegation($, delegator); + } + + /// @notice Allows a delegator to cancel (and remove) the outgoing pending delegation. + function cancelOutgoingPendingDelegation() external { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + PassportDelegationLogicV2.cancelOutgoingPendingDelegation($); + } + + /// @notice Signals a user + function signalUser(address _user) external onlyRoleOrAdmin(SIGNALER_ROLE) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + PassportSignalingLogicV2.signalUser($, _user); + } + + /// @notice Signals a user with a reason + function signalUserWithReason(address _user, string memory reason) external onlyRoleOrAdmin(SIGNALER_ROLE) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + PassportSignalingLogicV2.signalUserWithReason($, _user, reason); + } + + /// @notice this method allows an app admin to assign a signaler to an app + /// @param app - the app to assign the signaler to + /// @param user - the signaler to assign to the app + function assignSignalerToAppByAppAdmin(bytes32 app, address user) external { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + PassportSignalingLogicV2.assignSignalerToAppByAppAdmin($, app, user); + _grantRole(SIGNALER_ROLE, user); + } + + /// @notice this method allows an app admin to remove a signaler from an app + /// @param user - the signaler to remove from the app + function removeSignalerFromAppByAppAdmin(address user) external { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + PassportSignalingLogicV2.removeSignalerFromAppByAppAdmin($, user); + _revokeRole(SIGNALER_ROLE, user); + } + + /// @notice Sets the signaling threshold + /// @param threshold - the signaling threshold + function setSignalingThreshold(uint256 threshold) external onlyRoleOrAdmin(DEFAULT_ADMIN_ROLE) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + PassportSignalingLogicV2.setSignalingThreshold($, threshold); + } + + /// @dev Assigns a signaler to an app, allowing us to track the amount of signals from a specific app + /// @notice to be used together with grantRole + /// @param app - the app ID + /// @param user - the signaler address + function assignSignalerToApp(bytes32 app, address user) external onlyRoleOrAdmin(ROLE_GRANTER) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + PassportSignalingLogicV2.assignSignalerToApp($, app, user); + _grantRole(SIGNALER_ROLE, user); + } + + /// @dev Removes a signaler from an app + /// @notice to be used together with revokeRole + /// @param user - the signaler address + function removeSignalerFromApp(address user) external onlyRoleOrAdmin(ROLE_GRANTER) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + PassportSignalingLogicV2.removeSignalerFromApp($, user); + _revokeRole(SIGNALER_ROLE, user); + } + + /// @notice Resets the signals of a user with a given reason + /// @dev assigns the signals of a user to zero + /// @param user - the address of the user + /// @param reason - the reason for resetting the signals + function resetUserSignalsWithReason(address user, string memory reason) external onlyRole(DEFAULT_ADMIN_ROLE) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + PassportSignalingLogicV2.resetUserSignals($, user, reason); + } + + /// @notice Resets the signals of a user by app admin + /// @param user - the user to reset the signals of + /// @param reason - the reason for resetting the signals + function resetUserSignalsByAppAdminWithReason(address user, string memory reason) external { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + PassportSignalingLogicV2.resetUserSignalsByAppAdminWithReason($, user, reason); + } + + /// @notice Sets the minimum galaxy member level + /// @param _minimumGalaxyMemberLevel The new minimum galaxy member level + function setMinimumGalaxyMemberLevel(uint256 _minimumGalaxyMemberLevel) external onlyRole(SETTINGS_MANAGER_ROLE) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + PassportChecksLogicV2.setMinimumGalaxyMemberLevel($, _minimumGalaxyMemberLevel); + } + + /// @dev Sets the xAllocationVoting contract + /// @param xAllocationVoting - the xAllocationVoting contract address + function setXAllocationVoting( + IXAllocationVotingGovernor xAllocationVoting + ) external onlyRoleOrAdmin(DEFAULT_ADMIN_ROLE) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + PassportConfiguratorV2.setXAllocationVoting($, xAllocationVoting); + } + + /// @dev Sets the galaxy member contract + /// @param galaxyMember - the galaxy member contract address + function setGalaxyMember(IGalaxyMemberV2 galaxyMember) external onlyRoleOrAdmin(DEFAULT_ADMIN_ROLE) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + PassportConfiguratorV2.setGalaxyMember($, galaxyMember); + } + + /// @notice Sets the x2EarnApps contract address + /// @param _x2EarnApps - the X2EarnApps contract address + function setX2EarnApps(IX2EarnApps _x2EarnApps) external override onlyRole(DEFAULT_ADMIN_ROLE) { + PassportStorageTypesV2.PassportStorage storage $ = getPassportStorage(); + PassportConfiguratorV2.setX2EarnApps($, _x2EarnApps); + } + + // ---------- Overrides ---------- // + + /// @dev Grants a role to an account + /// @notice Overrides the grantRole function to add a modifier to check if the user has the required role or is the DEFAULT_ADMIN_ROLE + /// @param role - the role to grant + /// @param account - the account to grant the role to + function grantRole( + bytes32 role, + address account + ) public override(AccessControlUpgradeable, IVeBetterPassportV2) onlyRoleOrAdmin(ROLE_GRANTER) { + _grantRole(role, account); + } + + /// @dev Revokes a role from an account + /// @notice Overrides the revokeRole function to add a modifier to check if the user has the required role or is the DEFAULT_ADMIN_ROLE + /// @param role - the role to revoke + /// @param account - the account to revoke the role from + function revokeRole( + bytes32 role, + address account + ) public override(AccessControlUpgradeable, IVeBetterPassportV2) onlyRoleOrAdmin(ROLE_GRANTER) { + _revokeRole(role, account); + } +} diff --git a/contracts/deprecated/V2/ve-better-passport/libraries/PassportChecksLogicV2.sol b/contracts/deprecated/V2/ve-better-passport/libraries/PassportChecksLogicV2.sol new file mode 100644 index 0000000..d013b23 --- /dev/null +++ b/contracts/deprecated/V2/ve-better-passport/libraries/PassportChecksLogicV2.sol @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: MIT + +// ####### +// ################ +// #################### +// ########### ######### +// ######### ######### +// ####### ######### ######### +// ######### ######### ########## +// ########## ######## #################### +// ########## ######### ######################### +// ################### ############################ +// ################# ########## ######## +// ############## ### ######## +// ############ ######### +// ########## ########## +// ######## ########### +// ### ############ +// ############## +// ################# +// ############## +// ######### + +pragma solidity 0.8.20; + +import { PassportStorageTypesV2 } from "./PassportStorageTypesV2.sol"; +import { PassportTypesV2 } from "./PassportTypesV2.sol"; + +/** + * @title PassportChecksLogicV2 + * @dev A library that manages various checks related to personhood in the Passport contract. + * It provides the ability to enable or disable specific personhood checks (such as whitelist, blacklist, signaling, etc.) + * and to update certain configurations such as the minimum Galaxy Member level. + * This library operates using a bitmask for efficient storage and toggling of checks. + */ +library PassportChecksLogicV2 { + // ---------- Consants ---------- // + uint256 constant WHITELIST_CHECK = 1 << 0; // Bitwise shift to the left by 0 + uint256 constant BLACKLIST_CHECK = 1 << 1; // Bitwise shift to the left by 1 + uint256 constant SIGNALING_CHECK = 1 << 2; // Bitwise shift to the left by 2 + uint256 constant PARTICIPATION_SCORE_CHECK = 1 << 3; // Bitwise shift to the left by 3 + uint256 constant GM_OWNERSHIP_CHECK = 1 << 4; // Bitwise shift to the left by 4 + + string constant WHITELIST_CHECK_NAME = "Whitelist Check"; + string constant BLACKLIST_CHECK_NAME = "Blacklist Check"; + string constant SIGNALING_CHECK_NAME = "Signaling Check"; + string constant PARTICIPATION_SCORE_CHECK_NAME = "Participation Score Check"; + string constant GM_OWNERSHIP_CHECK_NAME = "GM Ownership Check"; + + // ---------- Events ---------- // + /// @notice Emitted when a specific check is toggled. + /// @param checkName The name of the check being toggled. + /// @param enabled True if the check is enabled, false if disabled. + event CheckToggled(string indexed checkName, bool enabled); + + /// @notice Emitted when the minimum galaxy member level is set. + /// @param minimumGalaxyMemberLevel The new minimum galaxy member level. + event MinimumGalaxyMemberLevelSet(uint256 minimumGalaxyMemberLevel); + + // ---------- Private Functions ---------- // + + /// @notice Maps the PassportTypesV2.CheckType enum to the corresponding bitmask constant. + /// @param checkType The type of check from the enum. + /// @return The bitmask constant and the check name for the specified check. + function _mapCheckTypeToBitmask(PassportTypesV2.CheckType checkType) private pure returns (uint256, string memory) { + if (checkType == PassportTypesV2.CheckType.WHITELIST_CHECK) return (WHITELIST_CHECK, WHITELIST_CHECK_NAME); + if (checkType == PassportTypesV2.CheckType.BLACKLIST_CHECK) return (BLACKLIST_CHECK, BLACKLIST_CHECK_NAME); + if (checkType == PassportTypesV2.CheckType.SIGNALING_CHECK) return (SIGNALING_CHECK, SIGNALING_CHECK_NAME); + if (checkType == PassportTypesV2.CheckType.PARTICIPATION_SCORE_CHECK) + return (PARTICIPATION_SCORE_CHECK, PARTICIPATION_SCORE_CHECK_NAME); + if (checkType == PassportTypesV2.CheckType.GM_OWNERSHIP_CHECK) return (GM_OWNERSHIP_CHECK, GM_OWNERSHIP_CHECK_NAME); + revert("Invalid PassportTypesV2"); + } + + /// @notice Checks if a specific check is enabled + /// @param checkType The type of check to query (from the enum) + /// @return True if the check is enabled, false otherwise + function _isCheckEnabled( + PassportStorageTypesV2.PassportStorage storage self, + PassportTypesV2.CheckType checkType + ) internal view returns (bool) { + require(checkType != PassportTypesV2.CheckType.UNDEFINED, "Invalid check type"); + + (uint256 checkBit, ) = _mapCheckTypeToBitmask(checkType); + return (self.personhoodChecks & checkBit) != 0; + } + + // ---------- Getters ---------- // + + /// @notice Checks if a specific check is enabled. + /// @param self The storage object for the Passport contract containing all checks. + /// @param checkType The type of check to query (from the enum). + /// @return True if the check is enabled, false otherwise. + function isCheckEnabled( + PassportStorageTypesV2.PassportStorage storage self, + PassportTypesV2.CheckType checkType + ) external view returns (bool) { + return _isCheckEnabled(self, checkType); + } + + /// @notice Returns the minimum galaxy member level + function getMinimumGalaxyMemberLevel( + PassportStorageTypesV2.PassportStorage storage self + ) internal view returns (uint256) { + return self.minimumGalaxyMemberLevel; + } + + // ---------- Setters ---------- // + /// @notice Toggles the specified check between enabled and disabled. + /// @param self The storage object for the Passport contract containing all checks. + /// @param checkType The type of check to toggle (from the enum). + function toggleCheck( + PassportStorageTypesV2.PassportStorage storage self, + PassportTypesV2.CheckType checkType + ) external { + require(checkType != PassportTypesV2.CheckType.UNDEFINED, "Invalid check type"); + + (uint256 checkBit, string memory checkName) = _mapCheckTypeToBitmask(checkType); + + // Check if the check is currently enabled + if ((self.personhoodChecks & checkBit) != 0) { + // Disable the check by clearing the bit + self.personhoodChecks &= ~checkBit; + emit CheckToggled(checkName, false); + } else { + // Enable the check by setting the bit + self.personhoodChecks |= checkBit; + emit CheckToggled(checkName, true); + } + } + + /// @notice Sets the minimum galaxy member level + /// @param minimumGalaxyMemberLevel The new minimum galaxy member level + function setMinimumGalaxyMemberLevel( + PassportStorageTypesV2.PassportStorage storage self, + uint256 minimumGalaxyMemberLevel + ) external { + require(minimumGalaxyMemberLevel > 0, "VeBetterPassport: minimum galaxy member level must be greater than 0"); + + self.minimumGalaxyMemberLevel = minimumGalaxyMemberLevel; + emit MinimumGalaxyMemberLevelSet(minimumGalaxyMemberLevel); + } +} diff --git a/contracts/deprecated/V2/ve-better-passport/libraries/PassportClockLogicV2.sol b/contracts/deprecated/V2/ve-better-passport/libraries/PassportClockLogicV2.sol new file mode 100644 index 0000000..f625bcf --- /dev/null +++ b/contracts/deprecated/V2/ve-better-passport/libraries/PassportClockLogicV2.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: MIT + +// ####### +// ################ +// #################### +// ########### ######### +// ######### ######### +// ####### ######### ######### +// ######### ######### ########## +// ########## ######## #################### +// ########## ######### ######################### +// ################### ############################ +// ################# ########## ######## +// ############## ### ######## +// ############ ######### +// ########## ########## +// ######## ########### +// ### ############ +// ############## +// ################# +// ############## +// ######### + +pragma solidity 0.8.20; + +import { PassportStorageTypesV2 } from "./PassportStorageTypesV2.sol"; +import { Time } from "@openzeppelin/contracts/utils/types/Time.sol"; + +/// @title PassportClockLogicV2 Library +/// @notice Library for managing the clock logic as specified in EIP-6372. +library PassportClockLogicV2 { + /** + * @notice Returns the current timepoint which is the current block number. + * @return The current block number. + */ + function clock() internal view returns (uint48) { + return Time.blockNumber(); + } + + /** + * @notice Returns the machine-readable description of the clock mode as specified in EIP-6372. + * @dev It returns the default block number mode. + * @return The clock mode as a string. + */ + // solhint-disable-next-line func-name-mixedcase + function CLOCK_MODE() internal pure returns (string memory) { + return "mode=blocknumber&from=default"; + } +} diff --git a/contracts/deprecated/V2/ve-better-passport/libraries/PassportConfiguratorV2.sol b/contracts/deprecated/V2/ve-better-passport/libraries/PassportConfiguratorV2.sol new file mode 100644 index 0000000..e855db9 --- /dev/null +++ b/contracts/deprecated/V2/ve-better-passport/libraries/PassportConfiguratorV2.sol @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: MIT + +// ####### +// ################ +// #################### +// ########### ######### +// ######### ######### +// ####### ######### ######### +// ######### ######### ########## +// ########## ######## #################### +// ########## ######### ######################### +// ################### ############################ +// ################# ########## ######## +// ############## ### ######## +// ############ ######### +// ########## ########## +// ######## ########### +// ### ############ +// ############## +// ################# +// ############## +// ######### + +pragma solidity 0.8.20; + +import { PassportStorageTypesV2 } from "./PassportStorageTypesV2.sol"; +import { PassportTypesV2 } from "./PassportTypesV2.sol"; +import { PassportClockLogicV2 } from "./PassportClockLogicV2.sol"; +import { IX2EarnApps } from "../../../../interfaces/IX2EarnApps.sol"; +import { IXAllocationVotingGovernor } from "../../../../interfaces/IXAllocationVotingGovernor.sol"; +import { IGalaxyMemberV2 } from "../../../V2/interfaces/IGalaxyMemberV2.sol"; +import { Checkpoints } from "@openzeppelin/contracts/utils/structs/Checkpoints.sol"; + +/// @title PassportConfiguratorV2 Library +/// @notice Library for managing the configuration of a Passport contract. +/// @dev This library provides functions to set and get various configuration parameters and contracts used by the Passport contract. +library PassportConfiguratorV2 { + using Checkpoints for Checkpoints.Trace208; + + // ---------- Getters ---------- // + /// @notice Gets the x2EarnApps contract address + function getX2EarnApps(PassportStorageTypesV2.PassportStorage storage self) internal view returns (IX2EarnApps) { + return self.x2EarnApps; + } + + /// @notice Gets the xAllocationVoting contract address + function getXAllocationVoting( + PassportStorageTypesV2.PassportStorage storage self + ) internal view returns (IXAllocationVotingGovernor) { + return self.xAllocationVoting; + } + + /// @notice Gets the galaxy member contract address + function getGalaxyMember(PassportStorageTypesV2.PassportStorage storage self) internal view returns (IGalaxyMemberV2) { + return self.galaxyMember; + } + + // ---------- Setters ---------- // + + /// @notice Initializes the PassportStorage struct with the provided initialization data + function initializePassportStorage( + PassportStorageTypesV2.PassportStorage storage self, + PassportTypesV2.InitializationData memory initializationData + ) external { + // Initialize the external contracts + setX2EarnApps(self, initializationData.x2EarnApps); + setXAllocationVoting(self, initializationData.xAllocationVoting); + setGalaxyMember(self, initializationData.galaxyMember); + + // Initialize the bot signals threshold + self.signalsThreshold = initializationData.signalingThreshold; + + // Initialize the minimum Galaxy Member level to be considered human by Personhood checks + self.minimumGalaxyMemberLevel = initializationData.minimumGalaxyMemberLevel; + + // Initialize the participant score threshold to be considered human by Personhood checks + self.popScoreThreshold.push(PassportClockLogicV2.clock(), 0); + + // Initialize the number of rounds for cumulative score + self.roundsForCumulativeScore = initializationData.roundsForCumulativeScore; + + // Initialize the secuirty multiplier + self.securityMultiplier[PassportTypesV2.APP_SECURITY.LOW] = 100; + self.securityMultiplier[PassportTypesV2.APP_SECURITY.MEDIUM] = 200; + self.securityMultiplier[PassportTypesV2.APP_SECURITY.HIGH] = 400; + + // Decay + self.decayRate = initializationData.decayRate; + + // Set the threshold percentage of blacklisted or whitelisted entities to consider a passport user as blacklisted or whitelisted + self.blacklistThreshold = initializationData.blacklistThreshold; + self.whitelistThreshold = initializationData.whitelistThreshold; + + // Set the maximum number of entities per passport + self.maxEntitiesPerPassport = initializationData.maxEntitiesPerPassport; + } + + /// @notice Sets the X2EarnApps contract address + /// @dev The X2EarnApps contract address can be modified by the CONTRACTS_ADDRESS_MANAGER_ROLE + /// @param _x2EarnApps - the X2EarnApps contract address + function setX2EarnApps(PassportStorageTypesV2.PassportStorage storage self, IX2EarnApps _x2EarnApps) public { + require(address(_x2EarnApps) != address(0), "VeBetterPassport: x2EarnApps is the zero address"); + + self.x2EarnApps = _x2EarnApps; + } + + /// @dev Sets the xAllocationVoting contract + /// @param self - the PassportStorage struct + /// @param _xAllocationVoting - the xAllocationVoting contract address + function setXAllocationVoting( + PassportStorageTypesV2.PassportStorage storage self, + IXAllocationVotingGovernor _xAllocationVoting + ) public { + require(address(_xAllocationVoting) != address(0), "VeBetterPassport: xAllocationVoting is the zero address"); + + self.xAllocationVoting = _xAllocationVoting; + } + + /// @notice Sets the galaxy member contract address + /// @param self - the PassportStorage struct + /// @param _galaxyMember - the galaxy member contract address + function setGalaxyMember(PassportStorageTypesV2.PassportStorage storage self, IGalaxyMemberV2 _galaxyMember) public { + require(address(_galaxyMember) != address(0), "VeBetterPassport: galaxyMember is the zero address"); + + self.galaxyMember = _galaxyMember; + } +} diff --git a/contracts/deprecated/V2/ve-better-passport/libraries/PassportDelegationLogicV2.sol b/contracts/deprecated/V2/ve-better-passport/libraries/PassportDelegationLogicV2.sol new file mode 100644 index 0000000..cdd0c98 --- /dev/null +++ b/contracts/deprecated/V2/ve-better-passport/libraries/PassportDelegationLogicV2.sol @@ -0,0 +1,501 @@ +// SPDX-License-Identifier: MIT + +// ####### +// ################ +// #################### +// ########### ######### +// ######### ######### +// ####### ######### ######### +// ######### ######### ########## +// ########## ######## #################### +// ########## ######### ######################### +// ################### ############################ +// ################# ########## ######## +// ############## ### ######## +// ############ ######### +// ########## ########## +// ######## ########### +// ### ############ +// ############## +// ################# +// ############## +// ######### + +pragma solidity 0.8.20; + +import { PassportStorageTypesV2 } from "./PassportStorageTypesV2.sol"; +import { PassportClockLogicV2 } from "./PassportClockLogicV2.sol"; +import { PassportEIP712SigningLogicV2 } from "./PassportEIP712SigningLogicV2.sol"; +import { PassportEntityLogicV2 } from "./PassportEntityLogicV2.sol"; +import { PassportTypesV2 } from "./PassportTypesV2.sol"; +import { Checkpoints } from "@openzeppelin/contracts/utils/structs/Checkpoints.sol"; +import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; + +/** + * @title PassportDelegationLogicV2 + * @dev A library that manages the delegation of passports between users in the Passport system. + * It allows users to delegate their passports to others, revoke delegations, and check the delegation status. + * Delegations can be created with or without signatures, and certain rules are enforced, such as preventing + * delegation to oneself or to entities associated with a passport. + * + * This library also emits various events for delegation creation, revocation, and pending delegations, allowing + * external systems to track delegation status. + */ +library PassportDelegationLogicV2 { + // Ethereum addresses are uint160, we can store addresses as uint160 values within the Checkpoints.Trace160 + using Checkpoints for Checkpoints.Trace160; + // Extends the bytes32 type to support ECDSA signatures + using ECDSA for bytes32; + + // ---------- Constants ---------- // + string private constant SIGNING_DOMAIN = "VeBetterPassport"; + string private constant SIGNATURE_VERSION = "1"; + bytes32 private constant DELEGATION_TYPEHASH = + keccak256("Delegation(address delegator,address delegatee,uint256 deadline)"); + + // ---------- Errors ---------- // + /// @notice Emitted when a user does not have permission to delegate passport. + error PassportDelegationUnauthorizedUser(address user); + + /// @notice Emitted when a user tries to delegate passport to themselves. + error CannotDelegateToSelf(address user); + + /// @notice Emitted when a user tries to revoke a delegation that does not exist. + error NotDelegated(address user); + + /// @notice Emitted when a user tries to delegate passport to more than one user. + error OnlyOneUserAllowed(); + + /// @notice Emitted when an entity tries to delegate a passport. + error PassportDelegationFromEntity(); + + /// @notice Emitted when a user tries to delegate a passport to another entity. + error PassportDelegationToEntity(); + + /// @notice Emitted when a user tries to delegate with a + error SignatureExpired(); + + /// @notice Emitted when a user tries to delegate with a + error InvalidSignature(); + + // ---------- Events ---------- // + /// @notice Emitted when a user delegates passport to another user. + event DelegationCreated(address indexed delegator, address indexed delegatee); + + /// @notice Emitted when a user delegates passport to another user pending acceptance. + event DelegationPending(address indexed delegator, address indexed delegatee); + + /// @notice Emitted when a user revokes the delegation of passport to another user. + event DelegationRevoked(address indexed delegator, address indexed delegatee); + + // ---------- Getters ---------- // + + /** + * @notice Returns the delegatee for a given delegator. + * @param self The storage object for the Passport contract containing delegation data. + * @param delegator The address of the delegator. + * @return The address of the delegatee for the given delegator. + */ + function getDelegatee( + PassportStorageTypesV2.PassportStorage storage self, + address delegator + ) public view returns (address) { + return _addressFromUint160(self.delegatorToDelegatee[delegator].latest()); + } + + /** + * @notice Returns the delegatee for a delegator at a specific timepoint. + * @param self The storage object for the Passport contract containing delegation data. + * @param delegator The address of the delegator. + * @param timepoint The timepoint to query. + * @return The delegatee address at the given timepoint. + */ + function getDelegateeInTimepoint( + PassportStorageTypesV2.PassportStorage storage self, + address delegator, + uint256 timepoint + ) external view returns (address) { + return _addressFromUint160(self.delegatorToDelegatee[delegator].upperLookupRecent(SafeCast.toUint48(timepoint))); + } + + /** + * @notice Returns the delegator for a given delegatee. + * @param self The storage object for the Passport contract containing delegation data. + * @param delegatee The address of the delegatee. + * @return The address of the delegator for the given delegatee. + */ + function getDelegator( + PassportStorageTypesV2.PassportStorage storage self, + address delegatee + ) public view returns (address) { + return _addressFromUint160(self.delegateeToDelegator[delegatee].latest()); + } + + /** + * @notice Returns the delegator for a deleagtee at a specific timepoint. + * @param self The storage object for the Passport contract containing delegation data. + * @param delegatee The address of the delegatee. + * @param timepoint The timepoint to query. + * @return The delegator address at the given timepoint. + */ + function getDelegatorInTimepoint( + PassportStorageTypesV2.PassportStorage storage self, + address delegatee, + uint256 timepoint + ) external view returns (address) { + return _getDelegatorInTimepoint(self, delegatee, timepoint); + } + + /** + * @notice Checks if the given user is currently a delegator. + * @param self The storage object for the Passport contract containing delegation data. + * @param user The address of the user being queried. + * @return True if the user is a delegator, false otherwise. + */ + function isDelegator(PassportStorageTypesV2.PassportStorage storage self, address user) internal view returns (bool) { + return self.delegatorToDelegatee[user].latest() != 0; + } + + /** + * @notice Checks if the given user is a delegator at a specific timepoint. + * @param self The storage object for the Passport contract containing delegation data. + * @param user The address of the user being queried. + * @param timepoint The specific timepoint (block number or timestamp) to check. + * @return True if the user is a delegator at the given timepoint, false otherwise. + */ + function isDelegatorInTimepoint( + PassportStorageTypesV2.PassportStorage storage self, + address user, + uint256 timepoint + ) external view returns (bool) { + return _isDelegatorInTimepoint(self, user, timepoint); + } + + /** + * @notice Checks if the given user is currently a delegatee. + * @param self The storage object for the Passport contract containing delegation data. + * @param user The address of the user being queried. + * @return True if the user is a delegatee, false otherwise. + */ + function isDelegatee(PassportStorageTypesV2.PassportStorage storage self, address user) internal view returns (bool) { + return self.delegateeToDelegator[user].latest() != 0; + } + + /** + * @notice Checks if the given user is a delegatee at a specific timepoint. + * @param self The storage object for the Passport contract containing delegation data. + * @param user The address of the user being queried. + * @param timepoint The specific timepoint (block number or timestamp) to check. + * @return True if the user is a delegatee at the given timepoint, false otherwise. + */ + function isDelegateeInTimepoint( + PassportStorageTypesV2.PassportStorage storage self, + address user, + uint256 timepoint + ) external view returns (bool) { + return _isDelegateeInTimepoint(self, user, timepoint); + } + + /** + * @notice Returns a list of pending delegations for the given user. + * @param self The storage object for the Passport contract containing delegation data. + * @param user The address of the user whose pending delegations are being queried. + * @return incoming The addresses of users that are delegating to the user. + * @return outgoing The address that the user is delegating to. + */ + function getPendingDelegations( + PassportStorageTypesV2.PassportStorage storage self, + address user + ) internal view returns (address[] memory incoming, address outgoing) { + return (self.pendingDelegationsDelegateeToDelegators[user], self.pendingDelegationsDelegatorToDelegatee[user]); + } + + // ---------- Setters ------------ // + + /** + * @notice Allows a delegator to delegate their passport to a delegatee with a signed message. + * The signature ensures the delegation is authorized by the delegator. + * Eg: Alice has a passport where she is not considered a person, she delegates her passport to Bob, which + * is considered a person. Bob now cannot vote because he is not considered a person anymore. + * @param self The storage object for the Passport contract. + * @param delegator The address of the delegator. + * @param deadline The expiration time of the delegation. + * @param signature The ECDSA signature for authorization. + */ + function delegateWithSignature( + PassportStorageTypesV2.PassportStorage storage self, + address delegator, + uint256 deadline, + bytes memory signature + ) external { + if (block.timestamp > deadline) { + revert SignatureExpired(); + } + + // Recover the signer address from the signature + bytes32 structHash = keccak256(abi.encode(DELEGATION_TYPEHASH, delegator, msg.sender, deadline)); + bytes32 digest = PassportEIP712SigningLogicV2.hashTypedDataV4(structHash); + address signer = digest.recover(signature); + + // Check if the signer is the delegator + if (signer != delegator) { + revert InvalidSignature(); + } + + // Check delegation rules + _checkDelegation(self, delegator, msg.sender); + + // Check if the delegatee has already been delegated + if (isDelegatee(self, msg.sender)) { + _removeDelegation(self, _addressFromUint160(self.delegateeToDelegator[msg.sender].latest()), msg.sender); + } + + _pushCheckpoint(self.delegatorToDelegatee[delegator], msg.sender); + _pushCheckpoint(self.delegateeToDelegator[msg.sender], delegator); + + emit DelegationCreated(delegator, msg.sender); + } + + /** + * @notice Allows a delegator to delegate their passport to a delegatee. + * The delegatee must accept the delegation for it to become active. + * Eg: Alice has a passport where she is not considered a person, she delegates her passport to Bob, which + * is considered a person. Bob now cannot vote because he is not considered a person anymore. + * @param self The storage object for the Passport contract. + * @param delegatee The address of the delegatee. + */ + function delegatePassport(PassportStorageTypesV2.PassportStorage storage self, address delegatee) external { + // Check delegation rules + _checkDelegation(self, msg.sender, delegatee); + + // Get the length of the pending delegations + uint256 length = self.pendingDelegationsDelegateeToDelegators[delegatee].length; + + // Add the delegator to the pending delegations indexes + self.pendingDelegationsIndexes[msg.sender] = length + 1; + + // Add the delegator to the pending delegations of the delegatee + self.pendingDelegationsDelegateeToDelegators[delegatee].push(msg.sender); + self.pendingDelegationsDelegatorToDelegatee[msg.sender] = delegatee; + + emit DelegationPending(msg.sender, delegatee); + } + + /** + * @notice Allows the delegatee to accept a pending delegation. + * @param self The storage object for the Passport contract. + * @param delegator The address of the delegator. + */ + function acceptDelegation(PassportStorageTypesV2.PassportStorage storage self, address delegator) external { + address delegatee = self.pendingDelegationsDelegatorToDelegatee[delegator]; + + // Check if the pending delegation exists + if (delegatee == address(0)) { + revert NotDelegated(msg.sender); // Delegator not found in the pending delegations + } + + // Check if the caller is the delegatee + if (delegatee != msg.sender) { + revert PassportDelegationUnauthorizedUser(msg.sender); // Delegation does not match + } + + // Check if the delegatee has already accepted a delegation + if (isDelegatee(self, msg.sender)) { + _removeDelegation(self, _addressFromUint160(self.delegateeToDelegator[msg.sender].latest()), msg.sender); + } + + // Add the delegator to the delegatee and the delegatee to the delegator + _pushCheckpoint(self.delegateeToDelegator[msg.sender], delegator); + _pushCheckpoint(self.delegatorToDelegatee[delegator], msg.sender); + + // Remove the pending delegation + _removePendingDelegation(self, delegator, msg.sender); + + emit DelegationCreated(delegator, msg.sender); + } + + /** + * @notice Allows a user to deny (and remove) an incoming pending delegation. + * @param self The storage object for the Passport contract. + * @param delegator the user who is delegating to me (aka the delegator) + */ + function denyIncomingPendingDelegation( + PassportStorageTypesV2.PassportStorage storage self, + address delegator + ) external { + address delegatee = self.pendingDelegationsDelegatorToDelegatee[delegator]; + + // Check if the pending delegation exists + if (delegatee == address(0)) { + revert NotDelegated(delegator); + } + + // Check caller is the delegatee + if (msg.sender != delegatee) { + revert PassportDelegationUnauthorizedUser(msg.sender); + } + + // Use the _removePendingDelegation function to handle the deletion logic + _removePendingDelegation(self, delegator, delegatee); + + emit DelegationRevoked(delegator, delegatee); + } + + /** + * @notice Allows a delegator to cancel (and remove) the outgoing pending delegation. + * @param self The storage object for the Passport contract. + */ + function cancelOutgoingPendingDelegation(PassportStorageTypesV2.PassportStorage storage self) external { + address delegatee = self.pendingDelegationsDelegatorToDelegatee[msg.sender]; + + // Check if the pending delegation exists + if (delegatee == address(0)) { + revert NotDelegated(msg.sender); + } + + // Use the _removePendingDelegation function to handle the deletion logic + _removePendingDelegation(self, msg.sender, delegatee); + + emit DelegationRevoked(msg.sender, delegatee); + } + + /** + * @notice Allows a delegator or delegatee to revoke an existing delegation. + * This removes the delegation between the delegator and the delegatee. + * @param self The storage object for the Passport contract. + */ + function revokeDelegation(PassportStorageTypesV2.PassportStorage storage self) external { + address user = msg.sender; + address delegator; + address delegatee; + + // Check if user is either a delegator or delegatee + if (isDelegator(self, user)) { + delegator = user; + delegatee = getDelegatee(self, user); + } else if (isDelegatee(self, user)) { + delegatee = user; + delegator = getDelegator(self, user); + } else { + revert NotDelegated(user); + } + + // Revoke the delegation and reset the checkpoints + _removeDelegation(self, delegator, delegatee); + } + + // ---------- Private ---------- // + /// @notice Push a new checkpoint for the delegator and delegatee + function _pushCheckpoint(Checkpoints.Trace160 storage store, address value) private { + store.push(PassportClockLogicV2.clock(), uint160(value)); + } + + /// @notice Removes a pending delegation between a delegator and a delegatee. + /// @dev This function removes the delegator from the delegatee's pending delegation list and updates the pendingDelegationsIndexes for the delegator. + /// The function swaps the last element in the pending delegation array with the one being removed and pops the last element to avoid leaving gaps. + /// @param self The PassportStorage structure containing delegation mappings and lists. + /// @param delegator The address of the delegator who initiated the pending delegation. + /// @param delegatee The address of the delegatee to whom the delegator is delegating. + function _removePendingDelegation( + PassportStorageTypesV2.PassportStorage storage self, + address delegator, + address delegatee + ) private { + uint256 index = self.pendingDelegationsIndexes[delegator]; + + uint256 pendingDelegationsLength = self.pendingDelegationsDelegateeToDelegators[delegatee].length; + + // Adjust index (since it's stored as index + 1) + index -= 1; + + // Swap the last element with the element to delete + if (index != pendingDelegationsLength - 1) { + address lastDelegator = self.pendingDelegationsDelegateeToDelegators[delegatee][pendingDelegationsLength - 1]; + self.pendingDelegationsDelegateeToDelegators[delegatee][index] = lastDelegator; + self.pendingDelegationsIndexes[lastDelegator] = index + 1; // Update the index + } + + // Pop the last element (removes the duplicate or the swapped one) + self.pendingDelegationsDelegateeToDelegators[delegatee].pop(); + + // Clear the pending delegation index for the removed delegator + delete self.pendingDelegationsIndexes[delegator]; + delete self.pendingDelegationsDelegatorToDelegatee[delegator]; + } + + /// @dev Removes the delegation relationship between a delegator and a delegatee. + function _removeDelegation( + PassportStorageTypesV2.PassportStorage storage self, + address delegator, + address delegatee + ) private { + _pushCheckpoint(self.delegatorToDelegatee[delegator], address(0)); + _pushCheckpoint(self.delegateeToDelegator[delegatee], address(0)); + + emit DelegationRevoked(delegator, delegatee); + } + + /// @notice Convert a uint160 value to an address + function _addressFromUint160(uint160 value) private pure returns (address) { + return address(uint160(value)); + } + + /// @notice Checks if user is a delegatee at a specific timepoint + function _isDelegateeInTimepoint( + PassportStorageTypesV2.PassportStorage storage self, + address user, + uint256 timepoint + ) internal view returns (bool) { + return self.delegateeToDelegator[user].upperLookupRecent(SafeCast.toUint48(timepoint)) != 0; + } + + /// @notice Returns the delegator for a given delegatee. + function _getDelegatorInTimepoint( + PassportStorageTypesV2.PassportStorage storage self, + address delegatee, + uint256 timepoint + ) internal view returns (address) { + return _addressFromUint160(self.delegateeToDelegator[delegatee].upperLookupRecent(SafeCast.toUint48(timepoint))); + } + + /// @notice Checks if the given user is a delegator at a specific timepoint. + function _isDelegatorInTimepoint( + PassportStorageTypesV2.PassportStorage storage self, + address user, + uint256 timepoint + ) internal view returns (bool) { + return self.delegatorToDelegatee[user].upperLookupRecent(SafeCast.toUint48(timepoint)) != 0; + } + + function _checkDelegation( + PassportStorageTypesV2.PassportStorage storage self, + address delegator, + address delegatee + ) private { + // Check if the delegator is trying to delegate to themselves + if (delegator == delegatee) { + revert CannotDelegateToSelf(delegator); + } + + // Check if the delegator is an entity linked to a passport or has a pending link + if (PassportEntityLogicV2.isEntity(self, delegator) || self.pendingLinksEntityToPassport[delegator] != address(0)) { + revert PassportDelegationFromEntity(); + } + + // Check if the delegatee is an entity linked to a passport or has a pending link + if (PassportEntityLogicV2.isEntity(self, delegatee) || self.pendingLinksEntityToPassport[delegatee] != address(0)) { + revert PassportDelegationToEntity(); + } + + // Check if the passport has already been delegated removing the previous delegation + if (isDelegator(self, delegator)) { + _removeDelegation(self, delegator, _addressFromUint160(self.delegatorToDelegatee[delegator].latest())); + } + + // Check if the passport is already pending delegation + if (self.pendingDelegationsDelegatorToDelegatee[delegator] != address(0)) { + _removePendingDelegation(self, delegator, self.pendingDelegationsDelegatorToDelegatee[delegator]); + } + } +} diff --git a/contracts/deprecated/V2/ve-better-passport/libraries/PassportEIP712SigningLogicV2.sol b/contracts/deprecated/V2/ve-better-passport/libraries/PassportEIP712SigningLogicV2.sol new file mode 100644 index 0000000..b241283 --- /dev/null +++ b/contracts/deprecated/V2/ve-better-passport/libraries/PassportEIP712SigningLogicV2.sol @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: MIT + +// ####### +// ################ +// #################### +// ########### ######### +// ######### ######### +// ####### ######### ######### +// ######### ######### ########## +// ########## ######## #################### +// ########## ######### ######################### +// ################### ############################ +// ################# ########## ######## +// ############## ### ######## +// ############ ######### +// ########## ########## +// ######## ########### +// ### ############ +// ############## +// ################# +// ############## +// ######### + +pragma solidity ^0.8.20; + +import { MessageHashUtils } from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; + +/** + * @dev https://eips.ethereum.org/EIPS/eip-712[EIP 712] is a standard for hashing and signing of typed structured data. + * + * The encoding scheme specified in the EIP requires a domain separator and a hash of the typed structured data, whose + * encoding is very generic and therefore its implementation in Solidity is not feasible, thus this contract + * does not implement the encoding itself. Protocols need to implement the type-specific encoding they need in order to + * produce the hash of their typed data using a combination of `abi.encode` and `keccak256`. + * + * This contract implements the EIP 712 domain separator ({_domainSeparatorV4}) that is used as part of the encoding + * scheme, and the final step of the encoding to obtain the message digest that is then signed via ECDSA + * ({_hashTypedDataV4}). + * + * The implementation of the domain separator was designed to be as efficient as possible while still properly updating + * the chain id to protect against replay attacks on an eventual fork of the chain. + * + * NOTE: This contract implements the version of the encoding known as "v4", as implemented by the JSON RPC method + * https://docs.metamask.io/guide/signing-data.html[`eth_signTypedDataV4` in MetaMask]. + * + * NOTE: In the upgradeable version of this contract, the cached values will correspond to the address, and the domain + * separator of the implementation contract. This will cause the {_domainSeparatorV4} function to always rebuild the + * separator from the immutable values, which is cheaper than accessing a cached version in cold storage. + */ +library PassportEIP712SigningLogicV2 { + // ---------- Constants ------------ // + + bytes32 private constant TYPE_HASH = + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); + + string private constant SIGNING_DOMAIN = "VeBetterPassport"; + string private constant SIGNATURE_VERSION = "1"; + bytes32 private constant SIGNING_DOMAIN_HASH = keccak256(bytes(SIGNING_DOMAIN)); + bytes32 private constant SIGNATURE_VERSION_HASH = keccak256(bytes(SIGNATURE_VERSION)); + + // ---------- Getters ---------- // + + /** + * @dev See {IERC-5267}. + */ + function eip712Domain() + internal + view + returns ( + bytes1 fields, + string memory name, + string memory signatureVersion, + uint256 chainId, + address verifyingContract, + bytes32 salt, + uint256[] memory extensions + ) + { + return ( + hex"0f", // 01111 + SIGNING_DOMAIN, + SIGNATURE_VERSION, + block.chainid, + address(this), + bytes32(0), + new uint256[](0) + ); + } + + // ---------- Internal and Private ---------- // + + /** + * @dev Given an already https://eips.ethereum.org/EIPS/eip-712#definition-of-hashstruct[hashed struct], this + * function returns the hash of the fully encoded EIP712 message for this domain. + * + * This hash can be used together with {ECDSA-recover} to obtain the signer of a message. For example: + * + * ```solidity + * bytes32 digest = _hashTypedDataV4(keccak256(abi.encode( + * keccak256("Mail(address to,string contents)"), + * mailTo, + * keccak256(bytes(mailContents)) + * ))); + * address signer = ECDSA.recover(digest, signature); + * ``` + */ + function hashTypedDataV4(bytes32 structHash) internal view returns (bytes32) { + return MessageHashUtils.toTypedDataHash(_domainSeparatorV4(), structHash); + } + + /** + * @dev Returns the domain separator for the current chain. + */ + function _domainSeparatorV4() private view returns (bytes32) { + return _buildDomainSeparator(); + } + + function _buildDomainSeparator() private view returns (bytes32) { + return keccak256(abi.encode(TYPE_HASH, SIGNING_DOMAIN_HASH, SIGNATURE_VERSION_HASH, block.chainid, address(this))); + } +} diff --git a/contracts/deprecated/V2/ve-better-passport/libraries/PassportEntityLogicV2.sol b/contracts/deprecated/V2/ve-better-passport/libraries/PassportEntityLogicV2.sol new file mode 100644 index 0000000..ff3f14a --- /dev/null +++ b/contracts/deprecated/V2/ve-better-passport/libraries/PassportEntityLogicV2.sol @@ -0,0 +1,611 @@ +// SPDX-License-Identifier: MIT + +// ####### +// ################ +// #################### +// ########### ######### +// ######### ######### +// ####### ######### ######### +// ######### ######### ########## +// ########## ######## #################### +// ########## ######### ######################### +// ################### ############################ +// ################# ########## ######## +// ############## ### ######## +// ############ ######### +// ########## ########## +// ######## ########### +// ### ############ +// ############## +// ################# +// ############## +// ######### + +pragma solidity 0.8.20; + +import { PassportStorageTypesV2 } from "./PassportStorageTypesV2.sol"; +import { PassportClockLogicV2 } from "./PassportClockLogicV2.sol"; +import { PassportEIP712SigningLogicV2 } from "./PassportEIP712SigningLogicV2.sol"; +import { PassportSignalingLogicV2 } from "./PassportSignalingLogicV2.sol"; +import { PassportWhitelistAndBlacklistLogicV2 } from "./PassportWhitelistAndBlacklistLogicV2.sol"; +import { PassportDelegationLogicV2 } from "./PassportDelegationLogicV2.sol"; +import { PassportTypesV2 } from "./PassportTypesV2.sol"; +import { Checkpoints } from "@openzeppelin/contracts/utils/structs/Checkpoints.sol"; +import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; + +/** + * @title PassportEntityLogicV2 + * @notice This library manages the core logic for linking and managing entities associated with a passport. + * + * @dev The passport serves as the central identity in the system, and entities (such as wallets, accounts, etc.) + * can be linked to the passport. Each entity linked to the passport contributes to the overall makeup of the passport, + * including its score, whitelist/blacklist status, VeChain node holder status, and other attributes. + * + * Each passport maintains a history of the entities that have been linked to it over time. This library provides + * functions to link entities to passports, verify the links, and maintain the historical state through checkpoints. + * + * The passport is the core identity, and all linked entities are secondary but critical components, each + * contributing to the overall score and state of the passport. + * + * Linking entities to a passport won't move the past entitie's score to the passport, but only the score of the future actions. + * + * The linkage process is secured using signatures to ensure that the entities and passports are linked with consent. + */ +library PassportEntityLogicV2 { + // Ethereum addresses are uint160, we can store addresses as uint160 values within the Checkpoints.Trace160 + using Checkpoints for Checkpoints.Trace160; + // Extends the bytes32 type to support ECDSA signatures + using ECDSA for bytes32; + + // ---------- Constants ---------- // + string private constant SIGNING_DOMAIN = "VeBetterPassport"; + string private constant SIGNATURE_VERSION = "1"; + bytes32 private constant LINK_TYPEHASH = keccak256("LinkEntity(address entity,address passport,uint256 deadline)"); + + // ---------- Errors ---------- // + /** + * @notice Thrown when the user is not authorized to perform the action. + * @param user The address of the unauthorized user. + */ + error UnauthorizedUser(address user); + + /** + * @notice Thrown when an entity is already linked. + * @param entity The address of the entity that is already linked. + */ + error AlreadyLinked(address entity); + + /** + * @notice Thrown when a user attempts to link to themselves, which is not allowed. + * @param user The address of the user attempting to link to themselves. + */ + error CannotLinkToSelf(address user); + + /** + * @notice Thrown when a user tries to perform an action but is not linked. + * @param user The address of the user that is not linked. + */ + error NotLinked(address user); + + /** + * @notice Thrown when only one link is allowed, but the user tries to create more links. + */ + error OnlyOneLinkAllowed(); + + /** + * @notice Thrown when a signature provided for an action has expired. + */ + error SignatureExpired(); + + /** + * @notice Thrown when a signature provided for an action is invalid. + */ + error InvalidSignature(); + + /** + * @notice Thrown when a user tries to link a entity to a passport that has reached the maximum number of entities. + */ + error MaxEntitiesPerPassportReached(); + + /** + * @notice Thrown when a user tries to link a entity that has delegated to another passport. + */ + error DelegatedEntity(address entity); + + // ---------- Events ---------- // + /** + * @notice Emitted when a link between an entity and a passport is successfully created. + * @param entity The address of the entity being linked. + * @param passport The address of the passport that the entity is linked to. + */ + event LinkCreated(address indexed entity, address indexed passport); + + /** + * @notice Emitted when a link is initiated but still pending confirmation. + * @param entity The address of the entity being linked. + * @param passport The address of the passport awaiting confirmation. + */ + event LinkPending(address indexed entity, address indexed passport); + + /** + * @notice Emitted when a link between an entity and a passport is removed. + * @param entity The address of the entity being unlinked. + * @param passport The address of the passport being unlinked. + */ + event LinkRemoved(address indexed entity, address indexed passport); + + // ---------- Getters ---------- // + + /** + * @notice Returns the passport linked to an entity. + * @param entity The address of the entity whose linked passport is being retrieved. + * @return The address of the linked passport. + */ + function getPassportForEntity( + PassportStorageTypesV2.PassportStorage storage self, + address entity + ) external view returns (address) { + return _getPassportForEntity(self, entity); + } + + /** + * @notice Returns the passport linked to an entity at a specific timepoint. + * @param entity The address of the entity whose linked passport is being queried. + * @param timepoint The timepoint to query. + * @return The address of the passport linked at the specified timepoint. + */ + function getPassportForEntityAtTimepoint( + PassportStorageTypesV2.PassportStorage storage self, + address entity, + uint256 timepoint + ) external view returns (address) { + return _addressFromUint160(self.entityToPassport[entity].upperLookupRecent(SafeCast.toUint48(timepoint))); + } + + /** + * @notice Returns the latest entities linked to a passport. + * @param passport The address of the passport. + * @return An array of addresses representing the entities currently linked to the passport. + */ + function getEntitiesLinkedToPassport( + PassportStorageTypesV2.PassportStorage storage self, + address passport + ) internal view returns (address[] memory) { + return self.passportToEntities[passport]; + } + + /** + * @notice Returns whether an entity is currently linked to a passport. + * @param entity The address of the entity being checked. + * @return True if the entity is linked to a passport, false otherwise. + */ + function isEntity(PassportStorageTypesV2.PassportStorage storage self, address entity) internal view returns (bool) { + return self.entityToPassport[entity].latest() != 0; + } + + /** + * @notice Checks if an entity was linked to a passport at a specific timepoint. + * @dev This function allows historical queries to determine if an entity was linked to a passport at a particular time. + * The function uses a checkpointing mechanism to retrieve the state at the given timepoint. + * @param self The storage reference for PassportStorage. + * @param entity The address of the entity. + * @param timepoint The timepoint (block number) at which to check the linkage. + * @return True if the entity was linked to the passport at the specified timepoint, false otherwise. + */ + function isEntityInTimepoint( + PassportStorageTypesV2.PassportStorage storage self, + address entity, + uint256 timepoint + ) external view returns (bool) { + return _isEntityInTimepoint(self, entity, timepoint); + } + + /** + * @notice Checks if the given address is a passport, i.e., not linked to an entity. + * @dev A passport is defined as an account that is not an entity for another passport, i.e., it is not linked to any passport. + * This function checks whether the given address is not an entity by checking it does not exist in the `passportEntitiesIndexes` mapping. + * @param self The storage reference for PassportStorage. + * @param passport The address to be checked. + * @return True if the address is a passport, false otherwise. + */ + function isPassport( + PassportStorageTypesV2.PassportStorage storage self, + address passport + ) internal view returns (bool) { + return self.passportEntitiesIndexes[passport] == 0; + } + + /** + * @notice Checks if the given address was a passport at a specific timepoint, i.e., not linked to an entity. + * @dev A passport is defined as an account that is not an entity for another passport at the given timepoint. + * This function checks whether the given address was not an entity at the specified timepoint by ensuring it was + * not linked to any other passport. + * + * It uses the `Checkpoints.Trace160` mechanism to perform an upper bound lookup to retrieve the state at the given timepoint. + * @param self The storage reference for PassportStorage. + * @param passport The address to be checked. + * @param timepoint The timepoint (block number) at which to check if the address was a passport. + * @return True if the address was a passport (i.e., not linked to any entity) at the specified timepoint, false otherwise. + */ + function isPassportInTimepoint( + PassportStorageTypesV2.PassportStorage storage self, + address passport, + uint256 timepoint + ) external view returns (bool) { + return self.entityToPassport[passport].upperLookupRecent(SafeCast.toUint48(timepoint)) == 0; + } + + /** + * @notice Returns the pending links for a user (both incoming and outgoing) + * @param user The address of the user + * @return incoming The addresss of users that want to link to the user. + * @return outgoing The address that the user wants to link to. + */ + function getPendingLinkings( + PassportStorageTypesV2.PassportStorage storage self, + address user + ) internal view returns (address[] memory incoming, address outgoing) { + return (self.pendingLinksPassportToEntities[user], self.pendingLinksEntityToPassport[user]); + } + + /** + * @notice Returns the maximum number of entities that can be linked to a passport. + */ + function getMaxEntitiesPerPassport( + PassportStorageTypesV2.PassportStorage storage self + ) internal view returns (uint256) { + return self.maxEntitiesPerPassport; + } + + // ---------- Setters ------------ // + + /** + * @notice Links an entity to a passport with a signature, ensuring consent for the link. + * @param entity The address of the entity being linked. + * @param deadline The expiration time for the link signature. + * @param signature The signature authorizing the link. + */ + function linkEntityToPassportWithSignature( + PassportStorageTypesV2.PassportStorage storage self, + address entity, + uint256 deadline, + bytes memory signature + ) external { + if (block.timestamp > deadline) { + revert SignatureExpired(); + } + + bytes32 structHash = keccak256(abi.encode(LINK_TYPEHASH, entity, msg.sender, deadline)); + bytes32 digest = PassportEIP712SigningLogicV2.hashTypedDataV4(structHash); + address signer = digest.recover(signature); + + // Ensure the signature is valid + if (signer != entity) { + revert InvalidSignature(); + } + + // Check if the entity is ok to link + _checkLink(self, msg.sender, entity); + + // Check if the passport has reached the maximum number of entities, if so, revert + if (self.passportToEntities[msg.sender].length >= self.maxEntitiesPerPassport) { + revert MaxEntitiesPerPassportReached(); + } + + // Add the entity to the list of links for the passport + _linkEntity(self, entity, msg.sender); + } + + /** + * @notice Links an entity to a passport. + * @param passport The address of the passport to which the entity is being linked. + */ + function linkEntityToPassport(PassportStorageTypesV2.PassportStorage storage self, address passport) external { + _checkLink(self, passport, msg.sender); + + // Add the entity to the list of pending links for the passport + uint256 length = self.pendingLinksPassportToEntities[passport].length; + self.pendingLinksIndexes[msg.sender] = length + 1; + self.pendingLinksPassportToEntities[passport].push(msg.sender); + self.pendingLinksEntityToPassport[msg.sender] = passport; + + emit LinkPending(msg.sender, passport); + } + + /** + * @notice Accepts the pending entity link to a passport. + * @dev The entity must have been previously linked in a pending state. + * @param entity The address of the entity to link to the passport. + */ + function acceptEntityLink(PassportStorageTypesV2.PassportStorage storage self, address entity) external { + address passport = self.pendingLinksEntityToPassport[entity]; + + // Ensure the entity is in a pending link state + if (passport == address(0)) { + revert NotLinked(entity); + } + + // Ensure that the caller is the passport that the entity is trying to link to + if (passport != msg.sender) { + revert UnauthorizedUser(msg.sender); + } + + // Check if the passport has reached the maximum number of entities + if (self.passportToEntities[msg.sender].length >= self.maxEntitiesPerPassport) { + revert MaxEntitiesPerPassportReached(); + } + + // Remove the pending link + _removePendingEntityLink(self, entity, msg.sender); + + // Link the entity to the passport + _linkEntity(self, entity, msg.sender); + } + + /** + * @notice Removes an entity link from a passport. + * @dev Only the passport or the entity itself can remove the link. + * @param entity The address of the entity to be unlinked. + */ + function removeEntityLink(PassportStorageTypesV2.PassportStorage storage self, address entity) external { + // Get the passport linked to the entity + address passport = _getPassportForEntity(self, entity); + + // Revert if the entity is not linked to any passport + if (passport == entity) { + revert NotLinked(entity); + } + + // Ensure the caller is either the passport or the entity + if (msg.sender != entity && msg.sender != passport) { + revert UnauthorizedUser(msg.sender); + } + + // Push a checkpoint to mark the entity as unlinked from the passport + _pushCheckpoint(self.entityToPassport[entity], address(0)); + + // Remove the entity link from the passport + _removeEntityLink(self, entity, passport); + + emit LinkRemoved(entity, passport); + } + + /** + * @notice Deny an incoming pending entity link to the sender's passport. + * @dev Only the passport can deny an incoming pending link. + * @param entity The address of the entity with a pending link to the passport. + */ + function denyIncomingPendingEntityLink(PassportStorageTypesV2.PassportStorage storage self, address entity) external { + address passport = self.pendingLinksEntityToPassport[entity]; + if (passport == address(0)) { + revert NotLinked(entity); + } + + // Ensure the caller is the passport that the entity is trying to link to + if (passport != msg.sender) { + revert UnauthorizedUser(msg.sender); + } + + _removePendingEntityLink(self, entity, passport); + + emit LinkRemoved(entity, passport); + } + + /** + * @notice Cancel an outgoing pending entity link from the sender. + */ + function cancelOutgoingPendingEntityLink(PassportStorageTypesV2.PassportStorage storage self) external { + address passport = self.pendingLinksEntityToPassport[msg.sender]; + if (passport == address(0)) { + revert NotLinked(msg.sender); + } + + _removePendingEntityLink(self, msg.sender, passport); + + emit LinkRemoved(msg.sender, passport); + } + + /** + * @notice Sets the maximum number of entities that can be linked to a passport. + * @param maxEntities The maximum number of entities that can be linked to a passport. + */ + function setMaxEntitiesPerPassport( + PassportStorageTypesV2.PassportStorage storage self, + uint256 maxEntities + ) external { + self.maxEntitiesPerPassport = maxEntities; + } + + // ---------- Private Helper Functions ---------- // + + /** + * @notice Internal function to push a checkpoint to the entity-to-passport mapping. + * @param store The Checkpoints.Trace160 storage where the link will be updated. + * @param value The address of the passport (or address(0) if unlinking). + */ + function _pushCheckpoint(Checkpoints.Trace160 storage store, address value) private { + store.push(PassportClockLogicV2.clock(), uint160(value)); + } + + /** + * @notice Internal function to remove a pending entity link between an entity and a passport. + * @param self The storage reference for PassportStorage. + * @param entity The address of the entity being unlinked from the passport. + * @param passport The address of the passport. + */ + + function _removePendingEntityLink( + PassportStorageTypesV2.PassportStorage storage self, + address entity, + address passport + ) private { + // Get the index of the entity in the pending links array + uint256 index = self.pendingLinksIndexes[entity]; + + // Get the length of the pending links array + uint256 pendingLinksLength = self.pendingLinksPassportToEntities[passport].length; + + // Decrement the index to match the array index + index -= 1; + + // If the entity is not the last in the array, move the last entity to the removed entity's position + if (index != pendingLinksLength - 1) { + address lastEntity = self.pendingLinksPassportToEntities[passport][pendingLinksLength - 1]; + self.pendingLinksPassportToEntities[passport][index] = lastEntity; + self.pendingLinksIndexes[lastEntity] = index + 1; + } + + // Remove the entity from the pending links array + self.pendingLinksPassportToEntities[passport].pop(); + + // Remove the entity from the pending links indexes + delete self.pendingLinksIndexes[entity]; + delete self.pendingLinksEntityToPassport[entity]; + } + + /** + * @notice Removes an entity linked to a passport, preserving the snapshot history. + * @param self The storage reference for PassportStorage. + * @param entity The address of the entity to be removed from the passport. + * @param passport The address of the passport from which the entity is being removed. + */ + function _removeEntityLink( + PassportStorageTypesV2.PassportStorage storage self, + address entity, + address passport + ) private { + // Get the index of the entity in the passport's entities array + uint256 index = self.passportEntitiesIndexes[entity]; + + // Get the length of the entities array + uint256 linksLength = self.passportToEntities[passport].length; + + // Decrement the index to match the array index + index -= 1; + + // If the entity is not the last in the array, move the last entity to the removed entity's position + if (index != linksLength - 1) { + address lastEntity = self.passportToEntities[passport][linksLength - 1]; + self.passportToEntities[passport][index] = lastEntity; + self.passportEntitiesIndexes[lastEntity] = index + 1; + } + + // Remove the entity from the passport's entities array + self.passportToEntities[passport].pop(); + + // Remove the entity from the passport's entities indexes + delete self.passportEntitiesIndexes[entity]; + delete self.passportToEntities[entity]; + + // Remove signals, and black/white lists from the passport + PassportSignalingLogicV2.removeEntitySignalsFromPassport(self, entity, passport); + PassportWhitelistAndBlacklistLogicV2.removeEntitiesBlackAndWhiteListsFromPassport(self, entity, passport); + } + + /** + * @notice Links an entity to a passport and creates a snapshot at the current timepoint. + * @param self The storage reference for PassportStorage. + * @param entity The address of the entity to be linked to the passport. + * @param passport The address of the passport to which the entity is being linked. + */ + function _linkEntity(PassportStorageTypesV2.PassportStorage storage self, address entity, address passport) private { + // Push a checkpoint to mark the entity as linked to the passport + _pushCheckpoint(self.entityToPassport[entity], passport); + + // Get the index of the entity in the passport's entities array + uint256 length = self.passportToEntities[passport].length; + + // Increment the index to match the array index + self.passportEntitiesIndexes[entity] = length + 1; + self.passportToEntities[passport].push(entity); + + // Assign the signals, and black/white lists to the passport + PassportSignalingLogicV2.attachEntitySignalsToPassport(self, entity, passport); + PassportWhitelistAndBlacklistLogicV2.attachEntitiesBlackAndWhiteListsToPassport(self, entity, passport); + + emit LinkCreated(entity, passport); + } + + function _addressFromUint160(uint160 value) private pure returns (address) { + return address(uint160(value)); + } + + /** + * @dev Internal function for getting the passport linked to an entity. + * @param entity The address of the entity whose linked passport is being retrieved. + * @return The address of the linked passport. + */ + function _getPassportForEntity( + PassportStorageTypesV2.PassportStorage storage self, + address entity + ) internal view returns (address) { + address passport = _addressFromUint160(self.entityToPassport[entity].latest()); + // If the entity is not linked to a passport, return the entity itself + if (passport == address(0)) { + return entity; + } + // Otherwise, return the linked passport + return passport; + } + + /** + * @notice Checks if an entity is linked to a passport. + * @param entity The address of the entity being checked. + * @return True if the entity is linked to a passport, false otherwise. + */ + function _isEntityInTimepoint( + PassportStorageTypesV2.PassportStorage storage self, + address entity, + uint256 timepoint + ) internal view returns (bool) { + return self.entityToPassport[entity].upperLookupRecent(SafeCast.toUint48(timepoint)) != 0; + } + + /** + * @notice Checks if passport and entity are eligible for linking. + * @param passport The address of the passport being checked. + * @param entity The address of the entity being checked. + */ + function _checkLink( + PassportStorageTypesV2.PassportStorage storage self, + address passport, + address entity + ) private view { + // Check if the entity is already an entity or pending entity, if so revert + if (self.entityToPassport[entity].latest() != 0 || self.pendingLinksIndexes[entity] != 0) { + revert AlreadyLinked(entity); + } + + // Check if the passport is an entity or pending entity, if so revert + if (self.entityToPassport[passport].latest() != 0 || self.pendingLinksEntityToPassport[passport] != address(0)) { + revert AlreadyLinked(passport); + } + + // Check if the entity is a passport or pending passport, if so revert + if (self.passportToEntities[entity].length != 0 || self.pendingLinksPassportToEntities[entity].length != 0) { + revert AlreadyLinked(passport); + } + + // Check if entity has delegated to another passport or has a pending delegation, if so revert + if ( + PassportDelegationLogicV2.isDelegator(self, entity) || + self.pendingDelegationsDelegatorToDelegatee[entity] != address(0) + ) { + revert DelegatedEntity(entity); + } + + // Check if the entity is a delegatee or pending delegatee, if so revert + if ( + PassportDelegationLogicV2.isDelegatee(self, entity) || + self.pendingDelegationsDelegateeToDelegators[entity].length != 0 + ) { + revert DelegatedEntity(entity); + } + + // Prevent self-linking (an entity cannot be its own passport) + if (entity == passport) { + revert CannotLinkToSelf(entity); + } + } +} diff --git a/contracts/deprecated/V2/ve-better-passport/libraries/PassportPersonhoodLogicV2.sol b/contracts/deprecated/V2/ve-better-passport/libraries/PassportPersonhoodLogicV2.sol new file mode 100644 index 0000000..ac13381 --- /dev/null +++ b/contracts/deprecated/V2/ve-better-passport/libraries/PassportPersonhoodLogicV2.sol @@ -0,0 +1,191 @@ +// SPDX-License-Identifier: MIT + +// ####### +// ################ +// #################### +// ########### ######### +// ######### ######### +// ####### ######### ######### +// ######### ######### ########## +// ########## ######## #################### +// ########## ######### ######################### +// ################### ############################ +// ################# ########## ######## +// ############## ### ######## +// ############ ######### +// ########## ########## +// ######## ########### +// ### ############ +// ############## +// ################# +// ############## +// ######### + +pragma solidity 0.8.20; + +import { PassportStorageTypesV2 } from "./PassportStorageTypesV2.sol"; +import { PassportChecksLogicV2 } from "./PassportChecksLogicV2.sol"; +import { PassportSignalingLogicV2 } from "./PassportSignalingLogicV2.sol"; +import { PassportDelegationLogicV2 } from "./PassportDelegationLogicV2.sol"; +import { PassportPoPScoreLogicV2 } from "./PassportPoPScoreLogicV2.sol"; +import { PassportClockLogicV2 } from "./PassportClockLogicV2.sol"; +import { PassportEntityLogicV2 } from "./PassportEntityLogicV2.sol"; +import { PassportWhitelistAndBlacklistLogicV2 } from "./PassportWhitelistAndBlacklistLogicV2.sol"; +import { PassportTypesV2 } from "./PassportTypesV2.sol"; + +/** + * @title PassportPersonhoodLogicV2 + * @dev A library that provides logic to determine whether a wallet is considered a "person" based on various checks. + * It evaluates factors such as participation score, blacklist status, and delegation status. + * This library supports both real-time personhood checks and checks at specific timepoints. + */ +library PassportPersonhoodLogicV2 { + /** + * @dev Checks if a wallet is a person or not based on the participation score, blacklisting, and GM holdings + * @return person bool representing if the user is considered a person + * @return reason string representing the reason for the result + */ + function isPerson( + PassportStorageTypesV2.PassportStorage storage self, + address user + ) external view returns (bool person, string memory reason) { + // Get the current timepoint + uint48 timepoint = PassportClockLogicV2.clock(); + + // Resolve the address of the person based on the delegation status + user = _resolvePersonhoodAddress(self, user, timepoint); + + // Check is the user is a person + return _checkPassport(self, user, timepoint); + } + + /** + * @dev Checks if a wallet is a person or not at a specific timepoint based on the participation score, blacklisting, and GM holdings + * @param user address of the user + * @param timepoint uint256 of the timepoint + * @return person bool representing if the user is considered a person + * @return reason string representing the reason for the result + */ + function isPersonAtTimepoint( + PassportStorageTypesV2.PassportStorage storage self, + address user, + uint48 timepoint + ) external view returns (bool person, string memory reason) { + // Resolve the address of the person based on the delegation status + user = _resolvePersonhoodAddress(self, user, timepoint); + + // Check is the user is a person + return _checkPassport(self, user, timepoint); + } + + // ---------- Internal & Private Functions ---------- // + + /** + * @dev Resolves the address of the person based on their delegation status at a given timepoint. + * If the user is a delegatee at the given timepoint, it returns the delegator's passport address. + * If the user is neither a delegatee nor a delegator (or entity), it returns the user's own address, + * representing their passport. + * + * @param self The storage object for the Passport contract containing all delegation data. + * @param user The address of the user whose personhood is being resolved. + * @param timepoint The timepoint (block number or timestamp) at which the delegation status is checked. + * + * @return The address of the resolved passport. + * - Returns the delegator's passport if the user is a delegatee. + * - Returns `address(0)` if the user is either a delegator or an entity at that timepoint. + * - Returns the user's own address (passport) if no delegation is found. + */ + function _resolvePersonhoodAddress( + PassportStorageTypesV2.PassportStorage storage self, + address user, + uint256 timepoint + ) private view returns (address) { + if (PassportDelegationLogicV2._isDelegateeInTimepoint(self, user, timepoint)) { + return PassportDelegationLogicV2._getDelegatorInTimepoint(self, user, timepoint); // Return the delegator's passport address + } else if ( + PassportDelegationLogicV2._isDelegatorInTimepoint(self, user, timepoint) || + PassportEntityLogicV2._isEntityInTimepoint(self, user, timepoint) + ) { + return address(0); // Return zero address if they delegated their personhood or entity + } else { + return user; // Return the user's own passport address + } + } + + /** + * @dev Checks whether a user meets the criteria to be considered a person (i.e., a valid passport holder) + * based on various conditions such as delegation status, whitelist/blacklist status, signaling, participation score, + * and node ownership. + * + * @param self The storage object for the Passport contract containing all relevant data. + * @param user The address of the user whose passport status is being checked. + * + * @return person bool indicating whether the user meets the criteria. + * @return reason string providing the reason or status for the result. + * + * Conditions checked: + * - Returns `(false, "User has delegated their personhood")` if the user has delegated their personhood. + * - Returns `(true, "User is whitelisted")` if the user is whitelisted. + * - Returns `(false, "User is blacklisted")` if the user is blacklisted. + * - Returns `(false, "User has been signaled too many times")` if the user has been signaled more than the threshold. + * - Returns `(true, "User's participation score is above the threshold")` if the user's participation score meets or exceeds the threshold. + * - Returns `(false, "User does not meet the criteria to be considered a person")` if none of the conditions are met. + * + * Additional considerations: + * - Checks for delegation status: If the user has delegated their personhood, they are not considered a valid passport holder. + * - Checks if the user is in the whitelist or blacklist, with priority given to whitelist status. + * - Evaluates the user's signaling status, participation score, and node ownership to determine validity. + */ + function _checkPassport( + PassportStorageTypesV2.PassportStorage storage self, + address user, + uint48 timepoint + ) private view returns (bool person, string memory reason) { + // Check if the user has delegated their personhood to another wallet + if (user == address(0)) { + return (false, "User has delegated their personhood"); + } + + // If a wallet is whitelisted, it is a person + if ( + PassportChecksLogicV2._isCheckEnabled(self, PassportTypesV2.CheckType.WHITELIST_CHECK) && + PassportWhitelistAndBlacklistLogicV2._isPassportWhitelisted(self, user) + ) { + return (true, "User is whitelisted"); + } + + // If a wallet is blacklisted, it is not a person + if ( + PassportChecksLogicV2._isCheckEnabled(self, PassportTypesV2.CheckType.BLACKLIST_CHECK) && + PassportWhitelistAndBlacklistLogicV2._isPassportBlacklisted(self, user) + ) { + return (false, "User is blacklisted"); + } + + // If a wallet is not whitelisted and has been signaled more than X times + if ( + (PassportChecksLogicV2._isCheckEnabled(self, PassportTypesV2.CheckType.SIGNALING_CHECK) && + PassportSignalingLogicV2.signaledCounter(self, user) >= PassportSignalingLogicV2.signalingThreshold(self)) + ) { + return (false, "User has been signaled too many times"); + } + + if (PassportChecksLogicV2._isCheckEnabled(self, PassportTypesV2.CheckType.PARTICIPATION_SCORE_CHECK)) { + uint256 participationScore = PassportPoPScoreLogicV2._cumulativeScoreWithDecay( + self, + user, + self.xAllocationVoting.currentRoundId() + ); + + // If the user's cumulated score in the last rounds is greater than or equal to the threshold + if ((participationScore >= PassportPoPScoreLogicV2._thresholdPoPScoreAtTimepoint(self, timepoint))) { + return (true, "User's participation score is above the threshold"); + } + } + + // TODO: With `GalaxyMember` version 2, Check if user's selected `GalaxyMember` `tokenId` is greater than `getMinimumGalaxyMemberLevel(self)` + + // If none of the conditions are met, return false with the default reason + return (false, "User does not meet the criteria to be considered a person"); + } +} diff --git a/contracts/deprecated/V2/ve-better-passport/libraries/PassportPoPScoreLogicV2.sol b/contracts/deprecated/V2/ve-better-passport/libraries/PassportPoPScoreLogicV2.sol new file mode 100644 index 0000000..b525ef2 --- /dev/null +++ b/contracts/deprecated/V2/ve-better-passport/libraries/PassportPoPScoreLogicV2.sol @@ -0,0 +1,378 @@ +// SPDX-License-Identifier: MIT + +// ####### +// ################ +// #################### +// ########### ######### +// ######### ######### +// ####### ######### ######### +// ######### ######### ########## +// ########## ######## #################### +// ########## ######### ######################### +// ################### ############################ +// ################# ########## ######## +// ############## ### ######## +// ############ ######### +// ########## ########## +// ######## ########### +// ### ############ +// ############## +// ################# +// ############## +// ######### + +pragma solidity 0.8.20; + +import { PassportStorageTypesV2 } from "./PassportStorageTypesV2.sol"; +import { PassportTypesV2 } from "./PassportTypesV2.sol"; +import { PassportEntityLogicV2 } from "./PassportEntityLogicV2.sol"; +import { Checkpoints } from "@openzeppelin/contracts/utils/structs/Checkpoints.sol"; +import { PassportClockLogicV2 } from "./PassportClockLogicV2.sol"; + +/** + * @title PassportPoPScoreLogicV2 + * @dev This library manages the Proof of Participation (PoP) score system for the Passport system. + * Users gain PoP scores by performing actions in XApps. The scores are influenced by the security level of the app, + * exponential decay, and various other factors. The PoP score can determine if a user qualifies as a person in the Passport system. + */ +library PassportPoPScoreLogicV2 { + using Checkpoints for Checkpoints.Trace208; + + // ---------- Events ---------- // + /// @notice Emitted when a user registers an action + /// @param user - the user that registered the action + /// @param passport - the passport address of the user + /// @param appId - the app id of the action + /// @param round - the round of the action + /// @param actionScore - the score of the action + event RegisteredAction( + address indexed user, + address passport, + bytes32 indexed appId, + uint256 indexed round, + uint256 actionScore + ); + // ---------- Constants ---------- // + + /// @dev Scaling factor for the exponential decay + uint256 private constant scalingFactor = 1e18; + + // ---------- Getters ---------- // + /// @notice Gets the cumulative score of a user based on exponential decay for a number of last roundst + /// @param user - the user address + /// @param lastRound - the round to consider as a starting point for the cumulative score + function getCumulativeScoreWithDecay( + PassportStorageTypesV2.PassportStorage storage self, + address user, + uint256 lastRound + ) external view returns (uint256) { + return _cumulativeScoreWithDecay(self, user, lastRound); + } + + /// @notice Gets the round score of a user + /// @param user - the user address + /// @param round - the round + function userRoundScore( + PassportStorageTypesV2.PassportStorage storage self, + address user, + uint256 round + ) internal view returns (uint256) { + return self.userRoundScore[user][round]; + } + + /// @notice Gets the total score of a user + /// @param user - the user address + function userTotalScore( + PassportStorageTypesV2.PassportStorage storage self, + address user + ) internal view returns (uint256) { + return self.userTotalScore[user]; + } + + /// @notice Gets the score of a user for an app in a round + /// @param user - the user address + /// @param round - the round + /// @param appId - the app id + function userRoundScoreApp( + PassportStorageTypesV2.PassportStorage storage self, + address user, + uint256 round, + bytes32 appId + ) internal view returns (uint256) { + return self.userAppRoundScore[user][round][appId]; + } + + /// @notice Gets the total score of a user for an app + /// @param user - the user address + /// @param appId - the app id + function userAppTotalScore( + PassportStorageTypesV2.PassportStorage storage self, + address user, + bytes32 appId + ) internal view returns (uint256) { + return self.userAppTotalScore[user][appId]; + } + + /// @notice Gets the threshold for a user to be considered a person + function thresholdPoPScore(PassportStorageTypesV2.PassportStorage storage self) internal view returns (uint256) { + return self.popScoreThreshold.latest(); + } + + /// @notice Gets the threshold for a user to be considered a person at a specific timepoint + function thresholdPoPScoreAtTimepoint( + PassportStorageTypesV2.PassportStorage storage self, + uint48 timepoint + ) external view returns (uint256) { + return _thresholdPoPScoreAtTimepoint(self, timepoint); + } + + /// @notice Gets the security multiplier for an app security + /// @param security - the app security between LOW, MEDIUM, HIGH + function securityMultiplier( + PassportStorageTypesV2.PassportStorage storage self, + PassportTypesV2.APP_SECURITY security + ) internal view returns (uint256) { + return self.securityMultiplier[security]; + } + + /// @notice Gets the security level of an app + /// @param appId - the app id + function appSecurity( + PassportStorageTypesV2.PassportStorage storage self, + bytes32 appId + ) internal view returns (PassportTypesV2.APP_SECURITY) { + return self.appSecurity[appId]; + } + + /// @notice Gets the round threshold for a user to be considered a person + function roundsForCumulativeScore( + PassportStorageTypesV2.PassportStorage storage self + ) internal view returns (uint256) { + return self.roundsForCumulativeScore; + } + + /// @notice Gets the decay rate for the cumulative score + function decayRate(PassportStorageTypesV2.PassportStorage storage self) internal view returns (uint256) { + return self.decayRate; + } + + // ---------- Setters ---------- // + + /// @notice Registers an action for a user + /// @param user - the user that performed the action + /// @param appId - the app id of the action + function registerAction(PassportStorageTypesV2.PassportStorage storage self, address user, bytes32 appId) external { + _registerAction(self, user, appId, self.xAllocationVoting.currentRoundId()); + } + + /// @notice Registers an action for a user in a round + /// @param user - the user that performed the action + /// @param appId - the app id of the action + /// @param round - the round id of the action + function registerActionForRound( + PassportStorageTypesV2.PassportStorage storage self, + address user, + bytes32 appId, + uint256 round + ) external { + _registerAction(self, user, appId, round); + } + + /// @notice Function used to seed the passport with old actions by aggregating them + /// based on (user, appId, round) and summing up the total score offchain + /// @param user - the user that performed the actions + /// @param appId - the app id of the actions + /// @param round - the round id of the actions + /// @param totalScore - the total score of the actions + function registerAggregatedActionsForRound( + PassportStorageTypesV2.PassportStorage storage self, + address user, + bytes32 appId, + uint256 round, + uint256 totalScore + ) external { + require(user != address(0), "ProofOfParticipation: user is the zero address"); + require(self.x2EarnApps.appExists(appId), "ProofOfParticipation: app does not exist"); + + // Check if the user has attached their entity to a passport, if so, use the passport address, else use the users address (passport) + address passport = PassportEntityLogicV2._getPassportForEntity(self, user); + + // Track unique apps core user has interacted with + if (!self.userUniqueAppInteraction[passport][appId]) { + updateUniqueAppInteractions(self, passport, appId); + } + + // If the entity is linked to a passport and the entity has not interacted with the app track interaction + if (passport != user && !self.userUniqueAppInteraction[user][appId]) { + updateUniqueAppInteractions(self, user, appId); + } + + // Update the user's score for the round + self.userRoundScore[passport][round] += totalScore; + // Update the user's total score + self.userTotalScore[passport] += totalScore; + // Update the user's score for the app in the round + self.userAppRoundScore[passport][round][appId] += totalScore; + // Update the user's total score for the app + self.userAppTotalScore[passport][appId] += totalScore; + + emit RegisteredAction(user, passport, appId, round, totalScore); + } + + /// @notice Sets the threshold for a user to be considered a person + /// @param threshold - the round threshold + function setThresholdPoPScore(PassportStorageTypesV2.PassportStorage storage self, uint208 threshold) external { + self.popScoreThreshold.push(PassportClockLogicV2.clock(), threshold); + } + + /// @notice Sets the number of rounds to consider for the cumulative score + /// @param rounds - the number of rounds + function setRoundsForCumulativeScore(PassportStorageTypesV2.PassportStorage storage self, uint256 rounds) external { + require(rounds > 0, "ProofOfParticipation: rounds is zero"); + + self.roundsForCumulativeScore = rounds; + } + + /// @notice Sets the security multiplier + /// @param security - the app security between LOW, MEDIUM, HIGH + /// @param multiplier - the multiplier + function setSecurityMultiplier( + PassportStorageTypesV2.PassportStorage storage self, + PassportTypesV2.APP_SECURITY security, + uint256 multiplier + ) external { + require(multiplier > 0, "ProofOfParticipation: multiplier is zero"); + + self.securityMultiplier[security] = multiplier; + } + + /// @dev Sets the security level of an app + /// @param appId - the app id + /// @param security - the security level + function setAppSecurity( + PassportStorageTypesV2.PassportStorage storage self, + bytes32 appId, + PassportTypesV2.APP_SECURITY security + ) external { + self.appSecurity[appId] = security; + } + + /// @notice Sets the decay rate for the exponential decay + /// @param newDecayRate - the decay rate + function setDecayRate(PassportStorageTypesV2.PassportStorage storage self, uint256 newDecayRate) external { + self.decayRate = newDecayRate; + } + + // ---------- Internal & Private ---------- // + + /// @dev Gets the cumulative score of a user based on exponential decay for a number of last rounds + /// @dev This function calculates the decayed score f(t) = a * (1 - r)^t + /// @param user - the user address + /// @param lastRound - the round to consider as a starting point for the cumulative score + function _cumulativeScoreWithDecay( + PassportStorageTypesV2.PassportStorage storage self, + address user, + uint256 lastRound + ) internal view returns (uint256) { + // Calculate the starting round for the cumulative score. If the last round is less than the rounds for cumulative score, start from the first round + uint256 startingRound = lastRound <= self.roundsForCumulativeScore + ? 1 + : lastRound - self.roundsForCumulativeScore + 1; + + uint256 decayFactor = ((100 - self.decayRate) * scalingFactor) / 100; + + // Calculate the cumulative score with exponential decay + uint256 cumulativeScore = 0; + for (uint256 round = startingRound; round <= lastRound; round++) { + cumulativeScore = self.userRoundScore[user][round] + (cumulativeScore * decayFactor) / scalingFactor; + } + + return cumulativeScore; + } + + /** + * @dev Internal funciton to get the threshold for a user to be considered a person at a specific timepoint + */ + function _thresholdPoPScoreAtTimepoint( + PassportStorageTypesV2.PassportStorage storage self, + uint48 timepoint + ) internal view returns (uint256) { + return self.popScoreThreshold.upperLookupRecent(timepoint); + } + + /** + * @dev Registers an action for a user in a specific round. If the user is an entity attached to a passport, + * the passport will receive the score instead of the entity. The score is calculated based on the security level of the app. + * @param self The storage object for the Passport contract. + * @param user The address of the user (or entity) that performed the action. + * @param appId The ID of the app where the action took place. + * @param round The round or timepoint in which the action occurred. + */ + function _registerAction( + PassportStorageTypesV2.PassportStorage storage self, + address user, + bytes32 appId, + uint256 round + ) private { + require(user != address(0), "ProofOfParticipation: user is the zero address"); + + require(self.x2EarnApps.appExists(appId), "ProofOfParticipation: app does not exist"); + + // If app was just added and the security level is not set, set it to LOW by default + if (self.appSecurity[appId] == PassportTypesV2.APP_SECURITY.NONE) { + return; + } + + // If user is blacklisted, do not register the action + if (self.blacklisted[user]) { + return; + } + + // Check if the user has attached their entity to a passport, if so, use the passport address, else use the users address (passport) + address passport = PassportEntityLogicV2._getPassportForEntity(self, user); + + // Track unique apps core user has interacted with + if (!self.userUniqueAppInteraction[passport][appId]) { + updateUniqueAppInteractions(self, passport, appId); + } + + // If the entity is linked to a passport and the entity has not interacted with the app track interaction + if (passport != user && !self.userUniqueAppInteraction[user][appId]) { + updateUniqueAppInteractions(self, user, appId); + } + + // Calculate the action score, can be min 0, max 6 + uint256 actionScore = self.securityMultiplier[self.appSecurity[appId]]; + + // Update the user's score for the round + self.userRoundScore[passport][round] += actionScore; + // Update the user's total score + self.userTotalScore[passport] += actionScore; + // Update the user's score for the app in the round + self.userAppRoundScore[passport][round][appId] += actionScore; + // Update the user's total score for the app + self.userAppTotalScore[passport][appId] += actionScore; + + emit RegisteredAction(user, passport, appId, round, actionScore); + } + + /** + * @dev Updates the record of unique app interactions for a user. If this is the user's first interaction + * with the specified app, the function marks the interaction as unique and stores the app ID in the user's + * list of interacted apps. + * @param self The storage object for the Passport contract. + * @param user The address of the user whose app interactions are being tracked. + * @param appId The ID of the app that the user has interacted with. + */ + function updateUniqueAppInteractions( + PassportStorageTypesV2.PassportStorage storage self, + address user, + bytes32 appId + ) internal { + // This is the first time the user interacts with this app + self.userUniqueAppInteraction[user][appId] = true; + + // Add the appId to the user's interacted apps array + self.userInteractedApps[user].push(appId); + } +} diff --git a/contracts/deprecated/V2/ve-better-passport/libraries/PassportSignalingLogicV2.sol b/contracts/deprecated/V2/ve-better-passport/libraries/PassportSignalingLogicV2.sol new file mode 100644 index 0000000..f87d193 --- /dev/null +++ b/contracts/deprecated/V2/ve-better-passport/libraries/PassportSignalingLogicV2.sol @@ -0,0 +1,313 @@ +// SPDX-License-Identifier: MIT + +// ####### +// ################ +// #################### +// ########### ######### +// ######### ######### +// ####### ######### ######### +// ######### ######### ########## +// ########## ######## #################### +// ########## ######### ######################### +// ################### ############################ +// ################# ########## ######## +// ############## ### ######## +// ############ ######### +// ########## ########## +// ######## ########### +// ### ############ +// ############## +// ################# +// ############## +// ######### + +pragma solidity 0.8.20; + +import { PassportStorageTypesV2 } from "./PassportStorageTypesV2.sol"; +import { PassportClockLogicV2 } from "./PassportClockLogicV2.sol"; +import { PassportEntityLogicV2 } from "./PassportEntityLogicV2.sol"; +import { PassportEIP712SigningLogicV2 } from "./PassportEIP712SigningLogicV2.sol"; +import { PassportTypesV2 } from "./PassportTypesV2.sol"; +import { Checkpoints } from "@openzeppelin/contracts/utils/structs/Checkpoints.sol"; +import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; + +/** + * @title PassportSignalingLogicV2 + * @dev A library that manages the signaling system within the Passport ecosystem. + * Signaling is used to track negative or positive behavior for users based on interactions in specific apps. + * This library allows for signaling users, assigning signalers to apps, resetting signals, and managing app-specific signals. + */ +library PassportSignalingLogicV2 { + // ---------- Events ---------- // + /// @notice Emitted when a user is signaled. + /// @param user The address of the user that was signaled. + /// @param signaler The address of the user that signaled the user. + /// @param app The app that the user was signaled for. + /// @param reason The reason for signaling the user. + event UserSignaled(address indexed user, address indexed signaler, bytes32 indexed app, string reason); + + /// @notice Emited when an address is associated with an app. + /// @param signaler The address of the signaler. + /// @param app The app that the signaler was associated with. + event SignalerAssignedToApp(address indexed signaler, bytes32 indexed app); + + /// @notice Emitted when an address is removed from an app. + /// @param signaler The address of the signaler. + /// @param app The app that the signaler was removed from. + event SignalerRemovedFromApp(address indexed signaler, bytes32 indexed app); + + /// @notice Emitted when a user's signals are reset. + /// @param user The address of the user that had their signals reset. + /// @param reason The reason for resetting the signals. + event UserSignalsReset(address indexed user, string reason); + + /// @notice Emitted when a user's signals are reset for an app. + /// @param user The address of the user that had their signals reset. + /// @param app The app that the user had their signals reset for. + /// @param reason - The reason for resetting the signals. + event UserSignalsResetForApp(address indexed user, bytes32 indexed app, string reason); + + // ---------- Getters ---------- // + + /// @notice Returns the number of times a user has been signaled + function signaledCounter( + PassportStorageTypesV2.PassportStorage storage self, + address user + ) internal view returns (uint256) { + return self.signaledCounter[user]; + } + + /// @notice Returns the belonging app of a signaler + function appOfSignaler( + PassportStorageTypesV2.PassportStorage storage self, + address signaler + ) internal view returns (bytes32) { + return self.appOfSignaler[signaler]; + } + + /// @notice Returns the number of times a user has been signaled by an app + function appSignalsCounter( + PassportStorageTypesV2.PassportStorage storage self, + bytes32 app, + address user + ) internal view returns (uint256) { + return self.appSignalsCounter[app][user]; + } + + /// @notice Returns the total number of signals for an app + function appTotalSignalsCounter( + PassportStorageTypesV2.PassportStorage storage self, + bytes32 app + ) internal view returns (uint256) { + return self.appTotalSignalsCounter[app]; + } + + /// @notice Returns the signaling threshold + function signalingThreshold(PassportStorageTypesV2.PassportStorage storage self) internal view returns (uint256) { + return self.signalsThreshold; + } + + // ---------- Setters ---------- // + + /// @notice Signals a user + function signalUser(PassportStorageTypesV2.PassportStorage storage self, address user) external { + _signalUser(self, user, ""); + } + + /// @notice Signals a user with a reason + function signalUserWithReason( + PassportStorageTypesV2.PassportStorage storage self, + address user, + string memory reason + ) external { + _signalUser(self, user, reason); + } + + /// @notice this method allows an app admin to assign a signaler to an app + /// @param app - the app to assign the signaler to + /// @param user - the signaler to assign to the app + function assignSignalerToAppByAppAdmin( + PassportStorageTypesV2.PassportStorage storage self, + bytes32 app, + address user + ) external { + require(self.x2EarnApps.isAppAdmin(app, msg.sender), "BotSignaling: caller is not an admin of the app"); + + assignSignalerToApp(self, app, user); + } + + /// @notice this method allows an app admin to remove a signaler from an app + /// @param user - the signaler to remove from the app + function removeSignalerFromAppByAppAdmin(PassportStorageTypesV2.PassportStorage storage self, address user) external { + bytes32 app = self.appOfSignaler[user]; + require(self.x2EarnApps.isAppAdmin(app, msg.sender), "BotSignaling: caller is not an admin of the app"); + + removeSignalerFromApp(self, user); + } + + /// @notice Sets the signaling threshold + function setSignalingThreshold(PassportStorageTypesV2.PassportStorage storage self, uint256 threshold) external { + self.signalsThreshold = threshold; + } + + /// @notice Private function to remove a signaler from an app + function removeSignalerFromApp(PassportStorageTypesV2.PassportStorage storage self, address user) public { + require(user != address(0), "BotSignaling: user cannot be zero"); + + // to emit in the event + bytes32 app = self.appOfSignaler[user]; + + self.appOfSignaler[user] = bytes32(0); + + emit SignalerRemovedFromApp(user, app); + } + + /// @notice Private function to assign a signaler to an app + function assignSignalerToApp(PassportStorageTypesV2.PassportStorage storage self, bytes32 app, address user) public { + require(app != bytes32(0), "BotSignaling: app cannot be zero"); + require(user != address(0), "BotSignaling: user cannot be zero"); + + self.appOfSignaler[user] = app; + emit SignalerAssignedToApp(user, app); + } + + /// @notice Resets the signals of a user + ///@param self - the passport storage + /// @param user - the user to reset the signals of + /// @param reason - the reason for resetting the signals + function resetUserSignals( + PassportStorageTypesV2.PassportStorage storage self, + address user, + string memory reason + ) external { + // Get the signals + uint256 signals = self.signaledCounter[user]; + + // Reset the signals + self.signaledCounter[user] = 0; + + // Get the passport address if the user has attached their entity to a passport + address passport = PassportEntityLogicV2._getPassportForEntity(self, user); + if (user != passport) { + self.signaledCounter[passport] -= signals; + } + + emit UserSignalsReset(user, reason); + } + + /// @notice Resets the signals of a user + /// @param user - the user to reset the signals of + /// @param reason - the reason for resetting the signals + function resetUserSignalsByAppAdminWithReason( + PassportStorageTypesV2.PassportStorage storage self, + address user, + string memory reason + ) external { + bytes32 app = self.appOfSignaler[msg.sender]; + require(self.x2EarnApps.isAppAdmin(app, msg.sender), "BotSignaling: caller is not an admin of the app"); + + _resetUserSignalsOfApp(self, user, app, reason); + } + + // ---------- Private ---------- // + + /// @notice Private function to signal a user + function _signalUser( + PassportStorageTypesV2.PassportStorage storage self, + address user, + string memory reason + ) private { + self.signaledCounter[user]++; + + bytes32 app = self.appOfSignaler[msg.sender]; + self.appSignalsCounter[app][user]++; + self.appTotalSignalsCounter[app]++; + + // Check if the user has attached their entity to a passport, if so, also signal the passport + address passport = PassportEntityLogicV2._getPassportForEntity(self, user); + if (user != passport) { + self.signaledCounter[passport]++; + self.appSignalsCounter[app][passport]++; + } + + emit UserSignaled(user, msg.sender, app, reason); + } + + /// @notice Resets the signals of a user for an app + /// @param user - the user to reset the signals of + /// @param app - the app to reset the signals for + /// @param reason - the reason for resetting the signals + function _resetUserSignalsOfApp( + PassportStorageTypesV2.PassportStorage storage self, + address user, + bytes32 app, + string memory reason + ) private { + // Get the passport address if the user has attached their entity to a passport + address passport = PassportEntityLogicV2._getPassportForEntity(self, user); + + uint256 signals = self.appSignalsCounter[app][user]; + + self.appSignalsCounter[app][user] = 0; + self.appTotalSignalsCounter[app] -= signals; + self.signaledCounter[user] -= signals; + + if (user != passport) { + self.signaledCounter[passport] -= signals; + self.appSignalsCounter[app][passport] -= signals; + } + + emit UserSignalsResetForApp(user, app, reason); + } + + /** + * @dev Attaches the signals of an entity to its corresponding passport. If an entity has interacted with apps + * and accumulated signals, this function aggregates those signals and assigns them to the passport. + * This includes both the total signal count and the signals for each app the entity has interacted with. + * @param self The storage object for the Passport contract. + * @param entity The address of the entity whose signals are being attached to the passport. + * @param passport The address of the passport to which the entity's signals will be attached. + */ + function attachEntitySignalsToPassport( + PassportStorageTypesV2.PassportStorage storage self, + address entity, + address passport + ) internal { + // Attach the signals of the entity to the passport + self.signaledCounter[passport] += self.signaledCounter[entity]; + + // Get the unique apps that the entity has interacted with + bytes32[] memory apps = self.userInteractedApps[entity]; + // Attach the signals of the entity to the passport for each app + for (uint256 i = 0; i < apps.length; i++) { + bytes32 appId = apps[i]; + self.appSignalsCounter[appId][passport] += self.appSignalsCounter[appId][entity]; + } + } + + /** + * @dev Removes the signals of an entity from the corresponding passport. This function deducts + * all signal data from the entity that was previously transferred to the passport, including both the total signal count + * and app-specific signals. + * @param self The storage object for the Passport contract. + * @param entity The address of the entity whose signals will be removed from the passport. + * @param passport The address of the passport that will have the signals removed. + */ + function removeEntitySignalsFromPassport( + PassportStorageTypesV2.PassportStorage storage self, + address entity, + address passport + ) internal { + // Remove the signals of the entity from the passport + self.signaledCounter[passport] -= self.signaledCounter[entity]; + + // Get the unique apps that the entity has interacted with + bytes32[] memory apps = self.userInteractedApps[entity]; + // Remove the signals of the entity from the passport for each app + for (uint256 i = 0; i < apps.length; i++) { + bytes32 appId = apps[i]; + self.appSignalsCounter[appId][passport] -= self.appSignalsCounter[appId][entity]; + } + } +} diff --git a/contracts/deprecated/V2/ve-better-passport/libraries/PassportStorageTypesV2.sol b/contracts/deprecated/V2/ve-better-passport/libraries/PassportStorageTypesV2.sol new file mode 100644 index 0000000..95a6580 --- /dev/null +++ b/contracts/deprecated/V2/ve-better-passport/libraries/PassportStorageTypesV2.sol @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: MIT + +// ####### +// ################ +// #################### +// ########### ######### +// ######### ######### +// ####### ######### ######### +// ######### ######### ########## +// ########## ######## #################### +// ########## ######### ######################### +// ################### ############################ +// ################# ########## ######## +// ############## ### ######## +// ############ ######### +// ########## ########## +// ######## ########### +// ### ############ +// ############## +// ################# +// ############## +// ######### + +pragma solidity 0.8.20; + +import { IXAllocationVotingGovernor } from "../../../../interfaces/IXAllocationVotingGovernor.sol"; +import { IGalaxyMemberV2 } from "../../../V2/interfaces/IGalaxyMemberV2.sol"; +import { IX2EarnApps } from "../../../../interfaces/IX2EarnApps.sol"; +import { PassportTypesV2 } from "./PassportTypesV2.sol"; +import { Checkpoints } from "@openzeppelin/contracts/utils/structs/Checkpoints.sol"; + +/** + * @title PassportStorageTypesV2 + * @notice This library defines the primary storage types used within the Passport contract. + * It uses the ERC-7201 Storage Namespaces standard to separate storage concerns efficiently. + * + * The storage includes configurations for personhood checks, external contract references, + * whitelisting/blacklisting, proof of participation, passport delegation, bot signaling, + * and entity linkage to passports. + * + * @dev This library manages complex contract state by grouping mappings and settings into + * distinct storage types. It leverages the ERC-7201 standard for organizing these namespaces. + */ +library PassportStorageTypesV2 { + struct PassportStorage { + // ------------------ Passport Settings ------------------ // + // Bitmask of enabled checks (e.g. whitelist, blacklist, signaling, etc.) + uint256 personhoodChecks; + // Minimum galaxy member level required for personhood + uint256 minimumGalaxyMemberLevel; + // ---------- External Contracts ---------- // + // Address of the xAllocationVoting contract + IXAllocationVotingGovernor xAllocationVoting; + // Address of the galaxy member contract + IGalaxyMemberV2 galaxyMember; + // Address of the x2EarnApps contract + IX2EarnApps x2EarnApps; + // ---------- Blacklisted and Whitelisted info ---------- // + // Mapping of whitelisted users + mapping(address user => bool) whitelisted; + // Mapping of blacklisted users + mapping(address user => bool) blacklisted; + // Track number of whitelisted entities + mapping(address => uint256) whitelistedEntitiesCounter; + // Track number of blacklisted entities + mapping(address => uint256) blacklistedEntitiesCounter; + // Threshold percentage of whitelisted entities for a passport to be considered whitelisted + uint256 whitelistThreshold; + // Threshold percentage of blacklisted entities for a passport to be considered blacklisted + uint256 blacklistThreshold; + // ---------- Proof of Participation ---------- // + // Multiplier of the base action score based on the app security + mapping(PassportTypesV2.APP_SECURITY security => uint256 multiplier) securityMultiplier; + // Security level of an app -> will be UNDEFINED and set to LOW by default + mapping(bytes32 appId => PassportTypesV2.APP_SECURITY security) appSecurity; + // All-time total score of a user + mapping(address user => uint256 totalScore) userTotalScore; + // All-time total score of a user for a specific app + mapping(address user => mapping(bytes32 appId => uint256 totalScore)) userAppTotalScore; + // Score of a user in a specific round + mapping(address user => mapping(uint256 round => uint256 score)) userRoundScore; + // Score of a user for a specific app in a specific round + mapping(address user => mapping(uint256 round => mapping(bytes32 appId => uint256 score))) userAppRoundScore; + // Checkpointed threshold for a user to be considered a person in a round + Checkpoints.Trace208 popScoreThreshold; + // Number of rounds to consider for the cumulative score + uint256 roundsForCumulativeScore; + // Decay rate for the exponential decay + uint256 decayRate; + // Track which apps a user has interacted with + mapping(address => mapping(bytes32 => bool)) userUniqueAppInteraction; + // Store the list of apps a user has interacted with + mapping(address => bytes32[]) userInteractedApps; + // Track when as user attached an entity to their passport + mapping(address => uint256) entityAttachRound; + // ---------- Passport Entities ---------- // + // Mapping of entity to passport + mapping(address => Checkpoints.Trace160) entityToPassport; + // Mapping to track index of entities for each passport + mapping(address => uint256) passportEntitiesIndexes; + // Mapping of passport to entities + mapping(address => address[]) passportToEntities; + // Mapping of passport to pending entities indexes + mapping(address => uint256) pendingLinksIndexes; + // Mapping of passport to pending entities + mapping(address => address[]) pendingLinksPassportToEntities; + // Mapping of pending entities to passport + mapping(address => address) pendingLinksEntityToPassport; + // Limit of entities that can be attached to a passport + uint256 maxEntitiesPerPassport; + // ---------- Passport Delegation ---------- // + // Mapping of delegator to delegatee + mapping(address => Checkpoints.Trace160) delegatorToDelegatee; + // Mapping of delegatee to delegator + mapping(address => Checkpoints.Trace160) delegateeToDelegator; + // Mapping to track index of pending delegations for each delegator + mapping(address => uint256) pendingDelegationsIndexes; + // Mapping to track pending delegations for each delegatee + mapping(address => address[]) pendingDelegationsDelegateeToDelegators; + // Mapping to map delagator to delegatee for pending delegations + mapping(address => address) pendingDelegationsDelegatorToDelegatee; + // ---------- Bot Signaling ---------- // + // Counter for the number of signals per user + mapping(address user => uint256) signaledCounter; + // Threshold for a user to be considered a bot + uint256 signalsThreshold; + // Mapping of signaler to app + mapping(address signaler => bytes32 app) appOfSignaler; + // Mapping of apps to signaled users + mapping(bytes32 app => mapping(address user => uint256)) appSignalsCounter; + // Mapping of apps to total signals + mapping(bytes32 app => uint256) appTotalSignalsCounter; + } +} diff --git a/contracts/deprecated/V2/ve-better-passport/libraries/PassportTypesV2.sol b/contracts/deprecated/V2/ve-better-passport/libraries/PassportTypesV2.sol new file mode 100644 index 0000000..ed3d80e --- /dev/null +++ b/contracts/deprecated/V2/ve-better-passport/libraries/PassportTypesV2.sol @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: MIT + +// ####### +// ################ +// #################### +// ########### ######### +// ######### ######### +// ####### ######### ######### +// ######### ######### ########## +// ########## ######## #################### +// ########## ######### ######################### +// ################### ############################ +// ################# ########## ######## +// ############## ### ######## +// ############ ######### +// ########## ########## +// ######## ########### +// ### ############ +// ############## +// ################# +// ############## +// ######### + +pragma solidity 0.8.20; + +import { IXAllocationVotingGovernor } from "../../../../interfaces/IXAllocationVotingGovernor.sol"; +import { IX2EarnApps } from "../../../../interfaces/IX2EarnApps.sol"; +import { IGalaxyMemberV2 } from "../../../V2/interfaces/IGalaxyMemberV2.sol"; + +/** + * @title PassportTypesV2 + * @notice This library defines various data types, enumerations, and initialization parameters used within the Passport contract. + * It includes the `InitializationData` struct, which contains references to external contracts and configurations for personhood checks, + * proof of participation, signaling, and passport delegation. It also includes role-based configuration settings. + */ +library PassportTypesV2 { + /** + * @dev Struct containing data to initialize the contract + * @param xAllocationVoting The address of the xAllocationVoting + * @param x2EarnApps The address of the x2EarnApps + * @param galaxyMember The address of the galaxy member contract + * @param upgrader The address of the upgrader + * @param admins The addresses of the admins + * @param settingsManagers The addresses of the settings managers + * @param roleGranters The addresses of the role granters + * @param blacklisters The addresses of the blacklisters + * @param whitelisters The addresses of the whitelisters + * @param actionRegistrar The address of the action registrar + * @param actionScoreManager The address of the action score manager + * @param popScoreThreshold The threshold proof of participation score for a wallet to be considered a person + * @param signalingThreshold The threshold for a proposal to be active + * @param roundsForCumulativeScore The number of rounds for cumulative score + */ + struct InitializationData { + IXAllocationVotingGovernor xAllocationVoting; + IX2EarnApps x2EarnApps; + IGalaxyMemberV2 galaxyMember; + uint256 signalingThreshold; + uint256 roundsForCumulativeScore; + uint256 minimumGalaxyMemberLevel; + uint256 blacklistThreshold; + uint256 whitelistThreshold; + uint256 maxEntitiesPerPassport; + uint256 decayRate; + } + + struct InitializationRoleData { + address admin; + address botSignaler; + address upgrader; + address settingsManager; + address roleGranter; + address blacklister; + address whitelister; + address actionRegistrar; + address actionScoreManager; + } + + enum CheckType { + UNDEFINED, // Default value for invalid or uninitialized checks + WHITELIST_CHECK, // Check if the user is whitelisted + BLACKLIST_CHECK, // Check if the user is blacklisted + SIGNALING_CHECK, // Check if the user has been signaled too many times + PARTICIPATION_SCORE_CHECK, // Check the user's participation score + GM_OWNERSHIP_CHECK // Check if the user owns a GM token + } + + /// @notice Security level indicates how secure the app is + /// @dev App security is used to calculate the overall score of a sustainable action + enum APP_SECURITY { + NONE, + LOW, + MEDIUM, + HIGH + } +} diff --git a/contracts/deprecated/V2/ve-better-passport/libraries/PassportWhitelistAndBlacklistLogicV2.sol b/contracts/deprecated/V2/ve-better-passport/libraries/PassportWhitelistAndBlacklistLogicV2.sol new file mode 100644 index 0000000..25501a4 --- /dev/null +++ b/contracts/deprecated/V2/ve-better-passport/libraries/PassportWhitelistAndBlacklistLogicV2.sol @@ -0,0 +1,344 @@ +// SPDX-License-Identifier: MIT + +// ####### +// ################ +// #################### +// ########### ######### +// ######### ######### +// ####### ######### ######### +// ######### ######### ########## +// ########## ######## #################### +// ########## ######### ######################### +// ################### ############################ +// ################# ########## ######## +// ############## ### ######## +// ############ ######### +// ########## ########## +// ######## ########### +// ### ############ +// ############## +// ################# +// ############## +// ######### + +pragma solidity 0.8.20; + +import { PassportStorageTypesV2 } from "./PassportStorageTypesV2.sol"; +import { PassportEntityLogicV2 } from "./PassportEntityLogicV2.sol"; + +/** + * @title PassportWhitelistAndBlacklistLogicV2 + * @dev This library manages the whitelisting and blacklisting of users and passports in the Passport system. + * It provides functionality to add or remove users from the whitelist/blacklist, and to check a passport's status based on linked entities. + */ +library PassportWhitelistAndBlacklistLogicV2 { + // ---------- Events ---------- // + /// @notice Emitted when a user is whitelisted + /// @param user - the user that is whitelisted + /// @param whitelistedBy - the user that whitelisted the user + event UserWhitelisted(address indexed user, address indexed whitelistedBy); + + /// @notice Emitted when a user is removed from the whitelist + /// @param user - the user that is removed from the whitelist + /// @param removedBy - the user that removed the user from the whitelist + event RemovedUserFromWhitelist(address indexed user, address indexed removedBy); + + /// @notice Emitted when a user is blacklisted + /// @param user - the user that is blacklisted + /// @param blacklistedBy - the user that blacklisted the user + event UserBlacklisted(address indexed user, address indexed blacklistedBy); + + /// @notice Emitted when a user is removed from the blacklist + /// @param user - the user that is removed from the blacklist + /// @param removedBy - the user that removed the user from the blacklist + event RemovedUserFromBlacklist(address indexed user, address indexed removedBy); + + // ---------- Getters ---------- // + + /// @notice Returns if a user is whitelisted + function isWhitelisted( + PassportStorageTypesV2.PassportStorage storage self, + address user + ) internal view returns (bool) { + return self.whitelisted[user]; + } + + /// @notice Returns if a user is blacklisted + function isBlacklisted( + PassportStorageTypesV2.PassportStorage storage self, + address user + ) internal view returns (bool) { + return self.blacklisted[user]; + } + + /// @notice return the blacklist threshold + function blacklistThreshold(PassportStorageTypesV2.PassportStorage storage self) internal view returns (uint256) { + return self.blacklistThreshold; + } + + /// @notice return the whitelist threshold + function whitelistThreshold(PassportStorageTypesV2.PassportStorage storage self) internal view returns (uint256) { + return self.whitelistThreshold; + } + + /** + * @notice Checks if a passport is whitelisted based on a threshold percentage of linked entities. + * @dev This function checks if the passport itself is whitelisted or if the number of whitelisted entities + * linked to the passport exceeds the given threshold percentage of the total entities linked to the passport. + * It first checks if the passport is directly whitelisted. If not, it calculates the percentage of whitelisted + * entities linked to the passport and compares it to the threshold. + * @param self The storage reference for PassportStorage. + * @param passport The address of the passport being checked. + * @return True if the passport is whitelisted based on the threshold, otherwise false. + */ + function isPassportWhitelisted( + PassportStorageTypesV2.PassportStorage storage self, + address passport + ) external view returns (bool) { + return _isPassportWhitelisted(self, passport); + } + + /** + * @notice Checks if a passport is blacklisted based on a threshold percentage of linked entities. + * @dev This function checks if the passport itself is blacklisted or if the number of blacklisted entities + * linked to the passport exceeds the given threshold percentage of the total entities linked to the passport. + * It first checks if the passport is directly blacklisted. If not, it calculates the percentage of blacklisted + * entities linked to the passport and compares it to the specified threshold. + * @param self The storage reference for PassportStorage. + * @param passport The address of the passport being checked. + * @return True if the passport is blacklisted based on the threshold, otherwise false. + */ + function isPassportBlacklisted( + PassportStorageTypesV2.PassportStorage storage self, + address passport + ) external view returns (bool) { + return _isPassportBlacklisted(self, passport); + } + + // ---------- Setters ---------- // + + /// @notice user can be whitelisted but the counter will not be reset + function whitelist(PassportStorageTypesV2.PassportStorage storage self, address user) external { + // Check if the user is blacklisted and remove them from the blacklist + if (isBlacklisted(self, user)) removeFromBlacklist(self, user); + + // Whitelist the user + self.whitelisted[user] = true; + + // Check if the user has a passport and update the whitelist counter + _updatePassportWhitelistCounter(self, user, true); + + emit UserWhitelisted(user, msg.sender); + } + + /// @notice Removes a user from the whitelist + function removeFromWhitelist(PassportStorageTypesV2.PassportStorage storage self, address user) public { + self.whitelisted[user] = false; + + // Check if the user has a passport and update the whitelist counter + _updatePassportWhitelistCounter(self, user, false); + + emit RemovedUserFromWhitelist(user, msg.sender); + } + + /// @notice user can be blacklisted but the counter will not be reset + function blacklist(PassportStorageTypesV2.PassportStorage storage self, address user) external { + // Check if the user is whitelisted and remove them from the whitelist + if (isWhitelisted(self, user)) removeFromWhitelist(self, user); + + self.blacklisted[user] = true; + + // Check if the user has a passport and update the blacklist counter + _updatePassportBlacklistCounter(self, user, true); + + emit UserBlacklisted(user, msg.sender); + } + + /// @notice Removes a user from the blacklist + function removeFromBlacklist(PassportStorageTypesV2.PassportStorage storage self, address user) public { + self.blacklisted[user] = false; + + // Check if the user has a passport and update the blacklist counter + _updatePassportBlacklistCounter(self, user, false); + + emit RemovedUserFromBlacklist(user, msg.sender); + } + + /// @notice Sets the threshold percentage of whitelisted entities for a passport to be considered whitelisted + function setWhitelistThreshold(PassportStorageTypesV2.PassportStorage storage self, uint256 threshold) external { + self.whitelistThreshold = threshold; + } + + /// @notice Sets the threshold percentage of blacklisted entities for a passport to be considered blacklisted + function setBlacklistThreshold(PassportStorageTypesV2.PassportStorage storage self, uint256 threshold) external { + self.blacklistThreshold = threshold; + } + + // ---------- Internal & Private ---------- // + /** + * @notice Assigns an entity's whitelist and blacklist status to a passport when an entity is added to a passport. + * @dev This function checks whether the entity is whitelisted or blacklisted and updates the corresponding counters on the passport. + * If the entity is whitelisted, the passport's whitelist counter is incremented. Similarly, if the entity is blacklisted, the blacklist counter is incremented. + * @param self The storage reference for PassportStorage. + * @param entity The address of the entity whose whitelist/blacklist status is being assigned. + * @param passport The address of the passport to which the entity's whitelist/blacklist status is being assigned. + */ + function attachEntitiesBlackAndWhiteListsToPassport( + PassportStorageTypesV2.PassportStorage storage self, + address entity, + address passport + ) internal { + uint256 _whitelist = isWhitelisted(self, entity) ? 1 : 0; + uint256 _blacklist = isBlacklisted(self, entity) ? 1 : 0; + + self.whitelistedEntitiesCounter[passport] += _whitelist; + self.blacklistedEntitiesCounter[passport] += _blacklist; + } + + /** + * @notice Removes an entity's whitelist and blacklist status from a passport when an entity is removed from a passport. + * @dev This function checks whether the entity is whitelisted or blacklisted and decrements the corresponding counters on the passport. + * If the entity is whitelisted, the passport's whitelist counter is decremented. Similarly, if the entity is blacklisted, the blacklist counter is decremented. + * @param self The storage reference for PassportStorage. + * @param entity The address of the entity whose whitelist/blacklist status is being removed. + * @param passport The address of the passport from which the entity's whitelist/blacklist status is being removed. + */ + function removeEntitiesBlackAndWhiteListsFromPassport( + PassportStorageTypesV2.PassportStorage storage self, + address entity, + address passport + ) internal { + uint256 _whitelist = isWhitelisted(self, entity) ? 1 : 0; + uint256 _blacklist = isBlacklisted(self, entity) ? 1 : 0; + + self.whitelistedEntitiesCounter[passport] -= _whitelist; + self.blacklistedEntitiesCounter[passport] -= _blacklist; + } + + /** + * @notice Updates the blacklist counter for a passport based on the increment flag. + * @dev This private function adjusts the blacklist counter of the passport by either incrementing or decrementing it. + * The function checks whether the user is different from the passport before updating the counter. + * @param self The storage reference for PassportStorage. + * @param user The address of the user whose blacklisy status is being checked. + * @param increment A boolean flag indicating whether to increment (true) or decrement (false) the blacklist counter. + */ + function _updatePassportBlacklistCounter( + PassportStorageTypesV2.PassportStorage storage self, + address user, + bool increment + ) private { + address passport = PassportEntityLogicV2._getPassportForEntity(self, user); + + // If the user is the passport, no need to update the counter + if (passport == user) { + return; + } else if (increment) { + self.blacklistedEntitiesCounter[passport] += 1; + } else { + self.blacklistedEntitiesCounter[passport] -= 1; + } + } + + /** + * @notice Updates the whitelist counter for a passport based on the increment flag. + * @dev This private function adjusts the whitelist counter of the passport by either incrementing or decrementing it. + * The function checks whether the user is different from the passport before updating the counter. + * @param self The storage reference for PassportStorage. + * @param user The address of the user whose whitelist status is being checked. + * @param increment A boolean flag indicating whether to increment (true) or decrement (false) the whitelist counter. + */ + function _updatePassportWhitelistCounter( + PassportStorageTypesV2.PassportStorage storage self, + address user, + bool increment + ) private { + address passport = PassportEntityLogicV2._getPassportForEntity(self, user); + + // If the user is the passport, no need to update the counter + if (passport == user) { + return; + } else if (increment) { + self.whitelistedEntitiesCounter[passport] += 1; + } else { + self.whitelistedEntitiesCounter[passport] -= 1; + } + } + + /** + * @notice Checks if a passport is whitelisted based on a threshold percentage of linked entities. + * @dev This function checks if the passport itself is whitelisted or if the number of whitelisted entities + * linked to the passport exceeds the given threshold percentage of the total entities linked to the passport. + * It first checks if the passport is directly whitelisted. If not, it calculates the percentage of whitelisted + * entities linked to the passport and compares it to the threshold. + * @param self The storage reference for PassportStorage. + * @param passport The address of the passport being checked. + * @return True if the passport is whitelisted based on the threshold, otherwise false. + */ + function _isPassportWhitelisted( + PassportStorageTypesV2.PassportStorage storage self, + address passport + ) internal view returns (bool) { + passport = PassportEntityLogicV2._getPassportForEntity(self, passport); + + // Check if the passport itself is whitelisted + if (isWhitelisted(self, passport)) { + return true; + } + + // Get the number of entities the passport has attached + uint256 totalEntities = PassportEntityLogicV2.getEntitiesLinkedToPassport(self, passport).length; + + // If there are no entities, the passport can't be considered whitelisted based on app interactions + if (totalEntities == 0) { + return false; + } + + // Get the number of whitelisted entities attached to the passport + uint256 whitelistedEntities = self.whitelistedEntitiesCounter[passport]; + + // Calculate the percentage of whitelisted entities + uint256 whitelistPercentage = (whitelistedEntities * 100) / totalEntities; + + // Return true if the whitelist percentage exceeds the given threshold percentage + return whitelistPercentage >= self.whitelistThreshold; + } + + /** + * @notice Checks if a passport is blacklisted based on a threshold percentage of linked entities. + * @dev This function checks if the passport itself is blacklisted or if the number of blacklisted entities + * linked to the passport exceeds the given threshold percentage of the total entities linked to the passport. + * It first checks if the passport is directly blacklisted. If not, it calculates the percentage of blacklisted + * entities linked to the passport and compares it to the specified threshold. + * @param self The storage reference for PassportStorage. + * @param passport The address of the passport being checked. + * @return True if the passport is blacklisted based on the threshold, otherwise false. + */ + function _isPassportBlacklisted( + PassportStorageTypesV2.PassportStorage storage self, + address passport + ) internal view returns (bool) { + passport = PassportEntityLogicV2._getPassportForEntity(self, passport); + + // Check if the passport itself is blacklisted + if (isBlacklisted(self, passport)) { + return true; + } + + // Get the number of entities the passport has interacted with + uint256 totalEntities = PassportEntityLogicV2.getEntitiesLinkedToPassport(self, passport).length; + if (totalEntities == 0) { + // If there are no entities, the passport can't be considered blacklisted based on app interactions + return false; + } + + // Get the number of blacklisted entities attached to the passport + uint256 blacklistedEntities = self.blacklistedEntitiesCounter[passport]; + + // Calculate the percentage of blacklisted entities + uint256 blacklistPercentage = (blacklistedEntities * 100) / totalEntities; + + // Return true if the blacklist percentage exceeds the given threshold percentage + return blacklistPercentage >= self.blacklistThreshold; + } +} diff --git a/contracts/deprecated/V3/VoterRewardsV3.sol b/contracts/deprecated/V3/VoterRewardsV3.sol new file mode 100644 index 0000000..4a009ff --- /dev/null +++ b/contracts/deprecated/V3/VoterRewardsV3.sol @@ -0,0 +1,505 @@ +// SPDX-License-Identifier: MIT + +// ####### +// ################ +// #################### +// ########### ######### +// ######### ######### +// ####### ######### ######### +// ######### ######### ########## +// ########## ######## #################### +// ########## ######### ######################### +// ################### ############################ +// ################# ########## ######## +// ############## ### ######## +// ############ ######### +// ########## ########## +// ######## ########### +// ### ############ +// ############## +// ################# +// ############## +// ######### + +pragma solidity 0.8.20; + +import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; +import "../V2/interfaces/IGalaxyMemberV2.sol"; +import "../../interfaces/IB3TRGovernor.sol"; +import "../../interfaces/IXAllocationVotingGovernor.sol"; +import "../../interfaces/IEmissions.sol"; +import "../../interfaces/IB3TR.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import "@openzeppelin/contracts/utils/structs/Checkpoints.sol"; +import "@openzeppelin/contracts/utils/types/Time.sol"; + +/** + * @title VoterRewards + * @author VeBetterDAO + * + * @notice This contract handles the rewards for voters in the VeBetterDAO ecosystem. + * It calculates the rewards for voters based on their voting power and the level of their Galaxy Member NFT. + * + * @dev The contract is + * - upgradeable using UUPSUpgradeable. + * - using AccessControl to handle the admin and upgrader roles. + * - using ReentrancyGuard to prevent reentrancy attacks. + * - following the ERC-7201 standard for storage layout. + * + * Roles: + * - DEFAULT_ADMIN_ROLE: The role that can add new admins and upgraders. It is also the role that can set scaling factor and the Galaxy Member level to multiplier mapping. + * - UPGRADER_ROLE: The role that can upgrade the contract. + * - VOTE_REGISTRAR_ROLE: The role that can register votes for rewards calculation. + * - CONTRACTS_ADDRESS_MANAGER_ROLE: The role that can set the addresses of the contracts used by the VoterRewards contract. + * + * ------------------ Version 2 Changes ------------------ + * - Added quadratic rewarding disabled checkpoints to disable quadratic rewarding for a specific cycle. + * - Added the clock function to get the current block number. + * - Added functions to check if quadratic rewarding is disabled at a specific block number or for the current cycle. + * - Added function to disable quadratic rewarding or re-enable it. + * + * ------------------ Version 3 Changes ------------------ + * - Added the ability to track if a Galaxy Member NFT has voted in a proposal. + * - Added the ability to track if a Vechain node attached to a Galaxy Member NFT has voted in a proposal. + * - Proposal Id is now required when registering votes instead of proposal snapshot. + * - Core logic functions are now virtual allowing to be overridden through inheritance. + */ +contract VoterRewardsV3 is AccessControlUpgradeable, ReentrancyGuardUpgradeable, UUPSUpgradeable { + using Checkpoints for Checkpoints.Trace208; // Checkpoints library for managing checkpoints of the selected level of the user + + /// @notice The role that can register votes for rewards calculation. + bytes32 public constant VOTE_REGISTRAR_ROLE = keccak256("VOTE_REGISTRAR_ROLE"); + + /// @notice The role that can upgrade the contract. + bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE"); + + /// @notice The role that can set the addresses of the contracts used by the VoterRewards contract. + bytes32 public constant CONTRACTS_ADDRESS_MANAGER_ROLE = keccak256("CONTRACTS_ADDRESS_MANAGER_ROLE"); + + /// @notice The scaling factor for the rewards calculation. + uint256 public constant SCALING_FACTOR = 1e6; + + /// @custom:storage-location erc7201:b3tr.storage.VoterRewards + struct VoterRewardsStorage { + IGalaxyMemberV2 galaxyMember; + IB3TR b3tr; + IEmissions emissions; + // level => percentage multiplier for the level of the GM NFT + mapping(uint256 => uint256) levelToMultiplier; + // cycle => total weighted votes in the cycle + mapping(uint256 => uint256) cycleToTotal; + // cycle => voter => total weighted votes for the voter in the cycle + mapping(uint256 cycle => mapping(address voter => uint256 total)) cycleToVoterToTotal; + // ----------------- V2 Additions ---------------- // + // checkpoints for the quadratic rewarding status for each cycle + Checkpoints.Trace208 quadraticRewardingDisabled; + // --------------------------- V3 Additions --------------------------- // + // proposalId => tokenId => hasVoted (keeps track of whether a galaxy member has voted in a proposal) + mapping(uint256 proposalId => mapping(uint256 tokenId => bool)) proposalToGalaxyMemberToHasVoted; + // proposalId => nodeId => hasVoted (keeps track of whether a vechain node has been used while attached to a galaxy member NFT when voting for a proposal) + mapping(uint256 proposalId => mapping(uint256 nodeId => bool)) proposalToNodeToHasVoted; + } + + // keccak256(abi.encode(uint256(keccak256("b3tr.storage.VoterRewards")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant VoterRewardsStorageLocation = + 0x114e7ffaaf205d38cd05b17b56f3357806ef2ce889cb4748445ae91cdfc37c00; + + /// @notice Get the VoterRewardsStorage struct from the specified storage slot specified by the VoterRewardsStorageLocation. + function _getVoterRewardsStorage() internal pure returns (VoterRewardsStorage storage $) { + assembly { + $.slot := VoterRewardsStorageLocation + } + } + + /// @notice Emitted when a user registers their votes for rewards calculation. + /// @param cycle - The cycle in which the votes were registered. + /// @param voter- The address of the voter. + /// @param votes - The number of votes cast by the voter. + /// @param rewardWeightedVote - The reward-weighted vote power for the voter based on their voting power and GM NFT level. + event VoteRegistered(uint256 indexed cycle, address indexed voter, uint256 votes, uint256 rewardWeightedVote); + + /// @notice Emitted when a user claims their rewards. + /// @param cycle - The cycle in which the rewards were claimed. + /// @param voter - The address of the voter. + /// @param reward - The amount of B3TR reward claimed by the voter. + event RewardClaimed(uint256 indexed cycle, address indexed voter, uint256 reward); + + /// @notice Emitted when the Galaxy Member contract address is set. + /// @param newAddress - The address of the new Galaxy Member contract. + /// @param oldAddress - The address of the old Galaxy Member contract. + event GalaxyMemberAddressUpdated(address indexed newAddress, address indexed oldAddress); + + /// @notice Emitted when the Emissions contract address is set. + /// @param newAddress - The address of the new Emissions contract. + /// @param oldAddress - The address of the old Emissions contract. + event EmissionsAddressUpdated(address indexed newAddress, address indexed oldAddress); + + /// @notice Emitted when the level to multiplier mapping is set. + /// @param level - The level of the Galaxy Member NFT. + /// @param multiplier - The percentage multiplier for the level of the Galaxy Member NFT. + event LevelToMultiplierSet(uint256 indexed level, uint256 multiplier); + + /// @notice Emits true if quadratic rewarding is disabled, false otherwise. + /// @param disabled - The flag to enable or disable quadratic rewarding. + event QuadraticRewardingToggled(bool indexed disabled); + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + /// @notice Initialize the VoterRewards contract. + /// @param admin - The address of the admin. + /// @param upgrader - The address of the upgrader. + /// @param contractsAddressManager - The address of the contract address manager. + /// @param _emissions - The address of the emissions contract. + /// @param _galaxyMember - The address of the Galaxy Member contract. + /// @param _b3tr - The address of the B3TR token contract. + /// @param levels - The levels of the Galaxy Member NFTs. + /// @param multipliers - The multipliers for the levels of the Galaxy Member NFTs. + function initialize( + address admin, + address upgrader, + address contractsAddressManager, + address _emissions, + address _galaxyMember, + address _b3tr, + uint256[] memory levels, + uint256[] memory multipliers + ) external initializer { + require(_galaxyMember != address(0), "VoterRewards: _galaxyMember cannot be the zero address"); + require(_emissions != address(0), "VoterRewards: emissions cannot be the zero address"); + require(_b3tr != address(0), "VoterRewards: _b3tr cannot be the zero address"); + + require(levels.length > 0, "VoterRewards: levels must have at least one element"); + require(levels.length == multipliers.length, "VoterRewards: levels and multipliers must have the same length"); + + __AccessControl_init(); + __ReentrancyGuard_init(); + __UUPSUpgradeable_init(); + + VoterRewardsStorage storage $ = _getVoterRewardsStorage(); + + $.galaxyMember = IGalaxyMemberV2(_galaxyMember); + $.b3tr = IB3TR(_b3tr); + $.emissions = IEmissions(_emissions); + + // Set the level to multiplier mapping. + for (uint256 i; i < levels.length; i++) { + $.levelToMultiplier[levels[i]] = multipliers[i]; + } + + require(admin != address(0), "VoterRewards: admin cannot be the zero address"); + _grantRole(DEFAULT_ADMIN_ROLE, admin); + _grantRole(UPGRADER_ROLE, upgrader); + _grantRole(CONTRACTS_ADDRESS_MANAGER_ROLE, contractsAddressManager); + } + + /// @notice Upgrade the implementation of the VoterRewards contract. + /// @dev Only the address with the UPGRADER_ROLE can call this function. + /// @param newImplementation - The address of the new implementation contract. + function _authorizeUpgrade(address newImplementation) internal override onlyRole(UPGRADER_ROLE) {} + + /// @notice Register the votes of a user for rewards calculation. + /// @dev Quadratic rewarding is used to reward users with quadratic-weight based on their voting power and the level of their Galaxy Member NFT. + /// @param proposalId - The ID of the proposal. + /// @param voter - The address of the voter. + /// @param votes - The number of votes cast by the voter. + /// @param votePower - The square root of the total votes cast by the voter. + function registerVote( + uint256 proposalId, + address voter, + uint256 votes, + uint256 votePower + ) public virtual onlyRole(VOTE_REGISTRAR_ROLE) { + // If votePower is zero, exit the function to avoid unnecessary computations. + if (votePower == 0) { + return; + } + + require(proposalId != 0, "VoterRewards: proposalId cannot be 0"); + require(voter != address(0), "VoterRewards: voter cannot be the zero address"); + + VoterRewardsStorage storage $ = _getVoterRewardsStorage(); + + uint256 selectedGMNFT = $.galaxyMember.getSelectedTokenId(voter); + + // Get the current cycle number. + uint256 cycle = $.emissions.getCurrentCycle(); + + // Determine the reward multiplier based on the GM NFT level and if the GM NFT or Vechain node attached have already voted on this proposal. + uint256 multiplier = getMultiplier(selectedGMNFT, proposalId); + + // Set the scaled vote power to the total votes cast by the voter. + uint256 scaledVotePower = votes; + + // Get the block number the emission cycle started. + uint48 emissionCycleStartBlock = SafeCast.toUint48($.emissions.lastEmissionBlock()); + + // If quadratic rewarding is enabled, scale the vote power by 1e9 to counteract the square root operation on 1e18. (0: enabled, 1: disabled) + if ($.quadraticRewardingDisabled.upperLookupRecent(emissionCycleStartBlock) == 0) { + scaledVotePower = votePower * 1e9; + } + + // Calculate the weighted vote power for rewards, adjusting vote power with the level-based multiplier. + // votePower is the square root of the total votes cast by the voter. + uint256 rewardWeightedVote = scaledVotePower + ((scaledVotePower * multiplier) / 100); // Adjusted vote power used for rewards calculation. + + // Update the total reward-weighted votes in the cycle. + $.cycleToTotal[cycle] += rewardWeightedVote; + + // Record the reward-weighted vote power for the voter in the cycle. + $.cycleToVoterToTotal[cycle][voter] += rewardWeightedVote; + + // Record that the GM NFT has voted in the proposal, if it exists. + if (selectedGMNFT != 0) { + $.proposalToGalaxyMemberToHasVoted[proposalId][selectedGMNFT] = true; + } + + uint256 nodeIdAttached = $.galaxyMember.getNodeIdAttached(selectedGMNFT); + + // Record that the Vechain node attached to the GM NFT has voted in the proposal, if it exists. + if (nodeIdAttached != 0) { + $.proposalToNodeToHasVoted[proposalId][nodeIdAttached] = true; + } + + // Emit an event to log the registration of the votes. + emit VoteRegistered(cycle, voter, votes, rewardWeightedVote); + } + + /// @notice Claim the rewards for a user in a specific cycle. + /// @dev The rewards are claimed based on the reward-weighted votes of the user in the cycle. + /// @param cycle - The cycle in which the rewards are claimed. + /// @param voter - The address of the voter. + function claimReward(uint256 cycle, address voter) public virtual nonReentrant { + require(cycle > 0, "VoterRewards: cycle must be greater than 0"); + require(voter != address(0), "VoterRewards: voter cannot be the zero address"); + VoterRewardsStorage storage $ = _getVoterRewardsStorage(); + + // Check if the cycle has ended before claiming rewards. + require($.emissions.isCycleEnded(cycle), "VoterRewards: cycle must be ended"); + + // Get the reward for the voter in the cycle. + uint256 reward = getReward(cycle, voter); + + require(reward > 0, "VoterRewards: reward must be greater than 0"); + require($.b3tr.balanceOf(address(this)) >= reward, "VoterRewards: not enough B3TR in the contract to pay reward"); + + // Reset the reward-weighted votes for the voter in the cycle. + $.cycleToVoterToTotal[cycle][voter] = 0; + + // transfer reward to voter + require($.b3tr.transfer(voter, reward), "VoterRewards: transfer failed"); + + // Emit an event to log the reward claimed by the voter. + emit RewardClaimed(cycle, voter, reward); + } + + // ----------------- Getters ----------------- // + + /// @notice Get the reward multiplier for a user in a specific proposal. + /// @param tokenId Id of the Galaxy Member NFT + /// @param proposalId Id of the proposal + function getMultiplier(uint256 tokenId, uint256 proposalId) public view virtual returns (uint256) { + VoterRewardsStorage storage $ = _getVoterRewardsStorage(); + + if (hasTokenVoted(tokenId, proposalId)) return 0; + + uint256 nodeIdAttached = $.galaxyMember.getNodeIdAttached(tokenId); + + if (hasNodeVoted(nodeIdAttached, proposalId)) return 0; + + uint256 gmNftLevel = $.galaxyMember.levelOf(tokenId); + + return $.levelToMultiplier[gmNftLevel]; + } + + /// @notice Check if a Vechain Node has voted in a proposal + /// @param nodeId Id of the Vechain node + /// @param proposalId Id of the proposal + function hasNodeVoted(uint256 nodeId, uint256 proposalId) public view virtual returns (bool) { + VoterRewardsStorage storage $ = _getVoterRewardsStorage(); + + return $.proposalToNodeToHasVoted[proposalId][nodeId]; + } + + /// @notice Check if a Galaxy Member has voted in a proposal + /// @param tokenId Id of the Galaxy Member NFT + /// @param proposalId Id of the proposal + function hasTokenVoted(uint256 tokenId, uint256 proposalId) public view virtual returns (bool) { + VoterRewardsStorage storage $ = _getVoterRewardsStorage(); + + return $.proposalToGalaxyMemberToHasVoted[proposalId][tokenId]; + } + + /// @notice Get the reward for a user in a specific cycle. + /// @param cycle - The cycle in which the rewards are claimed. + /// @param voter - The address of the voter. + function getReward(uint256 cycle, address voter) public view virtual returns (uint256) { + VoterRewardsStorage storage $ = _getVoterRewardsStorage(); + + // Get the total reward-weighted votes for the voter in the cycle. + uint256 total = $.cycleToVoterToTotal[cycle][voter]; + + // If total is zero, return 0 + if (total == 0) { + return 0; + } + + // Get the total reward-weighted votes in the cycle. + uint256 totalCycle = $.cycleToTotal[cycle]; + + // If totalCycle is zero, return 0 + if (totalCycle == 0) { + return 0; + } + + // Get the emissions for voter rewards in the cycle. + uint256 emissionsAmount = $.emissions.getVote2EarnAmount(cycle); + + // If emissionsAmount is zero, return 0 + if (emissionsAmount == 0) { + return 0; + } + + // Scale up the numerator before division to improve precision + uint256 scaledNumerator = total * emissionsAmount * SCALING_FACTOR; // Scale by a factor of SCALING_FACTOR for precision + uint256 reward = scaledNumerator / totalCycle; + + // Scale down the reward to the original scale + return reward / SCALING_FACTOR; + } + + /// @notice Get the total reward-weighted votes for a user in a specific cycle. + /// @param cycle - The cycle in which the rewards are claimed. + /// @param voter - The address of the voter. + function cycleToVoterToTotal(uint256 cycle, address voter) external view returns (uint256) { + VoterRewardsStorage storage $ = _getVoterRewardsStorage(); + return $.cycleToVoterToTotal[cycle][voter]; + } + + /// @notice Get the total reward-weighted votes in a specific cycle. + /// @param cycle - The cycle in which the rewards are claimed. + function cycleToTotal(uint256 cycle) external view returns (uint256) { + VoterRewardsStorage storage $ = _getVoterRewardsStorage(); + return $.cycleToTotal[cycle]; + } + + /// @notice Get the reward multiplier for a specific level of the Galaxy Member NFT. + /// @param level - The level of the Galaxy Member NFT. + function levelToMultiplier(uint256 level) external view returns (uint256) { + VoterRewardsStorage storage $ = _getVoterRewardsStorage(); + return $.levelToMultiplier[level]; + } + + /// @notice Get the Galaxy Member contract. + function galaxyMember() external view returns (IGalaxyMemberV2) { + VoterRewardsStorage storage $ = _getVoterRewardsStorage(); + return $.galaxyMember; + } + + /// @notice Get the Emissions contract. + function emissions() external view returns (IEmissions) { + VoterRewardsStorage storage $ = _getVoterRewardsStorage(); + return $.emissions; + } + + /// @notice Get the B3TR token contract. + function b3tr() external view returns (IB3TR) { + VoterRewardsStorage storage $ = _getVoterRewardsStorage(); + return $.b3tr; + } + + /// @notice Check if quadratic rewarding is disabled at a specific block number. + /// @dev To check if quadratic rewarding was disabled for a cycle, use the block number the cycle started. + /// @param blockNumber - The block number to check the quadratic rewarding status. + /// @return true if quadratic rewarding is disabled, false otherwise. + function isQuadraticRewardingDisabledAtBlock(uint48 blockNumber) public view returns (bool) { + VoterRewardsStorage storage $ = _getVoterRewardsStorage(); + + // Check if quadratic rewarding is enabled or disabled at the block number. + return $.quadraticRewardingDisabled.upperLookupRecent(blockNumber) == 1; // 0: enabled, 1: disabled + } + + /// @notice Check if quadratic rewarding is disabled for the current cycle. + /// @return true if quadratic rewarding is disabled, false otherwise. + function isQuadraticRewardingDisabledForCurrentCycle() public view returns (bool) { + VoterRewardsStorage storage $ = _getVoterRewardsStorage(); + // Get the block number the emission cycle started. + uint256 emissionCycleStartBlock = $.emissions.lastEmissionBlock(); + + uint208 currentStatus = $.quadraticRewardingDisabled.upperLookupRecent(SafeCast.toUint48(emissionCycleStartBlock)); + + // Check if quadratic rewarding is enabled or disabled for the current cycle. + return currentStatus == 1; // 0: enabled, 1: disabled + } + + // ----------------- Setters ----------------- // + + /// @notice Set the Galaxy Member contract. + /// @param _galaxyMember - The address of the Galaxy Member contract. + function setGalaxyMember(address _galaxyMember) public virtual onlyRole(CONTRACTS_ADDRESS_MANAGER_ROLE) { + require(_galaxyMember != address(0), "VoterRewards: _galaxyMember cannot be the zero address"); + + VoterRewardsStorage storage $ = _getVoterRewardsStorage(); + + emit GalaxyMemberAddressUpdated(_galaxyMember, address($.galaxyMember)); + + $.galaxyMember = IGalaxyMemberV2(_galaxyMember); + } + + /// @notice Set the Galaxy Member level to multiplier mapping. + /// @param level - The level of the Galaxy Member NFT. + /// @param multiplier - The percentage multiplier for the level of the Galaxy Member NFT. + function setLevelToMultiplier(uint256 level, uint256 multiplier) public virtual onlyRole(DEFAULT_ADMIN_ROLE) { + require(level > 0, "VoterRewards: level must be greater than 0"); + require(multiplier > 0, "VoterRewards: multiplier must be greater than 0"); + + VoterRewardsStorage storage $ = _getVoterRewardsStorage(); + $.levelToMultiplier[level] = multiplier; + + emit LevelToMultiplierSet(level, multiplier); + } + + /// @notice Set the Emmissions contract. + /// @param _emissions - The address of the emissions contract. + function setEmissions(address _emissions) public virtual onlyRole(CONTRACTS_ADDRESS_MANAGER_ROLE) { + require(_emissions != address(0), "VoterRewards: emissions cannot be the zero address"); + + VoterRewardsStorage storage $ = _getVoterRewardsStorage(); + + emit EmissionsAddressUpdated(_emissions, address($.emissions)); + + $.emissions = IEmissions(_emissions); + } + + /// @notice Toggle quadratic rewarding for a specific cycle. + /// @dev This function toggles the state of quadratic rewarding for a specific cycle. + /// The state will flip between enabled and disabled each time the function is called. + function toggleQuadraticRewarding() external onlyRole(DEFAULT_ADMIN_ROLE) { + VoterRewardsStorage storage $ = _getVoterRewardsStorage(); + + // Get the current status + bool currentStatus = isQuadraticRewardingDisabledForCurrentCycle(); + + // Toggle the status -> 0: enabled, 1: disabled + $.quadraticRewardingDisabled.push(clock(), currentStatus ? 0 : 1); + + // Emit an event to log the new quadratic rewarding status. + emit QuadraticRewardingToggled(!currentStatus); + } + + /// @notice Returns the version of the contract + /// @dev This should be updated every time a new version of implementation is deployed + /// @return string The version of the contract + function version() external pure virtual returns (string memory) { + return "3"; + } + + /// @dev Clock used for flagging checkpoints. + function clock() public view virtual returns (uint48) { + return Time.blockNumber(); + } +} diff --git a/contracts/interfaces/IGalaxyMember.sol b/contracts/interfaces/IGalaxyMember.sol index 780756a..4fd7c0a 100644 --- a/contracts/interfaces/IGalaxyMember.sol +++ b/contracts/interfaces/IGalaxyMember.sol @@ -180,6 +180,12 @@ interface IGalaxyMember { /// @return Selected token ID function getSelectedTokenId(address owner) external view returns (uint256); + /// @notice Returns the selected token ID for an owner in a specific block number + /// @param owner Address to query + /// @param blockNumber Block number to query + /// @return Selected token ID + function getSelectedTokenIdAtBlock(address owner, uint48 blockNumber) external view returns (uint256); + /// @notice Grants a role to an account /// @param role Role to grant /// @param account Account to receive the role diff --git a/contracts/interfaces/INodeManagement.sol b/contracts/interfaces/INodeManagement.sol index 6bdb090..ecb69cb 100644 --- a/contracts/interfaces/INodeManagement.sol +++ b/contracts/interfaces/INodeManagement.sol @@ -92,6 +92,34 @@ interface INodeManagement { */ function isNodeManager(address user, uint256 nodeId) external view returns (bool); + /** + * @notice Check if a node is delegated. + * @param nodeId The node ID to check for. + * @return bool True if the node is delegated. + */ + function isNodeDelegated(uint256 nodeId) external view returns (bool); + + /** + * @notice Check if a user is a delegator. + * @param user The address of the user to check. + * @return bool True if the user is a delegator. + */ + function isNodeDelegator(address user) external view returns (bool); + + /** + * @notice Check if a user is a node holder (either directly or through delegation). + * @param user The address of the user to check. + * @return bool True if the user is a node holder. + */ + function isNodeHolder(address user) external view returns (bool); + + /** + * @notice Check if a user directly owns a node (not delegated). + * @param user The address of the user to check. + * @return uint256 The ID of the owned node (0 if none). + */ + function getDirectNodeOwnership(address user) external view returns (uint256); + /** * @notice Retrieves the node level of a given node ID. * @param nodeId The token ID of the endorsing node. diff --git a/contracts/ve-better-passport/VeBetterPassport.sol b/contracts/ve-better-passport/VeBetterPassport.sol index f83f221..be93539 100644 --- a/contracts/ve-better-passport/VeBetterPassport.sol +++ b/contracts/ve-better-passport/VeBetterPassport.sol @@ -463,7 +463,7 @@ contract VeBetterPassport is AccessControlUpgradeable, UUPSUpgradeable, IVeBette /// @notice Returns the version of the contract function version() external pure returns (string memory) { - return "2"; + return "3"; } // ---------- Setters ---------- // diff --git a/contracts/ve-better-passport/libraries/PassportPersonhoodLogic.sol b/contracts/ve-better-passport/libraries/PassportPersonhoodLogic.sol index c402717..f128232 100644 --- a/contracts/ve-better-passport/libraries/PassportPersonhoodLogic.sol +++ b/contracts/ve-better-passport/libraries/PassportPersonhoodLogic.sol @@ -130,11 +130,13 @@ library PassportPersonhoodLogic { * - Returns `(false, "User has been signaled too many times")` if the user has been signaled more than the threshold. * - Returns `(true, "User's participation score is above the threshold")` if the user's participation score meets or exceeds the threshold. * - Returns `(false, "User does not meet the criteria to be considered a person")` if none of the conditions are met. + * - Returns `(true, "User's selected Galaxy Member is above the minimum level")` if the user's selected Galaxy Member is above the minimum level. * * Additional considerations: * - Checks for delegation status: If the user has delegated their personhood, they are not considered a valid passport holder. * - Checks if the user is in the whitelist or blacklist, with priority given to whitelist status. * - Evaluates the user's signaling status, participation score, and node ownership to determine validity. + * - Checks if the user's selected Galaxy Member is above the minimum level. */ function _checkPassport( PassportStorageTypes.PassportStorage storage self, @@ -183,7 +185,17 @@ library PassportPersonhoodLogic { } } - // TODO: With `GalaxyMember` version 2, Check if user's selected `GalaxyMember` `tokenId` is greater than `getMinimumGalaxyMemberLevel(self)` + // Check if user's selected GalaxyMember, in the timepoint, was above the minimum level + if (PassportChecksLogic._isCheckEnabled(self, PassportTypes.CheckType.GM_OWNERSHIP_CHECK)) { + uint256 selectedTokenId = self.galaxyMember.getSelectedTokenIdAtBlock(user, timepoint); + + if ( + selectedTokenId != 0 && + self.galaxyMember.levelOf(selectedTokenId) >= PassportChecksLogic.getMinimumGalaxyMemberLevel(self) + ) { + return (true, "User's selected Galaxy Member is above the minimum level"); + } + } // If none of the conditions are met, return false with the default reason return (false, "User does not meet the criteria to be considered a person"); diff --git a/scripts/deploy/deploy.ts b/scripts/deploy/deploy.ts index d530c80..15a0422 100644 --- a/scripts/deploy/deploy.ts +++ b/scripts/deploy/deploy.ts @@ -16,6 +16,7 @@ import { VeBetterPassport, VeBetterPassportV1, X2EarnCreator, + VeBetterPassportV2, } from "../../typechain-types" import { ContractsConfig } from "../../config/contracts" import { HttpNetworkConfig } from "hardhat/types" @@ -115,6 +116,14 @@ export async function deployAll(config: ContractsConfig) { PassportPoPScoreLogicV1, PassportSignalingLogicV1, PassportWhitelistAndBlacklistLogicV1, + PassportChecksLogicV2, + PassportConfiguratorV2, + PassportEntityLogicV2, + PassportDelegationLogicV2, + PassportPersonhoodLogicV2, + PassportPoPScoreLogicV2, + PassportSignalingLogicV2, + PassportWhitelistAndBlacklistLogicV2, PassportChecksLogic, PassportConfigurator, PassportEntityLogic, @@ -203,11 +212,13 @@ export async function deployAll(config: ContractsConfig) { )) as Treasury // Deploy NodeManagement - const nodeManagement = (await deployProxy( - "NodeManagement", - [vechainNodesAddress, config.CONTRACTS_ADMIN_ADDRESS, config.CONTRACTS_ADMIN_ADDRESS], - undefined, - true, + const nodeManagement = (await deployAndUpgrade( + ["NodeManagementV1", "NodeManagement"], + [[vechainNodesAddress, config.CONTRACTS_ADMIN_ADDRESS, config.CONTRACTS_ADMIN_ADDRESS], []], + { + versions: [undefined, 2], + logOutput: true, + }, )) as NodeManagement // Initialization requires the address of the x2EarnRewardsPool, for this reason we will initialize it after @@ -296,7 +307,7 @@ export async function deployAll(config: ContractsConfig) { )) as XAllocationPool const galaxyMember = (await deployAndUpgrade( - ["GalaxyMemberV1", "GalaxyMember"], + ["GalaxyMemberV1", "GalaxyMemberV2", "GalaxyMember"], [ [ { @@ -320,9 +331,11 @@ export async function deployAll(config: ContractsConfig) { TEMP_ADMIN, config.GM_NFT_NODE_TO_FREE_LEVEL, ], + [], ], { - versions: [undefined, 2], + versions: [undefined, 2, 3], + logOutput: true, }, )) as GalaxyMember @@ -365,7 +378,7 @@ export async function deployAll(config: ContractsConfig) { )) as Emissions const voterRewards = (await deployAndUpgrade( - ["VoterRewardsV1", "VoterRewardsV2", "VoterRewards"], + ["VoterRewardsV1", "VoterRewardsV2", "VoterRewardsV3", "VoterRewards"], [ [ TEMP_ADMIN, // admin @@ -379,9 +392,10 @@ export async function deployAll(config: ContractsConfig) { ], [], [], + [], ], { - versions: [undefined, 2, 3], + versions: [undefined, 2, 3, 4], logOutput: true, }, )) as VoterRewards @@ -455,13 +469,33 @@ export async function deployAll(config: ContractsConfig) { }, )) as VeBetterPassportV1 - const veBetterPassport = (await upgradeProxy( + const veBetterPassportV2 = (await upgradeProxy( "VeBetterPassportV1", - "VeBetterPassport", + "VeBetterPassportV2", await veBetterPassportV1.getAddress(), [], { version: 2, + libraries: { + PassportChecksLogicV2: await PassportChecksLogicV2.getAddress(), + PassportConfiguratorV2: await PassportConfiguratorV2.getAddress(), + PassportEntityLogicV2: await PassportEntityLogicV2.getAddress(), + PassportDelegationLogicV2: await PassportDelegationLogicV2.getAddress(), + PassportPersonhoodLogicV2: await PassportPersonhoodLogicV2.getAddress(), + PassportPoPScoreLogicV2: await PassportPoPScoreLogicV2.getAddress(), + PassportSignalingLogicV2: await PassportSignalingLogicV2.getAddress(), + PassportWhitelistAndBlacklistLogicV2: await PassportWhitelistAndBlacklistLogicV2.getAddress(), + }, + }, + )) as VeBetterPassportV2 + + const veBetterPassport = (await upgradeProxy( + "VeBetterPassportV2", + "VeBetterPassportV3", + await veBetterPassportV2.getAddress(), + [], + { + version: 3, libraries: { PassportChecksLogic: await PassportChecksLogic.getAddress(), PassportConfigurator: await PassportConfigurator.getAddress(), diff --git a/scripts/deploy/setup.ts b/scripts/deploy/setup.ts index e9d3650..7bc668d 100644 --- a/scripts/deploy/setup.ts +++ b/scripts/deploy/setup.ts @@ -101,10 +101,10 @@ export const setupLocalEnvironment = async ( * Second seed account will have a Thunder X Node * Third seed account will have a Strength X Node * Forth seed account will have a Mjölnir Economic Node - * Fifth seed account will have a Thunder Economic Node + * Fifth seed account will have a Strength Economic Node * Remaining accounts with have a Mjolnir X Node -> These will have an endorsement score of 100 */ - await mintVechainNodes(vechainNodesMock, endorserAccounts, padNodeTypes([7, 6, 5, 3, 2], endorserAccounts.length)) + await mintVechainNodes(vechainNodesMock, endorserAccounts, padNodeTypes([7, 6, 5, 3, 1], endorserAccounts.length)) // Get unendorsed XAPPs const unedorsedApps = await x2EarnApps.unendorsedAppIds() diff --git a/scripts/galaxyMember/getGMowners.ts b/scripts/galaxyMember/getGMowners.ts new file mode 100644 index 0000000..bb476e6 --- /dev/null +++ b/scripts/galaxyMember/getGMowners.ts @@ -0,0 +1,55 @@ +import { getConfig } from "../../config" +import { GalaxyMember__factory } from "../../typechain-types" +import { ethers } from "hardhat" +import fs from "fs/promises" + +type Owner = { + owner: string + tokenId: string +} + +/** + * Starts a new round of emissions. + * + * @throws if the round cannot be started. + */ +const getGmOwners = async () => { + const [signer] = await ethers.getSigners() + + const galaxyMember = GalaxyMember__factory.connect(getConfig().galaxyMemberContractAddress, signer) + + const totalSupply = await galaxyMember.totalSupply() + + const owners: Owner[] = [] + + for (let i = 1; i <= Number(totalSupply) + 1; i++) { + console.log(`Getting owner of token ${i}`) + try { + const owner = await galaxyMember.ownerOf(i) + owners.push({ owner, tokenId: i.toString() }) + } catch (e) { + console.log(`Token ${i} does not exist or has been burned`) + } + } + + // Remove duplicate owners, keeping only the first occurrence + const uniqueOwners: Owner[] = [] + const seenOwners = new Set() + + for (const item of owners) { + if (!seenOwners.has(item.owner)) { + seenOwners.add(item.owner) + uniqueOwners.push(item) + } + } + + // Save the unique owners to a file + await fs.writeFile("gmOwners.json", JSON.stringify({ recipients: uniqueOwners }, null, 2)) +} + +getGmOwners() + .then(() => process.exit(0)) + .catch(error => { + console.error("Error starting the round:", error) + process.exit(1) + }) diff --git a/scripts/galaxyMember/getGmSelectedTokens.ts b/scripts/galaxyMember/getGmSelectedTokens.ts new file mode 100644 index 0000000..4288206 --- /dev/null +++ b/scripts/galaxyMember/getGmSelectedTokens.ts @@ -0,0 +1,60 @@ +import { getConfig } from "../../config" +import { GalaxyMember__factory } from "../../typechain-types" +import { ethers } from "hardhat" +import fs from "fs/promises" + +type Owner = { + owner: string + tokenId: string +} + +/** + * Starts a new round of emissions. + * + * @throws if the round cannot be started. + */ +const getGmSelectedTokens = async () => { + const [signer] = await ethers.getSigners() + + const galaxyMember = GalaxyMember__factory.connect(getConfig().galaxyMemberContractAddress, signer) + + const totalSupply = await galaxyMember.totalSupply() + + const ownersWithoutSelectedToken: Owner[] = [] + + for (let i = 1; i <= Number(totalSupply) + 1; i++) { + console.log(`Getting owner of token ${i}`) + try { + const owner = await galaxyMember.ownerOf(i) + const selectedTokenId = await galaxyMember.getSelectedTokenId(owner) + + if (selectedTokenId === 0n) { + console.log(`Token ${i} of owner ${owner} is not selected`) + ownersWithoutSelectedToken.push({ owner, tokenId: i.toString() }) + } + } catch (e) { + console.log(`Token ${i} does not exist or has been burned`) + } + } + + // Remove duplicate owners, keeping only the first occurrence + const uniqueOwners: Owner[] = [] + const seenOwners = new Set() + + for (const item of ownersWithoutSelectedToken) { + if (!seenOwners.has(item.owner)) { + seenOwners.add(item.owner) + uniqueOwners.push(item) + } + } + + // Save the unique owners to a file + await fs.writeFile("gmOwnersWithSelected.json", JSON.stringify({ recipients: uniqueOwners }, null, 2)) +} + +getGmSelectedTokens() + .then(() => process.exit(0)) + .catch(error => { + console.error("Error starting the round:", error) + process.exit(1) + }) diff --git a/scripts/galaxyMember/index.ts b/scripts/galaxyMember/index.ts new file mode 100644 index 0000000..d945e1d --- /dev/null +++ b/scripts/galaxyMember/index.ts @@ -0,0 +1 @@ +export * from "./getGMowners" diff --git a/scripts/libraries/passportLibraries.ts b/scripts/libraries/passportLibraries.ts index c1b4b91..701534d 100644 --- a/scripts/libraries/passportLibraries.ts +++ b/scripts/libraries/passportLibraries.ts @@ -44,6 +44,49 @@ export async function passportLibraries() { await PassportWhitelistAndBlacklistLogicV1Lib.waitForDeployment() /// ______________ VERSION 2 ______________ + + // Deploy Passport Checks Logic + const PassportChecksLogicV2 = await ethers.getContractFactory("PassportChecksLogicV2") + const PassportChecksLogicLibV2 = await PassportChecksLogicV2.deploy() + await PassportChecksLogicLibV2.waitForDeployment() + + // Deploy Passport Configurator + const PassportConfiguratorV2 = await ethers.getContractFactory("PassportConfiguratorV2") + const PassportConfiguratorLibV2 = await PassportConfiguratorV2.deploy() + await PassportConfiguratorLibV2.waitForDeployment() + + // Deploy Passport Delegation Logic + const PassportEntityLogicV2 = await ethers.getContractFactory("PassportEntityLogicV2") + const PassportEntityLogicLibV2 = await PassportEntityLogicV2.deploy() + await PassportEntityLogicLibV2.waitForDeployment() + + // Deploy Passport Delegation Logic + const PassportDelegationLogicV2 = await ethers.getContractFactory("PassportDelegationLogicV2") + const PassportDelegationLogicLibV2 = await PassportDelegationLogicV2.deploy() + await PassportDelegationLogicLibV2.waitForDeployment() + + // Deploy Passport PoP Score Logic + const PassportPoPScoreLogicV2 = await ethers.getContractFactory("PassportPoPScoreLogicV2") + const PassportPoPScoreLogicLibV2 = await PassportPoPScoreLogicV2.deploy() + await PassportPoPScoreLogicLibV2.waitForDeployment() + + // Deploy Passport Signaling Logic + const PassportSignalingLogicV2 = await ethers.getContractFactory("PassportSignalingLogicV2") + const PassportSignalingLogicLibV2 = await PassportSignalingLogicV2.deploy() + await PassportSignalingLogicLibV2.waitForDeployment() + + // Deploy Passport Personhood Logic + const PassportPersonhoodLogicV2 = await ethers.getContractFactory("PassportPersonhoodLogicV2") + const PassportPersonhoodLogicLibV2 = await PassportPersonhoodLogicV2.deploy() + await PassportPersonhoodLogicLibV2.waitForDeployment() + + // Deploy Passport Whitelist and Blacklist Logic + const PassportWhitelistAndBlacklistLogicV2 = await ethers.getContractFactory("PassportWhitelistAndBlacklistLogicV2") + const PassportWhitelistAndBlacklistLogicLibV2 = await PassportWhitelistAndBlacklistLogicV2.deploy() + await PassportWhitelistAndBlacklistLogicLibV2.waitForDeployment() + + //// ______________ VERSION 3 ______________ + // Deploy Passport Checks Logic const PassportChecksLogic = await ethers.getContractFactory("PassportChecksLogic") const PassportChecksLogicLib = await PassportChecksLogic.deploy() @@ -93,6 +136,14 @@ export async function passportLibraries() { PassportPoPScoreLogicV1: PassportPoPScoreLogicV1Lib, PassportSignalingLogicV1: PassportSignalingLogicV1Lib, PassportWhitelistAndBlacklistLogicV1: PassportWhitelistAndBlacklistLogicV1Lib, + PassportChecksLogicV2: PassportChecksLogicLibV2, + PassportConfiguratorV2: PassportConfiguratorLibV2, + PassportEntityLogicV2: PassportEntityLogicLibV2, + PassportDelegationLogicV2: PassportDelegationLogicLibV2, + PassportPersonhoodLogicV2: PassportPersonhoodLogicLibV2, + PassportPoPScoreLogicV2: PassportPoPScoreLogicLibV2, + PassportSignalingLogicV2: PassportSignalingLogicLibV2, + PassportWhitelistAndBlacklistLogicV2: PassportWhitelistAndBlacklistLogicLibV2, PassportChecksLogic: PassportChecksLogicLib, PassportConfigurator: PassportConfiguratorLib, PassportEntityLogic: PassportEntityLogicLib, diff --git a/test/GalaxyMember.test.ts b/test/GalaxyMember.test.ts index 76a7501..bf62737 100644 --- a/test/GalaxyMember.test.ts +++ b/test/GalaxyMember.test.ts @@ -662,10 +662,10 @@ describe("Galaxy Member - @shard1", () => { forceDeploy: true, }) - expect(await galaxyMember.version()).to.equal("2") + expect(await galaxyMember.version()).to.equal("3") }) - it("Should not have state conflict after upgrading to V2", async () => { + it("Should not have state conflict after upgrading to V3", async () => { const config = createLocalConfig() const { owner, @@ -757,18 +757,22 @@ describe("Galaxy Member - @shard1", () => { const galaxyMemberV2 = (await upgradeProxy( "GalaxyMemberV1", - "GalaxyMember", + "GalaxyMemberV2", await galaxyMember.getAddress(), [owner.address, await nodeManagement.getAddress(), owner.address, config.GM_NFT_NODE_TO_FREE_LEVEL], { version: 2 }, )) as unknown as GalaxyMember - const storageSlotsAfter = [] + let storageSlotsAfter = [] for (let i = initialSlot; i < initialSlot + BigInt(100); i++) { storageSlotsAfter.push(await ethers.provider.getStorage(await galaxyMemberV2.getAddress(), i)) } + storageSlotsAfter = storageSlotsAfter.filter( + slot => slot !== "0x0000000000000000000000000000000000000000000000000000000000000000", + ) // removing empty slots + // Check if storage slots are the same after upgrade for (let i = 0; i < storageSlots.length; i++) { expect(storageSlots[i]).to.equal(storageSlotsAfter[i]) @@ -813,6 +817,109 @@ describe("Galaxy Member - @shard1", () => { expect(await galaxyMemberV2.levelOf(1)).to.equal(7) expect(await galaxyMemberV2.tokenURI(1)).to.equal(config.GM_NFT_BASE_URI + "7.json") + + storageSlots = [] + for (let i = initialSlot; i < initialSlot + BigInt(100); i++) { + storageSlots.push(await ethers.provider.getStorage(await galaxyMemberV2.getAddress(), i)) + } + + storageSlots = storageSlots.filter( + slot => slot !== "0x0000000000000000000000000000000000000000000000000000000000000000", + ) + + const galaxyMemberV3 = (await upgradeProxy( + "GalaxyMemberV2", + "GalaxyMember", + await galaxyMember.getAddress(), + [], + { version: 3 }, + )) as unknown as GalaxyMember + + storageSlotsAfter = [] + for (let i = initialSlot; i < initialSlot + BigInt(100); i++) { + storageSlotsAfter.push(await ethers.provider.getStorage(await galaxyMemberV3.getAddress(), i)) + } + + storageSlotsAfter = storageSlotsAfter.filter( + slot => slot !== "0x0000000000000000000000000000000000000000000000000000000000000000", + ) + + // Check if storage slots are the same after upgrade + for (let i = 0; i < storageSlots.length; i++) { + expect(storageSlots[i]).to.equal(storageSlotsAfter[i]) + } + + expect(await galaxyMemberV3.balanceOf(await owner.getAddress())).to.equal(2) + expect(await galaxyMemberV3.balanceOf(await otherAccount.getAddress())).to.equal(1) + expect(await galaxyMemberV3.balanceOf(await otherAccounts[0].getAddress())).to.equal(1) + expect(await galaxyMemberV3.balanceOf(await otherAccounts[1].getAddress())).to.equal(1) + + expect(await galaxyMemberV3.ownerOf(4)).to.equal(await owner.getAddress()) + expect(await galaxyMemberV3.ownerOf(1)).to.equal(await otherAccount.getAddress()) + expect(await galaxyMemberV3.ownerOf(2)).to.equal(await otherAccounts[0].getAddress()) + expect(await galaxyMemberV3.ownerOf(3)).to.equal(await otherAccounts[1].getAddress()) + + await galaxyMemberV3.connect(owner).freeMint() + + expect(await galaxyMemberV3.balanceOf(await owner.getAddress())).to.equal(3) + expect(await galaxyMemberV3.ownerOf(5)).to.equal(await owner.getAddress()) + + expect(await galaxyMemberV3.levelOf(1)).to.equal(7) + + // Get checkpointed token Id + const checkpointedTokenId = await galaxyMemberV3.getSelectedTokenIdAtBlock( + owner.address, + await ethers.provider.getBlockNumber(), + ) + expect(checkpointedTokenId).to.equal(0n) + + // admin selects users token as part of upgrade + await galaxyMemberV3.connect(owner).selectFor(owner.getAddress(), 4) + expect(await galaxyMemberV3.getSelectedTokenId(owner.getAddress())).to.equal(4) + + // Get checkpointed token Id + const checkpointedTokenId2 = await galaxyMemberV3.getSelectedTokenIdAtBlock( + owner.address, + await ethers.provider.getBlockNumber(), + ) + expect(checkpointedTokenId2).to.equal(4n) + + await galaxyMemberV3.connect(owner).transferFrom(owner.address, otherAccounts[6].address, 4) + + // Get checkpointed token Id + expect( + await galaxyMemberV3.getSelectedTokenIdAtBlock(owner.address, await ethers.provider.getBlockNumber()), + ).to.equal(6n) + + expect(await galaxyMemberV3.getSelectedTokenId(owner.getAddress())).to.equal(6n) + + // Check if the token is transferred + expect(await galaxyMemberV3.ownerOf(4)).to.equal(await otherAccounts[6].getAddress()) + + // Get checkpointed token Id + expect( + await galaxyMemberV3.getSelectedTokenIdAtBlock( + otherAccounts[6].address, + await ethers.provider.getBlockNumber(), + ), + ).to.equal(4n) + + // Transfer the token + await galaxyMemberV3.connect(owner).transferFrom(owner.address, otherAccounts[6].address, 6) + await galaxyMemberV3.connect(owner).transferFrom(owner.address, otherAccounts[6].address, 5) + + // Check if the token is transferred + expect(await galaxyMemberV3.ownerOf(4)).to.equal(await otherAccounts[6].getAddress()) + + expect(await galaxyMemberV3.getSelectedTokenId(owner.getAddress())).to.equal(0n) + + // Get checkpointed token Id + expect( + await galaxyMemberV3.getSelectedTokenIdAtBlock( + otherAccounts[6].address, + await ethers.provider.getBlockNumber(), + ), + ).to.equal(4n) }) }) @@ -1791,9 +1898,9 @@ describe("Galaxy Member - @shard1", () => { // participation in governance is a requirement for minting await participateInAllocationVoting(otherAccount, true) - const tx = await galaxyMember.connect(otherAccount).freeMint() // Token id 1 + let tx = await galaxyMember.connect(otherAccount).freeMint() // Token id 1 - const receipt = await tx.wait() + let receipt = await tx.wait() if (!receipt?.blockNumber) throw new Error("No receipt block number") @@ -1806,6 +1913,97 @@ describe("Galaxy Member - @shard1", () => { expect(selectedTokenInfo?.tokenLevel).to.equal(1) expect(selectedTokenInfo?.b3trToUpgrade).to.equal(10000000000000000000000n) }) + + it("Admin should be able to select a token for an account", async () => { + const { galaxyMember, otherAccount, owner } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + // Bootstrap emissions + await bootstrapEmissions() + + // participation in governance is a requirement for minting + await participateInAllocationVoting(otherAccount, true) + + await galaxyMember.connect(otherAccount).freeMint() // Token id 1 + + await galaxyMember.connect(otherAccount).freeMint() // Token id 2 + + await galaxyMember.connect(owner).selectFor(await otherAccount.getAddress(), 2) + + expect(await galaxyMember.getSelectedTokenId(await otherAccount.getAddress())).to.equal(2) + + await galaxyMember.connect(owner).selectFor(await otherAccount.getAddress(), 1) + + expect(await galaxyMember.getSelectedTokenId(await otherAccount.getAddress())).to.equal(1) + }) + + it("Admin should not be able to select a token for an account if the token is not owned by the account", async () => { + const { galaxyMember, otherAccount, owner } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + // Bootstrap emissions + await bootstrapEmissions() + + // participation in governance is a requirement for minting + await participateInAllocationVoting(owner, true) + + await galaxyMember.connect(owner).freeMint() // Token id 1 + + await expect(galaxyMember.connect(owner).selectFor(await otherAccount.getAddress(), 1)).to.be.reverted + }) + + it("Should checkpoint selected token correctly", async () => { + const { galaxyMember, otherAccount, owner } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + // Bootstrap emissions + await bootstrapEmissions() + + // participation in governance is a requirement for minting + await participateInAllocationVoting(owner, true) + + const blockNumber1 = await ethers.provider.getBlockNumber() + + await galaxyMember.connect(owner).freeMint() // Token id 1 + + const blockNumber2 = await ethers.provider.getBlockNumber() + + await galaxyMember.connect(owner).freeMint() // Token id 2 + + const blockNumber3 = await ethers.provider.getBlockNumber() + + expect(await galaxyMember.getSelectedTokenId(await owner.getAddress())).to.equal(1) + + await galaxyMember.connect(owner).transferFrom(await owner.getAddress(), await otherAccount.getAddress(), 1) + + const blockNumber4 = await ethers.provider.getBlockNumber() + + expect(await galaxyMember.getSelectedTokenId(await owner.getAddress())).to.equal(2) + + expect(await galaxyMember.getSelectedTokenIdAtBlock(await owner.getAddress(), blockNumber1)).to.equal(0) + + expect(await galaxyMember.getSelectedTokenIdAtBlock(await owner.getAddress(), blockNumber2)).to.equal(1) + + expect(await galaxyMember.getSelectedTokenIdAtBlock(await owner.getAddress(), blockNumber3)).to.equal(1) + + expect(await galaxyMember.getSelectedTokenIdAtBlock(await owner.getAddress(), blockNumber4)).to.equal(2) + expect(await galaxyMember.getSelectedTokenIdAtBlock(await otherAccount.getAddress(), blockNumber4)).to.equal(1) + + await galaxyMember.connect(owner).burn(2) + expect( + await galaxyMember.getSelectedTokenIdAtBlock(await owner.getAddress(), await ethers.provider.getBlockNumber()), + ).to.equal(0) + expect(await galaxyMember.getSelectedTokenId(await owner.getAddress())).to.equal(0) + + await galaxyMember.connect(owner).freeMint() // Token id 3 + expect(await galaxyMember.getSelectedTokenId(await owner.getAddress())).to.equal(3) + expect( + await galaxyMember.getSelectedTokenIdAtBlock(await owner.getAddress(), await ethers.provider.getBlockNumber()), + ).to.equal(3) + }) }) describe("Upgrading", () => { @@ -1920,17 +2118,17 @@ describe("Galaxy Member - @shard1", () => { expect(await galaxyMember.levelOf(2)).to.equal(2) // Level 2 - const tx = await galaxyMember + let tx = await galaxyMember .connect(owner) .transferFrom(await owner.getAddress(), await otherAccount.getAddress(), 2) - const receipt = await tx.wait() + let receipt = await tx.wait() if (!receipt?.blockNumber) throw new Error("No receipt block number") - const events = receipt?.logs + let events = receipt?.logs - const decodedEvents = events?.map(event => { + let decodedEvents = events?.map(event => { return galaxyMember.interface.parseLog({ topics: event?.topics as string[], data: event?.data as string, diff --git a/test/NodeManagement.test.ts b/test/NodeManagement.test.ts index a00f16e..ee8097d 100644 --- a/test/NodeManagement.test.ts +++ b/test/NodeManagement.test.ts @@ -10,6 +10,8 @@ import { import { describe, it } from "mocha" import { getImplementationAddress } from "@openzeppelin/upgrades-core" import { time } from "@nomicfoundation/hardhat-network-helpers" +import { NodeManagement, NodeManagementV1 } from "../typechain-types" +import { deployProxy, upgradeProxy } from "../scripts/helpers" describe("Node Management - @shard1", function () { describe("Contract upgradeablity", () => { @@ -71,7 +73,80 @@ describe("Node Management - @shard1", function () { forceDeploy: true, }) - expect(await nodeManagement.version()).to.equal("1") + expect(await nodeManagement.version()).to.equal("2") + }) + + it("Should be no state conflicts after upgrade", async () => { + const { owner, otherAccount, otherAccounts, vechainNodesMock } = await getOrDeployContractInstances({ + forceDeploy: true, + deployMocks: true, + }) + + const nodeManagementV1 = (await deployProxy("NodeManagementV1", [ + await vechainNodesMock.getAddress(), + owner.address, + owner.address, + ])) as NodeManagementV1 + + // Mock node ownership + await createNodeHolder(2, owner) // Node strength level 2 corresponds (Thunder) to an endorsement score of 13 + await createNodeHolder(4, otherAccounts[0]) // Node strength level 2 corresponds (Thunder) to an endorsement score of 13 + await createNodeHolder(7, otherAccounts[1]) // Node strength level 2 corresponds (Thunder) to an endorsement score of 13 + + const tx = await vechainNodesMock.addToken(otherAccounts[2].address, 7, false, 0, 0) + + // Wait for the transaction to be mined + const receipt = await tx.wait() + if (!receipt) throw new Error("No receipt") + + // Retrieve the block where the transaction was included + const block = await ethers.provider.getBlock(receipt.blockNumber) + if (!block) throw new Error("No block") + + await nodeManagementV1.connect(owner).delegateNode(otherAccount.address) + await nodeManagementV1.connect(otherAccounts[0]).delegateNode(otherAccount.address) + await nodeManagementV1.connect(otherAccounts[1]).delegateNode(otherAccount.address) + + let storageSlots = [] + + const initialSlot = BigInt("0x895b04a03424f581b1c6717e3715bbb5ceb9c40a4e5b61a13e84096251cf8f00") // Slot 0 of VoterRewards + + for (let i = initialSlot; i < initialSlot + BigInt(100); i++) { + storageSlots.push(await ethers.provider.getStorage(await nodeManagementV1.getAddress(), i)) + } + + storageSlots = storageSlots.filter( + slot => slot !== "0x0000000000000000000000000000000000000000000000000000000000000000", + ) // removing empty slots + + const nodeManagement = (await upgradeProxy( + "NodeManagementV1", + "NodeManagement", + await nodeManagementV1.getAddress(), + [], + { + version: 2, + }, + )) as NodeManagement + + const storageSlotsAfter = [] + + for (let i = initialSlot; i < initialSlot + BigInt(100); i++) { + storageSlotsAfter.push(await ethers.provider.getStorage(await nodeManagement.getAddress(), i)) + } + + // Check if storage slots are the same after upgrade + for (let i = 0; i < storageSlots.length; i++) { + expect(storageSlots[i]).to.equal(storageSlotsAfter[i]) + } + + // Check if all nodes are delegated to the same address + expect(await nodeManagement.getNodeIds(otherAccount.address)).to.eql([1n, 2n, 3n]) + + // Check node owners are not delegated to themselves + expect(await nodeManagement.getNodeIds(owner.address)).to.eql([]) + expect(await nodeManagement.getNodeIds(otherAccounts[0].address)).to.eql([]) + expect(await nodeManagement.getNodeIds(otherAccounts[1].address)).to.eql([]) }) }) @@ -248,6 +323,9 @@ describe("Node Management - @shard1", function () { const nodeDelegated = filterEventsByName(txReceipt.logs, "NodeDelegated") expect(nodeDelegated.length).to.eql(2) + + // Other account should not be the manager + expect(await nodeManagement.getNodeIds(otherAccount.address)).to.eql([]) }) it("Should revert if non node owner is trying to remove delegation", async function () { @@ -628,4 +706,338 @@ describe("Node Management - @shard1", function () { expect(manager).to.equal(false) }) }) + + describe("isNodeHolder Function", () => { + it("Should return true for a user who owns a node", async function () { + const { owner, nodeManagement } = await getOrDeployContractInstances({ + forceDeploy: true, + deployMocks: true, + }) + + // Mock node ownership + await createNodeHolder(2, owner) // Node strength level 2 corresponds (Thunder) to an endorsement score of 13 + + // Check if the owner is a node holder + const isHolder = await nodeManagement.isNodeHolder(owner.address) + expect(isHolder).to.equal(true) + }) + + it("Should return true for a user who only has delegated nodes", async function () { + const { owner, otherAccount, nodeManagement } = await getOrDeployContractInstances({ + forceDeploy: true, + deployMocks: true, + }) + + // Mock node ownership and delegation + await createNodeHolder(2, owner) + await nodeManagement.connect(owner).delegateNode(otherAccount.address) + + // Check if the delegatee is a node holder + const isHolder = await nodeManagement.isNodeHolder(otherAccount.address) + expect(isHolder).to.equal(true) + }) + + it("Should return true for a user who both owns and has delegated nodes", async function () { + const { owner, otherAccount, nodeManagement } = await getOrDeployContractInstances({ + forceDeploy: true, + deployMocks: true, + }) + + // Mock node ownership for both owned and delegated nodes + await createNodeHolder(2, otherAccount) // Own node + await createNodeHolder(4, owner) // Node to delegate + + // Delegate owner's node to otherAccount + await nodeManagement.connect(owner).delegateNode(otherAccount.address) + + // Check if the user with both owned and delegated nodes is a holder + const isHolder = await nodeManagement.isNodeHolder(otherAccount.address) + expect(isHolder).to.equal(true) + }) + + it("Should return false for a user who neither owns nor has delegated nodes", async function () { + const { otherAccount, nodeManagement } = await getOrDeployContractInstances({ + forceDeploy: true, + deployMocks: true, + }) + + // Check if a user with no nodes is a holder + const isHolder = await nodeManagement.isNodeHolder(otherAccount.address) + expect(isHolder).to.equal(false) + }) + + it("Should return false for zero address", async function () { + const { nodeManagement } = await getOrDeployContractInstances({ + forceDeploy: true, + deployMocks: true, + }) + + // Check if zero address is a holder + const isHolder = await nodeManagement.isNodeHolder(ZERO_ADDRESS) + expect(isHolder).to.equal(false) + }) + }) + + describe("Additional Node Management Functions", () => { + it("Should correctly identify if a node is delegated", async function () { + const { owner, otherAccount, nodeManagement } = await getOrDeployContractInstances({ + forceDeploy: true, + deployMocks: true, + }) + + // Mock node ownership + await createNodeHolder(2, owner) + + // Initially node should not be delegated + expect(await nodeManagement.isNodeDelegated(1)).to.equal(false) + + // Delegate the node + await nodeManagement.connect(owner).delegateNode(otherAccount.address) + + // Now node should be delegated + expect(await nodeManagement.isNodeDelegated(1)).to.equal(true) + }) + + it("Should correctly identify if a user is a node delegator", async function () { + const { owner, otherAccount, nodeManagement } = await getOrDeployContractInstances({ + forceDeploy: true, + deployMocks: true, + }) + + // Mock node ownership + await createNodeHolder(2, owner) + + // Initially owner should not be a delegator + expect(await nodeManagement.isNodeDelegator(owner.address)).to.equal(false) + + // Delegate the node + await nodeManagement.connect(owner).delegateNode(otherAccount.address) + + // Now owner should be a delegator + expect(await nodeManagement.isNodeDelegator(owner.address)).to.equal(true) + + // Other account should not be a delegator + expect(await nodeManagement.isNodeDelegator(otherAccount.address)).to.equal(false) + }) + + it("Should return correct direct node ownership", async function () { + const { owner, otherAccount, nodeManagement } = await getOrDeployContractInstances({ + forceDeploy: true, + deployMocks: true, + }) + + // Mock node ownership + await createNodeHolder(2, owner) + + // Owner should have node ID 1 + expect(await nodeManagement.getDirectNodeOwnership(owner.address)).to.equal(1n) + + // Other account should have no node + expect(await nodeManagement.getDirectNodeOwnership(otherAccount.address)).to.equal(0n) + }) + + it("Should return correct user node details for a single node", async function () { + const { owner, otherAccount, nodeManagement } = await getOrDeployContractInstances({ + forceDeploy: true, + deployMocks: true, + }) + + // Mock node ownership + await createNodeHolder(2, owner) // Level 2 = Thunder node + + // Check owner's node details before delegation + const nodesInfo = await nodeManagement.getUserNodes(owner.address) + expect(nodesInfo.length).to.equal(1) + + const nodeInfo = nodesInfo[0] + expect(nodeInfo.nodeId).to.equal(1n) + expect(nodeInfo.nodeLevel).to.equal(2) // Thunder node + expect(nodeInfo.xNodeOwner).to.equal(owner.address) + expect(nodeInfo.isXNodeHolder).to.equal(true) + expect(nodeInfo.isXNodeDelegated).to.equal(false) + expect(nodeInfo.isXNodeDelegator).to.equal(false) + expect(nodeInfo.isXNodeDelegatee).to.equal(false) + expect(nodeInfo.delegatee).to.equal(ZERO_ADDRESS) + + // Delegate the node + await nodeManagement.connect(owner).delegateNode(otherAccount.address) + + // Check owner's node details after delegation (should be empty array as node is delegated) + const ownerNodesAfterDelegation = await nodeManagement.getUserNodes(owner.address) + expect(ownerNodesAfterDelegation.length).to.equal(1) + const ownerNodesAfterDelegationInfo = ownerNodesAfterDelegation[0] + expect(ownerNodesAfterDelegationInfo.nodeId).to.equal(1n) + expect(ownerNodesAfterDelegationInfo.nodeLevel).to.equal(2) // Thunder node + expect(ownerNodesAfterDelegationInfo.xNodeOwner).to.equal(owner.address) + expect(ownerNodesAfterDelegationInfo.isXNodeHolder).to.equal(true) + expect(ownerNodesAfterDelegationInfo.isXNodeDelegated).to.equal(true) + expect(ownerNodesAfterDelegationInfo.isXNodeDelegator).to.equal(true) + expect(ownerNodesAfterDelegationInfo.isXNodeDelegatee).to.equal(false) + expect(ownerNodesAfterDelegationInfo.delegatee).to.equal(otherAccount.address) + + // Check delegatee's node details + const delegateeNodes = await nodeManagement.getUserNodes(otherAccount.address) + expect(delegateeNodes.length).to.equal(1) + + const delegatedNodeInfo = delegateeNodes[0] + expect(delegatedNodeInfo.nodeId).to.equal(1n) + expect(delegatedNodeInfo.nodeLevel).to.equal(2) // Thunder node + expect(delegatedNodeInfo.xNodeOwner).to.equal(owner.address) + expect(delegatedNodeInfo.isXNodeHolder).to.equal(true) + expect(delegatedNodeInfo.isXNodeDelegated).to.equal(true) + expect(delegatedNodeInfo.isXNodeDelegator).to.equal(false) + expect(delegatedNodeInfo.isXNodeDelegatee).to.equal(true) + expect(delegatedNodeInfo.delegatee).to.equal(otherAccount.address) + }) + + it("Should return correct user node details for multiple nodes", async function () { + const { owner, otherAccount, otherAccounts, nodeManagement } = await getOrDeployContractInstances({ + forceDeploy: true, + deployMocks: true, + }) + + // Mock multiple node ownerships with different levels + await createNodeHolder(2, owner) // Thunder node + await createNodeHolder(4, otherAccounts[0]) // Mjolnir node + await createNodeHolder(7, otherAccounts[1]) // VeThor X node + + // Delegate all nodes to otherAccount + await nodeManagement.connect(owner).delegateNode(otherAccount.address) + await nodeManagement.connect(otherAccounts[0]).delegateNode(otherAccount.address) + await nodeManagement.connect(otherAccounts[1]).delegateNode(otherAccount.address) + + // Check delegatee's node details + const delegateeNodes = await nodeManagement.getUserNodes(otherAccount.address) + expect(delegateeNodes.length).to.equal(3) + + // Check first node (Thunder) + const nodeInfo1 = delegateeNodes[0] + expect(nodeInfo1.nodeId).to.equal(1n) + expect(nodeInfo1.nodeLevel).to.equal(2) // Thunder node + expect(nodeInfo1.xNodeOwner).to.equal(owner.address) + expect(nodeInfo1.isXNodeHolder).to.equal(true) + expect(nodeInfo1.isXNodeDelegated).to.equal(true) + expect(nodeInfo1.isXNodeDelegator).to.equal(false) + expect(nodeInfo1.isXNodeDelegatee).to.equal(true) + expect(nodeInfo1.delegatee).to.equal(otherAccount.address) + + // Check second node (Mjolnir) + const nodeInfo2 = delegateeNodes[1] + expect(nodeInfo2.nodeId).to.equal(2n) + expect(nodeInfo2.nodeLevel).to.equal(4) // Mjolnir node + expect(nodeInfo2.xNodeOwner).to.equal(otherAccounts[0].address) + expect(nodeInfo2.isXNodeHolder).to.equal(true) + expect(nodeInfo2.isXNodeDelegated).to.equal(true) + expect(nodeInfo2.isXNodeDelegator).to.equal(false) + expect(nodeInfo2.isXNodeDelegatee).to.equal(true) + expect(nodeInfo2.delegatee).to.equal(otherAccount.address) + + // Check third node (VeThor X) + const nodeInfo3 = delegateeNodes[2] + expect(nodeInfo3.nodeId).to.equal(3n) + expect(nodeInfo3.nodeLevel).to.equal(7) // VeThor X node + expect(nodeInfo3.xNodeOwner).to.equal(otherAccounts[1].address) + expect(nodeInfo3.isXNodeHolder).to.equal(true) + expect(nodeInfo3.isXNodeDelegated).to.equal(true) + expect(nodeInfo3.isXNodeDelegator).to.equal(false) + expect(nodeInfo3.isXNodeDelegatee).to.equal(true) + expect(nodeInfo3.delegatee).to.equal(otherAccount.address) + }) + + it("Should return empty array for user without any nodes", async function () { + const { otherAccount, nodeManagement } = await getOrDeployContractInstances({ + forceDeploy: true, + deployMocks: true, + }) + + // Check nodes for user without any ownership or delegation + const nodesInfo = await nodeManagement.getUserNodes(otherAccount.address) + + // Should return empty array + expect(nodesInfo.length).to.equal(0) + expect(nodesInfo).to.eql([]) + }) + + it("Should return empty array for zero address", async function () { + const { nodeManagement } = await getOrDeployContractInstances({ + forceDeploy: true, + deployMocks: true, + }) + + // Check nodes for zero address + const nodesInfo = await nodeManagement.getUserNodes(ZERO_ADDRESS) + + // Should return empty array + expect(nodesInfo.length).to.equal(0) + expect(nodesInfo).to.eql([]) + }) + }) + + describe("Storage Preservation During Upgrades", () => { + it("Should not break storage when upgrading from v1 to current version", async function () { + const { owner, otherAccount, vechainNodesMock } = await getOrDeployContractInstances({ + forceDeploy: false, + deployMocks: true, + }) + + // Deploy current version first to set up initial state + const nodeManagementV1 = (await deployProxy("NodeManagementV1", [ + await vechainNodesMock.getAddress(), + owner.address, + owner.address, + ])) as NodeManagementV1 + + // Set up initial state with current version + await createNodeHolder(2, owner) + await nodeManagementV1.connect(owner).delegateNode(otherAccount.address) + + const nodeId = await vechainNodesMock.ownerToId(owner.address) + + // Verify initial state + expect(await nodeManagementV1.getNodeManager(nodeId)).to.equal(otherAccount.address) + + // Get storage slots before upgrade + const initialSlot = BigInt(0) + const storageSlots = [] + + for (let i = initialSlot; i < initialSlot + BigInt(100); i++) { + storageSlots.push(await ethers.provider.getStorage(await nodeManagementV1.getAddress(), i)) + } + + // Filter out empty slots + const filteredSlots = storageSlots.filter( + slot => slot !== "0x0000000000000000000000000000000000000000000000000000000000000000", + ) + + // Deploy V1 implementation and upgrade to it + const nodeManagement = (await upgradeProxy( + "NodeManagementV1", + "NodeManagement", + await nodeManagementV1.getAddress(), + [], + { + version: 2, + }, + )) as NodeManagement + + // Get storage slots after downgrade + const storageSlotsAfter = [] + for (let i = initialSlot; i < initialSlot + BigInt(100); i++) { + storageSlotsAfter.push(await ethers.provider.getStorage(await nodeManagement.getAddress(), i)) + } + + // Filter empty slots + const filteredSlotsAfter = storageSlotsAfter.filter( + slot => slot !== "0x0000000000000000000000000000000000000000000000000000000000000000", + ) + + // Verify storage slots remain unchanged + for (let i = 0; i < filteredSlots.length; i++) { + expect(filteredSlots[i]).to.equal(filteredSlotsAfter[i]) + } + + // Verify functionality still works + expect(await nodeManagement.getNodeManager(nodeId)).to.equal(otherAccount.address) + }) + }) }) diff --git a/test/VeBetterPassport.test.ts b/test/VeBetterPassport.test.ts index 4df641c..e76326f 100644 --- a/test/VeBetterPassport.test.ts +++ b/test/VeBetterPassport.test.ts @@ -18,6 +18,8 @@ import { moveBlocks, waitForBlock, waitForNextBlock, + upgradeNFTtoLevel, + participateInAllocationVoting, } from "./helpers" import { describe, it } from "mocha" import { getImplementationAddress } from "@openzeppelin/upgrades-core" @@ -29,6 +31,7 @@ import { Emissions, VeBetterPassport, VeBetterPassportV1, + VeBetterPassportV2, VoterRewards, X2EarnRewardsPool, XAllocationPool, @@ -132,7 +135,7 @@ describe("VeBetterPassport - @shard2", function () { forceDeploy: true, }) - expect(await veBetterPassport.version()).to.equal("2") + expect(await veBetterPassport.version()).to.equal("3") }) it("Should not be able to initialize twice", async function () { const config = createTestConfig() @@ -312,7 +315,7 @@ describe("VeBetterPassport - @shard2", function () { expect(newImplAddress.toUpperCase()).to.eql((await implementation.getAddress()).toUpperCase()) }) - it("Should not have any state conflicts after upgrading to V2", async function () { + it("Should not have any state conflicts after upgrading to V3", async function () { const config = createTestConfig() config.VEPASSPORT_DECAY_RATE = 20 config.EMISSIONS_CYCLE_DURATION = 20 @@ -335,6 +338,14 @@ describe("VeBetterPassport - @shard2", function () { passportSignalingLogicV1, passportEntityLogicV1, passportWhitelistBlacklistLogicV1, + passportChecksLogicV2, + passportConfiguratorV2, + passportDelegationLogicV2, + passportPersonhoodLogicV2, + passportPoPScoreLogicV2, + passportSignalingLogicV2, + passportEntityLogicV2, + passportWhitelistBlacklistLogicV2, passportChecksLogic, passportConfigurator, passportDelegationLogic, @@ -654,8 +665,12 @@ describe("VeBetterPassport - @shard2", function () { .grantRole(roundStarterRole, owner.address) .then(async tx => await tx.wait()) + // Set XAllocation address in GalaxyMember + await galaxyMember.connect(owner).setXAllocationsGovernorAddress(await xAllocationVoting.getAddress()) + await getVot3Tokens(otherAccount, "10000") await getVot3Tokens(owner, "10000") + await getVot3Tokens(otherAccounts[4], "10000") //Add apps const app1Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[2].address)) @@ -696,7 +711,7 @@ describe("VeBetterPassport - @shard2", function () { // Whitelist function const funcSig = B3trContract.interface.getFunction("tokenDetails")?.selector - await governor.connect(owner).setWhitelistFunction(await b3tr.getAddress(), funcSig, true) + await governor.connect(owner).setWhitelistFunction(await b3tr.getAddress(), funcSig as string, true) // Create a proposal for next round // create a new proposal active from round 2 @@ -739,6 +754,8 @@ describe("VeBetterPassport - @shard2", function () { [ethers.parseEther("0"), ethers.parseEther("900"), ethers.parseEther("100")], ) + await xAllocationVoting.connect(otherAccounts[4]).castVote(1, [app1Id], [ethers.parseEther("1")]) + // Set minimum participation score to 500 await veBetterPassportV1.setThresholdPoPScore(500) @@ -791,11 +808,35 @@ describe("VeBetterPassport - @shard2", function () { // Increase participation score threshold to 1000 await veBetterPassportV1.setThresholdPoPScore(1000) + // Toggle GM level check + await veBetterPassportV1.toggleCheck(5) + + // Upgrade to V2 + const veBetterPassportV2 = (await upgradeProxy( + "VeBetterPassportV1", + "VeBetterPassportV2", + await veBetterPassportV1.getAddress(), + [], + { + version: 2, + libraries: { + PassportChecksLogicV2: await passportChecksLogicV2.getAddress(), + PassportConfiguratorV2: await passportConfiguratorV2.getAddress(), + PassportEntityLogicV2: await passportEntityLogicV2.getAddress(), + PassportDelegationLogicV2: await passportDelegationLogicV2.getAddress(), + PassportPersonhoodLogicV2: await passportPersonhoodLogicV2.getAddress(), + PassportPoPScoreLogicV2: await passportPoPScoreLogicV2.getAddress(), + PassportSignalingLogicV2: await passportSignalingLogicV2.getAddress(), + PassportWhitelistAndBlacklistLogicV2: await passportWhitelistBlacklistLogicV2.getAddress(), + }, + }, + )) as VeBetterPassportV2 + blockNextCycle = await emissions.getNextCycleBlock() await waitForBlock(Number(blockNextCycle)) // Increase participation score threshold to 1000 - await veBetterPassportV1.setThresholdPoPScore(1000) + await veBetterPassportV2.setThresholdPoPScore(1000) await emissions.distribute() @@ -813,7 +854,7 @@ describe("VeBetterPassport - @shard2", function () { ).to.be.revertedWithCustomError(xAllocationVoting, "GovernorPersonhoodVerificationFailed") // Register action for round 3 - await veBetterPassportV1.connect(owner).registerAction(otherAccount, app1Id) + await veBetterPassportV2.connect(owner).registerAction(otherAccount, app1Id) /* User's cumulative score: @@ -821,7 +862,7 @@ describe("VeBetterPassport - @shard2", function () { round 2 = 600 + (300 * 0.8) = 840 round 3 = 100 + (840 * 0.8) = 772 */ - expect(await veBetterPassportV1.getCumulativeScoreWithDecay(otherAccount, 3)).to.equal(772) + expect(await veBetterPassportV2.getCumulativeScoreWithDecay(otherAccount, 3)).to.equal(772) // User still doesn't meet the participation score threshold and can't vote await expect( @@ -835,8 +876,8 @@ describe("VeBetterPassport - @shard2", function () { ).to.be.revertedWithCustomError(xAllocationVoting, "GovernorPersonhoodVerificationFailed") // register more actions for round 3 - await veBetterPassportV1.connect(owner).registerAction(otherAccount, app2Id) - await veBetterPassportV1.connect(owner).registerAction(otherAccount, app3Id) + await veBetterPassportV2.connect(owner).registerAction(otherAccount, app2Id) + await veBetterPassportV2.connect(owner).registerAction(otherAccount, app3Id) /* User's cumulative score: @@ -844,7 +885,7 @@ describe("VeBetterPassport - @shard2", function () { round 2 = 600 + (300 * 0.8) = 840 round 3 = 700 + (840 * 0.8) = 1072 */ - expect(await veBetterPassportV1.getCumulativeScoreWithDecay(otherAccount, 3)).to.equal(1372) + expect(await veBetterPassportV2.getCumulativeScoreWithDecay(otherAccount, 3)).to.equal(1372) // User now meets the participation score threshold and can vote await xAllocationVoting @@ -856,27 +897,27 @@ describe("VeBetterPassport - @shard2", function () { ) // "Before linking passport should have 0" - expect(await veBetterPassportV1.getCumulativeScoreWithDecay(owner, 3)).to.equal(0) + expect(await veBetterPassportV2.getCumulativeScoreWithDecay(owner, 3)).to.equal(0) // Before linking passport should not be considered person expect( - (await veBetterPassportV1.isPersonAtTimepoint(owner.address, await xAllocationVoting.roundSnapshot(3)))[0], + (await veBetterPassportV2.isPersonAtTimepoint(owner.address, await xAllocationVoting.roundSnapshot(3)))[0], ).to.be.equal(false) // Delegate passport to owner and try to vote - await linkEntityToPassportWithSignature(veBetterPassportV1, owner, otherAccount, 3600) + await linkEntityToPassportWithSignature(veBetterPassportV2, owner, otherAccount, 3600) // After linking "other account" should be entity - expect(await veBetterPassportV1.isEntity(otherAccount.address)).to.be.true + expect(await veBetterPassportV2.isEntity(otherAccount.address)).to.be.true // After linking owner should be passport - expect(await veBetterPassportV1.isPassport(owner.address)).to.be.true + expect(await veBetterPassportV2.isPassport(owner.address)).to.be.true // After linking passport should not be considered person at the beginning of the round expect( - (await veBetterPassportV1.isPersonAtTimepoint(owner.address, await xAllocationVoting.roundSnapshot(3)))[0], + (await veBetterPassportV2.isPersonAtTimepoint(owner.address, await xAllocationVoting.roundSnapshot(3)))[0], ).to.be.equal(false) - expect(await veBetterPassportV1.isPassport(owner.address)).to.be.true + expect(await veBetterPassportV2.isPassport(owner.address)).to.be.true // Owner can't vote yet because the delegation is checkpointed and is active from the next round await expect( @@ -892,6 +933,18 @@ describe("VeBetterPassport - @shard2", function () { blockNextCycle = await emissions.getNextCycleBlock() await waitForBlock(Number(blockNextCycle)) + // Mint user GM token + await galaxyMember.connect(otherAccounts[4]).freeMint() + await galaxyMember.setMaxLevel(2) + // Set user GM level to 2 + await upgradeNFTtoLevel(1, 2, galaxyMember, b3tr, otherAccounts[4], minterAccount) + + // Checking if user is person based on GM level will not work in V2 because the check is not implemented + expect(await veBetterPassportV2.isPerson(otherAccounts[4].address)).to.deep.equal([ + false, + "User does not meet the criteria to be considered a person", + ]) + // Record contract storage state at this point let storageSlots = [] @@ -907,12 +960,12 @@ describe("VeBetterPassport - @shard2", function () { // Upgrade to V2 const veBetterPassport = (await upgradeProxy( - "VeBetterPassportV1", + "VeBetterPassportV2", "VeBetterPassport", - await veBetterPassportV1.getAddress(), + await veBetterPassportV2.getAddress(), [], { - version: 2, + version: 3, libraries: { PassportChecksLogic: await passportChecksLogic.getAddress(), PassportConfigurator: await passportConfigurator.getAddress(), @@ -946,6 +999,12 @@ describe("VeBetterPassport - @shard2", function () { expect(await xAllocationVoting.currentRoundId()).to.equal(4) + // Checking if user is person based on GM level will work in V3 because the check is implemented + expect(await veBetterPassport.isPerson(otherAccounts[4].address)).to.deep.equal([ + true, + "User's selected Galaxy Member is above the minimum level", + ]) + // During linking points are not brought over, so we need to register some actions // on both the entity and the passport to see that they are grouped together and can vote expect(await veBetterPassportV1.getCumulativeScoreWithDecay(otherAccount, 4)).to.equal(1097) @@ -1955,7 +2014,7 @@ describe("VeBetterPassport - @shard2", function () { chainId: 1337, verifyingContract: await veBetterPassport.getAddress(), } - const types = { + let types = { LinkEntity: [ { name: "entity", type: "address" }, { name: "passport", type: "address" }, @@ -2467,7 +2526,7 @@ describe("VeBetterPassport - @shard2", function () { } // Make the signature invalid - const types = { + let types = { INVALID: [ { name: "entity", type: "address" }, { name: "passport", type: "address" }, @@ -2525,7 +2584,7 @@ describe("VeBetterPassport - @shard2", function () { verifyingContract: await veBetterPassport.getAddress(), } - const types = { + let types = { LinkEntity: [ { name: "entity", type: "address" }, { name: "passport", type: "address" }, @@ -3508,7 +3567,7 @@ describe("VeBetterPassport - @shard2", function () { chainId: 1337, verifyingContract: await veBetterPassport.getAddress(), } - const types = { + let types = { Delegation: [ { name: "delegator", type: "address" }, { name: "delegatee", type: "address" }, @@ -4592,7 +4651,7 @@ describe("VeBetterPassport - @shard2", function () { chainId: 1337, verifyingContract: await veBetterPassport.getAddress(), } - const types = { + let types = { Delegation: [ { name: "wrong_field_1", type: "address" }, { name: "wrong_field_2", type: "address" }, @@ -6605,6 +6664,411 @@ describe("VeBetterPassport - @shard2", function () { }) }) + describe("Passport GM check", function () { + it("isPerson should return true if user has GM token above threshold level", async function () { + const config = createTestConfig() + config.VEPASSPORT_GALAXY_MEMBER_MINIMUM_LEVEL = 5 + const { veBetterPassport, owner, otherAccount, galaxyMember, b3tr, minterAccount } = + await getOrDeployContractInstances({ + forceDeploy: true, + config, + }) + + // Toggle GM check + await veBetterPassport.connect(owner).toggleCheck(5) + + // Set GM token level to 5 + await galaxyMember.connect(owner).setMaxLevel(5) + + // Bootstrap emissions + await bootstrapEmissions() + + // Should be able to free mint after participating in allocation voting -> User is whitelisted at this point + await participateInAllocationVoting(otherAccount) + + // Check if user is whitelisted + expect(await veBetterPassport.isPerson(otherAccount.address)).to.deep.equal([true, "User is whitelisted"]) + + // Mint GM token + await galaxyMember.connect(otherAccount).freeMint() + + // User should have GM token of level 1 at this point + expect( + await galaxyMember.levelOf(await galaxyMember.getSelectedTokenId(await otherAccount.getAddress())), + ).to.equal(1) + + // Disable whitelist check + await veBetterPassport.connect(owner).toggleCheck(1) + + // User should not be a person here as there GM totken level is below threshold of 5 + expect(await veBetterPassport.isPerson(otherAccount.address)).to.deep.equal([ + false, + "User does not meet the criteria to be considered a person", + ]) + + // Upgrade GM token to level 5 + await upgradeNFTtoLevel(1, 5, galaxyMember, b3tr, otherAccount, minterAccount) + + // User should have GM token of level 5 at this point + expect( + await galaxyMember.levelOf(await galaxyMember.getSelectedTokenId(await otherAccount.getAddress())), + ).to.equal(5) // Level 5 + + // User should be a person here as there GM token level is above threshold of 5 + expect(await veBetterPassport.isPerson(otherAccount.address)).to.deep.equal([ + true, + "User's selected Galaxy Member is above the minimum level", + ]) + }) + + it("isPersonAtTimePoint should return true if user has GM token above threshold level", async function () { + const config = createTestConfig() + config.VEPASSPORT_GALAXY_MEMBER_MINIMUM_LEVEL = 5 + const { veBetterPassport, owner, otherAccount, galaxyMember, b3tr, minterAccount } = + await getOrDeployContractInstances({ + forceDeploy: true, + config, + }) + + // Toggle GM check + await veBetterPassport.connect(owner).toggleCheck(5) + + // Set GM token level to 5 + await galaxyMember.connect(owner).setMaxLevel(5) + + // Bootstrap emissions + await bootstrapEmissions() + + // Should be able to free mint after participating in allocation voting -> User is whitelisted at this point + await participateInAllocationVoting(otherAccount) + + // Check if user is whitelisted + expect(await veBetterPassport.isPerson(otherAccount.address)).to.deep.equal([true, "User is whitelisted"]) + + // Mint GM token + await galaxyMember.connect(otherAccount).freeMint() + + // User should have GM token of level 1 at this point + expect( + await galaxyMember.levelOf(await galaxyMember.getSelectedTokenId(await otherAccount.getAddress())), + ).to.equal(1) + + // Disable whitelist check + await veBetterPassport.connect(owner).toggleCheck(1) + + // User should not be a person here as there GM totken level is below threshold of 5 + expect(await veBetterPassport.isPerson(otherAccount.address)).to.deep.equal([ + false, + "User does not meet the criteria to be considered a person", + ]) + + // Upgrade GM token to level 5 + await upgradeNFTtoLevel(1, 5, galaxyMember, b3tr, otherAccount, minterAccount) + + // Get block number post upgrading GM token + const blockNumberPost = await ethers.provider.getBlockNumber() + + // User should have GM token of level 5 at this point + expect( + await galaxyMember.levelOf(await galaxyMember.getSelectedTokenId(await otherAccount.getAddress())), + ).to.equal(5) // Level 5 + + // User should be a person here as there GM token level is above threshold of 5 + expect(await veBetterPassport.isPersonAtTimepoint(otherAccount.address, blockNumberPost)).to.deep.equal([ + true, + "User's selected Galaxy Member is above the minimum level", + ]) + }) + + it("isPersonAtTimePoint should return true if user GM was upgraded since timepoint", async function () { + const config = createTestConfig() + config.VEPASSPORT_GALAXY_MEMBER_MINIMUM_LEVEL = 5 + const { veBetterPassport, owner, otherAccount, galaxyMember, b3tr, minterAccount } = + await getOrDeployContractInstances({ + forceDeploy: true, + config, + }) + + // Toggle GM check + await veBetterPassport.connect(owner).toggleCheck(5) + + // Set GM token level to 5 + await galaxyMember.connect(owner).setMaxLevel(5) + + // Bootstrap emissions + await bootstrapEmissions() + + // Should be able to free mint after participating in allocation voting -> User is whitelisted at this point + await participateInAllocationVoting(otherAccount) + + // Check if user is whitelisted + expect(await veBetterPassport.isPerson(otherAccount.address)).to.deep.equal([true, "User is whitelisted"]) + + // Mint GM token + await galaxyMember.connect(otherAccount).freeMint() + + // User should have GM token of level 1 at this point + expect( + await galaxyMember.levelOf(await galaxyMember.getSelectedTokenId(await otherAccount.getAddress())), + ).to.equal(1) + + // Disable whitelist check + await veBetterPassport.connect(owner).toggleCheck(1) + + // User should not be a person here as there GM totken level is below threshold of 5 + expect(await veBetterPassport.isPerson(otherAccount.address)).to.deep.equal([ + false, + "User does not meet the criteria to be considered a person", + ]) + + // Get block number prior to upgrading GM token + const blockNumberPre = await ethers.provider.getBlockNumber() + + // Upgrade GM token to level 5 + await upgradeNFTtoLevel(1, 5, galaxyMember, b3tr, otherAccount, minterAccount) + + // Get block number post upgrading GM token + const blockNumberPost = await ethers.provider.getBlockNumber() + + // User should have GM token of level 5 at this point + expect( + await galaxyMember.levelOf(await galaxyMember.getSelectedTokenId(await otherAccount.getAddress())), + ).to.equal(5) // Level 5 + + // User should be a person prior to upgrading GM token as token has since been upgraded + expect(await veBetterPassport.isPersonAtTimepoint(otherAccount.address, blockNumberPre)).to.deep.equal([ + true, + "User's selected Galaxy Member is above the minimum level", + ]) + + // User should be a person here as there GM token level is above threshold of 5 + expect(await veBetterPassport.isPersonAtTimepoint(otherAccount.address, blockNumberPost)).to.deep.equal([ + true, + "User's selected Galaxy Member is above the minimum level", + ]) + }) + + it("isPersonAtTimePoint should return false if user did not own GM at timepoint", async function () { + const config = createTestConfig() + config.VEPASSPORT_GALAXY_MEMBER_MINIMUM_LEVEL = 5 + const { veBetterPassport, owner, otherAccount, galaxyMember, b3tr, minterAccount } = + await getOrDeployContractInstances({ + forceDeploy: true, + config, + }) + + // Toggle GM check + await veBetterPassport.connect(owner).toggleCheck(5) + + // Set GM token level to 5 + await galaxyMember.connect(owner).setMaxLevel(5) + + // Bootstrap emissions + await bootstrapEmissions() + + // Should be able to free mint after participating in allocation voting -> User is whitelisted at this point + await participateInAllocationVoting(otherAccount) + + // Check if user is whitelisted + expect(await veBetterPassport.isPerson(otherAccount.address)).to.deep.equal([true, "User is whitelisted"]) + + // Mint GM token + await galaxyMember.connect(otherAccount).freeMint() + // Upgrade GM token to level 5 + await upgradeNFTtoLevel(1, 5, galaxyMember, b3tr, otherAccount, minterAccount) + + // User should have GM token of level 5 at this point + expect( + await galaxyMember.levelOf(await galaxyMember.getSelectedTokenId(await otherAccount.getAddress())), + ).to.equal(5) + + // Disable whitelist check + await veBetterPassport.connect(owner).toggleCheck(1) + + // Get block number prior to transferring GM token + const blockNumberPre = await ethers.provider.getBlockNumber() + + await galaxyMember.connect(otherAccount).transferFrom(otherAccount.address, owner.address, 1) + + // Get block number post upgrading GM token + const blockNumberPost = await ethers.provider.getBlockNumber() + + // User who no longer owns GM token be a person at timepoint pre transfer + expect(await veBetterPassport.isPersonAtTimepoint(otherAccount.address, blockNumberPre)).to.deep.equal([ + true, + "User's selected Galaxy Member is above the minimum level", + ]) + + // User who no longer owns GM token be a person at timepoint post transfer + expect(await veBetterPassport.isPersonAtTimepoint(otherAccount.address, blockNumberPost)).to.deep.equal([ + false, + "User does not meet the criteria to be considered a person", + ]) + + // User who received GM token be a person at timepoint post transfer + expect(await veBetterPassport.isPersonAtTimepoint(owner.address, blockNumberPost)).to.deep.equal([ + true, + "User's selected Galaxy Member is above the minimum level", + ]) + + // User who received GM token shoould not be a person at timepoint pre transfer + expect(await veBetterPassport.isPersonAtTimepoint(owner.address, blockNumberPre)).to.deep.equal([ + false, + "User does not meet the criteria to be considered a person", + ]) + + // They should be a person at the current block + expect(await veBetterPassport.isPerson(owner.address)).to.deep.equal([ + true, + "User's selected Galaxy Member is above the minimum level", + ]) + + // User who no longer owns GM token should not be a person at the current block + expect(await veBetterPassport.isPerson(otherAccount.address)).to.deep.equal([ + false, + "User does not meet the criteria to be considered a person", + ]) + }) + + it("isPersonAtTimePoint should return false if user did have selected GM at timepoint", async function () { + const config = createTestConfig() + config.VEPASSPORT_GALAXY_MEMBER_MINIMUM_LEVEL = 5 + const { veBetterPassport, owner, otherAccount, galaxyMember, b3tr, minterAccount } = + await getOrDeployContractInstances({ + forceDeploy: true, + config, + }) + + // Toggle GM check + await veBetterPassport.connect(owner).toggleCheck(5) + + // Set GM token level to 5 + await galaxyMember.connect(owner).setMaxLevel(5) + + // Bootstrap emissions + await bootstrapEmissions() + + // Should be able to free mint after participating in allocation voting -> User is whitelisted at this point + await participateInAllocationVoting(otherAccount) + + // Disable whitelist check + await veBetterPassport.connect(owner).toggleCheck(1) + + // Mint 2 GM tokens + await galaxyMember.connect(otherAccount).freeMint() + await galaxyMember.connect(otherAccount).freeMint() + + // User should own 2 GM tokens + expect(await galaxyMember.getTokensInfoByOwner(otherAccount.address, 0, 5)).to.have.lengthOf(2) + + // Users selected GM token should be token 1 + expect(await galaxyMember.getSelectedTokenId(otherAccount.address)).to.equal(1) + + // Users selected should have GM token of level 1 at this point + expect( + await galaxyMember.levelOf(await galaxyMember.getSelectedTokenId(await otherAccount.getAddress())), + ).to.equal(1) + + // Upgrade the users other GM token to level 5 + await upgradeNFTtoLevel(2, 5, galaxyMember, b3tr, otherAccount, minterAccount) + + // Get block number prior to changing selected GM token + const blockNumberPre = await ethers.provider.getBlockNumber() + + // Change selected GM token to token 2 + await galaxyMember.connect(otherAccount).select(2) + + // Get block number post changing selected GM token + const blockNumberPost = await ethers.provider.getBlockNumber() + + // Users selected GM token should be token 2 + expect(await galaxyMember.getSelectedTokenId(otherAccount.address)).to.equal(2) + + // User should not be a person at timepoint pre changing selected GM token + expect(await veBetterPassport.isPersonAtTimepoint(otherAccount.address, blockNumberPre)).to.deep.equal([ + false, + "User does not meet the criteria to be considered a person", + ]) + + // User should be a person at timepoint post changing selected GM token + expect(await veBetterPassport.isPersonAtTimepoint(otherAccount.address, blockNumberPost)).to.deep.equal([ + true, + "User's selected Galaxy Member is above the minimum level", + ]) + + // User should be a person at the current block + expect(await veBetterPassport.isPerson(otherAccount.address)).to.deep.equal([ + true, + "User's selected Galaxy Member is above the minimum level", + ]) + }) + + it("if GM check threshold is updated, should return correct personhood status", async function () { + const config = createTestConfig() + config.VEPASSPORT_GALAXY_MEMBER_MINIMUM_LEVEL = 5 + const { veBetterPassport, owner, otherAccount, galaxyMember, b3tr, minterAccount } = + await getOrDeployContractInstances({ + forceDeploy: true, + config, + }) + + // Toggle GM check + await veBetterPassport.connect(owner).toggleCheck(5) + + // Set GM token level to 5 + await galaxyMember.connect(owner).setMaxLevel(5) + + // Bootstrap emissions + await bootstrapEmissions() + + // Should be able to free mint after participating in allocation voting -> User is whitelisted at this point + await participateInAllocationVoting(otherAccount) + + // Check if user is whitelisted + expect(await veBetterPassport.isPerson(otherAccount.address)).to.deep.equal([true, "User is whitelisted"]) + + // Mint GM token + await galaxyMember.connect(otherAccount).freeMint() + + // User should have GM token of level 1 at this point + expect( + await galaxyMember.levelOf(await galaxyMember.getSelectedTokenId(await otherAccount.getAddress())), + ).to.equal(1) + + // Disable whitelist check + await veBetterPassport.connect(owner).toggleCheck(1) + + // User should not be a person here as there GM totken level is below threshold of 5 + expect(await veBetterPassport.isPerson(otherAccount.address)).to.deep.equal([ + false, + "User does not meet the criteria to be considered a person", + ]) + + // Upgrade GM token to level 5 + await upgradeNFTtoLevel(1, 5, galaxyMember, b3tr, otherAccount, minterAccount) + + // User should have GM token of level 5 at this point + expect( + await galaxyMember.levelOf(await galaxyMember.getSelectedTokenId(await otherAccount.getAddress())), + ).to.equal(5) // Level 5 + + // User should be a person here as there GM token level is above threshold of 5 + expect(await veBetterPassport.isPerson(otherAccount.address)).to.deep.equal([ + true, + "User's selected Galaxy Member is above the minimum level", + ]) + + // Update GM check threshold to 100 + await veBetterPassport.connect(owner).setMinimumGalaxyMemberLevel(100) + + // User should not be a person here as there GM token level is below threshold of 100 + expect(await veBetterPassport.isPerson(otherAccount.address)).to.deep.equal([ + false, + "User does not meet the criteria to be considered a person", + ]) + }) + }) + describe("Governance & X Allocation Voting", function () { it("Should register participation correctly through emission's cycles", async function () { const config = createTestConfig() diff --git a/test/VoterRewards.test.ts b/test/VoterRewards.test.ts index 9d18e1a..18bf0df 100644 --- a/test/VoterRewards.test.ts +++ b/test/VoterRewards.test.ts @@ -21,6 +21,7 @@ import { addNodeToken, bootstrapAndStartEmissions, payDeposit, + moveBlocks, } from "./helpers" import { expect } from "chai" import { ethers } from "hardhat" @@ -32,9 +33,11 @@ import { B3TRGovernor, GalaxyMember, GalaxyMemberV1, + GalaxyMemberV2, VoterRewards, VoterRewardsV1, VoterRewardsV2, + VoterRewardsV3, XAllocationVoting, } from "../typechain-types" import { time } from "@nomicfoundation/hardhat-network-helpers" @@ -441,10 +444,10 @@ describe("VoterRewards - @shard2", () => { forceDeploy: true, }) - expect(await voterRewards.version()).to.equal("3") + expect(await voterRewards.version()).to.equal("4") }) - it("Should not have state conflict after upgrading to V2 and V3", async () => { + it("Should not have state conflict after upgrading to V3 and V4", async () => { const config = createLocalConfig() const { otherAccounts, @@ -892,26 +895,26 @@ describe("VoterRewards - @shard2", () => { ;(await upgradeProxy( "GalaxyMemberV1", - "GalaxyMember", + "GalaxyMemberV2", await galaxyMemberV1.getAddress(), [ await vechainNodesMock.getAddress(), await nodeManagement.getAddress(), owner.address, - config.GM_NFT_NODE_TO_FREE_LEVEL + config.GM_NFT_NODE_TO_FREE_LEVEL, ], { version: 2 }, - )) as unknown as GalaxyMember + )) as unknown as GalaxyMemberV2 const voterRewardsV3 = (await upgradeProxy( "VoterRewardsV2", - "VoterRewards", + "VoterRewardsV3", await voterRewardsV1.getAddress(), [], { version: 3, }, - )) as VoterRewards + )) as VoterRewardsV3 await waitForNextCycle() @@ -942,6 +945,55 @@ describe("VoterRewards - @shard2", () => { for (let i = 0; i < storageSlots.length; i++) { expect(storageSlots[i]).to.equal(storageSlotsAfter[i]) } + + // Upgrade to V4 + await upgradeProxy("GalaxyMemberV2", "GalaxyMember", await galaxyMemberV1.getAddress(), [], { + version: 3, + }) + + const voterRewardsV4 = (await upgradeProxy( + "VoterRewardsV3", + "VoterRewards", + await voterRewardsV1.getAddress(), + [], + { + version: 3, + }, + )) as VoterRewards + + let storageSlotsV4 = [] + for (let i = initialSlot; i < initialSlot + BigInt(100); i++) { + storageSlotsV4.push(await ethers.provider.getStorage(await voterRewardsV4.getAddress(), i)) + } + + // Check if storage slots are the same after upgrade + for (let i = 0; i < storageSlots.length; i++) { + expect(storageSlotsAfter[i]).to.equal(storageSlotsV4[i]) + } + + await waitForNextCycle() + + // start round + await emissions.connect(voter1).distribute() // Anyone can distribute the cycle + + const roundId4 = await xAllocationVoting.currentRoundId() + + expect(roundId4).to.equal(4) + + await xAllocationVoting + .connect(voter1) + .castVote(roundId4, [app1, app2], [ethers.parseEther("0"), ethers.parseEther("1000")]) + await xAllocationVoting + .connect(voter2) + .castVote(roundId4, [app1, app2], [ethers.parseEther("100"), ethers.parseEther("500")]) + + // Wait for round to end + const deadline = await xAllocationVoting.roundDeadline(roundId4) + const currentBlock = await xAllocationVoting.clock() + await moveBlocks(parseInt((deadline - currentBlock + BigInt(1)).toString())) + + await expect(voterRewardsV4.connect(voter1).claimReward(4, voter1)).to.emit(voterRewardsV4, "RewardClaimed") + await expect(voterRewardsV4.connect(voter2).claimReward(4, voter2)).to.emit(voterRewardsV4, "RewardClaimed") }) }) @@ -1614,7 +1666,7 @@ describe("VoterRewards - @shard2", () => { await vechainNodesMock.getAddress(), await nodeManagement.getAddress(), owner.address, - config.GM_NFT_NODE_TO_FREE_LEVEL + config.GM_NFT_NODE_TO_FREE_LEVEL, ], { version: 2 }, )) as unknown as GalaxyMember @@ -1765,7 +1817,7 @@ describe("VoterRewards - @shard2", () => { await vechainNodesMock.getAddress(), await nodeManagement.getAddress(), owner.address, - config.GM_NFT_NODE_TO_FREE_LEVEL + config.GM_NFT_NODE_TO_FREE_LEVEL, ], { version: 2 }, )) as unknown as GalaxyMember @@ -1924,7 +1976,7 @@ describe("VoterRewards - @shard2", () => { await vechainNodesMock.getAddress(), await nodeManagement.getAddress(), owner.address, - config.GM_NFT_NODE_TO_FREE_LEVEL + config.GM_NFT_NODE_TO_FREE_LEVEL, ], { version: 2 }, )) as unknown as GalaxyMember @@ -2108,7 +2160,7 @@ describe("VoterRewards - @shard2", () => { await vechainNodesMock.getAddress(), await nodeManagement.getAddress(), owner.address, - config.GM_NFT_NODE_TO_FREE_LEVEL + config.GM_NFT_NODE_TO_FREE_LEVEL, ], { version: 2 }, )) as unknown as GalaxyMember @@ -2261,7 +2313,7 @@ describe("VoterRewards - @shard2", () => { await vechainNodesMock.getAddress(), await nodeManagement.getAddress(), owner.address, - config.GM_NFT_NODE_TO_FREE_LEVEL + config.GM_NFT_NODE_TO_FREE_LEVEL, ], { version: 2 }, )) as unknown as GalaxyMember @@ -2653,7 +2705,7 @@ describe("VoterRewards - @shard2", () => { await vechainNodesMock.getAddress(), await nodeManagement.getAddress(), owner.address, - config.GM_NFT_NODE_TO_FREE_LEVEL + config.GM_NFT_NODE_TO_FREE_LEVEL, ], { version: 2 }, )) as unknown as GalaxyMember @@ -2782,7 +2834,7 @@ describe("VoterRewards - @shard2", () => { await vechainNodesMock.getAddress(), await nodeManagement.getAddress(), owner.address, - config.GM_NFT_NODE_TO_FREE_LEVEL + config.GM_NFT_NODE_TO_FREE_LEVEL, ], { version: 2 }, )) as unknown as GalaxyMember diff --git a/test/XApps.test.ts b/test/XApps.test.ts index b3ca5bb..99284e1 100644 --- a/test/XApps.test.ts +++ b/test/XApps.test.ts @@ -916,7 +916,7 @@ describe("X-Apps - @shard3", function () { const testKeys = getTestKeys(50) - const eligibleAppIds: string[] = [] + let eligibleAppIds: string[] = [] APPS.forEach(async (app, index) => { const tx = await x2EarnAppsV1.addApp(app.teamWalletAddress, app.admin, app.name, app.metadataURI) await tx.wait() @@ -1230,17 +1230,17 @@ describe("X-Apps - @shard3", function () { const { x2EarnApps, otherAccounts, owner } = await getOrDeployContractInstances({ forceDeploy: true }) const app1Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[0].address)) - const tx = await x2EarnApps + let tx = await x2EarnApps .connect(owner) .submitApp(otherAccounts[0].address, otherAccounts[0].address, otherAccounts[0].address, "metadataURI") - const receipt = await tx.wait() + let receipt = await tx.wait() if (!receipt) throw new Error("No receipt") - const appAdded = filterEventsByName(receipt.logs, "AppAdded") + let appAdded = filterEventsByName(receipt.logs, "AppAdded") expect(appAdded).not.to.eql([]) - const { id, address } = await parseAppAddedEvent(appAdded[0]) + let { id, address } = await parseAppAddedEvent(appAdded[0]) expect(id).to.eql(app1Id) expect(address).to.eql(otherAccounts[0].address) }) @@ -1543,7 +1543,7 @@ describe("X-Apps - @shard3", function () { const appId = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[0].address)) await endorseApp(appId, otherAccounts[0]) - const roundId = await startNewAllocationRound() + let roundId = await startNewAllocationRound() const isEligibleForVote = await xAllocationVoting.isEligibleForVote(app1Id, roundId) expect(isEligibleForVote).to.eql(true) @@ -1562,7 +1562,7 @@ describe("X-Apps - @shard3", function () { .submitApp(otherAccounts[0].address, otherAccounts[0].address, otherAccounts[0].address, "metadataURI") await endorseApp(app1Id, otherAccounts[0]) - const round1 = await startNewAllocationRound() + let round1 = await startNewAllocationRound() await x2EarnApps.connect(owner).setVotingEligibility(app1Id, false) @@ -1574,7 +1574,7 @@ describe("X-Apps - @shard3", function () { expect(appsVotedInSpecificRound.length).to.equal(1n) await waitForRoundToEnd(round1) - const round2 = await startNewAllocationRound() + let round2 = await startNewAllocationRound() // app should not be eligible from this round isEligibleForVote = await xAllocationVoting.isEligibleForVote(app1Id, round2) @@ -1606,7 +1606,7 @@ describe("X-Apps - @shard3", function () { await x2EarnApps.connect(owner).setVotingEligibility(app1Id, false) expect(await x2EarnApps.isEligibleNow(app1Id)).to.eql(false) - const round1 = await startNewAllocationRound() + let round1 = await startNewAllocationRound() // app should still be eligible for the current round let isEligibleForVote = await xAllocationVoting.isEligibleForVote(app1Id, round1) @@ -1621,7 +1621,7 @@ describe("X-Apps - @shard3", function () { await waitForRoundToEnd(round1) - const round2 = await startNewAllocationRound() + let round2 = await startNewAllocationRound() // app should be eligible from this round isEligibleForVote = await xAllocationVoting.isEligibleForVote(app1Id, round2) @@ -1704,7 +1704,7 @@ describe("X-Apps - @shard3", function () { // start new round await emissions.distribute() - const round1 = await xAllocationVoting.currentRoundId() + let round1 = await xAllocationVoting.currentRoundId() let isEligibleForVote = await xAllocationVoting.isEligibleForVote(app1Id, round1) expect(isEligibleForVote).to.eql(true) @@ -1733,7 +1733,7 @@ describe("X-Apps - @shard3", function () { await waitForCurrentRoundToEnd() await emissions.distribute() - const round2 = await xAllocationVoting.currentRoundId() + let round2 = await xAllocationVoting.currentRoundId() // app should not be eligible from this round isEligibleForVote = await xAllocationVoting.isEligibleForVote(app1Id, round2) @@ -1764,7 +1764,7 @@ describe("X-Apps - @shard3", function () { const app1Id = await x2EarnApps.hashAppName(otherAccounts[0].address) - const round1 = await startNewAllocationRound() + let round1 = await startNewAllocationRound() await veBetterPassport.whitelist(voter.address) await veBetterPassport.toggleCheck(1) @@ -1784,11 +1784,11 @@ describe("X-Apps - @shard3", function () { let appVotes = await xAllocationVoting.getAppVotes(round1, app1Id) expect(appVotes).to.equal(0n) - const appsVotedInSpecificRound = await xAllocationVoting.getAppIdsOfRound(round1) + let appsVotedInSpecificRound = await xAllocationVoting.getAppIdsOfRound(round1) expect(appsVotedInSpecificRound.length).to.equal(0) await waitForRoundToEnd(round1) - const round2 = await startNewAllocationRound() + let round2 = await startNewAllocationRound() // app should be eligible from this round isEligibleForVote = await xAllocationVoting.isEligibleForVote(app1Id, round2) @@ -3135,7 +3135,7 @@ describe("X-Apps - @shard3", function () { .submitApp(otherAccounts[0].address, otherAccounts[0].address, "My app", "metadataURI") await x2EarnApps.connect(owner).setTeamAllocationPercentage(app1Id, 50) - const teamAllocationPercentage = await x2EarnApps.teamAllocationPercentage(app1Id) + let teamAllocationPercentage = await x2EarnApps.teamAllocationPercentage(app1Id) expect(teamAllocationPercentage).to.eql(50n) }) @@ -3185,7 +3185,7 @@ describe("X-Apps - @shard3", function () { .submitApp(otherAccounts[0].address, otherAccounts[0].address, "My app", "metadataURI") await x2EarnApps.connect(owner).setTeamAllocationPercentage(app1Id, 50) - const teamAllocationPercentage = await x2EarnApps.teamAllocationPercentage(app1Id) + let teamAllocationPercentage = await x2EarnApps.teamAllocationPercentage(app1Id) expect(teamAllocationPercentage).to.eql(50n) }) @@ -3377,7 +3377,7 @@ describe("X-Apps - @shard3", function () { expect(await x2EarnApps.nodeToEndorsedApp(1)).to.eql(app1Id) // Node ID 1 has endorsed app1Id - const round1 = await startNewAllocationRound() + let round1 = await startNewAllocationRound() // app should be eligible for the current round let isEligibleForVote = await xAllocationVoting.isEligibleForVote(app1Id, round1) @@ -3394,9 +3394,9 @@ describe("X-Apps - @shard3", function () { const receipt = await tx.wait() if (!receipt) throw new Error("No receipt") - const events = receipt?.logs + let events = receipt?.logs - const decodedEvents = events?.map(event => { + let decodedEvents = events?.map(event => { return x2EarnApps.interface.parseLog({ topics: event?.topics as string[], data: event?.data as string, @@ -3416,7 +3416,7 @@ describe("X-Apps - @shard3", function () { await waitForCurrentRoundToEnd() // start new round - const round2 = await startNewAllocationRound() + let round2 = await startNewAllocationRound() // app should still be eligible for the current round as it is in the grace period isEligibleForVote = await xAllocationVoting.isEligibleForVote(app1Id, round2) @@ -3447,7 +3447,7 @@ describe("X-Apps - @shard3", function () { await x2EarnApps.connect(otherAccounts[1]).endorseApp(app1Id, 1) // Node holder endorsement score is 50 await x2EarnApps.connect(otherAccounts[2]).endorseApp(app1Id, 2) // Node holder endorsement score is 50 - const round1 = await startNewAllocationRound() + let round1 = await startNewAllocationRound() // app should be eligible for the current round let isEligibleForVote = await xAllocationVoting.isEligibleForVote(app1Id, round1) @@ -3462,9 +3462,9 @@ describe("X-Apps - @shard3", function () { const receipt = await tx.wait() if (!receipt) throw new Error("No receipt") - const events = receipt?.logs + let events = receipt?.logs - const decodedEvents = events?.map(event => { + let decodedEvents = events?.map(event => { return x2EarnApps.interface.parseLog({ topics: event?.topics as string[], data: event?.data as string, @@ -3484,7 +3484,7 @@ describe("X-Apps - @shard3", function () { await waitForCurrentRoundToEnd() // start new round - const round2 = await startNewAllocationRound() + let round2 = await startNewAllocationRound() // app should still be eligible for the current round as it is in the grace period isEligibleForVote = await xAllocationVoting.isEligibleForVote(app1Id, round2) @@ -3515,7 +3515,7 @@ describe("X-Apps - @shard3", function () { await x2EarnApps.connect(otherAccounts[1]).endorseApp(app1Id, 1) // Node holder endorsement score is 50 await x2EarnApps.connect(otherAccounts[2]).endorseApp(app1Id, 2) // Node holder endorsement score is 50 - const round1 = await startNewAllocationRound() + let round1 = await startNewAllocationRound() // app should be eligible for the current round let isEligibleForVote = await xAllocationVoting.isEligibleForVote(app1Id, round1) @@ -3547,7 +3547,7 @@ describe("X-Apps - @shard3", function () { await waitForCurrentRoundToEnd() // start new round -> 1st cycle unedorsed - const round2 = await startNewAllocationRound() + let round2 = await startNewAllocationRound() // app should still be eligible for the current round as it is in the grace period isEligibleForVote = await xAllocationVoting.isEligibleForVote(app1Id, round2) @@ -3560,7 +3560,7 @@ describe("X-Apps - @shard3", function () { await waitForCurrentRoundToEnd() // start new round -> 2nd cycle unendorsed - const round3 = await startNewAllocationRound() + let round3 = await startNewAllocationRound() // app should still be eligible for the current round as it is in the grace period isEligibleForVote = await xAllocationVoting.isEligibleForVote(app1Id, round3) @@ -3573,7 +3573,7 @@ describe("X-Apps - @shard3", function () { await waitForCurrentRoundToEnd() // start new round -> 3rd cycle unendorsed - const round4 = await startNewAllocationRound() + let round4 = await startNewAllocationRound() // app should not be eligible for the current round as it is not in the grace period isEligibleForVote = await xAllocationVoting.isEligibleForVote(app1Id, round4) @@ -3608,7 +3608,7 @@ describe("X-Apps - @shard3", function () { await x2EarnApps.connect(otherAccounts[1]).endorseApp(app1Id, 1) // Node holder endorsement score is 50 await x2EarnApps.connect(otherAccounts[2]).endorseApp(app1Id, 2) // Node holder endorsement score is 50 - const round1 = await startNewAllocationRound() + let round1 = await startNewAllocationRound() // app should be eligible for the current round let isEligibleForVote = await xAllocationVoting.isEligibleForVote(app1Id, round1) @@ -3624,8 +3624,8 @@ describe("X-Apps - @shard3", function () { if (!receipt) throw new Error("No receipt") // check event emitted - const events = receipt?.logs - const decodedEvents = events?.map(event => { + let events = receipt?.logs + let decodedEvents = events?.map(event => { return x2EarnApps.interface.parseLog({ topics: event?.topics as string[], data: event?.data as string, @@ -3652,7 +3652,7 @@ describe("X-Apps - @shard3", function () { await waitForCurrentRoundToEnd() // start new round -> 1st cycle unedorsed - const round2 = await startNewAllocationRound() + let round2 = await startNewAllocationRound() // app should still be eligible for the current round as it is in the grace period isEligibleForVote = await xAllocationVoting.isEligibleForVote(app1Id, round2) @@ -3665,7 +3665,7 @@ describe("X-Apps - @shard3", function () { await waitForCurrentRoundToEnd() // start new round -> 2nd cycle unendorsed - const round3 = await startNewAllocationRound() + let round3 = await startNewAllocationRound() // app should still be eligible for the current round as it is in the grace period isEligibleForVote = await xAllocationVoting.isEligibleForVote(app1Id, round3) @@ -3678,7 +3678,7 @@ describe("X-Apps - @shard3", function () { await waitForCurrentRoundToEnd() // start new round -> 3rd cycle unendorsed - const round4 = await startNewAllocationRound() + let round4 = await startNewAllocationRound() // app should not be eligible for the current round as it is not in the grace period isEligibleForVote = await xAllocationVoting.isEligibleForVote(app1Id, round4) @@ -3710,7 +3710,7 @@ describe("X-Apps - @shard3", function () { await x2EarnApps.connect(otherAccounts[1]).endorseApp(app1Id, 1) // Node holder endorsement score is 50 await x2EarnApps.connect(otherAccounts[2]).endorseApp(app1Id, 2) // Node holder endorsement score is 50 - const round1 = await startNewAllocationRound() + let round1 = await startNewAllocationRound() // app should be eligible for the current round let isEligibleForVote = await xAllocationVoting.isEligibleForVote(app1Id, round1) @@ -3726,8 +3726,8 @@ describe("X-Apps - @shard3", function () { if (!receipt) throw new Error("No receipt") // check event emitted - const events = receipt?.logs - const decodedEvents = events?.map(event => { + let events = receipt?.logs + let decodedEvents = events?.map(event => { return x2EarnApps.interface.parseLog({ topics: event?.topics as string[], data: event?.data as string, @@ -3754,7 +3754,7 @@ describe("X-Apps - @shard3", function () { await waitForCurrentRoundToEnd() // start new round -> 1st cycle unedorsed - const round2 = await startNewAllocationRound() + let round2 = await startNewAllocationRound() // app should still be eligible for the current round as it is in the grace period isEligibleForVote = await xAllocationVoting.isEligibleForVote(app1Id, round2) @@ -3767,7 +3767,7 @@ describe("X-Apps - @shard3", function () { await waitForCurrentRoundToEnd() // start new round -> 2nd cycle unendorsed - const round3 = await startNewAllocationRound() + let round3 = await startNewAllocationRound() // app should still be eligible for the current round as it is in the grace period isEligibleForVote = await xAllocationVoting.isEligibleForVote(app1Id, round3) @@ -3780,7 +3780,7 @@ describe("X-Apps - @shard3", function () { await waitForCurrentRoundToEnd() // start new round -> 3rd cycle unendorsed - const round4 = await startNewAllocationRound() + let round4 = await startNewAllocationRound() // app should not be eligible for the current round as it is not in the grace period isEligibleForVote = await xAllocationVoting.isEligibleForVote(app1Id, round4) @@ -3815,7 +3815,7 @@ describe("X-Apps - @shard3", function () { await x2EarnApps.connect(otherAccounts[1]).endorseApp(app1Id, 1) // Node holder endorsement score is 50 await x2EarnApps.connect(otherAccounts[2]).endorseApp(app1Id, 2) // Node holder endorsement score is 50 - const round1 = await startNewAllocationRound() + let round1 = await startNewAllocationRound() // app should be eligible for the current round let isEligibleForVote = await xAllocationVoting.isEligibleForVote(app1Id, round1) @@ -3831,8 +3831,8 @@ describe("X-Apps - @shard3", function () { if (!receipt) throw new Error("No receipt") // check event emitted - const events = receipt?.logs - const decodedEvents = events?.map(event => { + let events = receipt?.logs + let decodedEvents = events?.map(event => { return x2EarnApps.interface.parseLog({ topics: event?.topics as string[], data: event?.data as string, @@ -3859,7 +3859,7 @@ describe("X-Apps - @shard3", function () { await waitForCurrentRoundToEnd() // start new round -> 1st cycle unedorsed - const round2 = await startNewAllocationRound() + let round2 = await startNewAllocationRound() // app should still be eligible for the current round as it is in the grace period isEligibleForVote = await xAllocationVoting.isEligibleForVote(app1Id, round2) @@ -3888,7 +3888,7 @@ describe("X-Apps - @shard3", function () { await waitForCurrentRoundToEnd() // start new round -> 2nd cycle unendorsed - const round3 = await startNewAllocationRound() + let round3 = await startNewAllocationRound() // app id not eligible for the current round isEligibleForVote = await xAllocationVoting.isEligibleForVote(app1Id, round3) @@ -3928,7 +3928,7 @@ describe("X-Apps - @shard3", function () { await x2EarnApps.connect(otherAccounts[1]).endorseApp(app1Id, 1) // Node holder endorsement score is 50 await x2EarnApps.connect(otherAccounts[2]).endorseApp(app1Id, 2) // Node holder endorsement score is 50 - const round1 = await startNewAllocationRound() + let round1 = await startNewAllocationRound() // app should be eligible for the current round let isEligibleForVote = await xAllocationVoting.isEligibleForVote(app1Id, round1) @@ -3954,7 +3954,7 @@ describe("X-Apps - @shard3", function () { await waitForCurrentRoundToEnd() // start new round -> 1st cycle unedorsed - const round2 = await startNewAllocationRound() + let round2 = await startNewAllocationRound() // app should still be eligible for the current round as it is in the grace period isEligibleForVote = await xAllocationVoting.isEligibleForVote(app1Id, round2) @@ -3967,7 +3967,7 @@ describe("X-Apps - @shard3", function () { await waitForCurrentRoundToEnd() // start new round -> 2nd cycle unendorsed - const round3 = await startNewAllocationRound() + let round3 = await startNewAllocationRound() // app should still be eligible for the current round as it is in the grace period isEligibleForVote = await xAllocationVoting.isEligibleForVote(app1Id, round3) @@ -3983,7 +3983,7 @@ describe("X-Apps - @shard3", function () { await waitForCurrentRoundToEnd() // start new round -> 3rd cycle unendorsed - const round4 = await startNewAllocationRound() + let round4 = await startNewAllocationRound() // app should not be eligible for the current round as it is not in the grace period isEligibleForVote = await xAllocationVoting.isEligibleForVote(app1Id, round4) @@ -4005,7 +4005,7 @@ describe("X-Apps - @shard3", function () { await waitForCurrentRoundToEnd() // start new round -> 4th cycle reendorsed - const round5 = await startNewAllocationRound() + let round5 = await startNewAllocationRound() isEligibleForVote = await xAllocationVoting.isEligibleForVote(app1Id, round5) expect(isEligibleForVote).to.eql(true) }) @@ -4035,10 +4035,10 @@ describe("X-Apps - @shard3", function () { await x2EarnApps.connect(otherAccounts[1]).endorseApp(app1Id, 1) // Node holder endorsement score is 50 await x2EarnApps.connect(otherAccounts[2]).endorseApp(app1Id, 2) // Node holder endorsement score is 50 - const round1 = await startNewAllocationRound() + let round1 = await startNewAllocationRound() // app should be eligible for the current round - const isEligibleForVote = await xAllocationVoting.isEligibleForVote(app1Id, round1) + let isEligibleForVote = await xAllocationVoting.isEligibleForVote(app1Id, round1) expect(isEligibleForVote).to.eql(true) // Skip ahead 1 day to be able to transfer node @@ -4094,12 +4094,12 @@ describe("X-Apps - @shard3", function () { const tx = await x2EarnApps.connect(otherAccounts[1]).endorseApp(app1Id, 1) // Node holder endorsement score is 100 // Check event emitted - const receipt = await tx.wait() + let receipt = await tx.wait() if (!receipt) throw new Error("No receipt") - const events = receipt?.logs + let events = receipt?.logs - const decodedEvents = events?.map(event => { + let decodedEvents = events?.map(event => { return x2EarnApps.interface.parseLog({ topics: event?.topics as string[], data: event?.data as string, @@ -4755,7 +4755,7 @@ describe("X-Apps - @shard3", function () { await x2EarnApps.connect(otherAccounts[1]).endorseApp(app1Id, 1) // Node holder endorsement score is 50 await x2EarnApps.connect(otherAccounts[2]).endorseApp(app1Id, 2) // Node holder endorsement score is 50 - const round1 = await startNewAllocationRound() + let round1 = await startNewAllocationRound() // app should be eligible for the current round let isEligibleForVote = await xAllocationVoting.isEligibleForVote(app1Id, round1) @@ -4778,7 +4778,7 @@ describe("X-Apps - @shard3", function () { await waitForCurrentRoundToEnd() // start new round -> 1st cycle unedorsed - const round2 = await startNewAllocationRound() + let round2 = await startNewAllocationRound() // app should not be eligible for voting in current round isEligibleForVote = await xAllocationVoting.isEligibleForVote(app1Id, round2) @@ -4795,7 +4795,7 @@ describe("X-Apps - @shard3", function () { await waitForCurrentRoundToEnd() // start new round -> 2nd cycle unendorsed - const round3 = await startNewAllocationRound() + let round3 = await startNewAllocationRound() // app should still still not be eligible for the current round isEligibleForVote = await xAllocationVoting.isEligibleForVote(app1Id, round3) @@ -4974,7 +4974,7 @@ describe("X-Apps - @shard3", function () { await x2EarnApps.connect(otherAccounts[1]).endorseApp(app1Id, 1) // Node holder endorsement score is 50 await x2EarnApps.connect(otherAccounts[2]).endorseApp(app1Id, 2) // Node holder endorsement score is 50 - const round1 = await startNewAllocationRound() + let round1 = await startNewAllocationRound() // app should be eligible for the current round let isEligibleForVote = await xAllocationVoting.isEligibleForVote(app1Id, round1) @@ -4997,7 +4997,7 @@ describe("X-Apps - @shard3", function () { await waitForCurrentRoundToEnd() // start new round -> 1st cycle unedorsed - const round2 = await startNewAllocationRound() + let round2 = await startNewAllocationRound() // app should not be eligible for voting in current round isEligibleForVote = await xAllocationVoting.isEligibleForVote(app1Id, round2) @@ -5016,7 +5016,7 @@ describe("X-Apps - @shard3", function () { expect(await x2EarnApps.isAppUnendorsed(app1Id)).to.eql(false) // start new round - const round3 = await startNewAllocationRound() + let round3 = await startNewAllocationRound() expect(await xAllocationVoting.isEligibleForVote(app1Id, round3)).to.eql(true) @@ -5064,7 +5064,7 @@ describe("X-Apps - @shard3", function () { const appsInfo2 = await x2EarnApps.apps() expect(appsInfo2.length).to.eql(1) - const round1 = await startNewAllocationRound() + let round1 = await startNewAllocationRound() // app should be eligible for the current round let isEligibleForVote = await xAllocationVoting.isEligibleForVote(app1Id, round1) @@ -5087,7 +5087,7 @@ describe("X-Apps - @shard3", function () { await waitForCurrentRoundToEnd() // start new round -> 1st cycle unedorsed - const round2 = await startNewAllocationRound() + let round2 = await startNewAllocationRound() // app should not be eligible for voting in current round isEligibleForVote = await xAllocationVoting.isEligibleForVote(app1Id, round2) @@ -5128,7 +5128,7 @@ describe("X-Apps - @shard3", function () { expect((await x2EarnApps.unendorsedAppIds()).length).to.eql(1) // start new round - const round3 = await startNewAllocationRound() + let round3 = await startNewAllocationRound() expect(await xAllocationVoting.isEligibleForVote(app1Id, round3)).to.eql(true) @@ -5225,7 +5225,7 @@ describe("X-Apps - @shard3", function () { await waitForCurrentRoundToEnd() // start new round -> 1st cycle unedorsed - const round2 = await startNewAllocationRound() + let round2 = await startNewAllocationRound() // app should not be eligible for voting in current round expect(await xAllocationVoting.isEligibleForVote(app1Id, round2)).to.eql(false) @@ -5247,7 +5247,7 @@ describe("X-Apps - @shard3", function () { expect(eligbleApps.length).to.eql(1) // start new round - const round3 = await startNewAllocationRound() + let round3 = await startNewAllocationRound() // app should be eligible for the current round expect(await xAllocationVoting.isEligibleForVote(app1Id, round3)).to.eql(true) @@ -5698,10 +5698,10 @@ describe("X-Apps - @shard3", function () { expect(await x2EarnApps.nodeToEndorsedApp(1)).to.eql(app1Id) // Node ID 1 has endorsed app1Id - const round1 = await startNewAllocationRound() + let round1 = await startNewAllocationRound() // app should be eligible for the current round - const isEligibleForVote = await xAllocationVoting.isEligibleForVote(app1Id, round1) + let isEligibleForVote = await xAllocationVoting.isEligibleForVote(app1Id, round1) expect(isEligibleForVote).to.eql(true) // App is not pending endorsement @@ -5897,10 +5897,10 @@ describe("X-Apps - @shard3", function () { // app should be eligible for voting expect(await x2EarnApps.isEligibleNow(app1Id)).to.eql(true) - const round1 = await startNewAllocationRound() + let round1 = await startNewAllocationRound() // app should be eligible for the current round - const isEligibleForVote = await xAllocationVoting.isEligibleForVote(app1Id, round1) + let isEligibleForVote = await xAllocationVoting.isEligibleForVote(app1Id, round1) expect(isEligibleForVote).to.eql(true) // App is not pending endorsement @@ -5928,10 +5928,10 @@ describe("X-Apps - @shard3", function () { // app should be eligible for voting expect(await x2EarnApps.isEligibleNow(app1Id)).to.eql(true) - const round1 = await startNewAllocationRound() + let round1 = await startNewAllocationRound() // app should be eligible for the current round - const isEligibleForVote = await xAllocationVoting.isEligibleForVote(app1Id, round1) + let isEligibleForVote = await xAllocationVoting.isEligibleForVote(app1Id, round1) expect(isEligibleForVote).to.eql(true) await x2EarnApps.connect(owner).unendorseApp(app1Id, 1) diff --git a/test/helpers/deploy.ts b/test/helpers/deploy.ts index 2551e64..415a160 100644 --- a/test/helpers/deploy.ts +++ b/test/helpers/deploy.ts @@ -73,18 +73,38 @@ import { VoteEligibilityUtils, EndorsementUtils, X2EarnCreator, - B3TRGovernorV4, - VoterRewardsV2, - GalaxyMemberV1, + NodeManagementV1, + VeBetterPassportV2, + PassportConfiguratorV2, + PassportWhitelistAndBlacklistLogicV2, + PassportPoPScoreLogicV2, + PassportPersonhoodLogicV2, + PassportEntityLogicV2, + PassportDelegationLogicV2, + PassportChecksLogicV2, + PassportSignalingLogicV2, + VoterRewardsV3, } from "../../typechain-types" import { createLocalConfig } from "../../config/contracts/envs/local" -import { deployProxy, deployProxyOnly, initializeProxy, upgradeProxy } from "../../scripts/helpers" +import { deployAndUpgrade, deployProxy, deployProxyOnly, initializeProxy, upgradeProxy } from "../../scripts/helpers" import { bootstrapAndStartEmissions as callBootstrapAndStartEmissions } from "./common" import { governanceLibraries, passportLibraries } from "../../scripts/libraries" import { setWhitelistedFunctions } from "../../scripts/deploy/deploy" +import { B3TRGovernorV4 } from "../../typechain-types/contracts/deprecated/V4" +import { VoterRewardsV2 } from "../../typechain-types/contracts/deprecated/V2/VoterRewardsV2" import { XAllocationVotingV2 } from "../../typechain-types/contracts/deprecated/V2/XAllocationVotingV2" import { XAllocationPoolV2 } from "../../typechain-types/contracts/deprecated/V2/XAllocationPoolV2" import { X2EarnAppsV1 } from "../../typechain-types/contracts/deprecated/V1/X2EarnAppsV1" +import { + GovernorClockLogicV4, + GovernorConfiguratorV4, + GovernorDepositLogicV4, + GovernorFunctionRestrictionsLogicV4, + GovernorProposalLogicV4, + GovernorQuorumLogicV4, + GovernorStateLogicV4, + GovernorVotesLogicV4, +} from "../../typechain-types/contracts/deprecated/V4/governance/libraries" import { x2EarnLibraries } from "../../scripts/libraries/x2EarnLibraries" interface DeployInstance { @@ -98,7 +118,6 @@ interface DeployInstance { governorV3: B3TRGovernorV3 governorV4: B3TRGovernorV4 galaxyMember: GalaxyMember - galaxyMemberV1: GalaxyMemberV1 x2EarnApps: X2EarnApps xAllocationVoting: XAllocationVoting xAllocationPool: XAllocationPool @@ -142,11 +161,11 @@ interface DeployInstance { governorStateLogicLibV3: GovernorStateLogicV3 governorVotesLogicLibV3: GovernorVotesLogicV3 governorClockLogicLibV4: GovernorClockLogicV4 - governorConfiguratorLibV4: GovernorConfiguratorLibV4 - governorDepositLogicLibV4: GovernorDepositLogicLibV4 - governorFunctionRestrictionsLogicLibV4: GovernorFunctionRestrictionsLogicLibV4 - governorProposalLogicLibV4: GovernorProposalLogicLibV4 - governorQuorumLogicLibV4: GovernorQuorumLogicLibV4 + governorConfiguratorLibV4: GovernorConfiguratorV4 + governorDepositLogicLibV4: GovernorDepositLogicV4 + governorFunctionRestrictionsLogicLibV4: GovernorFunctionRestrictionsLogicV4 + governorProposalLogicLibV4: GovernorProposalLogicV4 + governorQuorumLogicLibV4: GovernorQuorumLogicV4 governorStateLogicLibV4: GovernorStateLogicV4 governorVotesLogicLibV4: GovernorVotesLogicV4 passportChecksLogic: PassportChecksLogic @@ -164,6 +183,14 @@ interface DeployInstance { passportSignalingLogicV1: PassportSignalingLogicV1 passportWhitelistBlacklistLogicV1: PassportWhitelistAndBlacklistLogicV1 passportConfiguratorV1: PassportConfiguratorV1 + passportChecksLogicV2: PassportChecksLogicV2 + passportDelegationLogicV2: PassportDelegationLogicV2 + passportEntityLogicV2: PassportEntityLogicV2 + passportPersonhoodLogicV2: PassportPersonhoodLogicV2 + passportPoPScoreLogicV2: PassportPoPScoreLogicV2 + passportSignalingLogicV2: PassportSignalingLogicV2 + passportWhitelistBlacklistLogicV2: PassportWhitelistAndBlacklistLogicV2 + passportConfiguratorV2: PassportConfiguratorV2 passportConfigurator: any // no abi for this library, which means a typechain is not generated administrationUtils: AdministrationUtils endorsementUtils: EndorsementUtils @@ -234,6 +261,14 @@ export const getOrDeployContractInstances = async ({ // Deploy Passport Libraries const { + PassportChecksLogicV2, + PassportConfiguratorV2, + PassportEntityLogicV2, + PassportDelegationLogicV2, + PassportPersonhoodLogicV2, + PassportPoPScoreLogicV2, + PassportSignalingLogicV2, + PassportWhitelistAndBlacklistLogicV2, PassportChecksLogicV1, PassportConfiguratorV1, PassportEntityLogicV1, @@ -318,44 +353,55 @@ export const getOrDeployContractInstances = async ({ config.TREASURY_TRANSFER_LIMIT_VTHO, ])) as Treasury - // Deploy GalaxyMember - const galaxyMemberV1 = (await deployProxy("GalaxyMemberV1", [ - { - name: NFT_NAME, - symbol: NFT_SYMBOL, - admin: owner.address, - upgrader: owner.address, - pauser: owner.address, - minter: owner.address, - contractsAddressManager: owner.address, - maxLevel: maxMintableLevel, - baseTokenURI: config.GM_NFT_BASE_URI, - b3trToUpgradeToLevel: config.GM_NFT_B3TR_REQUIRED_TO_UPGRADE_TO_LEVEL, - b3tr: await b3tr.getAddress(), - treasury: await treasury.getAddress(), - }, - ])) as GalaxyMemberV1 - const x2EarnCreator = (await deployProxy("X2EarnCreator", [config.CREATOR_NFT_URI, owner.address])) as X2EarnCreator // Deploy NodeManagement - const nodeManagement = (await deployProxy("NodeManagement", [ + const nodeManagementV1 = (await deployProxy("NodeManagementV1", [ await vechainNodesMock.getAddress(), owner.address, owner.address, - ])) as NodeManagement + ])) as NodeManagementV1 + + const nodeManagement = (await upgradeProxy( + "NodeManagementV1", + "NodeManagement", + await nodeManagementV1.getAddress(), + [], + { + version: 2, + }, + )) as NodeManagement - const galaxyMember = (await upgradeProxy( - "GalaxyMemberV1", - "GalaxyMember", - await galaxyMemberV1.getAddress(), + const galaxyMember = (await deployAndUpgrade( + ["GalaxyMemberV1", "GalaxyMemberV2", "GalaxyMember"], [ - await vechainNodesMock.getAddress(), - await nodeManagement.getAddress(), - owner.address, - config.GM_NFT_NODE_TO_FREE_LEVEL, + [ + { + name: NFT_NAME, + symbol: NFT_SYMBOL, + admin: owner.address, + upgrader: owner.address, + pauser: owner.address, + minter: owner.address, + contractsAddressManager: owner.address, + maxLevel: maxMintableLevel, + baseTokenURI: config.GM_NFT_BASE_URI, + b3trToUpgradeToLevel: config.GM_NFT_B3TR_REQUIRED_TO_UPGRADE_TO_LEVEL, + b3tr: await b3tr.getAddress(), + treasury: await treasury.getAddress(), + }, + ], + [ + await vechainNodesMock.getAddress(), + await nodeManagement.getAddress(), + owner.address, + config.GM_NFT_NODE_TO_FREE_LEVEL, + ], + [], ], - { version: 2 }, + { + versions: [undefined, 2, 3], + }, )) as GalaxyMember // Initialization requires the address of the x2EarnRewardsPool, for this reason we will initialize it after @@ -518,16 +564,19 @@ export const getOrDeployContractInstances = async ({ ;(await upgradeProxy("VoterRewardsV1", "VoterRewardsV2", await voterRewardsV1.getAddress(), [], { version: 2, })) as VoterRewardsV2 - - const voterRewards = (await upgradeProxy("VoterRewardsV2", "VoterRewards", await voterRewardsV1.getAddress(), [], { + ;(await upgradeProxy("VoterRewardsV2", "VoterRewardsV3", await voterRewardsV1.getAddress(), [], { version: 3, + })) as VoterRewardsV3 + + const voterRewards = (await upgradeProxy("VoterRewardsV3", "VoterRewards", await voterRewardsV1.getAddress(), [], { + version: 4, })) as VoterRewards // Set vote 2 earn (VoterRewards deployed contract) address in emissions await emissions.connect(owner).setVote2EarnAddress(await voterRewardsV1.getAddress()) // Deploy XAllocationVoting - const xAllocationVotingV1 = (await deployProxy("XAllocationVotingV1", [ + let xAllocationVotingV1 = (await deployProxy("XAllocationVotingV1", [ { vot3Token: await vot3.getAddress(), quorumPercentage: config.X_ALLOCATION_VOTING_QUORUM_PERCENTAGE, // quorum percentage @@ -605,13 +654,33 @@ export const getOrDeployContractInstances = async ({ }, )) as VeBetterPassportV1 - const veBetterPassport = (await upgradeProxy( + const veBetterPassportV2 = (await upgradeProxy( "VeBetterPassportV1", - "VeBetterPassport", + "VeBetterPassportV2", await veBetterPassportV1.getAddress(), [], { version: 2, + libraries: { + PassportChecksLogicV2: await PassportChecksLogicV2.getAddress(), + PassportConfiguratorV2: await PassportConfiguratorV2.getAddress(), + PassportEntityLogicV2: await PassportEntityLogicV2.getAddress(), + PassportDelegationLogicV2: await PassportDelegationLogicV2.getAddress(), + PassportPersonhoodLogicV2: await PassportPersonhoodLogicV2.getAddress(), + PassportPoPScoreLogicV2: await PassportPoPScoreLogicV2.getAddress(), + PassportSignalingLogicV2: await PassportSignalingLogicV2.getAddress(), + PassportWhitelistAndBlacklistLogicV2: await PassportWhitelistAndBlacklistLogicV2.getAddress(), + }, + }, + )) as VeBetterPassportV2 + + const veBetterPassport = (await upgradeProxy( + "VeBetterPassportV2", + "VeBetterPassport", + await veBetterPassportV1.getAddress(), + [], + { + version: 3, libraries: { PassportChecksLogic: await PassportChecksLogic.getAddress(), PassportConfigurator: await PassportConfigurator.getAddress(), @@ -832,7 +901,6 @@ export const getOrDeployContractInstances = async ({ governorV3, governorV4, galaxyMember, - galaxyMemberV1, x2EarnApps, xAllocationVoting, nodeManagement, @@ -898,6 +966,14 @@ export const getOrDeployContractInstances = async ({ passportPoPScoreLogicV1: PassportPoPScoreLogicV1, passportSignalingLogicV1: PassportSignalingLogicV1, passportWhitelistBlacklistLogicV1: PassportWhitelistAndBlacklistLogicV1, + passportChecksLogicV2: PassportChecksLogicV2, + passportDelegationLogicV2: PassportDelegationLogicV2, + passportEntityLogicV2: PassportEntityLogicV2, + passportPersonhoodLogicV2: PassportPersonhoodLogicV2, + passportPoPScoreLogicV2: PassportPoPScoreLogicV2, + passportSignalingLogicV2: PassportSignalingLogicV2, + passportWhitelistBlacklistLogicV2: PassportWhitelistAndBlacklistLogicV2, + passportConfiguratorV2: PassportConfiguratorV2, administrationUtils: AdministrationUtils, endorsementUtils: EndorsementUtils, voteEligibilityUtils: VoteEligibilityUtils, From 09fa6c368a7cb852c7b72810ad98533d600e60dc Mon Sep 17 00:00:00 2001 From: Dan Rusnac Date: Mon, 2 Dec 2024 13:02:44 +0100 Subject: [PATCH 2/2] chore: changelog --- CONTRACTS_CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRACTS_CHANGELOG.md b/CONTRACTS_CHANGELOG.md index 755bdc1..7ac3581 100644 --- a/CONTRACTS_CHANGELOG.md +++ b/CONTRACTS_CHANGELOG.md @@ -22,7 +22,7 @@ This document provides a detailed log of upgrades to the smart contract suite, e --- -## Upgrade `VeBetterPassport` to Version 3, and `GalaxyMember` to Version 3 +## Upgrade `VeBetterPassport` to Version 3, `GalaxyMember` to Version 3, and `VoterRewards` version 4 Added new personhood check in VeBetter passport, if a user owns a GM with a level greater than 1 they are considered a person.