FuelLabs / fuel-bridge

The canonical Fuel bridge mono repo.

Home Page:https://app.fuel.network/portal/bridge

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

TOB-FUEL-4: `claim_refund` does not refund the original asset

xgreenx opened this issue · comments

Description

Funds in the FuelERC20Gateway can be drained due to returning an incorrect asset when claiming a refund.
Refunds are registered when there is a mismatch between the L1 and L2 assets.

Refunds are accounted for in the register_refund function and stored in the storage variable refund_amounts.

Figure 4.2: The register_refund function in bridge-fungible-token/bridge-fungible-token/src/bridge_fungible_token.sw

// Storage-dependant private functions
#[storage(write)]
fn register_refund(from: b256, asset: b256, amount: b256) {
    storage.refund_amounts.get(from).insert(asset, amount);
    log(RefundRegisteredEvent {
        from,
        asset,
        amount,
    });
}

For a user to withdraw registered refunds the claim_refund function should be called. This function however incorrectly sets as the token to be withdrawn the BRIDGED_TOKEN instead of the asset variable.

Figure 4.3: The claim_refund function in bridge-fungible-token/bridge-fungible-token/src/bridge_fungible_token.sw

130    fn claim_refund(originator: b256, asset: b256) {
131       let stored_amount =
storage.refund_amounts.get(originator).get(asset).read();
132       require(stored_amount != ZERO_B256,
BridgeFungibleTokenError::NoRefundAvailable);
133
134       // reset the refund amount to 0
135       storage.refund_amounts.get(originator).insert(asset, ZERO_B256);
136
137       // send a message to unlock this amount on the base layer gateway contract
138       send_message(BRIDGED_TOKEN_GATEWAY, encode_data(originator, stored_amount,
BRIDGED_TOKEN), 0);
139    }

This allows an attacker to deposit any fake asset and to withdraw the true asset when claiming a refund.

Figure 4.4: The finalizeWithdrawal function in fuel-v2-contracts/contracts/messaging/gateway/FuelERC20Gateway.sol

    function finalizeWithdrawal(
        address to,
        address tokenId,
        uint256 amount
    ) external payable whenNotPaused onlyFromPortal {
        require(amount > 0, "Cannot withdraw zero");
        bytes32 fuelTokenId = messageSender();

        //reduce deposit balance and transfer tokens (math will underflow if amount is larger than allowed)
        _deposits[tokenId][fuelTokenId] = _deposits[tokenId][fuelTokenId] - amount;
        IERC20Upgradeable(tokenId).safeTransfer(to, amount);

        //emit event for successful token withdraw
        emit Withdrawal(bytes32(uint256(uint160(to))), tokenId, fuelTokenId, amount);
    }

This is possible because there is no deposit accounting per user in the FuelERC20Gateway which allows for withdrawals to different addresses other than the depositors.

Exploit Scenario

Alice creates a fake token and bridges 10K FAKE while specifying the Fuel ETH token id on the Fuel chain. Because the bridged asset is not appropriate, Alice is able to claim a refund. Due to the error, she is able to refund 10K ETH on Ethereum.

Recommendations

Short term, update the encoded data to include the deposited asset instead of the bridge asset. Alternatively, the gateway could be re-designed to not require specifying the Fuel token id on Ethereum which could eliminate the need for refunds.
Long term, pay extra attention to the intersection between blockchains. Make sure these are well-documented and tested, since they are typically harder to test and more prone to errors.

Fixed by #4, duplicate of #5