Core Contracts

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

The Governance Contract Does Not Forward ETH When Executing Proposals and the TimelockController Can’t Be Funded Directly. This Will Lead ETH-Required Proposals to Fail.

Summary

the core issue is that proposals requiring ETH transfers may fail because the system isn’t designed to easily receive or forward ETH. The Governance contract’s execute function is non-payable, so it can’t pass along any ETH when it triggers proposal execution. At the same time, the TimelockController—responsible for executing scheduled operations—has a payable executeBatch function but lacks a receive or fallback function to accept ETH deposits. This means that if a proposal needs to send ETH, there’s no built-in way to ensure that the timelock contract is properly funded, leading to potential execution failures.

Vulnerability Details

The Governance contract defines its execute function like this:

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

function execute(uint256 proposalId) external override nonReentrant {
ProposalCore storage proposal = _proposals[proposalId];
if (proposal.executed) revert ProposalAlreadyExecuted(proposalId, block.timestamp);
ProposalState currentState = state(proposalId);
if (currentState == ProposalState.Succeeded) {
_queueProposal(proposalId);
} else if (currentState == ProposalState.Queued) {
_executeProposal(proposalId);
} else {
revert InvalidProposalState(
proposalId,
currentState,
currentState == ProposalState.Active ? ProposalState.Succeeded : ProposalState.Queued,
"Invalid state for execution"
);
}
}

Because this function is not marked as payable, it cannot forward any ETH that might be sent along with the transaction when calling the internal _executeProposal function which in turn calls the timelock's executeBatch function. This is acceptable if no ETH is needed, but becomes an issue if a proposal requires ETH transfers as we will see soon.

This is the timelock's executeBatch payable function that is called from the Governance contract:

https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/core/governance/proposals/TimelockController.sol#L162-L185

function executeBatch(
address[] calldata targets,
uint256[] calldata values,
bytes[] calldata calldatas,
bytes32 predecessor,
bytes32 salt
) external override payable nonReentrant onlyRole(EXECUTOR_ROLE) {
// ... (checks omitted for brevity)
for (uint256 i = 0; i < targets.length; i++) {
(bool success, bytes memory returndata) = targets[i].call{value: values[i]}(calldatas[i]);
if (!success) {
revert CallReverted(id, i);
}
}
emit OperationExecuted(id, targets, values, calldatas, predecessor, salt);
}

This function is payable, meaning it can forward ETH to the target calls. However, the TimelockController itself has no function to receive ETH directly (no receive or fallback function). This means there is no built-in way to fund the timelock with ETH outside of calling its payable functions.

How it Fits Together:

  • If a proposal is meant to transfer ETH, the expectation is that the timelock contract should have enough ETH in its balance to cover those transfers.

  • However, because the Governance execute function is non-payable, it cannot contribute ETH when executing the proposal.

  • And since the timelock cannot be directly funded through plain ETH transfers (due to the lack of a deposit mechanism), proposals that require ETH may fail if the timelock contract is not already pre-funded by some external process.

Impact

Proposals that require ETH transfers will not execute successfully if the timelock lacks sufficient ETH. Hence, this flaw could halt or delay important governance actions, as proposals expecting ETH transfers would simply fail.

Tools Used

  • Manual code review

Recommendations

To mitigate this issue, I recommend making the Governance contract’s execute function payable so it can forward ETH when needed, and adding a receive or fallback function to the TimelockController so it can be directly funded with ETH.

Updates

Lead Judging Commences

inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Governance.execute lacks payable modifier and ETH forwarding mechanism, preventing proposals with ETH transfers from being executed through TimelockController

inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Governance.execute lacks payable modifier and ETH forwarding mechanism, preventing proposals with ETH transfers from being executed through TimelockController

Support

FAQs

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

Give us feedback!