@@ -76,6 +76,7 @@ abstract contract GovernanceModule is
7676 mapping (address => ProposalTypes.TimeConfig) public timeConfigs;
7777 mapping (bytes32 => ProposalTypes.RoleConfig) public roleConfigs;
7878 mapping (address => bytes32 ) public upgradeProposals;
79+ mapping (bytes32 => uint256 ) public proposalIndex;
7980
8081 /// @notice Proposal tracking
8182 uint256 public proposalCount;
@@ -179,7 +180,7 @@ abstract contract GovernanceModule is
179180 }
180181
181182 /// @notice Get pending owner address
182- function pendingOwner () public view virtual returns (address ) {
183+ function pendingOwner () external view virtual returns (address ) {
183184 return _pendingOwnerRequest;
184185 }
185186
@@ -196,7 +197,7 @@ abstract contract GovernanceModule is
196197 }
197198
198199 /// @notice checks if an account has already approved a specific proposal
199- function hasProposalApproval (bytes32 proposalId , address account ) public view returns (bool ) {
200+ function hasProposalApproval (bytes32 proposalId , address account ) external view returns (bool ) {
200201 return proposals[proposalId].hasApproved[account];
201202 }
202203
@@ -272,6 +273,7 @@ abstract contract GovernanceModule is
272273
273274 // Register proposal
274275 proposalRegistry[proposalCount] = proposalId;
276+ proposalIndex[proposalId] = proposalCount;
275277 proposalCount += 1 ;
276278 pendingProposals[target].proposalType = uint8 (proposalType);
277279
@@ -362,7 +364,6 @@ abstract contract GovernanceModule is
362364 // Delete entire record
363365 delete pendingProposals[proposal.target];
364366 delete proposals[proposalId];
365- if (proposalCount > 0 ) proposalCount -= 1 ;
366367 _removeFromRegistry (proposalId);
367368
368369 emit ProposalExpired (proposalId, proposal.proposalType, proposal.target);
@@ -371,18 +372,46 @@ abstract contract GovernanceModule is
371372 return false ; // Indicate that the proposal has not expired
372373 }
373374
374- function _removeFromRegistry (bytes32 proposalId ) internal {
375- uint256 i;
376- while (i < proposalCount && gasleft () > GAS_BUFFER) {
377- if (proposalRegistry[i] == proposalId) {
378- if (i != -- proposalCount) {
379- proposalRegistry[i] = proposalRegistry[proposalCount];
380- }
381- delete proposalRegistry[proposalCount];
382- break ;
375+ /// @notice Cleans up expired proposals in batches to avoid gas limits
376+ /// @param maxProposalsToCheck Maximum number of proposals to check in this batch
377+ function cleanupExpiredProposals (uint256 maxProposalsToCheck )
378+ external
379+ whenNotPaused
380+ nonReentrant
381+ onlyRole (ProposalTypes.ADMIN_ROLE)
382+ {
383+ uint256 checked = 0 ;
384+ uint256 index = proposalCount;
385+
386+ // Process from the end of the registry to minimize storage changes
387+ while (checked < maxProposalsToCheck && index > 0 ) {
388+ unchecked { index-- ; } // Gas optimization: unchecked as index > 0
389+ checked++ ;
390+
391+ bytes32 proposalId = proposalRegistry[index];
392+ if (proposals[proposalId].target == address (0 )) {
393+ continue ; // Skip already cleaned proposals
383394 }
384- unchecked { ++ i; }
395+
396+ // Check expiration and automatically clean if needed
397+ _checkExpiredProposal (proposalId);
385398 }
399+ _updateActivityTimestamp ();
400+ }
401+
402+ function _removeFromRegistry (bytes32 proposalId ) internal {
403+ uint256 index = proposalIndex[proposalId];
404+ if (index >= proposalCount) return ;
405+
406+ // Swap with the last element
407+ bytes32 lastProposalId = proposalRegistry[proposalCount - 1 ];
408+ proposalRegistry[index] = lastProposalId;
409+ proposalIndex[lastProposalId] = index;
410+
411+ // Delete the last element
412+ delete proposalRegistry[proposalCount - 1 ];
413+ delete proposalIndex[proposalId];
414+ proposalCount -= 1 ;
386415 }
387416
388417 // Virtual function for contract-specific proposals
@@ -483,7 +512,6 @@ abstract contract GovernanceModule is
483512 // Mark proposal as executed
484513 proposal.config.status = 1 ;
485514 delete proposals[proposalId];
486- if (proposalCount > 0 ) proposalCount -= 1 ;
487515 _removeFromRegistry (proposalId);
488516 } else if (proposal.proposalType == uint8 (ProposalTypes.ProposalType.Upgrade)) {
489517 // Nothing
@@ -500,7 +528,6 @@ abstract contract GovernanceModule is
500528 // Add cleanup
501529 delete pendingProposals[proposal.target];
502530 delete proposals[proposalId];
503- if (proposalCount > 0 ) proposalCount -= 1 ;
504531 _removeFromRegistry (proposalId);
505532 emit ProposalExecuted (proposalId, proposal.proposalType, proposal.target);
506533 } else {
@@ -512,7 +539,6 @@ abstract contract GovernanceModule is
512539 // Mark proposal as executed
513540 proposal.config.status = 1 ;
514541 delete proposals[proposalId];
515- if (proposalCount > 0 ) proposalCount -= 1 ;
516542 _removeFromRegistry (proposalId);
517543 }
518544 }
@@ -663,7 +689,6 @@ abstract contract GovernanceModule is
663689 // Delete pending proposals if all flags are cleared
664690 delete pendingProposals[target];
665691 delete proposals[currentId];
666- if (proposalCount > 0 ) proposalCount -= 1 ;
667692 _removeFromRegistry (currentId);
668693 _updateActivityTimestamp ();
669694 emit ProposalExecuted (currentId, currentProposal.proposalType, target);
0 commit comments