#43274 [SC-Low] `TRANSFER_NATIVE` Command in Dispatcher Does Not Check Return Value of Low-Level Call

Submitted on Apr 4th 2025 at 08:28:32 UTC by @Coyote25049 for Audit Comp | Spectra Finance

  • Report ID: #43274

  • Report Type: Smart Contract

  • Report severity: Low

  • Target: https://github.com/immunefi-team/Spectra-Audit-Competition/blob/main/src/router/Dispatcher.sol

  • Impacts:

    • Direct theft of any user funds, whether at-rest or in-motion, other than unclaimed yield

Description

Location:

  • https://github.com/immunefi-team/Spectra-Audit-Competition/blob/main/src/router/Dispatcher.sol#L483-L486

Finding description and impact

The _dispatch function in the Dispatcher.sol contract handles various commands. Among these, the TRANSFER_NATIVE command (L483-L486) is used to send native ETH to a specified recipient. This command uses a low-level call to perform the transfer: (bool success, ) = payable(recipient).call{value: amount}("");

However, the code completely fails to check the returned success boolean after executing the call.

The low-level call will return false instead of reverting in the following situations:

  1. The recipient contract does not have a receive() or fallback() payable function to receive ETH.

  2. The recipient contract's receive() or fallback() function execution fails (e.g., insufficient gas or explicit revert).

  3. The value being sent exceeds the recipient's balance limit (although uncommon).

  4. The call depth reaches 1024.

If call returns false, it means that the ETH transfer actually failed, and the funds were not sent to the recipient. However, since the Dispatcher contract does not check success, it will continue to execute subsequent commands (if they exist in a multi-command sequence) as if the transfer had succeeded.

Impact: This can lead to an inconsistency between the protocol's internal state (which assumes the transfer has occurred) and the actual on-chain state of funds (ETH is still in the Dispatcher contract). This state inconsistency can be exploited in complex transaction sequences involving multi-step operations:

  • Loss of Funds: Subsequent operations may be calculated or have state updates based on the incorrect assumption that "ETH has been sent," leading to a loss of funds for the protocol or users. For example, if a subsequent command relies on the recipient having received the ETH, but they haven't, it could trigger an incorrect logical path.

  • Logic Errors: The protocol may enter an unexpected or inconsistent state, disrupting its core functionality or invariants.

Proof of Concept

  1. A user constructs a transaction sequence containing a TRANSFER_NATIVE command, with the target recipient being a contract address that does not have a receive() or fallback() payable function.

  2. The user calls the Router to execute this sequence.

  3. The _dispatch function executes the TRANSFER_NATIVE command.

  4. payable(recipient).call{value: amount}("") is called. Because the target contract cannot receive ETH, the call fails, returning success = false.

  5. The Dispatcher contract does not check success and continues to execute the next command in the sequence.

  6. Subsequent commands execute based on the incorrect assumption that the ETH transfer was successful, potentially leading to:

    • Incorrect balance calculations.

    • Incorrect permission grants.

    • Incorrect event triggering.

    • The protocol entering an inconsistent state.

Or:

  1. A user constructs a transaction sequence containing a TRANSFER_NATIVE command, with the target recipient being a malicious contract whose receive() function reverts.

  2. The user calls the Router to execute this sequence.

  3. The _dispatch function executes the TRANSFER_NATIVE command.

  4. payable(recipient).call{value: amount}("") is called. The target contract's receive() function reverts, causing call to return success = false.

  5. The Dispatcher contract does not check success and continues executing subsequent commands, potentially leading to state inconsistencies or loss of funds.

After executing the low-level call, the returned success value must be checked, and the transaction should revert if it fails.

Note: _resolveTokenValue needs to handle the case of native ETH, which may require passing a special address (such as 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE or address(0)) to represent ETH, or modifying the logic of _resolveTokenValue. If amount comes directly from abi.decode and does not need to resolve CONTRACT_BALANCE, it can be used directly. The above fix assumes that _resolveTokenValue can correctly handle amount resolution for native ETH.

By adding if (!success) { revert CallFailed(); }, you ensure that the contract only continues to execute if the ETH was successfully sent, thereby maintaining the consistency of the protocol state and the security of funds.

Proof of Concept

Test

forge test -vv --match-path "test/DispatcherNativeTransferPOC.sol"

Result

POC

Was this helpful?