Skip to content
This repository has been archived by the owner on Apr 28, 2024. It is now read-only.

seeques - Collateral sent during borrowing is lost due to not accounting for borrowingCollateral in borrowing.DailyRateCollateralBalance #67

Closed
sherlock-admin opened this issue Oct 23, 2023 · 18 comments
Assignees
Labels
Escalation Resolved This issue's escalations have been approved/rejected Non-Reward This issue will not receive a payout Sponsor Disputed The sponsor disputed this issue's validity Won't Fix The sponsor confirmed this issue will not be fixed

Comments

@sherlock-admin
Copy link
Contributor

sherlock-admin commented Oct 23, 2023

seeques

high

Collateral sent during borrowing is lost due to not accounting for borrowingCollateral in borrowing.DailyRateCollateralBalance

Summary

During borrowing, a borrower has to pay some collateral in holdToken plus deposit for holding a position for 24 hours, which is a product of the borrowedAmount and the currentDailyRate. The latter is added to the collateral balance of the borrower, while the former is not. This makes the collateral stuck in the Vault.

Vulnerability Detail

In the borrow() function only the dailyRateCollateral is added to the borrowing.DailyRateCollateralBalance, while both dailyRateCollateral and borrowingCollateral are transferred to the system.

borrowing.DailyRateCollateralBalance is the collateral balance before fees accrual and it is used to determine if a loan is liquidatable or not by calculating the actual collateral balance of the loan (that is, collateral balance minus the fees accrued).

            (collateralBalance, currentFees) = _calculateCollateralBalance(
                borrowing.borrowedAmount,
                borrowing.accLoanRatePerSeconds,
                borrowing.dailyRateCollateralBalance,
                accLoanRatePerSeconds
            );

The only way of retrieving collateral from the Vault is calling the repay() function which can be called is case of liquidation or closing a position by the borrower.

In case of liquidation (i.e., when borrowing.DailyRateCollateralBalance minus the fees < 0), the borrowing.DailyRateCollateralBalance is added to borrowing.feesOwed after which tokens are transferred from the Vault and liquidity is being restored in uniswap position in the call to _restoreLiquidity(), during which fees owed to a position owner is also transferred from the Vault.

In case of closing position by the borrower the same happens, only his borrowing.DailyRateCollateralBalance is applied not to borrowing.feesOwed, but to the liquidationBonus which he receives at the end of the call.

There is a 3rd case of emergency closure by the uniswap position owner but it is the same in a sense that no more tokens are drawn from the Vault that are borrowed and owed.

This means that the borrowingCollateral which is transferred upon borrowing is lost in the system.

Impact

Loss of funds.

Code Snippet

https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L488-L490
https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L492-L503
https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L552-L557
https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/DailyRateAndCollateral.sol#L103-L118

Tool used

Manual Review

Recommendation

Add borrowing collateral to the collateral balance

borrowing.dailyRateCollateralBalance +=
            (cache.dailyRateCollateral *
            Constants.COLLATERAL_BALANCE_PRECISION) + (borrowingCollateral * Constants.COLLATERAL_BALANCE_PRECISION);
@github-actions github-actions bot added the High A valid High severity issue label Oct 26, 2023
@fann95 fann95 added Sponsor Disputed The sponsor disputed this issue's validity Won't Fix The sponsor confirmed this issue will not be fixed labels Oct 29, 2023
@fann95
Copy link

fann95 commented Oct 29, 2023

The borrowingCollateral is the margin deposit which is included in the borrowedAmount, therefore, it is only lost when the price moves in an unfavourable direction. In other cases, upon closing, after liquidity is restored, the remaining balance of borrowedAmount (including borrowed collateral) is returned to the trader (borrower).

@fann95 fann95 self-assigned this Oct 29, 2023
@cvetanovv cvetanovv removed the High A valid High severity issue label Oct 30, 2023
@sherlock-admin sherlock-admin changed the title Dandy Taupe Barracuda - Collateral sent during borrowing is lost due to not accounting for borrowingCollateral in borrowing.DailyRateCollateralBalance seeques - Collateral sent during borrowing is lost due to not accounting for borrowingCollateral in borrowing.DailyRateCollateralBalance Oct 30, 2023
@sherlock-admin sherlock-admin added the Non-Reward This issue will not receive a payout label Oct 30, 2023
@seeques
Copy link

seeques commented Oct 31, 2023

Escalate

As the code suggests, both borrowedAmount and borrowingCollateral are included into holdTokenBalance (as per #86). borrowedAmount is calculated as a single-sided position and holdTokenBalance is the borrowedAmount + swapped saleTokens. If the position is in the current tick range, only those holdTokens would be a part of borrowedAmount which were extracted from the position BEFORE swap was made, and those holdTokens that were received through swapping would not be accredited to anything in the system and they would be lost.

At first I thought that borrowingCollateral was this missing part of holdTokens that were swapped and which were not attributed to the borrowedAmount. I understand now that the borrowingCollateral is the margin deposit and that it should have been included in the borrowedAmount as per design. Hovewer, as per case explained above, the holdTokens that were swapped should be added to borrowedAmount still. Otherwise, the restoredLiquidity would be lower than the liquidity.loan, the repayment would revert and tokens would be stuck in the vault.

@sherlock-admin2
Copy link
Contributor

sherlock-admin2 commented Oct 31, 2023

Escalate

As the code suggests, both borrowedAmount and borrowingCollateral are included into holdTokenBalance (as per #86). borrowedAmount is calculated as a single-sided position and holdTokenBalance is the borrowedAmount + swapped saleTokens. If the position is in the current tick range, only those holdTokens would be a part of borrowedAmount which were extracted from the position BEFORE swap was made, and those holdTokens that were received through swapping would not be accredited to anything in the system and they would be lost.

At first I thought that borrowingCollateral was this missing part of holdTokens that were swapped and which were not attributed to the borrowedAmount. I understand now that the borrowingCollateral is the margin deposit and that it should have been included in the borrowedAmount as per design. Hovewer, as per case explained above, the holdTokens that were swapped should be added to borrowedAmount still. Otherwise, the restoredLiquidity would be lower than the liquidity.loan, the repayment would revert and tokens would be stuck in the vault.

You've created a valid escalation!

To remove the escalation from consideration: Delete your comment.

You may delete or edit your escalation comment anytime before the 48-hour escalation window closes. After that, the escalation becomes final.

@sherlock-admin sherlock-admin added the Escalated This issue contains a pending escalation label Oct 31, 2023
@Czar102
Copy link

Czar102 commented Nov 4, 2023

@cvetanovv @fann95 what do you think?

@fann95
Copy link

fann95 commented Nov 4, 2023

and holdTokenBalance is the borrowedAmount + swapped saleTokens.

This is incorrect. holdTokenBalance - these are the tokens received during liquidity extraction (including the result of the swap).
borrowedAmount = holdTokenBalance+marginDeposit
borrowedAmount is the maximum amount calculated on the border of the position range(single mode).

@seeques
Copy link

seeques commented Nov 4, 2023

Hi @fann95
Yes, excuse me for this confusion. My main point is that nowhere in the code this part of holdTokenBalance that was received as the result of the swap is added to borrowedAmount, which makes the borrowedAmount less than intended.

@fann95
Copy link

fann95 commented Nov 4, 2023

It should not be added, it is part of borrowedAmount. That is, this is what you actually received. borrowedAmount.will be more than holdTokenBalance or equal if the position is outside the current tick and swap will not occur

@seeques
Copy link

seeques commented Nov 4, 2023

If the position is outside, that is correct. But if the position is inside the current tick range, which would be in 90% of the cases, and the swap will occur, these swapped tokens are just sent to the Vault, where they would be stuck and lost as they are not represented anywhere in the system, which is a loss of liquidity for LPs.

@fann95
Copy link

fann95 commented Nov 4, 2023

If the position is outside, that is correct. But if the position is inside the current tick range, which would be in 90% of the cases, and the swap will occur, these swapped tokens are just sent to the Vault, where they would be stuck and lost as they are not represented anywhere in the system, which is a loss of liquidity for LPs.

No, these exchanged tokens are part of the borrowedAmount . Because if the position is inside a tick, the result of extracted+swaped tokens will be less than the borrowedAmount. extracted+swaped+marginDeposit=borrowedAmount (which is stored)

@seeques
Copy link

seeques commented Nov 4, 2023

borrowedAmount is calculated single-sidedly (only as amount0 or amount1, depending on the zeroForSaleToken flag):

                // Calculate borrowed amount
                borrowedAmount += _getSingleSideRoundUpBorrowedAmount(
                    zeroForSaleToken,
                    tickLower,
                    tickUpper,
                    liquidity
        borrowedAmount = (
            zeroForSaleToken
                ? LiquidityAmounts.getAmount1ForLiquidity(
                    TickMath.getSqrtRatioAtTick(tickLower),
                    TickMath.getSqrtRatioAtTick(tickUpper),
                    liquidity
                )
                : LiquidityAmounts.getAmount0ForLiquidity(
                    TickMath.getSqrtRatioAtTick(tickLower),
                    TickMath.getSqrtRatioAtTick(tickUpper),
                    liquidity
                )
        );

But both amount0 and amount1 are borrowed from LP's position during liquidity extraction if the position is in the current tick range. Hence my reasoning that those tokens are not part of the borrowedAmount.
Moreover, my issue #86, which is deemed valid and was confirmed by you, also explains why borrowedAmount in this case is less than extracted+swapped

@fann95
Copy link

fann95 commented Nov 4, 2023

Probably such a fantastic case is possible with positive slippage when the extracted+swaped tokens will be slightly more than the borrowedAmount, but in this case this dust will remain on the contract and it will be taken by the first one who calls the repay. We did not process these cases because they are unlikely and we wouldn't want to complicate the code and waste extra gas. The number of tokens in the single-side position will be more than on the current tick even if you exchange the second token.The price of a token will be worse in single positions.That is why borrowedAmount is calculated at the worst price that can be. I accepted #86 problem since it was possible to get DOS simply by sending a small number of tokens to the contract.
P.S. It would be great to receive more tokens when extracting liquidity on the current tick than after extracting in a single position.))

@Czar102
Copy link

Czar102 commented Nov 5, 2023

@seeques would you agree with the above?

@fann95 can you elaborate on what do you mean by "positive slippage" and "dust"? Does "dust" imply that the loss is constrained to a small amount? In what way?

@seeques
Copy link

seeques commented Nov 6, 2023

Yes, I agree. I would argue though that any dust would not remain in the contract, but as a part of holdTokenBalance would be transferred to the Vault. And upon repayment this dust would not be taken as it would not be tranferred back to the contract from the Vault.

@Czar102
Copy link

Czar102 commented Nov 6, 2023

@seeques does it mean that this behavior doesn't present a high/medium severity issue?

@seeques
Copy link

seeques commented Nov 6, 2023

It does not. I misinterpreted initially amounts calculation, but now I can see where my problem was.

@Czar102
Copy link

Czar102 commented Nov 6, 2023

Great that consensus has been achieved. Will be rejecting the escalation and keeping the issue invalid.

@Evert0x
Copy link

Evert0x commented Nov 7, 2023

Result:
Invalid
Unique

@sherlock-admin2 sherlock-admin2 removed the Escalated This issue contains a pending escalation label Nov 7, 2023
@sherlock-admin2
Copy link
Contributor

Escalations have been resolved successfully!

Escalation status:

@sherlock-admin sherlock-admin added the Escalation Resolved This issue's escalations have been approved/rejected label Nov 7, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Escalation Resolved This issue's escalations have been approved/rejected Non-Reward This issue will not receive a payout Sponsor Disputed The sponsor disputed this issue's validity Won't Fix The sponsor confirmed this issue will not be fixed
Projects
None yet
Development

No branches or pull requests

7 participants