Core Contracts

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

Treasury Timelock Bypass Through Governance Proposals

Summary

The timelock delay enforcement for treasury withdrawals can be bypassed through proposal execution, which allows immediate execution of treasury withdrawals without the mandatory waiting period, putting protocol funds at risk.

A governance proposal targeting the treasury can be executed without respecting the timelock delay. When a proposal is created and executed, the TimelockController's delay validation is circumvented because the Governance contract doesn't properly enforce the delay period between queueing and execution. Just like analogous to bypassing a bank's mandatory waiting period for large withdrawals.

Vulnerability Details

Begins with RAAC's core mission tokenizing real estate assets for DeFi integration. The governance system protects these valuable assets through a mandatory timelock delay, but there's a critical gap in this defense.

When examining the governance flow, we see how a malicious actor could craft a proposal targeting the treasury. The TimelockController specifies a 2-day delay, but the Governance contract's execute() function bypasses this protection entirely. This creates an immediate execution path to treasury funds.

function execute(uint256 proposalId) external override nonReentrant {
ProposalCore storage proposal = _proposals[proposalId];
if (proposal.executed) revert ProposalAlreadyExecuted(proposalId, block.timestamp);
ProposalState currentState = state(proposalId);
// 🚦 State validation checkpoint
if (currentState == ProposalState.Succeeded) {
// 📥 Queue the proposal
_queueProposal(proposalId); // 🚩 Missing delay enforcement
} else if (currentState == ProposalState.Queued) {
// ⚡ Execute the queued proposal
_executeProposal(proposalId); // 💥 Immediate execution possible
} else {
// ⛔ Invalid state handling
revert InvalidProposalState(
proposalId,
currentState,
currentState == ProposalState.Active ? ProposalState.Succeeded : ProposalState.Queued,
"Invalid state for execution"
);
}
}

This shows the execution flow where the timelock delay validation is missing between queueing and execution, allowing immediate treasury access after proposal success.

The attack narrative unfolds naturally: An attacker creates a proposal, waits for voting to conclude, then executes it immediately. The treasury's assets, which could include significant RWA-backed positions, become vulnerable to instant withdrawal.

Looking at the code, the Governance contract's execute() function, this isn't just about potential fund loss, it undermines the RAAC's entire real estate tokenization model.

Impact

Looking at the RAAC protocol's governance system, the TimelockController's 2-day delay should protect treasury actions. Yet shows a direct path to instant treasury access completely bypassing this critical security layer.

The RAAC whitepaper (Governance Security) specifies mandatory timelocks for treasury actions. However, the current implementation allows proposals to execute treasury withdrawals without enforcing the TimelockController's delay period.

For RAAC's real estate asset protocol:

  • Treasury holds significant RWA-backed assets

  • Instant withdrawals break the core security assumption

  • Integration with RAACHousePrices.sol and lending pools at risk

Recommendations

Since the vulnerability exists in the interaction between Governance.sol and TimelockController.sol.

function execute(uint256 proposalId) external override nonReentrant {
ProposalCore storage proposal = _proposals[proposalId];
if (proposal.executed) revert ProposalAlreadyExecuted(proposalId, block.timestamp);
ProposalState currentState = state(proposalId);
// 🚦 State validation checkpoint
if (currentState == ProposalState.Succeeded) {
// 📥 Queue the proposal in timelock
bytes32 timelockId = _queueProposal(proposalId);
// ⏳ Validate timelock scheduling
require(_timelock.isOperationPending(timelockId), "Proposal not queued in timelock");
} else if (currentState == ProposalState.Queued) {
// ⚡ Execute only if timelock delay passed
bytes32 timelockId = _getTimelockId(proposalId);
require(_timelock.isOperationReady(timelockId), "Timelock delay not met");
_executeProposal(proposalId);
} else {
// ⛔ Invalid state handling
revert InvalidProposalState(
proposalId,
currentState,
currentState == ProposalState.Active ? ProposalState.Succeeded : ProposalState.Queued,
"Invalid state for execution"
);
}
}

You can see here we leverages the existing TimelockController functionality rather than adding redundant delay checks in Governance.sol.

Updates

Lead Judging Commences

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

Support

FAQs

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