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:
The failure of the external call could go unnoticed by the operators since
executeTransaction()
went through successfully.The operations multisig would need to reach quorum again before being able to queue up the transaction again.
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.
If calling the transaction calldata is restricted to only the operations multisig, the community multisig would be of no help.
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:
First, let's take a look at the
MINIMUM_DELAY
value in the Timelock.sol contract:
Operation multisig queues up a transaction to call
callData
ontarget
contract using functionqueueTransaction()
:
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.
In function _executeTransation(), the
callData
is called on thetarget
contract, which provides the return values to the executeTransaction() function. These values are stored insuccess
andreturnData
on Line 256 above.
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 eventTransactionExecuted
is still emitted though the external call did not succeed.
Since the
queue[txHash]
was cleared previously, the operations multisig would be required to queue the transaction again and wait for anotherdelay
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 anInvalidDelay
error.The POC also demonstrates how
queue[txHash]
is deleted for a failed external call, due to whichexecuteTransaction()
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 thoughsuccess
is false.The POC also asserts that
queue[txHash]
is cleared i.e. 0It also demonstrates that
executeTransaction
cannot be called again.
Last updated