sherlock-audit / 2024-02-leverage-contracts-judging

1 stars 0 forks source link

FastTiger - By using slippage control on saleToken in the `repay` function, the borrower may not be able to repay the borrowed liquidity. #17

Closed sherlock-admin closed 6 months ago

sherlock-admin commented 6 months ago

FastTiger

medium

By using slippage control on saleToken in the repay function, the borrower may not be able to repay the borrowed liquidity.

Summary

Using slippage control on saleToken in the repay function may cause some unusual problems. As a result, the borrower may not be able to repay the liquidity.

Vulnerability Detail

LiquidityBorrowingManager.sol#repay function used for repaying loans, optionally with liquidation or emergency liquidity withdrawal. In this function, slippage control on saleToken is used when the borrower repays the liquidity.

https://github.com/RealWagmi/wagmi-leverage/blob/main/contracts/LiquidityBorrowingManager.sol#L740-L741

The saleToekn is the amount remaining after being exchanged from holdToken and repaid to the Uniswap pool when the borrower repays the liquidity. Therefore, it is difficult for the borrower to predict minSaleTokenOut. Therefore the repay function may revert frequently, so the borrower only consumes gas.

Also the malicious owners of positions can be burned their positions.

In this case, the liquidity is not repaid. At this time, saleToken becomes 0 and is reverted by L740.

https://github.com/RealWagmi/wagmi-leverage/blob/main/contracts/LiquidityBorrowingManager.sol#L686-L707 https://github.com/RealWagmi/wagmi-leverage/blob/main/contracts/abstract/LiquidityManager.sol#L242-L303

/**
     * @notice Used for repaying loans, optionally with liquidation or emergency liquidity withdrawal.
     * The position is closed either by the trader or by the liquidator if the trader has not paid for holding the position
     * and the moment of liquidation has arrived.The positions borrowed from liquidation providers are restored from the held
     * token and the remainder is sent to the caller.In the event of liquidation, the liquidity provider
     * whose liquidity is present in the trader’s position can use the emergency mode and withdraw their liquidity.In this case,
     * he will receive hold tokens and liquidity will not be restored in the uniswap pool.
     * @param params The repayment parameters including
     *  activation of the emergency liquidity restoration mode (available only to the lender)
     *  internal swap pool fee,
     *  external swap parameters,
     *  borrowing key,
     *  swap slippage allowance.
     * @param deadline The deadline by which the repayment must be made.
     *
     * @return saleTokenOut The amount of saleToken returned back to the user after repayment.
     * @return holdTokenOut The amount of holdToken returned back to the user after repayment or emergency withdrawal.
     */
    function repay(
        RepayParams calldata params,
        uint256 deadline
    )
        external
        nonReentrant
        checkDeadline(deadline)
        returns (uint256 saleTokenOut, uint256 holdTokenOut)
    {
        BorrowingInfo memory borrowing = borrowingsInfo[params.borrowingKey];
        // Check if the borrowing key is valid
        _existenceCheck(borrowing.borrowedAmount);

        bool zeroForSaleToken = borrowing.saleToken < borrowing.holdToken;
        uint256 liquidationBonus = borrowing.liquidationBonus;
        int256 collateralBalance;
        // Update token rate information and get holdTokenRateInfo storage reference
        (, TokenInfo storage holdTokenRateInfo) = _updateHoldTokenRateInfo(
            borrowing.saleToken,
            borrowing.holdToken
        );
        {
            // Calculate collateral balance and validate caller
            uint256 accLoanRatePerSeconds = holdTokenRateInfo.accLoanRatePerSeconds;
            uint256 currentFees;
            (collateralBalance, currentFees) = _calculateCollateralBalance(
                borrowing.borrowedAmount,
                borrowing.accLoanRatePerSeconds,
                borrowing.dailyRateCollateralBalance,
                accLoanRatePerSeconds
            );

            (msg.sender != borrowing.borrower && collateralBalance >= 0).revertError(
                ErrLib.ErrorCode.INVALID_CALLER
            );

            // Calculate liquidation bonus and adjust fees owed

            if (collateralBalance > 0) {
                uint256 compensation = _calcFeeCompensationUpToMin(
                    collateralBalance,
                    currentFees,
                    borrowing.feesOwed
                );
                currentFees += compensation;
                collateralBalance -= int256(compensation);
                liquidationBonus +=
                    uint256(collateralBalance) /
                    Constants.COLLATERAL_BALANCE_PRECISION;
            } else {
                currentFees = borrowing.dailyRateCollateralBalance;
            }

            // Calculate platform fees and adjust fees owed
            borrowing.feesOwed += _pickUpPlatformFees(borrowing.holdToken, currentFees);
        }
        // Check if it's an emergency repayment
        if (params.isEmergency) {
            (collateralBalance >= 0).revertError(ErrLib.ErrorCode.FORBIDDEN);
            (
                uint256 removedAmt,
                uint256 feesAmt,
                bool completeRepayment
            ) = _calculateEmergencyLoanClosure(
                    zeroForSaleToken,
                    params.borrowingKey,
                    borrowing.feesOwed,
                    borrowing.borrowedAmount
                );
            (removedAmt == 0).revertError(ErrLib.ErrorCode.LIQUIDITY_IS_ZERO);
            // Subtract the removed amount and fees from borrowedAmount and feesOwed
            borrowing.borrowedAmount -= removedAmt;
            borrowing.feesOwed -= feesAmt;
            feesAmt /= Constants.COLLATERAL_BALANCE_PRECISION;
            // Deduct the removed amount from totalBorrowed
            holdTokenRateInfo.totalBorrowed -= removedAmt;
            // If loansInfoLength is 0, remove the borrowing key from storage and get the liquidation bonus
            if (completeRepayment) {
                LoanInfo[] memory empty;
                _removeKeysAndClearStorage(borrowing.borrower, params.borrowingKey, empty);
                feesAmt += liquidationBonus;
            } else {
                // make changes to the storage
                BorrowingInfo storage borrowingStorage = borrowingsInfo[params.borrowingKey];
                borrowingStorage.dailyRateCollateralBalance = 0;
                borrowingStorage.feesOwed = borrowing.feesOwed;
                borrowingStorage.borrowedAmount = borrowing.borrowedAmount;
            }
            holdTokenOut = removedAmt + feesAmt;
            // Transfer removedAmt + feesAmt to msg.sender and emit EmergencyLoanClosure event
            Vault(VAULT_ADDRESS).transferToken(borrowing.holdToken, msg.sender, holdTokenOut);
            emit EmergencyLoanClosure(borrowing.borrower, msg.sender, params.borrowingKey);
        } else {
            // Deduct borrowedAmount from totalBorrowed
            holdTokenRateInfo.totalBorrowed -= borrowing.borrowedAmount;

            // Transfer the borrowed amount and liquidation bonus from the VAULT to this contract
            Vault(VAULT_ADDRESS).transferToken(
                borrowing.holdToken,
                address(this),
                borrowing.borrowedAmount + liquidationBonus
            );

            if (params.externalSwap.length != 0) {
                _callExternalSwap(borrowing.holdToken, params.externalSwap);
            }

686         // Restore liquidity using the borrowed amount and pay a daily rate fee
687         LoanInfo[] memory loans = loansInfo[params.borrowingKey];
688         _maxApproveIfNecessary(
689             borrowing.holdToken,
690             address(underlyingPositionManager),
691             type(uint128).max
692         );
693         _maxApproveIfNecessary(
694             borrowing.saleToken,
695             address(underlyingPositionManager),
696             type(uint128).max
697         );
698
699         _restoreLiquidity(
700             RestoreLiquidityParams({
701                 zeroForSaleToken: zeroForSaleToken,
702                 swapPoolfeeTier: params.internalSwapPoolfee,
703                 totalfeesOwed: borrowing.feesOwed,
704                 totalBorrowedAmount: borrowing.borrowedAmount
705             }),
706             loans
707         );

            // Remove borrowing key from related data structures
            _removeKeysAndClearStorage(borrowing.borrower, params.borrowingKey, loans);

            // Get the remaining balance of saleToken and holdToken
            (saleTokenOut, holdTokenOut) = _getPairBalance(
                borrowing.saleToken,
                borrowing.holdToken
            );

            if (saleTokenOut > 0 && params.returnOnlyHoldToken) {
                (, uint256 holdTokenAmountOut) = _simulateSwap(
                    zeroForSaleToken,
                    params.internalSwapPoolfee,
                    borrowing.saleToken, // saleToken is tokenIn
                    borrowing.holdToken,
                    saleTokenOut
                );
                if (holdTokenAmountOut > 0) {
                    // Call the internal v3SwapExactInput function
                    holdTokenOut += _v3SwapExactInput(
                        v3SwapExactInputParams({
                            fee: params.internalSwapPoolfee,
                            tokenIn: borrowing.saleToken,
                            tokenOut: borrowing.holdToken,
                            amountIn: saleTokenOut
                        })
                    );
                    saleTokenOut = 0;
                }
            }

740         (holdTokenOut < params.minHoldTokenOut || saleTokenOut < params.minSaleTokenOut)
741             .revertError(ErrLib.ErrorCode.PRICE_SLIPPAGE_CHECK);

            // Pay a profit to a msg.sender
            _pay(borrowing.holdToken, address(this), msg.sender, holdTokenOut);
            _pay(borrowing.saleToken, address(this), msg.sender, saleTokenOut);

            emit Repay(borrowing.borrower, msg.sender, params.borrowingKey);
        }
    }

Impact

The borrower may not be able to repay the borrowed liquidity.

Code Snippet

https://github.com/sherlock-audit/2024-02-leverage-contracts/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L579C5-L749C6

Tool used

Manual Review

Recommendation

Because saleToken is exchanged from holdToken, only use slippage control for holdToken.

nevillehuang commented 6 months ago

Invalid, borrower should take into account and implement appropriate slippages during repayment. This is not a security risk