xanoutas/solidity-security-findings huggingface dataset:
The fee in Gnosis trading isn't included in the calculation of the minBuyAmount
Audit: 2024-07-reserve-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-07-reserve/blob/3f133997e186465f4904553b0f8e86ecb7bbacbf/contracts/p1/mixins/TradeLib.sol#L76-L80
https://github.com/code-423n4/2024-07-reserve/blob/3f133997e186465f4904553b0f8e86ecb7bbacbf/contracts/plugins/trading/GnosisTrade.sol#L114-L120


# Vulnerability details

## Impact
When we sell tokens to buy other tokens in a `trade`, we determine the `minimum buy amount` based on the `sell amount`. 
This means that if we receive less than this `minimum amount`, the `trade` will fail. 
Normally, we consider a worst-case scenario where we sell tokens at a `lower price` and buy tokens at a `higher price`. 
If we don’t meet the `minimum buy amount`, the `trade` should fail.

However, in `Gnosis trading`, a `fee` is applied, which means the actual `sell amount` is less than the `input amount`. 
Despite this, the `minimum buy amount` is still calculated using the `input amount`, not the actual `sell amount`. 
As a result, a `trade` that could have succeeded might fail due to this discrepancy.
## Proof of Concept
The `minimum buy amount` is calculated based on the `sell amount` as follows:
```
function prepareTradeSell(
    TradeInfo memory trade,
    uint192 minTradeVolume,
    uint192 maxTradeSlippage
) internal view returns (bool notDust, TradeRequest memory req) {
    // {buyTok} = {sellTok} * {1} * {UoA/sellTok} / {UoA/buyTok}
    uint192 b = s.mul(FIX_ONE.minus(maxTradeSlippage)).safeMulDiv(
        trade.prices.sellLow,
        trade.prices.buyHigh,
        CEIL
    );

    req.sellAmount = s.shiftl_toUint(int8(trade.sell.erc20Decimals()), FLOOR);
    req.minBuyAmount = b.shiftl_toUint(int8(trade.buy.erc20Decimals()), CEIL);
    req.sell = trade.sell;
    req.buy = trade.buy;

    return (notDust, req);
}
```
The numerical expression is as follow.
```
minBuyAmount = sellAmount * (1 - maxTradeSlippage) * sellLow / buyHigh
```
These amounts are then passed to the `Gnosis Trade` (`lines 91-92`).
```
function init(
    IBroker broker_,
    address origin_,
    IGnosis gnosis_,
    uint48 batchAuctionLength,
    TradeRequest calldata req
) external stateTransition(TradeStatus.NOT_STARTED, TradeStatus.OPEN) {
91:    require(req.sellAmount <= type(uint96).max, "sellAmount too large");
92:    require(req.minBuyAmount <= type(uint96).max, "minBuyAmount too large");

114:    uint96 _sellAmount = uint96(
115:        _divrnd(
116:            req.sellAmount * FEE_DENOMINATOR,
117:            FEE_DENOMINATOR + gnosis.feeNumerator(),
118:            FLOOR
119:        )
120:    );

  
    auctionId = gnosis.initiateAuction(
        sell,
        buy,
        endTime,
        endTime,
149:        _sellAmount,
150:        minBuyAmount,
        minBuyAmtPerOrder,
        0,
        false,
        address(0),
        new bytes(0)
    );
}
```
However, the actual `sell amount` decreases due to the `fee` in the `Gnosis Trade` (`lines 114-120`). 
This reduced `sell amount` is what gets passed to the `Gnosis contract`(`li
The recollateralization process isn't working properly
Audit: 2024-07-reserve-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-07-reserve/blob/3f133997e186465f4904553b0f8e86ecb7bbacbf/contracts/p1/mixins/RecollateralizationLib.sol#L198


# Vulnerability details

## Impact

When the `basket` isn't fully `collateralized`, a `recollateralization` process takes place, which is crucial. 
The `basket` consists of several tokens, and during this process, surplus tokens are sold to buy those that are in deficit.
To determine the amounts to sell and buy, we first set a range of `baskets` that can be achieved after optimal trading, referred to as `[bottom, top]`. 
If a token has enough to cover the `top` of this range, the excess can be sold, but it should not fall below the `top`. 
Conversely, if a token does not have enough to cover the `bottom`, we need to buy more, with the amount based on the `bottom`.
However, there's an issue: the `bottom` calculation is incorrect. 
It's essential to fully `collateralize` the `baskets` as quickly as possible. 
Even if we could achieve full `collateralization` in one trade, the incorrect `bottom` calculation might prevent this, requiring manual adjustment of the `basketsNeeded`, which could lead to losses for depositors.
Alternatively, the trading could be split into several smaller trades, each taking time—potentially up to a week per trade.
## Proof of Concept
Below is the `basketRange` function, which determines the target `basket` range.
```
function basketRange(TradingContext memory ctx, Registry memory reg)
    internal
    view
    returns (BasketRange memory range)
{
    for (uint256 i = 0; i < reg.erc20s.length; ++i) {
143:       (uint192 low, uint192 high) = reg.assets[i].price(); // {UoA/tok}

        if (
148:            ctx.quantities[i] == 0 &&
149:            !TradeLib.isEnoughToSell(reg.assets[i], ctx.bals[i], low, ctx.minTradeVolume)
        ) {
            continue;
        }
        {

            uint192 anchor = ctx.quantities[i].mul(ctx.basketsHeld.top, CEIL);  
			 ...
        }

        {
185:            uint192 anchor = ctx.quantities[i].mul(ctx.basketsHeld.bottom, FLOOR);

189:            uint192 val = low.mul(ctx.bals[i] - anchor, FLOOR);

198:            uoaBottom += (val < ctx.minTradeVolume) ? 0 : val - ctx.minTradeVolume;

        }
    }
}
```
In `line 148`, if the `quantity` of a token is `0`, it means this token is not a `backing token` for the `basket`, so we can sell all of these tokens. However, the sale must meet the `minimum trading volume`. If a token's quantity is insufficient to satisfy this minimum, we skip it.

By the time we reach `line 198`, the token is either a `backing token` or a `non-backing token` with a quantity sufficient to exceed the `minimum trading volume`. At this point, all `backing tokens` should have enough quantity to cover the current `bottom`.
The idea is to sell all excess tokens at a `lower price`, buy `baskets` at a `higher price`, and see the final `bottom` we can achieve. 
It seems the reason for subtracting the `minimum
Users are incentivized  to not withdraw immediately after the market is closed.
Audit: 2024-08-wildcat-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-08-wildcat/blob/main/src/market/WildcatMarketBase.sol#L665


# Vulnerability details

## Impact
Users are incentivized to not withdraw immediately after market is closed.

## Proof of Concept
Within a withdraw batch, all users within said batch are paid equally - at the same rate, despite what exactly was the rate when each individual one created their withdraw.

While this usually is not a problem as it is a way to reward users who queue the withdrawal and start the expiry cooldown, it creates a problematic situation when the market is closed with an outstanding expiry batch.

The problem is that up until the expiry timestamp comes, all new withdraw requests are added to this old batch where the rate of the previous requests drags the overall withdraw rate down.

Consider the following scenario:
1. A withdraw batch is created and its expiry time is 1 year.
2. 6 months in, the withdraw batch has half of the markets value in it and the market is closed. The current rate is `1.12` and the batch is currently filled at `1.06`
3. Now users have two choices - to either withdraw their funds now at `~1.06` rate or wait 6 months to be able to withdraw their funds at `1.12` rate.

This creates a very unpleasant situation as the users have an incentive to hold their funds within the contract, despite not providing any value. 

Looked from slightly different POV, these early withdraw requesters force everyone else to lock their funds for additional 6 months, for the APY they should've usually received for just holding up until now.

## Tools Used
Manual review


## Recommended Mitigation Steps
After closing a market and filling the current expiry, delete it from `pendingWithdrawalExpiry`. Introduce a `closedExpiry` variable so you later make sure a future expiry is not made at that same timestamp to avoid collision.


## Assessed type

Context
A borrower can create a market with disabled  `hookTemplate`.
Audit: 2024-08-wildcat-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-08-wildcat/blob/fe746cc0fbedc4447a981a50e6ba4c95f98b9fe1/src/HooksFactory.sol#L491-L516


# Vulnerability details

## Impact
Borrower creates a market with the `hookTemplate` that was disabled. This may lead to undetermined consequences as it is not intended by the admin.

## Proof of Concept
Borrower creates a market with a hook that was disabled previously via `HooksFactory.disableHooksTemplate()` function

https://github.com/code-423n4/2024-08-wildcat/blob/fe746cc0fbedc4447a981a50e6ba4c95f98b9fe1/src/HooksFactory.sol#L491-L516

## Tools Used
Manual review, foundry

## Recommended Mitigation Steps
Consider adding a function that allows the owner to enable `hookTemplate`, or implement a check to ensure that the `hookTemplate` is `enabled` when the borrower deploys a market.


## Assessed type

Other
`WildcantMarketWithdrawals.sol::executeWithdrawals` function does not have validation check for the sufficient balance  is there to repay all the address in the queue
Audit: 2024-08-wildcat-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-08-wildcat/blob/fe746cc0fbedc4447a981a50e6ba4c95f98b9fe1/src/market/WildcatMarketWithdrawals.sol#L217


# Vulnerability details

## Impact
 This will revert unexpectedly and user wont be able to withdraw funds. It also dont allow other queue address to withdraw funds wheather pool 
 has enough to fulfill there withdrawal request

## Proof of Concept
Below function will revert the under or overflow error if there is less required amount to fulfill the request.
```Javascript
 function test_executeWithdrawalsWithOutCheckingTotalLiquidityAvailable() external {
    parameters.annualInterestBips = 0;
    setUp();
    _deposit(alice, 1e18);
    _deposit(bob, 1e18);
    uint32 expiry1 = uint32(block.timestamp + parameters.withdrawalBatchDuration);
    uint32 expiry2 = uint32(expiry1 + parameters.withdrawalBatchDuration + 1);
    _prepareBatch(0.6e18, 0.5e18, true);
    _prepareBatch(0.5e18, 0.6e18, true);
    _checkBatch(expiry1, 1e18, 1e18, 1e18);
    _checkBatch(expiry2, 1e18, 1e18, 1e18);
    
    address[] memory accounts = new address[](4);
    accounts[0] = alice;
    accounts[1] = bob;
    accounts[2] = alice;
    accounts[3] = bob;
    uint32[] memory expiries = new uint32[](4);
    expiries[0] = expiry1;
    expiries[1] = expiry1;
    expiries[2] = expiry2;
    expiries[3] = expiry2;
    MarketState memory state = pendingState();
    _trackExecuteWithdrawal(state, expiry1, alice);
    _trackExecuteWithdrawal(state, expiry1, bob);
    _trackExecuteWithdrawal(state, expiry2, alice);
    _trackExecuteWithdrawal(state, expiry2, bob);
    market.executeWithdrawals(accounts, expiries);
  }
```

## Tools Used
Manual review

## Recommended Mitigation Steps
Add some checks to check the total amount of liquidity and how to address requests it can fulfill.


## Assessed type

Under/Overflow
originationFeeAsset can be a sanctioned asset
Audit: 2024-08-wildcat-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-08-wildcat/blob/fe746cc0fbedc4447a981a50e6ba4c95f98b9fe1/src/HooksFactory.sol#L120


# Vulnerability details

During market deployment borrowers are required to pay a `originationFeeAsset` by specifying an already listed `originationFeeAsset`. 

however the protection to exclude flagged/blacklisted addresses from interacting with the protocol wont be effective if the `originationFeeAsset` is flagged and used to create/update the hooks template.

## Impact
allows blacklisted assets interact with the protocol.


## Proof of Concept
https://github.com/code-423n4/2024-08-wildcat/blob/fe746cc0fbedc4447a981a50e6ba4c95f98b9fe1/src/HooksFactory.sol#L120
```
  function addHooksTemplate(
    address hooksTemplate,
    string calldata name,
    address feeRecipient,
    address originationFeeAsset,
    uint80 originationFeeAmount,
    uint16 protocolFeeBips
  ) external override onlyArchControllerOwner {
    if (_templateDetails[hooksTemplate].exists) {
      revert HooksTemplateAlreadyExists();
    }
    _validateFees(feeRecipient, originationFeeAsset, originationFeeAmount, protocolFeeBips);
```

https://github.com/code-423n4/2024-08-wildcat/blob/fe746cc0fbedc4447a981a50e6ba4c95f98b9fe1/src/HooksFactory.sol#L176
```
  function updateHooksTemplateFees(
    address hooksTemplate,
    address feeRecipient,
    address originationFeeAsset,
    uint80 originationFeeAmount,
    uint16 protocolFeeBips
  ) external override onlyArchControllerOwner {
    if (!_templateDetails[hooksTemplate].exists) {
      revert HooksTemplateNotFound();
    }
    _validateFees(feeRecipient, originationFeeAsset, originationFeeAmount, protocolFeeBips);
    HooksTemplate storage template = _templateDetails[hooksTemplate];
    template.feeRecipient = feeRecipient;
    template.originationFeeAsset = originationFeeAsset;
    template.originationFeeAmount = originationFeeAmount;
    template.protocolFeeBips = protocolFeeBips;
    emit HooksTemplateFeesUpdated(
      hooksTemplate,
      feeRecipient,
      originationFeeAsset,
      originationFeeAmount,
      protocolFeeBips
    );
  }
```
https://github.com/code-423n4/2024-08-wildcat/blob/fe746cc0fbedc4447a981a50e6ba4c95f98b9fe1/src/HooksFactory.sol#L153
```
  function _validateFees(
    address feeRecipient,
    address originationFeeAsset,
    uint80 originationFeeAmount,
    uint16 protocolFeeBips
  ) internal pure {
    bool hasOriginationFee = originationFeeAmount > 0;
    bool nullFeeRecipient = feeRecipient == address(0);
    bool nullOriginationFeeAsset = originationFeeAsset == address(0);
    if (
      (protocolFeeBips > 0 && nullFeeRecipient) ||
      (hasOriginationFee && nullFeeRecipient) ||
      (hasOriginationFee && nullOriginationFeeAsset) ||
      protocolFeeBips > 1_000
    ) {
      revert InvalidFeeConfiguration();
    }
  }
```
`_validateFees` do not consider black listed asset address.

when the archController is setting/updating the hooks template the `originat
Delinquency fee can be totally or partially avoided due to MarketState.isDelinquent only being updated at the end of a market operation
Audit: 2024-08-wildcat-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-08-wildcat/blob/main/src/market/WildcatMarketBase.sol#L406
https://github.com/code-423n4/2024-08-wildcat/blob/main/src/market/WildcatMarketWithdrawals.sol#L283
https://github.com/code-423n4/2024-08-wildcat/blob/main/src/market/WildcatMarket.sol#L202


# Vulnerability details

## Impact

`MarketState.isDelinquent` is only updated at the end of a market operation. When a non-static call made to the market happens, if the last state update was not delinquent, the fees and scale factor will be computed as though the market is not delinquent, even if at that moment the market's required liquidity is greater than the total assets and the delinquency grace period has already passed.

The market computes fees incrementally on non-static call made to it. If the call that changes the state from non delinquent to delinquent happens after the `delinquencyGracePeriod`, then no delinquencyFee will be charged for the period between when the grace period ended and when the call was made. In the worse case, if that call happens to be a repayment which increases the totalAssets above the required liquidity, then the market would never enter in delinquency and no delinquency fees will be charged to the borrower.

## Proof of Concept
Scenario 1: Avoidance of delinquency fee payment:

1. Lender deposits funds into a market
2. Borrower borrow's the maximum borrowable amount
3. The next second, the required liquidity will be greater than the total assets in the market
4. Time passes beyond the `delinquencyGracePeriod` (for example: delinquencyGracePeriod + 3 days go by)
5. The borrower makes a repayment to leave the total assets above the required liquidity

The payment that the borrower makes in this scenario does not include delinquency fees (the new scale factor was computed with delinquencyFee = 0). The market never enters in delinquency, and the delinquency cooldown period (3 days in this case) never enters into play.


Scenario 2: Partial avoidance of delinquency fee payment:
1. Steps 1 to 4 of the previous example are the same
5. Make a non static call to the contract, for example, the lender might make a deposit

In this scenario, the borrower will not pay delinquency fees for the first 3 days after the grace period where the total assets where below the required liquidity


The following test shows a proof of concept of the first scenario
```solidity
pragma solidity >=0.8.20;

import './BaseMarketTest.sol';
import 'src/libraries/MathUtils.sol';
import 'src/libraries/MarketState.sol';

contract AuditMarketTest is BaseMarketTest {
  using MathUtils for uint256;

  function test_audit_delinquencyFeeAvoided() external {
    vm.prank(alice);
    market.depositUpTo(50_000e18);

    // borrow the maximum amount possible to force delinquency in the next block
    uint256 borrowTimestamp = block.timestamp;
    uint256 borrowedAmount = market.borrowableAssets();

    vm.prank(borrower);
    market.borrow(borrowedAmount
Borrower can lock `lender` funds in `market` via `WildcatMarket::closeMarket`
Audit: 2024-08-wildcat-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-08-wildcat/blob/fe746cc0fbedc4447a981a50e6ba4c95f98b9fe1/src/market/WildcatMarket.sol#L226-L287


# Vulnerability details

## Impact

The `WildcatMarket::closeMarket` is callable only by the market's `borrower` should they decide to end a formerly operating `market`. This transfers asset out of market in 2 cases.

1. To `borrower` if `currentlyHeld > totalDebts`. `currentlyHeld  = totalAssets()` held in `market` and `totalDebts = state.totalDebts()` withdrawn from `market` by `borrower`.
2. To `lender` if there is a pending withdrawal batch which is not fully paid off.

And what about `lenders` who don't withdraw from the market?

There is an edge case here. `Lenders` who deposit into a market but don't request a withdraw can lose funds if `borrower` closes the market.

If `lenders` deposit `loans` into an operational `market`, and the `borrower` takes no `debt`, and the `lenders` do not queue a `withdrawalRequest`, the `borrower` can intentionally lock the `lenders` funds by calling `WildcatMarket::closeMarket`. 

Damage of the protocol's reputation is most likely the goal since the `borrower` has nothing to lose here.


## Proof of Concept


Below is a test with logs.
```javascript

  function test_borrower_can_lock_lenders_funds_via_closeMarket() external asAccount(borrower) {
    address depositor1 = makeAddr("DEPOSITOR1");
    address depositor2 = makeAddr("DEPOSITOR2");
    // asset.mint(depositor, 1e18);
    // _approve(depositor, address(market), 1e18);
    // No need to mint and approve asset tokens for depositor
    // because _deposit() does both for the amount to be deposited.

    console.log("Depositor 1 balance before deposit: ", asset.balanceOf(depositor1)); 
    console.log("Depositor 2 balance before deposit: ", asset.balanceOf(depositor2)); 
    console.log("Borrower balance before deposits: ", asset.balanceOf(borrower));
    console.log("Market balance before deposits: ", asset.balanceOf(address(market)));

    _deposit(depositor1, 1e18); 
    // vm.warp(1 days);
    _deposit(depositor2, 1e18);

    console.log("Depositor 1 balance after deposit: ", asset.balanceOf(depositor1));
    console.log("Depositor 2 balance after deposit: ", asset.balanceOf(depositor2));
    console.log("Market balance after deposits: ", asset.balanceOf(address(market)));
    // startPrank(borrower);
    asset.approve(address(market), 1e18); //8e17
    
    // vm.warp(1 days); // Doesn't execute if up to 1 day is allowed to pass after deposit is made
    // Must be immediately after deposit 

    market.closeMarket();

    console.log("////////// AFTER CLOSING MARKET //////////");

    console.log("Borrower balance after closing market: ", asset.balanceOf(borrower)); // Took no debt from market
    console.log("Market balance after its closing: ", asset.balanceOf(address(market)));

    // The below scenario will revert since token is same as market underlying asset

    // console.log("////////// AFT
Repaying outstanding debt by the borrower will not clear the debt in the system thus the contract still incurs more debt after calling this function
Audit: 2024-08-wildcat-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-08-wildcat/blob/fe746cc0fbedc4447a981a50e6ba4c95f98b9fe1/src/market/WildcatMarket.sol#L179-L184


# Vulnerability details

## Impact
When a borrower calls the `repayOutstandingDebt()` function, the expected behavior is that all outstanding debts should be repaid in full. However, due to a flaw in the implementation, specifically not processing expired or batched withdrawals after the debt repayment, the debt accumulates again after the repayment. This results in the system incurring more debt than expected, creating a financial discrepancy and making the repayment function ineffective as borrower will still have to pay more in a case where everyone has queued withdrawals.

In essence, this issue allows debt to persist even after a full repayment is attempted, leading to inconsistent system behavior where the borrower continues to owe money even after settling their outstanding obligations. 

## Proof of Concept
The following two test setups demonstrate the bug in the `repayOutstandingDebt()` function:

### Test Case 1: Debt Repayment Without Updating
```solidity
function test_repayOutstandingDebt() external {
    _depositBorrowWithdraw(alice, 1e18, 8e17, 1e18);  // Deposit and withdraw setup
    asset.mint(address(this), 8e17);  // Mint tokens for debt repayment
    asset.approve(address(market), 8e17);  // Approve repayment
    vm.expectEmit(address(market));
    emit DebtRepaid(address(this), 8e17);  // Expect debt repayment event

    MarketState memory state = pendingState();  // Get current market state

    market.repayOutstandingDebt();  // Repay the outstanding debt

    fastForward(1);  // Move time forward by 1 second
    assertEq(market.totalDebts() - market.totalAssets(), 0);  // Check if debt is cleared
}
```
This test case fails because, 1 second after repayment, debt accumulates again. The reason is that pending withdrawals or accrued fees are not processed before the repayment, causing debt to resurface immediately.

```solidity
Failing tests:
Encountered 1 failing test in test/market/WildcatMarket.t.sol:WildcatMarketTest
[FAIL. Reason: assertion failed: 241095890410958 != 0] test_repayOutstandingDebt() (gas: 1237051)
```

### Test Case 2: Debt Repayment with Market Update
```solidity
function test_repayOutstandingDebt() external {
    _depositBorrowWithdraw(alice, 1e18, 8e17, 1e18);  // Deposit and withdraw setup
    asset.mint(address(this), 8e17);  // Mint tokens for debt repayment
    asset.approve(address(market), 8e17);  // Approve repayment
    vm.expectEmit(address(market));
    emit DebtRepaid(address(this), 8e17);  // Expect debt repayment event

    MarketState memory state = pendingState();  // Get current market state

    market.repayOutstandingDebt();  // Repay the outstanding debt
    market.updateState();  // Update the market state after repayment

    fastForward(1 days);  // Move time forward by 1 day
    assertEq(market.totalDebts() - market.totalAssets(), 0)