28789 - [SC - Low] Return value of call is not checked causing fai...

Submitted on Feb 27th 2024 at 11:05:49 UTC by @MrPotatoMagic for Boost | Puffer Finance

Report ID: #28789

Report type: Smart Contract

Report severity: Low

Target: https://etherscan.io/address/0x3C28B7c7Ba1A1f55c9Ce66b263B33B204f2126eA#code

Impacts:

  • Failed transactions continue execution without reverting

Description

Brief/Intro

The executeTransaction() function allows operators (with a delay) to execute calldata on a target contract.

The issue is that if the operation multisig executes some calldata on a target contract and the external call fails, the return value of success is not checked to be true or false.

Impact Details

Since the return value of success is not checked, the operation multisig would need to wait for another delay amount of time before being able to execute the transaction again. Due to this, normal and/or important operations are delayed further (by atleast MINIMUM_DELAY amount of time).

Some points to note:

  1. The failure of the external call could go unnoticed by the operators since executeTransaction() went through successfully.

  2. The operations multisig would need to reach quorum again before being able to queue up the transaction again.

  3. On request of the operations multisig, the community multisig may or may not take over this task since it would require them to reach the expected quorum among themselves in the first place.

  4. If calling the transaction calldata is restricted to only the operations multisig, the community multisig would be of no help.

  5. Overall, there would still be a significant delay caused in the execution of the transaction, which may or may not hurt the protocol or users depending on the reason behind the call.

Vulnerability Details

Here is the whole process:

  1. First, let's take a look at the MINIMUM_DELAY value in the Timelock.sol contract:

  1. Operation multisig queues up a transaction to call callData on target contract using function queueTransaction():

  1. Once the delay has passed, the operations multisig calls function executeTransaction():

  • Note: Line 255 below clears the queue[txHash] value before calling internal function _executeTransaction() on the next line.

  1. In function _executeTransation(), the callData is called on the target contract, which provides the return values to the executeTransaction() function. These values are stored in success and returnData on Line 256 above.

  1. Now if the external call had succeeded, the event TransactionExecuted is emitted and the execution ends. But if the external call failed, there is no check put in place after Line 256 to ensure the whole execution context reverts. Due to this, the event TransactionExecuted is still emitted though the external call did not succeed.

  1. Since the queue[txHash] was cleared previously, the operations multisig would be required to queue the transaction again and wait for another delay amount of time before being able to execute it.

The responsibility should be upto the contract to handle and ensure that failed transactions revert to avoid this issue of further delays in transaction execution.

Mitigation

The operations multisig would not need to queue the transaction again if the value of success is checked for the failed call and the whole execution context is reverted.

Solution: Check the return value for both calls made by operation multisig and community multisig as shown below:

For operations multisig:

For community multisig:

Proof of Concept

Some points to understand the POC:

  • Add the POC to the Timelock.t.sol file

  • Run the test using forge test --fork-url <ETH_MAINNET_RPC_URL> --match-test testReturnValueNotCheckedIssue -vvvvv

  • The use of -vvvvv displays the traces, which shows how the call goes through though we encountered an InvalidDelay error.

  • The POC also demonstrates how queue[txHash] is deleted for a failed external call, due to which executeTransaction() cannot be called again, forcing the operations multisig to queue up the transaction again.

  • The traces/logs have been added below the POC for proof as well as through a screenshot.

  • Note, although in the POC I've demonstrated the problem through a simple transaction (i.e failing call to setDelay() - which succeeds), the issue exists for any reverting external call.

Traces

  • We can observe the event emission of TransactionExecuted() occurring though success is false.

  • The POC also asserts that queue[txHash] is cleared i.e. 0

  • It also demonstrates that executeTransaction cannot be called again.

Last updated

Was this helpful?