Farseen0/scar-pairs huggingface dataset:
HIGH severity: `TradingAccountBranch::depositMargin` attempts to transfer greater amount than user deposited for tokens with less than 18 decimals. **Description:** For collateral tokens with less than 18 decimals, `TradingAccountBranch::depositMargin` attempts to transfer a greater amount of tokens than what the user is actually depositing as:
* input `amount` is converted into `ud60x18Amount` via...

Vulnerable code:
function depositMargin(uint128 tradingAccountId, address collateralType, uint256 amount) public virtual {
    // load margin collateral config for this collateral type
    MarginCollateralConfiguration.Data storage marginCollateralConfiguration =
        MarginCollateralConfiguration.load(collateralType);

    // @audit convert uint256 -> UD60x18; scales input amount to 18 decimals
    UD60x18 ud60x18Amount = marginCollateralConfiguration.convertTokenAmountToUd60x18(amount);

    // *snip* //

    // @audit fetch tokens from the user; using the ud60x18Amount which has been
    // scaled up to 18 decimals. Will attempt to transfer more tokens than
    // user actually depositing
    IERC20(collateralType).safeTransferFrom(msg.sender, address(this), ud60x18Amount.intoUint256());

- IERC20(collateralType).safeTransferFrom(msg.sender, address(this), ud60x18Amount.intoUint256());
+ IERC20(collateralType).safeTransferFrom(msg.sender, address(this), amount);
HIGH severity: Trader can't reduce open position size when under initial margin requirement but over maintenance margin requirement. **Description:** [`OrderBranch::createMarketOrder`](https://github.com/zaros-labs/zaros-core-audit/blob/de09d030c780942b70f1bebcb2d245214144acd2/src/perpetuals/branches/OrderBranch.sol#L204-L208) and [`SettlementBranch::_fillOrder`](https://github.com/zaros-labs/zaros-core-audit/blob/de09d030c780942...

Vulnerable code:
function test_TraderCantReducePositionSizeWhenCollateralUnderIntitialRequired() external {
    // give naruto some tokens
    uint256 USER_STARTING_BALANCE = 100_000e18;
    int128  USER_POS_SIZE_DELTA   = 10e18;
    deal({ token: address(usdToken), to: users.naruto, give: USER_STARTING_BALANCE });

    // naruto creates a trading account and deposits their tokens as collateral
    changePrank({ msgSender: users.naruto });
    uint128 tradingAccountId = createAccountAndDeposit(USER_STARTING_BALANCE, address(usdToken));

    // naruto opens position in BTC market
    openManualPosition(BTC_USD_MARKET_ID, BTC_USD_STREAM_ID, MOCK_BTC_USD_PRICE, tradingAccountId, USER_POS_SIZE_DELTA);

    // market moves against Naruto's position
    // giving Naruto a negative PNL but not to the point of liquidation
    // if changed this to "/10" instead of "/11" naruto would be liquidatable,
    // so this is just on the verge of being liquidated
    uint256 updatedPrice = MOCK_BTC_USD_PRICE-MOCK_BTC_USD_PRICE/11;
    updateMockPriceFeed(BTC_USD_MARKET_ID, updatedPrice);

    // naruto's position now is below the initial margin requirement
    // but above the maintenance requirement. Naruto attempts to
    // reduce their position size to limit exposure but this reverts
    // with `InsufficientMargin` since `OrderBranch::createMarketOrder`
    // and `SettlementBranch::_fillOrder` always check against initial
    // margin requirements even when reducing already opened positions
    openManualPosition(BTC_USD_MARKET_ID,
                       BTC_USD_STREAM_ID,
                       updatedPrice,
                       tradingAccountId,
                       USER_POS_SIZE_DELTA-USER_POS_SIZE_DELTA/2);
}
MEDIUM severity: Users can easily bypass collateral `depositCap` limit using multiple deposits under the limit. **Description:** Margin collateral has a `depositCap` configuration to limit the total deposited amount for a particular collateral type.

But validation in `_requireEnoughDepositCap()` reverts when the current amount being deposited is greater than `depositCap`.

As it doesn't check the total...

Vulnerable code:
function _requireEnoughDepositCap(address collateralType, UD60x18 amount, UD60x18 depositCap) internal pure {
    if (amount.gt(depositCap)) {
        revert Errors.DepositCap(collateralType, amount.intoUint256(), depositCap.intoUint256());
    }
}
MEDIUM severity: Anyone can cancel traders' market orders due to missing access control in `OrderBranch::cancelMarketOrder`. **Description:** Anyone can cancel traders' market orders due to missing access control in `OrderBranch::cancelMarketOrder`:

**Impact:** Anyone can cancel traders' market orders.

**Recommended Mitigation:** `OrderBranch::cancelMarketOrder` should check if the caller is an owner of the trading...

Vulnerable code:
function cancelMarketOrder(uint128 tradingAccountId) external {
    MarketOrder.Data storage marketOrder = MarketOrder.loadExisting(tradingAccountId);

    marketOrder.clear();

    emit LogCancelMarketOrder(msg.sender, tradingAccountId);
}

function cancelMarketOrder(uint128 tradingAccountId) external {
+       TradingAccount.loadExistingAccountAndVerifySender(tradingAccountId);

        MarketOrder.Data storage marketOrder = MarketOrder.loadExisting(tradingAccountId);

        marketOrder.clear();

        emit LogCancelMarketOrder(msg.sender, tradingAccountId);
    }
MEDIUM severity: Protocol operator can disable market with open positions, making it impossible for traders to close their open positions but still subjecting them to potential liquidation. **Description:** `GlobalConfigurationBranch::updatePerpMarketStatus` allows the protocol operator to disable markets without checking if those markets have open positions; this allows the operator to disable a market with open positions.

**Impact:** The protocol can enter a state where traders...

Vulnerable code:
function test_ImpossibleToClosePositionIfMarkedDisabledButStillLiquidatable() external {
    // give naruto some tokens
    uint256 USER_STARTING_BALANCE = 100_000e18;
    int128  USER_POS_SIZE_DELTA   = 10e18;
    deal({ token: address(usdToken), to: users.naruto, give: USER_STARTING_BALANCE });

    // naruto creates a trading account and deposits their tokens as collateral
    changePrank({ msgSender: users.naruto });
    uint128 tradingAccountId = createAccountAndDeposit(USER_STARTING_BALANCE, address(usdToken));

    // naruto opens first position in BTC market
    openManualPosition(BTC_USD_MARKET_ID, BTC_USD_STREAM_ID, MOCK_BTC_USD_PRICE, tradingAccountId, USER_POS_SIZE_DELTA);

    // protocol operator disables the BTC market
    changePrank({ msgSender: users.owner });
    perpsEngine.updatePerpMarketStatus({ marketId: BTC_USD_MARKET_ID, enable: false });

    // naruto attempts to close their position
    changePrank({ msgSender: users.naruto });

    // naruto attmpts to close their opened leverage BTC position but it
    // reverts with PerpMarketDisabled error. However the position is still
    // subject to liquidation!
    //
    // after running this test the first time to verify it reverts with PerpMarketDisabled,
    // comment out this next line then re-run test to see Naruto can be liquidated
    // even though Naruto can't close their open position - very unfair!
    openManualPosition(BTC_USD_MARKET_ID, BTC_USD_STREAM_ID, MOCK_BTC_USD_PRICE, tradingAccountId, -USER_POS_SIZE_DELTA);

    // make BTC position liquidatable
    updateMockPriceFeed(BTC_USD_MARKET_ID, MOCK_BTC_USD_PRICE/2);

    // verify naruto can now be liquidated
    uint128[] memory liquidatableAccountsIds = perpsEngine.checkLiquidatableAccounts(0, 1);
    assertEq(1, liquidatableAccountsIds.length);
    assertEq(tradingAccountId, liquidatableAccountsIds[0]);

    // liquidate naruto - works fine! naruto was liquidated even though
    // they couldn't close their position!
    changePrank({ msgSender: liquidationKeeper });
    perpsEngine.liquidateAccounts(liquidatableAccountsIds, users.settlementFeeRecipient);
}
MEDIUM severity: Liquidation leaves traders with unhealthier and riskier collateral basket, making them more likely to be liquidated in future trades. **Description:** The protocol's proposed collateral priority queue with associated Loan-To-Value (LTV) is:

This means that the protocol will:
* first liquidate the more stable collateral with higher LTV
* only after these have been exhausted will it liquidate the less stable, riskier collaterals...

Vulnerable code:
1 - USDz   - 1e18 LTV
2 - USDC   - 1e18 LTV
3 - WETH   - 0.8e18 LTV
4 - WBTC   - 0.8e18 LTV
5 - wstETH - 0.7e18 LTV
6 - weETH  - 0.7e18 LTV
MEDIUM severity: Keeper fills market order using incorrect old `marketId` but newly updated position size when trader updates open order immediately before keeper processes original order. **Description:** Consider the following scenario:
1) A trader creates an order for the BTC market with position size POS_SIZE_A
2) Some time passes and the order is not filled by the keeper
3) At the same time:
3a) The keeper attempts to fill the trader's current open BTC order
3b) The trader...

Vulnerable code:
// additional import at top
import { Position } from "@zaros/perpetuals/leaves/Position.sol";

function test_KeeperFillsOrderToIncorrectMarketAfterUserUpdatesOpenOrder() external {
    // give naruto some tokens
    uint256 USER_STARTING_BALANCE = 100_000e18;
    int128  USER_POS_SIZE_DELTA   = 10e18;
    deal({ token: address(usdToken), to: users.naruto, give: USER_STARTING_BALANCE });

    // naruto creates a trading account and deposits their tokens as collateral
    changePrank({ msgSender: users.naruto });
    uint128 tradingAccountId = createAccountAndDeposit(USER_STARTING_BALANCE, address(usdToken));

    // naruto creates an open order in the BTC market
    perpsEngine.createMarketOrder(
        OrderBranch.CreateMarketOrderParams({
            tradingAccountId: tradingAccountId,
            marketId: BTC_USD_MARKET_ID,
            sizeDelta: USER_POS_SIZE_DELTA
        })
    );

    // some time passes and the order is not filled
    vm.warp(block.timestamp + MARKET_ORDER_MAX_LIFETIME + 1);

    // at the same time:
    // 1) keeper creates a transaction to fill naruto's open BTC order
    // 2) naruto updates their open order to place it on ETH market

    // 2) gets executed first; naruto changes position size and market id
    int128  USER_POS_2_SIZE_DELTA = 5e18;

    perpsEngine.createMarketOrder(
        OrderBranch.CreateMarketOrderParams({
            tradingAccountId: tradingAccountId,
            marketId: ETH_USD_MARKET_ID,
            sizeDelta: USER_POS_2_SIZE_DELTA
        })
    );

    // 1) gets executed afterwards - the keeper is calling this
    // with the parameters of the first opened order, in this case
    // with BTC's market id and price !
    bytes memory mockSignedReport = getMockedSignedReport(BTC_USD_STREAM_ID, MOCK_BTC_USD_PRICE);
    changePrank({ msgSender: marketOrderKeepers[BTC_USD_MARKET_ID] });
    perpsEngine.fillMarketOrder(tradingAccountId, BTC_USD_MARKET_ID, mockSignedReport);

    // the keeper filled Naruto's original BTC order even though
    // Naruto had first updated the order to be for the ETH market;
    // Naruto now has an open BTC position. Also it was filled using
    // the *updated* order size!
    changePrank({ msgSender: users.naruto });

    // load naruto's position for BTC market
    Position.State memory positionState = perpsEngine.getPositionState(tradingAccountId, BTC_USD_MARKET_ID, MOCK_BTC_USD_PRICE);

    // verify that the position size of the filled BTC position
    // matches the size of the updated ETH order!
    assertEq(USER_POS_2_SIZE_DELTA, positionState.sizeX18.intoInt256());
}
MEDIUM severity: `TradingAccount::getAccountMarginRequirementUsdAndUnrealizedPnlUsd` uses incorrect price during order settlement. **Description:** During order settlement `SettlementBranch::_fillOrder` uses an off-chain price provided by the keeper:

All variables including `ctx.fillPrice` and `ctx.fundingFeePerUnit` are calculated based on this price.

But during the margin requirement validation in...

Vulnerable code:
File: SettlementBranch.sol
120:         ctx.fillPrice = perpMarket.getMarkPrice(
121:             ctx.sizeDelta, settlementConfiguration.verifyOffchainPrice(priceData, ctx.sizeDelta.gt(SD_ZERO))
122:         );
123:
124:         ctx.fundingRate = perpMarket.getCurrentFundingRate();
125:         ctx.fundingFeePerUnit = perpMarket.getNextFundingFeePerUnit(ctx.fundingRate, ctx.fillPrice);

File: TradingAccount.sol
207:             UD60x18 markPrice = perpMarket.getMarkPrice(sizeDeltaX18, perpMarket.getIndexPrice());
208:             SD59x18 fundingFeePerUnit =
209:                 perpMarket.getNextFundingFeePerUnit(perpMarket.getCurrentFundingRate(), markPrice);
LOW severity: `SettlementBranch::_fillOrder` doesn't pay order and settlement fees if PNL is positive. **Description:** `SettlementBranch::_fillOrder` while calculating the PNL at L161 deducts these fees from the calculated PNL, but if the PNL is positive later on around L204 it doesn't pay order and settlement fees to the appropriate fee recipients.

**Impact:** Order and settlement fee recipients.....

Vulnerable code:
File: SettlementBranch.sol
159:         ctx.pnl = oldPosition.getUnrealizedPnl(ctx.fillPrice).add(
160:             oldPosition.getAccruedFunding(ctx.fundingFeePerUnit)
161:         ).add(unary(ctx.orderFeeUsdX18.add(ctx.settlementFeeUsdX18.intoSD59x18())));
...
204:         } else if (ctx.pnl.gt(SD_ZERO)) {
205:             UD60x18 amountToIncrease = ctx.pnl.intoUD60x18();
206:             tradingAccount.deposit(ctx.usdToken, amountToIncrease);
LOW severity: `TradingAccount::deductAccountMargin` can incorrectly add the same values multiple times to output parameter `marginDeductedUsdX18`. **Description:** In `TradingAccount::deductAccountMargin`, `ctx.settlementFeeDeductedUsdX18`, `ctx.orderFeeDeductedUsdX18`, and `ctx.pnlDeductedUsdX18` are added to `marginDeductedUsdX18` inside the `for` loop:

**Impact:** As those 3 values are accumulated withdrawn collateral amounts, the same...

Vulnerable code:
File: TradingAccount.sol
443:             marginDeductedUsdX18 = marginDeductedUsdX18.add(ctx.settlementFeeDeductedUsdX18);
458:             marginDeductedUsdX18 = marginDeductedUsdX18.add(ctx.orderFeeDeductedUsdX18);
475:             marginDeductedUsdX18 = marginDeductedUsdX18.add(ctx.pnlDeductedUsdX18);

function deductAccountMargin() {
    for (uint256 i = 0; i < globalConfiguration.collateralLiquidationPriority.length(); i++) {
        /* snip: loop processing */
        // @audit removed updates to `marginDeductedUsdX18` during loop
    }

    // @audit update `marginDeductedUsdX18` only once at end of loop
    marginDeductedUsdX18 = ctx.settlementFeeDeductedUsdX18.add(ctx.orderFeeDeductedUsdX18).add(ctx.pnlDeductedUsdX18);
}
LOW severity: `SettlementBranch::_fillOrder` can revert for positions with negative PNL in markets with negative maker/taker fees. **Description:** `SettlementBranch::_fillOrder` calculates the position's PNL as `pnl = (unrealizedPnl + accruedFunding) + (unary(orderFee + settlementFee))`:

If the PNL is negative, `marginToDeductUsdX18` is calculated by deducting the setttlement/order fees.

As we can see at L183, it assumes...

Vulnerable code:
File: SettlementBranch.sol
141: ctx.pnl = oldPosition.getUnrealizedPnl(ctx.fillPrice).add(
142:     oldPosition.getAccruedFunding(ctx.fundingFeePerUnit)
143:     ).add(unary(ctx.orderFeeUsdX18.add(ctx.settlementFeeUsdX18.intoSD59x18())));
144:

171: if (ctx.pnl.lt(SD_ZERO)) {
172:     UD60x18 marginToDeductUsdX18 = ctx.orderFeeUsdX18.add(ctx.settlementFeeUsdX18.intoSD59x18()).gt(SD_ZERO)
173:     ? ctx.pnl.abs().intoUD60x18().sub(ctx.orderFeeUsdX18.intoUD60x18().add(ctx.settlementFeeUsdX18))
174:     : ctx.pnl.abs().intoUD60x18();
175:
176:     tradingAccount.deductAccountMargin({
177:         feeRecipients: FeeRecipients.Data({
178:         marginCollateralRecipient: globalConfiguration.marginCollateralRecipient,
179:         orderFeeRecipient: globalConfiguration.orderFeeRecipient,
180:         settlementFeeRecipient: globalConfiguration.settlementFeeRecipient
181:     }),
182:     pnlUsdX18: marginToDeductUsdX18,
183:     orderFeeUsdX18: ctx.orderFeeUsdX18.gt(SD_ZERO) ? ctx.orderFeeUsdX18.intoUD60x18() : UD_ZERO,
184:     settlementFeeUsdX18: ctx.settlementFeeUsdX18
185:     });

/// @notice Casts an SD59x18 number into UD60x18.
/// @dev Requirements:
/// - x must be positive.
function intoUD60x18(SD59x18 x) pure returns (UD60x18 result) {
    int256 xInt = SD59x18.unwrap(x);
    if (xInt < 0) {
        revert CastingErrors.PRBMath_SD59x18_IntoUD60x18_Underflow(x);
    }
    result = UD60x18.wrap(uint256(xInt));
}
LOW severity: Remove max open interest check when liquidating in `LiquidationBranch::liquidateAccounts`. **Description:** In `LiquidationBranch::liquidateAccounts` there is no point calling `PerpMarket::checkOpenInterestLimits` since:
* the liquidation will always be decreasing the open interest so the liquidation can't cause the max open interest limit to be breached
* the skew check is not...

Vulnerable code:
- (ctx.newOpenInterestX18, ctx.newSkewX18) = perpMarket.checkOpenInterestLimits(
-     ctx.liquidationSizeX18, ctx.oldPositionSizeX18, sd59x18(0), false
- );
LOW severity: `MarketOrder` minimum lifetime can be easily bypassed. **Description:** `OrderBranch::createMarketOrder` validates the `marketOrderMinLifetime` of the previous pending market order before canceling it and opening a new market order:

But in `OrderBranch::cancelMarketOrder` users can cancel the pending market order without any validation:

Hence users...

Vulnerable code:
File: OrderBranch.sol
                // @audit `createMarketOrder` enforces minimum market order lifetime
210:         marketOrder.checkPendingOrder();
211:         marketOrder.update({ marketId: params.marketId, sizeDelta: params.sizeDelta });

File: MarketOrder.sol
55:     function checkPendingOrder(Data storage self) internal view {
56:         GlobalConfiguration.Data storage globalConfiguration = GlobalConfiguration.load();
57:         uint128 marketOrderMinLifetime = globalConfiguration.marketOrderMinLifetime;
58:
59:         if (self.timestamp != 0 && block.timestamp - self.timestamp <= marketOrderMinLifetime) {
60:             revert Errors.MarketOrderStillPending(self.timestamp);
61:         }
62:     }

File: OrderBranch.sol
219:     function cancelMarketOrder(uint128 tradingAccountId) external {
220:         MarketOrder.Data storage marketOrder = MarketOrder.loadExisting(tradingAccountId);
221:         // @audit doesn't enforce minimum market order lifetime
222:         marketOrder.clear();
223:
224:         emit LogCancelMarketOrder(msg.sender, tradingAccountId);
225:     }

function cancelMarketOrder(uint128 tradingAccountId) external {
        MarketOrder.Data storage marketOrder = MarketOrder.loadExisting(tradingAccountId);

+       marketOrder.checkPendingOrder();

        marketOrder.clear();

        emit LogCancelMarketOrder(msg.sender, tradingAccountId);
    }
LOW severity: `PerpMarket::getOrderFeeUsd` incorrectly charges `makerFee` when skew is zero and trade is buy order. **Description:** In `PerpMarket::getOrderFeeUsd` there is this comment to which I've added (*) at the end:

But doing a truth table for the `if` statement's boolean expression shows this comment is not always true:

When `isSkewGtZero == false && isBuyOrder = true`, the *maker* fee will be...

Vulnerable code:
/// @dev When the skew is zero, taker fee will be charged. (*)

// isSkewGtZero = true,  isBuyOrder = true  -> taker fee
// isSkewGtZero = true,  isBuyOrder = false -> maker fee
// isSkewGtZero = false, isBuyOrder = true  -> maker fee (*)
// isSkewGtZero = false, isBuyOrder = false -> taker fee
if (isSkewGtZero != isBuyOrder) {
    // not equal charge maker fee
    feeBps = sd59x18((self.configuration.orderFees.makerFee));
} else {
    // equal charge taker fee
    feeBps = sd59x18((self.configuration.orderFees.takerFee));
}
HIGH severity: `OrderBranch::getMarginRequirementForTrade` doesn't include order and settlement fees when calculating margin requirements. **Description:** `OrderBranch::getMarginRequirementForTrade` doesn't include order and settlement fees when calculating margin requirements but this occurs in other places such as [`OrderBranch::createMarketOrder`](https://github.com/zaros-labs/zaros-core-audit/blob/de09d030c780942b70f1bebcb2d245214...

Vulnerable code:
orderFeeUsdX18 = perpMarket.getOrderFeeUsd(sd59x18(sizeDelta), fillPriceX18);
settlementFeeUsdX18 = ud60x18(uint256(settlementConfiguration.fee));
HIGH severity: Unused variables. **Description:** Some errors and constants aren't used in the codebase.

**Zaros:** Fixed in commit [37071ed](https://github.com/zaros-labs/zaros-core/commit/37071ed2c6845121e2eda4143ad233d20a1d7e6f).

**Cyfrin:** Verified.

Vulnerable code:
File: Errors.sol
11:     error InvalidParameter(string parameter, string reason);
38:     error OnlyForwarder(address sender, address forwarder);
79:     error InvalidLiquidationReward(uint128 liquidationFeeUsdX18);

File: Constants.sol
10:     uint32 internal constant MAX_MIN_DELEGATE_TIME = 30 days;
HIGH severity: Remove redundant `uint256` cast in `PerpMarket::getMarkPrice`. **Description:** Remove redundant `uint256` cast in `PerpMarket::getMarkPrice` since `self.configuration.skewScale` is [already](https://github.com/zaros-labs/zaros-core-audit/blob/de09d030c780942b70f1bebcb2d245214144acd2/src/perpetuals/leaves/MarketConfiguration.sol#L30) `uint256`.

**Recommended.....

Vulnerable code:
SD59x18 skewScale = sd59x18(self.configuration.skewScale.toInt256());
HIGH severity: Use input `amount` in `TradingAccountBranch::withdrawMargin` when calling `safeTransfer`. **Description:** Remove redundant conversion by using input `amount` in `TradingAccountBranch::withdrawMargin` when [calling](https://github.com/zaros-labs/zaros-core-audit/blob/de09d030c780942b70f1bebcb2d245214144acd2/src/perpetuals/branches/TradingAccountBranch.sol#L298-L300) `safeTransfer` at...

Vulnerable code:
- uint256 tokenAmount = marginCollateralConfiguration.convertUd60x18ToTokenAmount(ud60x18Amount);
- IERC20(collateralType).safeTransfer(msg.sender, tokenAmount);
+IERC20(collateralType).safeTransfer(msg.sender, amount);
HIGH severity: Remove redundant `unary` call from `TradingAccount::getAccountMarginRequirementUsdAndUnrealizedPnlUsd`. **Description:** `TradingAccount::getAccountMarginRequirementUsdAndUnrealizedPnlUsd` [L239](https://github.com/zaros-labs/zaros-core-audit/blob/de09d030c780942b70f1bebcb2d245214144acd2/src/perpetuals/leaves/TradingAccount.sol#L239) does this:

The `unary`...

Vulnerable code:
UD60x18 markPrice = perpMarket.getMarkPrice(unary(sd59x18(position.size)), perpMarket.getIndexPrice());

UD60x18 markPrice = perpMarket.getMarkPrice(sd59x18(-position.size), perpMarket.getIndexPrice());

ctx.oldPositionSizeX18 = sd59x18(position.size);
- ctx.liquidationSizeX18 = unary(ctx.oldPositionSizeX18);
+ ctx.liquidationSizeX18 = sd59x18(-position.size);
LOW severity: `LiquidationKeeper::checkUpkeep` should only continue processing if lower bounds are smaller than upper bounds. **Description:** `LiquidationKeeper::checkUpkeep` should only continue processing if `checkLowerBound < checkUpperBound || performLowerBound < performUpperBound` at L70.

Currently the revert check uses `>` instead of `>=` but if `checkLowerBound == checkUpperBound` then...

Vulnerable code:
- if (checkLowerBound  > checkUpperBound || performLowerBound > performUpperBound) {
+ if (checkLowerBound >= checkUpperBound || performLowerBound >= performUpperBound) {
    revert Errors.InvalidBounds();
}