xanoutas/solidity-security-findings huggingface dataset:
inconsistency in variable pool liquidity validation
Audit: 2024-06-size-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact

Impact - High, Could result in a DOS on creating perfectly viable loan positions.
LikeliHood - Low, as protocol fees are almost negligible in most cases.

## Proof of Concept

Variable pool liquidity is checked when creating credit/debt positions via the following functions: [buyCreditMarket](https://github.com/code-423n4/2024-06-size/blob/8850e25fb088898e9cf86f9be1c401ad155bea86/src/Size.sol#L184-L184), [sellCreditMarket](https://github.com/code-423n4/2024-06-size/blob/8850e25fb088898e9cf86f9be1c401ad155bea86/src/Size.sol#L194-L194) and [liquidateWithReplacement](https://github.com/code-423n4/2024-06-size/blob/8850e25fb088898e9cf86f9be1c401ad155bea86/src/Size.sol#L243-L243). The issue is that the amount(cashAmountIn) returned from `executeBuyCreditMarket` and validated in `buyCreditMarket` includes the protocol fees as seen here 
![image](https://gist.github.com/assets/133224432/104c6970-9eb5-46b1-b11f-f4186c37c1a0)
![image2](https://gist.github.com/assets/133224432/5d00dd14-ae7a-4a78-b8cb-3ecb05fe6d72)
This implies that in cases where there's just enough liquidity on aave to grant a loan the added protocol fees might Dos the transaction.

Admittedly, protocol fees are almost negligible for short term/average amount loans , it can be an issue for long term/high amount loans e.g a 6 month 500k loan will have a protocol fee at $1,250 (0.5% APR).

## Tools Used

Manual Review

## Recommended Mitigation Steps

subtract fees from returned amount

```sol
function executeBuyCreditMarket(State storage state, BuyCreditMarketParams memory params)
        external
->      returns (uint256 amountToBorrower)
    {
      
        //...code...

->      amountToBorrower = cashAmountIn - fees;
->      state.data.borrowAToken.transferFrom(msg.sender, borrower, amountToBorrower);
        state.data.borrowAToken.transferFrom(msg.sender, state.feeConfig.feeRecipient, fees);
    }
```







## Assessed type

Invalid Validation
Legitimate call with small cash out can be reverted.
Audit: 2024-06-size-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
Legitimate call with small cash out can be reverted.

## Proof of Concept
`SellCreditMarket.sol#executeSellCreditMarket()` function which sells credit is as follows.
```solidity
    function executeSellCreditMarket(State storage state, SellCreditMarketParams calldata params)
        external
        returns (uint256 cashAmountOut)
    {
        ...

        uint256 creditAmountIn;
        uint256 fees;

        if (params.exactAmountIn) {
160         creditAmountIn = params.amount;

            (cashAmountOut, fees) = state.getCashAmountOut({
                creditAmountIn: creditAmountIn,
                maxCredit: params.creditPositionId == RESERVED_ID ? creditAmountIn : creditPosition.credit,
                ratePerTenor: ratePerTenor,
                tenor: tenor
            });
        } else {
169         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
            });
        }

       ...
    }
```
As we can see on L160 and L169, `params.amount` can be for credit or cash.   
But in `validateSellCreditMarket()` function it always assumes that `params.amount` is for credit.
```solidity
    function validateSellCreditMarket(State storage state, SellCreditMarketParams calldata params) external view {
        ...

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

        ...

        // validate exactAmountIn
        // N/A
    }
```
So, by L93, legitimate call with small cash out can be reverted although credit amount is bigger than `minimumCreditBorrowAToken`.   
This problems exist in `BuyCreditMarket.sol`.   

In fact, this validation check is not needed because validation check for dust amount is done in `createCreditPosition()` and `createDebtAndCreditPositions()` function.
```solidity
    function createCreditPosition(State storage state, uint256 exitCreditPositionId, address lender, uint256 credit)
        external
    {
        CreditPosition storage exitCreditPosition = state.getCreditPosit
borrowATokenCap can be 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/Multicall.sol#L29
https://github.com/code-423n4/2024-06-size/blob/8850e25fb088898e9cf86f9be1c401ad155bea86/src/libraries/Multicall.sol#L37


# Vulnerability details

## Impact
The `multicall()` function bypasses the `borrowATokenCap` when making a deposit:
```
if (!state.data.isMulticall) {
                state.validateBorrowATokenCap();
            }
```
When `multicall()` is called, `validateBorrowATokenIncreaseLteDebtTokenDecrease()` is internally invoked with four arguments: `borrowATokenSupplyBefore`, `debtTokenSupplyBefore`, `borrowATokenSupplyAfter`, and `debtTokenSupplyAfter`.

The `validateBorrowATokenIncreaseLteDebtTokenDecrease()` function performs the following check if the supply is above the cap: 
- `borrowATokenSupplyIncrease` is calculated if `borrowATokenSupplyAfter` is greater than `borrowATokenSupplyBefore` 
- `debtATokenSupplyDecrease` is  calculated if `debtTokenSupplyBefore` is greater than `borrowATokenSupplyAfter`.

If the supply increase exceeds the debt reduction, the transaction reverts.

However, `borrowATokenSupplyBefore` and `borrowATokenSupplyAfter` represent the borrowAToken balance of the contract (`address(this)`). These variables does not represent the total supply, because `borrowAToken` is minted to the `to` address specified in the parameters (`params.to`) when `depositUnderlyingBorrowTokenToVariablePool()` is internally called by `executeDeposit()`.

The balance of the size contract only increases when a debt position is repaid. When `executeRepay()` is called, the future value of the debt position is transferred from the borrower to the size contract.
```
state.data.borrowAToken.transferFrom(msg.sender, address(this), debtPosition.futureValue);
```
The balance of the size contract decreases when `claim()` is called. The `executeClaim()` function transfers the claim amount from the size contract to the lender
```
state.data.borrowAToken.transferFrom(address(this), creditPosition.lender, claimAmount);
```
Therefore, anyone could deposit using `multicall()` even if the `borrowTokenCap` is reached, rendering the function `validateBorrowATokenIncreaseLteDebtTokenDecrease()` useless. 
The protocol won’t be able to prevent growing at a faster pace than its security/safety/trust. 

## Proof of Concept
Add the following import and POC to `Multicall.t.sol`
```
import {UserView} from "@src/SizeView.sol";
```
```
    function test_Multicall_multicall_can_bypass_borrowATokenCap() public {
        uint256 amountBorrowTokenCap = 1000000e6;

        _deposit(bob, usdc, amountBorrowTokenCap);
        UserView memory bobUser = size.getUserView(bob);
        assertEq(bobUser.borrowATokenBalance, amountBorrowTokenCap);

        _mint(address(usdc), alice, amountBorrowTokenCap);

        vm.startPrank(alice);
        IERC20Metadata(address(usdc)).approve(address(size), amountBorrowTokenCap);

        vm.expec
When liquidateWithReplacement is called, the new borrower does not pay the swapFee 
Audit: 2024-06-size-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact

When `liquidateWithReplacement` is called, the new borrower does not pay the swapFee which results in less fees for the protocol and an unfair advantage for the picked borrower compared to any other user with the same yield curve. 


## Proof of Concept

If a user’s collateral ratio (value of collateral / value of debt) is below the set liquidationCollateralRatio (crLiquidation), all debtPositions of the user can be liquidated. Beside the “normal” liquidation which can be called by anyone, an address holding the `KEEPER_ROLE` can alternatively call `liquidateWithReplacement()`. This function liquidates a debtPosition of a user who is under water and replaces him with another user who has a healthier collateral ratio and has an open borrow offer which allows for a profitable swap of borrowers.   

Within the SIZE protocol there is a swapFee which is taken by the protocol `on the exchange of credit for cash operations`.`The cash recipient always pays for the swap fee`. This means that each time a new loan is given out and the borrower receives cash, he must pay the swapFee.

The issue arises from the fact that even though the new borrower receives a loan / cash when `liquidateWithReplacement()`is called, he does not pay the swapFee. 

When calling `liquidateWithReplacement`, the function `executeLiquidateWithReplacement` is called. As a first step, the debtPosition is liquidated. 

```java
        liquidatorProfitCollateralToken = state.executeLiquidate(
            LiquidateParams({
                debtPositionId: params.debtPositionId,
                minimumCollateralProfit: params.minimumCollateralProfit
            })
        );
```


After that the interest rate for the remaining tenor of the liquidated debtPosition is calculated based on the borrowers yield curve and the `issuanceValue` is determined based on the futureValue of the liquidated debtPosition:

```java
//@audit: how much interest in % 1e18 must the borrower pay
uint256 ratePerTenor = borrowOffer.getRatePerTenor( 
            VariablePoolBorrowRateParams({
                variablePoolBorrowRate: state.oracle.variablePoolBorrowRate,
                variablePoolBorrowRateUpdatedAt: state.oracle.variablePoolBorrowRateUpdatedAt,
                variablePoolBorrowRateStaleRateInterval: state.oracle.variablePoolBorrowRateStaleRateInterval
            }),
            tenor
        );
```

The issuance value is calculated as the discounted futureValue of the liquidated debtPosition:
```java
issuanceValue = Math.mulDivDown(debtPositionCopy.futureValue, PERCENT, PERCENT + ratePerTenor);
```
The issuanceValue is send to the new borrower at the end of the function call without been reduced by the swapFee the borrower should pay when receiving cash/selling credit.

```java
state.data.borrowA
protocol blocks partial repayment, even though they improve borrowers health in certain conditions
Audit: 2024-06-size-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact 
repayment is a healthy operation, that should always be allowed as it improves the health of the protocol in general. In compensate howeveer the protocol checks the health factor of the position after compensation is done with `state.validateUserIsNotUnderwater(msg.sender);`, this check will always fail if the user is underwater (cr  < minCollateralisationRatio (either the one set by the user or the one set by the protocol (max))) after the compensation, and thus always preventing users from partially repaying their positions.

## Proof of concept

https://github.com/code-423n4/2024-06-size/blob/8850e25fb088898e9cf86f9be1c401ad155bea86/src/Size.sol#L247-L251
```solidity
    function compensate(CompensateParams calldata params) external payable override(ISize) whenNotPaused {
            // * Compensate a borrower's debt with his credit in another loan
            // NOTE validate the enough credit (and healthy) and the owner of credditCompensate same as borrower of debtPositionToRepay
            state.validateCompensate(params);
            state.executeCompensate(params);
            // NOTE validate user is not underwater after the repayment
            // @audit-issue this will revert if the user position is still liquidatable after the repayment 
            state.validateUserIsNotUnderwater(msg.sender);
        }
```
In the compensate function transaction will always fail if the cr of the user is negative.
E.g:

- User owns multiple small Credit positions in the protocol and have set the minCollateralisationRatio to 150% and doesn't want his cr to go bellow it (protocol minCR= 130%)
- The user position Cr has dropped bellow 150% and the user will want to improve his health
- The user chooses to use his multiple creditPositions to repay his debt in a multicall to improve the health of his position
- all the compensate() call that the user will call will revert because neither of his small credit position will bring the health of his position up to 150% in one go
## recomendation
we should allways allow partial repayment as long as the repaid amount > fragemnetation fee, as it will improve the borrower health and protects the solvancy of the protocol. In the case if the params.creditPositionToCompensateId == RESRVED_ID we should also check for the health because there was no repayment done yet (only the debt Position is split)

```diff
    function compensate(CompensateParams calldata params) external payable override(ISize) whenNotPaused {
            // * Compensate a borrower's debt with his credit in another loan
            state.validateCompensate(params);
-            state.executeCompensate(params);
+            uint256 fragmentaionFeeValue, uint256  repayAmount= state.executeCompensate(params);
-            state.validateUserIsNotUnderwater(msg.sender);
+ //@audit only
BranchPort.toggleStrategyToken used on unregistered STRATEGY TOKEN  will allow STRATEGIES to drain full token balance
Audit: 2023-05-maia-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-05-maia/blob/54a45beb1428d85999da3f721f923cbf36ee3d35/src/ulysses-omnichain/BranchPort.sol#L342-L346
https://github.com/code-423n4/2023-05-maia/blob/54a45beb1428d85999da3f721f923cbf36ee3d35/src/ulysses-omnichain/BranchPort.sol#L331-L339
https://github.com/code-423n4/2023-05-maia/blob/54a45beb1428d85999da3f721f923cbf36ee3d35/src/ulysses-omnichain/BranchPort.sol#L149-L151


# Vulnerability details

## Impact
`BranchPort.toggleStrategyToken` may be called on a token not registered as a strategy token effectively registering it without setting a `getMinimumTokenReserveRatio`. In such a case `_minimumReserves` will always return a value smaller than the current balance which in turn will make `_minimumReserves` always return some reserves available while `_reservesLacking` always return 0. 
```
function _minimumReserves(uint256 _currBalance, address _token) internal view returns (uint256) {
        return ((_currBalance + getStrategyTokenDebt[_token]) * getMinimumTokenReserveRatio[_token]) / DIVISIONER;
    }
```

## Tools Used
Pen and paper
## Recommended Mitigation Steps
Add check to  `BranchPort.toggleStrategyToken`


## Assessed type

Other
the mint function in erc4626 will mint incorrect amount  
Audit: 2023-05-maia-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-05-maia/blob/54a45beb1428d85999da3f721f923cbf36ee3d35/src/erc-4626/ERC4626.sol#L53-L54


# Vulnerability details

## Impact
if you look at the `ERC4626` contract the function mint minting the **wrong** amount at line **53** it should be minting the assets amount not the amount of the share and because the shares and assets are not 1:1 it will lead to unwanted results and different mint amounts.
## Proof of Concept
the `ERC4626` codes are written from solmate contract but in the mint function, there is a difference that will cause a difference in the calculation of mints which is very mandatory to consider. 

this is the mint function of ERC4626 of this protocol


```solidity
  /// @inheritdoc IERC4626
    function mint(uint256 shares, address receiver) public virtual returns (uint256 assets) {
        assets = previewMint(shares); // No need to check for rounding error, previewMint rounds up.

        // Need to transfer before minting or ERC777s could reenter.
        address(asset).safeTransferFrom(msg.sender, address(this), assets);

        _mint(receiver, shares);

        emit Deposit(msg.sender, receiver, assets, shares);

        afterDeposit(assets, shares);
    }
```

and now look at the original contract that is developed by Solmate team 

```solidity 
   function mint(uint256 shares, address to) public virtual returns (uint256 amount) {
        amount = previewMint(shares); // No need to check for rounding error, previewMint rounds up.

        // Need to transfer before minting or ERC777s could reenter.
        asset.safeTransferFrom(msg.sender, address(this), amount);

        _mint(to, amount);

        emit Deposit(msg.sender, to, amount, shares);

        afterDeposit(amount, shares);
    }

```

the difference is in `_mint(to, amount);`(solmate) part which is written like that in protocol `_mint(receiver, shares);`

the protocol uses solmate structure as erc4626 in every single function of erc4626 and its the same but there is a difference in the mint part that will cause a difference in the results of mints and here in the protocol since the assets (amount ) and the shares are not 1:1 it will cause dangerous problems for the codebase. 

## Tools Used
vs code/ manually audited 
## Recommended Mitigation Steps

- consider implementing the mint function as solmate contract mint function like other functions that are the same and designed with solmates contract instructor


implement the function in that way 

```soldity
   function mint(uint256 shares, address to) public virtual returns (uint256 amount) {
        amount = previewMint(shares); // No need to check for rounding error, previewMint rounds up.

        // Need to transfer before minting or ERC777s could reenter.
        asset.safeTransferFrom(msg.sender, address(this), amount);

        _mint(to, amount);

        emit Deposit(msg.sender, to, amount, shares);

        afterDeposit(amount, shares);
    }
```

use `+
Reward clarinets can claim rewards multiple times 
Audit: 2023-05-maia-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-05-maia/blob/cfed0dfa3bebdac0993b1b42239b4944eb0b196c/src/rewards/rewards/FlywheelGaugeRewards.sol#L234


# Vulnerability details

## Impact 

Reward claimers can call the get accrued reward function multiple times and maybe even drain the contract 
## Proof of Concept
As we can see there’s no check setting the accrued reward to zero after the rewards have been transferred 

## Tools Used
Manual review 
## Recommended Mitigation Steps

Add a setter than sets the reward to zero after the function has been called 


## Assessed type

Reentrancy
Reactivated gauges can’t queue up rewards
Audit: 2023-05-maia-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-05-maia/blob/cfed0dfa3bebdac0993b1b42239b4944eb0b196c/src/rewards/rewards/FlywheelGaugeRewards.sol#L189-L193


# Vulnerability details

## Impact


Reactivated gauges can’t queue up rewards 

## Proof of Concept

Active gauges as set  by authorised users get their rewards queued up in the FlywheelGaugeRewards._queueRewards() function. As part of it, their associated struct QueuedRewards updates its storedCycle value to the cycle in which they get queued up:

https://github.com/code-423n4/2023-05-maia/blob/cfed0dfa3bebdac0993b1b42239b4944eb0b196c/src/rewards/rewards/FlywheelGaugeRewards.sol#L189-L193

```
gaugeQueuedRewards[gauge] = QueuedRewards({
    priorCycleRewards: queuedRewards.priorCycleRewards + completedRewards,
    cycleRewards: uint112(nextRewards),
    storedCycle: currentCycle
});
```

However, these gauges may be deactivated  and they will now be ignored in either FlywheelGaugeRewards.queueRewardsForCycle() or FlywheelGaugeRewards.queueRewardsForCyclePaginated() because both use gaugeToken.gauges() to get the set of gauges for which to queue up rewards for the cycle, and that only gives active gauges. Therefore, any updates FlywheelGaugeRewards makes to its state will not be done to deactivated gauges' QueuedRewards structs. In particular, the gaugeCycle contract state variable will keep advancing throughout its cycles, while QueuedRewards.storedCycle will retain its previously set value, which is the cycle where it was queued and not 0.

Once reactivated later with at least 1 full cycle being done without it, it will produce issues. It will now be returned by gaugeToken.gauges() to be processed in either FlywheelGaugeRewards.queueRewardsForCycle()or FlywheelGaugeRewards.queueRewardsForCyclePaginated(), but, once the reactivated gauge is passed to _queueRewards(), it will fail an assert:

https://github.com/code-423n4/2023-05-maia/blob/cfed0dfa3bebdac0993b1b42239b4944eb0b196c/src/rewards/rewards/FlywheelGaugeRewards.sol#L183

```
assert(queuedRewards.storedCycle == 0 || queuedRewards.storedCycle >= lastCycle);
```
This is because it already has a set value from the cycle it was processed in previously (i.e. storedCycle>0), and, since that cycle is at least 1 full cycle behind the state contract, it will also not pass the second condition queuedRewards.storedCycle >= lastCycle.

The result is that this gauge is locked out of queuing up for rewards because queuedRewards.storedCycle is only synchronised with the contract’s cycle later in _queueRewards() which will now always fail for this gauge.


## Tools Used
Code inspection

## Recommended Mitigation Steps

Account for the reactivated gauges that previously went through the rewards queue process, such as introducing a separate flow for newly activated gauges. However, any changes such as removing the above mentioned assert() should be carefully validated for other downstream logic that may use the QueuedRewards.storedCycle value. T