Vulnerability: DoS of lenders and gas griefing by packing tokenIdToBorrowingKeys arrays
Severity: medium

Description:
`LiquidityBorrowingManager.borrow()` calls the function `_addKeysAndLoansInfo()`, which adds user keys to the `tokenIdToBorrowingKeys` array of the borrowed-from LP position:\\n```\\n    function _addKeysAndLoansInfo(\\n        bool update,\\n        bytes32 borrowingKey,\\n        LoanInfo[] memory sourceLoans\\n    ) private {\\n        // Get the storage reference to the loans array for the borrowing key\\n        LoanInfo[] storage loans = loansInfo[borrowingKey];\\n        // Iterate through the sourceLoans array\\n        for (uint256 i; i < sourceLoans.length; ) {\\n            // Get the current loan from the sourceLoans array\\n            LoanInfo memory loan = sourceLoans[i];\\n            // Get the storage reference to the tokenIdLoansKeys array for the loan's token ID\\n            bytes32[] storage tokenIdLoansKeys = tokenIdToBorrowingKeys[loan.tokenId];\\n            // Conditionally add or push the borrowing key to the tokenIdLoansKeys array based on the 'update' flag\\n            update\\n                ? tokenIdLoansKeys.addKeyIfNotExists(borrowingKey)\\n                : tokenIdLoansKeys.push(borrowingKey);\\n    // rest of code\\n```\\n\\nA user key is calculated in the `Keys` library like so:\\n```\\n    function computeBorrowingKey(\\n        address borrower,\\n        address saleToken,\\n        address holdToken\\n    ) internal pure returns (bytes32) {\\n        return keccak256(abi.encodePacked(borrower, saleToken, holdToken));\\n    }\\n```\\n\\n```\\n    function addKeyIfNotExists(bytes32[] storage self, bytes32 key) internal {\\n        uint256 length = self.length;\\n        for (uint256 i; i < length; ) {\\n            if (self.unsafeAccess(i).value == key) {\\n                return;\\n            }\\n            unchecked {\\n                ++i;\\n            }\\n        }\\n        self.push(key);\\n    }\\n\\n    function removeKey(bytes32[] storage self, bytes32 key) internal {\\n        uint256 length = self.length;\\n        for (uint256 i; i < length; ) {\\n            if (self.unsafeAccess(i).value == key) {\\n                self.unsafeAccess(i).value = self.unsafeAccess(length - 1).value;\\n                self.pop();\\n                break;\\n            }\\n            unchecked {\\n                ++i;\\n            }\\n        }\\n    }\\n```\\n\\nLet's give an example to see the potential impact and cost of the attack:\\nAn LP provider authorizes the contract to give loans from their large position. Let's say USDC/WETH pool.\\nThe 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.\\nThe 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.\\nThe 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.\\nThe 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.

Recommended mitigation:
`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.