xanoutas/solidity-security-findings huggingface dataset:
Borrow Token Withdrawals Blocked Due to Incorrect Opening Collateral Ratio Check
Audit: 2024-06-size-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-06-size/blob/8850e25fb088898e9cf86f9be1c401ad155bea86/src/Size.sol#L162


# Vulnerability details

## Impact

The current implementation of the `withdraw` function prevents users from withdrawing their underlying borrow tokens if their collateral ratio (CR) is below their opening collateral ratio, even when their actual CR is above the liquidation threshold. This lead to users being unable to withdraw their borrow tokens even if they have a safe actual CR (even with an unhealthy CR users should be able to withdraw their borrow tokens).

## Proof of Concept

- In the Size protocol, the main tokens for borrowing and collateral are **USDC** and **WETH**, which are converted into non-transferable ERC-20 tokens when deposited (`szaUSDC` and `szWETH`).
- `aszUSDC` is the borrow token that tracks `USDC` in the system, while `szWETH` is the collateral token that tracks `WETH` for all users.

- The withdrawal function is essential for managing user withdrawals in a secure and validated manner. Withdrawals of borrow tokens (`USDC`) request the amount from the **Variable Pool** (**Aave v3**), which is then sent to the user's wallet. Collateral tokens (`WETH`) are transferred from the **Size contract** directly.

- Users should always be able to withdraw their borrow tokens(`USDC`) without concerning themselves about the collateral ratio (CR), since the borrow token does not directly affect the CR. The CR is calculated based on the value of the collateral and the value of the debt.
```js
function collateralRatio(
        State storage state,
        address account
    ) public view returns (uint256) {
        uint256 collateral = state.data.collateralToken.balanceOf(account);
        uint256 debt = state.data.debtToken.balanceOf(account);
        uint256 debtWad = Math.amountToWad(
            debt,
            state.data.underlyingBorrowToken.decimals()
        );
        uint256 price = state.oracle.priceFeed.getPrice();

        if (debt != 0) {
            return Math.mulDivDown(collateral, price, debtWad);
        } else {
            return type(uint256).max;
        }
    }

```
- The issue arises when a user attempts to withdraw their underlying borrow tokens. If the user's CR is below their opening collateral ratio, the transaction will revert due to the `validateUserIsNotBelowOpeningLimitBorrowCR` check, even though the actual CR is above the liquidation threshold.
```js
 function withdraw(WithdrawParams calldata params) external payable override(ISize) whenNotPaused {
        state.validateWithdraw(params);
        state.executeWithdraw(params);
       // this check does not consider if is a borrow or collateral token 
   >>   state.validateUserIsNotBelowOpeningLimitBorrowCR(msg.sender);
    }

```
- This check does not consider whether the withdrawal is for a borrow or collateral token, leading to incorrect reverts.

- **Example Scenario**:
  - Initial Setup:
    - User opening CR: 200%
    - S
Withdrawal can fail even if user is not underwater
Audit: 2024-06-size-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-06-size/blob/main/src/Size.sol#L162


# Vulnerability details

## Impact
Users can't withdraw borrow or collateral tokens if their collateral ratio is below opening ratio but above liquidation ratio.

[Size.withdraw()](https://github.com/code-423n4/2024-06-size/blob/main/src/Size.sol#L162) checks that user has collateral ratio above opening ratio. The problem is that:
1. Users potentially can't withdraw USDC if their `CR < openingCR`, even if `CR > liquidationCR`.
2. Arguably, collateral withdrawal should only be prevented if that pushes the user underwater.
3. Centralization risk of `updateConfig()` being used to increase `openingCR` and preventing all withdrawals.

## Proof of Concept

Following test case added to `Withdrawal.t.sol` fails.

```
    function test_Withdraw_user_can_withdraw_if_they_are_not_underwater() public {
        _setPrice(1e18);
        _deposit(alice, usdc, 150e6);
        _deposit(bob, usdc, 150e6);
        _deposit(bob, weth, 150e18);
        _buyCreditLimit(alice, block.timestamp + 12 days, YieldCurveHelper.pointCurve(12 days, 0));
        _sellCreditMarket(bob, alice, RESERVED_ID, 50e6, 12 days, false);

        _setPrice(1e18 / 2);
        assertLt(size.collateralRatio(bob), size.riskConfig().crOpening);
        assertTrue(!size.isUserUnderwater(bob));

        vm.startPrank(bob);
        size.withdraw(WithdrawParams({token: address(usdc), amount: 1, to: bob}));
    }
```

## Recommended Mitigation Steps
Use `liquidationCR` instead of `openingCR` as the threshold. Also, consider always allowing borrow token withdrawal.


## Assessed type

DoS
Upgraded Q -> 2 from #135 [1720794005467]
Audit: 2024-06-size-findings
Severity: MEDIUM

Judge has assessed an item in Issue #135 as 2 risk. The relevant finding follows:

 The params.amount validation could be wrong if the value of params.amount is amountCashOut in Size.sellCreditMarket() and amountCashIn in Size.buyCreditMarket() potentially leads to opportunity loss
The total supply of borrowAToken can go over the specified cap
Audit: 2024-06-size-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-06-size/blob/8850e25fb088898e9cf86f9be1c401ad155bea86/src/Size.sol#L153-L156


# Vulnerability details

## Impact
The `borrowAToken` has a total supply cap it should not go over. In reality, the total supply can go over the specified cap.

## Proof of Concept
Whenever a user deposits into the system, he calls `deposit()` which then validates the provided params and calls `executeDeposit()` on the `Deposit.sol` library.
This is a part of the function that is responsible for handling the case where a user has deposited a borrow token:
```solidity
if (params.token == address(state.data.underlyingBorrowToken)) {
            state.depositUnderlyingBorrowTokenToVariablePool(from, params.to, amount);
            if (!state.data.isMulticall) {
                state.validateBorrowATokenCap();
            }
        }
```
The `depositUnderlyingBorrowTokenToVariablePool()` function that is called sends the token from the `from` (msg.sender in this case) address to the contract, then deposits those tokens into AAVE for extra yield and it mints `borrowAToken` based on the increase of `aToken` received from AAVE.

Then, we validate whether this function is called through `multicall()` and if it isn't, then we validate whether the total supply of `borrowAToken` is less or equal to the `borrowATokenCap` and reverts otherwise.

If this is a multicall, then this gets skipped as a user could be doing debt reduction operations and that check would disallow the user from doing so in certain cases. Instead, the `Multicall` library has a way of dealing with this.
```solidity
    function multicall(State storage state, bytes[] calldata data) internal returns (bytes[] memory results) {
        state.data.isMulticall = true;

        uint256 borrowATokenSupplyBefore = state.data.borrowAToken.balanceOf(address(this));
        uint256 debtTokenSupplyBefore = state.data.debtToken.totalSupply();

        results = new bytes[](data.length);
        for (uint256 i = 0; i < data.length; i++) {
            results[i] = Address.functionDelegateCall(address(this), data[i]);
        }

        uint256 borrowATokenSupplyAfter = state.data.borrowAToken.balanceOf(address(this));
        uint256 debtTokenSupplyAfter = state.data.debtToken.totalSupply();

        state.validateBorrowATokenIncreaseLteDebtTokenDecrease(
            borrowATokenSupplyBefore, debtTokenSupplyBefore, borrowATokenSupplyAfter, debtTokenSupplyAfter
        );

        state.data.isMulticall = false;
    }
```
We get the supplies of `borrowAToken` and `debtToken` before and after the calls and we check whether the decrease of debt is lower or equal to the increase of supply and we revert otherwise.

The issue is because of these 2 lines:
```solidity
uint256 borrowATokenSupplyBefore = state.data.borrowAToken.balanceOf(address(this));
uint256 borrowATokenSupplyAfter = state.data.borrowAToken.balanceOf(address(this));
```
They are using the balance of the contract wh
Incorrect Credit Amount Calculation in `getCreditAmountIn` Function Due to Improper Fragmentation Fee Handling
Audit: 2024-06-size-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-06-size/blob/8850e25fb088898e9cf86f9be1c401ad155bea86/src/libraries/AccountingLibrary.sol#L253-L255
https://github.com/code-423n4/2024-06-size/blob/8850e25fb088898e9cf86f9be1c401ad155bea86/src/libraries/AccountingLibrary.sol#L219-L301


# Vulnerability details

## Impact
[getCreditAmountIn:L253-L255](https://github.com/code-423n4/2024-06-size/blob/8850e25fb088898e9cf86f9be1c401ad155bea86/src/libraries/AccountingLibrary.sol#L253-L255)
```solidity
creditAmountIn = Math.mulDivUp(
    cashAmountOut + state.feeConfig.fragmentationFee, PERCENT + ratePerTenor, PERCENT - swapFeePercent
);
```
In the case where `cashAmountOut < maxCashAmountOutFragmentation`, the `creditAmountIn` calculation is incorrect. It should be.
```solidity
creditAmountIn = Math.mulDivUp(
    cashAmountOut, PERCENT + ratePerTenor, PERCENT - swapFeePercent
) + Math.mulDivUp(state.feeConfig.fragmentationFee, PERCENT + ratePerTenor, PERCENT);
```
The current implementation adds the `fragmentationFee` to `cashAmountOut` before calculating `creditAmountIn`, which is incorrect. The `fragmentationFee should be added separately to the `creditAmountIn` after the calculation.

The corrected formula ensures that the `creditAmountIn` is calculated based on the actual `cashAmountOut` (excluding the fragmentation fee) and then adds the fragmentation fee converted to credit terms.

## Proof of Concept
1. `getCreditAmountIn` and `getCreditAmountOut` functions in `AccountingLibrary.sol`

    - [AccountingLibrary.sol#L219-L301](https://github.com/code-423n4/2024-06-size/blob/8850e25fb088898e9cf86f9be1c401ad155bea86/src/libraries/AccountingLibrary.sol#L219-L301)

2. `Errors.sol` library

    - [Errors.sol](https://github.com/code-423n4/2024-06-size/blob/main/src/libraries/Errors.sol)

3. `Math.sol` library:

    - [Math.sol](https://github.com/code-423n4/2024-06-size/blob/main/src/libraries/Math.sol)

**`Suppose we have the following values`**
    - `cashAmountOut` = 900
    - `maxCashAmountOut` = 1000
    - `state.feeConfig.fragmentationFee` = 100
    - `ratePerTenor` = 10 (10%)
    - `swapFeePercent` = 5 (5%)

In this case, `maxCashAmountOutFragmentation` would be 900 (1000 - 100).

Since `cashAmountOut` (900) equals `maxCashAmountOutFragmentation` (900), the code will enter the `else if` block in the `getCreditAmountIn` function.

With the current implementation, the `creditAmountIn` calculation would be: [creditAmountIn#L253-L255](https://github.com/code-423n4/2024-06-size/blob/8850e25fb088898e9cf86f9be1c401ad155bea86/src/libraries/AccountingLibrary.sol#L253-L255)
```solidity
creditAmountIn = Math.mulDivUp(
    cashAmountOut + state.feeConfig.fragmentationFee, PERCENT + ratePerTenor, PERCENT - swapFeePercent
);
```
Plugging in the values.
```solidity
creditAmountIn = Math.mulDivUp(900 + 100, 100 + 10, 100 - 5);
               = Math.mulDivUp(1000, 110, 95);
               = 1158
```
However, this calculation is incorrect because it includes the
Double-Counting of Fragmentation Fee in getCashAmountIn Function Leads to Incorrect Calculations
Audit: 2024-06-size-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-06-size/blob/8850e25fb088898e9cf86f9be1c401ad155bea86/src/libraries/AccountingLibrary.sol#L326-L327
https://github.com/code-423n4/2024-06-size/blob/8850e25fb088898e9cf86f9be1c401ad155bea86/src/libraries/AccountingLibrary.sol#L329
https://github.com/code-423n4/2024-06-size/blob/8850e25fb088898e9cf86f9be1c401ad155bea86/src/libraries/AccountingLibrary.sol#L303-L336


# Vulnerability details

## Impact
The calculation of `cashAmountIn` when there is credit fractionalization. In the case of credit fractionalization (`creditAmountOut < maxCredit)`, the code calculates `cashAmountIn` as follows: [getCashAmountIn#L326-L327](https://github.com/code-423n4/2024-06-size/blob/8850e25fb088898e9cf86f9be1c401ad155bea86/src/libraries/AccountingLibrary.sol#L326-L327)
```solidity
uint256 netCashAmountIn = Math.mulDivUp(creditAmountOut, PERCENT, PERCENT + ratePerTenor);
cashAmountIn = netCashAmountIn + state.feeConfig.fragmentationFee;
```
However, this calculation needs to be corrected. The `fragmentationFee` should not be added to `cashAmountIn` because it is already included in the calculation of the fees in the next line: [getCashAmountIn#L329](https://github.com/code-423n4/2024-06-size/blob/8850e25fb088898e9cf86f9be1c401ad155bea86/src/libraries/AccountingLibrary.sol#L329)
```solidity
fees = getSwapFee(state, netCashAmountIn, tenor) + state.feeConfig.fragmentationFee;
```
By adding `fragmentationFee` to both `cashAmountIn` and `fees`, the function is double-counting the fragmentation fee, which may lead to incorrect results.

## Proof of Concept
Link to the `getCashAmountIn` function in the `AccountingLibrary.sol`https://github.com/code-423n4/2024-06-size/blob/8850e25fb088898e9cf86f9be1c401ad155bea86/src/libraries/AccountingLibrary.sol#L303-L336

In the case of credit fractionalization `(creditAmountOut < maxCredit)`, the fragmentationFee is added to both `cashAmountIn` and fees. This leads to double-counting of the fragmentation fee, resulting in an incorrect calculation of `cashAmountIn`.

_The correct calculation should only include the `fragmentationFee` in the `fees` calculation, not in `cashAmountIn`._

Let's say `creditAmountOut` is 800, `maxCredit` is 1000, `ratePerTenor` is 10%, and `state.feeConfig.fragmentationFee` is 5.

With the current implementation
    - `netCashAmountIn` = 800 * 100 / (100 + 10) ≈ 727.27
    - `cashAmountIn` = 727.27 + 5 = 732.27
    - `fees` = getSwapFee(state, 727.27, tenor) + 5

However, the correct calculation should be
    - `netCashAmountIn` = 800 * 100 / (100 + 10) ≈ 727.27
    - `cashAmountIn` = 727.27
    - `fees` = getSwapFee(state, 727.27, tenor) + 5

## Tools Used
Vs
## Recommended Mitigation Steps
The calculation of `cashAmountIn` should be updated to
```solidity
uint256 netCashAmountIn = Math.mulDivUp(creditAmountOut, PERCENT, PERCENT + ratePerTenor);
cashAmountIn = netCashAmountIn;
```
This way, the `fragmentationFee` is only included in the `fees` calculation, a
A borrower can force set other lender's credit position for sale, by calling compensate and passing `RESERVED ID`
Audit: 2024-06-size-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-06-size/blob/main/src/libraries/actions/Compensate.sol#L106-L156
https://github.com/code-423n4/2024-06-size/blob/main/src/libraries/AccountingLibrary.sol#L103-L127


# Vulnerability details

## Impact
When lenders create credit positions, they are set by default for sale, however, lenders can set specific positions as not for sale, in other words, if a lender doesn't want to sell this specific credit, this can be done by calling `setUserConfiguration`. On the other hand, a borrower (who has a corresponding debt position from that credit position) can split this lender's credit position into multiple pieces which sets them by default as for sale, without the lender's acknowledgment.

This can be done by the borrower calling `compensate` while passing `creditPositionToCompensateId` as `RESERVED_ID`. `executeCompensate` creates a new credit position while having `forSale: true` according to the amount input, which can be 99% of the lender's credit position.

This allows a user (borrower) to alter another user's (lender) credit position config, by moving its credit to another defaulted settings position.

## Proof of Concept

```solidity
function test_borrower_can_force_position_for_sale() public {
    _deposit(alice, weth, 100e18);
    _deposit(alice, usdc, 500e6);
    _deposit(bob, weth, 100e18);
    _deposit(bob, usdc, 500e6);
    _deposit(candy, weth, 100e18);
    _deposit(candy, usdc, 500e6);

    int256[] memory aprs = new int256[](1);
    uint256[] memory tenors = new uint256[](1);
    uint256[] memory marketRateMultipliers = new uint256[](1);

    aprs[0] = 0.2e18;
    tenors[0] = 365 days;
    marketRateMultipliers[0] = 0;

    // Bob creates a limit order
    vm.prank(bob);
    size.buyCreditLimit(
        BuyCreditLimitParams({
            curveRelativeTime: YieldCurve({
                tenors: tenors,
                marketRateMultipliers: marketRateMultipliers,
                aprs: aprs
            }),
            maxDueDate: block.timestamp + 365 days
        })
    );

    uint256 amount = 100e6;

    // Alice sells credit market (borrows)
    vm.prank(alice);
    size.sellCreditMarket(
        SellCreditMarketParams({
            lender: bob,
            creditPositionId: type(uint256).max,
            tenor: 365 days,
            amount: amount,
            exactAmountIn: true,
            deadline: block.timestamp,
            maxAPR: type(uint256).max
        })
    );

    uint256 creditPositionId = type(uint256).max / 2;

    // Bob's credit position is for sale
    assertTrue(size.getCreditPosition(creditPositionId).forSale);

    uint256[] memory creditPositionIds = new uint256[](1);
    creditPositionIds[0] = creditPositionId;

    // Bob sets that position as not for sale
    vm.prank(bob);
    size.setUserConfiguration(
        SetUserConfigurationParams({
            openingLimitBorrowCR: 0,
            allCreditPositionsForSaleDisabled: false,
            creditPositionI
Muticall deposits entirely bypass the borrowAToken cap
Audit: 2024-06-size-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-06-size/blob/8850e25fb088898e9cf86f9be1c401ad155bea86/src/libraries/Multicall.sol#L37
https://github.com/code-423n4/2024-06-size/blob/8850e25fb088898e9cf86f9be1c401ad155bea86/src/libraries/CapsLibrary.sol#L27-L41


# Vulnerability details

## Impact
The `borrowAToken` cap is ineffective when depositing USDC via `multicall`. As a result, users can bypass protocol risk parameters set by the admin.

In the case of regular deposits, `validateBorrowATokenCap` checks the `borrowATokenCap` against the total supply of `borrowAToken`. However, in multicall, `state.data.borrowAToken.balanceOf(address(this))` is used which returns the AToken balance of the Size contract. The Size contract only holds a small chunk of the total supply (i.e. repaid, but not yet claimed tokens).

## Proof of Concept
I created a test to reproduce this issue:

```
    function test_Multicall_multicall_always_bypasses_cap() public {
        _setPrice(1e18);
        uint256 amount = 100e6;
        uint256 cap = 100e6;
        _updateConfig("borrowATokenCap", cap);

        _deposit(alice, usdc, amount);
        // _deposit(bob, weth, 200e18);

        _mint(address(usdc), bob, amount);
        _approve(bob, address(usdc), address(size), amount);

        // attempt to reposit via multicall
        bytes[] memory data = new bytes[](1);
        data[0] = abi.encodeCall(size.deposit, DepositParams({token: address(usdc), amount: amount, to: bob}));
        vm.prank(bob);
        size.multicall(data);

        assertGt(size.data().borrowAToken.totalSupply() , cap);        
    }
```

## Tools Used

Manual review

## Recommended Mitigation Steps

 To get the total outstanding balance of `borrowATokens`, this should be either:

```
uint256 borrowATokenSupplyBefore = state.data.borrowAToken.totalSupply();
```

Or:

```
uint256 borrowATokenSupplyBefore = state.data.underlyingBorrowToken.balanceOf(address(this));
```

## Assessed type

Invalid Validation








## Assessed type

Invalid Validation
Some users will be required to borrow a higher amount of tokens than others
Audit: 2024-06-size-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-06-size/blob/8850e25fb088898e9cf86f9be1c401ad155bea86/src/libraries/actions/BuyCreditMarket.sol#L91-L93


# Vulnerability details

## Impact
Some users will be required to borrow a higher amount of tokens than others. For example, with an APR of 20%, some users will be required to borrow 20% more tokens than others otherwise they wouldn't be able to borrow.

## Proof of Concept
Upon borrowing in `SellCreditMarket`, users specify an `amount` parameter. This parameter would either equal the amount of credits they have to pay to the lender or the amount of tokens they will receive for the borrow (depending on the `exactAmountIn` boolean parameter). Then, upon borrowing, we have this __require__:
```solidity
 if (params.amount < state.riskConfig.minimumCreditBorrowAToken) {
            revert Errors.CREDIT_LOWER_THAN_MINIMUM_CREDIT(params.amount, state.riskConfig.minimumCreditBorrowAToken);
        }

```
If the `amount` parameter is lower than the configured `minimumCreditBorrowAToken`, then the transaction will revert. The issue here is that the `amount` parameter doesn't necessarily equal the credit amount as explained above.

For example, if a user has specified `exactAmountIn` as false, then the `amount` parameter will equal `cashAmountOut` (the tokens he will receive). If the `tenor` is 1 year and the APR is 20% and he has specified 1000 tokens as the `amount` parameter and `exactAmountIn` as false, then he would have to repay 1200 tokens (this is the credit amount, also excluding fees for simplicity purposes). With that information, if the `minimumCreditBorrowAToken` is equal to 1200, this would have to successfully pass as the credit is also 1200. However, since the __require__ statement checks the `amount` parameter the same way regardless of the `exactAmountIn` parameter, then the scenario above would revert as the `amount` parameter is 1000. The user would have to borrow 20% more than the actual minimum and the credit amount would now equal 1440.

Any user who has specified the `amount` parameter as cash instead of credit, will have to pay more than the minimum.

A similar situation is also possible in `BuyCreditMarket` (lending).
## Tools Used
Manual Review
## Recommended Mitigation Steps
There are 2 options. Either incorporate the `exactAmountIn` boolean into the __require__ and properly check the credit amount or another option is to just remove that __require__ statement as the minimum is checked again during the `executeSellCreditMarket()` execution.








## Assessed type

Invalid Validation