From 127d6123266a003779000a3739b191f2643c9d6d Mon Sep 17 00:00:00 2001 From: Dan Rusnac Date: Fri, 11 Oct 2024 18:08:42 +0200 Subject: [PATCH 1/5] feat: add ve better passport and upgraded contracts --- CONTRACTS_CHANGELOG.md | 187 + config/contracts/envs/local.ts | 9 + config/contracts/type.ts | 9 + config/scripts/generateMockLocalConfig.ts | 1 + contracts/B3TRGovernor.sol | 21 +- contracts/X2EarnRewardsPool.sol | 47 +- contracts/XAllocationVoting.sol | 10 +- contracts/deprecated/V1/B3TRGovernorV1.sol | 2 +- contracts/deprecated/V1/EmissionsV1.sol | 10 +- .../deprecated/V1/XAllocationVotingV1.sol | 240 + .../deprecated/V1/interfaces/IEmissionsV1.sol | 142 + .../V1/interfaces/IX2EarnAppsV1.sol | 326 + .../IXAllocationVotingGovernorV1.sol | 2 +- .../V1/libraries/X2EarnAppsDataTypes.sol | 19 + .../XAllocationVotingGovernorV1.sol | 311 + .../ExternalContractsUpgradeableV1.sol | 156 + .../RoundEarningsSettingsUpgradeableV1.sol | 158 + .../RoundFinalizationUpgradeableV1.sol | 107 + .../RoundVotesCountingUpgradeableV1.sol | 292 + .../modules/RoundsStorageUpgradeableV1.sol | 197 + .../VotesQuorumFractionUpgradeableV1.sol | 153 + .../modules/VotesUpgradeableV1.sol | 104 + .../modules/VotingSettingsUpgradeableV1.sol | 91 + .../deprecated/V2/X2EarnRewardsPoolV2.sol | 529 ++ .../V2/interfaces/IX2EarnRewardsPoolV2.sol | 155 + contracts/deprecated/V3/B3TRGovernorV3.sol | 977 +++ .../V3/governance/GovernorStorageV3.sol | 92 + .../libraries/GovernorClockLogicV3.sol | 64 + .../libraries/GovernorConfiguratorV3.sol | 173 + .../libraries/GovernorDepositLogicV3.sol | 197 + .../GovernorFunctionRestrictionsLogicV3.sol | 149 + .../libraries/GovernorGovernanceLogicV3.sol | 83 + .../libraries/GovernorProposalLogicV3.sol | 789 +++ .../libraries/GovernorQuorumLogicV3.sol | 148 + .../libraries/GovernorStateLogicV3.sol | 159 + .../libraries/GovernorStorageTypesV3.sol | 94 + .../governance/libraries/GovernorTypesV3.sol | 112 + .../libraries/GovernorVotesLogicV3.sol | 300 + .../V3/interfaces/IB3TRGovernor.sol | 503 ++ contracts/governance/GovernorStorage.sol | 6 + .../libraries/GovernorDepositLogic.sol | 5 + .../libraries/GovernorStorageTypes.sol | 8 +- .../libraries/GovernorVotesLogic.sol | 33 +- contracts/interfaces/IB3TRGovernor.sol | 7 + contracts/interfaces/IGalaxyMember.sol | 2 +- contracts/interfaces/IVeBetterPassport.sol | 519 ++ contracts/interfaces/IVoterRewards.sol | 2 +- contracts/interfaces/IX2EarnRewardsPool.sol | 8 + .../interfaces/IXAllocationVotingGovernor.sol | 11 + .../mocks/VechainNodes/SupportsInterface.sol | 58 + .../mocks/VechainNodes/ThunderFactory.sol | 400 ++ contracts/mocks/VechainNodes/TokenAuction.sol | 185 + .../mocks/VechainNodes/XAccessControl.sol | 71 + contracts/mocks/VechainNodes/XOwnership.sol | 215 + .../VechainNodes/auction/ClockAuction.sol | 210 + .../VechainNodes/auction/ClockAuctionBase.sol | 160 + .../mocks/VechainNodes/utility/Ownable.sol | 36 + .../mocks/VechainNodes/utility/Pausable.sol | 52 + .../mocks/VechainNodes/utility/SafeMath.sol | 101 + .../mocks/VechainNodes/utility/Strings.sol | 42 + .../utility/interfaces/IERC165.sol | 23 + .../utility/interfaces/IVIP181.sol | 31 + .../utility/interfaces/IVIP181Basic.sol | 82 + contracts/templates/BaseUpgradeable.sol | 72 + contracts/templates/ModuleInitializable.sol | 60 + .../ve-better-passport/VeBetterPassport.sol | 817 +++ .../libraries/PassportChecksLogic.sol | 140 + .../libraries/PassportClockLogic.sol | 49 + .../libraries/PassportConfigurator.sol | 127 + .../libraries/PassportDelegationLogic.sol | 514 ++ .../libraries/PassportEIP712SigningLogic.sol | 121 + .../libraries/PassportEntityLogic.sol | 570 ++ .../libraries/PassportPersonhoodLogic.sol | 191 + .../libraries/PassportPoPScoreLogic.sol | 378 ++ .../libraries/PassportSignalingLogic.sol | 309 + .../libraries/PassportStorageTypes.sol | 134 + .../libraries/PassportTypes.sol | 96 + .../PassportWhitelistAndBlacklistLogic.sol | 338 + .../XAllocationVotingGovernor.sol | 32 +- .../modules/RoundVotesCountingUpgradeable.sol | 4 +- .../modules/RoundsStorageUpgradeable.sol | 2 +- deploy_output/contracts.txt | 15 + deploy_output/libraries.txt | 22 + package.json | 4 +- scripts/deploy/deploy.ts | 707 +- scripts/helpers/fs.ts | 119 +- scripts/helpers/roles.ts | 295 + scripts/helpers/upgrades.ts | 11 +- scripts/libraries/governanceLibraries.ts | 233 + scripts/libraries/index.ts | 2 + scripts/libraries/passportLibraries.ts | 58 + test/B3TR.test.ts | 2 +- test/Emissions.test.ts | 11 +- test/GalaxyMember.test.ts | 20 +- test/Governance.test.ts | 401 +- test/Timelock.test.ts | 2 +- test/Treasury.test.ts | 2 +- test/VOT3.test.ts | 4 +- test/VeBetterPassport.test.ts | 5693 +++++++++++++++++ test/VoterRewards.test.ts | 275 +- test/X2EarnRewardsPool.test.ts | 293 +- test/XAllocationPool.test.ts | 816 +-- test/XAllocationVoting.test.ts | 431 +- test/XApps.test.ts | 61 +- test/helpers/common.ts | 136 +- test/helpers/config.ts | 9 + test/helpers/deploy.ts | 244 +- yarn.lock | 162 +- 108 files changed, 22180 insertions(+), 1187 deletions(-) create mode 100644 CONTRACTS_CHANGELOG.md create mode 100644 contracts/deprecated/V1/XAllocationVotingV1.sol create mode 100644 contracts/deprecated/V1/interfaces/IEmissionsV1.sol create mode 100644 contracts/deprecated/V1/interfaces/IX2EarnAppsV1.sol create mode 100644 contracts/deprecated/V1/libraries/X2EarnAppsDataTypes.sol create mode 100644 contracts/deprecated/V1/x-allocation-voting-governance/XAllocationVotingGovernorV1.sol create mode 100644 contracts/deprecated/V1/x-allocation-voting-governance/modules/ExternalContractsUpgradeableV1.sol create mode 100644 contracts/deprecated/V1/x-allocation-voting-governance/modules/RoundEarningsSettingsUpgradeableV1.sol create mode 100644 contracts/deprecated/V1/x-allocation-voting-governance/modules/RoundFinalizationUpgradeableV1.sol create mode 100644 contracts/deprecated/V1/x-allocation-voting-governance/modules/RoundVotesCountingUpgradeableV1.sol create mode 100644 contracts/deprecated/V1/x-allocation-voting-governance/modules/RoundsStorageUpgradeableV1.sol create mode 100644 contracts/deprecated/V1/x-allocation-voting-governance/modules/VotesQuorumFractionUpgradeableV1.sol create mode 100644 contracts/deprecated/V1/x-allocation-voting-governance/modules/VotesUpgradeableV1.sol create mode 100644 contracts/deprecated/V1/x-allocation-voting-governance/modules/VotingSettingsUpgradeableV1.sol create mode 100644 contracts/deprecated/V2/X2EarnRewardsPoolV2.sol create mode 100644 contracts/deprecated/V2/interfaces/IX2EarnRewardsPoolV2.sol create mode 100644 contracts/deprecated/V3/B3TRGovernorV3.sol create mode 100644 contracts/deprecated/V3/governance/GovernorStorageV3.sol create mode 100644 contracts/deprecated/V3/governance/libraries/GovernorClockLogicV3.sol create mode 100644 contracts/deprecated/V3/governance/libraries/GovernorConfiguratorV3.sol create mode 100644 contracts/deprecated/V3/governance/libraries/GovernorDepositLogicV3.sol create mode 100644 contracts/deprecated/V3/governance/libraries/GovernorFunctionRestrictionsLogicV3.sol create mode 100644 contracts/deprecated/V3/governance/libraries/GovernorGovernanceLogicV3.sol create mode 100644 contracts/deprecated/V3/governance/libraries/GovernorProposalLogicV3.sol create mode 100644 contracts/deprecated/V3/governance/libraries/GovernorQuorumLogicV3.sol create mode 100644 contracts/deprecated/V3/governance/libraries/GovernorStateLogicV3.sol create mode 100644 contracts/deprecated/V3/governance/libraries/GovernorStorageTypesV3.sol create mode 100644 contracts/deprecated/V3/governance/libraries/GovernorTypesV3.sol create mode 100644 contracts/deprecated/V3/governance/libraries/GovernorVotesLogicV3.sol create mode 100644 contracts/deprecated/V3/interfaces/IB3TRGovernor.sol create mode 100644 contracts/interfaces/IVeBetterPassport.sol create mode 100644 contracts/mocks/VechainNodes/SupportsInterface.sol create mode 100644 contracts/mocks/VechainNodes/ThunderFactory.sol create mode 100644 contracts/mocks/VechainNodes/TokenAuction.sol create mode 100644 contracts/mocks/VechainNodes/XAccessControl.sol create mode 100644 contracts/mocks/VechainNodes/XOwnership.sol create mode 100644 contracts/mocks/VechainNodes/auction/ClockAuction.sol create mode 100644 contracts/mocks/VechainNodes/auction/ClockAuctionBase.sol create mode 100644 contracts/mocks/VechainNodes/utility/Ownable.sol create mode 100644 contracts/mocks/VechainNodes/utility/Pausable.sol create mode 100644 contracts/mocks/VechainNodes/utility/SafeMath.sol create mode 100644 contracts/mocks/VechainNodes/utility/Strings.sol create mode 100644 contracts/mocks/VechainNodes/utility/interfaces/IERC165.sol create mode 100644 contracts/mocks/VechainNodes/utility/interfaces/IVIP181.sol create mode 100644 contracts/mocks/VechainNodes/utility/interfaces/IVIP181Basic.sol create mode 100644 contracts/templates/BaseUpgradeable.sol create mode 100644 contracts/templates/ModuleInitializable.sol create mode 100644 contracts/ve-better-passport/VeBetterPassport.sol create mode 100644 contracts/ve-better-passport/libraries/PassportChecksLogic.sol create mode 100644 contracts/ve-better-passport/libraries/PassportClockLogic.sol create mode 100644 contracts/ve-better-passport/libraries/PassportConfigurator.sol create mode 100644 contracts/ve-better-passport/libraries/PassportDelegationLogic.sol create mode 100644 contracts/ve-better-passport/libraries/PassportEIP712SigningLogic.sol create mode 100644 contracts/ve-better-passport/libraries/PassportEntityLogic.sol create mode 100644 contracts/ve-better-passport/libraries/PassportPersonhoodLogic.sol create mode 100644 contracts/ve-better-passport/libraries/PassportPoPScoreLogic.sol create mode 100644 contracts/ve-better-passport/libraries/PassportSignalingLogic.sol create mode 100644 contracts/ve-better-passport/libraries/PassportStorageTypes.sol create mode 100644 contracts/ve-better-passport/libraries/PassportTypes.sol create mode 100644 contracts/ve-better-passport/libraries/PassportWhitelistAndBlacklistLogic.sol create mode 100644 deploy_output/contracts.txt create mode 100644 deploy_output/libraries.txt create mode 100644 scripts/helpers/roles.ts create mode 100644 scripts/libraries/governanceLibraries.ts create mode 100644 scripts/libraries/index.ts create mode 100644 scripts/libraries/passportLibraries.ts create mode 100644 test/VeBetterPassport.test.ts diff --git a/CONTRACTS_CHANGELOG.md b/CONTRACTS_CHANGELOG.md new file mode 100644 index 0000000..4e8f236 --- /dev/null +++ b/CONTRACTS_CHANGELOG.md @@ -0,0 +1,187 @@ +# Smart Contracts Changelog + +This document provides a detailed log of upgrades to the smart contract suite, ensuring clear tracking of changes, improvements, bug fixes, and versioning across all contracts. + +## Version History + +| Date | Contract(s) | Summary | +| ------------------- | --------------------------------------------------------- | ----------------------------------------------------------- | +| 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 `XAllocationVoting` to Version 2, `B3TRGovernor` to version 4, and `X2EarnRewardsPool` to version 3 (9th October 2024) + +This upgrade ensures that the `isPerson` check is performed when casting a vote in the `XAllocationVoting` and `B3TRGovernor` contracts. Additionally, the `X2EarnRewardsPool` contract now registers actions in the `VeBetter Passport` contract. + +Another change in the `XAllocationVoting` contract is the fixed weight during the vote, ensuring that the weight cannot be lower than 1. + +### Changes 🚀 + +- **Upgraded Contract(s):** + - `XAllocationVoting.sol` to version `2` + - `B3TRGovernor.sol` to version `4` + - `X2EarnRewardsPool.sol` to version `3` + +### Storage Changes 📦 + +- **`XAllocationVoting.sol`**: + - Added veBetterPassport contract address. +- **`B3TRGovernor.sol`**: + - Added veBetterPassport contract address. +- **`X2EarnRewardsPool.sol`**: + - Added veBetterPassport contract address. + +### New Features 🚀 + +- **`XAllocationVoting.sol`**: + - Added `isPerson` check when casting a vote. +- **`B3TRGovernor.sol`**: + - Added `isPerson` check when casting a vote. +- **`X2EarnRewardsPool.sol`**: + - Register actions in the `VeBetter Passport` contract. + +### Bug Fixes 🐛 + +- **`XAllocationVoting.sol`**: + - Fixed weight during vote to ensure it cannot be lower than 1. + +--- + +## Upgrade `Emissions` to Version 2 (27th September 2024) + +This upgrade aligns the emissions with the expected schedule by correcting previous configuration errors. + +### Changes 🚀 + +- **Upgraded Contract(s):** `Emissions.sol` to version `2` + +### Storage Changes 📦 + +- Added `_isEmissionsNotAligned` to store the emissions alignment status. + +### New Features 🚀 + +- In `_calculateNextXAllocation` function, added logic to calculate the next X Allocation based on the emissions alignment status. + +### Bug Fixes 🐛 + +- Corrected `xAllocationsDecay` from `912` to `12`, fixing the erroneous value set in version `1`. +- Applied a reduction of `200,000` B3TR emissions for round `14` to align with the expected emissions schedule. + +--- + +## Upgrade `B3TRGovernor` to Version 3 and `XAllocationPool` to Version 2 (13th September 2024) + +This upgrade adds the ability to toggle quadratic voting and quadratic funding on or off, providing greater control over governance and allocation mechanisms. + +### Changes 🚀 + +- **Upgraded Contract(s):** + - `B3TRGovernor.sol` to version `3` + - `XAllocationPool.sol` to version `2` + +### Storage Changes 📦 + +- **`B3TRGovernor.sol`**: + - Added `quadraticVotingDisabled` checkpoints to store the quadratic voting disabled status. +- **`XAllocationPool.sol`**: + - Added `quadraticFundingDisabled` checkpoints to store the quadratic funding disabled status. + +### New Features 🚀 + +- **`B3TRGovernor`**: + - Ability to toggle quadratic voting on or off. +- **`XAllocationPool`**: + - Ability to toggle quadratic funding on or off. + +### Bug Fixes 🐛 + +- None. + +--- + +## Upgrade `X2EarnRewardsPool` to Version 2 (4th September 2024) + +This upgrade introduces impact key management and the ability to build proofs of sustainable impact. + +### Changes 🚀 + +- **Upgraded Contract(s):** `X2EarnRewardsPool.sol` to version `2` + +### Storage Changes 📦 + +- Added `impactKeyIndex` to store allowed impact keys index for proof of sustainable impact building. +- Added `allowedImpactKeys` to store the array of allowed impact keys. + +### New Features 🚀 + +- Introduced the `IMPACT_KEY_MANAGER_ROLE` to manage allowed impact keys. +- Introduced the `onlyRoleOrAdmin` modifier to restrict access to the `IMPACT_KEY_MANAGER_ROLE` or admin. +- Added `buildProof` function to build proof of sustainable impact. + +### Bug Fixes 🐛 + +- None. + +--- + +## Upgrade `VoterRewards` to Version 2 (31st August 2024) + +This upgrade adds the ability to disable quadratic rewarding for specific cycles, providing greater flexibility in reward distribution. Introduced as first step of sybil mitigation. + +### Changes 🚀 + +- **Upgraded Contract(s):** `VoterRewards.sol` to version `2` + +### Storage Changes 📦 + +- Added `quadraticRewardingDisabled` checkpoints to store the quadratic rewarding status for each cycle. + +### New Features 🚀 + +- Added functions to: + - Disable or re-enable quadratic rewarding for specific cycles. + - Check if quadratic rewarding is disabled at a specific block number or for the current cycle. +- Added the `clock` function to get the current block number. + +### Bug Fixes 🐛 + +- None. + +--- + +## Upgrade `B3TRGovernor` to Version 2 (29th August 2024) + +This upgrade enhances access control by allowing the `DEFAULT_ADMIN_ROLE` to execute critical functions without requiring a governance proposal. + +### Changes 🚀 + +- **Upgraded Contract(s):** `B3TRGovernor.sol` to version `2` + +### Storage Changes 📦 + +- **Storage Changes:** None. + +### New Features 🚀 + +- Updated functions previously restricted by `onlyGovernance` to use `onlyRoleOrGovernance`, permitting `DEFAULT_ADMIN_ROLE` direct access. + +### Bug Fixes 🐛 + +- None. + +--- + +## Glossary + +- **Quadratic Voting**: A voting system where the cost of votes increases quadratically with the number of votes cast. +- **Quadratic Funding**: A funding mechanism that allocates resources based on the square of contributions received. +- **Checkpoint**: A recorded state at a specific point in time for tracking changes or status. diff --git a/config/contracts/envs/local.ts b/config/contracts/envs/local.ts index 920ae0d..9aaf4a4 100644 --- a/config/contracts/envs/local.ts +++ b/config/contracts/envs/local.ts @@ -125,5 +125,14 @@ export function createLocalConfig() { "plastic", "trees_planted", ], + + // VeBetterPassport + VEPASSPORT_BOT_SIGNALING_THRESHOLD: 2, + VEPASSPORT_ROUNDS_FOR_CUMULATIVE_PARTICIPATION_SCORE: 12, + VEPASSPORT_GALAXY_MEMBER_MINIMUM_LEVEL: 2, + VEPASSPORT_BLACKLIST_THRESHOLD_PERCENTAGE: 20, + VEPASSPORT_WHITELIST_THRESHOLD_PERCENTAGE: 20, + VEPASSPORT_PASSPORT_MAX_ENTITIES: 5, + VEPASSPORT_DECAY_RATE: 0, }) } diff --git a/config/contracts/type.ts b/config/contracts/type.ts index 6097a81..a468a04 100644 --- a/config/contracts/type.ts +++ b/config/contracts/type.ts @@ -48,4 +48,13 @@ export type ContractsConfig = { // X 2 Earn Rewards Pool X_2_EARN_INITIAL_IMPACT_KEYS: string[] + + // VeBetterPassport + VEPASSPORT_BOT_SIGNALING_THRESHOLD: number + VEPASSPORT_ROUNDS_FOR_CUMULATIVE_PARTICIPATION_SCORE: number + VEPASSPORT_GALAXY_MEMBER_MINIMUM_LEVEL: number + VEPASSPORT_BLACKLIST_THRESHOLD_PERCENTAGE: number + VEPASSPORT_WHITELIST_THRESHOLD_PERCENTAGE: number + VEPASSPORT_PASSPORT_MAX_ENTITIES: number + VEPASSPORT_DECAY_RATE: number } diff --git a/config/scripts/generateMockLocalConfig.ts b/config/scripts/generateMockLocalConfig.ts index 0567bff..6517a52 100644 --- a/config/scripts/generateMockLocalConfig.ts +++ b/config/scripts/generateMockLocalConfig.ts @@ -27,6 +27,7 @@ export const generateMockLocalConfig = () => { galaxyMemberContractAddress: "0x45d5CA3f295ad8BCa291cC4ecd33382DE40E4FAc", treasuryContractAddress: "0x45d5CA3f295ad8BCa291cC4ecd33382DE40E4FAc", x2EarnAppsContractAddress: "0x45d5CA3f295ad8BCa291cC4ecd33382DE40E4FAc", + veBetterPassportContractAddress: "0x45d5CA3f295ad8BCa291cC4ecd33382DE40E4FAc", "nodeUrl": "http://localhost:8669", "network": { "id": "solo", diff --git a/contracts/B3TRGovernor.sol b/contracts/B3TRGovernor.sol index 3978cd6..f1f3f77 100644 --- a/contracts/B3TRGovernor.sol +++ b/contracts/B3TRGovernor.sol @@ -48,6 +48,7 @@ import { Address } from "@openzeppelin/contracts/utils/Address.sol"; import "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import { IVeBetterPassport } from "./interfaces/IVeBetterPassport.sol"; /** * @title B3TRGovernor @@ -67,11 +68,13 @@ import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; * * The contract is upgradeable and uses the UUPS pattern. * @dev The contract is upgradeable and uses the UUPS pattern. All logic is stored in libraries. - * + * * ------------------ VERSION 2 ------------------ * - Replaced onlyGovernance modifier with onlyRoleOrGovernance which checks if the caller has the DEFAULT_ADMIN_ROLE role or if the function is called through a governance proposal * ------------------ VERSION 3 ------------------ * - Added the ability to toggle the quadratic voting mechanism on and off + * ------------------ VERSION 4 ------------------ + * - Integrated VeBetterPassport contract */ contract B3TRGovernor is IB3TRGovernor, @@ -160,6 +163,10 @@ contract B3TRGovernor is _grantRole(PROPOSAL_EXECUTOR_ROLE, rolesData.proposalExecutor); } + function initializeV4(IVeBetterPassport _veBetterPassport) public reinitializer(4) { + __GovernorStorage_init_v4(_veBetterPassport); + } + /** * @dev Function to receive VET that will be handled by the governor (disabled if executor is a third party contract) */ @@ -179,7 +186,11 @@ contract B3TRGovernor is * @param value The amount of ether to send * @param data The data to call the target with */ - function relay(address target, uint256 value, bytes calldata data) external payable virtual onlyRoleOrGovernance(DEFAULT_ADMIN_ROLE) { + function relay( + address target, + uint256 value, + bytes calldata data + ) external payable virtual onlyRoleOrGovernance(DEFAULT_ADMIN_ROLE) { (bool success, bytes memory returndata) = target.call{ value: value }(data); Address.verifyCallResult(success, returndata); } @@ -562,7 +573,7 @@ contract B3TRGovernor is * @return string The version of the governor */ function version() external pure returns (string memory) { - return "3"; + return "4"; } /** @@ -903,7 +914,9 @@ contract B3TRGovernor is * CAUTION: It is not recommended to change the timelock while there are other queued governance proposals. * @param newTimelock The new timelock controller */ - function updateTimelock(TimelockControllerUpgradeable newTimelock) external virtual onlyRoleOrGovernance(DEFAULT_ADMIN_ROLE) { + function updateTimelock( + TimelockControllerUpgradeable newTimelock + ) external virtual onlyRoleOrGovernance(DEFAULT_ADMIN_ROLE) { GovernorStorageTypes.GovernorStorage storage $ = getGovernorStorage(); GovernorConfigurator.updateTimelock($, newTimelock); } diff --git a/contracts/X2EarnRewardsPool.sol b/contracts/X2EarnRewardsPool.sol index 7ab00b7..3388a44 100644 --- a/contracts/X2EarnRewardsPool.sol +++ b/contracts/X2EarnRewardsPool.sol @@ -32,6 +32,7 @@ import { IX2EarnRewardsPool } from "./interfaces/IX2EarnRewardsPool.sol"; import { IERC1155Receiver } from "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol"; import { IERC721Receiver } from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; import "@openzeppelin/contracts/utils/Strings.sol"; +import { IVeBetterPassport } from "./interfaces/IVeBetterPassport.sol"; /** * @title X2EarnRewardsPool @@ -42,6 +43,11 @@ import "@openzeppelin/contracts/utils/Strings.sol"; * Reward distributors of a x2Earn app can distribute rewards to users that performed sustainable actions or withdraw funds * to the team wallet. * The contract is upgradable through the UUPS proxy pattern and UPGRADER_ROLE can authorize the upgrade. + * + * ----- Version 2 ----- + * - Added onchain proof and impact tracking + * ----- Version 3 ----- + * - Added VeBetterPassport integration */ contract X2EarnRewardsPool is IX2EarnRewardsPool, @@ -65,6 +71,7 @@ contract X2EarnRewardsPool is mapping(bytes32 appId => uint256) availableFunds; // Funds that the app can use to reward users mapping(string => uint256) impactKeyIndex; // Mapping from impact key to its index (1-based to distinguish from non-existent) string[] allowedImpactKeys; // Array storing impact keys + IVeBetterPassport veBetterPassport; } // keccak256(abi.encode(uint256(keccak256("b3tr.storage.X2EarnRewardsPool")) - 1)) & ~bytes32(uint256(0xff)) @@ -116,6 +123,13 @@ contract X2EarnRewardsPool is } } + function initializeV3(address _veBetterPassport) external reinitializer(3) { + require(address(_veBetterPassport) != address(0), "X2EarnRewardsPool: veBetterPassport is the zero address"); + + X2EarnRewardsPoolStorage storage $ = _getX2EarnRewardsPoolStorage(); + $.veBetterPassport = IVeBetterPassport(_veBetterPassport); + } + // ---------- Modifiers ---------- // /** * @notice Modifier to check if the user has the required role or is the DEFAULT_ADMIN_ROLE @@ -243,6 +257,17 @@ contract X2EarnRewardsPool is // Transfer the rewards to the receiver $.availableFunds[appId] -= amount; require($.b3tr.transfer(receiver, amount), "X2EarnRewardsPool: Allocation transfer to app failed"); + + // Try to register the action in the veBetterPassport contract + try $.veBetterPassport.registerAction(receiver, appId) { + // If the call succeeds, you can optionally handle success here. + } catch Error(string memory reason) { + // If the call reverts with a revert reason string, this block is executed. + emit RegisterActionFailed(reason, ""); + } catch (bytes memory lowLevelData) { + // If the call reverts without a revert reason or with a custom error, this block is executed. + emit RegisterActionFailed("Low-level error", lowLevelData); + } } /** @@ -441,6 +466,18 @@ contract X2EarnRewardsPool is delete $.impactKeyIndex[keyToRemove]; } + /** + * @dev Sets the VeBetterPassport contract address. + * + * @param _veBetterPassport the new VeBetterPassport contract + */ + function setVeBetterPassport(IVeBetterPassport _veBetterPassport) external onlyRole(CONTRACTS_ADDRESS_MANAGER_ROLE) { + require(address(_veBetterPassport) != address(0), "X2EarnRewardsPool: veBetterPassport is the zero address"); + + X2EarnRewardsPoolStorage storage $ = _getX2EarnRewardsPoolStorage(); + $.veBetterPassport = _veBetterPassport; + } + // ---------- Getters ---------- // /** @@ -455,7 +492,7 @@ contract X2EarnRewardsPool is * @dev See {IX2EarnRewardsPool-version} */ function version() external pure virtual returns (string memory) { - return "2"; + return "3"; } /** @@ -482,6 +519,14 @@ contract X2EarnRewardsPool is return $.allowedImpactKeys; } + /** + * @dev Retrieves the VeBetterPassport contract. + */ + function veBetterPassport() external view returns (IVeBetterPassport) { + X2EarnRewardsPoolStorage storage $ = _getX2EarnRewardsPoolStorage(); + return $.veBetterPassport; + } + // ---------- Fallbacks ---------- // /** diff --git a/contracts/XAllocationVoting.sol b/contracts/XAllocationVoting.sol index a6606cb..54e3010 100644 --- a/contracts/XAllocationVoting.sol +++ b/contracts/XAllocationVoting.sol @@ -43,6 +43,10 @@ import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; * @dev Interacts with the X2EarnApps contract to get the app data (eg: app IDs, app existence, eligible apps for each round). * @dev Interacts with the VotingRewards contract to save the user from casting a vote. * @dev The contract is using AccessControl to handle roles for admin, governance, and round-starting operations. + * + * ----- Version 2 ----- + * - Integrated VeBetterPassport + * - Added check to ensure that the vote weight for an XApp cast by a user is greater than the voting threshold */ contract XAllocationVoting is XAllocationVotingGovernor, @@ -111,7 +115,7 @@ contract XAllocationVoting is require(address(data.vot3Token) != address(0), "XAllocationVoting: invalid VOT3 token address"); require(address(data.voterRewards) != address(0), "XAllocationVoting: invalid VoterRewards address"); require(address(data.emissions) != address(0), "XAllocationVoting: invalid Emissions address"); - + __XAllocationVotingGovernor_init("XAllocationVoting"); __ExternalContracts_init(data.x2EarnAppsAddress, data.emissions, data.voterRewards); __VotingSettings_init(data.initialVotingPeriod); @@ -134,6 +138,10 @@ contract XAllocationVoting is _grantRole(CONTRACTS_ADDRESS_MANAGER_ROLE, data.contractsAddressManager); } + function initializeV2(IVeBetterPassport _veBetterPassport) public reinitializer(2) { + __XAllocationVotingGovernor_init_v2(_veBetterPassport); + } + // ---------- Setters ---------- // /** * @dev Set the address of the X2EarnApps contract diff --git a/contracts/deprecated/V1/B3TRGovernorV1.sol b/contracts/deprecated/V1/B3TRGovernorV1.sol index 7112d3e..4f1eef4 100644 --- a/contracts/deprecated/V1/B3TRGovernorV1.sol +++ b/contracts/deprecated/V1/B3TRGovernorV1.sol @@ -939,4 +939,4 @@ contract B3TRGovernorV1 is } return this.onERC1155BatchReceived.selector; } -} \ No newline at end of file +} diff --git a/contracts/deprecated/V1/EmissionsV1.sol b/contracts/deprecated/V1/EmissionsV1.sol index 77c08ad..2b22132 100644 --- a/contracts/deprecated/V1/EmissionsV1.sol +++ b/contracts/deprecated/V1/EmissionsV1.sol @@ -100,7 +100,7 @@ contract EmissionsV1 is AccessControlUpgradeable, ReentrancyGuardUpgradeable, UU /// @custom:storage-location erc7201:b3tr.storage.Emissions struct EmissionsStorage { IB3TR b3tr; // B3TR token contract - IXAllocationVotingGovernor xAllocationsGovernor; // XAllocationVotingGovernor contract + IXAllocationVotingGovernorV1 xAllocationsGovernor; // XAllocationVotingGovernor contract // Destinations for emissions address _xAllocations; address _vote2Earn; @@ -497,7 +497,7 @@ contract EmissionsV1 is AccessControlUpgradeable, ReentrancyGuardUpgradeable, UU } /// @notice Returns the XAllocations Governance contract - function xAllocationsGovernor() public view returns (IXAllocationVotingGovernor) { + function xAllocationsGovernor() public view returns (IXAllocationVotingGovernorV1) { return _getEmissionsStorage().xAllocationsGovernor; } @@ -619,7 +619,7 @@ contract EmissionsV1 is AccessControlUpgradeable, ReentrancyGuardUpgradeable, UU require(_cycleDuration > 0, "Emissions: Cycle duration must be greater than 0"); EmissionsStorage storage $ = _getEmissionsStorage(); require( - IXAllocationVotingGovernor($.xAllocationsGovernor).votingPeriod() < _cycleDuration, + IXAllocationVotingGovernorV1($.xAllocationsGovernor).votingPeriod() < _cycleDuration, "Emissions: Voting period must be less than cycle duration" ); emit EmissionCycleDurationUpdated(_cycleDuration, $.cycleDuration); @@ -696,12 +696,12 @@ contract EmissionsV1 is AccessControlUpgradeable, ReentrancyGuardUpgradeable, UU ) public onlyRole(CONTRACTS_ADDRESS_MANAGER_ROLE) { require(_xAllocationsGovernor != address(0), "Emissions: _xAllocationsGovernor cannot be the zero address"); require( - IXAllocationVotingGovernor(_xAllocationsGovernor).votingPeriod() < cycleDuration(), + IXAllocationVotingGovernorV1(_xAllocationsGovernor).votingPeriod() < cycleDuration(), "Emissions: Voting period must be less than cycle duration" ); EmissionsStorage storage $ = _getEmissionsStorage(); emit XAllocationsGovernorAddressUpdated(_xAllocationsGovernor, address($.xAllocationsGovernor)); - $.xAllocationsGovernor = IXAllocationVotingGovernor(_xAllocationsGovernor); + $.xAllocationsGovernor = IXAllocationVotingGovernorV1(_xAllocationsGovernor); } } diff --git a/contracts/deprecated/V1/XAllocationVotingV1.sol b/contracts/deprecated/V1/XAllocationVotingV1.sol new file mode 100644 index 0000000..ce286a9 --- /dev/null +++ b/contracts/deprecated/V1/XAllocationVotingV1.sol @@ -0,0 +1,240 @@ +// SPDX-License-Identifier: MIT + +// ####### +// ################ +// #################### +// ########### ######### +// ######### ######### +// ####### ######### ######### +// ######### ######### ########## +// ########## ######## #################### +// ########## ######### ######################### +// ################### ############################ +// ################# ########## ######## +// ############## ### ######## +// ############ ######### +// ########## ########## +// ######## ########### +// ### ############ +// ############## +// ################# +// ############## +// ######### + +pragma solidity 0.8.20; + +import "./x-allocation-voting-governance/XAllocationVotingGovernorV1.sol"; +import "./x-allocation-voting-governance/modules/RoundVotesCountingUpgradeableV1.sol"; +import "./x-allocation-voting-governance/modules/VotesUpgradeableV1.sol"; +import "./x-allocation-voting-governance/modules/VotesQuorumFractionUpgradeableV1.sol"; +import "./x-allocation-voting-governance/modules/VotingSettingsUpgradeableV1.sol"; +import "./x-allocation-voting-governance/modules/RoundEarningsSettingsUpgradeableV1.sol"; +import "./x-allocation-voting-governance/modules/RoundFinalizationUpgradeableV1.sol"; +import "./x-allocation-voting-governance/modules/RoundsStorageUpgradeableV1.sol"; +import "./x-allocation-voting-governance/modules/ExternalContractsUpgradeableV1.sol"; +import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; + +/** + * @title XAllocationVoting + * @notice This contract handles the voting for the most supported x2Earn applications through periodic allocation rounds. + * The user's voting power is calculated on his VOT3 holdings at the start of each round, using a "Quadratic Funding" formula. + * @dev Rounds are started by the Emissions contract. + * @dev Interacts with the X2EarnApps contract to get the app data (eg: app IDs, app existence, eligible apps for each round). + * @dev Interacts with the VotingRewards contract to save the user from casting a vote. + * @dev The contract is using AccessControl to handle roles for admin, governance, and round-starting operations. + */ +contract XAllocationVotingV1 is + XAllocationVotingGovernorV1, + VotingSettingsUpgradeableV1, + RoundVotesCountingUpgradeableV1, + VotesUpgradeableV1, + VotesQuorumFractionUpgradeableV1, + RoundEarningsSettingsUpgradeableV1, + ExternalContractsUpgradeableV1, + RoundsStorageUpgradeableV1, + RoundFinalizationUpgradeableV1, + AccessControlUpgradeable, + UUPSUpgradeable +{ + /// @notice Role identifier for the address that can start a new round + bytes32 public constant ROUND_STARTER_ROLE = keccak256("ROUND_STARTER_ROLE"); + /// @notice Role identifier for the address that can upgrade the contract + bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE"); + /// @notice Role identifier for governance operations + bytes32 public constant GOVERNANCE_ROLE = keccak256("GOVERNANCE_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 Data for initializing the contract + * @param vot3Token The address of the VOT3 token used for voting + * @param quorumPercentage quorum as a percentage of the total supply + * @param initialVotingPeriod The round duration + * @param timeLock Address of the timelock contract controlling governance actions + * @param voterRewards The address of the VoterRewards contract + * @param emissions The address of the Emissions contract + * @param admins The addresses of the admins + * @param upgrader The address of the upgrader + * @param contractsAddressManager The address of the contracts address manager. + * @param x2EarnAppsAddress The address of the X2EarnApps contract + * @param baseAllocationPercentage A percentage of the total amount of allocations that should be equaly distributed to all apps in a round + * @param appSharesCap Max amount of % of votes an app can get in a round + * @param votingThreshold Minimum amount of VOT3 balance to cast a vote + */ + struct InitializationData { + IVotes vot3Token; + uint256 quorumPercentage; + uint32 initialVotingPeriod; + address timeLock; + IVoterRewardsV1 voterRewards; + IEmissionsV1 emissions; + address[] admins; + address upgrader; + address contractsAddressManager; + IX2EarnAppsV1 x2EarnAppsAddress; + uint256 baseAllocationPercentage; + uint256 appSharesCap; + uint256 votingThreshold; + } + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + /** + * @notice Initialize the contract + * @param data The initialization data + */ + function initialize(InitializationData memory data) public initializer { + require(address(data.vot3Token) != address(0), "XAllocationVoting: invalid VOT3 token address"); + require(address(data.voterRewards) != address(0), "XAllocationVoting: invalid VoterRewards address"); + require(address(data.emissions) != address(0), "XAllocationVoting: invalid Emissions address"); + + __XAllocationVotingGovernor_init("XAllocationVoting"); + __ExternalContracts_init(data.x2EarnAppsAddress, data.emissions, data.voterRewards); + __VotingSettings_init(data.initialVotingPeriod); + __RoundVotesCounting_init(data.votingThreshold); + __Votes_init(data.vot3Token); + __VotesQuorumFraction_init(data.quorumPercentage); + __RoundEarningsSettings_init(data.baseAllocationPercentage, data.appSharesCap); + __RoundFinalization_init(); + __RoundsStorage_init(); + __AccessControl_init(); + __UUPSUpgradeable_init(); + + for (uint256 i; i < data.admins.length; i++) { + require(data.admins[i] != address(0), "XAllocationVoting: invalid admin address"); + _grantRole(DEFAULT_ADMIN_ROLE, data.admins[i]); + } + + _grantRole(UPGRADER_ROLE, data.upgrader); + _grantRole(GOVERNANCE_ROLE, data.timeLock); + _grantRole(CONTRACTS_ADDRESS_MANAGER_ROLE, data.contractsAddressManager); + } + + // ---------- Setters ---------- // + /** + * @dev Set the address of the X2EarnApps contract + */ + function setX2EarnAppsAddress(IX2EarnAppsV1 newX2EarnApps) external onlyRole(CONTRACTS_ADDRESS_MANAGER_ROLE) { + _setX2EarnApps(newX2EarnApps); + } + + /** + * @dev Set the address of the Emissions contract + */ + function setEmissionsAddress(IEmissionsV1 newEmissions) external onlyRole(CONTRACTS_ADDRESS_MANAGER_ROLE) { + _setEmissions(newEmissions); + } + + /** + * @dev Set the address of the VoterRewards contract + */ + function setVoterRewardsAddress(IVoterRewardsV1 newVoterRewards) external onlyRole(CONTRACTS_ADDRESS_MANAGER_ROLE) { + _setVoterRewards(newVoterRewards); + } + + /** + * @dev Update the voting threshold. This operation can only be performed through a governance proposal. + * + * Emits a {VotingThresholdSet} event. + */ + function setVotingThreshold(uint256 newVotingThreshold) public virtual override onlyRole(GOVERNANCE_ROLE) { + super.setVotingThreshold(newVotingThreshold); + } + + /** + * @dev Start a new voting round for allocating funds to the x-apps + */ + function startNewRound() public override onlyRole(ROUND_STARTER_ROLE) returns (uint256) { + return super.startNewRound(); + } + + /** + * @dev Set the max amount of shares an app can get in a round + */ + function setAppSharesCap(uint256 appSharesCap_) external virtual override onlyRole(GOVERNANCE_ROLE) { + _setAppSharesCap(appSharesCap_); + } + + /** + * @dev Set the base allocation percentage for funds distribution in a round + */ + function setBaseAllocationPercentage( + uint256 baseAllocationPercentage_ + ) public virtual override onlyRole(GOVERNANCE_ROLE) { + _setBaseAllocationPercentage(baseAllocationPercentage_); + } + + /** + * @dev Set the voting period for a round + */ + function setVotingPeriod(uint32 newVotingPeriod) public virtual onlyRole(GOVERNANCE_ROLE) { + _setVotingPeriod(newVotingPeriod); + } + + /** + * @dev Update the quorum a round needs to reach to be successful + */ + function updateQuorumNumerator(uint256 newQuorumNumerator) public virtual override onlyRole(GOVERNANCE_ROLE) { + super.updateQuorumNumerator(newQuorumNumerator); + } + + // ---------- Getters ---------- // + + /** + * Returns the quorum for a given round + */ + function roundQuorum(uint256 roundId) external view returns (uint256) { + return quorum(roundSnapshot(roundId)); + } + + // ---------- Required overrides ---------- // + + function votingPeriod() + public + view + override(XAllocationVotingGovernorV1, VotingSettingsUpgradeableV1) + returns (uint256) + { + return super.votingPeriod(); + } + + function quorum( + uint256 blockNumber + ) public view override(XAllocationVotingGovernorV1, VotesQuorumFractionUpgradeableV1) returns (uint256) { + return super.quorum(blockNumber); + } + + function supportsInterface( + bytes4 interfaceId + ) public view override(AccessControlUpgradeable, XAllocationVotingGovernorV1) returns (bool) { + return super.supportsInterface(interfaceId); + } + + // ---------- Authorizations ------------ // + + function _authorizeUpgrade(address newImplementation) internal override onlyRole(UPGRADER_ROLE) {} +} diff --git a/contracts/deprecated/V1/interfaces/IEmissionsV1.sol b/contracts/deprecated/V1/interfaces/IEmissionsV1.sol new file mode 100644 index 0000000..bbc50e5 --- /dev/null +++ b/contracts/deprecated/V1/interfaces/IEmissionsV1.sol @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.20; + +import "./IB3TR.sol"; +import "./IXAllocationVotingGovernorV1.sol"; + +interface IEmissionsV1 { + struct Emission { + uint256 xAllocations; + uint256 vote2Earn; + uint256 treasury; + } + + error AccessControlBadConfirmation(); + + error AccessControlUnauthorizedAccount(address account, bytes32 neededRole); + + error ReentrancyGuardReentrantCall(); + + 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 EmissionDistributed(uint256 indexed cycle, uint256 xAllocations, uint256 vote2Earn, uint256 treasury); + + event XAllocationsAddressUpdated(address indexed newAddress, address indexed oldAddress); + + event Vote2EarnAddressUpdated(address indexed newAddress, address indexed oldAddress); + + event XAllocationsGovernorAddressUpdated(address indexed newAddress, address indexed oldAddress); + + event TreasuryAddressUpdated(address indexed newAddress, address indexed oldAddress); + + event EmissionCycleDurationUpdated(uint256 indexed newDuration, uint256 indexed oldDuration); + + event XAllocationsDecayUpdated(uint256 indexed newDecay, uint256 indexed oldDecay); + + event Vote2EarnDecayUpdated(uint256 indexed newDecay, uint256 indexed oldDecay); + + event Vote2EarnDecayPeriodUpdated(uint256 indexed newPeriod, uint256 indexed oldPeriod); + + event MaxVote2EarnDecayUpdated(uint256 indexed newDecay, uint256 indexed oldDecay); + + event XAllocationsDecayPeriodUpdated(uint256 indexed newPeriod, uint256 indexed oldPeriod); + + event TreasuryPercentageUpdated(uint256 indexed newPercentage, uint256 indexed oldPercentage); + + function DEFAULT_ADMIN_ROLE() external view returns (bytes32); + + function MINTER_ROLE() external view returns (bytes32); + + function b3tr() external view returns (IB3TR); + + function bootstrap() external; + + function start() external; + + function cycleDuration() external view returns (uint256); + + function distribute() external; + + function emissions(uint256) external view returns (Emission memory); + + function getCurrentCycle() external view returns (uint256); + + function getNextCycleBlock() external view returns (uint256); + + function getRemainingEmissions() external view returns (uint256); + + function getRoleAdmin(bytes32 role) external view returns (bytes32); + + function getTreasuryAmount(uint256 cycle) external view returns (uint256); + + function getVote2EarnAmount(uint256 cycle) external view returns (uint256); + + function getXAllocationAmount(uint256 cycle) external view returns (uint256); + + function grantRole(bytes32 role, address account) external; + + function hasRole(bytes32 role, address account) external view returns (bool); + + function isCycleDistributed(uint256 cycle) external view returns (bool); + + function isCycleEnded(uint256 cycle) external view returns (bool); + + function isNextCycleDistributable() external view returns (bool); + + function lastEmissionBlock() external view returns (uint256); + + function maxVote2EarnDecay() external view returns (uint256); + + function nextCycle() external view returns (uint256); + + function renounceRole(bytes32 role, address callerConfirmation) external; + + function revokeRole(bytes32 role, address account) external; + + function setCycleDuration(uint256 _cycleDuration) external; + + function setMaxVote2EarnDecay(uint256 _maxVote2EarnDecay) external; + + function setTreasuryAddress(address treasuryAddress) external; + + function setTreasuryPercentage(uint256 _percentage) external; + + function setVote2EarnAddress(address vote2EarnAddress) external; + + function setVote2EarnDecay(uint256 _decay) external; + + function setVote2EarnDecayPeriod(uint256 _delay) external; + + function setXAllocationsDecay(uint256 _decay) external; + + function setXAllocationsDecayPeriod(uint256 _delay) external; + + function setXAllocationsGovernorAddress(address _xAllocationsGovernor) external; + + function setXallocationsAddress(address xAllocationAddress) external; + + function supportsInterface(bytes4 interfaceId) external view returns (bool); + + function totalEmissions() external view returns (uint256); + + function treasury() external view returns (address); + + function treasuryPercentage() external view returns (uint256); + + function vote2Earn() external view returns (address); + + function vote2EarnDecay() external view returns (uint256); + + function xAllocations() external view returns (address); + + function xAllocationsDecay() external view returns (uint256); + + function xAllocationsGovernor() external view returns (IXAllocationVotingGovernorV1); + + function version() external view returns (string memory); +} diff --git a/contracts/deprecated/V1/interfaces/IX2EarnAppsV1.sol b/contracts/deprecated/V1/interfaces/IX2EarnAppsV1.sol new file mode 100644 index 0000000..ee59487 --- /dev/null +++ b/contracts/deprecated/V1/interfaces/IX2EarnAppsV1.sol @@ -0,0 +1,326 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.20; + +import { X2EarnAppsDataTypes } from "../libraries/X2EarnAppsDataTypes.sol"; + +/** + * @title IX2EarnApps + * @notice Interface for the X2EarnApps contract. + * @dev The contract inheriting this interface should be able to manage the x2earn apps and their Eligibility for allocation voting. + */ +interface IX2EarnAppsV1 { + /** + * @dev The clock was incorrectly modified. + */ + error ERC6372InconsistentClock(); + + /** + * @dev The `appId` doesn't exist. + */ + error X2EarnNonexistentApp(bytes32 appId); + + /** + * @dev The `addr` is not valid (eg: is the ZERO ADDRESS). + */ + error X2EarnInvalidAddress(address addr); + + /** + * @dev An app with the specified `appId` already exists. + */ + error X2EarnAppAlreadyExists(bytes32 appId); + + /** + * @dev The user is not authorized to perform the action. + */ + error X2EarnUnauthorizedUser(address user); + + /** + * @dev Invalid start index for get apps pagination + */ + error X2EarnInvalidStartIndex(); + + /** + * @dev Lookup to future votes is not available. + */ + error ERC5805FutureLookup(uint256 timepoint, uint48 clock); + + /** + * @dev The `percentage` is not valid. + */ + error X2EarnInvalidAllocationPercentage(uint256 percentage); + + /** + * @dev The `distributorAddress` is not valid. + */ + error X2EarnNonexistentRewardDistributor(bytes32 appId, address distributorAddress); + + /** + * @dev The `moderator` is not valid. + */ + error X2EarnNonexistentModerator(bytes32 appId, address moderator); + + /** + * @dev The maximum number of moderators has been reached. + */ + error X2EarnMaxModeratorsReached(bytes32 appId); + + /** + * @dev The maximum number of reward distributors has been reached. + */ + error X2EarnMaxRewardDistributorsReached(bytes32 appId); + + /** + * @dev Event fired when a new app is added. + */ + event AppAdded(bytes32 indexed id, address addr, string name, bool appAvailableForAllocationVoting); + + /** + * @dev Event fired when an app Eligibility for allocation voting changes. + */ + event VotingEligibilityUpdated(bytes32 indexed appId, bool isAvailable); + + /** + * @dev Event fired when the admin adds a new moderator to the app. + */ + event ModeratorAddedToApp(bytes32 indexed appId, address moderator); + + /** + * @dev Event fired when the admin removes a moderator from the app. + */ + event ModeratorRemovedFromApp(bytes32 indexed appId, address moderator); + + /** + * @dev Event fired when the admin adds a new reward distributor to the app. + */ + event RewardDistributorAddedToApp(bytes32 indexed appId, address distributorAddress); + + /** + * @dev Event fired when the admin removes a reward distributor from the app. + */ + event RewardDistributorRemovedFromApp(bytes32 indexed appId, address distributorAddress); + + /** + * @dev Event fired when the admin of an app changes. + */ + event AppAdminUpdated(bytes32 indexed appId, address oldAdmin, address newAdmin); + + /** + * @dev Event fired when the address where the x2earn app receives allocation funds is changed. + */ + event TeamWalletAddressUpdated(bytes32 indexed appId, address oldTeamWalletAddress, address newTeamWalletAddress); + + /** + * @dev Event fired when the metadata URI of the app is changed. + */ + event AppMetadataURIUpdated(bytes32 indexed appId, string oldMetadataURI, string newMetadataURI); + + /** + * @dev Event fired when the base URI is updated. + */ + event BaseURIUpdated(string oldBaseURI, string newBaseURI); + + /** + * @dev Event fired when the team allocation percentage is updated. + */ + event TeamAllocationPercentageUpdated(bytes32 indexed appId, uint256 oldPercentage, uint256 newPercentage); + + /** + * @dev Generates the hash of the app name to be used as the app id. + * + * @param name the name of the app + */ + function hashAppName(string memory name) external pure returns (bytes32); + + /** + * @dev Add a new app to the x2earn apps. + * + * @param teamWalletAddress the address where the app should receive allocation funds + * @param admin the address of the admin that will be able to manage the app and perform all administration actions + * @param appName the name of the app + * @param metadataURI the metadata URI of the app + * + * Emits a {AppAdded} event. + */ + function addApp(address teamWalletAddress, address admin, string memory appName, string memory metadataURI) external; + + /** + * @dev Get the app data by its id. + * + * @param appId the id of the app + */ + function app(bytes32 appId) external view returns (X2EarnAppsDataTypes.AppWithDetailsReturnType memory); + + /** + * @dev Function to get the number of apps. + */ + function appsCount() external view returns (uint256); + + /** + * @dev Get a paginated list of apps + * @param startIndex The starting index of the pagination + * @param count The number of items to return + */ + function getPaginatedApps(uint startIndex, uint count) external view returns (X2EarnAppsDataTypes.App[] memory); + + /** + * @dev Get all apps + */ + function apps() external view returns (X2EarnAppsDataTypes.AppWithDetailsReturnType[] memory); + + /** + * @dev Add a new moderator to the app. + * + * @param appId the id of the app + * @param moderator the address of the moderator + * + * Emits a {ModeratorAddedToApp} event. + */ + function addAppModerator(bytes32 appId, address moderator) external; + + /** + * @dev Remove a moderator from the app. + * + * @param appId the id of the app + * @param moderator the address of the moderator + * + * Emits a {ModeratorRemovedFromApp} event. + */ + function removeAppModerator(bytes32 appId, address moderator) external; + + /** + * @dev Set the app admin. + * + * @param appId the id of the app + * @param admin the address of the admin + * + * Emits a {AppAdminUpdated} event. + */ + function setAppAdmin(bytes32 appId, address admin) external; + + /** + * @dev Get the app admin. + * + * @param appId the id of the app + */ + function appAdmin(bytes32 appId) external view returns (address); + + /** + * @dev Check if an account is the admin of the app + * + * @param appId the hashed name of the app + * @param account the address of the account + */ + function isAppAdmin(bytes32 appId, address account) external view returns (bool); + + /** + * @dev Update the address where the x2earn app receives allocation funds. + * + * @param appId the id of the app + * @param newTeamWalletAddress the new address where the app should receive allocation funds + * + * Emits a {TeamWalletAddressUpdated} event. + */ + function updateTeamWalletAddress(bytes32 appId, address newTeamWalletAddress) external; + + /** + * @dev Get the address where the x2earn app receives allocation funds. + * + * @param appId the id of the app + */ + function teamWalletAddress(bytes32 appId) external view returns (address); + + /** + * @dev Function to get the percentage of the allocation sent to the team address each round. + * + * @param appId the app id + */ + function teamAllocationPercentage(bytes32 appId) external view returns (uint256); + + /** + * @dev Update the allocation percentage to be sent to the team + * + * @param appId the id of the app + * @param percentage the new percentage of the allocation + */ + function setTeamAllocationPercentage(bytes32 appId, uint256 percentage) external; + + /** + * @dev Add a new reward distributor to the app. + * + * @param appId the id of the app + * @param distributorAddress the address of the reward distributor + * + * Emits a {RewardDistributorAddedToApp} event. + */ + function addRewardDistributor(bytes32 appId, address distributorAddress) external; + + /** + * @dev Remove a reward distributor from the app. + * + * @param appId the id of the app + * @param distributorAddress the address of the reward distributor + * + * Emits a {RewardDistributorRemovedFromApp} event. + */ + function removeRewardDistributor(bytes32 appId, address distributorAddress) external; + + /** + * @dev Returns true if an account is a reward distributor of the app + * + * @param appId the id of the app + * @param distributorAddress the address of the account + */ + function isRewardDistributor(bytes32 appId, address distributorAddress) external view returns (bool); + + /** + * @dev Update the metadata URI of the app. + * + * @param appId the id of the app + * @param metadataURI the new metadata URI of the app containing details about the app + * + * Emits a {AppMetadataURIUpdated} event. + */ + function updateAppMetadata(bytes32 appId, string memory metadataURI) external; + + /** + * @dev Check if there is an app with the specified `appId`. + * + * @param appId the id of the app + */ + function appExists(bytes32 appId) external view returns (bool); + + /** + * @dev Allow or deny an app to participate in the next allocation voting rounds. + * + * @param _appId the id of the app + * @param _isEligible true if the app should be eligible for voting, false otherwise + * + * Emits a {VotingEligibilityUpdated} event. + */ + function setVotingEligibility(bytes32 _appId, bool _isEligible) external; + + /** + * @dev Get all the app ids that are eligible for voting in the next allocation rounds. + */ + function allEligibleApps() external view returns (bytes32[] memory); + + /** + * @dev Check if an app was allowed to participate in the allocation rounds in a specific timepoint. + * XAllocationVoting contract can use this function to check if an app was eligible for voting in the block when the round starts. + * + * @param appId the id of the app + * @param timepoint the timepoint when the app should be checked for Eligibility + */ + function isEligible(bytes32 appId, uint256 timepoint) external view returns (bool); + + /** + * @dev return the base URI for the contract + */ + function baseURI() external view returns (string memory); + + /** + * @notice Get the version of the contract. + * @dev This should be updated every time a new version of implementation is deployed. + */ + function version() external view returns (string memory); +} diff --git a/contracts/deprecated/V1/interfaces/IXAllocationVotingGovernorV1.sol b/contracts/deprecated/V1/interfaces/IXAllocationVotingGovernorV1.sol index 51c0492..8e63250 100644 --- a/contracts/deprecated/V1/interfaces/IXAllocationVotingGovernorV1.sol +++ b/contracts/deprecated/V1/interfaces/IXAllocationVotingGovernorV1.sol @@ -21,7 +21,7 @@ import { IERC6372 } from "@openzeppelin/contracts/interfaces/IERC6372.sol"; * where shares should be calculated. Anyone can finalize the failed round, * but it will be automatically done when a new round starts. */ -interface IXAllocationVotingGovernor is IERC165, IERC6372 { +interface IXAllocationVotingGovernorV1 is IERC165, IERC6372 { enum RoundState { Active, Failed, diff --git a/contracts/deprecated/V1/libraries/X2EarnAppsDataTypes.sol b/contracts/deprecated/V1/libraries/X2EarnAppsDataTypes.sol new file mode 100644 index 0000000..67b4434 --- /dev/null +++ b/contracts/deprecated/V1/libraries/X2EarnAppsDataTypes.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.20; + +library X2EarnAppsDataTypes { + struct App { + bytes32 id; + string name; + uint256 createdAtTimestamp; + } + + struct AppWithDetailsReturnType { + bytes32 id; + address teamWalletAddress; + string name; + string metadataURI; + uint256 createdAtTimestamp; + bool appAvailableForAllocationVoting; + } +} diff --git a/contracts/deprecated/V1/x-allocation-voting-governance/XAllocationVotingGovernorV1.sol b/contracts/deprecated/V1/x-allocation-voting-governance/XAllocationVotingGovernorV1.sol new file mode 100644 index 0000000..acec2b1 --- /dev/null +++ b/contracts/deprecated/V1/x-allocation-voting-governance/XAllocationVotingGovernorV1.sol @@ -0,0 +1,311 @@ +// SPDX-License-Identifier: MIT + +// ####### +// ################ +// #################### +// ########### ######### +// ######### ######### +// ####### ######### ######### +// ######### ######### ########## +// ########## ######## #################### +// ########## ######### ######################### +// ################### ############################ +// ################# ########## ######## +// ############## ### ######## +// ############ ######### +// ########## ########## +// ######## ########### +// ### ############ +// ############## +// ################# +// ############## +// ######### + +pragma solidity 0.8.20; + +import { ERC165Upgradeable } from "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol"; +import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import { ContextUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol"; +import { IXAllocationVotingGovernorV1, IERC6372 } from "../interfaces/IXAllocationVotingGovernorV1.sol"; +import { IXAllocationPoolV1 } from "../interfaces/IXAllocationPoolV1.sol"; +import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import { IX2EarnAppsV1 } from "../interfaces/IX2EarnAppsV1.sol"; +import { IEmissionsV1 } from "../interfaces/IEmissionsV1.sol"; +import { IVoterRewardsV1 } from "../interfaces/IVoterRewardsV1.sol"; + +/** + * @title XAllocationVotingGovernorV1 + * @dev Core of the voting system of allocation rounds, designed to be extended through various modules. + * + * This contract is abstract and requires several functions to be implemented in various modules: + * - A counting module must implement {quorum}, {_quorumReached}, {_voteSucceeded}, and {_countVote} + * - A voting module must implement {_getVotes}, {clock}, and {CLOCK_MODE} + * - A settings module must implement {votingPeriod} + * - An external contracts module must implement {x2EarnApps}, {emissions} and {voterRewards} + * - A rounds storage module must implement {_startNewRound}, {roundSnapshot}, {roundDeadline}, and {currentRoundId} + * - A rounds finalization module must implement {finalize} + * - A earnings settings module must implement {_snapshotRoundEarningsCap} + */ +abstract contract XAllocationVotingGovernorV1 is + Initializable, + ContextUpgradeable, + ERC165Upgradeable, + IXAllocationVotingGovernorV1 +{ + bytes32 private constant ALL_ROUND_STATES_BITMAP = bytes32((2 ** (uint8(type(RoundState).max) + 1)) - 1); + + /// @custom:storage-location erc7201:b3tr.storage.XAllocationVotingGovernor + struct XAllocationVotingGovernorStorage { + string _name; + } + + // keccak256(abi.encode(uint256(keccak256("b3tr.storage.XAllocationVotingGovernor")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant XAllocationVotingGovernorStorageLocation = + 0x7fb63bcd433c69110ad961bfbe38aef51814cbb9e11af6fe21011ae43fb4be00; + + function _getXAllocationVotingGovernorStorage() internal pure returns (XAllocationVotingGovernorStorage storage $) { + assembly { + $.slot := XAllocationVotingGovernorStorageLocation + } + } + + /** + * @dev Sets the value for {name} + */ + function __XAllocationVotingGovernor_init(string memory name_) internal onlyInitializing { + __XAllocationVotingGovernor_init_unchained(name_); + } + + function __XAllocationVotingGovernor_init_unchained(string memory name_) internal onlyInitializing { + XAllocationVotingGovernorStorage storage $ = _getXAllocationVotingGovernorStorage(); + $._name = name_; + } + + // ---------- Setters ---------- // + + /** + * @dev Starts a new round of voting to allocate funds to x-2-earn applications. + */ + function startNewRound() public virtual returns (uint256) { + address proposer = _msgSender(); + + // check that there isn't an already ongoing round + // but only do it after we have at least 1 round otherwise it will fail with `GovernorNonexistentRound` + uint256 currentRound = currentRoundId(); + if (currentRound > 0) { + require(!isActive(currentRound), "XAllocationVotingGovernor: there can be only one round per time"); + } + + return _startNewRound(proposer); + } + + /** + * @dev Cast a vote for a set of x-2-earn applications. + */ + function castVote(uint256 roundId, bytes32[] memory appIds, uint256[] memory voteWeights) public virtual { + _validateStateBitmap(roundId, _encodeStateBitmap(RoundState.Active)); + + require(appIds.length == voteWeights.length, "XAllocationVotingGovernor: apps and weights length mismatch"); + require(appIds.length > 0, "XAllocationVotingGovernor: no apps to vote for"); + + address voter = _msgSender(); + + _countVote(roundId, voter, appIds, voteWeights); + } + + // ---------- Internal and Private ---------- // + + /** + * @dev Check that the current state of a round matches the requirements described by the `allowedStates` bitmap. + * This bitmap should be built using `_encodeStateBitmap`. + * + * If requirements are not met, reverts with a {GovernorUnexpectedRoundState} error. + */ + function _validateStateBitmap(uint256 roundId, bytes32 allowedStates) private view returns (RoundState) { + RoundState currentState = state(roundId); + if (_encodeStateBitmap(currentState) & allowedStates == bytes32(0)) { + revert GovernorUnexpectedRoundState(roundId, currentState, allowedStates); + } + return currentState; + } + + // ---------- Getters ---------- // + + function supportsInterface( + bytes4 interfaceId + ) public view virtual override(IERC165, ERC165Upgradeable) returns (bool) { + return interfaceId == type(IXAllocationVotingGovernorV1).interfaceId || super.supportsInterface(interfaceId); + } + + /** + * @dev Returns the name of the governor. + */ + function name() public view virtual returns (string memory) { + XAllocationVotingGovernorStorage storage $ = _getXAllocationVotingGovernorStorage(); + return $._name; + } + + /** + * @dev Returns the version of the governor. + */ + function version() public view virtual returns (string memory) { + return "1"; + } + + /** + * @dev Checks if the specified round is in active state or not. + */ + function isActive(uint256 roundId) public view virtual override returns (bool) { + return state(roundId) == RoundState.Active; + } + + /** + * @dev Returns the current state of a round. + */ + function state(uint256 roundId) public view virtual returns (RoundState) { + uint256 snapshot = roundSnapshot(roundId); + + if (snapshot == 0) { + revert GovernorNonexistentRound(roundId); + } + + uint256 currentTimepoint = clock(); + + uint256 deadline = roundDeadline(roundId); + + if (deadline >= currentTimepoint) { + return RoundState.Active; + } else if (!_voteSucceeded(roundId)) { + return RoundState.Failed; + } else { + return RoundState.Succeeded; + } + } + + /** + * @dev Checks if the quorum has been reached for a given round. + */ + function quorumReached(uint256 roundId) public view returns (bool) { + return _quorumReached(roundId); + } + + /** + * @dev Returns the available votes votes for a given account at a given timepoint. + */ + function getVotes(address account, uint256 timepoint) public view virtual returns (uint256) { + return _getVotes(account, timepoint, ""); + } + + /** + * @dev Checks if the given appId can be voted for in the given round. + */ + function isEligibleForVote(bytes32 appId, uint256 roundId) public view virtual returns (bool) { + return x2EarnApps().isEligible(appId, roundSnapshot(roundId)); + } + + /** + * @dev Encodes a `RoundState` into a `bytes32` representation where each bit enabled corresponds to + * the underlying position in the `RoundState` enum. For example: + * + * 0x000...10000 + * ^^^^^^------ ... + * ^---- Succeeded + * ^--- Failed + * ^-- Active + */ + function _encodeStateBitmap(RoundState roundState) internal pure returns (bytes32) { + return bytes32(1 << uint8(roundState)); + } + + // ---------- Virtual ---------- // + + /** + * @dev Internal function to store a vote in storage. + */ + function _countVote( + uint256 roundId, + address account, + bytes32[] memory appIds, + uint256[] memory voteWeights + ) internal virtual; + + /** + * @dev Internal function to save the app shares cap and base allocation percentage for a round. + */ + function _snapshotRoundEarningsCap(uint256 roundId) internal virtual; + + /** + * @dev Internal function to check if the quorum has been reached for a given round. + */ + function _quorumReached(uint256 roundId) internal view virtual returns (bool); + + /** + * @dev Internal function to check if the vote has succeeded for a given round. + */ + function _voteSucceeded(uint256 roundId) internal view virtual returns (bool); + + /** + * @dev Internal function that starts a new round of voting to allocate funds to x-2-earn applications. + */ + function _startNewRound(address proposer) internal virtual returns (uint256); + + /** + * @dev Internal function to get the available votes for a given account at a given timepoint. + */ + function _getVotes(address account, uint256 timepoint, bytes memory params) internal view virtual returns (uint256); + + /** + * @dev Function to store the last succeeded round once a round ends. + */ + function finalizeRound(uint256 roundId) public virtual; + + /** + * @dev Clock used for flagging checkpoints. Can be overridden to implement timestamp based checkpoints (and voting), in which case {CLOCK_MODE} should be overridden as well to match. + */ + function clock() public view virtual returns (uint48); + + /** + * @dev Machine-readable description of the clock as specified in EIP-6372. + */ + function CLOCK_MODE() public view virtual returns (string memory); + + /** + * @dev Returns the voting duration. + */ + function votingPeriod() public view virtual returns (uint256); + + /** + * @dev Returns the quorum for a given timepoint. + */ + function quorum(uint256 timepoint) public view virtual returns (uint256); + + /** + * @dev Returns the block number when the round starts. + */ + function roundSnapshot(uint256 roundId) public view virtual returns (uint256); + + /** + * @dev Returns the block number when the round ends. + */ + function roundDeadline(uint256 roundId) public view virtual returns (uint256); + + /** + * @dev Returns the latest round id. + */ + function currentRoundId() public view virtual returns (uint256); + + /** + * @dev Returns the X2EarnApps contract. + */ + function x2EarnApps() public view virtual returns (IX2EarnAppsV1); + + /** + * @dev Returns the Emissions contract. + */ + function emissions() public view virtual returns (IEmissionsV1); + + /** + * @dev Returns the VoterRewards contract. + */ + function voterRewards() public view virtual returns (IVoterRewardsV1); +} diff --git a/contracts/deprecated/V1/x-allocation-voting-governance/modules/ExternalContractsUpgradeableV1.sol b/contracts/deprecated/V1/x-allocation-voting-governance/modules/ExternalContractsUpgradeableV1.sol new file mode 100644 index 0000000..d163f78 --- /dev/null +++ b/contracts/deprecated/V1/x-allocation-voting-governance/modules/ExternalContractsUpgradeableV1.sol @@ -0,0 +1,156 @@ +// SPDX-License-Identifier: MIT + +// ####### +// ################ +// #################### +// ########### ######### +// ######### ######### +// ####### ######### ######### +// ######### ######### ########## +// ########## ######## #################### +// ########## ######### ######################### +// ################### ############################ +// ################# ########## ######## +// ############## ### ######## +// ############ ######### +// ########## ########## +// ######## ########### +// ### ############ +// ############## +// ################# +// ############## +// ######### + +pragma solidity 0.8.20; + +import { XAllocationVotingGovernorV1 } from "../XAllocationVotingGovernorV1.sol"; +import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import { IEmissionsV1 } from "../../interfaces/IEmissionsV1.sol"; +import { IX2EarnAppsV1 } from "../../interfaces/IX2EarnAppsV1.sol"; +import { IVoterRewardsV1 } from "../../interfaces/IVoterRewardsV1.sol"; + +/** + * @title ExternalContractsUpgradeable + * @dev Extension of {XAllocationVotingGovernorV1} that handles the storage of external contracts for the XAllocationVotingGovernorV1. + */ +abstract contract ExternalContractsUpgradeableV1 is Initializable, XAllocationVotingGovernorV1 { + /// @custom:storage-location erc7201:b3tr.storage.XAllocationVotingGovernorV1.ExternalContracts + struct ExternalContractsStorage { + IX2EarnAppsV1 _x2EarnApps; + IEmissionsV1 _emissions; + IVoterRewardsV1 _voterRewards; + } + + // keccak256(abi.encode(uint256(keccak256("b3tr.storage.XAllocationVotingGovernorV1.ExternalContracts")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant ExternalContractsStorageLocation = + 0x1da8cbbb2b12987a437595605432a6bbe84c08e9685afaaee593f05659f50d00; + + function _getExternalContractsStorage() internal pure returns (ExternalContractsStorage storage $) { + assembly { + $.slot := ExternalContractsStorageLocation + } + } + + // @dev Emit when the emissions contract is set + event EmissionsSet(address oldContractAddress, address newContractAddress); + // @dev Emit when the X2EarnApps contract is set + event X2EarnAppsSet(address oldContractAddress, address newContractAddress); + // @dev Emit when the voter rewards contract is set + event VoterRewardsSet(address oldContractAddress, address newContractAddress); + + /** + * @dev Initializes the contract + * @param initialX2EarnApps The initial X2EarnApps contract address + * @param initialEmissions The initial Emissions contract address + * @param initialVoterRewards The initial VoterRewards contract address + */ + function __ExternalContracts_init( + IX2EarnAppsV1 initialX2EarnApps, + IEmissionsV1 initialEmissions, + IVoterRewardsV1 initialVoterRewards + ) internal onlyInitializing { + __ExternalContracts_init_unchained(initialX2EarnApps, initialEmissions, initialVoterRewards); + } + + function __ExternalContracts_init_unchained( + IX2EarnAppsV1 initialX2EarnApps, + IEmissionsV1 initialEmissions, + IVoterRewardsV1 initialVoterRewards + ) internal onlyInitializing { + ExternalContractsStorage storage $ = _getExternalContractsStorage(); + $._x2EarnApps = initialX2EarnApps; + $._emissions = initialEmissions; + $._voterRewards = initialVoterRewards; + } + + // ------- Getters ------- // + /** + * @dev The X2EarnApps contract. + */ + function x2EarnApps() public view override returns (IX2EarnAppsV1) { + ExternalContractsStorage storage $ = _getExternalContractsStorage(); + return $._x2EarnApps; + } + + /** + * @dev The emissions contract. + */ + function emissions() public view override returns (IEmissionsV1) { + ExternalContractsStorage storage $ = _getExternalContractsStorage(); + return $._emissions; + } + + /** + * @dev Get the voter rewards contract + */ + function voterRewards() public view override returns (IVoterRewardsV1) { + ExternalContractsStorage storage $ = _getExternalContractsStorage(); + return $._voterRewards; + } + + // ------- Internal Functions ------- // + + /** + * @dev Sets the emissions contract. + * + * Emits a {EmissionContractSet} event + */ + function _setEmissions(IEmissionsV1 newEmisionsAddress) internal virtual { + require(address(newEmisionsAddress) != address(0), "XAllocationVotingGovernorV1: emissions is the zero address"); + ExternalContractsStorage storage $ = _getExternalContractsStorage(); + + emit EmissionsSet(address($._emissions), address(newEmisionsAddress)); + $._emissions = IEmissionsV1(newEmisionsAddress); + } + + /** + * @dev Sets the X2EarnApps contract + * @param newX2EarnApps The new X2EarnApps contract address + * + * Emits a {X2EarnAppsSet} event + */ + function _setX2EarnApps(IX2EarnAppsV1 newX2EarnApps) internal virtual { + require(address(newX2EarnApps) != address(0), "XAllocationVotingGovernorV1: new X2EarnApps is the zero address"); + + ExternalContractsStorage storage $ = _getExternalContractsStorage(); + + emit X2EarnAppsSet(address($._x2EarnApps), address(newX2EarnApps)); + $._x2EarnApps = newX2EarnApps; + } + + /** + * @dev Sets the voter rewards contract + * @param newVoterRewards The new voter rewards contract address + */ + function _setVoterRewards(IVoterRewardsV1 newVoterRewards) internal virtual { + require( + address(newVoterRewards) != address(0), + "XAllocationVotingGovernorV1: new voter rewards is the zero address" + ); + + ExternalContractsStorage storage $ = _getExternalContractsStorage(); + + emit VoterRewardsSet(address($._voterRewards), address(newVoterRewards)); + $._voterRewards = newVoterRewards; + } +} diff --git a/contracts/deprecated/V1/x-allocation-voting-governance/modules/RoundEarningsSettingsUpgradeableV1.sol b/contracts/deprecated/V1/x-allocation-voting-governance/modules/RoundEarningsSettingsUpgradeableV1.sol new file mode 100644 index 0000000..80f587f --- /dev/null +++ b/contracts/deprecated/V1/x-allocation-voting-governance/modules/RoundEarningsSettingsUpgradeableV1.sol @@ -0,0 +1,158 @@ +// SPDX-License-Identifier: MIT + +// ####### +// ################ +// #################### +// ########### ######### +// ######### ######### +// ####### ######### ######### +// ######### ######### ########## +// ########## ######## #################### +// ########## ######### ######################### +// ################### ############################ +// ################# ########## ######## +// ############## ### ######## +// ############ ######### +// ########## ########## +// ######## ########### +// ### ############ +// ############## +// ################# +// ############## +// ######### + +pragma solidity 0.8.20; + +import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import { XAllocationVotingGovernorV1 } from "../XAllocationVotingGovernorV1.sol"; + +/** + * @title RoundEarningsSettingsUpgradeable + * @notice Extension of {XAllocationVotingGovernorV1} to handle the settings for the x-allocation earnings calculations: + * - baseAllocationPercentage: The base allocation percentage to be divided among the x-apps each round + * - appSharesCap: The maximum percentage of shares an x-app can reach in each round + * + * Since the base allocation percentage and app shares cap can be updated, we store the values for each round. + */ +abstract contract RoundEarningsSettingsUpgradeableV1 is Initializable, XAllocationVotingGovernorV1 { + /// @custom:storage-location erc7201:b3tr.storage.XAllocationVotingGovernorV1.RoundEarningsSettings + struct EarningsSettingsStorage { + uint256 baseAllocationPercentage; + uint256 appSharesCap; + mapping(uint256 roundId => uint256) _roundBaseAllocationPercentage; + mapping(uint256 roundId => uint256) _roundAppSharesCap; + } + + // keccak256(abi.encode(uint256(keccak256("b3tr.storage.XAllocationVotingGovernorV1.RoundEarningsSettings")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant EarningsSettingsStorageLocation = + 0xc74db4e191410c7a6c18f14684e1218b5e87c449d0f81ab47e8c67bf971c3500; + + function _getEarningsSettingsStorage() internal pure returns (EarningsSettingsStorage storage $) { + assembly { + $.slot := EarningsSettingsStorageLocation + } + } + + // ---------- Initialization ---------- // + + /** + * @dev Initialize the contract + * @param initialBaseAllocationPercentage The initial base allocation percentage + * @param initialAppSharesCap The initial app shares cap + */ + function __RoundEarningsSettings_init( + uint256 initialBaseAllocationPercentage, + uint256 initialAppSharesCap + ) internal onlyInitializing { + __RoundEarningsSettings_init_unchained(initialBaseAllocationPercentage, initialAppSharesCap); + } + + function __RoundEarningsSettings_init_unchained( + uint256 initialBaseAllocationPercentage, + uint256 initialAppSharesCap + ) internal onlyInitializing { + _setBaseAllocationPercentage(initialBaseAllocationPercentage); + _setAppSharesCap(initialAppSharesCap); + } + + // ---------- Getters ---------- // + + /** + * @notice Get the base allocation percentage + */ + function baseAllocationPercentage() public view returns (uint256) { + EarningsSettingsStorage storage $ = _getEarningsSettingsStorage(); + return $.baseAllocationPercentage; + } + + /** + * @notice Get the app shares cap + */ + function appSharesCap() public view returns (uint256) { + EarningsSettingsStorage storage $ = _getEarningsSettingsStorage(); + return $.appSharesCap; + } + + /** + * Returns the base allocation percentage for a given round + */ + function getRoundBaseAllocationPercentage(uint256 roundId) public view returns (uint256) { + EarningsSettingsStorage storage $ = _getEarningsSettingsStorage(); + return $._roundBaseAllocationPercentage[roundId]; + } + + /** + * Returns the app shares cap for a given round + */ + function getRoundAppSharesCap(uint256 roundId) public view returns (uint256) { + EarningsSettingsStorage storage $ = _getEarningsSettingsStorage(); + return $._roundAppSharesCap[roundId]; + } + + // ---------- Internal ---------- // + + /** + * @notice Set the base allocation percentage + * @param baseAllocationPercentage_ The new base allocation percentage + */ + function _setBaseAllocationPercentage(uint256 baseAllocationPercentage_) internal { + require( + baseAllocationPercentage_ <= 100, + "XAllocationVotingGovernorV1: Base allocation percentage must be less than or equal to 100" + ); + EarningsSettingsStorage storage $ = _getEarningsSettingsStorage(); + $.baseAllocationPercentage = baseAllocationPercentage_; + } + + /** + * @notice Set the app shares cap + * @param appSharesCap_ The new app shares cap + */ + function _setAppSharesCap(uint256 appSharesCap_) internal { + require(appSharesCap_ <= 100, "XAllocationVotingGovernorV1: App shares cap must be less than or equal to 100"); + EarningsSettingsStorage storage $ = _getEarningsSettingsStorage(); + $.appSharesCap = appSharesCap_; + } + + /** + * @dev Save the earnings settings for a new round + * @param roundId The id of the new round + */ + function _snapshotRoundEarningsCap(uint256 roundId) internal virtual override { + EarningsSettingsStorage storage $ = _getEarningsSettingsStorage(); + $._roundBaseAllocationPercentage[roundId] = $.baseAllocationPercentage; + $._roundAppSharesCap[roundId] = $.appSharesCap; + } + + // ---------- Setters ---------- // + + /** + * @dev See {IXAllocationVotingGovernor-setBaseAllocationPercentage}. + */ + function setAppSharesCap(uint256 appSharesCap_) external virtual; + + /** + * @dev See {IXAllocationVotingGovernor-setBaseAllocationPercentage}. + */ + function setBaseAllocationPercentage(uint256 baseAllocationPercentage_) external virtual; +} diff --git a/contracts/deprecated/V1/x-allocation-voting-governance/modules/RoundFinalizationUpgradeableV1.sol b/contracts/deprecated/V1/x-allocation-voting-governance/modules/RoundFinalizationUpgradeableV1.sol new file mode 100644 index 0000000..30209f2 --- /dev/null +++ b/contracts/deprecated/V1/x-allocation-voting-governance/modules/RoundFinalizationUpgradeableV1.sol @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: MIT + +// ####### +// ################ +// #################### +// ########### ######### +// ######### ######### +// ####### ######### ######### +// ######### ######### ########## +// ########## ######## #################### +// ########## ######### ######################### +// ################### ############################ +// ################# ########## ######## +// ############## ### ######## +// ############ ######### +// ########## ########## +// ######## ########### +// ### ############ +// ############## +// ################# +// ############## +// ######### + +pragma solidity 0.8.20; + +import { XAllocationVotingGovernorV1 } from "../XAllocationVotingGovernorV1.sol"; +import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; + +/** + * @title RoundFinalizationUpgradeable + * @notice Extension of {XAllocationVotingGovernorV1} that handles the finalization of rounds + * @dev If a round does not meet the quorum (RoundState.Failed) we need to know the last round that succeeded, + * so we can calculate the earnings for the x-2-earn-apps upon that round. By always pointing each round at the last succeeded one, if a round fails, + * it will be enough to look at what round the previous one points to. + */ +abstract contract RoundFinalizationUpgradeableV1 is Initializable, XAllocationVotingGovernorV1 { + /// @custom:storage-location erc7201:b3tr.storage.XAllocationVotingGovernorV1.RoundFinalization + struct RoundFinalizationStorage { + mapping(uint256 roundId => uint256) _latestSucceededRoundId; + mapping(uint256 roundId => bool) _roundFinalized; + } + + // keccak256(abi.encode(uint256(keccak256("b3tr.storage.XAllocationVotingGovernorV1.RoundFinalization")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant RoundFinalizationStorageLocation = + 0x7dd3251b9882a8b07dc283a0b43197aa2be3a6af1a7f0284070fe5d86e502500; + + function _getRoundFinalizationStorage() internal pure returns (RoundFinalizationStorage storage $) { + assembly { + $.slot := RoundFinalizationStorageLocation + } + } + + /** + * @dev Initializes the contract + */ + function __RoundFinalization_init() internal onlyInitializing { + __RoundFinalization_init_unchained(); + } + + function __RoundFinalization_init_unchained() internal onlyInitializing {} + + // ------- Setters ------- // + + /** + * @dev Store the last succeeded round for the given round + * @param roundId The round to finalize + */ + function finalizeRound(uint256 roundId) public virtual override { + require(!isActive(roundId), "XAllocationVotingGovernorV1: round is not ended yet"); + + RoundFinalizationStorage storage $ = _getRoundFinalizationStorage(); + // First round is always considered succeeded + if (roundId == 1) { + $._latestSucceededRoundId[roundId] = 1; + $._roundFinalized[roundId] = true; + return; + } + + if (state(roundId) == RoundState.Succeeded) { + // if round is succeeded, it is the last succeeded round + $._latestSucceededRoundId[roundId] = roundId; + $._roundFinalized[roundId] = true; + } else if (state(roundId) == RoundState.Failed) { + // if round is failed, it points to the last succeeded round + $._latestSucceededRoundId[roundId] = $._latestSucceededRoundId[roundId - 1]; + $._roundFinalized[roundId] = true; + } + } + + // ------- Getters ------- // + + /** + * @dev Get the last succeeded round for the given round + */ + function latestSucceededRoundId(uint256 roundId) external view returns (uint256) { + RoundFinalizationStorage storage $ = _getRoundFinalizationStorage(); + return $._latestSucceededRoundId[roundId]; + } + + /** + * @dev Check if the round is finalized + */ + function isFinalized(uint256 roundId) external view returns (bool) { + RoundFinalizationStorage storage $ = _getRoundFinalizationStorage(); + return $._roundFinalized[roundId]; + } +} diff --git a/contracts/deprecated/V1/x-allocation-voting-governance/modules/RoundVotesCountingUpgradeableV1.sol b/contracts/deprecated/V1/x-allocation-voting-governance/modules/RoundVotesCountingUpgradeableV1.sol new file mode 100644 index 0000000..6f5c80b --- /dev/null +++ b/contracts/deprecated/V1/x-allocation-voting-governance/modules/RoundVotesCountingUpgradeableV1.sol @@ -0,0 +1,292 @@ +// SPDX-License-Identifier: MIT + +// ####### +// ################ +// #################### +// ########### ######### +// ######### ######### +// ####### ######### ######### +// ######### ######### ########## +// ########## ######## #################### +// ########## ######### ######################### +// ################### ############################ +// ################# ########## ######## +// ############## ### ######## +// ############ ######### +// ########## ########## +// ######## ########### +// ### ############ +// ############## +// ################# +// ############## +// ######### + +pragma solidity 0.8.20; + +import { XAllocationVotingGovernorV1 } from "../XAllocationVotingGovernorV1.sol"; +import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; + +/** + * @title RoundVotesCountingUpgradeable + * + * @dev Extension of {XAllocationVotingGovernorV1} for counting votes for allocation rounds. + * + * In every round users can vote a fraction of their balance for the eligible apps in that round. + */ +abstract contract RoundVotesCountingUpgradeableV1 is Initializable, XAllocationVotingGovernorV1 { + struct RoundVote { + // Total votes received for each app + mapping(bytes32 appId => uint256) votesReceived; + // Total votes received for each app in quadratic funding + mapping(bytes32 appId => uint256) votesReceivedQF; // ∑(sqrt(votes)) -> sqrt(votes1) + sqrt(votes2) + ... + // Total votes cast in the round + uint256 totalVotes; + // Total votes cast in the round in quadratic funding + uint256 totalVotesQF; // ∑(∑sqrt(votes))^2 -> (sqrt(votesAppX1) + sqrt(votesAppX2) + ...)^2 + (sqrt(votesAppY1) + sqrt(votesAppY2) + ...)^2 + ... + // Mapping to store if a user has voted + mapping(address user => bool) hasVoted; + // Total number of voters in the round + uint256 totalVoters; + } + + /// @custom:storage-location erc7201:b3tr.storage.XAllocationVotingGovernorV1.RoundVotesCounting + struct RoundVotesCountingStorage { + mapping(address user => bool) _hasVotedOnce; // mapping to store that a user has voted at least one time + mapping(uint256 roundId => RoundVote) _roundVotes; // mapping to store the votes for each round + uint256 votingThreshold; // minimum number of tokens needed to cast a vote + } + + // keccak256(abi.encode(uint256(keccak256("b3tr.storage.XAllocationVotingGovernorV1.RoundVotesCounting")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant RoundVotesCountingStorageLocation = + 0xa760c041d4a9fa3a2c67d0d325f3592ba2c7e4330f7ba2283ebf9fe63913d500; + + function _getRoundVotesCountingStorage() private pure returns (RoundVotesCountingStorage storage $) { + assembly { + $.slot := RoundVotesCountingStorageLocation + } + } + + //@notice emitted when a the minimum number of tokens needed to cast a vote is updated + event VotingThresholdSet(uint256 oldVotingThreshold, uint256 newVotingThreshold); + + /** + * @dev Initializes the contract + */ + function __RoundVotesCounting_init(uint256 _votingThreshold) internal onlyInitializing { + __RoundVotesCounting_init_unchained(_votingThreshold); + } + + function __RoundVotesCounting_init_unchained(uint256 _votingThreshold) internal onlyInitializing { + RoundVotesCountingStorage storage $ = _getRoundVotesCountingStorage(); + + $.votingThreshold = _votingThreshold; + } + + /** + * @dev See {IXAllocationVotingGovernor-COUNTING_MODE}. + */ + // solhint-disable-next-line func-name-mixedcase + function COUNTING_MODE() public pure virtual override returns (string memory) { + return "support=x-allocations&quorum=auto"; + } + + /** + * @dev Update the voting threshold. This operation can only be performed through a governance proposal. + * + * Emits a {VotingThresholdSet} event. + */ + function setVotingThreshold(uint256 newVotingThreshold) public virtual { + _setVotingThreshold(newVotingThreshold); + } + + /** + * @dev Internal setter for the voting threshold. + * + * Emits a {VotingThresholdSet} event. + */ + function _setVotingThreshold(uint256 newVotingThreshold) internal virtual { + RoundVotesCountingStorage storage $ = _getRoundVotesCountingStorage(); + + emit VotingThresholdSet($.votingThreshold, newVotingThreshold); + $.votingThreshold = newVotingThreshold; + } + + /** + * @dev Counts votes for a given round of voting, applying quadratic funding principles. + * Allows a voter to allocate weights to various applications (apps) for a specific voting round, + * ensuring each voter votes only once per round to prevent double voting. + * + * Quadratic funding is used to calculate the impact of each vote. For each app, the square root of the + * individual vote's weight is computed and added to the total sum of square roots for that app. + * After updating with each vote, this sum of square roots is squared to determine the total quadratic funding votes for the app. + * This method aims to democratize the voting process by amplifying the influence of a larger number of smaller votes. + + * Requirements: + * - The voter must not have voted in this round already. + * - The total voting weight allocated by the voter must not exceed the voter's available voting power. + * - Each app voted on must be eligible for votes in the current round. + * + * @param roundId The identifier of the current voting round. + * @param voter The address of the voter casting the votes. + * @param apps An array of app identifiers that the voter is allocating votes to. + * @param weights An array of vote weights corresponding to each app. + */ + + function _countVote( + uint256 roundId, + address voter, + bytes32[] memory apps, + uint256[] memory weights + ) internal virtual override { + if (hasVoted(roundId, voter)) { + revert GovernorAlreadyCastVote(voter); + } + + RoundVotesCountingStorage storage $ = _getRoundVotesCountingStorage(); + + // Get the start of the round + uint256 roundStart = roundSnapshot(roundId); + + // To hold the total weight of votes cast by the voter + uint256 totalWeight; + // To hold the total adjustment to the quadratic funding value for the given app + uint256 totalQFVotesAdjustment; + + // Get the total voting power of the voter to use in the for loop to check + // if the total weight of votes cast by the voter is greater than the voter's available voting power + uint256 voterAvailableVotes = getVotes(voter, roundStart); + + // Iterate through the apps and weights to calculate the total weight of votes cast by the voter + for (uint256 i; i < apps.length; i++) { + // Update the total weight of votes cast by the voter + totalWeight += weights[i]; + + if (totalWeight > voterAvailableVotes) { + revert GovernorInsufficientVotingPower(); + } + + // Check if the app is eligible for votes in the current round + if (!isEligibleForVote(apps[i], roundId)) { + revert GovernorAppNotAvailableForVoting(apps[i]); + } + + // Get the current sum of the square roots of individual votes for the given project + uint256 qfAppVotesPreVote = $._roundVotes[roundId].votesReceivedQF[apps[i]]; // ∑(sqrt(votes)) -> sqrt(votes1) + sqrt(votes2) + ... + sqrt(votesN) + + // Calculate the new sum of the square roots of individual votes for the given project + uint256 newQFVotes = Math.sqrt(weights[i]); // sqrt(votes) + uint256 qfAppVotesPostVote = qfAppVotesPreVote + newQFVotes; // ∑(sqrt(votes)) -> sqrt(votes1) + sqrt(votes2) + ... + sqrt(votesN) + sqrt(votesN+1) + + // Calculate the adjustment to the quadratic funding value for the given app + totalQFVotesAdjustment += (qfAppVotesPostVote * qfAppVotesPostVote) - (qfAppVotesPreVote * qfAppVotesPreVote); // (sqrt(votes1) + ... + sqrt(votesN+1))^2 - (sqrt(votes1) + ... + sqrt(votesN))^2 + + // Update the quadratic funding votes received for the given app - sum of the square roots of individual votes + $._roundVotes[roundId].votesReceivedQF[apps[i]] = qfAppVotesPostVote; // ∑(sqrt(votes)) -> sqrt(votes1) + sqrt(votes2) + ... + sqrt(votesN+1) + $._roundVotes[roundId].votesReceived[apps[i]] += weights[i]; // ∑votes + votesN+1 + } + + // Check if the total weight of votes cast by the voter is greater than the voting threshold + if (totalWeight < votingThreshold()) { + revert GovernorVotingThresholdNotMet(votingThreshold(), totalWeight); + } + + // Apply the total adjustment to storage + $._roundVotes[roundId].totalVotesQF += totalQFVotesAdjustment; // update the total quadratic funding value for the round - ∑(∑sqrt(votes))^2 -> (sqrt(votesAppX1) + sqrt(votesAppX2) + ...)^2 + (sqrt(votesAppY1) + sqrt(votesAppY2) + ...)^2 + ... + $._roundVotes[roundId].totalVotes += totalWeight; // update total votes -> ∑votes + votesN+1 + $._roundVotes[roundId].hasVoted[voter] = true; // mark the voter as having voted + $._roundVotes[roundId].totalVoters++; // increment the total number of voters + + // save that user cast vote only the first time + if (!$._hasVotedOnce[voter]) { + $._hasVotedOnce[voter] = true; + } + + // Register the vote for rewards calculation where the vote power is the square root of the total votes cast by the voter + voterRewards().registerVote(roundStart, voter, totalWeight, Math.sqrt(totalWeight)); + + // Emit the AllocationVoteCast event + emit AllocationVoteCast(voter, roundId, apps, weights); + } + + /** + * @dev Get the votes received by a specific application in a given round + */ + function getAppVotes(uint256 roundId, bytes32 app) public view override returns (uint256) { + RoundVotesCountingStorage storage $ = _getRoundVotesCountingStorage(); + return $._roundVotes[roundId].votesReceived[app]; + } + + /** + * @dev Get the quadratic funding votes received by a specific application in a given round + */ + function getAppVotesQF(uint256 roundId, bytes32 app) public view override returns (uint256) { + RoundVotesCountingStorage storage $ = _getRoundVotesCountingStorage(); + return $._roundVotes[roundId].votesReceivedQF[app]; + } + + /** + * @dev Get the total quadratic funding votes cast in a given round + */ + function totalVotesQF(uint256 roundId) public view override returns (uint256) { + RoundVotesCountingStorage storage $ = _getRoundVotesCountingStorage(); + return $._roundVotes[roundId].totalVotesQF; + } + + /** + * @dev Get the total votes cast in a given round + */ + function totalVotes(uint256 roundId) public view override returns (uint256) { + RoundVotesCountingStorage storage $ = _getRoundVotesCountingStorage(); + return $._roundVotes[roundId].totalVotes; + } + + /** + * @dev Get the total number of voters in a given round + */ + function totalVoters(uint256 roundId) public view override returns (uint256) { + RoundVotesCountingStorage storage $ = _getRoundVotesCountingStorage(); + return $._roundVotes[roundId].totalVoters; + } + + /** + * @notice The voting threshold. + * @dev The minimum number of tokens needed to cast a vote. + */ + function votingThreshold() public view virtual returns (uint256) { + RoundVotesCountingStorage storage $ = _getRoundVotesCountingStorage(); + return $.votingThreshold; + } + + /** + * @dev Check if a user has voted in a given round + */ + function hasVoted(uint256 roundId, address user) public view returns (bool) { + RoundVotesCountingStorage storage $ = _getRoundVotesCountingStorage(); + return $._roundVotes[roundId].hasVoted[user]; + } + + /** + * @dev Internal function to check if the quorum is reached for a given round + */ + function _quorumReached(uint256 roundId) internal view virtual override returns (bool) { + return quorum(roundSnapshot(roundId)) <= totalVotes(roundId); + } + + /** + * @dev Internal function to check if the vote succeeded for a given round + */ + function _voteSucceeded(uint256 roundId) internal view virtual override returns (bool) { + // vote is successful if quorum is reached + return _quorumReached(roundId); + } + + /** + * @dev Check if a user has voted at least once from the deployment of the contract + */ + function hasVotedOnce(address user) public view returns (bool) { + RoundVotesCountingStorage storage $ = _getRoundVotesCountingStorage(); + return $._hasVotedOnce[user]; + } +} diff --git a/contracts/deprecated/V1/x-allocation-voting-governance/modules/RoundsStorageUpgradeableV1.sol b/contracts/deprecated/V1/x-allocation-voting-governance/modules/RoundsStorageUpgradeableV1.sol new file mode 100644 index 0000000..76e90e2 --- /dev/null +++ b/contracts/deprecated/V1/x-allocation-voting-governance/modules/RoundsStorageUpgradeableV1.sol @@ -0,0 +1,197 @@ +// SPDX-License-Identifier: MIT + +// ####### +// ################ +// #################### +// ########### ######### +// ######### ######### +// ####### ######### ######### +// ######### ######### ########## +// ########## ######## #################### +// ########## ######### ######################### +// ################### ############################ +// ################# ########## ######## +// ############## ### ######## +// ############ ######### +// ########## ########## +// ######## ########### +// ### ############ +// ############## +// ################# +// ############## +// ######### + +pragma solidity 0.8.20; + +import { XAllocationVotingGovernorV1 } from "../XAllocationVotingGovernorV1.sol"; +import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import { X2EarnAppsDataTypes } from "../../libraries/X2EarnAppsDataTypes.sol"; + +/** + * @title RoundsStorageUpgradeable + * @dev Extension of {XAllocationVotingGovernorV1} for storing rounds data and managing the rounds lifecycle. + */ +abstract contract RoundsStorageUpgradeableV1 is Initializable, XAllocationVotingGovernorV1 { + struct RoundCore { + address proposer; + uint48 voteStart; + uint32 voteDuration; + } + + /// @custom:storage-location erc7201:b3tr.storage.XAllocationVotingGovernorV1.RoundsStorage + struct RoundsStorageStorage { + uint256 _roundCount; // counter to count the number of proposals and also used to create the id + mapping(uint256 roundId => RoundCore) _rounds; // mapping to store the round data + mapping(uint256 roundId => bytes32[]) _appsEligibleForVoting; // mapping to store the apps eligible for voting in each round + } + + // keccak256(abi.encode(uint256(keccak256("b3tr.storage.XAllocationVotingGovernorV1.RoundsStorage")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant RoundsStorageStorageLocation = + 0x0f5210c47c3bb73c471770a1cbb5b7ddc03c0ec886694cc17ae21d1f595f1900; + + function _getRoundsStorageStorage() internal pure returns (RoundsStorageStorage storage $) { + assembly { + $.slot := RoundsStorageStorageLocation + } + } + + /** + * @dev Initializes the contract + */ + function __RoundsStorage_init() internal onlyInitializing { + __RoundsStorage_init_unchained(); + } + + function __RoundsStorage_init_unchained() internal onlyInitializing {} + + // ------- Setters ------- // + + /** + * @dev Internal function to start a new round + * @param proposer The address of the proposer + * @return roundId The id of the new round + * + * Emits a {RoundCreated} event + */ + function _startNewRound(address proposer) internal virtual override returns (uint256 roundId) { + RoundsStorageStorage storage $ = _getRoundsStorageStorage(); + + ++$._roundCount; + roundId = $._roundCount; + if ($._rounds[roundId].voteStart != 0) { + revert GovernorUnexpectedRoundState(roundId, state(roundId), bytes32(0)); + } + + // Do not run for the first round + if (roundId > 1) { + // finalize the previous round + finalizeRound(roundId - 1); + } + + // save x-apps that users can vote for + bytes32[] memory apps = x2EarnApps().allEligibleApps(); + $._appsEligibleForVoting[roundId] = apps; + + _snapshotRoundEarningsCap(roundId); + + uint256 snapshot = clock(); + uint256 duration = votingPeriod(); + + RoundCore storage round = $._rounds[roundId]; + round.proposer = proposer; + round.voteStart = SafeCast.toUint48(snapshot); + round.voteDuration = SafeCast.toUint32(duration); + + emit RoundCreated(roundId, proposer, snapshot, snapshot + duration, apps); + + // Using a named return variable to avoid stack too deep errors + } + + // ------- Getters ------- // + + /** + * @dev Get the data of a round + */ + function getRound(uint256 roundId) external view returns (RoundCore memory) { + RoundsStorageStorage storage $ = _getRoundsStorageStorage(); + return $._rounds[roundId]; + } + + /** + * @dev Get the current round id + */ + function currentRoundId() public view virtual override returns (uint256) { + RoundsStorageStorage storage $ = _getRoundsStorageStorage(); + return $._roundCount; + } + + /** + * @dev Get the current round start block + */ + function currentRoundSnapshot() public view virtual override returns (uint256) { + return roundSnapshot(currentRoundId()); + } + + /** + * @dev Get the current round deadline block + */ + function currentRoundDeadline() public view virtual returns (uint256) { + return roundDeadline(currentRoundId()); + } + + /** + * @dev Get the start block of a round + */ + function roundSnapshot(uint256 roundId) public view virtual override returns (uint256) { + RoundsStorageStorage storage $ = _getRoundsStorageStorage(); + return $._rounds[roundId].voteStart; + } + + /** + * @dev Get the deadline block of a round + */ + function roundDeadline(uint256 roundId) public view virtual override returns (uint256) { + RoundsStorageStorage storage $ = _getRoundsStorageStorage(); + return $._rounds[roundId].voteStart + $._rounds[roundId].voteDuration; + } + + /** + * @dev Get the proposer of a round + */ + function roundProposer(uint256 roundId) public view virtual returns (address) { + RoundsStorageStorage storage $ = _getRoundsStorageStorage(); + return $._rounds[roundId].proposer; + } + + /** + * @dev Get the ids of the apps eligible for voting in a round + */ + function getAppIdsOfRound(uint256 roundId) public view override returns (bytes32[] memory) { + RoundsStorageStorage storage $ = _getRoundsStorageStorage(); + return $._appsEligibleForVoting[roundId]; + } + + /** + * @dev Get all the apps in the form of {App} eligible for voting in a round + * + * @notice This function could not be efficient with a large number of apps, in that case, use {getAppIdsOfRound} + * and then call {IX2EarnApps-app} for each app id + */ + function getAppsOfRound( + uint256 roundId + ) external view returns (X2EarnAppsDataTypes.AppWithDetailsReturnType[] memory) { + RoundsStorageStorage storage $ = _getRoundsStorageStorage(); + + bytes32[] memory appsInRound = $._appsEligibleForVoting[roundId]; + uint256 length = appsInRound.length; + X2EarnAppsDataTypes.AppWithDetailsReturnType[] memory allApps = new X2EarnAppsDataTypes.AppWithDetailsReturnType[]( + length + ); + + for (uint i; i < length; i++) { + allApps[i] = x2EarnApps().app(appsInRound[i]); + } + return allApps; + } +} diff --git a/contracts/deprecated/V1/x-allocation-voting-governance/modules/VotesQuorumFractionUpgradeableV1.sol b/contracts/deprecated/V1/x-allocation-voting-governance/modules/VotesQuorumFractionUpgradeableV1.sol new file mode 100644 index 0000000..159b11c --- /dev/null +++ b/contracts/deprecated/V1/x-allocation-voting-governance/modules/VotesQuorumFractionUpgradeableV1.sol @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: MIT + +// ####### +// ################ +// #################### +// ########### ######### +// ######### ######### +// ####### ######### ######### +// ######### ######### ########## +// ########## ######## #################### +// ########## ######### ######################### +// ################### ############################ +// ################# ########## ######## +// ############## ### ######## +// ############ ######### +// ########## ########## +// ######## ########### +// ### ############ +// ############## +// ################# +// ############## +// ######### + +pragma solidity 0.8.20; + +import { VotesUpgradeableV1 } from "./VotesUpgradeableV1.sol"; +import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import { Checkpoints } from "@openzeppelin/contracts/utils/structs/Checkpoints.sol"; +import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; + +/** + * @title VotesQuorumFractionUpgradeable + * @dev Extension of {XAllocationVotingGovernorV1} for voting weight extraction from an {ERC20Votes} token and a quorum expressed as a + * fraction of the total supply. + */ +abstract contract VotesQuorumFractionUpgradeableV1 is Initializable, VotesUpgradeableV1 { + using Checkpoints for Checkpoints.Trace208; + + /// @custom:storage-location erc7201:b3tr.storage.XAllocationVotingGovernorV1.VotesQuorumFraction + struct VotesQuorumFractionStorage { + Checkpoints.Trace208 _quorumNumeratorHistory; + } + + // keccak256(abi.encode(uint256(keccak256("b3tr.storage.XAllocationVotingGovernorV1.VotesQuorumFraction")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant VotesQuorumFractionStorageLocation = + 0x49d99284d013647f52e2a267fd5944583bd36be17443e784ec3e86bbd4c32400; + + function _getVotesQuorumFractionStorage() private pure returns (VotesQuorumFractionStorage storage $) { + assembly { + $.slot := VotesQuorumFractionStorageLocation + } + } + + event QuorumNumeratorUpdated(uint256 oldQuorumNumerator, uint256 newQuorumNumerator); + + /** + * @dev The quorum set is not a valid fraction. + */ + error GovernorInvalidQuorumFraction(uint256 quorumNumerator, uint256 quorumDenominator); + + /** + * @dev Initialize quorum as a fraction of the token's total supply. + * + * The fraction is specified as `numerator / denominator`. By default the denominator is 100, so quorum is + * specified as a percent: a numerator of 10 corresponds to quorum being 10% of total supply. The denominator can be + * customized by overriding {quorumDenominator}. + */ + function __VotesQuorumFraction_init(uint256 quorumNumeratorValue) internal onlyInitializing { + __VotesQuorumFraction_init_unchained(quorumNumeratorValue); + } + + function __VotesQuorumFraction_init_unchained(uint256 quorumNumeratorValue) internal onlyInitializing { + _updateQuorumNumerator(quorumNumeratorValue); + } + + /** + * @dev Returns the current quorum numerator. See {quorumDenominator}. + */ + function quorumNumerator() public view virtual returns (uint256) { + VotesQuorumFractionStorage storage $ = _getVotesQuorumFractionStorage(); + return $._quorumNumeratorHistory.latest(); + } + + /** + * @dev Returns the quorum numerator at a specific timepoint. See {quorumDenominator}. + */ + function quorumNumerator(uint256 timepoint) public view virtual returns (uint256) { + VotesQuorumFractionStorage storage $ = _getVotesQuorumFractionStorage(); + + uint256 length = $._quorumNumeratorHistory._checkpoints.length; + + // Optimistic search, check the latest checkpoint + Checkpoints.Checkpoint208 storage latest = $._quorumNumeratorHistory._checkpoints[length - 1]; + uint48 latestKey = latest._key; + uint208 latestValue = latest._value; + if (latestKey <= timepoint) { + return latestValue; + } + + // Otherwise, do the binary search + return $._quorumNumeratorHistory.upperLookupRecent(SafeCast.toUint48(timepoint)); + } + + /** + * @dev Returns the quorum denominator. Defaults to 100, but may be overridden. + */ + function quorumDenominator() public view virtual returns (uint256) { + return 100; + } + + /** + * @dev Returns the quorum for a timepoint, in terms of number of votes: `supply * numerator / denominator`. + */ + function quorum(uint256 timepoint) public view virtual override returns (uint256) { + return (token().getPastTotalSupply(timepoint) * quorumNumerator(timepoint)) / quorumDenominator(); + } + + /** + * @dev Changes the quorum numerator. + * + * Emits a {QuorumNumeratorUpdated} event. + * + * Requirements: + * + * - New numerator must be smaller or equal to the denominator. + */ + function updateQuorumNumerator(uint256 newQuorumNumerator) public virtual { + _updateQuorumNumerator(newQuorumNumerator); + } + + /** + * @dev Changes the quorum numerator. + * + * Emits a {QuorumNumeratorUpdated} event. + * + * Requirements: + * + * - New numerator must be smaller or equal to the denominator. + */ + function _updateQuorumNumerator(uint256 newQuorumNumerator) internal virtual { + uint256 denominator = quorumDenominator(); + if (newQuorumNumerator > denominator) { + revert GovernorInvalidQuorumFraction(newQuorumNumerator, denominator); + } + + uint256 oldQuorumNumerator = quorumNumerator(); + + VotesQuorumFractionStorage storage $ = _getVotesQuorumFractionStorage(); + $._quorumNumeratorHistory.push(clock(), SafeCast.toUint208(newQuorumNumerator)); + + emit QuorumNumeratorUpdated(oldQuorumNumerator, newQuorumNumerator); + } +} diff --git a/contracts/deprecated/V1/x-allocation-voting-governance/modules/VotesUpgradeableV1.sol b/contracts/deprecated/V1/x-allocation-voting-governance/modules/VotesUpgradeableV1.sol new file mode 100644 index 0000000..636ea1b --- /dev/null +++ b/contracts/deprecated/V1/x-allocation-voting-governance/modules/VotesUpgradeableV1.sol @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: MIT + +// ####### +// ################ +// #################### +// ########### ######### +// ######### ######### +// ####### ######### ######### +// ######### ######### ########## +// ########## ######## #################### +// ########## ######### ######################### +// ################### ############################ +// ################# ########## ######## +// ############## ### ######## +// ############ ######### +// ########## ########## +// ######## ########### +// ### ############ +// ############## +// ################# +// ############## +// ######### + +pragma solidity 0.8.20; + +import { XAllocationVotingGovernorV1 } from "../XAllocationVotingGovernorV1.sol"; +import { IVotes } from "@openzeppelin/contracts/governance/utils/IVotes.sol"; +import { IERC5805 } from "@openzeppelin/contracts/interfaces/IERC5805.sol"; +import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import { Time } from "@openzeppelin/contracts/utils/types/Time.sol"; +import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; + +/** + * @title VotesUpgradeable + * @dev Extension of {XAllocationVotingGovernorV1} for voting weight extraction from an {ERC20Votes} token, or since v4.5 an {ERC721Votes} + * token. + */ +abstract contract VotesUpgradeableV1 is Initializable, XAllocationVotingGovernorV1 { + /// @custom:storage-location erc7201:b3tr.storage.XAllocationVotingGovernorV1.VotesUpgradeable + struct VotesStorage { + IERC5805 _token; + } + + // keccak256(abi.encode(uint256(keccak256("b3tr.storage.XAllocationVotingGovernorV1.VotesUpgradeable")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant VotesStorageLocation = 0x6eb1bf0a160cdf1b5e63f5e5c6b310f6c2542cd9e2a47ff1bc977c526dfab500; + + function _getVotesStorage() private pure returns (VotesStorage storage $) { + assembly { + $.slot := VotesStorageLocation + } + } + + function __Votes_init(IVotes tokenAddress) internal onlyInitializing { + __Votes_init_unchained(tokenAddress); + } + + function __Votes_init_unchained(IVotes tokenAddress) internal onlyInitializing { + VotesStorage storage $ = _getVotesStorage(); + $._token = IERC5805(address(tokenAddress)); + } + + /** + * @dev The token that voting power is sourced from. + */ + function token() public view virtual returns (IERC5805) { + VotesStorage storage $ = _getVotesStorage(); + return $._token; + } + + /** + * @dev Clock (as specified in EIP-6372) is set to match the token's clock. Fallback to block numbers if the token + * does not implement EIP-6372. + */ + function clock() public view virtual override returns (uint48) { + try token().clock() returns (uint48 timepoint) { + return timepoint; + } catch { + return Time.blockNumber(); + } + } + + /** + * @dev Machine-readable description of the clock as specified in EIP-6372. + */ + // solhint-disable-next-line func-name-mixedcase + function CLOCK_MODE() public view virtual override returns (string memory) { + try token().CLOCK_MODE() returns (string memory clockmode) { + return clockmode; + } catch { + return "mode=blocknumber&from=default"; + } + } + + /** + * Read the voting weight from the token's built in snapshot mechanism (see {Governor-_getVotes}). + */ + function _getVotes( + address account, + uint256 timepoint, + bytes memory /*params*/ + ) internal view virtual override returns (uint256) { + return token().getPastVotes(account, timepoint); + } +} diff --git a/contracts/deprecated/V1/x-allocation-voting-governance/modules/VotingSettingsUpgradeableV1.sol b/contracts/deprecated/V1/x-allocation-voting-governance/modules/VotingSettingsUpgradeableV1.sol new file mode 100644 index 0000000..3191565 --- /dev/null +++ b/contracts/deprecated/V1/x-allocation-voting-governance/modules/VotingSettingsUpgradeableV1.sol @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: MIT + +// ####### +// ################ +// #################### +// ########### ######### +// ######### ######### +// ####### ######### ######### +// ######### ######### ########## +// ########## ######## #################### +// ########## ######### ######################### +// ################### ############################ +// ################# ########## ######## +// ############## ### ######## +// ############ ######### +// ########## ########## +// ######## ########### +// ### ############ +// ############## +// ################# +// ############## +// ######### + +pragma solidity 0.8.20; + +import { XAllocationVotingGovernorV1 } from "../XAllocationVotingGovernorV1.sol"; +import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; + +/** + * @title VotingSettingsUpgradeable + * @dev Extension of {XAllocationVotingGovernorV1} for voting settings. + */ +abstract contract VotingSettingsUpgradeableV1 is Initializable, XAllocationVotingGovernorV1 { + /// @custom:storage-location erc7201:b3tr.storage.XAllocationVotingGovernorV1.VotingSettings + struct VotingSettingsStorage { + uint32 _votingPeriod; + } + + // keccak256(abi.encode(uint256(keccak256("b3tr.storage.XAllocationVotingGovernorV1.VotingSettings")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant VotingSettingsStorageLocation = + 0xd69d068053671881d25a4d751dcad1e692749d9b24184f608cb1d01af3a99900; + + function _getVotingSettingsStorage() private pure returns (VotingSettingsStorage storage $) { + assembly { + $.slot := VotingSettingsStorageLocation + } + } + + event VotingPeriodSet(uint256 oldVotingPeriod, uint256 newVotingPeriod); + + /** + * @dev Initialize the governance parameters. + */ + function __VotingSettings_init(uint32 initialVotingPeriod) internal onlyInitializing { + __VotingSettings_init_unchained(initialVotingPeriod); + } + + function __VotingSettings_init_unchained(uint32 initialVotingPeriod) internal onlyInitializing { + _setVotingPeriod(initialVotingPeriod); + } + + /** + * @dev See {IXAllocationVotingGovernor-votingPeriod}. + */ + function votingPeriod() public view virtual override returns (uint256) { + VotingSettingsStorage storage $ = _getVotingSettingsStorage(); + return $._votingPeriod; + } + + /** + * @dev Internal setter for the voting period. + * + * Emits a {VotingPeriodSet} event. + */ + function _setVotingPeriod(uint32 newVotingPeriod) internal virtual { + if (newVotingPeriod == 0) { + revert GovernorInvalidVotingPeriod(0); + } + + // Ensure the voting period is less than the emissions cycle duration. + uint256 emissionsCycleDuration = emissions().cycleDuration(); + if (newVotingPeriod >= emissionsCycleDuration) { + revert GovernorInvalidVotingPeriod(newVotingPeriod); + } + + VotingSettingsStorage storage $ = _getVotingSettingsStorage(); + + emit VotingPeriodSet($._votingPeriod, newVotingPeriod); + $._votingPeriod = newVotingPeriod; + } +} diff --git a/contracts/deprecated/V2/X2EarnRewardsPoolV2.sol b/contracts/deprecated/V2/X2EarnRewardsPoolV2.sol new file mode 100644 index 0000000..b2ff884 --- /dev/null +++ b/contracts/deprecated/V2/X2EarnRewardsPoolV2.sol @@ -0,0 +1,529 @@ +// SPDX-License-Identifier: MIT + +// ####### +// ################ +// #################### +// ########### ######### +// ######### ######### +// ####### ######### ######### +// ######### ######### ########## +// ########## ######## #################### +// ########## ######### ######################### +// ################### ############################ +// ################# ########## ######## +// ############## ### ######## +// ############ ######### +// ########## ########## +// ######## ########### +// ### ############ +// ############## +// ################# +// ############## +// ######### + +pragma solidity 0.8.20; + +import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import { AccessControlUpgradeable } from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; +import { IB3TR } from "../../interfaces/IB3TR.sol"; +import { IX2EarnAppsV1 } from "../V1/interfaces/IX2EarnAppsV1.sol"; +import { IX2EarnRewardsPoolV2 } from "./interfaces/IX2EarnRewardsPoolV2.sol"; +import { IERC1155Receiver } from "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol"; +import { IERC721Receiver } from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; +import "@openzeppelin/contracts/utils/Strings.sol"; + +/** + * @title X2EarnRewardsPool + * @dev This contract is used by x2Earn apps to reward users that performed sustainable actions. + * The XAllocationPool contract or other contracts/users can deposit funds into this contract by specifying the app + * that can access the funds. + * Admins of x2EarnApps can withdraw funds from the rewards pool, whihch are sent to the team wallet. + * Reward distributors of a x2Earn app can distribute rewards to users that performed sustainable actions or withdraw funds + * to the team wallet. + * The contract is upgradable through the UUPS proxy pattern and UPGRADER_ROLE can authorize the upgrade. + */ +contract X2EarnRewardsPoolV2 is + IX2EarnRewardsPoolV2, + UUPSUpgradeable, + AccessControlUpgradeable, + ReentrancyGuardUpgradeable +{ + bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE"); + bytes32 public constant CONTRACTS_ADDRESS_MANAGER_ROLE = keccak256("CONTRACTS_ADDRESS_MANAGER_ROLE"); + bytes32 public constant IMPACT_KEY_MANAGER_ROLE = keccak256("IMPACT_KEY_MANAGER_ROLE"); + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + /// @custom:storage-location erc7201:b3tr.storage.X2EarnRewardsPool + struct X2EarnRewardsPoolStorage { + IB3TR b3tr; + IX2EarnAppsV1 x2EarnApps; + mapping(bytes32 appId => uint256) availableFunds; // Funds that the app can use to reward users + mapping(string => uint256) impactKeyIndex; // Mapping from impact key to its index (1-based to distinguish from non-existent) + string[] allowedImpactKeys; // Array storing impact keys + } + + // keccak256(abi.encode(uint256(keccak256("b3tr.storage.X2EarnRewardsPool")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant X2EarnRewardsPoolStorageLocation = + 0x7c0dcc5654efea34bf150fefe2d7f927494d4026026590e81037cb4c7a9cdc00; + + function _getX2EarnRewardsPoolStorage() private pure returns (X2EarnRewardsPoolStorage storage $) { + assembly { + $.slot := X2EarnRewardsPoolStorageLocation + } + } + + function initialize( + address _admin, + address _contractsManagerAdmin, + address _upgrader, + IB3TR _b3tr, + IX2EarnAppsV1 _x2EarnApps + ) external initializer { + require(_admin != address(0), "X2EarnRewardsPool: admin is the zero address"); + require(_contractsManagerAdmin != address(0), "X2EarnRewardsPool: contracts manager admin is the zero address"); + require(_upgrader != address(0), "X2EarnRewardsPool: upgrader is the zero address"); + require(address(_b3tr) != address(0), "X2EarnRewardsPool: b3tr is the zero address"); + require(address(_x2EarnApps) != address(0), "X2EarnRewardsPool: x2EarnApps is the zero address"); + + __UUPSUpgradeable_init(); + __AccessControl_init(); + __ReentrancyGuard_init(); + + _grantRole(DEFAULT_ADMIN_ROLE, _admin); + _grantRole(UPGRADER_ROLE, _upgrader); + _grantRole(CONTRACTS_ADDRESS_MANAGER_ROLE, _contractsManagerAdmin); + + X2EarnRewardsPoolStorage storage $ = _getX2EarnRewardsPoolStorage(); + $.b3tr = _b3tr; + $.x2EarnApps = _x2EarnApps; + } + + function initializeV2(address _impactKeyManager, string[] memory _initialImpactKeys) external reinitializer(2) { + require(_impactKeyManager != address(0), "X2EarnRewardsPool: impactKeyManager is the zero address"); + require(_initialImpactKeys.length > 0, "X2EarnRewardsPool: initialImpactKeys is empty"); + + _grantRole(IMPACT_KEY_MANAGER_ROLE, _impactKeyManager); + + X2EarnRewardsPoolStorage storage $ = _getX2EarnRewardsPoolStorage(); + + for (uint256 i; i < _initialImpactKeys.length; i++) { + _addImpactKey(_initialImpactKeys[i], $); + } + } + + // ---------- 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("X2EarnRewardsPool: sender is not an admin nor has the required role"); + } + _; + } + + // ---------- Authorizers ---------- // + + function _authorizeUpgrade(address newImplementation) internal virtual override onlyRole(UPGRADER_ROLE) {} + + // ---------- Setters ---------- // + + /** + * @dev See {IX2EarnRewardsPoolV1-deposit} + */ + function deposit(uint256 amount, bytes32 appId) external returns (bool) { + X2EarnRewardsPoolStorage storage $ = _getX2EarnRewardsPoolStorage(); + + // check that app exists + require($.x2EarnApps.appExists(appId), "X2EarnRewardsPool: app does not exist"); + + // increase available amount for the app + $.availableFunds[appId] += amount; + + // transfer tokens to this contract + require($.b3tr.transferFrom(msg.sender, address(this), amount), "X2EarnRewardsPool: deposit transfer failed"); + + emit NewDeposit(amount, appId, msg.sender); + + return true; + } + + /** + * @dev See {IX2EarnRewardsPoolV1-withdraw} + */ + function withdraw(uint256 amount, bytes32 appId, string memory reason) external nonReentrant { + X2EarnRewardsPoolStorage storage $ = _getX2EarnRewardsPoolStorage(); + + require($.x2EarnApps.appExists(appId), "X2EarnRewardsPool: app does not exist"); + + require( + $.x2EarnApps.isAppAdmin(appId, msg.sender) || $.x2EarnApps.isRewardDistributor(appId, msg.sender), + "X2EarnRewardsPool: not an app admin nor a reward distributor" + ); + + // check if the app has enough available funds to withdraw + require($.availableFunds[appId] >= amount, "X2EarnRewardsPool: app has insufficient funds"); + + // check if the contract has enough funds + require($.b3tr.balanceOf(address(this)) >= amount, "X2EarnRewardsPool: insufficient funds on contract"); + + // Get the team wallet address + address teamWalletAddress = $.x2EarnApps.teamWalletAddress(appId); + + // transfer the rewards to the team wallet + $.availableFunds[appId] -= amount; + require($.b3tr.transfer(teamWalletAddress, amount), "X2EarnRewardsPool: Allocation transfer to app failed"); + + emit TeamWithdrawal(amount, appId, teamWalletAddress, msg.sender, reason); + } + + /** + * @dev Distribute rewards to a user with a self provided proof. + * @notice This function is deprecated and kept for backwards compatibility, will be removed in future versions. + */ + function distributeRewardDeprecated(bytes32 appId, uint256 amount, address receiver, string memory proof) external { + // emit event with provided json proof + emit RewardDistributed(amount, appId, receiver, proof, msg.sender); + + _distributeReward(appId, amount, receiver); + } + + /** + * @dev {IX2EarnRewardsPoolV1-distributeReward} + * @notice the proof argument is unused but kept for backwards compatibility + */ + function distributeReward(bytes32 appId, uint256 amount, address receiver, string memory /*proof*/) external { + // emit event with empty proof + emit RewardDistributed(amount, appId, receiver, "", msg.sender); + + _distributeReward(appId, amount, receiver); + } + + /** + * @dev See {IX2EarnRewardsPoolV1-distributeRewardWithProof} + */ + function distributeRewardWithProof( + bytes32 appId, + uint256 amount, + address receiver, + string[] memory proofTypes, + string[] memory proofValues, + string[] memory impactCodes, + uint256[] memory impactValues, + string memory description + ) external { + _emitProof(appId, amount, receiver, proofTypes, proofValues, impactCodes, impactValues, description); + _distributeReward(appId, amount, receiver); + } + + /** + * @dev See {IX2EarnRewardsPoolV1-distributeReward} + * @notice The impact is an array of integers and codes that represent the impact of the action. + * Each index of the array represents a different impact. + * The codes are predefined and the values are the impact values. + * Example: ["carbon", "water", "energy"], [100, 200, 300] + */ + function _distributeReward(bytes32 appId, uint256 amount, address receiver) internal nonReentrant { + X2EarnRewardsPoolStorage storage $ = _getX2EarnRewardsPoolStorage(); + + // check authorization + require($.x2EarnApps.appExists(appId), "X2EarnRewardsPool: app does not exist"); + require($.x2EarnApps.isRewardDistributor(appId, msg.sender), "X2EarnRewardsPool: not a reward distributor"); + + // check if the app has enough available funds to distribute + require($.availableFunds[appId] >= amount, "X2EarnRewardsPool: app has insufficient funds"); + require($.b3tr.balanceOf(address(this)) >= amount, "X2EarnRewardsPool: insufficient funds on contract"); + + // Transfer the rewards to the receiver + $.availableFunds[appId] -= amount; + require($.b3tr.transfer(receiver, amount), "X2EarnRewardsPool: Allocation transfer to app failed"); + } + + /** + * @dev Emits the RewardDistributed event with the provided proofs and impacts. + */ + function _emitProof( + bytes32 appId, + uint256 amount, + address receiver, + string[] memory proofTypes, + string[] memory proofValues, + string[] memory impactCodes, + uint256[] memory impactValues, + string memory description + ) internal { + // Build the JSON proof string from the proof and impact data + string memory jsonProof = buildProof(proofTypes, proofValues, impactCodes, impactValues, description); + + // emit event + emit RewardDistributed(amount, appId, receiver, jsonProof, msg.sender); + } + + /** + * @dev see {IX2EarnRewardsPoolV1-buildProof} + */ + function buildProof( + string[] memory proofTypes, + string[] memory proofValues, + string[] memory impactCodes, + uint256[] memory impactValues, + string memory description + ) public view virtual returns (string memory) { + bool hasProof = proofTypes.length > 0 && proofValues.length > 0; + bool hasImpact = impactCodes.length > 0 && impactValues.length > 0; + bool hasDescription = bytes(description).length > 0; + + // If neither proof nor impact is provided, return an empty string + if (!hasProof && !hasImpact) { + return ""; + } + + // Initialize an empty JSON bytes array with version + bytes memory json = abi.encodePacked('{"version": 2'); + + // Add description if available + if (hasDescription) { + json = abi.encodePacked(json, ',"description": "', description, '"'); + } + + // Add proof if available + if (hasProof) { + bytes memory jsonProof = _buildProofJson(proofTypes, proofValues); + + json = abi.encodePacked(json, ',"proof": ', jsonProof); + } + + // Add impact if available + if (hasImpact) { + bytes memory jsonImpact = _buildImpactJson(impactCodes, impactValues); + + json = abi.encodePacked(json, ',"impact": ', jsonImpact); + } + + // Close the JSON object + json = abi.encodePacked(json, "}"); + + return string(json); + } + + /** + * @dev Builds the proof JSON string from the proof data. + * @param proofTypes the proof types + * @param proofValues the proof values + */ + function _buildProofJson( + string[] memory proofTypes, + string[] memory proofValues + ) internal pure returns (bytes memory) { + require(proofTypes.length == proofValues.length, "X2EarnRewardsPool: Mismatched input lengths for Proof"); + + bytes memory json = abi.encodePacked("{"); + + for (uint256 i; i < proofTypes.length; i++) { + if (_isValidProofType(proofTypes[i])) { + json = abi.encodePacked(json, '"', proofTypes[i], '":', '"', proofValues[i], '"'); + if (i < proofTypes.length - 1) { + json = abi.encodePacked(json, ","); + } + } else { + revert("X2EarnRewardsPool: Invalid proof type"); + } + } + + json = abi.encodePacked(json, "}"); + + return json; + } + + /** + * @dev Builds the impact JSON string from the impact data. + * + * @param impactCodes the impact codes + * @param impactValues the impact values + */ + function _buildImpactJson( + string[] memory impactCodes, + uint256[] memory impactValues + ) internal view returns (bytes memory) { + require(impactCodes.length == impactValues.length, "X2EarnRewardsPool: Mismatched input lengths for Impact"); + + bytes memory json = abi.encodePacked("{"); + + for (uint256 i; i < impactValues.length; i++) { + if (_isAllowedImpactKey(impactCodes[i])) { + json = abi.encodePacked(json, '"', impactCodes[i], '":', Strings.toString(impactValues[i])); + if (i < impactValues.length - 1) { + json = abi.encodePacked(json, ","); + } + } else { + revert("X2EarnRewardsPool: Invalid impact key"); + } + } + + json = abi.encodePacked(json, "}"); + + return json; + } + + /** + * @dev Checks if the key is allowed. + */ + function _isAllowedImpactKey(string memory key) internal view returns (bool) { + X2EarnRewardsPoolStorage storage $ = _getX2EarnRewardsPoolStorage(); + return $.impactKeyIndex[key] > 0; + } + + /** + * @dev Checks if the proof type is valid. + */ + function _isValidProofType(string memory proofType) internal pure returns (bool) { + return + keccak256(abi.encodePacked(proofType)) == keccak256(abi.encodePacked("image")) || + keccak256(abi.encodePacked(proofType)) == keccak256(abi.encodePacked("link")) || + keccak256(abi.encodePacked(proofType)) == keccak256(abi.encodePacked("text")) || + keccak256(abi.encodePacked(proofType)) == keccak256(abi.encodePacked("video")); + } + + /** + * @dev Sets the X2EarnApps contract address. + * + * @param _x2EarnApps the new X2EarnApps contract + */ + function setX2EarnApps(IX2EarnAppsV1 _x2EarnApps) external onlyRole(CONTRACTS_ADDRESS_MANAGER_ROLE) { + require(address(_x2EarnApps) != address(0), "X2EarnRewardsPool: x2EarnApps is the zero address"); + + X2EarnRewardsPoolStorage storage $ = _getX2EarnRewardsPoolStorage(); + $.x2EarnApps = _x2EarnApps; + } + + /** + * @dev Adds a new allowed impact key. + * @param newKey the new key to add + */ + function addImpactKey(string memory newKey) external onlyRoleOrAdmin(IMPACT_KEY_MANAGER_ROLE) { + X2EarnRewardsPoolStorage storage $ = _getX2EarnRewardsPoolStorage(); + _addImpactKey(newKey, $); + } + + /** + * @dev Internal function to add a new allowed impact key. + * @param key the new key to add + * @param $ the storage pointer + */ + function _addImpactKey(string memory key, X2EarnRewardsPoolStorage storage $) internal { + require($.impactKeyIndex[key] == 0, "X2EarnRewardsPool: Key already exists"); + $.allowedImpactKeys.push(key); + $.impactKeyIndex[key] = $.allowedImpactKeys.length; // Store 1-based index + } + + /** + * @dev Removes an allowed impact key. + * @param keyToRemove the key to remove + */ + function removeImpactKey(string memory keyToRemove) external onlyRoleOrAdmin(IMPACT_KEY_MANAGER_ROLE) { + X2EarnRewardsPoolStorage storage $ = _getX2EarnRewardsPoolStorage(); + uint256 index = $.impactKeyIndex[keyToRemove]; + require(index > 0, "X2EarnRewardsPool: Key not found"); + + // Move the last element into the place to delete + string memory lastKey = $.allowedImpactKeys[$.allowedImpactKeys.length - 1]; + $.allowedImpactKeys[index - 1] = lastKey; + $.impactKeyIndex[lastKey] = index; // Update the index of the last key + + // Remove the last element + $.allowedImpactKeys.pop(); + delete $.impactKeyIndex[keyToRemove]; + } + + // ---------- Getters ---------- // + + /** + * @dev See {IX2EarnRewardsPoolV1-availableFunds} + */ + function availableFunds(bytes32 appId) external view returns (uint256) { + X2EarnRewardsPoolStorage storage $ = _getX2EarnRewardsPoolStorage(); + return $.availableFunds[appId]; + } + + /** + * @dev See {IX2EarnRewardsPoolV1-version} + */ + function version() external pure virtual returns (string memory) { + return "2"; + } + + /** + * @dev Retrieves the B3TR token contract. + */ + function b3tr() external view returns (IB3TR) { + X2EarnRewardsPoolStorage storage $ = _getX2EarnRewardsPoolStorage(); + return $.b3tr; + } + + /** + * @dev Retrieves the X2EarnApps contract. + */ + function x2EarnApps() external view returns (IX2EarnAppsV1) { + X2EarnRewardsPoolStorage storage $ = _getX2EarnRewardsPoolStorage(); + return $.x2EarnApps; + } + + /** + * @dev Retrieves the allowed impact keys. + */ + function getAllowedImpactKeys() external view returns (string[] memory) { + X2EarnRewardsPoolStorage storage $ = _getX2EarnRewardsPoolStorage(); + return $.allowedImpactKeys; + } + + // ---------- Fallbacks ---------- // + + /** + * @dev Transfers of VET to this contract are not allowed. + */ + receive() external payable virtual { + revert("X2EarnRewardsPool: contract does not accept VET"); + } + + /** + * @dev Contract does not accept calls/data. + */ + fallback() external payable { + revert("X2EarnRewardsPool: contract does not accept calls/data"); + } + + /** + * @dev Transfers of ERC721 tokens to this contract are not allowed. + * + * @notice supported only when safeTransferFrom is used + */ + function onERC721Received(address, address, uint256, bytes memory) public virtual returns (bytes4) { + revert("X2EarnRewardsPool: contract does not accept ERC721 tokens"); + } + + /** + * @dev Transfers of ERC1155 tokens to this contract are not allowed. + */ + function onERC1155Received(address, address, uint256, uint256, bytes memory) public virtual returns (bytes4) { + revert("X2EarnRewardsPool: contract does not accept ERC1155 tokens"); + } + + /** + * @dev Transfers of ERC1155 tokens to this contract are not allowed. + */ + function onERC1155BatchReceived( + address, + address, + uint256[] memory, + uint256[] memory, + bytes memory + ) public virtual returns (bytes4) { + revert("X2EarnRewardsPool: contract does not accept batch transfers of ERC1155 tokens"); + } +} diff --git a/contracts/deprecated/V2/interfaces/IX2EarnRewardsPoolV2.sol b/contracts/deprecated/V2/interfaces/IX2EarnRewardsPoolV2.sol new file mode 100644 index 0000000..a5d7474 --- /dev/null +++ b/contracts/deprecated/V2/interfaces/IX2EarnRewardsPoolV2.sol @@ -0,0 +1,155 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.20; + +/** + * @title IX2EarnRewardsPool + * @dev Interface designed to be used by a contract that allows x2Earn apps to reward users that performed sustainable actions. + * Funds can be deposited into this contract by specifying the app id that can access the funds. + * Admins of x2EarnApps can withdraw funds from the rewards pool, whihc are sent to the team wallet. + */ +interface IX2EarnRewardsPoolV2 { + /** + * @dev Event emitted when a new deposit is made into the rewards pool. + * + * @param amount The amount of $B3TR deposited. + * @param appId The ID of the app for which the deposit was made. + * @param depositor The address of the user that deposited the funds. + */ + event NewDeposit(uint256 amount, bytes32 indexed appId, address indexed depositor); + + /** + * @dev Event emitted when a team withdraws funds from the rewards pool. + * + * @param amount The amount of $B3TR withdrawn. + * @param appId The ID of the app for which the withdrawal was made. + * @param teamWallet The address of the team wallet that received the funds. + * @param withdrawer The address of the user that withdrew the funds. + * @param reason The reason for the withdrawal. + */ + event TeamWithdrawal( + uint256 amount, + bytes32 indexed appId, + address indexed teamWallet, + address withdrawer, + string reason + ); + + /** + * @dev Event emitted when a reward is emitted by an app. + * + * @param amount The amount of $B3TR rewarded. + * @param appId The ID of the app that emitted the reward. + * @param receiver The address of the user that received the reward. + * @param proof The proof of the sustainable action that was performed. + * @param distributor The address of the user that distributed the reward. + */ + event RewardDistributed( + uint256 amount, + bytes32 indexed appId, + address indexed receiver, + string proof, + address indexed distributor + ); + + /** + * @dev Event emitted when the proof of sustainability external contract call fails. + * + * @param reason The reason for the failure. + * @param lowLevelData The low level data returned by the external contract. + */ + event RegisterActionFailed(string reason, bytes lowLevelData); + + /** + * @dev Retrieves the current version of the contract. + * + * @return The version of the contract. + */ + function version() external pure returns (string memory); + + /** + * @dev Function used by x2earn apps to deposit funds into the rewards pool. + * + * @param amount The amount of $B3TR to deposit. + * @param appId The ID of the app. + */ + function deposit(uint256 amount, bytes32 appId) external returns (bool); + + /** + * @dev Function used by x2earn apps to withdraw funds from the rewards pool. + * + * @param amount The amount of $B3TR to withdraw. + * @param appId The ID of the app. + * @param reason The reason for the withdrawal. + */ + function withdraw(uint256 amount, bytes32 appId, string memory reason) external; + + /** + * @dev Gets the amount of funds available for an app to reward users. + * + * @param appId The ID of the app. + */ + function availableFunds(bytes32 appId) external view returns (uint256); + + /** + * @dev Function used by x2earn apps to reward users that performed sustainable actions. + * + * @param appId the app id that is emitting the reward + * @param amount the amount of B3TR token the user is rewarded with + * @param receiver the address of the user that performed the sustainable action and is rewarded + * @param proof deprecated argument, pass an empty string instead + */ + function distributeReward(bytes32 appId, uint256 amount, address receiver, string memory proof) external; + + /** + * @dev Function used by x2earn apps to reward users that performed sustainable actions. + * @notice This function is depracted in favor of distributeRewardWithProof. + * + * @param appId the app id that is emitting the reward + * @param amount the amount of B3TR token the user is rewarded with + * @param receiver the address of the user that performed the sustainable action and is rewarded + * @param proof the JSON string that contains the proof and impact of the sustainable action + */ + function distributeRewardDeprecated(bytes32 appId, uint256 amount, address receiver, string memory proof) external; + + /** + * @dev Function used by x2earn apps to reward users that performed sustainable actions. + * + * @param appId the app id that is emitting the reward + * @param amount the amount of B3TR token the user is rewarded with + * @param receiver the address of the user that performed the sustainable action and is rewarded + * @param proofTypes the types of the proof of the sustainable action + * @param proofValues the values of the proof of the sustainable action + * @param impactCodes the codes of the impacts of the sustainable action + * @param impactValues the values of the impacts of the sustainable action + * @param description the description of the sustainable action + */ + function distributeRewardWithProof( + bytes32 appId, + uint256 amount, + address receiver, + string[] memory proofTypes, // link, image, video, text, etc. + string[] memory proofValues, // "https://...", "Qm...", etc., + string[] memory impactCodes, // carbon, water, etc. + uint256[] memory impactValues, // 100, 200, etc., + string memory description + ) external; + + /** + * @dev Builds the JSON proof string that will be stored + * on chain regarding the proofs, impacts and description of the sustainable action. + * + * @param proofTypes the types of the proof of the sustainable action + * @param proofValues the values of the proof of the sustainable action + * @param impactCodes the codes of the impacts of the sustainable action + * @param impactValues the values of the impacts of the sustainable action + * @param description the description of the sustainable action + */ + function buildProof( + string[] memory proofTypes, // link, photo, video, text, etc. + string[] memory proofValues, // "https://...", "Qm...", etc., + string[] memory impactCodes, // carbon, water, etc. + uint256[] memory impactValues, // 100, 200, etc., + string memory description + ) external returns (string memory); +} diff --git a/contracts/deprecated/V3/B3TRGovernorV3.sol b/contracts/deprecated/V3/B3TRGovernorV3.sol new file mode 100644 index 0000000..121aa3e --- /dev/null +++ b/contracts/deprecated/V3/B3TRGovernorV3.sol @@ -0,0 +1,977 @@ +//SPDX-License-Identifier: MIT + +// ####### +// ################ +// #################### +// ########### ######### +// ######### ######### +// ####### ######### ######### +// ######### ######### ########## +// ########## ######## #################### +// ########## ######### ######################### +// ################### ############################ +// ################# ########## ######## +// ############## ### ######## +// ############ ######### +// ########## ########## +// ######## ########### +// ### ############ +// ############## +// ################# +// ############## +// ######### + +pragma solidity 0.8.20; + +import { GovernorProposalLogicV3 } from "./governance/libraries/GovernorProposalLogicV3.sol"; +import { GovernorStateLogicV3 } from "./governance/libraries/GovernorStateLogicV3.sol"; +import { GovernorVotesLogicV3 } from "./governance/libraries/GovernorVotesLogicV3.sol"; +import { GovernorQuorumLogicV3 } from "./governance/libraries/GovernorQuorumLogicV3.sol"; +import { GovernorDepositLogicV3 } from "./governance/libraries/GovernorDepositLogicV3.sol"; +import { GovernorStorageTypesV3 } from "./governance/libraries/GovernorStorageTypesV3.sol"; +import { GovernorGovernanceLogicV3 } from "./governance/libraries/GovernorGovernanceLogicV3.sol"; +import { GovernorFunctionRestrictionsLogicV3 } from "./governance/libraries/GovernorFunctionRestrictionsLogicV3.sol"; +import { GovernorClockLogicV3 } from "./governance/libraries/GovernorClockLogicV3.sol"; +import { GovernorConfiguratorV3 } from "./governance/libraries/GovernorConfiguratorV3.sol"; +import { GovernorTypesV3 } from "./governance/libraries/GovernorTypesV3.sol"; +import { GovernorStorageV3 } from "./governance/GovernorStorageV3.sol"; +import { IVoterRewards } from "../../interfaces/IVoterRewards.sol"; +import { IVOT3 } from "../../interfaces/IVOT3.sol"; +import { IB3TR } from "../../interfaces/IB3TR.sol"; +import { IB3TRGovernor } from "./interfaces/IB3TRGovernor.sol"; +import { IXAllocationVotingGovernor } from "../../interfaces/IXAllocationVotingGovernor.sol"; +import { TimelockControllerUpgradeable } from "@openzeppelin/contracts-upgradeable/governance/TimelockControllerUpgradeable.sol"; +import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import { IERC1155Receiver } from "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol"; +import { IERC721Receiver } from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; +import { Address } from "@openzeppelin/contracts/utils/Address.sol"; +import "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; + +/** + * @title B3TRGovernor + * @notice This contract is the main governance contract for the VeBetterDAO ecosystem. + * Anyone can create a proposal to both change the state of the contract, to execute a transaction + * on the timelock or to ask for a vote from the community without performing any onchain action. + * In order for the proposal to become active, the community needs to deposit a certain amount of VOT3 tokens. + * This is used as a heat check for the proposal, and funds are returned to the depositors after vote is concluded. + * Votes for proposals start periodically, based on the allocation rounds (see xAllocationVoting contract), and the round + * in which the proposal should be active is specified by the proposer during the proposal creation. + * + * A minimum amount of voting power is required in order to vote on a proposal. + * The voting power is calculated through the quadratic vote formula based on the amount of VOT3 tokens held by the + * voter at the block when the proposal becomes active. + * + * Once a proposal succeeds, it can be queued and executed. The execution is done through the timelock contract. + * + * The contract is upgradeable and uses the UUPS pattern. + * @dev The contract is upgradeable and uses the UUPS pattern. All logic is stored in libraries. + * + * ------------------ VERSION 2 ------------------ + * - Replaced onlyGovernance modifier with onlyRoleOrGovernance which checks if the caller has the DEFAULT_ADMIN_ROLE role or if the function is called through a governance proposal + * ------------------ VERSION 3 ------------------ + * - Added the ability to toggle the quadratic voting mechanism on and off + */ +contract B3TRGovernorV3 is + IB3TRGovernor, + GovernorStorageV3, + AccessControlUpgradeable, + UUPSUpgradeable, + PausableUpgradeable +{ + /// @notice The role that can whitelist allowed functions in the propose function + bytes32 public constant GOVERNOR_FUNCTIONS_SETTINGS_ROLE = keccak256("GOVERNOR_FUNCTIONS_SETTINGS_ROLE"); + /// @notice The role that can pause the contract + bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); + /// @notice The role that can set external contracts addresses + bytes32 public constant CONTRACTS_ADDRESS_MANAGER_ROLE = keccak256("CONTRACTS_ADDRESS_MANAGER_ROLE"); + /// @notice The role that can execute a proposal + bytes32 public constant PROPOSAL_EXECUTOR_ROLE = keccak256("PROPOSAL_EXECUTOR_ROLE"); + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + /** + * @dev Restricts a function so it can only be executed through governance proposals. For example, governance + * parameter setters in {GovernorSettings} are protected using this modifier. + * + * The governance executing address may be different from the Governor's own address, for example it could be a + * timelock. This can be customized by modules by overriding {_executor}. The executor is only able to invoke these + * functions during the execution of the governor's {execute} function, and not under any other circumstances. Thus, + * for example, additional timelock proposers are not able to change governance parameters without going through the + * governance protocol (since v4.6). + */ + modifier onlyGovernance() { + GovernorStorageTypesV3.GovernorStorage storage $ = getGovernorStorage(); + GovernorGovernanceLogicV3.checkGovernance($, _msgSender(), _msgData(), address(this)); + _; + } + + /** + * @notice Modifier to check if the caller has the specified role or if the function is called through a governance proposal + * @param role The role to check against + */ + modifier onlyRoleOrGovernance(bytes32 role) { + GovernorStorageTypesV3.GovernorStorage storage $ = getGovernorStorage(); + if (!hasRole(role, _msgSender())) + GovernorGovernanceLogicV3.checkGovernance($, _msgSender(), _msgData(), address(this)); + _; + } + + /** + * @dev Modifier to make a function callable only by a certain role. In + * addition to checking the sender's role, `address(0)` 's role is also + * considered. Granting a role to `address(0)` is equivalent to enabling + * this role for everyone. + */ + modifier onlyRoleOrOpenRole(bytes32 role) { + if (!hasRole(role, address(0))) { + _checkRole(role, _msgSender()); + } + _; + } + + /** + * @notice Initializes the contract with the initial parameters + * @dev This function is called only once during the contract deployment + * @param data Initialization data containing the initial settings for the governor + */ + function initialize( + GovernorTypesV3.InitializationData memory data, + GovernorTypesV3.InitializationRolesData memory rolesData + ) external initializer { + __GovernorStorage_init(data, "B3TRGovernor"); + __AccessControl_init(); + __UUPSUpgradeable_init(); + __Pausable_init(); + + GovernorStorageTypesV3.GovernorStorage storage $ = getGovernorStorage(); + GovernorQuorumLogicV3.updateQuorumNumerator($, data.quorumPercentage); + + // Validate and set the governor external contracts storage + require(address(rolesData.governorAdmin) != address(0), "B3TRGovernor: governor admin address cannot be zero"); + _grantRole(DEFAULT_ADMIN_ROLE, rolesData.governorAdmin); + _grantRole(GOVERNOR_FUNCTIONS_SETTINGS_ROLE, rolesData.governorFunctionSettingsRoleAddress); + _grantRole(PAUSER_ROLE, rolesData.pauser); + _grantRole(CONTRACTS_ADDRESS_MANAGER_ROLE, rolesData.contractsAddressManager); + _grantRole(PROPOSAL_EXECUTOR_ROLE, rolesData.proposalExecutor); + } + + /** + * @dev Function to receive VET that will be handled by the governor (disabled if executor is a third party contract) + */ + receive() external payable virtual { + GovernorStorageTypesV3.GovernorStorage storage $ = getGovernorStorage(); + if (GovernorGovernanceLogicV3.executor($) != address(this)) { + revert GovernorDisabledDeposit(); + } + } + + /** + * @notice Relays a transaction or function call to an arbitrary target. In cases where the governance executor + * is some contract other than the governor itself, like when using a timelock, this function can be invoked + * in a governance proposal to recover tokens or Ether that was sent to the governor contract by mistake. + * Note that if the executor is simply the governor itself, use of `relay` is redundant. + * @param target The target address + * @param value The amount of ether to send + * @param data The data to call the target with + */ + function relay(address target, uint256 value, bytes calldata data) external payable virtual onlyRoleOrGovernance(DEFAULT_ADMIN_ROLE) { + (bool success, bytes memory returndata) = target.call{ value: value }(data); + Address.verifyCallResult(success, returndata); + } + + // ------------------ GETTERS ------------------ // + + /** + * @notice Function to know if a proposal is executable or not. + * If the proposal was created without any targets, values, or calldatas, it is not executable. + * to check if the proposal is executable. + * @dev If no calldatas or targets then it's not executable, otherwise it will check if the governance can execute transactions or not. + * @param proposalId The id of the proposal + * @return bool True if the proposal needs queuing, false otherwise + */ + function proposalNeedsQueuing(uint256 proposalId) external view returns (bool) { + GovernorStorageTypesV3.GovernorStorage storage $ = getGovernorStorage(); + return GovernorProposalLogicV3.proposalNeedsQueuing($, proposalId); + } + + /** + * @notice Returns the state of a proposal + * @param proposalId The id of the proposal + * @return GovernorTypesV3.ProposalState The state of the proposal + */ + function state(uint256 proposalId) external view returns (GovernorTypesV3.ProposalState) { + GovernorStorageTypesV3.GovernorStorage storage $ = getGovernorStorage(); + return GovernorStateLogicV3.state($, proposalId); + } + + /** + * @notice Check if the proposal can start in the next round + * @return bool True if the proposal can start in the next round, false otherwise + */ + function canProposalStartInNextRound() public view returns (bool) { + GovernorStorageTypesV3.GovernorStorage storage $ = getGovernorStorage(); + return GovernorProposalLogicV3.canProposalStartInNextRound($); + } + + /** + * @notice See {IB3TRGovernor-proposalProposer}. + * @param proposalId The id of the proposal + * @return address The address of the proposer + */ + function proposalProposer(uint256 proposalId) public view virtual returns (address) { + GovernorStorageTypesV3.GovernorStorage storage $ = getGovernorStorage(); + return GovernorProposalLogicV3.proposalProposer($, proposalId); + } + + /** + * @notice See {IB3TRGovernor-proposalEta}. + * @param proposalId The id of the proposal + * @return uint256 The ETA of the proposal + */ + function proposalEta(uint256 proposalId) public view virtual returns (uint256) { + GovernorStorageTypesV3.GovernorStorage storage $ = getGovernorStorage(); + return GovernorProposalLogicV3.proposalEta($, proposalId); + } + + /** + * @notice See {IB3TRGovernor-proposalStartRound} + * @param proposalId The id of the proposal + * @return uint256 The start round of the proposal + */ + function proposalStartRound(uint256 proposalId) public view returns (uint256) { + GovernorStorageTypesV3.GovernorStorage storage $ = getGovernorStorage(); + return GovernorProposalLogicV3.proposalStartRound($, proposalId); + } + + /** + * @notice See {IB3TRGovernor-proposalSnapshot}. + * We take for granted that the round starts the block after it ends. But it can happen that the round is not started yet for whatever reason. + * Knowing this, if the proposal starts 4 rounds in the future we need to consider also those extra blocks used to start the rounds. + * @param proposalId The id of the proposal + * @return uint256 The snapshot of the proposal + */ + function proposalSnapshot(uint256 proposalId) external view returns (uint256) { + GovernorStorageTypesV3.GovernorStorage storage $ = getGovernorStorage(); + return GovernorProposalLogicV3.proposalSnapshot($, proposalId); + } + + /** + * @notice See {IB3TRGovernor-proposalDeadline}. + * @param proposalId The id of the proposal + * @return uint256 The deadline of the proposal + */ + function proposalDeadline(uint256 proposalId) external view returns (uint256) { + GovernorStorageTypesV3.GovernorStorage storage $ = getGovernorStorage(); + return GovernorProposalLogicV3.proposalDeadline($, proposalId); + } + + /** + * @notice Returns the deposit threshold + * @return uint256 The deposit threshold + */ + function depositThreshold() external view returns (uint256) { + GovernorStorageTypesV3.GovernorStorage storage $ = getGovernorStorage(); + return GovernorDepositLogicV3.depositThreshold($); + } + + /** + * @notice See {Governor-depositThreshold}. + * @return uint256 The deposit threshold percentage + */ + function depositThresholdPercentage() external view returns (uint256) { + GovernorStorageTypesV3.GovernorStorage storage $ = getGovernorStorage(); + return $.depositThresholdPercentage; + } + + /** + * @notice See {Governor-votingThreshold}. + * @return uint256 The voting threshold + */ + function votingThreshold() external view returns (uint256) { + GovernorStorageTypesV3.GovernorStorage storage $ = getGovernorStorage(); + return $.votingThreshold; + } + + /** + * @notice See {IB3TRGovernor-getVotes}. + * @param account The address of the account + * @param timepoint The timepoint to get the votes at + * @return uint256 The number of votes + */ + function getVotes(address account, uint256 timepoint) external view returns (uint256) { + GovernorStorageTypesV3.GovernorStorage storage $ = getGovernorStorage(); + return GovernorVotesLogicV3.getVotes($, account, timepoint); + } + + /** + * @notice Returns the quadratic voting power that `account` has. See {IB3TRGovernor-getQuadraticVotingPower}. + * @param account The address of the account + * @param timepoint The timepoint to get the voting power at + * @return uint256 The quadratic voting power + */ + function getQuadraticVotingPower(address account, uint256 timepoint) external view returns (uint256) { + GovernorStorageTypesV3.GovernorStorage storage $ = getGovernorStorage(); + return GovernorVotesLogicV3.getQuadraticVotingPower($, account, timepoint); + } + + /** + * @notice Clock (as specified in EIP-6372) is set to match the token's clock. Fallback to block numbers if the token + * does not implement EIP-6372. + * @return uint48 The current clock time + */ + function clock() external view returns (uint48) { + GovernorStorageTypesV3.GovernorStorage storage $ = getGovernorStorage(); + return GovernorClockLogicV3.clock($); + } + + /** + * @notice Machine-readable description of the clock as specified in EIP-6372. + * @return string The clock mode + */ + // solhint-disable-next-line func-name-mixedcase + function CLOCK_MODE() external view returns (string memory) { + GovernorStorageTypesV3.GovernorStorage storage $ = getGovernorStorage(); + return GovernorClockLogicV3.CLOCK_MODE($); + } + + /** + * @notice The token that voting power is sourced from. + * @return IVOT3 The voting token + */ + function token() external view returns (IVOT3) { + GovernorStorageTypesV3.GovernorStorage storage $ = getGovernorStorage(); + return $.vot3; + } + + /** + * @notice Returns the quorum for a timepoint, in terms of number of votes: `supply * numerator / denominator`. + * @param blockNumber The block number to get the quorum for + * @return uint256 The quorum + */ + function quorum(uint256 blockNumber) external view returns (uint256) { + GovernorStorageTypesV3.GovernorStorage storage $ = getGovernorStorage(); + return GovernorQuorumLogicV3.quorum($, blockNumber); + } + + /** + * @notice Returns the current quorum numerator. See {quorumDenominator}. + * @return uint256 The current quorum numerator + */ + function quorumNumerator() external view returns (uint256) { + GovernorStorageTypesV3.GovernorStorage storage $ = getGovernorStorage(); + return GovernorQuorumLogicV3.quorumNumerator($); + } + + /** + * @notice Returns the quorum numerator at a specific timepoint using the GovernorQuorumFraction library. + * @param timepoint The timepoint to get the quorum numerator for + * @return uint256 The quorum numerator at the given timepoint + */ + function quorumNumerator(uint256 timepoint) external view returns (uint256) { + GovernorStorageTypesV3.GovernorStorage storage $ = getGovernorStorage(); + return GovernorQuorumLogicV3.quorumNumerator($, timepoint); + } + + /** + * @notice Returns the quorum denominator using the GovernorQuorumFraction library. Defaults to 100, but may be overridden. + * @return uint256 The quorum denominator + */ + function quorumDenominator() external pure returns (uint256) { + return GovernorQuorumLogicV3.quorumDenominator(); + } + + /** + * @notice Check if a function is restricted by the governor + * @param target The address of the contract + * @param functionSelector The function selector + * @return bool True if the function is whitelisted, false otherwise + */ + function isFunctionWhitelisted(address target, bytes4 functionSelector) external view returns (bool) { + GovernorStorageTypesV3.GovernorStorage storage $ = getGovernorStorage(); + return GovernorFunctionRestrictionsLogicV3.isFunctionWhitelisted($, target, functionSelector); + } + + /** + * @notice See {B3TRGovernor-minVotingDelay}. + * @return uint256 The minimum voting delay + */ + function minVotingDelay() external view returns (uint256) { + GovernorStorageTypesV3.GovernorStorage storage $ = getGovernorStorage(); + return $.minVotingDelay; + } + + /** + * @notice See {IB3TRGovernor-votingPeriod}. + * @return uint256 The voting period + */ + function votingPeriod() external view returns (uint256) { + GovernorStorageTypesV3.GovernorStorage storage $ = getGovernorStorage(); + return $.xAllocationVoting.votingPeriod(); + } + + /** + * @notice Check if a user has voted at least one time. + * @param user The address of the user to check if has voted at least one time + * @return bool True if the user has voted once, false otherwise + */ + function hasVotedOnce(address user) external view returns (bool) { + GovernorStorageTypesV3.GovernorStorage storage $ = getGovernorStorage(); + return GovernorVotesLogicV3.userVotedOnce($, user); + } + + /** + * @notice Returns if quorum was reached or not + * @param proposalId The id of the proposal + * @return bool True if quorum was reached, false otherwise + */ + function quorumReached(uint256 proposalId) external view returns (bool) { + GovernorStorageTypesV3.GovernorStorage storage $ = getGovernorStorage(); + return GovernorQuorumLogicV3.isQuorumReached($, proposalId); + } + + /** + * @notice Returns the total votes for a proposal + * @param proposalId The id of the proposal + * @return uint256 The total votes for the proposal + */ + function proposalTotalVotes(uint256 proposalId) external view returns (uint256) { + GovernorStorageTypesV3.GovernorStorage storage $ = getGovernorStorage(); + return $.proposalTotalVotes[proposalId]; + } + + /** + * @notice Accessor to the internal vote counts, in terms of vote power. + * @param proposalId The id of the proposal + * @return againstVotes The votes against the proposal + * @return forVotes The votes for the proposal + * @return abstainVotes The votes abstaining the proposal + */ + function proposalVotes( + uint256 proposalId + ) external view returns (uint256 againstVotes, uint256 forVotes, uint256 abstainVotes) { + GovernorStorageTypesV3.GovernorStorage storage $ = getGovernorStorage(); + return GovernorVotesLogicV3.getProposalVotes($, proposalId); + } + + /** + * @notice Returns the counting mode + * @return string The counting mode + */ + // solhint-disable-next-line func-name-mixedcase + function COUNTING_MODE() external pure returns (string memory) { + return "support=bravo&quorum=for,abstain,against"; + } + + /** + * @notice See {IB3TRGovernor-hasVoted}. + * @param proposalId The id of the proposal + * @param account The address of the account + * @return bool True if the account has voted, false otherwise + */ + function hasVoted(uint256 proposalId, address account) external view returns (bool) { + GovernorStorageTypesV3.GovernorStorage storage $ = getGovernorStorage(); + return GovernorVotesLogicV3.hasVoted($, proposalId, account); + } + + /** + * @notice Returns the amount of deposits made to a proposal. + * @param proposalId The id of the proposal. + * @return uint256 The amount of deposits + */ + function getProposalDeposits(uint256 proposalId) external view returns (uint256) { + GovernorStorageTypesV3.GovernorStorage storage $ = getGovernorStorage(); + return GovernorDepositLogicV3.getProposalDeposits($, proposalId); + } + + /** + * @notice Returns true if the threshold of deposits required to reach a proposal has been reached. + * @param proposalId The id of the proposal. + * @return bool True if the threshold is reached, false otherwise + */ + function proposalDepositReached(uint256 proposalId) external view returns (bool) { + GovernorStorageTypesV3.GovernorStorage storage $ = getGovernorStorage(); + return GovernorDepositLogicV3.proposalDepositReached($, proposalId); + } + + /** + * @notice Returns the deposit threshold for a proposal. + * @param proposalId The id of the proposal. + * @return uint256 The deposit threshold for the proposal. + */ + function proposalDepositThreshold(uint256 proposalId) external view returns (uint256) { + GovernorStorageTypesV3.GovernorStorage storage $ = getGovernorStorage(); + return GovernorDepositLogicV3.proposalDepositThreshold($, proposalId); + } + + /** + * @notice Public endpoint to retrieve the timelock id of a proposal. + * @param proposalId The id of the proposal + * @return bytes32 The timelock id + */ + function getTimelockId(uint256 proposalId) public view returns (bytes32) { + GovernorStorageTypesV3.GovernorStorage storage $ = getGovernorStorage(); + return GovernorProposalLogicV3.getTimelockId($, proposalId); + } + + /** + * @notice Returns the amount of tokens a specific user has deposited to a proposal. + * @param proposalId The id of the proposal. + * @param user The address of the user. + * @return uint256 The amount of tokens deposited by the user + */ + function getUserDeposit(uint256 proposalId, address user) external view returns (uint256) { + GovernorStorageTypesV3.GovernorStorage storage $ = getGovernorStorage(); + return GovernorDepositLogicV3.getUserDeposit($, proposalId, user); + } + + /** + * @notice See {IB3TRGovernor-name}. + * @return string The name of the governor + */ + function name() external view returns (string memory) { + GovernorStorageTypesV3.GovernorStorage storage $ = getGovernorStorage(); + return $.name; + } + + /** + * @notice Check if quadratic voting is disabled for the current round. + * @return true if quadratic voting is disabled, false otherwise. + */ + function isQuadraticVotingDisabledForCurrentRound() external view returns (bool) { + GovernorStorageTypesV3.GovernorStorage storage $ = getGovernorStorage(); + return GovernorVotesLogicV3.isQuadraticVotingDisabledForCurrentRound($); + } + + /** + * @notice Check if quadratic voting is disabled at a specific round. + * @param roundId - The round ID for which to check if quadratic voting is disabled. + * @return true if quadratic voting is disabled, false otherwise. + */ + function isQuadraticVotingDisabledForRound(uint256 roundId) external view returns (bool) { + GovernorStorageTypesV3.GovernorStorage storage $ = getGovernorStorage(); + return GovernorVotesLogicV3.isQuadraticVotingDisabledForRound($, roundId); + } + + /** + * @notice See {IB3TRGovernor-version}. + * @return string The version of the governor + */ + function version() external pure returns (string memory) { + return "3"; + } + + /** + * @notice See {IB3TRGovernor-hashProposal}. + * The proposal id is produced by hashing the ABI encoded `targets` array, the `values` array, the `calldatas` array + * and the descriptionHash (bytes32 which itself is the keccak256 hash of the description string). This proposal id + * can be produced from the proposal data which is part of the {ProposalCreated} event. It can even be computed in + * advance, before the proposal is submitted. + * Note that the chainId and the governor address are not part of the proposal id computation. Consequently, the + * same proposal (with same operation and same description) will have the same id if submitted on multiple governors + * across multiple networks. This also means that in order to execute the same operation twice (on the same + * governor) the proposer will have to change the description in order to avoid proposal id conflicts. + * @param targets The list of target addresses + * @param values The list of values to send + * @param calldatas The list of call data + * @param descriptionHash The hash of the description + * @return uint256 The proposal id + */ + function hashProposal( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) public pure returns (uint256) { + return GovernorProposalLogicV3.hashProposal(targets, values, calldatas, descriptionHash); + } + + /** + * @notice Public endpoint to get the salt used for the timelock operation. + * @param descriptionHash The hash of the description + * @return bytes32 The timelock salt + */ + function timelockSalt(bytes32 descriptionHash) external view returns (bytes32) { + return GovernorGovernanceLogicV3.timelockSalt(descriptionHash, address(this)); + } + + /** + * @notice The voter rewards contract. + * @return IVoterRewards The voter rewards contract + */ + function voterRewards() external view returns (IVoterRewards) { + GovernorStorageTypesV3.GovernorStorage storage $ = getGovernorStorage(); + return $.voterRewards; + } + + /** + * @notice The XAllocationVotingGovernor contract. + * @return IXAllocationVotingGovernor The XAllocationVotingGovernor contract + */ + function xAllocationVoting() external view returns (IXAllocationVotingGovernor) { + GovernorStorageTypesV3.GovernorStorage storage $ = getGovernorStorage(); + return $.xAllocationVoting; + } + + /** + * @notice See {B3TRGovernor-b3tr}. + * @return IB3TR The B3TR contract + */ + function b3tr() external view returns (IB3TR) { + GovernorStorageTypesV3.GovernorStorage storage $ = getGovernorStorage(); + return $.b3tr; + } + + /** + * @notice Public accessor to check the address of the timelock + * @return address The address of the timelock + */ + function timelock() external view virtual returns (address) { + GovernorStorageTypesV3.GovernorStorage storage $ = getGovernorStorage(); + return address($.timelock); + } + + // ------------------ SETTERS ------------------ // + + /** + * @notice Pause the contract + */ + function pause() external onlyRole(PAUSER_ROLE) { + _pause(); + } + + /** + * @notice Unpause the contract + */ + function unpause() external onlyRole(PAUSER_ROLE) { + _unpause(); + } + + /** + * @notice See {IB3TRGovernor-propose}. + * Callable only when contract is not paused. + * @param targets The list of target addresses + * @param values The list of values to send + * @param calldatas The list of call data + * @param description The proposal description + * @param startRoundId The round in which the proposal should start + * @param depositAmount The amount of deposit for the proposal + * @return uint256 The proposal id + */ + function propose( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + string memory description, + uint256 startRoundId, + uint256 depositAmount + ) external whenNotPaused returns (uint256) { + GovernorStorageTypesV3.GovernorStorage storage $ = getGovernorStorage(); + return GovernorProposalLogicV3.propose($, targets, values, calldatas, description, startRoundId, depositAmount); + } + + /** + * @notice See {IB3TRGovernor-queue}. + * Callable only when contract is not paused. + * @param targets The list of target addresses + * @param values The list of values to send + * @param calldatas The list of call data + * @param descriptionHash The hash of the description + * @return uint256 The proposal id + */ + function queue( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) external whenNotPaused returns (uint256) { + GovernorStorageTypesV3.GovernorStorage storage $ = getGovernorStorage(); + return GovernorProposalLogicV3.queue($, address(this), targets, values, calldatas, descriptionHash); + } + + /** + * @notice See {IB3TRGovernor-execute}. + * Callable only when contract is not paused. + * @param targets The list of target addresses + * @param values The list of values to send + * @param calldatas The list of call data + * @param descriptionHash The hash of the description + * @return uint256 The proposal id + */ + function execute( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) external payable whenNotPaused onlyRoleOrOpenRole(PROPOSAL_EXECUTOR_ROLE) returns (uint256) { + GovernorStorageTypesV3.GovernorStorage storage $ = getGovernorStorage(); + return GovernorProposalLogicV3.execute($, address(this), targets, values, calldatas, descriptionHash); + } + + /** + * @notice See {Governor-cancel}. + * @param targets The list of target addresses + * @param values The list of values to send + * @param calldatas The list of call data + * @param descriptionHash The hash of the description + * @return uint256 The proposal id + */ + function cancel( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) external returns (uint256) { + GovernorStorageTypesV3.GovernorStorage storage $ = getGovernorStorage(); + return + GovernorProposalLogicV3.cancel( + $, + _msgSender(), + hasRole(DEFAULT_ADMIN_ROLE, _msgSender()), + targets, + values, + calldatas, + descriptionHash + ); + } + + /** + * @notice See {IB3TRGovernor-castVote}. + * @param proposalId The id of the proposal + * @param support The support value (0 = against, 1 = for, 2 = abstain) + * @return uint256 The voting power + */ + function castVote(uint256 proposalId, uint8 support) external returns (uint256) { + GovernorStorageTypesV3.GovernorStorage storage $ = getGovernorStorage(); + return GovernorVotesLogicV3.castVote($, proposalId, _msgSender(), support, ""); + } + + /** + * @notice See {IB3TRGovernor-castVoteWithReason}. + * @param proposalId The id of the proposal + * @param support The support value (0 = against, 1 = for, 2 = abstain) + * @param reason The reason for the vote + * @return uint256 The voting power + */ + function castVoteWithReason(uint256 proposalId, uint8 support, string calldata reason) external returns (uint256) { + GovernorStorageTypesV3.GovernorStorage storage $ = getGovernorStorage(); + return GovernorVotesLogicV3.castVote($, proposalId, _msgSender(), support, reason); + } + + /** + * @notice Withdraws deposits for a specific proposal + * @param proposalId The id of the proposal + * @param depositor The address of the depositor + */ + function withdraw(uint256 proposalId, address depositor) external { + GovernorStorageTypesV3.GovernorStorage storage $ = getGovernorStorage(); + GovernorDepositLogicV3.withdraw($, proposalId, depositor); + } + + /** + * @notice Deposits tokens for a specific proposal + * @param amount The amount of tokens to deposit + * @param proposalId The id of the proposal + */ + function deposit(uint256 amount, uint256 proposalId) external { + GovernorStorageTypesV3.GovernorStorage storage $ = getGovernorStorage(); + GovernorDepositLogicV3.deposit($, amount, proposalId); + } + + /** + * @notice Changes the quorum numerator. + * This operation can only be performed through a governance proposal. + * Emits a {QuorumNumeratorUpdated} event. + * @param newQuorumNumerator The new quorum numerator + */ + function updateQuorumNumerator(uint256 newQuorumNumerator) external onlyRoleOrGovernance(DEFAULT_ADMIN_ROLE) { + GovernorStorageTypesV3.GovernorStorage storage $ = getGovernorStorage(); + GovernorQuorumLogicV3.updateQuorumNumerator($, newQuorumNumerator); + } + + /** + * @notice Toggle quadratic voting for next round. + * @dev This function toggles the state of quadratic votingstarting from the next round. + * The state will flip between enabled and disabled each time the function is called. + */ + function toggleQuadraticVoting() external onlyRoleOrGovernance(DEFAULT_ADMIN_ROLE) { + GovernorStorageTypesV3.GovernorStorage storage $ = getGovernorStorage(); + GovernorVotesLogicV3.toggleQuadraticVoting($); + } + + /** + * @notice Method that allows to restrict functions that can be called by proposals for a single function selector + * @param target The address of the contract + * @param functionSelector The function selector + * @param isWhitelisted Bool indicating if function is whitelisted for proposals + */ + function setWhitelistFunction( + address target, + bytes4 functionSelector, + bool isWhitelisted + ) public onlyRoleOrGovernance(GOVERNOR_FUNCTIONS_SETTINGS_ROLE) { + GovernorStorageTypesV3.GovernorStorage storage $ = getGovernorStorage(); + GovernorFunctionRestrictionsLogicV3.setWhitelistFunction($, target, functionSelector, isWhitelisted); + } + + /** + * @notice Method that allows to restrict functions that can be called by proposals for multiple function selectors at once + * @param target The address of the contract + * @param functionSelectors Array of function selectors + * @param isWhitelisted Bool indicating if function is whitelisted for proposals + */ + function setWhitelistFunctions( + address target, + bytes4[] memory functionSelectors, + bool isWhitelisted + ) public onlyRoleOrGovernance(GOVERNOR_FUNCTIONS_SETTINGS_ROLE) { + GovernorStorageTypesV3.GovernorStorage storage $ = getGovernorStorage(); + GovernorFunctionRestrictionsLogicV3.setWhitelistFunctions($, target, functionSelectors, isWhitelisted); + } + + /** + * @notice Method that allows to toggle the function restriction on/off + * @param isEnabled Flag to enable/disable function restriction + */ + function setIsFunctionRestrictionEnabled( + bool isEnabled + ) public onlyRoleOrGovernance(GOVERNOR_FUNCTIONS_SETTINGS_ROLE) { + GovernorStorageTypesV3.GovernorStorage storage $ = getGovernorStorage(); + GovernorFunctionRestrictionsLogicV3.setIsFunctionRestrictionEnabled($, isEnabled); + } + + /** + * @notice Update the deposit threshold. This operation can only be performed through a governance proposal. + * Emits a {DepositThresholdSet} event. + * @param newDepositThreshold The new deposit threshold + */ + function setDepositThresholdPercentage(uint256 newDepositThreshold) public onlyRoleOrGovernance(DEFAULT_ADMIN_ROLE) { + GovernorStorageTypesV3.GovernorStorage storage $ = getGovernorStorage(); + GovernorConfiguratorV3.setDepositThresholdPercentage($, newDepositThreshold); + } + + /** + * @notice Update the voting threshold. This operation can only be performed through a governance proposal. + * Emits a {VotingThresholdSet} event. + * @param newVotingThreshold The new voting threshold + */ + function setVotingThreshold(uint256 newVotingThreshold) public onlyRoleOrGovernance(DEFAULT_ADMIN_ROLE) { + GovernorStorageTypesV3.GovernorStorage storage $ = getGovernorStorage(); + GovernorConfiguratorV3.setVotingThreshold($, newVotingThreshold); + } + + /** + * @notice Update the min voting delay before vote can start. + * This operation can only be performed through a governance proposal. + * Emits a {MinVotingDelaySet} event. + * @param newMinVotingDelay The new minimum voting delay + */ + function setMinVotingDelay(uint256 newMinVotingDelay) public onlyRoleOrGovernance(DEFAULT_ADMIN_ROLE) { + GovernorStorageTypesV3.GovernorStorage storage $ = getGovernorStorage(); + GovernorConfiguratorV3.setMinVotingDelay($, newMinVotingDelay); + } + + /** + * @notice Set the voter rewards contract + * This function is only callable through governance proposals or by the CONTRACTS_ADDRESS_MANAGER_ROLE + * @param newVoterRewards The new voter rewards contract + */ + function setVoterRewards(IVoterRewards newVoterRewards) public onlyRoleOrGovernance(CONTRACTS_ADDRESS_MANAGER_ROLE) { + GovernorStorageTypesV3.GovernorStorage storage $ = getGovernorStorage(); + GovernorConfiguratorV3.setVoterRewards($, newVoterRewards); + } + + /** + * @notice Set the xAllocationVoting contract + * This function is only callable through governance proposals or by the CONTRACTS_ADDRESS_MANAGER_ROLE + * @param newXAllocationVoting The new xAllocationVoting contract + */ + function setXAllocationVoting( + IXAllocationVotingGovernor newXAllocationVoting + ) public onlyRoleOrGovernance(CONTRACTS_ADDRESS_MANAGER_ROLE) { + GovernorStorageTypesV3.GovernorStorage storage $ = getGovernorStorage(); + GovernorConfiguratorV3.setXAllocationVoting($, newXAllocationVoting); + } + + /** + * @notice Public endpoint to update the underlying timelock instance. Restricted to the timelock itself, so updates + * must be proposed, scheduled, and executed through governance proposals. + * CAUTION: It is not recommended to change the timelock while there are other queued governance proposals. + * @param newTimelock The new timelock controller + */ + function updateTimelock(TimelockControllerUpgradeable newTimelock) external virtual onlyRoleOrGovernance(DEFAULT_ADMIN_ROLE) { + GovernorStorageTypesV3.GovernorStorage storage $ = getGovernorStorage(); + GovernorConfiguratorV3.updateTimelock($, newTimelock); + } + + // ------------------ Overrides ------------------ // + + /** + * @notice Authorizes upgrade to a new implementation + * @param newImplementation The address of the new implementation + */ + function _authorizeUpgrade(address newImplementation) internal override onlyRoleOrGovernance(DEFAULT_ADMIN_ROLE) {} + + /** + * @notice Checks if the contract supports a specific interface + * @param interfaceId The interface id to check + * @return bool True if the interface is supported, false otherwise + */ + function supportsInterface( + bytes4 interfaceId + ) public pure override(IERC165, AccessControlUpgradeable) returns (bool) { + return + interfaceId == type(IB3TRGovernor).interfaceId || + interfaceId == type(IERC1155Receiver).interfaceId || + interfaceId == type(IERC165).interfaceId; + } + + /** + * @notice See {IERC1155Receiver-onERC1155Received}. + * Receiving tokens is disabled if the governance executor is other than the governor itself (eg. when using with a timelock). + * @return bytes4 The selector of the function + */ + function onERC1155Received(address, address, uint256, uint256, bytes memory) public virtual returns (bytes4) { + GovernorStorageTypesV3.GovernorStorage storage $ = getGovernorStorage(); + if (GovernorGovernanceLogicV3.executor($) != address(this)) { + revert GovernorDisabledDeposit(); + } + return this.onERC1155Received.selector; + } + + /** + * @notice See {IERC721Receiver-onERC721Received}. + * Receiving tokens is disabled if the governance executor is other than the governor itself (eg. when using with a timelock). + * @return bytes4 The selector of the function + */ + function onERC721Received(address, address, uint256, bytes memory) public virtual returns (bytes4) { + GovernorStorageTypesV3.GovernorStorage storage $ = getGovernorStorage(); + if (GovernorGovernanceLogicV3.executor($) != address(this)) { + revert GovernorDisabledDeposit(); + } + return this.onERC721Received.selector; + } + + /** + * @notice See {IERC1155Receiver-onERC1155BatchReceived}. + * Receiving tokens is disabled if the governance executor is other than the governor itself (eg. when using with a timelock). + * @return bytes4 The selector of the function + */ + function onERC1155BatchReceived( + address, + address, + uint256[] memory, + uint256[] memory, + bytes memory + ) public virtual returns (bytes4) { + GovernorStorageTypesV3.GovernorStorage storage $ = getGovernorStorage(); + if (GovernorGovernanceLogicV3.executor($) != address(this)) { + revert GovernorDisabledDeposit(); + } + return this.onERC1155BatchReceived.selector; + } +} diff --git a/contracts/deprecated/V3/governance/GovernorStorageV3.sol b/contracts/deprecated/V3/governance/GovernorStorageV3.sol new file mode 100644 index 0000000..c0a9835 --- /dev/null +++ b/contracts/deprecated/V3/governance/GovernorStorageV3.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: MIT + +// ####### +// ################ +// #################### +// ########### ######### +// ######### ######### +// ####### ######### ######### +// ######### ######### ########## +// ########## ######## #################### +// ########## ######### ######################### +// ################### ############################ +// ################# ########## ######## +// ############## ### ######## +// ############ ######### +// ########## ########## +// ######## ########### +// ### ############ +// ############## +// ################# +// ############## +// ######### + +pragma solidity 0.8.20; + +import { GovernorStorageTypesV3 } from "./libraries/GovernorStorageTypesV3.sol"; +import { GovernorTypesV3 } from "./libraries/GovernorTypesV3.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; + +/// @title GovernorStorage +/// @notice Contract used as storage of the B3TRGovernor contract. +/// @dev It defines the storage layout of the B3TRGovernor contract. +contract GovernorStorageV3 is Initializable { + // keccak256(abi.encode(uint256(keccak256("GovernorStorageLocation")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant GovernorStorageLocation = 0xd09a0aaf4ab3087bae7fa25ef74ddd4e5a4950980903ce417e66228cf7dc7b00; + + /// @dev Internal function to access the governor storage slot. + function getGovernorStorage() internal pure returns (GovernorStorageTypesV3.GovernorStorage storage $) { + assembly { + $.slot := GovernorStorageLocation + } + } + + /// @dev Initializes the governor storage + function __GovernorStorage_init( + GovernorTypesV3.InitializationData memory initializationData, + string memory governorName + ) internal onlyInitializing { + __GovernorStorage_init_unchained(initializationData, governorName); + } + + /// @dev Part of the initialization process that configures the gGovernorTypesovernor storage. + function __GovernorStorage_init_unchained( + GovernorTypesV3.InitializationData memory initializationData, + string memory governorName + ) internal onlyInitializing { + GovernorStorageTypesV3.GovernorStorage storage governorStorage = getGovernorStorage(); + + // Validate and set the governor time lock storage + require(address(initializationData.timelock) != address(0), "B3TRGovernor: timelock address cannot be zero"); + governorStorage.timelock = initializationData.timelock; + + // Set the governor function restrictions storage + governorStorage.isFunctionRestrictionEnabled = initializationData.isFunctionRestrictionEnabled; + + // Validate and set the governor external contracts storage + require(address(initializationData.b3tr) != address(0), "B3TRGovernor: B3TR address cannot be zero"); + require(address(initializationData.vot3Token) != address(0), "B3TRGovernor: Vot3 address cannot be zero"); + require( + address(initializationData.xAllocationVoting) != address(0), + "B3TRGovernor: xAllocationVoting address cannot be zero" + ); + require( + address(initializationData.voterRewards) != address(0), + "B3TRGovernor: voterRewards address cannot be zero" + ); + governorStorage.voterRewards = initializationData.voterRewards; + governorStorage.xAllocationVoting = initializationData.xAllocationVoting; + governorStorage.b3tr = initializationData.b3tr; + governorStorage.vot3 = initializationData.vot3Token; + + // Set the governor general storage + governorStorage.name = governorName; + governorStorage.minVotingDelay = initializationData.initialMinVotingDelay; + + // Set the governor deposit storage + governorStorage.depositThresholdPercentage = initializationData.initialDepositThreshold; + + // Set the governor votes storage + governorStorage.votingThreshold = initializationData.initialVotingThreshold; + } +} diff --git a/contracts/deprecated/V3/governance/libraries/GovernorClockLogicV3.sol b/contracts/deprecated/V3/governance/libraries/GovernorClockLogicV3.sol new file mode 100644 index 0000000..bdd9fef --- /dev/null +++ b/contracts/deprecated/V3/governance/libraries/GovernorClockLogicV3.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: MIT + +// ####### +// ################ +// #################### +// ########### ######### +// ######### ######### +// ####### ######### ######### +// ######### ######### ########## +// ########## ######## #################### +// ########## ######### ######################### +// ################### ############################ +// ################# ########## ######## +// ############## ### ######## +// ############ ######### +// ########## ########## +// ######## ########### +// ### ############ +// ############## +// ################# +// ############## +// ######### + +pragma solidity 0.8.20; + +import { GovernorStorageTypesV3 } from "./GovernorStorageTypesV3.sol"; +import { IVOT3 } from "../../../../interfaces/IVOT3.sol"; +import { Time } from "@openzeppelin/contracts/utils/types/Time.sol"; + +/// @title GovernorClockLogic Library +/// @notice Library for managing the clock logic as specified in EIP-6372, with fallback to block numbers. +/// @dev This library interacts with the IVOT3 interface to get the clock time or mode. +library GovernorClockLogicV3 { + /** + * @notice Returns the current timepoint from the token's clock, falling back to the current block number if the token does not implement EIP-6372. + * @dev Tries to get the timepoint from the vot3 clock. If it fails, it returns the current block number. + * @param self The storage reference for the GovernorStorage. + * @return The current timepoint or block number. + */ + function clock(GovernorStorageTypesV3.GovernorStorage storage self) external view returns (uint48) { + try self.vot3.clock() returns (uint48 timepoint) { + return timepoint; + } catch { + return Time.blockNumber(); + } + } + + /** + * @notice Returns the machine-readable description of the clock mode as specified in EIP-6372. + * @dev Tries to get the clock mode from the vot3 interface. If it fails, it returns the default block number mode. + * @param self The storage reference for the GovernorStorage. + * @return The clock mode as a string. + */ + // solhint-disable-next-line func-name-mixedcase + function CLOCK_MODE( + GovernorStorageTypesV3.GovernorStorage storage self + ) external view returns (string memory) { + try self.vot3.CLOCK_MODE() returns (string memory clockmode) { + return clockmode; + } catch { + return "mode=blocknumber&from=default"; + } + } +} diff --git a/contracts/deprecated/V3/governance/libraries/GovernorConfiguratorV3.sol b/contracts/deprecated/V3/governance/libraries/GovernorConfiguratorV3.sol new file mode 100644 index 0000000..dbb0df3 --- /dev/null +++ b/contracts/deprecated/V3/governance/libraries/GovernorConfiguratorV3.sol @@ -0,0 +1,173 @@ +// SPDX-License-Identifier: MIT + +// ####### +// ################ +// #################### +// ########### ######### +// ######### ######### +// ####### ######### ######### +// ######### ######### ########## +// ########## ######## #################### +// ########## ######### ######################### +// ################### ############################ +// ################# ########## ######## +// ############## ### ######## +// ############ ######### +// ########## ########## +// ######## ########### +// ### ############ +// ############## +// ################# +// ############## +// ######### + +pragma solidity 0.8.20; + +import { GovernorStorageTypesV3 } from "./GovernorStorageTypesV3.sol"; +import { IVOT3 } from "../../../../interfaces/IVOT3.sol"; +import { IVoterRewards } from "../../../../interfaces/IVoterRewards.sol"; +import { IXAllocationVotingGovernor } from "../../../../interfaces/IXAllocationVotingGovernor.sol"; +import { TimelockControllerUpgradeable } from "@openzeppelin/contracts-upgradeable/governance/TimelockControllerUpgradeable.sol"; +import { IB3TR } from "../../../../interfaces/IB3TR.sol"; + +/// @title GovernorConfiguratorV3 Library +/// @notice Library for managing the configuration of a Governor contract. +/// @dev This library provides functions to set and get various configuration parameters and contracts used by the Governor contract. +library GovernorConfiguratorV3 { + /// @dev Emitted when the `votingThreshold` is set. + event VotingThresholdSet(uint256 oldVotingThreshold, uint256 newVotingThreshold); + + /// @dev Emitted when the minimum delay before vote starts is set. + event MinVotingDelaySet(uint256 oldMinMinVotingDelay, uint256 newMinVotingDelay); + + /// @dev Emitted when the deposit threshold percentage is set. + event DepositThresholdSet(uint256 oldDepositThreshold, uint256 newDepositThreshold); + + /// @dev Emitted when the voter rewards contract is set. + event VoterRewardsSet(address oldContractAddress, address newContractAddress); + + /// @dev Emitted when the XAllocationVotingGovernor contract is set. + event XAllocationVotingSet(address oldContractAddress, address newContractAddress); + + /// @dev Emitted when the timelock controller used for proposal execution is modified. + event TimelockChange(address oldTimelock, address newTimelock); + + /// @dev The deposit threshold is not in the valid range for a percentage - 0 to 100. + error GovernorDepositThresholdNotInRange(uint256 depositThreshold); + + /**------------------ SETTERS ------------------**/ + /** + * @notice Sets the voting threshold. + * @dev Sets a new voting threshold and emits a {VotingThresholdSet} event. + * @param self The storage reference for the GovernorStorage. + * @param newVotingThreshold The new voting threshold. + */ + function setVotingThreshold(GovernorStorageTypesV3.GovernorStorage storage self, uint256 newVotingThreshold) external { + emit VotingThresholdSet(self.votingThreshold, newVotingThreshold); + self.votingThreshold = newVotingThreshold; + } + + /** + * @notice Sets the minimum delay before vote starts. + * @dev Sets a new minimum voting delay and emits a {MinVotingDelaySet} event. + * @param self The storage reference for the GovernorStorage. + * @param newMinVotingDelay The new minimum voting delay. + */ + function setMinVotingDelay(GovernorStorageTypesV3.GovernorStorage storage self, uint256 newMinVotingDelay) external { + emit MinVotingDelaySet(self.minVotingDelay, newMinVotingDelay); + self.minVotingDelay = newMinVotingDelay; + } + + /** + * @notice Sets the voter rewards contract. + * @dev Sets a new voter rewards contract and emits a {VoterRewardsSet} event. + * @param self The storage reference for the GovernorStorage. + * @param newVoterRewards The new voter rewards contract. + */ + function setVoterRewards(GovernorStorageTypesV3.GovernorStorage storage self, IVoterRewards newVoterRewards) external { + require(address(newVoterRewards) != address(0), "GovernorConfiguratorV3: voterRewards address cannot be zero"); + emit VoterRewardsSet(address(self.voterRewards), address(newVoterRewards)); + self.voterRewards = newVoterRewards; + } + + /** + * @notice Sets the XAllocationVotingGovernor contract. + * @dev Sets a new XAllocationVotingGovernor contract and emits a {XAllocationVotingSet} event. + * @param self The storage reference for the GovernorStorage. + * @param newXAllocationVoting The new XAllocationVotingGovernor contract. + */ + function setXAllocationVoting( + GovernorStorageTypesV3.GovernorStorage storage self, + IXAllocationVotingGovernor newXAllocationVoting + ) external { + require( + address(newXAllocationVoting) != address(0), + "GovernorConfiguratorV3: xAllocationVoting address cannot be zero" + ); + emit XAllocationVotingSet(address(self.xAllocationVoting), address(newXAllocationVoting)); + self.xAllocationVoting = newXAllocationVoting; + } + + /** + * @notice Sets the deposit threshold percentage. + * @dev Sets a new deposit threshold percentage and emits a {DepositThresholdSet} event. Reverts if the threshold is not in range. + * @param self The storage reference for the GovernorStorage. + * @param newDepositThreshold The new deposit threshold percentage. + */ + function setDepositThresholdPercentage( + GovernorStorageTypesV3.GovernorStorage storage self, + uint256 newDepositThreshold + ) external { + if (newDepositThreshold > 100) { + revert GovernorDepositThresholdNotInRange(newDepositThreshold); + } + + emit DepositThresholdSet(self.depositThresholdPercentage, newDepositThreshold); + self.depositThresholdPercentage = newDepositThreshold; + } + + /** + * @notice Updates the timelock controller. + * @dev Sets a new timelock controller and emits a {TimelockChange} event. + * @param self The storage reference for the GovernorStorage. + * @param newTimelock The new timelock controller. + */ + function updateTimelock( + GovernorStorageTypesV3.GovernorStorage storage self, + TimelockControllerUpgradeable newTimelock + ) external { + require(address(newTimelock) != address(0), "GovernorConfiguratorV3: timelock address cannot be zero"); + emit TimelockChange(address(self.timelock), address(newTimelock)); + self.timelock = newTimelock; + } + + /**------------------ GETTERS ------------------**/ + /** + * @notice Returns the voting threshold. + * @param self The storage reference for the GovernorStorage. + * @return The current voting threshold. + */ + function getVotingThreshold(GovernorStorageTypesV3.GovernorStorage storage self) internal view returns (uint256) { + return self.votingThreshold; + } + + /** + * @notice Returns the minimum delay before vote starts. + * @param self The storage reference for the GovernorStorage. + * @return The current minimum voting delay. + */ + function getMinVotingDelay(GovernorStorageTypesV3.GovernorStorage storage self) internal view returns (uint256) { + return self.minVotingDelay; + } + + /** + * @notice Returns the deposit threshold percentage. + * @param self The storage reference for the GovernorStorage. + * @return The current deposit threshold percentage. + */ + function getDepositThresholdPercentage( + GovernorStorageTypesV3.GovernorStorage storage self + ) internal view returns (uint256) { + return self.depositThresholdPercentage; + } +} diff --git a/contracts/deprecated/V3/governance/libraries/GovernorDepositLogicV3.sol b/contracts/deprecated/V3/governance/libraries/GovernorDepositLogicV3.sol new file mode 100644 index 0000000..cc4b1df --- /dev/null +++ b/contracts/deprecated/V3/governance/libraries/GovernorDepositLogicV3.sol @@ -0,0 +1,197 @@ +// SPDX-License-Identifier: MIT + +// ####### +// ################ +// #################### +// ########### ######### +// ######### ######### +// ####### ######### ######### +// ######### ######### ########## +// ########## ######## #################### +// ########## ######### ######################### +// ################### ############################ +// ################# ########## ######## +// ############## ### ######## +// ############ ######### +// ########## ########## +// ######## ########### +// ### ############ +// ############## +// ################# +// ############## +// ######### + +pragma solidity 0.8.20; + +import { GovernorStorageTypesV3 } from "./GovernorStorageTypesV3.sol"; +import { GovernorStateLogicV3 } from "./GovernorStateLogicV3.sol"; +import { GovernorTypesV3 } from "./GovernorTypesV3.sol"; + +/// @title GovernorDepositLogicV3 Library +/// @notice Library for managing deposits related to proposals in the Governor contract. +/// @dev This library provides functions to deposit and withdraw tokens for proposals, and to get deposit-related information. +library GovernorDepositLogicV3 { + /// @dev Emitted when a deposit is made to a proposal. + event ProposalDeposit(address indexed depositor, uint256 indexed proposalId, uint256 amount); + + /// @dev Thrown when there is no deposit to withdraw. + error GovernorNoDepositToWithdraw(uint256 proposalId, address depositer); + + /// @dev Thrown when the deposit amount is invalid (must be greater than 0). + error GovernorInvalidDepositAmount(); + + /// @dev Thrown when the proposal ID does not exist. + error GovernorNonexistentProposal(uint256 proposalId); + + // --------------- SETTERS --------------- + /** + * @notice Deposits tokens for a proposal. + * @dev Proposer and proposal sponsors can contribute towards a proposal's deposit using this function. The proposal must be in the Pending state to make a deposit. The amount deposited from an address is tracked and can be withdrawn by the same address when the voting round is over. + * @param self The storage reference for the GovernorStorage. + * @param amount The amount of tokens to deposit. + * @param proposalId The ID of the proposal. + */ + function deposit(GovernorStorageTypesV3.GovernorStorage storage self, uint256 amount, uint256 proposalId) external { + if (amount == 0) { + revert GovernorInvalidDepositAmount(); + } + + GovernorTypesV3.ProposalCore storage proposal = self.proposals[proposalId]; + + if (proposal.roundIdVoteStart == 0) { + revert GovernorNonexistentProposal(proposalId); + } + + GovernorStateLogicV3.validateStateBitmap( + self, + proposalId, + GovernorStateLogicV3.encodeStateBitmap(GovernorTypesV3.ProposalState.Pending) + ); + + proposal.depositAmount += amount; + + depositFunds(self, amount, msg.sender, proposalId); + } + + /** + * @notice Withdraws tokens previously deposited to a proposal. + * @dev A depositor can only withdraw their tokens once the proposal is no longer Pending or Active. Each address can only withdraw once per proposal. Reverts if no deposits are available to withdraw or if the deposits have already been withdrawn by the message sender. Reverts if the token transfer fails. + * @param self The storage reference for the GovernorStorage. + * @param proposalId The ID of the proposal to withdraw deposits from. + * @param depositer The address of the depositor. + */ + function withdraw(GovernorStorageTypesV3.GovernorStorage storage self, uint256 proposalId, address depositer) external { + uint256 amount = self.deposits[proposalId][depositer]; + + GovernorStateLogicV3.validateStateBitmap( + self, + proposalId, + GovernorStateLogicV3.ALL_PROPOSAL_STATES_BITMAP ^ + GovernorStateLogicV3.encodeStateBitmap(GovernorTypesV3.ProposalState.Pending) + ); + + if (amount == 0) { + revert GovernorNoDepositToWithdraw(proposalId, depositer); + } + + self.deposits[proposalId][depositer] = 0; + + require(self.vot3.transfer(depositer, amount), "B3TRGovernor: transfer failed"); + } + + /** + * @notice Internal function to deposit tokens to a proposal. + * @dev Emits a {ProposalDeposit} event. + * @param self The storage reference for the GovernorStorage. + * @param amount The amount of tokens to deposit. + * @param depositor The address of the depositor. + * @param proposalId The ID of the proposal. + */ + function depositFunds( + GovernorStorageTypesV3.GovernorStorage storage self, + uint256 amount, + address depositor, + uint256 proposalId + ) internal { + require(self.vot3.transferFrom(depositor, address(this), amount), "B3TRGovernor: transfer failed"); + + self.deposits[proposalId][depositor] += amount; + + emit ProposalDeposit(depositor, proposalId, amount); + } + + // --------------- GETTERS --------------- + /** + * @notice Returns the amount of tokens deposited by a user for a proposal. + * @param self The storage reference for the GovernorStorage. + * @param proposalId The ID of the proposal. + * @param user The address of the user. + * @return uint256 The amount of tokens deposited by the user. + */ + function getUserDeposit( + GovernorStorageTypesV3.GovernorStorage storage self, + uint256 proposalId, + address user + ) internal view returns (uint256) { + return self.deposits[proposalId][user]; + } + + /** + * @notice Returns the deposit threshold for a proposal. + * @param self The storage reference for the GovernorStorage. + * @param proposalId The ID of the proposal. + * @return uint256 The deposit threshold for the proposal. + */ + function proposalDepositThreshold( + GovernorStorageTypesV3.GovernorStorage storage self, + uint256 proposalId + ) internal view returns (uint256) { + return self.proposals[proposalId].depositThreshold; + } + + /** + * @notice Returns the total amount of deposits made to a proposal. + * @param self The storage reference for the GovernorStorage. + * @param proposalId The ID of the proposal. + * @return uint256 The total amount of deposits made to the proposal. + */ + function getProposalDeposits( + GovernorStorageTypesV3.GovernorStorage storage self, + uint256 proposalId + ) internal view returns (uint256) { + return self.proposals[proposalId].depositAmount; + } + + /** + * @notice Returns true if the threshold of deposits required to reach a proposal has been reached. + * @param self The storage reference for the GovernorStorage. + * @param proposalId The ID of the proposal. + * @return True if the deposit threshold has been reached, false otherwise. + */ + function proposalDepositReached( + GovernorStorageTypesV3.GovernorStorage storage self, + uint256 proposalId + ) internal view returns (bool) { + GovernorTypesV3.ProposalCore storage proposal = self.proposals[proposalId]; + return proposal.depositAmount >= proposal.depositThreshold; + } + + /** + * @notice Returns the deposit threshold. + * @param self The storage reference for the GovernorStorage. + * @return uint256 The deposit threshold. + */ + function depositThreshold(GovernorStorageTypesV3.GovernorStorage storage self) external view returns (uint256) { + return _depositThreshold(self); + } + + /** + * @notice Internal function to calculate the deposit threshold as a percentage of the total supply of B3TR tokens. + * @param self The storage reference for the GovernorStorage. + * @return uint256 The deposit threshold. + */ + function _depositThreshold(GovernorStorageTypesV3.GovernorStorage storage self) internal view returns (uint256) { + // deposit threshold is a percentage of the total supply of B3TR tokens + return (self.depositThresholdPercentage * self.b3tr.totalSupply()) / 100; + } +} diff --git a/contracts/deprecated/V3/governance/libraries/GovernorFunctionRestrictionsLogicV3.sol b/contracts/deprecated/V3/governance/libraries/GovernorFunctionRestrictionsLogicV3.sol new file mode 100644 index 0000000..e78953e --- /dev/null +++ b/contracts/deprecated/V3/governance/libraries/GovernorFunctionRestrictionsLogicV3.sol @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: MIT + +// ####### +// ################ +// #################### +// ########### ######### +// ######### ######### +// ####### ######### ######### +// ######### ######### ########## +// ########## ######## #################### +// ########## ######### ######################### +// ################### ############################ +// ################# ########## ######## +// ############## ### ######## +// ############ ######### +// ########## ########## +// ######## ########### +// ### ############ +// ############## +// ################# +// ############## +// ######### + +pragma solidity 0.8.20; + +import { GovernorStorageTypesV3 } from "./GovernorStorageTypesV3.sol"; + +/// @title GovernorFunctionRestrictionsLogicV3 +/// @notice Library for managing function restrictions within the Governor contract. +/// @dev This library provides functions to whitelist or restrict functions that can be called by proposals. +library GovernorFunctionRestrictionsLogicV3 { + /// @notice Error message for when a function is restricted by the governor. + /// @param functionSelector The function selector that is restricted. + error GovernorRestrictedFunction(bytes4 functionSelector); + + /// @notice Error message for when a function selector is invalid. + /// @param selector The function selector that is invalid. + error GovernorFunctionInvalidSelector(bytes selector); + + /// @notice Emitted when a function is whitelisted by the governor. + /// @param target The address of the contract. + /// @param functionSelector The function selector. + /// @param isWhitelisted Boolean indicating if the function is whitelisted. + event FunctionWhitelisted(address indexed target, bytes4 indexed functionSelector, bool isWhitelisted); + + // --------------- SETTERS --------------- + /** + * @notice Set the whitelist status of a function for proposals. + * @dev This method allows restricting functions that can be called by proposals for a single function selector. + * @param self The storage reference for the GovernorStorage. + * @param target The address of the contract. + * @param functionSelector The function selector. + * @param isWhitelisted Boolean indicating if the function is whitelisted for proposals. + */ + function setWhitelistFunction( + GovernorStorageTypesV3.GovernorStorage storage self, + address target, + bytes4 functionSelector, + bool isWhitelisted + ) public { + require(target != address(0), "GovernorFunctionRestrictionsLogicV3: target is the zero address"); + self.whitelistedFunctions[target][functionSelector] = isWhitelisted; + emit FunctionWhitelisted(target, functionSelector, isWhitelisted); + } + + /** + * @notice Set the whitelist status of multiple functions for proposals. + * @dev This method allows restricting functions that can be called by proposals for multiple function selectors at once. + * @param self The storage reference for the GovernorStorage. + * @param target The address of the contract. + * @param functionSelectors An array of function selectors. + * @param isWhitelisted Boolean indicating if the functions are whitelisted for proposals. + */ + function setWhitelistFunctions( + GovernorStorageTypesV3.GovernorStorage storage self, + address target, + bytes4[] memory functionSelectors, + bool isWhitelisted + ) external { + for (uint256 i; i < functionSelectors.length; i++) { + setWhitelistFunction(self, target, functionSelectors[i], isWhitelisted); + } + } + + /** + * @notice Toggle the function restriction on or off. + * @dev This method allows enabling or disabling function restriction. + * @param self The storage reference for the GovernorStorage. + * @param isEnabled Flag to enable or disable function restriction. + */ + function setIsFunctionRestrictionEnabled(GovernorStorageTypesV3.GovernorStorage storage self, bool isEnabled) external { + self.isFunctionRestrictionEnabled = isEnabled; + } + + // --------------- GETTERS --------------- + /** + * @notice Check if a function is whitelisted by the governor. + * @dev This method checks if a specific function is whitelisted for proposals. + * @param self The storage reference for the GovernorStorage. + * @param target The address of the contract. + * @param functionSelector The function selector. + * @return Boolean indicating if the function is whitelisted. + */ + function isFunctionWhitelisted( + GovernorStorageTypesV3.GovernorStorage storage self, + address target, + bytes4 functionSelector + ) internal view returns (bool) { + return self.whitelistedFunctions[target][functionSelector]; + } + + /** + * @notice Check if the targets and calldatas are whitelisted. + * @dev Internal function to check if the provided targets and calldatas are whitelisted. + * @param self The storage reference for the GovernorStorage. + * @param targets The addresses of the contracts to call. + * @param calldatas Function signatures and arguments. + */ + function checkFunctionsRestriction( + GovernorStorageTypesV3.GovernorStorage storage self, + address[] memory targets, + bytes[] memory calldatas + ) internal view { + if (self.isFunctionRestrictionEnabled) { + for (uint256 i; i < targets.length; i++) { + bytes4 functionSelector = extractFunctionSelector(calldatas[i]); + if (!self.whitelistedFunctions[targets[i]][functionSelector]) { + revert GovernorRestrictedFunction(functionSelector); + } + } + } + } + + // --------------- PRIVATE FUNCTIONS --------------- + /** + * @notice Extract the function selector from the calldata. + * @dev Internal pure function to extract the function selector from the calldata. + * @param data The calldata from which to extract the function selector. + * @return bytes4 The extracted function selector. + */ + function extractFunctionSelector(bytes memory data) private pure returns (bytes4) { + if (data.length < 4) revert GovernorFunctionInvalidSelector(data); + bytes4 sig; + assembly { + sig := mload(add(data, 32)) + } + return sig; + } +} diff --git a/contracts/deprecated/V3/governance/libraries/GovernorGovernanceLogicV3.sol b/contracts/deprecated/V3/governance/libraries/GovernorGovernanceLogicV3.sol new file mode 100644 index 0000000..598a74d --- /dev/null +++ b/contracts/deprecated/V3/governance/libraries/GovernorGovernanceLogicV3.sol @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: MIT + +// ####### +// ################ +// #################### +// ########### ######### +// ######### ######### +// ####### ######### ######### +// ######### ######### ########## +// ########## ######## #################### +// ########## ######### ######################### +// ################### ############################ +// ################# ########## ######## +// ############## ### ######## +// ############ ######### +// ########## ########## +// ######## ########### +// ### ############ +// ############## +// ################# +// ############## +// ######### + +pragma solidity 0.8.20; + +import { GovernorStorageTypesV3 } from "./GovernorStorageTypesV3.sol"; +import { DoubleEndedQueue } from "@openzeppelin/contracts/utils/structs/DoubleEndedQueue.sol"; + +/// @title GovernorGovernanceLogic +/// @notice Library for validating descriptions in governance proposals based on the proposer's address suffix. +/// @dev This library provides functions to manage the governance execution flow and validate the governance executor. +library GovernorGovernanceLogicV3 { + using DoubleEndedQueue for DoubleEndedQueue.Bytes32Deque; + + /// @dev Thrown when the `account` is not the governance executor. + /// @param account The address that attempted the unauthorized action. + error GovernorOnlyExecutor(address account); + + /** + * @notice Get the salt used for the timelock operation. + * @dev Combines the contract address and description hash to generate a unique salt. + * @param descriptionHash The hash of the proposal description. + * @param contractAddress The address of the calling governance contract. + * @return The generated salt as a bytes32 value. + */ + function timelockSalt(bytes32 descriptionHash, address contractAddress) internal pure returns (bytes32) { + return bytes20(contractAddress) ^ descriptionHash; + } + + /** + * @notice Get the address through which the governor executes actions. + * @dev Returns the timelock address used by the governor. + * @param self The storage reference for the GovernorStorage. + * @return The executor address. + */ + function executor(GovernorStorageTypesV3.GovernorStorage storage self) internal view returns (address) { + return address(self.timelock); + } + + /** + * @notice Validates that the `msg.sender` is the executor. + * @dev Reverts if the `msg.sender` is not the executor. If the executor is not the calling contract itself, it verifies that the `msg.data` is whitelisted. + * @param self The storage reference for the GovernorStorage. + * @param sender The address of the sender. + * @param data The calldata to be validated. + * @param contractAddress The address of the calling governance contract. + */ + function checkGovernance( + GovernorStorageTypesV3.GovernorStorage storage self, + address sender, + bytes calldata data, + address contractAddress + ) internal { + if (executor(self) != sender) { + revert GovernorOnlyExecutor(sender); + } + if (executor(self) != contractAddress) { + bytes32 msgDataHash = keccak256(data); + // Loop until popping the expected operation, revert if deque is empty (operation not authorized) + while (self.governanceCall.popFront() != msgDataHash) {} + } + } +} diff --git a/contracts/deprecated/V3/governance/libraries/GovernorProposalLogicV3.sol b/contracts/deprecated/V3/governance/libraries/GovernorProposalLogicV3.sol new file mode 100644 index 0000000..086cdeb --- /dev/null +++ b/contracts/deprecated/V3/governance/libraries/GovernorProposalLogicV3.sol @@ -0,0 +1,789 @@ +// SPDX-License-Identifier: MIT + +// ####### +// ################ +// #################### +// ########### ######### +// ######### ######### +// ####### ######### ######### +// ######### ######### ########## +// ########## ######## #################### +// ########## ######### ######################### +// ################### ############################ +// ################# ########## ######## +// ############## ### ######## +// ############ ######### +// ########## ########## +// ######## ########### +// ### ############ +// ############## +// ################# +// ############## +// ######### + +pragma solidity 0.8.20; + +import { GovernorStorageTypesV3 } from "./GovernorStorageTypesV3.sol"; +import { GovernorTypesV3 } from "./GovernorTypesV3.sol"; +import { GovernorStateLogicV3 } from "./GovernorStateLogicV3.sol"; +import { GovernorClockLogicV3 } from "./GovernorClockLogicV3.sol"; +import { GovernorDepositLogicV3 } from "./GovernorDepositLogicV3.sol"; +import { GovernorGovernanceLogicV3 } from "./GovernorGovernanceLogicV3.sol"; +import { GovernorFunctionRestrictionsLogicV3 } from "./GovernorFunctionRestrictionsLogicV3.sol"; +import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import { Address } from "@openzeppelin/contracts/utils/Address.sol"; +import { DoubleEndedQueue } from "@openzeppelin/contracts/utils/structs/DoubleEndedQueue.sol"; + +/// @title GovernorProposalLogicV3 +/// @notice Library for managing proposals in the Governor contract. +/// @dev This library provides functions to create, cancel, execute, and validate proposals. +library GovernorProposalLogicV3 { + using DoubleEndedQueue for DoubleEndedQueue.Bytes32Deque; + + /** + * @dev Emitted when a proposal is canceled. + */ + event ProposalCanceled(uint256 proposalId); + + /** + * @dev Emitted when a proposal is created. + */ + event ProposalCreated( + uint256 indexed proposalId, + address indexed proposer, + address[] targets, + uint256[] values, + string[] signatures, + bytes[] calldatas, + string description, + uint256 indexed roundIdVoteStart, + uint256 depositThreshold + ); + + /** + * @dev Emitted when a proposal is executed. + */ + event ProposalExecuted(uint256 proposalId); + + /** + * @dev Emitted when a proposal is queued. + */ + event ProposalQueued(uint256 proposalId, uint256 etaSeconds); + + /** + * @dev Thrown when the current state of a proposal is not the expected state for an operation. + */ + error GovernorUnexpectedProposalState( + uint256 proposalId, + GovernorTypesV3.ProposalState current, + bytes32 expectedStates + ); + + /** + * @dev Thrown when a user is not authorized to perform an action. + */ + error UnauthorizedAccess(address user); + + /** + * @dev Thrown when the round for proposal start is invalid. + */ + error GovernorInvalidStartRound(uint256 roundId); + + /** + * @dev Thrown when a queue operation is not implemented. + */ + error GovernorQueueNotImplemented(); + + /** + * @dev Thrown when there is an empty proposal or a mismatch between parameters length for a proposal call. + */ + error GovernorInvalidProposalLength(uint256 targets, uint256 calldatas, uint256 values); + + /** + * @dev Thrown when the proposer is not allowed to create a proposal. + */ + error GovernorRestrictedProposer(address proposer); + + /** ------------------ GETTERS ------------------ **/ + + /** + * @notice Returns the hash of a proposal. + * @dev Hashes the proposal parameters to produce a unique proposal id. + * @param targets The addresses of the contracts to call. + * @param values The values to send to the contracts. + * @param calldatas The function signatures and arguments. + * @param descriptionHash The hash of the proposal description. + * @return The proposal id. + */ + function hashProposal( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) internal pure returns (uint256) { + return uint256(keccak256(abi.encode(targets, values, calldatas, descriptionHash))); + } + + /** + * @notice Returns the proposer of a proposal. + * @param self The storage reference for the GovernorStorage. + * @param proposalId The id of the proposal. + * @return The address of the proposer. + */ + function proposalProposer( + GovernorStorageTypesV3.GovernorStorage storage self, + uint256 proposalId + ) internal view returns (address) { + return self.proposals[proposalId].proposer; + } + + /** + * @notice Returns the eta (estimated time of arrival) of a proposal. + * @param self The storage reference for the GovernorStorage. + * @param proposalId The id of the proposal. + * @return The eta in seconds. + */ + function proposalEta( + GovernorStorageTypesV3.GovernorStorage storage self, + uint256 proposalId + ) internal view returns (uint256) { + return self.proposals[proposalId].etaSeconds; + } + + /** + * @notice Returns the start round of a proposal. + * @param self The storage reference for the GovernorStorage. + * @param proposalId The id of the proposal. + * @return The start round id. + */ + function proposalStartRound( + GovernorStorageTypesV3.GovernorStorage storage self, + uint256 proposalId + ) internal view returns (uint256) { + return self.proposals[proposalId].roundIdVoteStart; + } + + /** + * @notice Returns the snapshot block of a proposal. + * @dev Determines the block number at which the proposal was snapshot. + * @param self The storage reference for the GovernorStorage. + * @param proposalId The id of the proposal. + * @return The snapshot block number. + */ + function proposalSnapshot( + GovernorStorageTypesV3.GovernorStorage storage self, + uint256 proposalId + ) external view returns (uint256) { + return _proposalSnapshot(self, proposalId); + } + + /** + * @notice Returns the deadline block of a proposal. + * @dev Determines the block number at which the proposal will be considered expired. + * @param self The storage reference for the GovernorStorage. + * @param proposalId The id of the proposal. + * @return The deadline block number. + */ + function proposalDeadline( + GovernorStorageTypesV3.GovernorStorage storage self, + uint256 proposalId + ) external view returns (uint256) { + return _proposalDeadline(self, proposalId); + } + + /** + * @notice Returns whether a proposal can start in the next round. + * @param self The storage reference for the GovernorStorage. + * @return True if the proposal can start in the next round, false otherwise. + */ + function canProposalStartInNextRound(GovernorStorageTypesV3.GovernorStorage storage self) external view returns (bool) { + return _canProposalStartInNextRound(self); + } + + /** + * @notice Returns the total votes for a proposal. + * @param self The storage reference for the GovernorStorage. + * @param proposalId The id of the proposal. + * @return The total votes for the proposal. + */ + function getProposalTotalVotes( + GovernorStorageTypesV3.GovernorStorage storage self, + uint256 proposalId + ) internal view returns (uint256) { + return self.proposalTotalVotes[proposalId]; + } + + /** + * @notice Returns the timelock id of a proposal. + * @param self The storage reference for the GovernorStorage. + * @param proposalId The id of the proposal. + * @return The timelock id of the proposal. + */ + function getTimelockId( + GovernorStorageTypesV3.GovernorStorage storage self, + uint256 proposalId + ) internal view returns (bytes32) { + return self.timelockIds[proposalId]; + } + + /** ------------------ SETTERS ------------------ **/ + + /** + * @notice Proposes a new governance action. + * @dev Creates a new proposal and validates the proposal parameters. + * @param self The storage reference for the GovernorStorage. + * @param targets The addresses of the contracts to call. + * @param values The values to send to the contracts. + * @param calldatas The function signatures and arguments. + * @param description The description of the proposal. + * @param startRoundId The round in which the proposal should be active. + * @param depositAmount The amount of tokens the proposer intends to deposit. + * @return The proposal id. + */ + function propose( + GovernorStorageTypesV3.GovernorStorage storage self, + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + string memory description, + uint256 startRoundId, + uint256 depositAmount + ) external returns (uint256) { + address proposer = msg.sender; + + uint256 proposalId = hashProposal(targets, values, calldatas, keccak256(bytes(description))); + + validateProposeParams(self, proposer, startRoundId, description, targets, values, calldatas, proposalId); + + return _propose(self, proposer, proposalId, targets, values, calldatas, description, startRoundId, depositAmount); + } + + /** + * @dev Function to know if a proposal is executable or not. + * If the proposal was creted without any targets, values, or calldatas, it is not executable. + * to check if the proposal is executable. + * + * @param proposalId The id of the proposal + */ + function proposalNeedsQueuing( + GovernorStorageTypesV3.GovernorStorage storage self, + uint256 proposalId + ) external view returns (bool) { + GovernorTypesV3.ProposalCore storage proposal = self.proposals[proposalId]; + if (proposal.roundIdVoteStart == 0) { + return false; + } + + if (proposal.isExecutable) { + return true; + } else { + return false; + } + } + + /** + * @notice Queues a proposal for execution. + * @dev Queues the proposal in the timelock. + * @param self The storage reference for the GovernorStorage. + * @param contractAddress The address of the calling contract. + * @param targets The addresses of the contracts to call. + * @param values The values to send to the contracts. + * @param calldatas The function signatures and arguments. + * @param descriptionHash The hash of the proposal description. + * @return The proposal id. + */ + function queue( + GovernorStorageTypesV3.GovernorStorage storage self, + address contractAddress, // Address of the calling contract + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) external returns (uint256) { + uint256 proposalId = hashProposal(targets, values, calldatas, descriptionHash); + + GovernorStateLogicV3.validateStateBitmap( + self, + proposalId, + GovernorStateLogicV3.encodeStateBitmap(GovernorTypesV3.ProposalState.Succeeded) + ); + + uint48 etaSeconds = _queueOperations( + self, + contractAddress, + proposalId, + targets, + values, + calldatas, + descriptionHash + ); + + if (etaSeconds != 0) { + self.proposals[proposalId].etaSeconds = etaSeconds; + emit ProposalQueued(proposalId, etaSeconds); + } else { + revert GovernorQueueNotImplemented(); + } + + return proposalId; + } + + /** + * @notice Executes a queued proposal. + * @dev Executes the proposal in the timelock. + * @param self The storage reference for the GovernorStorage. + * @param contractAddress The address of the calling contract. + * @param targets The addresses of the contracts to call. + * @param values The values to send to the contracts. + * @param calldatas The function signatures and arguments. + * @param descriptionHash The hash of the proposal description. + * @return The proposal id. + */ + function execute( + GovernorStorageTypesV3.GovernorStorage storage self, + address contractAddress, // Address of the calling contract + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) external returns (uint256) { + uint256 proposalId = hashProposal(targets, values, calldatas, descriptionHash); + + GovernorStateLogicV3.validateStateBitmap( + self, + proposalId, + GovernorStateLogicV3.encodeStateBitmap(GovernorTypesV3.ProposalState.Succeeded) | + GovernorStateLogicV3.encodeStateBitmap(GovernorTypesV3.ProposalState.Queued) + ); + + // mark as executed before calls to avoid reentrancy + self.proposals[proposalId].executed = true; + + // before execute: register governance call in queue. + if (GovernorGovernanceLogicV3.executor(self) != contractAddress) { + for (uint256 i; i < targets.length; ++i) { + if (targets[i] == address(this)) { + self.governanceCall.pushBack(keccak256(calldatas[i])); + } + } + } + + _executeOperations(self, contractAddress, proposalId, targets, values, calldatas, descriptionHash); + + // after execute: cleanup governance call queue. + if (GovernorGovernanceLogicV3.executor(self) != contractAddress && !self.governanceCall.empty()) { + self.governanceCall.clear(); + } + + emit ProposalExecuted(proposalId); + + return proposalId; + } + + /** + * @notice Cancels a proposal. + * @dev Cancels a proposal in any state other than Canceled or Executed. + * @param self The storage reference for the GovernorStorage. + * @param targets The addresses of the contracts to call. + * @param values The values to send to the contracts. + * @param calldatas The function signatures and arguments. + * @param descriptionHash The hash of the proposal description. + * @return The proposal id. + */ + function cancel( + GovernorStorageTypesV3.GovernorStorage storage self, + address account, + bool admin, + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) external returns (uint256) { + uint256 proposalId = hashProposal(targets, values, calldatas, descriptionHash); + + if (account != proposalProposer(self, proposalId) && !admin) { + revert UnauthorizedAccess(account); + } + + GovernorStateLogicV3.validateStateBitmap( + self, + proposalId, + GovernorStateLogicV3.ALL_PROPOSAL_STATES_BITMAP ^ + GovernorStateLogicV3.encodeStateBitmap(GovernorTypesV3.ProposalState.Canceled) ^ + GovernorStateLogicV3.encodeStateBitmap(GovernorTypesV3.ProposalState.Executed) + ); + + if (account == proposalProposer(self, proposalId)) { + require( + GovernorStateLogicV3._state(self, proposalId) == GovernorTypesV3.ProposalState.Pending, + "Governor: proposal not pending" + ); + } + + bytes32 timelockId = self.timelockIds[proposalId]; + if (timelockId != 0) { + // cancel + self.timelock.cancel(timelockId); + // cleanup + delete self.timelockIds[proposalId]; + } + + return _cancel(self, proposalId); + } + + /** ------------------ INTERNAL FUNCTIONS ------------------ **/ + + /** + * @dev Internal function to propose a new governance action. + * @param self The storage reference for the GovernorStorage. + * @param proposer The address of the proposer. + * @param proposalId The id of the proposal. + * @param targets The addresses of the contracts to call. + * @param values The values to send to the contracts. + * @param calldatas The function signatures and arguments. + * @param description The description of the proposal. + * @param startRoundId The round in which the proposal should be active. + * @param depositAmount The amount of tokens the proposer intends to deposit. + * @return The proposal id. + */ + function _propose( + GovernorStorageTypesV3.GovernorStorage storage self, + address proposer, + uint256 proposalId, + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + string memory description, + uint256 startRoundId, + uint256 depositAmount + ) private returns (uint256) { + uint256 depositThresholdAmount = GovernorDepositLogicV3._depositThreshold(self); + + _setProposal( + self, + proposalId, + proposer, + SafeCast.toUint32(self.xAllocationVoting.votingPeriod()), + startRoundId, + targets.length > 0, + depositAmount, + depositThresholdAmount + ); + + if (depositAmount > 0) { + GovernorDepositLogicV3.depositFunds(self, depositAmount, proposer, proposalId); + } + + emit ProposalCreated( + proposalId, + proposer, + targets, + values, + new string[](targets.length), + calldatas, + description, + startRoundId, + depositThresholdAmount + ); + + return proposalId; + } + + /** + * @dev Internal function to validate the parameters of a proposal. + * @param self The storage reference for the GovernorStorage. + * @param proposer The address of the proposer. + * @param startRoundId The round in which the proposal should be active. + * @param description The description of the proposal. + * @param targets The addresses of the contracts to call. + * @param values The values to send to the contracts. + * @param calldatas The function signatures and arguments. + * @param proposalId The id of the proposal. + */ + function validateProposeParams( + GovernorStorageTypesV3.GovernorStorage storage self, + address proposer, + uint256 startRoundId, + string memory description, + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + uint256 proposalId + ) private view { + // round must be in the future + if (startRoundId <= self.xAllocationVoting.currentRoundId()) { + revert GovernorInvalidStartRound(startRoundId); + } + + // only do this check if user wants to start proposal in the next round + if (startRoundId == self.xAllocationVoting.currentRoundId() + 1) { + if (!_canProposalStartInNextRound(self)) { + revert GovernorInvalidStartRound(startRoundId); + } + } + + // check description restriction + if (!isValidDescriptionForProposer(proposer, description)) { + revert GovernorRestrictedProposer(proposer); + } + + if (targets.length != values.length || targets.length != calldatas.length) { + revert GovernorInvalidProposalLength(targets.length, calldatas.length, values.length); + } + + if (self.proposals[proposalId].roundIdVoteStart != 0) { + // Proposal already exists + revert GovernorUnexpectedProposalState(proposalId, GovernorStateLogicV3._state(self, proposalId), bytes32(0)); + } + + GovernorFunctionRestrictionsLogicV3.checkFunctionsRestriction(self, targets, calldatas); + } + + /** + * @dev Internal function to set the data of a proposal in storage. + * @param self The storage reference for the GovernorStorage. + * @param proposalId The id of the proposal. + * @param proposer The address of the proposer. + * @param voteDuration The duration of the vote. + * @param roundIdVoteStart The round in which the proposal should be active. + * @param isExecutable Whether the proposal is executable. + * @param depositAmount The amount of tokens the proposer intends to deposit. + * @param proposalDepositThreshold The deposit threshold for the proposal. + */ + function _setProposal( + GovernorStorageTypesV3.GovernorStorage storage self, + uint256 proposalId, + address proposer, + uint32 voteDuration, + uint256 roundIdVoteStart, + bool isExecutable, + uint256 depositAmount, + uint256 proposalDepositThreshold + ) private { + GovernorTypesV3.ProposalCore storage proposal = self.proposals[proposalId]; + + proposal.proposer = proposer; + proposal.roundIdVoteStart = roundIdVoteStart; + proposal.voteDuration = voteDuration; + proposal.isExecutable = isExecutable; + proposal.depositAmount = depositAmount; + proposal.depositThreshold = proposalDepositThreshold; + } + + /** + * @dev Internal function to execute operations of a proposal. + * @param self The storage reference for the GovernorStorage. + * @param contractAddress The address of the calling contract. + * @param proposalId The id of the proposal. + * @param targets The addresses of the contracts to call. + * @param values The values to send to the contracts. + * @param calldatas The function signatures and arguments. + * @param descriptionHash The hash of the proposal description. + */ + function _executeOperations( + GovernorStorageTypesV3.GovernorStorage storage self, + address contractAddress, // Address of the calling contract + uint256 proposalId, + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) private { + // execute + self.timelock.executeBatch{ value: msg.value }( + targets, + values, + calldatas, + 0, + GovernorGovernanceLogicV3.timelockSalt(descriptionHash, contractAddress) + ); + // cleanup for refund + delete self.timelockIds[proposalId]; + } + + /** + * @dev Internal function to queue operations of a proposal in the timelock. + * @param self The storage reference for the GovernorStorage. + * @param contractAddress The address of the calling contract. + * @param proposalId The id of the proposal. + * @param targets The addresses of the contracts to call. + * @param values The values to send to the contracts. + * @param calldatas The function signatures and arguments. + * @param descriptionHash The hash of the proposal description. + * @return The eta (estimated time of arrival) in seconds. + */ + function _queueOperations( + GovernorStorageTypesV3.GovernorStorage storage self, + address contractAddress, // Address of the calling contract + uint256 proposalId, + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) private returns (uint48) { + uint256 delay = self.timelock.getMinDelay(); + + bytes32 salt = GovernorGovernanceLogicV3.timelockSalt(descriptionHash, contractAddress); + self.timelockIds[proposalId] = self.timelock.hashOperationBatch(targets, values, calldatas, 0, salt); + self.timelock.scheduleBatch(targets, values, calldatas, 0, salt, delay); + + return SafeCast.toUint48(block.timestamp + delay); + } + + /** + * @dev Internal function to cancel a proposal. + * @param self The storage reference for the GovernorStorage. + * @param proposalId The id of the proposal. + * @return The proposal id. + */ + function _cancel(GovernorStorageTypesV3.GovernorStorage storage self, uint256 proposalId) private returns (uint256) { + self.proposals[proposalId].canceled = true; + emit ProposalCanceled(proposalId); + + return proposalId; + } + + /** + * @dev Internal function to validate if a proposal can start in the next round. + * @param self The storage reference for the GovernorStorage. + * @return True if the proposal can start in the next round, false otherwise. + */ + function _canProposalStartInNextRound( + GovernorStorageTypesV3.GovernorStorage storage self + ) internal view returns (bool) { + uint256 currentRoundId = self.xAllocationVoting.currentRoundId(); + uint256 currentRoundDeadline = self.xAllocationVoting.roundDeadline(currentRoundId); + uint48 currentBlock = GovernorClockLogicV3.clock(self); + + // this could happen if the round ended and the next one not started yet + if (currentRoundDeadline <= currentBlock) { + return false; + } + + // if between now and the start of the new round is less then the min delay, revert + if (self.minVotingDelay > currentRoundDeadline - currentBlock) { + return false; + } + + return true; + } + + /** + * @dev Internal function to get the snapshot block of a proposal. + * @param self The storage reference for the GovernorStorage. + * @param proposalId The id of the proposal. + * @return The snapshot block number. + */ + function _proposalSnapshot( + GovernorStorageTypesV3.GovernorStorage storage self, + uint256 proposalId + ) internal view returns (uint256) { + // round when proposal should be active is already started + if (self.xAllocationVoting.currentRoundId() >= self.proposals[proposalId].roundIdVoteStart) { + return self.xAllocationVoting.roundSnapshot(self.proposals[proposalId].roundIdVoteStart); + } + + uint256 amountOfRoundsLeft = self.proposals[proposalId].roundIdVoteStart - self.xAllocationVoting.currentRoundId(); + uint256 roundsDurationLeft = self.xAllocationVoting.votingPeriod() * (amountOfRoundsLeft - 1); // -1 because if only 1 round left we want this to be 0 + uint256 currentRoundDeadline = self.xAllocationVoting.currentRoundDeadline(); + + // if current round ended and a new one did not start yet + if (currentRoundDeadline <= GovernorClockLogicV3.clock(self)) { + currentRoundDeadline = GovernorClockLogicV3.clock(self); + } + + return currentRoundDeadline + roundsDurationLeft + amountOfRoundsLeft; + } + + /** + * @dev Internal function to get the deadline block of a proposal. + * @param self The storage reference for the GovernorStorage. + * @param proposalId The id of the proposal. + * @return The deadline block number. + */ + function _proposalDeadline( + GovernorStorageTypesV3.GovernorStorage storage self, + uint256 proposalId + ) internal view returns (uint256) { + // if round is active or already occured proposal end block is the block when round ends + if (self.xAllocationVoting.currentRoundId() >= self.proposals[proposalId].roundIdVoteStart) { + return self.xAllocationVoting.roundDeadline(self.proposals[proposalId].roundIdVoteStart); + } + + // if we call this function before the round starts, it will return 0, so we need to estimate the end block + return _proposalSnapshot(self, proposalId) + self.xAllocationVoting.votingPeriod(); + } + + /** ------------------ PRIVATE FUNCTIONS ------------------ **/ + + /** + * @dev Checks if the description string ends with a proposer's address suffix. + * @param proposer The address of the proposer. + * @param description The description of the proposal. + * @return True if the suffix matches the proposer's address or if there is no suffix, false otherwise. + */ + function isValidDescriptionForProposer(address proposer, string memory description) private pure returns (bool) { + uint256 len = bytes(description).length; + + // Length is too short to contain a valid proposer suffix + if (len < 52) { + return true; + } + + // Extract what would be the `#proposer=0x` marker beginning the suffix + bytes12 marker; + assembly { + // Start of the string contents in memory = description + 32 + // First character of the marker = len - 52 + // We read the memory word starting at the first character of the marker: + // (description + 32) + (len - 52) = description + (len - 20) + marker := mload(add(description, sub(len, 20))) + } + + // If the marker is not found, there is no proposer suffix to check + if (marker != bytes12("#proposer=0x")) { + return true; + } + + // Parse the 40 characters following the marker as uint160 + uint160 recovered; + for (uint256 i = len - 40; i < len; ++i) { + (bool isHex, uint8 value) = tryHexToUint(bytes(description)[i]); + // If any of the characters is not a hex digit, ignore the suffix entirely + if (!isHex) { + return true; + } + recovered = (recovered << 4) | value; + } + + return recovered == uint160(proposer); + } + + /** + * @dev Tries to parse a character from a string as a hex value. + * @param char The character to parse. + * @return isHex True if the character is a valid hex digit, false otherwise. + * @return value The parsed hex value. + */ + function tryHexToUint(bytes1 char) private pure returns (bool, uint8) { + uint8 c = uint8(char); + unchecked { + // Case 0-9 + if (47 < c && c < 58) { + return (true, c - 48); + } + // Case A-F + else if (64 < c && c < 71) { + return (true, c - 55); + } + // Case a-f + else if (96 < c && c < 103) { + return (true, c - 87); + } + // Else: not a hex char + else { + return (false, 0); + } + } + } +} diff --git a/contracts/deprecated/V3/governance/libraries/GovernorQuorumLogicV3.sol b/contracts/deprecated/V3/governance/libraries/GovernorQuorumLogicV3.sol new file mode 100644 index 0000000..36ba415 --- /dev/null +++ b/contracts/deprecated/V3/governance/libraries/GovernorQuorumLogicV3.sol @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: MIT + +// ####### +// ################ +// #################### +// ########### ######### +// ######### ######### +// ####### ######### ######### +// ######### ######### ########## +// ########## ######## #################### +// ########## ######### ######################### +// ################### ############################ +// ################# ########## ######## +// ############## ### ######## +// ############ ######### +// ########## ########## +// ######## ########### +// ### ############ +// ############## +// ################# +// ############## +// ######### + +pragma solidity 0.8.20; + +import { GovernorStorageTypesV3 } from "./GovernorStorageTypesV3.sol"; +import { GovernorClockLogicV3 } from "./GovernorClockLogicV3.sol"; +import { GovernorProposalLogicV3 } from "./GovernorProposalLogicV3.sol"; +import { GovernorProposalLogicV3 } from "./GovernorProposalLogicV3.sol"; +import "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import { Checkpoints } from "@openzeppelin/contracts/utils/structs/Checkpoints.sol"; + +/// @title GovernorQuorumLogicV3 +/// @notice Library for managing quorum numerators using checkpointed data structures. +library GovernorQuorumLogicV3 { + using Checkpoints for Checkpoints.Trace208; + + /// @notice Error that is thrown when the new quorum numerator exceeds the denominator. + /// @param quorumNumerator The attempted new numerator that failed the update. + /// @param quorumDenominator The denominator against which the numerator was compared. + error GovernorInvalidQuorumFraction(uint256 quorumNumerator, uint256 quorumDenominator); + + /// @notice Emitted when the quorum numerator is updated. + /// @param oldNumerator The numerator before the update. + /// @param newNumerator The numerator after the update. + event QuorumNumeratorUpdated(uint256 oldNumerator, uint256 newNumerator); + + /** ------------------ GETTERS ------------------ **/ + + /// @notice Retrieves the quorum denominator, which is a constant in this implementation. + /// @return The quorum denominator (constant value of 100). + function quorumDenominator() internal pure returns (uint256) { + return 100; + } + + /// @notice Retrieves the quorum numerator at a specific timepoint using checkpoint data. + /// @param self The storage structure containing the quorum numerator history. + /// @param timepoint The specific timepoint for which to fetch the numerator. + /// @return The quorum numerator at the given timepoint. + function quorumNumerator( + GovernorStorageTypesV3.GovernorStorage storage self, + uint256 timepoint + ) public view returns (uint256) { + uint256 length = self.quorumNumeratorHistory._checkpoints.length; + + // Optimistic search, check the latest checkpoint + Checkpoints.Checkpoint208 storage latest = self.quorumNumeratorHistory._checkpoints[length - 1]; + uint48 latestKey = latest._key; + uint208 latestValue = latest._value; + if (latestKey <= timepoint) { + return latestValue; + } + + // Otherwise, do the binary search + return self.quorumNumeratorHistory.upperLookupRecent(SafeCast.toUint48(timepoint)); + } + + /// @notice Retrieves the latest quorum numerator using the GovernorClockLogicV3 library. + /// @param self The storage structure containing the quorum numerator history. + /// @return The latest quorum numerator. + function quorumNumerator(GovernorStorageTypesV3.GovernorStorage storage self) public view returns (uint256) { + return self.quorumNumeratorHistory.latest(); + } + + /** + * @notice Checks if the quorum has been reached for a proposal. + * @param self The storage reference for the GovernorStorage. + * @param proposalId The ID of the proposal. + * @return True if the quorum has been reached, false otherwise. + */ + function isQuorumReached( + GovernorStorageTypesV3.GovernorStorage storage self, + uint256 proposalId + ) external view returns (bool) { + return quorumReached(self, proposalId); + } + + /** + * @notice Returns the quorum for a specific timepoint. + * @param self The storage reference for the GovernorStorage. + * @param timepoint The specific timepoint. + * @return The quorum at the given timepoint. + */ + function quorum(GovernorStorageTypesV3.GovernorStorage storage self, uint256 timepoint) public view returns (uint256) { + return (self.vot3.getPastTotalSupply(timepoint) * quorumNumerator(self, timepoint)) / quorumDenominator(); + } + + /** ------------------ SETTERS ------------------ **/ + + /** + * @notice Updates the quorum numerator to a new value at a specified time, emitting an event upon success. + * @dev This function should only be called from governance actions where numerators need updating. + * @dev New numerator must be smaller or equal to the denominator. + * @param self The storage structure containing the quorum numerator history. + * @param newQuorumNumerator The new value for the quorum numerator. + */ + function updateQuorumNumerator( + GovernorStorageTypesV3.GovernorStorage storage self, + uint256 newQuorumNumerator + ) external { + uint256 denominator = quorumDenominator(); + uint256 oldQuorumNumerator = quorumNumerator(self); + + if (newQuorumNumerator > denominator) { + revert GovernorInvalidQuorumFraction(newQuorumNumerator, denominator); + } + + self.quorumNumeratorHistory.push(GovernorClockLogicV3.clock(self), SafeCast.toUint208(newQuorumNumerator)); + + emit QuorumNumeratorUpdated(oldQuorumNumerator, newQuorumNumerator); + } + + /** ------------------ INTERNAL FUNCTIONS ------------------ **/ + + /** + * @dev Internal function to check if the quorum has been reached for a proposal. + * @param self The storage reference for the GovernorStorage. + * @param proposalId The ID of the proposal. + * @return True if the quorum has been reached, false otherwise. + */ + function quorumReached( + GovernorStorageTypesV3.GovernorStorage storage self, + uint256 proposalId + ) internal view returns (bool) { + return + quorum(self, GovernorProposalLogicV3._proposalSnapshot(self, proposalId)) <= self.proposalTotalVotes[proposalId]; + } +} diff --git a/contracts/deprecated/V3/governance/libraries/GovernorStateLogicV3.sol b/contracts/deprecated/V3/governance/libraries/GovernorStateLogicV3.sol new file mode 100644 index 0000000..36d5ec3 --- /dev/null +++ b/contracts/deprecated/V3/governance/libraries/GovernorStateLogicV3.sol @@ -0,0 +1,159 @@ +// SPDX-License-Identifier: MIT + +// ####### +// ################ +// #################### +// ########### ######### +// ######### ######### +// ####### ######### ######### +// ######### ######### ########## +// ########## ######## #################### +// ########## ######### ######################### +// ################### ############################ +// ################# ########## ######## +// ############## ### ######## +// ############ ######### +// ########## ########## +// ######## ########### +// ### ############ +// ############## +// ################# +// ############## +// ######### + +pragma solidity 0.8.20; + +import { GovernorTypesV3 } from "./GovernorTypesV3.sol"; +import { GovernorStorageTypesV3 } from "./GovernorStorageTypesV3.sol"; +import { GovernorProposalLogicV3 } from "./GovernorProposalLogicV3.sol"; +import { GovernorVotesLogicV3 } from "./GovernorVotesLogicV3.sol"; +import { GovernorQuorumLogicV3 } from "./GovernorQuorumLogicV3.sol"; +import { GovernorClockLogicV3 } from "./GovernorClockLogicV3.sol"; +import { GovernorDepositLogicV3 } from "./GovernorDepositLogicV3.sol"; + +/// @title GovernorStateLogicV3 +/// @notice Library for Governor state logic, managing the state transitions and validations of governance proposals. +library GovernorStateLogicV3 { + /// @notice Bitmap representing all possible proposal states. + bytes32 internal constant ALL_PROPOSAL_STATES_BITMAP = + bytes32((2 ** (uint8(type(GovernorTypesV3.ProposalState).max) + 1)) - 1); + + /// @dev Thrown when the `proposalId` does not exist. + /// @param proposalId The ID of the proposal that does not exist. + error GovernorNonexistentProposal(uint256 proposalId); + + /// @dev Thrown when the current state of a proposal does not match the expected states. + /// @param proposalId The ID of the proposal. + /// @param current The current state of the proposal. + /// @param expectedStates The expected states of the proposal as a bitmap. + error GovernorUnexpectedProposalState( + uint256 proposalId, + GovernorTypesV3.ProposalState current, + bytes32 expectedStates + ); + + /** ------------------ GETTERS ------------------ **/ + + /** + * @notice Retrieves the current state of a proposal. + * @param self The storage reference for the GovernorStorage. + * @param proposalId The ID of the proposal. + * @return The current state of the proposal. + */ + function state( + GovernorStorageTypesV3.GovernorStorage storage self, + uint256 proposalId + ) external view returns (GovernorTypesV3.ProposalState) { + return _state(self, proposalId); + } + + /** ------------------ INTERNAL FUNCTIONS ------------------ **/ + + /** + * @dev Internal function to validate the current state of a proposal against expected states. + * @param self The storage reference for the GovernorStorage. + * @param proposalId The ID of the proposal. + * @param allowedStates The bitmap of allowed states. + * @return The current state of the proposal. + */ + function validateStateBitmap( + GovernorStorageTypesV3.GovernorStorage storage self, + uint256 proposalId, + bytes32 allowedStates + ) internal view returns (GovernorTypesV3.ProposalState) { + GovernorTypesV3.ProposalState currentState = _state(self, proposalId); + if (encodeStateBitmap(currentState) & allowedStates == bytes32(0)) { + revert GovernorUnexpectedProposalState(proposalId, currentState, allowedStates); + } + return currentState; + } + + /** + * @dev Encodes a `ProposalState` into a `bytes32` representation where each bit enabled corresponds to the underlying position in the `ProposalState` enum. + * @param proposalState The state to encode. + * @return The encoded state bitmap. + */ + function encodeStateBitmap(GovernorTypesV3.ProposalState proposalState) internal pure returns (bytes32) { + return bytes32(1 << uint8(proposalState)); + } + + /** + * @notice Retrieves the current state of a proposal. + * @dev See {IB3TRGovernor-state}. + * @param self The storage reference for the GovernorStorage. + * @param proposalId The ID of the proposal. + * @return The current state of the proposal. + */ + function _state( + GovernorStorageTypesV3.GovernorStorage storage self, + uint256 proposalId + ) internal view returns (GovernorTypesV3.ProposalState) { + // Load the proposal into memory + GovernorTypesV3.ProposalCore storage proposal = self.proposals[proposalId]; + bool proposalExecuted = proposal.executed; + bool proposalCanceled = proposal.canceled; + + if (proposalExecuted) { + return GovernorTypesV3.ProposalState.Executed; + } + + if (proposalCanceled) { + return GovernorTypesV3.ProposalState.Canceled; + } + + if (proposal.roundIdVoteStart == 0) { + revert GovernorNonexistentProposal(proposalId); + } + + // Check if the proposal is pending + if (self.xAllocationVoting.currentRoundId() < proposal.roundIdVoteStart) { + return GovernorTypesV3.ProposalState.Pending; + } + + uint256 currentTimepoint = GovernorClockLogicV3.clock(self); + uint256 deadline = GovernorProposalLogicV3._proposalDeadline(self, proposalId); + + if (!GovernorDepositLogicV3.proposalDepositReached(self, proposalId)) { + return GovernorTypesV3.ProposalState.DepositNotMet; + } + + if (deadline >= currentTimepoint) { + return GovernorTypesV3.ProposalState.Active; + } else if ( + !GovernorQuorumLogicV3.quorumReached(self, proposalId) || !GovernorVotesLogicV3.voteSucceeded(self, proposalId) + ) { + return GovernorTypesV3.ProposalState.Defeated; + } else if (GovernorProposalLogicV3.proposalEta(self, proposalId) == 0) { + return GovernorTypesV3.ProposalState.Succeeded; + } else { + bytes32 queueid = self.timelockIds[proposalId]; + if (self.timelock.isOperationPending(queueid)) { + return GovernorTypesV3.ProposalState.Queued; + } else if (self.timelock.isOperationDone(queueid)) { + return GovernorTypesV3.ProposalState.Executed; + } else { + return GovernorTypesV3.ProposalState.Canceled; + } + } + } +} diff --git a/contracts/deprecated/V3/governance/libraries/GovernorStorageTypesV3.sol b/contracts/deprecated/V3/governance/libraries/GovernorStorageTypesV3.sol new file mode 100644 index 0000000..c84c220 --- /dev/null +++ b/contracts/deprecated/V3/governance/libraries/GovernorStorageTypesV3.sol @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: MIT + +// ####### +// ################ +// #################### +// ########### ######### +// ######### ######### +// ####### ######### ######### +// ######### ######### ########## +// ########## ######## #################### +// ########## ######### ######################### +// ################### ############################ +// ################# ########## ######## +// ############## ### ######## +// ############ ######### +// ########## ########## +// ######## ########### +// ### ############ +// ############## +// ################# +// ############## +// ######### + +pragma solidity 0.8.20; + +import { GovernorTypesV3 } from "./GovernorTypesV3.sol"; +import { IVoterRewards } from "../../../../interfaces/IVoterRewards.sol"; +import { IXAllocationVotingGovernor } from "../../../../interfaces/IXAllocationVotingGovernor.sol"; +import { IB3TR } from "../../../../interfaces/IB3TR.sol"; +import { IVOT3 } from "../../../../interfaces/IVOT3.sol"; +import { DoubleEndedQueue } from "@openzeppelin/contracts/utils/structs/DoubleEndedQueue.sol"; +import { TimelockControllerUpgradeable } from "@openzeppelin/contracts-upgradeable/governance/TimelockControllerUpgradeable.sol"; +import { Checkpoints } from "@openzeppelin/contracts/utils/structs/Checkpoints.sol"; + +/// @title GovernorStorageTypesV3 +/// @notice Library for defining storage types used in the Governor contract. +library GovernorStorageTypesV3 { + struct GovernorStorage { + // ------------------------------- Version 1 ------------------------------- + + // ------------------------------- General Storage ------------------------------- + string name; // name of the Governor + mapping(uint256 proposalId => GovernorTypesV3.ProposalCore) proposals; + // This queue keeps track of the governor operating on itself. Calls to functions protected by the {onlyGovernance} + // modifier needs to be whitelisted in this queue. Whitelisting is set in {execute}, consumed by the + // {onlyGovernance} modifier and eventually reset after {_executeOperations} completes. This ensures that the + // execution of {onlyGovernance} protected calls can only be achieved through successful proposals. + DoubleEndedQueue.Bytes32Deque governanceCall; + // min delay before voting can start + uint256 minVotingDelay; + // ------------------------------- Quorum Storage ------------------------------- + // quorum numerator history + Checkpoints.Trace208 quorumNumeratorHistory; + // ------------------------------- Timelock Storage ------------------------------- + // Timelock contract + TimelockControllerUpgradeable timelock; + // mapping of proposalId to timelockId + mapping(uint256 proposalId => bytes32) timelockIds; + // ------------------------------- Function Restriction Storage ------------------------------- + // mapping of target address to function selector to bool indicating if function is whitelisted for proposals + mapping(address => mapping(bytes4 => bool)) whitelistedFunctions; + // flag to enable/disable function restrictions + bool isFunctionRestrictionEnabled; + // ------------------------------- External Contracts Storage ------------------------------- + // Voter Rewards contract + IVoterRewards voterRewards; + // XAllocationVotingGovernor contract + IXAllocationVotingGovernor xAllocationVoting; + // B3TR contract + IB3TR b3tr; + // VOT3 contract + IVOT3 vot3; + // ------------------------------- Desposits Storage ------------------------------- + // mapping to track deposits made to proposals by address + mapping(uint256 => mapping(address => uint256)) deposits; + // percentage of the total supply of B3TR tokens that need to be deposited in VOT3 to create a proposal + uint256 depositThresholdPercentage; + // ------------------------------- Voting Storage ------------------------------- + // mapping to store the votes for a proposal + mapping(uint256 => GovernorTypesV3.ProposalVote) proposalVotes; + // mapping to store that a user has voted at least one time + mapping(address => bool) hasVotedOnce; + // mapping to store the total votes for a proposal + mapping(uint256 => uint256) proposalTotalVotes; + // minimum amount of tokens needed to cast a vote + uint256 votingThreshold; + + // ------------------------------- Version 2 ------------------------------- + + // ------------------------------- Voting Storage ------------------------------- + // checkpoints for the quadratic voting status for each round + Checkpoints.Trace208 quadraticVotingDisabled; + } +} diff --git a/contracts/deprecated/V3/governance/libraries/GovernorTypesV3.sol b/contracts/deprecated/V3/governance/libraries/GovernorTypesV3.sol new file mode 100644 index 0000000..f4ea524 --- /dev/null +++ b/contracts/deprecated/V3/governance/libraries/GovernorTypesV3.sol @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: MIT + +// ####### +// ################ +// #################### +// ########### ######### +// ######### ######### +// ####### ######### ######### +// ######### ######### ########## +// ########## ######## #################### +// ########## ######### ######################### +// ################### ############################ +// ################# ########## ######## +// ############## ### ######## +// ############ ######### +// ########## ########## +// ######## ########### +// ### ############ +// ############## +// ################# +// ############## +// ######### + +pragma solidity 0.8.20; + +import { IVoterRewards } from "../../../../interfaces/IVoterRewards.sol"; +import { IXAllocationVotingGovernor } from "../../../../interfaces/IXAllocationVotingGovernor.sol"; +import { IB3TR } from "../../../../interfaces/IB3TR.sol"; +import { IVOT3 } from "../../../../interfaces/IVOT3.sol"; +import { TimelockControllerUpgradeable } from "@openzeppelin/contracts-upgradeable/governance/TimelockControllerUpgradeable.sol"; + +library GovernorTypesV3 { + /** + * @dev Struct containing data to initialize the contract + * @param vot3Token The address of the Vot3 token used for voting + * @param timelock The address of the Timelock + * @param xAllocationVoting The address of the xAllocationVoting + * @param quorumPercentage quorum as a percentage of the total supply of VOT3 tokens + * @param initialDepositThreshold The Deposit Threshold for a proposal to be active + * @param initialMinVotingDelay The minimum amount of blocks a proposal needs to wait before it can start + * @param initialVotingThreshold The minimum amount of voting power needed in order to vote + * @param voterRewards The address of the voter rewards contract + * @param isFunctionRestrictionEnabled If the function restriction is enabled + */ + struct InitializationData { + IVOT3 vot3Token; + TimelockControllerUpgradeable timelock; + IXAllocationVotingGovernor xAllocationVoting; + IB3TR b3tr; + uint256 quorumPercentage; + uint256 initialDepositThreshold; + uint256 initialMinVotingDelay; + uint256 initialVotingThreshold; + IVoterRewards voterRewards; + bool isFunctionRestrictionEnabled; + } + + /** + * @param governorAdmin The address of the governor admin + * @param pauser The address of the pauser + * @param contractsAddressManager The address of the contracts address manager + * @param proposalExecutor The address that should be set as executor and have the PROPOSAL_EXECUTOR_ROLE + * @param governorFunctionSettingsRoleAddress The address that should have the GOVERNOR_FUNCTIONS_SETTINGS_ROLE + */ + struct InitializationRolesData { + address governorAdmin; + address pauser; + address contractsAddressManager; + address proposalExecutor; + address governorFunctionSettingsRoleAddress; + } + + // Proposal vote types + enum VoteType { + Against, + For, + Abstain + } + + // ProposalVote struct to store the votes for a proposal + struct ProposalVote { + uint256 againstVotes; + uint256 forVotes; + uint256 abstainVotes; + mapping(address => bool) hasVoted; + } + + // ProposalCore struct to store the core data for a proposal + struct ProposalCore { + address proposer; + uint256 roundIdVoteStart; + uint32 voteDuration; + bool isExecutable; + bool executed; + bool canceled; + uint48 etaSeconds; + uint256 depositAmount; + uint256 depositThreshold; + } + + // ProposalState enum to store the state of a proposal + enum ProposalState { + Pending, + Active, + Canceled, + Defeated, + Succeeded, + Queued, + Executed, + DepositNotMet + } +} diff --git a/contracts/deprecated/V3/governance/libraries/GovernorVotesLogicV3.sol b/contracts/deprecated/V3/governance/libraries/GovernorVotesLogicV3.sol new file mode 100644 index 0000000..7a8bc86 --- /dev/null +++ b/contracts/deprecated/V3/governance/libraries/GovernorVotesLogicV3.sol @@ -0,0 +1,300 @@ +// SPDX-License-Identifier: MIT + +// ####### +// ################ +// #################### +// ########### ######### +// ######### ######### +// ####### ######### ######### +// ######### ######### ########## +// ########## ######## #################### +// ########## ######### ######################### +// ################### ############################ +// ################# ########## ######## +// ############## ### ######## +// ############ ######### +// ########## ########## +// ######## ########### +// ### ############ +// ############## +// ################# +// ############## +// ######### + +pragma solidity 0.8.20; + +import { GovernorStorageTypesV3 } from "./GovernorStorageTypesV3.sol"; +import { GovernorTypesV3 } from "./GovernorTypesV3.sol"; +import { GovernorStateLogicV3 } from "./GovernorStateLogicV3.sol"; +import { GovernorConfiguratorV3 } from "./GovernorConfiguratorV3.sol"; +import { GovernorProposalLogicV3 } from "./GovernorProposalLogicV3.sol"; +import { GovernorClockLogicV3 } from "./GovernorClockLogicV3.sol"; +import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; +import { Checkpoints } from "@openzeppelin/contracts/utils/structs/Checkpoints.sol"; +import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; + +/// @title GovernorVotesLogicV3 +/// @notice Library for handling voting logic in the Governor contract. +library GovernorVotesLogicV3 { + using Checkpoints for Checkpoints.Trace208; + + /// @dev Thrown when a vote has already been cast by the voter. + /// @param voter The address of the voter who already cast a vote. + error GovernorAlreadyCastVote(address voter); + + /// @dev Thrown when an invalid vote type is used. + error GovernorInvalidVoteType(); + + /// @dev Thrown when the voting threshold is not met. + /// @param threshold The required voting threshold. + /// @param votes The actual votes received. + error GovernorVotingThresholdNotMet(uint256 threshold, uint256 votes); + + /// @notice Emitted when a vote is cast without parameters. + /// @param voter The address of the voter. + /// @param proposalId The ID of the proposal being voted on. + /// @param support The support value of the vote. + /// @param weight The weight of the vote. + /// @param power The voting power of the voter. + /// @param reason The reason for the vote. + event VoteCast( + address indexed voter, + uint256 indexed proposalId, + uint8 support, + uint256 weight, + uint256 power, + string reason + ); + + /// @notice Emits true if quadratic voting is disabled, false otherwise. + /// @param disabled - The flag to enable or disable quadratic voting. + event QuadraticVotingToggled(bool indexed disabled); + + /** ------------------ INTERNAL FUNCTIONS ------------------ **/ + + /** + * @dev Internal function to count a vote for a proposal. + * @param self The storage reference for the GovernorStorage. + * @param proposalId The ID of the proposal. + * @param account The address of the voter. + * @param support The support value of the vote. + * @param weight The weight of the vote. + * @param power The voting power of the voter. + */ + function _countVote( + GovernorStorageTypesV3.GovernorStorage storage self, + uint256 proposalId, + address account, + uint8 support, + uint256 weight, + uint256 power + ) private { + GovernorTypesV3.ProposalVote storage proposalVote = self.proposalVotes[proposalId]; + + if (proposalVote.hasVoted[account]) { + revert GovernorAlreadyCastVote(account); + } + proposalVote.hasVoted[account] = true; + + // if quadratic voting is disabled, use the weight as the vote otherwise use the power as the vote + uint256 vote = isQuadraticVotingDisabledForCurrentRound(self) ? weight : power; + + if (support == uint8(GovernorTypesV3.VoteType.Against)) { + proposalVote.againstVotes += vote; + } else if (support == uint8(GovernorTypesV3.VoteType.For)) { + proposalVote.forVotes += vote; + } else if (support == uint8(GovernorTypesV3.VoteType.Abstain)) { + proposalVote.abstainVotes += vote; + } else { + revert GovernorInvalidVoteType(); + } + + self.proposalTotalVotes[proposalId] += weight; + + // Save that user cast vote only the first time + if (!self.hasVotedOnce[account]) { + self.hasVotedOnce[account] = true; + } + } + + /** + * @dev Internal function to check if the vote succeeded. + * @param self The storage reference for the GovernorStorage. + * @param proposalId The ID of the proposal. + * @return True if the vote succeeded, false otherwise. + */ + function voteSucceeded( + GovernorStorageTypesV3.GovernorStorage storage self, + uint256 proposalId + ) internal view returns (bool) { + GovernorTypesV3.ProposalVote storage proposalVote = self.proposalVotes[proposalId]; + return proposalVote.forVotes > proposalVote.againstVotes; + } + + /** ------------------ GETTERS ------------------ **/ + + /** + * @notice Retrieves the votes for a specific account at a given timepoint. + * @param self The storage reference for the GovernorStorage. + * @param account The address of the account. + * @param timepoint The specific timepoint. + * @return The votes of the account at the given timepoint. + */ + function getVotes( + GovernorStorageTypesV3.GovernorStorage storage self, + address account, + uint256 timepoint + ) internal view returns (uint256) { + return self.vot3.getPastVotes(account, timepoint); + } + + /** + * @notice Retrieves the quadratic voting power of an account at a given timepoint. + * @param self The storage reference for the GovernorStorage. + * @param account The address of the account. + * @param timepoint The specific timepoint. + * @return The quadratic voting power of the account. + */ + function getQuadraticVotingPower( + GovernorStorageTypesV3.GovernorStorage storage self, + address account, + uint256 timepoint + ) external view returns (uint256) { + // Scale the votes by 1e9 so that the number returned is 1e18 + return Math.sqrt(self.vot3.getPastVotes(account, timepoint)) * 1e9; + } + + /** + * @notice Checks if an account has voted on a specific proposal. + * @param self The storage reference for the GovernorStorage. + * @param proposalId The ID of the proposal. + * @param account The address of the account. + * @return True if the account has voted, false otherwise. + */ + function hasVoted( + GovernorStorageTypesV3.GovernorStorage storage self, + uint256 proposalId, + address account + ) internal view returns (bool) { + return self.proposalVotes[proposalId].hasVoted[account]; + } + + /** + * @notice Retrieves the votes for a proposal. + * @param self The storage reference for the GovernorStorage. + * @param proposalId The ID of the proposal. + * @return againstVotes The number of votes against the proposal. + * @return forVotes The number of votes for the proposal. + * @return abstainVotes The number of abstain votes. + */ + function getProposalVotes( + GovernorStorageTypesV3.GovernorStorage storage self, + uint256 proposalId + ) internal view returns (uint256 againstVotes, uint256 forVotes, uint256 abstainVotes) { + GovernorTypesV3.ProposalVote storage proposalVote = self.proposalVotes[proposalId]; + return (proposalVote.againstVotes, proposalVote.forVotes, proposalVote.abstainVotes); + } + + /** + * @notice Checks if a user has voted at least once. + * @param self The storage reference for the GovernorStorage. + * @param user The address of the user. + * @return True if the user has voted once, false otherwise. + */ + function userVotedOnce(GovernorStorageTypesV3.GovernorStorage storage self, address user) internal view returns (bool) { + return self.hasVotedOnce[user]; + } + + /** ------------------ EXTERNAL FUNCTIONS ------------------ **/ + + /** + * @notice Casts a vote on a proposal. + * @param self The storage reference for the GovernorStorage. + * @param proposalId The ID of the proposal. + * @param voter The address of the voter. + * @param support The support value of the vote. + * @param reason The reason for the vote. + * @return The weight of the vote. + */ + function castVote( + GovernorStorageTypesV3.GovernorStorage storage self, + uint256 proposalId, + address voter, + uint8 support, + string calldata reason + ) external returns (uint256) { + GovernorStateLogicV3.validateStateBitmap( + self, + proposalId, + GovernorStateLogicV3.encodeStateBitmap(GovernorTypesV3.ProposalState.Active) + ); + + uint256 proposalSnapshot = GovernorProposalLogicV3._proposalSnapshot(self, proposalId); + uint256 weight = self.vot3.getPastVotes(voter, proposalSnapshot); + uint256 power = Math.sqrt(weight) * 1e9; + + if (weight < GovernorConfiguratorV3.getVotingThreshold(self)) { + revert GovernorVotingThresholdNotMet(weight, GovernorConfiguratorV3.getVotingThreshold(self)); + } + + _countVote(self, proposalId, voter, support, weight, power); + + self.voterRewards.registerVote(proposalSnapshot, voter, weight, Math.sqrt(weight)); + + emit VoteCast(voter, proposalId, support, weight, power, reason); + + return weight; + } + + /** + * @notice Toggle quadratic voting for a specific cycle. + * @dev This function toggles the state of quadratic voting for a specific cycle. + * @param self - The storage reference for the GovernorStorage. + * The state will flip between enabled and disabled each time the function is called. + */ + function toggleQuadraticVoting(GovernorStorageTypesV3.GovernorStorage storage self) external { + bool isQuadraticDisabled = self.quadraticVotingDisabled.upperLookupRecent(GovernorClockLogicV3.clock(self)) == 1; // 0: enabled, 1: disabled + + // If quadratic voting is disabled, set the new status to enabled, otherwise set it to disabled. + uint208 newStatus = isQuadraticDisabled ? 0 : 1; + + // Toggle the status -> 0: enabled, 1: disabled + self.quadraticVotingDisabled.push(GovernorClockLogicV3.clock(self), newStatus); + + // Emit an event to log the new quadratic voting status. + emit QuadraticVotingToggled(!isQuadraticDisabled); + } + + /** + * @notice Check if quadratic voting is disabled at a specific round. + * @dev To check if quadratic voting was disabled for a round, use the block number the round started. + * @param self - The storage reference for the GovernorStorage. + * @param roundId - The round ID for which to check if quadratic voting is disabled. + * @return true if quadratic voting is disabled, false otherwise. + */ + function isQuadraticVotingDisabledForRound( + GovernorStorageTypesV3.GovernorStorage storage self, + uint256 roundId + ) external view returns (bool) { + // Get the block number the round started. + uint48 blockNumber = SafeCast.toUint48(self.xAllocationVoting.roundSnapshot(roundId)); + + // Check if quadratic voting is enabled or disabled at the block number. + return self.quadraticVotingDisabled.upperLookupRecent(blockNumber) == 1; // 0: enabled, 1: disabled + } + + /** + * @notice Check if quadratic voting is disabled for the current round. + * @param self - The storage reference for the GovernorStorage. + * @return true if quadratic voting is disabled, false otherwise. + */ + function isQuadraticVotingDisabledForCurrentRound( + GovernorStorageTypesV3.GovernorStorage storage self + ) public view returns (bool) { + // Get the block number the emission round started. + uint256 roundStartBlock = self.xAllocationVoting.currentRoundSnapshot(); + + // Check if quadratic voting is enabled or disabled for the current round. + return self.quadraticVotingDisabled.upperLookupRecent(SafeCast.toUint48(roundStartBlock)) == 1; // 0: enabled, 1: disabled + } +} diff --git a/contracts/deprecated/V3/interfaces/IB3TRGovernor.sol b/contracts/deprecated/V3/interfaces/IB3TRGovernor.sol new file mode 100644 index 0000000..b8d787f --- /dev/null +++ b/contracts/deprecated/V3/interfaces/IB3TRGovernor.sol @@ -0,0 +1,503 @@ +// SPDX-License-Identifier: MIT +// Forked from OpenZeppelin Contracts (last updated v5.0.0) (governance/IGovernor.sol) + +pragma solidity 0.8.20; + +import { IERC165 } from "@openzeppelin/contracts/interfaces/IERC165.sol"; +import { IERC6372 } from "@openzeppelin/contracts/interfaces/IERC6372.sol"; +import { IB3TR } from "../../../interfaces/IB3TR.sol"; +import { IVoterRewards } from "../../../interfaces/IVoterRewards.sol"; +import { IXAllocationVotingGovernor } from "../../../interfaces/IXAllocationVotingGovernor.sol"; +import { GovernorTypesV3 } from "../governance/libraries/GovernorTypesV3.sol"; + +/** + * @dev Interface of the {B3TRGovernor} core. + * + * Modifications to original forked contract from OZ: + * - Removed votingDelay() + * - Removed the possibility to cast vote with params and with signature + * - Updated propose() and ProposalCreated event to accept the x allocation round id as param when proposal should become active + * - Added proposalStartRound() to get the round when the proposal should become active + * - Added canProposalStartInNextRound() to check if the proposal can start in the next allocation round + * - Added new state `DepositNotMet` to ProposalState enum + * - Added depositThreshold() to get the minimum required deposit for a proposal and removed proposalThreshold + */ +interface IB3TRGovernor is IERC165, IERC6372 { + /** + * @dev Empty proposal or a mismatch between the parameters length for a proposal call. + */ + error GovernorInvalidProposalLength(uint256 targets, uint256 calldatas, uint256 values); + + /** + * @dev The vote was already cast. + */ + error GovernorAlreadyCastVote(address voter); + + /** + * @dev Token deposits are disabled in this contract. + */ + error GovernorDisabledDeposit(); + + /** + * @dev The `account` is not a proposer. + */ + error GovernorOnlyProposer(address account); + + /** + * @dev The `account` is not the governance executor. + */ + error GovernorOnlyExecutor(address account); + + /** + * @dev The `proposalId` doesn't exist. + */ + error GovernorNonexistentProposal(uint256 proposalId); + + /** + * @dev The `votingThreshold` is not met. + */ + error GovernorVotingThresholdNotMet(uint256 threshold, uint256 votes); + + /** + * @dev The quorum numerator is greater than the quorum denominator. + */ + error GovernorInvalidQuorumFraction(uint256 quorumNumerator, uint256 quorumDenominator); + + /** + * @dev The current state of a proposal is not the required for performing an operation. + * The `expectedStates` is a bitmap with the bits enabled for each ProposalState enum position + * counting from right to left. + * + * NOTE: If `expectedState` is `bytes32(0)`, the proposal is expected to not be in any state (i.e. not exist). + * This is the case when a proposal that is expected to be unset is already initiated (the proposal is duplicated). + * + * See {Governor-_encodeStateBitmap}. + */ + error GovernorUnexpectedProposalState( + uint256 proposalId, + GovernorTypesV3.ProposalState current, + bytes32 expectedStates + ); + + /** + * @dev The voting period set is not a valid period. + */ + error GovernorInvalidVotingPeriod(uint256 votingPeriod); + + /** + * @dev The `proposer` does not have the required votes to create a proposal. + */ + error GovernorInsufficientProposerVotes(address proposer, uint256 votes, uint256 threshold); + + /** + * @dev The `proposer` is not allowed to create a proposal. + */ + error GovernorRestrictedProposer(address proposer); + + /** + * @dev The vote type used is not valid for the corresponding counting module. + */ + error GovernorInvalidVoteType(); + + /** + * @dev Queue operation is not implemented for this governor. Execute should be called directly. + */ + error GovernorQueueNotImplemented(); + + /** + * @dev The proposal hasn't been queued yet. + */ + error GovernorNotQueuedProposal(uint256 proposalId); + + /** + * @dev The proposal has already been queued. + */ + error GovernorAlreadyQueuedProposal(uint256 proposalId); + + /** + * @dev The round when proposal should start is not valid. + */ + error GovernorInvalidStartRound(uint256 roundId); + + /** + * @dev There is no deposit to withdraw. + */ + error GovernorNoDepositToWithdraw(uint256 proposalId, address depositer); + + /** + * @dev The deposit amount must be greater than 0. + */ + error GovernorInvalidDepositAmount(); + + /** + * @dev The deposit threshold is not in the valid range for a percentage - 0 to 100. + */ + error GovernorDepositThresholdNotInRange(uint256 depositThreshold); + + /** + * @dev User is not authorized to perform the action. + */ + error UnauthorizedAccess(address user); + + /** + * @dev Emitted when a proposal is created + */ + event ProposalCreated( + uint256 indexed proposalId, + address indexed proposer, + address[] targets, + uint256[] values, + string[] signatures, + bytes[] calldatas, + string description, + uint256 indexed roundIdVoteStart, + uint256 depositThreshold + ); + + /** + * @dev Emitted when a proposal is queued. + */ + event ProposalQueued(uint256 proposalId, uint256 etaSeconds); + + /** + * @dev Emitted when a proposal is executed. + */ + event ProposalExecuted(uint256 proposalId); + + /** + * @dev Emitted when a proposal is canceled. + */ + event ProposalCanceled(uint256 proposalId); + + /** + * @dev Emitted when the quorum numerator is updated. + */ + event QuorumNumeratorUpdated(uint256 oldNumerator, uint256 newNumerator); + + /** + * @dev Emitted when the timelock controller used for proposal execution is modified. + */ + event TimelockChange(address oldTimelock, address newTimelock); + + /** + * @dev Emitted when a function is whitelisted or restricted by the governor. + */ + event FunctionWhitelisted(address indexed target, bytes4 indexed functionSelector, bool isWhitelisted); + + /** + * @dev Emitted when a vote is cast without params. + * + * Note: `support` values should be seen as buckets. Their interpretation depends on the voting module used. + */ + event VoteCast( + address indexed voter, + uint256 indexed proposalId, + uint8 support, + uint256 weight, + uint256 power, + string reason + ); + + /** + * @notice Emits true if quadratic voting is disabled, false otherwise. + * @param disabled - The flag to enable or disable quadratic voting. + */ + event QuadraticVotingToggled(bool indexed disabled); + + /** + * @dev Emitted when a deposit is made to a proposal. + */ + event ProposalDeposit(address indexed depositor, uint256 indexed proposalId, uint256 amount); + + /** + * @notice module:core + * @dev Name of the governor instance (used in building the ERC712 domain separator). + */ + function name() external view returns (string memory); + + /** + * @notice module:core + * @dev Version of the governor instance (used in building the ERC712 domain separator). Default: "1" + */ + function version() external view returns (string memory); + + /** + * @notice module:voting + * @dev A description of the possible `support` values for {castVote} and the way these votes are counted, meant to + * be consumed by UIs to show correct vote options and interpret the results. The string is a URL-encoded sequence of + * key-value pairs that each describe one aspect, for example `support=bravo&quorum=for,abstain`. + * + * There are 2 standard keys: `support` and `quorum`. + * + * - `support=bravo` refers to the vote options 0 = Against, 1 = For, 2 = Abstain, as in `GovernorBravo`. + * - `quorum=bravo` means that only For votes are counted towards quorum. + * - `quorum=for,abstain` means that both For and Abstain votes are counted towards quorum. + * + * If a counting module makes use of encoded `params`, it should include this under a `params` key with a unique + * name that describes the behavior. For example: + * + * - `params=fractional` might refer to a scheme where votes are divided fractionally between for/against/abstain. + * - `params=erc721` might refer to a scheme where specific NFTs are delegated to vote. + * + * NOTE: The string can be decoded by the standard + * https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams[`URLSearchParams`] + * JavaScript class. + */ + // solhint-disable-next-line func-name-mixedcase + function COUNTING_MODE() external view returns (string memory); + + /** + * @notice module:core + * @dev Hashing function used to (re)build the proposal id from the proposal details.. + */ + function hashProposal( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) external pure returns (uint256); + + /** + * @notice module:core + * @dev Current state of a proposal, following Compound's convention + */ + function state(uint256 proposalId) external view returns (GovernorTypesV3.ProposalState); + + /** + * @notice module:core + * @dev Get the minimum required delay between proposal cretion and start when creating a proposal. + */ + function minVotingDelay() external view returns (uint256); + + /** + * @notice module:core + * @dev The B3TR contract address + */ + function b3tr() external view returns (IB3TR); + + /** + * @notice module:core + * @dev Getter for the VoterRewards contract + */ + function voterRewards() external view returns (IVoterRewards); + + /** + * @notice module:core + * @dev Getter for the XAllocationVoting contract + */ + function xAllocationVoting() external view returns (IXAllocationVotingGovernor); + + /** + * @notice module:core + * @dev The number of votes in support of a proposal required in order for a proposal to become active. + */ + function depositThreshold() external view returns (uint256); + + /** + * @notice module:core + * @dev The deposit threshold percentage of the total supply of B3TR tokens that need to be deposited to create a proposal + */ + function depositThresholdPercentage() external view returns (uint256); + + /** + * @notice module:core + * @dev The minimum number of vote tokens needed to cast a vote + */ + function votingThreshold() external view returns (uint256); + + /** + * @notice module:core + * @dev Timepoint used to retrieve user's votes and quorum. If using block number (as per Compound's Comp), the + * snapshot is performed at the end of this block. Hence, voting for this proposal starts at the beginning of the + * following block. + */ + function proposalSnapshot(uint256 proposalId) external view returns (uint256); + + /** + * @notice module:core + * @dev Timepoint at which votes close. If using block number, votes close at the end of this block, so it is + * possible to cast a vote during this block. + */ + function proposalDeadline(uint256 proposalId) external view returns (uint256); + + /** + * @notice module:core + * @dev The account that created a proposal. + */ + function proposalProposer(uint256 proposalId) external view returns (address); + + /** + * @notice module:core + * @dev The time when a queued proposal becomes executable ("ETA"). Unlike {proposalSnapshot} and + * {proposalDeadline}, this doesn't use the governor clock, and instead relies on the executor's clock which may be + * different. In most cases this will be a timestamp. + */ + function proposalEta(uint256 proposalId) external view returns (uint256); + + /** + * @notice module:core + * @dev Whether a proposal needs to be queued before execution. + */ + function proposalNeedsQueuing(uint256 proposalId) external view returns (bool); + + /** + * @notice module:user-config + * @dev Delay between the vote start and vote end. The unit this duration is expressed in depends on the clock + * (see EIP-6372) this contract uses. + * + * NOTE: The {votingDelay} can delay the start of the vote. This must be considered when setting the voting + * duration compared to the voting delay. + * + * NOTE: This value is stored when the proposal is submitted so that possible changes to the value do not affect + * proposals that have already been submitted. The type used to save it is a uint32. Consequently, while this + * interface returns a uint256, the value it returns should fit in a uint32. + */ + function votingPeriod() external view returns (uint256); + + /** + * @notice module:user-config + * @dev Minimum number of cast voted required for a proposal to be successful. + * + * NOTE: The `timepoint` parameter corresponds to the snapshot used for counting vote. This allows to scale the + * quorum depending on values such as the totalSupply of a token at this timepoint (see {ERC20Votes}). + */ + function quorum(uint256 timepoint) external view returns (uint256); + + /** + * @notice module:reputation + * @dev Voting power of an `account` at a specific `timepoint`. + * + * Note: this can be implemented in a number of ways, for example by reading the delegated balance from one (or + * multiple), {ERC20Votes} tokens. + */ + function getVotes(address account, uint256 timepoint) external view returns (uint256); + + /** + * @notice module:reputation + * @dev Voting power using quadratic voting of an `account` at a specific `timepoint`. + * + */ + function getQuadraticVotingPower(address account, uint256 timepoint) external view returns (uint256); + + /** + * @notice module:voting + * @dev Returns whether `account` has cast a vote on `proposalId`. + */ + function hasVoted(uint256 proposalId, address account) external view returns (bool); + + /** + * @dev Create a new proposal. Specify the allocation round when vote should become active. + * The duration is specified by {IGovernor-votingPeriod}. + * + * Emits a {ProposalCreated} event. + */ + function propose( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + string memory description, + uint256 startRoundId, + uint256 depositAmount + ) external returns (uint256 proposalId); + + /** + * @dev Queue a proposal. Some governors require this step to be performed before execution can happen. If queuing + * is not necessary, this function may revert. + * Queuing a proposal requires the quorum to be reached, the vote to be successful, and the deadline to be reached. + * + * Emits a {ProposalQueued} event. + */ + function queue( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) external returns (uint256 proposalId); + + /** + * @dev Execute a successful proposal. This requires the quorum to be reached, the vote to be successful, and the + * deadline to be reached. Depending on the governor it might also be required that the proposal was queued and + * that some delay passed. + * + * Emits a {ProposalExecuted} event. + * + * NOTE: Some modules can modify the requirements for execution, for example by adding an additional timelock. + */ + function execute( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) external payable returns (uint256 proposalId); + + /** + * @dev Cancel a proposal. A proposal is cancellable by the proposer, but only while it is Pending state, i.e. + * before the vote starts. + * + * Emits a {ProposalCanceled} event. + */ + function cancel( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) external returns (uint256 proposalId); + + /** + * @dev Cast a vote + * + * Emits a {VoteCast} event. + */ + function castVote(uint256 proposalId, uint8 support) external returns (uint256 balance); + + /** + * @dev Cast a vote with a reason + * + * Emits a {VoteCast} event. + */ + function castVoteWithReason( + uint256 proposalId, + uint8 support, + string calldata reason + ) external returns (uint256 balance); + + /** + * @dev Check if user has cast vote at least in one proposal. + */ + function hasVotedOnce(address user) external view returns (bool); + + /** + * @dev Round when the proposal should become active. + */ + function proposalStartRound(uint256 proposalId) external view returns (uint256); + + /** + * @dev Check if proposal can start in the next allocation round. + */ + function canProposalStartInNextRound() external view returns (bool); + + /** + * @dev Function to deposit tokens to a proposal + */ + function deposit(uint256 amount, uint256 proposalId) external; + + /** + * @dev Function to withdraw tokens from a proposal + */ + function withdraw(uint256 proposalId, address depositer) external; + + /** + * @dev Getter to retrieve the total amount of tokens deposited to a proposal + */ + function getProposalDeposits(uint256 proposalId) external view returns (uint256); + + /** + * @dev Function to check if the deposit threshold for a proposal has been reached + */ + function proposalDepositReached(uint256 proposalId) external view returns (bool); + + /** + * @dev Getter to retrieve the amount of tokens a specific user has deposited to a proposal + */ + function getUserDeposit(uint256 proposalId, address user) external view returns (uint256); +} diff --git a/contracts/governance/GovernorStorage.sol b/contracts/governance/GovernorStorage.sol index e3eed5e..33910d3 100644 --- a/contracts/governance/GovernorStorage.sol +++ b/contracts/governance/GovernorStorage.sol @@ -26,6 +26,7 @@ pragma solidity 0.8.20; import { GovernorStorageTypes } from "./libraries/GovernorStorageTypes.sol"; import { GovernorTypes } from "./libraries/GovernorTypes.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import { IVeBetterPassport } from "../interfaces/IVeBetterPassport.sol"; /// @title GovernorStorage /// @notice Contract used as storage of the B3TRGovernor contract. @@ -89,4 +90,9 @@ contract GovernorStorage is Initializable { // Set the governor votes storage governorStorage.votingThreshold = initializationData.initialVotingThreshold; } + + function __GovernorStorage_init_v4(IVeBetterPassport veBetterPassport) internal onlyInitializing { + GovernorStorageTypes.GovernorStorage storage governorStorage = getGovernorStorage(); + governorStorage.veBetterPassport = veBetterPassport; + } } diff --git a/contracts/governance/libraries/GovernorDepositLogic.sol b/contracts/governance/libraries/GovernorDepositLogic.sol index 8a6c186..a007d64 100644 --- a/contracts/governance/libraries/GovernorDepositLogic.sol +++ b/contracts/governance/libraries/GovernorDepositLogic.sol @@ -34,6 +34,9 @@ library GovernorDepositLogic { /// @dev Emitted when a deposit is made to a proposal. event ProposalDeposit(address indexed depositor, uint256 indexed proposalId, uint256 amount); + /// @dev Emitted when a deposit is withdrawn from a proposal. + event ProposalWithdraw(address indexed withdrawer, uint256 indexed proposalId, uint256 amount); + /// @dev Thrown when there is no deposit to withdraw. error GovernorNoDepositToWithdraw(uint256 proposalId, address depositer); @@ -97,6 +100,8 @@ library GovernorDepositLogic { self.deposits[proposalId][depositer] = 0; require(self.vot3.transfer(depositer, amount), "B3TRGovernor: transfer failed"); + + emit ProposalWithdraw(depositer, proposalId, amount); } /** diff --git a/contracts/governance/libraries/GovernorStorageTypes.sol b/contracts/governance/libraries/GovernorStorageTypes.sol index 0bdbc32..5bbdf6d 100644 --- a/contracts/governance/libraries/GovernorStorageTypes.sol +++ b/contracts/governance/libraries/GovernorStorageTypes.sol @@ -31,6 +31,7 @@ import { IVOT3 } from "../../interfaces/IVOT3.sol"; import { DoubleEndedQueue } from "@openzeppelin/contracts/utils/structs/DoubleEndedQueue.sol"; import { TimelockControllerUpgradeable } from "@openzeppelin/contracts-upgradeable/governance/TimelockControllerUpgradeable.sol"; import { Checkpoints } from "@openzeppelin/contracts/utils/structs/Checkpoints.sol"; +import { IVeBetterPassport } from "../../interfaces/IVeBetterPassport.sol"; /// @title GovernorStorageTypes /// @notice Library for defining storage types used in the Governor contract. @@ -84,11 +85,14 @@ library GovernorStorageTypes { mapping(uint256 => uint256) proposalTotalVotes; // minimum amount of tokens needed to cast a vote uint256 votingThreshold; - - // ------------------------------- Version 2 ------------------------------- + // ------------------------------- Version 3 ------------------------------- // ------------------------------- Voting Storage ------------------------------- // checkpoints for the quadratic voting status for each round Checkpoints.Trace208 quadraticVotingDisabled; + // ------------------------------- Version 2 ------------------------------- + + // ------------------------------- Passport ------------------------------- + IVeBetterPassport veBetterPassport; } } diff --git a/contracts/governance/libraries/GovernorVotesLogic.sol b/contracts/governance/libraries/GovernorVotesLogic.sol index f59a279..62a3534 100644 --- a/contracts/governance/libraries/GovernorVotesLogic.sol +++ b/contracts/governance/libraries/GovernorVotesLogic.sol @@ -50,6 +50,11 @@ library GovernorVotesLogic { /// @param votes The actual votes received. error GovernorVotingThresholdNotMet(uint256 threshold, uint256 votes); + /// @dev Thrown when the personhood verification fails. + /// @param voter The address of the voter. + /// @param explanation The reason for the failure. + error GovernorPersonhoodVerificationFailed(address voter, string explanation); + /// @notice Emitted when a vote is cast without parameters. /// @param voter The address of the voter. /// @param proposalId The ID of the proposal being voted on. @@ -230,12 +235,21 @@ library GovernorVotesLogic { ); uint256 proposalSnapshot = GovernorProposalLogic._proposalSnapshot(self, proposalId); + + (bool isPerson, string memory explanation) = self.veBetterPassport.isPersonAtTimepoint( + voter, + SafeCast.toUint48(proposalSnapshot) + ); + + // Check if the voter or the delegator of personhood to the voter is a person and returning error with the reason + if (!isPerson) { + revert GovernorPersonhoodVerificationFailed(voter, explanation); + } + uint256 weight = self.vot3.getPastVotes(voter, proposalSnapshot); uint256 power = Math.sqrt(weight) * 1e9; - if (weight < GovernorConfigurator.getVotingThreshold(self)) { - revert GovernorVotingThresholdNotMet(weight, GovernorConfigurator.getVotingThreshold(self)); - } + _checkVotingThreshold(self, weight); _countVote(self, proposalId, voter, support, weight, power); @@ -246,6 +260,17 @@ library GovernorVotesLogic { return weight; } + /** + * @notice Checks if the voting threshold is met. + * @param self - GovernorStorage + * @param weight - The weight of the vote. + */ + function _checkVotingThreshold(GovernorStorageTypes.GovernorStorage storage self, uint256 weight) private view { + uint256 threshold = GovernorConfigurator.getVotingThreshold(self); + if (weight < threshold) { + revert GovernorVotingThresholdNotMet(threshold, weight); + } + } /** * @notice Toggle quadratic voting for a specific cycle. * @dev This function toggles the state of quadratic voting for a specific cycle. @@ -253,7 +278,7 @@ library GovernorVotesLogic { * The state will flip between enabled and disabled each time the function is called. */ function toggleQuadraticVoting(GovernorStorageTypes.GovernorStorage storage self) external { - bool isQuadraticDisabled = self.quadraticVotingDisabled.upperLookupRecent(GovernorClockLogic.clock(self)) == 1; // 0: enabled, 1: disabled + bool isQuadraticDisabled = self.quadraticVotingDisabled.upperLookupRecent(GovernorClockLogic.clock(self)) == 1; // 0: enabled, 1: disabled // If quadratic voting is disabled, set the new status to enabled, otherwise set it to disabled. uint208 newStatus = isQuadraticDisabled ? 0 : 1; diff --git a/contracts/interfaces/IB3TRGovernor.sol b/contracts/interfaces/IB3TRGovernor.sol index 927e582..4ae0148 100644 --- a/contracts/interfaces/IB3TRGovernor.sol +++ b/contracts/interfaces/IB3TRGovernor.sol @@ -63,6 +63,13 @@ interface IB3TRGovernor is IERC165, IERC6372 { */ error GovernorInvalidQuorumFraction(uint256 quorumNumerator, uint256 quorumDenominator); + /** + * @dev Thrown when the personhood verification fails. + * @param voter The address of the voter. + * @param explanation The reason for the failure. + */ + error GovernorPersonhoodVerificationFailed(address voter, string explanation); + /** * @dev The current state of a proposal is not the required for performing an operation. * The `expectedStates` is a bitmap with the bits enabled for each ProposalState enum position diff --git a/contracts/interfaces/IGalaxyMember.sol b/contracts/interfaces/IGalaxyMember.sol index 9d2e3e1..4984619 100644 --- a/contracts/interfaces/IGalaxyMember.sol +++ b/contracts/interfaces/IGalaxyMember.sol @@ -223,4 +223,4 @@ interface IGalaxyMember { function xAllocationsGovernor() external view returns (address); function version() external view returns (string memory); -} \ No newline at end of file +} diff --git a/contracts/interfaces/IVeBetterPassport.sol b/contracts/interfaces/IVeBetterPassport.sol new file mode 100644 index 0000000..cd604d6 --- /dev/null +++ b/contracts/interfaces/IVeBetterPassport.sol @@ -0,0 +1,519 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.20; + +import { PassportTypes } from "../ve-better-passport/libraries/PassportTypes.sol"; +import { IX2EarnApps } from "./IX2EarnApps.sol"; +import { IXAllocationVotingGovernor } from "./IXAllocationVotingGovernor.sol"; + +interface IVeBetterPassport { + // ---------- 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); + + // ---------- 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( + PassportTypes.InitializationData calldata data, + PassportTypes.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(PassportTypes.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(PassportTypes.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 (PassportTypes.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(PassportTypes.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(PassportTypes.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, PassportTypes.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 Delegate personhood to another address + /// @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 Delegate the personhood to another address + /// @dev The passport 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 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/interfaces/IVoterRewards.sol b/contracts/interfaces/IVoterRewards.sol index 26942bc..bab07e6 100644 --- a/contracts/interfaces/IVoterRewards.sol +++ b/contracts/interfaces/IVoterRewards.sol @@ -109,4 +109,4 @@ interface IVoterRewards { function isQuadraticRewardingDisabledForCurrentCycle() external view returns (bool); function disableQuadraticRewarding(bool _disableQuadraticRewarding) external; -} \ No newline at end of file +} diff --git a/contracts/interfaces/IX2EarnRewardsPool.sol b/contracts/interfaces/IX2EarnRewardsPool.sol index 7494d5c..ec2ef50 100644 --- a/contracts/interfaces/IX2EarnRewardsPool.sol +++ b/contracts/interfaces/IX2EarnRewardsPool.sol @@ -52,6 +52,14 @@ interface IX2EarnRewardsPool { address indexed distributor ); + /** + * @dev Event emitted when the proof of sustainability external contract call fails. + * + * @param reason The reason for the failure. + * @param lowLevelData The low level data returned by the external contract. + */ + event RegisterActionFailed(string reason, bytes lowLevelData); + /** * @dev Retrieves the current version of the contract. * diff --git a/contracts/interfaces/IXAllocationVotingGovernor.sol b/contracts/interfaces/IXAllocationVotingGovernor.sol index 51c0492..0a28ee4 100644 --- a/contracts/interfaces/IXAllocationVotingGovernor.sol +++ b/contracts/interfaces/IXAllocationVotingGovernor.sol @@ -75,6 +75,17 @@ interface IXAllocationVotingGovernor is IERC165, IERC6372 { */ error GovernorInsufficientVotingPower(); + /** + * The `voter` is not identified as a person via the VeBetterPassport. + */ + error GovernorPersonhoodVerificationFailed(address person, string reason); + + + /** + * @dev The `person` is not identified as a person via the VeBetterPassport. + */ + error XAllocationVotingPersonhoodVerificationFailed(address person); + /** * @dev Emitted when a round is created. */ diff --git a/contracts/mocks/VechainNodes/SupportsInterface.sol b/contracts/mocks/VechainNodes/SupportsInterface.sol new file mode 100644 index 0000000..1f4b592 --- /dev/null +++ b/contracts/mocks/VechainNodes/SupportsInterface.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: MIT + +// Copyright (c) 2018 The VeChainThor developers + +// Distributed under the GNU Lesser General Public License v3.0 software license, see the accompanying +// file LICENSE or + +pragma solidity 0.8.20; + +import "./utility/interfaces/IERC165.sol"; + + +/** + * @title SupportsInterfaceWithLookup + * @dev Implements ERC165 using a lookup table. + */ +contract SupportsInterface is IERC165 { + /** + * 0x01ffc9a7 === + * bytes4(keccak256('supportsInterface(bytes4)')) + */ + bytes4 public constant InterfaceId_ERC165 = 0x01ffc9a7; + + /** + * @dev a mapping of interface id to whether or not it's supported + */ + mapping(bytes4 => bool) internal supportedInterfaces; + + /** + * @dev A contract implementing SupportsInterfaceWithLookup + * implement ERC165 itself + */ + constructor() + { + _registerInterface(InterfaceId_ERC165); + } + + /** + * @dev implement supportsInterface(bytes4) using a lookup table + */ + function supportsInterface(bytes4 _interfaceId) + external + view + returns(bool) + { + return supportedInterfaces[_interfaceId]; + } + + /** + * @dev private method for registering an interface + */ + function _registerInterface(bytes4 _interfaceId) + internal + { + require(_interfaceId != 0xffffffff, "invalid interfaceid"); + supportedInterfaces[_interfaceId] = true; + } +} \ No newline at end of file diff --git a/contracts/mocks/VechainNodes/ThunderFactory.sol b/contracts/mocks/VechainNodes/ThunderFactory.sol new file mode 100644 index 0000000..7d83b23 --- /dev/null +++ b/contracts/mocks/VechainNodes/ThunderFactory.sol @@ -0,0 +1,400 @@ +// SPDX-License-Identifier: MIT + +// Copyright (c) 2018 The VeChainThor developers + +// Distributed under the GNU Lesser General Public License v3.0 software license, see the accompanying +// file LICENSE or + +pragma solidity 0.8.20; + +import "./XAccessControl.sol"; +import "./auction/ClockAuction.sol"; + +abstract contract IEnergy { + function transfer(address _to, uint256 _amount) external virtual; +} + +contract ThunderFactory is XAccessControl { + + IEnergy constant Energy = IEnergy(0x0000000000000000000000000000456E65726779); + + /// @dev The address of the ClockAuction contract that handles sales of xtoken + ClockAuction public saleAuction; + /// @dev The interval between two transfers + uint64 public transferCooldown = 1 days; + /// @dev A time delay when to start monitor after the token is transfered + uint64 public leadTime = 4 hours; + + /// @dev The XToken param struct + struct TokenParameters { + uint256 minBalance; + uint64 ripeDays; + uint64 rewardRatio; + uint64 rewardRatioX; + } + + enum strengthLevel { + None, + + // Normal Token + Strength, + Thunder, + Mjolnir, + + // X Token + VeThorX, + StrengthX, + ThunderX, + MjolnirX + } + + /// @dev Mapping from strength level to token params + mapping(uint8 => TokenParameters) internal strengthParams; + + /// @dev The main Token struct. Each token is represented by a copy of this structure. + struct Token { + uint64 createdAt; + uint64 updatedAt; + + bool onUpgrade; + strengthLevel level; + + uint64 lastTransferTime; + } + + /// @dev An array containing the Token struct for all XTokens in existence. + /// The ID of each token is actually an index into this array and starts at 1. + Token[] internal tokens; + /// @dev The counter of normal tokens and xtokens + uint64 public normalTokenCount; + uint64 public xTokenCount; + + /// @dev Mapping from token ID to owner and its reverse mapping. + /// Every address can only hold one token at most. + mapping(uint256 => address) public idToOwner; + mapping(address => uint256) public ownerToId; + + // Mapping from token ID to approved address + mapping (uint256 => address) internal tokenApprovals; + + event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId); + event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId); + event NewUpgradeApply(uint256 indexed _tokenId, address indexed _applier, strengthLevel _level, uint64 _applyTime, uint64 _applyBlockno); + event CancelUpgrade(uint256 indexed _tokenId, address indexed _owner); + event LevelChanged(uint256 indexed _tokenId, address indexed _owner, strengthLevel _fromLevel, strengthLevel _toLevel); + event AuctionCancelled(uint256 indexed _auctionId, uint256 indexed _tokenId); + + constructor() { + // the index of valid tokens should start from 1 + tokens.push(Token(0, 0, false, strengthLevel.None, 0)); + + // The params of normal token + strengthParams[1] = TokenParameters(1000000 ether, 10, 0, 100); // Strength + strengthParams[2] = TokenParameters(5000000 ether, 20, 0, 150); // Thunder + strengthParams[3] = TokenParameters(15000000 ether, 30, 0, 200); // Mjolnir + + // The params of X tokens + strengthParams[4] = TokenParameters(600000 ether, 0, 25, 0); // VeThorX + strengthParams[5] = TokenParameters(1600000 ether, 30, 100, 100); // StrengthX + strengthParams[6] = TokenParameters(5600000 ether, 60, 150, 150); // ThunderX + strengthParams[7] = TokenParameters(15600000 ether, 90, 200, 200); // MjolnirX + } + + /// @dev To tell whether the address is holding an x token + function isX(address _target) + public + view + returns(bool) + { + // return false if given address doesn't hold a token + return tokens[ownerToId[_target]].level >= strengthLevel.VeThorX; + } + + /// @dev To tell whether the address is holding a normal token + function isNormalToken(address _target) + public + view + returns(bool) + { + // return false if given address doesn't hold a token + return isToken(_target) && !isX(_target); + } + + /// @dev To tell whether the address is holding a token(x or normal) + function isToken(address _target) + public + view + returns(bool) + { + return tokens[ownerToId[_target]].level > strengthLevel.None; + } + + /// @dev Apply for a token or upgrade the holding token. + /// Note that bypass the level is forbided, it has to upgrade one by one. + function applyUpgrade(strengthLevel _toLvl) + external + whenNotPaused + { + uint256 _tokenId = ownerToId[msg.sender]; + if (_tokenId == 0) { + // a new token + _tokenId = _add(msg.sender, strengthLevel.None, false); + } + + Token storage token = tokens[_tokenId]; + require(!token.onUpgrade, "still upgrading"); + require(!saleAuction.isOnAuction(_tokenId), "cancel auction first"); + + // Bypass check. Note that normal token couldn't upgrade to x token. + require( + uint8(token.level) + 1 == uint8(_toLvl) + && _toLvl != strengthLevel.VeThorX + && _toLvl <= strengthLevel.MjolnirX, + "invalid _toLvl"); + // The balance of msg.sender must meet the requirement of target level's minbalance + require(msg.sender.balance >= strengthParams[uint8(_toLvl)].minBalance, "insufficient balance"); + + token.onUpgrade = true; + token.updatedAt = uint64(block.timestamp); + + emit NewUpgradeApply(_tokenId, msg.sender, _toLvl, uint64(block.timestamp), uint64(block.number)); + } + + /// @dev Cancel the upgrade application. + /// Note that this method can be called by the token holder or admin. + function cancelUpgrade(uint256 _tokenId) + public + { + require(_exist(_tokenId), "token not exist"); + + Token storage token = tokens[_tokenId]; + address _owner = idToOwner[_tokenId]; + + require(token.onUpgrade, "not on upgrading"); + // The token holder or admin allowed. + require(_owner == msg.sender || operators[msg.sender], "permission denied"); + + if (token.level == strengthLevel.None) { + _destroy(_tokenId); + } else { + token.onUpgrade = false; + token.updatedAt = uint64(block.timestamp); + } + + emit CancelUpgrade(_tokenId, _owner); + } + + function getMetadata(uint256 _tokenId) + public + view + returns(address, strengthLevel, bool, bool, uint64, uint64, uint64) + { + if (_exist(_tokenId)) { + Token memory token = tokens[_tokenId]; + return ( + idToOwner[_tokenId], + token.level, + token.onUpgrade, + saleAuction.isOnAuction(_tokenId), + token.lastTransferTime, + token.createdAt, + token.updatedAt + ); + } + } + + function getTokenParams(strengthLevel _level) + public + view + returns(uint256, uint64, uint64, uint64) + { + TokenParameters memory _params = strengthParams[uint8(_level)]; + return (_params.minBalance, _params.ripeDays, _params.rewardRatio, _params.rewardRatioX); + } + + /// @dev To tell whether a token can be transfered. + function canTransfer(uint256 _tokenId) + public + view + returns(bool) + { + return + _exist(_tokenId) + && !tokens[_tokenId].onUpgrade + && !blackList[idToOwner[_tokenId]] // token not in black list + && block.timestamp > (tokens[_tokenId].lastTransferTime + transferCooldown); + } + + /// Admin Methods + + function setTransferCooldown(uint64 _cooldown) + external + onlyOperator + { + transferCooldown = _cooldown; + } + + function setLeadTime(uint64 _leadtime) + external + onlyOperator + { + leadTime = _leadtime; + } + + /// @dev Upgrade a token to the passed level. + function upgradeTo(uint256 _tokenId, strengthLevel _toLvl) + external + onlyOperator + { + require(tokens[_tokenId].level < _toLvl, "invalid level"); + require(!saleAuction.isOnAuction(_tokenId), "cancel auction first"); + + tokens[_tokenId].onUpgrade = false; + + _levelChange(_tokenId, _toLvl); + } + + /// @dev Downgrade a token to the passed level. + function downgradeTo(uint256 _tokenId, strengthLevel _toLvl) + external + onlyOperator + { + require(tokens[_tokenId].level > _toLvl, "invalid level"); + require(block.timestamp > (tokens[_tokenId].lastTransferTime + leadTime), "cannot downgrade token"); + + if (saleAuction.isOnAuction(_tokenId)) { + _cancelAuction(_tokenId); + } + if (tokens[_tokenId].onUpgrade) { + cancelUpgrade(_tokenId); + } + + _levelChange(_tokenId, _toLvl); + } + + /// @dev Adds a new token and stores it. This method should be called + /// when the input data is block.timestampn to be valid and will generate a Transfer event. + function addToken(address _addr, strengthLevel _lvl, bool _onUpgrade, uint64 _applyUpgradeTime, uint64 _applyUpgradeBlockno) + external + onlyOperator + { + require(!_exist(_addr), "you already hold a token"); + + // This will assign ownership, and also emit the Transfer event. + uint256 newTokenId = _add(_addr, _lvl, _onUpgrade); + + // Update token counter + if(strengthLevel.Strength <= _lvl && _lvl <= strengthLevel.Mjolnir) normalTokenCount++; + else if(strengthLevel.VeThorX <= _lvl && _lvl <= strengthLevel.MjolnirX) xTokenCount++; + + // For data imgaration + if (_onUpgrade) { + emit NewUpgradeApply(newTokenId, _addr, _lvl, _applyUpgradeTime, _applyUpgradeBlockno); + } + } + + /// @dev Send VTHO bonus to the token's holder + function sendBonusTo(address _to, uint256 _amount) + external + onlyOperator + { + require(_to != address(0), "invalid address"); + require(_amount > 0, "invalid amount"); + // Transfer VTHO from this contract to _to address, it will throw when fail + Energy.transfer(_to, _amount); + } + + /// Internal Methods + + function _add(address _owner, strengthLevel _lvl, bool _onUpgrade) + internal + returns(uint256) + { + Token memory _token = Token(uint64(block.timestamp), uint64(block.timestamp), _onUpgrade, _lvl, uint64(block.timestamp)); + tokens.push(_token); // Push the token to the array + uint256 _newTokenId = tokens.length - 1; // Get the index of the new token, which is the length of the array minus one + + ownerToId[_owner] = _newTokenId; + idToOwner[_newTokenId] = _owner; + + emit Transfer(address(0), _owner, _newTokenId); // Emit a Transfer event indicating a new token creation + + return _newTokenId; + } + + function _destroy(uint256 _tokenId) + internal + { + address _owner = idToOwner[_tokenId]; + delete idToOwner[_tokenId]; + delete ownerToId[_owner]; + delete tokens[_tokenId]; + // + emit Transfer(_owner, address(0), _tokenId); + } + + function _levelChange(uint256 _tokenId, strengthLevel _toLvl) + internal + { + address _owner = idToOwner[_tokenId]; + Token storage token = tokens[_tokenId]; + + strengthLevel _fromLvl = token.level; + if (_toLvl == strengthLevel.None) { + _destroy(_tokenId); + } else { + token.level = _toLvl; + token.updatedAt = uint64(block.timestamp); + } + + // Update token counter + if(strengthLevel.Strength <= _fromLvl && _fromLvl <= strengthLevel.Mjolnir) { + normalTokenCount--; + } else if(strengthLevel.VeThorX <= _fromLvl && _fromLvl <= strengthLevel.MjolnirX) { + xTokenCount--; + } + if(strengthLevel.Strength <= _toLvl && _toLvl <= strengthLevel.Mjolnir ) { + normalTokenCount++; + } else if(strengthLevel.VeThorX <= _toLvl && _toLvl <= strengthLevel.MjolnirX ) { + xTokenCount++; + } + + emit LevelChanged(_tokenId, _owner, _fromLvl, _toLvl); + } + + function _exist(uint256 _tokenId) + internal + view + returns(bool) + { + return idToOwner[_tokenId] > address(0); + } + + function _exist(address _owner) + internal + view + returns(bool) + { + return ownerToId[_owner] > 0; + } + + /// @notice Internal function to clear current approval of a given token ID + /// @param _tokenId uint256 ID of the token to be transferred + function _clearApproval(uint256 _tokenId) + internal + { + delete tokenApprovals[_tokenId]; + } + + /// @notice Internal function to cancel the ongoing auction + /// @param _tokenId uint256 ID of the token + function _cancelAuction(uint256 _tokenId) + internal + { + _clearApproval(_tokenId); + (uint256 _autionId,,,,,) = saleAuction.getAuction(_tokenId); + emit AuctionCancelled(_autionId, _tokenId); + saleAuction.cancelAuction(_tokenId); + } + +} diff --git a/contracts/mocks/VechainNodes/TokenAuction.sol b/contracts/mocks/VechainNodes/TokenAuction.sol new file mode 100644 index 0000000..b73242b --- /dev/null +++ b/contracts/mocks/VechainNodes/TokenAuction.sol @@ -0,0 +1,185 @@ +// SPDX-License-Identifier: MIT + +// Copyright (c) 2018 The VeChainThor developers + +// Distributed under the GNU Lesser General Public License v3.0 software license, see the accompanying +// file LICENSE or + +pragma solidity 0.8.20; + +import "./XOwnership.sol"; +import "./auction/ClockAuction.sol"; +import "./utility/SafeMath.sol"; + +contract TokenAuction is XOwnership { + + using SafeMath for uint256; + + uint256 public auctionCount; + + event AuctionCreated( + uint256 indexed _auctionId, + uint256 indexed _tokenId, + uint256 _startingPrice, + uint256 _endingPrice, + uint64 _duration + ); + event AuctionSuccessful( + uint256 indexed _auctionId, + uint256 indexed _tokenId, + address indexed _seller, + address _winner, + uint256 _finalPrice + ); + + event AddAuctionWhiteList(uint256 indexed _auctionId, uint256 indexed _tokenId, address indexed _candidate); + event RemoveAuctionWhiteList(uint256 indexed _auctionId, uint256 indexed _tokenId, address indexed _candidate); + + /// @dev Sets the reference to the sale auction. + /// @param _address - Address of sale contract. + function setSaleAuctionAddress(address _address) + public + onlyOwner + { + require(_address != address(0), "invalid address"); + saleAuction = ClockAuction(_address); + emit ProtocolUpgrade(_address); + } + + + /// @dev Put a token up for auction. + function createSaleAuction( + uint256 _tokenId, + uint128 _startingPrice, + uint128 _endingPrice, + uint64 _duration + ) + public + whenNotPaused + { + require(ownerOf(_tokenId) == msg.sender, "permission denied"); + require(isToken(msg.sender), "is not a token"); + require(!tokens[_tokenId].onUpgrade, "cancel upgrading first"); + + // Does some ownership trickery to create auctions in one tx. + _approve(_tokenId, address(saleAuction)); + + auctionCount = auctionCount.add(1); + // If token is already on any auction, this will throw + saleAuction.createAuction( + auctionCount, + _tokenId, + _startingPrice, + _endingPrice, + _duration, + uint64(block.timestamp), + msg.sender + ); + + emit AuctionCreated( + auctionCount, + _tokenId, + _startingPrice, + _endingPrice, + _duration + ); + } + + /// @dev Put a token up for directional auction. + function createDirectionalSaleAuction( + uint256 _tokenId, + uint128 _price, + uint64 _duration, + address _toAddress + ) + public + whenNotPaused + { + require(ownerOf(_tokenId) == msg.sender, "permission denied"); + require(isToken(msg.sender), "is not a token"); + require(!tokens[_tokenId].onUpgrade, "cancel upgrading first"); + + // Does some ownership trickery to create auctions in one tx. + _approve(_tokenId, address(saleAuction)); + + auctionCount = auctionCount.add(1); + + // If token is already on any auction, this will throw + saleAuction.createAuction( + auctionCount, + _tokenId, + _price, + _price, + _duration, + uint64(block.timestamp), + msg.sender + ); + + emit AuctionCreated( + auctionCount, + _tokenId, + _price, + _price, + _duration + ); + + // Set candidates + saleAuction.addAuctionWhiteList(_tokenId, _toAddress); + emit AddAuctionWhiteList(auctionCount, _tokenId, _toAddress); + } + + /// @dev Bids on an open auction, completing the auction and transferring + /// ownership of the NFT if enough Ether is supplied. + function bid(uint256 _tokenId) + public + payable + whenNotPaused + { + (uint256 _autionId, address _seller,,,,) = saleAuction.getAuction(_tokenId); + + // Will throw if the bid fails + uint256 _price = saleAuction.bid{value: msg.value}(msg.sender, _tokenId); + + emit AuctionSuccessful(_autionId, _tokenId, _seller, msg.sender, _price); + } + + /// @dev Cancels an auction that hasn't been won yet. + /// This methods can be called while the protocol is paused. + function cancelAuction(uint256 _tokenId) + public + whenNotPaused + { + require(ownerOf(_tokenId) == msg.sender, "permission denied"); + + _cancelAuction(_tokenId); + } + + /// @dev Add condidate for the auction of the passed token. + function addAuctionWhiteList(uint256 _tokenId, address _address) + public + whenNotPaused + { + require(ownerOf(_tokenId) == msg.sender, "permission denied"); + require(isToken(msg.sender), "is not a token"); + saleAuction.addAuctionWhiteList(_tokenId, _address); + + (uint256 _autionId,,,,,) = saleAuction.getAuction(_tokenId); + + emit AddAuctionWhiteList(_autionId, _tokenId, _address); + } + + /// @dev Remove address from whitelist. + function removeAuctionWhiteList(uint256 _tokenId, address _address) + public + whenNotPaused + { + require(ownerOf(_tokenId) == msg.sender, "permission denied"); + require(isToken(msg.sender), "is not a token"); + saleAuction.removeAuctionWhiteList(_tokenId, _address); + + (uint256 _autionId,,,,,) = saleAuction.getAuction(_tokenId); + + emit RemoveAuctionWhiteList(_autionId, _tokenId, _address); + } + +} diff --git a/contracts/mocks/VechainNodes/XAccessControl.sol b/contracts/mocks/VechainNodes/XAccessControl.sol new file mode 100644 index 0000000..1a68207 --- /dev/null +++ b/contracts/mocks/VechainNodes/XAccessControl.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: MIT + +// Copyright (c) 2018 The VeChainThor developers + +// Distributed under the GNU Lesser General Public License v3.0 software license, see the accompanying +// file LICENSE or + +pragma solidity 0.8.20; + +import "./utility/Pausable.sol"; + +contract XAccessControl is Pausable { + event ProtocolUpgrade(address _saleAuction); + event OperatorUpdated(address _op, bool _enabled); + event BlackListUpdated(address _person, bool _op); + + mapping(address => bool) public operators; + mapping(address => bool) public blackList; + + modifier onlyOperator { + require(operators[msg.sender], "permission denied"); + _; + } + + modifier inBlackList { + require(blackList[msg.sender], "operation blocked"); + _; + } + + modifier notInBlackList { + require(!blackList[msg.sender], "operation blocked"); + _; + } + + function addOperator(address _operator) + external + onlyOwner + whenNotPaused + { + operators[_operator] = true; + emit OperatorUpdated(_operator, true); + } + + function removeOperator(address _operator) + external + onlyOwner + whenNotPaused + { + operators[_operator] = false; + emit OperatorUpdated(_operator, false); + } + + function addToBlackList(address _badGuy) + external + onlyOwner + whenNotPaused + { + blackList[_badGuy] = true; + emit BlackListUpdated(_badGuy, true); + } + + function removeFromBlackList(address _innocent) + external + onlyOwner + whenNotPaused + { + blackList[_innocent] = false; + emit BlackListUpdated(_innocent, false); + } +} + diff --git a/contracts/mocks/VechainNodes/XOwnership.sol b/contracts/mocks/VechainNodes/XOwnership.sol new file mode 100644 index 0000000..4be8e4c --- /dev/null +++ b/contracts/mocks/VechainNodes/XOwnership.sol @@ -0,0 +1,215 @@ +// SPDX-License-Identifier: MIT + +// Copyright (c) 2018 The VeChainThor developers + +// Distributed under the GNU Lesser General Public License v3.0 software license, see the accompanying +// file LICENSE or + +pragma solidity 0.8.20; + +import "./utility/Strings.sol"; +import "./utility/SafeMath.sol"; + +import "./SupportsInterface.sol"; +import "./ThunderFactory.sol"; + +import "./utility/interfaces/IVIP181Basic.sol"; + + +contract XOwnership is ThunderFactory, IVIP181Basic, SupportsInterface { + + using SafeMath for uint256; + + string public name = "VeChainThor Node Token"; + string public symbol = "VNT"; + + string internal tokenMetadataBaseURI = ""; + + constructor() { + // register the supported interfaces to conform to VIP181 via ERC165 + _registerInterface(InterfaceId_VIP181); + _registerInterface(InterfaceId_VIP181Metadata); + } + + function tokenURI(uint256 _tokenId) + external + view + returns (string memory) + { + return Strings.strConcat(tokenMetadataBaseURI, Strings.uint2str(_tokenId)); + } + + /// @dev Gets the balance of the specified address + /// @param _owner address to query the balance of + /// @return uint256 representing the amount owned by the passed address + function balanceOf(address _owner) + public + override + view + returns (uint256) + { + // Everyone can only possess one token at most + return isToken(_owner) ? 1 : 0; + } + + /// @dev Gets the owner of the specified token ID + /// @param _tokenId uint256 ID of the token to query the owner of + /// @return owner address currently marked as the owner of the given token ID + function ownerOf(uint256 _tokenId) + public + override + view + returns (address) + { + return idToOwner[_tokenId]; + } + + function totalSupply() + public + view + returns(uint256) + { + return uint256(normalTokenCount + xTokenCount); + } + + function setTokenMetadataBaseURI(string memory _newBaseURI) + external + onlyOwner + { + tokenMetadataBaseURI = _newBaseURI; + } + + /// @dev Approves another address to transfer the given token ID + /// The zero address indicates there is no approved address. + /// There can only be one approved address per token at a given time. + /// Can only be called by the token owner or an approved operator. + /// @param _to address to be approved for the given token ID + /// @param _tokenId uint256 ID of the token to be approved + function approve(address _to, uint256 _tokenId) + public + override + whenNotPaused + { + address _owner = ownerOf(_tokenId); + require(_to != _owner, "cannot approve your own token"); + require(msg.sender == _owner, "permission denied"); + + _approve(_tokenId, _to); + } + + /// @dev Gets the approved address for a token ID, or zero if no address set + /// @param _tokenId uint256 ID of the token to query the approval of + /// @return address currently approved for the given token ID + function getApproved(uint256 _tokenId) + public + override + view + returns (address) + { + return tokenApprovals[_tokenId]; + } + + function transfer(address _to, uint256 _tokenId) + public + whenNotPaused + { + require(_to != address(0), "invalid address"); + // Can only transfer your own token. + require(ownerOf(_tokenId) == msg.sender, "permission denied"); + // Token is not in blacklist and cooldown time + require(canTransfer(_tokenId), "cannot transfer this token"); + + if (saleAuction.isOnAuction(_tokenId)) { + _cancelAuction(_tokenId); + } + + _clearApprovalAndTransfer(msg.sender, _to, _tokenId); + } + + /// @dev Transfers the ownership of a given token ID to another address + /// Requires the msg sender to be the owner, approved, or operator + /// @param _from current owner of the token + /// @param _to address to receive the ownership of the given token ID + /// @param _tokenId uint256 ID of the token to be transferred + function transferFrom(address _from, address _to, uint256 _tokenId) + public + override + whenNotPaused + { + require(_to != address(0), "invalid address"); + // Check for approval and valid ownership + require(ownerOf(_tokenId) == _from, "permission denied"); + require(isApprovedOrOwner(msg.sender, _tokenId), "permission denied"); + // Token is not in blacklist and cooldown time + require(canTransfer(_tokenId), "cannot transfer this token"); + + if (saleAuction.isOnAuction(_tokenId)) { + _cancelAuction(_tokenId); + } + + _clearApprovalAndTransfer(_from, _to, _tokenId); + } + + /// Internal Functions + + /// @dev Returns whether the given spender can transfer a given token ID + /// @param _spender address of the spender to query + /// @param _tokenId uint256 ID of the token to be transferred + /// @return bool whether the msg.sender is approved for the given token ID, + /// is an operator of the owner, or is the owner of the token + function isApprovedOrOwner(address _spender, uint256 _tokenId) + internal + view + returns (bool) + { + address _owner = ownerOf(_tokenId); + return (_spender == _owner || getApproved(_tokenId) == _spender); + } + + function _clearApprovalAndTransfer(address _from, address _to, uint256 _tokenId) + internal + { + _clearApproval(_tokenId); + _transfer(_from, _to, _tokenId); + } + + // INTERNAL FUNCTIONS + + function _approve(uint256 _tokenId, address _to) + internal + { + tokenApprovals[_tokenId] = _to; + address _owner = ownerOf(_tokenId); + emit Approval(_owner, _to, _tokenId); + } + + function _transfer(address _from, address _to, uint256 _tokenId) + internal + { + require(!_exist(_to), "_to already hold a token"); + require(!_isContract(_to), "_to mustn't a contract"); + + // update the token info and cooldown the token + tokens[_tokenId].updatedAt = uint64(block.timestamp); + tokens[_tokenId].lastTransferTime = uint64(block.timestamp); + + // transfer ownership + delete ownerToId[_from]; + idToOwner[_tokenId] = _to; + ownerToId[_to] = _tokenId; + + emit Transfer(_from, _to, _tokenId); + } + + function _isContract(address addr) + internal + view + returns (bool) + { + uint size; + /* solium-disable-next-line security/no-inline-assembly */ + assembly { size := extcodesize(addr) } + return size > 0; + } + +} \ No newline at end of file diff --git a/contracts/mocks/VechainNodes/auction/ClockAuction.sol b/contracts/mocks/VechainNodes/auction/ClockAuction.sol new file mode 100644 index 0000000..2c313c8 --- /dev/null +++ b/contracts/mocks/VechainNodes/auction/ClockAuction.sol @@ -0,0 +1,210 @@ +// SPDX-License-Identifier: MIT + +// Copyright (c) 2018 The VeChainThor developers + +// Distributed under the GNU Lesser General Public License v3.0 software license, see the accompanying +// file LICENSE or + +pragma solidity 0.8.20; + +import "./ClockAuctionBase.sol"; + +import "../utility/interfaces/IVIP181.sol"; + + +contract ClockAuction is ClockAuctionBase { + + /// @dev Constructor creates a reference to the token ownership contract + /// and verifies the owner cut is in the valid range. + /// @param _nftAddress - address of a deployed contract implementing + /// the Nonfungible Interface. + constructor(address _nftAddress, address _feePool) { + require(_nftAddress != address(0), "invalid address"); + require(_feePool != address(0), "invalid address"); + VIP181 = IVIP181(_nftAddress); + + feePool = payable(_feePool); + } + + /// @dev Creates and begins a new auction. + /// @param _auctionId - ID of auction. + /// @param _tokenId - ID of token to auction, sender must be owner. + /// @param _startingPrice - Price of item (in wei) at beginning of auction. + /// @param _endingPrice - Price of item (in wei) at end of auction. + /// @param _duration - Length of time to move between starting + /// price and ending price (in seconds). + /// @param _startedAt - StartedAt, if not the message sender + /// @param _seller - Seller, if not the message sender + function createAuction( + uint256 _auctionId, + uint256 _tokenId, + uint128 _startingPrice, + uint128 _endingPrice, + uint64 _duration, + uint64 _startedAt, + address _seller + ) + external + whenNotPaused + { + require(msg.sender == address(VIP181), "permission denied"); + require(!isOnAuction(_tokenId), "token is on auction"); + // the duration of any auction should between 2 hours and 7 days. + require(_duration >= 2 hours, "at least 2 hours"); + require(_duration <= 7 days, "at most 7 days"); + + // remove expired auction first if exist + _cancelAuction(_tokenId); + // add new auction + _addAuction(_auctionId, _tokenId, _startingPrice, _endingPrice, _duration, _startedAt, payable(_seller)); + } + + /// @dev Bids on an open auction, completing the auction and transferring + /// ownership of the token if enough Ether is supplied. + /// @param _buyer - address of token buyer. + /// @param _tokenId - ID of token to bid on. + function bid(address _buyer, uint256 _tokenId) + external + payable + whenNotPaused + returns(uint256) + { + require(msg.sender == address(VIP181), "permission denied"); + require(isOnAuction(_tokenId), "auction not found"); + // if the candidates not empty check the _buyer in + if(hasWhiteList(_tokenId)) { + require(inWhiteList(_tokenId, _buyer), "blocked"); + } + + Auction storage auction = tokenIdToAuction[_tokenId]; + address payable _seller = payable(auction.seller); + // _bid will throw if the bid or funds transfer fails + uint256 _price = _bid(payable(_buyer), _tokenId, msg.value); + + VIP181.transferFrom(_seller, _buyer, _tokenId); + return _price; + } + + /// @dev Cancels an auction that hasn't been won yet. + /// @param _tokenId - ID of token on auction + function cancelAuction(uint256 _tokenId) + external + whenNotPaused + { + require(msg.sender == address(VIP181), "permission denied"); + require(exist(_tokenId), "auction not found"); + _cancelAuction(_tokenId); + } + + /// @dev Returns auction info for an token on auction. + /// @param _tokenId - ID of token on auction. + function getAuction(uint256 _tokenId) + public + view + returns ( + uint256 autionId, + address seller, + uint256 startingPrice, + uint256 endingPrice, + uint64 duration, + uint64 startedAt + ) { + Auction storage auction = tokenIdToAuction[_tokenId]; + + return ( + auction.auctionId, + auction.seller, + auction.startingPrice, + auction.endingPrice, + auction.duration, + auction.startedAt + ); + } + + /// @dev Returns true if the auction exists + function exist(uint256 _tokenId) + public + view + returns(bool) + { + return tokenIdToAuction[_tokenId].auctionId > 0; + } + + /// @dev Returns true if the token is on auction. + function isOnAuction(uint256 _tokenId) + public + view + returns (bool) + { + Auction storage _auction = tokenIdToAuction[_tokenId]; + return _auction.startedAt > 0 && _auction.startedAt <= block.timestamp && block.timestamp < (_auction.startedAt + _auction.duration); + } + + /// @dev Returns the current price of an auction. + /// @param _tokenId - ID of the token price we are checking. + function getCurrentPrice(uint256 _tokenId) + public + view + returns (uint256) + { + if (!isOnAuction(_tokenId)) { + return 0; + } + Auction storage auction = tokenIdToAuction[_tokenId]; + return _currentPrice(auction); + } + + function hasWhiteList(uint256 _tokenId) + public + view + returns (bool) + { + uint256 _auctionId = tokenIdToAuction[_tokenId].auctionId; + // always return false when tokenId is not on auction. + return auctionWhiteList[_auctionId].count > 0; + } + + function inWhiteList(uint256 _tokenId, address _address) + public + view + returns (bool) + { + uint256 _auctionId = tokenIdToAuction[_tokenId].auctionId; + // always return false when tokenId is not on auction. + return auctionWhiteList[_auctionId].whiteList[_address]; + } + + /// @dev Add condidate for the auction of the passed token. + function addAuctionWhiteList(uint256 _tokenId, address _address) + external + whenNotPaused + { + require(msg.sender == address(VIP181), "permission denied"); + require(isOnAuction(_tokenId), "auction not found"); + require(!inWhiteList(_tokenId, _address), "in the list"); + + uint256 _auctionId = tokenIdToAuction[_tokenId].auctionId; + uint64 _count = auctionWhiteList[_auctionId].count; + auctionWhiteList[_auctionId].count++; + + // Overflow check + assert(_count < auctionWhiteList[_auctionId].count); + + auctionWhiteList[_auctionId].whiteList[_address] = true; + } + + /// @dev Remove address from whitelist. + function removeAuctionWhiteList(uint256 _tokenId, address _address) + external + whenNotPaused + { + require(msg.sender == address(VIP181), "permission denied"); + require(isOnAuction(_tokenId), "auction not found"); + require(inWhiteList(_tokenId, _address), "not in the list"); + + uint256 _auctionId = tokenIdToAuction[_tokenId].auctionId; + auctionWhiteList[_auctionId].count--; + auctionWhiteList[_auctionId].whiteList[_address] = false; + } + +} diff --git a/contracts/mocks/VechainNodes/auction/ClockAuctionBase.sol b/contracts/mocks/VechainNodes/auction/ClockAuctionBase.sol new file mode 100644 index 0000000..ababab7 --- /dev/null +++ b/contracts/mocks/VechainNodes/auction/ClockAuctionBase.sol @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: MIT + +// Copyright (c) 2018 The VeChainThor developers + +// Distributed under the GNU Lesser General Public License v3.0 software license, see the accompanying +// file LICENSE or + +pragma solidity 0.8.20; + +import "../utility/Pausable.sol"; +import "../utility/SafeMath.sol"; +import "../utility/interfaces/IVIP181.sol"; + +contract ClockAuctionBase is Pausable { + using SafeMath for uint256; + + struct Auction { + uint256 auctionId; + address payable seller; + uint128 startingPrice; + uint128 endingPrice; + uint64 duration; + uint64 startedAt; + } + + struct WhiteList { + mapping(address => bool) whiteList; + uint64 count; + } + + // The reference of VIP181 Token + IVIP181 public VIP181; + + // The address of storing service fee + address payable public feePool; + uint8 public feePercnt = 0; // 0% + + // Mapping from tokenId to auction struct + mapping(uint256 => Auction) tokenIdToAuction; + // Mapping from auctionId to whitelist + mapping(uint256 => WhiteList) auctionWhiteList; + + // Events + event FeePoolAddressUpdated(address _newFeePoolAddr); + event FeePercentUpdated(uint8 _newPercent); + + function setFeePoolAddress(address payable _newFeePoolAddr) + public + onlyOwner + { + require(_newFeePoolAddr != address(0), "invalid address"); + feePool = _newFeePoolAddr; + emit FeePoolAddressUpdated(_newFeePoolAddr); + } + + function setFeePercent(uint8 _newPercent) + public + onlyOwner + { + require(_newPercent < 100, "must less than 100"); + feePercnt = _newPercent; + emit FeePercentUpdated(_newPercent); + } + + /// Internal Methods + + /// @dev Adds an auction to the list of open auctions. Also fires the AuctionCreated event. + function _addAuction( + uint256 _auctionId, + uint256 _tokenId, + uint128 _startingPrice, + uint128 _endingPrice, + uint64 _duration, + uint64 _startedAt, + address payable _seller + ) + internal + { + Auction memory _auction = Auction( + _auctionId, + _seller, + _startingPrice, + _endingPrice, + _duration, + _startedAt + ); + + tokenIdToAuction[_tokenId] = _auction; + } + + /// @dev Computes the price and transfers winnings. Does NOT transfer ownership of token. + function _bid(address payable _buyer, uint256 _tokenId, uint256 _bidAmount) + internal + returns (uint256) + { + Auction storage auction = tokenIdToAuction[_tokenId]; + + // Check that the bid is greater than or equal to the current price + uint256 price = _currentPrice(auction); + require(_bidAmount >= price, "purchase failed"); + + address payable _seller = auction.seller; + + // Remove auction before sending the fees to the sender to avoid the reentrancy attack. + _cancelAuction(_tokenId); + + // Transfer proceeds to seller if there are any + if (price > 0) { + uint256 _fee = price.mul(feePercnt) / 100; + uint256 _price = price.sub(_fee); + feePool.transfer(_fee); + _seller.transfer(_price); + } + + // Calculate any excess funds included with the bid and transfer it back to bidder. + uint256 bidExcess = _bidAmount.sub(price); + + // Return the funds + _buyer.transfer(bidExcess); + + return price; + } + + /// @dev Cancels an auction unconditionally. + /// It will removes an auction from the list of open auctions. + function _cancelAuction(uint256 _tokenId) + internal + { + delete auctionWhiteList[tokenIdToAuction[_tokenId].auctionId]; + delete tokenIdToAuction[_tokenId]; + } + + /// @dev Returns current price of an token on auction. Broken into two + /// functions (this one, that computes the duration from the auction + /// structure, and the other that does the price computation) so we + /// can easily test that the price computation works correctly. + function _currentPrice(Auction storage _auction) + internal + view + returns (uint256) + { + uint64 secondsPassed = uint64(block.timestamp) - _auction.startedAt; + + if (secondsPassed >= _auction.duration) { + // auction has expired then return the end price. + return _auction.endingPrice; + } + + // Count the times of price-change. + // The price of auction will change per 300s + uint256 changeTimes = (uint256(secondsPassed) - 1) / 300; + // The total price-change times + uint256 totalTimes = (uint256(_auction.duration) - 1) / 300; + // The amount of every change + uint256 perTimesChange = (uint256(_auction.endingPrice) - uint256(_auction.startingPrice)) / totalTimes; + + return uint256(uint256(_auction.startingPrice) + changeTimes * perTimesChange); + } + +} \ No newline at end of file diff --git a/contracts/mocks/VechainNodes/utility/Ownable.sol b/contracts/mocks/VechainNodes/utility/Ownable.sol new file mode 100644 index 0000000..cd113e5 --- /dev/null +++ b/contracts/mocks/VechainNodes/utility/Ownable.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT + +// Copyright (c) 2018 The VeChainThor developers + +// Distributed under the GNU Lesser General Public License v3.0 software license, see the accompanying +// file LICENSE or + +pragma solidity 0.8.20; + +contract Ownable { + address public owner; + + // Emit when ownership transfer to new owner + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + + /// @dev The Ownable constructor sets the original `owner` of the contract to the sender + /// account. + constructor() { + owner = msg.sender; + } + + /// @dev Throws if called by any account other than the owner. + modifier onlyOwner() { + require(msg.sender == owner, "only owner"); + _; + } + + /// @dev Allows the current owner to transfer control of the contract to a newOwner. + /// @param newOwner The address to transfer ownership to. + function transferOwnership(address newOwner) public onlyOwner { + require(newOwner != address(0), "invalid address"); + emit OwnershipTransferred(owner, newOwner); + owner = newOwner; + } + +} diff --git a/contracts/mocks/VechainNodes/utility/Pausable.sol b/contracts/mocks/VechainNodes/utility/Pausable.sol new file mode 100644 index 0000000..f9ce5d2 --- /dev/null +++ b/contracts/mocks/VechainNodes/utility/Pausable.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT + +// Copyright (c) 2018 The VeChainThor developers + +// Distributed under the GNU Lesser General Public License v3.0 software license, see the accompanying +// file LICENSE or + +pragma solidity 0.8.20; +import "./Ownable.sol"; + +/// @title Pausable +/// @dev Base contract which allows children to implement an emergency stop mechanism. +contract Pausable is Ownable { + event Pause(); + event Unpause(); + + bool public paused = false; + + modifier whenNotPaused() { + require(!paused, "protocol has paused"); + _; + } + + modifier whenPaused { + require(paused, "needs protocol paused"); + _; + } + + /// @dev called by the owner to pause, triggers stopped state + function pause() + public + onlyOwner + whenNotPaused + returns (bool) + { + paused = true; + emit Pause(); + return true; + } + + /// @dev called by the owner to unpause, returns to normal state + function unpause() + public + onlyOwner + whenPaused + returns (bool) + { + paused = false; + emit Unpause(); + return true; + } +} \ No newline at end of file diff --git a/contracts/mocks/VechainNodes/utility/SafeMath.sol b/contracts/mocks/VechainNodes/utility/SafeMath.sol new file mode 100644 index 0000000..1fb2970 --- /dev/null +++ b/contracts/mocks/VechainNodes/utility/SafeMath.sol @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: MIT + +// Copyright (c) 2018 The VeChainThor developers + +// Distributed under the GNU Lesser General Public License v3.0 software license, see the accompanying +// file LICENSE or + +pragma solidity 0.8.20; + +library SafeMath { + + /** + * @dev Subtracts two numbers, throws on overflow (i.e. if subtrahend is greater than minuend). + */ + function sub(uint256 a, uint256 b) + internal + pure + returns (uint256) + { + require(b <= a, "SafeMath sub failed"); + return a - b; + } + + /** + * @dev Adds two numbers, throws on overflow. + */ + function add(uint256 a, uint256 b) + internal + pure + returns (uint256 c) + { + c = a + b; + require(c >= a, "SafeMath add failed"); + return c; + } + + /** + * @dev Multiplies two numbers, throws on overflow. + */ + function mul(uint256 a, uint256 b) + internal + pure + returns (uint256 c) + { + if (a == 0) { + return 0; + } + c = a * b; + require(c / a == b, "SafeMath mul failed"); + return c; + } + + /** + * @dev gives square root of given x. + */ + function sqrt(uint256 x) + internal + pure + returns (uint256 y) + { + uint256 z = ((add(x,1)) / 2); + y = x; + while (z < y) + { + y = z; + z = ((add((x / z),z)) / 2); + } + } + + /** + * @dev gives square. multiplies x by x + */ + function sq(uint256 x) + internal + pure + returns (uint256) + { + return (mul(x,x)); + } + + /** + * @dev x to the power of y + */ + function pwr(uint256 x, uint256 y) + internal + pure + returns (uint256) + { + if (x == 0) + return (0); + else if (y == 0) + return (1); + else + { + uint256 z = x; + for (uint256 i = 1; i < y; i++) + z = mul(z, x); + return (z); + } + } +} diff --git a/contracts/mocks/VechainNodes/utility/Strings.sol b/contracts/mocks/VechainNodes/utility/Strings.sol new file mode 100644 index 0000000..f25334b --- /dev/null +++ b/contracts/mocks/VechainNodes/utility/Strings.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: MIT + +// Copyright (c) 2018 The VeChainThor developers + +// Distributed under the GNU Lesser General Public License v3.0 software license, see the accompanying +// file LICENSE or + +pragma solidity 0.8.20; + +library Strings { + + function strConcat(string memory _a, string memory _b) internal pure returns (string memory) { + bytes memory _ba = bytes(_a); + bytes memory _bb = bytes(_b); + + string memory ab = new string(_ba.length + _bb.length); + bytes memory bab = bytes(ab); + uint k = 0; + for (uint i = 0; i < _ba.length; i++) bab[k++] = _ba[i]; + for (uint i = 0; i < _bb.length; i++) bab[k++] = _bb[i]; + return string(bab); + } + + function uint2str(uint256 i) internal pure returns (string memory) { + if (i == 0) return "0"; + uint j = i; + uint len; + while (j != 0){ + len++; + j /= 10; + } + j = i; + bytes memory bstr = new bytes(len); + uint k = len - 1; + while (j != 0){ + bstr[k--] = bytes1(uint8(48 + j % 10)); + j /= 10; + } + return string(bstr); + } + +} diff --git a/contracts/mocks/VechainNodes/utility/interfaces/IERC165.sol b/contracts/mocks/VechainNodes/utility/interfaces/IERC165.sol new file mode 100644 index 0000000..87ef4b8 --- /dev/null +++ b/contracts/mocks/VechainNodes/utility/interfaces/IERC165.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT + +// Copyright (c) 2018 The VeChainThor developers + +// Distributed under the GNU Lesser General Public License v3.0 software license, see the accompanying +// file LICENSE or + +pragma solidity 0.8.20; + + +/// @title ERC165 +/// @dev https://github.com/ethereum/EIPs/blob/master/EIPS/eip-165.md +interface IERC165 { + + /// @notice Query if a contract implements an interface + /// @param _interfaceId The interface identifier, as specified in ERC-165 + /// @dev Interface identification is specified in ERC-165. This function + /// uses less than 30,000 gas. + function supportsInterface(bytes4 _interfaceId) + external + view + returns (bool); +} diff --git a/contracts/mocks/VechainNodes/utility/interfaces/IVIP181.sol b/contracts/mocks/VechainNodes/utility/interfaces/IVIP181.sol new file mode 100644 index 0000000..c1279b1 --- /dev/null +++ b/contracts/mocks/VechainNodes/utility/interfaces/IVIP181.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT + +// Copyright (c) 2018 The VeChainThor developers + +// Distributed under the GNU Lesser General Public License v3.0 software license, see the accompanying +// file LICENSE or + +pragma solidity 0.8.20; + +import "./IVIP181Basic.sol"; + + + /// @title VIP181 Non-Fungible Token Standard, optional enumeration extension +abstract contract IVIP181Enumerable is IVIP181Basic { + function totalSupply() public virtual view returns (uint256); + function tokenOfOwnerByIndex(address _owner, uint256 _index) public virtual view returns (uint256 _tokenId); + + function tokenByIndex(uint256 _index) public virtual view returns (uint256); +} + + +/// @title VIP181 Non-Fungible Token Standard, optional metadata extension +abstract contract IVIP181Metadata is IVIP181Basic { + function name() external virtual view returns (string memory _name); + function symbol() external virtual view returns (string memory _symbol); + function tokenURI(uint256 _tokenId) public virtual view returns (string memory); +} + + +/// @title EVIP181 Non-Fungible Token Standard, full implementation interface +abstract contract IVIP181 is IVIP181Basic, IVIP181Enumerable, IVIP181Metadata {} diff --git a/contracts/mocks/VechainNodes/utility/interfaces/IVIP181Basic.sol b/contracts/mocks/VechainNodes/utility/interfaces/IVIP181Basic.sol new file mode 100644 index 0000000..ceef27b --- /dev/null +++ b/contracts/mocks/VechainNodes/utility/interfaces/IVIP181Basic.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: MIT + +// Copyright (c) 2018 The VeChainThor developers + +// Distributed under the GNU Lesser General Public License v3.0 software license, see the accompanying +// file LICENSE or + +pragma solidity 0.8.20; + + +/** + * @title VIP181 Non-Fungible Token Standard basic interface + */ +abstract contract IVIP181Basic { + + bytes4 internal constant InterfaceId_VIP181 = 0x291a6960; + /* + * 0x291a6960 === + * bytes4(keccak256('totalSupply()')) ^ + * bytes4(keccak256('balanceOf(address)')) ^ + * bytes4(keccak256('ownerOf(uint256)')) ^ + * bytes4(keccak256('approve(address,uint256)')) ^ + * bytes4(keccak256('getApproved(uint256)')) ^ + * bytes4(keccak256('transferFrom(address,address,uint256)')) + */ + + bytes4 internal constant InterfaceId_VIP181Metadata = 0x5b5e139f; + /** + * 0x5b5e139f === + * bytes4(keccak256('name()')) ^ + * bytes4(keccak256('symbol()')) ^ + * bytes4(keccak256('tokenURI(uint256)')) + */ + + + /// @dev This emits when an operator is enabled or disabled for an owner. + /// The operator can manage all NFTs of the owner. + event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved); + + /// @notice Count all NFTs assigned to an owner + /// @dev NFTs assigned to the zero address are considered invalid, and this + /// function throws for queries about the zero address. + /// @param _owner An address for whom to query the balance + /// @return The number of NFTs owned by `_owner`, possibly zero + function balanceOf(address _owner) public virtual view returns (uint256); + + /// @notice Find the owner of an NFT + /// @dev NFTs assigned to zero address are considered invalid, and queries + /// about them do throw. + /// @param _tokenId The identifier for an NFT + /// @return The address of the owner of the NFT + function ownerOf(uint256 _tokenId) public virtual view returns (address); + + + /// @notice Set or reaffirm the approved address for an NFT + /// @dev The zero address indicates there is no approved address. + /// @dev Throws unless `msg.sender` is the current NFT owner, or an authorized + /// operator of the current owner. + /// @param _to The new approved NFT controller + /// @param _tokenId The NFT to approve + function approve(address _to, uint256 _tokenId) public virtual; + + /// @notice Get the approved address for a single NFT + /// @dev Throws if `_tokenId` is not a valid NFT + /// @param _tokenId The NFT to find the approved address for + /// @return The approved address for this NFT, or the zero address if there is none + function getApproved(uint256 _tokenId) public virtual view returns (address); + + /// @notice Transfer ownership of an NFT -- THE CALLER IS RESPONSIBLE + /// TO CONFIRM THAT `_to` IS CAPABLE OF RECEIVING NFTS OR ELSE + /// THEY MAY BE PERMANENTLY LOST + /// @dev Throws unless `msg.sender` is the current owner, an authorized + /// operator, or the approved address for this NFT. Throws if `_from` is + /// not the current owner. Throws if `_to` is the zero address. Throws if + /// `_tokenId` is not a valid NFT. + /// @param _from The current owner of the NFT + /// @param _to The new owner + /// @param _tokenId The NFT to transfer + function transferFrom(address _from, address _to, uint256 _tokenId) public virtual; + + +} diff --git a/contracts/templates/BaseUpgradeable.sol b/contracts/templates/BaseUpgradeable.sol new file mode 100644 index 0000000..7b9fcb2 --- /dev/null +++ b/contracts/templates/BaseUpgradeable.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.20; + +import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import { AccessControlUpgradeable } from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; + +contract BaseUpgradeable is AccessControlUpgradeable, UUPSUpgradeable { + bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE"); + + error UnauthorizedUser(address user); + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + // ---------- Storage ------------ // + + struct BaseUpgradeableStorage { + uint256 version; // TODO: remove to standalone function + } + + // keccak256(abi.encode(uint256(keccak256("storage.BaseUpgradeable")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant BaseUpgradeableStorageLocation = + 0xc9931bd7ecbba177fc71b0ded00eb01d4035361d4a0ee711add00987aca69000; + + function _getBaseUpgradeableStorage() private pure returns (BaseUpgradeableStorage storage $) { + assembly { + $.slot := BaseUpgradeableStorageLocation + } + } + + /// @notice Initializes the contract + function initialize(address _upgrader, address[] memory _admins) external initializer { + require(_upgrader != address(0), "BaseUpgradeable: upgrader is the zero address"); + + __UUPSUpgradeable_init(); + __AccessControl_init(); + + _grantRole(UPGRADER_ROLE, _upgrader); + + for (uint256 i; i < _admins.length; i++) { + require(_admins[i] != address(0), "BaseUpgradeable: admin address cannot be zero"); + _grantRole(DEFAULT_ADMIN_ROLE, _admins[i]); + } + } + + // ---------- Modifiers ------------ // + + /** + * @dev Modifier to restrict access to only the admin role and the app admin role. + * @param appId the app ID + */ + /// @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 UnauthorizedUser(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) {} + + // ---------- Setters ---------- // + + // ---------- Getters ---------- // +} diff --git a/contracts/templates/ModuleInitializable.sol b/contracts/templates/ModuleInitializable.sol new file mode 100644 index 0000000..5cc2347 --- /dev/null +++ b/contracts/templates/ModuleInitializable.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.20; + +import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import { AccessControlUpgradeable } from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; + +contract ModuleInitializable is Initializable, AccessControlUpgradeable { + error UnauthorizedUser(address user); + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + // ---------- Storage ------------ // + + struct ModuleInitializableStorage { + uint256 version; // TODO: remove to standalone function + } + + // keccak256(abi.encode(uint256(keccak256("storage.ModuleInitializable")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant ModuleInitializableStorageLocation = + 0xc9931bd7ecbba177fc71b0ded00eb01d4035361d4a0ee711add00987aca69000; + + function _getModuleInitializableStorage() private pure returns (ModuleInitializableStorage storage $) { + assembly { + $.slot := ModuleInitializableStorageLocation + } + } + + /** + * @dev Initializes the contract + */ + function __ModuleInitializable_init() internal onlyInitializing { + __ModuleInitializable_init_unchained(); + } + + function __ModuleInitializable_init_unchained() internal onlyInitializing { + // ModuleInitializableStorage storage $ = _getModuleInitializableStorage(); + } + + // ---------- Modifiers ------------ // + + /** + * @dev Modifier to restrict access to only the admin role and the app admin role. + * @param appId the app ID + */ + /// @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) virtual { + if (!hasRole(role, msg.sender) && !hasRole(DEFAULT_ADMIN_ROLE, msg.sender)) { + revert UnauthorizedUser(msg.sender); + } + _; + } + + // ---------- Setters ---------- // + + // ---------- Getters ---------- // +} diff --git a/contracts/ve-better-passport/VeBetterPassport.sol b/contracts/ve-better-passport/VeBetterPassport.sol new file mode 100644 index 0000000..69f7dc2 --- /dev/null +++ b/contracts/ve-better-passport/VeBetterPassport.sol @@ -0,0 +1,817 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.20; + +import { PassportTypes } from "./libraries/PassportTypes.sol"; +import { PassportStorageTypes } from "./libraries/PassportStorageTypes.sol"; +import { PassportChecksLogic } from "./libraries/PassportChecksLogic.sol"; +import { PassportWhitelistAndBlacklistLogic } from "./libraries/PassportWhitelistAndBlacklistLogic.sol"; +import { PassportPoPScoreLogic } from "./libraries/PassportPoPScoreLogic.sol"; +import { PassportEntityLogic } from "./libraries/PassportEntityLogic.sol"; +import { PassportClockLogic } from "./libraries/PassportClockLogic.sol"; +import { PassportDelegationLogic } from "./libraries/PassportDelegationLogic.sol"; +import { PassportSignalingLogic } from "./libraries/PassportSignalingLogic.sol"; +import { PassportPersonhoodLogic } from "./libraries/PassportPersonhoodLogic.sol"; +import { PassportEIP712SigningLogic } from "./libraries/PassportEIP712SigningLogic.sol"; +import { PassportConfigurator } from "./libraries/PassportConfigurator.sol"; +import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import { AccessControlUpgradeable } from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import { IVeBetterPassport } from "../interfaces/IVeBetterPassport.sol"; +import { IXAllocationVotingGovernor } from "../interfaces/IXAllocationVotingGovernor.sol"; +import { IGalaxyMember } from "../interfaces/IGalaxyMember.sol"; +import { IX2EarnApps } from "../interfaces/IX2EarnApps.sol"; + +/// @title VeBetterPassport +/// @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 VeBetterPassport is AccessControlUpgradeable, UUPSUpgradeable, IVeBetterPassport { + 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 (PassportStorageTypes.PassportStorage storage $) { + assembly { + $.slot := PassportStorageLocation + } + } + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + /// @notice Initializes the contract + function initialize( + PassportTypes.InitializationData memory data, + PassportTypes.InitializationRoleData memory roles + ) external initializer { + __UUPSUpgradeable_init(); + __AccessControl_init(); + + PassportConfigurator.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) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + return PassportPersonhoodLogic.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) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + return PassportPersonhoodLogic.isPersonAtTimepoint($, user, timepoint); + } + + /// @notice Returns if the specific check is enabled + function isCheckEnabled(PassportTypes.CheckType check) external view returns (bool) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + return PassportChecksLogic.isCheckEnabled($, check); + } + + /// @notice Returns the minimum galaxy member level + function getMinimumGalaxyMemberLevel() external view returns (uint256) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + return PassportChecksLogic.getMinimumGalaxyMemberLevel($); + } + + /// @notice Returns if a user is whitelisted + function isWhitelisted(address _user) external view returns (bool) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + return PassportWhitelistAndBlacklistLogic.isWhitelisted($, _user); + } + + /// @notice Returns if a user is blacklisted + function isBlacklisted(address _user) external view returns (bool) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + return PassportWhitelistAndBlacklistLogic.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) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + return PassportWhitelistAndBlacklistLogic.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) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + return PassportWhitelistAndBlacklistLogic.isPassportBlacklisted($, passport); + } + + /// @notice Gets the threshold percentage of blacklisted entities for a passport to be considered blacklisted + function blacklistThreshold() external view returns (uint256) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + return PassportWhitelistAndBlacklistLogic.blacklistThreshold($); + } + + /// @notice Gets the threshold percentage of whitelisted entities for a passport to be considered whitelisted + function whitelistThreshold() external view returns (uint256) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + return PassportWhitelistAndBlacklistLogic.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) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + return PassportPoPScoreLogic.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) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + return PassportPoPScoreLogic.userRoundScore($, user, round); + } + + /// @notice Gets the total score of a user + /// @param user - the user address + function userTotalScore(address user) external view returns (uint256) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + return PassportPoPScoreLogic.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) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + return PassportPoPScoreLogic.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) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + return PassportPoPScoreLogic.userAppTotalScore($, user, appId); + } + + /// @notice Gets the threshold for a user to be considered a person + function thresholdPoPScore() external view returns (uint256) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + return PassportPoPScoreLogic.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) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + return PassportPoPScoreLogic.thresholdPoPScoreAtTimepoint($, timepoint); + } + + /// @notice Gets the security multiplier for an app security + /// @param security - the app security between LOW, MEDIUM, HIGH + function securityMultiplier(PassportTypes.APP_SECURITY security) external view returns (uint256) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + return PassportPoPScoreLogic.securityMultiplier($, security); + } + + /// @notice Gets the security level of an app + /// @param appId - the app id + function appSecurity(bytes32 appId) external view returns (PassportTypes.APP_SECURITY) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + return PassportPoPScoreLogic.appSecurity($, appId); + } + + /// @notice Gets the round threshold for a user to be considered a person + function roundsForCumulativeScore() external view returns (uint256) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + return PassportPoPScoreLogic.roundsForCumulativeScore($); + } + + /// @notice Gets the decay rate for the cumulative score + function decayRate() external view returns (uint256) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + return PassportPoPScoreLogic.decayRate($); + } + + /// @notice Gets the minimum galaxy member level to be considered a person + function minimumGalaxyMemberLevel() external view returns (uint256) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + return $.minimumGalaxyMemberLevel; + } + + /// @notice Returns the maximum number of entities per passport + function maxEntitiesPerPassport() external view returns (uint256) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + return PassportEntityLogic.getMaxEntitiesPerPassport($); + } + + /// @notice Returns the passport address for a entity + /// @param entity - the entity address + function getPassportForEntity(address entity) external view returns (address) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + return PassportEntityLogic.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) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + return PassportEntityLogic.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) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + return PassportEntityLogic.getEntitiesLinkedToPassport($, passport); + } + + /// @notice Returns if a user is a entity + /// @param user - the user address + function isEntity(address user) external view returns (bool) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + return PassportEntityLogic.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) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + return PassportEntityLogic.isEntityInTimepoint($, user, timepoint); + } + + /// @notice Returns if a user is a passport + /// @param user - the user address + function isPassport(address user) external view returns (bool) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + return PassportEntityLogic.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) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + return PassportEntityLogic.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) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + return PassportEntityLogic.getPendingLinkings($, user); + } + + /// @notice Returns the delegatee address for a delegator + /// @param delegator - the delegator address + function getDelegatee(address delegator) external view returns (address) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + return PassportDelegationLogic.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) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + return PassportDelegationLogic.getDelegateeInTimepoint($, delegator, timepoint); + } + + /// @notice Returns the delegator address for a delegatee + /// @param delegatee - the delegatee address + function getDelegator(address delegatee) external view returns (address) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + return PassportDelegationLogic.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) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + return PassportDelegationLogic.getDelegatorInTimepoint($, delegatee, timepoint); + } + + /// @notice Returns if a user is a delegator + /// @param user - the user address + function isDelegator(address user) external view returns (bool) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + return PassportDelegationLogic.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) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + return PassportDelegationLogic.isDelegatorInTimepoint($, user, timepoint); + } + + /// @notice Returns if a user is a delegatee + /// @param user - the user address + function isDelegatee(address user) external view returns (bool) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + return PassportDelegationLogic.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) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + return PassportDelegationLogic.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) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + return PassportDelegationLogic.getPendingDelegations($, user); + } + + /// @notice Returns the number of times a user has been signaled + function signaledCounter(address _user) external view returns (uint256) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + return PassportSignalingLogic.signaledCounter($, _user); + } + + /// @notice Returns the belonging app of a signaler + function appOfSignaler(address _signaler) external view returns (bytes32) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + return PassportSignalingLogic.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) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + return PassportSignalingLogic.appSignalsCounter($, _app, _user); + } + + /// @notice Returns the total number of signals for an app + function appTotalSignalsCounter(bytes32 _app) external view returns (uint256) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + return PassportSignalingLogic.appTotalSignalsCounter($, _app); + } + + /// @notice Returns the signaling threshold + function signalingThreshold() external view returns (uint256) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + return PassportSignalingLogic.signalingThreshold($); + } + + /// @notice Gets the x2EarnApps contract address + function getX2EarnApps() external view returns (IX2EarnApps) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + return PassportConfigurator.getX2EarnApps($); + } + + /// @notice Gets the xAllocationVoting contract address + function getXAllocationVoting() external view returns (IXAllocationVotingGovernor) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + return PassportConfigurator.getXAllocationVoting($); + } + + /// @notice Gets the galaxy member contract address + function getGalaxyMember() external view returns (IGalaxyMember) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + return PassportConfigurator.getGalaxyMember($); + } + + /// @notice Get the current block number + function clock() external view returns (uint48) { + return PassportClockLogic.clock(); + } + + /// @notice Get the clock mode + function CLOCK_MODE() external pure returns (string memory) { + return PassportClockLogic.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 PassportEIP712SigningLogic.eip712Domain(); + } + + /// @notice Returns the version of the contract + function version() external pure returns (string memory) { + return "1"; + } + + // ---------- Setters ---------- // + /// @notice Toggles the specified check + function toggleCheck(PassportTypes.CheckType check) external onlyRole(SETTINGS_MANAGER_ROLE) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + PassportChecksLogic.toggleCheck($, check); + } + + /// @notice user can be whitelisted but the counter will not be reset + function whitelist(address _user) external onlyRoleOrAdmin(WHITELISTER_ROLE) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + PassportWhitelistAndBlacklistLogic.whitelist($, _user); + } + + /// @notice Removes a user from the whitelist + function removeFromWhitelist(address _user) external onlyRoleOrAdmin(WHITELISTER_ROLE) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + PassportWhitelistAndBlacklistLogic.removeFromWhitelist($, _user); + } + + /// @notice user can be blacklisted but the counter will not be reset + function blacklist(address _user) external onlyRoleOrAdmin(WHITELISTER_ROLE) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + PassportWhitelistAndBlacklistLogic.blacklist($, _user); + } + + /// @notice Removes a user from the blacklist + function removeFromBlacklist(address _user) external onlyRoleOrAdmin(WHITELISTER_ROLE) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + PassportWhitelistAndBlacklistLogic.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) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + PassportWhitelistAndBlacklistLogic.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) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + PassportWhitelistAndBlacklistLogic.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) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + PassportPoPScoreLogic.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) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + PassportPoPScoreLogic.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) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + PassportPoPScoreLogic.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) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + PassportPoPScoreLogic.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) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + PassportPoPScoreLogic.setRoundsForCumulativeScore($, rounds); + } + + /// @notice Sets the security multiplier + /// @param security - the app security between LOW, MEDIUM, HIGH + /// @param multiplier - the multiplier + function setSecurityMultiplier( + PassportTypes.APP_SECURITY security, + uint256 multiplier + ) external onlyRoleOrAdmin(ACTION_SCORE_MANAGER_ROLE) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + PassportPoPScoreLogic.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, + PassportTypes.APP_SECURITY security + ) external onlyRoleOrAdmin(ACTION_SCORE_MANAGER_ROLE) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + PassportPoPScoreLogic.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) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + PassportPoPScoreLogic.setDecayRate($, _decayRate); + } + + /// @notice Delegate the personhood to another address + /// The entity must sign a message where he authorizes the passport 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 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 { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + PassportEntityLogic.linkEntityToPassportWithSignature($, entity, deadline, signature); + } + + /// @notice Delegate the personhood to another address + /// @dev The passport 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 linkEntityToPassport(address passport) external { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + PassportEntityLogic.linkEntityToPassport($, passport); + } + + /// @notice Allow the passport to accept the delegation + /// @param entity - the entity address + function acceptEntityLink(address entity) external { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + PassportEntityLogic.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 { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + PassportEntityLogic.removeEntityLink($, entity); + } + + /// @notice Deny an incoming pending entity link to the sender's passport. + /// @param entity - the entity address + function denyIncomingPendingEntityLink(address entity) external { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + PassportEntityLogic.denyIncomingPendingEntityLink($, entity); + } + + /// @notice Cancel an outgoing pending entity link from the sender. + function cancelOutgoingPendingEntityLink() external { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + PassportEntityLogic.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) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + PassportEntityLogic.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 { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + PassportDelegationLogic.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 { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + PassportDelegationLogic.delegatePassport($, delegatee); + } + + /// @notice Allow the delegatee to accept the delegation + /// @param delegator - the delegator address + function acceptDelegation(address delegator) external { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + PassportDelegationLogic.acceptDelegation($, delegator); + } + + /// @notice Revoke the delegation (can be done by the delegator or the delegatee) + function revokeDelegation() external { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + PassportDelegationLogic.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 { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + PassportDelegationLogic.denyIncomingPendingDelegation($, delegator); + } + + /// @notice Allows a delegator to cancel (and remove) the outgoing pending delegation. + function cancelOutgoingPendingDelegation() external { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + PassportDelegationLogic.cancelOutgoingPendingDelegation($); + } + + /// @notice Signals a user + function signalUser(address _user) external onlyRoleOrAdmin(SIGNALER_ROLE) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + PassportSignalingLogic.signalUser($, _user); + } + + /// @notice Signals a user with a reason + function signalUserWithReason(address _user, string memory reason) external onlyRoleOrAdmin(SIGNALER_ROLE) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + PassportSignalingLogic.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 { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + PassportSignalingLogic.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 { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + PassportSignalingLogic.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) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + PassportSignalingLogic.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) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + PassportSignalingLogic.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) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + PassportSignalingLogic.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) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + PassportSignalingLogic.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 { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + PassportSignalingLogic.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) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + PassportChecksLogic.setMinimumGalaxyMemberLevel($, _minimumGalaxyMemberLevel); + } + + /// @dev Sets the xAllocationVoting contract + /// @param xAllocationVoting - the xAllocationVoting contract address + function setXAllocationVoting( + IXAllocationVotingGovernor xAllocationVoting + ) external onlyRoleOrAdmin(DEFAULT_ADMIN_ROLE) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + PassportConfigurator.setXAllocationVoting($, xAllocationVoting); + } + + /// @dev Sets the galaxy member contract + /// @param galaxyMember - the galaxy member contract address + function setGalaxyMember(IGalaxyMember galaxyMember) external onlyRoleOrAdmin(DEFAULT_ADMIN_ROLE) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + PassportConfigurator.setGalaxyMember($, galaxyMember); + } + + /// @notice Sets the x2EarnApps contract address + /// @param _x2EarnApps - the X2EarnApps contract address + function setX2EarnApps(IX2EarnApps _x2EarnApps) external override onlyRole(DEFAULT_ADMIN_ROLE) { + PassportStorageTypes.PassportStorage storage $ = getPassportStorage(); + PassportConfigurator.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, IVeBetterPassport) 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, IVeBetterPassport) onlyRoleOrAdmin(ROLE_GRANTER) { + _revokeRole(role, account); + } +} diff --git a/contracts/ve-better-passport/libraries/PassportChecksLogic.sol b/contracts/ve-better-passport/libraries/PassportChecksLogic.sol new file mode 100644 index 0000000..94375c2 --- /dev/null +++ b/contracts/ve-better-passport/libraries/PassportChecksLogic.sol @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: MIT + +// ####### +// ################ +// #################### +// ########### ######### +// ######### ######### +// ####### ######### ######### +// ######### ######### ########## +// ########## ######## #################### +// ########## ######### ######################### +// ################### ############################ +// ################# ########## ######## +// ############## ### ######## +// ############ ######### +// ########## ########## +// ######## ########### +// ### ############ +// ############## +// ################# +// ############## +// ######### + +pragma solidity 0.8.20; + +import { PassportStorageTypes } from "./PassportStorageTypes.sol"; +import { PassportTypes } from "./PassportTypes.sol"; + +/** + * @title PassportChecksLogic + * @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 PassportChecksLogic { + // ---------- 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 PassportTypes.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(PassportTypes.CheckType checkType) private pure returns (uint256, string memory) { + if (checkType == PassportTypes.CheckType.WHITELIST_CHECK) return (WHITELIST_CHECK, WHITELIST_CHECK_NAME); + if (checkType == PassportTypes.CheckType.BLACKLIST_CHECK) return (BLACKLIST_CHECK, BLACKLIST_CHECK_NAME); + if (checkType == PassportTypes.CheckType.SIGNALING_CHECK) return (SIGNALING_CHECK, SIGNALING_CHECK_NAME); + if (checkType == PassportTypes.CheckType.PARTICIPATION_SCORE_CHECK) + return (PARTICIPATION_SCORE_CHECK, PARTICIPATION_SCORE_CHECK_NAME); + if (checkType == PassportTypes.CheckType.GM_OWNERSHIP_CHECK) return (GM_OWNERSHIP_CHECK, GM_OWNERSHIP_CHECK_NAME); + revert("Invalid PassportTypes"); + } + + /// @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( + PassportStorageTypes.PassportStorage storage self, + PassportTypes.CheckType checkType + ) internal view returns (bool) { + require(checkType != PassportTypes.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( + PassportStorageTypes.PassportStorage storage self, + PassportTypes.CheckType checkType + ) external view returns (bool) { + return _isCheckEnabled(self, checkType); + } + + /// @notice Returns the minimum galaxy member level + function getMinimumGalaxyMemberLevel( + PassportStorageTypes.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(PassportStorageTypes.PassportStorage storage self, PassportTypes.CheckType checkType) external { + require(checkType != PassportTypes.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( + PassportStorageTypes.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/ve-better-passport/libraries/PassportClockLogic.sol b/contracts/ve-better-passport/libraries/PassportClockLogic.sol new file mode 100644 index 0000000..745f78c --- /dev/null +++ b/contracts/ve-better-passport/libraries/PassportClockLogic.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: MIT + +// ####### +// ################ +// #################### +// ########### ######### +// ######### ######### +// ####### ######### ######### +// ######### ######### ########## +// ########## ######## #################### +// ########## ######### ######################### +// ################### ############################ +// ################# ########## ######## +// ############## ### ######## +// ############ ######### +// ########## ########## +// ######## ########### +// ### ############ +// ############## +// ################# +// ############## +// ######### + +pragma solidity 0.8.20; + +import { PassportStorageTypes } from "./PassportStorageTypes.sol"; +import { Time } from "@openzeppelin/contracts/utils/types/Time.sol"; + +/// @title PassportClockLogic Library +/// @notice Library for managing the clock logic as specified in EIP-6372. +library PassportClockLogic { + /** + * @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/ve-better-passport/libraries/PassportConfigurator.sol b/contracts/ve-better-passport/libraries/PassportConfigurator.sol new file mode 100644 index 0000000..fc7c389 --- /dev/null +++ b/contracts/ve-better-passport/libraries/PassportConfigurator.sol @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: MIT + +// ####### +// ################ +// #################### +// ########### ######### +// ######### ######### +// ####### ######### ######### +// ######### ######### ########## +// ########## ######## #################### +// ########## ######### ######################### +// ################### ############################ +// ################# ########## ######## +// ############## ### ######## +// ############ ######### +// ########## ########## +// ######## ########### +// ### ############ +// ############## +// ################# +// ############## +// ######### + +pragma solidity 0.8.20; + +import { PassportStorageTypes } from "./PassportStorageTypes.sol"; +import { PassportTypes } from "./PassportTypes.sol"; +import { PassportClockLogic } from "./PassportClockLogic.sol"; +import { IX2EarnApps } from "../../interfaces/IX2EarnApps.sol"; +import { IXAllocationVotingGovernor } from "../../interfaces/IXAllocationVotingGovernor.sol"; +import { IGalaxyMember } from "../../interfaces/IGalaxyMember.sol"; +import { Checkpoints } from "@openzeppelin/contracts/utils/structs/Checkpoints.sol"; + +/// @title PassportConfigurator 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 PassportConfigurator { + using Checkpoints for Checkpoints.Trace208; + + // ---------- Getters ---------- // + /// @notice Gets the x2EarnApps contract address + function getX2EarnApps(PassportStorageTypes.PassportStorage storage self) internal view returns (IX2EarnApps) { + return self.x2EarnApps; + } + + /// @notice Gets the xAllocationVoting contract address + function getXAllocationVoting( + PassportStorageTypes.PassportStorage storage self + ) internal view returns (IXAllocationVotingGovernor) { + return self.xAllocationVoting; + } + + /// @notice Gets the galaxy member contract address + function getGalaxyMember(PassportStorageTypes.PassportStorage storage self) internal view returns (IGalaxyMember) { + return self.galaxyMember; + } + + // ---------- Setters ---------- // + + /// @notice Initializes the PassportStorage struct with the provided initialization data + function initializePassportStorage( + PassportStorageTypes.PassportStorage storage self, + PassportTypes.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(PassportClockLogic.clock(), 0); + + // Initialize the number of rounds for cumulative score + self.roundsForCumulativeScore = initializationData.roundsForCumulativeScore; + + // Initialize the secuirty multiplier + self.securityMultiplier[PassportTypes.APP_SECURITY.LOW] = 100; + self.securityMultiplier[PassportTypes.APP_SECURITY.MEDIUM] = 200; + self.securityMultiplier[PassportTypes.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(PassportStorageTypes.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( + PassportStorageTypes.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(PassportStorageTypes.PassportStorage storage self, IGalaxyMember _galaxyMember) public { + require(address(_galaxyMember) != address(0), "VeBetterPassport: galaxyMember is the zero address"); + + self.galaxyMember = _galaxyMember; + } +} diff --git a/contracts/ve-better-passport/libraries/PassportDelegationLogic.sol b/contracts/ve-better-passport/libraries/PassportDelegationLogic.sol new file mode 100644 index 0000000..e28bfb6 --- /dev/null +++ b/contracts/ve-better-passport/libraries/PassportDelegationLogic.sol @@ -0,0 +1,514 @@ +// SPDX-License-Identifier: MIT + +// ####### +// ################ +// #################### +// ########### ######### +// ######### ######### +// ####### ######### ######### +// ######### ######### ########## +// ########## ######## #################### +// ########## ######### ######################### +// ################### ############################ +// ################# ########## ######## +// ############## ### ######## +// ############ ######### +// ########## ########## +// ######## ########### +// ### ############ +// ############## +// ################# +// ############## +// ######### + +pragma solidity 0.8.20; + +import { PassportStorageTypes } from "./PassportStorageTypes.sol"; +import { PassportClockLogic } from "./PassportClockLogic.sol"; +import { PassportEIP712SigningLogic } from "./PassportEIP712SigningLogic.sol"; +import { PassportEntityLogic } from "./PassportEntityLogic.sol"; +import { PassportTypes } from "./PassportTypes.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 PassportDelegationLogic + * @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 PassportDelegationLogic { + // 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( + PassportStorageTypes.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( + PassportStorageTypes.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( + PassportStorageTypes.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( + PassportStorageTypes.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(PassportStorageTypes.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( + PassportStorageTypes.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(PassportStorageTypes.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( + PassportStorageTypes.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( + PassportStorageTypes.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( + PassportStorageTypes.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 = PassportEIP712SigningLogic.hashTypedDataV4(structHash); + address signer = digest.recover(signature); + + // Check if the signer is the delegator + if (signer != delegator) { + revert InvalidSignature(); + } + + // Cannot delegate passport to owner + if (signer == msg.sender) { + revert CannotDelegateToSelf(signer); + } + + // Cannot delegate enitity attached to passport + if (PassportEntityLogic.isEntity(self, delegator)) { + revert PassportDelegationFromEntity(); + } + + // Cannot delegate passport to entity + if (PassportEntityLogic.isEntity(self, msg.sender)) { + revert PassportDelegationToEntity(); + } + + // Check if the passport has already delegated + if (isDelegator(self, delegator)) { + _removeDelegation(self, delegator, _addressFromUint160(self.delegatorToDelegatee[delegator].latest())); + } + + // Check if the passport is already pending delegation + address pendingDelegatee = self.pendingDelegationsDelegatorToDelegatee[delegator]; + if (pendingDelegatee != address(0)) { + _removePendingDelegation(self, delegator, pendingDelegatee); + } + + // 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(PassportStorageTypes.PassportStorage storage self, address delegatee) external { + // Check if the delegatee is trying to delegate to themselves + if (msg.sender == delegatee) { + revert CannotDelegateToSelf(msg.sender); + } + + // Check if the delegator is an entity linked to a passport + if (PassportEntityLogic.isEntity(self, msg.sender)) { + revert PassportDelegationFromEntity(); + } + + if (PassportEntityLogic.isEntity(self, delegatee)) { + revert PassportDelegationToEntity(); + } + + // Check if the passport has already delegated removing the previous delegation + if (isDelegator(self, msg.sender)) { + _removeDelegation(self, msg.sender, _addressFromUint160(self.delegatorToDelegatee[msg.sender].latest())); + } + + // Check if the passport is already pending delegation + if (self.pendingDelegationsDelegatorToDelegatee[msg.sender] != address(0)) { + _removePendingDelegation(self, msg.sender, self.pendingDelegationsDelegatorToDelegatee[msg.sender]); + } + + // 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(PassportStorageTypes.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( + PassportStorageTypes.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(PassportStorageTypes.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(PassportStorageTypes.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(PassportClockLogic.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( + PassportStorageTypes.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( + PassportStorageTypes.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( + PassportStorageTypes.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( + PassportStorageTypes.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( + PassportStorageTypes.PassportStorage storage self, + address user, + uint256 timepoint + ) internal view returns (bool) { + return self.delegatorToDelegatee[user].upperLookupRecent(SafeCast.toUint48(timepoint)) != 0; + } +} diff --git a/contracts/ve-better-passport/libraries/PassportEIP712SigningLogic.sol b/contracts/ve-better-passport/libraries/PassportEIP712SigningLogic.sol new file mode 100644 index 0000000..6cc3edb --- /dev/null +++ b/contracts/ve-better-passport/libraries/PassportEIP712SigningLogic.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 PassportEIP712SigningLogic { + // ---------- 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/ve-better-passport/libraries/PassportEntityLogic.sol b/contracts/ve-better-passport/libraries/PassportEntityLogic.sol new file mode 100644 index 0000000..f8b4c5d --- /dev/null +++ b/contracts/ve-better-passport/libraries/PassportEntityLogic.sol @@ -0,0 +1,570 @@ +// SPDX-License-Identifier: MIT + +// ####### +// ################ +// #################### +// ########### ######### +// ######### ######### +// ####### ######### ######### +// ######### ######### ########## +// ########## ######## #################### +// ########## ######### ######################### +// ################### ############################ +// ################# ########## ######## +// ############## ### ######## +// ############ ######### +// ########## ########## +// ######## ########### +// ### ############ +// ############## +// ################# +// ############## +// ######### + +pragma solidity 0.8.20; + +import { PassportStorageTypes } from "./PassportStorageTypes.sol"; +import { PassportClockLogic } from "./PassportClockLogic.sol"; +import { PassportEIP712SigningLogic } from "./PassportEIP712SigningLogic.sol"; +import { PassportSignalingLogic } from "./PassportSignalingLogic.sol"; +import { PassportWhitelistAndBlacklistLogic } from "./PassportWhitelistAndBlacklistLogic.sol"; +import { PassportTypes } from "./PassportTypes.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 PassportEntityLogic + * @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 PassportEntityLogic { + // 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(); + + // ---------- 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( + PassportStorageTypes.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( + PassportStorageTypes.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( + PassportStorageTypes.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(PassportStorageTypes.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( + PassportStorageTypes.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( + PassportStorageTypes.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( + PassportStorageTypes.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( + PassportStorageTypes.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( + PassportStorageTypes.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( + PassportStorageTypes.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 = PassportEIP712SigningLogic.hashTypedDataV4(structHash); + address signer = digest.recover(signature); + + // Ensure the signature is valid + if (signer != entity) { + revert InvalidSignature(); + } + + // Ensure the entity trying to link is not the passport itself + if (signer == msg.sender) { + revert CannotLinkToSelf(signer); + } + + // Check if the entity is already linked, if so revert + if (self.entityToPassport[entity].latest() != 0 || self.pendingLinksEntityToPassport[entity] != address(0)) { + revert AlreadyLinked(msg.sender); + } + + // 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(PassportStorageTypes.PassportStorage storage self, address passport) external { + // Check if the entity (msg.sender) is already linked + if (self.entityToPassport[msg.sender].latest() != 0 || self.pendingLinksIndexes[msg.sender] != 0) { + revert AlreadyLinked(msg.sender); + } + + // Prevent self-linking (an entity cannot be its own passport) + if (msg.sender == passport) { + revert CannotLinkToSelf(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(PassportStorageTypes.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(PassportStorageTypes.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(PassportStorageTypes.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(PassportStorageTypes.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(PassportStorageTypes.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(PassportClockLogic.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( + PassportStorageTypes.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( + PassportStorageTypes.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 + PassportSignalingLogic.removeEntitySignalsFromPassport(self, entity, passport); + PassportWhitelistAndBlacklistLogic.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(PassportStorageTypes.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 + PassportSignalingLogic.attachEntitySignalsToPassport(self, entity, passport); + PassportWhitelistAndBlacklistLogic.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( + PassportStorageTypes.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( + PassportStorageTypes.PassportStorage storage self, + address entity, + uint256 timepoint + ) internal view returns (bool) { + return self.entityToPassport[entity].upperLookupRecent(SafeCast.toUint48(timepoint)) != 0; + } +} diff --git a/contracts/ve-better-passport/libraries/PassportPersonhoodLogic.sol b/contracts/ve-better-passport/libraries/PassportPersonhoodLogic.sol new file mode 100644 index 0000000..c13aabc --- /dev/null +++ b/contracts/ve-better-passport/libraries/PassportPersonhoodLogic.sol @@ -0,0 +1,191 @@ +// SPDX-License-Identifier: MIT + +// ####### +// ################ +// #################### +// ########### ######### +// ######### ######### +// ####### ######### ######### +// ######### ######### ########## +// ########## ######## #################### +// ########## ######### ######################### +// ################### ############################ +// ################# ########## ######## +// ############## ### ######## +// ############ ######### +// ########## ########## +// ######## ########### +// ### ############ +// ############## +// ################# +// ############## +// ######### + +pragma solidity 0.8.20; + +import { PassportStorageTypes } from "./PassportStorageTypes.sol"; +import { PassportChecksLogic } from "./PassportChecksLogic.sol"; +import { PassportSignalingLogic } from "./PassportSignalingLogic.sol"; +import { PassportDelegationLogic } from "./PassportDelegationLogic.sol"; +import { PassportPoPScoreLogic } from "./PassportPoPScoreLogic.sol"; +import { PassportClockLogic } from "./PassportClockLogic.sol"; +import { PassportEntityLogic } from "./PassportEntityLogic.sol"; +import { PassportWhitelistAndBlacklistLogic } from "./PassportWhitelistAndBlacklistLogic.sol"; +import { PassportTypes } from "./PassportTypes.sol"; + +/** + * @title PassportPersonhoodLogic + * @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 PassportPersonhoodLogic { + /** + * @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( + PassportStorageTypes.PassportStorage storage self, + address user + ) external view returns (bool person, string memory reason) { + // Get the current timepoint + uint48 timepoint = PassportClockLogic.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( + PassportStorageTypes.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( + PassportStorageTypes.PassportStorage storage self, + address user, + uint256 timepoint + ) private view returns (address) { + if (PassportDelegationLogic._isDelegateeInTimepoint(self, user, timepoint)) { + return PassportDelegationLogic._getDelegatorInTimepoint(self, user, timepoint); // Return the delegator's passport address + } else if ( + PassportDelegationLogic._isDelegatorInTimepoint(self, user, timepoint) || + PassportEntityLogic._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( + PassportStorageTypes.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 ( + PassportChecksLogic._isCheckEnabled(self, PassportTypes.CheckType.WHITELIST_CHECK) && + PassportWhitelistAndBlacklistLogic._isPassportWhitelisted(self, user) + ) { + return (true, "User is whitelisted"); + } + + // If a wallet is blacklisted, it is not a person + if ( + PassportChecksLogic._isCheckEnabled(self, PassportTypes.CheckType.BLACKLIST_CHECK) && + PassportWhitelistAndBlacklistLogic._isPassportBlacklisted(self, user) + ) { + return (false, "User is blacklisted"); + } + + // If a wallet is not whitelisted and has been signaled more than X times + if ( + (PassportChecksLogic._isCheckEnabled(self, PassportTypes.CheckType.SIGNALING_CHECK) && + PassportSignalingLogic.signaledCounter(self, user) >= PassportSignalingLogic.signalingThreshold(self)) + ) { + return (false, "User has been signaled too many times"); + } + + if (PassportChecksLogic._isCheckEnabled(self, PassportTypes.CheckType.PARTICIPATION_SCORE_CHECK)) { + uint256 participationScore = PassportPoPScoreLogic.getCumulativeScoreWithDecay( + 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 >= PassportPoPScoreLogic._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/ve-better-passport/libraries/PassportPoPScoreLogic.sol b/contracts/ve-better-passport/libraries/PassportPoPScoreLogic.sol new file mode 100644 index 0000000..a814d75 --- /dev/null +++ b/contracts/ve-better-passport/libraries/PassportPoPScoreLogic.sol @@ -0,0 +1,378 @@ +// SPDX-License-Identifier: MIT + +// ####### +// ################ +// #################### +// ########### ######### +// ######### ######### +// ####### ######### ######### +// ######### ######### ########## +// ########## ######## #################### +// ########## ######### ######################### +// ################### ############################ +// ################# ########## ######## +// ############## ### ######## +// ############ ######### +// ########## ########## +// ######## ########### +// ### ############ +// ############## +// ################# +// ############## +// ######### + +pragma solidity 0.8.20; + +import { PassportStorageTypes } from "./PassportStorageTypes.sol"; +import { PassportTypes } from "./PassportTypes.sol"; +import { PassportEntityLogic } from "./PassportEntityLogic.sol"; +import { Checkpoints } from "@openzeppelin/contracts/utils/structs/Checkpoints.sol"; +import { PassportClockLogic } from "./PassportClockLogic.sol"; + +/** + * @title PassportPoPScoreLogic + * @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 PassportPoPScoreLogic { + 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( + PassportStorageTypes.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( + PassportStorageTypes.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( + PassportStorageTypes.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( + PassportStorageTypes.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( + PassportStorageTypes.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(PassportStorageTypes.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( + PassportStorageTypes.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( + PassportStorageTypes.PassportStorage storage self, + PassportTypes.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( + PassportStorageTypes.PassportStorage storage self, + bytes32 appId + ) internal view returns (PassportTypes.APP_SECURITY) { + return self.appSecurity[appId]; + } + + /// @notice Gets the round threshold for a user to be considered a person + function roundsForCumulativeScore(PassportStorageTypes.PassportStorage storage self) internal view returns (uint256) { + return self.roundsForCumulativeScore; + } + + /// @notice Gets the decay rate for the cumulative score + function decayRate(PassportStorageTypes.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(PassportStorageTypes.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( + PassportStorageTypes.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( + PassportStorageTypes.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 = PassportEntityLogic._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(PassportStorageTypes.PassportStorage storage self, uint208 threshold) external { + require(threshold > 0, "ProofOfParticipation: threshold is zero"); + + self.popScoreThreshold.push(PassportClockLogic.clock(), threshold); + } + + /// @notice Sets the number of rounds to consider for the cumulative score + /// @param rounds - the number of rounds + function setRoundsForCumulativeScore(PassportStorageTypes.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( + PassportStorageTypes.PassportStorage storage self, + PassportTypes.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( + PassportStorageTypes.PassportStorage storage self, + bytes32 appId, + PassportTypes.APP_SECURITY security + ) external { + self.appSecurity[appId] = security; + } + + /// @notice Sets the decay rate for the exponential decay + /// @param newDecayRate - the decay rate + function setDecayRate(PassportStorageTypes.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( + PassportStorageTypes.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( + PassportStorageTypes.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( + PassportStorageTypes.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] == PassportTypes.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 = PassportEntityLogic._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( + PassportStorageTypes.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/ve-better-passport/libraries/PassportSignalingLogic.sol b/contracts/ve-better-passport/libraries/PassportSignalingLogic.sol new file mode 100644 index 0000000..089ebe0 --- /dev/null +++ b/contracts/ve-better-passport/libraries/PassportSignalingLogic.sol @@ -0,0 +1,309 @@ +// SPDX-License-Identifier: MIT + +// ####### +// ################ +// #################### +// ########### ######### +// ######### ######### +// ####### ######### ######### +// ######### ######### ########## +// ########## ######## #################### +// ########## ######### ######################### +// ################### ############################ +// ################# ########## ######## +// ############## ### ######## +// ############ ######### +// ########## ########## +// ######## ########### +// ### ############ +// ############## +// ################# +// ############## +// ######### + +pragma solidity 0.8.20; + +import { PassportStorageTypes } from "./PassportStorageTypes.sol"; +import { PassportClockLogic } from "./PassportClockLogic.sol"; +import { PassportEntityLogic } from "./PassportEntityLogic.sol"; +import { PassportEIP712SigningLogic } from "./PassportEIP712SigningLogic.sol"; +import { PassportTypes } from "./PassportTypes.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 PassportSignalingLogic + * @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 PassportSignalingLogic { + // ---------- 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( + PassportStorageTypes.PassportStorage storage self, + address user + ) internal view returns (uint256) { + return self.signaledCounter[user]; + } + + /// @notice Returns the belonging app of a signaler + function appOfSignaler( + PassportStorageTypes.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( + PassportStorageTypes.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( + PassportStorageTypes.PassportStorage storage self, + bytes32 app + ) internal view returns (uint256) { + return self.appTotalSignalsCounter[app]; + } + + /// @notice Returns the signaling threshold + function signalingThreshold(PassportStorageTypes.PassportStorage storage self) internal view returns (uint256) { + return self.signalsThreshold; + } + + // ---------- Setters ---------- // + + /// @notice Signals a user + function signalUser(PassportStorageTypes.PassportStorage storage self, address user) external { + _signalUser(self, user, ""); + } + + /// @notice Signals a user with a reason + function signalUserWithReason( + PassportStorageTypes.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( + PassportStorageTypes.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(PassportStorageTypes.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(PassportStorageTypes.PassportStorage storage self, uint256 threshold) external { + self.signalsThreshold = threshold; + } + + /// @notice Private function to remove a signaler from an app + function removeSignalerFromApp(PassportStorageTypes.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(PassportStorageTypes.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( + PassportStorageTypes.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 = PassportEntityLogic._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( + PassportStorageTypes.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(PassportStorageTypes.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 = PassportEntityLogic._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( + PassportStorageTypes.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 = PassportEntityLogic._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( + PassportStorageTypes.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( + PassportStorageTypes.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/ve-better-passport/libraries/PassportStorageTypes.sol b/contracts/ve-better-passport/libraries/PassportStorageTypes.sol new file mode 100644 index 0000000..1d10643 --- /dev/null +++ b/contracts/ve-better-passport/libraries/PassportStorageTypes.sol @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: MIT + +// ####### +// ################ +// #################### +// ########### ######### +// ######### ######### +// ####### ######### ######### +// ######### ######### ########## +// ########## ######## #################### +// ########## ######### ######################### +// ################### ############################ +// ################# ########## ######## +// ############## ### ######## +// ############ ######### +// ########## ########## +// ######## ########### +// ### ############ +// ############## +// ################# +// ############## +// ######### + +pragma solidity 0.8.20; + +import { IXAllocationVotingGovernor } from "../../interfaces/IXAllocationVotingGovernor.sol"; +import { IGalaxyMember } from "../../interfaces/IGalaxyMember.sol"; +import { IX2EarnApps } from "../../interfaces/IX2EarnApps.sol"; +import { PassportTypes } from "./PassportTypes.sol"; +import { Checkpoints } from "@openzeppelin/contracts/utils/structs/Checkpoints.sol"; + +/** + * @title PassportStorageTypes + * @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 PassportStorageTypes { + 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 + IGalaxyMember 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(PassportTypes.APP_SECURITY security => uint256 multiplier) securityMultiplier; + // Security level of an app -> will be UNDEFINED and set to LOW by default + mapping(bytes32 appId => PassportTypes.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/ve-better-passport/libraries/PassportTypes.sol b/contracts/ve-better-passport/libraries/PassportTypes.sol new file mode 100644 index 0000000..ad3c6d4 --- /dev/null +++ b/contracts/ve-better-passport/libraries/PassportTypes.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 { IGalaxyMember } from "../../interfaces/IGalaxyMember.sol"; + +/** + * @title PassportTypes + * @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 PassportTypes { + /** + * @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; + IGalaxyMember 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/ve-better-passport/libraries/PassportWhitelistAndBlacklistLogic.sol b/contracts/ve-better-passport/libraries/PassportWhitelistAndBlacklistLogic.sol new file mode 100644 index 0000000..2bef144 --- /dev/null +++ b/contracts/ve-better-passport/libraries/PassportWhitelistAndBlacklistLogic.sol @@ -0,0 +1,338 @@ +// SPDX-License-Identifier: MIT + +// ####### +// ################ +// #################### +// ########### ######### +// ######### ######### +// ####### ######### ######### +// ######### ######### ########## +// ########## ######## #################### +// ########## ######### ######################### +// ################### ############################ +// ################# ########## ######## +// ############## ### ######## +// ############ ######### +// ########## ########## +// ######## ########### +// ### ############ +// ############## +// ################# +// ############## +// ######### + +pragma solidity 0.8.20; + +import { PassportStorageTypes } from "./PassportStorageTypes.sol"; +import { PassportEntityLogic } from "./PassportEntityLogic.sol"; + +/** + * @title PassportWhitelistAndBlacklistLogic + * @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 PassportWhitelistAndBlacklistLogic { + // ---------- 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(PassportStorageTypes.PassportStorage storage self, address user) internal view returns (bool) { + return self.whitelisted[user]; + } + + /// @notice Returns if a user is blacklisted + function isBlacklisted(PassportStorageTypes.PassportStorage storage self, address user) internal view returns (bool) { + return self.blacklisted[user]; + } + + /// @notice return the blacklist threshold + function blacklistThreshold(PassportStorageTypes.PassportStorage storage self) internal view returns (uint256) { + return self.blacklistThreshold; + } + + /// @notice return the whitelist threshold + function whitelistThreshold(PassportStorageTypes.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( + PassportStorageTypes.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( + PassportStorageTypes.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(PassportStorageTypes.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(PassportStorageTypes.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(PassportStorageTypes.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(PassportStorageTypes.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(PassportStorageTypes.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(PassportStorageTypes.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( + PassportStorageTypes.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( + PassportStorageTypes.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( + PassportStorageTypes.PassportStorage storage self, + address user, + bool increment + ) private { + address passport = PassportEntityLogic._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( + PassportStorageTypes.PassportStorage storage self, + address user, + bool increment + ) private { + address passport = PassportEntityLogic._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( + PassportStorageTypes.PassportStorage storage self, + address passport + ) internal view returns (bool) { + passport = PassportEntityLogic._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 = PassportEntityLogic.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( + PassportStorageTypes.PassportStorage storage self, + address passport + ) internal view returns (bool) { + passport = PassportEntityLogic._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 = PassportEntityLogic.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/x-allocation-voting-governance/XAllocationVotingGovernor.sol b/contracts/x-allocation-voting-governance/XAllocationVotingGovernor.sol index bda2712..8f53b97 100644 --- a/contracts/x-allocation-voting-governance/XAllocationVotingGovernor.sol +++ b/contracts/x-allocation-voting-governance/XAllocationVotingGovernor.sol @@ -32,6 +32,8 @@ import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/I import { IX2EarnApps } from "../interfaces/IX2EarnApps.sol"; import { IEmissions } from "../interfaces/IEmissions.sol"; import { IVoterRewards } from "../interfaces/IVoterRewards.sol"; +import { IVeBetterPassport } from "../interfaces/IVeBetterPassport.sol"; +import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; /** * @title XAllocationVotingGovernor @@ -45,6 +47,9 @@ import { IVoterRewards } from "../interfaces/IVoterRewards.sol"; * - A rounds storage module must implement {_startNewRound}, {roundSnapshot}, {roundDeadline}, and {currentRoundId} * - A rounds finalization module must implement {finalize} * - A earnings settings module must implement {_snapshotRoundEarningsCap} + * + * ----- Version 2 ----- + * - Integrated VeBetterPassport */ abstract contract XAllocationVotingGovernor is Initializable, @@ -57,6 +62,7 @@ abstract contract XAllocationVotingGovernor is /// @custom:storage-location erc7201:b3tr.storage.XAllocationVotingGovernor struct XAllocationVotingGovernorStorage { string _name; + IVeBetterPassport _veBetterPassport; } // keccak256(abi.encode(uint256(keccak256("b3tr.storage.XAllocationVotingGovernor")) - 1)) & ~bytes32(uint256(0xff)) @@ -81,6 +87,11 @@ abstract contract XAllocationVotingGovernor is $._name = name_; } + function __XAllocationVotingGovernor_init_v2(IVeBetterPassport veBetterPassport_) internal onlyInitializing { + XAllocationVotingGovernorStorage storage $ = _getXAllocationVotingGovernorStorage(); + $._veBetterPassport = veBetterPassport_; + } + // ---------- Setters ---------- // /** @@ -101,6 +112,7 @@ abstract contract XAllocationVotingGovernor is /** * @dev Cast a vote for a set of x-2-earn applications. + * @notice Only addresses with a valid passport can vote. */ function castVote(uint256 roundId, bytes32[] memory appIds, uint256[] memory voteWeights) public virtual { _validateStateBitmap(roundId, _encodeStateBitmap(RoundState.Active)); @@ -108,6 +120,19 @@ abstract contract XAllocationVotingGovernor is require(appIds.length == voteWeights.length, "XAllocationVotingGovernor: apps and weights length mismatch"); require(appIds.length > 0, "XAllocationVotingGovernor: no apps to vote for"); + uint256 _currentRoundSnapshot = currentRoundSnapshot(); + XAllocationVotingGovernorStorage storage $ = _getXAllocationVotingGovernorStorage(); + + (bool isPerson, string memory explanation) = $._veBetterPassport.isPersonAtTimepoint( + _msgSender(), + SafeCast.toUint48(_currentRoundSnapshot) + ); + + // Check if the voter or the delegator of personhood to the voter is a person and returning error with the reason + if (!isPerson) { + revert GovernorPersonhoodVerificationFailed(_msgSender(), explanation); + } + address voter = _msgSender(); _countVote(roundId, voter, appIds, voteWeights); @@ -149,7 +174,7 @@ abstract contract XAllocationVotingGovernor is * @dev Returns the version of the governor. */ function version() public view virtual returns (string memory) { - return "1"; + return "2"; } /** @@ -294,6 +319,11 @@ abstract contract XAllocationVotingGovernor is */ function currentRoundId() public view virtual returns (uint256); + /** + * @dev Returns the X2EarnApps contract. + */ + function currentRoundSnapshot() public view virtual returns (uint256); + /** * @dev Returns the X2EarnApps contract. */ diff --git a/contracts/x-allocation-voting-governance/modules/RoundVotesCountingUpgradeable.sol b/contracts/x-allocation-voting-governance/modules/RoundVotesCountingUpgradeable.sol index 8e46e88..b9b7a4e 100644 --- a/contracts/x-allocation-voting-governance/modules/RoundVotesCountingUpgradeable.sol +++ b/contracts/x-allocation-voting-governance/modules/RoundVotesCountingUpgradeable.sol @@ -175,8 +175,8 @@ abstract contract RoundVotesCountingUpgradeable is Initializable, XAllocationVot // Get the current sum of the square roots of individual votes for the given project uint256 qfAppVotesPreVote = $._roundVotes[roundId].votesReceivedQF[apps[i]]; // ∑(sqrt(votes)) -> sqrt(votes1) + sqrt(votes2) + ... + sqrt(votesN) - // Calculate the new sum of the square roots of individual votes for the given project - uint256 newQFVotes = Math.sqrt(weights[i]); // sqrt(votes) + // Calculate the new sum of the square roots of individual votes for the given project -> If the weight is greater than 1, calculate the square root of the weight, otherwise use the weight and divide it by 1e9 ((sqrt(1e18)) = 1e9) + uint256 newQFVotes = weights[i] > 1e18 ? Math.sqrt(weights[i]) : weights[i] / 1e9; // sqrt(votes) uint256 qfAppVotesPostVote = qfAppVotesPreVote + newQFVotes; // ∑(sqrt(votes)) -> sqrt(votes1) + sqrt(votes2) + ... + sqrt(votesN) + sqrt(votesN+1) // Calculate the adjustment to the quadratic funding value for the given app diff --git a/contracts/x-allocation-voting-governance/modules/RoundsStorageUpgradeable.sol b/contracts/x-allocation-voting-governance/modules/RoundsStorageUpgradeable.sol index 4940b78..0cb868d 100644 --- a/contracts/x-allocation-voting-governance/modules/RoundsStorageUpgradeable.sol +++ b/contracts/x-allocation-voting-governance/modules/RoundsStorageUpgradeable.sol @@ -129,7 +129,7 @@ abstract contract RoundsStorageUpgradeable is Initializable, XAllocationVotingGo /** * @dev Get the current round start block */ - function currentRoundSnapshot() public view virtual returns (uint256) { + function currentRoundSnapshot() public view virtual override returns (uint256) { return roundSnapshot(currentRoundId()); } diff --git a/deploy_output/contracts.txt b/deploy_output/contracts.txt new file mode 100644 index 0000000..eb3558c --- /dev/null +++ b/deploy_output/contracts.txt @@ -0,0 +1,15 @@ +{ + "B3TR": "0x998abeb3E57409262aE5b751f60747921B33613E", + "B3TRGovernor": "0x8198f5d8F8CfFE8f9C413d98a0A55aEB8ab9FbB7", + "Emissions": "0x162A433068F51e18b7d13932F27e66a3f99E6890", + "GalaxyMember": "0x1429859428C0aBc9C2C47C8Ee9FBaf82cFA0F20f", + "TimeLock": "0x0E801D84Fa97b50751Dbf25036d067dCf18858bF", + "Treasury": "0x9d4454B023096f34B160D6B654540c56A1F81688", + "VOT3": "0x4826533B4897376654Bb4d4AD88B7faFD0C98528", + "VoterRewards": "0xdbC43Ba45381e02825b14322cDdd15eC4B3164E6", + "X2EarnApps": "0x36C02dA8a0983159322a80FFE9F24b1acfF8B570", + "X2EarnRewardsPool": "0x5f3f1dBD7B74C6B46e8c44f98792A1dAf8d69154", + "XAllocationPool": "0x7bc06c482DEAd17c0e297aFbC32f6e63d3846650", + "XAllocationVoting": "0x2E2Ed0Cfd3AD2f1d34481277b3204d807Ca2F8c2", + "VeBetterPassport": "0x4c5859f0F772848b2D91F1D83E2Fe57935348029" +} \ No newline at end of file diff --git a/deploy_output/libraries.txt b/deploy_output/libraries.txt new file mode 100644 index 0000000..fa7c2a0 --- /dev/null +++ b/deploy_output/libraries.txt @@ -0,0 +1,22 @@ +{ + "B3TRGovernor": { + "GovernorClockLogic": "0x68B1D87F95878fE05B998F19b66F4baba5De1aed", + "GovernorConfigurator": "0x3Aa5ebB10DC797CAC828524e59A333d0A371443c", + "GovernorDepositLogic": "0x4A679253410272dd5232B3Ff7cF5dbB88f295319", + "GovernorFunctionRestrictionsLogic": "0xc6e7DF5E7b4f2A278906862b61205850344D4e7d", + "GovernorProposalLogic": "0x322813Fd9A801c5507c9de605d63CEA4f2CE6c44", + "GovernorQuorumLogic": "0x4ed7c70F96B99c776995fB64377f0d4aB3B0e1C1", + "GovernorStateLogic": "0x7a2088a1bFc9d81c55368AE168C2C02570cB814F", + "GovernorVotesLogic": "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f" + }, + "VeBetterPassport": { + "PassportChecksLogic": "0x09635F643e140090A9A8Dcd712eD6285858ceBef", + "PassportConfigurator": "0xc5a5C42992dECbae36851359345FE25997F5C42d", + "PassportEntityLogic": "0x67d269191c92Caf3cD7723F116c85e6E9bf55933", + "PassportDelegationLogic": "0xE6E340D132b5f46d1e472DebcD681B2aBc16e57E", + "PassportPersonhoodLogic": "0x9E545E3C0baAB3E08CdfD552C960A1050f373042", + "PassportPoPScoreLogic": "0xc3e53F4d16Ae77Db1c982e75a937B9f60FE63690", + "PassportSignalingLogic": "0x84eA74d481Ee0A5332c457a4d796187F6Ba67fEB", + "PassportWhitelistAndBlacklistLogic": "0xa82fF9aFd8f496c3d6ac40E2a0F282E47488CFc9" + } +} \ No newline at end of file diff --git a/package.json b/package.json index 947fec8..a362a42 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "test:hardhat": "yarn check-or-generate-local-config && dotenv -v NEXT_PUBLIC_APP_ENV=local -e .env.example -- hardhat test --network hardhat", "test:coverage:solidity": "yarn check-or-generate-local-config && dotenv -v NEXT_PUBLIC_APP_ENV=local -v IS_TEST_COVERAGE=true -e .env.example -- hardhat coverage --testfiles ./test/**/*test.ts", "deploy": "yarn check-or-generate-local-config && yarn start-solo && dotenv -v NEXT_PUBLIC_APP_ENV=local -e .env.example -- npx hardhat run scripts/checkContractsDeployment.ts", + "deploy:hardhat": "dotenv -v NEXT_PUBLIC_APP_ENV=local -e .env.example -- npx hardhat run scripts/deploy --network hardhat", "deploy:simulation": "export RUN_SIMULATION=true; yarn deploy", "start-solo": "make solo-up", "stop-solo": "make solo-down", @@ -82,7 +83,8 @@ "tsup": "^8.1.0", "hardhat": "^2.22.6", "typechain": "^8.1.0", - "typescript": ">=4.5.0" + "typescript": ">=4.5.0", + "archiver": "^7.0.1" }, "dependencies": { "@openzeppelin/contracts": "^5.0.2", diff --git a/scripts/deploy/deploy.ts b/scripts/deploy/deploy.ts index a355365..f05f64d 100644 --- a/scripts/deploy/deploy.ts +++ b/scripts/deploy/deploy.ts @@ -12,14 +12,27 @@ import { Treasury, X2EarnApps, X2EarnRewardsPool, + VeBetterPassport, } from "../../typechain-types" -import { ContractsConfig } from "../../config/contracts/type" +import { ContractsConfig } from "../../config/contracts" import { HttpNetworkConfig } from "hardhat/types" import { setupLocalEnvironment, setupTestEnvironment } from "./setup" import { simulateRounds } from "./simulateRounds" import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers" -import { deployAndUpgrade, deployProxy, deployLibraries } from "../helpers" +import { deployAndUpgrade, deployProxy, deployProxyOnly, initializeProxy, saveContractsToFile } from "../helpers" import { shouldRunSimulation } from "../../config/contracts" +import { governanceLibraries, passportLibraries } from "../libraries" +import { + transferAdminRole, + transferContractsAddressManagerRole, + transferDecaySettingsManagerRole, + transferGovernanceRole, + transferGovernorFunctionSettingsRole, + transferMinterRole, + transferSettingsManagerRole, + transferUpgraderRole, + validateContractRole, +} from "../helpers/roles" // GalaxyMember NFT Values const name = "VeBetterDAO Galaxy Member" @@ -29,18 +42,22 @@ export async function deployAll(config: ContractsConfig) { const start = performance.now() const networkConfig = network.config as HttpNetworkConfig console.log( - `================ Deploying contracts on ${network.name} (${networkConfig.url}) with ${config.NEXT_PUBLIC_APP_ENV} configurations ================`, + `================ Deploying contracts on ${network.name} (${networkConfig.url}) with ${config.NEXT_PUBLIC_APP_ENV} configurations `, ) const [deployer] = await ethers.getSigners() + console.log(`================ Address used to deploy: ${deployer.address}`) // We use a temporary admin to deploy and initialize contracts then transfer role to the real admin // Also we have many roles in our contracts but we currently use one wallet for all roles const TEMP_ADMIN = network.name === "vechain_solo" ? config.CONTRACTS_ADMIN_ADDRESS : deployer.address + console.log("================================================================================") console.log("Temporary admin set to ", TEMP_ADMIN) - + console.log("Final admin will be set to ", config.CONTRACTS_ADMIN_ADDRESS) + console.log("================================================================================") // ---------- Contracts Deployment ---------- // - + console.log(`================ Contracts Deployment Initiated `) // ---------------------- Deploy Libraries ---------------------- + console.log("Deploying Governance Libraries") const { GovernorClockLogicLibV1, GovernorConfiguratorLibV1, @@ -50,6 +67,14 @@ export async function deployAll(config: ContractsConfig) { GovernorQuorumLogicLibV1, GovernorVotesLogicLibV1, GovernorStateLogicLibV1, + GovernorClockLogicLibV3, + GovernorConfiguratorLibV3, + GovernorFunctionRestrictionsLogicLibV3, + GovernorQuorumLogicLibV3, + GovernorProposalLogicLibV3, + GovernorVotesLogicLibV3, + GovernorDepositLogicLibV3, + GovernorStateLogicLibV3, GovernorClockLogicLib, GovernorConfiguratorLib, GovernorDepositLogicLib, @@ -58,61 +83,126 @@ export async function deployAll(config: ContractsConfig) { GovernorQuorumLogicLib, GovernorVotesLogicLib, GovernorStateLogicLib, - } = await deployLibraries() + } = await governanceLibraries() + + console.log("Deploying VeBetter Passport Libraries") + // Deploy Passport Libraries + const { + PassportChecksLogic, + PassportConfigurator, + PassportEntityLogic, + PassportDelegationLogic, + PassportPersonhoodLogic, + PassportPoPScoreLogic, + PassportSignalingLogic, + PassportWhitelistAndBlacklistLogic, + } = await passportLibraries() + + let vechainNodesAddress = "0xb81E9C5f9644Dec9e5e3Cac86b4461A222072302" // this is the mainnet address + if (network.name !== "vechain_mainnet") { + console.log("Deploying Vechain Nodes mock contracts") + + const TokenAuctionLock = await ethers.getContractFactory("TokenAuction") + const vechainNodesMock = await TokenAuctionLock.deploy() + await vechainNodesMock.waitForDeployment() + + const ClockAuctionLock = await ethers.getContractFactory("ClockAuction") + const clockAuctionContract = await ClockAuctionLock.deploy(await vechainNodesMock.getAddress(), TEMP_ADMIN) + + await vechainNodesMock.setSaleAuctionAddress(await clockAuctionContract.getAddress()) + + await vechainNodesMock.addOperator(TEMP_ADMIN) + vechainNodesAddress = await vechainNodesMock.getAddress() + + console.log("Vechain Nodes Mock deployed at: ", await vechainNodesMock.getAddress()) + } // ---------------------- Deploy Contracts ---------------------- + console.log("Deploying VeBetter DAO contracts") const b3tr = await deployB3trToken( TEMP_ADMIN, TEMP_ADMIN, // Minter config.CONTRACTS_ADMIN_ADDRESS, // Pauser ) - const vot3 = (await deployProxy("VOT3", [ - config.CONTRACTS_ADMIN_ADDRESS, // admin - config.CONTRACTS_ADMIN_ADDRESS, // pauser - config.CONTRACTS_ADMIN_ADDRESS, // upgrader - await b3tr.getAddress(), - ])) as VOT3 - console.log(`Vot3 deployed at ${await vot3.getAddress()}`) - - const timelock = (await deployProxy("TimeLock", [ - config.TIMELOCK_MIN_DELAY, - [], // proposers - [], // executors - TEMP_ADMIN, // admin - config.CONTRACTS_ADMIN_ADDRESS, // upgrader - ])) as TimeLock - console.log(`TimeLock deployed at ${await timelock.getAddress()}`) - - const treasury = (await deployProxy("Treasury", [ - await b3tr.getAddress(), - await vot3.getAddress(), - await timelock.getAddress(), - TEMP_ADMIN, // admin - config.CONTRACTS_ADMIN_ADDRESS, // upgrader - config.CONTRACTS_ADMIN_ADDRESS, //pauser - config.TREASURY_TRANSFER_LIMIT_VET, - config.TREASURY_TRANSFER_LIMIT_B3TR, - config.TREASURY_TRANSFER_LIMIT_VOT3, - config.TREASURY_TRANSFER_LIMIT_VTHO, - ])) as Treasury - console.log(`Treasury deployed at ${await treasury.getAddress()}`) - - const x2EarnApps = (await deployProxy("X2EarnApps", [ - config.XAPP_BASE_URI, - [TEMP_ADMIN], //admins - config.CONTRACTS_ADMIN_ADDRESS, // upgrader - TEMP_ADMIN, // governance role - ])) as X2EarnApps - console.log(`X2EarnApps deployed at ${await x2EarnApps.getAddress()}`) + const vot3 = (await deployProxy( + "VOT3", + [ + config.CONTRACTS_ADMIN_ADDRESS, // admin + config.CONTRACTS_ADMIN_ADDRESS, // pauser + config.CONTRACTS_ADMIN_ADDRESS, // upgrader + await b3tr.getAddress(), + ], + undefined, + true, + )) as VOT3 + + const timelock = (await deployProxy( + "TimeLock", + [ + config.TIMELOCK_MIN_DELAY, + [], // proposers + [], // executors + TEMP_ADMIN, // admin + config.CONTRACTS_ADMIN_ADDRESS, // upgrader + ], + undefined, + true, + )) as TimeLock + + const treasury = (await deployProxy( + "Treasury", + [ + await b3tr.getAddress(), + await vot3.getAddress(), + await timelock.getAddress(), + TEMP_ADMIN, // admin + config.CONTRACTS_ADMIN_ADDRESS, // upgrader + config.CONTRACTS_ADMIN_ADDRESS, //pauser + config.TREASURY_TRANSFER_LIMIT_VET, + config.TREASURY_TRANSFER_LIMIT_B3TR, + config.TREASURY_TRANSFER_LIMIT_VOT3, + config.TREASURY_TRANSFER_LIMIT_VTHO, + ], + undefined, + true, + )) as Treasury + + const x2EarnApps = (await deployProxy( + "X2EarnApps", + [ + config.XAPP_BASE_URI, + [TEMP_ADMIN], //admins + config.CONTRACTS_ADMIN_ADDRESS, // upgrader + TEMP_ADMIN, // governance role + ], + undefined, + true, + )) as X2EarnApps + + // Initialization requires the address of the x2EarnRewardsPool, for this reason we will initialize it after + const veBetterPassportContractAddress = await deployProxyOnly( + "VeBetterPassport", + { + PassportChecksLogic: await PassportChecksLogic.getAddress(), + PassportConfigurator: await PassportConfigurator.getAddress(), + PassportEntityLogic: await PassportEntityLogic.getAddress(), + PassportDelegationLogic: await PassportDelegationLogic.getAddress(), + PassportPersonhoodLogic: await PassportPersonhoodLogic.getAddress(), + PassportPoPScoreLogic: await PassportPoPScoreLogic.getAddress(), + PassportSignalingLogic: await PassportSignalingLogic.getAddress(), + PassportWhitelistAndBlacklistLogic: await PassportWhitelistAndBlacklistLogic.getAddress(), + }, + true, + ) const x2EarnRewardsPool = (await deployAndUpgrade( - ["X2EarnRewardsPoolV1", "X2EarnRewardsPool"], + ["X2EarnRewardsPoolV1", "X2EarnRewardsPoolV2", "X2EarnRewardsPool"], [ [ config.CONTRACTS_ADMIN_ADDRESS, // admin config.CONTRACTS_ADMIN_ADDRESS, // contracts address manager - config.CONTRACTS_ADMIN_ADDRESS, // upgrader + TEMP_ADMIN, // upgrader //TODO: transferRole await b3tr.getAddress(), await x2EarnApps.getAddress(), ], @@ -120,20 +210,20 @@ export async function deployAll(config: ContractsConfig) { config.CONTRACTS_ADMIN_ADDRESS, // impact admin address config.X_2_EARN_INITIAL_IMPACT_KEYS, // impact keys ], + [veBetterPassportContractAddress], ], { - versions: [undefined, 2], + logOutput: true, + versions: [undefined, 2, 3], }, )) as X2EarnRewardsPool - console.log(`X2EarnRewardsPool deployed at ${await x2EarnRewardsPool.getAddress()}`) - const xAllocationPool = (await deployAndUpgrade( ["XAllocationPoolV1", "XAllocationPool"], [ [ TEMP_ADMIN, // admin - config.CONTRACTS_ADMIN_ADDRESS, // upgrader + TEMP_ADMIN, // upgrader TEMP_ADMIN, // contractsAddressManager await b3tr.getAddress(), await treasury.getAddress(), @@ -144,65 +234,77 @@ export async function deployAll(config: ContractsConfig) { ], { versions: [undefined, 2], + logOutput: true, }, )) as XAllocationPool - console.log(`XAllocationPool deployed at ${await xAllocationPool.getAddress()}`) - // Deploy the GalaxyMember contract with Max Mintable Level 1 - const galaxyMember = (await deployProxy("GalaxyMember", [ - { - name: name, - symbol: symbol, - admin: TEMP_ADMIN, - upgrader: config.CONTRACTS_ADMIN_ADDRESS, - pauser: config.CONTRACTS_ADMIN_ADDRESS, - minter: config.CONTRACTS_ADMIN_ADDRESS, - contractsAddressManager: TEMP_ADMIN, - maxLevel: 1, - 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 GalaxyMember - console.log(`GalaxyMember deployed at ${await galaxyMember.getAddress()}`) + const galaxyMember = (await deployProxy( + "GalaxyMember", + [ + { + name: name, + symbol: symbol, + admin: TEMP_ADMIN, + upgrader: config.CONTRACTS_ADMIN_ADDRESS, + pauser: config.CONTRACTS_ADMIN_ADDRESS, + minter: config.CONTRACTS_ADMIN_ADDRESS, + contractsAddressManager: TEMP_ADMIN, + maxLevel: 1, + baseTokenURI: config.GM_NFT_BASE_URI, + b3trToUpgradeToLevel: config.GM_NFT_B3TR_REQUIRED_TO_UPGRADE_TO_LEVEL, + b3tr: await b3tr.getAddress(), + treasury: await treasury.getAddress(), + }, + ], + undefined, + true, + )) as GalaxyMember - const emissions = (await deployProxy("Emissions", [ - { - minter: TEMP_ADMIN, - admin: TEMP_ADMIN, - upgrader: config.CONTRACTS_ADMIN_ADDRESS, - contractsAddressManager: TEMP_ADMIN, - decaySettingsManager: TEMP_ADMIN, - b3trAddress: await b3tr.getAddress(), - destinations: [ - await xAllocationPool.getAddress(), - config.VOTE_2_EARN_POOL_ADDRESS, - await treasury.getAddress(), - config.MIGRATION_ADDRESS, - ], - initialXAppAllocation: config.INITIAL_X_ALLOCATION, - cycleDuration: config.EMISSIONS_CYCLE_DURATION, - decaySettings: [ - config.EMISSIONS_X_ALLOCATION_DECAY_PERCENTAGE, - config.EMISSIONS_VOTE_2_EARN_DECAY_PERCENTAGE, - config.EMISSIONS_X_ALLOCATION_DECAY_PERIOD, - config.EMISSIONS_VOTE_2_EARN_ALLOCATION_DECAY_PERIOD, + const emissions = (await deployAndUpgrade( + ["EmissionsV1", "Emissions"], + [ + [ + { + minter: TEMP_ADMIN, + admin: TEMP_ADMIN, + upgrader: TEMP_ADMIN, + contractsAddressManager: TEMP_ADMIN, + decaySettingsManager: TEMP_ADMIN, + b3trAddress: await b3tr.getAddress(), + destinations: [ + await xAllocationPool.getAddress(), + config.VOTE_2_EARN_POOL_ADDRESS, + await treasury.getAddress(), + config.MIGRATION_ADDRESS, + ], + initialXAppAllocation: config.INITIAL_X_ALLOCATION, + cycleDuration: config.EMISSIONS_CYCLE_DURATION, + decaySettings: [ + config.EMISSIONS_X_ALLOCATION_DECAY_PERCENTAGE, + config.EMISSIONS_VOTE_2_EARN_DECAY_PERCENTAGE, + config.EMISSIONS_X_ALLOCATION_DECAY_PERIOD, + config.EMISSIONS_VOTE_2_EARN_ALLOCATION_DECAY_PERIOD, + ], + treasuryPercentage: config.EMISSIONS_TREASURY_PERCENTAGE, + maxVote2EarnDecay: config.EMISSIONS_MAX_VOTE_2_EARN_DECAY_PERCENTAGE, + migrationAmount: config.MIGRATION_AMOUNT, + }, ], - treasuryPercentage: config.EMISSIONS_TREASURY_PERCENTAGE, - maxVote2EarnDecay: config.EMISSIONS_MAX_VOTE_2_EARN_DECAY_PERCENTAGE, - migrationAmount: config.MIGRATION_AMOUNT, + [config.EMISSIONS_IS_NOT_ALIGNED], + ], + { + versions: [undefined, 2], + logOutput: true, }, - ])) as Emissions - console.log(`Emissions deployed at ${await emissions.getAddress()}`) + )) as Emissions const voterRewards = (await deployAndUpgrade( ["VoterRewardsV1", "VoterRewards"], [ [ TEMP_ADMIN, // admin - config.CONTRACTS_ADMIN_ADDRESS, // upgrader + TEMP_ADMIN, // upgrader // TODO: transferRole config.CONTRACTS_ADMIN_ADDRESS, // contractsAddressManager await emissions.getAddress(), await galaxyMember.getAddress(), @@ -214,30 +316,80 @@ export async function deployAll(config: ContractsConfig) { ], { versions: [undefined, 2], + logOutput: true, }, )) as VoterRewards - const xAllocationVoting = (await deployProxy("XAllocationVoting", [ + const xAllocationVoting = (await deployAndUpgrade( + ["XAllocationVotingV1", "XAllocationVoting"], + [ + [ + { + vot3Token: await vot3.getAddress(), + quorumPercentage: config.X_ALLOCATION_VOTING_QUORUM_PERCENTAGE, + initialVotingPeriod: config.EMISSIONS_CYCLE_DURATION - 1, + timeLock: await timelock.getAddress(), + voterRewards: await voterRewards.getAddress(), + emissions: await emissions.getAddress(), + admins: [await timelock.getAddress(), TEMP_ADMIN], + upgrader: TEMP_ADMIN, + contractsAddressManager: TEMP_ADMIN, + x2EarnAppsAddress: await x2EarnApps.getAddress(), + baseAllocationPercentage: config.X_ALLOCATION_POOL_BASE_ALLOCATION_PERCENTAGE, + appSharesCap: config.X_ALLOCATION_POOL_APP_SHARES_MAX_CAP, + votingThreshold: config.X_ALLOCATION_VOTING_VOTING_THRESHOLD, + }, + ], + [veBetterPassportContractAddress], + ], + { + versions: [undefined, 2], + logOutput: true, + }, + )) as XAllocationVoting + + const veBetterPassport = (await initializeProxy( + veBetterPassportContractAddress, + "VeBetterPassport", + [ + { + x2EarnApps: await x2EarnApps.getAddress(), + xAllocationVoting: await xAllocationVoting.getAddress(), + galaxyMember: await galaxyMember.getAddress(), + signalingThreshold: config.VEPASSPORT_BOT_SIGNALING_THRESHOLD, //signalingThreshold + roundsForCumulativeScore: config.VEPASSPORT_ROUNDS_FOR_CUMULATIVE_PARTICIPATION_SCORE, //roundsForCumulativeScore + minimumGalaxyMemberLevel: config.VEPASSPORT_GALAXY_MEMBER_MINIMUM_LEVEL, //galaxyMemberMinimumLevel + blacklistThreshold: config.VEPASSPORT_BLACKLIST_THRESHOLD_PERCENTAGE, //blacklistThreshold + whitelistThreshold: config.VEPASSPORT_WHITELIST_THRESHOLD_PERCENTAGE, //whitelistThreshold + maxEntitiesPerPassport: config.VEPASSPORT_PASSPORT_MAX_ENTITIES, //maxEntitiesPerPassport + decayRate: config.VEPASSPORT_DECAY_RATE, //decayRate + }, + { + admin: config.CONTRACTS_ADMIN_ADDRESS, // admins + botSignaler: config.CONTRACTS_ADMIN_ADDRESS, // botSignaler + upgrader: config.CONTRACTS_ADMIN_ADDRESS, // upgrader + settingsManager: TEMP_ADMIN, // settingsManager + roleGranter: config.CONTRACTS_ADMIN_ADDRESS, // roleGranter + blacklister: config.CONTRACTS_ADMIN_ADDRESS, // blacklister + whitelister: config.CONTRACTS_ADMIN_ADDRESS, // whitelistManager + actionRegistrar: config.CONTRACTS_ADMIN_ADDRESS, // actionRegistrar + actionScoreManager: config.CONTRACTS_ADMIN_ADDRESS, // actionScoreManager + }, + ], { - vot3Token: await vot3.getAddress(), - quorumPercentage: config.X_ALLOCATION_VOTING_QUORUM_PERCENTAGE, - initialVotingPeriod: config.EMISSIONS_CYCLE_DURATION - 1, - timeLock: await timelock.getAddress(), - voterRewards: await voterRewards.getAddress(), - emissions: await emissions.getAddress(), - admins: [await timelock.getAddress(), TEMP_ADMIN], - upgrader: config.CONTRACTS_ADMIN_ADDRESS, - contractsAddressManager: TEMP_ADMIN, - x2EarnAppsAddress: await x2EarnApps.getAddress(), - baseAllocationPercentage: config.X_ALLOCATION_POOL_BASE_ALLOCATION_PERCENTAGE, - appSharesCap: config.X_ALLOCATION_POOL_APP_SHARES_MAX_CAP, - votingThreshold: config.X_ALLOCATION_VOTING_VOTING_THRESHOLD, + PassportChecksLogic: await PassportChecksLogic.getAddress(), + PassportConfigurator: await PassportConfigurator.getAddress(), + PassportEntityLogic: await PassportEntityLogic.getAddress(), + PassportDelegationLogic: await PassportDelegationLogic.getAddress(), + PassportPersonhoodLogic: await PassportPersonhoodLogic.getAddress(), + PassportPoPScoreLogic: await PassportPoPScoreLogic.getAddress(), + PassportSignalingLogic: await PassportSignalingLogic.getAddress(), + PassportWhitelistAndBlacklistLogic: await PassportWhitelistAndBlacklistLogic.getAddress(), }, - ])) as XAllocationVoting - console.log(`XAllocationVoting deployed at ${await xAllocationVoting.getAddress()}`) + )) as VeBetterPassport const governor = (await deployAndUpgrade( - ["B3TRGovernorV1", "B3TRGovernorV2", "B3TRGovernor"], + ["B3TRGovernorV1", "B3TRGovernorV2", "B3TRGovernorV3", "B3TRGovernor"], [ [ { @@ -262,9 +414,10 @@ export async function deployAll(config: ContractsConfig) { ], [], [], + [veBetterPassportContractAddress], ], { - versions: [undefined, 2, 3], + versions: [undefined, 2, 3, 4], libraries: [ { GovernorClockLogicV1: await GovernorClockLogicLibV1.getAddress(), @@ -286,6 +439,16 @@ export async function deployAll(config: ContractsConfig) { GovernorStateLogicV1: await GovernorStateLogicLibV1.getAddress(), GovernorVotesLogicV1: await GovernorVotesLogicLibV1.getAddress(), }, + { + GovernorClockLogicV3: await GovernorClockLogicLibV3.getAddress(), + GovernorConfiguratorV3: await GovernorConfiguratorLibV3.getAddress(), + GovernorDepositLogicV3: await GovernorDepositLogicLibV3.getAddress(), + GovernorFunctionRestrictionsLogicV3: await GovernorFunctionRestrictionsLogicLibV3.getAddress(), + GovernorProposalLogicV3: await GovernorProposalLogicLibV3.getAddress(), + GovernorQuorumLogicV3: await GovernorQuorumLogicLibV3.getAddress(), + GovernorStateLogicV3: await GovernorStateLogicLibV3.getAddress(), + GovernorVotesLogicV3: await GovernorVotesLogicLibV3.getAddress(), + }, { GovernorClockLogic: await GovernorClockLogicLib.getAddress(), GovernorConfigurator: await GovernorConfiguratorLib.getAddress(), @@ -297,13 +460,12 @@ export async function deployAll(config: ContractsConfig) { GovernorVotesLogic: await GovernorVotesLogicLib.getAddress(), }, ], + logOutput: true, }, )) as B3TRGovernor - console.log(`Governor deployed at ${await governor.getAddress()}`) - const date = new Date(performance.now() - start) - console.log(`Contracts deployed in ${date.getMinutes()}m ${date.getSeconds()}s`) + console.log(`================ Contracts deployed in ${date.getMinutes()}m ${date.getSeconds()}s `) const contractAddresses: Record = { B3TR: await b3tr.getAddress(), @@ -318,10 +480,12 @@ export async function deployAll(config: ContractsConfig) { X2EarnRewardsPool: await x2EarnRewardsPool.getAddress(), XAllocationPool: await xAllocationPool.getAddress(), XAllocationVoting: await xAllocationVoting.getAddress(), + VeBetterPassport: await veBetterPassport.getAddress(), } const libraries: { B3TRGovernor: Record + VeBetterPassport: Record } = { B3TRGovernor: { GovernorClockLogic: await GovernorClockLogicLib.getAddress(), @@ -333,9 +497,25 @@ export async function deployAll(config: ContractsConfig) { GovernorStateLogic: await GovernorStateLogicLib.getAddress(), GovernorVotesLogic: await GovernorVotesLogicLib.getAddress(), }, + VeBetterPassport: { + PassportChecksLogic: await PassportChecksLogic.getAddress(), + PassportConfigurator: await PassportConfigurator.getAddress(), + PassportEntityLogic: await PassportEntityLogic.getAddress(), + PassportDelegationLogic: await PassportDelegationLogic.getAddress(), + PassportPersonhoodLogic: await PassportPersonhoodLogic.getAddress(), + PassportPoPScoreLogic: await PassportPoPScoreLogic.getAddress(), + PassportSignalingLogic: await PassportSignalingLogic.getAddress(), + PassportWhitelistAndBlacklistLogic: await PassportWhitelistAndBlacklistLogic.getAddress(), + }, } - await setWhitelistedFunctions(contractAddresses, config, governor, deployer, libraries) // Set whitelisted functions for governor proposals + await setWhitelistedFunctions(contractAddresses, config, governor, deployer, libraries, true) // Set whitelisted functions for governor proposals + + // Enable Participation Score for VeBetterPassport + await veBetterPassport + .connect(deployer) + .toggleCheck(4) + .then(async tx => await tx.wait()) // ---------- Configure contract roles for setup ---------- // @@ -460,6 +640,8 @@ export async function deployAll(config: ContractsConfig) { await transferAdminRole(galaxyMember, deployer, config.CONTRACTS_ADMIN_ADDRESS) await transferMinterRole(emissions, deployer, deployer.address, config.CONTRACTS_ADMIN_ADDRESS) + await transferContractsAddressManagerRole(emissions, deployer, config.CONTRACTS_ADMIN_ADDRESS) + await transferDecaySettingsManagerRole(emissions, deployer, config.CONTRACTS_ADMIN_ADDRESS) await transferAdminRole(emissions, deployer, config.CONTRACTS_ADMIN_ADDRESS) await transferAdminRole(voterRewards, deployer, config.CONTRACTS_ADMIN_ADDRESS) @@ -486,9 +668,23 @@ export async function deployAll(config: ContractsConfig) { await transferAdminRole(timelock, deployer, config.CONTRACTS_ADMIN_ADDRESS) + await transferSettingsManagerRole(veBetterPassport, deployer, config.CONTRACTS_ADMIN_ADDRESS) + + await transferUpgraderRole(xAllocationPool, deployer, config.CONTRACTS_ADMIN_ADDRESS) + await transferUpgraderRole(emissions, deployer, config.CONTRACTS_ADMIN_ADDRESS) + console.log("Roles updated successfully!") - console.log("================ Validating roles ================ ") + console.log("================ Validating roles") + + // VeBetterPassport + await validateContractRole( + veBetterPassport, + config.CONTRACTS_ADMIN_ADDRESS, + TEMP_ADMIN, + await veBetterPassport.SETTINGS_MANAGER_ROLE(), + ) + // B3TR await validateContractRole(b3tr, await emissions.getAddress(), TEMP_ADMIN, await b3tr.MINTER_ROLE()) await validateContractRole(b3tr, config.CONTRACTS_ADMIN_ADDRESS, TEMP_ADMIN, await b3tr.MINTER_ROLE()) @@ -548,6 +744,18 @@ export async function deployAll(config: ContractsConfig) { TEMP_ADMIN, await emissions.DEFAULT_ADMIN_ROLE(), ) + await validateContractRole( + emissions, + config.CONTRACTS_ADMIN_ADDRESS, + TEMP_ADMIN, + await emissions.CONTRACTS_ADDRESS_MANAGER_ROLE(), + ) + await validateContractRole( + emissions, + config.CONTRACTS_ADMIN_ADDRESS, + TEMP_ADMIN, + await emissions.DECAY_SETTINGS_MANAGER_ROLE(), + ) await validateContractRole(emissions, config.CONTRACTS_ADMIN_ADDRESS, TEMP_ADMIN, await emissions.UPGRADER_ROLE()) // VoterRewards @@ -582,6 +790,28 @@ export async function deployAll(config: ContractsConfig) { await voterRewards.CONTRACTS_ADDRESS_MANAGER_ROLE(), ) + // X2EarnRewardsPool + await validateContractRole( + x2EarnRewardsPool, + config.CONTRACTS_ADMIN_ADDRESS, + TEMP_ADMIN, + await x2EarnRewardsPool.DEFAULT_ADMIN_ROLE(), + ) + + await validateContractRole( + x2EarnRewardsPool, + config.CONTRACTS_ADMIN_ADDRESS, + TEMP_ADMIN, + await x2EarnRewardsPool.CONTRACTS_ADDRESS_MANAGER_ROLE(), + ) + + await validateContractRole( + x2EarnRewardsPool, + config.CONTRACTS_ADMIN_ADDRESS, + TEMP_ADMIN, + await x2EarnRewardsPool.UPGRADER_ROLE(), + ) + // XAllocationPool await validateContractRole( xAllocationPool, @@ -701,19 +931,13 @@ export async function deployAll(config: ContractsConfig) { console.log("Roles validated successfully!") } - console.log("contracts", { - b3trContractAddress: await b3tr.getAddress(), - vot3ContractAddress: await vot3.getAddress(), - b3trGovernorAddress: await governor.getAddress(), - timelockContractAddress: await timelock.getAddress(), - xAllocationPoolContractAddress: await xAllocationPool.getAddress(), - xAllocationVotingContractAddress: await xAllocationVoting.getAddress(), - emissionsContractAddress: await emissions.getAddress(), - voterRewardsContractAddress: await voterRewards.getAddress(), - galaxyMemberContractAddress: await galaxyMember.getAddress(), - treasuryContractAddress: await treasury.getAddress(), - x2EarnAppsContractAddress: await x2EarnApps.getAddress(), - }) + console.log("================================================================================") + console.log("Deployment completed successfully!") + console.log("================================================================================") + + console.log("Libraries", libraries) + console.log("Contracts", contractAddresses) + await saveContractsToFile(contractAddresses, libraries) const end = new Date(performance.now() - start) console.log(`Total execution time: ${end.getMinutes()}m ${end.getSeconds()}s`) @@ -731,190 +955,29 @@ export async function deployAll(config: ContractsConfig) { treasury: treasury, x2EarnApps: x2EarnApps, x2EarnRewardsPool: x2EarnRewardsPool, + vechainNodesMock: vechainNodesAddress, + veBetterPassport: veBetterPassport, + libraries: { + governorClockLogic: GovernorClockLogicLib, + governorConfigurator: GovernorConfiguratorLib, + governorDepositLogic: GovernorDepositLogicLib, + governorFunctionRestrictionsLogic: GovernorFunctionRestrictionsLogicLib, + governorProposalLogic: GovernorProposalLogicLib, + governorQuorumLogic: GovernorQuorumLogicLib, + governorStateLogic: GovernorStateLogicLib, + governorVotesLogic: GovernorVotesLogicLib, + }, } // close the script } -const transferAdminRole = async ( - contract: - | B3TR - | VOT3 - | GalaxyMember - | Emissions - | VoterRewards - | XAllocationPool - | XAllocationVoting - | Treasury - | B3TRGovernor - | X2EarnApps - | TimeLock, - oldAdmin: HardhatEthersSigner, - newAdminAddress: string, -) => { - if (oldAdmin.address === newAdminAddress) - throw new Error("Admin role not transferred. New admin is the same as old admin") - - const adminRole = await contract.DEFAULT_ADMIN_ROLE() - await contract - .connect(oldAdmin) - .grantRole(adminRole, newAdminAddress) - .then(async tx => await tx.wait()) - await contract - .connect(oldAdmin) - .renounceRole(adminRole, oldAdmin.address) - .then(async tx => await tx.wait()) - - const newAdminSet = await contract.hasRole(adminRole, newAdminAddress) - const oldAdminRemoved = !(await contract.hasRole(adminRole, oldAdmin.address)) - if (!newAdminSet || !oldAdminRemoved) - throw new Error("Admin role not set correctly on " + (await contract.getAddress())) - - console.log("Admin role transferred successfully on " + (await contract.getAddress())) -} - -const transferMinterRole = async ( - contract: Emissions | B3TR, - admin: HardhatEthersSigner, - oldMinterAddress: string, - newMinterAddress?: string, -) => { - if (!newMinterAddress && oldMinterAddress === newMinterAddress) - throw new Error("Minter role not transferred. New minter is the same as old minter") - - const minterRole = await contract.MINTER_ROLE() - - // If newMinterAddress is provided, set a new minter before revoking the old one - // otherwise just revoke the old one - if (newMinterAddress) { - await contract - .connect(admin) - .grantRole(minterRole, newMinterAddress) - .then(async tx => await tx.wait()) - await contract - .connect(admin) - .revokeRole(minterRole, oldMinterAddress) - .then(async tx => await tx.wait()) - - const newMinterSet = await contract.hasRole(minterRole, newMinterAddress) - const oldMinterRemoved = !(await contract.hasRole(minterRole, oldMinterAddress)) - if (!newMinterSet || !oldMinterRemoved) - throw new Error("Minter role not set correctly on " + (await contract.getAddress())) - - console.log("Minter role transferred successfully on " + (await contract.getAddress())) - } else { - await contract - .connect(admin) - .revokeRole(minterRole, oldMinterAddress) - .then(async tx => await tx.wait()) - - const oldMinterRemoved = !(await contract.hasRole(minterRole, oldMinterAddress)) - if (!oldMinterRemoved) throw new Error("Minter role not removed correctly on " + (await contract.getAddress())) - - console.log("Minter role revoked (without granting new) successfully on " + (await contract.getAddress())) - } -} - -// Transfer governance role to treasury contract admin for intial phases of project -const transferGovernanceRole = async ( - contract: Treasury | X2EarnApps, - admin: HardhatEthersSigner, - oldAddress: string, - newAddress?: string, -) => { - if (!newAddress && oldAddress === newAddress) - throw new Error("Governance role not transferred. New governance is the same as old governance") - - const governanceRole = await contract.GOVERNANCE_ROLE() - - // If newAddress is provided, set a new admin before revoking the old one - // otherwise just revoke the old one - if (newAddress) { - await contract - .connect(admin) - .grantRole(governanceRole, newAddress) - .then(async tx => await tx.wait()) - await contract - .connect(admin) - .revokeRole(governanceRole, oldAddress) - .then(async tx => await tx.wait()) - - const newGovernanceSet = await contract.hasRole(governanceRole, newAddress) - const oldGovernanceRemoved = !(await contract.hasRole(governanceRole, oldAddress)) - if (!newGovernanceSet || !oldGovernanceRemoved) - throw new Error("Minter role not set correctly on " + (await contract.getAddress())) - - console.log("Governance role transferred successfully on " + (await contract.getAddress())) - } else { - await contract - .connect(admin) - .revokeRole(governanceRole, oldAddress) - .then(async tx => await tx.wait()) - - const oldGovernanceRemoved = !(await contract.hasRole(governanceRole, oldAddress)) - if (!oldGovernanceRemoved) - throw new Error("Governance role not removed correctly on " + (await contract.getAddress())) - - console.log("Governance role revoked (without granting new) successfully on " + (await contract.getAddress())) - } -} - -const transferContractsAddressManagerRole = async ( - contract: GalaxyMember | XAllocationPool | XAllocationVoting | Emissions, - admin: HardhatEthersSigner, - newAddress: string, -) => { - if (admin.address === newAddress) throw new Error("Role not transferred. New address is the same as old address") - - const contractsAddressManagerRole = await contract.CONTRACTS_ADDRESS_MANAGER_ROLE() - - await contract - .connect(admin) - .grantRole(contractsAddressManagerRole, newAddress) - .then(async tx => await tx.wait()) - await contract - .connect(admin) - .renounceRole(contractsAddressManagerRole, admin.address) - .then(async tx => await tx.wait()) - - const newRoleSet = await contract.hasRole(contractsAddressManagerRole, newAddress) - const oldRoleRemoved = !(await contract.hasRole(contractsAddressManagerRole, admin.address)) - - if (!newRoleSet || !oldRoleRemoved) throw new Error("Role not set correctly on " + (await contract.getAddress())) - - console.log("Contract Address Manager Role transferred successfully on " + (await contract.getAddress())) -} - -const transferGovernorFunctionSettingsRole = async ( - contract: B3TRGovernor, - admin: HardhatEthersSigner, - newAddress: string, -) => { - const governorFunctionSettingsRole = await contract.GOVERNOR_FUNCTIONS_SETTINGS_ROLE() - - await contract - .connect(admin) - .grantRole(governorFunctionSettingsRole, newAddress) - .then(async tx => await tx.wait()) - await contract - .connect(admin) - .renounceRole(governorFunctionSettingsRole, admin.address) - .then(async tx => await tx.wait()) - - const newRoleSet = await contract.hasRole(governorFunctionSettingsRole, newAddress) - const oldRoleRemoved = !(await contract.hasRole(governorFunctionSettingsRole, admin.address)) - - if (!newRoleSet || !oldRoleRemoved) throw new Error("Role not set correctly on " + (await contract.getAddress())) - - console.log("Governor Function Settings Role transferred successfully on " + (await contract.getAddress())) -} - async function deployB3trToken(admin: string, minter: string, pauser: string): Promise { const B3trContract = await ethers.getContractFactory("B3TR") // Use the global variable const contract = await B3trContract.deploy(admin, minter, pauser) await contract.waitForDeployment() - console.log(`B3tr deployed at ${await contract.getAddress()}`) + console.log(`B3TR impl.: ${await contract.getAddress()}`) return contract } @@ -939,7 +1002,10 @@ export const setWhitelistedFunctions = async ( governor: B3TRGovernor, admin: HardhatEthersSigner, libraries: Record>, + logOutput = false, ) => { + if (logOutput) console.log("================ Setting whitelisted functions in B3TRGovernor contract") + const { B3TR_GOVERNOR_WHITELISTED_METHODS } = config for (const [contract, functions] of Object.entries(B3TR_GOVERNOR_WHITELISTED_METHODS)) { @@ -964,33 +1030,8 @@ export const setWhitelistedFunctions = async ( .connect(admin) .setWhitelistFunctions(contractAddresses[contract], whitelistFunctionSelectors, true) .then(async tx => await tx.wait()) + + if (logOutput) console.log(`Whitelisted functions set for ${contract} in B3TRGovernor contract`) } } } - -// Function that checks that roles are set correctly on the contracts -const validateContractRole = async ( - contract: - | B3TR - | VOT3 - | GalaxyMember - | Emissions - | VoterRewards - | XAllocationPool - | XAllocationVoting - | Treasury - | TimeLock - | B3TRGovernor - | X2EarnRewardsPool - | X2EarnApps, - expectedAddress: string, - tempAdmin: string, - role: string, -) => { - const roleSet = await contract.hasRole(role, expectedAddress) - // Check that the temporary admin does not have the role - const roleRemoved = !(await contract.hasRole(role, tempAdmin)) - - if (!roleSet || !roleRemoved) - throw new Error("Role " + role + " not set correctly on " + (await contract.getAddress())) -} diff --git a/scripts/helpers/fs.ts b/scripts/helpers/fs.ts index f8063ee..6be1eeb 100644 --- a/scripts/helpers/fs.ts +++ b/scripts/helpers/fs.ts @@ -1,5 +1,7 @@ -import fs from "fs/promises" +import fs from "fs" import path from "path" +import FormData from "form-data" +import archiver from "archiver" /** * Reads files from a directory and returns an array of `File` objects. @@ -10,13 +12,13 @@ import path from "path" * @throws An error if the directory does not exist. */ async function readFilesFromDirectory(dirPath: string): Promise { - const entries = await fs.readdir(dirPath, { withFileTypes: true }) + const entries = await fs.promises.readdir(dirPath, { withFileTypes: true }) const files: File[] = [] for (const entry of entries) { if (entry.isFile()) { const filePath = path.join(dirPath, entry.name) - const content = await fs.readFile(filePath) + const content = await fs.promises.readFile(filePath) const mimeType = "image/png" // TODO: Get the MIME type from the file const file: File = new File([content], entry.name, { type: mimeType }) files.push(file) @@ -26,4 +28,113 @@ async function readFilesFromDirectory(dirPath: string): Promise { return files } -export { readFilesFromDirectory } +function formData(path: string): FormData { + // Create a form data instance + const form = new FormData() + form.append("file", fs.createReadStream(path)) + return form +} + +function getFolderName(folderPath: string): string { + return path.basename(folderPath) +} + +function copyImages(srcFolder: string, destFolder: string): string { + // Ensure the destination folder exists + if (!fs.existsSync(destFolder)) { + fs.mkdirSync(destFolder, { recursive: true }) + } + + // Read all files in the source folder + const files = fs.readdirSync(srcFolder) + + // Copy each image file from the source folder to the destination folder + files.forEach(file => { + const srcFilePath = path.join(srcFolder, file) + const destFilePath = path.join(destFolder, file) + + // Check if the file is an image (you can extend this to check for specific image types) + if (file.match(/\.(jpg|jpeg|png|gif)$/i)) { + fs.copyFileSync(srcFilePath, destFilePath) + } + }) + + return destFolder +} + +async function zipFolder(sourceDir: string, outPath: fs.PathLike): Promise { + // Ensure that the stream truncates (overwrites) the file if it already exists + const stream = fs.createWriteStream(outPath, { flags: "w" }) + + const archive = archiver("zip", { zlib: { level: 9 } }) + + return new Promise((resolve, reject) => { + archive + .directory(sourceDir, path.basename(sourceDir)) // Keeps the root folder + .on("error", (err: any) => reject(err)) + .pipe(stream) + + stream.on("close", () => resolve()) + + archive.finalize() + }) +} + +/** + * Save the deployed contracts addresses to a file. + * @param contracts - The deployed contracts + * @param libraries - The deployed libraries + */ +async function saveContractsToFile( + contracts: Record, + libraries: { + B3TRGovernor: Record + }, +): Promise { + const OUTPUT_PATH = path.join(__dirname, `../../deploy_output`) + + // Reset the output directory + if (fs.existsSync(OUTPUT_PATH)) { + fs.rmSync(OUTPUT_PATH, { recursive: true }) + } + // Ensure the output directory exists + fs.mkdirSync(OUTPUT_PATH) + + await fs.promises.writeFile(`${OUTPUT_PATH}/contracts.txt`, JSON.stringify(contracts, null, 2)) + await fs.promises.writeFile(`${OUTPUT_PATH}/libraries.txt`, JSON.stringify(libraries, null, 2)) + console.log(`Contracts and libraries addresses saved to ${OUTPUT_PATH}`) +} + +/** + * Save new libraries deployed to a file + * @param contracts - The deployed contracts + * @param libraries - The deployed libraries + */ +async function saveLibrariesToFile(libraries: { B3TRGovernor: Record }): Promise { + const OUTPUT_PATH = path.join(__dirname, `../../deploy_output`) + const LIBRARY_FILE_PATH = path.join(OUTPUT_PATH, "libraries.txt") + + // Ensure the output directory exists + if (!fs.existsSync(OUTPUT_PATH)) { + fs.mkdirSync(OUTPUT_PATH) + } + + // Remove the existing libraries file if it exists + if (fs.existsSync(LIBRARY_FILE_PATH)) { + fs.unlinkSync(LIBRARY_FILE_PATH) + } + + // Write the new libraries file + await fs.promises.writeFile(LIBRARY_FILE_PATH, JSON.stringify(libraries, null, 2)) + console.log(`Libraries addresses saved to ${LIBRARY_FILE_PATH}`) +} + +export { + readFilesFromDirectory, + formData, + getFolderName, + zipFolder, + copyImages, + saveContractsToFile, + saveLibrariesToFile, +} diff --git a/scripts/helpers/roles.ts b/scripts/helpers/roles.ts new file mode 100644 index 0000000..165ae21 --- /dev/null +++ b/scripts/helpers/roles.ts @@ -0,0 +1,295 @@ +import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers" +import { + B3TR, + B3TRGovernor, + Emissions, + GalaxyMember, + TimeLock, + Treasury, + VeBetterPassport, + VOT3, + VoterRewards, + X2EarnApps, + X2EarnRewardsPool, + XAllocationPool, + XAllocationVoting, +} from "../../typechain-types" + +export const transferAdminRole = async ( + contract: + | B3TR + | VOT3 + | GalaxyMember + | Emissions + | VoterRewards + | XAllocationPool + | XAllocationVoting + | Treasury + | B3TRGovernor + | X2EarnApps + | TimeLock, + oldAdmin: HardhatEthersSigner, + newAdminAddress: string, +) => { + if (oldAdmin.address === newAdminAddress) + throw new Error("Admin role not transferred. New admin is the same as old admin") + + const adminRole = await contract.DEFAULT_ADMIN_ROLE() + await contract + .connect(oldAdmin) + .grantRole(adminRole, newAdminAddress) + .then(async tx => await tx.wait()) + await contract + .connect(oldAdmin) + .renounceRole(adminRole, oldAdmin.address) + .then(async tx => await tx.wait()) + + const newAdminSet = await contract.hasRole(adminRole, newAdminAddress) + const oldAdminRemoved = !(await contract.hasRole(adminRole, oldAdmin.address)) + if (!newAdminSet || !oldAdminRemoved) + throw new Error("Admin role not set correctly on " + (await contract.getAddress())) + + console.log("Admin role transferred successfully on " + (await contract.getAddress())) +} + +export const transferMinterRole = async ( + contract: Emissions | B3TR, + admin: HardhatEthersSigner, + oldMinterAddress: string, + newMinterAddress?: string, +) => { + if (!newMinterAddress && oldMinterAddress === newMinterAddress) + throw new Error("Minter role not transferred. New minter is the same as old minter") + + const minterRole = await contract.MINTER_ROLE() + + // If newMinterAddress is provided, set a new minter before revoking the old one + // otherwise just revoke the old one + if (newMinterAddress) { + await contract + .connect(admin) + .grantRole(minterRole, newMinterAddress) + .then(async tx => await tx.wait()) + await contract + .connect(admin) + .revokeRole(minterRole, oldMinterAddress) + .then(async tx => await tx.wait()) + + const newMinterSet = await contract.hasRole(minterRole, newMinterAddress) + const oldMinterRemoved = !(await contract.hasRole(minterRole, oldMinterAddress)) + if (!newMinterSet || !oldMinterRemoved) + throw new Error("Minter role not set correctly on " + (await contract.getAddress())) + + console.log("Minter role transferred successfully on " + (await contract.getAddress())) + } else { + await contract + .connect(admin) + .revokeRole(minterRole, oldMinterAddress) + .then(async tx => await tx.wait()) + + const oldMinterRemoved = !(await contract.hasRole(minterRole, oldMinterAddress)) + if (!oldMinterRemoved) throw new Error("Minter role not removed correctly on " + (await contract.getAddress())) + + console.log("Minter role revoked (without granting new) successfully on " + (await contract.getAddress())) + } +} + +// Transfer governance role to treasury contract admin for intial phases of project +export const transferGovernanceRole = async ( + contract: Treasury | X2EarnApps, + admin: HardhatEthersSigner, + oldAddress: string, + newAddress?: string, +) => { + if (!newAddress && oldAddress === newAddress) + throw new Error("Governance role not transferred. New governance is the same as old governance") + + const governanceRole = await contract.GOVERNANCE_ROLE() + + // If newAddress is provided, set a new admin before revoking the old one + // otherwise just revoke the old one + if (newAddress) { + await contract + .connect(admin) + .grantRole(governanceRole, newAddress) + .then(async tx => await tx.wait()) + await contract + .connect(admin) + .revokeRole(governanceRole, oldAddress) + .then(async tx => await tx.wait()) + + const newGovernanceSet = await contract.hasRole(governanceRole, newAddress) + const oldGovernanceRemoved = !(await contract.hasRole(governanceRole, oldAddress)) + if (!newGovernanceSet || !oldGovernanceRemoved) + throw new Error("Minter role not set correctly on " + (await contract.getAddress())) + + console.log("Governance role transferred successfully on " + (await contract.getAddress())) + } else { + await contract + .connect(admin) + .revokeRole(governanceRole, oldAddress) + .then(async tx => await tx.wait()) + + const oldGovernanceRemoved = !(await contract.hasRole(governanceRole, oldAddress)) + if (!oldGovernanceRemoved) + throw new Error("Governance role not removed correctly on " + (await contract.getAddress())) + + console.log("Governance role revoked (without granting new) successfully on " + (await contract.getAddress())) + } +} + +export const transferContractsAddressManagerRole = async ( + contract: GalaxyMember | XAllocationPool | XAllocationVoting | Emissions, + admin: HardhatEthersSigner, + newAddress: string, +) => { + if (admin.address === newAddress) throw new Error("Role not transferred. New address is the same as old address") + + const contractsAddressManagerRole = await contract.CONTRACTS_ADDRESS_MANAGER_ROLE() + + await contract + .connect(admin) + .grantRole(contractsAddressManagerRole, newAddress) + .then(async tx => await tx.wait()) + await contract + .connect(admin) + .renounceRole(contractsAddressManagerRole, admin.address) + .then(async tx => await tx.wait()) + + const newRoleSet = await contract.hasRole(contractsAddressManagerRole, newAddress) + const oldRoleRemoved = !(await contract.hasRole(contractsAddressManagerRole, admin.address)) + + if (!newRoleSet || !oldRoleRemoved) throw new Error("Role not set correctly on " + (await contract.getAddress())) + + console.log("Contract Address Manager Role transferred successfully on " + (await contract.getAddress())) +} + +export const transferDecaySettingsManagerRole = async ( + contract: Emissions, + admin: HardhatEthersSigner, + newAddress: string, +) => { + if (admin.address === newAddress) throw new Error("Role not transferred. New address is the same as old address") + + const decaySettingsManagerRole = await contract.DECAY_SETTINGS_MANAGER_ROLE() + + await contract + .connect(admin) + .grantRole(decaySettingsManagerRole, newAddress) + .then(async tx => await tx.wait()) + await contract + .connect(admin) + .renounceRole(decaySettingsManagerRole, admin.address) + .then(async tx => await tx.wait()) + + const newRoleSet = await contract.hasRole(decaySettingsManagerRole, newAddress) + const oldRoleRemoved = !(await contract.hasRole(decaySettingsManagerRole, admin.address)) + + if (!newRoleSet || !oldRoleRemoved) throw new Error("Role not set correctly on " + (await contract.getAddress())) + + console.log("Decay Settings Manager Role transferred successfully on " + (await contract.getAddress())) +} + +export const transferGovernorFunctionSettingsRole = async ( + contract: B3TRGovernor, + admin: HardhatEthersSigner, + newAddress: string, +) => { + const governorFunctionSettingsRole = await contract.GOVERNOR_FUNCTIONS_SETTINGS_ROLE() + + await contract + .connect(admin) + .grantRole(governorFunctionSettingsRole, newAddress) + .then(async tx => await tx.wait()) + await contract + .connect(admin) + .renounceRole(governorFunctionSettingsRole, admin.address) + .then(async tx => await tx.wait()) + + const newRoleSet = await contract.hasRole(governorFunctionSettingsRole, newAddress) + const oldRoleRemoved = !(await contract.hasRole(governorFunctionSettingsRole, admin.address)) + + if (!newRoleSet || !oldRoleRemoved) throw new Error("Role not set correctly on " + (await contract.getAddress())) + + console.log("Governor Function Settings Role transferred successfully on " + (await contract.getAddress())) +} + +// Function that checks that roles are set correctly on the contracts +export const validateContractRole = async ( + contract: + | B3TR + | VOT3 + | GalaxyMember + | Emissions + | VoterRewards + | XAllocationPool + | XAllocationVoting + | Treasury + | TimeLock + | B3TRGovernor + | X2EarnRewardsPool + | X2EarnApps + | VeBetterPassport, + expectedAddress: string, + tempAdmin: string, + role: string, +) => { + if (expectedAddress === tempAdmin) return + + const roleSet = await contract.hasRole(role, expectedAddress) + // Check that the temporary admin does not have the role + const roleRemoved = !(await contract.hasRole(role, tempAdmin)) + + if (!roleSet || !roleRemoved) + throw new Error("Role " + role + " not set correctly on " + (await contract.getAddress())) +} + +export const transferSettingsManagerRole = async ( + contract: VeBetterPassport, + admin: HardhatEthersSigner, + newAddress: string, +) => { + if (admin.address === newAddress) return + + const settingsManagerRole = await contract.SETTINGS_MANAGER_ROLE() + + await contract + .connect(admin) + .grantRole(settingsManagerRole, newAddress) + .then(async tx => await tx.wait()) + await contract + .connect(admin) + .renounceRole(settingsManagerRole, admin.address) + .then(async tx => await tx.wait()) + + const newRoleSet = await contract.hasRole(settingsManagerRole, newAddress) + const oldRoleRemoved = !(await contract.hasRole(settingsManagerRole, admin.address)) + + if (!newRoleSet || !oldRoleRemoved) throw new Error("Role not set correctly on " + (await contract.getAddress())) + + console.log("Settings Manager Role transferred successfully on " + (await contract.getAddress())) +} + +export const transferUpgraderRole = async ( + contract: Emissions | XAllocationPool, + admin: HardhatEthersSigner, + newAddress: string, +) => { + if (admin.address === newAddress) return + + const upgraderRole = await contract.UPGRADER_ROLE() + + await contract + .connect(admin) + .grantRole(upgraderRole, newAddress) + .then(async tx => await tx.wait()) + await contract + .connect(admin) + .renounceRole(upgraderRole, admin.address) + .then(async tx => await tx.wait()) + + const newRoleSet = await contract.hasRole(upgraderRole, newAddress) + const oldRoleRemoved = !(await contract.hasRole(upgraderRole, admin.address)) + + if (!newRoleSet || !oldRoleRemoved) throw new Error("Role not set correctly on " + (await contract.getAddress())) +} diff --git a/scripts/helpers/upgrades.ts b/scripts/helpers/upgrades.ts index e191874..847cafd 100644 --- a/scripts/helpers/upgrades.ts +++ b/scripts/helpers/upgrades.ts @@ -73,10 +73,13 @@ export const initializeProxy = async ( proxyAddress: string, contractName: string, args: any[], + libraries: { [libraryName: string]: string } = {}, version?: number, ): Promise => { // Get the ContractFactory - const Contract = await ethers.getContractFactory(contractName) + const Contract = await ethers.getContractFactory(contractName, { + libraries: libraries, + }) // Prepare the initializer data using getInitializerData const initializerData = getInitializerData(Contract.interface, args, version) @@ -98,7 +101,7 @@ export const upgradeProxy = async ( newVersionContractName: string, proxyAddress: string, args: any[] = [], - options?: { version?: number; libraries?: { [libraryName: string]: string } }, + options?: { version?: number; libraries?: { [libraryName: string]: string }; logOutput?: boolean }, ): Promise => { // Deploy the implementation contract const Contract = await ethers.getContractFactory(newVersionContractName, { @@ -109,6 +112,8 @@ export const upgradeProxy = async ( const currentImplementationContract = await ethers.getContractAt(previousVersionContractName, proxyAddress) + options?.logOutput && console.log(`${newVersionContractName} impl.: ${await implementation.getAddress()}`) + const tx = await currentImplementationContract.upgradeToAndCall( await implementation.getAddress(), args.length > 0 ? getInitializerData(Contract.interface, args, options?.version) : "0x", @@ -162,7 +167,7 @@ export const deployAndUpgrade = async ( newVersionContractName, await proxy.getAddress(), contractArgs, - { version: options.versions?.[i], libraries: options.libraries?.[i] }, + { version: options.versions?.[i], libraries: options.libraries?.[i], logOutput: options.logOutput }, ) } diff --git a/scripts/libraries/governanceLibraries.ts b/scripts/libraries/governanceLibraries.ts new file mode 100644 index 0000000..36f8ad5 --- /dev/null +++ b/scripts/libraries/governanceLibraries.ts @@ -0,0 +1,233 @@ +import { ethers } from "hardhat" + +export async function governanceLibraries() { + // ---------------------- Version 1 ---------------------- + // Deploy Governor Clock Logic + const GovernorClockLogicV1 = await ethers.getContractFactory("GovernorClockLogicV1") + const GovernorClockLogicLibV1 = await GovernorClockLogicV1.deploy() + await GovernorClockLogicLibV1.waitForDeployment() + + // Deploy Governor Configurator + const GovernorConfiguratorV1 = await ethers.getContractFactory("GovernorConfiguratorV1") + const GovernorConfiguratorLibV1 = await GovernorConfiguratorV1.deploy() + await GovernorConfiguratorLibV1.waitForDeployment() + + // Deploy Governor Function Restrictions Logic + const GovernorFunctionRestrictionsLogicV1 = await ethers.getContractFactory("GovernorFunctionRestrictionsLogicV1") + const GovernorFunctionRestrictionsLogicLibV1 = await GovernorFunctionRestrictionsLogicV1.deploy() + await GovernorFunctionRestrictionsLogicLibV1.waitForDeployment() + + // Deploy Governor Governance Logic + const GovernorGovernanceLogicV1 = await ethers.getContractFactory("GovernorGovernanceLogicV1") + const GovernorGovernanceLogicLibV1 = await GovernorGovernanceLogicV1.deploy() + await GovernorGovernanceLogicLibV1.waitForDeployment() + + // Deploy Governor Quorum Logic + const GovernorQuorumLogicV1 = await ethers.getContractFactory("GovernorQuorumLogicV1", { + libraries: { + GovernorClockLogicV1: await GovernorClockLogicLibV1.getAddress(), + }, + }) + const GovernorQuorumLogicLibV1 = await GovernorQuorumLogicV1.deploy() + await GovernorQuorumLogicLibV1.waitForDeployment() + + // Deploy Governor Proposal Logic + const GovernorProposalLogicV1 = await ethers.getContractFactory("GovernorProposalLogicV1", { + libraries: { + GovernorClockLogicV1: await GovernorClockLogicLibV1.getAddress(), + }, + }) + const GovernorProposalLogicLibV1 = await GovernorProposalLogicV1.deploy() + await GovernorProposalLogicLibV1.waitForDeployment() + + // Governance Voting Logic + // Deploy Governor Votes Logic + const GovernorVotesLogicV1 = await ethers.getContractFactory("GovernorVotesLogicV1", { + libraries: { + GovernorClockLogicV1: await GovernorClockLogicLibV1.getAddress(), + }, + }) + const GovernorVotesLogicLibV1 = await GovernorVotesLogicV1.deploy() + await GovernorVotesLogicLibV1.waitForDeployment() + + // Deploy Governor Deposit Logic + const GovernorDepositLogicV1 = await ethers.getContractFactory("GovernorDepositLogicV1", { + libraries: { + GovernorClockLogicV1: await GovernorClockLogicLibV1.getAddress(), + }, + }) + const GovernorDepositLogicLibV1 = await GovernorDepositLogicV1.deploy() + await GovernorDepositLogicLibV1.waitForDeployment() + + // Deploy Governor State Logic + const GovernorStateLogicV1 = await ethers.getContractFactory("GovernorStateLogicV1", { + libraries: { + GovernorClockLogicV1: await GovernorClockLogicLibV1.getAddress(), + }, + }) + const GovernorStateLogicLibV1 = await GovernorStateLogicV1.deploy() + await GovernorStateLogicLibV1.waitForDeployment() + + // ---------------------- Version 3 ---------------------- + + const GovernorClockLogicV3 = await ethers.getContractFactory("GovernorClockLogicV3") + const GovernorClockLogicLibV3 = await GovernorClockLogicV3.deploy() + await GovernorClockLogicLibV3.waitForDeployment() + + // Deploy Governor Configurator + const GovernorConfiguratorV3 = await ethers.getContractFactory("GovernorConfiguratorV3") + const GovernorConfiguratorLibV3 = await GovernorConfiguratorV3.deploy() + await GovernorConfiguratorLibV3.waitForDeployment() + + // Deploy Governor Function Restrictions Logic + const GovernorFunctionRestrictionsLogicV3 = await ethers.getContractFactory("GovernorFunctionRestrictionsLogicV3") + const GovernorFunctionRestrictionsLogicLibV3 = await GovernorFunctionRestrictionsLogicV3.deploy() + await GovernorFunctionRestrictionsLogicLibV3.waitForDeployment() + + // Deploy Governor Governance Logic + const GovernorGovernanceLogicV3 = await ethers.getContractFactory("GovernorGovernanceLogicV3") + const GovernorGovernanceLogicLibV3 = await GovernorGovernanceLogicV3.deploy() + await GovernorGovernanceLogicLibV3.waitForDeployment() + + // Deploy Governor Quorum Logic + const GovernorQuorumLogicV3 = await ethers.getContractFactory("GovernorQuorumLogicV3", { + libraries: { + GovernorClockLogicV3: await GovernorClockLogicLibV3.getAddress(), + }, + }) + const GovernorQuorumLogicLibV3 = await GovernorQuorumLogicV3.deploy() + await GovernorQuorumLogicLibV3.waitForDeployment() + + // Deploy Governor Proposal Logic + const GovernorProposalLogicV3 = await ethers.getContractFactory("GovernorProposalLogicV3", { + libraries: { + GovernorClockLogicV3: await GovernorClockLogicLibV3.getAddress(), + }, + }) + const GovernorProposalLogicLibV3 = await GovernorProposalLogicV3.deploy() + await GovernorProposalLogicLibV3.waitForDeployment() + + // Governance Voting Logic + // Deploy Governor Votes Logic + const GovernorVotesLogicV3 = await ethers.getContractFactory("GovernorVotesLogicV3", { + libraries: { + GovernorClockLogicV3: await GovernorClockLogicLibV3.getAddress(), + }, + }) + const GovernorVotesLogicLibV3 = await GovernorVotesLogicV3.deploy() + await GovernorVotesLogicLibV3.waitForDeployment() + + // Deploy Governor Deposit Logic + const GovernorDepositLogicV3 = await ethers.getContractFactory("GovernorDepositLogicV3", { + libraries: { + GovernorClockLogicV3: await GovernorClockLogicLibV3.getAddress(), + }, + }) + const GovernorDepositLogicLibV3 = await GovernorDepositLogicV3.deploy() + await GovernorDepositLogicLibV3.waitForDeployment() + + // Deploy Governor State Logic + const GovernorStateLogicV3 = await ethers.getContractFactory("GovernorStateLogicV3", { + libraries: { + GovernorClockLogicV3: await GovernorClockLogicLibV3.getAddress(), + }, + }) + const GovernorStateLogicLibV3 = await GovernorStateLogicV3.deploy() + await GovernorStateLogicLibV3.waitForDeployment() + + /// ---------------------- Version 4 ---------------------- + // Deploy Governor Clock Logic + const GovernorClockLogic = await ethers.getContractFactory("GovernorClockLogic") + const GovernorClockLogicLib = await GovernorClockLogic.deploy() + await GovernorClockLogicLib.waitForDeployment() + + // Deploy Governor Configurator + const GovernorConfigurator = await ethers.getContractFactory("GovernorConfigurator") + const GovernorConfiguratorLib = await GovernorConfigurator.deploy() + await GovernorConfiguratorLib.waitForDeployment() + + // Deploy Governor Function Restrictions Logic + const GovernorFunctionRestrictionsLogic = await ethers.getContractFactory("GovernorFunctionRestrictionsLogic") + const GovernorFunctionRestrictionsLogicLib = await GovernorFunctionRestrictionsLogic.deploy() + await GovernorFunctionRestrictionsLogicLib.waitForDeployment() + + // Deploy Governor Governance Logic + const GovernorGovernanceLogic = await ethers.getContractFactory("GovernorGovernanceLogic") + const GovernorGovernanceLogicLib = await GovernorGovernanceLogic.deploy() + await GovernorGovernanceLogicLib.waitForDeployment() + + // Deploy Governor Quorum Logic + const GovernorQuorumLogic = await ethers.getContractFactory("GovernorQuorumLogic", { + libraries: { + GovernorClockLogic: await GovernorClockLogicLib.getAddress(), + }, + }) + const GovernorQuorumLogicLib = await GovernorQuorumLogic.deploy() + await GovernorQuorumLogicLib.waitForDeployment() + + // Deploy Governor Proposal Logic + const GovernorProposalLogic = await ethers.getContractFactory("GovernorProposalLogic", { + libraries: { + GovernorClockLogic: await GovernorClockLogicLib.getAddress(), + }, + }) + const GovernorProposalLogicLib = await GovernorProposalLogic.deploy() + await GovernorProposalLogicLib.waitForDeployment() + + // Deploy Governor Votes Logic + const GovernorVotesLogic = await ethers.getContractFactory("GovernorVotesLogic", { + libraries: { + GovernorClockLogic: await GovernorClockLogicLib.getAddress(), + }, + }) + const GovernorVotesLogicLib = await GovernorVotesLogic.deploy() + await GovernorVotesLogicLib.waitForDeployment() + + // Deploy Governor Deposit Logic + const GovernorDepositLogic = await ethers.getContractFactory("GovernorDepositLogic", { + libraries: { + GovernorClockLogic: await GovernorClockLogicLib.getAddress(), + }, + }) + const GovernorDepositLogicLib = await GovernorDepositLogic.deploy() + await GovernorDepositLogicLib.waitForDeployment() + + // Deploy Governor State Logic + const GovernorStateLogic = await ethers.getContractFactory("GovernorStateLogic", { + libraries: { + GovernorClockLogic: await GovernorClockLogicLib.getAddress(), + }, + }) + const GovernorStateLogicLib = await GovernorStateLogic.deploy() + await GovernorStateLogicLib.waitForDeployment() + + return { + GovernorClockLogicLibV1, + GovernorConfiguratorLibV1, + GovernorFunctionRestrictionsLogicLibV1, + GovernorGovernanceLogicLibV1, + GovernorQuorumLogicLibV1, + GovernorProposalLogicLibV1, + GovernorVotesLogicLibV1, + GovernorDepositLogicLibV1, + GovernorStateLogicLibV1, + GovernorClockLogicLibV3, + GovernorConfiguratorLibV3, + GovernorFunctionRestrictionsLogicLibV3, + GovernorGovernanceLogicLibV3, + GovernorQuorumLogicLibV3, + GovernorProposalLogicLibV3, + GovernorVotesLogicLibV3, + GovernorDepositLogicLibV3, + GovernorStateLogicLibV3, + GovernorClockLogicLib, + GovernorConfiguratorLib, + GovernorFunctionRestrictionsLogicLib, + GovernorGovernanceLogicLib, + GovernorQuorumLogicLib, + GovernorProposalLogicLib, + GovernorVotesLogicLib, + GovernorDepositLogicLib, + GovernorStateLogicLib, + } +} diff --git a/scripts/libraries/index.ts b/scripts/libraries/index.ts new file mode 100644 index 0000000..db1d891 --- /dev/null +++ b/scripts/libraries/index.ts @@ -0,0 +1,2 @@ +export * from "./governanceLibraries" +export * from "./passportLibraries" diff --git a/scripts/libraries/passportLibraries.ts b/scripts/libraries/passportLibraries.ts new file mode 100644 index 0000000..23d4077 --- /dev/null +++ b/scripts/libraries/passportLibraries.ts @@ -0,0 +1,58 @@ +import { ethers } from "hardhat" + +export async function passportLibraries() { + // Deploy Passport Checks Logic + const PassportChecksLogic = await ethers.getContractFactory("PassportChecksLogic") + const PassportChecksLogicLib = await PassportChecksLogic.deploy() + await PassportChecksLogicLib.waitForDeployment() + + // Deploy Passport Configurator + const PassportConfigurator = await ethers.getContractFactory("PassportConfigurator") + const PassportConfiguratorLib = await PassportConfigurator.deploy() + await PassportConfiguratorLib.waitForDeployment() + + // Deploy Passport Delegation Logic + const PassportEntityLogic = await ethers.getContractFactory("PassportEntityLogic") + const PassportEntityLogicLib = await PassportEntityLogic.deploy() + await PassportEntityLogicLib.waitForDeployment() + + // Deploy Passport Delegation Logic + const PassportDelegationLogic = await ethers.getContractFactory("PassportDelegationLogic") + const PassportDelegationLogicLib = await PassportDelegationLogic.deploy() + await PassportDelegationLogicLib.waitForDeployment() + + // Deploy Passport PoP Score Logic + const PassportPoPScoreLogic = await ethers.getContractFactory("PassportPoPScoreLogic") + const PassportPoPScoreLogicLib = await PassportPoPScoreLogic.deploy() + await PassportPoPScoreLogicLib.waitForDeployment() + + // Deploy Passport Signaling Logic + const PassportSignalingLogic = await ethers.getContractFactory("PassportSignalingLogic") + const PassportSignalingLogicLib = await PassportSignalingLogic.deploy() + await PassportSignalingLogicLib.waitForDeployment() + + // Deploy Passport Personhood Logic + const PassportPersonhoodLogic = await ethers.getContractFactory("PassportPersonhoodLogic", { + libraries: { + PassportPoPScoreLogic: await PassportPoPScoreLogicLib.getAddress(), + }, + }) + const PassportPersonhoodLogicLib = await PassportPersonhoodLogic.deploy() + await PassportPersonhoodLogicLib.waitForDeployment() + + // Deploy Passport Whitelist and Blacklist Logic + const PassportWhitelistAndBlacklistLogic = await ethers.getContractFactory("PassportWhitelistAndBlacklistLogic") + const PassportWhitelistAndBlacklistLogicLib = await PassportWhitelistAndBlacklistLogic.deploy() + await PassportWhitelistAndBlacklistLogicLib.waitForDeployment() + + return { + PassportChecksLogic: PassportChecksLogicLib, + PassportConfigurator: PassportConfiguratorLib, + PassportEntityLogic: PassportEntityLogicLib, + PassportDelegationLogic: PassportDelegationLogicLib, + PassportPersonhoodLogic: PassportPersonhoodLogicLib, + PassportPoPScoreLogic: PassportPoPScoreLogicLib, + PassportSignalingLogic: PassportSignalingLogicLib, + PassportWhitelistAndBlacklistLogic: PassportWhitelistAndBlacklistLogicLib, + } +} diff --git a/test/B3TR.test.ts b/test/B3TR.test.ts index da94920..2abf959 100644 --- a/test/B3TR.test.ts +++ b/test/B3TR.test.ts @@ -4,7 +4,7 @@ import { catchRevert, getOrDeployContractInstances } from "./helpers" import { describe, it } from "mocha" import { createLocalConfig } from "../config/contracts/envs/local" -describe("B3TR Token", function () { +describe("B3TR Token - @shard2", function () { describe("Deployment", function () { it("should deploy the contract", async function () { const { b3tr } = await getOrDeployContractInstances({ forceDeploy: false }) diff --git a/test/Emissions.test.ts b/test/Emissions.test.ts index e76534b..450a946 100644 --- a/test/Emissions.test.ts +++ b/test/Emissions.test.ts @@ -19,7 +19,7 @@ import { getImplementationAddress } from "@openzeppelin/upgrades-core" import { deployProxy } from "../scripts/helpers" import b3trAllocationsEmissionsDisaligned from "./fixture/full-allocations-round-14-decay.json" -describe.only("Emissions", () => { +describe("Emissions - @shard2", () => { describe("Contract parameters", () => { it("Should have correct parameters set on deployment", async () => { const config = createLocalConfig() @@ -209,11 +209,10 @@ describe.only("Emissions", () => { it("Should revert if Treasury is set to zero address in initilisation", async () => { const config = createLocalConfig() - const { owner, b3tr, minterAccount, xAllocationPool, voterRewards, treasury } = - await getOrDeployContractInstances({ - forceDeploy: true, - config, - }) + const { owner, b3tr, minterAccount, xAllocationPool, voterRewards } = await getOrDeployContractInstances({ + forceDeploy: true, + config, + }) await expect( deployProxy("Emissions", [ diff --git a/test/GalaxyMember.test.ts b/test/GalaxyMember.test.ts index f6d849c..fd53cfa 100644 --- a/test/GalaxyMember.test.ts +++ b/test/GalaxyMember.test.ts @@ -25,7 +25,7 @@ import { getImplementationAddress } from "@openzeppelin/upgrades-core" import { deployProxy } from "../scripts/helpers" import { GalaxyMember } from "../typechain-types" -describe("Galaxy Member", () => { +describe("Galaxy Member - @shard2", () => { describe("Contract parameters", () => { it("Should have correct parameters set on deployment", async () => { const { galaxyMember, owner } = await getOrDeployContractInstances({ forceDeploy: true }) @@ -675,7 +675,7 @@ describe("Galaxy Member", () => { }) it("User can free mint if he participated in B3TR Governance", async () => { - const { galaxyMember, otherAccount, b3tr, otherAccounts, governor, B3trContract } = + const { galaxyMember, otherAccount, b3tr, otherAccounts, governor, B3trContract, veBetterPassport } = await getOrDeployContractInstances({ forceDeploy: true, }) @@ -691,6 +691,9 @@ describe("Galaxy Member", () => { // we do it here but will use in the next test await getVot3Tokens(voter, "30000") + await veBetterPassport.whitelist(voter.address) + await veBetterPassport.toggleCheck(1) + // Now we can create a new proposal const tx = await createProposal(b3tr, B3trContract, otherAccount, "", "tokenDetails", []) const proposalId = await getProposalIdFromTx(tx) @@ -709,7 +712,7 @@ describe("Galaxy Member", () => { }) it("User can free mint if he participated both in B3TR Governance and in x-allocation voting", async () => { - const { galaxyMember, otherAccount, b3tr, otherAccounts, governor, B3trContract } = + const { galaxyMember, otherAccount, b3tr, otherAccounts, governor, B3trContract, veBetterPassport } = await getOrDeployContractInstances({ forceDeploy: true, }) @@ -719,6 +722,9 @@ describe("Galaxy Member", () => { const voter = otherAccounts[0] + await veBetterPassport.whitelist(voter.address) + await veBetterPassport.toggleCheck(1) + // Should not be able to free mint await catchRevert(galaxyMember.connect(voter).freeMint()) @@ -1442,17 +1448,17 @@ describe("Galaxy Member", () => { expect(await galaxyMember.levelOf(0)).to.equal(2) // Level 2 - const tx = await galaxyMember + let tx = await galaxyMember .connect(owner) .transferFrom(await owner.getAddress(), await otherAccount.getAddress(), 0) - 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/Governance.test.ts b/test/Governance.test.ts index 5220152..c111af6 100644 --- a/test/Governance.test.ts +++ b/test/Governance.test.ts @@ -27,10 +27,10 @@ import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers" import { describe, it } from "mocha" import { createLocalConfig } from "../config/contracts/envs/local" import { getImplementationAddress } from "@openzeppelin/upgrades-core" -import { B3TRGovernor, B3TRGovernor__factory } from "../typechain-types" -import { deployProxy } from "../scripts/helpers" +import { B3TRGovernor, B3TRGovernorV1, B3TRGovernorV3, B3TRGovernor__factory } from "../typechain-types" +import { deployAndUpgrade, deployProxy, getInitializerData } from "../scripts/helpers" -describe("Governor and TimeLock", function () { +describe("Governor and TimeLock - @shard1", function () { describe("Governor deployment", function () { it("Should set constructors correctly", async function () { const config = createLocalConfig() @@ -77,7 +77,7 @@ describe("Governor and TimeLock", function () { // check version const version = await governor.version() - expect(version).to.eql("3") + expect(version).to.eql("4") // deposit threshold is set correctly const depositThreshold = await governor.depositThresholdPercentage() @@ -113,6 +113,7 @@ describe("Governor and TimeLock", function () { governorQuorumLogicLibV1, governorStateLogicLibV1, governorVotesLogicLibV1, + veBetterPassport, } = await getOrDeployContractInstances({ forceDeploy: true, }) @@ -136,6 +137,9 @@ describe("Governor and TimeLock", function () { const implementation = await Contract.deploy() await implementation.waitForDeployment() + await veBetterPassport.whitelist(otherAccount.address) + await veBetterPassport.toggleCheck(1) + // V1 Contract const V1Contract = await ethers.getContractAt("B3TRGovernor", await governor.getAddress()) @@ -239,9 +243,13 @@ describe("Governor and TimeLock", function () { forceDeploy: false, }) - const { governor, owner, otherAccount, xAllocationVoting, vot3 } = await getOrDeployContractInstances({ - forceDeploy: true, - }) + const { governor, owner, otherAccount, xAllocationVoting, vot3, veBetterPassport } = + await getOrDeployContractInstances({ + forceDeploy: true, + }) + + await veBetterPassport.whitelist(otherAccount.address) + await veBetterPassport.toggleCheck(1) // Start emissions await bootstrapAndStartEmissions() @@ -793,7 +801,7 @@ describe("Governor and TimeLock", function () { expect(await governor.depositThresholdPercentage()).to.eql(2n) }) - 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 { owner, @@ -820,56 +828,94 @@ describe("Governor and TimeLock", function () { governorQuorumLogicLibV1, governorStateLogicLibV1, governorVotesLogicLibV1, + governorClockLogicLibV3, + governorConfiguratorLibV3, + governorDepositLogicLibV3, + governorFunctionRestrictionsLogicLibV3, + governorProposalLogicLibV3, + governorQuorumLogicLibV3, + governorStateLogicLibV3, + governorVotesLogicLibV3, + veBetterPassport, } = await getOrDeployContractInstances({ forceDeploy: true, }) // Deploy Governor - const governorV1 = (await deployProxy( - "B3TRGovernorV1", + const governorV1 = (await deployAndUpgrade( + ["B3TRGovernorV1", "B3TRGovernorV2", "B3TRGovernorV3"], [ - { - vot3Token: await vot3.getAddress(), - timelock: await timeLock.getAddress(), - xAllocationVoting: await xAllocationVoting.getAddress(), - b3tr: await b3tr.getAddress(), - quorumPercentage: config.B3TR_GOVERNOR_QUORUM_PERCENTAGE, // quorum percentage - initialDepositThreshold: config.B3TR_GOVERNOR_DEPOSIT_THRESHOLD, // deposit threshold - initialMinVotingDelay: config.B3TR_GOVERNOR_MIN_VOTING_DELAY, // delay before vote starts - initialVotingThreshold: config.B3TR_GOVERNOR_VOTING_THRESHOLD, // voting threshold - voterRewards: await voterRewards.getAddress(), - isFunctionRestrictionEnabled: true, - }, - { - governorAdmin: owner.address, - pauser: owner.address, - contractsAddressManager: owner.address, - proposalExecutor: owner.address, - governorFunctionSettingsRoleAddress: owner.address, - }, + [ + { + vot3Token: await vot3.getAddress(), + timelock: await timeLock.getAddress(), + xAllocationVoting: await xAllocationVoting.getAddress(), + b3tr: await b3tr.getAddress(), + quorumPercentage: config.B3TR_GOVERNOR_QUORUM_PERCENTAGE, + initialDepositThreshold: config.B3TR_GOVERNOR_DEPOSIT_THRESHOLD, + initialMinVotingDelay: config.B3TR_GOVERNOR_MIN_VOTING_DELAY, + initialVotingThreshold: config.B3TR_GOVERNOR_VOTING_THRESHOLD, + voterRewards: await voterRewards.getAddress(), + isFunctionRestrictionEnabled: true, + }, + { + governorAdmin: owner.address, + pauser: owner.address, + contractsAddressManager: owner.address, + proposalExecutor: owner.address, + governorFunctionSettingsRoleAddress: owner.address, + }, + ], + [], + [], ], { - GovernorClockLogicV1: await governorClockLogicLibV1.getAddress(), - GovernorConfiguratorV1: await governorConfiguratorLibV1.getAddress(), - GovernorDepositLogicV1: await governorDepositLogicLibV1.getAddress(), - GovernorFunctionRestrictionsLogicV1: await governorFunctionRestrictionsLogicLibV1.getAddress(), - GovernorProposalLogicV1: await governorProposalLogicLibV1.getAddress(), - GovernorQuorumLogicV1: await governorQuorumLogicLibV1.getAddress(), - GovernorStateLogicV1: await governorStateLogicLibV1.getAddress(), - GovernorVotesLogicV1: await governorVotesLogicLibV1.getAddress(), + versions: [undefined, 2, 3], + libraries: [ + { + GovernorClockLogicV1: await governorClockLogicLibV1.getAddress(), + GovernorConfiguratorV1: await governorConfiguratorLibV1.getAddress(), + GovernorDepositLogicV1: await governorDepositLogicLibV1.getAddress(), + GovernorFunctionRestrictionsLogicV1: await governorFunctionRestrictionsLogicLibV1.getAddress(), + GovernorProposalLogicV1: await governorProposalLogicLibV1.getAddress(), + GovernorQuorumLogicV1: await governorQuorumLogicLibV1.getAddress(), + GovernorStateLogicV1: await governorStateLogicLibV1.getAddress(), + GovernorVotesLogicV1: await governorVotesLogicLibV1.getAddress(), + }, + { + GovernorClockLogicV1: await governorClockLogicLibV1.getAddress(), + GovernorConfiguratorV1: await governorConfiguratorLibV1.getAddress(), + GovernorDepositLogicV1: await governorDepositLogicLibV1.getAddress(), + GovernorFunctionRestrictionsLogicV1: await governorFunctionRestrictionsLogicLibV1.getAddress(), + GovernorProposalLogicV1: await governorProposalLogicLibV1.getAddress(), + GovernorQuorumLogicV1: await governorQuorumLogicLibV1.getAddress(), + GovernorStateLogicV1: await governorStateLogicLibV1.getAddress(), + GovernorVotesLogicV1: await governorVotesLogicLibV1.getAddress(), + }, + { + GovernorClockLogicV3: await governorClockLogicLibV3.getAddress(), + GovernorConfiguratorV3: await governorConfiguratorLibV3.getAddress(), + GovernorDepositLogicV3: await governorDepositLogicLibV3.getAddress(), + GovernorFunctionRestrictionsLogicV3: await governorFunctionRestrictionsLogicLibV3.getAddress(), + GovernorProposalLogicV3: await governorProposalLogicLibV3.getAddress(), + GovernorQuorumLogicV3: await governorQuorumLogicLibV3.getAddress(), + GovernorStateLogicV3: await governorStateLogicLibV3.getAddress(), + GovernorVotesLogicV3: await governorVotesLogicLibV3.getAddress(), + }, + ], }, - )) as B3TRGovernor + )) as B3TRGovernorV3 - const b3trGovernorFactory = await ethers.getContractFactory("B3TRGovernorV1", { + const b3trGovernorFactory = await ethers.getContractFactory("B3TRGovernorV3", { libraries: { - GovernorClockLogicV1: await governorClockLogicLibV1.getAddress(), - GovernorConfiguratorV1: await governorConfiguratorLibV1.getAddress(), - GovernorDepositLogicV1: await governorDepositLogicLibV1.getAddress(), - GovernorFunctionRestrictionsLogicV1: await governorFunctionRestrictionsLogicLibV1.getAddress(), - GovernorProposalLogicV1: await governorProposalLogicLibV1.getAddress(), - GovernorQuorumLogicV1: await governorQuorumLogicLibV1.getAddress(), - GovernorStateLogicV1: await governorStateLogicLibV1.getAddress(), - GovernorVotesLogicV1: await governorVotesLogicLibV1.getAddress(), + GovernorClockLogicV3: await governorClockLogicLibV3.getAddress(), + GovernorConfiguratorV3: await governorConfiguratorLibV3.getAddress(), + GovernorDepositLogicV3: await governorDepositLogicLibV3.getAddress(), + GovernorFunctionRestrictionsLogicV3: await governorFunctionRestrictionsLogicLibV3.getAddress(), + GovernorProposalLogicV3: await governorProposalLogicLibV3.getAddress(), + GovernorQuorumLogicV3: await governorQuorumLogicLibV3.getAddress(), + GovernorStateLogicV3: await governorStateLogicLibV3.getAddress(), + GovernorVotesLogicV3: await governorVotesLogicLibV3.getAddress(), }, }) @@ -968,16 +1014,16 @@ describe("Governor and TimeLock", function () { ) // removing empty slots and slots that track governance proposals getting executed on the governor // Upgrade to V2 via governance - const Contract = await ethers.getContractFactory("B3TRGovernorV2", { + const Contract = await ethers.getContractFactory("B3TRGovernor", { libraries: { - GovernorClockLogicV1: await governorClockLogicLibV1.getAddress(), - GovernorConfiguratorV1: await governorConfiguratorLibV1.getAddress(), - GovernorDepositLogicV1: await governorDepositLogicLibV1.getAddress(), - GovernorFunctionRestrictionsLogicV1: await governorFunctionRestrictionsLogicLibV1.getAddress(), - GovernorProposalLogicV1: await governorProposalLogicLibV1.getAddress(), - GovernorQuorumLogicV1: await governorQuorumLogicLibV1.getAddress(), - GovernorStateLogicV1: await governorStateLogicLibV1.getAddress(), - GovernorVotesLogicV1: await governorVotesLogicLibV1.getAddress(), + GovernorClockLogic: await governorClockLogicLib.getAddress(), + GovernorConfigurator: await governorConfiguratorLib.getAddress(), + GovernorDepositLogic: await governorDepositLogicLib.getAddress(), + GovernorFunctionRestrictionsLogic: await governorFunctionRestrictionsLogicLib.getAddress(), + GovernorProposalLogic: await governorProposalLogicLib.getAddress(), + GovernorQuorumLogic: await governorQuorumLogicLib.getAddress(), + GovernorStateLogic: await governorStateLogicLib.getAddress(), + GovernorVotesLogic: await governorVotesLogicLib.getAddress(), }, }) const implementation = await Contract.deploy() @@ -986,7 +1032,7 @@ describe("Governor and TimeLock", function () { // Now we can create a proposal const encodedFunctionCall2 = b3trGovernorFactory.interface.encodeFunctionData("upgradeToAndCall", [ await implementation.getAddress(), - "0x", + getInitializerData(Contract.interface, [await veBetterPassport.getAddress()], 4), ]) const description = "Upgrading Governance contracts" const descriptionHash2 = ethers.keccak256(ethers.toUtf8Bytes(description)) @@ -1067,35 +1113,6 @@ describe("Governor and TimeLock", function () { expect(proposalVotesPostUpgrade).to.eql(proposalVotesPreUpgrade) expect(quorumDenominatorPostUpgrade).to.eql(quorumDenominatorPreUpgrade) expect(quorumDepositThresholdPostUpgrade).to.eql(quorumDepositThresholdPreUpgrade) - - let storageSlotsV2 = [] - - for (let i = initialSlot; i < initialSlot + BigInt(50); i++) { - storageSlotsV2.push(await ethers.provider.getStorage(await governorV1.getAddress(), i)) - } - - storageSlots = storageSlotsV2.filter( - slot => - slot !== "0x0000000000000000000000000000000000000000000000000000000000000000" && - slot !== "0x0000000000000000000000000000000200000000000000000000000000000002", - ) // removing empty slots and slots that track governance proposals getting executed on the governor - - // Upgrade to V3 via admin - const ContractV3 = await ethers.getContractFactory("B3TRGovernor", { - libraries: { - GovernorClockLogic: await governorClockLogicLib.getAddress(), - GovernorConfigurator: await governorConfiguratorLib.getAddress(), - GovernorDepositLogic: await governorDepositLogicLib.getAddress(), - GovernorFunctionRestrictionsLogic: await governorFunctionRestrictionsLogicLib.getAddress(), - GovernorProposalLogic: await governorProposalLogicLib.getAddress(), - GovernorQuorumLogic: await governorQuorumLogicLib.getAddress(), - GovernorStateLogic: await governorStateLogicLib.getAddress(), - GovernorVotesLogic: await governorVotesLogicLib.getAddress(), - }, - }) - - const implementationV3 = await ContractV3.deploy() - await implementationV3.waitForDeployment() }) }) @@ -1826,6 +1843,7 @@ describe("Governor and TimeLock", function () { otherAccount: proposer, otherAccounts, owner, + veBetterPassport, } = await getOrDeployContractInstances({ forceDeploy: true }) const functionToCall = "tokenDetails" const description = "Get token details" @@ -1838,6 +1856,9 @@ describe("Governor and TimeLock", function () { await getVot3Tokens(voter, "30000") await waitForNextBlock() + await veBetterPassport.whitelist(voter.address) + await veBetterPassport.toggleCheck(1) + // create a new proposal const tx = await createProposal( b3tr, @@ -1892,6 +1913,7 @@ describe("Governor and TimeLock", function () { B3trContract, otherAccount: proposer, otherAccounts, + veBetterPassport, owner, } = await getOrDeployContractInstances({ forceDeploy: true }) const functionToCall = "tokenDetails" @@ -1905,6 +1927,9 @@ describe("Governor and TimeLock", function () { await getVot3Tokens(voter, "30000") await waitForNextBlock() + await veBetterPassport.whitelist(voter.address) + await veBetterPassport.toggleCheck(1) + // create a new proposal const tx = await createProposal( b3tr, @@ -2179,7 +2204,7 @@ describe("Governor and TimeLock", function () { const config = createLocalConfig() config.B3TR_GOVERNOR_DEPOSIT_THRESHOLD = 1 config.EMISSIONS_CYCLE_DURATION = 5 - const { b3tr, otherAccounts, governor, B3trContract, xAllocationVoting, emissions, vot3 } = + const { b3tr, otherAccounts, governor, B3trContract, xAllocationVoting, emissions, vot3, veBetterPassport } = await getOrDeployContractInstances({ forceDeploy: true, config, @@ -2201,6 +2226,9 @@ describe("Governor and TimeLock", function () { const depositThreshold = await governor.depositThreshold() + await veBetterPassport.whitelist(proposer.address) + await veBetterPassport.toggleCheck(1) + const tx = await governor .connect(proposer) .propose([address], [0], [encodedFunctionCall], "", voteStartsInRoundId.toString(), deposit, { @@ -2526,16 +2554,21 @@ describe("Governor and TimeLock", function () { const config = createLocalConfig() config.B3TR_GOVERNOR_DEPOSIT_THRESHOLD = 1 config.EMISSIONS_CYCLE_DURATION = 7 - const { otherAccounts, governor, xAllocationVoting, vot3 } = await getOrDeployContractInstances({ - forceDeploy: true, - config, - }) + const { otherAccounts, governor, xAllocationVoting, vot3, veBetterPassport } = await getOrDeployContractInstances( + { + forceDeploy: true, + config, + }, + ) const proposer = otherAccounts[0] const voter = otherAccounts[1] await getVot3Tokens(voter, "30000") + await veBetterPassport.whitelist(voter.address) + await veBetterPassport.toggleCheck(1) + // Start emissions await bootstrapAndStartEmissions() @@ -3243,7 +3276,7 @@ describe("Governor and TimeLock", function () { const config = createLocalConfig() config.B3TR_GOVERNOR_DEPOSIT_THRESHOLD = 1 config.EMISSIONS_CYCLE_DURATION = 15 - const { vot3, b3tr, otherAccounts, minterAccount, B3trContract, otherAccount } = + const { vot3, b3tr, otherAccounts, minterAccount, B3trContract, otherAccount, veBetterPassport } = await getOrDeployContractInstances({ forceDeploy: true, config, @@ -3257,6 +3290,15 @@ describe("Governor and TimeLock", function () { voter6 = otherAccounts[5] // with VOT3 and delegation voter7 = otherAccounts[6] // with VOT3 and delegation + await veBetterPassport.whitelist(voter1.address) + await veBetterPassport.whitelist(voter2.address) + await veBetterPassport.whitelist(voter3.address) + await veBetterPassport.whitelist(voter4.address) + await veBetterPassport.whitelist(voter5.address) + await veBetterPassport.whitelist(voter6.address) + await veBetterPassport.whitelist(voter7.address) + await veBetterPassport.toggleCheck(1) + // Before trying to vote we need to mint some VOT3 tokens to the voter2 await b3tr.connect(minterAccount).mint(voter2, ethers.parseEther("30000")) await b3tr.connect(voter2).approve(await vot3.getAddress(), ethers.parseEther("270")) @@ -3510,13 +3552,17 @@ describe("Governor and TimeLock", function () { }).timeout(1800000) it("Stores that a user voted at least once", async function () { - const { otherAccount, owner, governor, b3tr, B3trContract } = await getOrDeployContractInstances({ - forceDeploy: true, - }) + const { otherAccount, owner, governor, b3tr, B3trContract, veBetterPassport } = + await getOrDeployContractInstances({ + forceDeploy: true, + }) // Start emissions await bootstrapAndStartEmissions() + await veBetterPassport.whitelist(otherAccount.address) + await veBetterPassport.toggleCheck(1) + // Should be able to free mint after participating in allocation voting await participateInGovernanceVoting( otherAccount, @@ -3535,7 +3581,7 @@ describe("Governor and TimeLock", function () { }) it("Quorum is calculated correctly", async function () { - const { governor, otherAccounts, b3tr, B3trContract } = await getOrDeployContractInstances({ + const { governor, otherAccounts, b3tr, B3trContract, veBetterPassport } = await getOrDeployContractInstances({ forceDeploy: true, }) @@ -3552,6 +3598,12 @@ describe("Governor and TimeLock", function () { await getVot3Tokens(proposer, "30000") await waitForNextBlock() + await veBetterPassport.whitelist(voter.address) + await veBetterPassport.whitelist(voter2.address) + await veBetterPassport.whitelist(voter3.address) + + await veBetterPassport.toggleCheck(1) + // Create a proposal const tx = await createProposal( b3tr, @@ -3592,7 +3644,7 @@ describe("Governor and TimeLock", function () { }) it("[Quadratic] Against votes are counted correctly for quorum", async function () { - const { governor, otherAccounts, b3tr, B3trContract } = await getOrDeployContractInstances({ + const { governor, otherAccounts, b3tr, B3trContract, veBetterPassport } = await getOrDeployContractInstances({ forceDeploy: true, }) @@ -3607,6 +3659,12 @@ describe("Governor and TimeLock", function () { await getVot3Tokens(voter3, "30000") await waitForNextBlock() + await veBetterPassport.whitelist(voter.address) + await veBetterPassport.whitelist(voter2.address) + await veBetterPassport.whitelist(voter3.address) + + await veBetterPassport.toggleCheck(1) + // Create a proposal const tx = await createProposal( b3tr, @@ -3642,7 +3700,7 @@ describe("Governor and TimeLock", function () { }) it("[Linear] Against votes are counted correctly for quorum", async function () { - const { governor, otherAccounts, b3tr, B3trContract } = await getOrDeployContractInstances({ + const { governor, otherAccounts, b3tr, B3trContract, veBetterPassport } = await getOrDeployContractInstances({ forceDeploy: true, }) @@ -3660,6 +3718,12 @@ describe("Governor and TimeLock", function () { await getVot3Tokens(voter3, "30000") await waitForNextBlock() + await veBetterPassport.whitelist(voter.address) + await veBetterPassport.whitelist(voter2.address) + await veBetterPassport.whitelist(voter3.address) + + await veBetterPassport.toggleCheck(1) + // Create a proposal const tx = await createProposal( b3tr, @@ -3699,7 +3763,7 @@ describe("Governor and TimeLock", function () { }) it("[Quadratic] Abstain votes are counted correctly for quorum", async function () { - const { governor, otherAccounts, b3tr, B3trContract } = await getOrDeployContractInstances({ + const { governor, otherAccounts, b3tr, B3trContract, veBetterPassport } = await getOrDeployContractInstances({ forceDeploy: true, }) @@ -3714,6 +3778,12 @@ describe("Governor and TimeLock", function () { await getVot3Tokens(voter3, "30000") await waitForNextBlock() + await veBetterPassport.whitelist(voter.address) + await veBetterPassport.whitelist(voter2.address) + await veBetterPassport.whitelist(voter3.address) + + await veBetterPassport.toggleCheck(1) + // Create a proposal const tx = await createProposal( b3tr, @@ -3749,7 +3819,7 @@ describe("Governor and TimeLock", function () { }) it("[Linear] Abstain votes are counted correctly for quorum", async function () { - const { governor, otherAccounts, b3tr, B3trContract } = await getOrDeployContractInstances({ + const { governor, otherAccounts, b3tr, B3trContract, veBetterPassport } = await getOrDeployContractInstances({ forceDeploy: true, }) @@ -3767,6 +3837,12 @@ describe("Governor and TimeLock", function () { await getVot3Tokens(voter3, "30000") await waitForNextBlock() + await veBetterPassport.whitelist(voter.address) + await veBetterPassport.whitelist(voter2.address) + await veBetterPassport.whitelist(voter3.address) + + await veBetterPassport.toggleCheck(1) + // Create a proposal const tx = await createProposal( b3tr, @@ -3806,7 +3882,7 @@ describe("Governor and TimeLock", function () { }) it("[Quadratic] Yes votes are counted correctly for quorum", async function () { - const { governor, otherAccounts, b3tr, B3trContract } = await getOrDeployContractInstances({ + const { governor, otherAccounts, b3tr, B3trContract, veBetterPassport } = await getOrDeployContractInstances({ forceDeploy: true, }) @@ -3821,6 +3897,12 @@ describe("Governor and TimeLock", function () { await getVot3Tokens(voter3, "30000") await waitForNextBlock() + await veBetterPassport.whitelist(voter.address) + await veBetterPassport.whitelist(voter2.address) + await veBetterPassport.whitelist(voter3.address) + + await veBetterPassport.toggleCheck(1) + // Create a proposal const tx = await createProposal( b3tr, @@ -3857,7 +3939,7 @@ describe("Governor and TimeLock", function () { }) it("[Linear] Yes votes are counted correctly for quorum", async function () { - const { governor, otherAccounts, b3tr, B3trContract } = await getOrDeployContractInstances({ + const { governor, otherAccounts, b3tr, B3trContract, veBetterPassport } = await getOrDeployContractInstances({ forceDeploy: true, }) @@ -3875,6 +3957,12 @@ describe("Governor and TimeLock", function () { await getVot3Tokens(voter3, "30000") await waitForNextBlock() + await veBetterPassport.whitelist(voter.address) + await veBetterPassport.whitelist(voter2.address) + await veBetterPassport.whitelist(voter3.address) + + await veBetterPassport.toggleCheck(1) + // Create a proposal const tx = await createProposal( b3tr, @@ -3915,7 +4003,7 @@ describe("Governor and TimeLock", function () { }) it("[Quadratic] Can get correct quadratic voting power", async function () { - const { governor, otherAccounts, b3tr, B3trContract } = await getOrDeployContractInstances({ + const { governor, otherAccounts, b3tr, B3trContract, veBetterPassport } = await getOrDeployContractInstances({ forceDeploy: true, }) @@ -3930,6 +4018,12 @@ describe("Governor and TimeLock", function () { await getVot3Tokens(voter3, "30000") await waitForNextBlock() + await veBetterPassport.whitelist(voter.address) + await veBetterPassport.whitelist(voter2.address) + await veBetterPassport.whitelist(voter3.address) + + await veBetterPassport.toggleCheck(1) + // Create a proposal const tx = await createProposal( b3tr, @@ -3972,7 +4066,7 @@ describe("Governor and TimeLock", function () { }) it("[Linear] Can get correct voting power", async function () { - const { governor, otherAccounts, b3tr, B3trContract } = await getOrDeployContractInstances({ + const { governor, otherAccounts, b3tr, B3trContract, veBetterPassport } = await getOrDeployContractInstances({ forceDeploy: true, }) @@ -3994,6 +4088,12 @@ describe("Governor and TimeLock", function () { await getVot3Tokens(voter3, "30000") await waitForNextBlock() + await veBetterPassport.whitelist(voter.address) + await veBetterPassport.whitelist(voter2.address) + await veBetterPassport.whitelist(voter3.address) + + await veBetterPassport.toggleCheck(1) + // Create a proposal const tx = await createProposal( b3tr, @@ -4031,13 +4131,18 @@ describe("Governor and TimeLock", function () { }) it("Can correctly cast vote with reason", async () => { - const { governor, otherAccounts, b3tr, B3trContract, otherAccount } = await getOrDeployContractInstances({ - forceDeploy: true, - }) + const { governor, otherAccounts, b3tr, B3trContract, otherAccount, veBetterPassport } = + await getOrDeployContractInstances({ + forceDeploy: true, + }) const voter = otherAccounts[0] await getVot3Tokens(voter, "30000") + await veBetterPassport.whitelist(voter.address) + + await veBetterPassport.toggleCheck(1) + // Start emissions await bootstrapAndStartEmissions() @@ -4073,13 +4178,18 @@ describe("Governor and TimeLock", function () { }) it("Can abstain", async () => { - const { governor, otherAccounts, b3tr, B3trContract, otherAccount } = await getOrDeployContractInstances({ - forceDeploy: true, - }) + const { governor, otherAccounts, b3tr, B3trContract, otherAccount, veBetterPassport } = + await getOrDeployContractInstances({ + forceDeploy: true, + }) const voter = otherAccounts[0] await getVot3Tokens(voter, "30000") + await veBetterPassport.whitelist(voter.address) + + await veBetterPassport.toggleCheck(1) + // Start emissions await bootstrapAndStartEmissions() @@ -4118,17 +4228,31 @@ describe("Governor and TimeLock", function () { const config = createLocalConfig() // set deposit threshold to 0 so we can avoid depositing for proposals config.B3TR_GOVERNOR_DEPOSIT_THRESHOLD = 0 - const { governor, otherAccounts, b3tr, B3trContract, otherAccount, vot3, voterRewards, emissions } = - await getOrDeployContractInstances({ - forceDeploy: true, - config, - }) + const { + governor, + otherAccounts, + b3tr, + B3trContract, + otherAccount, + vot3, + voterRewards, + veBetterPassport, + emissions, + } = await getOrDeployContractInstances({ + forceDeploy: true, + config, + }) const voter = otherAccounts[0] const voter2 = otherAccounts[1] await getVot3Tokens(voter, "1000") await getVot3Tokens(voter2, "1") + await veBetterPassport.whitelist(voter.address) + await veBetterPassport.whitelist(voter2.address) + + await veBetterPassport.toggleCheck(1) + // Start emissions await bootstrapAndStartEmissions() @@ -4190,16 +4314,22 @@ describe("Governor and TimeLock", function () { const config = createLocalConfig() // set deposit threshold to 0 so we can avoid depositing for proposals config.B3TR_GOVERNOR_DEPOSIT_THRESHOLD = 0 - const { governor, otherAccounts, b3tr, B3trContract, otherAccount, vot3 } = await getOrDeployContractInstances({ - forceDeploy: true, - config, - }) + const { governor, otherAccounts, b3tr, B3trContract, otherAccount, vot3, veBetterPassport } = + await getOrDeployContractInstances({ + forceDeploy: true, + config, + }) const voter = otherAccounts[0] const voter2 = otherAccounts[1] await getVot3Tokens(voter, "1000") await getVot3Tokens(voter2, "1") + await veBetterPassport.whitelist(voter.address) + await veBetterPassport.whitelist(voter2.address) + + await veBetterPassport.toggleCheck(1) + // Start emissions await bootstrapAndStartEmissions() @@ -4241,6 +4371,7 @@ describe("Governor and TimeLock", function () { governorQuorumLogicLib, governorStateLogicLib, governorVotesLogicLib, + veBetterPassport, } = await getOrDeployContractInstances({ forceDeploy: true, }) @@ -4252,6 +4383,11 @@ describe("Governor and TimeLock", function () { await getVot3Tokens(voter2, "30000") await waitForNextBlock() + await veBetterPassport.whitelist(voter.address) + await veBetterPassport.whitelist(voter2.address) + + await veBetterPassport.toggleCheck(1) + // Only admin or governance can toggle quadratic voting await catchRevert(governor.connect(otherAccounts[0]).toggleQuadraticVoting()) await catchRevert(governor.connect(otherAccounts[1]).toggleQuadraticVoting()) @@ -4342,7 +4478,7 @@ describe("Governor and TimeLock", function () { const config = createLocalConfig() config.B3TR_GOVERNOR_DEPOSIT_THRESHOLD = 1 config.EMISSIONS_CYCLE_DURATION = 10 - const { otherAccounts } = await getOrDeployContractInstances({ + const { otherAccounts, veBetterPassport } = await getOrDeployContractInstances({ forceDeploy: true, config, }) @@ -4354,6 +4490,9 @@ describe("Governor and TimeLock", function () { voter = otherAccounts[0] await getVot3Tokens(voter, "30000") await waitForNextBlock() + + await veBetterPassport.whitelist(voter.address) + await veBetterPassport.toggleCheck(1) }) it("cannot queue a proposal if not in succeeded state", async function () { @@ -4617,8 +4756,12 @@ describe("Governor and TimeLock", function () { b3tr, B3trContract, otherAccount: proposer, + veBetterPassport, } = await getOrDeployContractInstances({ forceDeploy: true, config }) + await veBetterPassport.whitelist(voter.address) + await veBetterPassport.toggleCheck(1) + await getVot3Tokens(voter, "30000") // create a new proposal @@ -4685,11 +4828,16 @@ describe("Governor and TimeLock", function () { governor, b3tr, B3trContract, + veBetterPassport, otherAccount: proposer, } = await getOrDeployContractInstances({ forceDeploy: true, config }) await getVot3Tokens(voter, "30000") + await veBetterPassport.whitelist(voter.address) + + await veBetterPassport.toggleCheck(1) + // create a new proposal const tx = await createProposal( b3tr, @@ -4756,6 +4904,7 @@ describe("Governor and TimeLock", function () { B3trContract, otherAccount: proposer, otherAccounts, + veBetterPassport, timeLock, owner, } = await getOrDeployContractInstances({ forceDeploy: true, config }) @@ -4768,6 +4917,10 @@ describe("Governor and TimeLock", function () { await getVot3Tokens(voter, "30000") await waitForNextBlock() + await veBetterPassport.whitelist(voter.address) + + await veBetterPassport.toggleCheck(1) + // create a new proposal const tx = await createProposal( b3tr, @@ -4834,6 +4987,7 @@ describe("Governor and TimeLock", function () { governor, b3tr, B3trContract, + veBetterPassport, otherAccount: proposer, otherAccounts, owner, @@ -4847,6 +5001,10 @@ describe("Governor and TimeLock", function () { await getVot3Tokens(voter, "30000") await waitForNextBlock() + await veBetterPassport.whitelist(voter.address) + + await veBetterPassport.toggleCheck(1) + // create a new proposal const tx = await createProposal( b3tr, @@ -6012,6 +6170,7 @@ describe("Governor and TimeLock", function () { b3tr, B3trContract, otherAccount: proposer, + veBetterPassport, otherAccounts, } = await getOrDeployContractInstances({ forceDeploy: false }) @@ -6024,6 +6183,8 @@ describe("Governor and TimeLock", function () { await getVot3Tokens(voter, "150000") await waitForNextBlock() + await veBetterPassport.whitelist(voter.address) + // create a new proposal const tx = await createProposal( b3tr, diff --git a/test/Timelock.test.ts b/test/Timelock.test.ts index e1294ed..9d41a50 100644 --- a/test/Timelock.test.ts +++ b/test/Timelock.test.ts @@ -5,7 +5,7 @@ import { describe, it } from "mocha" import { getImplementationAddress } from "@openzeppelin/upgrades-core" // Tests about queueing and executing proposals are in the Governance.test.ts file -describe("TimeLock", function () { +describe("TimeLock - @shard2", function () { describe("Contract upgradeablity", () => { it("Admin should be able to upgrade the contract", async function () { const { timeLock, timelockAdmin } = await getOrDeployContractInstances({ diff --git a/test/Treasury.test.ts b/test/Treasury.test.ts index ddd9030..91b64d8 100644 --- a/test/Treasury.test.ts +++ b/test/Treasury.test.ts @@ -17,7 +17,7 @@ import { deployProxy } from "../scripts/helpers" import { getEventName } from "./helpers/events" import { ZERO_ADDRESS } from "./helpers" -describe("Treasury", () => { +describe("Treasury - @shard2", () => { let treasuryProxy: Treasury let b3tr: B3TR let vot3: any diff --git a/test/VOT3.test.ts b/test/VOT3.test.ts index 18c1895..05dcb3f 100644 --- a/test/VOT3.test.ts +++ b/test/VOT3.test.ts @@ -5,7 +5,7 @@ import { describe, it } from "mocha" import { getImplementationAddress } from "@openzeppelin/upgrades-core" import { deployProxy } from "../scripts/helpers" -describe("VOT3", function () { +describe("VOT3 - @shard2", function () { describe("Deployment", function () { it("should deploy the contract", async function () { const { vot3 } = await getOrDeployContractInstances({ forceDeploy: false }) @@ -433,7 +433,7 @@ describe("VOT3", function () { expect(await vot3.delegates(otherAccount)).to.eql(otherAccount.address) // transfer - const tx = await vot3 + let tx = await vot3 .connect(otherAccount) .transfer(minterAccount, ethers.parseEther("1"), { gasLimit: 10_000_000 }) const receipt = await tx.wait() diff --git a/test/VeBetterPassport.test.ts b/test/VeBetterPassport.test.ts new file mode 100644 index 0000000..e14579a --- /dev/null +++ b/test/VeBetterPassport.test.ts @@ -0,0 +1,5693 @@ +import { ethers } from "hardhat" +import { expect } from "chai" +import { + bootstrapAndStartEmissions, + bootstrapEmissions, + createProposal, + linkEntityToPassportWithSignature, + getOrDeployContractInstances, + getProposalIdFromTx, + getVot3Tokens, + payDeposit, + startNewAllocationRound, + waitForNextCycle, + waitForProposalToBeActive, + delegateWithSignature, + moveToCycle, + waitForCurrentRoundToEnd, + moveBlocks, +} from "./helpers" +import { describe, it } from "mocha" +import { getImplementationAddress } from "@openzeppelin/upgrades-core" +import { ZeroAddress } from "ethers" +import { createTestConfig } from "./helpers/config" + +describe("VeBetterPassport - @shard5", function () { + describe("Contract parameters", function () { + it("Should have contract addresses set correctly", async function () { + const { veBetterPassport, x2EarnApps, xAllocationVoting, galaxyMember } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + // Verify contract addresses + expect(await veBetterPassport.getXAllocationVoting()).to.equal(await xAllocationVoting.getAddress()) + expect(await veBetterPassport.getX2EarnApps()).to.equal(await x2EarnApps.getAddress()) + expect(await veBetterPassport.getGalaxyMember()).to.equal(await galaxyMember.getAddress()) + }) + + it("Should have correct roles set", async function () { + const { veBetterPassport, owner } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + expect(await veBetterPassport.hasRole(await veBetterPassport.DEFAULT_ADMIN_ROLE(), owner.address)).to.be.true + expect(await veBetterPassport.hasRole(await veBetterPassport.SETTINGS_MANAGER_ROLE(), owner.address)).to.be.true + expect(await veBetterPassport.hasRole(await veBetterPassport.ROLE_GRANTER(), owner.address)).to.be.true + expect(await veBetterPassport.hasRole(await veBetterPassport.SIGNALER_ROLE(), owner.address)).to.be.true + expect(await veBetterPassport.hasRole(await veBetterPassport.WHITELISTER_ROLE(), owner.address)).to.be.true + expect(await veBetterPassport.hasRole(await veBetterPassport.ACTION_REGISTRAR_ROLE(), owner.address)).to.be.true + expect(await veBetterPassport.hasRole(await veBetterPassport.ACTION_SCORE_MANAGER_ROLE(), owner.address)).to.be + .true + }) + + it("Should have action score thresholds set correctly", async function () { + const config = createTestConfig() + const { veBetterPassport } = await getOrDeployContractInstances({ + forceDeploy: true, + config: { + ...config, + VEPASSPORT_BOT_SIGNALING_THRESHOLD: 5, + VEPASSPORT_WHITELIST_THRESHOLD_PERCENTAGE: 20, + VEPASSPORT_BLACKLIST_THRESHOLD_PERCENTAGE: 10, + }, + }) + + expect(await veBetterPassport.thresholdPoPScore()).to.equal(0) + expect(await veBetterPassport.signalingThreshold()).to.equal(5) + expect(await veBetterPassport.whitelistThreshold()).to.equal(20) + expect(await veBetterPassport.blacklistThreshold()).to.equal(10) + }) + + it("Should have rounds for cumulative score set correctly", async function () { + const config = createTestConfig() + const { veBetterPassport } = await getOrDeployContractInstances({ + forceDeploy: true, + config: { + ...config, + VEPASSPORT_ROUNDS_FOR_CUMULATIVE_PARTICIPATION_SCORE: 5, + }, + }) + + expect(await veBetterPassport.roundsForCumulativeScore()).to.equal(5) + }) + + it("Should have minimum galaxy member level set correctly", async function () { + const config = createTestConfig() + const { veBetterPassport } = await getOrDeployContractInstances({ + forceDeploy: true, + config: { + ...config, + VEPASSPORT_GALAXY_MEMBER_MINIMUM_LEVEL: 5, + }, + }) + + expect(await veBetterPassport.getMinimumGalaxyMemberLevel()).to.equal(5) + }) + + it("Should return correct eip712 domain separator", async function () { + const { veBetterPassport } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + const domainSeparator = await veBetterPassport.eip712Domain() + expect(domainSeparator).to.deep.equal([ + "0x0f", + "VeBetterPassport", + "1", + 1337n, + await veBetterPassport.getAddress(), + "0x0000000000000000000000000000000000000000000000000000000000000000", + [], + ]) + }) + }) + // deployment + describe("Upgrades", function () { + it("should return the correct version", async function () { + const { veBetterPassport } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + expect(await veBetterPassport.version()).to.equal("1") + }) + it("Should not be able to initialize twice", async function () { + const config = createTestConfig() + const { veBetterPassport, owner, x2EarnApps, xAllocationVoting, galaxyMember } = + await getOrDeployContractInstances({ + forceDeploy: true, + }) + + await expect( + veBetterPassport.initialize( + { + x2EarnApps: await x2EarnApps.getAddress(), + xAllocationVoting: await xAllocationVoting.getAddress(), + galaxyMember: await galaxyMember.getAddress(), + signalingThreshold: config.VEPASSPORT_BOT_SIGNALING_THRESHOLD, //signalingThreshold + roundsForCumulativeScore: config.VEPASSPORT_ROUNDS_FOR_CUMULATIVE_PARTICIPATION_SCORE, //roundsForCumulativeScore + minimumGalaxyMemberLevel: config.VEPASSPORT_GALAXY_MEMBER_MINIMUM_LEVEL, //galaxyMemberMinimumLevel + blacklistThreshold: config.VEPASSPORT_BLACKLIST_THRESHOLD_PERCENTAGE, //blacklistThreshold + whitelistThreshold: config.VEPASSPORT_WHITELIST_THRESHOLD_PERCENTAGE, //whitelistThreshold + maxEntitiesPerPassport: config.VEPASSPORT_PASSPORT_MAX_ENTITIES, //maxEntitiesPerPassport + decayRate: config.VEPASSPORT_DECAY_RATE, //decayRate + }, + { + admin: owner.address, // admin + botSignaler: owner.address, // botSignaler + upgrader: owner.address, // upgrader + settingsManager: owner.address, // settingsManager + roleGranter: owner.address, // roleGranter + blacklister: owner.address, // blacklister + whitelister: owner.address, // whitelistManager + actionRegistrar: owner.address, // actionRegistrar + actionScoreManager: owner.address, // actionScoreManager + }, + ), + ).to.be.reverted + }) + + it("Should not be able to upgrade if without UPGRADER_ROLE", async function () { + const { + veBetterPassport, + otherAccount, + passportChecksLogic, + passportConfigurator, + passportDelegationLogic, + passportPersonhoodLogic, + passportPoPScoreLogic, + passportSignalingLogic, + passportEntityLogic, + passportWhitelistBlacklistLogic, + } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + // Deploy the implementation contract + const Contract = await ethers.getContractFactory("VeBetterPassport", { + libraries: { + PassportChecksLogic: await passportChecksLogic.getAddress(), + PassportConfigurator: await passportConfigurator.getAddress(), + PassportEntityLogic: await passportEntityLogic.getAddress(), + PassportPersonhoodLogic: await passportPersonhoodLogic.getAddress(), + PassportPoPScoreLogic: await passportPoPScoreLogic.getAddress(), + PassportDelegationLogic: await passportDelegationLogic.getAddress(), + PassportSignalingLogic: await passportSignalingLogic.getAddress(), + PassportWhitelistAndBlacklistLogic: await passportWhitelistBlacklistLogic.getAddress(), + }, + }) + + const implementation = await Contract.deploy() + await implementation.waitForDeployment() + + const UPGRADER_ROLE = await veBetterPassport.UPGRADER_ROLE() + expect(await veBetterPassport.hasRole(UPGRADER_ROLE, otherAccount)).to.eql(false) + + await expect(veBetterPassport.connect(otherAccount).upgradeToAndCall(await implementation.getAddress(), "0x")).to + .be.reverted + }) + + it("User with UPGRADER_ROLE should be able to upgrade the contract", async function () { + const { + owner, + veBetterPassport, + passportChecksLogic, + passportConfigurator, + passportDelegationLogic, + passportPersonhoodLogic, + passportPoPScoreLogic, + passportEntityLogic, + passportSignalingLogic, + passportWhitelistBlacklistLogic, + } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + // Deploy the implementation contract + // Deploy the implementation contract + const Contract = await ethers.getContractFactory("VeBetterPassport", { + libraries: { + PassportChecksLogic: await passportChecksLogic.getAddress(), + PassportConfigurator: await passportConfigurator.getAddress(), + PassportEntityLogic: await passportEntityLogic.getAddress(), + PassportPersonhoodLogic: await passportPersonhoodLogic.getAddress(), + PassportPoPScoreLogic: await passportPoPScoreLogic.getAddress(), + PassportDelegationLogic: await passportDelegationLogic.getAddress(), + PassportSignalingLogic: await passportSignalingLogic.getAddress(), + PassportWhitelistAndBlacklistLogic: await passportWhitelistBlacklistLogic.getAddress(), + }, + }) + const implementation = await Contract.deploy() + await implementation.waitForDeployment() + + const currentImplAddress = await getImplementationAddress(ethers.provider, await veBetterPassport.getAddress()) + + const UPGRADER_ROLE = await veBetterPassport.UPGRADER_ROLE() + expect(await veBetterPassport.hasRole(UPGRADER_ROLE, owner.address)).to.eql(true) + + await expect(veBetterPassport.connect(owner).upgradeToAndCall(await implementation.getAddress(), "0x")).to.not.be + .reverted + + const newImplAddress = await getImplementationAddress(ethers.provider, await veBetterPassport.getAddress()) + + expect(newImplAddress.toUpperCase()).to.not.eql(currentImplAddress.toUpperCase()) + expect(newImplAddress.toUpperCase()).to.eql((await implementation.getAddress()).toUpperCase()) + }) + + it("Only user with UPGRADER_ROLE should be able to upgrade the contract", async function () { + const { + owner, + veBetterPassport, + otherAccount, + passportChecksLogic, + passportConfigurator, + passportDelegationLogic, + passportPersonhoodLogic, + passportPoPScoreLogic, + passportSignalingLogic, + passportWhitelistBlacklistLogic, + } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + // Deploy the implementation contract + // Deploy the implementation contract + const Contract = await ethers.getContractFactory("VeBetterPassport", { + libraries: { + PassportChecksLogic: await passportChecksLogic.getAddress(), + PassportConfigurator: await passportConfigurator.getAddress(), + PassportEntityLogic: await passportDelegationLogic.getAddress(), + PassportDelegationLogic: await passportDelegationLogic.getAddress(), + PassportPersonhoodLogic: await passportPersonhoodLogic.getAddress(), + PassportPoPScoreLogic: await passportPoPScoreLogic.getAddress(), + PassportSignalingLogic: await passportSignalingLogic.getAddress(), + PassportWhitelistAndBlacklistLogic: await passportWhitelistBlacklistLogic.getAddress(), + }, + }) + const implementation = await Contract.deploy() + await implementation.waitForDeployment() + + const currentImplAddress = await getImplementationAddress(ethers.provider, await veBetterPassport.getAddress()) + + await veBetterPassport.revokeRole(await veBetterPassport.UPGRADER_ROLE(), owner.address) // Revoke the UPGRADER_ROLE from the owner + + expect(await veBetterPassport.hasRole(await veBetterPassport.UPGRADER_ROLE(), owner.address)).to.eql(false) + + await veBetterPassport.grantRole(await veBetterPassport.UPGRADER_ROLE(), otherAccount.address) // Grant the UPGRADER_ROLE to the otherAccount + + // Upgrade the VeBetterPassport implementation with NON-UPGRADER_ROLE user + await expect(veBetterPassport.connect(owner).upgradeToAndCall(await implementation.getAddress(), "0x")).to.be + .reverted + + // Upgrade the VeBetterPassport implementation with UPGRADER_ROLE user + await expect(veBetterPassport.connect(otherAccount).upgradeToAndCall(await implementation.getAddress(), "0x")).to + .not.be.reverted + + const newImplAddress = await getImplementationAddress(ethers.provider, await veBetterPassport.getAddress()) + + expect(newImplAddress.toUpperCase()).to.not.eql(currentImplAddress.toUpperCase()) + expect(newImplAddress.toUpperCase()).to.eql((await implementation.getAddress()).toUpperCase()) + }) + + /* + Note that when VeBetterPassport is upgraded to a version > 1, we should test also: + - that the new contract is initialized correctly + - that the new contract's version is returned correctly + - that there is no storage conflict between the old and new contract + */ + }) + + describe("Passport Checks", function () { + it("Should initialize correctly", async function () { + const { + owner: settingsManager, + veBetterPassport, + otherAccount, + } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + // Verify non admin account cannot toggle checks by default + await expect(veBetterPassport.connect(otherAccount).toggleCheck(1)).to.be.reverted + + const settingsManagerRole = await veBetterPassport.SETTINGS_MANAGER_ROLE() + + // Verify settingsManager has the role + expect(await veBetterPassport.hasRole(settingsManagerRole, settingsManager.address)).to.be.true + }) + + it("Should allow only settings manager to toggle checks", async function () { + const { + owner: settingsManager, + veBetterPassport, + otherAccount, + } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + await expect(veBetterPassport.connect(otherAccount).toggleCheck(1)).to.be.reverted + + // Settings manager should be able to toggle the checks + await expect(veBetterPassport.connect(settingsManager).toggleCheck(1)) // 1 is the + .to.emit(veBetterPassport, "CheckToggled") + .withArgs("Whitelist Check", true) + + // Whitelist check should be enabled + expect(await veBetterPassport.isCheckEnabled(1)).to.be.true + + // Cast SETTING_MANAGER_ROLE to otherAccount + const settingsManagerRole = await veBetterPassport.SETTINGS_MANAGER_ROLE() + await veBetterPassport.connect(settingsManager).grantRole(settingsManagerRole, otherAccount.address) + + // Other account should be able to toggle the checks + await expect(veBetterPassport.connect(otherAccount).toggleCheck(1)) + .to.emit(veBetterPassport, "CheckToggled") + .withArgs("Whitelist Check", false) + + // Whitelist check should be disabled + expect(await veBetterPassport.isCheckEnabled(1)).to.be.false + }) + + it("Should be able to toggle whitelist check", async function () { + const { owner: settingsManager, veBetterPassport } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + // Whitelist check should be disabled by default + expect(await veBetterPassport.isCheckEnabled(1)).to.be.false + + // Settings manager should be able to toggle the checks + await expect(veBetterPassport.connect(settingsManager).toggleCheck(1)) + .to.emit(veBetterPassport, "CheckToggled") + .withArgs("Whitelist Check", true) + + // Whitelist check should be enabled + expect(await veBetterPassport.isCheckEnabled(1)).to.be.true + }) + + it("Should be able to toggle blacklist check", async function () { + const { owner: settingsManager, veBetterPassport } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + // Blacklist check should be disabled by default + expect(await veBetterPassport.isCheckEnabled(2)).to.be.false + + // Settings manager should be able to toggle the checks + await expect(veBetterPassport.connect(settingsManager).toggleCheck(2)) + .to.emit(veBetterPassport, "CheckToggled") + .withArgs("Blacklist Check", true) + + // Blacklist check should be enabled + expect(await veBetterPassport.isCheckEnabled(2)).to.be.true + }) + + it("Should be able to toggle signaling check", async function () { + const { owner: settingsManager, veBetterPassport } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + // Signaling check should be disabled by default + expect(await veBetterPassport.isCheckEnabled(3)).to.be.false + + // Settings manager should be able to toggle the checks + await expect(veBetterPassport.connect(settingsManager).toggleCheck(3)) + .to.emit(veBetterPassport, "CheckToggled") + .withArgs("Signaling Check", true) + + // Signaling check should be enabled + expect(await veBetterPassport.isCheckEnabled(3)).to.be.true + }) + + it("Should be able to toggle participation check", async function () { + const { owner: settingsManager, veBetterPassport } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + // Participation check should be disabled by default + expect(await veBetterPassport.isCheckEnabled(4)).to.be.false + + // Settings manager should be able to toggle the checks + await expect(veBetterPassport.connect(settingsManager).toggleCheck(4)) + .to.emit(veBetterPassport, "CheckToggled") + .withArgs("Participation Score Check", true) + + // Participation check should be enabled + expect(await veBetterPassport.isCheckEnabled(4)).to.be.true + }) + + it("Should be able to toggle gm ownership check", async function () { + const { owner: settingsManager, veBetterPassport } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + // Whitelist check should be disabled by default + expect(await veBetterPassport.isCheckEnabled(5)).to.be.false + + // Settings manager should be able to toggle the checks + await expect(veBetterPassport.connect(settingsManager).toggleCheck(5)) + .to.emit(veBetterPassport, "CheckToggled") + .withArgs("GM Ownership Check", true) + + // Whitelist check should be enabled + expect(await veBetterPassport.isCheckEnabled(5)).to.be.true + }) + + it("Should be able to set the minimum galaxy member level", async function () { + const { owner: settingsManager, veBetterPassport } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + // Set the minimum galaxy member level + await expect(veBetterPassport.connect(settingsManager).setMinimumGalaxyMemberLevel(5)) + .to.emit(veBetterPassport, "MinimumGalaxyMemberLevelSet") + .withArgs(5) + + // Verify the minimum galaxy member level + expect(await veBetterPassport.getMinimumGalaxyMemberLevel()).to.equal(5) + }) + + it("Should not be able to toggle a a check that is not defined in enum", async function () { + const { owner: settingsManager, veBetterPassport } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + // Should revert if the check is not defined in the enum + await expect(veBetterPassport.connect(settingsManager).toggleCheck(8)).to.be.reverted + }) + }) + + describe("Passport Configurator", function () { + it("should be able to set the Galaxy Member address", async function () { + const { veBetterPassport, galaxyMember, otherAccount } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + // Check if the galaxy member is set correctly + expect(await veBetterPassport.getGalaxyMember()).to.equal(await galaxyMember.getAddress()) + + // Cannot set the galaxy member to the zero address + await expect(veBetterPassport.setGalaxyMember(ZeroAddress)).to.be.reverted + + // Set the galaxy member to another address + await veBetterPassport.setGalaxyMember(otherAccount.address) + + // Check if the galaxy member is set correctly + expect(await veBetterPassport.getGalaxyMember()).to.equal(otherAccount.address) + }) + + it("should be able to set the X2Earn address", async function () { + const { veBetterPassport, x2EarnApps, otherAccount } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + expect(await veBetterPassport.getX2EarnApps()).to.equal(await x2EarnApps.getAddress()) + + await expect(veBetterPassport.setX2EarnApps(ZeroAddress)).to.be.reverted + + await veBetterPassport.setX2EarnApps(otherAccount.address) + + expect(await veBetterPassport.getX2EarnApps()).to.equal(otherAccount.address) + }) + + it("should be able to set the xAllocationVoting address", async function () { + const { veBetterPassport, xAllocationVoting, otherAccount } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + expect(await veBetterPassport.getXAllocationVoting()).to.equal(await xAllocationVoting.getAddress()) + + await expect(veBetterPassport.setXAllocationVoting(ZeroAddress)).to.be.reverted + + await veBetterPassport.setXAllocationVoting(otherAccount.address) + + expect(await veBetterPassport.getXAllocationVoting()).to.equal(otherAccount.address) + }) + }) + + describe("Passport Signaling", function () { + it("Admin of App can assigner and revoker a signaler", async function () { + const { x2EarnApps, otherAccounts, otherAccount, owner, veBetterPassport } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + const appAdmin = otherAccounts[0] + + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[0].address, appAdmin, otherAccounts[0].address, "metadataURI") + + const appId = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[0].address)) + + await expect(veBetterPassport.connect(appAdmin).assignSignalerToAppByAppAdmin(appId, otherAccount.address)) + .to.emit(veBetterPassport, "SignalerAssignedToApp") + .withArgs(otherAccount.address, appId) + + expect(await veBetterPassport.appOfSignaler(otherAccount.address)).to.equal(appId) + + await expect(veBetterPassport.connect(appAdmin).removeSignalerFromAppByAppAdmin(otherAccount.address)) + .to.emit(veBetterPassport, "SignalerRemovedFromApp") + .withArgs(otherAccount.address, appId) + + expect(await veBetterPassport.appOfSignaler(otherAccount.address)).to.equal(ethers.ZeroHash) + }) + + it("Non-Admin of an app cannot add a signaler", async function () { + const { x2EarnApps, otherAccounts, otherAccount, owner, veBetterPassport } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + const appAdmin = otherAccounts[0] + + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[0].address, appAdmin, otherAccounts[0].address, "metadataURI") + + const appId = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[0].address)) + + await expect(veBetterPassport.connect(otherAccount).assignSignalerToAppByAppAdmin(appId, otherAccount.address)).to + .be.reverted + }) + + it("ROLE_GRANTER can add and remove app signalers", async function () { + const { x2EarnApps, otherAccounts, otherAccount, owner, veBetterPassport } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + const appAdmin = otherAccounts[0] + + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[0].address, appAdmin, otherAccounts[0].address, "metadataURI") + + const appId = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[0].address)) + + await expect(veBetterPassport.connect(owner).assignSignalerToApp(appId, otherAccount.address)) // Differs from `assignSignalerToAppByAppAdmin` + .to.emit(veBetterPassport, "SignalerAssignedToApp") + .withArgs(otherAccount.address, appId) + + expect(await veBetterPassport.appOfSignaler(otherAccount.address)).to.equal(appId) + + await expect(veBetterPassport.connect(owner).removeSignalerFromApp(otherAccount.address)) // Differs from `removeSignalerFromAppByAppAdmin` + .to.emit(veBetterPassport, "SignalerRemovedFromApp") + .withArgs(otherAccount.address, appId) + + expect(await veBetterPassport.appOfSignaler(otherAccount.address)).to.equal(ethers.ZeroHash) + }) + + it("Signaler can signal a user", async function () { + const { veBetterPassport, otherAccount, owner, otherAccounts, x2EarnApps } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + const appAdmin = otherAccounts[0] + + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[0].address, appAdmin, otherAccounts[0].address, "metadataURI") + + const appId = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[0].address)) + + await expect(veBetterPassport.connect(appAdmin).assignSignalerToAppByAppAdmin(appId, otherAccount.address)) + .to.emit(veBetterPassport, "SignalerAssignedToApp") + .withArgs(otherAccount.address, appId) + + expect(await veBetterPassport.hasRole(await veBetterPassport.SIGNALER_ROLE(), otherAccount.address)).to.be.true + + await expect(veBetterPassport.connect(otherAccount).signalUser(owner.address)) + .to.emit(veBetterPassport, "UserSignaled") + .withArgs(owner.address, otherAccount.address, appId, "") + + expect(await veBetterPassport.signaledCounter(owner.address)).to.equal(1) + expect(await veBetterPassport.appSignalsCounter(appId, owner.address)).to.equal(1) + + await expect(veBetterPassport.connect(appAdmin).removeSignalerFromAppByAppAdmin(otherAccount.address)) + .to.emit(veBetterPassport, "SignalerRemovedFromApp") + .withArgs(otherAccount.address, appId) + + await expect(veBetterPassport.connect(otherAccount).signalUser(owner.address)).to.be.reverted + }) + + it("Signaler can signal a user with reason", async function () { + const { veBetterPassport, otherAccount, owner, otherAccounts, x2EarnApps } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + const appAdmin = otherAccounts[0] + + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[0].address, appAdmin, otherAccounts[0].address, "metadataURI") + + const appId = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[0].address)) + + await expect(veBetterPassport.connect(appAdmin).assignSignalerToAppByAppAdmin(appId, otherAccount.address)) + .to.emit(veBetterPassport, "SignalerAssignedToApp") + .withArgs(otherAccount.address, appId) + + expect(await veBetterPassport.hasRole(await veBetterPassport.SIGNALER_ROLE(), otherAccount.address)).to.be.true + + await expect(veBetterPassport.connect(otherAccount).signalUserWithReason(owner.address, "Some reason")) + .to.emit(veBetterPassport, "UserSignaled") + .withArgs(owner.address, otherAccount.address, appId, "Some reason") + + expect(await veBetterPassport.signaledCounter(owner.address)).to.equal(1) + expect(await veBetterPassport.appSignalsCounter(appId, owner.address)).to.equal(1) + }) + + it("Admin can update the signaling threshold", async function () { + const config = createTestConfig() + config.VEPASSPORT_BOT_SIGNALING_THRESHOLD = 5 + const { veBetterPassport, otherAccount, owner, otherAccounts, x2EarnApps } = await getOrDeployContractInstances({ + forceDeploy: true, + config, + }) + + // Enable score check + await veBetterPassport.connect(owner).toggleCheck(4) + + // score check should be enabled + expect(await veBetterPassport.isCheckEnabled(4)).to.be.true + + // Expect score threshold to be 0 + expect(await veBetterPassport.thresholdPoPScore()).to.equal(0) + + const appAdmin = otherAccounts[0] + + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[0].address, appAdmin, otherAccounts[0].address, "metadataURI") + + const appId = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[0].address)) + + await expect(veBetterPassport.connect(appAdmin).assignSignalerToAppByAppAdmin(appId, otherAccount.address)) + .to.emit(veBetterPassport, "SignalerAssignedToApp") + .withArgs(otherAccount.address, appId) + + expect(await veBetterPassport.hasRole(await veBetterPassport.SIGNALER_ROLE(), otherAccount.address)).to.be.true + + await expect(veBetterPassport.connect(otherAccount).signalUserWithReason(owner.address, "Some reason")) + .to.emit(veBetterPassport, "UserSignaled") + .withArgs(owner.address, otherAccount.address, appId, "Some reason") + + expect(await veBetterPassport.signaledCounter(owner.address)).to.equal(1) + expect(await veBetterPassport.appSignalsCounter(appId, owner.address)).to.equal(1) + + // User should be a person as there score is 0 and participation score check is enabled with threshold of 0, even if they have been signaled as signaling threshold is 5 + expect(await veBetterPassport.isPerson(owner.address)).to.deep.equal([ + true, + "User's participation score is above the threshold", + ]) + + // Update the signaling threshold + await veBetterPassport.connect(owner).setSignalingThreshold(1) + + // Enable signaling check + await veBetterPassport.connect(owner).toggleCheck(3) + + // User should be a bot as they have been signaled and signaling threshold is 1 + expect(await veBetterPassport.isPerson(owner.address)).to.deep.equal([ + false, + "User has been signaled too many times", + ]) + }) + + it("DEFAULT_ADMIN_ROLE can reset signals of a user", async function () { + const { veBetterPassport, otherAccount, owner, otherAccounts, x2EarnApps } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + const appAdmin = otherAccounts[0] + + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[0].address, appAdmin, otherAccounts[0].address, "metadataURI") + + const appId = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[0].address)) + + await expect(veBetterPassport.connect(appAdmin).assignSignalerToAppByAppAdmin(appId, otherAccount.address)) + .to.emit(veBetterPassport, "SignalerAssignedToApp") + .withArgs(otherAccount.address, appId) + + expect(await veBetterPassport.hasRole(await veBetterPassport.SIGNALER_ROLE(), otherAccount.address)).to.be.true + + await expect(veBetterPassport.connect(otherAccount).signalUser(owner.address)) + .to.emit(veBetterPassport, "UserSignaled") + .withArgs(owner.address, otherAccount.address, appId, "") + + expect(await veBetterPassport.signaledCounter(owner.address)).to.equal(1) + expect(await veBetterPassport.appSignalsCounter(appId, owner.address)).to.equal(1) + + await expect( + veBetterPassport + .connect(owner) + .resetUserSignalsWithReason(owner.address, "User demonstrated erroneous signaling"), + ) + .to.emit(veBetterPassport, "UserSignalsReset") + .withArgs(owner.address, "User demonstrated erroneous signaling") + + expect(await veBetterPassport.signaledCounter(owner.address)).to.equal(0) + + // App signals remains 1 so we keep stored the number of signals occurred in the past + expect(await veBetterPassport.appSignalsCounter(appId, owner.address)).to.equal(1) + }) + + it("Passport signals should be updated when an enitys signals get reset", async function () { + const { + veBetterPassport, + otherAccount: entity, + owner: passport, + otherAccounts, + x2EarnApps, + } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + const appAdmin = otherAccounts[0] + + await x2EarnApps + .connect(passport) + .addApp(otherAccounts[0].address, appAdmin, otherAccounts[0].address, "metadataURI") + + // Link entity to passport + await linkEntityToPassportWithSignature(veBetterPassport, passport, entity, 100000) + + const appId = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[0].address)) + + await expect(veBetterPassport.connect(appAdmin).assignSignalerToAppByAppAdmin(appId, passport.address)) + .to.emit(veBetterPassport, "SignalerAssignedToApp") + .withArgs(passport.address, appId) + + expect(await veBetterPassport.hasRole(await veBetterPassport.SIGNALER_ROLE(), passport.address)).to.be.true + + await expect(veBetterPassport.connect(passport).signalUser(entity.address)) + .to.emit(veBetterPassport, "UserSignaled") + .withArgs(entity.address, passport.address, appId, "") + + expect(await veBetterPassport.signaledCounter(entity.address)).to.equal(1) + expect(await veBetterPassport.appSignalsCounter(appId, entity.address)).to.equal(1) + + // Passports signals should be same as entity signals + expect(await veBetterPassport.signaledCounter(passport.address)).to.equal(1) + // App signals counter for passport should be 0 + expect(await veBetterPassport.appSignalsCounter(appId, passport.address)).to.equal(1) + + await expect( + veBetterPassport + .connect(passport) + .resetUserSignalsWithReason(entity.address, "User demonstrated erroneous signaling"), + ) + .to.emit(veBetterPassport, "UserSignalsReset") + .withArgs(entity.address, "User demonstrated erroneous signaling") + + expect(await veBetterPassport.signaledCounter(entity.address)).to.equal(0) + // Passports signals should be same as entity signals + expect(await veBetterPassport.signaledCounter(passport.address)).to.equal(0) + + // App signals remains 1 so we keep stored the number of signals occurred in the past + expect(await veBetterPassport.appSignalsCounter(appId, entity.address)).to.equal(1) + + // App signals counter for passport should be 0 + expect(await veBetterPassport.appSignalsCounter(appId, passport.address)).to.equal(1) + }) + + it("Passport signals should be updated when an enitys apps signals get reset", async function () { + const { + veBetterPassport, + otherAccount: entity, + owner: passport, + otherAccounts, + x2EarnApps, + } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + const appAdmin = otherAccounts[0] + + await x2EarnApps + .connect(passport) + .addApp(otherAccounts[0].address, appAdmin, otherAccounts[0].address, "metadataURI") + + // Link entity to passport + await linkEntityToPassportWithSignature(veBetterPassport, passport, entity, 100000) + + const appId = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[0].address)) + + await expect(veBetterPassport.connect(appAdmin).assignSignalerToAppByAppAdmin(appId, appAdmin.address)) + .to.emit(veBetterPassport, "SignalerAssignedToApp") + .withArgs(appAdmin.address, appId) + + await expect(veBetterPassport.connect(appAdmin).signalUser(entity.address)) + .to.emit(veBetterPassport, "UserSignaled") + .withArgs(entity.address, appAdmin.address, appId, "") + + expect(await veBetterPassport.signaledCounter(entity.address)).to.equal(1) + expect(await veBetterPassport.appSignalsCounter(appId, entity.address)).to.equal(1) + + // Passports signals should be same as entity signals + expect(await veBetterPassport.signaledCounter(passport.address)).to.equal(1) + // App signals counter for passport should be 0 + expect(await veBetterPassport.appSignalsCounter(appId, passport.address)).to.equal(1) + + await expect( + veBetterPassport + .connect(appAdmin) + .resetUserSignalsByAppAdminWithReason(entity.address, "User demonstrated erroneous signaling"), + ) + .to.emit(veBetterPassport, "UserSignalsResetForApp") + .withArgs(entity.address, appId, "User demonstrated erroneous signaling") + + expect(await veBetterPassport.signaledCounter(entity.address)).to.equal(0) + // Passports signals should be same as entity signals + expect(await veBetterPassport.signaledCounter(passport.address)).to.equal(0) + + expect(await veBetterPassport.appSignalsCounter(appId, entity.address)).to.equal(0) + + expect(await veBetterPassport.appSignalsCounter(appId, passport.address)).to.equal(0) + }) + + it("Should not be able to reset signals without DEFAULT_ADMIN_ROLE", async function () { + const { veBetterPassport, otherAccount, owner, otherAccounts, x2EarnApps } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + const appAdmin = otherAccounts[0] + + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[0].address, appAdmin, otherAccounts[0].address, "metadataURI") + + const appId = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[0].address)) + + await expect(veBetterPassport.connect(appAdmin).assignSignalerToAppByAppAdmin(appId, otherAccount.address)) + .to.emit(veBetterPassport, "SignalerAssignedToApp") + .withArgs(otherAccount.address, appId) + + expect(await veBetterPassport.hasRole(await veBetterPassport.SIGNALER_ROLE(), otherAccount.address)).to.be.true + + await expect(veBetterPassport.connect(otherAccount).signalUser(owner.address)) + .to.emit(veBetterPassport, "UserSignaled") + .withArgs(owner.address, otherAccount.address, appId, "") + + expect(await veBetterPassport.signaledCounter(owner.address)).to.equal(1) + expect(await veBetterPassport.appSignalsCounter(appId, owner.address)).to.equal(1) + + await expect( + veBetterPassport + .connect(otherAccount) + .resetUserSignalsWithReason(owner.address, "User demonstrated erroneous signaling"), + ).to.be.reverted + }) + + it("App admin should be able to reset signals of a user and total signals should be tracked correctly", async function () { + const { veBetterPassport, otherAccount, owner, otherAccounts, x2EarnApps } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[0].address, otherAccount, otherAccounts[0].address, "metadataURI") + + await x2EarnApps.connect(owner).addApp(otherAccounts[1].address, owner, otherAccounts[1].address, "metadataURI") + + const app1Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[0].address)) + const app2Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[1].address)) + + await expect(veBetterPassport.connect(otherAccount).assignSignalerToAppByAppAdmin(app1Id, otherAccount.address)) + .to.emit(veBetterPassport, "SignalerAssignedToApp") + .withArgs(otherAccount.address, app1Id) + expect(await veBetterPassport.hasRole(await veBetterPassport.SIGNALER_ROLE(), otherAccount.address)).to.be.true + + await expect(veBetterPassport.connect(owner).assignSignalerToAppByAppAdmin(app2Id, owner.address)) + .to.emit(veBetterPassport, "SignalerAssignedToApp") + .withArgs(owner.address, app2Id) + expect(await veBetterPassport.hasRole(await veBetterPassport.SIGNALER_ROLE(), owner.address)).to.be.true + + // Signal user with app1Id + await expect(veBetterPassport.connect(otherAccount).signalUser(owner.address)) + .to.emit(veBetterPassport, "UserSignaled") + .withArgs(owner.address, otherAccount.address, app1Id, "") + await expect(veBetterPassport.connect(otherAccount).signalUser(owner.address)) + .to.emit(veBetterPassport, "UserSignaled") + .withArgs(owner.address, otherAccount.address, app1Id, "") + + // Signal user with app2Id + await expect(veBetterPassport.connect(owner).signalUser(owner.address)) + .to.emit(veBetterPassport, "UserSignaled") + .withArgs(owner.address, owner.address, app2Id, "") + + expect(await veBetterPassport.signaledCounter(owner.address)).to.equal(3) // 2 signals from app1Id and 1 signal from app2Id + expect(await veBetterPassport.appSignalsCounter(app1Id, owner.address)).to.equal(2) // 2 signals from app1Id + expect(await veBetterPassport.appSignalsCounter(app2Id, owner.address)).to.equal(1) // 1 signal from app2Id + + expect(await veBetterPassport.appTotalSignalsCounter(app1Id)).to.equal(2) // 2 signals from app1Id + expect(await veBetterPassport.appTotalSignalsCounter(app2Id)).to.equal(1) // 1 signal from app2Id + + // Reset signals of user by app1Id + await expect( + veBetterPassport + .connect(otherAccount) + .resetUserSignalsByAppAdminWithReason(owner.address, "User demonstrated erroneous signaling"), + ) + .to.emit(veBetterPassport, "UserSignalsResetForApp") + .withArgs(owner.address, app1Id, "User demonstrated erroneous signaling") + + expect(await veBetterPassport.signaledCounter(owner.address)).to.equal(1) // 1 signal from app2Id + expect(await veBetterPassport.appSignalsCounter(app1Id, owner.address)).to.equal(0) // 0 signals from app1Id + + expect(await veBetterPassport.appTotalSignalsCounter(app1Id)).to.equal(0) // 0 signals from app1Id + expect(await veBetterPassport.appTotalSignalsCounter(app2Id)).to.equal(1) // 1 signal from app2Id + }) + }) + + describe("Passport Entities", function () { + it("Should be able to register an entity by function calls", async function () { + const { + veBetterPassport, + owner: passport, + otherAccount: entity, + } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + await expect(veBetterPassport.connect(entity).linkEntityToPassport(passport.address)) + .to.emit(veBetterPassport, "LinkPending") + .withArgs(entity.address, passport.address) + + // Check if entity is linked to a passport + expect(await veBetterPassport.isEntity(entity.address)).to.be.false + // Expect pending link + expect((await veBetterPassport.getPendingLinkings(passport.address))[0].length).to.equal(1) + + // Approve the entity + await expect(veBetterPassport.connect(passport).acceptEntityLink(entity.address)) + .to.emit(veBetterPassport, "LinkCreated") + .withArgs(entity.address, passport.address) + + // Check if entity is linked to a passport + expect(await veBetterPassport.isEntity(entity.address)).to.be.true + // Check if passport is linked to an entity + expect(await veBetterPassport.isPassport(passport.address)).to.equal(true) + // Expect no pending link + expect((await veBetterPassport.getPendingLinkings(passport.address))[0].length).to.equal(0) + }) + + it("Should be able to register an entity by signature", async function () { + const { + veBetterPassport, + owner: passport, + otherAccount: entity, + } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + // Check if entity is linked to a passport + expect(await veBetterPassport.isEntity(entity.address)).to.be.false + // Check if passport is linked to an entity + expect(await veBetterPassport.isPassport(passport.address)).to.be.true + // No entity is linked to the passport + expect(await veBetterPassport.getEntitiesLinkedToPassport(passport.address)).to.be.empty + + // Approve the entity + // await veBetterPassport.delegateWithSignature(other) + // Set up EIP-712 domain + const domain = { + name: "VeBetterPassport", + version: "1", + chainId: 1337, + verifyingContract: await veBetterPassport.getAddress(), + } + let types = { + LinkEntity: [ + { name: "entity", type: "address" }, + { name: "passport", type: "address" }, + { name: "deadline", type: "uint256" }, + ], + } + + // Define a deadline timestamp + const currentBlock = await ethers.provider.getBlockNumber() + const block = await ethers.provider.getBlock(currentBlock) + + if (!block) { + throw new Error("Block not found") + } + + const deadline = block.timestamp + 3600 // 1 hour from + // Prepare the struct to sign + const linkData = { + entity: entity.address, + passport: passport.address, + deadline: deadline, + } + + // Create the EIP-712 signature for the delegator + const signature = await entity.signTypedData(domain, types, linkData) + + // Perform the delegation using the signature + await expect( + veBetterPassport.connect(passport).linkEntityToPassportWithSignature(entity.address, deadline, signature), + ) + .to.emit(veBetterPassport, "LinkCreated") + .withArgs(entity.address, passport.address) + + // Check if entity is linked to a passport + expect(await veBetterPassport.isEntity(entity.address)).to.be.true + // Check if passport is linked to an entity + expect(await veBetterPassport.isPassport(passport.address)).to.equal(true) + expect(await veBetterPassport.getEntitiesLinkedToPassport(passport.address)).to.have.lengthOf(1) + }) + + it("Should be ale to link multiple entities to pasport", async function () { + const { + veBetterPassport, + owner: passport, + otherAccounts, + } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + const entity1 = otherAccounts[0] + const entity2 = otherAccounts[1] + const entity3 = otherAccounts[2] + + // Check if entity is linked to a passport + expect(await veBetterPassport.isEntity(entity1.address)).to.be.false + expect(await veBetterPassport.isEntity(entity2.address)).to.be.false + expect(await veBetterPassport.isEntity(entity3.address)).to.be.false + + // Check if passport is linked to an entity + expect(await veBetterPassport.isPassport(passport.address)).to.be.true + expect(await veBetterPassport.getEntitiesLinkedToPassport(passport.address)).to.be.empty + + await linkEntityToPassportWithSignature(veBetterPassport, passport, entity1, 1000) + await linkEntityToPassportWithSignature(veBetterPassport, passport, entity2, 1000) + await linkEntityToPassportWithSignature(veBetterPassport, passport, entity3, 1000) + + // Check if entity is linked to a passport + expect(await veBetterPassport.isEntity(entity1.address)).to.be.true + expect(await veBetterPassport.isEntity(entity2.address)).to.be.true + expect(await veBetterPassport.isEntity(entity3.address)).to.be.true + + // Check if passport is linked to an entity + expect(await veBetterPassport.isPassport(passport.address)).to.equal(true) + expect(await veBetterPassport.getEntitiesLinkedToPassport(passport.address)).to.have.lengthOf(3) + }) + + it("Should be able to unlink an entity from a passport", async function () { + const { + veBetterPassport, + owner: passport, + otherAccount: entity, + } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + // Check if entity is linked to a passport + expect(await veBetterPassport.isEntity(entity.address)).to.be.false + // Check if passport is linked to an entity + expect(await veBetterPassport.isPassport(passport.address)).to.be.true + + // Approve the entity + await expect(veBetterPassport.connect(entity).linkEntityToPassport(passport.address)) + .to.emit(veBetterPassport, "LinkPending") + .withArgs(entity.address, passport.address) + + // Check if entity is linked to a passport + expect(await veBetterPassport.isEntity(entity.address)).to.be.false + // Expect pending link + expect((await veBetterPassport.getPendingLinkings(passport.address))[0].length).to.equal(1) + + // Approve the entity + await expect(veBetterPassport.connect(passport).acceptEntityLink(entity.address)) + .to.emit(veBetterPassport, "LinkCreated") + .withArgs(entity.address, passport.address) + + const block = await ethers.provider.getBlockNumber() + + // Check if entity is linked to a passport + expect(await veBetterPassport.isEntity(entity.address)).to.be.true + expect(await veBetterPassport.getPassportForEntity(entity.address)).to.equal(passport.address) + // Check if passport is linked to an entity + expect(await veBetterPassport.isPassport(passport.address)).to.equal(true) + // Expect no pending link + expect((await veBetterPassport.getPendingLinkings(passport.address))[0].length).to.equal(0) + + // Unlink the entity + await expect(veBetterPassport.connect(passport).removeEntityLink(entity.address)) + .to.emit(veBetterPassport, "LinkRemoved") + .withArgs(entity.address, passport.address) + + // Check if entity is linked to a passport + expect(await veBetterPassport.isEntity(entity.address)).to.be.false + // Check if passport is linked to an entity + expect(await veBetterPassport.isPassport(passport.address)).to.be.true + + // Entity should have been linked at the time of the block + expect(await veBetterPassport.isEntityInTimepoint(entity.address, block)).to.be.true + + expect(await veBetterPassport.getPassportForEntityAtTimepoint(entity.address, block)).to.equal(passport.address) + }) + + it("Should not be ale to link more entities than MAX allowed to be linked to a pasport", async function () { + const config = createTestConfig() + config.VEPASSPORT_PASSPORT_MAX_ENTITIES = 2 + const { + veBetterPassport, + owner: passport, + otherAccounts, + } = await getOrDeployContractInstances({ + forceDeploy: true, + config, + }) + + const entity1 = otherAccounts[0] + const entity2 = otherAccounts[1] + const entity3 = otherAccounts[2] + + // Check if entity is linked to a passport + expect(await veBetterPassport.isEntity(entity1.address)).to.be.false + expect(await veBetterPassport.isEntity(entity2.address)).to.be.false + expect(await veBetterPassport.isEntity(entity3.address)).to.be.false + + // Check if passport is linked to an entity + expect(await veBetterPassport.isPassport(passport.address)).to.be.true + expect(await veBetterPassport.getEntitiesLinkedToPassport(passport.address)).to.be.empty + + await linkEntityToPassportWithSignature(veBetterPassport, passport, entity1, 1000) + await linkEntityToPassportWithSignature(veBetterPassport, passport, entity2, 1000) + await expect( + linkEntityToPassportWithSignature(veBetterPassport, passport, entity3, 1000), + ).to.be.revertedWithCustomError(veBetterPassport, "MaxEntitiesPerPassportReached") + + // Check if entity is linked to a passport + expect(await veBetterPassport.isEntity(entity1.address)).to.be.true + expect(await veBetterPassport.isEntity(entity2.address)).to.be.true + expect(await veBetterPassport.isEntity(entity3.address)).to.be.false + + // Can update max entities per passport + await veBetterPassport.connect(passport).setMaxEntitiesPerPassport(3) + + // Can link the entity + await linkEntityToPassportWithSignature(veBetterPassport, passport, entity3, 1000) + expect(await veBetterPassport.isEntity(entity3.address)).to.be.true + + // Check if passport is linked to an entity + expect(await veBetterPassport.isPassport(passport.address)).to.equal(true) + expect(await veBetterPassport.getEntitiesLinkedToPassport(passport.address)).to.have.lengthOf(3) + }) + + it("Only passport or entity should be able to unlink an entity from a passport", async function () { + const { + veBetterPassport, + owner: passport, + otherAccount: entity, + otherAccounts, + } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + const randomWallet = otherAccounts[0] + + // Check if entity is linked to a passport + expect(await veBetterPassport.isEntity(entity.address)).to.be.false + // Check if passport is linked to an entity + expect(await veBetterPassport.isPassport(passport.address)).to.be.true + + // Approve the entity + await expect(veBetterPassport.connect(entity).linkEntityToPassport(passport.address)) + .to.emit(veBetterPassport, "LinkPending") + .withArgs(entity.address, passport.address) + + await expect(veBetterPassport.connect(randomWallet).denyIncomingPendingDelegation(entity.address)).to.be.reverted + + // Check if entity is linked to a passport + expect(await veBetterPassport.isEntity(entity.address)).to.be.false + // Expect pending link + expect((await veBetterPassport.getPendingLinkings(passport.address))[0].length).to.equal(1) + // Expect no outgoing pending link + expect((await veBetterPassport.getPendingLinkings(passport.address))[1]).to.equal(ZeroAddress) + // Expect an outgoing pending link from the random wallet to the entity + expect((await veBetterPassport.getPendingLinkings(entity.address))[1]).to.equal(passport.address) + // Expect no incoming pending link + expect((await veBetterPassport.getPendingLinkings(entity.address))[0].length).to.equal(0) + + // Approve the entity + await expect(veBetterPassport.connect(passport).acceptEntityLink(entity.address)) + .to.emit(veBetterPassport, "LinkCreated") + .withArgs(entity.address, passport.address) + + // Check if entity is linked to a passport + expect(await veBetterPassport.isEntity(entity.address)).to.be.true + // Check if passport is linked to an entity + expect(await veBetterPassport.isPassport(passport.address)).to.equal(true) + // Expect no pending link + expect((await veBetterPassport.getPendingLinkings(passport.address))[0].length).to.equal(0) + + // Unlink the entity + await expect( + veBetterPassport.connect(randomWallet).removeEntityLink(entity.address), + ).to.be.revertedWithCustomError(veBetterPassport, "UnauthorizedUser") + }) + + it("Should be able to unlink multiple entities from a passport", async function () { + const { + veBetterPassport, + owner: passport, + otherAccounts, + } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + const entity1 = otherAccounts[0] + const entity2 = otherAccounts[1] + const entity3 = otherAccounts[2] + + // Check if entity is linked to a passport + expect(await veBetterPassport.isEntity(entity1.address)).to.be.false + expect(await veBetterPassport.isEntity(entity2.address)).to.be.false + expect(await veBetterPassport.isEntity(entity3.address)).to.be.false + + // Check if passport is linked to an entity + expect(await veBetterPassport.isPassport(passport.address)).to.be.true + expect(await veBetterPassport.getEntitiesLinkedToPassport(passport.address)).to.be.empty + + await linkEntityToPassportWithSignature(veBetterPassport, passport, entity1, 1000) + await linkEntityToPassportWithSignature(veBetterPassport, passport, entity2, 1000) + await linkEntityToPassportWithSignature(veBetterPassport, passport, entity3, 1000) + + // Check if entity is linked to a passport + expect(await veBetterPassport.isEntity(entity1.address)).to.be.true + expect(await veBetterPassport.isEntity(entity2.address)).to.be.true + expect(await veBetterPassport.isEntity(entity3.address)).to.be.true + + // Check if passport is linked to an entity + expect(await veBetterPassport.isPassport(passport.address)).to.equal(true) + expect(await veBetterPassport.getEntitiesLinkedToPassport(passport.address)).to.have.lengthOf(3) + + // Unlink the entities + await expect(veBetterPassport.connect(passport).removeEntityLink(entity1.address)) + .to.emit(veBetterPassport, "LinkRemoved") + .withArgs(entity1.address, passport.address) + await expect(veBetterPassport.connect(passport).removeEntityLink(entity2.address)) + .to.emit(veBetterPassport, "LinkRemoved") + .withArgs(entity2.address, passport.address) + await expect(veBetterPassport.connect(passport).removeEntityLink(entity3.address)) + .to.emit(veBetterPassport, "LinkRemoved") + .withArgs(entity3.address, passport.address) + + // Check if entity is linked to a passport + expect(await veBetterPassport.isEntity(entity1.address)).to.be.false + expect(await veBetterPassport.isEntity(entity2.address)).to.be.false + expect(await veBetterPassport.isEntity(entity3.address)).to.be.false + + // Check if passport is linked to an entity + expect(await veBetterPassport.isPassport(passport.address)).to.be.true + expect(await veBetterPassport.getEntitiesLinkedToPassport(passport.address)).to.be.empty + }) + + it("Should be able to cancel a pending entity link", async function () { + const { + veBetterPassport, + owner: passport, + otherAccount: entity, + } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + // Check if entity is linked to a passport + expect(await veBetterPassport.isEntity(entity.address)).to.be.false + // Check if passport is linked to an entity + expect(await veBetterPassport.isPassport(passport.address)).to.be.true + + // Approve the entity + await expect(veBetterPassport.connect(entity).linkEntityToPassport(passport.address)) + .to.emit(veBetterPassport, "LinkPending") + .withArgs(entity.address, passport.address) + + // Check if entity is linked to a passport + expect(await veBetterPassport.isEntity(entity.address)).to.be.false + // Expect pending link + expect((await veBetterPassport.getPendingLinkings(passport.address))[0].length).to.equal(1) + + // Cancel the pending link + await expect(veBetterPassport.connect(passport).denyIncomingPendingEntityLink(entity.address)) + .to.emit(veBetterPassport, "LinkRemoved") + .withArgs(entity.address, passport.address) + + // Check if entity is linked to a passport + expect(await veBetterPassport.isEntity(entity.address)).to.be.false + // Check if passport is linked to an entity + expect(await veBetterPassport.isPassport(passport.address)).to.be.true + // Expect no pending link + expect((await veBetterPassport.getPendingLinkings(passport.address))[0].length).to.equal(0) + }) + + it("Only the link target can deny an incoming link request", async function () { + const { + veBetterPassport, + owner: passport, + otherAccount: entity, + otherAccounts, + } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + // Check if entity is linked to a passport + expect(await veBetterPassport.isEntity(entity.address)).to.be.false + // Check if passport is linked to an entity + expect(await veBetterPassport.isPassport(passport.address)).to.be.true + + // Approve the entity + await expect(veBetterPassport.connect(entity).linkEntityToPassport(passport.address)) + .to.emit(veBetterPassport, "LinkPending") + .withArgs(entity.address, passport.address) + + // Try to deny the link request + await expect( + veBetterPassport.connect(otherAccounts[1]).denyIncomingPendingEntityLink(entity.address), + ).to.be.revertedWithCustomError(veBetterPassport, "UnauthorizedUser") + + // The target of the link should be able to deny the link request + await expect(veBetterPassport.connect(passport).denyIncomingPendingEntityLink(entity.address)) + .to.emit(veBetterPassport, "LinkRemoved") + .withArgs(entity.address, passport.address) + }) + + it("Should revert if passport tries remove pending link without any pending link", async function () { + const { + veBetterPassport, + owner: passport, + otherAccount: entity, + } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + // Try to deny the link request + await expect(veBetterPassport.connect(passport).denyIncomingPendingEntityLink(entity.address)).to.be.reverted + }) + + it("Should revert if entity tries cancel pending link without any pending link", async function () { + const { veBetterPassport, otherAccount: entity } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + // Try to deny the link request + await expect(veBetterPassport.connect(entity).cancelOutgoingPendingEntityLink()).to.be.reverted + }) + + it("If A wants to link to C, and B wants to link to C, A should be able to deny only B's link request", async function () { + const { veBetterPassport, otherAccounts } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + const A = otherAccounts[0] + const B = otherAccounts[1] + const C = otherAccounts[2] + + // Check if entity is linked to a passport + expect(await veBetterPassport.isEntity(A.address)).to.be.false + expect(await veBetterPassport.isEntity(B.address)).to.be.false + expect(await veBetterPassport.isEntity(C.address)).to.be.false + + // A wants to link to C + await expect(veBetterPassport.connect(A).linkEntityToPassport(C.address)) + .to.emit(veBetterPassport, "LinkPending") + .withArgs(A.address, C.address) + + // B wants to link to C + await expect(veBetterPassport.connect(B).linkEntityToPassport(C.address)) + .to.emit(veBetterPassport, "LinkPending") + .withArgs(B.address, C.address) + + // C should have 2 incoming pending links and 0 outgoing pending links + expect((await veBetterPassport.getPendingLinkings(C.address))[0]).to.deep.equal([A.address, B.address]) + expect((await veBetterPassport.getPendingLinkings(C.address))[1]).to.equal(ZeroAddress) + + // C denies A's link request + await expect(veBetterPassport.connect(C).denyIncomingPendingEntityLink(A.address)) + .to.emit(veBetterPassport, "LinkRemoved") + .withArgs(A.address, C.address) + + // C should have 1 incoming pending links and 0 outgoing pending links + expect((await veBetterPassport.getPendingLinkings(C.address))[0]).to.deep.equal([B.address]) + expect((await veBetterPassport.getPendingLinkings(C.address))[1]).to.equal(ZeroAddress) + + // B should be able to cancel his link request to C + await expect(veBetterPassport.connect(B).cancelOutgoingPendingEntityLink()) + .to.emit(veBetterPassport, "LinkRemoved") + .withArgs(B.address, C.address) + + // C should have 1 incoming pending links and 0 outgoing pending links + expect((await veBetterPassport.getPendingLinkings(C.address))[0]).to.deep.equal([]) + expect((await veBetterPassport.getPendingLinkings(C.address))[1]).to.equal(ZeroAddress) + }) + + it("Should not be able to assign an entity to a passport if the entity is already linked to another passport", async function () { + const { + veBetterPassport, + owner: passport1, + otherAccounts, + } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + const entity = otherAccounts[0] + const passport2 = otherAccounts[1] + + // Check if entity is linked to a passport + expect(await veBetterPassport.isEntity(entity.address)).to.be.false + // Check if passport is linked to an entity + expect(await veBetterPassport.isPassport(passport1.address)).to.be.true + expect(await veBetterPassport.isPassport(passport2.address)).to.be.true + + expect(await veBetterPassport.getEntitiesLinkedToPassport(passport1.address)).to.be.empty + expect(await veBetterPassport.getEntitiesLinkedToPassport(passport2.address)).to.be.empty + + // Approve the entity + await expect(veBetterPassport.connect(entity).linkEntityToPassport(passport1.address)) + .to.emit(veBetterPassport, "LinkPending") + .withArgs(entity.address, passport1.address) + + // Check if entity is linked to a passport + expect(await veBetterPassport.isEntity(entity.address)).to.be.false + // Expect pending link + expect((await veBetterPassport.getPendingLinkings(passport1.address))[0].length).to.equal(1) + + // Approve the entity + await expect(veBetterPassport.connect(passport1).acceptEntityLink(entity.address)) + .to.emit(veBetterPassport, "LinkCreated") + .withArgs(entity.address, passport1.address) + + // Check if entity is linked to a passport + expect(await veBetterPassport.isEntity(entity.address)).to.be.true + // Check if passport is linked to an entity + expect(await veBetterPassport.isPassport(passport1.address)).to.be.true + expect(await veBetterPassport.isPassport(passport2.address)).to.be.true + // Expect no pending link + expect((await veBetterPassport.getPendingLinkings(passport1.address))[0].length).to.equal(0) + + // Try to link the entity to another passport + await expect( + veBetterPassport.connect(entity).linkEntityToPassport(passport2.address), + ).to.be.revertedWithCustomError(veBetterPassport, "AlreadyLinked") + }) + + it("Should not be able to assign an entity to a passport if the entity is already linked to another passport", async function () { + const { veBetterPassport, owner: passport } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + // Try to link the entity to another passport + await expect( + veBetterPassport.connect(passport).linkEntityToPassport(passport.address), + ).to.be.revertedWithCustomError(veBetterPassport, "CannotLinkToSelf") + }) + + it("Should not be able to assign an entity to a passport if the signature is invalid", async function () { + const { + veBetterPassport, + owner: passport, + otherAccount: entity, + } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + // Check if entity is linked to a passport + expect(await veBetterPassport.isEntity(entity.address)).to.be.false + // Check if passport is linked to an entity + expect(await veBetterPassport.isPassport(passport.address)).to.be.true + // No entity is linked to the passport + expect(await veBetterPassport.getEntitiesLinkedToPassport(passport.address)).to.be.empty + + // Approve the entity + // await veBetterPassport.linkEntityToPassportWithSignature(other) + // Set up EIP-712 domain + const domain = { + name: "VeBetterPassport", + version: "1", + chainId: 1337, + verifyingContract: await veBetterPassport.getAddress(), + } + + // Make the signature invalid + let types = { + INVALID: [ + { name: "entity", type: "address" }, + { name: "passport", type: "address" }, + { name: "deadline", type: "uint256" }, + ], + } + + // Define a deadline timestamp + const currentBlock = await ethers.provider.getBlockNumber() + const block = await ethers.provider.getBlock(currentBlock) + + if (!block) { + throw new Error("Block not found") + } + + const deadline = block.timestamp + 3600 // 1 hour from + // Prepare the struct to sign + const linkData = { + entity: entity.address, + passport: passport.address, + deadline: deadline, + } + + // Create the EIP-712 signature for the delegator + const signature = await entity.signTypedData(domain, types, linkData) + + // Perform the delegation using the signature + await expect( + veBetterPassport.connect(passport).linkEntityToPassportWithSignature(entity.address, deadline, signature), + ).to.be.revertedWithCustomError(veBetterPassport, "InvalidSignature") + }) + + it("Should not be able to assign an entity to a passport if the signature is expired", async function () { + const { + veBetterPassport, + owner: passport, + otherAccount: entity, + } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + // Check if entity is linked to a passport + expect(await veBetterPassport.isEntity(entity.address)).to.be.false + // Check if passport is linked to an entity + expect(await veBetterPassport.isPassport(passport.address)).to.be.true + // No entity is linked to the passport + expect(await veBetterPassport.getEntitiesLinkedToPassport(passport.address)).to.be.empty + + // Approve the entity + // await veBetterPassport.linkEntityToPassportWithSignature(other) + // Set up EIP-712 domain + const domain = { + name: "VeBetterPassport", + version: "1", + chainId: 1337, + verifyingContract: await veBetterPassport.getAddress(), + } + + let types = { + LinkEntity: [ + { name: "entity", type: "address" }, + { name: "passport", type: "address" }, + { name: "deadline", type: "uint256" }, + ], + } + + // Define a deadline timestamp + const currentBlock = await ethers.provider.getBlockNumber() + const block = await ethers.provider.getBlock(currentBlock) + + if (!block) { + throw new Error("Block not found") + } + + const deadline = block.timestamp - 1 // Ensure the deadline is in the past + // Prepare the struct to sign + const linkData = { + entity: entity.address, + passport: passport.address, + deadline: deadline, + } + + // Create the EIP-712 signature for the delegator + const signature = await entity.signTypedData(domain, types, linkData) + + // Perform the delegation using the expired signature + await expect( + veBetterPassport.connect(passport).linkEntityToPassportWithSignature(entity.address, deadline, signature), + ).to.be.revertedWithCustomError(veBetterPassport, "SignatureExpired") + }) + + it("Should not be able to assign an entity to a passport if it is linked to another passport", async function () { + const { + veBetterPassport, + owner: passport, + otherAccount: entity, + otherAccounts, + } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + const passport2 = otherAccounts[0] + + // Check if entity is linked to a passport + expect(await veBetterPassport.isEntity(entity.address)).to.be.false + // Check if passport is linked to an entity + expect(await veBetterPassport.isPassport(passport.address)).to.be.true + // No entity is linked to the passport + expect(await veBetterPassport.getEntitiesLinkedToPassport(passport.address)).to.be.empty + + // Link the entity to the passport + await linkEntityToPassportWithSignature(veBetterPassport, passport, entity, 1000) + + // Entity is linked to the passport + expect(await veBetterPassport.getEntitiesLinkedToPassport(passport.address)).to.not.be.empty + // Check entity is linked to a passport + expect(await veBetterPassport.isEntity(entity.address)).to.be.true + + await expect( + linkEntityToPassportWithSignature(veBetterPassport, passport2, entity, 1000), + ).to.be.revertedWithCustomError(veBetterPassport, "AlreadyLinked") + }) + + it("Should not be able to assign an entity to a passport if passport has the max number of entities already assigned", async function () { + const config = createTestConfig() + + config.VEPASSPORT_PASSPORT_MAX_ENTITIES = 2 + + const { + veBetterPassport, + owner: passport, + otherAccounts, + } = await getOrDeployContractInstances({ + forceDeploy: true, + config, + }) + + const entity1 = otherAccounts[0] + const entity2 = otherAccounts[1] + const entity3 = otherAccounts[2] + + // Ensure max number of entities per passport is 2 + expect(await veBetterPassport.maxEntitiesPerPassport()).to.be.equal(2) + + // Check if entity is linked to a passport + expect(await veBetterPassport.isEntity(entity1.address)).to.be.false + // Check if passport is linked to an entity + expect(await veBetterPassport.isPassport(passport.address)).to.be.true + // No entity is linked to the passport + expect(await veBetterPassport.getEntitiesLinkedToPassport(passport.address)).to.be.empty + + // Link the entities to the passport + await linkEntityToPassportWithSignature(veBetterPassport, passport, entity1, 1000) + await linkEntityToPassportWithSignature(veBetterPassport, passport, entity2, 1000) + + // Entity is linked to the passport + expect(await veBetterPassport.getEntitiesLinkedToPassport(passport.address)).to.lengthOf(2) + + await expect( + linkEntityToPassportWithSignature(veBetterPassport, passport, entity3, 1000), + ).to.be.revertedWithCustomError(veBetterPassport, "MaxEntitiesPerPassportReached") + }) + + it("Should assign an enities score correctly", async function () { + const config = createTestConfig() + config.VEPASSPORT_DECAY_RATE = 20 + config.VEPASSPORT_ROUNDS_FOR_CUMULATIVE_PARTICIPATION_SCORE = 5 + + const { veBetterPassport, owner, x2EarnApps, otherAccount, otherAccounts } = await getOrDeployContractInstances({ + forceDeploy: true, + config, + }) + + // Bootstrap emissions + await bootstrapAndStartEmissions() + + const passport = otherAccounts[0] + + //Add apps + const app1Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[2].address)) + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[2].address, otherAccounts[2].address, otherAccounts[2].address, "metadataURI") + + const app2Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[3].address)) + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[3].address, otherAccounts[3].address, otherAccounts[3].address, "metadataURI") + const app3Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[4].address)) + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[4].address, otherAccounts[4].address, otherAccounts[4].address, "metadataURI") + + await veBetterPassport.grantRole(await veBetterPassport.ACTION_SCORE_MANAGER_ROLE(), owner) + await veBetterPassport.grantRole(await veBetterPassport.ACTION_REGISTRAR_ROLE(), owner) + + expect(await veBetterPassport.hasRole(await veBetterPassport.ACTION_SCORE_MANAGER_ROLE(), owner.address)).to.be + .true + expect(await veBetterPassport.hasRole(await veBetterPassport.ACTION_REGISTRAR_ROLE(), owner.address)).to.be.true + + // Sets app1 security to APP_SECURITY.LOW + await veBetterPassport.connect(owner).setAppSecurity(app1Id, 1) + + // Sets app2 security to APP_SECURITY.MEDIUM + await veBetterPassport.connect(owner).setAppSecurity(app2Id, 2) + + // Sets app3 security to APP_SECURITY.HIGH + await veBetterPassport.connect(owner).setAppSecurity(app3Id, 3) + + await veBetterPassport.connect(owner).registerActionForRound(otherAccount, app1Id, 1) + + // Move through 5 rounds + await moveToCycle(6) + + await veBetterPassport.connect(owner).registerActionForRound(otherAccount, app1Id, 2) + await veBetterPassport.connect(owner).registerActionForRound(otherAccount, app2Id, 3) + await veBetterPassport.connect(owner).registerActionForRound(otherAccount, app2Id, 4) + await veBetterPassport.connect(owner).registerActionForRound(otherAccount, app3Id, 5) + + expect(await veBetterPassport.userRoundScore(otherAccount, 1)).to.equal(100) + expect(await veBetterPassport.userRoundScore(otherAccount, 2)).to.equal(100) + expect(await veBetterPassport.userRoundScore(otherAccount, 3)).to.equal(200) + expect(await veBetterPassport.userRoundScore(otherAccount, 4)).to.equal(200) + expect(await veBetterPassport.userRoundScore(otherAccount, 5)).to.equal(400) + + await linkEntityToPassportWithSignature(veBetterPassport, passport, otherAccount, 1000) + + /* + + The entitys score should remain the same for the same when first assigned + + Round 1 score: 100 + Round 2 score: 100 + Round 3 score: 200 + Round 4 score: 200 + Round 5 score: 400 + + round N = [round N score] + ([cumulative score] * [1 - decay factor]) + + round 1 = 100 + (0 * 0.8) = 100 + round 2 = 100 + (100 * 0.8) = 180 + round 3 = 200 + (180 * 0.8) = 344 + round 4 = 200 + (344 * 0.8) = 475,2 => 475 + round 5 = 400 + (475 * 0.8) = 780 + */ + expect(await veBetterPassport.getCumulativeScoreWithDecay(otherAccount, 5)).to.equal(780) + + /* + The passports score should not take into account the entitys score over the past VEPASSPORT_ROUNDS_FOR_ASSIGNING_ENTITY_SCORE (3) rounds + */ + expect(await veBetterPassport.getCumulativeScoreWithDecay(passport, 5)).to.equal(0) + + // The entitys score for APP1 should not be the same as the passport score (interactions with app1 happended in round 1 and 2) + expect(await veBetterPassport.userAppTotalScore(otherAccount, app1Id)).to.not.equal( + await veBetterPassport.userAppTotalScore(passport, app1Id), + ) + + // The entitys score for APP2 should not be the same as the passport score (interactions with app2 happended in round 3 and 4) + expect(await veBetterPassport.userAppTotalScore(otherAccount, app2Id)).to.not.equal( + await veBetterPassport.userAppTotalScore(passport, app2Id), + ) + + // The entitys score for APP3 should not be the same as the passport score (interactions with app3 happended in round 5) + expect(await veBetterPassport.userAppTotalScore(otherAccount, app3Id)).to.not.equal( + await veBetterPassport.userAppTotalScore(passport, app3Id), + ) + + // If we move to the next round and the entity earns more points, the passport score should increase and not the entity score + await moveToCycle(7) + + await veBetterPassport.connect(owner).registerActionForRound(otherAccount, app1Id, 6) + + expect(await veBetterPassport.userRoundScore(otherAccount, 6)).to.equal(0) + expect(await veBetterPassport.userRoundScore(passport, 6)).to.equal(100) + }) + + it("Should register aggregated actions for a round correctly", async function () { + const { + veBetterPassport, + owner, + x2EarnApps, + b3tr, + otherAccounts, + xAllocationVoting, + x2EarnRewardsPool, + minterAccount, + } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + const user1 = otherAccounts[0] + const user2 = otherAccounts[1] + + // Bootstrap emissions + await bootstrapAndStartEmissions() + // simulate 5 rounds of actions + await moveToCycle(6) + + // Add 3 apps + const app1Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[2].address)) + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[2].address, otherAccounts[2].address, otherAccounts[2].address, "metadataURI") + const app2Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[3].address)) + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[3].address, otherAccounts[3].address, otherAccounts[3].address, "metadataURI") + const app3Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[4].address)) + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[4].address, otherAccounts[4].address, otherAccounts[4].address, "metadataURI") + + // Set apps security to LOW + await veBetterPassport.connect(owner).setAppSecurity(app1Id, 1) + await veBetterPassport.connect(owner).setAppSecurity(app2Id, 1) + await veBetterPassport.connect(owner).setAppSecurity(app3Id, 1) + + // Owner can distribute rewards for all apps + await x2EarnApps.connect(owner).addRewardDistributor(app1Id, owner.address) + expect(await x2EarnApps.isRewardDistributor(app1Id, owner.address)).to.equal(true) + await x2EarnApps.connect(owner).addRewardDistributor(app2Id, owner.address) + expect(await x2EarnApps.isRewardDistributor(app2Id, owner.address)).to.equal(true) + await x2EarnApps.connect(owner).addRewardDistributor(app3Id, owner.address) + expect(await x2EarnApps.isRewardDistributor(app3Id, owner.address)).to.equal(true) + + // fill the pool + await b3tr.connect(minterAccount).mint(owner.address, ethers.parseEther("100")) + await b3tr.connect(owner).approve(await x2EarnRewardsPool.getAddress(), ethers.parseEther("100")) + await x2EarnRewardsPool.connect(owner).deposit(ethers.parseEther("30"), app1Id) + await x2EarnRewardsPool.connect(owner).deposit(ethers.parseEther("30"), app2Id) + await x2EarnRewardsPool.connect(owner).deposit(ethers.parseEther("30"), app3Id) + + const currentRound = await xAllocationVoting.currentRoundId() + const currentBlock = await ethers.provider.getBlockNumber() + const currentRoundSnapshot = await xAllocationVoting.currentRoundSnapshot() + const currentRoundDeadline = await xAllocationVoting.currentRoundDeadline() + expect(currentRound).to.equal(5n) + expect(currentBlock > currentRoundSnapshot && currentBlock < currentRoundDeadline).to.be.true + + // Let's assume we deployed VeBetterPassport in the middle of the 5th round + // and we want to register the actions for the first 4 rounds aggregating data offchain. + // Scenario: + // User 1 used app1 4 times, app2 2 times and app3 never (distributed in multiple rounds), before deploying VBP + // User 2 used app1 2 times, app2 2 times and app3 2 times (distributed in multiple rounds), before deploying VBP + + // Simulate that while we seed old actions, User1 uses app2 1 more time and app3 1 time + await x2EarnRewardsPool.connect(owner).distributeReward(app2Id, ethers.parseEther("1"), user1.address, "0x") + await x2EarnRewardsPool.connect(owner).distributeReward(app3Id, ethers.parseEther("1"), user1.address, "0x") + + // Now we seed old aggregated actions for the first 4 rounds and part of the 5th + await veBetterPassport.connect(owner).registerAggregatedActionsForRound(user1.address, app1Id, 1, 200) // user1 used app1 2 times in round 1 + await veBetterPassport.connect(owner).registerAggregatedActionsForRound(user1.address, app1Id, 2, 200) // user1 used app1 2 times in round 2 + await veBetterPassport.connect(owner).registerAggregatedActionsForRound(user1.address, app2Id, 2, 100) // user1 used app2 1 time in round 2 + await veBetterPassport.connect(owner).registerAggregatedActionsForRound(user1.address, app2Id, 5, 100) // user1 used app2 1 time in round 2 + + await veBetterPassport.connect(owner).registerAggregatedActionsForRound(user2.address, app3Id, 1, 100) // user2 used app3 1 time in round 1 + await veBetterPassport.connect(owner).registerAggregatedActionsForRound(user2.address, app1Id, 3, 100) // user2 used app1 1 time in round 3 + await veBetterPassport.connect(owner).registerAggregatedActionsForRound(user2.address, app3Id, 3, 100) // user2 used app3 1 time in round 3 + await veBetterPassport.connect(owner).registerAggregatedActionsForRound(user2.address, app1Id, 4, 100) // user2 used app1 1 time in round 4 + await veBetterPassport.connect(owner).registerAggregatedActionsForRound(user2.address, app2Id, 5, 200) // user2 used app2 2 time in round 5 + + // At the end this should be the result: + // user1: + // round 1: 200, using app1 2 times + // round 2: 200 + 100 = 300, using app1 2 times and app2 1 time + // round 3: 0 + // round 4: 0 + // round 5: 200 + 100 = 300, using app1 1 time, app2 1 time, app3 1 time + // user2: + // round 1: 100, using app1 1 time + // round 2: 0 + // round 3: 100 + 100 = 200, using app1 1 time and app3 1 time + // round 4: 100, using app1 1 time + // round 5: 100 + 100 = 200, using app2 2 times + // + // Now we should check that the following mappings were updated correctly in the contract: + // userRoundScore[passport][round] + // userTotalScore[passport] + // userAppRoundScore[passport][round][appId] + // userAppTotalScore[passport][appId] + //user1 + expect(await veBetterPassport.userRoundScore(user1.address, 1)).to.equal(200n) + expect(await veBetterPassport.userRoundScore(user1.address, 2)).to.equal(300n) + expect(await veBetterPassport.userRoundScore(user1.address, 3)).to.equal(0n) + expect(await veBetterPassport.userRoundScore(user1.address, 4)).to.equal(0n) + expect(await veBetterPassport.userRoundScore(user1.address, 5)).to.equal(300n) + expect(await veBetterPassport.userTotalScore(user1.address)).to.equal(800n) + expect(await veBetterPassport.userRoundScoreApp(user1.address, 1, app1Id)).to.equal(200n) + expect(await veBetterPassport.userRoundScoreApp(user1.address, 1, app2Id)).to.equal(0n) + expect(await veBetterPassport.userRoundScoreApp(user1.address, 1, app3Id)).to.equal(0n) + expect(await veBetterPassport.userRoundScoreApp(user1.address, 2, app1Id)).to.equal(200n) + expect(await veBetterPassport.userRoundScoreApp(user1.address, 2, app2Id)).to.equal(100n) + expect(await veBetterPassport.userRoundScoreApp(user1.address, 2, app3Id)).to.equal(0n) + expect(await veBetterPassport.userRoundScoreApp(user1.address, 3, app1Id)).to.equal(0n) + expect(await veBetterPassport.userRoundScoreApp(user1.address, 3, app2Id)).to.equal(0n) + expect(await veBetterPassport.userRoundScoreApp(user1.address, 3, app3Id)).to.equal(0n) + expect(await veBetterPassport.userRoundScoreApp(user1.address, 4, app1Id)).to.equal(0n) + expect(await veBetterPassport.userRoundScoreApp(user1.address, 4, app2Id)).to.equal(0n) + expect(await veBetterPassport.userRoundScoreApp(user1.address, 4, app3Id)).to.equal(0n) + expect(await veBetterPassport.userRoundScoreApp(user1.address, 5, app1Id)).to.equal(0n) + expect(await veBetterPassport.userRoundScoreApp(user1.address, 5, app2Id)).to.equal(200n) + expect(await veBetterPassport.userRoundScoreApp(user1.address, 5, app3Id)).to.equal(100n) + expect(await veBetterPassport.userAppTotalScore(user1.address, app1Id)).to.equal(400n) + expect(await veBetterPassport.userAppTotalScore(user1.address, app2Id)).to.equal(300n) + expect(await veBetterPassport.userAppTotalScore(user1.address, app3Id)).to.equal(100n) + //user2 + expect(await veBetterPassport.userRoundScore(user2.address, 1)).to.equal(100n) + expect(await veBetterPassport.userRoundScore(user2.address, 2)).to.equal(0n) + expect(await veBetterPassport.userRoundScore(user2.address, 3)).to.equal(200n) + expect(await veBetterPassport.userRoundScore(user2.address, 4)).to.equal(100n) + expect(await veBetterPassport.userRoundScore(user2.address, 5)).to.equal(200n) + expect(await veBetterPassport.userTotalScore(user2.address)).to.equal(600n) + expect(await veBetterPassport.userRoundScoreApp(user2.address, 1, app1Id)).to.equal(0n) + expect(await veBetterPassport.userRoundScoreApp(user2.address, 1, app2Id)).to.equal(0n) + expect(await veBetterPassport.userRoundScoreApp(user2.address, 1, app3Id)).to.equal(100n) + expect(await veBetterPassport.userRoundScoreApp(user2.address, 2, app1Id)).to.equal(0n) + expect(await veBetterPassport.userRoundScoreApp(user2.address, 2, app2Id)).to.equal(0n) + expect(await veBetterPassport.userRoundScoreApp(user2.address, 2, app3Id)).to.equal(0n) + expect(await veBetterPassport.userRoundScoreApp(user2.address, 3, app1Id)).to.equal(100n) + expect(await veBetterPassport.userRoundScoreApp(user2.address, 3, app2Id)).to.equal(0n) + expect(await veBetterPassport.userRoundScoreApp(user2.address, 3, app3Id)).to.equal(100n) + expect(await veBetterPassport.userRoundScoreApp(user2.address, 4, app1Id)).to.equal(100n) + expect(await veBetterPassport.userRoundScoreApp(user2.address, 4, app2Id)).to.equal(0n) + expect(await veBetterPassport.userRoundScoreApp(user2.address, 4, app3Id)).to.equal(0n) + expect(await veBetterPassport.userRoundScoreApp(user2.address, 5, app1Id)).to.equal(0n) + expect(await veBetterPassport.userRoundScoreApp(user2.address, 5, app2Id)).to.equal(200n) + expect(await veBetterPassport.userRoundScoreApp(user2.address, 5, app3Id)).to.equal(0n) + expect(await veBetterPassport.userAppTotalScore(user2.address, app1Id)).to.equal(200n) + expect(await veBetterPassport.userAppTotalScore(user2.address, app2Id)).to.equal(200n) + expect(await veBetterPassport.userAppTotalScore(user2.address, app3Id)).to.equal(200n) + }) + + it("Should remove an enities score correctly", async function () { + const config = createTestConfig() + config.VEPASSPORT_DECAY_RATE = 20 + const { veBetterPassport, owner, x2EarnApps, otherAccount, otherAccounts } = await getOrDeployContractInstances({ + forceDeploy: true, + config, + }) + + // Bootstrap emissions + await bootstrapAndStartEmissions() + + const passport = otherAccounts[0] + + //Add apps + const app1Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[2].address)) + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[2].address, otherAccounts[2].address, otherAccounts[2].address, "metadataURI") + + const app2Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[3].address)) + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[3].address, otherAccounts[3].address, otherAccounts[3].address, "metadataURI") + const app3Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[4].address)) + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[4].address, otherAccounts[4].address, otherAccounts[4].address, "metadataURI") + + await veBetterPassport.grantRole(await veBetterPassport.ACTION_SCORE_MANAGER_ROLE(), owner) + await veBetterPassport.grantRole(await veBetterPassport.ACTION_REGISTRAR_ROLE(), owner) + + expect(await veBetterPassport.hasRole(await veBetterPassport.ACTION_SCORE_MANAGER_ROLE(), owner.address)).to.be + .true + expect(await veBetterPassport.hasRole(await veBetterPassport.ACTION_REGISTRAR_ROLE(), owner.address)).to.be.true + + // Sets app1 security to APP_SECURITY.LOW + await veBetterPassport.connect(owner).setAppSecurity(app1Id, 1) + + // Sets app2 security to APP_SECURITY.MEDIUM + await veBetterPassport.connect(owner).setAppSecurity(app2Id, 2) + + // Sets app3 security to APP_SECURITY.HIGH + await veBetterPassport.connect(owner).setAppSecurity(app3Id, 3) + + await veBetterPassport.connect(owner).registerActionForRound(otherAccount, app1Id, 1) + + // Move through 5 rounds + await moveToCycle(6) + + await veBetterPassport.connect(owner).registerActionForRound(otherAccount, app1Id, 2) + await veBetterPassport.connect(owner).registerActionForRound(otherAccount, app2Id, 3) + await veBetterPassport.connect(owner).registerActionForRound(otherAccount, app2Id, 4) + await veBetterPassport.connect(owner).registerActionForRound(otherAccount, app3Id, 5) + + expect(await veBetterPassport.userRoundScore(otherAccount, 1)).to.equal(100) + expect(await veBetterPassport.userRoundScore(otherAccount, 2)).to.equal(100) + expect(await veBetterPassport.userRoundScore(otherAccount, 3)).to.equal(200) + expect(await veBetterPassport.userRoundScore(otherAccount, 4)).to.equal(200) + expect(await veBetterPassport.userRoundScore(otherAccount, 5)).to.equal(400) + + await linkEntityToPassportWithSignature(veBetterPassport, passport, otherAccount, 1000) + + /* + + The entitys score should remain the same as when first assigned + + Round 1 score: 100 + Round 2 score: 100 + Round 3 score: 200 + Round 4 score: 200 + Round 5 score: 400 + + round N = [round N score] + ([cumulative score] * [1 - decay factor]) + + round 1 = 100 + (0 * 0.8) = 100 + round 2 = 100 + (100 * 0.8) = 180 + round 3 = 200 + (180 * 0.8) = 344 + round 4 = 200 + (344 * 0.8) = 475,2 => 475 + round 5 = 400 + (475 * 0.8) = 780 + */ + expect(await veBetterPassport.getCumulativeScoreWithDecay(otherAccount, 5)).to.equal(780) + expect(await veBetterPassport.userTotalScore(otherAccount)).to.equal(1000) + + /* + The passports score should not take into account the entitys score over the past VEPASSPORT_ROUNDS_FOR_ASSIGNING_ENTITY_SCORE (3) rounds + */ + expect(await veBetterPassport.getCumulativeScoreWithDecay(passport, 5)).to.equal(0) + expect(await veBetterPassport.userTotalScore(passport)).to.equal(0) + + // The entitys score for APP1 should not be the same as the passport score (interactions with app1 happended in round 1 and 2) + expect(await veBetterPassport.userAppTotalScore(otherAccount, app1Id)).to.not.equal( + await veBetterPassport.userAppTotalScore(passport, app1Id), + ) + + // The entitys score for APP2 should not be the same as the passport score (interactions with app2 happended in round 3 and 4) + expect(await veBetterPassport.userAppTotalScore(otherAccount, app2Id)).to.not.equal( + await veBetterPassport.userAppTotalScore(passport, app2Id), + ) + + // The entitys score for APP3 should not be the same as the passport score (interactions with app3 happended in round 5) + expect(await veBetterPassport.userAppTotalScore(otherAccount, app3Id)).to.not.equal( + await veBetterPassport.userAppTotalScore(passport, app3Id), + ) + + // If we move to the next round and the entity earns more points, the passport score should increase and not the entity score + await moveToCycle(7) + + await veBetterPassport.connect(owner).registerActionForRound(otherAccount, app3Id, 6) + + expect(await veBetterPassport.userRoundScore(otherAccount, 6)).to.equal(0) + expect(await veBetterPassport.userRoundScore(passport, 6)).to.equal(400) + + /* + + The passports score should take into account the entitys score over the last round + + round 6 = 400 + */ + expect(await veBetterPassport.getCumulativeScoreWithDecay(passport, 6)).to.equal(400) + + // Remove the entity from the passport + await veBetterPassport.connect(passport).removeEntityLink(otherAccount) + + /* + + The entitys score should remain the same for the same when first assigned (did not earn any points in round 6) + + Round 1 score: 100 + Round 2 score: 100 + Round 3 score: 200 + Round 4 score: 200 + Round 5 score: 400 + + round N = [round N score] + ([cumulative score] * [1 - decay factor]) + + round 1 = 100 * 0.5^5 = 3.125 => 32.76 => 32 -> Not included as this is more than 5 rounds ago (roundsForCumulativeScore) + round 2 = 100 * 0.8^4 = 40.96 => 40 + round 3 = 200 * 0.8^3 = 102.4 => 102 + round 4 = 200 * 0.8^2 = 128 + round 5 = 400 * 0.8 = 320 + round 6 = 0 + */ + + expect(await veBetterPassport.userTotalScore(otherAccount)).to.equal(1000) + expect(await veBetterPassport.getCumulativeScoreWithDecay(otherAccount, 6)).to.equal(591) // Entities cumulative score should be decyaed by 0.8 + + /* + + The passports score should remain the same for the period the entity was linked to the passport + + round 6 = 400 + */ + expect(await veBetterPassport.getCumulativeScoreWithDecay(passport, 6)).to.equal(400) + expect(await veBetterPassport.userTotalScore(passport)).to.equal(400) // Score earned by the entity in the last round + }) + + it("Should assign an enities signals correctly", async function () { + const { veBetterPassport, owner, x2EarnApps, otherAccount, otherAccounts } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + const appAdmin = otherAccounts[0] + const passport = otherAccounts[1] + + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[0].address, appAdmin, otherAccounts[0].address, "metadataURI") + + const appId = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[0].address)) + + await expect(veBetterPassport.connect(appAdmin).assignSignalerToAppByAppAdmin(appId, otherAccount.address)) + .to.emit(veBetterPassport, "SignalerAssignedToApp") + .withArgs(otherAccount.address, appId) + + await veBetterPassport.connect(owner).setAppSecurity(appId, 1) + + // Register action for entity so that it is assigned a score + await veBetterPassport.connect(owner).registerActionForRound(owner, appId, 2) + + expect(await veBetterPassport.hasRole(await veBetterPassport.SIGNALER_ROLE(), otherAccount.address)).to.be.true + + await expect(veBetterPassport.connect(otherAccount).signalUser(owner.address)) + .to.emit(veBetterPassport, "UserSignaled") + .withArgs(owner.address, otherAccount.address, appId, "") + + expect(await veBetterPassport.signaledCounter(owner.address)).to.equal(1) + expect(await veBetterPassport.appSignalsCounter(appId, owner.address)).to.equal(1) + + // Passport should inherit the signals from the entity + await linkEntityToPassportWithSignature(veBetterPassport, passport, owner, 1000) + + expect(await veBetterPassport.signaledCounter(owner.address)).to.equal(1) + expect(await veBetterPassport.appSignalsCounter(appId, owner.address)).to.equal(1) + + // Passport should inherit the signals from the entity + expect(await veBetterPassport.signaledCounter(passport.address)).to.equal(1) + expect(await veBetterPassport.appSignalsCounter(appId, passport.address)).to.equal(1) + + await expect(veBetterPassport.connect(otherAccount).signalUser(owner.address)) + .to.emit(veBetterPassport, "UserSignaled") + .withArgs(owner.address, otherAccount.address, appId, "") + + expect(await veBetterPassport.signaledCounter(owner.address)).to.equal(2) + expect(await veBetterPassport.appSignalsCounter(appId, owner.address)).to.equal(2) + + expect(await veBetterPassport.signaledCounter(passport.address)).to.equal(2) + expect(await veBetterPassport.appSignalsCounter(appId, passport.address)).to.equal(2) + }) + + it("Should remove enity signals correctly when entity detaches from passport", async function () { + const { veBetterPassport, owner, x2EarnApps, otherAccount, otherAccounts } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + const appAdmin = otherAccounts[0] + const passport = otherAccounts[1] + + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[0].address, appAdmin, otherAccounts[0].address, "metadataURI") + + const appId = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[0].address)) + + await expect(veBetterPassport.connect(appAdmin).assignSignalerToAppByAppAdmin(appId, otherAccount.address)) + .to.emit(veBetterPassport, "SignalerAssignedToApp") + .withArgs(otherAccount.address, appId) + + await veBetterPassport.connect(owner).setAppSecurity(appId, 1) + + // Register action for entity so that it is assigned a score + await veBetterPassport.connect(owner).registerActionForRound(owner, appId, 2) + + expect(await veBetterPassport.hasRole(await veBetterPassport.SIGNALER_ROLE(), otherAccount.address)).to.be.true + + await expect(veBetterPassport.connect(otherAccount).signalUser(owner.address)) + .to.emit(veBetterPassport, "UserSignaled") + .withArgs(owner.address, otherAccount.address, appId, "") + + expect(await veBetterPassport.signaledCounter(owner.address)).to.equal(1) + expect(await veBetterPassport.appSignalsCounter(appId, owner.address)).to.equal(1) + + // Passport should inherit the signals from the entity + await linkEntityToPassportWithSignature(veBetterPassport, passport, owner, 1000) + + expect(await veBetterPassport.signaledCounter(owner.address)).to.equal(1) + expect(await veBetterPassport.appSignalsCounter(appId, owner.address)).to.equal(1) + + // Passport should inherit the signals from the entity + expect(await veBetterPassport.signaledCounter(passport.address)).to.equal(1) + expect(await veBetterPassport.appSignalsCounter(appId, passport.address)).to.equal(1) + + await expect(veBetterPassport.connect(otherAccount).signalUser(owner.address)) + .to.emit(veBetterPassport, "UserSignaled") + .withArgs(owner.address, otherAccount.address, appId, "") + + expect(await veBetterPassport.signaledCounter(owner.address)).to.equal(2) + expect(await veBetterPassport.appSignalsCounter(appId, owner.address)).to.equal(2) + + expect(await veBetterPassport.signaledCounter(passport.address)).to.equal(2) + expect(await veBetterPassport.appSignalsCounter(appId, passport.address)).to.equal(2) + + // Remove the entity from the passport + await veBetterPassport.connect(owner).removeEntityLink(owner) + + // Entity signals should remain the same + expect(await veBetterPassport.signaledCounter(owner.address)).to.equal(2) + expect(await veBetterPassport.appSignalsCounter(appId, owner.address)).to.equal(2) + + // Passport signals should be removed + expect(await veBetterPassport.signaledCounter(passport.address)).to.equal(0) + expect(await veBetterPassport.appSignalsCounter(appId, passport.address)).to.equal(0) + }) + + it("Should assign an enities blacklists and whitelists correctly", async function () { + const config = createTestConfig() + config.VEPASSPORT_BLACKLIST_THRESHOLD_PERCENTAGE = 0 + config.VEPASSPORT_WHITELIST_THRESHOLD_PERCENTAGE = 0 + const { + veBetterPassport, + owner: passport, + otherAccount: entity, + } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + // Check if entity is linked to a passport + expect(await veBetterPassport.isEntity(entity.address)).to.be.false + + // Blacklist the entity + await veBetterPassport.blacklist(entity.address) + + // Check if entity is blacklisted + expect(await veBetterPassport.isBlacklisted(entity.address)).to.be.true + + expect(await veBetterPassport.isPassportBlacklisted(passport.address)).to.be.false + + // Passport should inherit the signals from the entity + await linkEntityToPassportWithSignature(veBetterPassport, passport, entity, 1000) + + // Passport should be blacklisted + expect(await veBetterPassport.isPassportBlacklisted(passport.address)).to.be.true + + // Passport account is not blacklisted + expect(await veBetterPassport.isBlacklisted(passport.address)).to.be.false + + // whitelist the entity + await veBetterPassport.whitelist(entity.address) + + // Check if entity is whitelisted + expect(await veBetterPassport.isWhitelisted(entity.address)).to.be.true + expect(await veBetterPassport.isBlacklisted(entity.address)).to.be.false + + // Passport should inherit the lisitngs from the entity + expect(await veBetterPassport.isPassportWhitelisted(passport.address)).to.be.true + expect(await veBetterPassport.isPassportBlacklisted(passport.address)).to.be.false + }) + + it("Should remove any blacklists and whitelists an entity may have when it detaches", async function () { + const config = createTestConfig() + config.VEPASSPORT_BLACKLIST_THRESHOLD_PERCENTAGE = 0 + config.VEPASSPORT_WHITELIST_THRESHOLD_PERCENTAGE = 0 + const { + veBetterPassport, + owner: passport, + otherAccount: entity, + } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + // Check if entity is linked to a passport + expect(await veBetterPassport.isEntity(entity.address)).to.be.false + + // Blacklist the entity + await veBetterPassport.blacklist(entity.address) + + // Check if entity is blacklisted + expect(await veBetterPassport.isBlacklisted(entity.address)).to.be.true + + expect(await veBetterPassport.isPassportBlacklisted(passport.address)).to.be.false + + // Passport should inherit the signals from the entity + await linkEntityToPassportWithSignature(veBetterPassport, passport, entity, 1000) + + // Passport should be blacklisted + expect(await veBetterPassport.isPassportBlacklisted(passport.address)).to.be.true + + // Passport account is not blacklisted + expect(await veBetterPassport.isBlacklisted(passport.address)).to.be.false + + // whitelist the entity + await veBetterPassport.whitelist(entity.address) + + // Check if entity is whitelisted + expect(await veBetterPassport.isWhitelisted(entity.address)).to.be.true + expect(await veBetterPassport.isBlacklisted(entity.address)).to.be.false + + // Passport should inherit the lisitngs from the entity + expect(await veBetterPassport.isPassportWhitelisted(passport.address)).to.be.true + expect(await veBetterPassport.isPassportBlacklisted(passport.address)).to.be.false + + // Remove the entity from the passport + await veBetterPassport.connect(passport).removeEntityLink(entity) + + // Entity should be whitelisted + expect(await veBetterPassport.isWhitelisted(entity.address)).to.be.true + + // Entity should not be blacklisted + expect(await veBetterPassport.isBlacklisted(entity.address)).to.be.false + + // Passport should not be blacklisted + expect(await veBetterPassport.isPassportBlacklisted(passport.address)).to.be.false + + // Passport should not be whitelisted + expect(await veBetterPassport.isPassportWhitelisted(passport.address)).to.be.false + }) + + it("Should be able to assign multiple entites to a passport, do actions with entities and use the combintation to meet personhood status", async function () { + const config = createTestConfig() + const { veBetterPassport, x2EarnApps, owner, otherAccounts } = await getOrDeployContractInstances({ + forceDeploy: true, + config, + }) + + const enity1 = otherAccounts[0] + const enity2 = otherAccounts[1] + const passport = otherAccounts[2] + + // Set the threshold to 500 + await veBetterPassport.connect(owner).setThresholdPoPScore(500) + + // Bootstrap emissions + await bootstrapAndStartEmissions() + + //Add apps + const app1Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[2].address)) + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[2].address, otherAccounts[2].address, otherAccounts[2].address, "metadataURI") + + const app2Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[3].address)) + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[3].address, otherAccounts[3].address, otherAccounts[3].address, "metadataURI") + + await veBetterPassport.grantRole(await veBetterPassport.ACTION_SCORE_MANAGER_ROLE(), owner) + await veBetterPassport.grantRole(await veBetterPassport.ACTION_REGISTRAR_ROLE(), owner) + + expect(await veBetterPassport.hasRole(await veBetterPassport.ACTION_SCORE_MANAGER_ROLE(), owner.address)).to.be + .true + expect(await veBetterPassport.hasRole(await veBetterPassport.ACTION_REGISTRAR_ROLE(), owner.address)).to.be.true + + // Sets app1 security to APP_SECURITY.LOW + await veBetterPassport.connect(owner).setAppSecurity(app1Id, 1) + + // Sets app2 security to APP_SECURITY.MEDIUM + await veBetterPassport.connect(owner).setAppSecurity(app2Id, 2) + + await veBetterPassport.connect(owner).registerActionForRound(enity1, app1Id, 1) + await veBetterPassport.connect(owner).registerActionForRound(enity1, app2Id, 1) + + await veBetterPassport.connect(owner).registerActionForRound(enity2, app1Id, 1) + await veBetterPassport.connect(owner).registerActionForRound(enity2, app2Id, 1) + + // Move through 1 round + await moveToCycle(2) + + // Entity 1 should have a score of 300 + expect(await veBetterPassport.userTotalScore(enity1)).to.equal(300) + expect(await veBetterPassport.userTotalScore(enity2)).to.equal(300) + + // Cumulative score for entity 1 should be 300 + expect(await veBetterPassport.getCumulativeScoreWithDecay(enity1, 1)).to.equal(300) + expect(await veBetterPassport.getCumulativeScoreWithDecay(enity2, 1)).to.equal(300) + + // Score threshold should be 500 + expect(await veBetterPassport.thresholdPoPScore()).to.equal(500) + + // Enable PoP score check + await veBetterPassport.connect(owner).toggleCheck(4) + expect(await veBetterPassport.isCheckEnabled(4)).to.be.true + + // Entity 1 should not be a person + expect(await veBetterPassport.isPerson(enity1.address)).to.deep.equal([ + false, + "User does not meet the criteria to be considered a person", + ]) + + // Entity 2 should not be a person + expect(await veBetterPassport.isPerson(enity2.address)).to.deep.equal([ + false, + "User does not meet the criteria to be considered a person", + ]) + + // Assign entity 1 to passport + await linkEntityToPassportWithSignature(veBetterPassport, passport, enity1, 1000) + + // Passport should not be a person + expect(await veBetterPassport.isPerson(passport.address)).to.deep.equal([ + false, + "User does not meet the criteria to be considered a person", + ]) + + // Assign entity 2 to passport + await linkEntityToPassportWithSignature(veBetterPassport, passport, enity2, 1000) + + // Passport should not be a person + expect(await veBetterPassport.isPerson(passport.address)).to.deep.equal([ + false, + "User does not meet the criteria to be considered a person", + ]) + + // Entity 1 should not be a person + expect(await veBetterPassport.isPerson(enity1.address)).to.deep.equal([ + false, + "User has delegated their personhood", + ]) + + // Entity 2 should not be a person + expect(await veBetterPassport.isPerson(enity2.address)).to.deep.equal([ + false, + "User has delegated their personhood", + ]) + + // Make entities interact with apps + await veBetterPassport.connect(owner).registerActionForRound(enity1, app1Id, 1) + await veBetterPassport.connect(owner).registerActionForRound(enity1, app2Id, 1) + await veBetterPassport.connect(owner).registerActionForRound(enity1, app2Id, 1) + await veBetterPassport.connect(owner).registerActionForRound(enity2, app1Id, 1) + await veBetterPassport.connect(owner).registerActionForRound(enity2, app2Id, 1) + await veBetterPassport.connect(owner).registerActionForRound(enity2, app2Id, 1) + await veBetterPassport.connect(owner).registerActionForRound(enity2, app2Id, 1) + + // Now passport should have enough score to be a person + expect(await veBetterPassport.isPerson(passport.address)).to.deep.equal([ + true, + "User's participation score is above the threshold", + ]) + }) + }) + + describe("Passport Delegation", function () { + it("Should be able to delegate personhood with signature", async function () { + const { + xAllocationVoting, + x2EarnApps, + otherAccounts, + owner, + veBetterPassport, + otherAccount: delegatee, + } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + // Bootstrap emissions + await bootstrapEmissions() + + otherAccounts.forEach(async account => { + await getVot3Tokens(account, "10000") + }) + await getVot3Tokens(delegatee, "10000") + await getVot3Tokens(owner, "10000") + + // Whitelist owner + await expect(veBetterPassport.connect(owner).whitelist(owner.address)) + .to.emit(veBetterPassport, "UserWhitelisted") + .withArgs(owner.address, owner.address) + await veBetterPassport.connect(owner).whitelist(otherAccounts[1].address) + + // Expect owner to be whitelisted + expect(await veBetterPassport.isWhitelisted(owner.address)).to.be.true + + // Enable whitelist check + await veBetterPassport.connect(owner).toggleCheck(1) + + // whitelist check should be enabled + expect(await veBetterPassport.isCheckEnabled(1)).to.be.true + + // expect owner to be person + expect(await veBetterPassport.isPerson(owner.address)).to.deep.equal([true, "User is whitelisted"]) + + //Add apps + const app1Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[2].address)) + const app2Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[3].address)) + const app3Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[4].address)) + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[2].address, otherAccounts[2].address, otherAccounts[2].address, "metadataURI") + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[3].address, otherAccounts[3].address, otherAccounts[3].address, "metadataURI") + + // await veBetterPassport.delegateWithSignature(other) + // Set up EIP-712 domain + const domain = { + name: "VeBetterPassport", + version: "1", + chainId: 1337, + verifyingContract: await veBetterPassport.getAddress(), + } + let types = { + Delegation: [ + { name: "delegator", type: "address" }, + { name: "delegatee", type: "address" }, + { name: "deadline", type: "uint256" }, + ], + } + + // Define a deadline timestamp + const currentBlock = await ethers.provider.getBlockNumber() + const block = await ethers.provider.getBlock(currentBlock) + + if (!block) { + throw new Error("Block not found") + } + + const deadline = block.timestamp + 3600 // 1 hour from + // Prepare the struct to sign + const delegationData = { + delegator: owner.address, + delegatee: delegatee.address, + deadline: deadline, + } + + // Create the EIP-712 signature for the delegator + const signature = await owner.signTypedData(domain, types, delegationData) + + // Perform the delegation using the signature + await expect(veBetterPassport.connect(delegatee).delegateWithSignature(owner.address, deadline, signature)) + .to.emit(veBetterPassport, "DelegationCreated") + .withArgs(owner.address, delegatee.address) + + // Verify that the delegatee has been assigned the delegator + const storedDelegatee = await veBetterPassport.getDelegatee(owner.address) + expect(storedDelegatee).to.equal(delegatee.address) + + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[4].address, otherAccounts[4].address, otherAccounts[4].address, "metadataURI") + + //Start allocation round + const round1 = await startNewAllocationRound() + // Vote + await xAllocationVoting + .connect(delegatee) + .castVote( + round1, + [app1Id, app2Id, app3Id], + [ethers.parseEther("0"), ethers.parseEther("900"), ethers.parseEther("100")], + ) + + // Otheraccounts[1] has not delegated his passport and can vote + await xAllocationVoting + .connect(otherAccounts[1]) + .castVote( + round1, + [app1Id, app2Id, app3Id], + [ethers.parseEther("0"), ethers.parseEther("900"), ethers.parseEther("100")], + ) + }) + + it("Should be able to delegate personhood", async function () { + const { + xAllocationVoting, + x2EarnApps, + otherAccounts, + owner, + veBetterPassport, + otherAccount: delegatee, + } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + // Bootstrap emissions + await bootstrapEmissions() + + otherAccounts.forEach(async account => { + await getVot3Tokens(account, "10000") + }) + await getVot3Tokens(delegatee, "10000") + await getVot3Tokens(owner, "10000") + + // Whitelist owner + await expect(veBetterPassport.connect(owner).whitelist(owner.address)) + .to.emit(veBetterPassport, "UserWhitelisted") + .withArgs(owner.address, owner.address) + await veBetterPassport.connect(owner).whitelist(otherAccounts[1].address) + + // Expect owner to be whitelisted + expect(await veBetterPassport.isWhitelisted(owner.address)).to.be.true + + // Enable whitelist check + await veBetterPassport.connect(owner).toggleCheck(1) + + // whitelist check should be enabled + expect(await veBetterPassport.isCheckEnabled(1)).to.be.true + + // expect owner to be person + expect(await veBetterPassport.isPerson(owner.address)).to.deep.equal([true, "User is whitelisted"]) + + //Add apps + const app1Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[2].address)) + const app2Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[3].address)) + const app3Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[4].address)) + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[2].address, otherAccounts[2].address, otherAccounts[2].address, "metadataURI") + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[3].address, otherAccounts[3].address, otherAccounts[3].address, "metadataURI") + + // delegate personhood + await expect(veBetterPassport.connect(owner).delegatePassport(delegatee.address)) + .to.emit(veBetterPassport, "DelegationPending") + .withArgs(owner.address, delegatee.address) + + // Check the pending delegation: 1 incoming and 0 outgoing + const pendingDelegation = await veBetterPassport.getPendingDelegations(delegatee.address) + expect(pendingDelegation).to.deep.equal([[owner.address], ZeroAddress]) + + // Check the pending delegation from delegator POV: 0 incoming and 1 outgoing + const pendingDelegationForDelegator = await veBetterPassport.getPendingDelegations(owner.address) + expect(pendingDelegationForDelegator).to.deep.equal([[], delegatee.address]) + + // Perform the delegation using the signature + await expect(veBetterPassport.connect(delegatee).acceptDelegation(owner.address)) + .to.emit(veBetterPassport, "DelegationCreated") + .withArgs(owner.address, delegatee.address) + + // Check the pending delegation + const pendingDelegation2 = await veBetterPassport.getPendingDelegations(delegatee.address) + expect(pendingDelegation2).to.deep.equal([[], ZeroAddress]) + + // Check the pending delegation from delegator POV + const pendingDelegationForDelegator2 = await veBetterPassport.getPendingDelegations(owner.address) + expect(pendingDelegationForDelegator2).to.deep.equal([[], ZeroAddress]) + + // Verify that the delegatee has been assigned the delegator + const storedDelegatee = await veBetterPassport.getDelegatee(owner.address) + expect(storedDelegatee).to.equal(delegatee.address) + + expect(await veBetterPassport.isDelegatee(delegatee.address)).to.be.true + + expect(await veBetterPassport.isDelegateeInTimepoint(delegatee.address, await ethers.provider.getBlockNumber())) + .to.be.true + + expect(await veBetterPassport.isDelegator(owner.address)).to.be.true + + expect(await veBetterPassport.isDelegatorInTimepoint(owner.address, await ethers.provider.getBlockNumber())).to.be + .true + + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[4].address, otherAccounts[4].address, otherAccounts[4].address, "metadataURI") + + //Start allocation round + const round1 = await startNewAllocationRound() + // Vote + await xAllocationVoting + .connect(delegatee) + .castVote( + round1, + [app1Id, app2Id, app3Id], + [ethers.parseEther("0"), ethers.parseEther("900"), ethers.parseEther("100")], + ) + + // Otheraccounts[1] has not delegated his passport and can vote + await xAllocationVoting + .connect(otherAccounts[1]) + .castVote( + round1, + [app1Id, app2Id, app3Id], + [ethers.parseEther("0"), ethers.parseEther("900"), ethers.parseEther("100")], + ) + }) + + it("Should be able to reject delegation request", async function () { + const { + xAllocationVoting, + x2EarnApps, + otherAccounts, + owner, + veBetterPassport, + otherAccount: delegatee, + } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + // Bootstrap emissions + await bootstrapEmissions() + + otherAccounts.forEach(async account => { + await getVot3Tokens(account, "10000") + }) + await getVot3Tokens(delegatee, "10000") + await getVot3Tokens(owner, "10000") + + // Whitelist owner + await expect(veBetterPassport.connect(owner).whitelist(owner.address)) + .to.emit(veBetterPassport, "UserWhitelisted") + .withArgs(owner.address, owner.address) + await veBetterPassport.connect(owner).whitelist(otherAccounts[1].address) + + // Expect owner to be whitelisted + expect(await veBetterPassport.isWhitelisted(owner.address)).to.be.true + + // Enable whitelist check + await veBetterPassport.connect(owner).toggleCheck(1) + + // whitelist check should be enabled + expect(await veBetterPassport.isCheckEnabled(1)).to.be.true + + // expect owner to be person + expect(await veBetterPassport.isPerson(owner.address)).to.deep.equal([true, "User is whitelisted"]) + + //Add apps + const app1Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[2].address)) + const app2Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[3].address)) + const app3Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[4].address)) + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[2].address, otherAccounts[2].address, otherAccounts[2].address, "metadataURI") + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[3].address, otherAccounts[3].address, otherAccounts[3].address, "metadataURI") + + // delegate personhood + await expect(veBetterPassport.connect(owner).delegatePassport(delegatee.address)) + .to.emit(veBetterPassport, "DelegationPending") + .withArgs(owner.address, delegatee.address) + + // Check the pending delegation + const pendingDelegation = await veBetterPassport.getPendingDelegations(delegatee.address) + expect(pendingDelegation).to.deep.equal([[owner.address], ZeroAddress]) + + // Check the pending delegation from delegator POV + const pendingDelegationForDelegator = await veBetterPassport.getPendingDelegations(owner.address) + expect(pendingDelegationForDelegator).to.deep.equal([[], delegatee.address]) + + // Perform the delegation using the signature + await expect(veBetterPassport.connect(delegatee).denyIncomingPendingDelegation(owner.address)) + .to.emit(veBetterPassport, "DelegationRevoked") + .withArgs(owner.address, delegatee.address) + + // Check the pending delegation + const pendingDelegation2 = await veBetterPassport.getPendingDelegations(delegatee.address) + expect(pendingDelegation2).to.deep.equal([[], ZeroAddress]) + + // Check the pending delegation from delegator POV + const pendingDelegationForDelegator2 = await veBetterPassport.getPendingDelegations(owner.address) + expect(pendingDelegationForDelegator2).to.deep.equal([[], ZeroAddress]) + + // Owner can vote + await expect( + xAllocationVoting + .connect(owner) + .castVote( + await startNewAllocationRound(), + [app1Id, app2Id, app3Id], + [ethers.parseEther("0"), ethers.parseEther("900"), ethers.parseEther("100")], + ), + ).to.be.reverted + }) + + it("Only the target delegatee can deny an incoming delegation request", async function () { + const { + otherAccounts, + owner, + veBetterPassport, + otherAccount: delegatee, + } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + const delegator = owner + + // Delegate to delegatee + await expect(veBetterPassport.connect(delegator).delegatePassport(delegatee.address)) + .to.emit(veBetterPassport, "DelegationPending") + .withArgs(delegator.address, delegatee.address) + + // Try to deny the link request + await expect( + veBetterPassport.connect(otherAccounts[1]).denyIncomingPendingDelegation(delegator.address), + ).to.be.revertedWithCustomError(veBetterPassport, "PassportDelegationUnauthorizedUser") + + // The target of the link should be able to deny the link request + await expect(veBetterPassport.connect(delegatee).denyIncomingPendingDelegation(delegator.address)) + .to.emit(veBetterPassport, "DelegationRevoked") + .withArgs(delegator.address, delegatee.address) + }) + + it("User with one incoming and one outgoing delegation should be able to cancel only one", async function () { + const { + otherAccounts, + owner, + veBetterPassport, + otherAccount: delegatee, + } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + // Use case: A has a pending delegation to B, and B has a pending delegation to C. + // B should be able to cancel only the pending delegation to C or from A. + const A = owner + const B = delegatee + const C = otherAccounts[0] + + // A delegate to B + await expect(veBetterPassport.connect(A).delegatePassport(B.address)) + .to.emit(veBetterPassport, "DelegationPending") + .withArgs(A.address, B.address) + + // B delegate to C + await expect(veBetterPassport.connect(B).delegatePassport(C.address)) + .to.emit(veBetterPassport, "DelegationPending") + .withArgs(B.address, C.address) + + // If we check now the pending delegations of A we should see 1 outgoing to B + const pendingDelegationsOfA = await veBetterPassport.getPendingDelegations(A.address) + expect(pendingDelegationsOfA).to.deep.equal([[], B.address]) + + // If we check now the pending delegations of B we should see 1 incoming from A and 1 outgoing to C + const pendingDelegationsOfB = await veBetterPassport.getPendingDelegations(B.address) + expect(pendingDelegationsOfB).to.deep.equal([[A.address], C.address]) + + // If we check now the pending delegations of C we should see 1 incoming from B + const pendingDelegationsOfC = await veBetterPassport.getPendingDelegations(C.address) + expect(pendingDelegationsOfC).to.deep.equal([[B.address], ZeroAddress]) + + // B should be able to cancel only the outgoing delegation to C + await veBetterPassport.connect(B).cancelOutgoingPendingDelegation() + // should still have 1 incoming and 0 outgoing + expect(await veBetterPassport.getPendingDelegations(B.address)).to.deep.equal([[A.address], ZeroAddress]) + + // Now we return to original simulation with B trying to delegate again to C + await expect(veBetterPassport.connect(B).delegatePassport(C.address)) + .to.emit(veBetterPassport, "DelegationPending") + .withArgs(B.address, C.address) + + // should have 1 incoming and 1 outgoing + expect(await veBetterPassport.getPendingDelegations(B.address)).to.deep.equal([[A.address], C.address]) + + // This time B wants to remove pending delegation from A + await veBetterPassport.connect(B).denyIncomingPendingDelegation(A.address) + // should have 0 incoming and 1 outgoing + expect(await veBetterPassport.getPendingDelegations(B.address)).to.deep.equal([[], C.address]) + + const pendingDelegationsOfB2 = await veBetterPassport.getPendingDelegations(B.address) + expect(pendingDelegationsOfB2).to.deep.equal([[], C.address]) + + // If C want to delegate to A, B should not be able to remove it + await expect(veBetterPassport.connect(C).delegatePassport(A.address)) + .to.emit(veBetterPassport, "DelegationPending") + .withArgs(C.address, A.address) + + await expect(veBetterPassport.connect(B).denyIncomingPendingDelegation(A.address)).to.be.reverted + }) + + it("Should revert if a user tries to cancel pending delegation thta does not exist", async function () { + const { owner: A, veBetterPassport } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + // A has not created a pending delegation + + // B should not be able to cancel the pending delegation from A + await expect(veBetterPassport.connect(A).cancelOutgoingPendingDelegation()).to.be.revertedWithCustomError( + veBetterPassport, + "NotDelegated", + ) + }) + + it("If A has a pending delegation to B, and B has a pending delegation to A, then A or B should be able to cancel only one", async function () { + const { + owner, + veBetterPassport, + otherAccount: delegatee, + } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + const A = owner + const B = delegatee + + // A delegate to B + await expect(veBetterPassport.connect(A).delegatePassport(B.address)) + .to.emit(veBetterPassport, "DelegationPending") + .withArgs(A.address, B.address) + + // B delegate to A + await expect(veBetterPassport.connect(B).delegatePassport(A.address)) + .to.emit(veBetterPassport, "DelegationPending") + .withArgs(B.address, A.address) + + // Get pending delegations of A + const pendingDelegationsOfA = await veBetterPassport.getPendingDelegations(A.address) + expect(pendingDelegationsOfA).to.deep.equal([[B.address], B.address]) + + // Get pending delegations of B + const pendingDelegationsOfB = await veBetterPassport.getPendingDelegations(B.address) + expect(pendingDelegationsOfB).to.deep.equal([[A.address], A.address]) + + // A should be able to cancel only the pending delegation to B + await veBetterPassport.connect(A).cancelOutgoingPendingDelegation() + // should still have 1 incoming and 0 outgoing + expect(await veBetterPassport.getPendingDelegations(A.address)).to.deep.equal([[B.address], ZeroAddress]) + + // Now we return to original simulation with A trying to delegate to B + await expect(veBetterPassport.connect(A).delegatePassport(B.address)) + .to.emit(veBetterPassport, "DelegationPending") + .withArgs(A.address, B.address) + + // should have 1 incoming and 1 outgoing + expect(await veBetterPassport.getPendingDelegations(B.address)).to.deep.equal([[A.address], A.address]) + + // This time A wants to remove the incoming delegation from B + await veBetterPassport.connect(A).denyIncomingPendingDelegation(B.address) + // should have 0 incoming and 1 outgoing + expect(await veBetterPassport.getPendingDelegations(A.address)).to.deep.equal([[], B.address]) + }) + + it("If A delegates to B, and C delegates to B (both pending), then B should be able to cancel only one", async function () { + const { + owner, + veBetterPassport, + otherAccount: delegatee, + otherAccounts, + } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + const A = owner + const B = delegatee + const C = otherAccounts[0] + + // A delegate to B + await expect(veBetterPassport.connect(A).delegatePassport(B.address)) + .to.emit(veBetterPassport, "DelegationPending") + .withArgs(A.address, B.address) + + // C delegate to B + await expect(veBetterPassport.connect(C).delegatePassport(B.address)) + .to.emit(veBetterPassport, "DelegationPending") + .withArgs(C.address, B.address) + + // Get pending delegations of B: should have 2 incoming delegations from A and C, and 0 outgoing + const pendingDelegationsOfB = await veBetterPassport.getPendingDelegations(B.address) + expect(pendingDelegationsOfB).to.deep.equal([[A.address, C.address], ZeroAddress]) + + // B should be able to cancel only the pending delegation from A + await veBetterPassport.connect(B).denyIncomingPendingDelegation(A.address) + // should still have 1 incoming and 0 outgoing + expect(await veBetterPassport.getPendingDelegations(B.address)).to.deep.equal([[C.address], ZeroAddress]) + }) + + it("Should not be able to vote if delegating and not delegatee with allocation voting", async function () { + const { + xAllocationVoting, + x2EarnApps, + otherAccounts, + owner, + veBetterPassport, + otherAccount: delegatee, + } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + // Bootstrap emissions + await bootstrapEmissions() + + await getVot3Tokens(delegatee, "10000") + await getVot3Tokens(owner, "10000") + + // Whitelist owner + await veBetterPassport.connect(owner).whitelist(owner.address) + + // Enable whitelist check + await veBetterPassport.connect(owner).toggleCheck(1) + + // whitelist check should be enabled + expect(await veBetterPassport.isCheckEnabled(1)).to.be.true + + // expect owner to be person + expect(await veBetterPassport.isPerson(owner.address)).to.deep.equal([true, "User is whitelisted"]) + + //Add apps + const app1Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[2].address)) + const app2Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[3].address)) + const app3Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[4].address)) + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[2].address, otherAccounts[2].address, otherAccounts[2].address, "metadataURI") + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[3].address, otherAccounts[3].address, otherAccounts[3].address, "metadataURI") + + // delegate with signature + await delegateWithSignature(veBetterPassport, owner, delegatee, 3600) + + // Verify that the delegatee has been assigned the delegator + const storedDelegatee = await veBetterPassport.getDelegatee(owner.address) + expect(storedDelegatee).to.equal(delegatee.address) + expect(await veBetterPassport.getDelegator(delegatee.address)).to.equal(owner.address) + + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[4].address, otherAccounts[4].address, otherAccounts[4].address, "metadataURI") + + //Start allocation round + const round1 = await startNewAllocationRound() + + // Vote + await xAllocationVoting + .connect(delegatee) + .castVote( + round1, + [app1Id, app2Id, app3Id], + [ethers.parseEther("0"), ethers.parseEther("900"), ethers.parseEther("100")], + ) + + // Owner has delegated his passport and has no delegation to him (is not a delegatee) => owner can't vote + await expect( + xAllocationVoting + .connect(owner) + .castVote( + round1, + [app1Id, app2Id, app3Id], + [ethers.parseEther("0"), ethers.parseEther("900"), ethers.parseEther("100")], + ), + ).to.be.revertedWithCustomError(xAllocationVoting, "GovernorPersonhoodVerificationFailed") + }) + + it("Should not be able to vote if delegating and not delegatee with governor", async function () { + const { + governor, + b3tr, + B3trContract, + owner, + veBetterPassport, + otherAccount: delegatee, + } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + await getVot3Tokens(delegatee, "10000") + await getVot3Tokens(owner, "10000") + + // Start emissions + await bootstrapAndStartEmissions() + + // Whitelist owner + await veBetterPassport.connect(owner).whitelist(owner.address) + + // Enable whitelist check + await veBetterPassport.connect(owner).toggleCheck(1) + + // whitelist check should be enabled + expect(await veBetterPassport.isCheckEnabled(1)).to.be.true + + // expect owner to be person + expect(await veBetterPassport.isPerson(owner.address)).to.deep.equal([true, "User is whitelisted"]) + + // create a new proposal + const tx = await createProposal(b3tr, B3trContract, owner, "Get b3tr token details", "tokenDetails", []) + + const proposalId = await getProposalIdFromTx(tx) + + // pay deposit + await payDeposit(proposalId.toString(), owner) + + // Define a deadline timestamp + const time = Date.now() + const deadline = time + 3600 // 1 hour from now -> change from ms to s + + // delegate with signature + await delegateWithSignature(veBetterPassport, owner, delegatee, deadline) + + // wait + await waitForProposalToBeActive(proposalId) + + // Delegatee votes + await governor.connect(delegatee).castVote(proposalId, 2) // vote abstain + + await expect(governor.connect(owner).castVote(proposalId, 2)).to.be.revertedWithCustomError( + governor, + "GovernorPersonhoodVerificationFailed", + ) + }) + + it("should revert if a user tries to accept a pending delegation that does not exist", async function () { + const { owner: A, veBetterPassport } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + // A has not created a pending delegation + + // B should not be able to accept the pending delegation from A + await expect(veBetterPassport.connect(A).acceptDelegation(A.address)).to.be.revertedWithCustomError( + veBetterPassport, + "NotDelegated", + ) + }) + + it("should revert if a user tries to accept a pending delegation that is a not a delagatee", async function () { + const { + owner: A, + veBetterPassport, + otherAccounts, + } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + const B = otherAccounts[0] + const C = otherAccounts[1] + + // A has requested to delegate there passport to C + await veBetterPassport.connect(A).delegatePassport(C.address) + + // B should not be able to accept the pending delegation from A + await expect(veBetterPassport.connect(B).acceptDelegation(A.address)).to.be.revertedWithCustomError( + veBetterPassport, + "PassportDelegationUnauthorizedUser", + ) + }) + + it("should revert if a user tries to delegate a passport to themselves", async function () { + const { owner: A, veBetterPassport } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + // A should not be able to delegate there passport to themselves + await expect(veBetterPassport.connect(A).delegatePassport(A.address)).to.be.revertedWithCustomError( + veBetterPassport, + "CannotDelegateToSelf", + ) + }) + + it("If already a delagatee it should remove mapping of old delegation and accept new", async function () { + const { + owner: A, + veBetterPassport, + otherAccounts, + } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + const B = otherAccounts[0] + const C = otherAccounts[1] + + // A has delegated there passport to C + await delegateWithSignature(veBetterPassport, A, C, 3600) + + // C should be a delegatee of A + expect(await veBetterPassport.getDelegatee(A.address)).to.equal(C.address) + expect(await veBetterPassport.getDelegator(C.address)).to.equal(A.address) + + // B requests to delegate there passport to C + await veBetterPassport.connect(B).delegatePassport(C.address) + + // C should be pending delegatee of B + expect(await veBetterPassport.getPendingDelegations(C.address)).to.deep.equal([[B.address], ZeroAddress]) + + // C accepts the delegation + await veBetterPassport.connect(C).acceptDelegation(B.address) + + // C should be a delegatee of B + expect(await veBetterPassport.getDelegatee(B.address)).to.equal(C.address) + expect(await veBetterPassport.getDelegator(C.address)).to.equal(B.address) + + // A should no longer be a delegatee of C + expect(await veBetterPassport.getDelegatee(A.address)).to.equal(ZeroAddress) + }) + + it("If delegator is already a delegator remove mapping", async function () { + const { + owner: A, + veBetterPassport, + otherAccounts, + } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + const B = otherAccounts[0] + const C = otherAccounts[1] + + // A has delegated there passport to C + await delegateWithSignature(veBetterPassport, A, C, 3600) + + // C should be a delegatee of A + expect(await veBetterPassport.getDelegatee(A.address)).to.equal(C.address) + expect(await veBetterPassport.getDelegator(C.address)).to.equal(A.address) + + // A requests to delegate there passport to B + await veBetterPassport.connect(A).delegatePassport(B.address) + + // B should be pending delegatee of A + expect(await veBetterPassport.getPendingDelegations(B.address)).to.deep.equal([[A.address], ZeroAddress]) + + // C should no longer be a delegatee of A + expect(await veBetterPassport.getDelegatee(A.address)).to.equal(ZeroAddress) + expect(await veBetterPassport.getDelegator(C.address)).to.equal(ZeroAddress) + + // A requests to delegate there passport to C + await veBetterPassport.connect(A).delegatePassport(C.address) + + // B should no longer be a pending delegatee of A + expect(await veBetterPassport.getPendingDelegations(B.address)).to.deep.equal([[], ZeroAddress]) + + // C should be a pending delegatee of A + expect(await veBetterPassport.getPendingDelegations(C.address)).to.deep.equal([[A.address], ZeroAddress]) + }) + + it("If a delegator delegating with signature is already a delegator remove mapping", async function () { + const { + owner: A, + veBetterPassport, + otherAccounts, + } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + const B = otherAccounts[0] + const C = otherAccounts[1] + + // A has delegated there passport to C + await delegateWithSignature(veBetterPassport, A, C, 3600) + + // C should be a delegatee of A + expect(await veBetterPassport.getDelegatee(A.address)).to.equal(C.address) + expect(await veBetterPassport.getDelegator(C.address)).to.equal(A.address) + + // A now delegates to B + await delegateWithSignature(veBetterPassport, A, B, 3600) + + // C should no longer be a delegatee of A + expect(await veBetterPassport.getDelegator(C.address)).to.equal(ZeroAddress) + + // B should be a delegatee of A + expect(await veBetterPassport.getDelegatee(A.address)).to.equal(B.address) + + // B should be a delegator of A + expect(await veBetterPassport.getDelegator(B.address)).to.equal(A.address) + + // A now requests to delegate there passport to C + await veBetterPassport.connect(A).delegatePassport(C.address) + + // B should no longer be a delegatee of A + expect(await veBetterPassport.getDelegatee(A.address)).to.equal(ZeroAddress) + expect(await veBetterPassport.getPendingDelegations(A.address)).to.deep.equal([[], C.address]) + + // C should be a pending delegatee of A + expect(await veBetterPassport.getPendingDelegations(C.address)).to.deep.equal([[A.address], ZeroAddress]) + + // A decided to delegate with signature to B + await delegateWithSignature(veBetterPassport, A, B, 3600) + + // C should no longer be a pending delegatee of A + expect(await veBetterPassport.getPendingDelegations(C.address)).to.deep.equal([[], ZeroAddress]) + }) + + // If X delegates to Y, X can't vote. If Z delegates to X, X now can vote as he's a delegatee of Z. + it("Should be able to vote if delegatee and delegator with governor", async function () { + const { + governor, + b3tr, + B3trContract, + owner: X, + veBetterPassport, + otherAccount: Y, + otherAccounts, + } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + const Z = otherAccounts[0] + + await getVot3Tokens(X, "10000") + await getVot3Tokens(Y, "10000") + await getVot3Tokens(Z, "10000") + + // Start emissions + await bootstrapAndStartEmissions() + + // Whitelist owner + await veBetterPassport.connect(X).whitelist(X.address) + await veBetterPassport.connect(X).whitelist(Z.address) + + // Enable whitelist check + await veBetterPassport.connect(X).toggleCheck(1) + + // whitelist check should be enabled + expect(await veBetterPassport.isCheckEnabled(1)).to.be.true + + // expect owner to be person + expect(await veBetterPassport.isPerson(X.address)).to.deep.equal([true, "User is whitelisted"]) + + // create a new proposal + const tx = await createProposal(b3tr, B3trContract, X, "Get b3tr token details", "tokenDetails", []) + + const proposalId = await getProposalIdFromTx(tx) + + // pay deposit + await payDeposit(proposalId.toString(), X) + + // Define a deadline timestamp + const time = Date.now() + const deadline = time + 3600 // 1 hour from now -> change from ms to s + + // delegate with signature X to Y + await delegateWithSignature(veBetterPassport, X, Y, deadline) + + // delegate with signature Z to X + await delegateWithSignature(veBetterPassport, Z, X, deadline) + + // wait for proposal + await waitForProposalToBeActive(proposalId) + + // Y can vote + await governor.connect(Y).castVote(proposalId, 2) // vote abstain + + // X can vote even if he's a delegator to Y because X is delegatee of Z + await governor.connect(X).castVote(proposalId, 2) // vote abstain + + // Z is delegator of X and has no delegators for him. Thus he can't vote. + await expect(governor.connect(Z).castVote(proposalId, 2)).to.be.revertedWithCustomError( + governor, + "GovernorPersonhoodVerificationFailed", + ) + }) + + it("User A should be able to delegate passport A to B, user B should be able to delegate passport B to C and user C should be able to delegate passport C to A", async function () { + const { governor, b3tr, B3trContract, owner, veBetterPassport, otherAccounts } = + await getOrDeployContractInstances({ + forceDeploy: true, + }) + + const A = otherAccounts[0] + const B = otherAccounts[1] + const C = otherAccounts[2] + + await getVot3Tokens(owner, "10000") + await getVot3Tokens(A, "10000") + await getVot3Tokens(B, "10000") + await getVot3Tokens(C, "10000") + + // Start emissions + await bootstrapAndStartEmissions() + + // Whitelist all passport owners + await veBetterPassport.connect(owner).whitelist(A.address) + await veBetterPassport.connect(owner).whitelist(B.address) + await veBetterPassport.connect(owner).whitelist(C.address) + + // Enable whitelist check + await veBetterPassport.connect(owner).toggleCheck(1) + + // whitelist check should be enabled + expect(await veBetterPassport.isCheckEnabled(1)).to.be.true + + // expect all passports to be a person as whitelist is enabled + expect(await veBetterPassport.isPerson(A.address)).to.deep.equal([true, "User is whitelisted"]) + expect(await veBetterPassport.isPerson(B.address)).to.deep.equal([true, "User is whitelisted"]) + expect(await veBetterPassport.isPerson(C.address)).to.deep.equal([true, "User is whitelisted"]) + + // create a new proposal + const tx = await createProposal(b3tr, B3trContract, owner, "Get b3tr token details", "tokenDetails", []) + + const proposalId = await getProposalIdFromTx(tx) + + // pay deposit + await payDeposit(proposalId.toString(), owner) + + // delegate with signature A to B + await veBetterPassport.connect(A).delegatePassport(B.address) + await veBetterPassport.connect(B).acceptDelegation(A.address) + + // delegate with signature B to C + await veBetterPassport.connect(B).delegatePassport(C.address) + await veBetterPassport.connect(C).acceptDelegation(B.address) + + // delegate with signature C to A + await veBetterPassport.connect(C).delegatePassport(A.address) + await veBetterPassport.connect(A).acceptDelegation(C.address) + + // wait for proposal + await waitForProposalToBeActive(proposalId) + + // A can vote + await governor.connect(A).castVote(proposalId, 2) // vote abstain + + // B can vote + await governor.connect(B).castVote(proposalId, 2) // vote abstain + + // C can vote + await governor.connect(C).castVote(proposalId, 2) // vote abstain + }) + + it("User A should be able to delegate passport A to B, user B should be able to delegate passport B to C and user C should be able to delegate passport C to A, score threshold 0", async function () { + const { governor, b3tr, B3trContract, owner, veBetterPassport, otherAccounts } = + await getOrDeployContractInstances({ + forceDeploy: true, + }) + + const A = otherAccounts[0] + const B = otherAccounts[1] + const C = otherAccounts[2] + + await getVot3Tokens(owner, "10000") + await getVot3Tokens(A, "10000") + await getVot3Tokens(B, "10000") + await getVot3Tokens(C, "10000") + + // Start emissions + await bootstrapAndStartEmissions() + + // Enable score check + await veBetterPassport.connect(owner).toggleCheck(4) + + // score check should be enabled + expect(await veBetterPassport.isCheckEnabled(4)).to.be.true + + // Expect score threshold to be 0 + expect(await veBetterPassport.thresholdPoPScore()).to.equal(0) + + // expect all passports to be a person as score is enabled + expect(await veBetterPassport.isPerson(A.address)).to.deep.equal([ + true, + "User's participation score is above the threshold", + ]) + expect(await veBetterPassport.isPerson(B.address)).to.deep.equal([ + true, + "User's participation score is above the threshold", + ]) + expect(await veBetterPassport.isPerson(C.address)).to.deep.equal([ + true, + "User's participation score is above the threshold", + ]) + + // create a new proposal + const tx = await createProposal(b3tr, B3trContract, owner, "Get b3tr token details", "tokenDetails", []) + + const proposalId = await getProposalIdFromTx(tx) + + // pay deposit + await payDeposit(proposalId.toString(), owner) + + // Define a deadline timestamp + const time = Date.now() + const deadline = time + 3600 // 1 hour from now -> change from ms to s + + // delegate with signature A to B + await delegateWithSignature(veBetterPassport, A, B, deadline) + + // delegate with signature B to C + await delegateWithSignature(veBetterPassport, B, C, deadline) + + // delegate with signature C to A + await delegateWithSignature(veBetterPassport, C, A, deadline) + + // wait for proposal + await waitForProposalToBeActive(proposalId) + + // A can vote + await governor.connect(A).castVote(proposalId, 2) // vote abstain + + // B can vote + await governor.connect(B).castVote(proposalId, 2) // vote abstain + + // C can vote + await governor.connect(C).castVote(proposalId, 2) // vote abstain + }) + + it("When voting delegation must have been done before the start of the proposal", async function () { + const { + governor, + b3tr, + B3trContract, + owner: X, + veBetterPassport, + otherAccount: Y, + otherAccounts, + } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + const Z = otherAccounts[0] + + await getVot3Tokens(X, "10000") + await getVot3Tokens(Y, "10000") + await getVot3Tokens(Z, "10000") + + // Start emissions + await bootstrapAndStartEmissions() + + // Whitelist owner + await veBetterPassport.connect(X).whitelist(X.address) + await veBetterPassport.connect(X).whitelist(Z.address) + + // Enable whitelist check + await veBetterPassport.connect(X).toggleCheck(1) + + // whitelist check should be enabled + expect(await veBetterPassport.isCheckEnabled(1)).to.be.true + + // expect owner to be person + expect(await veBetterPassport.isPerson(X.address)).to.deep.equal([true, "User is whitelisted"]) + + // create a new proposal + const tx = await createProposal(b3tr, B3trContract, X, "Get b3tr token details", "tokenDetails", []) + + const proposalId = await getProposalIdFromTx(tx) + + // pay deposit + await payDeposit(proposalId.toString(), X) + + // Define a deadline timestamp + const time = Date.now() + const deadline = time + 3600 // 1 hour from now -> change from ms to s + + // wait for proposal + await waitForProposalToBeActive(proposalId) + + // delegate with signature X to Y + await delegateWithSignature(veBetterPassport, X, Y, deadline) + + // Y cannot vote because he is not human since delegation was done after proposal was started + await expect(governor.connect(Y).castVote(proposalId, 2)).to.be.reverted + }) + + it("Should not be able to delegate to self", async function () { + const { veBetterPassport, owner } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + await expect(delegateWithSignature(veBetterPassport, owner, owner, 3600)).to.be.reverted + }) + + it("Should not be able to revoke delegation if not delegated", async function () { + const { veBetterPassport } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + await expect(veBetterPassport.revokeDelegation()).to.be.reverted + }) + + it("Should not be able to delegate with signature if signature expired", async function () { + const { veBetterPassport, owner, otherAccount } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + await expect(delegateWithSignature(veBetterPassport, owner, otherAccount, 0)).to.be.revertedWithCustomError( + veBetterPassport, + "SignatureExpired", + ) + }) + + it("Should not be able to delegate with invalid singature", async function () { + const { veBetterPassport, owner, otherAccount } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + // Set up EIP-712 domain + const domain = { + name: "PassportDelegation", + version: "1", + chainId: 1337, + verifyingContract: await veBetterPassport.getAddress(), + } + let types = { + Delegation: [ + { name: "wrong_field_1", type: "address" }, + { name: "wrong_field_2", type: "address" }, + { name: "wrong_field_3", type: "address" }, + ], + } + + // Define a deadline timestamp + const currentBlock = await ethers.provider.getBlockNumber() + const block = await ethers.provider.getBlock(currentBlock) + + if (!block) { + throw new Error("Block not found") + } + + const deadline = block.timestamp + 3600 // 1 hour from + + // Prepare the struct to sign + const delegationData = { + wrong_field_1: owner.address, + wrong_field_2: otherAccount.address, + wrong_field_3: otherAccount.address, + } + + // Create the EIP-712 signature for the delegator + const signature = await owner.signTypedData(domain, types, delegationData) + + // Perform the delegation using the signature + await expect( + veBetterPassport.connect(otherAccount).delegateWithSignature(owner.address, deadline, signature), + ).to.be.revertedWithCustomError(veBetterPassport, "InvalidSignature") + }) + + it("If a delegator re-delegates its passport or delegatee accepts delegation of new passport it should update mappings", async function () { + const { veBetterPassport, owner, otherAccount, otherAccounts } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + await delegateWithSignature(veBetterPassport, owner, otherAccount, 3600) + + expect(await veBetterPassport.getDelegatee(owner.address)).to.equal(otherAccount.address) + expect(await veBetterPassport.getDelegator(otherAccount.address)).to.equal(owner.address) + + await delegateWithSignature(veBetterPassport, otherAccounts[0], otherAccount, 3600) + + expect(await veBetterPassport.getDelegatee(owner.address)).to.equal(ethers.ZeroAddress) + expect(await veBetterPassport.getDelegator(otherAccount.address)).to.equal(otherAccounts[0].address) + expect(await veBetterPassport.getDelegatee(otherAccounts[0].address)).to.equal(otherAccount.address) + }) + + it("Should be able to revoke delegation as delegatee", async function () { + const { + veBetterPassport, + owner: delegator, + otherAccount: delegatee, + } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + await delegateWithSignature(veBetterPassport, delegator, delegatee, 3600) + const block = await ethers.provider.getBlockNumber() + + await expect(veBetterPassport.connect(delegatee).revokeDelegation()).to.emit( + veBetterPassport, + "DelegationRevoked", + ) + expect(await veBetterPassport.getDelegatee(delegator.address)).to.equal(ethers.ZeroAddress) + expect(await veBetterPassport.getDelegator(delegatee.address)).to.equal(ethers.ZeroAddress) + + expect(await veBetterPassport.getDelegatorInTimepoint(delegatee.address, block)).to.equal(delegator.address) + + expect( + await veBetterPassport.getDelegatorInTimepoint(delegatee.address, await ethers.provider.getBlockNumber()), + ).to.equal(ZeroAddress) + + expect(await veBetterPassport.getDelegateeInTimepoint(delegator.address, block)).to.equal(delegatee.address) + }) + + it("Should be able to revoke delegation as delegator", async function () { + const { veBetterPassport, owner, otherAccount } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + await delegateWithSignature(veBetterPassport, owner, otherAccount, 3600) + + await expect(veBetterPassport.revokeDelegation()).to.emit(veBetterPassport, "DelegationRevoked") + expect(await veBetterPassport.getDelegatee(owner.address)).to.equal(ethers.ZeroAddress) + expect(await veBetterPassport.getDelegator(otherAccount.address)).to.equal(ethers.ZeroAddress) + }) + + it("Should not be able to revoke delegation if not delegator nor delegatee", async function () { + const { veBetterPassport, owner, otherAccount, otherAccounts } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + await delegateWithSignature(veBetterPassport, owner, otherAccount, 3600) + + await expect(veBetterPassport.connect(otherAccounts[0]).revokeDelegation()).to.be.reverted + expect(await veBetterPassport.getDelegatee(owner.address)).to.equal(otherAccount.address) + expect(await veBetterPassport.getDelegator(otherAccount.address)).to.equal(owner.address) + }) + + it("An entity should not be able to delegate a passport to a user", async function () { + const { + veBetterPassport, + owner: passport, + otherAccount: entity, + otherAccounts, + } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + const delegatee = otherAccounts[0] + + await linkEntityToPassportWithSignature(veBetterPassport, passport, entity, 1000) + + expect(await veBetterPassport.isEntity(entity.address)).to.be.true + + // Should not be able to delegate an entity + await expect(delegateWithSignature(veBetterPassport, entity, delegatee, 3600)).to.be.revertedWithCustomError( + veBetterPassport, + "PassportDelegationFromEntity", + ) + + // Should not be able to delegate an entity + await expect(veBetterPassport.connect(entity).delegatePassport(delegatee.address)).to.be.revertedWithCustomError( + veBetterPassport, + "PassportDelegationFromEntity", + ) + + // detach entity + await veBetterPassport.connect(passport).removeEntityLink(entity) + + // Should be able to delegate + await expect(delegateWithSignature(veBetterPassport, entity, delegatee, 3600)).to.not.be.reverted + }) + + it("A passport cannot be delegated to an entity", async function () { + const { + veBetterPassport, + owner: passport, + otherAccount: entity, + otherAccounts, + } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + const passport2 = otherAccounts[1] + const entity2 = otherAccounts[2] + + await linkEntityToPassportWithSignature(veBetterPassport, passport, entity, 1000) + await linkEntityToPassportWithSignature(veBetterPassport, passport2, entity2, 1000) + + expect(await veBetterPassport.isEntity(entity.address)).to.be.true + + // Should not be able to delegate a passport + await expect(delegateWithSignature(veBetterPassport, passport, entity2, 3600)).to.be.revertedWithCustomError( + veBetterPassport, + "PassportDelegationToEntity", + ) + + // Should not be able to delegate a passport + await expect(veBetterPassport.connect(passport).delegatePassport(entity2.address)).to.be.revertedWithCustomError( + veBetterPassport, + "PassportDelegationToEntity", + ) + + // detach entity + await veBetterPassport.connect(passport2).removeEntityLink(entity2) + + // Should be able to delegate + await expect(delegateWithSignature(veBetterPassport, passport, entity2, 3600)).to.not.be.reverted + }) + + it("should revert if a wallet not linked to a passport tries to be removed", async function () { + const { + veBetterPassport, + owner: user, + otherAccount: randomWallet, + } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + await expect(veBetterPassport.connect(user).removeEntityLink(randomWallet)).to.be.revertedWithCustomError( + veBetterPassport, + "NotLinked", + ) + }) + + it("Should be able to assign multiple entites to a passport, do actions and use the combintation to meet personhood status", async function () { + const config = createTestConfig() + const { + veBetterPassport, + x2EarnApps, + owner, + otherAccount: delegatee, + otherAccounts, + } = await getOrDeployContractInstances({ + forceDeploy: true, + config, + }) + + const enity1 = otherAccounts[0] + const enity2 = otherAccounts[1] + const passport = otherAccounts[2] + + // Set the score threshold to 500 + await veBetterPassport.connect(owner).setThresholdPoPScore(500) + + // Bootstrap emissions + await bootstrapAndStartEmissions() + + //Add apps + const app1Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[2].address)) + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[2].address, otherAccounts[2].address, otherAccounts[2].address, "metadataURI") + + const app2Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[3].address)) + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[3].address, otherAccounts[3].address, otherAccounts[3].address, "metadataURI") + + await veBetterPassport.grantRole(await veBetterPassport.ACTION_SCORE_MANAGER_ROLE(), owner) + await veBetterPassport.grantRole(await veBetterPassport.ACTION_REGISTRAR_ROLE(), owner) + + expect(await veBetterPassport.hasRole(await veBetterPassport.ACTION_SCORE_MANAGER_ROLE(), owner.address)).to.be + .true + expect(await veBetterPassport.hasRole(await veBetterPassport.ACTION_REGISTRAR_ROLE(), owner.address)).to.be.true + + // Sets app1 security to APP_SECURITY.LOW + await veBetterPassport.connect(owner).setAppSecurity(app1Id, 1) + + // Sets app2 security to APP_SECURITY.MEDIUM + await veBetterPassport.connect(owner).setAppSecurity(app2Id, 2) + + await veBetterPassport.connect(owner).registerActionForRound(enity1, app1Id, 1) + await veBetterPassport.connect(owner).registerActionForRound(enity1, app2Id, 1) + + await veBetterPassport.connect(owner).registerActionForRound(enity2, app1Id, 1) + await veBetterPassport.connect(owner).registerActionForRound(enity2, app2Id, 1) + + // Move through 1 round + await moveToCycle(2) + + // Entity 1 should have a score of 300 + expect(await veBetterPassport.userTotalScore(enity1)).to.equal(300) + expect(await veBetterPassport.userTotalScore(enity2)).to.equal(300) + + // Cumulative score for entity 1 should be 300 + expect(await veBetterPassport.getCumulativeScoreWithDecay(enity1, 1)).to.equal(300) + expect(await veBetterPassport.getCumulativeScoreWithDecay(enity2, 1)).to.equal(300) + + // Score threshold should be 500 + expect(await veBetterPassport.thresholdPoPScore()).to.equal(500) + + // Enable PoP score check + await veBetterPassport.connect(owner).toggleCheck(4) + expect(await veBetterPassport.isCheckEnabled(4)).to.be.true + + // Entity 1 should not be a person + expect(await veBetterPassport.isPerson(enity1.address)).to.deep.equal([ + false, + "User does not meet the criteria to be considered a person", + ]) + + // Entity 2 should not be a person + expect(await veBetterPassport.isPerson(enity2.address)).to.deep.equal([ + false, + "User does not meet the criteria to be considered a person", + ]) + + // Assign entity 1 to passport + await linkEntityToPassportWithSignature(veBetterPassport, passport, enity1, 1000) + + // Passport should not be a person + expect(await veBetterPassport.isPerson(passport.address)).to.deep.equal([ + false, + "User does not meet the criteria to be considered a person", + ]) + + // Assign entity 2 to passport + await linkEntityToPassportWithSignature(veBetterPassport, passport, enity2, 1000) + + // Passport should not be a person + expect(await veBetterPassport.isPerson(passport.address)).to.deep.equal([ + false, + "User does not meet the criteria to be considered a person", + ]) + + // Now we do actions with enities 1 and 2 to make the passport a person + await veBetterPassport.connect(owner).registerActionForRound(passport, app1Id, 1) + await veBetterPassport.connect(owner).registerActionForRound(enity1, app2Id, 1) + await veBetterPassport.connect(owner).registerActionForRound(enity2, app2Id, 1) + + // Passport should not be a person + expect((await veBetterPassport.isPerson(passport.address))[0]).to.equal(true) + + // Delegate is not a person + expect(await veBetterPassport.isPerson(delegatee.address)).to.deep.equal([ + false, + "User does not meet the criteria to be considered a person", + ]) + + // Delegate passport to delegatee + await delegateWithSignature(veBetterPassport, passport, delegatee, 3600) + + // Delegatee should be a person + expect(await veBetterPassport.isPerson(delegatee.address)).to.deep.equal([ + true, + "User's participation score is above the threshold", + ]) + + // Passport should not be a person + expect(await veBetterPassport.isPerson(passport.address)).to.deep.equal([ + false, + "User has delegated their personhood", + ]) + }) + + it("A user can have maximum on passport delegated to him per time", async function () { + const { veBetterPassport, owner, otherAccount, otherAccounts } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + const owner2 = otherAccounts[0] + + await delegateWithSignature(veBetterPassport, owner, otherAccount, 3600) + expect(await veBetterPassport.getDelegatee(owner.address)).to.equal(otherAccount.address) + expect(await veBetterPassport.getDelegator(otherAccount.address)).to.equal(owner.address) + + await delegateWithSignature(veBetterPassport, owner2, otherAccount, 3600) + // now that owner2 has delegetated to otherAccount, otherAccount should be delegatee of owner2 + expect(await veBetterPassport.getDelegatee(owner2.address)).to.equal(otherAccount.address) + expect(await veBetterPassport.getDelegator(otherAccount.address)).to.equal(owner2.address) + }) + + it("After linking an entity to a passport, the entity should non be able to vote", async function () { + const { veBetterPassport, xAllocationVoting, x2EarnApps, owner, otherAccount, otherAccounts } = + await getOrDeployContractInstances({ + forceDeploy: true, + }) + + // Bootstrap emissions + await bootstrapEmissions() + + await getVot3Tokens(otherAccount, "10000") + await getVot3Tokens(owner, "10000") + + //Add apps + const app1Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[2].address)) + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[2].address, otherAccounts[2].address, otherAccounts[2].address, "metadataURI") + + await veBetterPassport.setAppSecurity(app1Id, 3) // APP_SECURITY.HIGH + await veBetterPassport.connect(owner).toggleCheck(4) // Enable PoP score check + await veBetterPassport.connect(owner).setThresholdPoPScore(200) + + //Start allocation round + const round1 = await startNewAllocationRound() + + await veBetterPassport.connect(owner).registerActionForRound(otherAccount, app1Id, 1) + await veBetterPassport.connect(owner).registerActionForRound(otherAccount, app1Id, 1) + await veBetterPassport.connect(owner).registerActionForRound(otherAccount, app1Id, 1) + await veBetterPassport.connect(owner).registerActionForRound(otherAccount, app1Id, 1) + await veBetterPassport.connect(owner).registerActionForRound(otherAccount, app1Id, 1) + + // cumulative score of otherAccount should be 2000 + expect(await veBetterPassport.getCumulativeScoreWithDecay(otherAccount, 1)).to.be.equal(2000) + // cumulative score of owner should be 0 + expect(await veBetterPassport.getCumulativeScoreWithDecay(owner, 1)).to.be.equal(0) + + // Both should be considered passports at the start of the round + expect( + await veBetterPassport.isPassportInTimepoint(owner.address, await xAllocationVoting.currentRoundSnapshot()), + ).to.be.true + expect( + await veBetterPassport.isPassportInTimepoint( + otherAccount.address, + await xAllocationVoting.currentRoundSnapshot(), + ), + ).to.be.true + + // Now we link the entity to the passport; for voting this shoould have effect from next round, but for actions it should be immediate + await linkEntityToPassportWithSignature(veBetterPassport, owner, otherAccount, 3600) + + await veBetterPassport.connect(owner).registerActionForRound(otherAccount, app1Id, 1) + await veBetterPassport.connect(owner).registerActionForRound(otherAccount, app1Id, 1) + await veBetterPassport.connect(owner).registerActionForRound(otherAccount, app1Id, 1) + await veBetterPassport.connect(owner).registerActionForRound(otherAccount, app1Id, 1) + await veBetterPassport.connect(owner).registerActionForRound(otherAccount, app1Id, 1) + + // cumulative score of otherAccount should be 2000 (same as before) + expect(await veBetterPassport.getCumulativeScoreWithDecay(otherAccount, 1)).to.be.equal(2000) + // cumulative score of owner should be 2000 (because actions from otherAccount are now counted as owner's) + expect(await veBetterPassport.getCumulativeScoreWithDecay(owner, 1)).to.be.equal(2000) + + // Other account should not be considered a passport anymore now + expect(await veBetterPassport.isPassport(otherAccount.address)).to.be.false + // But it should be considered a passport at the start of the round + expect( + await veBetterPassport.isPassportInTimepoint( + otherAccount.address, + await xAllocationVoting.currentRoundSnapshot(), + ), + ).to.be.true + + // Owner should be considered a passport both both now and at the start of the round + expect(await veBetterPassport.isPassport(owner.address)).to.be.true + expect( + await veBetterPassport.isPassportInTimepoint(owner.address, await xAllocationVoting.currentRoundSnapshot()), + ).to.be.true + + // Since the the entity is considered a passport at the start of the round, and since it has enough score now, he can vote + await expect(xAllocationVoting.connect(otherAccount).castVote(round1, [app1Id], [ethers.parseEther("100")])).to + .not.be.reverted + + // Since the owner is considered a passport at the start of the round, and since it has enough score now, he can vote + await expect(xAllocationVoting.connect(owner).castVote(round1, [app1Id], [ethers.parseEther("100")])).to.not.be + .reverted + + // But when starting the next round only the owner should be able to vote, since otherAccount is now considered an enitity at 100% + await waitForCurrentRoundToEnd() + await startNewAllocationRound() + + await expect( + xAllocationVoting.connect(otherAccount).castVote(2, [app1Id], [ethers.parseEther("100")]), + ).to.be.revertedWithCustomError(xAllocationVoting, "GovernorPersonhoodVerificationFailed") + + await expect(xAllocationVoting.connect(owner).castVote(2, [app1Id], [ethers.parseEther("100")])).to.not.be + .reverted + }) + }) + + describe("Passport Clock", function () { + it("Should return current block number when calling clock", async function () { + const { veBetterPassport } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + expect(await veBetterPassport.clock()).to.be.equal(await ethers.provider.getBlockNumber()) + }) + + it("should return the clock mode", async function () { + const { veBetterPassport } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + expect(await veBetterPassport.CLOCK_MODE()).to.be.equal("mode=blocknumber&from=default") + }) + }) + + describe("Passport PoP Score", function () { + it("Should be able to register participation of user with ACTION_REGISTRAR_ROLE", async function () { + const { x2EarnApps, otherAccounts, owner, veBetterPassport, otherAccount, xAllocationVoting } = + await getOrDeployContractInstances({ + forceDeploy: true, + }) + + // Bootstrap emissions + await bootstrapEmissions() + + //Add apps + const app1Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[2].address)) + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[2].address, otherAccounts[2].address, otherAccounts[2].address, "metadataURI") + + await veBetterPassport.grantRole(await veBetterPassport.ACTION_REGISTRAR_ROLE(), owner) + + expect(await veBetterPassport.hasRole(await veBetterPassport.ACTION_REGISTRAR_ROLE(), owner.address)).to.be.true + + await veBetterPassport.connect(owner).setAppSecurity(app1Id, 1) // APP_SECURITY.LOW + + await veBetterPassport.connect(owner).registerAction(otherAccount, app1Id) + + expect(await veBetterPassport.userRoundScore(otherAccount, await xAllocationVoting.currentRoundId())).to.equal( + 100, + ) + }) + + it("Should correctly calculate cumulative score", async function () { + const config = createTestConfig() + config.VEPASSPORT_DECAY_RATE = 20 + const { x2EarnApps, otherAccounts, owner, veBetterPassport, otherAccount } = await getOrDeployContractInstances({ + forceDeploy: true, + config, + }) + + // Bootstrap emissions + await bootstrapEmissions() + + //Add apps + const app1Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[2].address)) + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[2].address, otherAccounts[2].address, otherAccounts[2].address, "metadataURI") + + await veBetterPassport.grantRole(await veBetterPassport.ACTION_REGISTRAR_ROLE(), owner) + + expect(await veBetterPassport.hasRole(await veBetterPassport.ACTION_REGISTRAR_ROLE(), owner.address)).to.be.true + + await veBetterPassport.connect(owner).setAppSecurity(app1Id, 1) // APP_SECURITY.LOW + + await veBetterPassport.connect(owner).registerActionForRound(otherAccount, app1Id, 1) + await veBetterPassport.connect(owner).registerActionForRound(otherAccount, app1Id, 2) + await veBetterPassport.connect(owner).registerActionForRound(otherAccount, app1Id, 3) + await veBetterPassport.connect(owner).registerActionForRound(otherAccount, app1Id, 4) + await veBetterPassport.connect(owner).registerActionForRound(otherAccount, app1Id, 5) + + expect(await veBetterPassport.userRoundScore(otherAccount, 1)).to.equal(100) + expect(await veBetterPassport.userRoundScore(otherAccount, 2)).to.equal(100) + expect(await veBetterPassport.userRoundScore(otherAccount, 3)).to.equal(100) + expect(await veBetterPassport.userRoundScore(otherAccount, 4)).to.equal(100) + expect(await veBetterPassport.userRoundScore(otherAccount, 5)).to.equal(100) + + /* + All 5 rounds the user has 100 score. + + round N = [round N score] + ([cumulative score] * [1 - decay factor]) + + round 1 = 100 + (0 * 0.8) = 100 + round 2 = 100 + (100 * 0.8) = 180 + round 3 = 100 + (180 * 0.8) = 244 + round 4 = 100 + (244 * 0.8) = 295,2 => 295 + round 5 = 100 + (295 * 0.8) = 336 + */ + expect(await veBetterPassport.getCumulativeScoreWithDecay(otherAccount, 5)).to.equal(336) + }) + + it("Should correctly transfer enities cumulative score", async function () { + const config = createTestConfig() + config.VEPASSPORT_DECAY_RATE = 20 + const { x2EarnApps, otherAccounts, owner, veBetterPassport, otherAccount } = await getOrDeployContractInstances({ + forceDeploy: true, + config, + }) + + // Bootstrap emissions + await bootstrapEmissions() + + //Add apps + const app1Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[2].address)) + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[2].address, otherAccounts[2].address, otherAccounts[2].address, "metadataURI") + + await veBetterPassport.grantRole(await veBetterPassport.ACTION_REGISTRAR_ROLE(), owner) + + expect(await veBetterPassport.hasRole(await veBetterPassport.ACTION_REGISTRAR_ROLE(), owner.address)).to.be.true + + await veBetterPassport.connect(owner).setAppSecurity(app1Id, 1) // APP_SECURITY.LOW + + await veBetterPassport.connect(owner).registerActionForRound(otherAccount, app1Id, 1) + await veBetterPassport.connect(owner).registerActionForRound(otherAccount, app1Id, 2) + await veBetterPassport.connect(owner).registerActionForRound(otherAccount, app1Id, 3) + await veBetterPassport.connect(owner).registerActionForRound(otherAccount, app1Id, 4) + await veBetterPassport.connect(owner).registerActionForRound(otherAccount, app1Id, 5) + + expect(await veBetterPassport.userRoundScore(otherAccount, 1)).to.equal(100) + expect(await veBetterPassport.userRoundScore(otherAccount, 2)).to.equal(100) + expect(await veBetterPassport.userRoundScore(otherAccount, 3)).to.equal(100) + expect(await veBetterPassport.userRoundScore(otherAccount, 4)).to.equal(100) + expect(await veBetterPassport.userRoundScore(otherAccount, 5)).to.equal(100) + + /* + All 5 rounds the user has 100 score. + + round N = [round N score] + ([cumulative score] * [1 - decay factor]) + + round 1 = 100 + (0 * 0.8) = 100 + round 2 = 100 + (100 * 0.8) = 180 + round 3 = 100 + (180 * 0.8) = 244 + round 4 = 100 + (244 * 0.8) = 295,2 => 295 + round 5 = 100 + (295 * 0.8) = 336 + */ + expect(await veBetterPassport.getCumulativeScoreWithDecay(otherAccount, 5)).to.equal(336) + }) + + it("Should be able to change security multiplier with ACTION_SCORE_MANAGER_ROLE", async function () { + const { veBetterPassport, owner, x2EarnApps, otherAccount, otherAccounts } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + //Add apps + const app1Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[2].address)) + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[2].address, otherAccounts[2].address, otherAccounts[2].address, "metadataURI") + + await veBetterPassport.grantRole(await veBetterPassport.ACTION_SCORE_MANAGER_ROLE(), owner) + await veBetterPassport.grantRole(await veBetterPassport.ACTION_REGISTRAR_ROLE(), owner) + + expect(await veBetterPassport.hasRole(await veBetterPassport.ACTION_SCORE_MANAGER_ROLE(), owner.address)).to.be + .true + expect(await veBetterPassport.hasRole(await veBetterPassport.ACTION_REGISTRAR_ROLE(), owner.address)).to.be.true + + // Sets APP_SECURITY.LOW multiplier to 1000 + await veBetterPassport.connect(owner).setSecurityMultiplier(1, 1000) + + await veBetterPassport.connect(owner).setAppSecurity(app1Id, 1) // APP_SECURITY.LOW + + expect(await veBetterPassport.securityMultiplier(1)).to.equal(1000) + + await veBetterPassport.registerActionForRound(otherAccount, app1Id, 1) + + expect(await veBetterPassport.userRoundScore(otherAccount, 1)).to.equal(1000) + expect(await veBetterPassport.userRoundScoreApp(otherAccount, 1, app1Id)).to.equal(1000) + }) + + it("Should be able to change app's security multiplier with ACTION_SCORE_MANAGER_ROLE", async function () { + const { veBetterPassport, owner, x2EarnApps, otherAccount, otherAccounts } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + //Add apps + const app1Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[2].address)) + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[2].address, otherAccounts[2].address, otherAccounts[2].address, "metadataURI") + + await veBetterPassport.grantRole(await veBetterPassport.ACTION_SCORE_MANAGER_ROLE(), owner) + await veBetterPassport.grantRole(await veBetterPassport.ACTION_REGISTRAR_ROLE(), owner) + + expect(await veBetterPassport.hasRole(await veBetterPassport.ACTION_SCORE_MANAGER_ROLE(), owner.address)).to.be + .true + expect(await veBetterPassport.hasRole(await veBetterPassport.ACTION_REGISTRAR_ROLE(), owner.address)).to.be.true + + // Sets app's security to APP_SECURITY.MEDIUM + await veBetterPassport.connect(owner).setAppSecurity(app1Id, 2) + + expect(await veBetterPassport.appSecurity(app1Id)).to.equal(2) + + await veBetterPassport.registerActionForRound(otherAccount, app1Id, 1) + + expect(await veBetterPassport.userRoundScore(otherAccount, 1)).to.equal(200) + expect(await veBetterPassport.userRoundScoreApp(otherAccount, 1, app1Id)).to.equal(200) + }) + + it("Should calculate cumulative score correctly with different security multipliers", async function () { + const config = createTestConfig() + config.VEPASSPORT_DECAY_RATE = 20 + const { veBetterPassport, owner, x2EarnApps, otherAccount, otherAccounts } = await getOrDeployContractInstances({ + forceDeploy: true, + config, + }) + + //Add apps + const app1Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[2].address)) + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[2].address, otherAccounts[2].address, otherAccounts[2].address, "metadataURI") + + const app2Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[3].address)) + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[3].address, otherAccounts[3].address, otherAccounts[3].address, "metadataURI") + const app3Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[4].address)) + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[4].address, otherAccounts[4].address, otherAccounts[4].address, "metadataURI") + + await veBetterPassport.grantRole(await veBetterPassport.ACTION_SCORE_MANAGER_ROLE(), owner) + await veBetterPassport.grantRole(await veBetterPassport.ACTION_REGISTRAR_ROLE(), owner) + + expect(await veBetterPassport.hasRole(await veBetterPassport.ACTION_SCORE_MANAGER_ROLE(), owner.address)).to.be + .true + expect(await veBetterPassport.hasRole(await veBetterPassport.ACTION_REGISTRAR_ROLE(), owner.address)).to.be.true + + // Sets app1 security to APP_SECURITY.LOW + await veBetterPassport.connect(owner).setAppSecurity(app1Id, 1) + + // Sets app2 security to APP_SECURITY.MEDIUM + await veBetterPassport.connect(owner).setAppSecurity(app2Id, 2) + + // Sets app3 security to APP_SECURITY.HIGH + await veBetterPassport.connect(owner).setAppSecurity(app3Id, 3) + + await veBetterPassport.connect(owner).registerActionForRound(otherAccount, app1Id, 1) + await veBetterPassport.connect(owner).registerActionForRound(otherAccount, app1Id, 2) + await veBetterPassport.connect(owner).registerActionForRound(otherAccount, app2Id, 3) + await veBetterPassport.connect(owner).registerActionForRound(otherAccount, app2Id, 4) + await veBetterPassport.connect(owner).registerActionForRound(otherAccount, app3Id, 5) + + expect(await veBetterPassport.userRoundScore(otherAccount, 1)).to.equal(100) + expect(await veBetterPassport.userRoundScore(otherAccount, 2)).to.equal(100) + expect(await veBetterPassport.userRoundScore(otherAccount, 3)).to.equal(200) + expect(await veBetterPassport.userRoundScore(otherAccount, 4)).to.equal(200) + expect(await veBetterPassport.userRoundScore(otherAccount, 5)).to.equal(400) + + /* + Round 1 score: 100 + Round 2 score: 100 + Round 3 score: 200 + Round 4 score: 200 + Round 5 score: 400 + + round N = [round N score] + ([cumulative score] * [1 - decay factor]) + + round 1 = 100 + (0 * 0.8) = 100 + round 2 = 100 + (100 * 0.8) = 180 + round 3 = 200 + (180 * 0.8) = 344 + round 4 = 200 + (344 * 0.8) = 475,2 => 475 + round 5 = 400 + (475 * 0.8) = 780 + */ + expect(await veBetterPassport.getCumulativeScoreWithDecay(otherAccount, 5)).to.equal(780) + }) + + it("Should calculate decay from first round if last round specified is greater than cumulative rounds to look for", async function () { + const config = createTestConfig() + config.VEPASSPORT_DECAY_RATE = 20 + const { veBetterPassport, owner, x2EarnApps, otherAccount, otherAccounts } = await getOrDeployContractInstances({ + forceDeploy: true, + config, + }) + + //Add apps + const app1Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[2].address)) + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[2].address, otherAccounts[2].address, otherAccounts[2].address, "metadataURI") + + await veBetterPassport.grantRole(await veBetterPassport.ACTION_SCORE_MANAGER_ROLE(), owner) + await veBetterPassport.grantRole(await veBetterPassport.ACTION_REGISTRAR_ROLE(), owner) + + expect(await veBetterPassport.hasRole(await veBetterPassport.ACTION_SCORE_MANAGER_ROLE(), owner.address)).to.be + .true + expect(await veBetterPassport.hasRole(await veBetterPassport.ACTION_REGISTRAR_ROLE(), owner.address)).to.be.true + + // Sets app1 security to APP_SECURITY.LOW + await veBetterPassport.connect(owner).setAppSecurity(app1Id, 1) + + await veBetterPassport.connect(owner).registerActionForRound(otherAccount, app1Id, 1) + await veBetterPassport.connect(owner).registerActionForRound(otherAccount, app1Id, 2) + await veBetterPassport.connect(owner).registerActionForRound(otherAccount, app1Id, 3) + await veBetterPassport.connect(owner).registerActionForRound(otherAccount, app1Id, 4) + await veBetterPassport.connect(owner).registerActionForRound(otherAccount, app1Id, 5) + + // Get cumulative score from lastRound = 2. first round to start iterations would be negative so we expect cumulative score to start from round 1: + // round 1 = 100 => round 2 = 100 + (100 * 0.8) = 180 + expect(await veBetterPassport.getCumulativeScoreWithDecay(otherAccount, 2)).to.equal(180) + }) + + it("Should not be able to register action without ACTION_REGISTRAR_ROLE", async function () { + const { veBetterPassport, owner, x2EarnApps, otherAccount, otherAccounts } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + //Add apps + const app1Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[2].address)) + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[2].address, otherAccounts[2].address, otherAccounts[2].address, "metadataURI") + + await expect(veBetterPassport.connect(otherAccounts[3]).registerAction(otherAccount, app1Id)).to.be.reverted + }) + + it("Should be able to change app's security multiplier with ACTION_SCORE_MANAGER_ROLE", async function () { + const { veBetterPassport, owner, x2EarnApps, otherAccount, otherAccounts } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + //Add apps + const app1Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[2].address)) + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[2].address, otherAccounts[2].address, otherAccounts[2].address, "metadataURI") + + await veBetterPassport.grantRole(await veBetterPassport.ACTION_SCORE_MANAGER_ROLE(), owner) + await veBetterPassport.grantRole(await veBetterPassport.ACTION_REGISTRAR_ROLE(), owner) + + expect(await veBetterPassport.hasRole(await veBetterPassport.ACTION_SCORE_MANAGER_ROLE(), owner.address)).to.be + .true + expect(await veBetterPassport.hasRole(await veBetterPassport.ACTION_REGISTRAR_ROLE(), owner.address)).to.be.true + + // Sets app's security to APP_SECURITY.MEDIUM + await veBetterPassport.connect(owner).setAppSecurity(app1Id, 2) + + expect(await veBetterPassport.appSecurity(app1Id)).to.equal(2) + + await veBetterPassport.registerActionForRound(otherAccount, app1Id, 1) + + expect(await veBetterPassport.userRoundScore(otherAccount, 1)).to.equal(200) + expect(await veBetterPassport.userRoundScoreApp(otherAccount, 1, app1Id)).to.equal(200) + }) + + it("Should calculate cumulative score correctly with different security multipliers", async function () { + const config = createTestConfig() + config.VEPASSPORT_DECAY_RATE = 20 + const { veBetterPassport, owner, x2EarnApps, otherAccount, otherAccounts } = await getOrDeployContractInstances({ + forceDeploy: true, + config, + }) + + //Add apps + const app1Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[2].address)) + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[2].address, otherAccounts[2].address, otherAccounts[2].address, "metadataURI") + + const app2Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[3].address)) + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[3].address, otherAccounts[3].address, otherAccounts[3].address, "metadataURI") + const app3Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[4].address)) + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[4].address, otherAccounts[4].address, otherAccounts[4].address, "metadataURI") + + await veBetterPassport.grantRole(await veBetterPassport.ACTION_SCORE_MANAGER_ROLE(), owner) + await veBetterPassport.grantRole(await veBetterPassport.ACTION_REGISTRAR_ROLE(), owner) + + expect(await veBetterPassport.hasRole(await veBetterPassport.ACTION_SCORE_MANAGER_ROLE(), owner.address)).to.be + .true + expect(await veBetterPassport.hasRole(await veBetterPassport.ACTION_REGISTRAR_ROLE(), owner.address)).to.be.true + + // Sets app1 security to APP_SECURITY.LOW + await veBetterPassport.connect(owner).setAppSecurity(app1Id, 1) + + // Sets app2 security to APP_SECURITY.MEDIUM + await veBetterPassport.connect(owner).setAppSecurity(app2Id, 2) + + // Sets app3 security to APP_SECURITY.HIGH + await veBetterPassport.connect(owner).setAppSecurity(app3Id, 3) + + await veBetterPassport.connect(owner).registerActionForRound(otherAccount, app1Id, 1) + await veBetterPassport.connect(owner).registerActionForRound(otherAccount, app1Id, 2) + await veBetterPassport.connect(owner).registerActionForRound(otherAccount, app2Id, 3) + await veBetterPassport.connect(owner).registerActionForRound(otherAccount, app2Id, 4) + await veBetterPassport.connect(owner).registerActionForRound(otherAccount, app3Id, 5) + + expect(await veBetterPassport.userRoundScore(otherAccount, 1)).to.equal(100) + expect(await veBetterPassport.userRoundScore(otherAccount, 2)).to.equal(100) + expect(await veBetterPassport.userRoundScore(otherAccount, 3)).to.equal(200) + expect(await veBetterPassport.userRoundScore(otherAccount, 4)).to.equal(200) + expect(await veBetterPassport.userRoundScore(otherAccount, 5)).to.equal(400) + + /* + Round 1 score: 100 + Round 2 score: 100 + Round 3 score: 200 + Round 4 score: 200 + Round 5 score: 400 + round N = [round N score] + ([cumulative score] * [1 - decay factor]) + round 1 = 100 + (0 * 0.8) = 100 + round 2 = 100 + (100 * 0.8) = 180 + round 3 = 200 + (180 * 0.8) = 344 + round 4 = 200 + (344 * 0.8) = 475,2 => 475 + round 5 = 400 + (475 * 0.8) = 780 + */ + expect(await veBetterPassport.getCumulativeScoreWithDecay(otherAccount, 5)).to.equal(780) + }) + + it("Should be able to update rounds for cumulative scores", async function () { + const config = createTestConfig() + config.VEPASSPORT_DECAY_RATE = 20 + const { veBetterPassport, owner, x2EarnApps, otherAccount, otherAccounts } = await getOrDeployContractInstances({ + forceDeploy: true, + config, + }) + + await bootstrapAndStartEmissions() + + //Add apps + const app1Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[2].address)) + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[2].address, otherAccounts[2].address, otherAccounts[2].address, "metadataURI") + + const app2Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[3].address)) + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[3].address, otherAccounts[3].address, otherAccounts[3].address, "metadataURI") + const app3Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[4].address)) + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[4].address, otherAccounts[4].address, otherAccounts[4].address, "metadataURI") + + await veBetterPassport.grantRole(await veBetterPassport.ACTION_SCORE_MANAGER_ROLE(), owner) + await veBetterPassport.grantRole(await veBetterPassport.ACTION_REGISTRAR_ROLE(), owner) + + expect(await veBetterPassport.hasRole(await veBetterPassport.ACTION_SCORE_MANAGER_ROLE(), owner.address)).to.be + .true + expect(await veBetterPassport.hasRole(await veBetterPassport.ACTION_REGISTRAR_ROLE(), owner.address)).to.be.true + + // Sets app1 security to APP_SECURITY.LOW + await veBetterPassport.connect(owner).setAppSecurity(app1Id, 1) + + // Sets app2 security to APP_SECURITY.MEDIUM + await veBetterPassport.connect(owner).setAppSecurity(app2Id, 2) + + // Sets app3 security to APP_SECURITY.HIGH + await veBetterPassport.connect(owner).setAppSecurity(app3Id, 3) + + await veBetterPassport.connect(owner).registerActionForRound(otherAccount, app1Id, 1) + await veBetterPassport.connect(owner).registerActionForRound(otherAccount, app1Id, 2) + await veBetterPassport.connect(owner).registerActionForRound(otherAccount, app2Id, 3) + await veBetterPassport.connect(owner).registerActionForRound(otherAccount, app2Id, 4) + await veBetterPassport.connect(owner).registerActionForRound(otherAccount, app3Id, 5) + + expect(await veBetterPassport.userRoundScore(otherAccount, 1)).to.equal(100) + expect(await veBetterPassport.userRoundScore(otherAccount, 2)).to.equal(100) + expect(await veBetterPassport.userRoundScore(otherAccount, 3)).to.equal(200) + expect(await veBetterPassport.userRoundScore(otherAccount, 4)).to.equal(200) + expect(await veBetterPassport.userRoundScore(otherAccount, 5)).to.equal(400) + + // skip through to round 5 + await moveToCycle(6) + + /* + Round 1 score: 100 + Round 2 score: 100 + Round 3 score: 200 + Round 4 score: 200 + Round 5 score: 400 + round N = [round N score] + ([cumulative score] * [1 - decay factor]) + round 1 = 100 + (0 * 0.8) = 100 + round 2 = 100 + (100 * 0.8) = 180 + round 3 = 200 + (180 * 0.8) = 344 + round 4 = 200 + (344 * 0.8) = 475,2 => 475 + round 5 = 400 + (475 * 0.8) = 780 + */ + expect(await veBetterPassport.getCumulativeScoreWithDecay(otherAccount, 5)).to.equal(780) + + // Only actions score manager can update rounds for cumulative scores + await expect(veBetterPassport.connect(otherAccount).setRoundsForCumulativeScore(2)).to.be.reverted + + // Update rounds to 2 + await veBetterPassport.connect(owner).setRoundsForCumulativeScore(2) + + // Cumulative score should now be 180 + expect(await veBetterPassport.getCumulativeScoreWithDecay(otherAccount, 5)).to.equal(560) + }) + + it("Should calculate decay from first round if last round specified is greater than cumulative rounds to look for", async function () { + const config = createTestConfig() + config.VEPASSPORT_DECAY_RATE = 20 + const { veBetterPassport, owner, x2EarnApps, otherAccount, otherAccounts } = await getOrDeployContractInstances({ + forceDeploy: true, + config, + }) + + //Add apps + const app1Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[2].address)) + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[2].address, otherAccounts[2].address, otherAccounts[2].address, "metadataURI") + + await veBetterPassport.grantRole(await veBetterPassport.ACTION_SCORE_MANAGER_ROLE(), owner) + await veBetterPassport.grantRole(await veBetterPassport.ACTION_REGISTRAR_ROLE(), owner) + + expect(await veBetterPassport.hasRole(await veBetterPassport.ACTION_SCORE_MANAGER_ROLE(), owner.address)).to.be + .true + expect(await veBetterPassport.hasRole(await veBetterPassport.ACTION_REGISTRAR_ROLE(), owner.address)).to.be.true + + await veBetterPassport.setAppSecurity(app1Id, 1) // APP_SECURITY.LOW + + await veBetterPassport.connect(owner).registerActionForRound(otherAccount, app1Id, 1) + await veBetterPassport.connect(owner).registerActionForRound(otherAccount, app1Id, 2) + await veBetterPassport.connect(owner).registerActionForRound(otherAccount, app1Id, 3) + await veBetterPassport.connect(owner).registerActionForRound(otherAccount, app1Id, 4) + await veBetterPassport.connect(owner).registerActionForRound(otherAccount, app1Id, 5) + + // Get cumulative score from lastRound = 2. first round to start iterations would be negative so we expect cumulative score to start from round 1: + // round 1 = 100 => round 2 = 100 + (100 * 0.8) = 180 + expect(await veBetterPassport.getCumulativeScoreWithDecay(otherAccount, 2)).to.equal(180) + }) + + it("Should not be able to register action without ACTION_REGISTRAR_ROLE", async function () { + const { veBetterPassport, owner, x2EarnApps, otherAccount, otherAccounts } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + //Add apps + const app1Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[2].address)) + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[2].address, otherAccounts[2].address, otherAccounts[2].address, "metadataURI") + + await expect(veBetterPassport.connect(otherAccount).registerAction(otherAccount, app1Id)).to.be.reverted + }) + + it("Should be able to change app security with ACTION_SCORE_MANAGER_ROLE", async function () { + const { veBetterPassport, owner, x2EarnApps, otherAccounts } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + //Add apps + const app1Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[2].address)) + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[2].address, otherAccounts[2].address, otherAccounts[2].address, "metadataURI") + + await veBetterPassport.grantRole(await veBetterPassport.ACTION_SCORE_MANAGER_ROLE(), owner) + + expect(await veBetterPassport.hasRole(await veBetterPassport.ACTION_SCORE_MANAGER_ROLE(), owner.address)).to.be + .true + + await veBetterPassport.connect(owner).setAppSecurity(app1Id, 2) + + expect(await veBetterPassport.appSecurity(app1Id)).to.equal(2) + }) + + it("Should be able to change decay rate with DEFAULT_ADMIN_ROLE", async function () { + const { veBetterPassport, owner, otherAccounts, otherAccount, x2EarnApps } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + await veBetterPassport.grantRole(await veBetterPassport.DEFAULT_ADMIN_ROLE(), owner) + + expect(await veBetterPassport.hasRole(await veBetterPassport.DEFAULT_ADMIN_ROLE(), owner.address)).to.be.true + + // 90% decay rate + await veBetterPassport.connect(owner).setDecayRate(90) + + //Add apps + const app1Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[2].address)) + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[2].address, otherAccounts[2].address, otherAccounts[2].address, "metadataURI") + + await veBetterPassport.grantRole(await veBetterPassport.ACTION_SCORE_MANAGER_ROLE(), owner) + await veBetterPassport.grantRole(await veBetterPassport.ACTION_REGISTRAR_ROLE(), owner) + + expect(await veBetterPassport.hasRole(await veBetterPassport.ACTION_SCORE_MANAGER_ROLE(), owner.address)).to.be + .true + expect(await veBetterPassport.hasRole(await veBetterPassport.ACTION_REGISTRAR_ROLE(), owner.address)).to.be.true + + await veBetterPassport.connect(owner).setAppSecurity(app1Id, 1) // APP_SECURITY.LOW + + await veBetterPassport.connect(owner).registerActionForRound(otherAccount, app1Id, 1) + await veBetterPassport.connect(owner).registerActionForRound(otherAccount, app1Id, 2) + await veBetterPassport.connect(owner).registerActionForRound(otherAccount, app1Id, 3) + await veBetterPassport.connect(owner).registerActionForRound(otherAccount, app1Id, 4) + await veBetterPassport.connect(owner).registerActionForRound(otherAccount, app1Id, 5) + + /* + round 1 = 100 + round 2 = 100 + (100 * 0.1) = 110 + round 3 = 100 + (110 * 0.1) = 111 + round 4 = 100 + (111 * 0.1) = 111.1 => 111 + round 5 = 100 + (111 * 0.1) = 111.1 => 111 + */ + expect(await veBetterPassport.getCumulativeScoreWithDecay(otherAccount, 5)).to.equal(111) + }) + + it("Should be able to set decay rate to 0", async function () { + const { veBetterPassport, owner } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + await veBetterPassport.connect(owner).setDecayRate(1) + expect(await veBetterPassport.decayRate()).to.equal(1) + + await veBetterPassport.connect(owner).setDecayRate(0) + expect(await veBetterPassport.decayRate()).to.equal(0) + }) + + it("Should not register action score if app security is not set", async function () { + const { veBetterPassport, owner, otherAccounts, otherAccount, x2EarnApps } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + await veBetterPassport.grantRole(await veBetterPassport.ACTION_SCORE_MANAGER_ROLE(), owner) + await veBetterPassport.grantRole(await veBetterPassport.ACTION_REGISTRAR_ROLE(), owner) + + expect(await veBetterPassport.hasRole(await veBetterPassport.ACTION_SCORE_MANAGER_ROLE(), owner.address)).to.be + .true + expect(await veBetterPassport.hasRole(await veBetterPassport.ACTION_REGISTRAR_ROLE(), owner.address)).to.be.true + + //Add apps + const app1Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[2].address)) + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[2].address, otherAccounts[2].address, otherAccounts[2].address, "metadataURI") + + await veBetterPassport.connect(owner).registerActionForRound(otherAccount, app1Id, 1) + + expect(await veBetterPassport.userRoundScore(otherAccount, 1)).to.equal(0) + expect(await veBetterPassport.userRoundScoreApp(otherAccount, 1, app1Id)).to.equal(0) + }) + + it("Should not register action score if user is blacklisted", async function () { + const { veBetterPassport, owner, otherAccounts, otherAccount, x2EarnApps } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + await veBetterPassport.grantRole(await veBetterPassport.ACTION_SCORE_MANAGER_ROLE(), owner) + await veBetterPassport.grantRole(await veBetterPassport.ACTION_REGISTRAR_ROLE(), owner) + + expect(await veBetterPassport.hasRole(await veBetterPassport.ACTION_SCORE_MANAGER_ROLE(), owner.address)).to.be + .true + expect(await veBetterPassport.hasRole(await veBetterPassport.ACTION_REGISTRAR_ROLE(), owner.address)).to.be.true + + //Add apps + const app1Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[2].address)) + + const app2Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[3].address)) + + // Set app security to APP_SECURITY.LOW + await veBetterPassport.connect(owner).setAppSecurity(app1Id, 1) + await veBetterPassport.connect(owner).setAppSecurity(app2Id, 1) + + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[2].address, otherAccounts[2].address, otherAccounts[2].address, "metadataURI") + + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[3].address, otherAccounts[3].address, otherAccounts[3].address, "metadataURI") + + await veBetterPassport.connect(owner).registerActionForRound(otherAccount, app1Id, 1) + + expect(await veBetterPassport.userRoundScore(otherAccount, 1)).to.equal(100) + expect(await veBetterPassport.userRoundScoreApp(otherAccount, 1, app1Id)).to.equal(100) + + // Blacklist user + await veBetterPassport.connect(owner).blacklist(otherAccount.address) + + await veBetterPassport.connect(owner).registerActionForRound(otherAccount, app1Id, 2) + + expect(await veBetterPassport.userRoundScore(otherAccount, 2)).to.equal(0) + + // Attach to passport + await linkEntityToPassportWithSignature(veBetterPassport, owner, otherAccount, 1000) + + // Passport score should be 0 + expect(await veBetterPassport.userRoundScore(owner.address, 2)).to.equal(0) + + // Actions registered by blacklisted user should not affect passport score + await veBetterPassport.connect(owner).registerActionForRound(otherAccount, app1Id, 3) + + expect(await veBetterPassport.userRoundScore(owner.address, 3)).to.equal(0) + expect(await veBetterPassport.userRoundScore(otherAccount.address, 3)).to.equal(0) + + // Remove user from blacklist + await veBetterPassport.connect(owner).whitelist(otherAccount.address) + + await veBetterPassport.connect(owner).registerActionForRound(otherAccount, app2Id, 4) + + expect(await veBetterPassport.userRoundScore(otherAccount, 4)).to.equal(0) + // Passport score should be 1 + expect(await veBetterPassport.userRoundScore(owner.address, 4)).to.equal(100) + }) + + it("should revert if you try to link passport to yourself via signature", async function () { + const { veBetterPassport, owner } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + await expect(linkEntityToPassportWithSignature(veBetterPassport, owner, owner, 1000)).to.be.reverted + }) + + it("Should checkpoint the PoP score threshold correctly", async function () { + const { veBetterPassport, owner } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + await veBetterPassport.grantRole(await veBetterPassport.ACTION_SCORE_MANAGER_ROLE(), owner) + + expect(await veBetterPassport.hasRole(await veBetterPassport.ACTION_SCORE_MANAGER_ROLE(), owner.address)).to.be + .true + + // Get block number post deployment + const blockNumber1 = await ethers.provider.getBlockNumber() + + // Threshold PoP score is 0 + expect(await veBetterPassport.thresholdPoPScore()).to.equal(0) + + // Wait for next block + await moveBlocks(1) + + // Set threshold PoP score to 100 + await veBetterPassport.connect(owner).setThresholdPoPScore(100) + + // Threshold PoP score is 100 + expect(await veBetterPassport.thresholdPoPScore()).to.equal(100) + + // Get block number post setting threshold + const blockNumber2 = await ethers.provider.getBlockNumber() + + // Wait for next block + await moveBlocks(1) + + // Set threshold PoP score to 200 + await veBetterPassport.connect(owner).setThresholdPoPScore(200) + + // Threshold PoP score is 200 + expect(await veBetterPassport.thresholdPoPScore()).to.equal(200) + + // Get block number post setting threshold + const blockNumber3 = await ethers.provider.getBlockNumber() + + // Checkpoints + expect(await veBetterPassport.thresholdPoPScoreAtTimepoint(blockNumber1)).to.equal(0) + + expect(await veBetterPassport.thresholdPoPScoreAtTimepoint(blockNumber2)).to.equal(100) + + expect(await veBetterPassport.thresholdPoPScoreAtTimepoint(blockNumber3)).to.equal(200) + }) + + it("should revert if you are trying to accept a link request that does not exist", async function () { + const { veBetterPassport, owner } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + await expect(veBetterPassport.connect(owner).acceptEntityLink(owner.address)).to.be.revertedWithCustomError( + veBetterPassport, + "NotLinked", + ) + }) + + it("should revert if you are trying to accept a link request and you are not the passport", async function () { + const { veBetterPassport, owner, otherAccount } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + await veBetterPassport.linkEntityToPassport(otherAccount.address) + + await expect(veBetterPassport.connect(owner).acceptEntityLink(owner.address)).to.be.revertedWithCustomError( + veBetterPassport, + "UnauthorizedUser", + ) + }) + + it("should revert if you are trying to accept a link request and passport has reached max amoutn entities", async function () { + const config = createTestConfig() + config.VEPASSPORT_PASSPORT_MAX_ENTITIES = 1 + const { + veBetterPassport, + owner: passport, + otherAccounts, + } = await getOrDeployContractInstances({ + forceDeploy: true, + config, + }) + + const entity1 = otherAccounts[0] + const entity2 = otherAccounts[1] + + // Entities create link requests + await veBetterPassport.connect(entity1).linkEntityToPassport(passport.address) + await veBetterPassport.connect(entity2).linkEntityToPassport(passport.address) + + // Entity 2 link accepts link request + await veBetterPassport.connect(passport).acceptEntityLink(entity2.address) + + //Entity 2 should be a passport entity + expect(await veBetterPassport.getPassportForEntity(entity2.address)).to.equal(passport.address) + + // Should revert if entity 1 tries to accept link request as max entities reached + await expect(veBetterPassport.connect(passport).acceptEntityLink(entity1.address)).to.be.revertedWithCustomError( + veBetterPassport, + "MaxEntitiesPerPassportReached", + ) + }) + }) + + describe("Passport Whitelisting & Blacklisting", function () { + it("WHITELISTER_ROLE should be able to whitelist and blacklist users", async function () { + const { veBetterPassport, owner, otherAccount } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + expect(await veBetterPassport.hasRole(await veBetterPassport.WHITELISTER_ROLE(), owner.address)).to.be.true + + await veBetterPassport.toggleCheck(1) + await veBetterPassport.toggleCheck(2) + + expect(await veBetterPassport.isCheckEnabled(1)).to.be.true + expect(await veBetterPassport.isCheckEnabled(2)).to.be.true + + await veBetterPassport.connect(owner).whitelist(otherAccount.address) + + expect(await veBetterPassport.isWhitelisted(otherAccount.address)).to.be.true + expect(await veBetterPassport.isPerson(otherAccount.address)).to.deep.equal([true, "User is whitelisted"]) + + await veBetterPassport.connect(owner).blacklist(otherAccount.address) + + expect(await veBetterPassport.isWhitelisted(otherAccount.address)).to.be.false + expect(await veBetterPassport.isPerson(otherAccount.address)).to.deep.equal([false, "User is blacklisted"]) + }) + + it("If whitelisted, blacklisting removes from whitelist", async function () { + const { veBetterPassport, owner, otherAccount } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + await veBetterPassport.connect(owner).whitelist(otherAccount.address) + + await veBetterPassport.toggleCheck(1) + await veBetterPassport.toggleCheck(2) + + expect(await veBetterPassport.isCheckEnabled(1)).to.be.true + expect(await veBetterPassport.isCheckEnabled(2)).to.be.true + + expect(await veBetterPassport.isWhitelisted(otherAccount.address)).to.be.true + expect(await veBetterPassport.isPerson(otherAccount.address)).to.deep.equal([true, "User is whitelisted"]) + + await veBetterPassport.connect(owner).blacklist(otherAccount.address) + + expect(await veBetterPassport.isWhitelisted(otherAccount.address)).to.be.false + expect(await veBetterPassport.isPerson(otherAccount.address)).to.deep.equal([false, "User is blacklisted"]) + }) + + it("If blacklisted, whitelisting removes from blacklist", async function () { + const { veBetterPassport, owner, otherAccount } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + await veBetterPassport.toggleCheck(1) + await veBetterPassport.toggleCheck(2) + + expect(await veBetterPassport.isCheckEnabled(1)).to.be.true + expect(await veBetterPassport.isCheckEnabled(2)).to.be.true + + await veBetterPassport.connect(owner).blacklist(otherAccount.address) + + expect(await veBetterPassport.isWhitelisted(otherAccount.address)).to.be.false + expect(await veBetterPassport.isPerson(otherAccount.address)).to.deep.equal([false, "User is blacklisted"]) + + await veBetterPassport.connect(owner).whitelist(otherAccount.address) + + expect(await veBetterPassport.isWhitelisted(otherAccount.address)).to.be.true + expect(await veBetterPassport.isPerson(otherAccount.address)).to.deep.equal([true, "User is whitelisted"]) + }) + + it("Without WHITELISTER_ROLE, should not be able to whitelist or blacklist users", async function () { + const { veBetterPassport, owner, otherAccount } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + await expect(veBetterPassport.connect(otherAccount).whitelist(owner.address)).to.be.reverted + await expect(veBetterPassport.connect(otherAccount).blacklist(owner.address)).to.be.reverted + }) + + it("If passport is whitelisted and enities are blacklisted, should return whitelisted", async function () { + const { + veBetterPassport, + owner: passport, + otherAccount: entity, + } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + await linkEntityToPassportWithSignature(veBetterPassport, passport, entity, 1000) + + await veBetterPassport.whitelist(passport.address) + await veBetterPassport.blacklist(entity.address) + + expect(await veBetterPassport.isWhitelisted(passport.address)).to.be.true + expect(await veBetterPassport.isBlacklisted(entity.address)).to.be.true + + // Passport is whitelisted, entity is blacklisted, should return whitelisted + expect(await veBetterPassport.isPassportWhitelisted(passport.address)).to.be.true + // Passport is whitelisted, entity is blacklisted, should return whitelisted + expect(await veBetterPassport.isPassportWhitelisted(entity.address)).to.be.true + }) + + it("If passport is blacklisted and enities are whitelisted, should return blacklisted", async function () { + const { + veBetterPassport, + owner: passport, + otherAccount: entity, + } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + await linkEntityToPassportWithSignature(veBetterPassport, passport, entity, 1000) + + await veBetterPassport.blacklist(passport.address) + await veBetterPassport.whitelist(entity.address) + + expect(await veBetterPassport.isBlacklisted(passport.address)).to.be.true + expect(await veBetterPassport.isWhitelisted(entity.address)).to.be.true + + // Passport is whitelisted, entity is blacklisted, should return whitelisted + expect(await veBetterPassport.isPassportBlacklisted(passport.address)).to.be.true + // Passport is whitelisted, entity is blacklisted, should return whitelisted + expect(await veBetterPassport.isPassportBlacklisted(entity.address)).to.be.true + }) + + it("Can update blacklist threshold", async function () { + const config = createTestConfig() + config.VEPASSPORT_BLACKLIST_THRESHOLD_PERCENTAGE = 60 // 60% of entities are blacklisted + const { + veBetterPassport, + owner: passport, + otherAccount: entity1, + otherAccounts, + } = await getOrDeployContractInstances({ + forceDeploy: true, + config, + }) + + const entity2 = otherAccounts[2] + + await linkEntityToPassportWithSignature(veBetterPassport, passport, entity1, 1000) + await linkEntityToPassportWithSignature(veBetterPassport, passport, entity2, 1000) + + await veBetterPassport.blacklist(entity1.address) + + expect(await veBetterPassport.isBlacklisted(entity1.address)).to.be.true + expect(await veBetterPassport.isBlacklisted(entity2.address)).to.be.false + + // 50% of entities are blacklisted, doesn't meet threshold of 60% + expect(await veBetterPassport.isPassportBlacklisted(passport.address)).to.be.false + + // Update threshold to 50% + await veBetterPassport.setBlacklistThreshold(50) + + // 50% of entities are blacklisted, meets threshold of 50% + expect(await veBetterPassport.isPassportBlacklisted(passport.address)).to.be.true + }) + + it("Can update whitelist threshold", async function () { + const config = createTestConfig() + config.VEPASSPORT_WHITELIST_THRESHOLD_PERCENTAGE = 60 // 60% of entities are blacklisted + const { + veBetterPassport, + owner: passport, + otherAccount: entity1, + otherAccounts, + } = await getOrDeployContractInstances({ + forceDeploy: true, + config, + }) + + const entity2 = otherAccounts[2] + + await linkEntityToPassportWithSignature(veBetterPassport, passport, entity1, 1000) + await linkEntityToPassportWithSignature(veBetterPassport, passport, entity2, 1000) + + await veBetterPassport.whitelist(entity1.address) + + expect(await veBetterPassport.isWhitelisted(entity1.address)).to.be.true + expect(await veBetterPassport.isWhitelisted(entity2.address)).to.be.false + + // 50% of entities are whitelisted, doesn't meet threshold of 60% + expect(await veBetterPassport.isPassportWhitelisted(passport.address)).to.be.false + + // Update threshold to 50% + await veBetterPassport.setWhitelistThreshold(50) + + // 50% of entities are whitelisted, meets threshold of 50% + expect(await veBetterPassport.isPassportWhitelisted(passport.address)).to.be.true + }) + + it("Can remove user from blacklist", async function () { + const config = createTestConfig() + config.VEPASSPORT_BLACKLIST_THRESHOLD_PERCENTAGE = 60 // 60% of entities are blacklisted + const { + veBetterPassport, + otherAccount: entity1, + otherAccounts, + } = await getOrDeployContractInstances({ + forceDeploy: true, + config, + }) + + const entity2 = otherAccounts[2] + + await veBetterPassport.blacklist(entity1.address) + + expect(await veBetterPassport.isBlacklisted(entity1.address)).to.be.true + expect(await veBetterPassport.isBlacklisted(entity2.address)).to.be.false + + // Remove entity1 from blacklist + await veBetterPassport.removeFromBlacklist(entity1.address) + + expect(await veBetterPassport.isBlacklisted(entity1.address)).to.be.false + }) + + it("Can remove user from whitelist", async function () { + const config = createTestConfig() + config.VEPASSPORT_WHITELIST_THRESHOLD_PERCENTAGE = 60 // 60% of entities are whitelisted + const { veBetterPassport, otherAccount: entity1 } = await getOrDeployContractInstances({ + forceDeploy: true, + config, + }) + + await veBetterPassport.whitelist(entity1.address) + + expect(await veBetterPassport.isWhitelisted(entity1.address)).to.be.true + + // Remove entity1 from whitelist + await veBetterPassport.removeFromWhitelist(entity1.address) + + // Entity1 should no longer be whitelisted + expect(await veBetterPassport.isWhitelisted(entity1.address)).to.be.false + }) + + it("Can remove an entity from whitelist", async function () { + const config = createTestConfig() + config.VEPASSPORT_WHITELIST_THRESHOLD_PERCENTAGE = 60 // 60% of entities are whitelisted + const { + veBetterPassport, + otherAccount: entity1, + owner: passport, + } = await getOrDeployContractInstances({ + forceDeploy: true, + config, + }) + + await linkEntityToPassportWithSignature(veBetterPassport, passport, entity1, 1000) + + await veBetterPassport.whitelist(entity1.address) + + expect(await veBetterPassport.isWhitelisted(entity1.address)).to.be.true + + expect(await veBetterPassport.isPassportWhitelisted(passport.address)).to.be.true + + // Remove entity1 from whitelist + await veBetterPassport.removeFromWhitelist(entity1.address) + + // Entity1 should no longer be whitelisted + expect(await veBetterPassport.isWhitelisted(entity1.address)).to.be.false + + // Passport should no longer be whitelisted + expect(await veBetterPassport.isPassportWhitelisted(passport.address)).to.be.false + }) + + it("If over the threshold amount of entities are blacklisted, passport should return blacklisted", async function () { + const config = createTestConfig() + config.VEPASSPORT_BLACKLIST_THRESHOLD_PERCENTAGE = 60 // 60% of entities are blacklisted + const { + veBetterPassport, + owner: passport, + otherAccount: entity1, + otherAccounts, + } = await getOrDeployContractInstances({ + forceDeploy: true, + config, + }) + + const entity2 = otherAccounts[2] + + await linkEntityToPassportWithSignature(veBetterPassport, passport, entity1, 1000) + + // 100% of entities are blacklisted + await veBetterPassport.blacklist(entity1.address) + + expect(await veBetterPassport.isBlacklisted(passport.address)).to.be.false + expect(await veBetterPassport.isBlacklisted(entity1.address)).to.be.true + expect(await veBetterPassport.isPassportBlacklisted(passport.address)).to.be.true + + // 50% of entities are blacklisted + await linkEntityToPassportWithSignature(veBetterPassport, passport, entity2, 1000) + + expect(await veBetterPassport.isBlacklisted(passport.address)).to.be.false + expect(await veBetterPassport.isBlacklisted(entity1.address)).to.be.true + expect(await veBetterPassport.isBlacklisted(entity2.address)).to.be.false + expect(await veBetterPassport.isPassportBlacklisted(passport.address)).to.be.false + + // Blacklist entity2 + await veBetterPassport.blacklist(entity2.address) + + expect(await veBetterPassport.isBlacklisted(passport.address)).to.be.false + expect(await veBetterPassport.isBlacklisted(entity1.address)).to.be.true + expect(await veBetterPassport.isBlacklisted(entity2.address)).to.be.true + expect(await veBetterPassport.isPassportBlacklisted(passport.address)).to.be.true + }) + }) + + describe("isPerson", function () { + it("Should return true if user is whitelisted", async function () { + const { veBetterPassport, owner, otherAccount } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + await veBetterPassport.connect(owner).toggleCheck(1) + + await veBetterPassport.connect(owner).whitelist(otherAccount.address) + + expect(await veBetterPassport.isPerson(otherAccount.address)).to.deep.equal([true, "User is whitelisted"]) + }) + + it("Should return false if user is blacklisted", async function () { + const { veBetterPassport, owner, otherAccount } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + await veBetterPassport.connect(owner).toggleCheck(2) + + await veBetterPassport.connect(owner).blacklist(otherAccount.address) + + expect(await veBetterPassport.isPerson(otherAccount.address)).to.deep.equal([false, "User is blacklisted"]) + }) + + it("Should return true if user does meet participation score threshold", async function () { + const config = createTestConfig() + const { veBetterPassport, owner, otherAccount, x2EarnApps, otherAccounts } = await getOrDeployContractInstances({ + forceDeploy: true, + config, + }) + + // Set the threshold to 100 + await veBetterPassport.connect(owner).setThresholdPoPScore(100) + + //Add apps + const app1Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[2].address)) + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[2].address, otherAccounts[2].address, otherAccounts[2].address, "metadataURI") + + // Bootstrap emissions + await bootstrapAndStartEmissions() + + await veBetterPassport.toggleCheck(4) + + await veBetterPassport.grantRole(await veBetterPassport.ACTION_REGISTRAR_ROLE(), owner) + + expect(await veBetterPassport.hasRole(await veBetterPassport.ACTION_REGISTRAR_ROLE(), owner.address)).to.be.true + + await veBetterPassport.connect(owner).setAppSecurity(app1Id, 1) // APP_SECURITY.LOW + + await veBetterPassport.connect(owner).registerAction(otherAccount, app1Id) + + expect(await veBetterPassport.userRoundScore(otherAccount, 1)).to.equal(100) + + expect(await veBetterPassport.getCumulativeScoreWithDecay(otherAccount, 1)).to.equal(100) + + expect(await veBetterPassport.isPerson(otherAccount.address)).to.deep.equal([ + true, + "User's participation score is above the threshold", + ]) + }) + + it("Should return false if user doesn't meet any valid personhood criteria", async function () { + const { veBetterPassport, otherAccount } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + 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() + config.VEPASSPORT_DECAY_RATE = 20 + const { + x2EarnApps, + owner, + otherAccount, + veBetterPassport, + otherAccounts, + b3tr, + B3trContract, + xAllocationVoting, + governor, + } = await getOrDeployContractInstances({ + forceDeploy: true, + config, + }) + + await getVot3Tokens(otherAccount, "10000") + await getVot3Tokens(owner, "10000") + + //Add apps + const app1Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[2].address)) + const app2Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[3].address)) + const app3Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[4].address)) + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[2].address, otherAccounts[2].address, otherAccounts[2].address, "metadataURI") + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[3].address, otherAccounts[3].address, otherAccounts[3].address, "metadataURI") + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[4].address, otherAccounts[4].address, otherAccounts[4].address, "metadataURI") + + // Set app security levels + await veBetterPassport.connect(owner).setAppSecurity(app1Id, 1) + await veBetterPassport.connect(owner).setAppSecurity(app2Id, 2) + await veBetterPassport.connect(owner).setAppSecurity(app3Id, 3) + + // Grant action registrar role + await veBetterPassport.grantRole(await veBetterPassport.ACTION_REGISTRAR_ROLE(), owner) + expect(await veBetterPassport.hasRole(await veBetterPassport.ACTION_REGISTRAR_ROLE(), owner.address)).to.be.true + + // Bootstrap emissions + await bootstrapAndStartEmissions() + + // Create a proposal for next round + // create a new proposal active from round 2 + const tx = await createProposal(b3tr, B3trContract, owner, "Get b3tr token details", "tokenDetails", [], 2) + + const proposalId = await getProposalIdFromTx(tx) + + // pay deposit + await payDeposit(proposalId.toString(), owner) + + // First round, participation score check is disabled + + // Register actions for round 1 + await veBetterPassport.connect(owner).registerAction(otherAccount, app1Id) + await veBetterPassport.connect(owner).registerAction(otherAccount, app2Id) + + // User's cumulative score = 100 (app1) + 200 (app2) = 300 + expect(await veBetterPassport.getCumulativeScoreWithDecay(otherAccount, 1)).to.equal(300) + + await veBetterPassport.toggleCheck(4) + + // Vote + // Note that `otherAccount` can vote because the participation score threshold is set to 0 + await xAllocationVoting + .connect(otherAccount) + .castVote( + 1, + [app1Id, app2Id, app3Id], + [ethers.parseEther("0"), ethers.parseEther("900"), ethers.parseEther("100")], + ) + + // Set minimum participation score to 500 + await veBetterPassport.setThresholdPoPScore(500) + + await waitForProposalToBeActive(proposalId) + + expect(await xAllocationVoting.currentRoundId()).to.equal(2) + + // User tries to vote both governance and x allocation voting but reverts due to not meeting the participation score threshold + await expect( + xAllocationVoting + .connect(otherAccount) + .castVote( + 2, + [app1Id, app2Id, app3Id], + [ethers.parseEther("0"), ethers.parseEther("900"), ethers.parseEther("100")], + ), + ).to.be.revertedWithCustomError(xAllocationVoting, "GovernorPersonhoodVerificationFailed") + + await expect(governor.connect(otherAccount).castVote(proposalId, 2)).to.be.revertedWithCustomError( + xAllocationVoting, + "GovernorPersonhoodVerificationFailed", + ) + + // Register actions for round 2 + await veBetterPassport.connect(owner).registerAction(otherAccount, app2Id) + await veBetterPassport.connect(owner).registerAction(otherAccount, app3Id) + + /* + User's cumulative score: + round 1 = 300 + round 2 = 600 + (300 * 0.8) = 840 + */ + expect(await veBetterPassport.getCumulativeScoreWithDecay(otherAccount, 2)).to.equal(840) + + // User now meets the participation score threshold and can vote + await xAllocationVoting + .connect(otherAccount) + .castVote( + 2, + [app1Id, app2Id, app3Id], + [ethers.parseEther("0"), ethers.parseEther("900"), ethers.parseEther("100")], + ) + + await governor.connect(otherAccount).castVote(proposalId, 2) + + // Increase participation score threshold to 1000 + await veBetterPassport.setThresholdPoPScore(1000) + + await waitForNextCycle() + + // Increase participation score threshold to 1000 + await veBetterPassport.setThresholdPoPScore(1000) + + await startNewAllocationRound() + + expect(await xAllocationVoting.currentRoundId()).to.equal(3) + + // User tries to vote x allocation voting but reverts due to not meeting the participation score threshold + await expect( + xAllocationVoting + .connect(otherAccount) + .castVote( + 3, + [app1Id, app2Id, app3Id], + [ethers.parseEther("0"), ethers.parseEther("900"), ethers.parseEther("100")], + ), + ).to.be.revertedWithCustomError(xAllocationVoting, "GovernorPersonhoodVerificationFailed") + + // Register action for round 3 + await veBetterPassport.connect(owner).registerAction(otherAccount, app1Id) + + /* + User's cumulative score: + round 1 = 300 + round 2 = 600 + (300 * 0.8) = 840 + round 3 = 100 + (840 * 0.8) = 772 + */ + expect(await veBetterPassport.getCumulativeScoreWithDecay(otherAccount, 3)).to.equal(772) + + // User still doesn't meet the participation score threshold and can't vote + await expect( + xAllocationVoting + .connect(otherAccount) + .castVote( + 3, + [app1Id, app2Id, app3Id], + [ethers.parseEther("0"), ethers.parseEther("900"), ethers.parseEther("100")], + ), + ).to.be.revertedWithCustomError(xAllocationVoting, "GovernorPersonhoodVerificationFailed") + + // register more actions for round 3 + await veBetterPassport.connect(owner).registerAction(otherAccount, app2Id) + await veBetterPassport.connect(owner).registerAction(otherAccount, app3Id) + + /* + User's cumulative score: + round 1 = 300 + round 2 = 600 + (300 * 0.8) = 840 + round 3 = 700 + (840 * 0.8) = 1072 + */ + expect(await veBetterPassport.getCumulativeScoreWithDecay(otherAccount, 3)).to.equal(1372) + + // User now meets the participation score threshold and can vote + await xAllocationVoting + .connect(otherAccount) + .castVote( + 3, + [app1Id, app2Id, app3Id], + [ethers.parseEther("0"), ethers.parseEther("900"), ethers.parseEther("100")], + ) + + // "Before linking passport should have 0" + expect(await veBetterPassport.getCumulativeScoreWithDecay(owner, 3)).to.equal(0) + + // Before linking passport should not be considered person + expect( + (await veBetterPassport.isPersonAtTimepoint(owner.address, await xAllocationVoting.roundSnapshot(3)))[0], + ).to.be.equal(false) + + // Delegate passport to owner and try to vote + await linkEntityToPassportWithSignature(veBetterPassport, owner, otherAccount, 3600) + // After linking "other account" should be entity + expect(await veBetterPassport.isEntity(otherAccount.address)).to.be.true + + // After linking owner should be passport + expect(await veBetterPassport.isPassport(owner.address)).to.be.true + + // After linking passport should not be considered person at the beginning of the round + expect( + (await veBetterPassport.isPersonAtTimepoint(owner.address, await xAllocationVoting.roundSnapshot(3)))[0], + ).to.be.equal(false) + + expect(await veBetterPassport.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( + xAllocationVoting + .connect(owner) + .castVote( + 3, + [app1Id, app2Id, app3Id], + [ethers.parseEther("0"), ethers.parseEther("900"), ethers.parseEther("100")], + ), + ).to.be.revertedWithCustomError(xAllocationVoting, "GovernorPersonhoodVerificationFailed") + await waitForNextCycle() + + await startNewAllocationRound() + + expect(await xAllocationVoting.currentRoundId()).to.equal(4) + + // 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 veBetterPassport.getCumulativeScoreWithDecay(otherAccount, 4)).to.equal(1097) + + // register more actions for round 4 (mixing entity and passport) + await veBetterPassport.connect(owner).registerAction(otherAccount, app2Id) + await veBetterPassport.connect(owner).registerAction(owner, app3Id) + await veBetterPassport.connect(owner).registerAction(owner, app3Id) + + // new points should be added to the passport, entity should not have any new points added + expect(await veBetterPassport.getCumulativeScoreWithDecay(otherAccount, 4)).to.equal(1097) + /* + Passport's cumulative score: + round 4 = 200 + 400 + 400 + */ + expect(await veBetterPassport.getCumulativeScoreWithDecay(owner, 4)).to.equal(1000) + + // Now that we reached threshold passport should be considered person + expect( + (await veBetterPassport.isPersonAtTimepoint(owner.address, await xAllocationVoting.roundSnapshot(4)))[0], + ).to.be.equal(true) + + // Owner can vote now + await xAllocationVoting + .connect(owner) + .castVote( + 4, + [app1Id, app2Id, app3Id], + [ethers.parseEther("0"), ethers.parseEther("900"), ethers.parseEther("100")], + ) + }) + + it("Should use checkpointed PoP score threshold for whole round regardless if PoP score changes", async function () { + const config = createTestConfig() + config.VEPASSPORT_DECAY_RATE = 20 + config.EMISSIONS_CYCLE_DURATION = 20 + const { + x2EarnApps, + owner, + otherAccount, + veBetterPassport, + otherAccounts, + b3tr, + B3trContract, + xAllocationVoting, + governor, + } = await getOrDeployContractInstances({ + forceDeploy: true, + config, + }) + + await getVot3Tokens(otherAccount, "10000") + await getVot3Tokens(owner, "10000") + + //Add apps + const app1Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[2].address)) + const app2Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[3].address)) + const app3Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[4].address)) + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[2].address, otherAccounts[2].address, otherAccounts[2].address, "metadataURI") + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[3].address, otherAccounts[3].address, otherAccounts[3].address, "metadataURI") + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[4].address, otherAccounts[4].address, otherAccounts[4].address, "metadataURI") + + // Set app security levels + await veBetterPassport.connect(owner).setAppSecurity(app1Id, 1) + await veBetterPassport.connect(owner).setAppSecurity(app2Id, 2) + await veBetterPassport.connect(owner).setAppSecurity(app3Id, 3) + + // Grant action registrar role + await veBetterPassport.grantRole(await veBetterPassport.ACTION_REGISTRAR_ROLE(), owner) + expect(await veBetterPassport.hasRole(await veBetterPassport.ACTION_REGISTRAR_ROLE(), owner.address)).to.be.true + + // Bootstrap emissions + await bootstrapAndStartEmissions() + + // Create a proposal for next round + // create a new proposal active from round 2 + const tx = await createProposal(b3tr, B3trContract, owner, "Get b3tr token details", "tokenDetails", [], 2) + + const proposalId = await getProposalIdFromTx(tx) + + // pay deposit + await payDeposit(proposalId.toString(), owner) + + // First round, participation score check is disabled + + // Register actions for round 1 + await veBetterPassport.connect(owner).registerAction(otherAccount, app1Id) + await veBetterPassport.connect(owner).registerAction(otherAccount, app2Id) + + // User's cumulative score = 100 (app1) + 200 (app2) = 300 + expect(await veBetterPassport.getCumulativeScoreWithDecay(otherAccount, 1)).to.equal(300) + + await veBetterPassport.toggleCheck(4) + + // Vote + // Note that `otherAccount` can vote because the participation score threshold is set to 0 + await xAllocationVoting + .connect(otherAccount) + .castVote( + 1, + [app1Id, app2Id, app3Id], + [ethers.parseEther("0"), ethers.parseEther("900"), ethers.parseEther("100")], + ) + + // Set minimum participation score to 500, which will take effect from the next round + await veBetterPassport.setThresholdPoPScore(500) + + // owmer has a threshold of 0 but can vote because the threshold is still 0 for the round + await xAllocationVoting + .connect(owner) + .castVote( + 1, + [app1Id, app2Id, app3Id], + [ethers.parseEther("0"), ethers.parseEther("900"), ethers.parseEther("100")], + ) + + await waitForProposalToBeActive(proposalId) + + // New round has started and the threshold is now 500 + expect(await xAllocationVoting.currentRoundId()).to.equal(2) + + // User tries to vote both governance and x allocation voting but reverts due to not meeting the participation score threshold + await expect( + xAllocationVoting + .connect(otherAccount) + .castVote( + 2, + [app1Id, app2Id, app3Id], + [ethers.parseEther("0"), ethers.parseEther("900"), ethers.parseEther("100")], + ), + ).to.be.revertedWithCustomError(xAllocationVoting, "GovernorPersonhoodVerificationFailed") + + await expect(governor.connect(otherAccount).castVote(proposalId, 2)).to.be.revertedWithCustomError( + xAllocationVoting, + "GovernorPersonhoodVerificationFailed", + ) + + // Register actions for round 2 + await veBetterPassport.connect(owner).registerAction(otherAccount, app2Id) + await veBetterPassport.connect(owner).registerAction(otherAccount, app3Id) + + /* + User's cumulative score: + round 1 = 300 + round 2 = 600 + (300 * 0.8) = 840 + */ + expect(await veBetterPassport.getCumulativeScoreWithDecay(otherAccount, 2)).to.equal(840) + + // User now meets the participation score threshold and can vote + await xAllocationVoting + .connect(otherAccount) + .castVote( + 2, + [app1Id, app2Id, app3Id], + [ethers.parseEther("0"), ethers.parseEther("900"), ethers.parseEther("100")], + ) + + // Owner can not as threshold is 500 + await expect( + xAllocationVoting + .connect(owner) + .castVote( + 2, + [app1Id, app2Id, app3Id], + [ethers.parseEther("0"), ethers.parseEther("900"), ethers.parseEther("100")], + ), + ).to.be.revertedWithCustomError(xAllocationVoting, "GovernorPersonhoodVerificationFailed") + + await governor.connect(otherAccount).castVote(proposalId, 2) + + await waitForNextCycle() + + await startNewAllocationRound() + + expect(await xAllocationVoting.currentRoundId()).to.equal(3) + + // Increase participation score threshold to 1000 + await veBetterPassport.setThresholdPoPScore(1000) + + // User tries to vote x allocation voting and can vote because the threshold is still 500 for the round + await expect( + xAllocationVoting + .connect(otherAccount) + .castVote( + 3, + [app1Id, app2Id, app3Id], + [ethers.parseEther("0"), ethers.parseEther("900"), ethers.parseEther("100")], + ), + ).to.not.be.reverted + + await waitForNextCycle() + + await startNewAllocationRound() + + // User tries to vote x allocation voting and can't vote because the threshold is now 1000 for the round + await expect( + xAllocationVoting + .connect(otherAccount) + .castVote( + 4, + [app1Id, app2Id, app3Id], + [ethers.parseEther("0"), ethers.parseEther("900"), ethers.parseEther("100")], + ), + ).to.be.revertedWithCustomError(xAllocationVoting, "GovernorPersonhoodVerificationFailed") + + // Register action for round 4 + await veBetterPassport.connect(owner).registerAction(otherAccount, app1Id) + + expect(await veBetterPassport.getCumulativeScoreWithDecay(otherAccount, 4)).to.equal(637) + + // User still doesn't meet the participation score threshold and can't vote + await expect( + xAllocationVoting + .connect(otherAccount) + .castVote( + 4, + [app1Id, app2Id, app3Id], + [ethers.parseEther("0"), ethers.parseEther("900"), ethers.parseEther("100")], + ), + ).to.be.revertedWithCustomError(xAllocationVoting, "GovernorPersonhoodVerificationFailed") + + // register more actions for round 4 + await veBetterPassport.connect(owner).registerAction(otherAccount, app2Id) + await veBetterPassport.connect(owner).registerAction(otherAccount, app3Id) + + expect(await veBetterPassport.getCumulativeScoreWithDecay(otherAccount, 4)).to.equal(1237) + + // User now meets the participation score threshold and can vote + await xAllocationVoting + .connect(otherAccount) + .castVote( + 4, + [app1Id, app2Id, app3Id], + [ethers.parseEther("0"), ethers.parseEther("900"), ethers.parseEther("100")], + ) + }) + }) +}) diff --git a/test/VoterRewards.test.ts b/test/VoterRewards.test.ts index bd87b1a..e5b5dc0 100644 --- a/test/VoterRewards.test.ts +++ b/test/VoterRewards.test.ts @@ -24,10 +24,10 @@ import { ethers } from "hardhat" import { createLocalConfig } from "../config/contracts/envs/local" import { createTestConfig } from "./helpers/config" import { getImplementationAddress } from "@openzeppelin/upgrades-core" -import { deployProxy, upgradeProxy } from "../scripts/helpers" +import { deployAndUpgrade, deployProxy, upgradeProxy } from "../scripts/helpers" import { B3TRGovernor, GalaxyMember, VoterRewards, VoterRewardsV1, XAllocationVoting } from "../typechain-types" -describe("VoterRewards", () => { +describe("VoterRewards - @shard2", () => { describe("Contract parameters", () => { it("Should have correct parameters set on deployment", async () => { const { voterRewards, owner, galaxyMember, emissions } = await getOrDeployContractInstances({ forceDeploy: true }) @@ -452,6 +452,23 @@ describe("VoterRewards", () => { governorQuorumLogicLib, governorStateLogicLib, governorVotesLogicLib, + governorClockLogicLibV1, + governorConfiguratorLibV1, + governorDepositLogicLibV1, + governorFunctionRestrictionsLogicLibV1, + governorProposalLogicLibV1, + governorQuorumLogicLibV1, + governorStateLogicLibV1, + governorVotesLogicLibV1, + governorClockLogicLibV3, + governorConfiguratorLibV3, + governorDepositLogicLibV3, + governorFunctionRestrictionsLogicLibV3, + governorProposalLogicLibV3, + governorQuorumLogicLibV3, + governorStateLogicLibV3, + governorVotesLogicLibV3, + veBetterPassport, } = await getOrDeployContractInstances({ forceDeploy: true, }) @@ -468,57 +485,106 @@ describe("VoterRewards", () => { ])) as VoterRewardsV1 // Deploy XAllocationVoting - const xAllocationVoting = (await deployProxy("XAllocationVoting", [ + const xAllocationVoting = (await deployAndUpgrade( + ["XAllocationVotingV1", "XAllocationVoting"], + [ + [ + { + vot3Token: await vot3.getAddress(), + quorumPercentage: config.X_ALLOCATION_VOTING_QUORUM_PERCENTAGE, // quorum percentage + initialVotingPeriod: config.EMISSIONS_CYCLE_DURATION - 1, // X Alloc voting period + timeLock: await timeLock.getAddress(), + voterRewards: await voterRewardsV1.getAddress(), + emissions: await emissions.getAddress(), + admins: [await timeLock.getAddress(), owner.address], + upgrader: owner.address, + contractsAddressManager: owner.address, + x2EarnAppsAddress: await x2EarnApps.getAddress(), + baseAllocationPercentage: config.X_ALLOCATION_POOL_BASE_ALLOCATION_PERCENTAGE, + appSharesCap: config.X_ALLOCATION_POOL_APP_SHARES_MAX_CAP, + votingThreshold: config.X_ALLOCATION_VOTING_VOTING_THRESHOLD, + }, + ], + [await veBetterPassport.getAddress()], + ], { - vot3Token: await vot3.getAddress(), - quorumPercentage: config.X_ALLOCATION_VOTING_QUORUM_PERCENTAGE, // quorum percentage - initialVotingPeriod: config.EMISSIONS_CYCLE_DURATION - 1, // X Alloc voting period - timeLock: await timeLock.getAddress(), - voterRewards: await voterRewardsV1.getAddress(), - emissions: await emissions.getAddress(), - admins: [await timeLock.getAddress(), owner.address], - upgrader: owner.address, - contractsAddressManager: owner.address, - x2EarnAppsAddress: await x2EarnApps.getAddress(), - baseAllocationPercentage: config.X_ALLOCATION_POOL_BASE_ALLOCATION_PERCENTAGE, - appSharesCap: config.X_ALLOCATION_POOL_APP_SHARES_MAX_CAP, - votingThreshold: config.X_ALLOCATION_VOTING_VOTING_THRESHOLD, + versions: [undefined, 2], }, - ])) as XAllocationVoting + )) as XAllocationVoting // Deploy Governor - const governor = (await deployProxy( - "B3TRGovernor", + const governor = (await deployAndUpgrade( + ["B3TRGovernorV1", "B3TRGovernorV2", "B3TRGovernorV3", "B3TRGovernor"], [ - { - vot3Token: await vot3.getAddress(), - timelock: await timeLock.getAddress(), - xAllocationVoting: await xAllocationVoting.getAddress(), - b3tr: await b3tr.getAddress(), - quorumPercentage: config.B3TR_GOVERNOR_QUORUM_PERCENTAGE, // quorum percentage - initialDepositThreshold: config.B3TR_GOVERNOR_DEPOSIT_THRESHOLD, // deposit threshold - initialMinVotingDelay: config.B3TR_GOVERNOR_MIN_VOTING_DELAY, // delay before vote starts - initialVotingThreshold: config.B3TR_GOVERNOR_VOTING_THRESHOLD, // voting threshold - voterRewards: await voterRewardsV1.getAddress(), - isFunctionRestrictionEnabled: true, - }, - { - governorAdmin: owner.address, - pauser: owner.address, - contractsAddressManager: owner.address, - proposalExecutor: owner.address, - governorFunctionSettingsRoleAddress: owner.address, - }, + [ + { + vot3Token: await vot3.getAddress(), + timelock: await timeLock.getAddress(), + xAllocationVoting: await xAllocationVoting.getAddress(), + b3tr: await b3tr.getAddress(), + quorumPercentage: config.B3TR_GOVERNOR_QUORUM_PERCENTAGE, // quorum percentage + initialDepositThreshold: config.B3TR_GOVERNOR_DEPOSIT_THRESHOLD, // deposit threshold + initialMinVotingDelay: config.B3TR_GOVERNOR_MIN_VOTING_DELAY, // delay before vote starts + initialVotingThreshold: config.B3TR_GOVERNOR_VOTING_THRESHOLD, // voting threshold + voterRewards: await voterRewardsV1.getAddress(), + isFunctionRestrictionEnabled: true, + }, + { + governorAdmin: owner.address, + pauser: owner.address, + contractsAddressManager: owner.address, + proposalExecutor: owner.address, + governorFunctionSettingsRoleAddress: owner.address, + }, + ], + [], + [], + [await veBetterPassport.getAddress()], ], { - GovernorClockLogic: await governorClockLogicLib.getAddress(), - GovernorConfigurator: await governorConfiguratorLib.getAddress(), - GovernorDepositLogic: await governorDepositLogicLib.getAddress(), - GovernorFunctionRestrictionsLogic: await governorFunctionRestrictionsLogicLib.getAddress(), - GovernorProposalLogic: await governorProposalLogicLib.getAddress(), - GovernorQuorumLogic: await governorQuorumLogicLib.getAddress(), - GovernorStateLogic: await governorStateLogicLib.getAddress(), - GovernorVotesLogic: await governorVotesLogicLib.getAddress(), + versions: [undefined, 2, 3, 4], + libraries: [ + { + GovernorClockLogicV1: await governorClockLogicLibV1.getAddress(), + GovernorConfiguratorV1: await governorConfiguratorLibV1.getAddress(), + GovernorDepositLogicV1: await governorDepositLogicLibV1.getAddress(), + GovernorFunctionRestrictionsLogicV1: await governorFunctionRestrictionsLogicLibV1.getAddress(), + GovernorProposalLogicV1: await governorProposalLogicLibV1.getAddress(), + GovernorQuorumLogicV1: await governorQuorumLogicLibV1.getAddress(), + GovernorStateLogicV1: await governorStateLogicLibV1.getAddress(), + GovernorVotesLogicV1: await governorVotesLogicLibV1.getAddress(), + }, + { + GovernorClockLogicV1: await governorClockLogicLibV1.getAddress(), + GovernorConfiguratorV1: await governorConfiguratorLibV1.getAddress(), + GovernorDepositLogicV1: await governorDepositLogicLibV1.getAddress(), + GovernorFunctionRestrictionsLogicV1: await governorFunctionRestrictionsLogicLibV1.getAddress(), + GovernorProposalLogicV1: await governorProposalLogicLibV1.getAddress(), + GovernorQuorumLogicV1: await governorQuorumLogicLibV1.getAddress(), + GovernorStateLogicV1: await governorStateLogicLibV1.getAddress(), + GovernorVotesLogicV1: await governorVotesLogicLibV1.getAddress(), + }, + { + GovernorClockLogicV3: await governorClockLogicLibV3.getAddress(), + GovernorConfiguratorV3: await governorConfiguratorLibV3.getAddress(), + GovernorDepositLogicV3: await governorDepositLogicLibV3.getAddress(), + GovernorFunctionRestrictionsLogicV3: await governorFunctionRestrictionsLogicLibV3.getAddress(), + GovernorProposalLogicV3: await governorProposalLogicLibV3.getAddress(), + GovernorQuorumLogicV3: await governorQuorumLogicLibV3.getAddress(), + GovernorStateLogicV3: await governorStateLogicLibV3.getAddress(), + GovernorVotesLogicV3: await governorVotesLogicLibV3.getAddress(), + }, + { + GovernorClockLogic: await governorClockLogicLib.getAddress(), + GovernorConfigurator: await governorConfiguratorLib.getAddress(), + GovernorDepositLogic: await governorDepositLogicLib.getAddress(), + GovernorFunctionRestrictionsLogic: await governorFunctionRestrictionsLogicLib.getAddress(), + GovernorProposalLogic: await governorProposalLogicLib.getAddress(), + GovernorQuorumLogic: await governorQuorumLogicLib.getAddress(), + GovernorStateLogic: await governorStateLogicLib.getAddress(), + GovernorVotesLogic: await governorVotesLogicLib.getAddress(), + }, + ], }, )) as B3TRGovernor @@ -581,9 +647,12 @@ describe("VoterRewards", () => { expect(await xAllocationVoting.roundDeadline(roundId)).to.lt(await emissions.getNextCycleBlock()) - await xAllocationVoting - .connect(voter1) - .castVote(roundId, [app1, app2], [ethers.parseEther("1000"), ethers.parseEther("0")]) + await veBetterPassport.whitelist(voter1.address) + await veBetterPassport.whitelist(voter2.address) + await veBetterPassport.whitelist(voter3.address) + await veBetterPassport.toggleCheck(1) + + await xAllocationVoting.connect(voter1).castVote(roundId, [app1], [ethers.parseEther("1000")]) await xAllocationVoting .connect(voter2) .castVote(roundId, [app1, app2], [ethers.parseEther("200"), ethers.parseEther("100")]) @@ -697,9 +766,7 @@ describe("VoterRewards", () => { expect(await xAllocationVoting.roundDeadline(roundId)).to.lt(await emissions.getNextCycleBlock()) - await xAllocationVoting - .connect(voter1) - .castVote(roundId2, [app1, app2], [ethers.parseEther("0"), ethers.parseEther("1000")]) + await xAllocationVoting.connect(voter1).castVote(roundId2, [app2], [ethers.parseEther("1000")]) await xAllocationVoting .connect(voter2) .castVote(roundId2, [app1, app2], [ethers.parseEther("100"), ethers.parseEther("500")]) @@ -797,6 +864,7 @@ describe("VoterRewards", () => { b3tr, minterAccount, x2EarnApps, + veBetterPassport, } = await getOrDeployContractInstances({ forceDeploy: true, config, @@ -813,6 +881,11 @@ describe("VoterRewards", () => { const voter2 = otherAccounts[3] const voter3 = otherAccounts[4] + await veBetterPassport.whitelist(otherAccount.address) + await veBetterPassport.whitelist(voter2.address) + await veBetterPassport.whitelist(voter3.address) + await veBetterPassport.toggleCheck(1) + await getVot3Tokens(otherAccount, "1000") await getVot3Tokens(voter2, "1000") await getVot3Tokens(voter3, "1000") @@ -984,6 +1057,7 @@ describe("VoterRewards", () => { b3tr, minterAccount, x2EarnApps, + veBetterPassport, } = await getOrDeployContractInstances({ forceDeploy: true, }) @@ -1004,6 +1078,11 @@ describe("VoterRewards", () => { const voter2 = otherAccounts[3] const voter3 = otherAccounts[4] + await veBetterPassport.whitelist(otherAccount.address) + await veBetterPassport.whitelist(voter2.address) + await veBetterPassport.whitelist(voter3.address) + await veBetterPassport.toggleCheck(1) + await getVot3Tokens(otherAccount, "1000") await getVot3Tokens(voter2, "1000") await getVot3Tokens(voter3, "1000") @@ -1173,6 +1252,7 @@ describe("VoterRewards", () => { b3tr, minterAccount, x2EarnApps, + veBetterPassport, } = await getOrDeployContractInstances({ forceDeploy: true, }) @@ -1188,6 +1268,11 @@ describe("VoterRewards", () => { const voter2 = otherAccounts[3] const voter3 = otherAccounts[4] + await veBetterPassport.whitelist(voter1.address) + await veBetterPassport.whitelist(voter2.address) + await veBetterPassport.whitelist(voter3.address) + await veBetterPassport.toggleCheck(1) + await getVot3Tokens(voter1, "1000") await getVot3Tokens(voter2, "1000") await getVot3Tokens(voter3, "1000") @@ -1392,6 +1477,7 @@ describe("VoterRewards", () => { minterAccount, governor, treasury, + veBetterPassport, x2EarnApps, } = await getOrDeployContractInstances({ forceDeploy: true, @@ -1435,6 +1521,11 @@ describe("VoterRewards", () => { await getVot3Tokens(voter2, "1000") await getVot3Tokens(voter3, "1000") + await veBetterPassport.whitelist(voter1.address) + await veBetterPassport.whitelist(voter2.address) + await veBetterPassport.whitelist(voter3.address) + await veBetterPassport.toggleCheck(1) + // Bootstrap emissions await bootstrapEmissions() @@ -1521,6 +1612,7 @@ describe("VoterRewards", () => { governor, treasury, x2EarnApps, + veBetterPassport, } = await getOrDeployContractInstances({ forceDeploy: true, }) @@ -1559,6 +1651,11 @@ describe("VoterRewards", () => { const voter2 = otherAccounts[3] const voter3 = otherAccounts[4] + await veBetterPassport.whitelist(voter1.address) + await veBetterPassport.whitelist(voter2.address) + await veBetterPassport.whitelist(voter3.address) + await veBetterPassport.toggleCheck(1) + await getVot3Tokens(voter1, "1000") await getVot3Tokens(voter2, "1000") await getVot3Tokens(voter3, "1000") @@ -1645,6 +1742,7 @@ describe("VoterRewards", () => { b3tr, minterAccount, governor, + veBetterPassport, treasury, x2EarnApps, } = await getOrDeployContractInstances({ @@ -1690,6 +1788,11 @@ describe("VoterRewards", () => { await getVot3Tokens(voter2, "1000") await getVot3Tokens(voter3, "1000") + await veBetterPassport.whitelist(voter1.address) + await veBetterPassport.whitelist(voter2.address) + await veBetterPassport.whitelist(voter3.address) + await veBetterPassport.toggleCheck(1) + // Bootstrap emissions await bootstrapEmissions() @@ -1806,6 +1909,7 @@ describe("VoterRewards", () => { minterAccount, governor, treasury, + veBetterPassport, x2EarnApps, } = await getOrDeployContractInstances({ forceDeploy: true, @@ -1850,6 +1954,11 @@ describe("VoterRewards", () => { await getVot3Tokens(voter2, "1000") await getVot3Tokens(voter3, "1000") + await veBetterPassport.whitelist(voter1.address) + await veBetterPassport.whitelist(voter2.address) + await veBetterPassport.whitelist(voter3.address) + await veBetterPassport.toggleCheck(1) + // Bootstrap emissions await bootstrapEmissions() @@ -1935,6 +2044,7 @@ describe("VoterRewards", () => { minterAccount, governor, treasury, + veBetterPassport, x2EarnApps, } = await getOrDeployContractInstances({ forceDeploy: true, @@ -1979,6 +2089,11 @@ describe("VoterRewards", () => { await getVot3Tokens(voter2, "1000") await getVot3Tokens(voter3, "1000") + await veBetterPassport.whitelist(voter1.address) + await veBetterPassport.whitelist(voter2.address) + await veBetterPassport.whitelist(voter3.address) + await veBetterPassport.toggleCheck(1) + // Bootstrap emissions await bootstrapEmissions() @@ -2117,7 +2232,7 @@ describe("VoterRewards", () => { }) it("Should revert if vote is registered by non vote registrar", async () => { - const { voterRewards, otherAccount, xAllocationVoting, emissions, minterAccount } = + const { voterRewards, otherAccount, xAllocationVoting, emissions, minterAccount, veBetterPassport } = await getOrDeployContractInstances({ forceDeploy: true, }) @@ -2131,6 +2246,9 @@ describe("VoterRewards", () => { const proposalStart = await xAllocationVoting.roundSnapshot(roundId) + await veBetterPassport.whitelist(otherAccount.address) + await veBetterPassport.toggleCheck(1) + await expect( voterRewards .connect(otherAccount) @@ -2152,9 +2270,10 @@ describe("VoterRewards", () => { }) it("Should not register any vote if voting power is zero", async () => { - const { voterRewards, otherAccount, owner, emissions, minterAccount } = await getOrDeployContractInstances({ - forceDeploy: true, - }) + const { voterRewards, otherAccount, owner, emissions, minterAccount, veBetterPassport } = + await getOrDeployContractInstances({ + forceDeploy: true, + }) await voterRewards.connect(owner).grantRole(await voterRewards.VOTE_REGISTRAR_ROLE(), otherAccount.address) @@ -2162,6 +2281,9 @@ describe("VoterRewards", () => { await emissions.connect(minterAccount).start() + await veBetterPassport.whitelist(otherAccount.address) + await veBetterPassport.toggleCheck(1) + const totalVotesBefore = await voterRewards.cycleToTotal(1) await voterRewards @@ -2185,6 +2307,7 @@ describe("VoterRewards", () => { b3tr, governor, B3trContract, + veBetterPassport, emissions, voterRewards, minterAccount, @@ -2205,6 +2328,10 @@ describe("VoterRewards", () => { await getVot3Tokens(voter2, "1000") await getVot3Tokens(proposar, "1000") + await veBetterPassport.whitelist(voter1.address) + await veBetterPassport.whitelist(voter2.address) + await veBetterPassport.toggleCheck(1) + // Now we can create a new proposal const tx = await createProposal(b3tr, B3trContract, proposar, description, functionToCall, []) const proposalId = await getProposalIdFromTx(tx) @@ -2241,6 +2368,7 @@ describe("VoterRewards", () => { b3tr, governor, B3trContract, + veBetterPassport, voterRewards, } = await getOrDeployContractInstances({ forceDeploy: true, @@ -2253,6 +2381,10 @@ describe("VoterRewards", () => { await getVot3Tokens(voter1, "1000") + await veBetterPassport.whitelist(voter1.address) + await veBetterPassport.whitelist(voter2.address) + await veBetterPassport.toggleCheck(1) + // Now we can create a new proposal const tx = await createProposal(b3tr, B3trContract, voter1, description, functionToCall, []) const proposalId = await getProposalIdFromTx(tx) @@ -2285,6 +2417,7 @@ describe("VoterRewards", () => { emissions, minterAccount, owner, + veBetterPassport, voterRewards, treasury, xAllocationVoting, @@ -2322,6 +2455,10 @@ describe("VoterRewards", () => { const voter2 = otherAccounts[1] const proposar = otherAccounts[2] + await veBetterPassport.whitelist(voter1.address) + await veBetterPassport.whitelist(voter2.address) + await veBetterPassport.toggleCheck(1) + await getVot3Tokens(voter1, "1000") await getVot3Tokens(voter2, "1000") await getVot3Tokens(proposar, "2000") @@ -2392,6 +2529,7 @@ describe("VoterRewards", () => { b3tr, governor, B3trContract, + veBetterPassport, emissions, minterAccount, owner, @@ -2449,6 +2587,11 @@ describe("VoterRewards", () => { await getVot3Tokens(voter3, "1000") await getVot3Tokens(proposar, "2000") + await veBetterPassport.whitelist(voter1.address) + await veBetterPassport.whitelist(voter2.address) + await veBetterPassport.whitelist(voter3.address) + await veBetterPassport.toggleCheck(1) + // Bootstrap emissions await bootstrapAndStartEmissions() // round 1 @@ -2561,6 +2704,7 @@ describe("VoterRewards", () => { owner, voterRewards, xAllocationVoting, + veBetterPassport, treasury, x2EarnApps, } = await getOrDeployContractInstances({ @@ -2613,6 +2757,11 @@ describe("VoterRewards", () => { const voter3 = otherAccounts[2] const proposar = otherAccounts[3] + await veBetterPassport.whitelist(voter1.address) + await veBetterPassport.whitelist(voter2.address) + await veBetterPassport.whitelist(voter3.address) + await veBetterPassport.toggleCheck(1) + await getVot3Tokens(voter1, "1000") await getVot3Tokens(voter2, "1000") await getVot3Tokens(voter3, "1000") @@ -2731,6 +2880,7 @@ describe("VoterRewards", () => { xAllocationVoting, treasury, x2EarnApps, + veBetterPassport, } = await getOrDeployContractInstances({ forceDeploy: true, config: { @@ -2781,6 +2931,11 @@ describe("VoterRewards", () => { await getVot3Tokens(voter3, "1000") await getVot3Tokens(proposar, "2000") + await veBetterPassport.whitelist(voter1.address) + await veBetterPassport.whitelist(voter2.address) + await veBetterPassport.whitelist(voter3.address) + await veBetterPassport.toggleCheck(1) + // Bootstrap emissions await bootstrapAndStartEmissions() // round 1 @@ -2897,6 +3052,7 @@ describe("VoterRewards", () => { minterAccount, owner, voterRewards, + veBetterPassport, xAllocationVoting, treasury, x2EarnApps, @@ -2952,6 +3108,11 @@ describe("VoterRewards", () => { await getVot3Tokens(voter3, "1000") await getVot3Tokens(proposar, "2000") + await veBetterPassport.whitelist(voter1.address) + await veBetterPassport.whitelist(voter2.address) + await veBetterPassport.whitelist(voter3.address) + await veBetterPassport.toggleCheck(1) + // Bootstrap emissions await bootstrapAndStartEmissions() // round 1 @@ -3057,4 +3218,4 @@ describe("VoterRewards", () => { expect(await voterRewards.getReward(xAllocationsRoundID, voter3.address)).to.equal(285714285714285714285714n) }) }) -}) \ No newline at end of file +}) diff --git a/test/X2EarnRewardsPool.test.ts b/test/X2EarnRewardsPool.test.ts index 3c9b7ba..23320d6 100644 --- a/test/X2EarnRewardsPool.test.ts +++ b/test/X2EarnRewardsPool.test.ts @@ -1,13 +1,20 @@ import { ethers } from "hardhat" import { expect } from "chai" -import { ZERO_ADDRESS, catchRevert, filterEventsByName, getOrDeployContractInstances } from "./helpers" +import { + ZERO_ADDRESS, + catchRevert, + filterEventsByName, + getOrDeployContractInstances, + waitForRoundToEnd, +} from "./helpers" import { describe, it } from "mocha" import { getImplementationAddress } from "@openzeppelin/upgrades-core" import { deployProxy, upgradeProxy } from "../scripts/helpers" -import { X2EarnRewardsPool, X2EarnRewardsPoolV1 } from "../typechain-types" +import { X2EarnRewardsPool, X2EarnRewardsPoolV2 } from "../typechain-types" +import { X2EarnRewardsPoolV1 } from "../typechain-types/contracts/deprecated/V1" import { createLocalConfig } from "../config/contracts/envs/local" -describe("X2EarnRewardsPool", function () { +describe("X2EarnRewardsPool - @shard3", function () { // deployment describe("Deployment", function () { it("Cannot deploy contract with zero address", async function () { @@ -47,8 +54,10 @@ describe("X2EarnRewardsPool", function () { }) it("Version should be set correctly", async function () { - const { x2EarnRewardsPool } = await getOrDeployContractInstances({ forceDeploy: false }) - expect(await x2EarnRewardsPool.version()).to.equal("2") + const { x2EarnRewardsPool } = await getOrDeployContractInstances({ + forceDeploy: false, + }) + expect(await x2EarnRewardsPool.version()).to.equal("3") }) it("X2EarnApps should be set correctly", async function () { @@ -130,7 +139,7 @@ describe("X2EarnRewardsPool", function () { it("Storage should be preserved after upgrade", async () => { const config = createLocalConfig() - const { owner, b3tr, x2EarnApps, minterAccount } = await getOrDeployContractInstances({ + const { owner, b3tr, x2EarnApps, minterAccount, veBetterPassport } = await getOrDeployContractInstances({ forceDeploy: true, config, }) @@ -166,17 +175,32 @@ describe("X2EarnRewardsPool", function () { // upgrade to new version const x2EarnRewardsPoolV2 = (await upgradeProxy( "X2EarnRewardsPoolV1", - "X2EarnRewardsPool", + "X2EarnRewardsPoolV2", await x2EarnRewardsPoolV1.getAddress(), [owner.address, config.X_2_EARN_INITIAL_IMPACT_KEYS], { version: 2, }, - )) as X2EarnRewardsPool + )) as X2EarnRewardsPoolV2 expect(await x2EarnRewardsPoolV2.version()).to.equal("2") expect(await x2EarnRewardsPoolV2.x2EarnApps()).to.equal(x2EarnAppsAddress) expect(await x2EarnRewardsPoolV2.availableFunds(await x2EarnApps.hashAppName("My app"))).to.equal(amount) + + // upgrade to new version + const x2EarnRewardsPoolV3 = (await upgradeProxy( + "X2EarnRewardsPoolV2", + "X2EarnRewardsPool", + await x2EarnRewardsPoolV1.getAddress(), + [await veBetterPassport.getAddress()], + { + version: 3, + }, + )) as X2EarnRewardsPool + + expect(await x2EarnRewardsPoolV3.version()).to.equal("3") + expect(await x2EarnRewardsPoolV3.x2EarnApps()).to.equal(x2EarnAppsAddress) + expect(await x2EarnRewardsPoolV3.availableFunds(await x2EarnApps.hashAppName("My app"))).to.equal(amount) }) it("Should not be able to upgrade if initial impact keys is empty", async () => { @@ -217,7 +241,7 @@ describe("X2EarnRewardsPool", function () { await expect( upgradeProxy( "X2EarnRewardsPoolV1", - "X2EarnRewardsPool", + "X2EarnRewardsPoolV2", await x2EarnRewardsPoolV1.getAddress(), [owner.address, []], { @@ -225,6 +249,90 @@ describe("X2EarnRewardsPool", function () { }, ), ).to.be.reverted + + await expect( + upgradeProxy( + "X2EarnRewardsPoolV1", + "X2EarnRewardsPoolV2", + await x2EarnRewardsPoolV1.getAddress(), + [owner.address, ["impact"]], + { + version: 2, + }, + ), + ).to.not.be.reverted + }) + + it("Should not be able to upgrade to V3 if veBetterPassport address is empty", async () => { + const config = createLocalConfig() + const { owner, b3tr, x2EarnApps, minterAccount } = await getOrDeployContractInstances({ + forceDeploy: true, + config, + }) + + const x2EarnRewardsPoolV1 = (await deployProxy("X2EarnRewardsPoolV1", [ + owner.address, + owner.address, + owner.address, + await b3tr.getAddress(), + await x2EarnApps.getAddress(), + ])) as X2EarnRewardsPoolV1 + + expect(await x2EarnRewardsPoolV1.version()).to.equal("1") + + // update x2EarnApps address + await x2EarnRewardsPoolV1.connect(owner).setX2EarnApps(await x2EarnApps.getAddress()) + + // deposit some funds + const amount = ethers.parseEther("100") + + await b3tr.connect(minterAccount).mint(owner.address, amount) + + // create app + await x2EarnApps.addApp(owner.address, owner.address, "My app", "metadataURI") + await x2EarnApps.addApp(owner.address, owner.address, "My app #2", "metadataURI") + + await b3tr.connect(owner).approve(await x2EarnRewardsPoolV1.getAddress(), amount) + await x2EarnRewardsPoolV1.connect(owner).deposit(amount, await x2EarnApps.hashAppName("My app")) + + expect(await b3tr.balanceOf(await x2EarnRewardsPoolV1.getAddress())).to.equal(amount) + + // upgrade to new version + await expect( + upgradeProxy( + "X2EarnRewardsPoolV1", + "X2EarnRewardsPoolV2", + await x2EarnRewardsPoolV1.getAddress(), + [owner.address, config.X_2_EARN_INITIAL_IMPACT_KEYS], + { + version: 2, + }, + ), + ).to.not.be.reverted + + await expect( + upgradeProxy( + "X2EarnRewardsPoolV2", + "X2EarnRewardsPool", + await x2EarnRewardsPoolV1.getAddress(), + [ZERO_ADDRESS], + { + version: 3, + }, + ), + ).to.be.reverted + + await expect( + upgradeProxy( + "X2EarnRewardsPoolV2", + "X2EarnRewardsPool", + await x2EarnRewardsPoolV1.getAddress(), + [owner.address], + { + version: 3, + }, + ), + ).to.not.be.reverted }) }) @@ -1628,4 +1736,171 @@ describe("X2EarnRewardsPool", function () { expect(onchainGeneratedProof).to.have.deep.property("impact", { carbon: 100, water: 200 }) }) }) + + it("Can register action in VeBetterPassport", async function () { + const { + x2EarnRewardsPool, + x2EarnApps, + xAllocationVoting, + veBetterPassport, + b3tr, + owner, + otherAccounts, + minterAccount, + } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + const teamWallet = otherAccounts[10] + const user = otherAccounts[11] + const amount = ethers.parseEther("100") + + await b3tr.connect(minterAccount).mint(owner.address, amount) + + await x2EarnApps.addApp(teamWallet.address, owner.address, "My app", "metadataURI") + const appId = await x2EarnApps.hashAppName("My app") + + await x2EarnApps.connect(owner).addRewardDistributor(appId, owner.address) + expect(await x2EarnApps.isRewardDistributor(appId, owner.address)).to.equal(true) + + // fill the pool + await b3tr.connect(owner).approve(await x2EarnRewardsPool.getAddress(), amount) + await x2EarnRewardsPool.connect(owner).deposit(amount, appId) + + // start round + await xAllocationVoting.connect(owner).startNewRound() + + await veBetterPassport.setAppSecurity(appId, 1) + + expect(await veBetterPassport.getAddress()).to.equal(await x2EarnRewardsPool.veBetterPassport()) + + const tx = await x2EarnRewardsPool.connect(owner).distributeReward(appId, ethers.parseEther("1"), user.address, "") + + const receipt = await tx.wait() + + // event emitted + if (!receipt) throw new Error("No receipt") + + const decodedEvents = receipt.logs?.map(event => { + return veBetterPassport.interface.parseLog({ + topics: event?.topics as string[], + data: event?.data as string, + }) + }) + + const registeredActionEvent = decodedEvents.filter( + (event: any) => event !== null && event.name === "RegisteredAction", + )[0] + + let roundId = await xAllocationVoting.currentRoundId() + + expect(registeredActionEvent).not.to.eql([]) + expect(registeredActionEvent?.args[0]).to.equal(user.address) + expect(registeredActionEvent?.args[1]).to.equal(user.address) + expect(registeredActionEvent?.args[2]).to.equal(appId) + expect(registeredActionEvent?.args[3]).to.equal(roundId) + + // check that the action score is correct + const appSecurity = await veBetterPassport.appSecurity(appId) + const multiplier = await veBetterPassport.securityMultiplier(appSecurity) + expect(registeredActionEvent?.args[4]).to.equal(multiplier) + + // check that the user score is correct + expect(await veBetterPassport.userAppTotalScore(user.address, appId)).to.equal(multiplier) + expect(await veBetterPassport.userRoundScoreApp(user.address, roundId, appId)).to.equal(multiplier) + expect(await veBetterPassport.userTotalScore(user.address)).to.equal(multiplier) + expect(await veBetterPassport.userRoundScore(user.address, roundId)).to.equal(multiplier) + + // start round + await waitForRoundToEnd(roundId) + await xAllocationVoting.connect(owner).startNewRound() + roundId = await xAllocationVoting.currentRoundId() + + // action is registered when the reward is distributed with proof + const tx2 = await x2EarnRewardsPool + .connect(owner) + .distributeRewardWithProof( + appId, + ethers.parseEther("1"), + user.address, + ["image"], + ["https://image.png"], + ["carbon", "water"], + [100, 200], + "The description of the action", + ) + + const receipt2 = await tx2.wait() + + // event emitted + if (!receipt2) throw new Error("No receipt") + + const decodedEvents2 = receipt2.logs?.map(event => { + return veBetterPassport.interface.parseLog({ + topics: event?.topics as string[], + data: event?.data as string, + }) + }) + + const registeredActionEvent2 = decodedEvents2.filter( + (event: any) => event !== null && event.name === "RegisteredAction", + )[0] + + expect(registeredActionEvent2).not.to.eql([]) + expect(registeredActionEvent2?.args[0]).to.equal(user.address) + expect(registeredActionEvent2?.args[1]).to.equal(user.address) + expect(registeredActionEvent2?.args[2]).to.equal(appId) + expect(registeredActionEvent2?.args[3]).to.equal(roundId) + + // check that the action score is correct + const supposedScore = multiplier + multiplier + expect(registeredActionEvent2?.args[4]).to.equal(multiplier) + + // check that the user score is correct + expect(await veBetterPassport.userAppTotalScore(user.address, appId)).to.equal(supposedScore) + expect(await veBetterPassport.userTotalScore(user.address)).to.equal(supposedScore) + expect(await veBetterPassport.userRoundScore(user.address, roundId)).to.equal(multiplier) + expect(await veBetterPassport.userRoundScoreApp(user.address, roundId, appId)).to.equal(multiplier) + + // start round + await waitForRoundToEnd(roundId) + await xAllocationVoting.connect(owner).startNewRound() + roundId = await xAllocationVoting.currentRoundId() + + // event is emitted when using depraceted distributeReward function + const tx3 = await x2EarnRewardsPool + .connect(owner) + .distributeRewardDeprecated(appId, ethers.parseEther("1"), user.address, "") + + const receipt3 = await tx3.wait() + + // event emitted + if (!receipt3) throw new Error("No receipt") + + const decodedEvents3 = receipt3.logs?.map(event => { + return veBetterPassport.interface.parseLog({ + topics: event?.topics as string[], + data: event?.data as string, + }) + }) + + const registeredActionEvent3 = decodedEvents3.filter( + (event: any) => event !== null && event.name === "RegisteredAction", + )[0] + + expect(registeredActionEvent3).not.to.eql([]) + expect(registeredActionEvent3?.args[0]).to.equal(user.address) + expect(registeredActionEvent3?.args[1]).to.equal(user.address) + expect(registeredActionEvent3?.args[2]).to.equal(appId) + expect(registeredActionEvent3?.args[3]).to.equal(roundId) + + // check that the action score is correct + const supposedScore2 = supposedScore + multiplier + expect(registeredActionEvent3?.args[4]).to.equal(multiplier) + + // check that the user score is correct + expect(await veBetterPassport.userRoundScoreApp(user.address, roundId, appId)).to.equal(multiplier) + expect(await veBetterPassport.userTotalScore(user.address)).to.equal(supposedScore2) + expect(await veBetterPassport.userRoundScore(user.address, roundId)).to.equal(multiplier) + }) }) diff --git a/test/XAllocationPool.test.ts b/test/XAllocationPool.test.ts index a370ad7..30f5cce 100644 --- a/test/XAllocationPool.test.ts +++ b/test/XAllocationPool.test.ts @@ -19,7 +19,7 @@ import { createLocalConfig } from "../config/contracts/envs/local" import { deployProxy, upgradeProxy } from "../scripts/helpers" import { XAllocationPool, XAllocationPoolV1 } from "../typechain-types" -describe("X-Allocation Pool", async function () { +describe("X-Allocation Pool - @shard3", async function () { describe("Deployment", async function () { it("Contract is correctly initialized", async function () { const { xAllocationPool, owner, x2EarnApps, emissions, b3tr, treasury } = await getOrDeployContractInstances({ @@ -218,11 +218,20 @@ describe("X-Allocation Pool", async function () { config.X_ALLOCATION_POOL_BASE_ALLOCATION_PERCENTAGE = 0 config.INITIAL_X_ALLOCATION = 10000n config.X_ALLOCATION_VOTING_QUORUM_PERCENTAGE = 0 - const { otherAccounts, owner, b3tr, x2EarnRewardsPool, emissions, x2EarnApps, xAllocationVoting, treasury } = - await getOrDeployContractInstances({ - forceDeploy: true, - config, - }) + const { + otherAccounts, + owner, + b3tr, + x2EarnRewardsPool, + emissions, + x2EarnApps, + xAllocationVoting, + treasury, + veBetterPassport, + } = await getOrDeployContractInstances({ + forceDeploy: true, + config, + }) // Deploy XAllocationPool const xAllocationPoolV1 = (await deployProxy("XAllocationPoolV1", [ @@ -242,9 +251,12 @@ describe("X-Allocation Pool", async function () { await bootstrapEmissions() otherAccounts.forEach(async account => { + await veBetterPassport.whitelist(account.address) await getVot3Tokens(account, "10000") }) + await veBetterPassport.toggleCheck(1) + //Add apps const app1Id = ethers.keccak256(ethers.toUtf8Bytes("My app")) const app2Id = ethers.keccak256(ethers.toUtf8Bytes("My app #2")) @@ -265,39 +277,19 @@ describe("X-Allocation Pool", async function () { // Vote await xAllocationVoting .connect(otherAccounts[1]) - .castVote( - round1, - [app1Id, app2Id, app3Id], - [ethers.parseEther("0"), ethers.parseEther("900"), ethers.parseEther("100")], - ) + .castVote(round1, [app2Id, app3Id], [ethers.parseEther("900"), ethers.parseEther("100")]) await xAllocationVoting .connect(otherAccounts[2]) - .castVote( - round1, - [app1Id, app2Id, app3Id], - [ethers.parseEther("0"), ethers.parseEther("500"), ethers.parseEther("100")], - ) + .castVote(round1, [app2Id, app3Id], [ethers.parseEther("500"), ethers.parseEther("100")]) await xAllocationVoting .connect(otherAccounts[3]) - .castVote( - round1, - [app1Id, app2Id, app3Id], - [ethers.parseEther("0"), ethers.parseEther("100"), ethers.parseEther("100")], - ) + .castVote(round1, [app2Id, app3Id], [ethers.parseEther("100"), ethers.parseEther("100")]) await xAllocationVoting .connect(otherAccounts[4]) - .castVote( - round1, - [app1Id, app2Id, app3Id], - [ethers.parseEther("0"), ethers.parseEther("100"), ethers.parseEther("100")], - ) + .castVote(round1, [app2Id, app3Id], [ethers.parseEther("100"), ethers.parseEther("100")]) await xAllocationVoting .connect(otherAccounts[5]) - .castVote( - round1, - [app1Id, app2Id, app3Id], - [ethers.parseEther("1000"), ethers.parseEther("0"), ethers.parseEther("100")], - ) + .castVote(round1, [app1Id, app3Id], [ethers.parseEther("1000"), ethers.parseEther("100")]) await waitForRoundToEnd(round1) @@ -315,39 +307,19 @@ describe("X-Allocation Pool", async function () { // Vote await xAllocationVoting .connect(otherAccounts[1]) - .castVote( - round2, - [app1Id, app2Id, app3Id], - [ethers.parseEther("0"), ethers.parseEther("900"), ethers.parseEther("100")], - ) + .castVote(round2, [app2Id, app3Id], [ethers.parseEther("900"), ethers.parseEther("100")]) await xAllocationVoting .connect(otherAccounts[2]) - .castVote( - round2, - [app1Id, app2Id, app3Id], - [ethers.parseEther("0"), ethers.parseEther("500"), ethers.parseEther("100")], - ) + .castVote(round2, [app2Id, app3Id], [ethers.parseEther("500"), ethers.parseEther("100")]) await xAllocationVoting .connect(otherAccounts[3]) - .castVote( - round2, - [app1Id, app2Id, app3Id], - [ethers.parseEther("0"), ethers.parseEther("100"), ethers.parseEther("100")], - ) + .castVote(round2, [app2Id, app3Id], [ethers.parseEther("100"), ethers.parseEther("100")]) await xAllocationVoting .connect(otherAccounts[4]) - .castVote( - round2, - [app1Id, app2Id, app3Id], - [ethers.parseEther("0"), ethers.parseEther("100"), ethers.parseEther("100")], - ) + .castVote(round2, [app2Id, app3Id], [ethers.parseEther("100"), ethers.parseEther("100")]) await xAllocationVoting .connect(otherAccounts[5]) - .castVote( - round2, - [app1Id, app2Id, app3Id], - [ethers.parseEther("1000"), ethers.parseEther("0"), ethers.parseEther("100")], - ) + .castVote(round2, [app1Id, app3Id], [ethers.parseEther("1000"), ethers.parseEther("100")]) await waitForRoundToEnd(round2) @@ -406,43 +378,23 @@ describe("X-Allocation Pool", async function () { // Vote await xAllocationVoting .connect(otherAccounts[1]) - .castVote( - round3, - [app1Id, app2Id, app3Id], - [ethers.parseEther("0"), ethers.parseEther("900"), ethers.parseEther("100")], - ) + .castVote(round3, [app2Id, app3Id], [ethers.parseEther("900"), ethers.parseEther("100")]) await xAllocationVoting .connect(otherAccounts[2]) - .castVote( - round3, - [app1Id, app2Id, app3Id], - [ethers.parseEther("0"), ethers.parseEther("500"), ethers.parseEther("100")], - ) + .castVote(round3, [app2Id, app3Id], [ethers.parseEther("500"), ethers.parseEther("100")]) await xAllocationVoting .connect(otherAccounts[3]) - .castVote( - round3, - [app1Id, app2Id, app3Id], - [ethers.parseEther("0"), ethers.parseEther("100"), ethers.parseEther("100")], - ) + .castVote(round3, [app2Id, app3Id], [ethers.parseEther("100"), ethers.parseEther("100")]) await xAllocationVoting .connect(otherAccounts[4]) - .castVote( - round3, - [app1Id, app2Id, app3Id], - [ethers.parseEther("0"), ethers.parseEther("100"), ethers.parseEther("100")], - ) + .castVote(round3, [app2Id, app3Id], [ethers.parseEther("100"), ethers.parseEther("100")]) // Turn off quadratic funding mid round await xAllocationPool.connect(owner).toggleQuadraticFunding() await xAllocationVoting .connect(otherAccounts[5]) - .castVote( - round3, - [app1Id, app2Id, app3Id], - [ethers.parseEther("1000"), ethers.parseEther("0"), ethers.parseEther("100")], - ) + .castVote(round3, [app1Id, app3Id], [ethers.parseEther("1000"), ethers.parseEther("100")]) await waitForRoundToEnd(round3) @@ -465,39 +417,19 @@ describe("X-Allocation Pool", async function () { // Vote await xAllocationVoting .connect(otherAccounts[1]) - .castVote( - round4, - [app1Id, app2Id, app3Id], - [ethers.parseEther("0"), ethers.parseEther("900"), ethers.parseEther("100")], - ) + .castVote(round4, [app2Id, app3Id], [ethers.parseEther("900"), ethers.parseEther("100")]) await xAllocationVoting .connect(otherAccounts[2]) - .castVote( - round4, - [app1Id, app2Id, app3Id], - [ethers.parseEther("0"), ethers.parseEther("500"), ethers.parseEther("100")], - ) + .castVote(round4, [app2Id, app3Id], [ethers.parseEther("500"), ethers.parseEther("100")]) await xAllocationVoting .connect(otherAccounts[3]) - .castVote( - round4, - [app1Id, app2Id, app3Id], - [ethers.parseEther("0"), ethers.parseEther("100"), ethers.parseEther("100")], - ) + .castVote(round4, [app2Id, app3Id], [ethers.parseEther("100"), ethers.parseEther("100")]) await xAllocationVoting .connect(otherAccounts[4]) - .castVote( - round4, - [app1Id, app2Id, app3Id], - [ethers.parseEther("0"), ethers.parseEther("100"), ethers.parseEther("100")], - ) + .castVote(round4, [app2Id, app3Id], [ethers.parseEther("100"), ethers.parseEther("100")]) await xAllocationVoting .connect(otherAccounts[5]) - .castVote( - round4, - [app1Id, app2Id, app3Id], - [ethers.parseEther("1000"), ethers.parseEther("0"), ethers.parseEther("100")], - ) + .castVote(round4, [app1Id, app3Id], [ethers.parseEther("1000"), ethers.parseEther("100")]) await waitForRoundToEnd(round4) @@ -756,7 +688,7 @@ describe("X-Allocation Pool", async function () { describe("Allocation rewards for x-apps", async function () { describe("App shares and base allocation", async function () { it("App can receive a max amount of allocation share and unallocated amount gets sent to treasury", async function () { - const { xAllocationVoting, otherAccounts, owner, xAllocationPool, x2EarnApps } = + const { xAllocationVoting, otherAccounts, owner, xAllocationPool, x2EarnApps, veBetterPassport } = await getOrDeployContractInstances({ forceDeploy: true, }) @@ -767,6 +699,9 @@ describe("X-Allocation Pool", async function () { const voter1 = otherAccounts[1] await getVot3Tokens(voter1, "1000") + await veBetterPassport.whitelist(voter1.address) + await veBetterPassport.toggleCheck(1) + //Add apps const app1Id = ethers.keccak256(ethers.toUtf8Bytes("My app")) const app2Id = ethers.keccak256(ethers.toUtf8Bytes("My app #2")) @@ -859,10 +794,19 @@ describe("X-Allocation Pool", async function () { }) it("New app of failed round receives a base allocation even if it was not eligible in previous round", async function () { - const { xAllocationVoting, otherAccounts, owner, xAllocationPool, b3tr, emissions, minterAccount, x2EarnApps } = - await getOrDeployContractInstances({ - forceDeploy: true, - }) + const { + xAllocationVoting, + otherAccounts, + owner, + veBetterPassport, + xAllocationPool, + b3tr, + emissions, + minterAccount, + x2EarnApps, + } = await getOrDeployContractInstances({ + forceDeploy: true, + }) // SEED DATA @@ -882,6 +826,9 @@ describe("X-Allocation Pool", async function () { await emissions.connect(minterAccount).start() + await veBetterPassport.whitelist(voter1.address) + await veBetterPassport.toggleCheck(1) + //Start allocation round const round1 = parseInt((await xAllocationVoting.currentRoundId()).toString()) @@ -933,10 +880,18 @@ describe("X-Allocation Pool", async function () { }) it("App shares cap and unallocated share of a past round should be checkpointed", async function () { - const { xAllocationVoting, otherAccounts, owner, xAllocationPool, emissions, minterAccount, x2EarnApps } = - await getOrDeployContractInstances({ - forceDeploy: true, - }) + const { + xAllocationVoting, + otherAccounts, + owner, + xAllocationPool, + emissions, + minterAccount, + x2EarnApps, + veBetterPassport, + } = await getOrDeployContractInstances({ + forceDeploy: true, + }) const voter1 = otherAccounts[1] await getVot3Tokens(voter1, "2000") @@ -956,12 +911,13 @@ describe("X-Allocation Pool", async function () { await emissions.connect(minterAccount).start() + await veBetterPassport.whitelist(voter1.address) + await veBetterPassport.toggleCheck(1) + const round1 = await xAllocationVoting.currentRoundId() // Vote - await xAllocationVoting - .connect(voter1) - .castVote(round1, [app1Id, app2Id], [ethers.parseEther("1000"), ethers.parseEther("0")]) + await xAllocationVoting.connect(voter1).castVote(round1, [app1Id], [ethers.parseEther("1000")]) await waitForRoundToEnd(Number(round1)) let state = await xAllocationVoting.state(round1) @@ -977,9 +933,7 @@ describe("X-Allocation Pool", async function () { const round2 = await xAllocationVoting.currentRoundId() // Vote - await xAllocationVoting - .connect(voter1) - .castVote(round2, [app1Id, app2Id], [ethers.parseEther("1000"), ethers.parseEther("0")]) + await xAllocationVoting.connect(voter1).castVote(round2, [app1Id], [ethers.parseEther("1000")]) await waitForRoundToEnd(Number(round2)) @@ -1021,10 +975,18 @@ describe("X-Allocation Pool", async function () { }) it("Base allocation of a past round should remain the same even if value has been updated", async function () { - const { xAllocationVoting, otherAccounts, owner, xAllocationPool, emissions, minterAccount, x2EarnApps } = - await getOrDeployContractInstances({ - forceDeploy: true, - }) + const { + xAllocationVoting, + otherAccounts, + owner, + xAllocationPool, + emissions, + minterAccount, + x2EarnApps, + veBetterPassport, + } = await getOrDeployContractInstances({ + forceDeploy: true, + }) const voter1 = otherAccounts[1] await getVot3Tokens(voter1, "2000") @@ -1046,10 +1008,11 @@ describe("X-Allocation Pool", async function () { const round1 = await xAllocationVoting.currentRoundId() + await veBetterPassport.whitelist(voter1.address) + await veBetterPassport.toggleCheck(1) + // Vote - await xAllocationVoting - .connect(voter1) - .castVote(round1, [app1Id, app2Id], [ethers.parseEther("1000"), ethers.parseEther("0")]) + await xAllocationVoting.connect(voter1).castVote(round1, [app1Id], [ethers.parseEther("1000")]) await waitForRoundToEnd(Number(round1)) let state = await xAllocationVoting.state(round1) @@ -1065,9 +1028,7 @@ describe("X-Allocation Pool", async function () { const round2 = await xAllocationVoting.currentRoundId() // Vote - await xAllocationVoting - .connect(voter1) - .castVote(round2, [app1Id, app2Id], [ethers.parseEther("1000"), ethers.parseEther("0")]) + await xAllocationVoting.connect(voter1).castVote(round2, [app1Id], [ethers.parseEther("1000")]) await waitForRoundToEnd(Number(round2)) @@ -1086,7 +1047,7 @@ describe("X-Allocation Pool", async function () { it("Cannot calculate base allocation amount and app shares if xAllocationVoting is not set", async function () { const config = createLocalConfig() - const { xAllocationVoting, otherAccounts, owner, xAllocationPool, x2EarnApps } = + const { xAllocationVoting, otherAccounts, owner, xAllocationPool, x2EarnApps, veBetterPassport } = await getOrDeployContractInstances({ forceDeploy: true, }) @@ -1097,6 +1058,9 @@ describe("X-Allocation Pool", async function () { const voter1 = otherAccounts[1] await getVot3Tokens(voter1, "1000") + await veBetterPassport.whitelist(voter1.address) + await veBetterPassport.toggleCheck(1) + //Add apps const app1Id = ethers.keccak256(ethers.toUtf8Bytes("My app")) const app2Id = ethers.keccak256(ethers.toUtf8Bytes("My app #2")) @@ -1140,10 +1104,18 @@ describe("X-Allocation Pool", async function () { describe("App earnings", async function () { it("Allocation rewards are calculated correctly", async function () { - const { xAllocationVoting, otherAccounts, owner, xAllocationPool, emissions, minterAccount, x2EarnApps } = - await getOrDeployContractInstances({ - forceDeploy: true, - }) + const { + xAllocationVoting, + otherAccounts, + owner, + xAllocationPool, + emissions, + minterAccount, + x2EarnApps, + veBetterPassport, + } = await getOrDeployContractInstances({ + forceDeploy: true, + }) const voter1 = otherAccounts[1] await getVot3Tokens(voter1, "1000") @@ -1165,6 +1137,9 @@ describe("X-Allocation Pool", async function () { const round1 = await xAllocationVoting.currentRoundId() + await veBetterPassport.whitelist(voter1.address) + await veBetterPassport.toggleCheck(1) + // Vote await xAllocationVoting .connect(voter1) @@ -1205,10 +1180,18 @@ describe("X-Allocation Pool", async function () { }) it("Should correctly count live earnings when current round failed (round > 1 )", async function () { - const { xAllocationVoting, otherAccounts, owner, xAllocationPool, emissions, minterAccount, x2EarnApps } = - await getOrDeployContractInstances({ - forceDeploy: true, - }) + const { + xAllocationVoting, + otherAccounts, + owner, + xAllocationPool, + emissions, + minterAccount, + x2EarnApps, + veBetterPassport, + } = await getOrDeployContractInstances({ + forceDeploy: true, + }) const voter1 = otherAccounts[1] await getVot3Tokens(voter1, "1000") @@ -1230,6 +1213,9 @@ describe("X-Allocation Pool", async function () { const round1 = await xAllocationVoting.currentRoundId() + await veBetterPassport.whitelist(voter1.address) + await veBetterPassport.toggleCheck(1) + // Vote await xAllocationVoting .connect(voter1) @@ -1267,10 +1253,18 @@ describe("X-Allocation Pool", async function () { }) it("When adding new app previous allocations should remain the same", async function () { - const { xAllocationVoting, otherAccounts, owner, xAllocationPool, emissions, minterAccount, x2EarnApps } = - await getOrDeployContractInstances({ - forceDeploy: true, - }) + const { + xAllocationVoting, + otherAccounts, + owner, + xAllocationPool, + emissions, + minterAccount, + x2EarnApps, + veBetterPassport, + } = await getOrDeployContractInstances({ + forceDeploy: true, + }) const voter1 = otherAccounts[1] await getVot3Tokens(voter1, "1000") @@ -1290,6 +1284,9 @@ describe("X-Allocation Pool", async function () { await bootstrapEmissions() await emissions.connect(minterAccount).start() + await veBetterPassport.whitelist(voter1.address) + await veBetterPassport.toggleCheck(1) + const round1 = await xAllocationVoting.currentRoundId() // Vote @@ -1373,14 +1370,25 @@ describe("X-Allocation Pool", async function () { }) it("Earnings should be calculated correctly when round failed", async function () { - const { xAllocationVoting, otherAccounts, owner, xAllocationPool, emissions, minterAccount, x2EarnApps } = - await getOrDeployContractInstances({ - forceDeploy: true, - }) + const { + xAllocationVoting, + otherAccounts, + owner, + xAllocationPool, + emissions, + minterAccount, + x2EarnApps, + veBetterPassport, + } = await getOrDeployContractInstances({ + forceDeploy: true, + }) const voter1 = otherAccounts[1] await getVot3Tokens(voter1, "1000") + await veBetterPassport.whitelist(voter1.address) + await veBetterPassport.toggleCheck(1) + //Add apps const app1Id = ethers.keccak256(ethers.toUtf8Bytes("My app")) const app2Id = ethers.keccak256(ethers.toUtf8Bytes("My app #2")) @@ -1428,10 +1436,19 @@ describe("X-Allocation Pool", async function () { }) describe("App claiming", async function () { it("Allocation rewards are claimed correctly", async function () { - const { xAllocationVoting, otherAccounts, owner, xAllocationPool, b3tr, emissions, minterAccount, x2EarnApps } = - await getOrDeployContractInstances({ - forceDeploy: true, - }) + const { + xAllocationVoting, + otherAccounts, + owner, + xAllocationPool, + b3tr, + emissions, + minterAccount, + x2EarnApps, + veBetterPassport, + } = await getOrDeployContractInstances({ + forceDeploy: true, + }) // SEED DATA @@ -1455,6 +1472,9 @@ describe("X-Allocation Pool", async function () { await emissions.connect(minterAccount).start() + await veBetterPassport.whitelist(voter1.address) + await veBetterPassport.toggleCheck(1) + //Start allocation round const round1 = parseInt((await xAllocationVoting.currentRoundId()).toString()) // Vote @@ -1493,16 +1513,27 @@ describe("X-Allocation Pool", async function () { }) it("App cannot claim two times in the same round", async function () { - const { xAllocationVoting, otherAccounts, owner, xAllocationPool, emissions, minterAccount, x2EarnApps } = - await getOrDeployContractInstances({ - forceDeploy: true, - }) + const { + xAllocationVoting, + otherAccounts, + owner, + xAllocationPool, + emissions, + minterAccount, + x2EarnApps, + veBetterPassport, + } = await getOrDeployContractInstances({ + forceDeploy: true, + }) // SEED DATA const voter1 = otherAccounts[1] await getVot3Tokens(voter1, "1000") + await veBetterPassport.whitelist(voter1.address) + await veBetterPassport.toggleCheck(1) + //Add apps const app1Id = ethers.keccak256(ethers.toUtf8Bytes("My app")) const app2Id = ethers.keccak256(ethers.toUtf8Bytes("My app #2")) @@ -1538,10 +1569,19 @@ describe("X-Allocation Pool", async function () { }) it("Anyone can trigger claiming of allocation to app", async function () { - const { xAllocationVoting, otherAccounts, owner, xAllocationPool, b3tr, emissions, minterAccount, x2EarnApps } = - await getOrDeployContractInstances({ - forceDeploy: true, - }) + const { + xAllocationVoting, + otherAccounts, + owner, + xAllocationPool, + b3tr, + emissions, + minterAccount, + x2EarnApps, + veBetterPassport, + } = await getOrDeployContractInstances({ + forceDeploy: true, + }) // SEED DATA @@ -1560,6 +1600,9 @@ describe("X-Allocation Pool", async function () { await x2EarnApps.connect(otherAccounts[3]).setTeamAllocationPercentage(app1Id, 100) await x2EarnApps.connect(otherAccounts[4]).setTeamAllocationPercentage(app2Id, 100) + await veBetterPassport.whitelist(voter1.address) + await veBetterPassport.toggleCheck(1) + // Bootstrap emissions await bootstrapEmissions() @@ -1590,16 +1633,27 @@ describe("X-Allocation Pool", async function () { }) it("Can claim first round even if it's not finalized", async function () { - const { xAllocationVoting, otherAccounts, owner, xAllocationPool, emissions, minterAccount, x2EarnApps } = - await getOrDeployContractInstances({ - forceDeploy: true, - }) + const { + xAllocationVoting, + otherAccounts, + owner, + xAllocationPool, + emissions, + minterAccount, + x2EarnApps, + veBetterPassport, + } = await getOrDeployContractInstances({ + forceDeploy: true, + }) // SEED DATA const voter1 = otherAccounts[1] await getVot3Tokens(voter1, "1000") + await veBetterPassport.whitelist(voter1.address) + await veBetterPassport.toggleCheck(1) + //Add apps const app1Id = ethers.keccak256(ethers.toUtf8Bytes("My app")) const app1ReceiverAddress = otherAccounts[3].address @@ -1632,14 +1686,25 @@ describe("X-Allocation Pool", async function () { }) it("Can claim failed not finalized round [ROUND > 1]", async function () { - const { xAllocationVoting, otherAccounts, owner, xAllocationPool, emissions, minterAccount, x2EarnApps } = - await getOrDeployContractInstances({ - forceDeploy: true, - }) + const { + xAllocationVoting, + otherAccounts, + owner, + xAllocationPool, + emissions, + minterAccount, + x2EarnApps, + veBetterPassport, + } = await getOrDeployContractInstances({ + forceDeploy: true, + }) const voter1 = otherAccounts[1] await getVot3Tokens(voter1, "1000") + await veBetterPassport.whitelist(voter1.address) + await veBetterPassport.toggleCheck(1) + //Add apps const app1Id = ethers.keccak256(ethers.toUtf8Bytes("My app")) const app2Id = ethers.keccak256(ethers.toUtf8Bytes("My app #2")) @@ -1687,16 +1752,27 @@ describe("X-Allocation Pool", async function () { }) it("Can claim failed round after it's finalized", async function () { - const { xAllocationVoting, otherAccounts, owner, xAllocationPool, emissions, minterAccount, x2EarnApps } = - await getOrDeployContractInstances({ - forceDeploy: true, - }) + const { + xAllocationVoting, + otherAccounts, + owner, + xAllocationPool, + emissions, + minterAccount, + x2EarnApps, + veBetterPassport, + } = await getOrDeployContractInstances({ + forceDeploy: true, + }) // SEED DATA const voter1 = otherAccounts[1] await getVot3Tokens(voter1, "1000") + await veBetterPassport.whitelist(voter1.address) + await veBetterPassport.toggleCheck(1) + //Add apps const app1Id = ethers.keccak256(ethers.toUtf8Bytes("My app")) const app1ReceiverAddress = otherAccounts[3].address @@ -1772,6 +1848,7 @@ describe("X-Allocation Pool", async function () { minterAccount, x2EarnApps, x2EarnRewardsPool, + veBetterPassport, } = await getOrDeployContractInstances({ forceDeploy: true, }) @@ -1779,6 +1856,9 @@ describe("X-Allocation Pool", async function () { const voter1 = otherAccounts[1] await getVot3Tokens(voter1, "1000") + await veBetterPassport.whitelist(voter1.address) + await veBetterPassport.toggleCheck(1) + //Add apps const app1Id = ethers.keccak256(ethers.toUtf8Bytes("My app")) const app2Id = ethers.keccak256(ethers.toUtf8Bytes("My app #2")) @@ -1846,14 +1926,25 @@ describe("X-Allocation Pool", async function () { }) it("Claimable amount for an active round should be 0", async function () { - const { xAllocationVoting, otherAccounts, owner, xAllocationPool, emissions, minterAccount, x2EarnApps } = - await getOrDeployContractInstances({ - forceDeploy: true, - }) + const { + xAllocationVoting, + otherAccounts, + owner, + xAllocationPool, + emissions, + minterAccount, + x2EarnApps, + veBetterPassport, + } = await getOrDeployContractInstances({ + forceDeploy: true, + }) const voter1 = otherAccounts[1] await getVot3Tokens(voter1, "1000") + await veBetterPassport.whitelist(voter1.address) + await veBetterPassport.toggleCheck(1) + //Add apps const app1Id = ethers.keccak256(ethers.toUtf8Bytes("My app")) const app2Id = ethers.keccak256(ethers.toUtf8Bytes("My app #2")) @@ -1884,14 +1975,25 @@ describe("X-Allocation Pool", async function () { }) it("Cannot claim 0 rewards", async function () { - const { xAllocationVoting, otherAccounts, owner, xAllocationPool, emissions, minterAccount, x2EarnApps } = - await getOrDeployContractInstances({ - forceDeploy: true, - }) + const { + xAllocationVoting, + otherAccounts, + owner, + xAllocationPool, + emissions, + minterAccount, + x2EarnApps, + veBetterPassport, + } = await getOrDeployContractInstances({ + forceDeploy: true, + }) const voter1 = otherAccounts[1] await getVot3Tokens(voter1, "1000") + await veBetterPassport.whitelist(voter1.address) + await veBetterPassport.toggleCheck(1) + const GOVERNANCE_ROLE = await xAllocationVoting.GOVERNANCE_ROLE() await xAllocationVoting.grantRole(GOVERNANCE_ROLE, owner.address) await xAllocationVoting.setBaseAllocationPercentage(0) @@ -1915,9 +2017,7 @@ describe("X-Allocation Pool", async function () { const round1 = await xAllocationVoting.currentRoundId() // Vote - await xAllocationVoting - .connect(voter1) - .castVote(round1, [app1Id, app2Id], [ethers.parseEther("0"), ethers.parseEther("1000")]) + await xAllocationVoting.connect(voter1).castVote(round1, [app2Id], [ethers.parseEther("1000")]) await waitForRoundToEnd(Number(round1)) let state = await xAllocationVoting.state(round1) @@ -1930,14 +2030,26 @@ describe("X-Allocation Pool", async function () { }) it("Cannot claim if b3tr token is paused", async function () { - const { xAllocationVoting, otherAccounts, owner, xAllocationPool, emissions, minterAccount, b3tr, x2EarnApps } = - await getOrDeployContractInstances({ - forceDeploy: true, - }) + const { + xAllocationVoting, + otherAccounts, + owner, + xAllocationPool, + emissions, + minterAccount, + b3tr, + x2EarnApps, + veBetterPassport, + } = await getOrDeployContractInstances({ + forceDeploy: true, + }) const voter1 = otherAccounts[1] await getVot3Tokens(voter1, "1000") + await veBetterPassport.whitelist(voter1.address) + await veBetterPassport.toggleCheck(1) + //Add apps const app1Id = ethers.keccak256(ethers.toUtf8Bytes("My app")) const app2Id = ethers.keccak256(ethers.toUtf8Bytes("My app #2")) @@ -1955,9 +2067,7 @@ describe("X-Allocation Pool", async function () { const round1 = await xAllocationVoting.currentRoundId() // Vote - await xAllocationVoting - .connect(voter1) - .castVote(round1, [app1Id, app2Id], [ethers.parseEther("0"), ethers.parseEther("1000")]) + await xAllocationVoting.connect(voter1).castVote(round1, [app2Id], [ethers.parseEther("1000")]) await waitForRoundToEnd(Number(round1)) @@ -1968,14 +2078,25 @@ describe("X-Allocation Pool", async function () { }) it("Cannot claim for app that does not exist", async function () { - const { xAllocationVoting, otherAccounts, owner, xAllocationPool, emissions, minterAccount, x2EarnApps } = - await getOrDeployContractInstances({ - forceDeploy: true, - }) + const { + xAllocationVoting, + otherAccounts, + owner, + xAllocationPool, + emissions, + minterAccount, + x2EarnApps, + veBetterPassport, + } = await getOrDeployContractInstances({ + forceDeploy: true, + }) const voter1 = otherAccounts[1] await getVot3Tokens(voter1, "1000") + await veBetterPassport.whitelist(voter1.address) + await veBetterPassport.toggleCheck(1) + //Add apps const app1Id = ethers.keccak256(ethers.toUtf8Bytes("My app")) await x2EarnApps @@ -1999,16 +2120,28 @@ describe("X-Allocation Pool", async function () { }) it("Should fail if not enough balance on contract", async function () { - const { xAllocationPool, otherAccounts, x2EarnApps, xAllocationVoting, b3tr, emissions, owner, minterAccount } = - await getOrDeployContractInstances({ - forceDeploy: true, - }) + const { + xAllocationPool, + otherAccounts, + x2EarnApps, + xAllocationVoting, + b3tr, + emissions, + owner, + minterAccount, + veBetterPassport, + } = await getOrDeployContractInstances({ + forceDeploy: true, + }) // Bootstrap emissions await bootstrapEmissions() await getVot3Tokens(otherAccounts[3], "10000") + await veBetterPassport.whitelist(otherAccounts[3].address) + await veBetterPassport.toggleCheck(1) + //Add apps const app1Id = ethers.keccak256(ethers.toUtf8Bytes("My app")) const app2Id = ethers.keccak256(ethers.toUtf8Bytes("My app #2")) @@ -2056,6 +2189,7 @@ describe("X-Allocation Pool", async function () { minterAccount, treasury, x2EarnApps, + veBetterPassport, } = await getOrDeployContractInstances({ forceDeploy: true, }) @@ -2065,6 +2199,9 @@ describe("X-Allocation Pool", async function () { const voter1 = otherAccounts[1] await getVot3Tokens(voter1, "1000") + await veBetterPassport.whitelist(voter1.address) + await veBetterPassport.toggleCheck(1) + //Add apps const app1Id = ethers.keccak256(ethers.toUtf8Bytes("My app")) const app2Id = ethers.keccak256(ethers.toUtf8Bytes("My app #2")) @@ -2116,6 +2253,7 @@ describe("X-Allocation Pool", async function () { b3tr, treasury, x2EarnApps, + veBetterPassport, } = await getOrDeployContractInstances({ forceDeploy: true, }) @@ -2123,6 +2261,9 @@ describe("X-Allocation Pool", async function () { const voter1 = otherAccounts[1] await getVot3Tokens(voter1, "1000") + await veBetterPassport.whitelist(voter1.address) + await veBetterPassport.toggleCheck(1) + //Add apps const app1Id = ethers.keccak256(ethers.toUtf8Bytes("My app")) const app2Id = ethers.keccak256(ethers.toUtf8Bytes("My app #2")) @@ -2140,9 +2281,7 @@ describe("X-Allocation Pool", async function () { const round1 = await xAllocationVoting.currentRoundId() // Vote - await xAllocationVoting - .connect(voter1) - .castVote(round1, [app1Id, app2Id], [ethers.parseEther("0"), ethers.parseEther("1000")]) + await xAllocationVoting.connect(voter1).castVote(round1, [app2Id], [ethers.parseEther("1000")]) await waitForRoundToEnd(Number(round1)) expect(await xAllocationVoting.state(round1)).to.eql(2n) // succeeded @@ -2168,7 +2307,7 @@ describe("X-Allocation Pool", async function () { describe("Quadratic funding & Linear Funding", async function () { it("[Quadratic] Should calculate correct app shares with Quadratic funding distrubiton with max cap at 20%", async function () { - const { xAllocationVoting, otherAccounts, owner, xAllocationPool, x2EarnApps } = + const { xAllocationVoting, otherAccounts, owner, xAllocationPool, x2EarnApps, veBetterPassport } = await getOrDeployContractInstances({ forceDeploy: true, }) @@ -2177,9 +2316,12 @@ describe("X-Allocation Pool", async function () { await bootstrapEmissions() otherAccounts.forEach(async account => { + await veBetterPassport.whitelist(account.address) await getVot3Tokens(account, "10000") }) + await veBetterPassport.toggleCheck(1) + //Add apps await x2EarnApps .connect(owner) @@ -2199,39 +2341,19 @@ describe("X-Allocation Pool", async function () { // Vote await xAllocationVoting .connect(otherAccounts[1]) - .castVote( - round1, - [app1Id, app2Id, app3Id], - [ethers.parseEther("0"), ethers.parseEther("900"), ethers.parseEther("100")], - ) + .castVote(round1, [app2Id, app3Id], [ethers.parseEther("900"), ethers.parseEther("100")]) await xAllocationVoting .connect(otherAccounts[2]) - .castVote( - round1, - [app1Id, app2Id, app3Id], - [ethers.parseEther("0"), ethers.parseEther("500"), ethers.parseEther("100")], - ) + .castVote(round1, [app2Id, app3Id], [ethers.parseEther("500"), ethers.parseEther("100")]) await xAllocationVoting .connect(otherAccounts[3]) - .castVote( - round1, - [app1Id, app2Id, app3Id], - [ethers.parseEther("0"), ethers.parseEther("100"), ethers.parseEther("100")], - ) + .castVote(round1, [app2Id, app3Id], [ethers.parseEther("100"), ethers.parseEther("100")]) await xAllocationVoting .connect(otherAccounts[4]) - .castVote( - round1, - [app1Id, app2Id, app3Id], - [ethers.parseEther("0"), ethers.parseEther("100"), ethers.parseEther("100")], - ) + .castVote(round1, [app2Id, app3Id], [ethers.parseEther("100"), ethers.parseEther("100")]) await xAllocationVoting .connect(otherAccounts[5]) - .castVote( - round1, - [app1Id, app2Id, app3Id], - [ethers.parseEther("1000"), ethers.parseEther("0"), ethers.parseEther("100")], - ) + .castVote(round1, [app1Id, app3Id], [ethers.parseEther("1000"), ethers.parseEther("100")]) await waitForRoundToEnd(round1) @@ -2245,7 +2367,7 @@ describe("X-Allocation Pool", async function () { }) it("[Linear] Should calculate correct app shares with Linear funding distrubiton with max cap at 20%", async function () { - const { xAllocationVoting, otherAccounts, owner, xAllocationPool, x2EarnApps } = + const { xAllocationVoting, otherAccounts, owner, xAllocationPool, x2EarnApps, veBetterPassport } = await getOrDeployContractInstances({ forceDeploy: true, }) @@ -2254,9 +2376,12 @@ describe("X-Allocation Pool", async function () { await bootstrapEmissions() otherAccounts.forEach(async account => { + await veBetterPassport.whitelist(account.address) await getVot3Tokens(account, "10000") }) + await veBetterPassport.toggleCheck(1) + // Turn off quadratic funding await xAllocationPool.connect(owner).toggleQuadraticFunding() @@ -2283,39 +2408,19 @@ describe("X-Allocation Pool", async function () { // Vote await xAllocationVoting .connect(otherAccounts[1]) - .castVote( - round1, - [app1Id, app2Id, app3Id], - [ethers.parseEther("0"), ethers.parseEther("900"), ethers.parseEther("100")], - ) + .castVote(round1, [app2Id, app3Id], [ethers.parseEther("900"), ethers.parseEther("100")]) await xAllocationVoting .connect(otherAccounts[2]) - .castVote( - round1, - [app1Id, app2Id, app3Id], - [ethers.parseEther("0"), ethers.parseEther("500"), ethers.parseEther("100")], - ) + .castVote(round1, [app2Id, app3Id], [ethers.parseEther("500"), ethers.parseEther("100")]) await xAllocationVoting .connect(otherAccounts[3]) - .castVote( - round1, - [app1Id, app2Id, app3Id], - [ethers.parseEther("0"), ethers.parseEther("100"), ethers.parseEther("100")], - ) + .castVote(round1, [app2Id, app3Id], [ethers.parseEther("100"), ethers.parseEther("100")]) await xAllocationVoting .connect(otherAccounts[4]) - .castVote( - round1, - [app1Id, app2Id, app3Id], - [ethers.parseEther("0"), ethers.parseEther("100"), ethers.parseEther("100")], - ) + .castVote(round1, [app2Id, app3Id], [ethers.parseEther("100"), ethers.parseEther("100")]) await xAllocationVoting .connect(otherAccounts[5]) - .castVote( - round1, - [app1Id, app2Id, app3Id], - [ethers.parseEther("1000"), ethers.parseEther("0"), ethers.parseEther("100")], - ) + .castVote(round1, [app1Id, app3Id], [ethers.parseEther("1000"), ethers.parseEther("100")]) /* app1: 1000 votes @@ -2344,7 +2449,7 @@ describe("X-Allocation Pool", async function () { it("[Quadratic] Should calculate correct app shares with Quadratic funding distrubiton with no max cap", async function () { const config = createLocalConfig() config.X_ALLOCATION_POOL_APP_SHARES_MAX_CAP = 100 - const { xAllocationVoting, otherAccounts, owner, xAllocationPool, x2EarnApps } = + const { xAllocationVoting, otherAccounts, owner, xAllocationPool, x2EarnApps, veBetterPassport } = await getOrDeployContractInstances({ forceDeploy: true, config, @@ -2354,9 +2459,12 @@ describe("X-Allocation Pool", async function () { await bootstrapEmissions() otherAccounts.forEach(async account => { + await veBetterPassport.whitelist(account.address) await getVot3Tokens(account, "10000") }) + await veBetterPassport.toggleCheck(1) + //Add apps const app1Id = ethers.keccak256(ethers.toUtf8Bytes("My app")) const app2Id = ethers.keccak256(ethers.toUtf8Bytes("My app #2")) @@ -2374,39 +2482,19 @@ describe("X-Allocation Pool", async function () { // Vote await xAllocationVoting .connect(otherAccounts[1]) - .castVote( - round1, - [app1Id, app2Id, app3Id], - [ethers.parseEther("0"), ethers.parseEther("900"), ethers.parseEther("100")], - ) + .castVote(round1, [app2Id, app3Id], [ethers.parseEther("900"), ethers.parseEther("100")]) await xAllocationVoting .connect(otherAccounts[2]) - .castVote( - round1, - [app1Id, app2Id, app3Id], - [ethers.parseEther("0"), ethers.parseEther("500"), ethers.parseEther("100")], - ) + .castVote(round1, [app2Id, app3Id], [ethers.parseEther("500"), ethers.parseEther("100")]) await xAllocationVoting .connect(otherAccounts[3]) - .castVote( - round1, - [app1Id, app2Id, app3Id], - [ethers.parseEther("0"), ethers.parseEther("100"), ethers.parseEther("100")], - ) + .castVote(round1, [app2Id, app3Id], [ethers.parseEther("100"), ethers.parseEther("100")]) await xAllocationVoting .connect(otherAccounts[4]) - .castVote( - round1, - [app1Id, app2Id, app3Id], - [ethers.parseEther("0"), ethers.parseEther("100"), ethers.parseEther("100")], - ) + .castVote(round1, [app2Id, app3Id], [ethers.parseEther("100"), ethers.parseEther("100")]) await xAllocationVoting .connect(otherAccounts[5]) - .castVote( - round1, - [app1Id, app2Id, app3Id], - [ethers.parseEther("1000"), ethers.parseEther("0"), ethers.parseEther("100")], - ) + .castVote(round1, [app1Id, app3Id], [ethers.parseEther("1000"), ethers.parseEther("100")]) await waitForRoundToEnd(round1) @@ -2422,7 +2510,7 @@ describe("X-Allocation Pool", async function () { it("[Linear] Should calculate correct app shares with Linear funding distrubiton with no max cap", async function () { const config = createLocalConfig() config.X_ALLOCATION_POOL_APP_SHARES_MAX_CAP = 100 - const { xAllocationVoting, otherAccounts, owner, xAllocationPool, x2EarnApps } = + const { xAllocationVoting, otherAccounts, owner, xAllocationPool, x2EarnApps, veBetterPassport } = await getOrDeployContractInstances({ forceDeploy: true, config, @@ -2435,9 +2523,12 @@ describe("X-Allocation Pool", async function () { await bootstrapEmissions() otherAccounts.forEach(async account => { + await veBetterPassport.whitelist(account.address) await getVot3Tokens(account, "10000") }) + await veBetterPassport.toggleCheck(1) + //Add apps const app1Id = ethers.keccak256(ethers.toUtf8Bytes("My app")) const app2Id = ethers.keccak256(ethers.toUtf8Bytes("My app #2")) @@ -2459,39 +2550,19 @@ describe("X-Allocation Pool", async function () { // Vote await xAllocationVoting .connect(otherAccounts[1]) - .castVote( - round1, - [app1Id, app2Id, app3Id], - [ethers.parseEther("0"), ethers.parseEther("900"), ethers.parseEther("100")], - ) + .castVote(round1, [app2Id, app3Id], [ethers.parseEther("900"), ethers.parseEther("100")]) await xAllocationVoting .connect(otherAccounts[2]) - .castVote( - round1, - [app1Id, app2Id, app3Id], - [ethers.parseEther("0"), ethers.parseEther("500"), ethers.parseEther("100")], - ) + .castVote(round1, [app2Id, app3Id], [ethers.parseEther("500"), ethers.parseEther("100")]) await xAllocationVoting .connect(otherAccounts[3]) - .castVote( - round1, - [app1Id, app2Id, app3Id], - [ethers.parseEther("0"), ethers.parseEther("100"), ethers.parseEther("100")], - ) + .castVote(round1, [app2Id, app3Id], [ethers.parseEther("100"), ethers.parseEther("100")]) await xAllocationVoting .connect(otherAccounts[4]) - .castVote( - round1, - [app1Id, app2Id, app3Id], - [ethers.parseEther("0"), ethers.parseEther("100"), ethers.parseEther("100")], - ) + .castVote(round1, [app2Id, app3Id], [ethers.parseEther("100"), ethers.parseEther("100")]) await xAllocationVoting .connect(otherAccounts[5]) - .castVote( - round1, - [app1Id, app2Id, app3Id], - [ethers.parseEther("1000"), ethers.parseEther("0"), ethers.parseEther("100")], - ) + .castVote(round1, [app1Id, app3Id], [ethers.parseEther("1000"), ethers.parseEther("100")]) /* app1: 1000 votes @@ -2520,7 +2591,7 @@ describe("X-Allocation Pool", async function () { config.X_ALLOCATION_POOL_APP_SHARES_MAX_CAP = 100 config.X_ALLOCATION_POOL_BASE_ALLOCATION_PERCENTAGE = 0 config.INITIAL_X_ALLOCATION = 10000n - const { xAllocationVoting, otherAccounts, owner, xAllocationPool, x2EarnApps } = + const { xAllocationVoting, otherAccounts, owner, xAllocationPool, x2EarnApps, veBetterPassport } = await getOrDeployContractInstances({ forceDeploy: true, config, @@ -2530,9 +2601,12 @@ describe("X-Allocation Pool", async function () { await bootstrapEmissions() otherAccounts.forEach(async account => { + await veBetterPassport.whitelist(account.address) await getVot3Tokens(account, "10000") }) + await veBetterPassport.toggleCheck(1) + //Add apps const app1Id = ethers.keccak256(ethers.toUtf8Bytes("My app")) const app2Id = ethers.keccak256(ethers.toUtf8Bytes("My app #2")) @@ -2552,39 +2626,19 @@ describe("X-Allocation Pool", async function () { // Vote await xAllocationVoting .connect(otherAccounts[1]) - .castVote( - round1, - [app1Id, app2Id, app3Id], - [ethers.parseEther("0"), ethers.parseEther("900"), ethers.parseEther("100")], - ) + .castVote(round1, [app2Id, app3Id], [ethers.parseEther("900"), ethers.parseEther("100")]) await xAllocationVoting .connect(otherAccounts[2]) - .castVote( - round1, - [app1Id, app2Id, app3Id], - [ethers.parseEther("0"), ethers.parseEther("500"), ethers.parseEther("100")], - ) + .castVote(round1, [app2Id, app3Id], [ethers.parseEther("500"), ethers.parseEther("100")]) await xAllocationVoting .connect(otherAccounts[3]) - .castVote( - round1, - [app1Id, app2Id, app3Id], - [ethers.parseEther("0"), ethers.parseEther("100"), ethers.parseEther("100")], - ) + .castVote(round1, [app2Id, app3Id], [ethers.parseEther("100"), ethers.parseEther("100")]) await xAllocationVoting .connect(otherAccounts[4]) - .castVote( - round1, - [app1Id, app2Id, app3Id], - [ethers.parseEther("0"), ethers.parseEther("100"), ethers.parseEther("100")], - ) + .castVote(round1, [app2Id, app3Id], [ethers.parseEther("100"), ethers.parseEther("100")]) await xAllocationVoting .connect(otherAccounts[5]) - .castVote( - round1, - [app1Id, app2Id, app3Id], - [ethers.parseEther("1000"), ethers.parseEther("0"), ethers.parseEther("100")], - ) + .castVote(round1, [app1Id, app3Id], [ethers.parseEther("1000"), ethers.parseEther("100")]) await waitForRoundToEnd(round1) @@ -2602,7 +2656,7 @@ describe("X-Allocation Pool", async function () { config.X_ALLOCATION_POOL_APP_SHARES_MAX_CAP = 100 config.X_ALLOCATION_POOL_BASE_ALLOCATION_PERCENTAGE = 0 config.INITIAL_X_ALLOCATION = 10000n - const { xAllocationVoting, otherAccounts, owner, xAllocationPool, x2EarnApps } = + const { xAllocationVoting, otherAccounts, owner, xAllocationPool, x2EarnApps, veBetterPassport } = await getOrDeployContractInstances({ forceDeploy: true, config, @@ -2612,9 +2666,12 @@ describe("X-Allocation Pool", async function () { await bootstrapEmissions() otherAccounts.forEach(async account => { + await veBetterPassport.whitelist(account.address) await getVot3Tokens(account, "10000") }) + await veBetterPassport.toggleCheck(1) + // Turn off quadratic funding await xAllocationPool.connect(owner).toggleQuadraticFunding() @@ -2641,39 +2698,19 @@ describe("X-Allocation Pool", async function () { // Vote await xAllocationVoting .connect(otherAccounts[1]) - .castVote( - round1, - [app1Id, app2Id, app3Id], - [ethers.parseEther("0"), ethers.parseEther("900"), ethers.parseEther("100")], - ) + .castVote(round1, [app2Id, app3Id], [ethers.parseEther("900"), ethers.parseEther("100")]) await xAllocationVoting .connect(otherAccounts[2]) - .castVote( - round1, - [app1Id, app2Id, app3Id], - [ethers.parseEther("0"), ethers.parseEther("500"), ethers.parseEther("100")], - ) + .castVote(round1, [app2Id, app3Id], [ethers.parseEther("500"), ethers.parseEther("100")]) await xAllocationVoting .connect(otherAccounts[3]) - .castVote( - round1, - [app1Id, app2Id, app3Id], - [ethers.parseEther("0"), ethers.parseEther("100"), ethers.parseEther("100")], - ) + .castVote(round1, [app2Id, app3Id], [ethers.parseEther("100"), ethers.parseEther("100")]) await xAllocationVoting .connect(otherAccounts[4]) - .castVote( - round1, - [app1Id, app2Id, app3Id], - [ethers.parseEther("0"), ethers.parseEther("100"), ethers.parseEther("100")], - ) + .castVote(round1, [app2Id, app3Id], [ethers.parseEther("100"), ethers.parseEther("100")]) await xAllocationVoting .connect(otherAccounts[5]) - .castVote( - round1, - [app1Id, app2Id, app3Id], - [ethers.parseEther("1000"), ethers.parseEther("0"), ethers.parseEther("100")], - ) + .castVote(round1, [app1Id, app3Id], [ethers.parseEther("1000"), ethers.parseEther("100")]) /* app1: 1000 votes @@ -2702,7 +2739,7 @@ describe("X-Allocation Pool", async function () { config.X_ALLOCATION_POOL_BASE_ALLOCATION_PERCENTAGE = 0 config.INITIAL_X_ALLOCATION = 10000n config.X_ALLOCATION_VOTING_QUORUM_PERCENTAGE = 0 - const { xAllocationVoting, otherAccounts, owner, xAllocationPool, x2EarnApps } = + const { xAllocationVoting, otherAccounts, owner, xAllocationPool, x2EarnApps, veBetterPassport } = await getOrDeployContractInstances({ forceDeploy: true, config, @@ -2712,9 +2749,12 @@ describe("X-Allocation Pool", async function () { await bootstrapEmissions() otherAccounts.forEach(async account => { + await veBetterPassport.whitelist(account.address) await getVot3Tokens(account, "10000") }) + await veBetterPassport.toggleCheck(1) + //Add apps const app1Id = ethers.keccak256(ethers.toUtf8Bytes("My app")) const app2Id = ethers.keccak256(ethers.toUtf8Bytes("My app #2")) @@ -2738,39 +2778,19 @@ describe("X-Allocation Pool", async function () { // Vote await xAllocationVoting .connect(otherAccounts[1]) - .castVote( - round1, - [app1Id, app2Id, app3Id], - [ethers.parseEther("0"), ethers.parseEther("900"), ethers.parseEther("100")], - ) + .castVote(round1, [app2Id, app3Id], [ethers.parseEther("900"), ethers.parseEther("100")]) await xAllocationVoting .connect(otherAccounts[2]) - .castVote( - round1, - [app1Id, app2Id, app3Id], - [ethers.parseEther("0"), ethers.parseEther("500"), ethers.parseEther("100")], - ) + .castVote(round1, [app2Id, app3Id], [ethers.parseEther("500"), ethers.parseEther("100")]) await xAllocationVoting .connect(otherAccounts[3]) - .castVote( - round1, - [app1Id, app2Id, app3Id], - [ethers.parseEther("0"), ethers.parseEther("100"), ethers.parseEther("100")], - ) + .castVote(round1, [app2Id, app3Id], [ethers.parseEther("100"), ethers.parseEther("100")]) await xAllocationVoting .connect(otherAccounts[4]) - .castVote( - round1, - [app1Id, app2Id, app3Id], - [ethers.parseEther("0"), ethers.parseEther("100"), ethers.parseEther("100")], - ) + .castVote(round1, [app2Id, app3Id], [ethers.parseEther("100"), ethers.parseEther("100")]) await xAllocationVoting .connect(otherAccounts[5]) - .castVote( - round1, - [app1Id, app2Id, app3Id], - [ethers.parseEther("1000"), ethers.parseEther("0"), ethers.parseEther("100")], - ) + .castVote(round1, [app1Id, app3Id], [ethers.parseEther("1000"), ethers.parseEther("100")]) // Turn off quadratic funding await xAllocationPool.connect(owner).toggleQuadraticFunding() @@ -2796,39 +2816,19 @@ describe("X-Allocation Pool", async function () { // Vote await xAllocationVoting .connect(otherAccounts[1]) - .castVote( - round2, - [app1Id, app2Id, app3Id], - [ethers.parseEther("0"), ethers.parseEther("900"), ethers.parseEther("100")], - ) + .castVote(round2, [app2Id, app3Id], [ethers.parseEther("900"), ethers.parseEther("100")]) await xAllocationVoting .connect(otherAccounts[2]) - .castVote( - round2, - [app1Id, app2Id, app3Id], - [ethers.parseEther("0"), ethers.parseEther("500"), ethers.parseEther("100")], - ) + .castVote(round2, [app2Id, app3Id], [ethers.parseEther("500"), ethers.parseEther("100")]) await xAllocationVoting .connect(otherAccounts[3]) - .castVote( - round2, - [app1Id, app2Id, app3Id], - [ethers.parseEther("0"), ethers.parseEther("100"), ethers.parseEther("100")], - ) + .castVote(round2, [app2Id, app3Id], [ethers.parseEther("100"), ethers.parseEther("100")]) await xAllocationVoting .connect(otherAccounts[4]) - .castVote( - round2, - [app1Id, app2Id, app3Id], - [ethers.parseEther("0"), ethers.parseEther("100"), ethers.parseEther("100")], - ) + .castVote(round2, [app2Id, app3Id], [ethers.parseEther("100"), ethers.parseEther("100")]) await xAllocationVoting .connect(otherAccounts[5]) - .castVote( - round2, - [app1Id, app2Id, app3Id], - [ethers.parseEther("1000"), ethers.parseEther("0"), ethers.parseEther("100")], - ) + .castVote(round2, [app1Id, app3Id], [ethers.parseEther("1000"), ethers.parseEther("100")]) await waitForRoundToEnd(round2) diff --git a/test/XAllocationVoting.test.ts b/test/XAllocationVoting.test.ts index 883adfe..347cad7 100644 --- a/test/XAllocationVoting.test.ts +++ b/test/XAllocationVoting.test.ts @@ -27,7 +27,7 @@ import { deployProxy } from "../scripts/helpers" import { XAllocationVoting } from "../typechain-types" import { createLocalConfig } from "../config/contracts/envs/local" -describe("X-Allocation Voting", function () { +describe("X-Allocation Voting - @shard4", function () { describe("Deployment", function () { it("Admins and addresses should be set correctly", async function () { const { xAllocationVoting, owner, timeLock, emissions, x2EarnApps } = await getOrDeployContractInstances({ @@ -98,7 +98,7 @@ describe("X-Allocation Voting", function () { }) expect(await xAllocationVoting.name()).to.eql("XAllocationVoting") - expect(await xAllocationVoting.version()).to.eql("1") + expect(await xAllocationVoting.version()).to.eql("2") }) it("Counting mode is set correctly", async function () { @@ -352,10 +352,11 @@ describe("X-Allocation Voting", function () { it("should be able to upgrade the xAllocationVoting contract through governance", async function () { const config = createLocalConfig() config.B3TR_GOVERNOR_DEPOSIT_THRESHOLD = 0 - const { xAllocationVoting, timeLock, governor, owner, otherAccount, vot3 } = await getOrDeployContractInstances({ - forceDeploy: true, - config, - }) + const { xAllocationVoting, timeLock, governor, owner, otherAccount, vot3, veBetterPassport } = + await getOrDeployContractInstances({ + forceDeploy: true, + config, + }) await getVot3Tokens(otherAccount, "1000") await vot3.connect(otherAccount).approve(await governor.getAddress(), "1000") @@ -371,6 +372,10 @@ describe("X-Allocation Voting", function () { await bootstrapAndStartEmissions() + // Whitelist user + await veBetterPassport.whitelist(otherAccount.address) + await veBetterPassport.toggleCheck(1) + // V1 Contract const V1Contract = await ethers.getContractAt("XAllocationVoting", await xAllocationVoting.getAddress()) @@ -440,7 +445,7 @@ describe("X-Allocation Voting", function () { forceDeploy: true, }) - expect(await xAllocationVoting.version()).to.equal("1") + expect(await xAllocationVoting.version()).to.equal("2") }) }) @@ -606,10 +611,14 @@ describe("X-Allocation Voting", function () { governorQuorumLogicLib, governorStateLogicLib, governorVotesLogicLib, + veBetterPassport, } = await getOrDeployContractInstances({ forceDeploy: true, }) + await veBetterPassport.whitelist(owner.address) + await veBetterPassport.toggleCheck(1) + const newThreshold = 10n await createProposalAndExecuteIt( owner, @@ -652,11 +661,14 @@ describe("X-Allocation Voting", function () { describe("Quorum", function () { it("Governance can change quorum percentage", async function () { - const { xAllocationVoting, owner } = await getOrDeployContractInstances({ + const { xAllocationVoting, owner, veBetterPassport } = await getOrDeployContractInstances({ forceDeploy: true, }) await bootstrapAndStartEmissions() + await veBetterPassport.whitelist(owner.address) + await veBetterPassport.toggleCheck(1) + await createProposalAndExecuteIt( owner, owner, @@ -679,11 +691,14 @@ describe("X-Allocation Voting", function () { }) it("Cannot set the quorum nominator higher than the denominator", async function () { - const { xAllocationVoting, owner } = await getOrDeployContractInstances({ + const { xAllocationVoting, owner, veBetterPassport } = await getOrDeployContractInstances({ forceDeploy: true, }) await bootstrapAndStartEmissions() + await veBetterPassport.whitelist(owner.address) + await veBetterPassport.toggleCheck(1) + await expect( createProposalAndExecuteIt( owner, @@ -698,7 +713,7 @@ describe("X-Allocation Voting", function () { }) it("Can get quorum of round successfully", async function () { - const { xAllocationVoting, otherAccount } = await getOrDeployContractInstances({ + const { xAllocationVoting, otherAccount, veBetterPassport } = await getOrDeployContractInstances({ forceDeploy: true, }) @@ -707,26 +722,34 @@ describe("X-Allocation Voting", function () { // Bootstrap emissions await bootstrapEmissions() - const round1 = await startNewAllocationRound() + // whitelist user + await veBetterPassport.whitelist(otherAccount.address) + await veBetterPassport.toggleCheck(1) + + let round1 = await startNewAllocationRound() await waitForRoundToEnd(round1) - const quorum = await xAllocationVoting.roundQuorum(round1) + let quorum = await xAllocationVoting.roundQuorum(round1) - const snapshot = await xAllocationVoting.roundSnapshot(round1) - const quorumAtSnapshot = await xAllocationVoting.quorum(snapshot) + let snapshot = await xAllocationVoting.roundSnapshot(round1) + let quorumAtSnapshot = await xAllocationVoting.quorum(snapshot) expect(quorum).to.eql(quorumAtSnapshot) }) it("Returns the quorum numerator correctly at a specific timepoint", async function () { - const { xAllocationVoting, otherAccount } = await getOrDeployContractInstances({ + const { xAllocationVoting, otherAccount, veBetterPassport } = await getOrDeployContractInstances({ forceDeploy: true, }) await getVot3Tokens(otherAccount, "1000") + // whitelist user + await veBetterPassport.whitelist(otherAccount.address) + await veBetterPassport.toggleCheck(1) + // @ts-ignore - const initialQuorumNumerator = await xAllocationVoting.quorumNumerator() + let initialQuorumNumerator = await xAllocationVoting.quorumNumerator() // Bootstrap emissions await bootstrapAndStartEmissions() @@ -741,9 +764,9 @@ describe("X-Allocation Voting", function () { [1], ) - const snapshot = await xAllocationVoting.roundSnapshot(1) + let snapshot = await xAllocationVoting.roundSnapshot(1) //@ts-ignore - const quorumNumerator = await xAllocationVoting.quorumNumerator(snapshot, {}) + let quorumNumerator = await xAllocationVoting.quorumNumerator(snapshot, {}) expect(quorumNumerator).to.eql(initialQuorumNumerator) }) @@ -756,13 +779,17 @@ describe("X-Allocation Voting", function () { }) it("Can set voting period if less than emissions cycle duration", async function () { - const { xAllocationVoting, owner, emissions, governor, otherAccount } = await getOrDeployContractInstances({ - forceDeploy: true, - }) + const { xAllocationVoting, owner, emissions, governor, otherAccount, veBetterPassport } = + await getOrDeployContractInstances({ + forceDeploy: true, + }) await bootstrapAndStartEmissions() await getVot3Tokens(otherAccount, "30000") const cycleDuration = await emissions.cycleDuration() + await veBetterPassport.whitelist(otherAccount.address) + await veBetterPassport.toggleCheck(1) + // Now we can create a proposal const encodedFunctionCall = xAllocationVoting.interface.encodeFunctionData("setVotingPeriod", [ cycleDuration - 1n, @@ -801,11 +828,14 @@ describe("X-Allocation Voting", function () { }) it("Cannot set voting period to 0", async function () { - const { xAllocationVoting, owner } = await getOrDeployContractInstances({ + const { xAllocationVoting, owner, veBetterPassport } = await getOrDeployContractInstances({ forceDeploy: true, }) await bootstrapAndStartEmissions() + await veBetterPassport.whitelist(owner.address) + await veBetterPassport.toggleCheck(1) + await expect( createProposalAndExecuteIt( owner, @@ -823,9 +853,14 @@ describe("X-Allocation Voting", function () { }) it("Cannot set voting period if not less than emissions cycle duration", async function () { - const { xAllocationVoting, owner, emissions, governor, otherAccount } = await getOrDeployContractInstances({ - forceDeploy: true, - }) + const { xAllocationVoting, owner, emissions, governor, otherAccount, veBetterPassport } = + await getOrDeployContractInstances({ + forceDeploy: true, + }) + + await veBetterPassport.whitelist(otherAccount.address) + await veBetterPassport.toggleCheck(1) + await bootstrapAndStartEmissions() await getVot3Tokens(otherAccount, "30000") const cycleDuration = await emissions.cycleDuration() @@ -997,15 +1032,15 @@ describe("X-Allocation Voting", function () { .connect(owner) .addApp(otherAccounts[0].address, otherAccounts[0].address, otherAccounts[0].address, "metadataURI") - const tx = await xAllocationVoting.connect(owner).startNewRound() - const receipt = await tx.wait() + let tx = await xAllocationVoting.connect(owner).startNewRound() + let receipt = await tx.wait() if (!receipt) throw new Error("No receipt") // Event should be emitted - const roundCreated = filterEventsByName(receipt.logs, "RoundCreated") + let roundCreated = filterEventsByName(receipt.logs, "RoundCreated") expect(roundCreated).not.to.eql([]) - const { roundId, proposer, voteStart, voteEnd, appsIds } = parseRoundStartedEvent( + let { roundId, proposer, voteStart, voteEnd, appsIds } = parseRoundStartedEvent( roundCreated[0], xAllocationVoting, ) @@ -1016,10 +1051,10 @@ describe("X-Allocation Voting", function () { expect(appsIds).to.eql(await xAllocationVoting.getAppIdsOfRound(roundId)) //Proposal should be active - const roundState = await xAllocationVoting.state(roundId) + let roundState = await xAllocationVoting.state(roundId) expect(roundState).to.eql(BigInt(0)) - const round = await xAllocationVoting.getRound(roundId) + let round = await xAllocationVoting.getRound(roundId) expect(round.proposer).to.eql(owner.address) expect(round.voteStart.toString()).to.eql(receipt.blockNumber.toString()) expect(round.voteDuration).to.eql(await xAllocationVoting.votingPeriod()) @@ -1032,18 +1067,18 @@ describe("X-Allocation Voting", function () { await getVot3Tokens(otherAccount, "1000") - const tx = await xAllocationVoting.connect(owner).startNewRound() - const receipt = await tx.wait() + let tx = await xAllocationVoting.connect(owner).startNewRound() + let receipt = await tx.wait() if (!receipt) throw new Error("No receipt") // Event should be emitted - const roundCreated = filterEventsByName(receipt.logs, "RoundCreated") + let roundCreated = filterEventsByName(receipt.logs, "RoundCreated") expect(roundCreated).not.to.eql([]) - const { roundId } = parseRoundStartedEvent(roundCreated[0], xAllocationVoting) + let { roundId } = parseRoundStartedEvent(roundCreated[0], xAllocationVoting) // Proposal should be active - const roundState = await xAllocationVoting.state(roundId) + let roundState = await xAllocationVoting.state(roundId) expect(roundState).to.eql(BigInt(0)) // should not be able to start a new allocation round if there is an active one @@ -1142,9 +1177,9 @@ describe("X-Allocation Voting", function () { await emissions.connect(minterAccount).start() - const roundId = await xAllocationVoting.currentRoundId() - const roundSnapshot = await xAllocationVoting.currentRoundSnapshot() - const deadline = await xAllocationVoting.currentRoundDeadline() + let roundId = await xAllocationVoting.currentRoundId() + let roundSnapshot = await xAllocationVoting.currentRoundSnapshot() + let deadline = await xAllocationVoting.currentRoundDeadline() expect(roundSnapshot).to.eql(await xAllocationVoting.roundSnapshot(roundId)) expect(deadline).to.eql(await xAllocationVoting.roundDeadline(roundId)) @@ -1160,8 +1195,8 @@ describe("X-Allocation Voting", function () { await emissions.connect(minterAccount).start() - const roundId = await xAllocationVoting.currentRoundId() - const roundProposer = await xAllocationVoting.roundProposer(roundId) + let roundId = await xAllocationVoting.currentRoundId() + let roundProposer = await xAllocationVoting.roundProposer(roundId) expect(roundProposer).to.eql(await emissions.getAddress()) }) @@ -1203,13 +1238,17 @@ describe("X-Allocation Voting", function () { describe("Allocation Voting", function () { it("I cannot cast a vote with higher balance than I have", async function () { - const { xAllocationVoting, x2EarnApps, otherAccounts, otherAccount, owner } = await getOrDeployContractInstances({ - forceDeploy: true, - }) + const { xAllocationVoting, x2EarnApps, otherAccounts, otherAccount, owner, veBetterPassport } = + await getOrDeployContractInstances({ + forceDeploy: true, + }) // Bootstrap emissions await bootstrapEmissions() + await veBetterPassport.whitelist(otherAccount.address) + await veBetterPassport.toggleCheck(1) + await x2EarnApps .connect(owner) .addApp(otherAccounts[0].address, otherAccounts[0].address, otherAccounts[0].address, "metadataURI") @@ -1217,22 +1256,33 @@ describe("X-Allocation Voting", function () { await getVot3Tokens(otherAccount, "1000") - const tx = await xAllocationVoting.startNewRound() - const receipt = await tx.wait() + let tx = await xAllocationVoting.startNewRound() + let receipt = await tx.wait() if (!receipt) throw new Error("No receipt") // Event should be emitted - const roundCreated = filterEventsByName(receipt.logs, "RoundCreated") - const { roundId } = parseRoundStartedEvent(roundCreated[0], xAllocationVoting) + let roundCreated = filterEventsByName(receipt.logs, "RoundCreated") + let { roundId } = parseRoundStartedEvent(roundCreated[0], xAllocationVoting) // I cannot cast a vote with higher balance than I have await catchRevert(xAllocationVoting.connect(otherAccount).castVote(roundId, [app1], [ethers.parseEther("1500")])) }) - it("I should be able to cast a vote", async function () { - const { xAllocationVoting, x2EarnApps, otherAccounts, otherAccount, owner, emissions, minterAccount } = - await getOrDeployContractInstances({ - forceDeploy: true, - }) + it("I should be able to cast a vote if I am considered a valid person", async function () { + const { + xAllocationVoting, + x2EarnApps, + veBetterPassport, + otherAccounts, + otherAccount, + owner, + emissions, + minterAccount, + } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + await veBetterPassport.whitelist(otherAccount.address) + await veBetterPassport.toggleCheck(1) // Bootstrap emissions await bootstrapEmissions() @@ -1246,40 +1296,68 @@ describe("X-Allocation Voting", function () { await emissions.connect(minterAccount).start() - const roundId = await xAllocationVoting.currentRoundId() + let roundId = await xAllocationVoting.currentRoundId() expect(roundId).to.eql(1n) // I should be able to cast a vote - const tx = await xAllocationVoting.connect(otherAccount).castVote(roundId, [app1], [ethers.parseEther("1000")]) - const receipt = await tx.wait() + let tx = await xAllocationVoting.connect(otherAccount).castVote(roundId, [app1], [ethers.parseEther("1000")]) + let receipt = await tx.wait() if (!receipt) throw new Error("No receipt") - const allocationVoteCast = filterEventsByName(receipt.logs, "AllocationVoteCast") + let allocationVoteCast = filterEventsByName(receipt.logs, "AllocationVoteCast") expect(allocationVoteCast).not.to.eql([]) - const { + let { voter, apps: votedApps, voteWeights, roundId: votedRoundId, } = parseAllocationVoteCastEvent(allocationVoteCast[0], xAllocationVoting) + expect(voter).to.eql(otherAccount.address) expect(votedRoundId).to.eql(roundId) expect(votedApps).to.eql([app1]) expect(voteWeights).to.eql([ethers.parseEther("1000")]) // Votes should be tracked correctly - const appVotes = await xAllocationVoting.getAppVotes(roundId, app1) + let appVotes = await xAllocationVoting.getAppVotes(roundId, app1) expect(appVotes).to.eql(ethers.parseEther("1000")) - const totalVotes = await xAllocationVoting.totalVotes(roundId) + let totalVotes = await xAllocationVoting.totalVotes(roundId) expect(totalVotes).to.eql(ethers.parseEther("1000")) }) + it("I should not be able to cast a vote if I am not considered a person", async function () { + const { xAllocationVoting, x2EarnApps, otherAccounts, otherAccount, owner, emissions, minterAccount } = + await getOrDeployContractInstances({ + forceDeploy: true, + }) + + // Bootstrap emissions + await bootstrapEmissions() + + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[0].address, otherAccounts[0].address, otherAccounts[0].address, "metadataURI") + const app1 = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[0].address)) + + await getVot3Tokens(otherAccount, "1000") + + await emissions.connect(minterAccount).start() + + let roundId = await xAllocationVoting.currentRoundId() + expect(roundId).to.eql(1n) + + // I should be able to cast a vote + await expect(xAllocationVoting.connect(otherAccount).castVote(roundId, [app1], [ethers.parseEther("1000")])).to.be + .reverted + }) + it("I should not be able to cast vote if my total VOT3 holding is less than 1", async function () { - const { x2EarnApps, xAllocationVoting, otherAccounts, otherAccount, owner } = await getOrDeployContractInstances({ - forceDeploy: true, - }) + const { x2EarnApps, xAllocationVoting, otherAccounts, otherAccount, owner, veBetterPassport } = + await getOrDeployContractInstances({ + forceDeploy: true, + }) // Bootstrap emissions await bootstrapEmissions() @@ -1288,14 +1366,17 @@ describe("X-Allocation Voting", function () { .addApp(otherAccounts[0].address, otherAccounts[0].address, otherAccounts[0].address, "metadataURI") const app1 = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[0].address)) + await veBetterPassport.whitelist(otherAccount.address) + await veBetterPassport.toggleCheck(1) + await getVot3Tokens(otherAccount, "0.1") - const tx = await xAllocationVoting.startNewRound() - const receipt = await tx.wait() + let tx = await xAllocationVoting.startNewRound() + let receipt = await tx.wait() if (!receipt) throw new Error("No receipt") // Event should be emitted - const roundCreated = filterEventsByName(receipt.logs, "RoundCreated") - const { roundId } = parseRoundStartedEvent(roundCreated[0], xAllocationVoting) + let roundCreated = filterEventsByName(receipt.logs, "RoundCreated") + let { roundId } = parseRoundStartedEvent(roundCreated[0], xAllocationVoting) // I cannot cast a vote twice for the same round await expect( @@ -1304,12 +1385,16 @@ describe("X-Allocation Voting", function () { }) it("I should not be able to cast vote twice", async function () { - const { xAllocationVoting, x2EarnApps, otherAccounts, otherAccount, owner } = await getOrDeployContractInstances({ - forceDeploy: true, - }) + const { xAllocationVoting, x2EarnApps, otherAccounts, otherAccount, owner, veBetterPassport } = + await getOrDeployContractInstances({ + forceDeploy: true, + }) // Bootstrap emissions await bootstrapEmissions() + await veBetterPassport.whitelist(otherAccount.address) + await veBetterPassport.toggleCheck(1) + await x2EarnApps .connect(owner) .addApp(otherAccounts[0].address, otherAccounts[0].address, otherAccounts[0].address, "metadataURI") @@ -1321,8 +1406,8 @@ describe("X-Allocation Voting", function () { let receipt = await tx.wait() if (!receipt) throw new Error("No receipt") // Event should be emitted - const roundCreated = filterEventsByName(receipt.logs, "RoundCreated") - const { roundId } = parseRoundStartedEvent(roundCreated[0], xAllocationVoting) + let roundCreated = filterEventsByName(receipt.logs, "RoundCreated") + let { roundId } = parseRoundStartedEvent(roundCreated[0], xAllocationVoting) // I should be able to cast a vote tx = await xAllocationVoting.connect(otherAccount).castVote(roundId, [app1], [ethers.parseEther("500")]) @@ -1333,9 +1418,13 @@ describe("X-Allocation Voting", function () { }) it("Cannot cast a vote if the allocation round ended", async function () { - const { xAllocationVoting, x2EarnApps, otherAccounts, otherAccount, owner } = await getOrDeployContractInstances({ - forceDeploy: true, - }) + const { xAllocationVoting, x2EarnApps, otherAccounts, otherAccount, owner, veBetterPassport } = + await getOrDeployContractInstances({ + forceDeploy: true, + }) + + await veBetterPassport.whitelist(otherAccount.address) + await veBetterPassport.toggleCheck(1) // Bootstrap emissions await bootstrapEmissions() @@ -1351,8 +1440,8 @@ describe("X-Allocation Voting", function () { let receipt = await tx.wait() if (!receipt) throw new Error("No receipt") // Event should be emitted - const roundCreated = filterEventsByName(receipt.logs, "RoundCreated") - const { roundId } = parseRoundStartedEvent(roundCreated[0], xAllocationVoting) + let roundCreated = filterEventsByName(receipt.logs, "RoundCreated") + let { roundId } = parseRoundStartedEvent(roundCreated[0], xAllocationVoting) // I should be able to cast a vote tx = await xAllocationVoting.connect(otherAccount).castVote(roundId, [app1], [ethers.parseEther("500")]) @@ -1365,12 +1454,16 @@ describe("X-Allocation Voting", function () { }) it("I should be able to vote for multiple apps", async function () { - const { xAllocationVoting, x2EarnApps, otherAccounts, otherAccount, owner } = await getOrDeployContractInstances({ - forceDeploy: true, - }) + const { xAllocationVoting, x2EarnApps, otherAccounts, otherAccount, owner, veBetterPassport } = + await getOrDeployContractInstances({ + forceDeploy: true, + }) // Bootstrap emissions await bootstrapEmissions() + await veBetterPassport.whitelist(otherAccount.address) + await veBetterPassport.toggleCheck(1) + await x2EarnApps .connect(owner) .addApp(otherAccounts[0].address, otherAccounts[0].address, otherAccounts[0].address, "metadataURI") @@ -1386,9 +1479,9 @@ describe("X-Allocation Voting", function () { let receipt = await tx.wait() if (!receipt) throw new Error("No receipt") - const roundCreated = filterEventsByName(receipt.logs, "RoundCreated") + let roundCreated = filterEventsByName(receipt.logs, "RoundCreated") expect(roundCreated).not.to.eql([]) - const { roundId } = parseRoundStartedEvent(roundCreated[0], xAllocationVoting) + let { roundId } = parseRoundStartedEvent(roundCreated[0], xAllocationVoting) await waitForNextBlock() @@ -1403,7 +1496,7 @@ describe("X-Allocation Voting", function () { expect(avaiableApps[0]).to.equal(app1) expect(avaiableApps[1]).to.equal(app2) - const appsVotedInSpecificRound = await xAllocationVoting.getAppIdsOfRound(roundId) + let appsVotedInSpecificRound = await xAllocationVoting.getAppIdsOfRound(roundId) expect(appsVotedInSpecificRound.length).to.equal(2) expect(appsVotedInSpecificRound[0]).to.equal(app1) expect(appsVotedInSpecificRound[1]).to.equal(app2) @@ -1415,9 +1508,9 @@ describe("X-Allocation Voting", function () { receipt = await tx.wait() if (!receipt) throw new Error("No receipt") - const allocationVoteCast = filterEventsByName(receipt.logs, "AllocationVoteCast") + let allocationVoteCast = filterEventsByName(receipt.logs, "AllocationVoteCast") expect(roundCreated).not.to.eql([]) - const { + let { voter, apps: votedApps, voteWeights, @@ -1434,14 +1527,15 @@ describe("X-Allocation Voting", function () { appVotes = await xAllocationVoting.getAppVotes(roundId, app2) expect(appVotes).to.eql(ethers.parseEther("200")) - const totalVotes = await xAllocationVoting.totalVotes(roundId) + let totalVotes = await xAllocationVoting.totalVotes(roundId) expect(totalVotes).to.eql(ethers.parseEther("500")) }) it("Votes should be tracked correctly", async function () { - const { xAllocationVoting, x2EarnApps, otherAccounts, otherAccount, owner } = await getOrDeployContractInstances({ - forceDeploy: true, - }) + const { xAllocationVoting, x2EarnApps, otherAccounts, otherAccount, owner, veBetterPassport } = + await getOrDeployContractInstances({ + forceDeploy: true, + }) // Bootstrap emissions await bootstrapEmissions() @@ -1457,6 +1551,11 @@ describe("X-Allocation Voting", function () { const voter2 = otherAccounts[3] const voter3 = otherAccounts[4] + await veBetterPassport.whitelist(otherAccount.address) + await veBetterPassport.whitelist(voter2.address) + await veBetterPassport.whitelist(voter3.address) + await veBetterPassport.toggleCheck(1) + await getVot3Tokens(otherAccount, "1000") await getVot3Tokens(voter2, "1000") await getVot3Tokens(voter3, "1000") @@ -1465,9 +1564,9 @@ describe("X-Allocation Voting", function () { let receipt = await tx.wait() if (!receipt) throw new Error("No receipt") - const roundCreated = filterEventsByName(receipt.logs, "RoundCreated") + let roundCreated = filterEventsByName(receipt.logs, "RoundCreated") expect(roundCreated).not.to.eql([]) - const { roundId } = parseRoundStartedEvent(roundCreated[0], xAllocationVoting) + let { roundId } = parseRoundStartedEvent(roundCreated[0], xAllocationVoting) tx = await xAllocationVoting .connect(otherAccount) @@ -1497,7 +1596,7 @@ describe("X-Allocation Voting", function () { expect(totalVotes).to.eql(ethers.parseEther("1400")) // Total voters should be tracked correctly - const totalVoters = await xAllocationVoting.totalVoters(roundId) + let totalVoters = await xAllocationVoting.totalVoters(roundId) expect(totalVoters).to.eql(BigInt(3)) await waitForRoundToEnd(roundId) @@ -1540,26 +1639,26 @@ describe("X-Allocation Voting", function () { await emissions.connect(minterAccount).start() - const roundId = await xAllocationVoting.currentRoundId() + let roundId = await xAllocationVoting.currentRoundId() expect(roundId).to.eql(1n) await waitForRoundToEnd(Number(roundId)) expect(await xAllocationVoting.state(roundId)).to.eql(1n) // quorum failed // Votes should be tracked correctly - const appVotes = await xAllocationVoting.getAppVotes(roundId, app1) + let appVotes = await xAllocationVoting.getAppVotes(roundId, app1) expect(appVotes).to.eql(ethers.parseEther("0")) - const totalVotes = await xAllocationVoting.totalVotes(roundId) + let totalVotes = await xAllocationVoting.totalVotes(roundId) expect(totalVotes).to.eql(ethers.parseEther("0")) - const totalVoters = await xAllocationVoting.totalVoters(roundId) + let totalVoters = await xAllocationVoting.totalVoters(roundId) expect(totalVoters).to.eql(BigInt(0)) - const appShares = await xAllocationPool.getAppShares(roundId, app1) + let appShares = await xAllocationPool.getAppShares(roundId, app1) expect(appShares).to.eql([0n, 0n]) - const appEarnings = await xAllocationPool.roundEarnings(roundId, app1) + let appEarnings = await xAllocationPool.roundEarnings(roundId, app1) expect(appEarnings).to.eql([ await xAllocationPool.baseAllocationAmount(roundId), 0n, @@ -1569,13 +1668,17 @@ describe("X-Allocation Voting", function () { }) it("I should be able to vote only for apps available in the allocation round", async function () { - const { xAllocationVoting, x2EarnApps, otherAccounts, otherAccount, owner } = await getOrDeployContractInstances({ - forceDeploy: true, - }) + const { xAllocationVoting, x2EarnApps, otherAccounts, otherAccount, owner, veBetterPassport } = + await getOrDeployContractInstances({ + forceDeploy: true, + }) // Bootstrap emissions await bootstrapEmissions() + await veBetterPassport.whitelist(otherAccount.address) + await veBetterPassport.toggleCheck(1) + await x2EarnApps .connect(owner) .addApp(otherAccounts[0].address, otherAccounts[0].address, otherAccounts[0].address, "metadataURI") @@ -1588,13 +1691,13 @@ describe("X-Allocation Voting", function () { await getVot3Tokens(otherAccount, "1000") - const tx = await xAllocationVoting.startNewRound() - const receipt = await tx.wait() + let tx = await xAllocationVoting.startNewRound() + let receipt = await tx.wait() if (!receipt) throw new Error("No receipt") - const roundCreated = filterEventsByName(receipt.logs, "RoundCreated") + let roundCreated = filterEventsByName(receipt.logs, "RoundCreated") expect(roundCreated).not.to.eql([]) - const { roundId } = parseRoundStartedEvent(roundCreated[0], xAllocationVoting) + let { roundId } = parseRoundStartedEvent(roundCreated[0], xAllocationVoting) await catchRevert(xAllocationVoting.connect(otherAccount).castVote(roundId, [app3], [ethers.parseEther("300")])) @@ -1606,16 +1709,19 @@ describe("X-Allocation Voting", function () { appVotes = await xAllocationVoting.getAppVotes(roundId, app3) expect(appVotes).to.eql(ethers.parseEther("0")) - const totalVotes = await xAllocationVoting.totalVotes(roundId) + let totalVotes = await xAllocationVoting.totalVotes(roundId) expect(totalVotes).to.eql(ethers.parseEther("0")) }) it("Allocation round should be successfull if quorum was reached", async function () { - const { xAllocationVoting, x2EarnApps, otherAccounts, otherAccount, owner, vot3 } = + const { xAllocationVoting, x2EarnApps, otherAccounts, otherAccount, owner, vot3, veBetterPassport } = await getOrDeployContractInstances({ forceDeploy: true, }) + await veBetterPassport.whitelist(otherAccount.address) + await veBetterPassport.toggleCheck(1) + // Bootstrap emissions await bootstrapEmissions() @@ -1631,14 +1737,14 @@ describe("X-Allocation Voting", function () { await getVot3Tokens(otherAccount, "1000") let tx = await xAllocationVoting.startNewRound() - const receipt = await tx.wait() + let receipt = await tx.wait() if (!receipt) throw new Error("No receipt") const timepoint = receipt.blockNumber - const roundCreated = filterEventsByName(receipt.logs, "RoundCreated") + let roundCreated = filterEventsByName(receipt.logs, "RoundCreated") expect(roundCreated).not.to.eql([]) - const { roundId } = parseRoundStartedEvent(roundCreated[0], xAllocationVoting) + let { roundId } = parseRoundStartedEvent(roundCreated[0], xAllocationVoting) tx = await xAllocationVoting .connect(otherAccount) @@ -1659,11 +1765,14 @@ describe("X-Allocation Voting", function () { }).timeout(18000000) it("Allocation round should be failed if quorum was not reached", async function () { - const { xAllocationVoting, x2EarnApps, otherAccounts, otherAccount, owner, vot3 } = + const { xAllocationVoting, x2EarnApps, otherAccounts, otherAccount, owner, vot3, veBetterPassport } = await getOrDeployContractInstances({ forceDeploy: true, }) + await veBetterPassport.whitelist(otherAccount.address) + await veBetterPassport.toggleCheck(1) + // Bootstrap emissions await bootstrapEmissions() @@ -1679,13 +1788,13 @@ describe("X-Allocation Voting", function () { await getVot3Tokens(otherAccount, "1000") let tx = await xAllocationVoting.startNewRound() - const receipt = await tx.wait() + let receipt = await tx.wait() if (!receipt) throw new Error("No receipt") const timepoint = receipt.blockNumber - const roundCreated = filterEventsByName(receipt.logs, "RoundCreated") + let roundCreated = filterEventsByName(receipt.logs, "RoundCreated") expect(roundCreated).not.to.eql([]) - const { roundId } = parseRoundStartedEvent(roundCreated[0], xAllocationVoting) + let { roundId } = parseRoundStartedEvent(roundCreated[0], xAllocationVoting) tx = await xAllocationVoting .connect(otherAccount) @@ -1721,7 +1830,7 @@ describe("X-Allocation Voting", function () { .connect(owner) .addApp(otherAccounts[1].address, otherAccounts[1].address, otherAccounts[1].address, "metadataURI") const app2 = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[1].address)) - const round1 = await startNewAllocationRound() + let round1 = await startNewAllocationRound() let getAppIdsOfRound = await xAllocationVoting.getAppIdsOfRound(round1) expect(getAppIdsOfRound.length).to.equal(2n) @@ -1735,7 +1844,7 @@ describe("X-Allocation Voting", function () { await waitForRoundToEnd(round1) // 4 apps in round2 - const round2 = await startNewAllocationRound() + let round2 = await startNewAllocationRound() getAppIdsOfRound = await xAllocationVoting.getAppIdsOfRound(round2) expect(getAppIdsOfRound.length).to.equal(4n) @@ -1745,7 +1854,7 @@ describe("X-Allocation Voting", function () { await waitForRoundToEnd(round2) // 2 app in round 3 - const round3 = await startNewAllocationRound() + let round3 = await startNewAllocationRound() getAppIdsOfRound = await xAllocationVoting.getAppIdsOfRound(round3) expect(getAppIdsOfRound.length).to.equal(2n) @@ -1756,7 +1865,7 @@ describe("X-Allocation Voting", function () { await waitForRoundToEnd(round3) // 3 apps in round 4 - const round4 = await startNewAllocationRound() + let round4 = await startNewAllocationRound() getAppIdsOfRound = await xAllocationVoting.getAppIdsOfRound(round4) expect(getAppIdsOfRound.length).to.equal(3n) @@ -1783,11 +1892,11 @@ describe("X-Allocation Voting", function () { .connect(owner) .addApp(otherAccounts[1].address, otherAccounts[1].address, otherAccounts[1].address, "metadataURI") const app2 = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[1].address)) - const round1 = await startNewAllocationRound() - const getAppIdsOfRound = await xAllocationVoting.getAppIdsOfRound(round1) + let round1 = await startNewAllocationRound() + let getAppIdsOfRound = await xAllocationVoting.getAppIdsOfRound(round1) expect(getAppIdsOfRound.length).to.equal(2n) - const apps = await xAllocationVoting.getAppsOfRound(round1) + let apps = await xAllocationVoting.getAppsOfRound(round1) expect(apps.length).to.equal(2n) expect(apps[0].id).to.equal(app1) expect(apps[1].id).to.equal(app2) @@ -1796,13 +1905,17 @@ describe("X-Allocation Voting", function () { }) it("Stores that a user voted at least once", async function () { - const { xAllocationVoting, x2EarnApps, otherAccount, owner } = await getOrDeployContractInstances({ - forceDeploy: true, - }) + const { xAllocationVoting, x2EarnApps, otherAccount, owner, veBetterPassport } = + await getOrDeployContractInstances({ + forceDeploy: true, + }) // Bootstrap emissions await bootstrapEmissions() + await veBetterPassport.whitelist(otherAccount.address) + await veBetterPassport.toggleCheck(1) + // Check if user voted let voted = await xAllocationVoting.hasVotedOnce(otherAccount.address) expect(voted).to.equal(false) @@ -1826,9 +1939,13 @@ describe("X-Allocation Voting", function () { }) it("Cannot cast vote with apps and weights length mismatch", async function () { - const { xAllocationVoting, x2EarnApps, otherAccounts, otherAccount, owner } = await getOrDeployContractInstances({ - forceDeploy: true, - }) + const { xAllocationVoting, x2EarnApps, otherAccounts, otherAccount, owner, veBetterPassport } = + await getOrDeployContractInstances({ + forceDeploy: true, + }) + + await veBetterPassport.whitelist(otherAccount.address) + await veBetterPassport.toggleCheck(1) await x2EarnApps .connect(owner) @@ -1840,7 +1957,7 @@ describe("X-Allocation Voting", function () { // Bootstrap emissions await bootstrapAndStartEmissions() - const roundId = await xAllocationVoting.currentRoundId() + let roundId = await xAllocationVoting.currentRoundId() expect(roundId).to.eql(1n) // I should be able to cast a vote @@ -1852,9 +1969,13 @@ describe("X-Allocation Voting", function () { }) it("Cannot cast vote with no apps to vote for", async function () { - const { xAllocationVoting, x2EarnApps, otherAccounts, otherAccount, owner } = await getOrDeployContractInstances({ - forceDeploy: true, - }) + const { xAllocationVoting, x2EarnApps, otherAccounts, otherAccount, owner, veBetterPassport } = + await getOrDeployContractInstances({ + forceDeploy: true, + }) + + await veBetterPassport.whitelist(otherAccount.address) + await veBetterPassport.toggleCheck(1) await x2EarnApps .connect(owner) @@ -1865,7 +1986,7 @@ describe("X-Allocation Voting", function () { // Bootstrap emissions await bootstrapAndStartEmissions() - const roundId = await xAllocationVoting.currentRoundId() + let roundId = await xAllocationVoting.currentRoundId() expect(roundId).to.eql(1n) // I should be able to cast a vote @@ -1874,9 +1995,13 @@ describe("X-Allocation Voting", function () { // quorumReached it("Quorum is reached correctly", async function () { - const { xAllocationVoting, x2EarnApps, otherAccount, otherAccounts, owner } = await getOrDeployContractInstances({ - forceDeploy: true, - }) + const { xAllocationVoting, x2EarnApps, otherAccount, otherAccounts, owner, veBetterPassport } = + await getOrDeployContractInstances({ + forceDeploy: true, + }) + + await veBetterPassport.whitelist(otherAccount.address) + await veBetterPassport.toggleCheck(1) // add apps await x2EarnApps @@ -1894,7 +2019,7 @@ describe("X-Allocation Voting", function () { await bootstrapAndStartEmissions() await waitForNextBlock() - const roundId = await xAllocationVoting.currentRoundId() + let roundId = await xAllocationVoting.currentRoundId() expect(roundId).to.eql(1n) expect(await xAllocationVoting.quorumReached(1)).to.eql(false) @@ -1947,11 +2072,11 @@ describe("X-Allocation Voting", function () { forceDeploy: true, }) - const round1 = await startNewAllocationRound() + let round1 = await startNewAllocationRound() await catchRevert(xAllocationVoting.finalizeRound(round1)) - const isFinalized = await xAllocationVoting.isFinalized(round1) + let isFinalized = await xAllocationVoting.isFinalized(round1) expect(isFinalized).to.eql(false) }) @@ -1960,7 +2085,7 @@ describe("X-Allocation Voting", function () { forceDeploy: true, }) - const round1 = await startNewAllocationRound() + let round1 = await startNewAllocationRound() let isFinalized = await xAllocationVoting.isFinalized(round1) expect(isFinalized).to.eql(false) await waitForRoundToEnd(round1) @@ -1980,11 +2105,11 @@ describe("X-Allocation Voting", function () { }) await getVot3Tokens(otherAccount, "1000") - const round1 = await startNewAllocationRound() + let round1 = await startNewAllocationRound() await waitForRoundToEnd(round1) // should be failed since quorum is not reached - const state = await xAllocationVoting.state(round1) + let state = await xAllocationVoting.state(round1) expect(state).to.eql(1n) let isFinalized = await xAllocationVoting.isFinalized(round1) @@ -2014,11 +2139,11 @@ describe("X-Allocation Voting", function () { // check that round 1 is finalized expect(await xAllocationVoting.isFinalized(1)).to.eql(true) - const roundId = await xAllocationVoting.currentRoundId() + let roundId = await xAllocationVoting.currentRoundId() await waitForCurrentRoundToEnd() // should be failed since quorum is not reached - const state = await xAllocationVoting.state(roundId) + let state = await xAllocationVoting.state(roundId) expect(state).to.eql(1n) let isFinalized = await xAllocationVoting.isFinalized(roundId) @@ -2084,11 +2209,11 @@ describe("X-Allocation Voting", function () { forceDeploy: true, }) - const round1 = await startNewAllocationRound() + let round1 = await startNewAllocationRound() await catchRevert(xAllocationVoting.finalizeRound(round1)) - const isFinalized = await xAllocationVoting.isFinalized(round1) + let isFinalized = await xAllocationVoting.isFinalized(round1) expect(isFinalized).to.eql(false) }) @@ -2119,9 +2244,18 @@ describe("X-Allocation Voting", function () { describe("Quadratic Funding", function () { it("Can get the correct QF app votes", async function () { - const { xAllocationVoting, x2EarnApps, otherAccounts, owner } = await getOrDeployContractInstances({ - forceDeploy: true, - }) + const { xAllocationVoting, x2EarnApps, otherAccounts, owner, veBetterPassport } = + await getOrDeployContractInstances({ + forceDeploy: true, + }) + + // Whitelist voters + await veBetterPassport.whitelist(otherAccounts[1].address) + await veBetterPassport.whitelist(otherAccounts[2].address) + await veBetterPassport.whitelist(otherAccounts[3].address) + await veBetterPassport.whitelist(otherAccounts[4].address) + await veBetterPassport.whitelist(otherAccounts[5].address) + await veBetterPassport.toggleCheck(1) // Bootstrap emissions await bootstrapEmissions() @@ -2131,7 +2265,6 @@ describe("X-Allocation Voting", function () { }) //Add apps - const app1Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[2].address)) const app2Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[3].address)) const app3Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[4].address)) diff --git a/test/XApps.test.ts b/test/XApps.test.ts index fd782df..dd7db4d 100644 --- a/test/XApps.test.ts +++ b/test/XApps.test.ts @@ -17,7 +17,7 @@ import { import { describe, it } from "mocha" import { getImplementationAddress } from "@openzeppelin/upgrades-core" -describe("X-Apps", function () { +describe("X-Apps - @shard3", function () { describe("Deployment", function () { it("Clock mode is set correctly", async function () { const { x2EarnApps } = await getOrDeployContractInstances({ forceDeploy: true }) @@ -114,16 +114,16 @@ describe("X-Apps", 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) .addApp(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) }) @@ -371,7 +371,7 @@ describe("X-Apps", function () { .connect(owner) .addApp(otherAccounts[0].address, otherAccounts[0].address, otherAccounts[0].address, "metadataURI") - const roundId = await startNewAllocationRound() + let roundId = await startNewAllocationRound() const isEligibleForVote = await xAllocationVoting.isEligibleForVote(app1Id, roundId) expect(isEligibleForVote).to.eql(true) @@ -389,7 +389,7 @@ describe("X-Apps", function () { .connect(owner) .addApp(otherAccounts[0].address, otherAccounts[0].address, otherAccounts[0].address, "metadataURI") - const round1 = await startNewAllocationRound() + let round1 = await startNewAllocationRound() await x2EarnApps.connect(owner).setVotingEligibility(app1Id, false) @@ -401,7 +401,7 @@ describe("X-Apps", 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) @@ -430,7 +430,7 @@ describe("X-Apps", 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) @@ -445,7 +445,7 @@ describe("X-Apps", 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) @@ -503,7 +503,7 @@ describe("X-Apps", 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) @@ -526,7 +526,7 @@ describe("X-Apps", 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) @@ -556,9 +556,10 @@ describe("X-Apps", function () { }) it("App needs to wait next round if added during an ongoing round", async function () { - const { otherAccounts, x2EarnApps, owner, xAllocationVoting } = await getOrDeployContractInstances({ - forceDeploy: true, - }) + const { otherAccounts, x2EarnApps, owner, xAllocationVoting, veBetterPassport } = + await getOrDeployContractInstances({ + forceDeploy: true, + }) // Bootstrap emissions await bootstrapEmissions() @@ -568,7 +569,10 @@ describe("X-Apps", 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) await x2EarnApps .connect(owner) @@ -582,11 +586,11 @@ describe("X-Apps", 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 not be eligible from this round isEligibleForVote = await xAllocationVoting.isEligibleForVote(app1Id, round2) @@ -749,7 +753,7 @@ describe("X-Apps", function () { expect(appURI).to.eql((await x2EarnApps.baseURI()) + newMetadataURI) }) - it("Unauthtorized users cannot update app metadata", async function () { + it("Unatuhtorized users cannot update app metadata", async function () { const { x2EarnApps, otherAccounts, owner } = await getOrDeployContractInstances({ forceDeploy: true }) const app1Id = ethers.keccak256(ethers.toUtf8Bytes("My app")) const appAdmin = otherAccounts[9] @@ -1389,7 +1393,7 @@ describe("X-Apps", function () { .addApp(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) }) @@ -1439,17 +1443,28 @@ describe("X-Apps", function () { .addApp(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) }) it("Team allocation percentage of an app is 0 and apps need to withdraw, then they can change this", async function () { - const { x2EarnApps, otherAccounts, owner, xAllocationVoting, xAllocationPool, b3tr, x2EarnRewardsPool } = - await getOrDeployContractInstances({ forceDeploy: true }) + const { + x2EarnApps, + otherAccounts, + owner, + xAllocationVoting, + xAllocationPool, + b3tr, + x2EarnRewardsPool, + veBetterPassport, + } = await getOrDeployContractInstances({ forceDeploy: true }) const voter = otherAccounts[1] await getVot3Tokens(voter, "1") + await veBetterPassport.whitelist(voter.address) + await veBetterPassport.toggleCheck(1) + const app1Id = await x2EarnApps.hashAppName("My app") await x2EarnApps .connect(owner) diff --git a/test/helpers/common.ts b/test/helpers/common.ts index 8b76489..230e4e9 100644 --- a/test/helpers/common.ts +++ b/test/helpers/common.ts @@ -1,5 +1,5 @@ import { ethers, network } from "hardhat" -import { B3TR, GalaxyMember } from "../../typechain-types" +import { B3TR, GalaxyMember, VeBetterPassport } from "../../typechain-types" import { BaseContract, ContractFactory, ContractTransactionResponse } from "ethers" import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers" import { getOrDeployContractInstances } from "./deploy" @@ -235,13 +235,16 @@ export const createProposalAndExecuteIt = async ( args: any[] = [], roundId?: string | bigint | number, ) => { - const { governor } = await getOrDeployContractInstances({}) + const { governor, veBetterPassport } = await getOrDeployContractInstances({}) // load votes // console.log("Loading votes"); await getVot3Tokens(voter, "30000") await waitForNextBlock() + await veBetterPassport.whitelist(voter.address) + if ((await veBetterPassport.isCheckEnabled(1)) === false) await veBetterPassport.toggleCheck(1) + // create a new proposal // console.log("Creating proposal"); const tx = await createProposal(contractToCall, Contract, proposer, description, functionToCall, args, roundId) @@ -294,7 +297,10 @@ export const createProposalWithMultipleFunctionsAndExecuteIt = async ( args: any[][], roundId?: string, ) => { - const { governor, emissions, xAllocationVoting } = await getOrDeployContractInstances({}) + const { governor, emissions, xAllocationVoting, veBetterPassport } = await getOrDeployContractInstances({}) + + await veBetterPassport.whitelist(voter.address) + if ((await veBetterPassport.isCheckEnabled(1)) === false) await veBetterPassport.toggleCheck(1) // load votes // console.log("Loading votes"); @@ -431,10 +437,32 @@ export const voteOnApps = async ( votes: Array>, roundId: bigint, ) => { - const { xAllocationVoting } = await getOrDeployContractInstances({}) + const { xAllocationVoting, veBetterPassport } = await getOrDeployContractInstances({}) + + if ((await veBetterPassport.isCheckEnabled(1)) === false) await veBetterPassport.toggleCheck(1) + + for (let i = 0; i < voters.length; i++) { + const voter = voters[i] + const voterVotes = votes[i] + + await veBetterPassport.whitelist(voter.address) + + // Filter out both zero votes and their corresponding apps + const filteredData = apps + .map((app, index) => ({ + app, + vote: voterVotes[index], + })) + .filter(data => data.vote !== BigInt(0)) + + // If there are any valid votes left, proceed with voting + if (filteredData.length > 0) { + const validApps = filteredData.map(data => data.app) + const validVotes = filteredData.map(data => data.vote) - for (const voter of voters) { - await xAllocationVoting.connect(voter).castVote(roundId, apps, votes[voters.indexOf(voter)]) + // Execute the vote with the filtered non-zero votes and corresponding apps + await xAllocationVoting.connect(voter).castVote(roundId, validApps, validVotes) + } } } @@ -515,11 +543,14 @@ export const calculateUnallocatedAppAllocationOffChain = async (roundId: number, } export const participateInAllocationVoting = async (user: HardhatEthersSigner, waitRoundToEnd: boolean = false) => { - const { xAllocationVoting, x2EarnApps, owner } = await getOrDeployContractInstances({}) + const { xAllocationVoting, x2EarnApps, owner, veBetterPassport } = await getOrDeployContractInstances({}) await getVot3Tokens(user, "1") await getVot3Tokens(owner, "1000") + await veBetterPassport.whitelist(user.address) + if ((await veBetterPassport.isCheckEnabled(1)) === false) await veBetterPassport.toggleCheck(1) + const appName = "App" + Math.random() await x2EarnApps.connect(owner).addApp(user.address, user.address, appName, "metadataURI") @@ -545,11 +576,14 @@ export const participateInGovernanceVoting = async ( args: any[] = [], waitProposalToEnd: boolean = false, ) => { - const { governor } = await getOrDeployContractInstances({}) + const { governor, veBetterPassport } = await getOrDeployContractInstances({}) await getVot3Tokens(user, "1") await getVot3Tokens(admin, "1000") + await veBetterPassport.connect(admin).whitelist(user.address) + if ((await veBetterPassport.isCheckEnabled(1)) === false) await veBetterPassport.toggleCheck(1) + const tx = await createProposal(contractToCall, Contract, admin, description, functionToCall, args) const proposalId = await getProposalIdFromTx(tx) @@ -613,3 +647,89 @@ export const upgradeNFTtoNextLevel = async ( await nft.connect(owner).upgrade(tokenId) } + +export const delegateWithSignature = async ( + veBetterPassport: VeBetterPassport, + delegator: HardhatEthersSigner, + delegatee: HardhatEthersSigner, + deadlineFromNow: number, // seconds from now +) => { + const blockNumber = await ethers.provider.getBlockNumber() + const currentBlockTimestamp = (await ethers.provider.getBlock(blockNumber))?.timestamp + + if (!currentBlockTimestamp) throw new Error("Could not get current block timestamp") + + // Calculate the deadline + const deadline = currentBlockTimestamp + deadlineFromNow + + // Set up EIP-712 domain + const domain = { + name: "VeBetterPassport", + version: "1", + chainId: 1337, + verifyingContract: await veBetterPassport.getAddress(), + } + let types = { + Delegation: [ + { name: "delegator", type: "address" }, + { name: "delegatee", type: "address" }, + { name: "deadline", type: "uint256" }, + ], + } + + // Prepare the struct to sign + const delegationData = { + delegator: delegator.address, + delegatee: delegatee.address, + deadline, + } + + // Create the EIP-712 signature for the delegator + const signature = await delegator.signTypedData(domain, types, delegationData) + + // Perform the delegation using the signature + await veBetterPassport.connect(delegatee).delegateWithSignature(delegator.address, deadline, signature) +} + +export const linkEntityToPassportWithSignature = async ( + veBetterPassport: VeBetterPassport, + passport: HardhatEthersSigner, + entity: HardhatEthersSigner, + deadlineFromNow: number, // seconds from now +) => { + const blockNumber = await ethers.provider.getBlockNumber() + const currentBlockTimestamp = (await ethers.provider.getBlock(blockNumber))?.timestamp + + if (!currentBlockTimestamp) throw new Error("Could not get current block timestamp") + + // Calculate the deadline + const deadline = currentBlockTimestamp + deadlineFromNow + + // Set up EIP-712 domain + const domain = { + name: "VeBetterPassport", + version: "1", + chainId: 1337, + verifyingContract: await veBetterPassport.getAddress(), + } + let types = { + LinkEntity: [ + { name: "entity", type: "address" }, + { name: "passport", type: "address" }, + { name: "deadline", type: "uint256" }, + ], + } + + // Prepare the struct to sign + const delegationData = { + entity: entity.address, + passport: passport.address, + deadline, + } + + // Create the EIP-712 signature for the delegator + const signature = await entity.signTypedData(domain, types, delegationData) + + // Perform the delegation using the signature + await veBetterPassport.connect(passport).linkEntityToPassportWithSignature(entity.address, deadline, signature) +} diff --git a/test/helpers/config.ts b/test/helpers/config.ts index 53efe7e..5436a07 100644 --- a/test/helpers/config.ts +++ b/test/helpers/config.ts @@ -102,5 +102,14 @@ export function createTestConfig() { "plastic", "trees_planted", ], + + // VeBetterPassport + VEPASSPORT_BOT_SIGNALING_THRESHOLD: 2, + VEPASSPORT_ROUNDS_FOR_CUMULATIVE_PARTICIPATION_SCORE: 5, + VEPASSPORT_GALAXY_MEMBER_MINIMUM_LEVEL: 2, + VEPASSPORT_BLACKLIST_THRESHOLD_PERCENTAGE: 2, + VEPASSPORT_WHITELIST_THRESHOLD_PERCENTAGE: 2, + VEPASSPORT_PASSPORT_MAX_ENTITIES: 5, + VEPASSPORT_DECAY_RATE: 0, }) } diff --git a/test/helpers/deploy.ts b/test/helpers/deploy.ts index a16e954..3b2df61 100644 --- a/test/helpers/deploy.ts +++ b/test/helpers/deploy.ts @@ -22,28 +22,48 @@ import { GovernorStateLogic, GovernorVotesLogic, X2EarnRewardsPool, + TokenAuction, MyERC721, MyERC1155, VoterRewardsV1, B3TRGovernorV1, XAllocationPoolV1, - B3TRGovernorV2, GovernorConfiguratorV1, GovernorFunctionRestrictionsLogicV1, GovernorStateLogicV1, GovernorProposalLogicV1, - GovernorVotesLogicV1, GovernorQuorumLogicV1, GovernorDepositLogicV1, GovernorClockLogicV1, X2EarnRewardsPoolV1, EmissionsV1, + VeBetterPassport, + XAllocationVotingV1, + B3TRGovernorV2, + GovernorVotesLogicV1, + B3TRGovernorV3, + GovernorClockLogicV3, + GovernorConfiguratorV3, + GovernorFunctionRestrictionsLogicV3, + GovernorProposalLogicV3, + GovernorDepositLogicV3, + GovernorQuorumLogicV3, + GovernorVotesLogicV3, + GovernorStateLogicV3, + PassportChecksLogic, + PassportEntityLogic, + PassportPoPScoreLogic, + PassportSignalingLogic, + PassportWhitelistAndBlacklistLogic, + PassportPersonhoodLogic, + PassportDelegationLogic, + X2EarnRewardsPoolV2, } from "../../typechain-types" import { createLocalConfig } from "../../config/contracts/envs/local" -import { deployProxy, upgradeProxy } from "../../scripts/helpers" -import { setWhitelistedFunctions } from "../../scripts/deploy/deploy" +import { deployProxy, deployProxyOnly, initializeProxy, upgradeProxy } from "../../scripts/helpers" import { bootstrapAndStartEmissions as callBootstrapAndStartEmissions } from "./common" -import { deployLibraries } from "../../scripts/helpers/deployLibraries" +import { governanceLibraries, passportLibraries } from "../../scripts/libraries" +import { setWhitelistedFunctions } from "../../scripts/deploy/deploy" interface DeployInstance { B3trContract: ContractFactory @@ -52,6 +72,8 @@ interface DeployInstance { timeLock: TimeLock governor: B3TRGovernor governorV1: B3TRGovernorV1 + governorV2: B3TRGovernorV2 + governorV3: B3TRGovernorV3 galaxyMember: GalaxyMember x2EarnApps: X2EarnApps xAllocationVoting: XAllocationVoting @@ -60,7 +82,10 @@ interface DeployInstance { voterRewards: VoterRewards voterRewardsV1: VoterRewardsV1 treasury: Treasury + x2EarnRewardsPoolV1: X2EarnRewardsPoolV1 + x2EarnRewardsPoolV2: X2EarnRewardsPoolV2 x2EarnRewardsPool: X2EarnRewardsPool + veBetterPassport: VeBetterPassport owner: HardhatEthersSigner otherAccount: HardhatEthersSigner minterAccount: HardhatEthersSigner @@ -82,8 +107,25 @@ interface DeployInstance { governorQuorumLogicLibV1: GovernorQuorumLogicV1 governorStateLogicLibV1: GovernorStateLogicV1 governorVotesLogicLibV1: GovernorVotesLogicV1 + governorClockLogicLibV3: GovernorClockLogicV3 + governorConfiguratorLibV3: GovernorConfiguratorV3 + governorDepositLogicLibV3: GovernorDepositLogicV3 + governorFunctionRestrictionsLogicLibV3: GovernorFunctionRestrictionsLogicV3 + governorProposalLogicLibV3: GovernorProposalLogicV3 + governorQuorumLogicLibV3: GovernorQuorumLogicV3 + governorStateLogicLibV3: GovernorStateLogicV3 + governorVotesLogicLibV3: GovernorVotesLogicV3 + passportChecksLogic: PassportChecksLogic + passportDelegationLogic: PassportDelegationLogic + passportEntityLogic: PassportEntityLogic + passportPersonhoodLogic: PassportPersonhoodLogic + passportPoPScoreLogic: PassportPoPScoreLogic + passportSignalingLogic: PassportSignalingLogic + passportWhitelistBlacklistLogic: PassportWhitelistAndBlacklistLogic + passportConfigurator: any // no abi for this library, which means a typechain is not generated myErc721: MyERC721 | undefined myErc1155: MyERC1155 | undefined + vechainNodesMock: TokenAuction } export const NFT_NAME = "GalaxyMember" @@ -127,7 +169,43 @@ export const getOrDeployContractInstances = async ({ GovernorQuorumLogicLib, GovernorVotesLogicLib, GovernorStateLogicLib, - } = await deployLibraries() + GovernorClockLogicLibV3, + GovernorConfiguratorLibV3, + GovernorFunctionRestrictionsLogicLibV3, + GovernorQuorumLogicLibV3, + GovernorProposalLogicLibV3, + GovernorVotesLogicLibV3, + GovernorDepositLogicLibV3, + GovernorStateLogicLibV3, + } = await governanceLibraries() + + // Deploy Passport Libraries + const { + PassportChecksLogic, + PassportConfigurator, + PassportEntityLogic, + PassportDelegationLogic, + PassportPersonhoodLogic, + PassportPoPScoreLogic, + PassportSignalingLogic, + PassportWhitelistAndBlacklistLogic, + } = await passportLibraries() + + // ---------------------- Deploy Mocks ---------------------- + // deploy Mocks + const TokenAuctionLock = await ethers.getContractFactory("TokenAuction") + const vechainNodesMock = await TokenAuctionLock.deploy() + await vechainNodesMock.waitForDeployment() + + const ClockAuctionLock = await ethers.getContractFactory("ClockAuction") + const clockAuctionContract = await ClockAuctionLock.deploy( + await vechainNodesMock.getAddress(), + await owner.getAddress(), + ) + + await vechainNodesMock.setSaleAuctionAddress(await clockAuctionContract.getAddress()) + + await vechainNodesMock.addOperator(await owner.getAddress()) // ---------------------- Deploy Contracts ---------------------- // Deploy B3TR @@ -191,6 +269,18 @@ export const getOrDeployContractInstances = async ({ owner.address, ])) as X2EarnApps + // Initialization requires the address of the x2EarnRewardsPool, for this reason we will initialize it after + const veBetterPassportContractAddress = await deployProxyOnly("VeBetterPassport", { + PassportChecksLogic: await PassportChecksLogic.getAddress(), + PassportConfigurator: await PassportConfigurator.getAddress(), + PassportEntityLogic: await PassportEntityLogic.getAddress(), + PassportDelegationLogic: await PassportDelegationLogic.getAddress(), + PassportPersonhoodLogic: await PassportPersonhoodLogic.getAddress(), + PassportPoPScoreLogic: await PassportPoPScoreLogic.getAddress(), + PassportSignalingLogic: await PassportSignalingLogic.getAddress(), + PassportWhitelistAndBlacklistLogic: await PassportWhitelistAndBlacklistLogic.getAddress(), + }) + const x2EarnRewardsPoolV1 = (await deployProxy("X2EarnRewardsPoolV1", [ owner.address, owner.address, @@ -199,14 +289,24 @@ export const getOrDeployContractInstances = async ({ await x2EarnApps.getAddress(), ])) as X2EarnRewardsPoolV1 - const x2EarnRewardsPool = (await upgradeProxy( + const x2EarnRewardsPoolV2 = (await upgradeProxy( "X2EarnRewardsPoolV1", - "X2EarnRewardsPool", + "X2EarnRewardsPoolV2", await x2EarnRewardsPoolV1.getAddress(), [owner.address, config.X_2_EARN_INITIAL_IMPACT_KEYS], { version: 2, }, + )) as X2EarnRewardsPoolV2 + + const x2EarnRewardsPool = (await upgradeProxy( + "X2EarnRewardsPoolV2", + "X2EarnRewardsPool", + await x2EarnRewardsPoolV2.getAddress(), + [veBetterPassportContractAddress], + { + version: 3, + }, )) as X2EarnRewardsPool // Deploy XAllocationPool @@ -233,7 +333,7 @@ export const getOrDeployContractInstances = async ({ const X_ALLOCATIONS_ADDRESS = await xAllocationPool.getAddress() const VOTE_2_EARN_ADDRESS = otherAccounts[1].address - const emissionsV1 = (await deployProxy("EmissionsV1", [ + const emissionsV1 = (await deployProxy("Emissions", [ { minter: minterAccount.address, admin: owner.address, @@ -285,7 +385,7 @@ export const getOrDeployContractInstances = async ({ await emissions.connect(owner).setVote2EarnAddress(await voterRewards.getAddress()) // Deploy XAllocationVoting - const xAllocationVoting = (await deployProxy("XAllocationVoting", [ + let xAllocationVotingV1 = (await deployProxy("XAllocationVotingV1", [ { vot3Token: await vot3.getAddress(), quorumPercentage: config.X_ALLOCATION_VOTING_QUORUM_PERCENTAGE, // quorum percentage @@ -301,7 +401,57 @@ export const getOrDeployContractInstances = async ({ appSharesCap: config.X_ALLOCATION_POOL_APP_SHARES_MAX_CAP, votingThreshold: config.X_ALLOCATION_VOTING_VOTING_THRESHOLD, }, - ])) as XAllocationVoting + ])) as XAllocationVotingV1 + + const xAllocationVoting = (await upgradeProxy( + "XAllocationVotingV1", + "XAllocationVoting", + await xAllocationVotingV1.getAddress(), + [veBetterPassportContractAddress], + { + version: 2, + }, + )) as XAllocationVoting + + const veBetterPassport = (await initializeProxy( + veBetterPassportContractAddress, + "VeBetterPassport", + [ + { + x2EarnApps: await x2EarnApps.getAddress(), + xAllocationVoting: await xAllocationVoting.getAddress(), + galaxyMember: await galaxyMember.getAddress(), + signalingThreshold: config.VEPASSPORT_BOT_SIGNALING_THRESHOLD, //signalingThreshold + roundsForCumulativeScore: config.VEPASSPORT_ROUNDS_FOR_CUMULATIVE_PARTICIPATION_SCORE, //roundsForCumulativeScore + minimumGalaxyMemberLevel: config.VEPASSPORT_GALAXY_MEMBER_MINIMUM_LEVEL, //galaxyMemberMinimumLevel + blacklistThreshold: config.VEPASSPORT_BLACKLIST_THRESHOLD_PERCENTAGE, //blacklistThreshold + whitelistThreshold: config.VEPASSPORT_WHITELIST_THRESHOLD_PERCENTAGE, //whitelistThreshold + maxEntitiesPerPassport: config.VEPASSPORT_PASSPORT_MAX_ENTITIES, //maxEntitiesPerPassport + decayRate: config.VEPASSPORT_DECAY_RATE, //decayRate + }, + { + admin: owner.address, // admin + botSignaler: owner.address, // botSignaler + upgrader: owner.address, // upgrader + settingsManager: owner.address, // settingsManager + roleGranter: owner.address, // roleGranter + blacklister: owner.address, // blacklister + whitelister: owner.address, // whitelistManager + actionRegistrar: owner.address, // actionRegistrar + actionScoreManager: owner.address, // actionScoreManager + }, + ], + { + PassportChecksLogic: await PassportChecksLogic.getAddress(), + PassportConfigurator: await PassportConfigurator.getAddress(), + PassportEntityLogic: await PassportEntityLogic.getAddress(), + PassportDelegationLogic: await PassportDelegationLogic.getAddress(), + PassportPersonhoodLogic: await PassportPersonhoodLogic.getAddress(), + PassportPoPScoreLogic: await PassportPoPScoreLogic.getAddress(), + PassportSignalingLogic: await PassportSignalingLogic.getAddress(), + PassportWhitelistAndBlacklistLogic: await PassportWhitelistAndBlacklistLogic.getAddress(), + }, + )) as VeBetterPassport // Deploy Governor const governorV1 = (await deployProxy( @@ -332,7 +482,7 @@ export const getOrDeployContractInstances = async ({ GovernorConfiguratorV1: await GovernorConfiguratorLibV1.getAddress(), GovernorDepositLogicV1: await GovernorDepositLogicLibV1.getAddress(), GovernorFunctionRestrictionsLogicV1: await GovernorFunctionRestrictionsLogicLibV1.getAddress(), - GovernorProposalLogicV1: await GovernorProposalLogicLib.getAddress(), + GovernorProposalLogicV1: await GovernorQuorumLogicLibV1.getAddress(), GovernorQuorumLogicV1: await GovernorQuorumLogicLibV1.getAddress(), GovernorStateLogicV1: await GovernorStateLogicLibV1.getAddress(), GovernorVotesLogicV1: await GovernorVotesLogicLibV1.getAddress(), @@ -346,26 +496,46 @@ export const getOrDeployContractInstances = async ({ GovernorConfiguratorV1: await GovernorConfiguratorLibV1.getAddress(), GovernorDepositLogicV1: await GovernorDepositLogicLibV1.getAddress(), GovernorFunctionRestrictionsLogicV1: await GovernorFunctionRestrictionsLogicLibV1.getAddress(), - GovernorProposalLogicV1: await GovernorProposalLogicLibV1.getAddress(), + GovernorProposalLogicV1: await GovernorQuorumLogicLibV1.getAddress(), GovernorQuorumLogicV1: await GovernorQuorumLogicLibV1.getAddress(), GovernorStateLogicV1: await GovernorStateLogicLibV1.getAddress(), GovernorVotesLogicV1: await GovernorVotesLogicLibV1.getAddress(), }, })) as B3TRGovernorV2 - const governor = (await upgradeProxy("B3TRGovernorV2", "B3TRGovernor", await governorV1.getAddress(), [], { + const governorV3 = (await upgradeProxy("B3TRGovernorV2", "B3TRGovernorV3", await governorV1.getAddress(), [], { version: 3, libraries: { - GovernorClockLogic: await GovernorClockLogicLib.getAddress(), - GovernorConfigurator: await GovernorConfiguratorLib.getAddress(), - GovernorDepositLogic: await GovernorDepositLogicLib.getAddress(), - GovernorFunctionRestrictionsLogic: await GovernorFunctionRestrictionsLogicLib.getAddress(), - GovernorProposalLogic: await GovernorProposalLogicLib.getAddress(), - GovernorQuorumLogic: await GovernorQuorumLogicLib.getAddress(), - GovernorStateLogic: await GovernorStateLogicLib.getAddress(), - GovernorVotesLogic: await GovernorVotesLogicLib.getAddress(), + GovernorClockLogicV3: await GovernorClockLogicLibV3.getAddress(), + GovernorConfiguratorV3: await GovernorConfiguratorLibV3.getAddress(), + GovernorDepositLogicV3: await GovernorDepositLogicLibV3.getAddress(), + GovernorFunctionRestrictionsLogicV3: await GovernorFunctionRestrictionsLogicLibV3.getAddress(), + GovernorProposalLogicV3: await GovernorProposalLogicLibV3.getAddress(), + GovernorQuorumLogicV3: await GovernorQuorumLogicLibV3.getAddress(), + GovernorStateLogicV3: await GovernorStateLogicLibV3.getAddress(), + GovernorVotesLogicV3: await GovernorVotesLogicLibV3.getAddress(), }, - })) as B3TRGovernor + })) as B3TRGovernorV3 + + const governor = (await upgradeProxy( + "B3TRGovernorV3", + "B3TRGovernor", + await governorV1.getAddress(), + [await veBetterPassport.getAddress()], + { + version: 4, + libraries: { + GovernorClockLogic: await GovernorClockLogicLib.getAddress(), + GovernorConfigurator: await GovernorConfiguratorLib.getAddress(), + GovernorDepositLogic: await GovernorDepositLogicLib.getAddress(), + GovernorFunctionRestrictionsLogic: await GovernorFunctionRestrictionsLogicLib.getAddress(), + GovernorProposalLogic: await GovernorProposalLogicLib.getAddress(), + GovernorQuorumLogic: await GovernorQuorumLogicLib.getAddress(), + GovernorStateLogic: await GovernorStateLogicLib.getAddress(), + GovernorVotesLogic: await GovernorVotesLogicLib.getAddress(), + }, + }, + )) as B3TRGovernor const contractAddresses: Record = { B3TR: await b3tr.getAddress(), @@ -379,6 +549,7 @@ export const getOrDeployContractInstances = async ({ XAllocationPool: await xAllocationPool.getAddress(), B3TRGovernor: await governor.getAddress(), X2EarnApps: await x2EarnApps.getAddress(), + VeBetterPassport: veBetterPassportContractAddress, } const libraries = { @@ -425,6 +596,11 @@ export const getOrDeployContractInstances = async ({ await xAllocationPool.connect(owner).setXAllocationVotingAddress(await xAllocationVoting.getAddress()) await xAllocationPool.connect(owner).setEmissionsAddress(await emissions.getAddress()) + // Set up veBetterPassport + await veBetterPassport + .connect(owner) + .grantRole(await veBetterPassport.ACTION_REGISTRAR_ROLE(), await x2EarnRewardsPool.getAddress()) + //Set the emissions address and the admin as the ROUND_STARTER_ROLE in XAllocationVoting const roundStarterRole = await xAllocationVoting.ROUND_STARTER_ROLE() await xAllocationVoting @@ -460,6 +636,8 @@ export const getOrDeployContractInstances = async ({ timeLock, governor, governorV1, + governorV2, + governorV3, galaxyMember, x2EarnApps, xAllocationVoting, @@ -473,7 +651,10 @@ export const getOrDeployContractInstances = async ({ timelockAdmin, otherAccounts, treasury, + x2EarnRewardsPoolV1, + x2EarnRewardsPoolV2, x2EarnRewardsPool, + veBetterPassport, governorClockLogicLib: GovernorClockLogicLib, governorConfiguratorLib: GovernorConfiguratorLib, governorDepositLogicLib: GovernorDepositLogicLib, @@ -490,8 +671,25 @@ export const getOrDeployContractInstances = async ({ governorQuorumLogicLibV1: GovernorQuorumLogicLibV1, governorStateLogicLibV1: GovernorStateLogicLibV1, governorVotesLogicLibV1: GovernorVotesLogicLibV1, + governorClockLogicLibV3: GovernorClockLogicLibV3, + governorConfiguratorLibV3: GovernorConfiguratorLibV3, + governorDepositLogicLibV3: GovernorDepositLogicLibV3, + governorFunctionRestrictionsLogicLibV3: GovernorFunctionRestrictionsLogicLibV3, + governorProposalLogicLibV3: GovernorProposalLogicLibV3, + governorQuorumLogicLibV3: GovernorQuorumLogicLibV3, + governorStateLogicLibV3: GovernorStateLogicLibV3, + governorVotesLogicLibV3: GovernorVotesLogicLibV3, + passportChecksLogic: PassportChecksLogic, + passportDelegationLogic: PassportDelegationLogic, + passportEntityLogic: PassportEntityLogic, + passportPersonhoodLogic: PassportPersonhoodLogic, + passportPoPScoreLogic: PassportPoPScoreLogic, + passportSignalingLogic: PassportSignalingLogic, + passportWhitelistBlacklistLogic: PassportWhitelistAndBlacklistLogic, + passportConfigurator: PassportConfigurator, myErc721: myErc721, myErc1155: myErc1155, + vechainNodesMock, } return cachedDeployInstance } diff --git a/yarn.lock b/yarn.lock index 70abfe2..a26aecb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1910,6 +1910,32 @@ anymatch@~3.1.2: normalize-path "^3.0.0" picomatch "^2.0.4" +archiver-utils@^5.0.0, archiver-utils@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/archiver-utils/-/archiver-utils-5.0.2.tgz#63bc719d951803efc72cf961a56ef810760dd14d" + integrity sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA== + dependencies: + glob "^10.0.0" + graceful-fs "^4.2.0" + is-stream "^2.0.1" + lazystream "^1.0.0" + lodash "^4.17.15" + normalize-path "^3.0.0" + readable-stream "^4.0.0" + +archiver@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/archiver/-/archiver-7.0.1.tgz#c9d91c350362040b8927379c7aa69c0655122f61" + integrity sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ== + dependencies: + archiver-utils "^5.0.2" + async "^3.2.4" + buffer-crc32 "^1.0.0" + readable-stream "^4.0.0" + readdir-glob "^1.1.2" + tar-stream "^3.0.0" + zip-stream "^6.0.1" + arg@^4.1.0: version "4.1.3" resolved "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz" @@ -2023,6 +2049,11 @@ async@1.x: resolved "https://registry.npmjs.org/async/-/async-1.5.2.tgz" integrity sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w== +async@^3.2.4: + version "3.2.6" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce" + integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA== + asynckit@^0.4.0: version "0.4.0" resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" @@ -2071,11 +2102,21 @@ axios@^1.5.1, axios@^1.6.7: form-data "^4.0.0" proxy-from-env "^1.1.0" +b4a@^1.6.4: + version "1.6.7" + resolved "https://registry.yarnpkg.com/b4a/-/b4a-1.6.7.tgz#a99587d4ebbfbd5a6e3b21bdb5d5fa385767abe4" + integrity sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg== + balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== +bare-events@^2.2.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/bare-events/-/bare-events-2.5.0.tgz#305b511e262ffd8b9d5616b056464f8e1b3329cc" + integrity sha512-/E8dDe9dsbLyh2qrZ64PEPadOQ0F4gbl1sUJOrmph7xOiIxfY8vwab/4bFLh4Y88/Hk/ujKcrQKc+ps0mv873A== + base-x@^3.0.2, base-x@^3.0.8: version "3.0.10" resolved "https://registry.npmjs.org/base-x/-/base-x-3.0.10.tgz" @@ -2247,6 +2288,11 @@ bs58check@^2.1.2: create-hash "^1.1.0" safe-buffer "^5.1.2" +buffer-crc32@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-1.0.0.tgz#a10993b9055081d55304bd9feb4a072de179f405" + integrity sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w== + buffer-from@^1.0.0: version "1.1.2" resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz" @@ -2678,6 +2724,17 @@ compare-versions@^6.0.0: resolved "https://registry.npmjs.org/compare-versions/-/compare-versions-6.1.0.tgz" integrity sha512-LNZQXhqUvqUTotpZ00qLSaify3b4VFD588aRr8MKFw4CMUr98ytzCW5wDH5qx/DEY5kCDXcbcRuCqL0szEf2tg== +compress-commons@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/compress-commons/-/compress-commons-6.0.2.tgz#26d31251a66b9d6ba23a84064ecd3a6a71d2609e" + integrity sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg== + dependencies: + crc-32 "^1.2.0" + crc32-stream "^6.0.0" + is-stream "^2.0.1" + normalize-path "^3.0.0" + readable-stream "^4.0.0" + concat-map@0.0.1: version "0.0.1" resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" @@ -2755,6 +2812,14 @@ crc-32@^1.2.0: resolved "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz" integrity sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ== +crc32-stream@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/crc32-stream/-/crc32-stream-6.0.0.tgz#8529a3868f8b27abb915f6c3617c0fadedbf9430" + integrity sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g== + dependencies: + crc-32 "^1.2.0" + readable-stream "^4.0.0" + create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz" @@ -3867,6 +3932,11 @@ fast-diff@^1.1.2: resolved "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz" integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw== +fast-fifo@^1.2.0, fast-fifo@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/fast-fifo/-/fast-fifo-1.3.2.tgz#286e31de96eb96d38a97899815740ba2a4f3640c" + integrity sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ== + fast-glob@^3.0.3, fast-glob@^3.2.9: version "3.3.2" resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz" @@ -4253,7 +4323,7 @@ glob@7.2.0, glob@^7.0.0, glob@^7.1.3: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^10.3.10: +glob@^10.0.0, glob@^10.3.10: version "10.4.5" resolved "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz" integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg== @@ -4971,7 +5041,7 @@ is-shared-array-buffer@^1.0.2, is-shared-array-buffer@^1.0.3: dependencies: call-bind "^1.0.7" -is-stream@^2.0.0: +is-stream@^2.0.0, is-stream@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz" integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== @@ -5191,6 +5261,13 @@ klaw@^1.0.0: optionalDependencies: graceful-fs "^4.1.9" +lazystream@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/lazystream/-/lazystream-1.0.1.tgz#494c831062f1f9408251ec44db1cba29242a2638" + integrity sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw== + dependencies: + readable-stream "^2.0.5" + lcid@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz" @@ -6301,6 +6378,11 @@ queue-microtask@^1.2.2: resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== +queue-tick@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/queue-tick/-/queue-tick-1.0.1.tgz#f6f07ac82c1fd60f82e098b417a80e52f1f4c142" + integrity sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag== + quick-format-unescaped@^4.0.3: version "4.0.4" resolved "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz" @@ -6350,7 +6432,7 @@ read-pkg@^1.0.0: normalize-package-data "^2.3.2" path-type "^1.0.0" -readable-stream@^2.2.2: +readable-stream@^2.0.5, readable-stream@^2.2.2: version "2.3.8" resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz" integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== @@ -6383,6 +6465,13 @@ readable-stream@^4.0.0: process "^0.11.10" string_decoder "^1.3.0" +readdir-glob@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/readdir-glob/-/readdir-glob-1.1.3.tgz#c3d831f51f5e7bfa62fa2ffbe4b508c640f09584" + integrity sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA== + dependencies: + minimatch "^5.1.0" + readdirp@~3.6.0: version "3.6.0" resolved "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz" @@ -7130,6 +7219,17 @@ statuses@2.0.1: resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz" integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== +streamx@^2.15.0: + version "2.20.1" + resolved "https://registry.yarnpkg.com/streamx/-/streamx-2.20.1.tgz#471c4f8b860f7b696feb83d5b125caab2fdbb93c" + integrity sha512-uTa0mU6WUC65iUvzKH4X9hEdvSW7rbPxPtwfWiLMSj3qTdQbAiUboZTxauKfpFuGIGa1C2BYijZ7wgdUXICJhA== + dependencies: + fast-fifo "^1.3.2" + queue-tick "^1.0.1" + text-decoder "^1.1.0" + optionalDependencies: + bare-events "^2.2.0" + strict-uri-encode@^1.0.0: version "1.1.0" resolved "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz" @@ -7140,7 +7240,7 @@ string-format@^2.0.0: resolved "https://registry.npmjs.org/string-format/-/string-format-2.0.0.tgz" integrity sha512-bbEs3scLeYNXLecRRuk6uJxdXUSj6le/8rNPHChIJTn2V79aXVTR1EH2OH5zLKKoz0V02fOUKZZcw01pLUShZA== -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -7166,6 +7266,15 @@ string-width@^2.1.1: is-fullwidth-code-point "^2.0.0" strip-ansi "^4.0.0" +string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" resolved "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz" @@ -7217,7 +7326,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -7238,6 +7347,13 @@ strip-ansi@^4.0.0: dependencies: ansi-regex "^3.0.0" +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz" @@ -7385,6 +7501,15 @@ table@^6.8.0: string-width "^4.2.3" strip-ansi "^6.0.1" +tar-stream@^3.0.0: + version "3.1.7" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-3.1.7.tgz#24b3fb5eabada19fe7338ed6d26e5f7c482e792b" + integrity sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ== + dependencies: + b4a "^1.6.4" + fast-fifo "^1.2.0" + streamx "^2.15.0" + tar@^4.0.2: version "4.4.19" resolved "https://registry.npmjs.org/tar/-/tar-4.4.19.tgz" @@ -7403,6 +7528,13 @@ testrpc@0.0.1: resolved "https://registry.npmjs.org/testrpc/-/testrpc-0.0.1.tgz" integrity sha512-afH1hO+SQ/VPlmaLUFj2636QMeDvPCeQMc/9RBMW0IfjNe9gFD9Ra3ShqYkB7py0do1ZcCna/9acHyzTJ+GcNA== +text-decoder@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/text-decoder/-/text-decoder-1.2.0.tgz#85f19d4d5088e0b45cd841bdfaeac458dbffeefc" + integrity sha512-n1yg1mOj9DNpk3NeZOx7T6jchTbyJS3i3cucbNN6FcdPriMZx7NsgrGpWWdWZZGxD7ES1XB+3uoqHMgOKaN+fg== + dependencies: + b4a "^1.6.4" + text-table@^0.2.0: version "0.2.0" resolved "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz" @@ -8280,7 +8412,7 @@ workerpool@^6.5.1: resolved "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz" integrity sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -8297,6 +8429,15 @@ wrap-ansi@^2.0.0: string-width "^1.0.1" strip-ansi "^3.0.1" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz" @@ -8465,3 +8606,12 @@ yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +zip-stream@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/zip-stream/-/zip-stream-6.0.1.tgz#e141b930ed60ccaf5d7fa9c8260e0d1748a2bbfb" + integrity sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA== + dependencies: + archiver-utils "^5.0.0" + compress-commons "^6.0.2" + readable-stream "^4.0.0" From b614ee7c20b60bbbdf8fe23c2fe069995e28b8c2 Mon Sep 17 00:00:00 2001 From: Dan Rusnac Date: Sat, 12 Oct 2024 13:30:38 +0200 Subject: [PATCH 2/5] feat: updated contracts to latest version --- Readme.md | 7 +- contracts/B3TRGovernor.sol | 20 + contracts/XAllocationVoting.sol | 9 +- .../libraries/GovernorConfigurator.sol | 30 ++ contracts/interfaces/IB3TRGovernor.sol | 18 + contracts/interfaces/IVeBetterPassport.sol | 3 + .../libraries/PassportDelegationLogic.sol | 83 ++-- .../libraries/PassportEntityLogic.sol | 66 +++- .../libraries/PassportPersonhoodLogic.sol | 2 +- .../libraries/PassportPoPScoreLogic.sol | 2 - .../XAllocationVotingGovernor.sol | 14 +- .../modules/ExternalContractsUpgradeable.sol | 28 ++ scripts/libraries/passportLibraries.ts | 6 +- test/Emissions.test.ts | 2 +- test/GalaxyMember.test.ts | 2 +- test/Governance.test.ts | 12 + test/Timelock.test.ts | 2 +- test/Treasury.test.ts | 2 +- test/VeBetterPassport.test.ts | 173 +++++++- test/VoterRewards.test.ts | 2 +- test/X2EarnRewardsPool.test.ts | 14 +- test/XAllocationVoting.test.ts | 371 +++++++++++++++++- 22 files changed, 775 insertions(+), 93 deletions(-) diff --git a/Readme.md b/Readme.md index 14c9bf8..9ed3fc8 100644 --- a/Readme.md +++ b/Readme.md @@ -27,6 +27,8 @@ Welcome to the VeBetterDAO Smart Contracts repository! This open-source reposito The complete documentation for the VeBetterDAO and the contracts can be found [here](https://docs.vebetterdao.org). +Our contracts are upgradeable and versioned. See the [contracts changelog](CONTRACTS_CHANGELOG.md) for more information on the changes introduced in each of new upgraded version. + ## Mainnet contract addresses ``` @@ -41,7 +43,8 @@ The complete documentation for the VeBetterDAO and the contracts can be found [h "X2EarnApps": "0x8392B7CCc763dB03b47afcD8E8f5e24F9cf0554D", "X2EarnRewardsPool": "0x6Bee7DDab6c99d5B2Af0554EaEA484CE18F52631", "XAllocationPool": "0x4191776F05f4bE4848d3f4d587345078B439C7d3", - "XAllocationVoting": "0x89A00Bb0947a30FF95BEeF77a66AEdE3842Fe5B7" + "XAllocationVoting": "0x89A00Bb0947a30FF95BEeF77a66AEdE3842Fe5B7", + "VeBetterPassport": "0x45d5CA3f295ad8BCa291cC4ecd33382DE40E4FAc" ``` ## Testnet contract addresses @@ -62,6 +65,8 @@ The complete documentation for the VeBetterDAO and the contracts can be found [h "B3TRFaucet": "0x5e9c1F0f52aC6b5004122059053b00017EAfB561" ``` +Notice: _VeBetter Passport contract deployed only on mainnet._ + ## Audit The VeBetterDAO smart contracts have undergone a comprehensive audit by [Hacken](https://hacken.io/). The audit report (`Hacken_Vechain Foundation_[SCA] VeChain _ VeBetter DAO _ May2024_P-2024-304_1_20240621 16_17`) can be found in the root of the repo. diff --git a/contracts/B3TRGovernor.sol b/contracts/B3TRGovernor.sol index f1f3f77..2a06f84 100644 --- a/contracts/B3TRGovernor.sol +++ b/contracts/B3TRGovernor.sol @@ -646,6 +646,15 @@ contract B3TRGovernor is return address($.timelock); } + /** + * @notice Returns the VeBetterPassport contract. + * @return The current VeBetterPassport contract. + */ + function veBetterPassport() external view returns (IVeBetterPassport) { + GovernorStorageTypes.GovernorStorage storage $ = getGovernorStorage(); + return $.veBetterPassport; + } + // ------------------ SETTERS ------------------ // /** @@ -921,6 +930,17 @@ contract B3TRGovernor is GovernorConfigurator.updateTimelock($, newTimelock); } + /** + * @notice Set the VeBetterPassport contract + * @param newVeBetterPassport The new VeBetterPassport contract + */ + function setVeBetterPassport( + IVeBetterPassport newVeBetterPassport + ) public onlyRoleOrGovernance(CONTRACTS_ADDRESS_MANAGER_ROLE) { + GovernorStorageTypes.GovernorStorage storage $ = getGovernorStorage(); + GovernorConfigurator.setVeBetterPassport($, newVeBetterPassport); + } + // ------------------ Overrides ------------------ // /** diff --git a/contracts/XAllocationVoting.sol b/contracts/XAllocationVoting.sol index 54e3010..68f479b 100644 --- a/contracts/XAllocationVoting.sol +++ b/contracts/XAllocationVoting.sol @@ -139,7 +139,7 @@ contract XAllocationVoting is } function initializeV2(IVeBetterPassport _veBetterPassport) public reinitializer(2) { - __XAllocationVotingGovernor_init_v2(_veBetterPassport); + __ExternalContracts_init_v2(_veBetterPassport); } // ---------- Setters ---------- // @@ -210,6 +210,13 @@ contract XAllocationVoting is super.updateQuorumNumerator(newQuorumNumerator); } + /** + * @dev Set the VeBetterPassport contract + */ + function setVeBetterPassport(IVeBetterPassport newVeBetterPassport) external onlyRole(GOVERNANCE_ROLE) { + _setVeBetterPassport(newVeBetterPassport); + } + // ---------- Getters ---------- // /** diff --git a/contracts/governance/libraries/GovernorConfigurator.sol b/contracts/governance/libraries/GovernorConfigurator.sol index 0288c7e..3a5444c 100644 --- a/contracts/governance/libraries/GovernorConfigurator.sol +++ b/contracts/governance/libraries/GovernorConfigurator.sol @@ -29,6 +29,7 @@ import { IVoterRewards } from "../../interfaces/IVoterRewards.sol"; import { IXAllocationVotingGovernor } from "../../interfaces/IXAllocationVotingGovernor.sol"; import { TimelockControllerUpgradeable } from "@openzeppelin/contracts-upgradeable/governance/TimelockControllerUpgradeable.sol"; import { IB3TR } from "../../interfaces/IB3TR.sol"; +import { IVeBetterPassport } from "../../interfaces/IVeBetterPassport.sol"; /// @title GovernorConfigurator Library /// @notice Library for managing the configuration of a Governor contract. @@ -52,10 +53,28 @@ library GovernorConfigurator { /// @dev Emitted when the timelock controller used for proposal execution is modified. event TimelockChange(address oldTimelock, address newTimelock); + /// @dev Emitted when the VeBetterPassport contract is set. + event VeBetterPassportSet(address oldVeBetterPassport, address newVeBetterPassport); + /// @dev The deposit threshold is not in the valid range for a percentage - 0 to 100. error GovernorDepositThresholdNotInRange(uint256 depositThreshold); /**------------------ SETTERS ------------------**/ + + /** + * @notice Sets the VeBetterPassport contract. + * @dev Sets a new VeBetterPassport contract and emits a {VeBetterPassportSet} event. + * @param self The storage reference for the GovernorStorage. + * @param newVeBetterPassport The new VeBetterPassport contract. + */ + function setVeBetterPassport( + GovernorStorageTypes.GovernorStorage storage self, + IVeBetterPassport newVeBetterPassport + ) external { + emit VeBetterPassportSet(address(self.veBetterPassport), address(newVeBetterPassport)); + self.veBetterPassport = newVeBetterPassport; + } + /** * @notice Sets the voting threshold. * @dev Sets a new voting threshold and emits a {VotingThresholdSet} event. @@ -170,4 +189,15 @@ library GovernorConfigurator { ) internal view returns (uint256) { return self.depositThresholdPercentage; } + + /** + * @notice Returns the VeBetterPassport contract. + * @param self The storage reference for the GovernorStorage. + * @return The current VeBetterPassport contract. + */ + function veBetterPassport( + GovernorStorageTypes.GovernorStorage storage self + ) internal view returns (IVeBetterPassport) { + return self.veBetterPassport; + } } diff --git a/contracts/interfaces/IB3TRGovernor.sol b/contracts/interfaces/IB3TRGovernor.sol index 4ae0148..fd12a38 100644 --- a/contracts/interfaces/IB3TRGovernor.sol +++ b/contracts/interfaces/IB3TRGovernor.sol @@ -9,6 +9,7 @@ import { IB3TR } from "./IB3TR.sol"; import { IVoterRewards } from "../interfaces/IVoterRewards.sol"; import { IXAllocationVotingGovernor } from "../interfaces/IXAllocationVotingGovernor.sol"; import { GovernorTypes } from "../governance/libraries/GovernorTypes.sol"; +import { IVeBetterPassport } from "./IVeBetterPassport.sol"; /** * @dev Interface of the {B3TRGovernor} core. @@ -216,6 +217,11 @@ interface IB3TRGovernor is IERC165, IERC6372 { */ event ProposalDeposit(address indexed depositor, uint256 indexed proposalId, uint256 amount); + /** + * @dev Emitted when the VeBetterPassport contract is set. + */ + event VeBetterPassportSet(address indexed oldVeBetterPassport, address indexed newVeBetterPassport); + /** * @notice module:core * @dev Name of the governor instance (used in building the ERC712 domain separator). @@ -507,4 +513,16 @@ interface IB3TRGovernor is IERC165, IERC6372 { * @dev Getter to retrieve the amount of tokens a specific user has deposited to a proposal */ function getUserDeposit(uint256 proposalId, address user) external view returns (uint256); + + /** + * @notice Returns the VeBetterPassport contract. + * @return The current VeBetterPassport contract. + */ + function veBetterPassport() external view returns (IVeBetterPassport); + + /** + * @notice Set the VeBetterPassport contract + * @param newVeBetterPassport The new VeBetterPassport contract + */ + function setVeBetterPassport(IVeBetterPassport newVeBetterPassport) external; } diff --git a/contracts/interfaces/IVeBetterPassport.sol b/contracts/interfaces/IVeBetterPassport.sol index cd604d6..82a2e9b 100644 --- a/contracts/interfaces/IVeBetterPassport.sol +++ b/contracts/interfaces/IVeBetterPassport.sol @@ -146,6 +146,9 @@ interface IVeBetterPassport { /// @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 diff --git a/contracts/ve-better-passport/libraries/PassportDelegationLogic.sol b/contracts/ve-better-passport/libraries/PassportDelegationLogic.sol index e28bfb6..fb79fed 100644 --- a/contracts/ve-better-passport/libraries/PassportDelegationLogic.sol +++ b/contracts/ve-better-passport/libraries/PassportDelegationLogic.sol @@ -243,31 +243,8 @@ library PassportDelegationLogic { revert InvalidSignature(); } - // Cannot delegate passport to owner - if (signer == msg.sender) { - revert CannotDelegateToSelf(signer); - } - - // Cannot delegate enitity attached to passport - if (PassportEntityLogic.isEntity(self, delegator)) { - revert PassportDelegationFromEntity(); - } - - // Cannot delegate passport to entity - if (PassportEntityLogic.isEntity(self, msg.sender)) { - revert PassportDelegationToEntity(); - } - - // Check if the passport has already delegated - if (isDelegator(self, delegator)) { - _removeDelegation(self, delegator, _addressFromUint160(self.delegatorToDelegatee[delegator].latest())); - } - - // Check if the passport is already pending delegation - address pendingDelegatee = self.pendingDelegationsDelegatorToDelegatee[delegator]; - if (pendingDelegatee != address(0)) { - _removePendingDelegation(self, delegator, pendingDelegatee); - } + // Check delegation rules + _checkDelegation(self, delegator, msg.sender); // Check if the delegatee has already been delegated if (isDelegatee(self, msg.sender)) { @@ -289,29 +266,8 @@ library PassportDelegationLogic { * @param delegatee The address of the delegatee. */ function delegatePassport(PassportStorageTypes.PassportStorage storage self, address delegatee) external { - // Check if the delegatee is trying to delegate to themselves - if (msg.sender == delegatee) { - revert CannotDelegateToSelf(msg.sender); - } - - // Check if the delegator is an entity linked to a passport - if (PassportEntityLogic.isEntity(self, msg.sender)) { - revert PassportDelegationFromEntity(); - } - - if (PassportEntityLogic.isEntity(self, delegatee)) { - revert PassportDelegationToEntity(); - } - - // Check if the passport has already delegated removing the previous delegation - if (isDelegator(self, msg.sender)) { - _removeDelegation(self, msg.sender, _addressFromUint160(self.delegatorToDelegatee[msg.sender].latest())); - } - - // Check if the passport is already pending delegation - if (self.pendingDelegationsDelegatorToDelegatee[msg.sender] != address(0)) { - _removePendingDelegation(self, msg.sender, self.pendingDelegationsDelegatorToDelegatee[msg.sender]); - } + // Check delegation rules + _checkDelegation(self, msg.sender, delegatee); // Get the length of the pending delegations uint256 length = self.pendingDelegationsDelegateeToDelegators[delegatee].length; @@ -511,4 +467,35 @@ library PassportDelegationLogic { ) internal view returns (bool) { return self.delegatorToDelegatee[user].upperLookupRecent(SafeCast.toUint48(timepoint)) != 0; } + + function _checkDelegation( + PassportStorageTypes.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 (PassportEntityLogic.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 (PassportEntityLogic.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/ve-better-passport/libraries/PassportEntityLogic.sol b/contracts/ve-better-passport/libraries/PassportEntityLogic.sol index f8b4c5d..d1db5d0 100644 --- a/contracts/ve-better-passport/libraries/PassportEntityLogic.sol +++ b/contracts/ve-better-passport/libraries/PassportEntityLogic.sol @@ -28,6 +28,7 @@ import { PassportClockLogic } from "./PassportClockLogic.sol"; import { PassportEIP712SigningLogic } from "./PassportEIP712SigningLogic.sol"; import { PassportSignalingLogic } from "./PassportSignalingLogic.sol"; import { PassportWhitelistAndBlacklistLogic } from "./PassportWhitelistAndBlacklistLogic.sol"; +import { PassportDelegationLogic } from "./PassportDelegationLogic.sol"; import { PassportTypes } from "./PassportTypes.sol"; import { Checkpoints } from "@openzeppelin/contracts/utils/structs/Checkpoints.sol"; import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; @@ -107,6 +108,11 @@ library PassportEntityLogic { */ 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. @@ -279,15 +285,8 @@ library PassportEntityLogic { revert InvalidSignature(); } - // Ensure the entity trying to link is not the passport itself - if (signer == msg.sender) { - revert CannotLinkToSelf(signer); - } - - // Check if the entity is already linked, if so revert - if (self.entityToPassport[entity].latest() != 0 || self.pendingLinksEntityToPassport[entity] != address(0)) { - revert AlreadyLinked(msg.sender); - } + // 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) { @@ -303,15 +302,7 @@ library PassportEntityLogic { * @param passport The address of the passport to which the entity is being linked. */ function linkEntityToPassport(PassportStorageTypes.PassportStorage storage self, address passport) external { - // Check if the entity (msg.sender) is already linked - if (self.entityToPassport[msg.sender].latest() != 0 || self.pendingLinksIndexes[msg.sender] != 0) { - revert AlreadyLinked(msg.sender); - } - - // Prevent self-linking (an entity cannot be its own passport) - if (msg.sender == passport) { - revert CannotLinkToSelf(msg.sender); - } + _checkLink(self, passport, msg.sender); // Add the entity to the list of pending links for the passport uint256 length = self.pendingLinksPassportToEntities[passport].length; @@ -567,4 +558,43 @@ library PassportEntityLogic { ) 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( + PassportStorageTypes.PassportStorage storage self, + address passport, + address entity + ) private view { + // Check if the entity is already an entity, if so revert + if (self.entityToPassport[entity].latest() != 0 || self.pendingLinksIndexes[entity] != 0) { + revert AlreadyLinked(entity); + } + + // Check if the passport is an 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, if so revert + if (self.passportToEntities[entity].length != 0) { + revert AlreadyLinked(passport); + } + + // Check if entity has delegated to another passport or has a pending delegation + if ( + PassportDelegationLogic.isDelegator(self, entity) || + self.pendingDelegationsDelegatorToDelegatee[entity] != address(0) + ) { + revert DelegatedEntity(entity); + } + + // Prevent self-linking (an entity cannot be its own passport) + if (entity == passport) { + revert CannotLinkToSelf(entity); + } + } } diff --git a/contracts/ve-better-passport/libraries/PassportPersonhoodLogic.sol b/contracts/ve-better-passport/libraries/PassportPersonhoodLogic.sol index c13aabc..c402717 100644 --- a/contracts/ve-better-passport/libraries/PassportPersonhoodLogic.sol +++ b/contracts/ve-better-passport/libraries/PassportPersonhoodLogic.sol @@ -171,7 +171,7 @@ library PassportPersonhoodLogic { } if (PassportChecksLogic._isCheckEnabled(self, PassportTypes.CheckType.PARTICIPATION_SCORE_CHECK)) { - uint256 participationScore = PassportPoPScoreLogic.getCumulativeScoreWithDecay( + uint256 participationScore = PassportPoPScoreLogic._cumulativeScoreWithDecay( self, user, self.xAllocationVoting.currentRoundId() diff --git a/contracts/ve-better-passport/libraries/PassportPoPScoreLogic.sol b/contracts/ve-better-passport/libraries/PassportPoPScoreLogic.sol index a814d75..7dd128b 100644 --- a/contracts/ve-better-passport/libraries/PassportPoPScoreLogic.sol +++ b/contracts/ve-better-passport/libraries/PassportPoPScoreLogic.sol @@ -220,8 +220,6 @@ library PassportPoPScoreLogic { /// @notice Sets the threshold for a user to be considered a person /// @param threshold - the round threshold function setThresholdPoPScore(PassportStorageTypes.PassportStorage storage self, uint208 threshold) external { - require(threshold > 0, "ProofOfParticipation: threshold is zero"); - self.popScoreThreshold.push(PassportClockLogic.clock(), threshold); } diff --git a/contracts/x-allocation-voting-governance/XAllocationVotingGovernor.sol b/contracts/x-allocation-voting-governance/XAllocationVotingGovernor.sol index 8f53b97..d068fc3 100644 --- a/contracts/x-allocation-voting-governance/XAllocationVotingGovernor.sol +++ b/contracts/x-allocation-voting-governance/XAllocationVotingGovernor.sol @@ -62,7 +62,6 @@ abstract contract XAllocationVotingGovernor is /// @custom:storage-location erc7201:b3tr.storage.XAllocationVotingGovernor struct XAllocationVotingGovernorStorage { string _name; - IVeBetterPassport _veBetterPassport; } // keccak256(abi.encode(uint256(keccak256("b3tr.storage.XAllocationVotingGovernor")) - 1)) & ~bytes32(uint256(0xff)) @@ -87,11 +86,6 @@ abstract contract XAllocationVotingGovernor is $._name = name_; } - function __XAllocationVotingGovernor_init_v2(IVeBetterPassport veBetterPassport_) internal onlyInitializing { - XAllocationVotingGovernorStorage storage $ = _getXAllocationVotingGovernorStorage(); - $._veBetterPassport = veBetterPassport_; - } - // ---------- Setters ---------- // /** @@ -121,9 +115,8 @@ abstract contract XAllocationVotingGovernor is require(appIds.length > 0, "XAllocationVotingGovernor: no apps to vote for"); uint256 _currentRoundSnapshot = currentRoundSnapshot(); - XAllocationVotingGovernorStorage storage $ = _getXAllocationVotingGovernorStorage(); - (bool isPerson, string memory explanation) = $._veBetterPassport.isPersonAtTimepoint( + (bool isPerson, string memory explanation) = veBetterPassport().isPersonAtTimepoint( _msgSender(), SafeCast.toUint48(_currentRoundSnapshot) ); @@ -329,6 +322,11 @@ abstract contract XAllocationVotingGovernor is */ function x2EarnApps() public view virtual returns (IX2EarnApps); + /** + * @dev Returns the VeBetterPassport contract. + */ + function veBetterPassport() public view virtual returns (IVeBetterPassport); + /** * @dev Returns the Emissions contract. */ diff --git a/contracts/x-allocation-voting-governance/modules/ExternalContractsUpgradeable.sol b/contracts/x-allocation-voting-governance/modules/ExternalContractsUpgradeable.sol index 674f1c8..b38b737 100644 --- a/contracts/x-allocation-voting-governance/modules/ExternalContractsUpgradeable.sol +++ b/contracts/x-allocation-voting-governance/modules/ExternalContractsUpgradeable.sol @@ -28,6 +28,7 @@ import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/I import { IEmissions } from "../../interfaces/IEmissions.sol"; import { IX2EarnApps } from "../../interfaces/IX2EarnApps.sol"; import { IVoterRewards } from "../../interfaces/IVoterRewards.sol"; +import { IVeBetterPassport } from "../../interfaces/IVeBetterPassport.sol"; /** * @title ExternalContractsUpgradeable @@ -39,6 +40,7 @@ abstract contract ExternalContractsUpgradeable is Initializable, XAllocationVoti IX2EarnApps _x2EarnApps; IEmissions _emissions; IVoterRewards _voterRewards; + IVeBetterPassport _veBetterPassport; } // keccak256(abi.encode(uint256(keccak256("b3tr.storage.XAllocationVotingGovernor.ExternalContracts")) - 1)) & ~bytes32(uint256(0xff)) @@ -57,6 +59,8 @@ abstract contract ExternalContractsUpgradeable is Initializable, XAllocationVoti event X2EarnAppsSet(address oldContractAddress, address newContractAddress); // @dev Emit when the voter rewards contract is set event VoterRewardsSet(address oldContractAddress, address newContractAddress); + // @dev Emit when the VeBetterPassport contract is set + event VeBetterPassportSet(address oldContractAddress, address newContractAddress); /** * @dev Initializes the contract @@ -83,6 +87,11 @@ abstract contract ExternalContractsUpgradeable is Initializable, XAllocationVoti $._voterRewards = initialVoterRewards; } + function __ExternalContracts_init_v2(IVeBetterPassport _veBetterPassport) internal onlyInitializing { + ExternalContractsStorage storage $ = _getExternalContractsStorage(); + $._veBetterPassport = _veBetterPassport; + } + // ------- Getters ------- // /** * @dev The X2EarnApps contract. @@ -108,6 +117,11 @@ abstract contract ExternalContractsUpgradeable is Initializable, XAllocationVoti return $._voterRewards; } + function veBetterPassport() public view override returns (IVeBetterPassport) { + ExternalContractsStorage storage $ = _getExternalContractsStorage(); + return $._veBetterPassport; + } + // ------- Internal Functions ------- // /** @@ -150,4 +164,18 @@ abstract contract ExternalContractsUpgradeable is Initializable, XAllocationVoti emit VoterRewardsSet(address($._voterRewards), address(newVoterRewards)); $._voterRewards = newVoterRewards; } + + /** + * @dev Sets the VeBetterPassport contract + * @param newVeBetterPassport The new VeBetterPassport contract address + */ + function _setVeBetterPassport(IVeBetterPassport newVeBetterPassport) internal virtual { + require( + address(newVeBetterPassport) != address(0), + "XAllocationVotingGovernor: new VeBetterPassport is the zero address" + ); + + ExternalContractsStorage storage $ = _getExternalContractsStorage(); + $._veBetterPassport = newVeBetterPassport; + } } diff --git a/scripts/libraries/passportLibraries.ts b/scripts/libraries/passportLibraries.ts index 23d4077..085d514 100644 --- a/scripts/libraries/passportLibraries.ts +++ b/scripts/libraries/passportLibraries.ts @@ -32,11 +32,7 @@ export async function passportLibraries() { await PassportSignalingLogicLib.waitForDeployment() // Deploy Passport Personhood Logic - const PassportPersonhoodLogic = await ethers.getContractFactory("PassportPersonhoodLogic", { - libraries: { - PassportPoPScoreLogic: await PassportPoPScoreLogicLib.getAddress(), - }, - }) + const PassportPersonhoodLogic = await ethers.getContractFactory("PassportPersonhoodLogic") const PassportPersonhoodLogicLib = await PassportPersonhoodLogic.deploy() await PassportPersonhoodLogicLib.waitForDeployment() diff --git a/test/Emissions.test.ts b/test/Emissions.test.ts index 450a946..6cdd3ed 100644 --- a/test/Emissions.test.ts +++ b/test/Emissions.test.ts @@ -1655,7 +1655,7 @@ describe("Emissions - @shard2", () => { expect(await b3tr.totalSupply()).to.equal(await emissions.totalEmissions()) console.log(`Total emissions: ${ethers.formatEther(await emissions.totalEmissions())}`) - }).timeout(1000 * 60 * 5) // 5 minutes + }).timeout(1000 * 60 * 10) // 10 minutes it("Should not be able to distribute if cycle is not ready", async () => { const { emissions, minterAccount } = await getOrDeployContractInstances({ diff --git a/test/GalaxyMember.test.ts b/test/GalaxyMember.test.ts index fd53cfa..5c54c31 100644 --- a/test/GalaxyMember.test.ts +++ b/test/GalaxyMember.test.ts @@ -25,7 +25,7 @@ import { getImplementationAddress } from "@openzeppelin/upgrades-core" import { deployProxy } from "../scripts/helpers" import { GalaxyMember } from "../typechain-types" -describe("Galaxy Member - @shard2", () => { +describe("Galaxy Member - @shard6", () => { describe("Contract parameters", () => { it("Should have correct parameters set on deployment", async () => { const { galaxyMember, owner } = await getOrDeployContractInstances({ forceDeploy: true }) diff --git a/test/Governance.test.ts b/test/Governance.test.ts index c111af6..3563a3f 100644 --- a/test/Governance.test.ts +++ b/test/Governance.test.ts @@ -1771,6 +1771,18 @@ describe("Governor and TimeLock - @shard1", function () { expect(updatedQuorum).to.not.eql(newQuorum) }) + it("Can get and set veBetterPassport address", async function () { + const { governor, owner, otherAccount } = await getOrDeployContractInstances({ forceDeploy: true }) + + await governor.connect(owner).setVeBetterPassport(owner.address) + + const updatedVeBetterPassportAddress = await governor.veBetterPassport() + expect(updatedVeBetterPassportAddress).to.eql(owner.address) + + // only admin can set the veBetterPassport address + await expect(governor.connect(otherAccount).setVeBetterPassport(otherAccount.address)).to.be.reverted + }) + describe("Pausability", function () { it("Admin with PAUSER_ROLE should be able to pause the contract", async function () { const { governor, owner } = await getOrDeployContractInstances({ diff --git a/test/Timelock.test.ts b/test/Timelock.test.ts index 9d41a50..73e8040 100644 --- a/test/Timelock.test.ts +++ b/test/Timelock.test.ts @@ -5,7 +5,7 @@ import { describe, it } from "mocha" import { getImplementationAddress } from "@openzeppelin/upgrades-core" // Tests about queueing and executing proposals are in the Governance.test.ts file -describe("TimeLock - @shard2", function () { +describe("TimeLock - @shard6", function () { describe("Contract upgradeablity", () => { it("Admin should be able to upgrade the contract", async function () { const { timeLock, timelockAdmin } = await getOrDeployContractInstances({ diff --git a/test/Treasury.test.ts b/test/Treasury.test.ts index 91b64d8..b22e6ec 100644 --- a/test/Treasury.test.ts +++ b/test/Treasury.test.ts @@ -17,7 +17,7 @@ import { deployProxy } from "../scripts/helpers" import { getEventName } from "./helpers/events" import { ZERO_ADDRESS } from "./helpers" -describe("Treasury - @shard2", () => { +describe("Treasury - @shard5", () => { let treasuryProxy: Treasury let b3tr: B3TR let vot3: any diff --git a/test/VeBetterPassport.test.ts b/test/VeBetterPassport.test.ts index e14579a..b969c2a 100644 --- a/test/VeBetterPassport.test.ts +++ b/test/VeBetterPassport.test.ts @@ -511,6 +511,18 @@ describe("VeBetterPassport - @shard5", function () { expect(await veBetterPassport.getXAllocationVoting()).to.equal(otherAccount.address) }) + + it("Can set pop threshold to 0", async function () { + const { owner, veBetterPassport } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + await veBetterPassport.connect(owner).setThresholdPoPScore(12) + expect(await veBetterPassport.thresholdPoPScore()).to.equal(12n) + + await veBetterPassport.connect(owner).setThresholdPoPScore(0) + expect(await veBetterPassport.thresholdPoPScore()).to.equal(0n) + }) }) describe("Passport Signaling", function () { @@ -949,6 +961,98 @@ describe("VeBetterPassport - @shard5", function () { }) describe("Passport Entities", function () { + it("Should revert if an entity is trying to become a passport", async function () { + const { veBetterPassport, otherAccounts } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + const A = otherAccounts[0] + const B = otherAccounts[1] + const C = otherAccounts[2] + + await linkEntityToPassportWithSignature(veBetterPassport, A, B, 1000) + + await expect(linkEntityToPassportWithSignature(veBetterPassport, B, C, 1000)).to.be.revertedWithCustomError( + veBetterPassport, + "AlreadyLinked", + ) + }) + + it("Should revert if an entity is trying to become a passport", async function () { + const { veBetterPassport, otherAccounts } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + const A = otherAccounts[0] + const B = otherAccounts[1] + const C = otherAccounts[2] + + await linkEntityToPassportWithSignature(veBetterPassport, A, B, 1000) + + await expect(veBetterPassport.connect(C).linkEntityToPassport(B.address)).to.be.revertedWithCustomError( + veBetterPassport, + "AlreadyLinked", + ) + }) + + it("Should revert if passport is trying to create a pending link to another passport", async function () { + const { veBetterPassport, otherAccounts } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + const A = otherAccounts[0] + const B = otherAccounts[1] + const C = otherAccounts[2] + + await linkEntityToPassportWithSignature(veBetterPassport, A, B, 1000) + + await expect(veBetterPassport.connect(A).linkEntityToPassport(C.address)).to.be.revertedWithCustomError( + veBetterPassport, + "AlreadyLinked", + ) + }) + + it("Entity check should be done also when accepting a link", async function () { + const { veBetterPassport, otherAccounts } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + const A = otherAccounts[0] + const B = otherAccounts[1] + const C = otherAccounts[2] + + // Scenario: + // A -> pending linking to B + // C -> pending linking to A + // B accepts linking + // A accepts linking -> should revert + + await veBetterPassport.connect(A).linkEntityToPassport(B.address) + await expect(veBetterPassport.connect(C).linkEntityToPassport(A.address)).to.be.revertedWithCustomError( + veBetterPassport, + "AlreadyLinked", + ) + }) + + it("Should revert if passport is trying to link to another passport with signature", async function () { + const { veBetterPassport, otherAccounts } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + const A = otherAccounts[0] + const B = otherAccounts[1] + const C = otherAccounts[2] + + // Enitity B links to passport A + await linkEntityToPassportWithSignature(veBetterPassport, A, B, 1000) + + // Passport A tries to link to passport C + await expect(linkEntityToPassportWithSignature(veBetterPassport, C, A, 1000)).to.be.revertedWithCustomError( + veBetterPassport, + "AlreadyLinked", + ) + }) + it("Should be able to register an entity by function calls", async function () { const { veBetterPassport, @@ -2447,6 +2551,34 @@ describe("VeBetterPassport - @shard5", function () { "User's participation score is above the threshold", ]) }) + + it("Cannot attach an entity if entity has delegated personhood", async function () { + const config = createTestConfig() + const { veBetterPassport, otherAccounts } = await getOrDeployContractInstances({ + forceDeploy: true, + config, + }) + + const enity1 = otherAccounts[0] + const enity2 = otherAccounts[1] + const passport = otherAccounts[2] + const passport2 = otherAccounts[3] + + // Delegate entity 1 personhood to entity 2 + await delegateWithSignature(veBetterPassport, enity1, passport2, 1000) + + await veBetterPassport.connect(enity2).delegatePassport(passport2.address) + + // Should revert if entity 1 tries to attach to passport + await expect( + linkEntityToPassportWithSignature(veBetterPassport, passport, enity1, 1000), + ).to.be.revertedWithCustomError(veBetterPassport, "DelegatedEntity") + + // Should revert if entity 2 tries to attach to passport + await expect( + veBetterPassport.connect(enity2).linkEntityToPassport(passport.address), + ).to.be.revertedWithCustomError(veBetterPassport, "DelegatedEntity") + }) }) describe("Passport Delegation", function () { @@ -2867,7 +2999,7 @@ describe("VeBetterPassport - @shard5", function () { await expect(veBetterPassport.connect(B).denyIncomingPendingDelegation(A.address)).to.be.reverted }) - it("Should revert if a user tries to cancel pending delegation thta does not exist", async function () { + it("Should revert if a user tries to cancel pending delegation that does not exist", async function () { const { owner: A, veBetterPassport } = await getOrDeployContractInstances({ forceDeploy: true, }) @@ -3752,6 +3884,45 @@ describe("VeBetterPassport - @shard5", function () { await expect(delegateWithSignature(veBetterPassport, passport, entity2, 3600)).to.not.be.reverted }) + it("A passport cannot be delegated if a pending entity", async function () { + const { + veBetterPassport, + owner: passport, + otherAccount: entity, + otherAccounts, + } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + const passport2 = otherAccounts[1] + const entity2 = otherAccounts[2] + const user = otherAccounts[3] + + await veBetterPassport.connect(entity).linkEntityToPassport(passport.address) + await veBetterPassport.connect(entity2).linkEntityToPassport(passport2.address) + + // Should be a pending entity + expect((await veBetterPassport.getPendingLinkings(entity.address))[1]).to.equal(passport.address) + + // Should not be able to delegate as a pending entity + await expect(delegateWithSignature(veBetterPassport, entity, user, 3600)).to.be.revertedWithCustomError( + veBetterPassport, + "PassportDelegationFromEntity", + ) + + // Should not be able to delegate a passport + await expect(veBetterPassport.connect(passport2).delegatePassport(entity2.address)).to.be.revertedWithCustomError( + veBetterPassport, + "PassportDelegationToEntity", + ) + + // detach entity + await veBetterPassport.connect(passport2).denyIncomingPendingEntityLink(entity2) + + // Should be able to delegate + await expect(delegateWithSignature(veBetterPassport, passport, entity2, 3600)).to.not.be.reverted + }) + it("should revert if a wallet not linked to a passport tries to be removed", async function () { const { veBetterPassport, diff --git a/test/VoterRewards.test.ts b/test/VoterRewards.test.ts index e5b5dc0..9658cf7 100644 --- a/test/VoterRewards.test.ts +++ b/test/VoterRewards.test.ts @@ -27,7 +27,7 @@ import { getImplementationAddress } from "@openzeppelin/upgrades-core" import { deployAndUpgrade, deployProxy, upgradeProxy } from "../scripts/helpers" import { B3TRGovernor, GalaxyMember, VoterRewards, VoterRewardsV1, XAllocationVoting } from "../typechain-types" -describe("VoterRewards - @shard2", () => { +describe("VoterRewards - @shard7", () => { describe("Contract parameters", () => { it("Should have correct parameters set on deployment", async () => { const { voterRewards, owner, galaxyMember, emissions } = await getOrDeployContractInstances({ forceDeploy: true }) diff --git a/test/X2EarnRewardsPool.test.ts b/test/X2EarnRewardsPool.test.ts index 23320d6..a1d1d6a 100644 --- a/test/X2EarnRewardsPool.test.ts +++ b/test/X2EarnRewardsPool.test.ts @@ -14,7 +14,7 @@ import { X2EarnRewardsPool, X2EarnRewardsPoolV2 } from "../typechain-types" import { X2EarnRewardsPoolV1 } from "../typechain-types/contracts/deprecated/V1" import { createLocalConfig } from "../config/contracts/envs/local" -describe("X2EarnRewardsPool - @shard3", function () { +describe("X2EarnRewardsPool - @shard7", function () { // deployment describe("Deployment", function () { it("Cannot deploy contract with zero address", async function () { @@ -441,6 +441,18 @@ describe("X2EarnRewardsPool - @shard3", function () { data: "0x1234", // some data }) }) + + it("Can get and set veBetterPassport address", async function () { + const { x2EarnRewardsPool, owner, otherAccount } = await getOrDeployContractInstances({ forceDeploy: true }) + + await x2EarnRewardsPool.connect(owner).setVeBetterPassport(owner.address) + + const updatedVeBetterPassportAddress = await x2EarnRewardsPool.veBetterPassport() + expect(updatedVeBetterPassportAddress).to.eql(owner.address) + + // only admin can set the veBetterPassport address + await expect(x2EarnRewardsPool.connect(otherAccount).setVeBetterPassport(otherAccount.address)).to.be.reverted + }) }) // deposit diff --git a/test/XAllocationVoting.test.ts b/test/XAllocationVoting.test.ts index 347cad7..c0da658 100644 --- a/test/XAllocationVoting.test.ts +++ b/test/XAllocationVoting.test.ts @@ -20,12 +20,21 @@ import { ZERO_ADDRESS, waitForNextBlock, payDeposit, + waitForBlock, } from "./helpers" import { describe, it } from "mocha" import { getImplementationAddress } from "@openzeppelin/upgrades-core" -import { deployProxy } from "../scripts/helpers" -import { XAllocationVoting } from "../typechain-types" +import { deployProxy, upgradeProxy } from "../scripts/helpers" +import { + Emissions, + EmissionsV1, + VoterRewards, + VoterRewardsV1, + XAllocationVoting, + XAllocationVotingV1, +} from "../typechain-types" import { createLocalConfig } from "../config/contracts/envs/local" +import { createTestConfig } from "./helpers/config" describe("X-Allocation Voting - @shard4", function () { describe("Deployment", function () { @@ -447,6 +456,235 @@ describe("X-Allocation Voting - @shard4", function () { expect(await xAllocationVoting.version()).to.equal("2") }) + + it("Should not break storage when upgrading to V2", async () => { + const config = createTestConfig() + const { + otherAccounts, + x2EarnApps, + xAllocationPool, + b3tr, + vot3, + galaxyMember, + timeLock, + treasury, + owner, + veBetterPassport, + minterAccount, + } = await getOrDeployContractInstances({ + forceDeploy: true, + }) + + const emissionsV1 = (await deployProxy("Emissions", [ + { + minter: minterAccount.address, + admin: owner.address, + upgrader: owner.address, + contractsAddressManager: owner.address, + decaySettingsManager: owner.address, + b3trAddress: await b3tr.getAddress(), + destinations: [ + await xAllocationPool.getAddress(), + owner.address, + await treasury.getAddress(), + config.MIGRATION_ADDRESS, + ], + initialXAppAllocation: config.INITIAL_X_ALLOCATION, + cycleDuration: config.EMISSIONS_CYCLE_DURATION, + decaySettings: [ + config.EMISSIONS_X_ALLOCATION_DECAY_PERCENTAGE, + config.EMISSIONS_VOTE_2_EARN_DECAY_PERCENTAGE, + config.EMISSIONS_X_ALLOCATION_DECAY_PERIOD, + config.EMISSIONS_VOTE_2_EARN_ALLOCATION_DECAY_PERIOD, + ], + treasuryPercentage: config.EMISSIONS_TREASURY_PERCENTAGE, + maxVote2EarnDecay: config.EMISSIONS_MAX_VOTE_2_EARN_DECAY_PERCENTAGE, + migrationAmount: config.MIGRATION_AMOUNT, + }, + ])) as EmissionsV1 + + const emissions = (await upgradeProxy( + "EmissionsV1", + "Emissions", + await emissionsV1.getAddress(), + [config.EMISSIONS_IS_NOT_ALIGNED ?? false], + { + version: 2, + }, + )) as Emissions + + const voterRewardsV1 = (await deployProxy("VoterRewardsV1", [ + owner.address, // admin + owner.address, // upgrader + owner.address, // contractsAddressManager + await emissions.getAddress(), + await galaxyMember.getAddress(), + await b3tr.getAddress(), + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + [0, 10, 20, 50, 100, 150, 200, 400, 900, 2400], + ])) as VoterRewardsV1 + + const voterRewards = (await upgradeProxy( + "VoterRewardsV1", + "VoterRewards", + await voterRewardsV1.getAddress(), + [], + { + version: 2, + }, + )) as VoterRewards + + // Set vote 2 earn (VoterRewards deployed contract) address in emissions + await emissions.connect(owner).setVote2EarnAddress(await voterRewards.getAddress()) + + // const deploy V1 contract + let xAllocationVotingV1 = (await deployProxy("XAllocationVotingV1", [ + { + vot3Token: await vot3.getAddress(), + quorumPercentage: config.X_ALLOCATION_VOTING_QUORUM_PERCENTAGE, // quorum percentage + initialVotingPeriod: config.EMISSIONS_CYCLE_DURATION - 1, // X Alloc voting period + timeLock: await timeLock.getAddress(), + voterRewards: await voterRewards.getAddress(), + emissions: await emissions.getAddress(), + admins: [await timeLock.getAddress(), owner.address], + upgrader: owner.address, + contractsAddressManager: owner.address, + x2EarnAppsAddress: await x2EarnApps.getAddress(), + baseAllocationPercentage: config.X_ALLOCATION_POOL_BASE_ALLOCATION_PERCENTAGE, + appSharesCap: config.X_ALLOCATION_POOL_APP_SHARES_MAX_CAP, + votingThreshold: config.X_ALLOCATION_VOTING_VOTING_THRESHOLD, + }, + ])) as XAllocationVotingV1 + expect(await xAllocationVotingV1.version()).to.equal("1") + + await emissions.setXAllocationsGovernorAddress(await xAllocationVotingV1.getAddress()) + expect(await emissions.xAllocationsGovernor()).to.eql(await xAllocationVotingV1.getAddress()) + + await xAllocationPool.setXAllocationVotingAddress(await xAllocationVotingV1.getAddress()) + expect(await xAllocationPool.xAllocationVoting()).to.eql(await xAllocationVotingV1.getAddress()) + await xAllocationPool.setEmissionsAddress(await emissions.getAddress()) + expect(await xAllocationPool.emissions()).to.eql(await emissions.getAddress()) + + // Grant Vote registrar role to XAllocationVoting + await voterRewards + .connect(owner) + .grantRole(await voterRewards.VOTE_REGISTRAR_ROLE(), await xAllocationVotingV1.getAddress()) + + // Grant admin role to voter rewards for registering x allocation voting + await xAllocationVotingV1 + .connect(owner) + .grantRole(await xAllocationVotingV1.DEFAULT_ADMIN_ROLE(), emissions.getAddress()) + + //Set the emissions address and the admin as the ROUND_STARTER_ROLE in XAllocationVoting + const roundStarterRole = await xAllocationVotingV1.ROUND_STARTER_ROLE() + await xAllocationVotingV1 + .connect(owner) + .grantRole(roundStarterRole, await emissions.getAddress()) + .then(async (tx: any) => await tx.wait()) + await xAllocationVotingV1 + .connect(owner) + .grantRole(roundStarterRole, owner.address) + .then(async (tx: any) => await tx.wait()) + + const user1 = otherAccounts[0] + const user2 = otherAccounts[1] + const user3 = otherAccounts[2] + + // fund wallets + await getVot3Tokens(user1, "1000") + await getVot3Tokens(user2, "1000") + await getVot3Tokens(user3, "1000") + + // add apps + const app1Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[2].address)) + const app2Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[3].address)) + const app3Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[4].address)) + await x2EarnApps.addApp( + otherAccounts[2].address, + otherAccounts[2].address, + otherAccounts[2].address, + "metadataURI", + ) + await x2EarnApps.addApp( + otherAccounts[3].address, + otherAccounts[3].address, + otherAccounts[3].address, + "metadataURI", + ) + await x2EarnApps.addApp( + otherAccounts[4].address, + otherAccounts[4].address, + otherAccounts[4].address, + "metadataURI", + ) + + // Grant minter role to emissions contract + await b3tr.connect(owner).grantRole(await b3tr.MINTER_ROLE(), await emissions.getAddress()) + // Bootstrap emissions + await emissions.connect(minterAccount).bootstrap() + + // start round + await emissions.connect(minterAccount).start() + expect(await xAllocationVotingV1.currentRoundId()).to.equal(1n) + + // make people vote + await xAllocationVotingV1.connect(user1).castVote(1, [app1Id], [ethers.parseEther("100")]) + await xAllocationVotingV1 + .connect(user2) + .castVote(1, [app1Id, app2Id], [ethers.parseEther("100"), ethers.parseEther("200")]) + + // upgrade to V2 + const xAllocationVotingV2 = (await upgradeProxy( + "XAllocationVotingV1", + "XAllocationVoting", + await xAllocationVotingV1.getAddress(), + [await veBetterPassport.getAddress()], + { + version: 2, + }, + )) as XAllocationVoting + expect(await xAllocationVotingV2.version()).to.equal("2") + + // set personhood threshold to 0 + await veBetterPassport.connect(owner).setThresholdPoPScore(0) + await veBetterPassport.toggleCheck(4) + + // check that round is ok + expect(await xAllocationVotingV2.currentRoundId()).to.equal(1n) + expect(await xAllocationVotingV2.state(1n)).to.equal(0n) // Active + + // check that previous votes are ok + const votes = await xAllocationVotingV2.totalVotes(1) + expect(votes).to.equal(ethers.parseEther("400")) + + expect(await xAllocationVotingV2.hasVoted(1, user1.address)).to.be.true + expect(await xAllocationVotingV2.hasVoted(1, user2.address)).to.be.true + expect(await xAllocationVotingV2.hasVoted(1, user3.address)).to.be.false + + expect(await xAllocationVotingV2.getAppVotes(1, app1Id)).to.equal(ethers.parseEther("200")) + expect(await xAllocationVotingV2.getAppVotes(1, app2Id)).to.equal(ethers.parseEther("200")) + expect(await xAllocationVotingV2.getAppVotes(1, app3Id)).to.equal(ethers.parseEther("0")) + + // check that can still vote on the new round + await xAllocationVotingV2.connect(user3).castVote(1, [app1Id], [ethers.parseEther("100")]) + expect(await xAllocationVotingV2.getAppVotes(1, app1Id)).to.equal(ethers.parseEther("300")) + + // check that round is over correctly + const blockNextCycle = await emissions.getNextCycleBlock() + await waitForBlock(Number(blockNextCycle)) + expect(await emissions.isCycleEnded(1)).to.be.true + + await emissions.distribute() + expect(await xAllocationVotingV2.currentRoundId()).to.equal(2n) + + // check that rewards are distributed correctly + await expect(xAllocationPool.claim(1, app1Id)).to.not.be.reverted + await expect(xAllocationPool.claim(1, app2Id)).to.not.be.reverted + await expect(xAllocationPool.claim(1, app3Id)).to.not.be.reverted + + // can cast vote for round 2 + await xAllocationVotingV2.connect(user1).castVote(2, [app1Id], [ethers.parseEther("100")]) + }) }) describe("Settings", function () { @@ -596,6 +834,24 @@ describe("X-Allocation Voting - @shard4", function () { .reverted }) }) + + it("Can get and set veBetterPassport address", async function () { + const { xAllocationVoting, owner, otherAccount } = await getOrDeployContractInstances({ forceDeploy: true }) + + // assign governance role to owner + await xAllocationVoting.grantRole(await xAllocationVoting.GOVERNANCE_ROLE(), owner.address) + expect(await xAllocationVoting.hasRole(await xAllocationVoting.GOVERNANCE_ROLE(), owner.address)).to.be.true + + await xAllocationVoting.connect(owner).setVeBetterPassport(owner.address) + + const updatedVeBetterPassportAddress = await xAllocationVoting.veBetterPassport() + expect(updatedVeBetterPassportAddress).to.eql(owner.address) + + // only GOVERNANCE_ROLE can set the veBetterPassport address + expect(await xAllocationVoting.hasRole(await xAllocationVoting.GOVERNANCE_ROLE(), otherAccount.address)).to.be + .false + await expect(xAllocationVoting.connect(otherAccount).setVeBetterPassport(otherAccount.address)).to.be.reverted + }) }) describe("Voting threshold", function () { @@ -1384,6 +1640,117 @@ describe("X-Allocation Voting - @shard4", function () { ).to.be.revertedWithCustomError(xAllocationVoting, "GovernorVotingThresholdNotMet") }) + it("If the vote weight for an XApp is less than 1, the exact vote weight should be applied to increase the XApp's total votes, rather than using the square root of the vote weight", async function () { + const { xAllocationVoting, x2EarnApps, otherAccounts, owner, veBetterPassport } = + await getOrDeployContractInstances({ + forceDeploy: true, + }) + + await veBetterPassport.toggleCheck(4) + + // Bootstrap emissions + await bootstrapEmissions() + + otherAccounts.forEach(async account => { + await getVot3Tokens(account, "10000") + }) + + //Add apps + + const app1Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[2].address)) + const app2Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[3].address)) + const app3Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[4].address)) + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[2].address, otherAccounts[2].address, otherAccounts[2].address, "metadataURI") + + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[3].address, otherAccounts[3].address, otherAccounts[3].address, "metadataURI") + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[4].address, otherAccounts[4].address, otherAccounts[4].address, "metadataURI") + + //Start allocation round + const round1 = await startNewAllocationRound() + // Vote + await xAllocationVoting + .connect(otherAccounts[1]) + .castVote( + round1, + [app1Id, app2Id, app3Id], + [ethers.parseEther("0.5"), ethers.parseEther("0.5"), ethers.parseEther("0.5")], + ) + + await xAllocationVoting + .connect(otherAccounts[2]) + .castVote( + round1, + [app1Id, app2Id, app3Id], + [ethers.parseEther("0.4"), ethers.parseEther("0.1"), ethers.parseEther("0.5")], + ) + + await xAllocationVoting + .connect(otherAccounts[3]) + .castVote( + round1, + [app1Id, app2Id, app3Id], + [ethers.parseEther("0.1"), ethers.parseEther("4"), ethers.parseEther("0")], + ) + + // Votes should be tracked correctly + let appVotes = await xAllocationVoting.getAppVotesQF(round1, app1Id) + expect(appVotes).to.eql(ethers.parseEther("1") / 1000000000n) + + appVotes = await xAllocationVoting.getAppVotesQF(round1, app2Id) + expect(appVotes).to.eql(ethers.parseEther("2.6") / 1000000000n) + + appVotes = await xAllocationVoting.getAppVotesQF(round1, app3Id) + expect(appVotes).to.eql(ethers.parseEther("1") / 1000000000n) + }) + + it("If a user votes for an XApp with a vote wieght < 1 we do not get the square of the number ", async function () { + const { xAllocationVoting, x2EarnApps, otherAccounts, owner, veBetterPassport } = + await getOrDeployContractInstances({ + forceDeploy: true, + }) + + // Bootstrap emissions + await bootstrapEmissions() + + await veBetterPassport.toggleCheck(4) + + otherAccounts.forEach(async account => { + await getVot3Tokens(account, "10000") + }) + + //Add apps + + const app1Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[2].address)) + const app2Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[3].address)) + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[2].address, otherAccounts[2].address, otherAccounts[2].address, "metadataURI") + await x2EarnApps + .connect(owner) + .addApp(otherAccounts[3].address, otherAccounts[3].address, otherAccounts[3].address, "metadataURI") + + //Start allocation round + const round1 = await startNewAllocationRound() + // Vote + await xAllocationVoting + .connect(otherAccounts[1]) + .castVote(round1, [app1Id, app2Id], [ethers.parseEther("0.5"), ethers.parseEther("9")]) + + await waitForRoundToEnd(round1) + + const app1VotesQF = await xAllocationVoting.getAppVotesQF(round1, app1Id) + const app2VotesQF = await xAllocationVoting.getAppVotesQF(round1, app2Id) + // sqrt of 10^18 is 10^9 hence we need to divide by 10^9 + expect(app1VotesQF).to.equal(ethers.parseEther("0.5") / 1000000000n) + expect(app2VotesQF).to.equal(ethers.parseEther("3") / 1000000000n) + }) + it("I should not be able to cast vote twice", async function () { const { xAllocationVoting, x2EarnApps, otherAccounts, otherAccount, owner, veBetterPassport } = await getOrDeployContractInstances({ From 6ab95b04e7403a5685302b2fc4619fdee445d3e2 Mon Sep 17 00:00:00 2001 From: Dan Rusnac Date: Sat, 12 Oct 2024 14:26:34 +0200 Subject: [PATCH 3/5] feat: added mainnet address for VBP --- Readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index 9ed3fc8..8e93796 100644 --- a/Readme.md +++ b/Readme.md @@ -44,7 +44,7 @@ Our contracts are upgradeable and versioned. See the [contracts changelog](CONTR "X2EarnRewardsPool": "0x6Bee7DDab6c99d5B2Af0554EaEA484CE18F52631", "XAllocationPool": "0x4191776F05f4bE4848d3f4d587345078B439C7d3", "XAllocationVoting": "0x89A00Bb0947a30FF95BEeF77a66AEdE3842Fe5B7", - "VeBetterPassport": "0x45d5CA3f295ad8BCa291cC4ecd33382DE40E4FAc" + "VeBetterPassport": "0x1311eE8c85e3B533756d5d31F3246834616E025D" ``` ## Testnet contract addresses From 913527b4088c4174a7641016c4eeff9d09704c90 Mon Sep 17 00:00:00 2001 From: Dan Rusnac Date: Sat, 12 Oct 2024 14:33:46 +0200 Subject: [PATCH 4/5] feat: added mainnet address for VBP --- Readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index 8e93796..62602a3 100644 --- a/Readme.md +++ b/Readme.md @@ -44,7 +44,7 @@ Our contracts are upgradeable and versioned. See the [contracts changelog](CONTR "X2EarnRewardsPool": "0x6Bee7DDab6c99d5B2Af0554EaEA484CE18F52631", "XAllocationPool": "0x4191776F05f4bE4848d3f4d587345078B439C7d3", "XAllocationVoting": "0x89A00Bb0947a30FF95BEeF77a66AEdE3842Fe5B7", - "VeBetterPassport": "0x1311eE8c85e3B533756d5d31F3246834616E025D" + "VeBetterPassport": "0x35a267671d8EDD607B2056A9a13E7ba7CF53c8b3" ``` ## Testnet contract addresses From d9d7ad7b6b98531ab18408baa47271b185dc55af Mon Sep 17 00:00:00 2001 From: Dan Rusnac Date: Sat, 12 Oct 2024 15:03:34 +0200 Subject: [PATCH 5/5] fix: deploy script --- scripts/deploy/deploy.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/scripts/deploy/deploy.ts b/scripts/deploy/deploy.ts index f05f64d..c1c0206 100644 --- a/scripts/deploy/deploy.ts +++ b/scripts/deploy/deploy.ts @@ -517,6 +517,12 @@ export async function deployAll(config: ContractsConfig) { .toggleCheck(4) .then(async tx => await tx.wait()) + // Assign ACTION_REGISTRAR_ROLE to X2EarnRewardsPool + await veBetterPassport + .connect(deployer) + .grantRole(await veBetterPassport.ACTION_REGISTRAR_ROLE(), await x2EarnRewardsPool.getAddress()) + .then(async tx => await tx.wait()) + // ---------- Configure contract roles for setup ---------- // console.log("================ Configuring contract roles for setup")