msc-smart-contract-auditing/vulnerable-functions-base huggingface dataset:
Vulnerability: rewardTokens removed from WAuraPool/WConvexPools will be lost forever
Severity: medium

Description:
pendingRewards pulls a fresh count of reward tokens each time it is called. This is problematic if reward tokens are ever removed from the the underlying Aura/Convex pools because it means that they will no longer be distributed and will be locked in the contract forever.\\nWAuraPools.sol#L166-L189\\n```\\n    uint extraRewardsCount = IAuraRewarder(crvRewarder)\\n        .extraRewardsLength();\\n    tokens = new address[](extraRewardsCount + 1);\\n    rewards = new uint256[](extraRewardsCount + 1);\\n\\n    tokens[0] = IAuraRewarder(crvRewarder).rewardToken();\\n    rewards[0] = _getPendingReward(\\n        stCrvPerShare,\\n        crvRewarder,\\n        amount,\\n        lpDecimals\\n    );\\n\\n    for (uint i = 0; i < extraRewardsCount; i++) {\\n        address rewarder = IAuraRewarder(crvRewarder).extraRewards(i);\\n        uint256 stRewardPerShare = accExtPerShare[tokenId][i];\\n        tokens[i + 1] = IAuraRewarder(rewarder).rewardToken();\\n        rewards[i + 1] = _getPendingReward(\\n            stRewardPerShare,\\n            rewarder,\\n            amount,\\n            lpDecimals\\n        );\\n    }\\n```\\n\\nIn the lines above we can see that only tokens that are currently available on the pool. This means that if tokens are removed then they are no longer claimable and will be lost to those entitled to shares.

Impact:
Users will lose reward tokens if they are removed

Recommended mitigation:
Reward tokens should be stored with the tokenID so that it can still be paid out even if it the extra rewardToken is removed.
Vulnerability: Swapper mechanism cannot incentivize ETH-WETH swaps without risking owner funds
Severity: medium

Description:
When `flash()` is called on the Swapper contract, pairs of tokens are passed in consisting of (a) a base token, which is currently held by the contract and (b) a quote token, which is the `$tokenToBeneficiary` that the owner would like to receive.\\nThese pairs are passed to the oracle to get the quoted value of each of them:\\n```\\namountsToBeneficiary = $oracle.getQuoteAmounts(quoteParams_);\\n```\\n\\nThe `UniV3OracleImpl.sol` contract returns a quote per pair of tokens. However, since Uniswap pools only consist of WETH (not ETH) and are ordered by token address, it performs two conversions first: `_convert()` converts ETH to WETH for both base and quote tokens, and `_sort()` orders the pairs by token address.\\n```\\nConvertedQuotePair memory cqp = quoteParams_.quotePair._convert(_convertToken);\\nSortedConvertedQuotePair memory scqp = cqp._sort();\\n```\\n\\nThe oracle goes on to check for pair overrides, and gets the `scaledOfferFactor` for the pair being quoted:\\n```\\nPairOverride memory po = _getPairOverride(scqp);\\nif (po.scaledOfferFactor == 0) {\\n    po.scaledOfferFactor = $defaultScaledOfferFactor;\\n}\\n```\\n\\nThe `scaledOfferFactor` is the discount being offered through the Swapper to perform the swap. The assumption is that this will be set to a moderate amount (approximately 5%) to incentivize bots to perform the swaps, but will be overridden with a value of ~0% for the same tokens, to ensure that bots aren't paid for swaps they don't need to perform.\\nThe problem is that these overrides are set on the `scqp` (sorted, converted tokens), not the actual token addresses. For this reason, ETH and WETH are considered identical in terms of overrides.\\nTherefore, Swapper owners who want to be paid out in ETH (ie where $tokenToBeneficiary = ETH) have two options:\\nThey can set the WETH-WETH override to 0%, which successfully stops bots from earning a fee on ETH-ETH trades, but will not provide any incentive for bots to swap WETH in the swapper into ETH. This makes the Swapper useless for WETH.\\nThey can keep the WETH-WETH pair at the original ~5%, which will incentivize WETH-ETH swaps, but will also pay 5% to bots for doing nothing when they take ETH out of the contract and return ETH. This makes the Swapper waste user funds.\\nThe same issues exist going in the other direction, when `$tokenToBeneficiary = WETH`.

Impact:
Users who want to be paid out in ETH or WETH will be forced to either (a) have the Swapper not function properly for a key pair or (b) pay bots to perform useless actions.

Recommended mitigation:
The `scaledOfferFactor` (along with its overrides) should be stored on the Swapper, not on the Oracle.\\nIn order to keep the system modular and logically organized, the Oracle should always return the accurate price for the `scqp`. Then, it is the job of the Swapper to determine what discount is offered for which asset.\\nThis will allow values to be stored in the actual `base` and `quote` assets being used, and not in their converted, sorted counterparts.
Vulnerability: CollateralManager#commitCollateral can be called on an active loan
Severity: high

Description:
CollateralManager#commitCollateral never checks if the loan has been accepted allowing users to add collaterals after which can DOS the loan.\\nCollateralManager.sol#L117-L130\\n```\\nfunction commitCollateral(\\n    uint256 _bidId,\\n    Collateral[] calldata _collateralInfo\\n) public returns (bool validation_) {\\n    address borrower = tellerV2.getLoanBorrower(_bidId);\\n    (validation_, ) = checkBalances(borrower, _collateralInfo); <- @audit-issue never checks that loan isn't active\\n\\n    if (validation_) {\\n        for (uint256 i; i < _collateralInfo.length; i++) {\\n            Collateral memory info = _collateralInfo[i];\\n            _commitCollateral(_bidId, info);\\n        }\\n    }\\n}\\n```\\n\\nCollateralManager#commitCollateral does not contain any check that the bidId is pending or at least that it isn't accepted. This means that collateral can be committed to an already accepted bid, modifying bidCollaterals.\\n```\\nfunction _withdraw(uint256 _bidId, address _receiver) internal virtual {\\n    for (\\n        uint256 i;\\n        i < _bidCollaterals[_bidId].collateralAddresses.length();\\n        i++\\n    ) {\\n        // Get collateral info\\n        Collateral storage collateralInfo = _bidCollaterals[_bidId]\\n            .collateralInfo[\\n                _bidCollaterals[_bidId].collateralAddresses.at(i)\\n            ];\\n        // Withdraw collateral from escrow and send it to bid lender\\n        ICollateralEscrowV1(_escrows[_bidId]).withdraw(\\n            collateralInfo._collateralAddress,\\n            collateralInfo._amount,\\n            _receiver\\n        );\\n```\\n\\nbidCollaterals is used to trigger the withdrawal from the escrow to the receiver, which closing the loan and liquidations. This can be used to DOS a loan AFTER it has already been filled.\\nUser A creates a bid for 10 ETH against 50,000 USDC at 10% APR\\nUser B sees this bid and decides to fill it\\nAfter the loan is accepted, User A calls CollateralManager#commitCollateral with a malicious token they create\\nUser A doesn't pay their loan and it becomes liquidatable\\nUser B calls liquidate but it reverts when the escrow attempts to transfer out the malicious token\\nUser A demands a ransom to return the funds\\nUser A enables the malicious token transfer once the ransom is paid

Impact:
Loans can be permanently DOS'd even after being accepted

Recommended mitigation:
CollateralManager#commitCollateral should revert if loan is active.
Vulnerability: CollateralManager#commitCollateral can be called by anyone
Severity: high

Description:
CollateralManager#commitCollateral has no access control allowing users to freely add malicious tokens to any bid\\nCollateralManager.sol#L117-L130\\n```\\nfunction commitCollateral(\\n    uint256 _bidId,\\n    Collateral[] calldata _collateralInfo\\n) public returns (bool validation_) {\\n    address borrower = tellerV2.getLoanBorrower(_bidId);\\n    (validation_, ) = checkBalances(borrower, _collateralInfo); <- @audit-issue no access control\\n\\n    if (validation_) {\\n        for (uint256 i; i < _collateralInfo.length; i++) {\\n            Collateral memory info = _collateralInfo[i];\\n            _commitCollateral(_bidId, info);\\n        }\\n    }\\n}\\n```\\n\\nCollateralManager#commitCollateral has no access control and can be called by anyone on any bidID. This allows an attacker to front-run lenders and add malicious tokens to a loan right before it is filled.\\nA malicious user creates a malicious token that can be transferred once before being paused and returns uint256.max for balanceOf\\nUser A creates a loan for 10e18 ETH against 50,000e6 USDC at 10% APR\\nUser B decides to fill this loan and calls TellerV2#lenderAcceptBid\\nThe malicious user sees this and front-runs with a CollateralManager#commitCollateral call adding the malicious token\\nMalicious token is now paused breaking both liquidations and fully paying off the loan\\nMalicious user leverages this to ransom the locked tokens, unpausing when it is paid

Impact:
User can add malicious collateral calls to any bid they wish

Recommended mitigation:
Cause CollateralManager#commitCollateral to revert if called by anyone other than the borrower, their approved forwarder or TellerV2
Vulnerability: CollateralManager#commitCollateral overwrites collateralInfo._amount if called with an existing collateral
Severity: high

Description:
When duplicate collateral is committed, the collateral amount is overwritten with the new value. This allows borrowers to front-run bid acceptance to change their collateral and steal from lenders.\\nCollateralManager.sol#L426-L442\\n```\\nfunction _commitCollateral(\\n    uint256 _bidId,\\n    Collateral memory _collateralInfo\\n) internal virtual {\\n    CollateralInfo storage collateral = _bidCollaterals[_bidId];\\n    collateral.collateralAddresses.add(_collateralInfo._collateralAddress);\\n    collateral.collateralInfo[\\n        _collateralInfo._collateralAddress\\n    ] = _collateralInfo; <- @audit-issue collateral info overwritten\\n    emit CollateralCommitted(\\n        _bidId,\\n        _collateralInfo._collateralType,\\n        _collateralInfo._collateralAddress,\\n        _collateralInfo._amount,\\n        _collateralInfo._tokenId\\n    );\\n}\\n```\\n\\nWhen a duplicate collateral is committed it overwrites the collateralInfo for that token, which is used to determine how much collateral to escrow from the borrower.\\nTellerV2.sol#L470-L484\\n```\\nfunction lenderAcceptBid(uint256 _bidId)\\n    external\\n    override\\n    pendingBid(_bidId, "lenderAcceptBid")\\n    whenNotPaused\\n    returns (\\n        uint256 amountToProtocol,\\n        uint256 amountToMarketplace,\\n        uint256 amountToBorrower\\n    )\\n{\\n    // Retrieve bid\\n    Bid storage bid = bids[_bidId];\\n\\n    address sender = _msgSenderForMarket(bid.marketplaceId);\\n```\\n\\nTellerV2#lenderAcceptBid only allows the lender input the bidId of the bid they wish to accept, not allowing them to specify the expected collateral. This allows lenders to be honeypot and front-run causing massive loss of funds:\\nMalicious user creates and commits a bid to take a loan of 10e18 ETH against 100,000e6 USDC with 15% APR\\nLender sees this and calls TellerV2#lenderAcceptBid\\nMalicious user front-runs transaction with commitCollateral call setting USDC to 1\\nBid is filled sending malicious user 10e18 ETH and escrowing 1 USDC\\nAttacker doesn't repay loan and has stolen 10e18 ETH for the price of 1 USDC

Impact:
Bid acceptance can be front-run to cause massive losses to lenders

Recommended mitigation:
Allow lender to specify collateral info and check that it matches the committed addresses and amounts
Vulnerability: _repayLoan will fail if lender is blacklisted
Severity: high

Description:
The internal function that repays a loan `_repayLoan` attempts to transfer the loan token back to the lender. If the loan token implements a blacklist like the common USDC token, the transfer may be impossible and the repayment will fail.\\nThis internal `_repayLoan` function is called during any partial / full repayment and during liquidation.\\nThe function to repay the loan to the lender directly transfers the token to the lender:\\n```\\n    function _repayLoan(// rest of code) internal virtual {\\n        // rest of code\\n        bid.loanDetails.lendingToken.safeTransferFrom(\\n            _msgSenderForMarket(bid.marketplaceId),\\n            lender,\\n            paymentAmount\\n        );\\n        // rest of code\\n```\\n\\nAny of these functions will fail if loan lender is blacklisted by the token.\\nDuring repayment the loan lender is computed by:\\n```\\n    function getLoanLender(uint256 _bidId)\\n        public\\n        view\\n        returns (address lender_)\\n    {\\n        lender_ = bids[_bidId].lender;\\n\\n        if (lender_ == address(lenderManager)) {\\n            return lenderManager.ownerOf(_bidId);\\n        }\\n    }\\n```\\n\\nIf the lender controls a blacklisted address, they can use the lenderManager to selectively transfer the loan to / from the blacklisted whenever they want.

Impact:
Any lender can prevent repayment of a loan and its liquidation. In particular, a lender can wait until a loan is almost completely repaid, transfer the loan to a blacklisted address (even one they do not control) to prevent the loan to be fully repaid / liquidated. The loan will default and borrower will not be able to withdraw their collateral.\\nThis result in a guaranteed griefing attack on the collateral of a user.\\nIf the lender controls a blacklisted address, they can additionally withdraw the collateral of the user.\\nI believe the impact is high since the griefing attack is always possible whenever lent token uses a blacklist, and results in a guaranteed loss of collateral.

Recommended mitigation:
Use a push/pull pattern for transferring tokens. Allow repayment of loan and withdraw the tokens of the user into `TellerV2` (or an escrow) and allow lender to withdraw the repayment from `TellerV2` (or the escrow). This way, the repayment will fail only if `TellerV2` is blacklisted.
Vulnerability: Malicious user can abuse UpdateCommitment to create commitments for other users
Severity: high

Description:
UpdateCommitment checks that the original lender is msg.sender but never validates that the original lender == new lender. This allows malicious users to effectively create a commitment for another user, allowing them to drain funds from them.\\nLenderCommitmentForwarder.sol#L208-L224\\n```\\nfunction updateCommitment(\\n    uint256 _commitmentId,\\n    Commitment calldata _commitment\\n) public commitmentLender(_commitmentId) { <- @audit-info checks that lender is msg.sender\\n    require(\\n        _commitment.principalTokenAddress ==\\n            commitments[_commitmentId].principalTokenAddress,\\n        "Principal token address cannot be updated."\\n    );\\n    require(\\n        _commitment.marketId == commitments[_commitmentId].marketId,\\n        "Market Id cannot be updated."\\n    );\\n\\n    commitments[_commitmentId] = _commitment; <- @audit-issue never checks _commitment.lender\\n\\n    validateCommitment(commitments[_commitmentId]);\\n```\\n\\nUpdateCommitment is intended to allow users to update their commitment but due to lack of verification of _commitment.lender, a malicious user create a commitment then update it to a new lender. By using bad loan parameters they can steal funds from the attacker user.

Impact:
UpdateCommitment can be used to create a malicious commitment for another user and steal their funds

Recommended mitigation:
Check that the update lender is the same the original lender
Vulnerability: lender could be forced to withdraw collateral even if he/she would rather wait for liquidation during default
Severity: medium

Description:
lender could be forced to withdraw collateral even if he/she would rather wait for liquidation during default\\nCollateralManager.withdraw would pass if the loan is defaulted (the borrower does not pay interest in time); in that case, anyone can trigger an withdrawal on behalf of the lender before the liquidation delay period passes.\\nwithdraw logic from CollateralManager.\\n```\\n     * @notice Withdraws deposited collateral from the created escrow of a bid that has been successfully repaid.\\n     * @param _bidId The id of the bid to withdraw collateral for.\\n     */\\n    function withdraw(uint256 _bidId) external {\\n        BidState bidState = tellerV2.getBidState(_bidId);\\n        console2.log("WITHDRAW %d", uint256(bidState));\\n        if (bidState == BidState.PAID) {\\n            _withdraw(_bidId, tellerV2.getLoanBorrower(_bidId));\\n        } else if (tellerV2.isLoanDefaulted(_bidId)) {  audit\\n            _withdraw(_bidId, tellerV2.getLoanLender(_bidId));\\n            emit CollateralClaimed(_bidId);\\n        } else {\\n            revert("collateral cannot be withdrawn");\\n        }\\n    }\\n```\\n

Impact:
anyone can force lender to take up collateral during liquidation delay and liquidation could be something that never happen. This does not match the intention based on the spec which implies that lender has an option: `3) When the loan is fully repaid, the borrower can withdraw the collateral. If the loan becomes defaulted instead, then the lender has a 24 hour grace period to claim the collateral (losing the principal)`

Recommended mitigation:
check that the caller is the lender\\n```\\n    function withdraw(uint256 _bidId) external {\\n        BidState bidState = tellerV2.getBidState(_bidId);\\n        console2.log("WITHDRAW %d", uint256(bidState));\\n        if (bidState == BidState.PAID) {\\n            _withdraw(_bidId, tellerV2.getLoanBorrower(_bidId));\\n        } else if (tellerV2.isLoanDefaulted(_bidId)) {\\n+++        uint256 _marketplaceId = bidState.marketplaceId; \\n+++        address sender = _msgSenderForMarket(_marketplaceId); \\n+++        address lender = tellerV2.getLoanLender(_bidId); \\n+++        require(sender == lender, "sender must be the lender"); \\n            _withdraw(_bidId, lender);\\n            emit CollateralClaimed(_bidId);\\n        } else {\\n            revert("collateral cannot be withdrawn");\\n        }\\n    }\\n```\\n
Vulnerability: The calculation time methods of `calculateNextDueDate` and `_canLiquidateLoan` are inconsistent
Severity: medium

Description:
The calculation time methods of `calculateNextDueDate` and `_canLiquidateLoan` are inconsistent\\n```\\nFile: TellerV2.sol\\n 854     function calculateNextDueDate(uint256 _bidId)\\n 855         public\\n 856         view\\n 857         returns (uint32 dueDate_)\\n 858     {\\n 859         Bid storage bid = bids[_bidId];\\n 860         if (bids[_bidId].state != BidState.ACCEPTED) return dueDate_;\\n 861\\n 862         uint32 lastRepaidTimestamp = lastRepaidTimestamp(_bidId);\\n 863\\n 864         // Calculate due date if payment cycle is set to monthly\\n 865         if (bidPaymentCycleType[_bidId] == PaymentCycleType.Monthly) {\\n 866             // Calculate the cycle number the last repayment was made\\n 867             uint256 lastPaymentCycle = BPBDTL.diffMonths(\\n 868                 bid.loanDetails.acceptedTimestamp,\\n 869               \\n```\\n\\nThe `calculateNextDueDate` function is used by the borrower to query the date of the next repayment. Generally speaking, the borrower will think that as long as the repayment is completed at this point in time, the collateral will not be liquidated.\\n```\\nFile: TellerV2.sol\\n 953     function _canLiquidateLoan(uint256 _bidId, uint32 _liquidationDelay)\\n 954         internal\\n 955         view\\n 956         returns (bool)\\n 957     {\\n 958         Bid storage bid = bids[_bidId];\\n 959\\n 960         // Make sure loan cannot be liquidated if it is not active\\n 961         if (bid.state != BidState.ACCEPTED) return false;\\n 962\\n 963         if (bidDefaultDuration[_bidId] == 0) return false;\\n 964\\n 965         return (uint32(block.timestamp) -\\n 966             _liquidationDelay -\\n 967             lastRepaidTimestamp(_bidId) >\\n 968             bidDefaultDuration[_bidId]);\\n 969     }\\n```\\n\\nHowever, when the `_canLiquidateLoan` function actually judges whether it can be liquidated, the time calculation mechanism is completely different from that of `calculateNextDueDate` function, which may cause that if the time point calculated by `_canLiquidateLoan` is earlier than the time point of `calculateNextDueDate` function, the borrower may also be liquidated in the case of legal repayment.\\nBorrowers cannot query the specific liquidation time point, but can only query whether they can be liquidated through the `isLoanDefaulted` function or `isLoanLiquidateable` function. When they query that they can be liquidated, they may have already been liquidated.

Impact:
Borrowers may be liquidated if repayments are made on time.

Recommended mitigation:
It is recommended to verify that the liquidation time point cannot be shorter than the repayment period and allow users to query the exact liquidation time point.
Vulnerability: updateCommitmentBorrowers does not delete all existing users
Severity: medium

Description:
The lender can update the list of borrowers by calling `LenderCommitmentForwarder.updateCommitmentBorrowers`. The list of borrowers is EnumerableSetUpgradeable.AddressSet that is a complex structure containing mapping. Using the `delete` keyword to `delete` this structure will not erase the mapping inside it. Let's look at the code of this function.\\n```\\nmapping(uint256 => EnumerableSetUpgradeable.AddressSet)\\n        internal commitmentBorrowersList;\\n        \\nfunction updateCommitmentBorrowers(\\n        uint256 _commitmentId,\\n        address[] calldata _borrowerAddressList\\n    ) public commitmentLender(_commitmentId) {\\n        delete commitmentBorrowersList[_commitmentId];\\n        _addBorrowersToCommitmentAllowlist(_commitmentId, _borrowerAddressList);\\n    }\\n```\\n\\nI wrote a similar function to prove the problem.\\n```\\nusing EnumerableSet for EnumerableSet.AddressSet;\\n    mapping(uint256 => EnumerableSet.AddressSet) internal users;\\n    \\n    function test_deleteEnumerableSet() public {\\n        uint256 id = 1;\\n        address[] memory newUsers = new address[](2);\\n        newUsers[0] = address(0x1);\\n        newUsers[1] = address(0x2);\\n\\n        for (uint256 i = 0; i < newUsers.length; i++) {\\n            users[id].add(newUsers[i]);\\n        }\\n        delete users[id];\\n        newUsers[0] = address(0x3);\\n        newUsers[1] = address(0x4);\\n        for (uint256 i = 0; i < newUsers.length; i++) {\\n            users[id].add(newUsers[i]);\\n        }\\n        bool exist = users[id].contains(address(0x1));\\n        if(exist) {\\n            emit log_string("address(0x1) exist");\\n        }\\n        exist = users[id].contains(address(0x2));\\n        if(exist) {\\n            emit log_string("address(0x2) exist");\\n        }\\n    }\\n/*\\n[PASS] test_deleteEnumerableSet() (gas: 174783)\\nLogs:\\n  address(0x1) exist\\n  address(0x2) exist\\n*/\\n```\\n

Impact:
The deleted Users can still successfully call `LenderCommitmentForwarder.acceptCommitment` to get a loan.

Recommended mitigation:
In order to clean an `EnumerableSet`, you can either remove all elements one by one or create a fresh instance using an array of `EnumerableSet`.