Core Contracts

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

Governance contract's execute() function lacks proper synchronization with the TimelockController's delay requirements

Summary

The governance system allows proposals to bypass the mandatory timelock delay, enabling immediate execution of sensitive protocol changes. This breaks a core security assumption of the protocol's governance mechanism. Governance.sol#execute

// Governance.sol
function execute(uint256 proposalId) external override nonReentrant {
ProposalCore storage proposal = _proposals[proposalId];
// First validation gap - no explicit timelock delay check before execution
if (proposal.executed) revert ProposalAlreadyExecuted(proposalId, block.timestamp);
ProposalState currentState = state(proposalId);
if (currentState == ProposalState.Succeeded) {
_queueProposal(proposalId);
} else if (currentState == ProposalState.Queued) {
// Second validation gap - relies entirely on TimelockController
// for delay validation without local verification
_executeProposal(proposalId);
}
}

The function fails to verify that sufficient time has passed since proposal queueing, allowing premature execution.

The vulnerability stems from these gaps working together, the Governance contract assumes TimelockController handles all delay validation, while TimelockController's checks are insufficient. This creates a timing vulnerability where proposals can execute before their mandatory delay period expires. TimelockController.sol#executeBatch

// TimelockController.sol
function executeBatch(...) external {
// Insufficient delay validation
// Only checks if timestamp has passed, but not if minimum delay period was respected
if (block.timestamp < op.timestamp) revert OperationNotReady(id);
// Missing critical check
// Should verify: block.timestamp >= op.timestamp + minDelay
}

The contract assumes the TimelockController handles all delay validation, but lacks synchronization between the two contracts' timing requirements, and this creates a race condition where proposals can execute before their mandatory delay period.

Vulnerability Details

The Broken Timelock Imagine a bank vault with a time-delayed lock except this lock can be opened immediately. The RAAC protocol's governance system has exactly this flaw. The TimelockController, designed to enforce a mandatory waiting period before executing sensitive protocol changes, can be circumvented through a subtle timing mismatch.

How The Vulnerability Works When a proposal passes voting in the Governance contract, it should enter a mandatory waiting period managed by the TimelockController. This delay gives the community time to react to potentially malicious proposals. However, the Governance contract's execute() function fails to properly synchronize with the timelock's delay requirements.

Impact

The consequences are severe for the protocol's security. A malicious actor could:

  1. Propose changes to drain the Treasury's funds

  2. Queue the proposal in the timelock

  3. Execute it immediately, before anyone can respond

This bypasses the protocol's 2-day mandatory delay period, turning carefully planned governance into a race to execution.

Recommendations

Adding delay verification restores the intended security model

// Governance.sol
function execute(uint256 proposalId) external override nonReentrant {
ProposalCore storage proposal = _proposals[proposalId];
ProposalState currentState = state(proposalId);
// Add timelock validation in the Queued state execution path
if (currentState == ProposalState.Succeeded) {
_queueProposal(proposalId);
} else if (currentState == ProposalState.Queued) {
// Add explicit timelock delay verification
bytes32 timelockId = _timelock.hashOperationBatch(
proposal.targets,
proposal.values,
proposal.calldatas,
bytes32(0),
proposal.descriptionHash
);
// Verify both pending status and delay period
require(_timelock.isOperationPending(timelockId), "Operation not pending");
require(block.timestamp >= proposal.queueTime + _timelock.getMinDelay(),
"Timelock delay not met");
_executeProposal(proposalId);
}
}
  1. Maintains the existing state machine logic

  2. Adds explicit delay verification using the timelock's ID

  3. Checks both operation status and delay period

  4. Integrates with the existing TimelockController interface

Updates

Lead Judging Commences

inallhonesty Lead Judge 3 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement
inallhonesty Lead Judge 3 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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