Core Contracts

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

Batch call operation susceptible to returndata bomb exploit

Target

contracts/core/governance/proposals/TimelockController.sol

Vulnerability Details

The executeBatch function within the TimelockController contract is responsible for executing a batch of scheduled operation, making calls to target addresses passed in as an array, each call is executed one at a time within a for loop, in a situation where one of the target contracts’ returndata is maliciously set to a large value, it would consume all the gas and cause the remaining call operations to fail since there would not be enough gas to continue the execution.

function executeBatch(
address[] calldata targets,
uint256[] calldata values,
bytes[] calldata calldatas,
bytes32 predecessor,
bytes32 salt
) external override payable nonReentrant onlyRole(EXECUTOR_ROLE) {
bytes32 id = hashOperationBatch(targets, values, calldatas, predecessor, salt);
// Check operation status
Operation storage op = _operations[id];
if (op.timestamp == 0) revert OperationNotFound(id);
if (op.executed) revert OperationAlreadyExecuted(id);
// Check timing conditions
if (block.timestamp < op.timestamp) revert OperationNotReady(id);
if (block.timestamp > op.timestamp + GRACE_PERIOD) revert OperationExpired(id);
// Check predecessor if specified
if (predecessor != bytes32(0)) {
if (!isOperationDone(predecessor)) {
revert PredecessorNotExecuted(predecessor);
}
}
// Mark as executed before external calls
op.executed = true;
// Execute each call
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);
}

TimelockController.executeBatch

Recall that when a contract makes a call to another contract, if no gas usage cap is set, it automatically sends 63/64 of the remaining gas to the called contract for it’s operation, the remaining 1/64 is reserved so the calling contract can perform any cleanups once control is handed back (e.g event emission) so in a case where the malicious contract uses up all the gas and returns a large returndata that would require more that the remaining 1/64 reserved gas, the overall transaction would continue to fail with an OOG error.

Sample code

// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
/**
* @title Returnbomb attack example
* @author pcaversaccio
*/
contract Evil {
uint256 public counter;
function startBomb() external returns (bytes memory) {
++counter;
// solhint-disable-next-line no-inline-assembly
assembly {
revert(0, 10000)
}
}
}
contract Victim {
function oops() public returns (bool, bytes memory) {
Evil evil = new Evil();
/// @dev If you put 3396 gas, the subcall will revert with an OOG error.
(bool success, bytes memory returnData) =
// solhint-disable-next-line avoid-low-level-calls
address(evil).call{gas: 3397}(abi.encodeWithSelector(evil.startBomb.selector));
return (success, returnData);
}
}

If you use Foundry, you can run the debugger in one line:

forge debug --optimize --optimizer-runs 200 --use 0.8.19 --via-ir ReturnBombExample.sol --target-contract Victim --sig "oops()" --debug

Impact

Batch executions can be reverted due to OOG errors leading to waste of gas

Tools Used

Manual Review

Recommendations

In this case there the returndata value isn’t used anywhere else in the function, it can be completely removed without introducing any other side effect while completely eliminating the attack surface

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

Support

FAQs

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

Give us feedback!