xanoutas/solidity-security-findings huggingface dataset:
Fragmentation fees aren't being paid in the context of partial repayment in `Size::compensate`
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#L119-L126


# Vulnerability details

## Impact
Users can use the compensation functionality to partially repay their debt, this can be done by calling `Size::compensate` while passing `creditPositionToCompensateId` as `RESERVED_ID`, [docs](https://docs.size.credit/technical-docs/contracts/3.4-repayment#id-3.4.2-partial-repayment-via-compensation). This splits the credit and the corresponding debt position, into smaller positions, so later on, the user can repay each position by itself.

This contains fragmentation as the credit is being split, however, the logic to charge fragmentation fees in the context of a "partial repayment" with compensation is wrong. In `executeCompensate`, `amountToCompensate` is being compared with `creditPositionToCompensate.credit` to check if there should be fragmentation fees to be paid, but in the case of `RESERVED_ID`, `creditPositionToCompensate` is being created with the same credit as `executeCompensate` in an earlier if block, so `amountToCompensate` will equal `creditPositionToCompensate.credit`, i.e. no fragmentation fees will be paid.

Bypassing fragmentation fees allows borrowers to grief lenders by splitting their credit positions into numerous smaller positions, which forces the lender to claim multiple times upon repayment or liquidation.

## Proof of Concept

```solidity
function test_compensate_no_fragmentation_fees_paid() public {
    _deposit(alice, weth, 100e18);
    _deposit(bob, 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
        })
    );

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

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

    // Credit position's credit is 100 USDC
    assertEq(size.getCreditPosition(creditPositionId).credit, 100e6);

    uint256 feeRecipientBalanceBefore = size
        .data()
        .collateralToken
        .balanceOf(feeRecipient);

    // Alice splits the credit position in two
    vm.prank(alice);
    size.compensate(
        CompensateParams({
            creditPositionWit
Malicious lenders can force borrowers into fragmenting their credit positions
Audit: 2024-06-size-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
Malicious lenders can force borrowers into fragmenting their credit positions

## Proof of Concept
Users can create a loan offer that other users might use to borrow tokens. The borrower might use a credit position he has in order for him to take the borrow. The borrower can also specify whether he wants the `amount` parameter he put as an input to equal `cashAmountOut` or `creditAmountIn` by using the `exactAmountIn` boolean. If he chooses that he wants `amount` to equal `cashAmountOut` (by setting the boolean as false), a malicious lender can force him into fragmenting his credit position and paying extra fees.

This is the code that is responsible for handling the case when the user has specified that he wants the `amount` parameter to equal `cashAmountOut`:
```solidity
else {
            cashAmountOut = params.amount;

            (creditAmountIn, fees) = state.getCreditAmountIn({
                cashAmountOut: cashAmountOut,
                maxCashAmountOut: params.creditPositionId == RESERVED_ID
                    ? cashAmountOut
                    : Math.mulDivDown(creditPosition.credit, PERCENT - state.getSwapFeePercent(tenor), PERCENT + ratePerTenor),
                maxCredit: params.creditPositionId == RESERVED_ID
                    ? Math.mulDivUp(cashAmountOut, PERCENT + ratePerTenor, PERCENT - state.getSwapFeePercent(tenor))
                    : creditPosition.credit,
                ratePerTenor: ratePerTenor,
                tenor: tenor
            });
        }
```
`RESERVED_ID` is used when doing a simple borrow, this is not the case here as the borrower wants to use a credit position he already has. If the `cashAmountOut` and `maxCashAmountOut`, then the borrower will have to pay a fragmentation fee. Even if he carefully inputted data that would not fragment him, the lender can still force him into fragmentation. That is because `maxCashAmountOut` gets computed like this:
```solidity
Math.mulDivDown(creditPosition.credit, PERCENT - state.getSwapFeePercent(tenor), PERCENT + ratePerTenor)
```
The malicious lender has control over 1 of the values here and that is `ratePerTenor`. The `ratePerTenor` value is the interest the borrower has to pay, in simplest terms. The lender can frontrun and change his yield curve and thus, change the `ratePerTenor` value which makes `maxCashAmountOut` a different value than the user expected. There is this check in `validateSellCreditMarket()`:
```solidity
if (apr > params.maxAPR) {
            revert Errors.APR_GREATER_THAN_MAX_APR(apr, params.maxAPR);
        }
```
However, this check does not prevent the issue. Even if the borrower specified the `maxAPR` parameter exactly equal to the expected `apr`, the lender can actually decrease the APR on his yield curve. This will make the check pass as it would ma
Users can deposit USDC above the AToken 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#L40-L42


# Vulnerability details

## Impact
When using `multicall`, the AToken cap is not checked, only the increase of AToken is checked against the decrease of debt. A user can exploit this to deposit USDC above the AToken cap.

## Proof of Concept
By doing these steps in one transaction, a user can deposit USDC above the AToken cap:

0. Already have `X` AToken and a small amount AToken for swapping fee
1. Flashloan ETH and deposit ETH 
2. Sell `X` credit to himself
3. Make a multicall:
   3a. Deposit `X` USDC
   3b. Repay the debt in step 2 (AToken in step 0 should cover the swapping fee)
4. Withdraw ETH and pay back the flashloan

Because the increase of AToken in step 3a is equals to the decrease of debt in step 3b, the check at the end of the multicall is passed. The user can repeat the steps above to deposit more AToken.

## Tools Used
Manual Review.

## Recommended Mitigation Steps
Add a check to cap the `debtToken` at the end of `buyCreditMarket` and `sellCreditMarket`.





## Assessed type

Other
Bad debt cannot be liquidated when contract is paused
Audit: 2024-06-size-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact

Pausing liquidation functions can result in unliquidated bad debt.

## Proof of Concept

### Scenario:
1. A user deposits 1 WETH (assuming WETH is worth $3000).
2. The user then borrows 1900 USDC, resulting in a health factor of 1.57.
3. The protocol is paused by the pauser for some reason.
4. The value of WETH drops to $2000.
5. The user's health factor falls to 1.05, which is below the liquidation threshold of 1.30. However, since the protocol is paused, no one can liquidate the user's position.

## Tools Used

Manual Review

## Recommended Mitigation Steps

Ensure that liquidation functions remain operational even when the protocol is paused.





## Assessed type

Context
Wrong check when `buyCreditMarket` is been called with `param.exactAmountIn` equal to true.
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


# Vulnerability details

When a lender want to match a `borrowOffer` he need to call `buyCreditMarket` this function first do some validation and then execute the loan. In this cases we going to check just the validation part since is what concern in this report:

```solidity

 function validateBuyCreditMarket(State storage state, BuyCreditMarketParams calldata params) external view {
        address borrower;
        uint256 tenor;

        // validate creditPositionId
        if (params.creditPositionId == RESERVED_ID) {
          ...
            }
        } else {
           ...
           
        }

       ...

        // validate amount
        if (params.amount < state.riskConfig.minimumCreditBorrowAToken) {
            revert Errors.CREDIT_LOWER_THAN_MINIMUM_CREDIT(params.amount, state.riskConfig.minimumCreditBorrowAToken); <------
        }

     ...
    }

```
[[Link]](https://github.com/code-423n4/2024-06-size/blob/8850e25fb088898e9cf86f9be1c401ad155bea86/src/libraries/actions/BuyCreditMarket.sol#L91)

As you can see the arrow above is checking if the `amount` is min than the `minimumCreditBorrowAToken` which is the minimum amount that a credit can be open. the problem is that this amount can be no only the value that a credit can be open (`creditAmountOut`) but can be also the amount that the borrower gonna receive (`cashAmountIn`) that depend on the value passing in `param.exactAmountIn`.



## Impact
The problem with this is that since this check is been doing wrongly when `param.exactAmountIn` equal to true. users trying to do the loan in this way may experiment a minimum cash amount that is more than it should be.

Note that this issue is `sellCreditMartket` function too, but in this report is just showing the error in `BuyCreditMarket`.

## Proof of Concept

We can see in the in the arrow number three below that the `creditAmountOut` is the one used to open the credit. And you can see in the arrow number one that param.amount can be used no only to the value that credit will be open but also with the amoun that will be send to the borrowe.

```solidity
 function executeBuyCreditMarket(State storage state, BuyCreditMarketParams memory params)
        external
        returns (uint256 cashAmountIn)
    {
    
      ...
        if (params.creditPositionId == RESERVED_ID) {
          ...
        } else {
          ...
        }

    ...

        uint256 creditAmountOut;
        uint256 fees;

        if (params.exactAmountIn) {
            cashAmountIn = params.amount; <--------
            (creditAmountOut, fees) = state.getCreditAmountOut({
                cashAmountIn: cashAmountIn,
                maxCashAmountIn: params.creditPositionId == RESERVED_ID
                    ? cashAmountIn
                    : Math.mulDivUp(creditPosition.credit, PERCENT, PERCENT + ratePerTenor),
The `borrowATokenCap` can be bypassed
Audit: 2024-06-size-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

The borrowAtoken have a cap to restrict the amount that can be minted, this can be seen in the  `state.riskConfig.borrowATokenCap`, we can see the check in the `executeDeposit` Function (see the arrow below):

```solidity

function executeDeposit(State storage state, DepositParams calldata params) public {
        address from = msg.sender;
        uint256 amount = params.amount;
        ...

        if (params.token == address(state.data.underlyingBorrowToken)) {
            state.depositUnderlyingBorrowTokenToVariablePool(from, params.to, amount);
            // borrow aToken cap is not validated in multicall,
            //   since users must be able to deposit more tokens to repay debt
            if (!state.data.isMulticall) {
                state.validateBorrowATokenCap(); <---------
            }
        } else {
            state.depositUnderlyingCollateralToken(from, params.to, amount);
        }

        emit Events.Deposit(params.token, params.to, amount);
    }

```
[[Link]](https://github.com/code-423n4/2024-06-size/blob/8850e25fb088898e9cf86f9be1c401ad155bea86/src/libraries/actions/Deposit.sol#L81).

Another point to understand the vuln is that the contract is not checking for the cap when the deposit is been made it trough the multicall functions as the comment say `borrow aToken cap is not validated in multicall, since users must be able to deposit more tokens to repay debt`.

With that been said an attacker can create a BuyCreditOrder limit  take his own offer, then repaid and deposit in a multicall tx successfully bypassing the borrow token cap. see Proof of concept.


## Impact
An attacker can successfully bypass the  `borrowATokenCap`

## Proof of Concept
Run the next PoC in Multicall.t.sol:

```solidity

 function test_Multicall_multicall_bypassing_cap() public {
        //@note poc (medium 7)
        _setPrice(1e18);
        uint256 amount = 100e6;
        uint256 cap = amount + size.getSwapFee(100e6, 365 days);
        _updateConfig("borrowATokenCap", cap);

        _deposit(alice, usdc, cap);
        _deposit(alice, weth, 200e18);

        _buyCreditLimit(alice, block.timestamp + 365 days, YieldCurveHelper.pointCurve(365 days, 0.1e18));//create a limit order
        uint256 debtPositionId = _sellCreditMarket(alice, alice, RESERVED_ID, cap, 365 days, false);// alice take his own limit order

        _mint(address(usdc), alice, 100e6);
        _approve(alice, address(usdc), address(size), 100e7);

        bytes[] memory data = new bytes[](2);
        data[0] = abi.encodeCall(size.deposit, DepositParams({token: address(usdc), amount: 100e6, to: alice}));
        data[1] = abi.encodeCall(size.repay, RepayParams({debtPositionId: debtPositionId}));

        vm.prank(alice); //alice repay his own position and despoti more usdc
        size.multicall(data);
Users can avoid paying fragmentation fees
Audit: 2024-06-size-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
Users can avoid paying fragmentation fees

## Proof of Concept
Users can use credit positions to take out loans. If they take an amount that splits their credit position, then they owe a fragmentation fee. The issue is that this can be avoided as users can use `compensate()` to split credit positions without a fee. They can split the position in such a way that whenever they input the amount they want to lend/borrow, that amount would be equal to the credit of the position and thus, they would avoid paying fragmentation fees.

For the POC, create an Event log in `BuyCreditMarket.sol` and add 2 events like shown below (on the 2 last lines):
```solidity
if (params.exactAmountIn) {
            cashAmountIn = params.amount;
            (creditAmountOut, fees) = state.getCreditAmountOut({
                cashAmountIn: cashAmountIn,
                maxCashAmountIn: params.creditPositionId == RESERVED_ID
                    ? cashAmountIn
                    : Math.mulDivUp(creditPosition.credit, PERCENT, PERCENT + ratePerTenor),
                maxCredit: params.creditPositionId == RESERVED_ID
                    ? Math.mulDivDown(cashAmountIn, PERCENT + ratePerTenor, PERCENT)
                    : creditPosition.credit,
                ratePerTenor: ratePerTenor,
                tenor: tenor
            });
        } else {
            creditAmountOut = params.amount;
            (cashAmountIn, fees) = state.getCashAmountIn({
                creditAmountOut: creditAmountOut,
                maxCredit: params.creditPositionId == RESERVED_ID ? creditAmountOut : creditPosition.credit,
                ratePerTenor: ratePerTenor,
                tenor: tenor
            });
            emit Log(creditAmountOut);
            emit Log(params.creditPositionId == RESERVED_ID ? creditAmountOut : creditPosition.credit);
        }
```
If these values are equal, then that means the position did not get fragmented and the user doesn't have to pay fragmentation fees. If they are not, the user has to pay fragmentation fees.
Paste the following POC into `Compensate.t.sol` (use `-vvvv` to see the events and you can also comment out the lines where Alice is compensating to see the difference in events):
```solidity
function testUserCanAvoidPayingFragmentationFees() public {
        _setPrice(1e18);

        _deposit(bob, usdc, 100e6);
        _deposit(alice, weth, 150e18);
        _deposit(alice, usdc, 100e6);

        _buyCreditLimit(bob, type(uint256).max, YieldCurveHelper.pointCurve(1 hours, 0));
        uint256 debtPositionId = _sellCreditMarket(alice, bob, RESERVED_ID, 100e6, 1 hours, true);
        uint256 creditPositionId = size.getCreditPositionIdsByDebtPositionId(debtPositionId)[0];

        _sellCreditLimit(bob, YieldCurveHelper.pointCurve(1 hours, 0));

        vm.prank(alice);
Whales might not be able to get liquidated due to no way to partially liquidate
Audit: 2024-06-size-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
Whales might not be able to get liquidated due to no way to partially liquidate

## Proof of Concept
The `liquidate()` function allows users to liquidate other users that are either underwater or their payment is due.

The issue is that for someone to liquidate another user, he has to pay `debtPosition.futureValue` which is essentially the amount owed by the liquidatable user. Since there is no mechanism for partial liquidations, the liquidator must have that amount at his disposal in order to liquidate the borrower. However, the position might be huge and thus, liquidators might not have enough funds making the position non-liquidatable. Even worse, `futureValue` is equal to the amount received by the borrower + interest based on the APR. This means that even the whale might not have funds to cover the position, let alone other liquidators.

## Tools Used
Manual Review
## Recommended Mitigation Steps
Implement a way for users to liquidate others partially


## Assessed type

Other
Debt repayment and collateral loss risk if Size contract is paused
Audit: 2024-06-size-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
Users can’t repay their debts if the Size contract is paused which can cause users to fall into liquidation and lose their collateral.

## Proof of Concept
The [Size::repay()](https://github.com/code-423n4/2024-06-size/blob/8850e25fb088898e9cf86f9be1c401ad155bea86/src/Size.sol#L198-L201) function has implemented the `whenNotPaused` modifier, which will prevent the function from being used if the contract is paused. The problem is that the usage of this function should not be prevented because if users are unable to repay their debts, their accounts can fall into liquidation status while the Size contract is paused, and once the contract is unpaused, and liquidations are enabled too, if the account felt into liquidation status, now the users and liquidators will be in a mev run to either repay the debt or liquidate the collateral.
```solidity
    function repay(RepayParams calldata params) external payable override(ISize) whenNotPaused {
        state.validateRepay(params);
        state.executeRepay(params);
    }
```
This presents an unnecessary risk to users by preventing them from repaying their debts.

## Tools Used
Manual Review

## Recommended Mitigation Steps
The mitigation is very straight forward, don’t disable the borrower’s repayments, and don’t interrupt the repayments. Remove the `whenNotPaused` modifier for `repay()` function.


## Assessed type

Other
BuyCreditMarket: Missing Fee Deduction from Buyer Payment (exactAmountIn=true)
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#L195-L196
https://github.com/code-423n4/2024-06-size/blob/8850e25fb088898e9cf86f9be1c401ad155bea86/src/libraries/actions/SellCreditMarket.sol#L201-L203


# Vulnerability details

## Impact
**1. `SellCreditMarket` applies the swap fee consistently**
   - When calculating `cashAmountOut` in both `exactAmountIn` and `exactAmountOut` scenarios, it considers the swap fee.
   - It explicitly subtracts the calculated fees from the `cashAmountOut` transferred to the seller.

**2. `BuyCreditMarket` lacks explicit fee handling for the buyer**
   - While it calculates fees in both `exactAmountIn` and `exactAmountOut` scenarios, it doesn't explicitly deduct these fees from the buyer's provided funds.
   - The buyer sends `cashAmountIn` regardless of the fee, potentially overpaying.

## Proof of Concept
**1. Overpayment by Buyers**
[// src\libraries\actions\BuyCreditMarket.sol](https://github.com/code-423n4/2024-06-size/blob/8850e25fb088898e9cf86f9be1c401ad155bea86/src/libraries/actions/BuyCreditMarket.sol#L195-L196)
```solidity
function executeBuyCreditMarket(...) external returns (uint256 cashAmountIn) {
    // ... (calculations for cashAmountIn, creditAmountOut, fees)

    // Funds transferred WITHOUT deducting fees:
    state.data.borrowAToken.transferFrom(msg.sender, borrower, cashAmountIn - fees); 
    state.data.borrowAToken.transferFrom(msg.sender, state.feeConfig.feeRecipient, fees);
}
```
* **Example:**
    - Alice wants to buy 100 USDC worth of credit.
    - The calculated swap fee is 1 USDC.
    - Alice approves the protocol to spend 100 USDC.
    - Due to the bug, Alice pays the full 100 USDC, but only receives 99 USDC worth of credit. The remaining 1 USDC is correctly allocated as a fee.

* **Impact:** Buyers are consistently overpaying for credit, eroding their returns and potentially discouraging participation in the protocol.

**2. Inconsistent Fee Distribution**
[// src\libraries\actions\SellCreditMarket.sol](https://github.com/code-423n4/2024-06-size/blob/8850e25fb088898e9cf86f9be1c401ad155bea86/src/libraries/actions/SellCreditMarket.sol#L201-L203)
```solidity
function executeSellCreditMarket(...) external returns (uint256 cashAmountOut) {
    // ... (calculations for cashAmountOut, creditAmountIn, fees)

    // Fees deducted BEFORE transferring to the seller:
    state.data.borrowAToken.transferFrom(params.lender, msg.sender, cashAmountOut); 
    state.data.borrowAToken.transferFrom(params.lender, state.feeConfig.feeRecipient, fees);
}
```
**Comparison:**
    - Sellers have their fees deducted before receiving their payout, ensuring they bear the cost.
    - Buyers, on the other hand, pay the full amount upfront, even though a portion is allocated as a fee.

* **Impact**: This inconsistency creates an imbalance in the fee structure, unfairly disadvantaging buyers and potentially distorting the