Boost _ Folks Finance 33652 - [Smart Contract - Insight] BridgeRouters Unprotected Reversal Function Compromises User Control

Submitted on Thu Jul 25 2024 15:22:10 GMT-0400 (Atlantic Standard Time) by @A2Security for Boost | Folks Finance

Report ID: #33652

Report type: Smart Contract

Report severity: Insight

Target: https://testnet.snowtrace.io/address/0xa9491a1f4f058832e5742b76eE3f1F1fD7bb6837

Impacts:

  • Griefing (e.g. no profit motive for an attacker, but damage to the users or the protocol)

Description

Brief/Intro

The BridgeRouter system allows unauthorized reversal of failed transactions, enabling attackers to disrupt operations and force users to incur unnecessary costs. This vulnerability could lead to financial losses, prevent legitimate transactions.

Vulnerability Details

  • The reverse message functionality in the BridgeRouter system is designed to cancle failed cross-chain messages. Here's how it operates:

  1. When a message fails during execution on the destination chain, it's stored in the BridgeRouter contract as a failed message.

  2. The reverseMessage function in the BridgeRouter can be called with the adapterId and messageId of the failed message: `

  3. This function retrieves the failed message, clears it from storage, and calls the reverseMessage function on the corresponding handler (typically the Hub contract).

   function reverseMessage(uint16 adapterId, bytes32 messageId, bytes memory extraArgs) external payable {
        // some code ..
        try BridgeMessenger(handler).reverseMessage(message, extraArgs) {
            // clear failure and emit message reverse as suceeded
            emit MessageReverseSucceeded(adapterId, message.messageId);
        } catch (bytes memory err) {
            // store and emit message reverse as failed
            failedMessages[adapterId][message.messageId] = message;
            emit MessageReverseFailed(adapterId, message.messageId, err);
        }
    }
  1. In the Hub contract, the _reverseMessage function processes the reversal:

  2. The Hub verifies the token receipt from the original transaction and initiates a token return to the user on the source chain.

  3. To complete the reversal, the Hub calls back to the BridgeRouter to send a cross-chain message returning the tokens.

    function _reverseMessage(Messages.MessageReceived memory message, bytes memory extraArgs) internal override {
        Messages.MessagePayload memory payload = Messages.decodeActionPayload(message.payload);

        // check sender has permission for relevant operations, overriding account id if neccessary
        bytes32 accountId = extraArgs.length == 0 ? payload.accountId : extraArgs.toBytes32(0);
        bool isRegistered =
            accountManager.isAddressRegisteredToAccount(accountId, message.sourceChainId, payload.userAddress);
        if (!isRegistered) {
            revert IAccountManager.NotRegisteredToAccount(accountId, message.sourceChainId, payload.userAddress);
        }
        // some code . .
        sendTokenToUser(
            message.returnAdapterId,
            message.returnGasLimit,
            accountId,
            payload.userAddress,
            SendToken({poolId: poolId, chainId: message.sourceChainId, amount: amount})
        );
    }

    function sendTokenToUser(
        uint16 adapterId,
        uint256 gasLimit,
        bytes32 accountId,
        bytes32 recipient,
        SendToken memory sendToken
    ) internal {
        // some code ..
        // send message (balance for user account already present in bridge router)
 >>       _sendMessage(messageToSend, 0);
    }
  1. The BridgeRouter sends this cross-chain message, using fees from the balance associated with the original user's accountId.

   function sendMessage(Messages.MessageToSend memory message)/*...*/ {

        // check if have sufficient funds to pay fee (can come from existing balance and/or msg.value)
        bytes32 userId = _getUserId(Messages.decodeActionPayload(message.payload));
  >>    uint256 userBalance = balances[userId];
        if (msg.value + userBalance < fee) revert NotEnoughFunds(userId);

        // update user balance considering fee and msg.value
 >>     balances[userId] = userBalance + msg.value - fee;

        // call given adapter to send message
        adapter.sendMessage{value: fee}(message);
    }
  • The core issue lies in the lack of propore access control and the ability for anyone to call the reverseMessage() function in bridgeRouter contract for any failed messages (for allowed actions like deposit/repay ..ect), usurping the rightful decision-making power of the original message sender:

This vulnerability allows malicious actors to force the reversal of transactions against the wishes of the original sender This is problematic for various reasons:

  1. User Autonomy: The users who initiate cross-chain messages are the only ones who should have the authority to decide whether to retry or reverse a failed message. They have the context of their intended operation and are best positioned to make this decision. Moreover, they bear the financial consequences of both the initial transaction and any subsequent actions.

  2. Financial Implications: When a reversal is initiated, the fees for the return message are deducted from the user's balance in the BridgeRouter. This can lead to unexpected costs for the user, especially if the reversal is to a high-fee network like Ethereum mainnet.

  3. Exploitation of Failed Transactions: Even in cases where a transaction fails due to easily rectifiable issues (like slightly insufficient gas), an attacker can force a costly reversal instead of allowing a simple retry.

  4. Smart Contract integration: Contracts interacting with this system may not be designed to handle unexpected reversals, potentially leading to locked funds or corrupted contract states.

  • The lack of restrictions on who can call reverseMessage() and the absence of a mechanism to prioritize the original sender's intentions make this a severe vulnerability in the current system design.

Impact Details

This vulnerability allows malicious users to reverse transactions sent by others, even if those transactions are retryable. When a transaction is reversed:

  1. The original user loses the cost of sending the initial transaction from the spoke chain.

  2. The user incurs the cost of reversing the transaction, which is deducted from their balance on the hub chain.

  3. Users lose control over the decision to retry or reverse their transactions.

  4. For actions that allow reversal, users can be griefed, preventing them from ever successfully retrying their transactions.

The attacker only needs to pay gas fees on the hub chain (Avalanche), which are relatively cheap. This creates an asymmetric situation where the attacker can cause significant financial damage to users at a low cost to themselves, potentially disrupting the entire cross-chain messaging system.

References

  • BridgeRouter.sol

  • Hub.sol

  • SpokeToken.sol

  • CCIPTokenAdapter.sol

Proof of concept

Proof of Concept

We have added a proof of concept, in foundry that forks avalanche fugi and interacts with the deployed version of the protocol in testnet. To run the proof of concept please add the following files under tests. Please also make sure foundry is initialized in the project, and declare the custom remapping @forge-std

First FIle: test/pocs/base_test.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.23;

import "@forge-std/Test.sol";
import "contracts/hub/Hub.sol";
import "contracts/spoke/SpokeToken.sol";
import "contracts/spoke/SpokeCommon.sol";
import "contracts/hub/HubPool.sol";
import "contracts/hub/LoanManager.sol";
import "contracts/bridge/libraries/Messages.sol";
import "contracts/hub/AccountManager.sol";
import "contracts/bridge/BridgeRouter.sol";
import "contracts/bridge/HubAdapter.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

contract baseTest is Test {
    using SafeERC20 for address;
    // Hub public hub;

    uint256 mainnetFork;
    bytes32 public constant PREFIX = "HUB_ADAPTER_V1";
    address constant HUB_ADDRESS = 0xaE4C62510F4d930a5C8796dbfB8C4Bc7b9B62140; // Assuming this is the Hub address
    address constant SPOKE_COMMON = 0x6628cE08b54e9C8358bE94f716D93AdDcca45b00;
    address constant SPOKE_USDC = 0x89df7db4af48Ec7A84DE09F755ade9AF1940420b;
    address constant HUBPOOL_USDC = 0x1968237f3a7D256D08BcAb212D7ae28fEda72c34;
    address constant HUBPOOL_AVAX = 0xd90B7614551E799Cdef87463143eCe2efd4054f9;