xanoutas/solidity-security-findings huggingface dataset:
When `sellCreditMarket()` is called to sell credit for a specific cash amount, the protocol might receive a lower swapping fee than expected.
Audit: 2024-06-size-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-06-size/blob/main/src/libraries/AccountingLibrary.sol#L249
https://github.com/code-423n4/2024-06-size/blob/main/src/libraries/AccountingLibrary.sol#L256


# Vulnerability details

## Impact
The protocol might receive a lower fee than expected when `sellCreditMarket()` is called to sell credit for a specific cash amount.

## Proof of Concept
The protocol allows a user to sell their `credit` for `szaUSDC`, which can be used to redeem USDC thereafter.
- If a specific amount of `credit` is sold for `szaUSDC`, `sellCreditMarket()` will calcualte the amount of `szaUSDC` received and `fees` paid to `feeReceipient` 
- If `credit` is sold for a specific amount of `szaUSDC`, `sellCreditMarket()` will calcualte the amount of `credit` sold and `fees` paid to `feeReceipient` 

The calculation should follow below rules:
```math
\begin{align*}
fee_{fragment} &=
  \begin{cases}
    feeConfig.fragmentationFee       & \quad \text{if } credit \text{  is fractionalized}\\
    0  & \quad \text{otherwise}
  \end{cases} \\
cash_{total} &= \frac{credit_{sold}}{1 + rate_{tenor}} &(1)\\
fee_{swap} &= cash_{total} * feeRate_{tenor} &(2)\\
cash_{out} &= cash_{total} - fee_{swap} - fee_{fragment} &(3)
\end{align*}
```
We can verify [the fee samples](https://docs.size.credit/technical-docs/contracts/2.3-fees)  with above formulas:

**Example 1**: Bob owned 120 `credit` and sell 120 `credit` to Candy for `szaUSDC`:
```math
\begin{align*}
credit_{sold} &= 120 \\
fee_{fragment} &= 0 \\
rate_{tenor} &= 50\% \\
feeRate_{tenor} &= 1\% \\
\end{align*}
```
$cash_{total}$, $fee_{swap}$ and $cash_{out}$ can be calculated as below:
```math
\begin{align*}
cash_{total} &=  \frac{credit_{sold}}{1 + rate_{tenor}} &(1)\\
&= \frac{120}{1 + 50\%} \\
&= 80 \\
\\
fee_{swap} &= cash_{total} * feeRate_{tenor} &(2)\\
&= 80 * 1\%\\
&= 0.8\\
\\
cash_{out} &= cash_{total} - fee_{swap} - fee_{fragment} &(3)\\
&= 80 - 0.8 - 0 \\
&= 79.2\\
\end{align*}
```
The all results of above calculation are same as [Example 1](https://docs.size.credit/technical-docs/contracts/2.3-fees)

**Example 2** : Bob owned 120 `credit` and sell `credit` to Candy for 50 `szaUSDC`:
```math
\begin{align*}
fee_{fragment} &= 5 \\
rate_{tenor} &= 50\% \\
feeRate_{tenor} &= 1\% \\
cash_{out} &= 50 \\
\end{align*}
```
$credit_{sold}$ and $fee_{swap}$ can be calculated as below:
```math
\begin{align*}
fee_{swap} &= cash_{total} * feeRate_{tenor} &(2)\\
cash_{out} &= cash_{total} - fee_{swap} - fee_{fragment}  &(3)\\
&= cash_{total} - cash_{total} * feeRate_{tenor} - fee_{fragment} \\
\Downarrow \\
cash_{total} &= \frac{cash_{out} + fee_{fragment}}{1 - feeRate_{tenor}} \\
\\
cash_{total} &= \frac{credit_{sold}}{1 + rate_{tenor}} &(1)\\
\Downarrow \\
credit_{sold} &= cash_{total} * (1 + rate_{tenor}) \\
&= \frac{(cash_{out} + fee_{fragment}) * (1 + rate_{tenor})}{1 - feeRate_{tenor}} &(4)\\
&= \frac{(50 + 5) * (1 + 50\%)}{1 - 1\%} \\
&= 83.33 \\
\\
fee_{swap} &= cash_{total} * feeRate_
Invalid CR validation in `Size::compensate`, blocking users from compensating their loans
Audit: 2024-06-size-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
Users can use their credit to compensate for some of their debt, this is done by calling `Size::compensate`. Compensate sometimes contains collateral transfers, however, it only happens if fragmentation happens on a credit position. But the protocol is checking if the user is liquidatable on every compensate TX, even if it doesn't include collateral transfers.

If a liquidatable borrower (has >1 loan), tries to compensate a loan with an exact credit position he owns, but he's unable to compensate 1 loan because he's still liquidatable with the remaining loan, this is because it's checking the user's CR, knowing that no fragmentation happened.

So a user won't be able to compensate his loan because of an invalid CR check, where it shouldn't be triggered as no fragmentation happened.

## Proof of Concept

The below test how the borrower is blocked from compensating his loan. To confirm that this works as expected, temporarily remove `state.validateUserIsNotUnderwater(msg.sender);` from `Size::compensate`, and verify that the below test doesn't revert.

```solidity
function test_borrower_cant_compensate() public {
    _deposit(bob, weth, 100e18);
    _deposit(bob, usdc, 5_000e6);
    _deposit(candy, weth, 100e18);
    _deposit(candy, usdc, 500e6);
    _deposit(alice, weth, 0.5e18);
    _deposit(alice, usdc, 100e6);

    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;

    // Alice creates a limit order
    vm.prank(alice);
    size.sellCreditLimit(
        SellCreditLimitParams({
            curveRelativeTime: YieldCurve({
                tenors: tenors,
                marketRateMultipliers: marketRateMultipliers,
                aprs: aprs
            })
        })
    );

    // Bob creates a limit order
    vm.prank(candy);
    size.sellCreditLimit(
        SellCreditLimitParams({
            curveRelativeTime: YieldCurve({
                tenors: tenors,
                marketRateMultipliers: marketRateMultipliers,
                aprs: aprs
            })
        })
    );

    // Bob buys credit from Alice (lends to Alice)
    vm.prank(bob);
    size.buyCreditMarket(
        BuyCreditMarketParams({
            borrower: alice,
            creditPositionId: type(uint256).max,
            amount: 50e6,
            tenor: 365 days,
            deadline: block.timestamp,
            minAPR: 0,
            exactAmountIn: true
        })
    );
    uint256 creditPositionId_1 = type(uint256).max / 2;
    uint256 debtPositionId_1 = 0;

    // Candy buys credit from Alice (lends to Alice)
    vm.prank(candy);
    size.buyCreditMarket(
        BuyCreditMarketParams({
            borrower: alice,
            creditPositionId: type(uint256).max,
Invalid CR validation in `Size::withdraw`, blocking liquidatable users from withdrawing their borrow tokens
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 withdraw their deposited tokens by calling `Size::withdraw`, which withdraws both the underlying collateral and borrow tokens, according to the user's input. At the end of the withdrawal, it checks if the user CR is above a certain limit to make sure he's not withdrawing collateral that puts him underwater. However, the withdrawal function checks the CR even when withdrawing the underlying borrow tokens.

This poses an issue, where if a user is liquidatable and has some underlying borrow tokens deposited he won't be able to withdraw those. In theory, he should be able to do so, because these underlying borrow tokens don't contribute to the user's CR.

As a result, users won't be able to withdraw their deposited underlying borrow tokens.

## Proof of Concept

```solidity
function test_borrower_cant_withdraw_borrow_tokens() public {
    _deposit(bob, weth, 100e18);
    _deposit(bob, usdc, 5_000e6);

    _deposit(alice, weth, 0.5e18);
    _deposit(alice, usdc, 100e6);

    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;

    // Alice creates a limit order
    vm.prank(alice);
    size.sellCreditLimit(
        SellCreditLimitParams({
            curveRelativeTime: YieldCurve({
                tenors: tenors,
                marketRateMultipliers: marketRateMultipliers,
                aprs: aprs
            })
        })
    );

    // Bob buys credit from Alice (lends to Alice)
    vm.prank(bob);
    size.buyCreditMarket(
        BuyCreditMarketParams({
            borrower: alice,
            creditPositionId: type(uint256).max,
            amount: 300e6,
            tenor: 365 days,
            deadline: block.timestamp,
            minAPR: 0,
            exactAmountIn: true
        })
    );

    // Collateral price drops
    _setPrice(1_000e18);

    uint256 aliceCR = size.collateralRatio(alice);
    uint256 crOpening = size.riskConfig().crOpening;

    // Alice is unable to withdraw borrow tokens
    vm.prank(alice);
    vm.expectRevert(
        abi.encodeWithSelector(
            Errors.CR_BELOW_OPENING_LIMIT_BORROW_CR.selector,
            alice,
            aliceCR,
            crOpening
        )
    );
    size.withdraw(
        WithdrawParams({amount: 50e6, token: address(usdc), to: alice})
    );
}
```

## Tools Used

Manual review

## Recommended Mitigation Steps

Instead of checking for the user's CR on every withdrawal, only do it when withdrawing the underlying collateral token, by having something like the following in `Size::withdraw`:
```
if (params.token == address(state.data.underlyingCollateralToken)) {
    state.validateUserIsNotUnderwater(msg.sender);
}
```





## Assessed type

Invalid Validation
Users can not withdraw USDC when they are below the opening limit borrow CR
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
In current code, users can not withdraw USDC when they are below the opening limit borrow CR. This might not be an optimal design for users, because withdrawing USDC does not change CR.

Consider this scenario, Alice deposited ETH to be able to borrow USDC. She borrowed USDC to minimum CR (150%), due to price fluctuation, her CR became lower than opening limit borrow, now she can not withdraw her USDC. 

Another scenario, Bob saw his debt could become liquidatable, so he wanted to withdraw USDC to swap into ETH to add more collateral, but he can not withdraw USDC because the CR is below 150%.

## Tools Used
Manual Review.

## Recommended Mitigation Steps
Only validate the CR when users withdraw WETH.

`Size.sol`

```diff
    function withdraw(WithdrawParams calldata params) external payable override(ISize) whenNotPaused {
        state.validateWithdraw(params);
        state.executeWithdraw(params);
-       state.validateUserIsNotBelowOpeningLimitBorrowCR(msg.sender);
    }
```

`libraries/actions/Withdraw.sol`

```diff
    function executeWithdraw(State storage state, WithdrawParams calldata params) public {
        uint256 amount;
        if (params.token == address(state.data.underlyingBorrowToken)) {
            amount = Math.min(params.amount, state.data.borrowAToken.balanceOf(msg.sender));
            if (amount > 0) {
                state.withdrawUnderlyingTokenFromVariablePool(msg.sender, params.to, amount);
            }
        } else {
            amount = Math.min(params.amount, state.data.collateralToken.balanceOf(msg.sender));
            if (amount > 0) {
                state.withdrawUnderlyingCollateralToken(msg.sender, params.to, amount);
            }
+           state.validateUserIsNotBelowOpeningLimitBorrowCR(msg.sender);
        }
```


## Assessed type

Invalid Validation
Users will not be able to withdraw their USDC in some cases
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
Users will not be able to withdraw their USDC in some cases

## Proof of Concept
Whenever a user tries to withdraw from the protocol, there is this check:
```solidity
function validateUserIsNotBelowOpeningLimitBorrowCR(State storage state, address account) external view {
        uint256 openingLimitBorrowCR = Math.max(
            state.riskConfig.crOpening,
            state.data.users[account].openingLimitBorrowCR // 0 by default, or user-defined if SetUserConfiguration has been used
        );
        if (collateralRatio(state, account) < openingLimitBorrowCR) {
            revert Errors.CR_BELOW_OPENING_LIMIT_BORROW_CR(account, collateralRatio(state, account), openingLimitBorrowCR);
        }
}
```
The function's goal is to validate that the user is not leaving himself under the `crOpening`. For example, if a user tries to withdraw his collateral when he has borrowed some tokens, this function would revert as that makes his position insolvent. The issue is that this function is called upon withdrawing both USDC (the borrow token) and ETH (the collateral token):
```solidity
function withdraw(WithdrawParams calldata params) external payable override(ISize) whenNotPaused {
        state.validateWithdraw(params);
        state.executeWithdraw(params);
        state.validateUserIsNotBelowOpeningLimitBorrowCR(msg.sender);
    }
```
This disallows users who have USDC and are below the `crOpening` to withdraw. This creates a few possible issues.

Situation A: Alice has deposited 1 ETH (price of 1 ETH is 1500\$ for the example) and borrows 1000\$, now her collateral ratio is equal to `crOpening` (1.5e18 according to deployment scripts). Price of ETH drops to 1499.99\$ and she won't be able to withdraw due to the check.

Situation B: Alice has deposited 1 ETH and borrows 1000\$, she has also deposited USDC and has lent it out. A month later, she receives her USDC back with interest from the borrower and tries to withdraw it. In the meantime, her collateral ratio has dropped to 1.4e18 and she is not able to.

While this check is effective and should be there, it should not disallow users from withdrawing their USDC. It should only disallow users to withdraw their ETH as that is the token responsible for their collateral ratio.

Paste the following POC into `Withdraw.t.sol`:
```solidity
function testUserWillNotBeAbleToWithdrawUsdc() public {
        _setPrice(1e18);
        _deposit(bob, usdc, 1000e6);
        _deposit(alice, weth, 1500e18);

        _buyCreditLimit(bob, type(uint256).max, YieldCurveHelper.pointCurve(1 hours, 0));
        _sellCreditMarket(alice, bob, RESERVED_ID, 1000e6, 1 hours, true);

        _setPrice(1e18 - 1);
        
        vm.expectRevert(abi.encodeWithSelector(Errors.CR_BELOW_OPENING_LIMIT_BORROW_CR.selector, alice, size.collateralRatio(alice), 1.5e18));
        _wit
The validateUserIsNotBelowOpeningLimitBorrowCR in withdraw can lead to DOS
Audit: 2024-06-size-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
After a user withdraws assets from the platform, the system checks `validateUserIsNotBelowOpeningLimitBorrowCR`. This can prevent the user from withdrawing `underlyingBorrowToken` when the current CR is between `crOpening` and `crLiquidation`.

## Proof of Concept
First let's check the definition of CR:
```solidity
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;
    }
}
```
From the above code we can see cr is only related to 2 variables, `collateralToken` and `debtToken`.

Let's back to `withdraw`:

```solidity
function executeWithdraw(State storage state, WithdrawParams calldata params) public {
    uint256 amount;
    if (params.token == address(state.data.underlyingBorrowToken)) {
        amount = Math.min(params.amount, state.data.borrowAToken.balanceOf(msg.sender));
        if (amount > 0) {
            state.withdrawUnderlyingTokenFromVariablePool(msg.sender, params.to, amount);
        }
    } else {
        amount = Math.min(params.amount, state.data.collateralToken.balanceOf(msg.sender));
        if (amount > 0) {
            state.withdrawUnderlyingCollateralToken(msg.sender, params.to, amount);
        }
    }

    emit Events.Withdraw(params.token, params.to, amount);
}
```
From above code we can see user can withdraw 2 assets , `underlyingBorrowToken(USDC)` , `underlyingCollateralToken(WETH)`. Regardless of which asset the user withdraws, the CR will be checked.

Thus when user's CR is between crOpening and crLiquidation user is unable to withdraw `underlyingBorrowToken(USDC)` even if user's CR is higher than crLiquidation.

test:
```soldiity
    function test_buyCreditLimit_Custom() public {
        _setPrice(1e18);
        _deposit(alice, usdc, 1_000e6);

        _deposit(bob, weth, 300e18);
        _deposit(bob, usdc, 1_000e6);

        uint256 maxDueDate = block.timestamp + 365 days;
        uint256[] memory marketRateMultipliers = new uint256[](2);
        uint256[] memory tenors = new uint256[](2);
        tenors[0] = 30 days;
        tenors[1] = 60 days;
        int256[] memory aprs = new int256[](2);
        aprs[0] = 0.15e18;
        aprs[0] = 0.12e18;

        vm.prank(alice);
        size.buyCreditLimit(
            BuyCreditLimitParams({
                maxDueDate: maxDueDate,
                curveRelativeTime: YieldCurve({tenors: tenors, marketRateMultipliers: marketRateMultipliers, aprs: aprs})
            })
        );

        //alice sell cred
Users cant withdraw borrowATokens if they are under the opening collateral limit
Audit: 2024-06-size-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
Withdrawing borrow tokens should not be impacted by whether a user is under the opening collateral limit or not, as borrow tokens are not used for collateral, but in the `Size::withdraw` a revert will happen if this is this case and DOS the borrowAToken withdrawal. The borrowATokens have nothing to do with collaterilization and the user could want to withdraw to swap to ETH and increase their collateral ratio

## Proof of Concept
In this function snippet here, when a user makes a withdrawal request if they are under the opening collateral ratio, it will  DOS the withdrawal as shown in the snippets below 

```solidity
function withdraw(WithdrawParams calldata params) external payable override(ISize) whenNotPaused {
        state.validateWithdraw(params);
        state.executeWithdraw(params);
        state.validateUserIsNotBelowOpeningLimitBorrowCR(msg.sender);
    }
```
`Size::validateUserIsNotBelowOpeningLimitBorrowCR`
```solidity
 function validateUserIsNotBelowOpeningLimitBorrowCR(State storage state, address account) external view {
        uint256 openingLimitBorrowCR = Math.max(
            state.riskConfig.crOpening,
            state.data.users[account].openingLimitBorrowCR // 0 by default, or user-defined if SetUserConfiguration has been used
        );
        if (collateralRatio(state, account) < openingLimitBorrowCR) {
            revert Errors.CR_BELOW_OPENING_LIMIT_BORROW_CR(
                account, collateralRatio(state, account), openingLimitBorrowCR
            );
        }
    }
```

## Tools Used
Manual Review

## Recommended Mitigation Steps
Since the protocol wants to implement the withdrawal sequence of both tokens together, they should use the `param.token` to make a conditional statement to know when collateralTokens are being withdrawn before making the check `validateUserIsNotBelowOpeningLimitBorrowCR` same as done in the executeWihdraw of the `Withdraw::executeWithdraw`


## Assessed type

DoS
Users can not withdraw and use their borrowed USDC tokens if their collateral ratio is below openingLimitBorrow CR
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

## Description
Users can call ```withdraw()``` function either because they want to get out their collateral ```szETH``` to ```WETH``` or because they want to withdraw the ```szaUSDC``` they borrowed and make them ```USDC``` so they can be indeed used in the broader Ethereum ecosystem. However, after every ```withdraw()``` call, there is a check that validates that the collateral ratio of the sender is not below the openingLimitBorrow CR which is by default 1.5 or bigger if user has set it. This check makes sense for ```szETH``` withdrawals since such a call will make user dangerously closer to liquidation. However, a user when he is borrow an amount of money should be able to withdraw them, so to actually use them if even if he is below the openeningLimitBorrow CR, as long as he is not liquidatable. In this way, the protocol prevents a healthy user from using the money he borrowed and payed fees for them for no reason.

## Impact
As a result of this vulnerability, healthy users can not withdraw the funds that they borrowed and actually use them, even if their collateral ratio is healthy.

## Proof of Concept
To understand this scenario better, consider the following example :
1. Alice has 1 szETH (3500 USDC) collateral and take a loan of 2300 szaUSDC (2400 USDC) so her collateral ratio is 1.52.
2. Some time pass and ETH loses value against USDC so the collateral ratio of Alice becomes 1.48.
3. Now, Alice tries to withdraw the szaUSDC she borrowed to make them USDC and use them for the reason she borrowed them but she can't, even if her collateral ratio is healthy and she is not liquidatable.
4. Now, Alice has to deposit more collateral szETH just to surpuss 1.5 or waits for an increase in the price of ETH against USDC.

## Tools Used
Manual review

## Recommended Mitigation Steps
Consider not allowing users withdrawing their ```szaUSDC``` only if they are liquidatable since they have to return them ASAP.


## Assessed type

Context