msc-smart-contract-auditing/vulnerable-functions-base huggingface dataset:
Vulnerability: Highest bidder can withdraw his collateral due to a missing check in _cancelAllBids
Severity: high

Description:
A bidder with the highest bid cannot cancel his bid since this would break the auction. A check to ensure this was implemented in `_cancelBid`.\\nHowever, this check was not implemented in `_cancelAllBids`, allowing the highest bidder to withdraw his collateral and win the auction for free.\\nThe highest bidder should not be able to cancel his bid, since this would break the entire auction mechanism.\\nIn `_cancelBid` we can find a require check that ensures this:\\n```\\n        require(\\n            bidder != l.highestBids[tokenId][round].bidder,\\n            'EnglishPeriodicAuction: Cannot cancel bid if highest bidder'\\n        );\\n```\\n\\nYet in `_cancelAllBids`, this check was not implemented.\\n```\\n     * @notice Cancel bids for all rounds\\n     */\\n    function _cancelAllBids(uint256 tokenId, address bidder) internal {\\n        EnglishPeriodicAuctionStorage.Layout\\n            storage l = EnglishPeriodicAuctionStorage.layout();\\n\\n        uint256 currentAuctionRound = l.currentAuctionRound[tokenId];\\n\\n        for (uint256 i = 0; i <= currentAuctionRound; i++) {\\n            Bid storage bid = l.bids[tokenId][i][bidder];\\n\\n            if (bid.collateralAmount > 0) {\\n                // Make collateral available to withdraw\\n                l.availableCollateral[bidder] += bid.collateralAmount;\\n\\n                // Reset collateral and bid\\n                bid.collateralAmount = 0;\\n                bid.bidAmount = 0;\\n            }\\n        }\\n    }\\n```\\n\\nExample: User Bob bids 10 eth and takes the highest bidder spot. Bob calls `cancelAllBidsAndWithdrawCollateral`.\\nThe `_cancelAllBids` function is called and this makes all the collateral from all his bids from every round available to Bob. This includes the current round `<=` and does not check if Bob is the current highest bidder. Nor is `l.highestBids[tokenId][round].bidder` reset, so the system still has Bob as the highest bidder.\\nThen `_withdrawCollateral` is automatically called and Bob receives his 10 eth back.\\nThe auction ends. If Bob is still the highest bidder, he wins the auction and his bidAmount of 10 eth is added to the availableCollateral of the oldBidder.\\nIf there currently is more than 10 eth in the contract (ongoing auctions, bids that have not withdrawn), then the oldBidder can withdraw 10 eth. But this means that in the future a withdraw will fail due to this missing 10 eth.

Impact:
A malicious user can win an auction for free.\\nAdditionally, either the oldBidder or some other user in the future will suffer the loss.\\nIf this is repeated multiple times, it will drain the contract balance and all users will lose their locked collateral.

Recommended mitigation:
Implement the require check from _cancelBid to _cancelAllBids.
Vulnerability: Currently auctioned NFTs can be transferred to a different address in a specific edge case
Severity: medium

Description:
Currently auctioned NFTs can be transferred to a different address in a specific edge case, leading to theft of funds.\\nThe protocol assumes that an NFT cannot change owner while it's being auctioned, this is generally the case but there is an exception, an NFT can change owner via mintToken() while an auction is ongoing when all the following conditions apply:\\nAn NFT is added `to` the collection without being minted (ie. `to` set `to` address(0)).\\nThe NFT is added `to` the collection with the parameter `tokenInitialPeriodStartTime[]` set `to` a timestamp lower than `l.initialPeriodStartTime` but bigger than 0(ie. `0` < `tokenInitialPeriodStartTime[]` < l.initialPeriodStartTime).\\nThe current `block.timestamp` is in-between `tokenInitialPeriodStartTime[]` and `l.initialPeriodStartTime`.\\nA malicious `initialBidder` can take advantage of this by:\\nBidding on the new added NFT via placeBid().\\nCalling mintToken() to transfer the NFT to a different address he controls.\\nClosing the auction via closeAuction()\\nAt point `3.`, because the NFT owner changed, the winning bidder (ie. initialBidder) is not the current NFT owner anymore. This will trigger the following line of code:\\n```\\nl.availableCollateral[oldBidder] += l.highestBids[tokenId][currentAuctionRound].bidAmount;\\n```\\n\\nWhich increases the `availableCollateral` of the `oldBidder` (ie. the address that owns the NFT after point 2.) by `bidAmount` of the highest bid. But because at the moment the highest bid was placed `initialBidder` was also the NFT owner, he only needed to transfer the `ETH` fee to the protocol instead of the whole bid amount.\\nThe `initialBidder` is now able to extract ETH from the protocol via the address used in point `2.` by calling withdrawCollateral() while also retaining the NFT license.

Impact:
Malicious initial bidder can potentially steal ETH from the protocol in an edge case. If the `ADD_TOKEN_TO_COLLECTION_ROLE` is also malicious, it's possible to drain the protocol.

Recommended mitigation:
Don't allow `tokenInitialPeriodStartTime[]` to be set at a timestamp beforel.initialPeriodStartTime.
Vulnerability: Tax refund is calculated based on the wrong amount
Severity: high

Description:
Tax refund is calculated based on the wrong amount\\nAfter the private period has finished, users can claim a tax refund, based on their max tax free allocation.\\n```\\n        (s.share, left) = _claim(s);\\n        require(left > 0, "TokenSale: Nothing to claim");\\n        uint256 refundTaxAmount;\\n        if (s.taxAmount > 0) {\\n            uint256 tax = userTaxRate(s.amount, msg.sender);\\n            uint256 taxFreeAllc = _maxTaxfreeAllocation(msg.sender) * PCT_BASE;\\n            if (taxFreeAllc >= s.share) {\\n                refundTaxAmount = s.taxAmount;\\n            } else {\\n                refundTaxAmount = (left * tax) / POINT_BASE;\\n            }\\n            usdc.safeTransferFrom(marketingWallet, msg.sender, refundTaxAmount);\\n        }\\n```\\n\\nThe problem is that in case `s.share > taxFreeAllc`, the tax refund is calculated wrongfully. Not only it should refund the tax on the unused USDC amount, but it should also refund the tax for the tax-free allocation the user has.\\nImagine the following.\\nUser deposits 1000 USDC.\\nPrivate period finishes, token oversells. Only half of the user's money actually go towards the sell (s.share = 500 USDC, s.left = 500 USDC)\\nThe user has 400 USDC tax-free allocation\\nThe user must be refunded the tax for the 500 unused USDC, as well as their 400 USDC tax-free allocation. In stead, they're only refunded for the 500 unused USDC. (note, if the user had 500 tax-free allocation, they would've been refunded all tax)

Impact:
Users are not refunded enough tax

Recommended mitigation:
change the code to the following:\\n```\\n                refundTaxAmount = ((left + taxFreeAllc) * tax) / POINT_BASE;\\n```\\n
Vulnerability: Vesting contract cannot work with ETH, although it's supposed to.
Severity: medium

Description:
Vesting contract cannot work with native token, although it's supposed to.\\nWithin the claim function, we can see that if `token` is set to address(1), the contract should operate with ETH\\n```\\n    function claim() external {\\n        address sender = msg.sender;\\n\\n        UserDetails storage s = userdetails[sender];\\n        require(s.userDeposit != 0, "No Deposit");\\n        require(s.index != vestingPoints.length, "already claimed");\\n        uint256 pctAmount;\\n        uint256 i = s.index;\\n        for (i; i <= vestingPoints.length - 1; i++) {\\n            if (block.timestamp >= vestingPoints[i][0]) {\\n                pctAmount += (s.userDeposit * vestingPoints[i][1]) / 10000;\\n            } else {\\n                break;\\n            }\\n        }\\n        if (pctAmount != 0) {\\n            if (address(token) == address(1)) {\\n                (bool sent, ) = payable(sender).call{value: pctAmount}("");  // @audit - here\\n                require(sent, "Failed to send BNB to receiver");\\n            } else {\\n                token.safeTransfer(sender, pctAmount);\\n            }\\n            s.index = uint128(i);\\n            s.amountClaimed += pctAmount;\\n        }\\n    }\\n```\\n\\nHowever, it is actually impossible for the contract to operate with ETH, since `updateUserDeposit` always attempts to do a token transfer.\\n```\\n    function updateUserDeposit(\\n        address[] memory _users,\\n        uint256[] memory _amount\\n    ) public onlyRole(DEFAULT_ADMIN_ROLE) {\\n        require(_users.length <= 250, "array length should be less than 250");\\n        require(_users.length == _amount.length, "array length should match");\\n        uint256 amount;\\n        for (uint256 i = 0; i < _users.length; i++) {\\n            userdetails[_users[i]].userDeposit = _amount[i];\\n            amount += _amount[i];\\n        }\\n        token.safeTransferFrom(distributionWallet, address(this), amount); // @audit - this will revert\\n    }\\n```\\n\\nSince when the contract is supposed to work with ETH, token is set to address(1), calling `safeTransferFrom` on that address will always revert, thus making it impossible to call this function.

Impact:
Vesting contract is unusable with ETH

Recommended mitigation:
make the following check\\n```\\n        if (address(token) != address(1)) token.safeTransferFrom(distributionWallet, address(this), amount);\\n```\\n
Vulnerability: If token does not oversell, users cannot claim tax refund on their tax free allocation.
Severity: high

Description:
Users may not be able to claim tax refund\\nWithin TokenSale, upon depositing users, users have to pay tax. Then, users can receive a tax-free allocation - meaning they'll be refunded the tax they've paid on part of their deposit.\\nThe problem is that due to a unnecessary require check, users cannot claim their tax refund, unless the token has oversold.\\n```\\n function claim() external {\\n        checkingEpoch();\\n        require(\\n            uint8(epoch) > 1 && !admin.blockClaim(address(this)),\\n            "TokenSale: Not time or not allowed"\\n        );\\n\\n        Staked storage s = stakes[msg.sender];\\n        require(s.amount != 0, "TokenSale: No Deposit"); \\n        require(!s.claimed, "TokenSale: Already Claimed");\\n\\n        uint256 left;\\n        (s.share, left) = _claim(s);\\n        require(left > 0, "TokenSale: Nothing to claim");  // @audit - problematic line \\n        uint256 refundTaxAmount;\\n        if (s.taxAmount > 0) {\\n            uint256 tax = userTaxRate(s.amount, msg.sender);\\n            uint256 taxFreeAllc = _maxTaxfreeAllocation(msg.sender) * PCT_BASE;\\n            if (taxFreeAllc >= s.share) {\\n                refundTaxAmount = s.taxAmount;\\n            } else {\\n                refundTaxAmount = (left * tax) / POINT_BASE; // tax refund is on the wrong amount \\n            }\\n            usdc.safeTransferFrom(marketingWallet, msg.sender, refundTaxAmount);\\n        }\\n        s.claimed = true;\\n        usdc.safeTransfer(msg.sender, left);\\n        emit Claim(msg.sender, left);\\n    }\\n```\\n\\n```\\n    function _claim(Staked memory _s) internal view returns (uint120, uint256) {\\n        uint256 left;\\n        if (state.totalPrivateSold > (state.totalSupplyInValue)) {\\n            uint256 rate = (state.totalSupplyInValue * PCT_BASE) /\\n                state.totalPrivateSold;\\n            _s.share = uint120((uint256(_s.amount) * rate) / PCT_BASE);\\n            left = uint256(_s.amount) - uint256(_s.share);\\n        } else {\\n            _s.share = uint120(_s.amount);\\n        }\\n\\n        return (_s.share, left);\\n    }\\n```\\n\\n`left` only has value if the token has oversold. Meaning that even if the user has an infinite tax free allocation, if the token has not oversold, they won't be able to claim a tax refund.

Impact:
loss of funds

Recommended mitigation:
Remove the require check
Vulnerability: Reentrancy in Vesting.sol:claim() will allow users to drain the contract due to executing .call() on user's address before setting s.index = uint128(i)
Severity: high

Description:
Reentrancy in Vesting.sol:claim() will allow users to drain the contract due to executing .call() on user's address before setting s.index = uint128(I)\\nHere is the Vesting.sol:claim() function:\\n```\\nfunction claim() external {\\n        address sender = msg.sender;\\n\\n        UserDetails storage s = userdetails[sender];\\n        require(s.userDeposit != 0, "No Deposit");\\n      require(s.index != vestingPoints.length, "already claimed");\\n        uint256 pctAmount;\\n        uint256 i = s.index;\\n        for (i; i <= vestingPoints.length - 1; i++) {\\n            if (block.timestamp >= vestingPoints[i][0]) {\\n                pctAmount += (s.userDeposit * vestingPoints[i][1]) / 10000;\\n            } else {\\n                break;\\n            }\\n        }\\n        if (pctAmount != 0) {\\n            if (address(token) == address(1)) {\\n              (bool sent, ) = payable(sender).call{value: pctAmount}("");\\n                require(sent, "Failed to send BNB to receiver");\\n            } else {\\n                token.safeTransfer(sender, pctAmount);\\n            }\\n          s.index = uint128(i);\\n            s.amountClaimed += pctAmount;\\n        }\\n    }\\n```\\n\\nFrom the above, You'll notice the claim() function checks if the caller already claimed by checking if the s.index has already been set to vestingPoints.length. You'll also notice the claim() function executes .call() and transfer the amount to the caller before setting the s.index = uint128(i), thereby allowing reentrancy.\\nLet's consider this sample scenario:\\nAn attacker contract(alice) has some native pctAmount to claim and calls `claim()`.\\n"already claimed" check will pass since it's the first time she's calling `claim()` so her s.index hasn't been set\\nHowever before updating Alice s.index, the Vesting contract performs external .call() to Alice with the amount sent as well\\nAlice reenters `claim()` again on receive of the amount\\nbypass index "already claimed" check since this hasn't been updated yet\\ncontract performs external .call() to Alice with the amount sent as well again,\\nSame thing happens again\\nAlice ends up draining the Vesting contract

Impact:
Reentrancy in Vesting.sol:claim() will allow users to drain the contract

Recommended mitigation:
Here is the recommended fix:\\n```\\nif (pctAmount != 0) {\\n// Add the line below\\n           s.index = uint128(i);\\n            if (address(token) == address(1)) {\\n                (bool sent, ) = payable(sender).call{value: pctAmount}("");\\n                require(sent, "Failed to send BNB to receiver");\\n            } else {\\n                token.safeTransfer(sender, pctAmount);\\n            }\\n// Remove the line below\\n           s.index = uint128(i);\\n            s.amountClaimed // Add the line below\\n= pctAmount;\\n        }\\n```\\n\\nI'll also recommend using reentrancyGuard.
Vulnerability: Blocklisted investors can still claim USDC in `TokenSale.sol`
Severity: medium

Description:
A wrong argument is passed when checking if a user is blacklisted for claiming in `TokenSale.claim()`. Because the check is insufficient, blocked users can claim their USDC.\\n`Admin.setClaimBlock()` blocks users from claiming. The function accepts the address of the user to be blocked and adds it to the `blockClaim` mapping.\\n```\\n    /**\\n     @dev Whitelist users\\n     @param _address Address of User\\n     */\\n    function setClaimBlock(address _address) external onlyRole(OPERATOR) {\\n        blockClaim[_address] = true;\\n    }\\n```\\n\\nThe check in `Admin.claim()` wrongly passes `address(this)` as argument when calling `Admin.blockClaim`.\\n```\\n        require(\\n            uint8(epoch) > 1 && !admin.blockClaim(address(this)),\\n            "TokenSale: Not time or not allowed"\\n        );\\n```\\n\\nIn this context, `address(this)` will be the address of the token sale contract and the require statement can be bypassed even by a blocked user.

Impact:
The whole functionality for blocking claims doesn't work properly.

Recommended mitigation:
Pass the address of the user.\\n```\\n        require(\\n// Remove the line below\\n            uint8(epoch) > 1 && !admin.blockClaim(address(this)),\\n// Add the line below\\n            uint8(epoch) > 1 && !admin.blockClaim(msg.sender)),\\n            "TokenSale: Not time or not allowed"\\n        );\\n```\\n
Vulnerability: Max allocations can be bypassed with multiple addresses because of guaranteed allocations
Severity: medium

Description:
`TokenSale._processPrivate()` ensures that a user cannot deposit more than their allocation amount. However, each address can deposit up to at least `maxAllocations`. This can be leveraged by a malicious user by using different addresses to claim all tokens without even staking.\\nThe idea of the protocol is to give everyone the right to have at least `maxAlocations` allocations. By completing missions, users level up and unlock new tiers. This process will be increasing their allocations. The problem is that when a user has no allocations, they have still a granted amount of `maxAllocations`.\\n`TokenSale.calculateMaxAllocation` returns $max(maxTierAlloc(), maxAllocation)$\\nFor a user with no allocations, `_maxTierAlloc()` will return 0. The final result will be that this user have `maxAllocation` allocations (because `maxAllocation` > 0).\\n```\\n        if (userTier == 0 && giftedTierAllc == 0) {\\n            return 0;\\n        }\\n```\\n\\nMultiple Ethereum accounts can be used by the same party to take control over the IDO and all its allocations, on top of that without even staking.\\nNOTE: setting `maxAllocation = 0` is not a solution in this case because the protocol wants to still give some allocations to their users.

Impact:
Buying all allocations without staking. This also violates a key property that only ION holders can deposit.

Recommended mitigation:
A possible solution may be to modify `calculateMaxAllocation` in the following way:\\n```\\n    function calculateMaxAllocation(address _sender) public returns (uint256) {\\n        uint256 userMaxAllc = _maxTierAllc(_sender);\\n// Add the line below\\n       if (userMaxAllc == 0) return 0;\\n\\n         if (userMaxAllc > maxAllocation) {\\n            return userMaxAllc;\\n        } else {\\n            return maxAllocation;\\n        }\\n    }\\n```\\n
Vulnerability: WooFi oracle can fail to validate its price with Chainlink price feed
Severity: medium

Description:
The price precision that the WooOracle uses is 8. However, if the quote token is an expensive token or the base token is a very cheap token, then the price will be too less in decimals and even "0" in some cases. This will lead to inefficient trades or inability to compare the woofi price with chainlink price due to chainlink price return with "0" value.\\nFirst, let's see how the chainlink price is calculated:\\n```\\nfunction _cloPriceInQuote(address _fromToken, address _toToken)\\n        internal\\n        view\\n        returns (uint256 refPrice, uint256 refTimestamp)\\n    {\\n        address baseOracle = clOracles[_fromToken].oracle;\\n        if (baseOracle == address(0)) {\\n            return (0, 0);\\n        }\\n        address quoteOracle = clOracles[_toToken].oracle;\\n        uint8 quoteDecimal = clOracles[_toToken].decimal;\\n\\n        (, int256 rawBaseRefPrice, , uint256 baseUpdatedAt, ) = AggregatorV3Interface(baseOracle).latestRoundData();\\n        (, int256 rawQuoteRefPrice, , uint256 quoteUpdatedAt, ) = AggregatorV3Interface(quoteOracle).latestRoundData();\\n        uint256 baseRefPrice = uint256(rawBaseRefPrice);\\n        uint256 quoteRefPrice = uint256(rawQuoteRefPrice);\\n\\n        // NOTE: Assume wooracle token decimal is same as chainlink token decimal.\\n        uint256 ceoff = uint256(10)**quoteDecimal;\\n        refPrice = (baseRefPrice * ceoff) / quoteRefPrice;\\n        refTimestamp = baseUpdatedAt >= quoteUpdatedAt ? quoteUpdatedAt : baseUpdatedAt;\\n    }\\n```\\n\\nNow, let's assume the quote token is WBTC price of 60,000$ and the baseToken is tokenX that has the price of 0.0001$. When the final price is calculated atrefPrice because of the divisions in solidity, the result will be "0" as follows: 60_000 * 1e8 * 1e8 / 0.0001 * 1e8 = 0\\nso the return amount will be "0".\\nWhen the derived chainlink price is compared with woofi oracle if the chainlink price is "0" then the `woPriceInBound` will be set to "true" assuming the chainlink price is not set. However, in our case that's not the case, the price returnt "0" because of divisions:\\n```\\n-> bool woPriceInBound = cloPrice_ == 0 ||\\n            ((cloPrice_ * (1e18 - bound)) / 1e18 <= woPrice_ && woPrice_ <= (cloPrice_ * (1e18 + bound)) / 1e18);\\n\\n        if (woFeasible) {\\n            priceOut = woPrice_;\\n            feasible = woPriceInBound;\\n        }\\n```\\n\\nIn such scenario, the chainlink comparison between woofi and chainlink price will not give correct results. The oracle will not be able to detect whether the chainlink price is in "bound" with the woofi's returnt price.\\nThis also applies if a baseToken price crushes. If the token price gets very less due to market, regardless of the quoteToken being WBTC or USDC the above scenario can happen.

Impact:
Oracle will fail to do a validation of its price with the chainlink price.

Recommended mitigation:
Precision of "8" is not enough on most of the cases. I'd suggest return the oracle price in "18" decimals to get more room on rounding.
Vulnerability: In the function _handleERC20Received, the fee was incorrectly charged
Severity: medium

Description:
In the function _handleERC20Received, the fee was incorrectly charged.\\nIn the contract, when external swap occurs, a portion of the fee will be charged. However, in function _handleERC20Received, the fee is also charged in internal swap.\\n```\\n} else {\\n    // Deduct the external swap fee\\n    uint256 fee = (bridgedAmount * dstExternalFeeRate) / FEE_BASE;\\n    bridgedAmount -= fee;  // @@audit: fee should not be applied to internal swap \\n\\n    TransferHelper.safeApprove(bridgedToken, address(wooRouter), bridgedAmount);\\n    if (dst1inch.swapRouter != address(0)) {\\n        try\\n            wooRouter.externalSwap(\\n```\\n\\nAt the same time, when the internal swap fails, this part of the fee will not be returned to the user.

Impact:
Internal swaps are incorrectly charged, and fees are not refunded when internal swap fail.

Recommended mitigation:
Apply fee calculation only to external swaps.\\n```\\nfunction _handleERC20Received(\\n    uint256 refId,\\n    address to,\\n    address toToken,\\n    address bridgedToken,\\n    uint256 bridgedAmount,\\n    uint256 minToAmount,\\n    Dst1inch memory dst1inch\\n) internal {\\n    address msgSender = _msgSender();\\n\\n    // // rest of code\\n\\n    } else {\\n        if (dst1inch.swapRouter != address(0)) {\\n            // Deduct the external swap fee\\n            uint256 fee = (bridgedAmount * dstExternalFeeRate) / FEE_BASE;\\n            bridgedAmount -= fee;  \\n\\n            TransferHelper.safeApprove(bridgedToken, address(wooRouter), bridgedAmount);\\n            try\\n                wooRouter.externalSwap(\\n                    // // rest of code\\n                )\\n            returns (uint256 realToAmount) {\\n                emit WooCrossSwapOnDstChain(\\n                    // // rest of code\\n                );\\n            } catch {\\n                bridgedAmount += fee;\\n                TransferHelper.safeTransfer(bridgedToken, to, bridgedAmount);\\n                emit WooCrossSwapOnDstChain(\\n                    // // rest of code\\n                );\\n            }\\n        } else {\\n            TransferHelper.safeApprove(bridgedToken, address(wooRouter), bridgedAmount);\\n            try wooRouter.swap(bridgedToken, toToken, bridgedAmount, minToAmount, payable(to), to) returns (\\n                uint256 realToAmount\\n            ) {\\n               // // rest of code\\n            } catch {\\n                // // rest of code\\n            }\\n        }\\n    }\\n}\\n```\\n