sherlock-audit / 2022-10-union-finance-judging

4 stars 1 forks source link

hansfriese - `AssetManager.removeAdapter()` doesn't update `withdrawSeq` after removing an adapter. #139

Closed sherlock-admin closed 2 years ago

sherlock-admin commented 2 years ago

hansfriese

high

AssetManager.removeAdapter() doesn't update withdrawSeq after removing an adapter.

Summary

AssetManager.removeAdapter() doesn't update withdrawSeq after removing an adapter.

Vulnerability Detail

In the AssetManager.sol, it stores all market adapters using moneyMarkets and priority sequence of money market indices using withdrawSeq.

As we can see from addAdapter(), it adds the index to withdrawSeq.

    function addAdapter(address adapterAddress) external override onlyAdmin {
        bool isExist = false;
        uint256 moneyMarketsLength = moneyMarkets.length;

        for (uint256 i = 0; i < moneyMarketsLength; i++) {
            if (adapterAddress == address(moneyMarkets[i])) isExist = true;
        }

        if (!isExist) {
            moneyMarkets.push(IMoneyMarketAdapter(adapterAddress));
            withdrawSeq.push(moneyMarkets.length - 1);
        }

        approveAllTokensMax(adapterAddress);
    }

But in the removeAdapter(), it doesn't update withdrawSeq properly.

    function removeAdapter(address adapterAddress) external override onlyAdmin {
        bool isExist = false;
        uint256 index;
        uint256 moneyMarketsLength = moneyMarkets.length;

        for (uint256 i = 0; i < moneyMarketsLength; i++) {
            if (adapterAddress == address(moneyMarkets[i])) {
                isExist = true;
                index = i;
                break;
            }
        }

        if (isExist) {
            moneyMarkets[index] = moneyMarkets[moneyMarketsLength - 1];
            moneyMarkets.pop();
        }
    }

So the below scenario would be possible.

    uint256 withdrawSeqLength = withdrawSeq.length;
    // iterate markets according to defined sequence and withdraw
    for (uint256 i = 0; i < withdrawSeqLength && remaining > 0; i++) {
        IMoneyMarketAdapter moneyMarket = moneyMarkets[withdrawSeq[i]];
        if (!moneyMarket.supportsToken(token)) continue;

        uint256 supply = moneyMarket.getSupply(token);
        if (supply == 0) continue;

        uint256 withdrawAmount = supply < remaining ? supply : remaining;
        remaining -= withdrawAmount;
        moneyMarket.withdraw(token, account, withdrawAmount);
    }
    function setWithdrawSequence(uint256[] calldata newSeq) external override onlyAdmin {
        if (newSeq.length != withdrawSeq.length) revert NotParity();
        withdrawSeq = newSeq;
    }

Impact

AssetManager.withdraw() will revert after some adapters are removed from moneyMarkets.

Code Snippet

https://github.com/sherlock-audit/2022-10-union-finance/blob/main/union-v2-contracts/contracts/asset/AssetManager.sol#L440-L457

Tool used

Manual Review

Recommendation

We should remove the last index from withdrawSeq in removeAdapter() like below.

    function removeAdapter(address adapterAddress) external override onlyAdmin {
        bool isExist = false;
        uint256 index;
        uint256 moneyMarketsLength = moneyMarkets.length;

        for (uint256 i = 0; i < moneyMarketsLength; i++) {
            if (adapterAddress == address(moneyMarkets[i])) {
                isExist = true;
                index = i;
                break;
            }
        }

        if (isExist) {
            moneyMarkets[index] = moneyMarkets[moneyMarketsLength - 1];
            moneyMarkets.pop();

            // remove moneyMarketsLength-1 from withdrawSeq
            for (uint256 i = 0; i < moneyMarketsLength; i++) {
                if (withdrawSeq[i] == moneyMarketsLength - 1) {
                    withdrawSeq[i] = withdrawSeq[moneyMarketsLength - 1];
                    withdrawSeq.pop();
                    break;
                }
            }
        }
    }

The above approach doesn't keep the original order after removing an adapter and we might try another method for that.

Duplicate of #76