From 3c7bd17d07cd4a2c0ec909aa49fac465eaf59651 Mon Sep 17 00:00:00 2001 From: hudsonhrh Date: Sun, 8 Mar 2026 18:51:35 -0400 Subject: [PATCH] feat: separate quorum (voter count) from threshold (support percentage) Renames existing "quorum percentage" concepts to "threshold" and introduces a new voter-count-based "quorum" concept. DirectDemocracyVoting and HybridVoting now enforce minimum voter participation independently from the winning percentage requirement. Updates all configuration, tests, and deployment scripts accordingly. Co-Authored-By: Claude Haiku 4.5 --- script/DeployOrg.s.sol | 14 +- script/MainDeploy.s.sol | 4 +- script/README.md | 2 +- script/README_RunOrgActions.md | 4 +- script/RunOrgActions.s.sol | 16 +-- script/RunOrgActionsAdvanced.s.sol | 16 +-- script/org-config-advanced-demo.json | 2 +- script/org-config-direct-democracy.json | 2 +- script/org-config-example.json | 2 +- script/org-config-governance-demo.json | 2 +- src/DirectDemocracyVoting.sol | 68 +++++----- src/HybridVoting.sol | 40 ++++-- src/OrgDeployer.sol | 12 +- src/factories/GovernanceFactory.sol | 8 +- src/lens/DirectDemocracyVotingLens.sol | 5 +- src/libs/HybridVotingCore.sol | 11 +- src/libs/ModuleDeploymentLib.sol | 16 ++- src/libs/VotingErrors.sol | 1 + src/libs/VotingMath.sol | 57 ++++----- test/DeployerTest.t.sol | 162 ++++++++++++------------ test/DirectDemocracyVoting.t.sol | 143 +++++++++++++++++++-- test/GovernanceCrossChainUpgrade.t.sol | 4 +- test/HybridVoting.t.sol | 131 +++++++++++++++---- test/VotingMath.t.sol | 46 +++---- test/VotingMathVerification.sol | 4 +- 25 files changed, 502 insertions(+), 270 deletions(-) diff --git a/script/DeployOrg.s.sol b/script/DeployOrg.s.sol index 369afe4..27ecd82 100644 --- a/script/DeployOrg.s.sol +++ b/script/DeployOrg.s.sol @@ -33,7 +33,7 @@ contract DeployOrg is Script { string orgId; string orgName; bool autoUpgrade; - QuorumConfig quorum; + ThresholdConfig threshold; RoleConfig[] roles; VotingClassConfig[] votingClasses; RoleAssignmentsConfig roleAssignments; @@ -43,7 +43,7 @@ contract DeployOrg is Script { BootstrapConfigJson bootstrap; // Optional: initial projects and tasks } - struct QuorumConfig { + struct ThresholdConfig { uint8 hybrid; uint8 directDemocracy; } @@ -212,9 +212,9 @@ contract DeployOrg is Script { config.withEducationHub = true; // Default to enabled for backward compatibility } - // Parse quorum - config.quorum.hybrid = uint8(vm.parseJsonUint(configJson, ".quorum.hybrid")); - config.quorum.directDemocracy = uint8(vm.parseJsonUint(configJson, ".quorum.directDemocracy")); + // Parse threshold + config.threshold.hybrid = uint8(vm.parseJsonUint(configJson, ".threshold.hybrid")); + config.threshold.directDemocracy = uint8(vm.parseJsonUint(configJson, ".threshold.directDemocracy")); // Parse roles array - use serializeJson to work around struct array limitations // First, manually count by trying to parse until we fail (reasonable max: 100) @@ -478,8 +478,8 @@ contract DeployOrg is Script { // Note: regDeadline/regNonce/regSignature left as default (0/"") = skip sig-based registration // The frontend should provide these when deployerUsername is non-empty params.autoUpgrade = config.autoUpgrade; - params.hybridQuorumPct = config.quorum.hybrid; - params.ddQuorumPct = config.quorum.directDemocracy; + params.hybridThresholdPct = config.threshold.hybrid; + params.ddThresholdPct = config.threshold.directDemocracy; params.ddInitialTargets = config.ddInitialTargets; // Build role configs diff --git a/script/MainDeploy.s.sol b/script/MainDeploy.s.sol index b7431c9..242a043 100644 --- a/script/MainDeploy.s.sol +++ b/script/MainDeploy.s.sol @@ -290,8 +290,8 @@ contract DeployHomeChain is DeployHelper { params.deployerUsername = ""; // regDeadline/regNonce/regSignature left as default (0/"") = skip registration params.autoUpgrade = true; - params.hybridQuorumPct = 50; - params.ddQuorumPct = 50; + params.hybridThresholdPct = 50; + params.ddThresholdPct = 50; // --- Roles --- params.roles = new RoleConfigStructs.RoleConfig[](2); diff --git a/script/README.md b/script/README.md index 886f67f..4801020 100644 --- a/script/README.md +++ b/script/README.md @@ -76,7 +76,7 @@ Create a JSON configuration file for your organization. See example configs: "orgId": "unique-org-identifier", "orgName": "Organization Display Name", "autoUpgrade": true, - "quorum": { + "threshold": { "hybrid": 50, "directDemocracy": 50 }, diff --git a/script/README_RunOrgActions.md b/script/README_RunOrgActions.md index c897f29..b31e287 100644 --- a/script/README_RunOrgActions.md +++ b/script/README_RunOrgActions.md @@ -32,7 +32,7 @@ The `org-config-governance-demo.json` defines a 4-role organization: - **Hybrid Voting**: 60% Direct Democracy / 40% Token-Weighted - **Quadratic Voting**: Enabled for token class -- **Quorum**: 50% for HybridVoting, 60% for DirectDemocracy +- **Threshold**: 50% for HybridVoting, 60% for DirectDemocracy ## Prerequisites @@ -175,7 +175,7 @@ The script uses Foundry's JSON parsing capabilities: ```solidity vm.parseJsonString(configJson, ".orgId") -vm.parseJsonUint(configJson, ".quorum.hybrid") +vm.parseJsonUint(configJson, ".threshold.hybrid") vm.parseJson(configJson, ".roleAssignments.quickJoinRoles") ``` diff --git a/script/RunOrgActions.s.sol b/script/RunOrgActions.s.sol index 782f8c1..76cb436 100644 --- a/script/RunOrgActions.s.sol +++ b/script/RunOrgActions.s.sol @@ -57,7 +57,7 @@ contract RunOrgActions is Script { string orgId; string orgName; bool autoUpgrade; - QuorumConfig quorum; + ThresholdConfig threshold; RoleConfig[] roles; VotingClassConfig[] votingClasses; RoleAssignmentsConfig roleAssignments; @@ -67,7 +67,7 @@ contract RunOrgActions is Script { BootstrapConfigJson bootstrap; // Optional: initial projects and tasks } - struct QuorumConfig { + struct ThresholdConfig { uint8 hybrid; uint8 directDemocracy; } @@ -600,7 +600,7 @@ contract RunOrgActions is Script { console.log(" [OK] Winner Announced"); console.log(" Winning Option:", winningOption); - console.log(" Is Valid (quorum met):", isValid); + console.log(" Is Valid (threshold met):", isValid); console.log("\n[OK] Governance Demonstration Complete - Full Cycle!"); console.log(" Proposal Created: 1"); @@ -628,9 +628,9 @@ contract RunOrgActions is Script { config.withEducationHub = true; // Default to enabled for backward compatibility } - // Parse quorum - config.quorum.hybrid = uint8(vm.parseJsonUint(configJson, ".quorum.hybrid")); - config.quorum.directDemocracy = uint8(vm.parseJsonUint(configJson, ".quorum.directDemocracy")); + // Parse threshold + config.threshold.hybrid = uint8(vm.parseJsonUint(configJson, ".threshold.hybrid")); + config.threshold.directDemocracy = uint8(vm.parseJsonUint(configJson, ".threshold.directDemocracy")); // Parse roles array uint256 rolesLength = 0; @@ -887,8 +887,8 @@ contract RunOrgActions is Script { params.deployerAddress = deployerAddress; // Address to receive ADMIN hat params.deployerUsername = ""; // Registration requires EIP-712 signature (regSignature) from frontend params.autoUpgrade = config.autoUpgrade; - params.hybridQuorumPct = config.quorum.hybrid; - params.ddQuorumPct = config.quorum.directDemocracy; + params.hybridThresholdPct = config.threshold.hybrid; + params.ddThresholdPct = config.threshold.directDemocracy; params.ddInitialTargets = config.ddInitialTargets; // Build role configs diff --git a/script/RunOrgActionsAdvanced.s.sol b/script/RunOrgActionsAdvanced.s.sol index 7a3887f..5be23a8 100644 --- a/script/RunOrgActionsAdvanced.s.sol +++ b/script/RunOrgActionsAdvanced.s.sol @@ -61,7 +61,7 @@ contract RunOrgActionsAdvanced is Script { string orgId; string orgName; bool autoUpgrade; - QuorumConfig quorum; + ThresholdConfig threshold; RoleConfig[] roles; VotingClassConfig[] votingClasses; RoleAssignmentsConfig roleAssignments; @@ -71,7 +71,7 @@ contract RunOrgActionsAdvanced is Script { BootstrapConfigJson bootstrap; // Optional: initial projects and tasks } - struct QuorumConfig { + struct ThresholdConfig { uint8 hybrid; uint8 directDemocracy; } @@ -717,7 +717,7 @@ contract RunOrgActionsAdvanced is Script { console.log(" [OK] Winner Announced"); console.log(" Winning Option:", winningOption); - console.log(" Is Valid (quorum met):", isValid); + console.log(" Is Valid (threshold met):", isValid); console.log("\n[OK] Governance Demonstration Complete - Full Cycle!"); console.log(" Proposal Created: 1"); @@ -745,9 +745,9 @@ contract RunOrgActionsAdvanced is Script { config.withEducationHub = true; // Default to enabled for backward compatibility } - // Parse quorum - config.quorum.hybrid = uint8(vm.parseJsonUint(configJson, ".quorum.hybrid")); - config.quorum.directDemocracy = uint8(vm.parseJsonUint(configJson, ".quorum.directDemocracy")); + // Parse threshold + config.threshold.hybrid = uint8(vm.parseJsonUint(configJson, ".threshold.hybrid")); + config.threshold.directDemocracy = uint8(vm.parseJsonUint(configJson, ".threshold.directDemocracy")); // Parse roles array uint256 rolesLength = 0; @@ -1004,8 +1004,8 @@ contract RunOrgActionsAdvanced is Script { params.deployerAddress = deployerAddress; // Address to receive ADMIN hat params.deployerUsername = ""; // Registration requires EIP-712 signature (regSignature) from frontend params.autoUpgrade = config.autoUpgrade; - params.hybridQuorumPct = config.quorum.hybrid; - params.ddQuorumPct = config.quorum.directDemocracy; + params.hybridThresholdPct = config.threshold.hybrid; + params.ddThresholdPct = config.threshold.directDemocracy; params.ddInitialTargets = config.ddInitialTargets; // Build role configs diff --git a/script/org-config-advanced-demo.json b/script/org-config-advanced-demo.json index 39d1026..9e2126a 100644 --- a/script/org-config-advanced-demo.json +++ b/script/org-config-advanced-demo.json @@ -2,7 +2,7 @@ "orgId": "advanced-demo-cooperative-with-vouching", "orgName": "Advanced Demo Cooperative", "autoUpgrade": true, - "quorum": { + "threshold": { "hybrid": 50, "directDemocracy": 60 }, diff --git a/script/org-config-direct-democracy.json b/script/org-config-direct-democracy.json index 6d6203f..e5b8e52 100644 --- a/script/org-config-direct-democracy.json +++ b/script/org-config-direct-democracy.json @@ -2,7 +2,7 @@ "orgId": "direct-democracy-collective", "orgName": "Direct Democracy Collective", "autoUpgrade": true, - "quorum": { + "threshold": { "hybrid": 60, "directDemocracy": 60 }, diff --git a/script/org-config-example.json b/script/org-config-example.json index 60e093f..976f917 100644 --- a/script/org-config-example.json +++ b/script/org-config-example.json @@ -2,7 +2,7 @@ "orgId": "worker-cooperative-alpha", "orgName": "Worker Cooperative Alpha", "autoUpgrade": true, - "quorum": { + "threshold": { "hybrid": 50, "directDemocracy": 50 }, diff --git a/script/org-config-governance-demo.json b/script/org-config-governance-demo.json index 659281e..f7661e3 100644 --- a/script/org-config-governance-demo.json +++ b/script/org-config-governance-demo.json @@ -2,7 +2,7 @@ "orgId": "governance-demo-cooperative", "orgName": "Governance Demo Cooperative", "autoUpgrade": true, - "quorum": { + "threshold": { "hybrid": 50, "directDemocracy": 60 }, diff --git a/src/DirectDemocracyVoting.sol b/src/DirectDemocracyVoting.sol index 79066f8..1072f56 100644 --- a/src/DirectDemocracyVoting.sol +++ b/src/DirectDemocracyVoting.sol @@ -26,10 +26,11 @@ contract DirectDemocracyVoting is Initializable { } enum ConfigKey { - QUORUM, + THRESHOLD, EXECUTOR, TARGET_ALLOWED, - HAT_ALLOWED + HAT_ALLOWED, + QUORUM } /* ─────────── Data Structures ─────────── */ @@ -57,10 +58,11 @@ contract DirectDemocracyVoting is Initializable { mapping(address => bool) allowedTarget; // execution allow‑list uint256[] votingHatIds; // Array of voting hat IDs uint256[] creatorHatIds; // Array of creator hat IDs - uint8 quorumPercentage; // 1‑100 + uint8 thresholdPct; // 1‑100 (min % of support for winning option) Proposal[] _proposals; bool _paused; // Inline pausable state uint256 _lock; // Inline reentrancy guard state + uint32 quorum; // minimum number of voters required (0 = disabled) } bytes32 private constant _STORAGE_SLOT = keccak256("poa.directdemocracy.storage"); @@ -123,7 +125,8 @@ contract DirectDemocracyVoting is Initializable { event ExecutorUpdated(address newExecutor); event TargetAllowed(address target, bool allowed); event ProposalCleaned(uint256 id, uint256 cleaned); - event QuorumPercentageSet(uint8 pct); + event ThresholdPctSet(uint8 pct); + event QuorumSet(uint32 quorum); /* ─────────── Initialiser ─────────── */ /// @custom:oz-upgrades-unsafe-allow constructor @@ -137,20 +140,20 @@ contract DirectDemocracyVoting is Initializable { uint256[] calldata initialHats, uint256[] calldata initialCreatorHats, address[] calldata initialTargets, - uint8 quorumPct + uint8 thresholdPct_ ) external initializer { if (hats_ == address(0) || executor_ == address(0)) { revert VotingErrors.ZeroAddress(); } - VotingMath.validateQuorum(quorumPct); + VotingMath.validateThreshold(thresholdPct_); Layout storage l = _layout(); l.hats = IHats(hats_); l.executor = IExecutor(executor_); - l.quorumPercentage = quorumPct; + l.thresholdPct = thresholdPct_; l._paused = false; // Initialize paused state l._lock = 0; // Initialize reentrancy guard state - emit QuorumPercentageSet(quorumPct); + emit ThresholdPctSet(thresholdPct_); uint256 len = initialHats.length; for (uint256 i; i < len;) { @@ -192,11 +195,11 @@ contract DirectDemocracyVoting is Initializable { function setConfig(ConfigKey key, bytes calldata value) external onlyExecutor { Layout storage l = _layout(); - if (key == ConfigKey.QUORUM) { + if (key == ConfigKey.THRESHOLD) { uint8 q = abi.decode(value, (uint8)); - VotingMath.validateQuorum(q); - l.quorumPercentage = q; - emit QuorumPercentageSet(q); + VotingMath.validateThreshold(q); + l.thresholdPct = q; + emit ThresholdPctSet(q); } else if (key == ConfigKey.EXECUTOR) { address newExecutor = abi.decode(value, (address)); if (newExecutor == address(0)) revert VotingErrors.ZeroAddress(); @@ -214,6 +217,10 @@ contract DirectDemocracyVoting is Initializable { HatManager.setHatInArray(l.creatorHatIds, hat, allowed); } emit HatSet(hatType, hat, allowed); + } else if (key == ConfigKey.QUORUM) { + uint32 q = abi.decode(value, (uint32)); + l.quorum = q; + emit QuorumSet(q); } } @@ -434,33 +441,16 @@ contract DirectDemocracyVoting is Initializable { emit Winner(id, winner, valid); } - /* ─────────── Cleanup ─────────── */ - // function cleanupProposal(uint256 id, address[] calldata voters) external exists(id) isExpired(id) { - // Layout storage l = _layout(); - // Proposal storage p = l._proposals[id]; - // require(p.batches.length > 0 || voters.length > 0, "nothing"); - // uint256 cleaned; - // uint256 len = voters.length; - // for (uint256 i; i < len && i < 4_000;) { - // if (p.hasVoted[voters[i]]) { - // delete p.hasVoted[voters[i]]; - // unchecked { - // ++cleaned; - // } - // } - // unchecked { - // ++i; - // } - // } - // if (cleaned == 0 && p.batches.length > 0) delete p.batches; - // emit ProposalCleaned(id, cleaned); - // } - /* ─────────── View helpers ─────────── */ function _calcWinner(uint256 id) internal view returns (uint256 win, bool ok) { Layout storage l = _layout(); Proposal storage p = l._proposals[id]; + // Check quorum: minimum number of voters required + if (l.quorum > 0 && p.totalWeight / 100 < l.quorum) { + return (0, false); + } + // Build option scores array for VoteCalc uint256 len = p.options.length; uint256[] memory optionScores = new uint256[](len); @@ -475,7 +465,7 @@ contract DirectDemocracyVoting is Initializable { (win, ok,,) = VotingMath.pickWinnerMajority( optionScores, p.totalWeight, - l.quorumPercentage, + l.thresholdPct, true // requireStrictMajority ); } @@ -485,8 +475,12 @@ contract DirectDemocracyVoting is Initializable { return _layout()._proposals.length; } - function quorumPercentage() external view returns (uint8) { - return _layout().quorumPercentage; + function thresholdPct() external view returns (uint8) { + return _layout().thresholdPct; + } + + function quorum() external view returns (uint32) { + return _layout().quorum; } function isTargetAllowed(address target) external view returns (bool) { diff --git a/src/HybridVoting.sol b/src/HybridVoting.sol index 8fa7c9c..9f00e06 100644 --- a/src/HybridVoting.sol +++ b/src/HybridVoting.sol @@ -53,6 +53,7 @@ contract HybridVoting is Initializable { mapping(uint256 => bool) pollHatAllowed; // O(1) lookup for poll hat permission ClassConfig[] classesSnapshot; // Snapshot the class config to freeze semantics for this proposal bool executed; // finalization guard + uint32 voterCount; // number of voters who cast a vote } /* ─────── ERC-7201 Storage ─────── */ @@ -63,13 +64,14 @@ contract HybridVoting is Initializable { IExecutor executor; mapping(address => bool) allowedTarget; // execution allow‑list uint256[] creatorHatIds; // enumeration array for creator hats - uint8 quorumPct; // 1‑100 + uint8 thresholdPct; // 1‑100 (min % of support for winning option) ClassConfig[] classes; // global N-class configuration /* Vote Bookkeeping */ Proposal[] _proposals; /* Inline State */ bool _paused; // Inline pausable state uint256 _lock; // Inline reentrancy guard state + uint32 quorum; // minimum number of voters required (0 = disabled) } bytes32 private constant _STORAGE_SLOT = keccak256("poa.hybridvoting.v2.storage"); @@ -114,7 +116,8 @@ contract HybridVoting is Initializable { event HatSet(HatType hatType, uint256 hat, bool allowed); event TargetAllowed(address target, bool allowed); event ExecutorUpdated(address newExec); - event QuorumSet(uint8 pct); + event ThresholdPctSet(uint8 pct); + event QuorumSet(uint32 quorum); /* ─────── Initialiser ─────── */ /// @custom:oz-upgrades-unsafe-allow constructor @@ -127,14 +130,14 @@ contract HybridVoting is Initializable { address executor_, uint256[] calldata initialCreatorHats, address[] calldata initialTargets, - uint8 quorum_, + uint8 thresholdPct_, ClassConfig[] calldata initialClasses ) external initializer { if (hats_ == address(0) || executor_ == address(0)) { revert VotingErrors.ZeroAddress(); } - VotingMath.validateQuorum(quorum_); + VotingMath.validateThreshold(thresholdPct_); Layout storage l = _layout(); l.hats = IHats(hats_); @@ -142,8 +145,8 @@ contract HybridVoting is Initializable { l._paused = false; // Initialize paused state l._lock = 0; // Initialize reentrancy guard state - l.quorumPct = quorum_; - emit QuorumSet(quorum_); + l.thresholdPct = thresholdPct_; + emit ThresholdPctSet(thresholdPct_); // Initialize creator hats and targets _initializeCreatorHats(initialCreatorHats); @@ -221,19 +224,20 @@ contract HybridVoting is Initializable { /* ─────── Configuration Setters ─────── */ enum ConfigKey { - QUORUM, + THRESHOLD, TARGET_ALLOWED, - EXECUTOR + EXECUTOR, + QUORUM } function setConfig(ConfigKey key, bytes calldata value) external onlyExecutor { Layout storage l = _layout(); - if (key == ConfigKey.QUORUM) { + if (key == ConfigKey.THRESHOLD) { uint8 q = abi.decode(value, (uint8)); - VotingMath.validateQuorum(q); - l.quorumPct = q; - emit QuorumSet(q); + VotingMath.validateThreshold(q); + l.thresholdPct = q; + emit ThresholdPctSet(q); } else if (key == ConfigKey.TARGET_ALLOWED) { (address target, bool allowed) = abi.decode(value, (address, bool)); l.allowedTarget[target] = allowed; @@ -243,6 +247,10 @@ contract HybridVoting is Initializable { if (newExecutor == address(0)) revert VotingErrors.ZeroAddress(); l.executor = IExecutor(newExecutor); emit ExecutorUpdated(newExecutor); + } else if (key == ConfigKey.QUORUM) { + uint32 q = abi.decode(value, (uint32)); + l.quorum = q; + emit QuorumSet(q); } } @@ -311,8 +319,12 @@ contract HybridVoting is Initializable { return _layout()._proposals.length; } - function quorumPct() external view returns (uint8) { - return _layout().quorumPct; + function thresholdPct() external view returns (uint8) { + return _layout().thresholdPct; + } + + function quorum() external view returns (uint32) { + return _layout().quorum; } function isTargetAllowed(address target) external view returns (bool) { diff --git a/src/OrgDeployer.sol b/src/OrgDeployer.sol index 2571bb1..d5c8cb3 100644 --- a/src/OrgDeployer.sol +++ b/src/OrgDeployer.sol @@ -260,8 +260,8 @@ contract OrgDeployer is Initializable { uint256 regNonce; // User's current nonce on the registry bytes regSignature; // User's EIP-712 ECDSA signature for username registration bool autoUpgrade; - uint8 hybridQuorumPct; - uint8 ddQuorumPct; + uint8 hybridThresholdPct; + uint8 ddThresholdPct; IHybridVotingInit.ClassConfig[] hybridClasses; address[] ddInitialTargets; RoleConfigStructs.RoleConfig[] roles; // Complete role configuration (replaces roleNames, roleImages, roleCanVote) @@ -630,8 +630,8 @@ contract OrgDeployer is Initializable { govParams.regNonce = params.regNonce; govParams.regSignature = params.regSignature; govParams.autoUpgrade = params.autoUpgrade; - govParams.hybridQuorumPct = params.hybridQuorumPct; - govParams.ddQuorumPct = params.ddQuorumPct; + govParams.hybridThresholdPct = params.hybridThresholdPct; + govParams.ddThresholdPct = params.ddThresholdPct; govParams.hybridClasses = params.hybridClasses; govParams.hybridProposalCreatorRolesBitmap = params.roleAssignments.hybridProposalCreatorRolesBitmap; govParams.ddVotingRolesBitmap = params.roleAssignments.ddVotingRolesBitmap; @@ -665,8 +665,8 @@ contract OrgDeployer is Initializable { votingParams.deployerAddress = params.deployerAddress; votingParams.participationToken = participationToken; votingParams.autoUpgrade = params.autoUpgrade; - votingParams.hybridQuorumPct = params.hybridQuorumPct; - votingParams.ddQuorumPct = params.ddQuorumPct; + votingParams.hybridThresholdPct = params.hybridThresholdPct; + votingParams.ddThresholdPct = params.ddThresholdPct; votingParams.hybridClasses = params.hybridClasses; votingParams.hybridProposalCreatorRolesBitmap = params.roleAssignments.hybridProposalCreatorRolesBitmap; votingParams.ddVotingRolesBitmap = params.roleAssignments.ddVotingRolesBitmap; diff --git a/src/factories/GovernanceFactory.sol b/src/factories/GovernanceFactory.sol index f639250..99b5829 100644 --- a/src/factories/GovernanceFactory.sol +++ b/src/factories/GovernanceFactory.sol @@ -79,8 +79,8 @@ contract GovernanceFactory { uint256 regNonce; // User's current nonce on the registry bytes regSignature; // User's EIP-712 ECDSA signature for username registration bool autoUpgrade; - uint8 hybridQuorumPct; // Quorum for HybridVoting - uint8 ddQuorumPct; // Quorum for DirectDemocracyVoting + uint8 hybridThresholdPct; // Support threshold for HybridVoting + uint8 ddThresholdPct; // Support threshold for DirectDemocracyVoting IHybridVotingInit.ClassConfig[] hybridClasses; // Voting class configuration uint256 hybridProposalCreatorRolesBitmap; // Bit N set = Role N can create proposals uint256 ddVotingRolesBitmap; // Bit N set = Role N can vote in polls @@ -265,7 +265,7 @@ contract GovernanceFactory { }); hybridVoting = ModuleDeploymentLib.deployHybridVoting( - config, executor, creatorHats, params.hybridQuorumPct, finalClasses, hybridBeacon + config, executor, creatorHats, params.hybridThresholdPct, finalClasses, hybridBeacon ); } @@ -295,7 +295,7 @@ contract GovernanceFactory { }); directDemocracyVoting = ModuleDeploymentLib.deployDirectDemocracyVoting( - config, executor, votingHats, creatorHats, params.ddInitialTargets, params.ddQuorumPct, ddBeacon + config, executor, votingHats, creatorHats, params.ddInitialTargets, params.ddThresholdPct, ddBeacon ); } diff --git a/src/lens/DirectDemocracyVotingLens.sol b/src/lens/DirectDemocracyVotingLens.sol index e955895..873b979 100644 --- a/src/lens/DirectDemocracyVotingLens.sol +++ b/src/lens/DirectDemocracyVotingLens.sol @@ -37,11 +37,12 @@ contract DirectDemocracyVotingLens { function getGovernanceConfig(DirectDemocracyVoting voting) external view - returns (address executor, address hats, uint8 quorumPercentage, uint256 proposalCount) + returns (address executor, address hats, uint8 thresholdPct, uint32 quorum, uint256 proposalCount) { executor = voting.executor(); hats = voting.hats(); - quorumPercentage = voting.quorumPercentage(); + thresholdPct = voting.thresholdPct(); + quorum = voting.quorum(); proposalCount = voting.proposalsCount(); } } diff --git a/src/libs/HybridVotingCore.sol b/src/libs/HybridVotingCore.sol index f5001a8..1e55bec 100644 --- a/src/libs/HybridVotingCore.sol +++ b/src/libs/HybridVotingCore.sol @@ -97,6 +97,9 @@ library HybridVotingCore { } p.hasVoted[voter] = true; + unchecked { + p.voterCount++; + } emit VoteCast(id, voter, idxs, weights, classRawPowers, uint64(block.timestamp)); } @@ -158,6 +161,12 @@ library HybridVotingCore { return (0, false); } + // Check quorum: minimum number of voters required + if (l.quorum > 0 && p.voterCount < l.quorum) { + emit Winner(id, 0, false, false, uint64(block.timestamp)); + return (0, false); + } + // Build matrix for N-class winner calculation uint256 numOptions = p.options.length; uint256 numClasses = p.classesSnapshot.length; @@ -189,7 +198,7 @@ library HybridVotingCore { perOptionPerClassRaw, p.classTotalsRaw, slices, - l.quorumPct, + l.thresholdPct, true // strict majority required ); diff --git a/src/libs/ModuleDeploymentLib.sol b/src/libs/ModuleDeploymentLib.sol index 87e03dd..99d933d 100644 --- a/src/libs/ModuleDeploymentLib.sol +++ b/src/libs/ModuleDeploymentLib.sol @@ -31,7 +31,7 @@ interface IHybridVotingInit { address executor_, uint256[] calldata initialCreatorHats, address[] calldata targets, - uint8 quorumPct, + uint8 thresholdPct, ClassConfig[] calldata initialClasses ) external; } @@ -242,7 +242,7 @@ library ModuleDeploymentLib { DeployConfig memory config, address executorAddr, uint256[] memory creatorHats, - uint8 quorumPct, + uint8 thresholdPct, IHybridVotingInit.ClassConfig[] memory classes, address beacon ) internal returns (address hvProxy) { @@ -251,7 +251,13 @@ library ModuleDeploymentLib { address[] memory targets = new address[](0); bytes memory init = abi.encodeWithSelector( - IHybridVotingInit.initialize.selector, config.hats, executorAddr, creatorHats, targets, quorumPct, classes + IHybridVotingInit.initialize.selector, + config.hats, + executorAddr, + creatorHats, + targets, + thresholdPct, + classes ); hvProxy = deployCore(config, ModuleTypes.HYBRID_VOTING_ID, init, beacon); } @@ -270,7 +276,7 @@ library ModuleDeploymentLib { uint256[] memory votingHats, uint256[] memory creatorHats, address[] memory initialTargets, - uint8 quorumPct, + uint8 thresholdPct, address beacon ) internal returns (address ddProxy) { bytes memory init = abi.encodeWithSignature( @@ -280,7 +286,7 @@ library ModuleDeploymentLib { votingHats, creatorHats, initialTargets, - quorumPct + thresholdPct ); ddProxy = deployCore(config, ModuleTypes.DIRECT_DEMOCRACY_VOTING_ID, init, beacon); } diff --git a/src/libs/VotingErrors.sol b/src/libs/VotingErrors.sol index 935b07a..e4f8d6f 100644 --- a/src/libs/VotingErrors.sol +++ b/src/libs/VotingErrors.sol @@ -22,6 +22,7 @@ library VotingErrors { error TargetSelf(); error InvalidTarget(); error EmptyBatch(); + error InvalidThreshold(); error InvalidQuorum(); error Paused(); error Overflow(); diff --git a/src/libs/VotingMath.sol b/src/libs/VotingMath.sol index d7df51b..699474a 100644 --- a/src/libs/VotingMath.sol +++ b/src/libs/VotingMath.sol @@ -8,7 +8,7 @@ pragma solidity ^0.8.20; */ library VotingMath { /* ─────────── Errors ─────────── */ - error InvalidQuorum(); + error InvalidThreshold(); error InvalidSplit(); error InvalidMinBalance(); error MinBalanceNotMet(uint256 required); @@ -35,11 +35,11 @@ library VotingMath { /* ─────────── Validation Functions ─────────── */ /** - * @notice Validate quorum percentage - * @param quorum Quorum percentage (1-100) + * @notice Validate threshold percentage + * @param threshold Threshold percentage (1-100) */ - function validateQuorum(uint8 quorum) internal pure { - if (quorum == 0 || quorum > 100) revert InvalidQuorum(); + function validateThreshold(uint8 threshold) internal pure { + if (threshold == 0 || threshold > 100) revert InvalidThreshold(); } /** @@ -251,39 +251,39 @@ library VotingMath { } } - /* ─────────── Winner & Quorum Functions ─────────── */ + /* ─────────── Winner & Threshold Functions ─────────── */ /** - * @notice Check if a proposal meets quorum requirements (legacy) + * @notice Check if a proposal meets threshold requirements (legacy) * @param highestVote Highest vote count * @param secondHighest Second highest vote count * @param totalWeight Total voting weight - * @param quorumPercentage Required quorum percentage - * @return valid Whether the proposal meets quorum + * @param thresholdPct Required support threshold percentage + * @return valid Whether the proposal meets threshold */ - function meetsQuorum(uint256 highestVote, uint256 secondHighest, uint256 totalWeight, uint8 quorumPercentage) + function meetsThreshold(uint256 highestVote, uint256 secondHighest, uint256 totalWeight, uint8 thresholdPct) internal pure returns (bool valid) { - return (highestVote * 100 >= totalWeight * quorumPercentage) && (highestVote > secondHighest); + return (highestVote * 100 >= totalWeight * thresholdPct) && (highestVote > secondHighest); } /** * @notice Determine winner using majority rules * @param optionScores Per-option vote totals * @param totalWeight Total voting weight (e.g., sum power or voters*100) - * @param quorumPct Required quorum percentage (1-100) + * @param thresholdPct Required support threshold percentage (1-100) * @param requireStrictMajority Whether winner must strictly exceed second place * @return win Winning option index - * @return ok Whether quorum is met and winner is valid + * @return ok Whether threshold is met and winner is valid * @return hi Highest score * @return second Second highest score */ function pickWinnerMajority( uint256[] memory optionScores, uint256 totalWeight, - uint8 quorumPct, + uint8 thresholdPct, bool requireStrictMajority ) internal pure returns (uint256 win, bool ok, uint256 hi, uint256 second) { uint256 len = optionScores.length; @@ -301,11 +301,10 @@ library VotingMath { if (hi == 0) return (win, false, hi, second); - // Threshold check: hi * 100 >= totalWeight * quorumPct - bool quorumMet = (hi * 100 >= totalWeight * quorumPct); + bool thresholdMet = (hi * 100 >= totalWeight * thresholdPct); bool meetsMargin = requireStrictMajority ? (hi > second) : (hi >= second); - ok = quorumMet && meetsMargin; + ok = thresholdMet && meetsMargin; } /** @@ -315,9 +314,9 @@ library VotingMath { * @param ddTotalRaw Total DD raw votes * @param ptTotalRaw Total PT raw votes * @param ddSharePct DD share percentage (e.g., 50 = 50%) - * @param quorumPct Required quorum percentage (1-100) + * @param thresholdPct Required support threshold percentage (1-100) * @return win Winning option index - * @return ok Whether quorum is met and winner is valid + * @return ok Whether threshold is met and winner is valid * @return hi Highest combined score * @return second Second highest combined score */ @@ -327,7 +326,7 @@ library VotingMath { uint256 ddTotalRaw, uint256 ptTotalRaw, uint8 ddSharePct, - uint8 quorumPct + uint8 thresholdPct ) internal pure returns (uint256 win, bool ok, uint256 hi, uint256 second) { if (ddTotalRaw == 0 && ptTotalRaw == 0) return (0, false, 0, 0); @@ -349,9 +348,9 @@ library VotingMath { } } - // Quorum interpreted on the final scaled total (max 100) + // Threshold on the final scaled total (max 100) // Requires strict margin for hybrid voting - ok = (hi > second) && (hi >= quorumPct); + ok = (hi > second) && (hi >= thresholdPct); } /** @@ -359,10 +358,10 @@ library VotingMath { * @param perOptionPerClassRaw [option][class] raw vote matrix * @param totalsRaw [class] total raw votes per class * @param slices [class] slice percentages (must sum to 100) - * @param quorumPct Required quorum percentage (1-100) + * @param thresholdPct Required support threshold percentage (1-100) * @param strict Whether to require strict majority (winner > second) * @return win Winning option index - * @return ok Whether quorum is met and winner is valid + * @return ok Whether threshold is met and winner is valid * @return hi Highest combined score * @return second Second highest combined score */ @@ -370,7 +369,7 @@ library VotingMath { uint256[][] memory perOptionPerClassRaw, uint256[] memory totalsRaw, uint8[] memory slices, - uint8 quorumPct, + uint8 thresholdPct, bool strict ) internal pure returns (uint256 win, bool ok, uint256 hi, uint256 second) { uint256 numOptions = perOptionPerClassRaw.length; @@ -400,10 +399,10 @@ library VotingMath { } } - // Check quorum and margin requirements - bool quorumMet = hi >= quorumPct; + // Check threshold and margin requirements + bool thresholdMet = hi >= thresholdPct; bool meetsMargin = strict ? (hi > second) : (hi >= second); - ok = quorumMet && meetsMargin; + ok = thresholdMet && meetsMargin; } /** @@ -411,7 +410,7 @@ library VotingMath { * @param slices Array of slice percentages */ function validateClassSlices(uint8[] memory slices) internal pure { - if (slices.length == 0) revert InvalidQuorum(); + if (slices.length == 0) revert InvalidSplit(); uint256 sum; for (uint256 i; i < slices.length; ++i) { if (slices[i] == 0 || slices[i] > 100) revert InvalidSplit(); diff --git a/test/DeployerTest.t.sol b/test/DeployerTest.t.sol index f02b2c0..0006253 100644 --- a/test/DeployerTest.t.sol +++ b/test/DeployerTest.t.sol @@ -174,8 +174,8 @@ contract DeployerTest is Test, IEligibilityModuleEvents { regNonce: 0, regSignature: "", autoUpgrade: true, - hybridQuorumPct: 50, - ddQuorumPct: 50, + hybridThresholdPct: 50, + ddThresholdPct: 50, hybridClasses: classes, ddInitialTargets: ddTargets, roles: _buildSimpleRoleConfigs(names, images, voting), @@ -430,8 +430,8 @@ contract DeployerTest is Test, IEligibilityModuleEvents { regNonce: 0, regSignature: "", autoUpgrade: true, - hybridQuorumPct: 50, - ddQuorumPct: 50, + hybridThresholdPct: 50, + ddThresholdPct: 50, hybridClasses: classes, ddInitialTargets: ddTargets, roles: _buildSimpleRoleConfigs(names, images, voting), @@ -491,8 +491,8 @@ contract DeployerTest is Test, IEligibilityModuleEvents { regNonce: 0, regSignature: "", autoUpgrade: true, - hybridQuorumPct: 50, - ddQuorumPct: 50, + hybridThresholdPct: 50, + ddThresholdPct: 50, hybridClasses: classes, ddInitialTargets: ddTargets, roles: _buildSimpleRoleConfigs(names, images, voting), @@ -856,8 +856,8 @@ contract DeployerTest is Test, IEligibilityModuleEvents { regNonce: 0, regSignature: "", autoUpgrade: true, - hybridQuorumPct: 50, - ddQuorumPct: 50, + hybridThresholdPct: 50, + ddThresholdPct: 50, hybridClasses: classes, ddInitialTargets: ddTargets, roles: _buildSimpleRoleConfigs(names, images, voting), @@ -922,7 +922,7 @@ contract DeployerTest is Test, IEligibilityModuleEvents { vm.warp(block.timestamp + 61 minutes); (uint256 winner, bool valid) = HybridVoting(hybridProxy).announceWinner(0); - assertTrue(valid, "quorum not reached"); + assertTrue(valid, "threshold not reached"); assertEq(winner, 0, "YES should win"); } @@ -989,8 +989,8 @@ contract DeployerTest is Test, IEligibilityModuleEvents { regNonce: 0, regSignature: "", autoUpgrade: true, - hybridQuorumPct: 50, - ddQuorumPct: 50, + hybridThresholdPct: 50, + ddThresholdPct: 50, hybridClasses: classes, ddInitialTargets: ddTargets, roles: _buildSimpleRoleConfigs(names, images, voting), @@ -1076,8 +1076,8 @@ contract DeployerTest is Test, IEligibilityModuleEvents { regNonce: 0, regSignature: "", autoUpgrade: true, - hybridQuorumPct: 50, - ddQuorumPct: 50, + hybridThresholdPct: 50, + ddThresholdPct: 50, hybridClasses: classes, ddInitialTargets: ddTargets, roles: _buildSimpleRoleConfigs(names, images, voting), @@ -1120,8 +1120,8 @@ contract DeployerTest is Test, IEligibilityModuleEvents { regNonce: 0, regSignature: "", autoUpgrade: true, - hybridQuorumPct: 50, - ddQuorumPct: 50, + hybridThresholdPct: 50, + ddThresholdPct: 50, hybridClasses: classes, ddInitialTargets: ddTargets, roles: _buildSimpleRoleConfigs(names, images, voting), @@ -1207,8 +1207,8 @@ contract DeployerTest is Test, IEligibilityModuleEvents { regNonce: 0, regSignature: "", autoUpgrade: true, - hybridQuorumPct: 50, - ddQuorumPct: 50, + hybridThresholdPct: 50, + ddThresholdPct: 50, hybridClasses: classes, ddInitialTargets: ddTargets, roles: _buildSimpleRoleConfigs(names, images, voting), @@ -1481,8 +1481,8 @@ contract DeployerTest is Test, IEligibilityModuleEvents { regNonce: 0, regSignature: "", autoUpgrade: true, - hybridQuorumPct: 50, - ddQuorumPct: 50, + hybridThresholdPct: 50, + ddThresholdPct: 50, hybridClasses: classes, ddInitialTargets: ddTargets, roles: _buildSimpleRoleConfigs(names, images, voting), @@ -1699,8 +1699,8 @@ contract DeployerTest is Test, IEligibilityModuleEvents { regNonce: 0, regSignature: "", autoUpgrade: true, - hybridQuorumPct: 50, - ddQuorumPct: 50, + hybridThresholdPct: 50, + ddThresholdPct: 50, hybridClasses: classes, ddInitialTargets: ddTargets, roles: _buildSimpleRoleConfigs(names, images, voting), @@ -1865,8 +1865,8 @@ contract DeployerTest is Test, IEligibilityModuleEvents { regNonce: 0, regSignature: "", autoUpgrade: true, - hybridQuorumPct: 50, - ddQuorumPct: 50, + hybridThresholdPct: 50, + ddThresholdPct: 50, hybridClasses: classes, ddInitialTargets: ddTargets, roles: _buildSimpleRoleConfigs(names, images, voting), @@ -2005,8 +2005,8 @@ contract DeployerTest is Test, IEligibilityModuleEvents { regNonce: 0, regSignature: "", autoUpgrade: true, - hybridQuorumPct: 50, - ddQuorumPct: 50, + hybridThresholdPct: 50, + ddThresholdPct: 50, hybridClasses: new IHybridVotingInit.ClassConfig[](0), ddInitialTargets: new address[](0), roles: emptyRoles, @@ -2172,8 +2172,8 @@ contract DeployerTest is Test, IEligibilityModuleEvents { // deployerAddress: orgOwner, // deployerUsername: "", // autoUpgrade: true, - // hybridQuorumPct: 50, - // ddQuorumPct: 50, + // hybridThresholdPct: 50, + // ddThresholdPct: 50, // hybridClasses: classes, // ddInitialTargets: ddTargets, // roles: complexRoles, @@ -2232,8 +2232,8 @@ contract DeployerTest is Test, IEligibilityModuleEvents { // deployerAddress: orgOwner, // deployerUsername: "", // autoUpgrade: true, - // hybridQuorumPct: 50, - // ddQuorumPct: 50, + // hybridThresholdPct: 50, + // ddThresholdPct: 50, // hybridClasses: classes, // ddInitialTargets: ddTargets, // roles: multiWearerRoles, @@ -2284,8 +2284,8 @@ contract DeployerTest is Test, IEligibilityModuleEvents { regNonce: 0, regSignature: "", autoUpgrade: true, - hybridQuorumPct: 50, - ddQuorumPct: 50, + hybridThresholdPct: 50, + ddThresholdPct: 50, hybridClasses: classes, ddInitialTargets: ddTargets, roles: _buildSimpleRoleConfigs(names, images, voting), @@ -2393,8 +2393,8 @@ contract DeployerTest is Test, IEligibilityModuleEvents { regNonce: 0, regSignature: "", autoUpgrade: true, - hybridQuorumPct: 50, - ddQuorumPct: 50, + hybridThresholdPct: 50, + ddThresholdPct: 50, hybridClasses: classes, ddInitialTargets: ddTargets, roles: _buildSimpleRoleConfigs(names, images, voting), @@ -2563,8 +2563,8 @@ contract DeployerTest is Test, IEligibilityModuleEvents { regNonce: 0, regSignature: "", autoUpgrade: true, - hybridQuorumPct: 50, - ddQuorumPct: 50, + hybridThresholdPct: 50, + ddThresholdPct: 50, hybridClasses: classes, ddInitialTargets: ddTargets, roles: _buildSimpleRoleConfigs(names, images, voting), @@ -3550,8 +3550,8 @@ contract DeployerTest is Test, IEligibilityModuleEvents { regNonce: 0, regSignature: "", autoUpgrade: true, - hybridQuorumPct: 50, - ddQuorumPct: 50, + hybridThresholdPct: 50, + ddThresholdPct: 50, hybridClasses: classes, ddInitialTargets: ddTargets, roles: roles, @@ -3654,8 +3654,8 @@ contract DeployerTest is Test, IEligibilityModuleEvents { regNonce: 0, regSignature: "", autoUpgrade: true, - hybridQuorumPct: 50, - ddQuorumPct: 50, + hybridThresholdPct: 50, + ddThresholdPct: 50, hybridClasses: classes, ddInitialTargets: ddTargets, roles: _buildSimpleRoleConfigs(names, images, voting), @@ -3719,8 +3719,8 @@ contract DeployerTest is Test, IEligibilityModuleEvents { regNonce: 0, regSignature: "", autoUpgrade: true, - hybridQuorumPct: 50, - ddQuorumPct: 50, + hybridThresholdPct: 50, + ddThresholdPct: 50, hybridClasses: classes, ddInitialTargets: ddTargets, roles: _buildSimpleRoleConfigs(names, images, voting), @@ -3786,8 +3786,8 @@ contract DeployerTest is Test, IEligibilityModuleEvents { regNonce: 0, regSignature: "", autoUpgrade: true, - hybridQuorumPct: 50, - ddQuorumPct: 50, + hybridThresholdPct: 50, + ddThresholdPct: 50, hybridClasses: classes, ddInitialTargets: ddTargets, roles: _buildSimpleRoleConfigs(names, images, voting), @@ -4222,8 +4222,8 @@ contract DeployerTest is Test, IEligibilityModuleEvents { regNonce: nonce, regSignature: sig, autoUpgrade: true, - hybridQuorumPct: 50, - ddQuorumPct: 50, + hybridThresholdPct: 50, + ddThresholdPct: 50, hybridClasses: _buildLegacyClasses(50, 50, false, 4 ether), ddInitialTargets: new address[](0), roles: _buildSimpleRoleConfigs(names, images, canVote), @@ -4268,8 +4268,8 @@ contract DeployerTest is Test, IEligibilityModuleEvents { regNonce: 0, regSignature: "", autoUpgrade: true, - hybridQuorumPct: 50, - ddQuorumPct: 50, + hybridThresholdPct: 50, + ddThresholdPct: 50, hybridClasses: _buildLegacyClasses(50, 50, false, 4 ether), ddInitialTargets: new address[](0), roles: _buildSimpleRoleConfigs(names, images, canVote), @@ -4325,8 +4325,8 @@ contract DeployerTest is Test, IEligibilityModuleEvents { regNonce: nonce, regSignature: sig, autoUpgrade: true, - hybridQuorumPct: 50, - ddQuorumPct: 50, + hybridThresholdPct: 50, + ddThresholdPct: 50, hybridClasses: _buildLegacyClasses(50, 50, false, 4 ether), ddInitialTargets: new address[](0), roles: _buildSimpleRoleConfigs(names, images, canVote), @@ -4378,8 +4378,8 @@ contract DeployerTest is Test, IEligibilityModuleEvents { regNonce: 0, regSignature: "", autoUpgrade: true, - hybridQuorumPct: 50, - ddQuorumPct: 50, + hybridThresholdPct: 50, + ddThresholdPct: 50, hybridClasses: _buildLegacyClasses(50, 50, false, 4 ether), ddInitialTargets: new address[](0), roles: _buildSimpleRoleConfigs(names, images, canVote), @@ -4429,8 +4429,8 @@ contract DeployerTest is Test, IEligibilityModuleEvents { regNonce: 0, regSignature: "", autoUpgrade: true, - hybridQuorumPct: 50, - ddQuorumPct: 50, + hybridThresholdPct: 50, + ddThresholdPct: 50, hybridClasses: _buildLegacyClasses(50, 50, false, 4 ether), ddInitialTargets: new address[](0), roles: _buildSimpleRoleConfigs(names, images, canVote), @@ -4484,8 +4484,8 @@ contract DeployerTest is Test, IEligibilityModuleEvents { regNonce: 0, regSignature: "", autoUpgrade: true, - hybridQuorumPct: 50, - ddQuorumPct: 50, + hybridThresholdPct: 50, + ddThresholdPct: 50, hybridClasses: _buildLegacyClasses(50, 50, false, 4 ether), ddInitialTargets: new address[](0), roles: _buildSimpleRoleConfigs(names, images, canVote), @@ -4533,8 +4533,8 @@ contract DeployerTest is Test, IEligibilityModuleEvents { regNonce: 0, regSignature: "", autoUpgrade: true, - hybridQuorumPct: 50, - ddQuorumPct: 50, + hybridThresholdPct: 50, + ddThresholdPct: 50, hybridClasses: _buildLegacyClasses(50, 50, false, 4 ether), ddInitialTargets: new address[](0), roles: _buildSimpleRoleConfigs(names, images, canVote), @@ -4584,8 +4584,8 @@ contract DeployerTest is Test, IEligibilityModuleEvents { regNonce: 0, regSignature: "", autoUpgrade: true, - hybridQuorumPct: 50, - ddQuorumPct: 50, + hybridThresholdPct: 50, + ddThresholdPct: 50, hybridClasses: classes, ddInitialTargets: ddTargets, roles: _buildSimpleRoleConfigs(names, images, voting), @@ -4651,8 +4651,8 @@ contract DeployerTest is Test, IEligibilityModuleEvents { regNonce: 0, regSignature: "", autoUpgrade: true, - hybridQuorumPct: 50, - ddQuorumPct: 50, + hybridThresholdPct: 50, + ddThresholdPct: 50, hybridClasses: classes, ddInitialTargets: ddTargets, roles: _buildSimpleRoleConfigs(names, images, voting), @@ -4745,8 +4745,8 @@ contract DeployerTest is Test, IEligibilityModuleEvents { regNonce: 0, regSignature: "", autoUpgrade: true, - hybridQuorumPct: 50, - ddQuorumPct: 50, + hybridThresholdPct: 50, + ddThresholdPct: 50, hybridClasses: classes, ddInitialTargets: ddTargets, roles: _buildSimpleRoleConfigs(names, images, voting), @@ -4799,8 +4799,8 @@ contract DeployerTest is Test, IEligibilityModuleEvents { regNonce: 0, regSignature: "", autoUpgrade: true, - hybridQuorumPct: 50, - ddQuorumPct: 50, + hybridThresholdPct: 50, + ddThresholdPct: 50, hybridClasses: classes, ddInitialTargets: ddTargets, roles: _buildSimpleRoleConfigs(names, images, voting), @@ -4869,8 +4869,8 @@ contract DeployerTest is Test, IEligibilityModuleEvents { regNonce: 0, regSignature: "", autoUpgrade: true, - hybridQuorumPct: 50, - ddQuorumPct: 50, + hybridThresholdPct: 50, + ddThresholdPct: 50, hybridClasses: classes, ddInitialTargets: ddTargets, roles: _buildSimpleRoleConfigs(names, images, voting), @@ -4936,8 +4936,8 @@ contract DeployerTest is Test, IEligibilityModuleEvents { regNonce: 0, regSignature: "", autoUpgrade: true, - hybridQuorumPct: 50, - ddQuorumPct: 50, + hybridThresholdPct: 50, + ddThresholdPct: 50, hybridClasses: classes, ddInitialTargets: ddTargets, roles: _buildSimpleRoleConfigs(names, images, voting), @@ -5012,8 +5012,8 @@ contract DeployerTest is Test, IEligibilityModuleEvents { regNonce: 0, regSignature: "", autoUpgrade: true, - hybridQuorumPct: 50, - ddQuorumPct: 50, + hybridThresholdPct: 50, + ddThresholdPct: 50, hybridClasses: classes, ddInitialTargets: ddTargets, roles: _buildSimpleRoleConfigs(names, images, voting), @@ -5213,8 +5213,8 @@ contract DeployerTest is Test, IEligibilityModuleEvents { regNonce: 0, regSignature: "", autoUpgrade: true, - hybridQuorumPct: 50, - ddQuorumPct: 50, + hybridThresholdPct: 50, + ddThresholdPct: 50, hybridClasses: classes, ddInitialTargets: ddTargets, roles: _buildSimpleRoleConfigs(names, images, voting), @@ -5288,8 +5288,8 @@ contract DeployerTest is Test, IEligibilityModuleEvents { regNonce: 0, regSignature: "", autoUpgrade: true, - hybridQuorumPct: 50, - ddQuorumPct: 50, + hybridThresholdPct: 50, + ddThresholdPct: 50, hybridClasses: classes, ddInitialTargets: ddTargets, roles: _buildSimpleRoleConfigs(names, images, voting), @@ -5357,8 +5357,8 @@ contract DeployerTest is Test, IEligibilityModuleEvents { regNonce: 0, regSignature: "", autoUpgrade: true, - hybridQuorumPct: 50, - ddQuorumPct: 50, + hybridThresholdPct: 50, + ddThresholdPct: 50, hybridClasses: classes, ddInitialTargets: ddTargets, roles: _buildSimpleRoleConfigs(names, images, voting), @@ -5425,8 +5425,8 @@ contract DeployerTest is Test, IEligibilityModuleEvents { regNonce: 0, regSignature: "", autoUpgrade: true, - hybridQuorumPct: 50, - ddQuorumPct: 50, + hybridThresholdPct: 50, + ddThresholdPct: 50, hybridClasses: classes, ddInitialTargets: ddTargets, roles: _buildSimpleRoleConfigs(names, images, voting), @@ -5664,8 +5664,8 @@ contract DeployerTest is Test, IEligibilityModuleEvents { regNonce: 0, regSignature: "", autoUpgrade: true, - hybridQuorumPct: 50, - ddQuorumPct: 50, + hybridThresholdPct: 50, + ddThresholdPct: 50, hybridClasses: _buildLegacyClasses(50, 50, false, 4 ether), ddInitialTargets: new address[](0), roles: _buildSimpleRoleConfigs(names, images, voting), diff --git a/test/DirectDemocracyVoting.t.sol b/test/DirectDemocracyVoting.t.sol index 1ef2528..f4df5ef 100644 --- a/test/DirectDemocracyVoting.t.sol +++ b/test/DirectDemocracyVoting.t.sol @@ -86,13 +86,13 @@ contract DDVotingTest is Test { new ERC1967Proxy(address(impl), data); } - function testInitializeBadQuorum() public { + function testInitializeBadThreshold() public { DirectDemocracyVoting impl = new DirectDemocracyVoting(); bytes memory data = abi.encodeCall( DirectDemocracyVoting.initialize, (address(hats), address(exec), new uint256[](0), new uint256[](0), new address[](0), 0) ); - vm.expectRevert(VotingMath.InvalidQuorum.selector); + vm.expectRevert(VotingMath.InvalidThreshold.selector); new ERC1967Proxy(address(impl), data); } @@ -209,21 +209,21 @@ contract DDVotingTest is Test { assertTrue(dd.isTargetAllowed(tgt)); } - function testSetQuorum() public { + function testSetThreshold() public { vm.prank(address(exec)); - dd.setConfig(DirectDemocracyVoting.ConfigKey.QUORUM, abi.encode(80)); - assertEq(dd.quorumPercentage(), 80); + dd.setConfig(DirectDemocracyVoting.ConfigKey.THRESHOLD, abi.encode(80)); + assertEq(dd.thresholdPct(), 80); } - function testSetQuorumBad() public { + function testSetThresholdBad() public { vm.prank(address(exec)); - vm.expectRevert(VotingMath.InvalidQuorum.selector); - dd.setConfig(DirectDemocracyVoting.ConfigKey.QUORUM, abi.encode(0)); + vm.expectRevert(VotingMath.InvalidThreshold.selector); + dd.setConfig(DirectDemocracyVoting.ConfigKey.THRESHOLD, abi.encode(0)); } - function testSetQuorumUnauthorized() public { + function testSetThresholdUnauthorized() public { vm.expectRevert(VotingErrors.Unauthorized.selector); - dd.setConfig(DirectDemocracyVoting.ConfigKey.QUORUM, abi.encode(80)); + dd.setConfig(DirectDemocracyVoting.ConfigKey.THRESHOLD, abi.encode(80)); } function testCreateProposalBasic() public { @@ -613,6 +613,129 @@ contract DDVotingTest is Test { assertEq(hats.balanceOf(bob, managerHatId), 0, "Bob should not have manager hat"); } + /*//////////////////////////////////////////////////////////// + QUORUM TESTS + ////////////////////////////////////////////////////////////*/ + + function testSetQuorum() public { + assertEq(dd.quorum(), 0, "Default quorum should be 0"); + vm.prank(address(exec)); + dd.setConfig(DirectDemocracyVoting.ConfigKey.QUORUM, abi.encode(uint32(5))); + assertEq(dd.quorum(), 5, "Quorum should be 5"); + } + + function testSetQuorumEmitsEvent() public { + vm.prank(address(exec)); + vm.expectEmit(true, true, true, true); + emit DirectDemocracyVoting.QuorumSet(uint32(5)); + dd.setConfig(DirectDemocracyVoting.ConfigKey.QUORUM, abi.encode(uint32(5))); + } + + function testSetQuorumUnauthorized() public { + vm.expectRevert(VotingErrors.Unauthorized.selector); + dd.setConfig(DirectDemocracyVoting.ConfigKey.QUORUM, abi.encode(uint32(5))); + } + + function testQuorumNotMet() public { + // Set quorum to 3 voters + vm.prank(address(exec)); + dd.setConfig(DirectDemocracyVoting.ConfigKey.QUORUM, abi.encode(uint32(3))); + + // Create proposal and have only 1 voter vote + uint256 id = _createSimple(2); + uint8[] memory idx = new uint8[](1); + idx[0] = 0; + uint8[] memory w = new uint8[](1); + w[0] = 100; + vm.prank(voter); + dd.vote(id, idx, w); + + // Announce winner - should be invalid due to quorum not met + vm.warp(block.timestamp + 11 minutes); + (uint256 winner, bool valid) = dd.announceWinner(id); + assertFalse(valid, "Should be invalid when quorum not met"); + assertEq(winner, 0, "Winner should be 0 when quorum not met"); + } + + function testQuorumMet() public { + // Set quorum to 2 voters + vm.prank(address(exec)); + dd.setConfig(DirectDemocracyVoting.ConfigKey.QUORUM, abi.encode(uint32(2))); + + // Create proposal and have 2 voters vote + uint256 id = _createSimple(2); + uint8[] memory idx = new uint8[](1); + idx[0] = 0; + uint8[] memory w = new uint8[](1); + w[0] = 100; + + vm.prank(creator); + dd.vote(id, idx, w); + vm.prank(voter); + dd.vote(id, idx, w); + + // Announce winner - should be valid since quorum met + vm.warp(block.timestamp + 11 minutes); + (uint256 winner, bool valid) = dd.announceWinner(id); + assertTrue(valid, "Should be valid when quorum met"); + assertEq(winner, 0, "Option 0 should win"); + } + + function testQuorumDisabledByDefault() public { + // Default quorum is 0, so even 1 voter should work + uint256 id = _createSimple(2); + uint8[] memory idx = new uint8[](1); + idx[0] = 0; + uint8[] memory w = new uint8[](1); + w[0] = 100; + vm.prank(voter); + dd.vote(id, idx, w); + + vm.warp(block.timestamp + 11 minutes); + (uint256 winner, bool valid) = dd.announceWinner(id); + assertTrue(valid, "Should be valid with quorum disabled (0)"); + } + + function testQuorumPassesButThresholdFails() public { + // Set quorum=1 and threshold=100% + vm.prank(address(exec)); + dd.setConfig(DirectDemocracyVoting.ConfigKey.QUORUM, abi.encode(uint32(1))); + vm.prank(address(exec)); + dd.setConfig(DirectDemocracyVoting.ConfigKey.THRESHOLD, abi.encode(uint8(100))); + + // Create 2-option proposal, split vote 50/50 between 2 voters + uint256 id = _createSimple(2); + uint8[] memory idx0 = new uint8[](1); + idx0[0] = 0; + uint8[] memory w0 = new uint8[](1); + w0[0] = 100; + vm.prank(creator); + dd.vote(id, idx0, w0); + + uint8[] memory idx1 = new uint8[](1); + idx1[0] = 1; + uint8[] memory w1 = new uint8[](1); + w1[0] = 100; + vm.prank(voter); + dd.vote(id, idx1, w1); + + vm.warp(block.timestamp + 11 minutes); + (uint256 winner, bool valid) = dd.announceWinner(id); + // Quorum met (2 >= 1) but threshold not met (50% < 100%) + assertFalse(valid, "Should fail threshold even though quorum met"); + } + + function testQuorumCanBeSetToZeroToDisable() public { + // Set quorum to 10, then back to 0 + vm.prank(address(exec)); + dd.setConfig(DirectDemocracyVoting.ConfigKey.QUORUM, abi.encode(uint32(10))); + assertEq(dd.quorum(), 10); + + vm.prank(address(exec)); + dd.setConfig(DirectDemocracyVoting.ConfigKey.QUORUM, abi.encode(uint32(0))); + assertEq(dd.quorum(), 0, "Quorum should be disabled after setting to 0"); + } + /*//////////////////////////////////////////////////////////// ANNOUNCE WINNER REPLAY PROTECTION ////////////////////////////////////////////////////////////*/ diff --git a/test/GovernanceCrossChainUpgrade.t.sol b/test/GovernanceCrossChainUpgrade.t.sol index ad16912..9076306 100644 --- a/test/GovernanceCrossChainUpgrade.t.sol +++ b/test/GovernanceCrossChainUpgrade.t.sol @@ -198,7 +198,7 @@ contract GovernanceCrossChainUpgradeTest is Test { uint256[] memory hatIds = new uint256[](0); hv.createProposal(bytes("Upgrade Widget to v2"), bytes32(0), 15, 2, batches, hatIds); - // 2. Vote YES (alice + carol = 100% quorum, all YES) + // 2. Vote YES (alice + carol = 100% threshold, all YES) uint8[] memory yesIdx = new uint8[](1); yesIdx[0] = 0; uint8[] memory weight = new uint8[](1); @@ -214,7 +214,7 @@ contract GovernanceCrossChainUpgradeTest is Test { vm.prank(alice); (uint256 winner, bool valid) = hv.announceWinner(0); - assertTrue(valid, "quorum should be met"); + assertTrue(valid, "threshold should be met"); assertEq(winner, 0, "YES should win"); // 4. Verify: home chain beacon upgraded to V2 diff --git a/test/HybridVoting.t.sol b/test/HybridVoting.t.sol index dc128d5..84d6af0 100644 --- a/test/HybridVoting.t.sol +++ b/test/HybridVoting.t.sol @@ -154,7 +154,7 @@ contract MockERC20 is IERC20 { address(exec), // executor creatorHats, // allowed creator hats targets, // allowed target(s) - uint8(50), // quorum % + uint8(50), // threshold % classes // class configurations ) ); @@ -314,7 +314,7 @@ contract MockERC20 is IERC20 { vm.prank(alice); (uint256 win, bool ok) = hv.announceWinner(id); - assertTrue(ok, "quorum not met"); + assertTrue(ok, "threshold not met"); assertEq(win, 0, "YES should win"); /* executor should be called with the winning option's batch */ @@ -457,9 +457,9 @@ contract MockERC20 is IERC20 { vm.expectRevert(); hv.setConfig(HybridVoting.ConfigKey.TARGET_ALLOWED, abi.encode(address(0xDEAD), true)); - // Set quorum + // Set threshold vm.expectRevert(); - hv.setConfig(HybridVoting.ConfigKey.QUORUM, abi.encode(60)); + hv.setConfig(HybridVoting.ConfigKey.THRESHOLD, abi.encode(60)); // Split, quadratic, and min balance are now configured via setClasses // These legacy config options no longer exist @@ -471,9 +471,9 @@ contract MockERC20 is IERC20 { // Test that executor can call admin functions vm.startPrank(address(exec)); - // Set quorum - hv.setConfig(HybridVoting.ConfigKey.QUORUM, abi.encode(60)); - assertEq(hv.quorumPct(), 60); + // Set threshold + hv.setConfig(HybridVoting.ConfigKey.THRESHOLD, abi.encode(60)); + assertEq(hv.thresholdPct(), 60); // Split, quadratic, and min balance are now configured via setClasses // Test class configuration update instead @@ -505,12 +505,12 @@ contract MockERC20 is IERC20 { // Old executor should no longer have permissions vm.prank(address(exec)); vm.expectRevert(); - hv.setConfig(HybridVoting.ConfigKey.QUORUM, abi.encode(70)); + hv.setConfig(HybridVoting.ConfigKey.THRESHOLD, abi.encode(70)); // New executor should have permissions vm.prank(newExecutor); - hv.setConfig(HybridVoting.ConfigKey.QUORUM, abi.encode(70)); - assertEq(hv.quorumPct(), 70); + hv.setConfig(HybridVoting.ConfigKey.THRESHOLD, abi.encode(70)); + assertEq(hv.thresholdPct(), 70); } // function testCleanup() public { @@ -789,7 +789,7 @@ contract MockERC20 is IERC20 { vm.prank(alice); (uint256 win, bool ok) = hv.announceWinner(id); - assertTrue(ok, "Quorum should be met"); + assertTrue(ok, "Threshold should be met"); assertEq(win, 0, "YES should win"); } @@ -921,7 +921,7 @@ contract MockERC20 is IERC20 { vm.warp(block.timestamp + 16 minutes); (uint256 win, bool ok) = hv.announceWinner(id); - assertTrue(ok, "Should have quorum"); + assertTrue(ok, "Should meet threshold"); // YES votes: alice (30% of 30% = 9%), carol (30% of 30% + her token share of 50%) // NO votes: bob (his token share of 50%), dave (100% of 20% = 20%) // Winner depends on token distribution @@ -1015,15 +1015,15 @@ contract MockERC20 is IERC20 { // Check results vm.warp(block.timestamp + 16 minutes); (uint256 win, bool ok) = hv.announceWinner(id); - assertTrue(ok, "Should meet quorum"); + assertTrue(ok, "Should meet threshold"); } - function testNClassQuorumCalculation() public { - // Test that quorum is calculated correctly across all classes + function testNClassThresholdCalculation() public { + // Test that threshold is calculated correctly across all classes vm.startPrank(address(exec)); - // Set up 2-class system with 40% quorum requirement - hv.setConfig(HybridVoting.ConfigKey.QUORUM, abi.encode(40)); + // Set up 2-class system with 40% threshold requirement + hv.setConfig(HybridVoting.ConfigKey.THRESHOLD, abi.encode(40)); HybridVoting.ClassConfig[] memory classes = new HybridVoting.ClassConfig[](2); uint256[] memory emptyHats = new uint256[](0); @@ -1063,12 +1063,12 @@ contract MockERC20 is IERC20 { w[0] = 100; hv.vote(id, idx, w); - // Should meet quorum with significant participation in token class + // Should meet threshold with significant participation in token class vm.warp(block.timestamp + 16 minutes); (uint256 win, bool ok) = hv.announceWinner(id); - // With only one voter in token class (40% of total), should meet 40% quorum - assertTrue(ok, "Should meet quorum with 40% participation"); + // With only one voter in token class (40% of total), should meet 40% threshold + assertTrue(ok, "Should meet threshold with 40% participation"); } function testNClassZeroBalanceVoters() public { @@ -1126,7 +1126,7 @@ contract MockERC20 is IERC20 { (uint256 win, bool ok) = hv.announceWinner(id); assertEq(win, 0, "Option 0 should win"); - assertTrue(ok, "Should have quorum from rich voter"); + assertTrue(ok, "Should meet threshold from rich voter"); } function testNClassMixedQuadraticLinear() public { @@ -1480,10 +1480,97 @@ contract MockERC20 is IERC20 { vm.warp(block.timestamp + 16 minutes); (uint256 win, bool ok) = hv.announceWinner(id); - assertTrue(ok, "Should have quorum with multiple participants"); + assertTrue(ok, "Should meet threshold with multiple participants"); // The result depends on the complex interaction of all classes } + /* ───────────────────────── QUORUM TESTS ───────────────────────── */ + + function testSetQuorum() public { + assertEq(hv.quorum(), 0, "Default quorum should be 0"); + vm.prank(address(exec)); + hv.setConfig(HybridVoting.ConfigKey.QUORUM, abi.encode(uint32(5))); + assertEq(hv.quorum(), 5, "Quorum should be 5"); + } + + function testSetQuorumEmitsEvent() public { + vm.prank(address(exec)); + vm.expectEmit(true, true, true, true); + emit HybridVoting.QuorumSet(uint32(5)); + hv.setConfig(HybridVoting.ConfigKey.QUORUM, abi.encode(uint32(5))); + } + + function testSetQuorumUnauthorized() public { + vm.expectRevert(VotingErrors.Unauthorized.selector); + hv.setConfig(HybridVoting.ConfigKey.QUORUM, abi.encode(uint32(5))); + } + + function testQuorumNotMet() public { + // Set quorum to 3 voters + vm.prank(address(exec)); + hv.setConfig(HybridVoting.ConfigKey.QUORUM, abi.encode(uint32(3))); + + // Create proposal and have only 1 voter vote + uint256 id = _create(); + _voteYES(alice); + + vm.warp(block.timestamp + 16 minutes); + (uint256 winner, bool valid) = hv.announceWinner(id); + assertFalse(valid, "Should be invalid when quorum not met"); + assertEq(winner, 0, "Winner should be 0 when quorum not met"); + } + + function testQuorumMet() public { + // Set quorum to 2 voters + vm.prank(address(exec)); + hv.setConfig(HybridVoting.ConfigKey.QUORUM, abi.encode(uint32(2))); + + // Create proposal and have 2 voters vote + uint256 id = _create(); + _voteYES(alice); + _voteYES(carol); + + vm.warp(block.timestamp + 16 minutes); + (uint256 winner, bool valid) = hv.announceWinner(id); + assertTrue(valid, "Should be valid when quorum met"); + } + + function testQuorumDisabledByDefault() public { + // Default quorum is 0, even 1 voter should work + uint256 id = _create(); + _voteYES(alice); + + vm.warp(block.timestamp + 16 minutes); + (uint256 winner, bool valid) = hv.announceWinner(id); + assertTrue(valid, "Should be valid with quorum disabled (0)"); + } + + function testVoterCountTracking() public { + uint256 id = _create(); + _voteYES(alice); + _voteYES(bob); + _voteYES(carol); + + // Verify voter count is tracked (3 voters) + // Set quorum to 3 - should pass exactly + vm.prank(address(exec)); + hv.setConfig(HybridVoting.ConfigKey.QUORUM, abi.encode(uint32(3))); + + vm.warp(block.timestamp + 16 minutes); + (uint256 winner, bool valid) = hv.announceWinner(id); + assertTrue(valid, "Should pass with exactly 3 voters and quorum of 3"); + } + + function testQuorumCanBeSetToZeroToDisable() public { + vm.prank(address(exec)); + hv.setConfig(HybridVoting.ConfigKey.QUORUM, abi.encode(uint32(10))); + assertEq(hv.quorum(), 10); + + vm.prank(address(exec)); + hv.setConfig(HybridVoting.ConfigKey.QUORUM, abi.encode(uint32(0))); + assertEq(hv.quorum(), 0, "Quorum should be disabled after setting to 0"); + } + /* ───────────── ANNOUNCE WINNER REPLAY PROTECTION ───────────── */ function testAnnounceWinnerDoubleCallReverts() public { diff --git a/test/VotingMath.t.sol b/test/VotingMath.t.sol index 7637bef..393e0f9 100644 --- a/test/VotingMath.t.sol +++ b/test/VotingMath.t.sol @@ -210,45 +210,45 @@ contract VotingMathTest is Test { scores[2] = 20; uint256 totalWeight = 100; - uint8 quorumPct = 40; + uint8 thresholdPct = 40; (uint256 win, bool ok, uint256 hi, uint256 second) = - VotingMath.pickWinnerMajority(scores, totalWeight, quorumPct, true); + VotingMath.pickWinnerMajority(scores, totalWeight, thresholdPct, true); assertEq(win, 1, "Option 1 should win"); - assertTrue(ok, "Should meet quorum"); + assertTrue(ok, "Should meet threshold"); assertEq(hi, 50, "Highest score should be 50"); assertEq(second, 30, "Second highest should be 30"); } - function testPickWinnerMajority_QuorumNotMet() public { + function testPickWinnerMajority_ThresholdNotMet() public { uint256[] memory scores = new uint256[](3); scores[0] = 10; scores[1] = 20; scores[2] = 15; uint256 totalWeight = 100; - uint8 quorumPct = 25; // Requires > 25% of total weight + uint8 thresholdPct = 25; // Requires > 25% of total weight - (uint256 win, bool ok,,) = VotingMath.pickWinnerMajority(scores, totalWeight, quorumPct, true); + (uint256 win, bool ok,,) = VotingMath.pickWinnerMajority(scores, totalWeight, thresholdPct, true); assertEq(win, 1, "Option 1 should be winner candidate"); - assertFalse(ok, "Should not meet quorum (20 * 100 = 2000, not >= 2500)"); + assertFalse(ok, "Should not meet threshold (20 * 100 = 2000, not >= 2500)"); } - function testPickWinnerMajority_ExactQuorumPasses() public { + function testPickWinnerMajority_ExactThresholdPasses() public { uint256[] memory scores = new uint256[](3); scores[0] = 10; scores[1] = 25; // Exactly 25% of 100 scores[2] = 15; uint256 totalWeight = 100; - uint8 quorumPct = 25; // Requires >= 25% of total weight + uint8 thresholdPct = 25; // Requires >= 25% of total weight - (uint256 win, bool ok,,) = VotingMath.pickWinnerMajority(scores, totalWeight, quorumPct, false); + (uint256 win, bool ok,,) = VotingMath.pickWinnerMajority(scores, totalWeight, thresholdPct, false); assertEq(win, 1, "Option 1 should be winner"); - assertTrue(ok, "Should meet quorum (25 * 100 = 2500, >= 2500)"); + assertTrue(ok, "Should meet threshold (25 * 100 = 2500, >= 2500)"); } function testPickWinnerMajority_TieWithStrictRequirement() public { @@ -257,9 +257,9 @@ contract VotingMathTest is Test { scores[1] = 50; uint256 totalWeight = 100; - uint8 quorumPct = 40; + uint8 thresholdPct = 40; - (uint256 win, bool ok,,) = VotingMath.pickWinnerMajority(scores, totalWeight, quorumPct, true); + (uint256 win, bool ok,,) = VotingMath.pickWinnerMajority(scores, totalWeight, thresholdPct, true); assertFalse(ok, "Should not be valid with tie and strict requirement"); } @@ -270,9 +270,9 @@ contract VotingMathTest is Test { scores[1] = 50; uint256 totalWeight = 100; - uint8 quorumPct = 40; + uint8 thresholdPct = 40; - (uint256 win, bool ok,,) = VotingMath.pickWinnerMajority(scores, totalWeight, quorumPct, false); + (uint256 win, bool ok,,) = VotingMath.pickWinnerMajority(scores, totalWeight, thresholdPct, false); assertTrue(ok, "Should be valid with tie when not strict"); assertEq(win, 0, "First option should win in tie"); @@ -293,17 +293,17 @@ contract VotingMathTest is Test { uint256 ddTotalRaw = 100; uint256 ptTotalRaw = 6000; uint8 ddSharePct = 50; // 50/50 split - uint8 quorumPct = 30; // Lowered quorum to 30% so 35% passes + uint8 thresholdPct = 30; // Lowered threshold to 30% so 35% passes (uint256 win, bool ok, uint256 hi, uint256 second) = - VotingMath.pickWinnerTwoSlice(ddRaw, ptRaw, ddTotalRaw, ptTotalRaw, ddSharePct, quorumPct); + VotingMath.pickWinnerTwoSlice(ddRaw, ptRaw, ddTotalRaw, ptTotalRaw, ddSharePct, thresholdPct); // DD scaled: [15, 25, 10] (out of 50) // PT scaled: [16.67, 8.33, 25] (out of 50) // Total: [31.67, 33.33, 35] assertEq(win, 2, "Option 2 should win"); - assertTrue(ok, "Should meet quorum (35% > 30%)"); + assertTrue(ok, "Should meet threshold (35% > 30%)"); } function testPickWinnerTwoSlice_ZeroTotals() public { @@ -396,8 +396,8 @@ contract VotingMathTest is Test { VotingMath.validateWeights(VotingMath.Weights({idxs: idxs, weights: weights, optionsLen: numOptions})); } - function testFuzz_PickWinnerMajority(uint256[5] memory rawScores, uint8 quorumPct, bool strictMajority) public { - vm.assume(quorumPct > 0 && quorumPct <= 100); + function testFuzz_PickWinnerMajority(uint256[5] memory rawScores, uint8 thresholdPct, bool strictMajority) public { + vm.assume(thresholdPct > 0 && thresholdPct <= 100); uint256[] memory scores = new uint256[](5); uint256 totalWeight; @@ -410,7 +410,7 @@ contract VotingMathTest is Test { if (totalWeight == 0) totalWeight = 1; // Avoid division by zero (uint256 win, bool ok, uint256 hi, uint256 second) = - VotingMath.pickWinnerMajority(scores, totalWeight, quorumPct, strictMajority); + VotingMath.pickWinnerMajority(scores, totalWeight, thresholdPct, strictMajority); // Verify winner has highest score if (hi > 0) { @@ -429,9 +429,9 @@ contract VotingMathTest is Test { assertTrue(foundSecond || second == hi, "Second should be a valid score"); } - // Verify quorum logic + // Verify threshold logic if (ok) { - assertTrue(hi * 100 > totalWeight * quorumPct, "Should meet quorum threshold"); + assertTrue(hi * 100 > totalWeight * thresholdPct, "Should meet threshold"); if (strictMajority) { assertTrue(hi > second, "Should have strict majority"); } else { diff --git a/test/VotingMathVerification.sol b/test/VotingMathVerification.sol index 0a7652b..e0ae2b3 100644 --- a/test/VotingMathVerification.sol +++ b/test/VotingMathVerification.sol @@ -91,7 +91,7 @@ contract VotingMathVerification { (uint256 win, bool ok, uint256 hi, uint256 second) = VotingMath.pickWinnerMajority(scores, 100, 40, true); require(win == 1, "Winner should be option 1"); - require(ok == true, "Should meet quorum"); + require(ok == true, "Should meet threshold"); require(hi == 50, "Highest should be 50"); require(second == 30, "Second should be 30"); @@ -114,7 +114,7 @@ contract VotingMathVerification { (uint256 win, bool ok,,) = VotingMath.pickWinnerTwoSlice(ddRaw, ptRaw, 100, 6000, 50, 30); require(win == 2, "Winner should be option 2"); - require(ok == true, "Should meet quorum"); + require(ok == true, "Should meet threshold"); return true; }