xanoutas/solidity-security-findings huggingface dataset:
Incorrect checked address in `CapsLibrary.validateVariablePoolHasEnoughLiquidity` causes critical functions to be in a DoS state
Audit: 2024-06-size-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-06-size/blob/8850e25fb088898e9cf86f9be1c401ad155bea86/src/libraries/CapsLibrary.sol#L67-L72


# Vulnerability details

## Impact
```
    function validateVariablePoolHasEnoughLiquidity(State storage state, uint256 amount) public view {
        uint256 liquidity = state.data.underlyingBorrowToken.balanceOf(address(state.data.variablePool));
        if (liquidity < amount) {
            revert Errors.NOT_ENOUGH_BORROW_ATOKEN_LIQUIDITY(liquidity, amount);
        }
    }
```

The `validateVariablePoolHasEnoughLiquidity()` function in the `CapsLibrary` contract is used by `buyCreditMarket()`, `sellCreditMarket()`, and `liquidateWithReplacement()` to verify that there is sufficient liquidity in AAVE for cash withdrawal.

However, the function incorrectly reads the USDC balance from the `variablePool` as `liquidity`. The actual liquidity is stored in the `aUSDC` token. This can be confirmed by checking the USDC balance of the following addresses on Ethereum at block height 20204585:
- [USDC](https://etherscan.io/address/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48#code) balance of [Aave Pool V3](https://etherscan.io/address/0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2): 0
- USDC balance of [Aave Ethereum USDC](https://etherscan.io/address/0x98C23E9d8f34FEFb1B7BD6a91B7FF122F4e16F5c): 194417877093548

Consequently, after the mainnet deployment of the Size codebase, `validateVariablePoolHasEnoughLiquidity()` will use 0 as `liquidity` and thus revert. This would cause `buyCreditMarket()` and `sellCreditMarket()`, the two main  functions, to enter a denial-of-service (DoS) state.

Simply put, the Size protocol will be completely non-functional due to this issue.

## Proof of Concept
```solidity
// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;

import {BaseTest} from "@test/BaseTest.sol";
import {USDC} from "@test/mocks/USDC.sol";
import {WETH} from "@test/mocks/WETH.sol";

contract PoC is BaseTest {
    function setUp() public override {
        vm.createSelectFork("eth");
        vm.rollFork(20174926);

        weth = WETH(payable(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2));
        usdc = USDC(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48);

        setupProduction({
            _owner: address(this),
            _feeRecipient: address(this),
            _weth: 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2,
            _usdc: 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48,
            _variablePool: 0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2,
            _wethAggregator: 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419,
            _usdcAggregator: 0x8fFfFfd4AfB6115b954Bd326cbe7B4BA576818f6,
            _sequencerUptimeFeed: address(0)
        });

        _labels();
    }

    function test_it() public {
        _deposit(alice, weth, 100e18);
        _deposit(alice, usdc, 100e6);
        _deposit(bob, weth, 100e18);
        _deposit(bob, usdc, 100e6);
        _sellCreditLimit(alice, 0.03e18, 365 days);
Bad amount validation could lead to DoS, when `buyCreditMarket` called
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
Core function DoS

## Proof of Concept
1. When user execute `Size::buyCreditMarket` with params.exactAmountIn = true, it means that user wants to give params.amount amount of `cash` for credit. 
2. Through the execution of `BuyCreditMarket::validateBuyCreditMarket` params.amount is validated as function reverts if `params.amount < state.riskConfig.minimumCreditBorrowAToken`, no matter params.exactAmountIn's value.  
```solidity
 function validateBuyCreditMarket(State storage state, BuyCreditMarketParams calldata params) external view {
        address borrower;
        uint256 tenor;

        // validate creditPositionId
        if (params.creditPositionId == RESERVED_ID) {
            borrower = params.borrower;
            tenor = params.tenor;

            // validate tenor
            if (tenor < state.riskConfig.minTenor || tenor > state.riskConfig.maxTenor) {
                revert Errors.TENOR_OUT_OF_RANGE(tenor, state.riskConfig.minTenor, state.riskConfig.maxTenor);
            }
        } else {
            CreditPosition storage creditPosition = state.getCreditPosition(params.creditPositionId);
            DebtPosition storage debtPosition = state.getDebtPositionByCreditPositionId(params.creditPositionId);
            if (!state.isCreditPositionTransferrable(params.creditPositionId)) {
                revert Errors.CREDIT_POSITION_NOT_TRANSFERRABLE(
                    params.creditPositionId,
                    state.getLoanStatus(params.creditPositionId),
                    state.collateralRatio(debtPosition.borrower)
                );
            }
            User storage user = state.data.users[creditPosition.lender];
            if (user.allCreditPositionsForSaleDisabled || !creditPosition.forSale) {
                revert Errors.CREDIT_NOT_FOR_SALE(params.creditPositionId);
            }

            borrower = creditPosition.lender;
            tenor = debtPosition.dueDate - block.timestamp; // positive since the credit position is transferrable, so the loan must be ACTIVE
        }

        BorrowOffer memory borrowOffer = state.data.users[borrower].borrowOffer;

        // validate borrower
        if (borrowOffer.isNull()) {
            revert Errors.INVALID_BORROW_OFFER(borrower);
        }

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

        // validate deadline
        if (params.deadline < block.timestamp) {
            revert Errors.PAST_DEADLINE(params.deadline);
        }

        // validate minAPR
        uint256 apr = borrowOffer.getAPRByTenor(
            VariablePoolBorrowRateParams({
                variablePoolBorrowRate: state.oracle.v
Inconsistent Cash Amount Out Calculation in Existing 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#L171-L181


# Vulnerability details

## Impact
I noticed an issue in the `SellCreditMarket.executeSellCreditMarket()` function. When `exactAmountIn` is false, meaning the caller is specifying the exact cash amount they want to receive (`params.amount`), the function calculates the `creditAmountIn` and `fees` using: [SellCreditMarket.sol#171-181](https://github.com/code-423n4/2024-06-size/blob/8850e25fb088898e9cf86f9be1c401ad155bea86/src/libraries/actions/SellCreditMarket.sol#L171-L181)
```solidity
(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
});
```
The issue is in the calculation of `maxCashAmountOut`. According to the technical docs, when selling credit and specifying an exact cash amount out, the maximum cash amount out should be calculated as follows:
```solidity
maxCashAmountOut = cashAmountOut
```
However, in the code, when `params.creditPositionId != RESERVED_ID`, it's calculated as:
```solidity
maxCashAmountOut = Math.mulDivDown(creditPosition.credit, PERCENT - state.getSwapFeePercent(tenor), PERCENT + ratePerTenor)
```
This calculation doesn't match the specification and could lead to incorrect fee calculations or credit amounts when selling credit from an existing credit position.

Specifically when `exactAmountIn` is false and `params.creditPositionId != RESERVED_ID`. This scenario corresponds to a user selling credit from an existing credit position and specifying the exact cash amount they want to receive.

According to the technical documentation in `/technical-docs/contracts/2.3-fees`, when selling credit and specifying an exact cash amount out, the maximum cash amount out should be calculated as:
```solidity
maxCashAmountOut = cashAmountOut
```
However, in the code, when `params.creditPositionId != RESERVED_ID`, `maxCashAmountOut` is calculated as:
```solidity
maxCashAmountOut = Math.mulDivDown(creditPosition.credit, PERCENT - state.getSwapFeePercent(tenor), PERCENT + ratePerTenor)
```
This calculation doesn't match the specification and could lead to incorrect fee calculations or credit amounts when selling credit from an existing credit position.

## Proof of Concept
Suppose a user wants to sell credit from an existing credit position with the following parameters:
   - `creditPosition.credit` = 1000 USDC
   - `ratePerTenor` = 0.1 (10%)
   - `tenor` = 365 days
   - `state.getSwapFeePercent(tenor)` = 0.01 (1%)
No incentives to write off bad debt when remaining collateral is very small
Audit: 2024-06-size-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
When a position only has a bit of collateral left (e.g., because a previous `liquidate` call provided just too little shares or the exchange rate just changed), there is no incentive anymore to call `liquidate` again. The maximum amount that a user will get is the remaining collateral balance, which does not cover the gas fees in such cases.

Therefore, the bad debt is never written off, meaning that the mechanism does not work in such cases and these pairs become toxic (as the last user will have to pay for the leftover borrow shares).

## Recommended Mitigation Steps
To avoid such scenarios, it should not be possible to remove almost all of the collateral. Consider introducing an upper limit, e.g. 95%. Above this limit, no dirty liquidations would be allowed.


## Assessed type

Other
Bad amount validation could lead to DoS, when `sellCreditMarket` called
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#L93-L95


# Vulnerability details

## Impact
Core function DoS

## Proof of Concept
1. When user execute `Size::sellCreditMarket` with params.exactAmountIn = false, it means that user wants to receive params.amount amount of `cash` for credit. 
2. Through the execution of `BuyCreditMarket::validateBuyCreditMarket` params.amount is validated as function reverts if `params.amount < state.riskConfig.minimumCreditBorrowAToken`, no matter params.exactAmountIn's value.  
```solidity

```
Finally from 1. and 2. we can assume that there is possible scenario where user can call `Size::sellCreditMarket` with `exactAmountIn = false` and `amount < minimumCreditBorrowAToken(while creditIn > minimumCreditBorrowAToken)`, then the function is going to revert no matter if actual creditIn is greather than `state.riskConfig.minimumCreditBorrowAToken`
## Tools Used

## Recommended Mitigation Steps
```diff
1. Rewrite params.amount validation as shown below:
function validateBuyCreditMarket(State storage state, BuyCreditMarketParams calldata params) external view {

        .....

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

        .....

    }
```
2. If exactAmountIn = false, validate creditAmountIn after it is computed


## Assessed type

DoS
There is a missing check when compensating of a credit position.
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#L139


# Vulnerability details

## Impact
When compensating a creditposition with another creditposition(borrower  creditposition), there is no check whether the creditposition forSale - false/true. As a result lender creditposition(after replacing with new creditposition ) other lenders without their intention before making forSale - false for the creditposition.



## Proof of Concept
1. Let’s assume, alice has 3 creditpositionid . they are creditpositionid 1, creditpositionid 2, creditpositionid 3.

2. Alice’s creditpositionid 2’s debtpositionid is 2 whose borrower is bob.

3. Alice’s creditpositionid 2’s borrower is Bob who has also 3 creditpositionid and those are  creditpositionid 4, creditpositionid 5, creditpositionid 6.

4. Bob wants to compensate for debt with his creditposition. So Bob compensated debt of creditposition 2 by giving creditposition 4 to alice. 

5. Here Alice's  creditpositionid 2’s forSale - false but Bob's creditposition 4’s  forSale - true. Now alice gets creditposition 4 which forSale is true which is unintentional for alice.

## Tools Used
manual review
## Recommended Mitigation Steps
When compensating , replace forSale condition from old creditpositionid to new creditpositioid for lenders.





## Assessed type

Context
Repay and liquidate are pausable but no grace period applied to liquidations after protocol gets unpaused which would lead to unfair liquidations of users
Audit: 2024-06-size-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details


## Proof of Concept

Take a look at https://github.com/code-423n4/2024-06-size/blob/8850e25fb088898e9cf86f9be1c401ad155bea86/src/Size.sol#L198-L221

```solidity
    function repay(RepayParams calldata params) external payable override(ISize) whenNotPaused {
        state.validateRepay(params);
        state.executeRepay(params);
    }


    function liquidate(LiquidateParams calldata params)
        external
        payable
        override(ISize)
        whenNotPaused
        returns (uint256 liquidatorProfitCollateralToken)
    {
        state.validateLiquidate(params);
        liquidatorProfitCollateralToken = state.executeLiquidate(params);
        state.validateMinimumCollateralProfit(params, liquidatorProfitCollateralToken);
    }

```

These are the external functions that query both repay/ liquidate, evidently, these functions require the protocol to not be in a paused state, issue however is that while the protocol a user's debt position is going to keep on accruing more dept, and could even become liquidatable, even if they would like to make a repayment deposit, cause they can't considering the `whenNotPaused`.Also we can't expect the user to immediately repay the debt position once the protocol is unpaused, cause in this case the liquidator can just front run the users tx and ensure that the user loses out on his funds via liquidation.

## Impact

A user loses out on his position unfairly, considering even if they would like to deposit and repay, they can't due to the modifier when not paused modifier.

## Recommended Mitigation Steps

Consider removing the `whenNotPaused` modifier from repay(), alternatively, consider allowing a grace period on liquidations after the protocol gets unpaused.



## Assessed type

Context
Inability to perform partial liquidations allows huge positions to accrue bad debt in the system
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#L70-L128


# Vulnerability details


## Proof of Concept

Take a look at https://github.com/code-423n4/2024-06-size/blob/8850e25fb088898e9cf86f9be1c401ad155bea86/src/libraries/actions/Liquidate.sol#L70-L128

```solidity

    function executeLiquidate(State storage state, LiquidateParams calldata params)
        external
        returns (uint256 liquidatorProfitCollateralToken)
    {
        DebtPosition storage debtPosition = state.getDebtPosition(params.debtPositionId);
        LoanStatus loanStatus = state.getLoanStatus(params.debtPositionId);
        uint256 collateralRatio = state.collateralRatio(debtPosition.borrower);

        emit Events.Liquidate(params.debtPositionId, params.minimumCollateralProfit, collateralRatio, loanStatus);

        // if the loan is both underwater and overdue, the protocol fee related to underwater liquidations takes precedence
        uint256 collateralProtocolPercent = state.isUserUnderwater(debtPosition.borrower)
            ? state.feeConfig.collateralProtocolPercent
            : state.feeConfig.overdueCollateralProtocolPercent;

        uint256 assignedCollateral = state.getDebtPositionAssignedCollateral(debtPosition);
        uint256 debtInCollateralToken = state.debtTokenAmountToCollateralTokenAmount(debtPosition.futureValue);
        uint256 protocolProfitCollateralToken = 0;

        // profitable liquidation
        if (assignedCollateral > debtInCollateralToken) {
            uint256 liquidatorReward = Math.min(
                assignedCollateral - debtInCollateralToken,
                Math.mulDivUp(debtPosition.futureValue, state.feeConfig.liquidationRewardPercent, PERCENT)
            );
            liquidatorProfitCollateralToken = debtInCollateralToken + liquidatorReward;

            // split the remaining collateral between the protocol and the borrower, capped by the crLiquidation
            uint256 collateralRemainder = assignedCollateral - liquidatorProfitCollateralToken;

            // cap the collateral remainder to the liquidation collateral ratio
            //   otherwise, the split for non-underwater overdue loans could be too much
            uint256 collateralRemainderCap =
                Math.mulDivDown(debtInCollateralToken, state.riskConfig.crLiquidation, PERCENT);

            collateralRemainder = Math.min(collateralRemainder, collateralRemainderCap);

            protocolProfitCollateralToken = Math.mulDivDown(collateralRemainder, collateralProtocolPercent, PERCENT);
        } else {
            // unprofitable liquidation
            liquidatorProfitCollateralToken = assignedCollateral;
        }

        state.data.borrowAToken.transferFrom(msg.sender, address(this), debtPosition.futureValue);
        state.data.collateralToken.transferFrom(debtPosition.borrower, msg.sender, liquidatorProfitCollateralToken);
        state.data.collate
AToken cap can be violated/bypassed through multicall
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#L76-L82
https://github.com/code-423n4/2024-06-size/blob/8850e25fb088898e9cf86f9be1c401ad155bea86/src/libraries/Multicall.sol#L29-L42


# Vulnerability details

## Impact

The borowATokenCap which is meant to limit the amount of credit in the protocol can be bypassed by depositing via multicall

## Proof of Concept

The borrowATokenCap check is deferred during a multicall to allow for loan repayments and liquidations that might result in the total borrowAToken exceeding the cap **provided** that an equal amount of debt is repaid in the same multicall. [here](https://github.com/code-423n4/2024-06-size/blob/8850e25fb088898e9cf86f9be1c401ad155bea86/src/libraries/actions/Deposit.sol#L76-L82)

However, the validation logic used after the multicall is incorrect as it allows a user to deposit tokens that might exceed the cap and would revert if done through a direct deposit call.

The AToken cap validation function used in multicall `validateBorrowATokenIncreaseLteDebtTokenDecrease` asserts that **if** the borrowATokenCap is exceeded the an equal or greater amount of debt must be burned, as stated earlier this is to allow loan repayments and liquidations that might excced the cap. The issue lies in the `borrowATokenSupplyBefore` and `borrowATokenSupplyAfter` parameters passed to the `validateBorrowATokenIncreaseLteDebtTokenDecrease` function.
As seen [here](https://github.com/code-423n4/2024-06-size/blob/8850e25fb088898e9cf86f9be1c401ad155bea86/src/libraries/Multicall.sol#L29-L42) the `balanceOf(address(this))` is used rather than `totalSupply()`, since deposits have no effect on `balanceOf(address(this))` as tokens is deposited directly to the  user's balance, the validation fails.

### Proof of Code

Lets extend the existing deposit validation test by adding the following lines of code
```solidity
// /test/local/actions/DepositValidation.t.sol

    function test_Deposit_validation_borrowATokenCap() public {
        uint256 amount = 4_000_000;
        _mint(address(weth), alice, amount * 1e18);
        _mint(address(usdc), alice, amount * 1e6);
        _approve(alice, address(weth), address(size), amount * 1e18);
        _approve(alice, address(usdc), address(size), amount * 1e6);

        vm.startPrank(alice);
        size.deposit(DepositParams({token: address(weth), amount: amount * 1e18, to: alice}));

        vm.expectRevert(
            abi.encodeWithSelector(
                Errors.BORROW_ATOKEN_CAP_EXCEEDED.selector, size.riskConfig().borrowATokenCap, amount * 1e6
            )
        );
        size.deposit(DepositParams({token: address(usdc), amount: amount * 1e6, to: alice}));

        //deposit fails, user uses multicall to deposit anyways!.
->      bytes[] memory data = new bytes[](1);
->      data[0] = abi.encodeCall(size.deposit, DepositParams({token: address(usdc), amount: amount * 1e6, to: alice}));