MEDIUM severity: Missing duplicate-token check. The function `containTokenset` is designed to verify that a larger token set contains all tokens from a smaller set with sufficient amounts. However, it does not check for duplicate tokens in the smaller set. This can lead to incorrect validation when the smaller set contains duplicate tokens with a...

Vulnerable code:
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.25;
import "./Interface.sol";
import {AssetController} from "./AssetController.sol";
import {Utils} from './Utils.sol';

// import "forge-std/console.sol";

contract AssetFeeManager is AssetController, IAssetFeeManager {
    Request[] burnFeeRequests;

    event AddBurnFeeRequest(uint nonce);
    event RejectBurnFeeRequest(uint nonce);
    event ConfirmBurnFeeRequest(uint nonce);

    function setFee(uint256 assetID, uint256 fee) external onlyOwner {
        IAssetFactory factory = IAssetFactory(factoryAddress);
        IAssetToken assetToken = IAssetToken(factory.assetTokens(assetID));
        require(assetToken.feeCollected(), "has fee not collected");
        require(assetToken.hasRole(assetToken.FEEMANAGER_ROLE(), address(this)), "not a fee manager");
        assetToken.setFee(fee);
    }

    function collectFeeTokenset(uint256 assetID) external onlyOwner {
        IAssetFactory factory = IAssetFactory(factoryAddress);
        IAssetToken assetToken = IAssetToken(factory.assetTokens(assetID));
        require(assetToken.hasRole(assetToken.FEEMANAGER_ROLE(), address(this)), "not a fee manager");
        require(assetToken.rebalancing() == false, "is rebalancing");
        require(assetToken.issuing() == false, "is issuing");
        assetToken.collectFeeTokenset();
    }

    function getBurnFeeRequestLength() external view returns (uint256) {
        return burnFeeRequests.length;
    }

    function getBurnFeeRequest(uint256 nonce) external view returns (Request memory) {
        return burnFeeRequests[nonce];
    }

    function addBurnFeeRequest(uint256 assetID, OrderInfo memory orderInfo) external onlyOwner returns (uint256) {
        IAssetFactory factory = IAssetFactory(factoryAddress);
        address assetTokenAddress = factory.assetTokens(assetID);
        IAssetToken assetToken = IAssetToken(assetTokenAddress);
        address swapAddress = factory.swaps(assetID);
        ISwap swap = ISwap(swapAddress);
        require(assetToken.hasRole(assetToken.FEEMANAGER_ROLE(), address(this)), "not a fee manager");
        require(assetToken.burningFee() == false, "is burning fee");
        require(swap.checkOrderInfo(orderInfo) == 0, "order not valid");
        Token[] memory sellTokenset = Utils.muldivTokenset(orderInfo.order.inTokenset, orderInfo.order.inAmount, 10**8);
        require(Utils.containTokenset(assetToken.getFeeTokenset(), sellTokenset), "not enough fee to sell");
        for (uint i = 0; i < orderInfo.order.outTokenset.length; i++) {
            require(Utils.stringToAddress(orderInfo.order.outAddressList[i]) == factory.vault(), "fee receiver not match");
            require(bytes32(bytes(orderInfo.order.outTokenset[i].chain)) == bytes32(bytes(factory.chain())), "outTokenset chain not match");
        }
        swap.addSwapRequest(orderInfo, false, true);
        burnFeeRequests.push(Request({
            nonce: burnFeeRequests.length,
            requester: msg.sender,
            assetTokenAddress: assetTokenAddress,
            amount: 0,
            swapAddress: swapAddress,
            orderHash: orderInfo.orderHash,
            status: RequestStatus.PENDING,
            requestTimestamp: block.timestamp,
            issueFee: 0
        }));
        assetToken.lockBurnFee();
        emit AddBurnFeeRequest(burnFeeRequests.length - 1);
        return burnFeeRequests.length - 1;
    }

    function rejectBurnFeeRequest(uint nonce) external onlyOwner {
        require(nonce < burnFeeRequests.length, "nonce too large");
        Request memory burnFeeRequest = burnFeeRequests[nonce];
        require(burnFeeRequest.status == RequestStatus.PENDING);
        ISwap swap = ISwap(burnFeeRequest.swapAddress);
        SwapRequest memory swapRequest = swap.getSwapRequest(burnFeeRequest.orderHash);
        require(swapRequest.status == SwapRequestStatus.REJECTED || swapRequest.status == SwapRequestStatus.CANCEL || swapRequest.status == SwapRequestStatus.FORCE_CANCEL);
        IAssetToken assetToken = IAssetToken(burnFeeRequest.assetTokenAddress);
        assetToken.unlockBurnFee();
        burnFeeRequests[nonce].status = RequestStatus.REJECTED;
        emit RejectBurnFeeRequest(nonce);
    }

    function confirmBurnFeeRequest(uint nonce, OrderInfo memory orderInfo, bytes[] memory inTxHashs) external onlyOwner {
        require(nonce < burnFeeRequests.length, "nonce too large");
        Request memory burnFeeRequest = burnFeeRequests[nonce];
        checkRequestOrderInfo(burnFeeRequest, orderInfo);
        require(burnFeeRequest.status == RequestStatus.PENDING);
        ISwap swap = ISwap(burnFeeRequest.swapAddress);
        SwapRequest memory swapRequest = swap.getSwapRequest(burnFeeRequest.orderHash);
        require(swapRequest.status == SwapRequestStatus.MAKER_CONFIRMED);
        swap.confirmSwapRequest(orderInfo, inTxHashs);
        IAssetToken assetToken = IAssetToken(burnFeeRequest.assetTokenAddress);
        Order memory order = orderInfo.order;
        Token[] memory sellTokenset = Utils.muldivTokenset(order.inTokenset, order.inAmount, 10**8);
        assetToken.burnFeeTokenset(sellTokenset);
        burnFeeRequests[nonce].status = RequestStatus.CONFIRMED;
        assetToken.unlockBurnFee();
        emit ConfirmBurnFeeRequest(nonce);
    }
}
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.25;
import './Interface.sol';
import "@openzeppelin/contracts/utils/Arrays.sol";

library Utils {
    function stringToAddress(string memory str) internal pure returns (address) {
        bytes memory strBytes = bytes(str);
        require(strBytes.length == 42, "Invalid address length");
        bytes memory addrBytes = new bytes(20);

        for (uint i = 0; i < 20; i++) {
            addrBytes[i] = bytes1(hexCharToByte(strBytes[2 + i * 2]) * 16 + hexCharToByte(strBytes[3 + i * 2]));
        }

        return address(uint160(bytes20(addrBytes)));
    }

    function hexCharToByte(bytes1 char) internal pure returns (uint8) {
        uint8 byteValue = uint8(char);
        if (byteValue >= uint8(bytes1('0')) && byteValue <= uint8(bytes1('9'))) {
            return byteValue - uint8(bytes1('0'));
        } else if (byteValue >= uint8(bytes1('a')) && byteValue <= uint8(bytes1('f'))) {
            return 10 + byteValue - uint8(bytes1('a'));
        } else if (byteValue >= uint8(bytes1('A')) && byteValue <= uint8(bytes1('F'))) {
            return 10 + byteValue - uint8(bytes1('A'));
        }
        revert("Invalid hex character");
    }

    function containTokenset(Token[] memory a, Token[] memory b) internal pure returns (bool) {
        uint k;
        for (uint i = 0; i < b.length; i++) {
            k = a.length;
            for (uint j = 0; j < a.length; j++) {
                if (isSameToken(b[i], a[j])) {
                    if (a[j].amount < b[i].amount) {
                        return false;
                    }
                    k = j;
                    break;
                }
            }
            if (k == a.length) {
                return false;
            }
        }
        return true;
    }

    function subTokenset(Token[] memory a_, Token[] memory b) internal pure returns (Token[] memory) {
        Token[] memory a = copyTokenset(a_);
        uint newLength = a.length;
        uint k;
        for (uint i = 0; i < b.length; i++) {
            k = a.length;
            for (uint j = 0; j < a.length; j++) {
                if (isSameToken(b[i], a[j])) {
                    require(a[j].amount >= b[i].amount, "a.amount less than b.amount");
                    a[j].amount -= b[i].amount;
                    if (a[j].amount == 0) {
                        newLength -= 1;
                    }
                    k = j;
                    break;
                }
            }
            require(k < a.length, "a not contains b");
        }
        Token[] memory res = new Token[](newLength);
        k = 0;
        for (uint i = 0; i < a.length; i++) {
            if (a[i].amount > 0) {
                res[k++] = a[i];
            }
        }
        return res;
    }

    function addTokenset(Token[] memory a_, Token[] memory b_) internal pure returns (Token[] memory) {
        Token[] memory a = copyTokenset(a_);
        Token[] memory b = copyTokenset(b_);
        uint k;
        uint newCnt = 0;
        for (uint i = 0; i < b.length; i++) {
            k = a.length;
            for (uint j = 0; j < a.length; j++) {
                if (isSameToken(a[j], b[i])) {
                    a[j].amount += b[i].amount;
                    k = j;
                    break;
                }
            }
            if (k == a.length) {
                if (newCnt < i) {
                    b[newCnt] = b[i];
                }
                newCnt += 1;
            }
        }
        Token[] memory res = new Token[](a.length + newCnt);
        for (uint i = 0; i < a.length; i++) {
            res[i] = a[i];
        }
        for (uint i = 0; i < newCnt; i++) {
            res[a.length + i] = b[i];
        }
        return res;
    }

    function copyTokenset(Token[] memory a) internal pure returns (Token[] memory) {
        Token[] memory b = new Token[](a.length);
        for (uint i = 0; i < a.length; i++) {
            b[i] = Token({
                chain: a[i].chain,
                symbol: a[i].symbol,
                addr: a[i].addr,
                decimals: a[i].decimals,
                amount: a[i].amount
            });
        }
        return b;
    }

    function muldivTokenset(Token[] memory a_, uint mul_factor, uint div_facotr) internal pure returns (Token[] memory) {
        Token[] memory a = copyTokenset(a_);
        for (uint i = 0; i < a.length; i++) {
            a[i].amount = a[i].amount * mul_factor / div_facotr;
        }
        return a;
    }

    function isSameToken(Token memory a, Token memory b) internal pure returns (bool) {
        return calcTokenHash(a) == calcTokenHash(b);
    }

    function calcTokenHash(Token memory token) internal pure returns (bytes32) {
        return keccak256(abi.encodePacked(token.chain, token.symbol, token.addr, token.decimals));
    }

    function hasDuplicates(Token[] memory a) internal pure returns (bool) {
        uint256[] memory b = new uint256[](a.length);
        for (uint i = 0; i < a.length; i++) {
            b[i] = uint256(calcTokenHash(a[i]));
        }
        uint256[] memory c = Arrays.sort(b);
        for (uint i = 0; i < c.length - 1; i++) {
            if (c[i] == c[i+1]) {
                return true;
            }
        }
        return false;
    }
}

Vulnerability type: CWE-707|CWE-20|CWE-1288