IOP _ ThunderNFT 34519 - [Smart Contract - High] users cant withdraw their tokens when specific asse

Submitted on Wed Aug 14 2024 12:53:37 GMT-0400 (Atlantic Standard Time) by @zeroK for IOP | ThunderNFT

Report ID: #34519

Report type: Smart Contract

Report severity: High

Target: https://github.com/ThunderFuel/smart-contracts/tree/main/contracts-v1/pool

Impacts:

  • Permanent freezing of funds

Description

Brief/Intro

the pool.sw contract allow users to deposit and withdraw whitelisted tokens into the pool contract, this is possible by calling the deposit function and withdraw function, however there is an issue when users want to withdraw their tokens, as the withdraw token check if the token is whitelisted or not, and this can cause issue and lock user funds in case if the assetManger contract decide to remove specific token from whitelist, this way users token will be stuck forever.

Vulnerability Details

the deposit function allow users to deposit specific amount for allowed tokens only as shown below:


    /// Deposits the supported asset into this contract
    /// and assign the deposited amount to the depositer as bid balance
    #[storage(read, write), payable]
    fn deposit() {
        let asset_manager_addr = storage.asset_manager.read().unwrap().bits();
        let asset_manager = abi(AssetManager, asset_manager_addr);
        require(asset_manager.is_asset_supported(msg_asset_id()), PoolErrors::AssetNotSupported); //@audit only supported

        let address = msg_sender().unwrap();
        let amount = msg_amount();
        let asset = msg_asset_id();

        let current_balance = _balance_of(address, asset);
        let new_balance = current_balance + amount;
        storage.balance_of.insert((address, asset), new_balance);

        log(Deposit {
            address,
            asset,
            amount
        });
    }

however same check exist in the withdraw function too:

    /// Withdraws the amount of assetId from the contract
    /// and sends to sender if sender has enough balance
    #[storage(read, write)]
    fn withdraw(asset: AssetId, amount: u64) {
        let sender = msg_sender().unwrap();
        let current_balance = _balance_of(sender, asset);
        require(current_balance >= amount, PoolErrors::AmountHigherThanBalance);

        let asset_manager_addr = storage.asset_manager.read().unwrap().bits();
        let asset_manager = abi(AssetManager, asset_manager_addr);
        require(asset_manager.is_asset_supported(asset), PoolErrors::AssetNotSupported); //@audit prevent if asset removed

        let new_balance = current_balance - amount;
        storage.balance_of.insert((sender, asset), new_balance);

        transfer(sender, asset, amount);

        log(Withdrawal {
            address: sender,
            asset,
            amount,
        });
    }

this can lead to stuck of funds since asset can be added and removed from whitelist using the add/remove asset function below:

    /// Adds asset into supported assets vec
    #[storage(read, write)]
    fn add_asset(asset: AssetId) {
        storage.owner.only_owner();
        require(
            !_is_asset_supported(asset),
            AssetManagerErrors::AssetAlreadySupported
        );

        storage.is_supported.insert(asset, true);
        storage.assets.push(asset);
    }

    /// Removes asset from supported assets vec
    #[storage(read, write)]
    fn remove_asset(index: u64) {
        storage.owner.only_owner();
        let asset = storage.assets.remove(index);
        storage.is_supported.insert(asset, false);
    }

if owner called the remove function while the tokens exist in the pool then all users with this token can not withdraw back their tokens amount.

we set the severity to high for reason below:

  • the owner can add back the token again by calling add_asset function but its not recommended behavior.

Impact Details

users amount can be temporary freez in pool contract.

References

there is no need to check for token whitelisted tokens when user withdraw since deposit allow to deposit whitelist tokens only

Proof of concept

Proof of Concept

the POC show that the token can be added and removed any time:

//run this test below asse_manger/src/main.sw

#[test]

fn test_attack_asset() {
    let assetManger = abi(AssetManager, CONTRACT_ID);
    assetManger.initialize();
    let asset_id = AssetId::default();
    assetManger.add_asset(asset_id);

    //users deposit between this gap

    assetManger.remove_asset(0);

}

Last updated