Boost _ Folks Finance 33534 - [Smart Contract - Medium] denial of service vulnerability and possible griefing in cross-chain account creation

Submitted on Mon Jul 22 2024 16:38:28 GMT-0400 (Atlantic Standard Time) by @A2Security for Boost | Folks Finance

Report ID: #33534

Report type: Smart Contract

Report severity: Medium

Target: https://testnet.snowtrace.io/address/0x3324B5BF2b5C85999C6DAf2f77b5a29aB74197cc?utm_source=immunefi

Impacts:

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

Description

Description

The implementation of the createAccount() function in the spoke and hub contract allows a potential Denial of Service (DoS) attack, where an attacker can exploit the account creation process across different chains. When a user attempts to create an account from a spoke chain, an attacker can listen for this transaction on the source chain and preemptively call the createAccount() function on the hub chain using the same account ID. This results in the victim's transaction reverting, as the attacker effectively blocks the user's ability to create an account with the intended ID.

Impact

This vulnerability can have catastrophic consequences, allowing attackers to prevent any or all users from creating cross-chain accounts. Users who send a cross-chain message will not only be unable to create the desired account but also risk losing the funds already paid to cover the cross-chain delivery and execution of the message. This leads to a significant loss of trust in the protocol, as users may find themselves unable to access the services they expected, further impacting user retention and engagement.

Code Snippet

Here’s how the createAccount() function is invoked on the spoke contract:

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

In this code snippet, the createAccount() function accepts transaction parameters and unique identifiers for the account being created. The critical part is the call to _doOperation, which is responsible for creating a message that will be forwarded through the bridging protocol (such as Wormhole or CCIP) to the hub chain. As we can see the accountId is a free parameter that the user can set it to anything want to.

The message will be then recieved in the Hub contract and the AccountManager where it will be processed by the createAccount() function, and if this accountId is already reserved the transaction will revert.

    function createAccount(
        bytes32 accountId,
        uint16 chainId,
        bytes32 addr,
        bytes32 refAccountId
    ) external override onlyRole(HUB_ROLE) {
        // check account is not already created (empty is reserved for admin)
        if (isAccountCreated(accountId) || accountId == bytes32(0)) revert AccountAlreadyCreated(accountId);

        // check address is not already registered
        if (isAddressRegistered(chainId, addr)) revert AddressPreviouslyRegistered(chainId, addr);

        // check referrer is well defined
>>        if (!(isAccountCreated(refAccountId) || refAccountId == bytes32(0)))
>>           revert InvalidReferrerAccount(refAccountId);

        // create account
        accounts[accountId] = true;
        accountAddresses[accountId][chainId] = AccountAddress({ addr: addr, invited: false, registered: true });
        registeredAddresses[addr][chainId] = accountId;

        emit CreateAccount(accountId, chainId, addr, refAccountId);
    }

Tools Used

Manual review

1Implement a mechanism to hash the user address (msg.sender in the source chain) along with the chain ID when generating account IDs. This will ensure that account IDs are unique across users and chains, preventing the possibility of attackers blocking legitimate account creation attempts.

Proof of concept

Proof of Concept

The vulnerability can be demonstrated as follows:

  1. A user sends a transaction to create an account from a spoke chain (e.g., Spoke A).

  2. An attacker observes this transaction on Spoke A and calls the createAccount() function on the hub chain (e.g., Hub B) using the same account ID.

  3. When the user's transaction is forwarded to the hub chain, it reverts because the account ID has already been registered by the attacker.

Last updated