sherlock-audit / 2024-04-teller-finance-judging

6 stars 6 forks source link

pkqs90 - `liquidateDefaultedLoanWithIncentive` cannot be called with 0 tokens even if 96400 seconds has passed for ERC20 tokens that doesn't support zero-transfer #270

Closed sherlock-admin3 closed 2 months ago

sherlock-admin3 commented 2 months ago

pkqs90

medium

liquidateDefaultedLoanWithIncentive cannot be called with 0 tokens even if 96400 seconds has passed for ERC20 tokens that doesn't support zero-transfer

Summary

During liquidation phase of LenderCommitmentGroup_Smart, users can call liquidateDefaultedLoanWithIncentive for liquidating loans that are default. If 96400 seconds has passed the default loan time, the liquidation should be able to finish without transfering any tokens. However, for ERC20 tokens that reverts on zero-transfer, this is not possible, and at least 1wei token must be supplied.

Vulnerability Detail

After 96400 has passed, user can pass _tokenAmountDifference equal to negative of amountDue to perform liquidation with 0 tokens. However, this is not possible for tokens that doesn't support zero-transfer, and at least 1 wei must be supplied by the user, which is unexpected.

An example of revert on zero-transfer token is LEND.

The contest README states that any tokens compatible with Uniswap V3 should be supported.

We are allowing any standard token that would be compatible with Uniswap V3 to work with our codebase, just as was the case for the original audit of TellerV2.sol. The tokens are assumed to be able to work with Uniswap V3.

    function liquidateDefaultedLoanWithIncentive(
        uint256 _bidId,
        int256 _tokenAmountDifference
    ) public bidIsActiveForGroup(_bidId) {
        uint256 amountDue = getAmountOwedForBid(_bidId, false);

        uint256 loanDefaultedTimeStamp = ITellerV2(TELLER_V2)
            .getLoanDefaultTimestamp(_bidId);

        int256 minAmountDifference = getMinimumAmountDifferenceToCloseDefaultedLoan(
                amountDue,
                loanDefaultedTimeStamp
            );

        require(
            _tokenAmountDifference >= minAmountDifference,
            "Insufficient tokenAmountDifference"
        );

        if (_tokenAmountDifference > 0) {
            //this is used when the collateral value is higher than the principal (rare)
            //the loan will be completely made whole and our contract gets extra funds too
            uint256 tokensToTakeFromSender = abs(_tokenAmountDifference);

            IERC20(principalToken).transferFrom(
                msg.sender,
                address(this),
                amountDue + tokensToTakeFromSender
            );

            tokenDifferenceFromLiquidations += int256(tokensToTakeFromSender);

            totalPrincipalTokensRepaid += amountDue;
        } else {

            uint256 tokensToGiveToSender = abs(_tokenAmountDifference);

>           IERC20(principalToken).transferFrom(
>               msg.sender,
>               address(this),
>               amountDue - tokensToGiveToSender
>           );

            tokenDifferenceFromLiquidations -= int256(tokensToGiveToSender);

            totalPrincipalTokensRepaid += amountDue;
        }

        //this will give collateral to the caller
        ITellerV2(TELLER_V2).lenderCloseLoanWithRecipient(_bidId, msg.sender);
    }

    function getMinimumAmountDifferenceToCloseDefaultedLoan(
        uint256 _amountOwed,
        uint256 _loanDefaultedTimestamp
    ) public view virtual returns (int256 amountDifference_) {
        require(
            _loanDefaultedTimestamp > 0,
            "Loan defaulted timestamp must be greater than zero"
        );
        require(
            block.timestamp > _loanDefaultedTimestamp,
            "Loan defaulted timestamp must be in the past"
        );

        uint256 secondsSinceDefaulted = block.timestamp -
            _loanDefaultedTimestamp;

        int256 incentiveMultiplier = int256(86400) -
            int256(secondsSinceDefaulted);

        if (incentiveMultiplier < -10000) {
            incentiveMultiplier = -10000;
        }

        amountDifference_ =
            (int256(_amountOwed) * incentiveMultiplier) /
            int256(10000);
    }

Impact

User must supply 1 wei of token to call liquidateDefaultedLoanWithIncentive() even though the required amount is 0 (after 96400 seconds).

Code Snippet

Tool used

Manual review

Recommendation

Add a check for token transfer value, and skip calling transfer if it is zero.

nevillehuang commented 2 months ago

Low severity, since there is a workaround with extremely small amount of funds involved