Boost _ Folks Finance 34066 - [Smart Contract - Medium] Account Creation Front-Running Vulnerability Leading to Gas Fee Theft

Submitted on Mon Aug 05 2024 04:00:21 GMT-0400 (Atlantic Standard Time) by @OxG0P1 for Boost | Folks Finance

Report ID: #34066

Report type: Smart Contract

Report severity: Medium

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

Impacts:

  • Theft of gas

Description

Brief/Intro

A vulnerability has been identified where an attacker can front-run or accidentally intercept the account creation transaction of users, leading to the theft of users' gas fees.

Vulnerability Details

Users can create an account by inputting a desired accountId through the spokeCommon contract:

function createAccount(
    Messages.MessageParams memory params,
    bytes32 accountId,
    bytes32 refAccountId
) external payable nonReentrant {
    _doOperation(params, Messages.Action.CreateAccount, accountId, abi.encodePacked(refAccountId));
}

Since account management is conducted on the hub chain, the account creation transaction must be relayed by bridges. To facilitate this, a bridge router routes the payload to the corresponding adapter and receives the payload on the hub chain. These bridge routers maintain user balances to cover the fees required by the adapter to relay messages. These balances are stored in a mapping of userId to amount.

function sendMessage(
    Messages.MessageToSend memory message
) external payable override onlyRole(MESSAGE_SENDER_ROLE) {
    // Check if valid adapter and retrieve
    IBridgeAdapter adapter = getAdapter(message.params.adapterId);

    // Verify if messager matches caller
    address messager = Messages.convertGenericAddressToEVMAddress(message.sender);
    if (messager != msg.sender) revert SenderDoesNotMatch(messager, msg.sender);

    // Get the fee from the adapter
    uint256 fee = adapter.getSendFee(message);

    // Check if sufficient funds are available (from 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 the adapter to send the message
    adapter.sendMessage{ value: fee }(message);
}

In the context of the bridge router deployed on the spoke chain, the userId is the msg.sender, which is the user's address:

function _getUserId(Messages.MessagePayload memory payload) internal pure override returns (bytes32) {
    return payload.userAddress;
}

However, in the context of the bridge router on the hub chain, the userId is the accountId specified by the user:

function _getUserId(Messages.MessagePayload memory payload) internal pure override returns (bytes32) {
    return payload.accountId;
}

Some bridges allow users to send more value than the fee required to relay the message. The extra amount, which is value - fee, is stored on the hub chain:

if (msg.value > 0) {
    bytes32 userId = _getUserId(Messages.decodeActionPayload(message.payload));
    balances[userId] += msg.value;
}

This balance can be used for round-trip messages. The vulnerability arises when an attacker front-runs the account creation transaction of a user and creates an account with the same accountId as the victim. The extra amount, other than the fee that the bridge holds, will then be credited to the attacker's account. Consequently, the victim's account creation transaction will be stored as a failed message.

Impact Details

The primary impact is the loss of funds for the user, specifically the funds used to relay bridge messages. This loss can occur every time a user attempts to create an account, and the extent of the loss depends on the extra value sent by the user beyond the required fee.

References

https://github.com/Folks-Finance/folks-finance-xchain-contracts/blob/fb92deccd27359ea4f0cf0bc41394c86448c7abb/contracts/bridge/BridgeRouterHub.sol#L25-L27

https://github.com/Folks-Finance/folks-finance-xchain-contracts/blob/fb92deccd27359ea4f0cf0bc41394c86448c7abb/contracts/bridge/BridgeRouterHub.sol#L25-L28

https://github.com/Folks-Finance/folks-finance-xchain-contracts/blob/fb92deccd27359ea4f0cf0bc41394c86448c7abb/contracts/bridge/BridgeRouter.sol#L107-L110

Proof of concept

Proof of Concept

Alice (Victim): Wants to create an account with a specific accountId and send extra funds to cover bridge fees.

Bob (Attacker): Monitors pending transactions, identifies Alice’s transaction, and creates an account with the same accountId just before Alice's transaction is processed.

  1. Alice's Transaction Preparation:

    • Alice prepares a transaction to create an account with a specific accountId and sends an extra amount of value (e.g., 2 ETH) to cover the bridge fee.

    • Transaction details:

      function createAccount(
          Messages.MessageParams memory params,
          bytes32 accountId,
          bytes32 refAccountId
      ) external payable nonReentrant {
          _doOperation(params, Messages.Action.CreateAccount, accountId, abi.encodePacked(refAccountId));
      }
    • Alice's transaction code:

      Messages.MessageParams memory params = /* ... */;
      bytes32 accountId = keccak256(abi.encodePacked("aliceAccountId"));
      bytes32 refAccountId = keccak256(abi.encodePacked("referenceAccountId"));
      
      bridgeRouter.createAccount{ value: 2 ether }(params, accountId, refAccountId);
  2. Bob Monitors Pending Transactions:

    • Bob uses tools to monitor pending transactions in the mempool, looking for createAccount transactions.

    • Upon identifying Alice’s transaction with the desired accountId, Bob prepares to front-run the transaction.

  3. Bob Front-Runs the Transaction:

    • Bob quickly sends a transaction to create an account with the same accountId specified by Alice. Bob ensures his transaction has a higher gas price to be mined before Alice's transaction.

    • Bob's transaction details:

      function createAccount(
          Messages.MessageParams memory params,
          bytes32 accountId,
          bytes32 refAccountId
      ) external payable nonReentrant {
          _doOperation(params, Messages.Action.CreateAccount, accountId, abi.encodePacked(refAccountId));
      }
    • Bob's transaction code:

      Messages.MessageParams memory params = /* ... */;
      bytes32 accountId = keccak256(abi.encodePacked("aliceAccountId"));
      bytes32 refAccountId = keccak256(abi.encodePacked("referenceAccountId"));
      
      bridgeRouter.createAccount{ value: 0.1 ether }(params, accountId, refAccountId);
  4. Alice's Transaction Execution:

    • Alice's transaction is now processed after Bob's transaction.

    • The bridge router on the spoke chain identifies the msg.sender as the user address and maps the balances accordingly.

    • On the hub chain, the accountId is used to identify the user.

    • Since Bob's transaction has already created an account with the specified accountId, Alice’s transaction fails, and the extra funds intended for the bridge fee are now associated with Bob’s account.

  5. Resulting State:

    • Bob successfully creates an account with Alice's intended accountId and receives the extra funds sent by Alice.

    • Alice's transaction fails, and the funds meant to cover the bridge fees are effectively stolen by Bob.

Note : This attack is especially profitable when the attacker front-runs the transaction from the hub chain. In this scenario, there is no bridge fee to be paid on the hub chain because there is no need to relay the message. The attacker can then withdraw the balance associated with the accountId on any other spoke chain.

Last updated