@@ -7,6 +7,7 @@ import { Proposal, ProposalState } from "../foundation/Types.sol";
77import { SystemAddresses } from "../foundation/SystemAddresses.sol " ;
88import { Errors } from "../foundation/Errors.sol " ;
99import { IStaking } from "../staking/IStaking.sol " ;
10+ import { IValidatorManagement } from "../staking/IValidatorManagement.sol " ;
1011import { ITimestamp } from "../runtime/ITimestamp.sol " ;
1112import { requireAllowed } from "../foundation/SystemAccessControl.sol " ;
1213import { Ownable2Step, Ownable } from "@openzeppelin/access/Ownable2Step.sol " ;
@@ -154,6 +155,11 @@ contract Governance is IGovernance, Ownable2Step {
154155 return IStaking (SystemAddresses.STAKING);
155156 }
156157
158+ /// @notice Get validator manager contract
159+ function _validatorManager () internal pure returns (IValidatorManagement) {
160+ return IValidatorManagement (SystemAddresses.VALIDATOR_MANAGER);
161+ }
162+
157163 /// @notice Verify caller is the pool's voter
158164 function _requireVoter (
159165 address stakePool
@@ -173,6 +179,40 @@ contract Governance is IGovernance, Ownable2Step {
173179 }
174180 }
175181
182+ /// @notice Return true if either side has reached the proposal's early resolution threshold.
183+ function _canBeResolvedEarly (
184+ Proposal storage p
185+ ) internal view returns (bool ) {
186+ uint128 threshold = p.earlyResolutionVoteThreshold;
187+ return threshold > 0 && (p.yesVotes >= threshold || p.noVotes >= threshold);
188+ }
189+
190+ /// @notice Return true when no more votes are needed for state evaluation.
191+ function _isVotingClosed (
192+ Proposal storage p
193+ ) internal view returns (bool ) {
194+ return _now () >= p.expirationTime || _canBeResolvedEarly (p);
195+ }
196+
197+ /// @notice Return true if the current vote totals satisfy the passing condition.
198+ function _hasSucceeded (
199+ Proposal storage p
200+ ) internal view returns (bool ) {
201+ return p.yesVotes > p.noVotes && p.yesVotes + p.noVotes >= p.minVoteThreshold;
202+ }
203+
204+ /// @notice Compute Aptos-style default early resolution threshold.
205+ /// @dev Uses current active validator total voting power as the voting-power supply.
206+ /// The threshold is 50% + 1, matching Aptos Governance.
207+ function _defaultEarlyResolutionVoteThreshold () internal view returns (uint128 ) {
208+ uint256 totalVotingPower = _validatorManager ().getTotalVotingPower ();
209+ uint256 threshold = totalVotingPower / 2 + 1 ;
210+ if (threshold > type (uint128 ).max) {
211+ threshold = type (uint128 ).max;
212+ }
213+ return uint128 (threshold);
214+ }
215+
176216 // ========================================================================
177217 // VIEW FUNCTIONS
178218 // ========================================================================
@@ -203,21 +243,17 @@ contract Governance is IGovernance, Ownable2Step {
203243
204244 // Check if resolved
205245 if (p.isResolved) {
206- // Determine if it passed
207- if (p.yesVotes > p.noVotes && p.yesVotes + p.noVotes >= p.minVoteThreshold) {
246+ if (_hasSucceeded (p)) {
208247 return ProposalState.SUCCEEDED;
209248 }
210249 return ProposalState.FAILED;
211250 }
212251
213- // Not resolved yet
214- uint64 now_ = _now ();
215- if (now_ < p.expirationTime) {
252+ if (! _isVotingClosed (p)) {
216253 return ProposalState.PENDING;
217254 }
218255
219- // Voting ended but not resolved - determine outcome
220- if (p.yesVotes > p.noVotes && p.yesVotes + p.noVotes >= p.minVoteThreshold) {
256+ if (_hasSucceeded (p)) {
221257 return ProposalState.SUCCEEDED;
222258 }
223259 return ProposalState.FAILED;
@@ -254,16 +290,14 @@ contract Governance is IGovernance, Ownable2Step {
254290 return false ;
255291 }
256292
257- uint64 now_ = _now ();
258-
259- // Can resolve if voting period ended
260- if (now_ < p.expirationTime) {
293+ if (! _isVotingClosed (p)) {
261294 return false ;
262295 }
263296
264297 // Atomicity guard: resolution must happen strictly after the last vote
265298 // This prevents flash loan attacks where someone borrows tokens, votes, and resolves in the same tx
266299 uint64 lastVote = lastVoteTime[proposalId];
300+ uint64 now_ = _now ();
267301 if (lastVote > 0 && now_ <= lastVote) {
268302 return false ;
269303 }
@@ -317,6 +351,17 @@ contract Governance is IGovernance, Ownable2Step {
317351 return lastVoteTime[proposalId];
318352 }
319353
354+ /// @inheritdoc IGovernance
355+ function getEarlyResolutionVoteThreshold (
356+ uint64 proposalId
357+ ) external view returns (uint128 ) {
358+ Proposal storage p = _proposals[proposalId];
359+ if (p.id == 0 ) {
360+ revert Errors.ProposalNotFound (proposalId);
361+ }
362+ return p.earlyResolutionVoteThreshold;
363+ }
364+
320365 // ========================================================================
321366 // PROPOSAL MANAGEMENT
322367 // ========================================================================
@@ -368,18 +413,25 @@ contract Governance is IGovernance, Ownable2Step {
368413 // Create proposal
369414 proposalId = nextProposalId++ ;
370415
416+ uint128 minVoteThreshold = _config ().minVotingThreshold ();
417+ uint128 earlyResolutionVoteThreshold = _defaultEarlyResolutionVoteThreshold ();
418+ if (earlyResolutionVoteThreshold < minVoteThreshold) {
419+ revert Errors.InvalidEarlyResolutionVoteThreshold (minVoteThreshold, earlyResolutionVoteThreshold);
420+ }
421+
371422 _proposals[proposalId] = Proposal ({
372423 id: proposalId,
373424 proposer: msg .sender ,
374425 executionHash: executionHash,
375426 metadataUri: metadataUri,
376427 creationTime: now_,
377428 expirationTime: expirationTime,
378- minVoteThreshold: _config (). minVotingThreshold () ,
429+ minVoteThreshold: minVoteThreshold ,
379430 yesVotes: 0 ,
380431 noVotes: 0 ,
381432 isResolved: false ,
382- resolutionTime: 0
433+ resolutionTime: 0 ,
434+ earlyResolutionVoteThreshold: earlyResolutionVoteThreshold
383435 });
384436
385437 emit ProposalCreated (proposalId, msg .sender , stakePool, executionHash, metadataUri);
@@ -506,8 +558,7 @@ contract Governance is IGovernance, Ownable2Step {
506558
507559 uint64 now_ = _now ();
508560
509- // Check voting period has ended
510- if (now_ < p.expirationTime) {
561+ if (! _isVotingClosed (p)) {
511562 revert Errors.VotingPeriodNotEnded (p.expirationTime);
512563 }
513564
@@ -625,4 +676,3 @@ contract Governance is IGovernance, Ownable2Step {
625676 }
626677 }
627678}
628-
0 commit comments