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.
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.
/**
* @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.
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
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.