GithubHelp home page GithubHelp logo

2023-10-real-wagmi-judging's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

2023-10-real-wagmi-judging's Issues

feelereth - Vulnerability where the current daily rate could be unintentionally defaulted to the Constants.DEFAULT_DAILY_RATE if it has not been properly set.

feelereth

medium

Vulnerability where the current daily rate could be unintentionally defaulted to the Constants.DEFAULT_DAILY_RATE if it has not been properly set.

Summary

The _getHoldTokenRateInfo() function does assume that the currentDailyRate is already set, but defaults it to Constants.DEFAULT_DAILY_RATE if it is not set. This could potentially allow the rate to be unintentionally defaulted

Vulnerability Detail

the _getHoldTokenRateInfo() function does have a potential vulnerability where the current daily rate could be unintentionally defaulted to the Constants.DEFAULT_DAILY_RATE if it has not been properly set.
Here is how it works:
In _getHoldTokenRateInfo(), the currentDailyRate is first read from the TokenInfo struct in storage:

 currentDailyRate = holdTokenRateInfo.currentDailyRate;

Then this is checked:

 if (currentDailyRate == 0) {
   currentDailyRate = Constants.DEFAULT_DAILY_RATE; 
 }

If the currentDailyRate has not been explicitly set, it will be 0. So in this case, it will get defaulted to Constants.DEFAULT_DAILY_RATE.
This could lead to unintended behavior, as any calling contracts or users expecting a specific rate to be set would instead get the default rate.
For example, if a lending contract calls this to calculate fees owed, it may end up charging less interest than intended if the rate gets defaulted.

Impact

Reduced expected revenue for lending activities due to lower interest rates. Incorrect interest fee calculations over time, leading to lost revenue for the protocol or unfair overcharging of users. The error compounds the more the contract is used.

Code Snippet

https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/DailyRateAndCollateral.sol#L42
https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/DailyRateAndCollateral.sol#L43-L45

Tool used

Manual Review

Recommendation

You should explicitly prevent defaulting in _getHoldTokenRateInfo():

 if (currentDailyRate == 0) {
   revert("Current daily rate not set");
 }

And require the calling contract to properly set the rate first via a separate write function or constructor before calling _getHoldTokenRateInfo(). This way there is no risk of accidentally relying on the default rate.

Soltho - Do not use abi.encode to interact with ERC20 functions

Soltho

medium

Do not use abi.encode to interact with ERC20 functions

Summary

Wrongly encoded calls are not detected by the compiler and revert at runtime.

Vulnerability Detail

While sometimes the contract uses the IERC20 interface to interact with tokens, other times it does use raw call encoding. This results in a incosistent implementation. Also, all the token interactions can be done safe and efficiently with specific libraries such as Solady's SafeTransferLib

/**
     * @dev This internal function attempts to approve a specific amount of tokens for a spender.
     * It performs a call to the `approve` function on the token contract using the provided parameters,
     * and returns a boolean indicating whether the approval was successful or not.
     * @param token The address of the token contract.
     * @param spender The address of the spender.
     * @param amount The amount of tokens to be approved.
     * @return A boolean indicating whether the approval was successful or not.
     */
    function _tryApprove(address token, address spender, uint256 amount) private returns (bool) {
        (bool success, bytes memory data) = token.call(
            abi.encodeWithSelector(IERC20.approve.selector, spender, amount)
        );
        return success && (data.length == 0 || abi.decode(data, (bool)));
    }

Impact

Low, since the code has been tested.

Code Snippet

https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/package.json#L16

Tool used

Manual Review

Recommendation

Use Solady's SafeTransferLib

IvanFitro - _pay() will always revert in some functions for not approving VAULT_ADDRESS address to spend the tokens of the borrower.

IvanFitro

medium

_pay() will always revert in some functions for not approving VAULT_ADDRESS address to spend the tokens of the borrower.

Summary

_pay() is employed to transfer tokens from the borrower to the VAULT_ADDRESS. To achieve this, a transferFrom() operation is used, which transfer the tokens from the borrower's address (EOA) indicated by msg.sender() to the VAULT_ADDRESS, but the VAULT_ADDRESS is never approved to spend the tokens of the borrower.

Vulnerability Detail

In several functions _pay() is called. These functions involve two addresses: msg.sender, representing the borrower (EOA) and VAULT_ADDRESS, which is where the tokens are intended to be transferred

       _pay(               
            params.holdToken,
            msg.sender,    
            VAULT_ADDRESS,
            borrowingCollateral + liquidationBonus + cache.dailyRateCollateral + feesDebt
        );

When the payer is not the contract, the safeTransferFrom() function is employed to do the token transfers. However, the crucial step in this process is approving VAULT_ADDRESS to enable to expend the tokens belonging to the payer (borrower). This approval is managed by _maxApproveIfNecessary().

The issue arises because the _maxApproveIfNecessary() is never invoked. This omission causes all transactions to revert.

function _pay(address token, address payer, address recipient, uint256 value) public {
        if (value > 0) {
            if (payer == address(this)) {   
                IERC20(token).safeTransfer(recipient, value);   
            } else {
                IERC20(token).safeTransferFrom(payer, recipient, value);    
            }
        }
    }
function _maxApproveIfNecessary(address token, address spender, uint256 amount) internal {
        if (IERC20(token).allowance(address(this), spender) < amount) {
            if (!_tryApprove(token, spender, type(uint256).max)) {
                if (!_tryApprove(token, spender, type(uint256).max - 1)) {
                    require(_tryApprove(token, spender, 0));
                    if (!_tryApprove(token, spender, type(uint256).max)) {
                        if (!_tryApprove(token, spender, type(uint256).max - 1)) {
                            true.revertError(ErrLib.ErrorCode.ERC20_APPROVE_DID_NOT_SUCCEED);
                        }
                    }
                }
            }
        }
    }

Impact

In increaseCollateralBalance(), takeOverDebt() and borrow() will always revert.

Code Snippet

https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L381
https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L451
https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L498-L503
https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/abstract/ApproveSwapAndPay.sol#L91-L104

Tool used

Manual Review

Recommendation

Before call _pay() approve the VAULT_ADDRESS to spend the tokens of the borrower using _maxApproveIfNecessary().

kaysoft - Use of UniswapV3 slot0() function to get sqrtPriceLimitX96 can lead to price manipulation.

kaysoft

high

Use of UniswapV3 slot0() function to get sqrtPriceLimitX96 can lead to price manipulation.

Summary

The UniswapV3 slot0() function is used to get the sqrtPriceX96 as shown below.

function _getCurrentSqrtPriceX96(
        bool zeroForA,
        address tokenA,
        address tokenB,
        uint24 fee
    ) private view returns (uint160 sqrtPriceX96) {
        if (!zeroForA) {
            (tokenA, tokenB) = (tokenB, tokenA);
        }
        address poolAddress = computePoolAddress(tokenA, tokenB, fee);
        (sqrtPriceX96, , , , , , ) = IUniswapV3Pool(poolAddress).slot0();//@audit prone to price manipulation search slot0
    }

Vulnerability Detail

The _getCurrentSqrtPriceX96(...) function above is used in _getHoldTokenAmountIn to calculate values which were used for swap here: https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L229-L308

LINK TO THE UNISWAP SLOT0 FUNCTION USED TO GET sqrtPriceX96

Impact

Using Uniswap's slot0 function to get sqrtPriceX96 can be manipulated which will cause loss of asset for the user.

Code Snippet

Tool used

Manual Review

Recommendation

Use the TWAP to get the value of sqrtPriceX96

Reference

Duplicate of #109

feelereth - Malicious users can avoid liquidation penalties by setting the liquidation bonus to 0

feelereth

high

Malicious users can avoid liquidation penalties by setting the liquidation bonus to 0

Summary

The liquidation bonus calculations make assumptions about default values if no bonus is set. An attacker could set the bonus to 0 to avoid penalties.

Vulnerability Detail

An attacker could manipulate the liquidation bonus calculation to avoid penalties by setting the bonus to 0. Here is an explanation:

The getLiquidationBonus function first retrieves the Liquidation struct for the given token:

  Liquidation memory liq = liquidationBonusForToken[token];

It then checks if the bonusBP field is 0:

  if (liq.bonusBP == 0) {
    // use default bonus  
  }

If bonusBP is 0, it will use the default bonus defined in the Constants contract instead of applying a token-specific bonus.
An attacker could exploit this by:

  1. Calling setLiquidationBonus as the owner to set bonusBP to 0 for a token:

        setLiquidationBonus(token, 0, 0);
    
  2. Borrowing that token.

  3. Defaulting on the loan.

When the loan is liquidated, getLiquidationBonus will be called and use the default bonus instead of applying a higher token-specific bonus. This allows the attacker to avoid the intended larger liquidation penalty

Impact

Attackers could borrow assets without being penalized for defaulting. This makes lending riskier and less viable, and exposes lenders to potential exploitation.

Code Snippet

https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L683-L703

Tool used

Manual Review

Recommendation

getLiquidationBonus could be changed to enforce a minimum bonus percentage if the token-specific bonus is 0:

  function getLiquidationBonus(
          address token,
          uint256 borrowedAmount,
          uint256 times
      ) public view returns (uint256 liquidationBonus) {

    Liquidation memory liq = liquidationBonusForToken[token];

    if (liq.bonusBP == 0) {
      // Enforce minimum bonus
      liq.bonusBP = MINIMUM_BONUS_BP; 
    }

    // Rest of function
  }

This would prevent attackers from avoiding liquidation penalties by setting the bonus to 0.

feelereth - Blindly adding 1 to amount0 and amount1 in _increaseLiquidity() can result in providing more liquidity than intended.

feelereth

high

Blindly adding 1 to amount0 and amount1 in _increaseLiquidity() can result in providing more liquidity than intended.

Summary

When calculating amounts for increaseLiquidity(), it blindly adds 1 to amount0 and amount1. This could result in providing more liquidity than intended if those amounts were already at non-zero value.

Vulnerability Detail

Blindly adding 1 to amount0 and amount1 in _increaseLiquidity() can result in providing more liquidity than intended.
The relevant code is:

  function _increaseLiquidity(
      address saleToken, 
      address holdToken,
      LoanInfo memory loan,
      uint256 amount0,
      uint256 amount1
  ) private {

    if (amount0 > 0) ++amount0;

    if (amount1 > 0) ++amount1;

       // Call increaseLiquidity with increased amounts

  }

Here is how it can lead to excess liquidity:
• Let's say amount0 and amount1 originally had non-zero values, say 100 and 200 respectively
• The _increaseLiquidity() function blindly adds 1 to these amounts
• So now amount0 becomes 101 and amount1 becomes 201
• These new inflated amounts are passed to increaseLiquidity()
• More liquidity will be added than required, based on the original amount0 and amount1 values of 100 and 200
This introduces a vulnerability where an attacker could extract some liquidity from the position, and then exploit this blind increment to add more liquidity than they returned, effectively stealing funds.

Impact

The impact is that if amount0 and amount1 already had non-zero values before calling _increaseLiquidity(), adding 1 to those values would increase the liquidity provided.
For example, if amount0 was 10 and amount1 was 20 before calling this function, they would be increased to 11 and 21 respectively. So the final liquidity provided would be higher than if the original amounts were used.
This introduces a vulnerability where an attacker could exploit this to steal funds by providing more liquidity than they should be able to.
In summary, the blind increment allows attackers to artificially modify liquidity holdings, disrupt pool accounting, and steal funds by depositing less assets than the liquidity added.

Code Snippet

https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L394-L395

Tool used

Manual Review

Recommendation

The increment should only happen if amount0 and amount1 values are 0 originally:

  if (amount0 == 0) {
    ++amount0; 
  }

  if (amount1 == 0) {
    ++amount1;
  }

This ensures the increment only happens to avoid rounding down from non-zero amounts to zero, but doesn't blindly add 1 in all cases.
So in summary, the blind increment can lead to excess liquidity vulnerability, and should be guarded to only apply when amount values are originally zero.

feelereth - completeRepayment flag just checks loans.length == 0. But loans could be manipulated inside the loop

feelereth

high

completeRepayment flag just checks loans.length == 0. But loans could be manipulated inside the loop

Summary

The key issue is that loans is a storage array that can be manipulated inside the _calculateEmergencyLoanClosure function.

Vulnerability Detail

The completeRepayment flag just checks loans.length == 0. But loans could be manipulated inside the loop, so this can not be accurate

The key issue here is that loans is a storage array that can be manipulated inside the _calculateEmergencyLoanClosure function.
Specifically, this line is problematic:

     loans[i] = loans[loans.length - 1];

This replaces the loan at index i with the last loan in the array. Then loans.pop() is called to remove the last element.
The problem is that loans.length is not decremented when an element is replaced, so it remains the same even though an element was removed.
This means completeRepayment = loans.length == 0 may not accurately reflect if all loans were removed or not.
For example:

 loans = [A, B, C]   // loans.length = 3

 Replace loan A with loan C
 loans = [C, B, C]

 Pop last element 
 loans = [C, B]  

 loans.length still equals 3 even though A was removed

So a malicious caller could remove loans but make it appear that loans still exist by manipulating the length in this way.

Impact

This could allow attackers to steal funds, avoid repaying debts, or wrongly claim liquidation bonuses. The contract's accounting could become corrupted. In essence, it jeopardizes the integrity of the borrowing and repayment process. It could lead to loss of funds or improper accounting by the contract.

Code Snippet

https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L735

Tool used

Manual Review

Recommendation

loans.length should be decremented whenever an element is removed:

 function _calculateEmergencyLoanClosure(...) {

   ...

   if (owner matches) {
     loans[i] = loans[loans.length - 1];
     loans.pop();

     // Decrement loans.length
     loans.length--;

   }

   ...

 }

Additionally, keeping a separate counter variable that tracks the number of loans removed would be more robust than relying on loans.length alone.

Bauer - Lack of slippage protection when reducing liquidity

Bauer

high

Lack of slippage protection when reducing liquidity

Summary

When calling Uniswap V3's decreaseLiquidityParams(), both amount0Min and amount1Min are set to zero. This absence of minimum acceptable amounts (slippage protection) could lead to unintended consequences during liquidity reduction.

Vulnerability Detail

In the function LiquidityManager._decreaseLiquidity(), when calling Uniswap V3's decreaseLiquidity(), setting both amount0Min and amount1Min to 0 essentially means that there is no slippage protection in place. This omission of slippage protection can lead to several issues, mainly due to the lack of a minimum expected amount for each asset when decreasing liquidity in a Uniswap V3 position.

  function _decreaseLiquidity(uint256 tokenId, uint128 liquidity) private {
        // Call the decreaseLiquidity function of underlyingPositionManager contract
        // with DecreaseLiquidityParams struct as argument
        (uint256 amount0, uint256 amount1) = underlyingPositionManager.decreaseLiquidity(
            INonfungiblePositionManager.DecreaseLiquidityParams({
                tokenId: tokenId,
                liquidity: liquidity,
                amount0Min: 0,
                amount1Min: 0,
                deadline: block.timestamp
            })
        );

Impact

Users may incur losses

Code Snippet

https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L349-L376

Tool used

Manual Review

Recommendation

It is advisable to set appropriate minimum amounts for both amount0Min and amount1Min when interacting with Uniswap V3's decreaseLiquidityParams().

feelereth - Incorrect collateral amount due to changes between when currentDailyRate is retrieved and when the final collateralAmt is calculated

feelereth

high

Incorrect collateral amount due to changes between when currentDailyRate is retrieved and when the final collateralAmt is calculated

Summary

The currentDailyRate can change right after it is retrieved, but before the final collateralAmt is calculated. This can lead to an incorrect collateral amount

Vulnerability Detail

The currentDailyRate can change between when it is retrieved and when the final collateralAmt is calculated, leading to an incorrect collateral amount. Here is a more detailed explanation:
In the calculateCollateralAmtForLifetime function, it first retrieves the currentDailyRate for the holdToken:

  (uint256 currentDailyRate, ) = _getHoldTokenRateInfo(
                  borrowing.saleToken, 
                  borrowing.holdToken
              );

Later in the function, it uses this currentDailyRate value to calculate the collateralAmt:

  uint256 everySecond = (
                  FullMath.mulDivRoundingUp(
                      borrowing.borrowedAmount,
                      currentDailyRate * Constants.COLLATERAL_BALANCE_PRECISION,  
                      1 days * Constants.BP
                  )
              );

  collateralAmt = FullMath.mulDivRoundingUp(
                  everySecond,
                  lifetimeInSeconds,
                  Constants.COLLATERAL_BALANCE_PRECISION
              );

The issue is that between retrieving currentDailyRate and using it to calculate collateralAmt, the currentDailyRate could be updated via the updateHoldTokenDailyRate function:

  function updateHoldTokenDailyRate(
          address saleToken,
          address holdToken,
          uint256 value
      ) external {
       // update currentDailyRate
  }

So the collateralAmt calculation would be using an outdated rate.

Impact

The unpredictable collateral requirements from the race condition introduce instability, capital inefficiency, unexpected liquidations, and can erode user trust in the platform over time

Code Snippet

https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L343-L346
https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L348-L360
https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L211

Tool used

Manual Review

Recommendation

Ensure the currentDailyRate value used in the calculation is the latest value. This can be done by moving the collateralAmt calculation right after retrieving the currentDailyRate. A suggestive example below:

  (uint256 currentDailyRate, ) = _getHoldTokenRateInfo(
                  borrowing.saleToken, 
                  borrowing.holdToken
              );
        
  // calculate collateralAmt using currentDailyRate            
  uint256 everySecond = (
                  FullMath.mulDivRoundingUp(
                      borrowing.borrowedAmount,
                      currentDailyRate * Constants.COLLATERAL_BALANCE_PRECISION,   
                      1 days * Constants.BP
                  )
              );

  uint256 collateralAmt = FullMath.mulDivRoundingUp(
                  everySecond,
                  lifetimeInSeconds,
                  Constants.COLLATERAL_BALANCE_PRECISION
              );

This ensures the collateralAmt is calculated atomically using the latest currentDailyRate value.

IceBear - _v3SwapExactInput() lack of the deadline parameter

IceBear

medium

_v3SwapExactInput() lack of the deadline parameter

Summary

_v3SwapExactInput() lack of the deadline parameter

Vulnerability Detail

Without a deadline parameter, the transaction may sit in the mempool and be executed at a much later time potentially resulting in a worse price.

Impact

Please refer to the Uniswap V3 doc for the design of the "swapExactInputSingle" parameters.
Includes a deadline parameter to protect against long-pending transactions and wild swings in prices
In ApproveSwapAndPay.sol, when using  _v3SwapExactInput() to perform a token swap using Uniswap V3 with exact input, deadline parameter should be added to avoid transactions waiting for an extended period in the mempool before execution.

Code Snippet

https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/ApproveSwapAndPay.sol#L204

Tool used

Manual Review

Recommendation

Recommend the protocol add deadline check

Soltho - Confusing function

Soltho

medium

Confusing function

Summary

This confusing function is more error-prone and can make the owner set the values wrong

Vulnerability Detail

The updateSettings function in OwnerSettings has a very confusing parameters. It uses array of 3 uint256 values that sometimes requires the first value to be a casted address, and in other cases it only uses the first value of the array, even tough you need to provide a array of 3.

function updateSettings(ITEM _item, uint256[] calldata values) external onlyOwner {
        if (_item == ITEM.LIQUIDATION_BONUS_FOR_TOKEN) {
            require(values.length == 3);
            if (values[1] > Constants.MAX_LIQUIDATION_BONUS) {
                revert InvalidSettingsValue(values[1]);
            }
            if (values[2] == 0) {
                revert InvalidSettingsValue(0);
            }
            liquidationBonusForToken[address(uint160(values[0]))] = Liquidation(
                values[1],
                values[2]
            );
        } else if (_item == ITEM.DAILY_RATE_OPERATOR) {
            require(values.length == 1);
            dailyRateOperator = address(uint160(values[0]));
        } else {
            if (_item == ITEM.PLATFORM_FEES_BP) {
                require(values.length == 1);
                if (values[0] > Constants.MAX_PLATFORM_FEE) {
                    revert InvalidSettingsValue(values[0]);
                }
                platformFeesBP = values[0];
            } else if (_item == ITEM.DEFAULT_LIQUIDATION_BONUS) {
                require(values.length == 1);
                if (values[0] > Constants.MAX_LIQUIDATION_BONUS) {
                    revert InvalidSettingsValue(values[0]);
                }
                dafaultLiquidationBonusBP = values[0];
            }
        }
    }

Impact

Medium, since it directly affects to the settings of the protocol.

Code Snippet

https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/OwnerSettings.sol#L65

Tool used

Manual Review

Recommendation

Use a setter function for each of the values: dailyRateOperator, platformFeesBP ,dafaultLiquidationBonusBP,liquidationBonusForToken, and get rid of the ITEM struct

0xDetermination - DoS of lenders and gas griefing by packing tokenIdToBorrowingKeys arrays

0xDetermination

high

DoS of lenders and gas griefing by packing tokenIdToBorrowingKeys arrays

Summary

In LiquidityBorrowingManager, tokenIdToBorrowingKeys arrays can be packed to gas grief and cause DoS of specific loans for an arbitrary period of time.

Vulnerability Detail

LiquidityBorrowingManager.borrow() calls the function _addKeysAndLoansInfo(), which adds user keys to the tokenIdToBorrowingKeys array of the borrowed-from LP position:

    function _addKeysAndLoansInfo(
        bool update,
        bytes32 borrowingKey,
        LoanInfo[] memory sourceLoans
    ) private {
        // Get the storage reference to the loans array for the borrowing key
        LoanInfo[] storage loans = loansInfo[borrowingKey];
        // Iterate through the sourceLoans array
        for (uint256 i; i < sourceLoans.length; ) {
            // Get the current loan from the sourceLoans array
            LoanInfo memory loan = sourceLoans[i];
            // Get the storage reference to the tokenIdLoansKeys array for the loan's token ID
            bytes32[] storage tokenIdLoansKeys = tokenIdToBorrowingKeys[loan.tokenId];
            // Conditionally add or push the borrowing key to the tokenIdLoansKeys array based on the 'update' flag
            update
                ? tokenIdLoansKeys.addKeyIfNotExists(borrowingKey)
                : tokenIdLoansKeys.push(borrowingKey);
    ...

A user key is calculated in the Keys library like so:

    function computeBorrowingKey(
        address borrower,
        address saleToken,
        address holdToken
    ) internal pure returns (bytes32) {
        return keccak256(abi.encodePacked(borrower, saleToken, holdToken));
    }

So every time a new user borrows some amount from a LP token, a new borrowKey is added to the tokenIdToBorrowingKeys[LP_Token_ID] array. The problem is that this array is iterated through by calling iterating methods (addKeyIfNotExists() or removeKey()) in the Keys library when updating a borrow (as seen in the first code block). Furthermore, emergency repays call removeKey() in _calculateEmergencyLoanClosure(), non-emergency repays call removeKey() in _removeKeysAndClearStorage(), and takeOverDebt() calls removeKey() in _removeKeysAndClearStorage(). The result is that all exit/repay/liquidation methods must iterate through the array. Both of the iterating methods in the Keys library access storage to compare array values to the key passed as argument, so every key in the array before the argument key will increase the gas cost of the transaction by (more than) a cold SLOAD, which costs 2100 gas (https://eips.ethereum.org/EIPS/eip-2929). Library methods below:

    function addKeyIfNotExists(bytes32[] storage self, bytes32 key) internal {
        uint256 length = self.length;
        for (uint256 i; i < length; ) {
            if (self.unsafeAccess(i).value == key) {
                return;
            }
            unchecked {
                ++i;
            }
        }
        self.push(key);
    }

    function removeKey(bytes32[] storage self, bytes32 key) internal {
        uint256 length = self.length;
        for (uint256 i; i < length; ) {
            if (self.unsafeAccess(i).value == key) {
                self.unsafeAccess(i).value = self.unsafeAccess(length - 1).value;
                self.pop();
                break;
            }
            unchecked {
                ++i;
            }
        }
    }

Let's give an example to see the potential impact and cost of the attack:

  1. An LP provider authorizes the contract to give loans from their large position. Let's say USDC/WETH pool.
  2. The attacker sees this and takes out minimum borrows of USDC using different addresses to pack the position's tokenIdToBorrowingKeys array. In Constants.sol, MINIMUM_BORROWED_AMOUNT = 100000 so the minimum borrow is $0.1 dollars since USDC has 6 decimal places. Add this to the estimated gas cost of the borrow transaction, let's say $3.9 dollars. The cost to add one key to the array is approx. $4. The max block gas limit on ethereum mainnet is 30,000,000, so divide that by 2000 gas, the approximate gas increase for one key added to the array. The result is 15,000, therefore the attacker can spend 60000 dollars to make any new borrows from the LP position unable to be repaid, transferred, or liquidated. Any new borrow will be stuck in the contract.
  3. The attacker now takes out a high leverage borrow on the LP position, for example $20,000 in collateral for a $1,000,000 borrow. The attacker's total expenditure is now $80,000, and the $1,000,000 from the LP is now locked in the contract for an arbitrary period of time.
  4. The attacker calls increaseCollateralBalance() on all of the spam positions. Default daily rate is .1% (max 1%), so over a year the attacker must pay 36.5% of each spam borrow amount to avoid liquidation and shortening of the array. If the gas cost of increasing collateral is $0.5 dollars, and the attacker spends another $0.5 dollars to increase collateral for each spam borrow, then the attacker can spend $1 on each spam borrow and keep them safe from liquidation for over 10 years for a cost of $15,000 dollars. The total attack expenditure is now $95,000. The protocol cannot easily increase the rate to hurt the attacker, because that would increase the rate for all users in the USDC/WETH market. Furthermore, the cost of the attack will not increase that much even if the daily rate is increased to the max of 1%. The attacker does not need to increase the collateral balance of the $1,000,000 borrow since repaying that borrow is DoSed.
  5. The result is that $1,000,000 of the loaner's liquidity is locked in the contract for over 10 years for an attack cost of $95,000.

Impact

Array packing causes users to spend more gas on loans of the affected LP token. User transactions may out-of-gas revert due to increased gas costs. An attacker can lock liquidity from LPs in the contract for arbitrary periods of time for asymmetric cost favoring the attacker. The LP will earn very little fees over the period of the DoS.

Code Snippet

https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L100-L101
https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L790-L826
https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/libraries/Keys.sol

Tool used

Manual Review

Recommendation

tokenIdToBorrowingKeys tracks borrowing keys and is used in view functions to return info (getLenderCreditsCount() and getLenderCreditsInfo()). This functionality is easier to implement with arrays, but it can be done with mappings to reduce gas costs and prevent gas griefing and DoS attacks. For example the protocol can emit the borrows for all LP tokens and keep track of them offchain, and pass borrow IDs in an array to a view function to look them up in the mapping. Alternatively, OpenZeppelin's EnumerableSet library could be used to replace the array and keep track of all the borrows on-chain.

newt - Not using deadline while swapping on UniswapV3

newt

medium

Not using deadline while swapping on UniswapV3

Summary

While making a swap on UniswapV3 the caller should use the slippage parameter amountOutMinimum and deadline parameter to avoid losing funds. Though the slippage parameter (amountOutMinimum) is there, the deadline param is missing.

Vulnerability Detail

deadline lets the caller specify a deadline parameter that enforces a time limit by which the transaction must be executed. Without a deadline parameter, the transaction may sit in the mempool and be executed at a much later time potentially resulting in a worse price for the user.

Impact

Inconvinience for the user and might be subjected to pay more.

Code Snippet

function _v3SwapExactInput(
    v3SwapExactInputParams memory params
) internal returns (uint256 amountOut) {
    // Determine if tokenIn has a 0th token
    bool zeroForTokenIn = params.tokenIn < params.tokenOut;
    // Compute the address of the Uniswap V3 pool based on tokenIn, tokenOut, and fee
    // Call the swap function on the Uniswap V3 pool contract
    (int256 amount0Delta, int256 amount1Delta) = IUniswapV3Pool(
        computePoolAddress(params.tokenIn, params.tokenOut, params.fee)
    ).swap(
            address(this), //recipient
            zeroForTokenIn,
            params.amountIn.toInt256(),
            (zeroForTokenIn ? MIN_SQRT_RATIO + 1 : MAX_SQRT_RATIO - 1),
            abi.encode(params.fee, params.tokenIn, params.tokenOut)
        );
    // Calculate the actual amount of output tokens received
    amountOut = uint256(-(zeroForTokenIn ? amount1Delta : amount0Delta));
    // Check if the received amount satisfies the minimum requirement
    if (amountOut < params.amountOutMinimum) {
        revert SwapSlippageCheckError(params.amountOutMinimum, amountOut);
    }
}

https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/ApproveSwapAndPay.sol#L204C1-L226C6

Tool used

Manual Review

Recommendation

Use parameter deadline correctly to avoid loss of funds.

feelereth - The emergency loan closure logic can lead to inconsistent state and abuse compared to normal repayment.

feelereth

high

The emergency loan closure logic can lead to inconsistent state and abuse compared to normal repayment.

Summary

• Emergency loan closure can lead to inconsistent state compared to normal repayment
• Only removing the lender's loans causes this
• Should fully close out position if all loans removed
• Properly closing out state avoids inconsistencies

Vulnerability Detail

The key functions are _calculateEmergencyLoanClosure and repay:

 function _calculateEmergencyLoanClosure(
   // args
 ) private returns (
   uint256 removedAmt, 
   uint256 feesAmt,
   bool completeRepayment
 ) {

   // Loop through loans
   for(uint i = 0; i < loans.length; i++) {

     // Only remove loans owned by msg.sender

     // Update removedAmt and feesAmt

   }

   // Return removedAmt, feesAmt, and completeRepayment
 }

 function repay(
   // args
 ) external {

   // Call _calculateEmergencyLoanClosure

   // If completeRepayment is true
     // Fully close out position (clear state)
   else 
     // Partially close out position

   // Transfer funds

 }

The issue is that _calculateEmergencyLoanClosure only partially closes out loans, but repay doesn't fully reset state if completeRepayment is true.

The emergency loan closure logic can lead to inconsistent state and abuse compared to normal repayment. Let's dive deep into how this works:
The key functions are _calculateEmergencyLoanClosure and repay.
In _calculateEmergencyLoanClosure, the contract loops through the loans for a borrowing position and removes any loans owned by the msg.sender (the lender). It calculates the removedAmt and feesAmt based on only the removed loans.
If all loans are removed, it sets completeRepayment = true.
The issues arise in repay function:
• If completeRepayment = true, it does NOT fully close out the position like in normal repayment. It only:
• Reduces borrowedAmount and feesOwed based on removedAmt
• Reduces totalBorrowed based on removedAmt
• Transfers removedAmt + feesAmt to lender
• So the position can still exist with borrowedAmount and feesOwed inconsistent with remaining loans
• And if completeRepayment = true, it indicates all loans removed but the position still exists
This can allow the borrower to continue using the position improperly.
For example:
• Borrower opens position with 2 loans, Loan A and Loan B
• Loan A owner (Lender A) uses emergency repayment
• This removes Loan A, and sets completeRepayment = true
• But position still exists with Loan B
• Borrower adds back more loans, increasing debt improperly

Impact

Potential for borrower abuse: If loans and collateral balances are not fully reset, a malicious borrower could try to reuse a partially closed position to take out more loans. The incomplete reset enables this potential abuse.

Code Snippet

https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L716
https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L532

Tool used

Manual Review

Recommendation

This can be mitigated by doing a full cleanup of the position's storage if completeRepayment is true.:

 function repay(
   // args
 ) external {

   // Call _calculateEmergencyLoanClosure

   if (completeRepayment) {
     // Full cleanup
     delete loansInfo[borrowingKey] 
     delete borrowingsInfo[borrowingKey]
     // Etc

   } else {
     // Partial cleanup
   }

   // Transfer funds

 }

This ensures the position storage is fully reset when all loans are removed.

seeques - takeOverDebt function assigns loans and adds keys to the previous borrowing key, not the new one

seeques

high

takeOverDebt function assigns loans and adds keys to the previous borrowing key, not the new one

Summary

A main goal of the LiquidityBorrowingManager.takeOverDebt() function is to transfer ownership of the debt to a function caller if the caller pays collateral for the undercollateralized position. The position's transfer happens at _addKeysAndLoansInfo() function. However, the argument for the function is not the newBorrowingKey, but the old one from which the position should be transferred from.

Vulnerability Detail

takeOverDebt() function takes a borrowingKey as its input which is then used to remove the borrowing key from the storage and to delete all information associated with it in a corresponding call to the _removeKeysAndClearStorage() function. Then, an initialization of a borrowing happens at the _initOrUpdateBorrowing() function, which returns a newBorrowingKey to which the ownership of a position should be assigned. However, the _addKeysAndLoansInfo() function does not use it.

_removeKeysAndClearStorage(oldBorrowing.borrower, borrowingKey, oldLoans);
        // Initialize a new borrowing using the same saleToken, holdToken
        (
            uint256 feesDebt,
            bytes32 newBorrowingKey,
            BorrowingInfo storage newBorrowing
        ) = _initOrUpdateBorrowing(
                oldBorrowing.saleToken,
                oldBorrowing.holdToken,
                accLoanRatePerSeconds
            );
        // Add the new borrowing key and old loans to the newBorrowing
        _addKeysAndLoansInfo(newBorrowing.borrowedAmount > 0, borrowingKey, oldLoans); //@audit

Since borrowing keys are hashes of msg.sender's address and tokens, the ownership stays with the previous owner. This means that the caller of the function loses his collateral by repaying the debt for the loan which should be transferred to him but which instead stays with the previous owner.

Impact

Loss of funds.

Code Snippet

https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L429-L441
https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L921

Tool used

Manual Review

Recommendation

Pass the newBorrowingKey as an argument to _addKeysAndLoansInfo()

_addKeysAndLoansInfo(newBorrowing.borrowedAmount > 0, newBorrowingKey, oldLoans);

Duplicate of #53

mstpr-brainbot - The internal _upRestoreLiquidityCache function is a view function and does not return the supposed struct variable "cache."

mstpr-brainbot

high

The internal _upRestoreLiquidityCache function is a view function and does not return the supposed struct variable "cache."

Summary

The internal _upRestoreLiquidityCache function does not return anything, and it is a view function. To ensure that the function _restoreLiquidity works as intended, the internal _upRestoreLiquidityCache function must return the RestoreLiquidityCache memory cache variable. This is necessary for the parent function to function correctly. Currently, these values default to Solidity's default type values, which is definitely not the intended behavior for this function.

Vulnerability Detail

Inside the _upRestoreLiquidityCache function, there is an internal function call as follows:
Link to Code

Within this function, several variables are assigned values to populate the struct "cache," as demonstrated here:
Link to Code

However, since the "cache" struct is never returned to the parent function, all the variables that begin with "cache.variableName" default to Solidity's default type values.
Link to Code

As these values are not correctly returned, the function does not operate as intended. Consequently, all calculations within the parent function are inaccurate.

Impact

This is definitely a major bug and needs to be corrected!

Code Snippet

https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L223-L321

https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L475-L513

Tool used

Manual Review

Recommendation

Inside the _upRestoreLiquidityCache return the "cache" variable

Milad-Sha - Unchecked return value for low level call in _tryApprove() function

Milad-Sha

medium

Unchecked return value for low level call in _tryApprove() function

Summary

Low-level calls will never throw an exception, instead they will return false if they encounter an exception, whereas contract calls will automatically throw.

Vulnerability Detail

If the return value of a low-level message call is not checked then the execution will resume even if the called contract throws an exception. If the call fails accidentally or an attacker forces the call to fail, then this may cause unexpected behavior in the subsequent program logic.

In the case that you use low-level calls, be sure to check the return value to handle possible failed calls.

Impact

Unchecked returns can cause unexpected behavior, As a result, a certain amount of tokens is not approved

Code Snippet

https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/ApproveSwapAndPay.sol?plain=1#L76-L79

        (bool success, bytes memory data) = token.call(
            abi.encodeWithSelector(IERC20.approve.selector, spender, amount)
        );
        return success && (data.length == 0 || abi.decode(data, (bool)));

Tool used

Manual Review

Recommendation

Ensure that the return value of a low-level call is checked or logged.

(bool success, bytes memory data) = token.call(
       abi.encodeWithSelector(IERC20.approve.selector, spender, amount)
);

if (success){
        return success && (data.length == 0 || abi.decode(data, (bool)));
}

or

(bool success, bytes memory data) = token.call(
       abi.encodeWithSelector(IERC20.approve.selector, spender, amount)
);
require(success, "...");
return success && (data.length == 0 || abi.decode(data, (bool)));

peanuts - Low decimal tokens such as EURS will not work as dailyRateCollateral will be overinflated

peanuts

medium

Low decimal tokens such as EURS will not work as dailyRateCollateral will be overinflated

Summary

Some tokens have an extremely low amount of decimals, like EURS which has 2 decimals. When calculating dailyRateCollateral, low decimal tokens will result in overinflated payments.

Vulnerability Detail

dailyRateCollateral is calculated by first calling _updateTokenRateInfo and setting the currentDailyRate. If there is no set amount, the currentDailyRate will be set to 10, which is the DEFAULT_DAILY_RATE. Once currentDailyRate is set, the borrowedAmount is calculated and this calculation will be executed. If the calculated dailyRateCollateral is less than 1000, it will be set to 1000.

        //@audit DailyRateCollateral calculation
        cache.dailyRateCollateral = FullMath.mulDivRoundingUp(
            cache.borrowedAmount,
            cache.dailyRateCollateral,
            Constants.BP
        );
        // Check if the dailyRateCollateral is less than the minimum amount defined in the Constants contract
        if (cache.dailyRateCollateral < Constants.MINIMUM_AMOUNT) {
            cache.dailyRateCollateral = Constants.MINIMUM_AMOUNT;
        }

dailyRateCollateral means the amount of collateral the user has to pay daily. For example, if the user borrows 1000 USDT, his daily rate collateral will be 1000e18 * 10 / 10000 = 1e18 , which is about 1 USDT. This means that the user has to pay 1 USDT per day. Since 1e18 is greater than 1000, dailyRateCollateral will be 1e18.

However, if the borrowed token has extremely low decimals, for example EURS, and the user borrows 1000 EURS, his daily rate collateral will be 1000e2 * 10 / 10000 = 100. Since 100 < 1000, dailyRateCollateral will be set to 1000. This means that if the user borrows 1000 EURS, he has to pay 1000 EURS, instead of 0.1% of that amount.

Since the protocol uses UniswapV3 positions, it is expected to accommodate to all types of ERC20 tokens.

Impact

Low token decimals will not work in this protocol, and if not careful, borrowers will overpay when calling borrow().

Code Snippet

https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L858-L866

Tool used

Manual Review

Recommendation

Recommend not adding the minimum sum. Also, recommend having a minimum amount to borrow and also probably whitelisting the types of tokens accepted to prevent low decimal issues all round.

Duplicate of #181

kutugu - Some chains do not support the solidity shanghai fork version

kutugu

medium

Some chains do not support the solidity shanghai fork version

Summary

Solidity >= 0.8.20 version introduces push0 instruction, which is still not supported by many chains, like Arbitrum and might be problematic for projects compiled.

Vulnerability Detail

pragma solidity 0.8.21;

Real Wagmi is compiled using the latest solidity version, which may cause problems on the L2 chain.
This could also become a problem if different versions of Solidity are used to compile contracts for different chains. The differences in bytecode between versions can impact the deterministic nature of contract addresses, potentially breaking counterfactuality.

Impact

  1. The contract may not compile/run properly
  2. Compiling with different versions of solidity may result in different contract addresses.

Code Snippet

Tool used

Manual Review

Recommendation

Change the Solidity compiler version to 0.8.19

Duplicate of #84

feelereth - Rounding down the collected amounts to uint128 in _decreaseLiquidity can potentially cause issues with lost precision when restoring liquidity in _increaseLiquidity.

feelereth

medium

Rounding down the collected amounts to uint128 in _decreaseLiquidity can potentially cause issues with lost precision when restoring liquidity in _increaseLiquidity.

Summary

Rounding down the collected amounts to uint128 in _decreaseLiquidity can potentially cause issues with lost precision when restoring liquidity in _increaseLiquidity.

Vulnerability Detail

Rounding down the collected amounts to uint128 in _decreaseLiquidity can potentially cause issues with lost precision when restoring liquidity in _increaseLiquidity. Here is a more detailed explanation:
The collect function in _decreaseLiquidity rounds the collected amounts to uint128:

  (amount0, amount1) = underlyingPositionManager.collect(
    INonfungiblePositionManager.CollectParams({
      tokenId: tokenId,  
      recipient: address(this),
      amount0Max: uint128(amount0), 
      amount1Max: uint128(amount1)
    })
  );

Later in _increaseLiquidity, these uint128 amounts are passed directly to increaseLiquidity:

  (uint128 restoredLiquidity, , ) = underlyingPositionManager.increaseLiquidity(
    INonfungiblePositionManager.IncreaseLiquidityParams({
      tokenId: loan.tokenId,
      amount0Desired: amount0, 
      amount1Desired: amount1,
      ...
     })
  );

The issue is that increaseLiquidity expects the full uint256 amounts, so passing the rounded uint128 values can lead to lost precision.
For example, let's say the actual collected amounts are:
• amount0 = 10000000000000001
• amount1 = 20000000000000001
When rounded to uint128, these become:
• amount0 = 10000000000000001
• amount1 = 20000000000000000
Now the lost precision in amount1 results in less liquidity being restored than expected.

Impact

Some amount of tokens may be left unaccounted for when increasing liquidity back in _increaseLiquidity().

Code Snippet

https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L368-L374
https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L398-L402

Tool used

Manual Review

Recommendation

The collect amounts should be kept as uint256 and only rounded when passing to increaseLiquidity:

  // Collect amounts as uint256
  (uint256 amount0, uint256 amount1) = underlyingPositionManager.collect(...);

  // Only round down when passing to increaseLiquidity
  (uint128 restoredLiquidity, , ) = underlyingPositionManager.increaseLiquidity(
    INonfungiblePositionManager.IncreaseLiquidityParams({
      amount0Desired: uint128(amount0), 
      amount1Desired: uint128(amount1), 
      ...
    })
  );

This ensures no precision is lost before increasing liquidity. The uint128 conversion is still needed for increaseLiquidity but done as late as possible.

kaysoft - Slippage loss because `amount0Min` and `amount1Min` are set to zero

kaysoft

high

Slippage loss because amount0Min and amount1Min are set to zero

Summary

Setting amount0Min and amount1Min for increasing and adding liquidity can result in loss of funds due to sandwich attack.

Vulnerability Detail

amount0Min and amount1Min are set to zero in the links below which indicates that the user is okay with 100% slippage loss. The only check done afterwards is to check that amount received are not zero which can be bypassed by allowing user receive just tiny amount of asset.

 INonfungiblePositionManager.DecreaseLiquidityParams({
                tokenId: tokenId,
                liquidity: liquidity,
                amount0Min: 0, //@audit sliipage loss. 
                amount1Min: 0,
                deadline: block.timestamp//@audit deadline.
            })
 INonfungiblePositionManager.IncreaseLiquidityParams({
                tokenId: loan.tokenId,
                amount0Desired: amount0,
                amount1Desired: amount1,
                amount0Min: 0,
                amount1Min: 0,//@audit slippage loss
                deadline: block.timestamp
            })

Impact

Loss of fund due to slippage

Code Snippet

 INonfungiblePositionManager.IncreaseLiquidityParams({
                tokenId: loan.tokenId,
                amount0Desired: amount0,
                amount1Desired: amount1,
                amount0Min: 0,
                amount1Min: 0,//@audit slippage loss
                deadline: block.timestamp
            })

Tool used

Manual Review

Recommendation

Allow amount0Min and amount1Min to be passed as input parameter offchain.

p-tsanev - LiquidityBorrowingManager#_addKeysAndLoansInfo() - user can have more than the allowed positions

p-tsanev

medium

LiquidityBorrowingManager#_addKeysAndLoansInfo() - user can have more than the allowed positions

Summary

The _addKeysAndLoansInfo is used to add loans and in case of new positions, push them to an array of positions owner by a specific user. There is a constant Constants.MAX_NUM_USER_POSOTION that limits the max positions to 10, but the check for it is incorrect, thus leading to an off-by-one error.

Vulnerability Detail

Upon calling the _addKeysAndLoansInfo function, requesting to add loans to a newly open positions, when adding the loans they are first pushed into the loans array and then the array's length is checked against the constant for the invariant.
However, when opening a new positions the reverse is done: first the invariant is checked that the positions array's length does not exceed the limit and then the new item is pushed. Thus if we are at the limit of 10 and try to open a new positions, instead of reverting we would create an 11th one since the pushing happens after the if check.

Impact

Broken protocol constant, above intended positions per user.

Code Snippet

https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L790-L826

Tool used

Manual Review

Recommendation

Swap the pushing operation and the if check's orders like so:

            bytes32[] storage allUserBorrowingKeys = userBorrowingKeys[msg.sender];
            allUserBorrowingKeys.push(borrowingKey);
           
            (allUserBorrowingKeys.length > Constants.MAX_NUM_USER_POSOTION).revertError(
                ErrLib.ErrorCode.TOO_MANY_USER_POSITIONS
            );

kevinkien - Inadequate Validation of Token Balance Data in ``getBalances`` Function

kevinkien

medium

Inadequate Validation of Token Balance Data in getBalances Function

Summary

The getBalances function in the provided smart contract lacks proper validation of the token balance data returned by the staticcall. This can potentially lead to security vulnerabilities and inaccurate token balance reporting.

Vulnerability Detail

The getBalances function iterates through an array of token addresses and uses staticcall to invoke the balanceOf function on each token contract. However, the code only checks the success of the staticcall and the length of the data returned. It does not verify the actual content of the data, making it susceptible to malicious or misbehaving token contracts.

Impact

The inadequate validation of token balance data can have the following security and usability impacts:

  • Security Risks: Malicious or misconfigured token contracts can return incorrect data, leading to security vulnerabilities or manipulation of reported token balances.
  • Inaccurate Reporting: Users relying on the getBalances function may receive incorrect balance information, affecting the integrity of their transactions and decisions.

Code Snippet

https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/Vault.sol#L37

function getBalances(
        address[] calldata tokens
    ) external view returns (uint256[] memory balances) {
        bytes memory callData = abi.encodeWithSelector(IERC20.balanceOf.selector, address(this));
        uint256 length = tokens.length;
        balances = new uint256[](length);
        for (uint256 i; i < length; ) {
            (bool success, bytes memory data) = tokens[i].staticcall(callData);
            require(success && data.length >= 32);
            balances[i] = abi.decode(data, (uint256));
            unchecked {
                ++i;
            }
        }
    }

Tool used

Manual Review

Recommendation

It is recommended to enhance the security and accuracy of the getBalances function by performing thorough data validation. This can be achieved by implementing the following changes to the code:

  • Check that the staticcall is successful.
  • Verify the length of the data returned to ensure it matches the expected length for an uint256.
  • Validate the content of the data to ensure it represents a non-negative token balance.
function getBalances(
    address[] calldata tokens
) external view returns (uint256[] memory balances) {
    bytes memory callData = abi.encodeWithSelector(IERC20.balanceOf.selector, address(this));
    uint256 length = tokens.length;
    balances = new uint256[](length);
    for (uint256 i = 0; i < length; i++) {
        (bool success, bytes memory data) = tokens[i].staticcall(callData);
        require(success && data.length == 32);
+      uint256 tokenBalance = abi.decode(data, (uint256));
-       balances[i] = abi.decode(data, (uint256));
+      require(tokenBalance >= 0, "Negative token balance");
+      balances[i] = tokenBalance;
        unchecked {
            ++i;
         }
    }
}

0xMAKEOUTHILL - Whenever a user wants to `takeOverDebt` will never work

0xMAKEOUTHILL

high

Whenever a user wants to takeOverDebt will never work

Summary

In LiquidityBorrowingManager.sol a user can takeOverDebt for a specific borrower by providing the borrower's borrowingKey:

function takeOverDebt(bytes32 borrowingKey, uint256 collateralAmt)

Vulnerability Detail

In order for a user to successfully take over a borrower's debt, he has to provide :

(collateralAmt <= minPayment).revertError(
                ErrLib.ErrorCode.COLLATERAL_AMOUNT_IS_NOT_ENOUGH
            );

collateralAmt is the amount of collateral to be provided by the new borrower.
minPayment is the minimum payment required based on the collateral balance for the old borrower.

Then loans and keys are removed from the old borrower

_removeKeysAndClearStorage(oldBorrowing.borrower, borrowingKey, oldLoans);

After that the

(uint256 feesDebt, bytes32 newBorrowingKey, BorrowingInfo storage newBorrowing) = _initOrUpdateBorrowing(
               oldBorrowing.saleToken,
               oldBorrowing.holdToken,
               accLoanRatePerSeconds
           );

is called which returns the msg.sender's (new borrower) bytes32 newBorrowingKey then:

// Add the new borrowing key and old loans to the newBorrowing
_addKeysAndLoansInfo(newBorrowing.borrowedAmount > 0, borrowingKey, oldLoans);

The problem is that the old loans and the initialization of the borrower are added again to the OLD borrower because borrowingKey is used in _addKeysAndLoansInfo rather than the new borrower's newBorrowingKey.

Impact

User can't take over another borrower's debt.

Code Snippet

https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L395-L453

https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L915-L956

Tool used

Manual Review

Recommendation

-- _addKeysAndLoansInfo(newBorrowing.borrowedAmount > 0, borrowingKey, oldLoans);
++ _addKeysAndLoansInfo(newBorrowing.borrowedAmount > 0, newBorrowingKey, oldLoans);

Duplicate of #53

Japy69 - Anyone can block any borrowing

Japy69

high

Anyone can block any borrowing

Summary

The LiquidityBorrowingManager.sol smart contract in the Real Wagmi project has a vulnerability that can lead to a situation where no one can borrow a specific ERC20 token. It works with any ERC20 token.

Vulnerability Detail

In the LiquidityBorrowingManager.sol smart contract, specifically in the borrow function, there is a check on borrowingCollateral . The relevant code snippet is as follows:

uint256 borrowingCollateral = cache.borrowedAmount - cache.holdTokenBalance;
(borrowingCollateral > params.maxCollateral).revertError(ErrLib.ErrorCode.TOO_BIG_COLLATERAL);

The borrowingCollateral variable is calculated as the difference between the borrowed amount and cache.holdTokenBalance. cache.holdTokenBalance is equivalent to the balance of the params.holdToken token held by the contract. In a normal world, this is equivalent to the tokens the contract just received by the position used here. Since anyone can send ERC20 tokens to this contract, an attacker can manipulate cache.holdTokenBalance by sending an amount of the params.holdToken token to the contract directly. This manipulation can result in cache.holdTokenBalance being larger than cache.borrowedAmount, causing the transaction and all the next one to revert.

To PoC this vulnerability, in the test file WagmiLeverageTests.ts, we just need to modify this line

[owner.address, alice.address, bob.address, aggregatorMock.address],

by

[owner.address, alice.address, bob.address, aggregatorMock.address, borrowingManager.address],

By this modification, we also send tokens to the smart contract. Then all the tests (run npx hardhat test) when someone borrows (and logically then other actions after borrowing) fail.

Impact

The impact of this vulnerability is significant. An attacker can effectively prevent anyone from borrowing the specific params.holdToken token. Moreover, the cost is not high since borrowingCollateral is normally not very high (in some cases it is equal to 1).

Code Snippet

The vulnerability comes from this line: https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L869-L872

Tool used

Manual Review

Recommendation

  1. Call the balanceOf function of the token at the beginning of the borrow function. When computing cache.holdTokenBalance, compare it to the previous balance.
  2. Add a function allowing to withdraw ERC20 tokens of this contract (and not in the vault!). Because the balance should be empty, in case of receipt, someone should be able to withdraw it.

Duplicate of #86

0xDetermination - Price changes after borrowing or slippage during borrowing can cause non-emergency repay() to revert

0xDetermination

medium

Price changes after borrowing or slippage during borrowing can cause non-emergency repay() to revert

Summary

Price changes after borrowing or slippage during borrowing can cause non-emergency repay() to revert. Lenders are forced to use emergency liquidation; borrowers have to use a non-obvious workaround to fix the issue since they cannot do emergency repays, and they will have to pay more fees until they figure out how to get their collateral un-stuck.

Vulnerability Detail

The Uniswap liquidity borrowed is stored in the LoanInfo struct to restore the loaner's liquidity when repay() is called to close/liquidate a position. When non-emergency repay() is called, an error will be thrown if the restored liquidity is less than the liquidity stored in the LoanInfo. See below:

//BELOW CODE IS FROM THE _increaseLiquidity() FUNCTION, WHICH IS CALLED BY repay() DURING NON EMERGENCY CALLS
        // Check if the restored liquidity is less than the loan liquidity amount
        // If true, revert with InvalidRestoredLiquidity exception
        if (restoredLiquidity < loan.liquidity) {
            ...
            revert InvalidRestoredLiquidity(

This protects the loaner, but the problem is that the original liquidity amount is set by the borrower and is equal to the liquidity reduced from the loaner's position, which does not account for slippage or price changes.

    /**
     * @dev Decreases the liquidity of a position by removing tokens. CALLED BY _extractLiquidity()
     * @param tokenId The ID of the position token.
     * @param liquidity The amount of liquidity to be removed. THIS VALUE IS SET BY THE BORROWER
     */
    function _decreaseLiquidity(uint256 tokenId, uint128 liquidity) private {
        // Call the decreaseLiquidity function of underlyingPositionManager contract
        // with DecreaseLiquidityParams struct as argument
        (uint256 amount0, uint256 amount1) = underlyingPositionManager.decreaseLiquidity(
            INonfungiblePositionManager.DecreaseLiquidityParams({
                tokenId: tokenId,
                liquidity: liquidity,
                amount0Min: 0,
                amount1Min: 0,
                deadline: block.timestamp
            })
        );

So if the liquidity restored during repayment is smaller due to price changes or borrow slippage, repay() will revert. Example:

  1. Loaner's position is 10 saleTokens to 10 holdTokens. The price ratio of the tokens is 1:1.
  2. Borrower calls borrow() to open a position, taking all the loaner's liquidity. Uniswap liquidity is calculated as $L=\sqrt{x*y}$, so the liquidity here is $\sqrt{100}$.
  3. All the saleTokens are swapped to holdTokens, so the balance of the loan is now 20 holdTokens.
  4. The price ratio of the pool changes over time to 1 saleToken to 2 holdTokens. $\sqrt{100}$ liquidity is now approx. equal to a LP position of 7 saleTokens and 14 holdTokens.
  5. The borrower calls repay(), and 14 holdTokens are swapped for 7 saleTokens to prepare for restoring liquidity (liquidity should be provided at the current price/ratio). 6 holdTokens are left over.
  6. 7 saleTokens and only 6 holdTokens are restaked, so the liquidity restored is far below $\sqrt{100}$. InvalidRestoredLiquidity error is thrown.

Impact

The borrower's collateral balance is used in the restoring liquidity swap, so the borrower could call increaseCollateralBalance() to enable repay() to work. However, there is a check in this function that prevents anyone who's not the borrower from increasing the borrower's collateral balance: (borrowing.borrowedAmount == 0 || borrowing.borrower != address(msg.sender)).revertError( ErrLib.ErrorCode.INVALID_BORROWING_KEY );.

So borrowers will have to call increaseCollateralBalance() (borrowers cannot do emergency liquidation) to get repay() to succeed. Loaners will be forced to use emergency liquidation for repay() to succeed, since they can't increase the borrower's collateral balance (emergency liquidation doesn't do swapping). The protocol's functionality is not working properly, and users will also lose gas from the reverted calls to repay().

This is particularly an issue for borrowers, because the solution to this issue is not obvious, and there is no functionality in the protocol that makes clear how much the borrower needs to increase their collateral to successfully call repay(). Borrowers will have their collateral stuck in the contract until they figure out the workaround, and during that time their loan will be collecting extra fees, so they will have to pay extra fees.

Code Snippet

https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L408-L425
https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L344-L360
https://uniswapv3book.com/docs/introduction/uniswap-v3/#the-mathematics-of-uniswap-v3
https://docs.uniswap.org/contracts/v2/guides/smart-contract-integration/providing-liquidity

Tool used

Manual Review

Recommendation

Add a transferFrom into repay() so that borrowers don't need to separately call increaseCollateralBalance() for repay() to succeed. If their collateral funds are not sufficient to restore liquidity, throw an error notifying them that they need to approve the contract for X amount of holdToken and have that amount in their balance.

shogoki - Protocol is not usable & possible lock of funds on zksync because of wrong address computation

shogoki

medium

Protocol is not usable & possible lock of funds on zksync because of wrong address computation

Summary

The Wagmi Leverage Protocol is supposed to be deployed on many chains, including the zksync Era chain.
On multiple occurences the address of the UniswapV3 pool to use is computed by using the deterministic way the create2 opcode for EVM works, which is different for zkSync ERA.

This results in the protocol not being correctly usable and the possibility for funds to be locked.

Vulnerability Detail

The contracts interact with UniswapV3 pools several times. The address of the pool is always computed by the contract using the UNDERLYING_V3_POOL_INIT_CODE_HASH the UNDERLYING_V3_FACTORY_ADDRESS and the salt which is the hash of the tokens and the fee.

This works, as the UniswapV3 pools are created by the Factory using the create2 opcode, which gives us an deterministic address based on these parameters. (see here

However, the address derivation works different on the zkSync Era chain, as they are stating in their docs.

export function create2Address(sender: Address, bytecodeHash: BytesLike, salt: BytesLike, input: BytesLike) {
  const prefix = ethers.utils.keccak256(ethers.utils.toUtf8Bytes("zksyncCreate2"));
  const inputHash = ethers.utils.keccak256(input);
  const addressBytes = ethers.utils.keccak256(ethers.utils.concat([prefix, ethers.utils.zeroPad(sender, 32), salt, bytecodeHash, inputHash])).slice(26);
  return ethers.utils.getAddress(addressBytes);
}

This will result in the computed address inside the contract to be wrong, which make any calls to these going to be reverted, or giving unexpected results.

As there is a possibility for the borrow function to go work fine, as the computed address will not be used, there is a potential for getting funds into the Vault. However, these would be locked forever, as every possible code path of the repay function is relying on the computed address of the UniswapV3 pool.

Impact

  • Protocol is not usable on zkSync Era network
  • Funds could be locked.

Code Snippet

https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/ApproveSwapAndPay.sol#L211-L213C16

https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/ApproveSwapAndPay.sol#L271C4-L291C6

Tool used

Manual Review

Recommendation

As zkEVM differs in some points from the EVM, i would consider writing a slightly adjusted version of the contract for zkEVM, in regards to the differences mentioned in their docs

Duplicate of #104

Milad-Sha - Uniswap callback is incorrectly protected

Milad-Sha

high

Uniswap callback is incorrectly protected

Summary

callback functions in Uniswap V3 are not properly protected and cause this function to always revert.

Vulnerability Detail

Uniswap callback is incorrectly protected

Considering that callback functions in Uniswap v2 or v3 are dangerous and must be properly protected, if this is not done, a malicious person can do things such as stealing money depending on the conditions.

Each callback must be validated to verify that the call originated from a genuine V3 pool. Otherwise, the pool contract would be vulnerable to attack via an EOA manipulating the callback function.

In the ApproveSwapAndPay.sol contract, authorization is done incorrectly for the uniswapV3SwapCallback function at all, and this is very dangerous.

A malicious person creates malicious contract and passes arbitrary data which calls the malicious contract.

The malicious Pool calls back the ApproveSwapAndPay.uniswapV3SwapCallback function by swapping and considering that authentication is done incorrectly here, it can easily steal users' assets and funds.

Unsafe downcast

When a type is downcast to a smaller type, the higher order bits are truncated, effectively applying a modulo to the original value. Without any other checks, this wrapping will lead to unexpected behavior and bugs

Solidity does not check if it is safe to cast an integer to a smaller one. Unless some business logic ensures that the downcasting is safe, a library like SafeCast should be used.

According to these two introductions:

The computePoolAddress() function is implemented incorrectly and occurs in the Unsafe downcast bug, and eventually the calculations and the result of the function will be wrong.

As a result of this mistake, the check performed in the uniswapV3SwapCallback function to compare Pool Address and msg.sender will not be established and this function will probably always revert.

Impact

A miscalculation in the computePoolAddress() function causes Unsafe downcast and the output of this function is used as an authentication mechanism on the uniswapV3SwapCallback function, which leads to a wrong result and finally the uniswapV3SwapCallback function always reverts.

But in theory, it is possible to consider a case where the malicious person finds arguments that the output of the computePoolAddress function is equal to msg.sender by checking a lot. In this case a malicious person can write uniswapV3SwapCallback in such a way that a callback is made by swapping and implement the process of stealing tokens and assets of other users in this function.

Code Snippet

https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/ApproveSwapAndPay.sol?plain=1#L242-L258

    function uniswapV3SwapCallback(
        int256 amount0Delta,
        int256 amount1Delta,
        bytes calldata data
    ) external {
        (amount0Delta <= 0 && amount1Delta <= 0).revertError(ErrLib.ErrorCode.INVALID_SWAP); // swaps entirely within 0-liquidity regions are not supported

        (uint24 fee, address tokenIn, address tokenOut) = abi.decode(
            data,
            (uint24, address, address)
        );
        (computePoolAddress(tokenIn, tokenOut, fee) != msg.sender).revertError(
            ErrLib.ErrorCode.INVALID_CALLER
        );
        uint256 amountToPay = amount0Delta > 0 ? uint256(amount0Delta) : uint256(amount1Delta);
        _pay(tokenIn, address(this), msg.sender, amountToPay);
    }

https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/ApproveSwapAndPay.sol?plain=1#L278-L279

            uint160(
                uint256(

Tool used

Manual Review

Recommendation

To fix the problem, you can use the verifyCallback function

CallbackValidation.verifyCallback(factory, tokenIn, tokenOut, fee);

This line defers the validation of the LP address (which should equal msg.sender) to the CallbackValidation library. The function verifyCallback executes this code:

pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey));
require(msg.sender == address(pool));

Where PoolAddress is another library.

The computeAddress function executes this somewhat complex check:

    function computeAddress(address factory, PoolKey memory key) internal pure returns (address pool) {
        require(key.token0 < key.token1);
        pool = address(
            uint256(
                keccak256(
                    abi.encodePacked(
                        hex'ff',
                        factory,
                        keccak256(abi.encode(key.token0, key.token1, key.fee)),
                        POOL_INIT_CODE_HASH
                    )
                )
            )
        );
    }

Or you can use the SafeCast library to prevent Unsafe downcast.

Duplicate of #158

kevinkien - Vulnerability in Address Check within the ``transferToken`` Function

kevinkien

medium

Vulnerability in Address Check within the transferToken Function

Summary

In the transferToken function of the application, there is no check to verify whether the _to address is address 0 (0x0) before conducting a fund transfer, which can potentially pose a security issue.

Vulnerability Detail

Within the source code, the transferToken function allows transferring funds from the _token address to the _to address if the _amount is positive. However, the function does not check the _to address before executing the transfer. If _to is the address 0 (0x0), the transaction will proceed without any validation, resulting in funds being sent to an invalid address, with no possibility of recovery.

Impact

Risk of fund loss: This vulnerability can lead to funds being sent to an invalid address (0x0) with no possibility of recovery.

Code Snippet

https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/Vault.sol#L17-L21

function transferToken(address _token, address _to, uint256 _amount) external onlyOwner {
        if (_amount > 0) {
            IERC20(_token).safeTransfer(_to, _amount);
        }
    }

Tool used

Manual Review

Recommendation

To address this vulnerability, it is recommended to perform a check on the _to address before executing the fund transfer. Here's an improved code snippet:

function transferToken(address _token, address _to, uint256 _amount) external onlyOwner {
    require(_to != address(0), "Invalid recipient address");
    require(_amount > 0, "Amount must be greater than 0");
    IERC20(_token).safeTransfer(_to, _amount);
}

By adding two require statements, ensure that _to is not address 0 and that _amount is greater than 0 before executing the transfer.

feelereth - Borrowers can abuse the ability to repay loans while still owing fees.

feelereth

high

Borrowers can abuse the ability to repay loans while still owing fees.

Summary

The repay function in LiquidityBorrowingManager contract allows the borrower to repay their loan even if the collateralBalance is negative.

Vulnerability Detail

Borrowers are allowed to repay loans even when the collateralBalance is negative.
Here is how it works:
In the repay() function, there is a check that allows the borrower to proceed if either:

  1. msg.sender == borrowing.borrower OR
  2. collateralBalance >= 0

This means that even if the collateralBalance is negative, the borrower can still call repay() and proceed, since the first condition is satisfied (msg.sender will be the borrower).
The impact of this is that borrowers could potentially walk away without fully repaying the lender if the collateralBalance is negative. When collateralBalance is negative, it means the borrower owes more in fees/interest than what they originally deposited as collateral. By allowing them to repay even in this scenario, they can get away without paying those additional fees they owe.
The relevant code is here:

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

Impact

• Borrowers can walk away from their debt by repaying only a portion of what they owe. This leaves the lender at a loss.
• It incentivizes borrowers to take risky leveraged positions without needing to repay the full debt if the trade goes against them.

In essence - Borrowers can abuse the ability to repay loans while still owing fees.

Code Snippet

https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L559-L561

Tool used

Manual Review

Recommendation

This check should be updated to:

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

By changing the AND to an OR, it will prevent borrowers from repaying unless they either are the original borrower OR have a non-negative collateral balance. This mitigates the potential vulnerability by ensuring borrowers cannot walk away without paying fees they owe if collateralBalance is negative. . This will prevent borrowers from abusing the ability to repay loans while still owing fees.

IceBear - Using block.timestamp as deadline is still dangerous

IceBear

medium

Using block.timestamp as deadline is still dangerous

Summary

Using block.timestamp as deadline is still dangerous

Vulnerability Detail

shouldn't set the deadline to block.timestamp as a validator can hold the transaction and the block it is eventually put into will be block.timestamp, so this offers no protection.
similar findings:
https://code4rena.com/reports/2023-05-maia#m-20-some-functions-in-the-talos-contracts-do-not-allow-user-to-supply-slippage-and-deadline-which-may-cause-swap-revert

Impact

It may be more profitable for a miner to deny the transaction from being mined until the transaction incurs the maximum amount of slippage.
A malicious miner can hold the transaction as deadline is set to block.timestamp which means that whenever the miner decides to include the transaction in a block, it will be valid at that time, since block.timestamp will be the current timestamp. The transaction might be left hanging in the mempool and be executed way later than the user wanted. The malicious miner can hold the transaction and execute the transaction only when he is profitable and no error would also be thrown as it will be valid at that time, since block.timestamp will be the current timestamp.

Code Snippet

https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L405

Tool used

Manual Review

Recommendation

Add deadline argument

IvanFitro - Users do not pay the plataformFees

IvanFitro

medium

Users do not pay the plataformFees

Summary

_pickUpPlatformFees() is designed to calculate protocol fees that users are required to pay. However and error in the formula to calculate it provocates that never will be paid.

Vulnerability Detail

_pickUpPlatformFees() calulcates the fees protocol platformFees = (fees * platformFeesBP) / Constants.BP.

Next calulates currentFees = fees - platformFees.

However, a critical issue arises when calculating currentFees as it incorrectly subtracts the platformFees from the total user fees. This error results in the user being charged fewer fees than required, rather than more.

function _pickUpPlatformFees(
        address holdToken,
        uint256 fees
    ) private returns (uint256 currentFees) {
        uint256 platformFees = (fees * platformFeesBP) / Constants.BP;
        platformsFeesInfo[holdToken] += platformFees;
        currentFees = fees - platformFees;  
    }

Impact

Users pays less fees and never pays the plataform fees.

Code Snippet

https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L967-L974

Tool used

Manual Review

Recommendation

Change the formula for the following:
currentFees = fees + platformFees

feelereth - Vulnerability where minPayment could be 0 if the collateral balance is positive, allowing takeover without proper collateral.

feelereth

high

Vulnerability where minPayment could be 0 if the collateral balance is positive, allowing takeover without proper collateral.

Summary

The calculation of minPayment depends on the collateral balance being negative. If positive, minPayment could be 0 which would allow takeover without proper collateral.

Vulnerability Detail

There is a potential vulnerability in the takeOverDebt function where minPayment could be 0 if the collateral balance is positive, allowing takeover without proper collateral.

If the collateral balance is positive, minPayment could potentially be 0 which would allow a takeover of the debt without providing proper collateral.
The key parts of the code related to this are:

  1. Calculating the collateral balance:

     (int256 collateralBalance, uint256 currentFees) = _calculateCollateralBalance(
       oldBorrowing.borrowedAmount, 
       oldBorrowing.accLoanRatePerSeconds,
       oldBorrowing.dailyRateCollateralBalance,
       accLoanRatePerSeconds
     );
    
  2. Calculating minPayment based on the collateral balance:

      minPayment = (uint256(-collateralBalance) / Constants.COLLATERAL_BALANCE_PRECISION) + 1;
    
  3. Validating the provided collateralAmt against minPayment:

      (collateralAmt <= minPayment).revertError(
        ErrLib.ErrorCode.COLLATERAL_AMOUNT_IS_NOT_ENOUGH
      );
    

The vulnerability arises because if collateralBalance is positive, minPayment will be calculated as 0 + 1 = 1.
This means the collateral validation would pass as long as collateralAmt >= 1, allowing the takeover without proper collateral.

This means if the old borrower has a positive collateral balance, the new borrower can take over the debt by only providing 1 wei of collateral, which is insecure.

Attackers could take over loans that are well collateralized by providing minimal collateral. This could drain collateral from the protocol when the attackers default on the loans.

Impact

  1. It would allow someone to take over a debt without providing proper collateral, leaving the protocol undercollateralized.

  2. Undercollateralization of the lending protocol over time as debts are taken over without proper collateral being provided. This could lead to insolvency of the protocol if debts are not able to be repaid.

Code Snippet

https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L422
https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L423-L424
https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L410-L414

Tool used

Manual Review

Recommendation

The minPayment calculation should be changed to:

   minPayment = collateralBalance < 0 ? 
                 (uint256(-collateralBalance) / Constants.COLLATERAL_BALANCE_PRECISION) + 1 :
                 1;

This will ensure minPayment is at least 1 even if collateralBalance is positive. The takeover collateral validation will then properly enforce a minimum amount of collateral for the debt takeover.

HHK - Borrower cannot `repay()` if lender burns its NFT

HHK

high

Borrower cannot repay() if lender burns its NFT

Summary

Lenders approve their Uniswapv3 NFTs on the wagmi contract. When borrowers borrow() liquidity to long an asset, the NFT position's liquidity is decreased.

Later when the borrower wants to repay() its loan, the tokens he borrowed are added back to the liquidity position. But if the liquidity position doesn't exist anymore then the call will revert not allowing borrowers to close their positions.

Vulnerability Detail

In the repay() function of the LiquidityBorrowingManager contract, we try to recreate the liquidity position of the lenders.

This happens in the _restoreLiquidity() function, where we pass all the loans that belong to the borrowing position.

There for each loan the function queries the NonfungiblePositionManager contract with the tokenId to get all the infos of the position and then will try to increaseLiquidity().

But if one of the loan position was burned by the lender after being borrowed, the call to the NonfungiblePositionManager will revert here as the tokenId doesn't exist anymore.

Consider this POC that can be copied and pasted in the test files (replace all tests and just keep the setup & NFT creation):

it("LEFT_OUTRANGE_TOKEN_1 borrowing liquidity (long position WBTC zeroForSaleToken = false)  will be successful", async () => {
        //create the borrowing position
        const amountWBTC = ethers.utils.parseUnits("0.05", 8); //token0
        const deadline = (await time.latest()) + 60;
        const minLeverageDesired = 50;
        const maxCollateralWBTC = amountWBTC.div(minLeverageDesired);

        const loans = [
            {
                liquidity: nftpos[3].liquidity,
                tokenId: nftpos[3].tokenId,
            },
        ];

        const swapParams: ApproveSwapAndPay.SwapParamsStruct = {
            swapTarget: constants.AddressZero,
            swapAmountInDataIndex: 0,
            maxGasForCall: 0,
            swapData: swapData,
        };

        let params: LiquidityBorrowingManager.BorrowParamsStruct = {
            internalSwapPoolfee: 500,
            saleToken: WETH_ADDRESS,
            holdToken: WBTC_ADDRESS,
            minHoldTokenOut: amountWBTC.mul(2), //<=TooLittleReceivedError
            maxCollateral: maxCollateralWBTC,
            externalSwap: swapParams,
            loans: loans,
        };

        await expect(borrowingManager.connect(bob).borrow(params, deadline)).to.be.reverted;

        params = {
            internalSwapPoolfee: 500,
            saleToken: WETH_ADDRESS,
            holdToken: WBTC_ADDRESS,
            minHoldTokenOut: amountWBTC,
            maxCollateral: maxCollateralWBTC,
            externalSwap: swapParams,
            loans: loans,
        };

        await borrowingManager.connect(bob).borrow(params, deadline);

        //Alice burns her NFT
        nonfungiblePositionManager.connect(alice).burn(nftpos[3].tokenId);
    });

    it("repay borrowing and restore liquidity (long position WBTC zeroForSaleToken = false) will be unsuccessful because NFT burned", async () => {
        const borrowingKey = await borrowingManager.userBorrowingKeys(bob.address, 0);
        const deadline = (await time.latest()) + 60;
        const swapParams: ApproveSwapAndPay.SwapParamsStruct = {
            swapTarget: constants.AddressZero,
            swapAmountInDataIndex: 0,
            maxGasForCall: 0,
            swapData: swapData,
        };
        let params = {
            isEmergency: false,
            internalSwapPoolfee: 500,
            externalSwap: swapParams,
            borrowingKey: borrowingKey,
            swapSlippageBP1000: 990, //1%
        };

        //BOB cannot repay his loan and loose his liquidation bonus and potential profits
        await expect(borrowingManager.connect(bob).repay(params, deadline)).to.be.revertedWith("Invalid token ID");
    });

Impact

High.

  • Borrower will not be able to close its position, loosing his liquidation bonus and potential profits.
  • Position cannot be liquidated by liquidators/bots.
  • Since borrower will stop paying fees, lenders that didn't go rogue will not be earning anymore until they monitor their position and find out that they need to do an emergency liquidity restoration.

Code Snippet

https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L627-L673

https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L223

Tool used

Manual Review

Recommendation

Consider adding a vault/router contract that will hold the NFT positions so lenders cannot burn their positions.

Duplicate of #78

newt - Modifier lacks appropriate symbole

newt

medium

Modifier lacks appropriate symbole

Summary

In LiquidityBorrowingManager.sol the modifier in this case is suppose to check if the current block timestamp is before or equal to the deadline according to the documentation. But the modifier only checks if the blocktimestamp is before and not equal to the deadline.

Vulnerability Detail

Every function that uses checkDeadline() modifier will only check if deadline is before not on the specific deadline date.

Impact

Borrowers at borrow() line465 can only borrow tokens before the deadline and not on the deadline date same goes for repaying the loans at repay() line532. This can cause a huge inconvenience for the users.

Code Snippet

modifier checkDeadline(uint256 deadline) {
    (_blockTimestamp() > deadline).revertError(ErrLib.ErrorCode.TOO_OLD_TRANSACTION);
    _;
}

https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L136C1-L139C6

Tool used

Manual Review

Recommendation

Add a equal symbol to the modifier like so,

modifier checkDeadline(uint256 deadline) {
    (_blockTimestamp() => deadline).revertError(ErrLib.ErrorCode.TOO_OLD_TRANSACTION);
    _;
}

nmirchev8 - No slippage protection on _decreaseLiquidity call

nmirchev8

medium

No slippage protection on _decreaseLiquidity call

Summary

Inside liquidityManager we call NonfungiblePositionManager v3 to decrease the liquidity of a position, but the problem is that we don't provide valid params for amount0Min amount1Min, which it the protection against slippages.

Vulnerability Detail

The only check is wether the returned values are greater than 0, which is not enough

 if (amount0 == 0 && amount1 == 0) {
     revert InvalidBorrowedLiquidity(tokenId);
}

You can see in the docs it is written that In production, amount0Min and amount1Min should be adjusted to create slippage protections.

Impact

This could result in potential lost of funds

Code Snippet

https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L356-L357

Tool used

Manual Review

Recommendation

Implement recommended params for slippage protection with valid values and check the returned values.

   (uint256 amount0, uint256 amount1) = underlyingPositionManager.decreaseLiquidity(
            INonfungiblePositionManager.DecreaseLiquidityParams({
                tokenId: tokenId,
                liquidity: liquidity,
                amount0Min: validValue,
                amount1Min: validValue,
                deadline: block.timestamp
            })
        );
        // Check if both amount0 and amount1 are zero after decreasing liquidity
        // If true, revert with InvalidBorrowedLiquidity exception
        if (amount0 <  amount0Min || amount1 < amount1Min) {
            revert InvalidBorrowedLiquidity(tokenId);
        }

ali_shehab - DOS blocking users from opening positions on loans

ali_shehab

high

DOS blocking users from opening positions on loans

Summary

It is possible to DOS the borrow function to always revert when users call it while passing a targeted holdToken, this is done simply by transferring a small amount of the targeted holdToken directly to the LiquidityBorrowingManager, which will mess up the _getPairBalance function and will return messed up values that violate the actual/expected balance.

Vulnerability Detail

The difference between cache.borrowedAmount and cache.holdTokenBalance in borrow function will always be small for example, if we print the values of cache.borrowedAmount and cache.holdTokenBalance you will notice it is too small.

image

So if a user send small amount of token to LiquidityBorrowingManager it will cause the cache.holdTokenBalance to be greater than cache.borrowedAmount then line
https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L492
will always revert when anyone try to call the borrow function.

POC:

it("poc", async () => {
    const amountWBTC = ethers.utils.parseUnits("0.05", 8); //token0
    const deadline = (await time.latest()) + 60;
    const minLeverageDesired = 50;
    const maxCollateralWBTC = amountWBTC.div(minLeverageDesired);

    const loans = [
        {
            liquidity: nftpos[3].liquidity,
            tokenId: nftpos[3].tokenId,
        },
    ];

    const swapParams: ApproveSwapAndPay.SwapParamsStruct = {
        swapTarget: constants.AddressZero,
        swapAmountInDataIndex: 0,
        maxGasForCall: 0,
        swapData: swapData,
    };

    const params: LiquidityBorrowingManager.BorrowParamsStruct = {
        internalSwapPoolfee: 500,
        saleToken: WETH_ADDRESS,
        holdToken: WBTC_ADDRESS,
        minHoldTokenOut: amountWBTC,
        maxCollateral: maxCollateralWBTC,
        externalSwap: swapParams,
        loans: loans,
    };

    await borrowingManager.connect(bob).borrow(params, deadline);

    await WBTC.connect(alice).transfer(borrowingManager.address, ethers.utils.parseUnits("1", 1));

    const params2: LiquidityBorrowingManager.BorrowParamsStruct = {
        ...params,
        loans: [
            {
                liquidity: nftpos[4].liquidity,
                tokenId: nftpos[4].tokenId,
            },
        ],
    };

    await expect(borrowingManager.connect(bob).borrow(params2, deadline)).to.be.reverted;
});

you will see that it will always revert.

Impact

DOS blocking users from opening positions on loans

Code Snippet

https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L492

uint256 borrowingCollateral = cache.borrowedAmount - cache.holdTokenBalance;

https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/ApproveSwapAndPay.sol#L113-L118

 function _getBalance(address token) internal view returns (uint256 balance) {
        bytes memory callData = abi.encodeWithSelector(IERC20.balanceOf.selector, address(this));
        (bool success, bytes memory data) = token.staticcall(callData);
        require(success && data.length >= 32);
        balance = abi.decode(data, (uint256));
    }

Tool used

Manual Review

Recommendation

use local variables to track balances instead of balance(this)?

Duplicate of #86

jonatascm - Solidity version not run in all chains

jonatascm

high

Solidity version not run in all chains

Summary

The Solidity version 0.8.21 includes the push0 instruction, which is not supported in some chains, such as [Arbitrum](https://docs.arbitrum.io/solidity-support).

Vulnerability Detail

In newer versions of Solidity, the push0 instruction is included. However, some chains do not support this instruction. The Real Wagmi should be deployed on multiple chains. However, some of them do not support the push0 instruction. This means that the contract will be deployed but will not be executed.

Impact

Code deployed in not supported chains will not execute.

Code Snippet

In all files the solidity version is set to 0.8.21

// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.21;

https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L2
https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/Vault.sol#L2

Tool used

Manual Review

Recommendation

Downgrade the version of solidity for deployment.

Duplicate of #84

peanuts - Max collateral check is not done when increasing collateral balance

peanuts

medium

Max collateral check is not done when increasing collateral balance

Summary

There is no max collateral check when increasing the collateral balance using increaseCollateralBalance()

Vulnerability Detail

When a user calls borrow(), there is a check for borrowingCollateral. The check makes sure that borrowingCollateral is not greater than maxCollateral

File: LiquidityBorrowingManager.sol
492:         uint256 borrowingCollateral = cache.borrowedAmount - cache.holdTokenBalance;
493:         (borrowingCollateral > params.maxCollateral).revertError(
494:             ErrLib.ErrorCode.TOO_BIG_COLLATERAL
495:         );

maxCollateral is defined as the maximum amount of collateral that can be provided for the loan.

File: LiquidityBorrowingManager.sol
35:         /// @notice The maximum amount of collateral that can be provided for the loan
36:         uint256 maxCollateral;

However, when increaseCollateralBalance is called, the maxCollateral variable is not checked.

File: LiquidityBorrowingManager.sol
371:     function increaseCollateralBalance(bytes32 borrowingKey, uint256 collateralAmt) external {
372:         BorrowingInfo storage borrowing = borrowingsInfo[borrowingKey];
373:         // Ensure that the borrowed position exists and the borrower is the message sender
374:         (borrowing.borrowedAmount == 0 || borrowing.borrower != address(msg.sender)).revertError(
375:             ErrLib.ErrorCode.INVALID_BORROWING_KEY
376:         );
377:         // Increase the daily rate collateral balance by the specified collateral amount
               //@audit -- No max collateral balance check
378:         borrowing.dailyRateCollateralBalance +=
379:             collateralAmt *
380:             Constants.COLLATERAL_BALANCE_PRECISION;
381:         _pay(borrowing.holdToken, msg.sender, VAULT_ADDRESS, collateralAmt);
382:         emit IncreaseCollateralBalance(msg.sender, borrowingKey, collateralAmt);
383:     }

Impact

Without checking maxCollateral, the leverage position may be unnecessarily overcollaterized.

Code Snippet

https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L371-L383

Tool used

Manual Review

Recommendation

Recommend checking the max collateral amount when calling increase collateral balance

feelereth - New borrowers can manipulate the accrued loan rate per second when taking over the debt

feelereth

high

New borrowers can manipulate the accrued loan rate per second when taking over the debt

Summary

The takeOverDebt function allows the new borrower to provide the accrued loan rate instead of persisting the rate from the old borrowing
• The accrued loan rate is passed in to _initOrUpdateBorrowing from the new borrower.
• It is not inherited from the old borrowing's rate.
• This allows the new borrower to provide a lower rate, reducing their interest fees.

Vulnerability Detail

The takeOverDebt function allows the new borrower to provide the accrued loan rate instead of persisting the rate from the old borrowing

The key points are:

  1. In takeOverDebt, the new borrower provides the accLoanRatePerSeconds
  2. This is passed to _initOrUpdateBorrowing and set as the rate for the new borrowing
  3. The old rate is not carried over or validated against the new rate
  4. This allows the new borrower to manipulate the accrued rate lower than the actual rate
  5. Reducing the accrued rate would make their collateral requirements lower

Here is the relevant code:

  function takeOverDebt(bytes32 borrowingKey, uint256 collateralAmt) external {

    // New borrower provides accLoanRatePerSeconds
    accLoanRatePerSeconds = holdTokenRateInfo.accLoanRatePerSeconds;

    // Rate passed to _initOrUpdateBorrowing
    _initOrUpdateBorrowing(
          oldBorrowing.saleToken,
          oldBorrowing.holdToken,
          accLoanRatePerSeconds
  );

  }

  function _initOrUpdateBorrowing(
    // Sets provided rate without validation
    borrowing.accLoanRatePerSeconds = accLoanRatePerSeconds;

This could allow the new borrower to take over debt cheaply by manipulating the accrued rate.

This means the new borrower can provide any value they want for accLoanRatePerSeconds.
The accLoanRatePerSeconds represents the accumulated loan interest rate per second for the debt. It is used to calculate the fees owed when closing the position.
By letting the new borrower manipulate this value on takeover, they can incorrectly calculate the fees owed to underpay when closing the position.

Impact

• It allows the new borrower to potentially commit fraud by underpaying the loan interest fees on closure.
• The lender will lose money, since the new borrower repays less than they owe due to the lower manipulated accrued loan rate.
• It compromises the integrity of the accrued loan rates in the system

Code Snippet

https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L408
https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L435-L439
https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L955

Tool used

Manual Review

Recommendation

It could be mitigated by persisting the rate from the previous borrowing:

  // Take over debt

  // Retrieve previous rate
  uint256 oldRate = oldBorrowing.accLoanRatePerSeconds;

  // Pass old rate to _initOrUpdateBorrowing
  _initOrUpdateBorrowing(
    oldBorrowing.saleToken, 
    oldBorrowing.holdToken,
    oldRate
  );

  // _initOrUpdateBorrowing

  // Set rate from previous borrowing 
  borrowing.accLoanRatePerSeconds = oldRate;

This would prevent the new borrower from manipulating the accrued loan rate.

peanuts - MAX_NUM_USER_POSOTION can be bypassed

peanuts

medium

MAX_NUM_USER_POSOTION can be bypassed

Summary

MAX_NUM_USER_POSOTION can be bypassed by 1 position.

Vulnerability Detail

In LiquidityBorrowingManager.sol, if the user calls borrow() or takeOverDebt() and if the borrowed amount is 0, then the borrow position is a new position. This position will be added to the allUserBorrowingKeys struct.

However, the check for maximum number of position is called first before pushing the array:

File: LiquidityBorrowingManager.sol
817:         if (!update) {
818:             // If it's a new position, ensure that the user does not have too many positions
819:             bytes32[] storage allUserBorrowingKeys = userBorrowingKeys[msg.sender];
820:             (allUserBorrowingKeys.length > Constants.MAX_NUM_USER_POSOTION).revertError(
821:                 ErrLib.ErrorCode.TOO_MANY_USER_POSITIONS
822:             );
823:             // Add the borrowingKey to the user's borrowing keys
824:             allUserBorrowingKeys.push(borrowingKey);
825:         }

If the max number of user position is 10, and there are 10 positions so far, the check will pass and a new, 11 position will be pushed into the mapping, which should not be the case.

Impact

Max user position will be overcounted by 1

Code Snippet

https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L817-L825

Tool used

Manual Review

Recommendation

Add the borrowingKey to the user's borrowing keys first before checking for the max number of position.

Soltho - ERC20 transfer return value not checked

Soltho

medium

ERC20 transfer return value not checked

Summary

ERC20 transfer function returns a boolean, should be checked. Found in transferToken of Vault

Vulnerability Detail

Some ERC20 tokens dont revert when transfer fails but they return false instead.

    /**
     * @notice Transfers tokens to a specified address
     * @param _token The address of the token to be transferred
     * @param _to The address to which the tokens will be transferred
     * @param _amount The amount of tokens to be transferred
     */
    function transferToken(address _token, address _to, uint256 _amount) external onlyOwner {
        if (_amount > 0) {
            IERC20(_token).safeTransfer(_to, _amount);
        }
    }

Impact

Low in this case, because it doesn't affect to the protocol, but it could be worse.

Code Snippet

https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/Vault.sol#L17

Tool used

Manual Review

Recommendation

Always check the return value of the transfers and require they are true

feelereth - The fees are always rounded up in _calculateCollateralBalance(), which benefits the protocol over the user

feelereth

medium

The fees are always rounded up in _calculateCollateralBalance(), which benefits the protocol over the user

Summary

Rounding fees up benefits the protocol at the cost of users paying slightly higher fees.

Vulnerability Detail

The code in _calculateCollateralBalance() does round up fees in a way that benefits the protocol over the user.
The key lines are:

 currentFees = FullMath.mulDivRoundingUp(
     borrowedAmount, 
     accLoanRatePerSeconds - borrowingAccLoanRatePerShare,
     Constants.BP
 );

This uses FullMath.mulDivRoundingUp() to calculate the fees. As the name suggests, this rounds the result up to the nearest integer.
Normally in division, remainders are rounded down. But here, rounding up ensures any remainder is added to the fees. This increases the fees collected by the protocol.
For example, if the actual fee calculation resulted in a value like 100.000001, rounding down would give fees of 100. But rounding up makes it 101.
Over many transactions, these small roundings add up to significant extra fees for the protocol. The user pays more fees than they should based on the exact calculation.

Impact

Users pay slightly higher fees, reducing their collateral balance.

Code Snippet

https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/DailyRateAndCollateral.sol#L110-L114

Tool used

Manual Review

Recommendation

This could be mitigated by using standard rounding instead of always rounding up:

 currentFees = FullMath.mulDiv(
     borrowedAmount, 
     accLoanRatePerSeconds - borrowingAccLoanRatePerShare,
     Constants.BP
 );

Now remainders are rounded to the nearest integer, rather than always up. This removes the bias and calculates fees fairly.

Bauer - Revert on Large Approvals & Transfers

Bauer

medium

Revert on Large Approvals & Transfers

Summary

Some tokens (e.g. UNI, COMP) revert if the value passed to approve or transfer is larger than uint96.

Vulnerability Detail

Some tokens (e.g. UNI, COMP) have special case logic in approve that sets allowance to type(uint96).max .

function approve(address spender, uint rawAmount) external returns (bool) {
        uint96 amount;
        if (rawAmount == uint(-1)) {
            amount = uint96(-1);
        } else {
            amount = safe96(rawAmount, "Uni::approve: amount exceeds 96 bits");
        }

        allowances[msg.sender][spender] = amount;

        emit Approval(msg.sender, spender, amount);
        return true;
    }

if the approval amount is type(uint256).max), which may cause issues with systems that expect the value passed to approve to be reflected in the allowances mapping.

  function _maxApproveIfNecessary(address token, address spender, uint256 amount) internal {
        if (IERC20(token).allowance(address(this), spender) < amount) {
            if (!_tryApprove(token, spender, type(uint256).max)) {
                if (!_tryApprove(token, spender, type(uint256).max - 1)) {
                    require(_tryApprove(token, spender, 0));
                    if (!_tryApprove(token, spender, type(uint256).max)) {
                        if (!_tryApprove(token, spender, type(uint256).max - 1)) {
                            true.revertError(ErrLib.ErrorCode.ERC20_APPROVE_DID_NOT_SUCCEED);
                        }
                    }
                }
            }
        }
    }

Impact

The function's multiple attempts will fail

Code Snippet

https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/ApproveSwapAndPay.sol#L91-L104

Tool used

Manual Review

Recommendation

It is advisable to dynamically adjust the approval amount.

p-tsanev - LiquidityManager.sol - usage of ``slot0`` is a bad practice

p-tsanev

medium

LiquidityManager.sol - usage of slot0 is a bad practice

Summary

The LiquidityManager abstract contract introduces possible operations with liquidity such as adding, removing, extracting and restoring liquidity. It uses a cache variable to store various elements such as the sqrtPricex96, which checks the spot price of the univ3 pool.

Vulnerability Detail

The slot0 or spot price has been historically proven to be a bad practice since it is easily manipulatable and core operations regarding liquidity in this codebase rely on that spot price. Large intentional trades can cause unfair price movement via front-running to shift the spot price of underlying pair assets and mess up the token amounts calculation

Impact

Wrong liquidity calculation, potential use losses

Code Snippet

https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L331-L342
https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L475-L514

Tool used

Manual Review

Recommendation

To make any calculation use a TWAP instead of slot0.

Duplicate of #109

p-tsanev - LiquidityBorrowingManager.sol#takeOverDebt() - wrong key used when pushing loans to the new borrow

p-tsanev

high

LiquidityBorrowingManager.sol#takeOverDebt() - wrong key used when pushing loans to the new borrow

Summary

The takeOverDebt() function is supposed, upon met criteria, to transfer a borrowing to a different owner, by creating a new borrowing key for the mapping and passing the old loans to the new borrow struct. There is a critical flaw in this functionality, that would not allow for correct overtaking.

Vulnerability Detail

The function updates the old borrow's fees up to the moment of taking over and then generates a new key and a new borrow struct. However, when the function _addKeysAndLoansInfo is invoked to transfer the old loans to the new key, instead of the newBorrowingKey, the old borrowingKey variable is passed instead.
This would lead to old loans being pushed again in the array for the old key, potentially reverting on exceeding the maximum allowed loans per positions.
Taking over would not be possible in such cases.

Impact

Impossible to take over loans, which is a core functionality of the protocol

Code Snippet

https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L431-L441

Tool used

Manual Review

Recommendation

Pass the correct value to the function as such:
_addKeysAndLoansInfo(newBorrowing.borrowedAmount > 0, newBorrowingKey, oldLoans)

Duplicate of #53

Bauer - `_patchAmountsAndCallSwap() will not operate correctly

Bauer

high

`_patchAmountsAndCallSwap() will not operate correctly

Summary

The amountOutMin parameter is set to 0 when passed to the _patchAmountsAndCallSwap() function. Consequently, the subsequent check to ensure that amountOut is greater than amountOutMin will always fail.

Vulnerability Detail

The function ApproveSwapAndPay._patchAmountsAndCallSwap() is used for executing token swaps within the protocol, ensuring that the external swap targets are appropriately approved and that the swaps meet the specified criteria.
The function verifies whether the received amount meets the minimum requirement specified by amountOutMin. If the received amount is zero or falls short of the minimum requirement, the function will revert the transaction.

     amountOut = _getBalance(tokenOut) - balanceOutBefore;
        // Checking if the received amount satisfies the minimum requirement
        if (amountOut == 0 || amountOut < amountOutMin) {
            revert SwapSlippageCheckError(amountOutMin, amountOut);
        }

In the LiquidityBorrowingManager._precalculateBorrowing() function, the amountOutMin parameter is being set to 0 when it's passed to the _patchAmountsAndCallSwap() function. This configuration means that the subsequent check, if (amountOut == 0 || amountOut < amountOutMin), will always fail.

   if (saleTokenBalance > 0) {
            if (params.externalSwap.swapTarget != address(0)) {
                // Call the external swap function and update the hold token balance in the cache
                cache.holdTokenBalance += _patchAmountsAndCallSwap(
                    params.saleToken,
                    params.holdToken,
                    params.externalSwap,
                    saleTokenBalance,
                    0
                );

Impact

The subsequent check to ensure that amountOut is greater than amountOutMin will always fail

Code Snippet

https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/ApproveSwapAndPay.sol#L171

Tool used

Manual Review

Recommendation

To address this issue, it's important to correctly set the amountOutMin parameter based on the specific requirements of the swap and the expected output.

Soltho - No slippage protection for Uniswap V3 swaps

Soltho

medium

No slippage protection for Uniswap V3 swaps

Summary

The _v3SwapExactInput function in ApproveSwapAndPay has fixed a fixed slipagge tolerance values(MIN_SQRT_RATIO and MAX_SQRT_RATIO) set. User can get a poor result from his swap and the transaction doesn't revert.

Vulnerability Detail

Contract doesn't protect users from a high slipagge when using Uniswap V3 swaps, so it can result in undesired operations being successfull , while _patchAmountsAndCallSwap function, that allows for arbitrary calls, allows user to customize every single parameter.

  (int256 amount0Delta, int256 amount1Delta) = IUniswapV3Pool(
            computePoolAddress(params.tokenIn, params.tokenOut, params.fee)
        ).swap(
                address(this), //recipient
                zeroForTokenIn,
                params.amountIn.toInt256(),
                (zeroForTokenIn ? MIN_SQRT_RATIO + 1 : MAX_SQRT_RATIO - 1),
                abi.encode(params.fee, params.tokenIn, params.tokenOut)
            );

Impact

Medium, since it can make undesired operations for users successfully.

Code Snippet

https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/ApproveSwapAndPay.sol#L204

Tool used

Manual Review

Recommendation

Add extra slippage protection in the function, preferrably allow to introduce arbitrary sqrtPriceLimitX96 values for it, just like in _patchAmountsAndCallSwap .

feelereth - The loop counter i is incremented without checks on the length of the tokens array. This can cause an overflow and potentially overwrite memory.

feelereth

high

The loop counter i is incremented without checks on the length of the tokens array. This can cause an overflow and potentially overwrite memory.

Summary

• Incrementing a loop counter without checks can cause overflow and memory corruption
• Should validate the counter against array length before incrementing
• Bounded increment prevents out of bounds access

Vulnerability Detail

The loop counter i is incremented without checking the length of the tokens array. This can cause an overflow and potentially overwrite memory.
Here is an example to illustrate the issue:

 function getBalances(address[] calldata tokens) external view returns (uint256[] memory balances) {

   uint256 length = tokens.length;

   balances = new uint256[](length);

   for (uint256 i; i < length; ) {
     // i is incremented without bounds checking
     unchecked {
       ++i; 
     }
   }
 }

If tokens has a length of 0, then length will be 0. But the loop will still increment i, causing it to become 1, then 2, etc. This will overwrite memory outside the bounds of the balances array.

Impact

  1. Incrementing the loop counter i without checking the length of the loans array is dangerous and can lead to memory corruption.
  2. the unchecked loop counter increment can lead to unauthorized modifications of critical contract storage variables. This has a huge potential impact - from simple errors and bugs at best, to exploitability and stolen funds at worst.

Code Snippet

https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/Vault.sol#L28-L40

Tool used

Manual Review

Recommendation

To fix this, the increment should be bounded by the length:

 for (uint256 i = 0; i < length; ) {
   // do stuff

   if (i < length - 1) { 
     ++i; 
   }
 }

Now i can only increment up to length - 1, preventing the overflow.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.