code-423n4 / 2024-02-wise-lending-findings

11 stars 8 forks source link

User can DoS liquidations for extended period of time #185

Closed c4-bot-4 closed 5 months ago

c4-bot-4 commented 6 months ago

Lines of code

https://github.com/code-423n4/2024-02-wise-lending/blob/79186b243d8553e66358c05497e5ccfd9488b5e2/contracts/WiseSecurity/WiseSecurityHelper.sol#L876-L900 https://github.com/code-423n4/2024-02-wise-lending/blob/79186b243d8553e66358c05497e5ccfd9488b5e2/contracts/WiseLending.sol#L1250-L1309

Vulnerability details

The WiseSecurity.sol has a glitch in the checksLiquidation and checkMaxShares functions, where a user can prevent liquidation by front-running the liquidator. This is achieved by topping up their position with a minimal amount (e.g., 1 share) just before the liquidation transaction is processed. This method exploits the liquidation logic that determines the maximum shares a liquidator can acquire based on the current debt and collateral level. If the liquidator attempts to liquidate the maximum possible value to drive the price to healthly state or up to 50% for liquidations that don't have enough bad debt, the user can make a small top-up to their position, causing the liquidator's transaction to revert due to the TooManyShares error. Moreover, on Layer 2 networks where transaction costs are relatively low, users can repeatedly exploit this vulnerability with minimal expense, further driving their positions into bad debt.

Impact

Unhealthly positions can delay being liquidated by making minimal top-ups to their accounts.

Proof of Concept

  1. A user's account is on the verge of liquidation.

  2. A liquidator attempts to liquidate this account by calculating the maximum shares they can acquire, as defined in the checkMaxShares function.

    function checkMaxShares(
        uint256 _nftId,
        address _tokenToPayback,
        uint256 _borrowETHTotal,
        uint256 _unweightedCollateralETH,
        uint256 _shareAmountToPay
    )
        public
        view
    {
        uint256 totalSharesUser = WISE_LENDING.getPositionBorrowShares(
            _nftId,
            _tokenToPayback
        );
    
        uint256 maxShares = checkBadDebtThreshold(_borrowETHTotal, _unweightedCollateralETH)
            ? totalSharesUser
            : totalSharesUser * MAX_LIQUIDATION_50 / PRECISION_FACTOR_E18;
    
        if (_shareAmountToPay <= maxShares) {
            return;
        }
    
        revert TooManyShares();
    }
  3. Before the liquidator's transaction is mined, the user sends a transaction to slightly increase their collateral (by as little as 1 share or min amount that will revert).

  4. The liquidator's transaction then checks if the share amount to pay exceeds the maximum allowed shares, which now includes the user's tiny top-up.

  5. Because of this top-up, the liquidation transaction exceeds the maximum allowed shares and is reverted by the TooManyShares error, preventing the liquidation.

The user can pay back any amount of shares, as low as 1, because there is no check for min value:

    function manuallyPaybackShares(
        uint256 _keyId,
        uint256 _paybackShares
    )
        external
        updatePools
    {
        _manuallyPaybackShares(
            farmingKeys[_keyId],
            _paybackShares
        );

        emit ManualPaybackShares(
            _keyId,
            farmingKeys[_keyId],
            _paybackShares,
            block.timestamp
        );
    }
    function paybackExactAmount(
        uint256 _nftId,
        address _poolToken,
        uint256 _amount
    )
        external
        syncPool(_poolToken)
        returns (uint256)
    {
        uint256 paybackShares = calculateBorrowShares(
            {
                _poolToken: _poolToken,
                _amount: _amount,
                _maxSharePrice: false
            }
        );

        _validateNonZero(
            paybackShares
        );

        _handlePayback(
            msg.sender,
            _nftId,
            _poolToken,
            _amount,
            paybackShares
        );

       // [...]

Tools Used

Manual Review

Recommended Mitigation Steps

Require minimum amount of repayment from user as with deposits, for example min(userDebtInUSD, WISE_SECURITY.checkPoolWithMinDeposit(...)) . This will be more costly for the user and will limit the possible duration of bad debt attack.

Assessed type

DoS

c4-pre-sort commented 5 months ago

GalloDaSballo marked the issue as duplicate of #237

c4-pre-sort commented 5 months ago

GalloDaSballo marked the issue as insufficient quality report

c4-judge commented 5 months ago

trust1995 marked the issue as partial-50

c4-judge commented 5 months ago

trust1995 marked the issue as full credit

c4-judge commented 5 months ago

trust1995 marked the issue as satisfactory