Core Contracts

Regnum Aurum Acquisition Corp
HardhatReal World AssetsNFT
77,280 USDC
View results
Submission Details
Severity: medium
Invalid

Governance execute() Function Has Dual Behavior Leading to User Confusion and Failed Transactions

Relevant GitHub Links

https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/core/governance/proposals/Governance.sol#L227-L239

Summary

The execute() function in the Governance contract has dual behavior - it either queues or executes a proposal depending on the proposal state. This violates the principle of least surprise and can lead to user confusion, failed transactions, and gas losses.

Vulnerability Details

The execute() function in Governance.sol has two different behaviors based on proposal state:

function execute(uint256 proposalId) external override nonReentrant {
ProposalCore storage proposal = _proposals[proposalId];
if (proposal.executed) revert ProposalAlreadyExecuted();
ProposalState currentState = state(proposalId);
// First call - queues the proposal
if (currentState == ProposalState.Succeeded) {
_queueProposal(proposalId);
}
// Second call - executes the proposal
else if (currentState == ProposalState.Queued) {
_executeProposal(proposalId);
} else {
revert InvalidProposalState();
}
}

When a proposal succeeds, users need to:

  1. Call execute() first time -> This queues the proposal

  2. Wait for timelock delay

  3. Call execute() again -> This actually executes the proposal

This dual behavior is not obvious from the function name or documentation.

Impact

  1. Users may think their proposal executed when it only queued, leading to governance delays

  2. Gas losses from failed second execute() calls if timelock hasn't passed

  3. Integration errors with protocols expecting immediate execution

  4. Confusion about actual proposal state

  5. Poor user experience in governance participation

Tools Used

Manual review

Recommendations

Split the functionality into two explicit functions:

function queueProposal(uint256 proposalId) external nonReentrant {
// Current _queueProposal logic
}
function executeProposal(uint256 proposalId) external nonReentrant {
// Current _executeProposal logic
}

Add clear events and documentation about the two-phase execution process:

event ProposalQueued(uint256 indexed proposalId, uint256 executionTime);
event ProposalExecuted(uint256 indexed proposalId);
Updates

Lead Judging Commences

inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!