xanoutas/solidity-security-findings huggingface dataset:
Racing condition between settleAuction and placeBid might allow previous highest bidder to prevent others to bid
Audit: 2024-04-gondi-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e0917e18b036c/src/lib/AuctionLoanLiquidator.sol#L238
https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e0917e18b036c/src/lib/AuctionLoanLiquidator.sol#L273


# Vulnerability details

## Impact
Racing condition between settleAuction and placeBid might allow previous highest bidder to prevent others to bid

## Proof of Concept
In AuctionLoanLiquidator.sol, placeBid and settleAuction windows overlap. Both are allowed when currentTime == max (withMargin or expiration).

```solidity
    function placeBid(address _nftAddress, uint256 _tokenId, Auction memory _auction, uint256 _bid)
...
        uint96 max = withMargin > expiration ? withMargin : expiration;
|>        if (max < currentTime && currentHighestBid > 0) {
            revert AuctionOverError(max);
        }
...
```
(https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e0917e18b036c/src/lib/AuctionLoanLiquidator.sol#L237-L239)
```solidity
    function settleAuction(Auction calldata _auction, IMultiSourceLoan.Loan calldata _loan) external nonReentrant {
...
        uint96 expiration = _auction.startTime + _auction.duration;
        uint96 withMargin = _auction.lastBidTime + _MIN_NO_ACTION_MARGIN;
|>        if ((withMargin > currentTime) || (currentTime < expiration)) {
            uint96 max = withMargin > expiration ? withMargin : expiration;
            revert AuctionNotOverError(max);

```

This is problematic because a user whose placeBid tx settles at max time might be reverted when another user front-runs with settleAuction at the same max time. This encourages a racing between settleAuction and placeBid, which allows previous highest bidder to prevent others to bid by front-running. 


## Tools Used

Manual

## Recommended Mitigation Steps
In settleAuction, change the if control flow into ` if ((withMargin >= currentTime) || (currentTime <= expiration)) {//revert}`


## Assessed type

Other
validateOffer() shouldn't be able to use getCollectedFees
Audit: 2024-04-gondi-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e0917e18b036c/src/lib/pools/Pool.sol#L396


# Vulnerability details

## Vulnerability details

in `validateOffer() ` can use `getCollectedFees` for payments

```solidity
    function validateOffer(bytes calldata _offer, uint256 _protocolFee) external override onlyAcceptedCallers {
        if (!isActive) {
            revert PoolStatusError();
        }
@>      uint256 currentBalance = asset.balanceOf(address(this)) - getAvailableToWithdraw;
        uint256 baseRateBalance = IBaseInterestAllocator(getBaseInterestAllocator).getAssetsAllocated();
        uint256 undeployedAssets = currentBalance + baseRateBalance;
        (uint256 principalAmount, uint256 apr) = IPoolOfferHandler(getUnderwriter).validateOffer(
            IBaseInterestAllocator(getBaseInterestAllocator).getBaseAprWithUpdate(), _offer
        );
```
`currentBalance` contains `getCollectedFees`.

But `_getUndeployedAssets()` subtracts `getCollectedFees`.

This may cause `_getUndeployedAssets()` to underflow

```solidity
    function _getUndeployedAssets() private view returns (uint256) {
        return asset.balanceOf(address(this)) + IBaseInterestAllocator(getBaseInterestAllocator).getAssetsAllocated()
@>          - getAvailableToWithdraw - getCollectedFees;
    }
```

Example:
balanceOf(address(this))  = 150
getCollectedFees = 50
getAvailableToWithdraw = 0
IBaseInterestAllocator(getBaseInterestAllocator).getAssetsAllocated() = 0
offer.principalAmount = 150

This way when the `offer` is executed, it is transferred away 150
_getUndeployedAssets() = 0 + 0 - 0 - 50 = -50 (underflow)

## Impact
`_getUndeployedAssets() ` underflow will cause most of `ERC4626` methods to fail.

## Recommended Mitigation

```diff 
    function validateOffer(bytes calldata _offer, uint256 _protocolFee) external override onlyAcceptedCallers {
        if (!isActive) {
            revert PoolStatusError();
        }
-       uint256 currentBalance = asset.balanceOf(address(this)) - getAvailableToWithdraw;
+      uint256 currentBalance =  asset.balanceOf(address(this)) - getAvailableToWithdraw - getCollectedFees;
        uint256 baseRateBalance = IBaseInterestAllocator(getBaseInterestAllocator).getAssetsAllocated();
        uint256 undeployedAssets = currentBalance + baseRateBalance;
        (uint256 principalAmount, uint256 apr) = IPoolOfferHandler(getUnderwriter).validateOffer(
            IBaseInterestAllocator(getBaseInterestAllocator).getBaseAprWithUpdate(), _offer
        );
```


## Assessed type

Context
emitLoan() lack of checks <=getMaxTranches
Audit: 2024-04-gondi-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e0917e18b036c/src/lib/loans/MultiSourceLoan.sol#L124-L128


# Vulnerability details

## Vulnerability details

Currently `emitLoan()` doesn't limit `_loan.tranche.length <= getMaxTranches`

And in `addNewTranche()` it's determining that `_loan.tranche.length == getMaxTranches` will revert TooManyTranchesError(). 
```solidity
    function addNewTranche(
        RenegotiationOffer calldata _renegotiationOffer,
        Loan memory _loan,
        bytes calldata _renegotiationOfferSignature
    ) external nonReentrant returns (uint256, Loan memory) {
...
        if (_loan.tranche.length == getMaxTranches) {
            revert TooManyTranchesError();
        }
```

This way, as long as `emitLoan()` is executed with `tranche.length == getMaxTranches + 1`

and then `addNewTranche()` to skip the limit and add unlimited tranches.

## Impact

Adding too many `tranches` causes GAS_OUT, which can lead to failure of liquidation， and so on.

## Recommended Mitigation

```diff
    function _processOffersFromExecutionData(
        address _borrower,
        address _principalReceiver,
        address _principalAddress,
        address _nftCollateralAddress,
        uint256 _tokenId,
        uint256 _duration,
        OfferExecution[] calldata _offerExecution
    ) private returns (uint256, uint256[] memory, Loan memory, uint256) {
...

+       if (tranche.length > getMaxTranches) {
+           revert TooManyTranchesError();
+       }
      
        Loan memory loan = Loan(
            _borrower,
            _tokenId,
            _nftCollateralAddress,
            _principalAddress,
            totalAmount,
            block.timestamp,
            _duration,
            tranche,
            protocolFee.fraction
        );

        return (loanId, offerIds, loan, totalFee);
    }
```


## Assessed type

Context
Pool.getMinTimeBetweenWithdrawalQueues current calculations may not be sufficient 
Audit: 2024-04-gondi-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e0917e18b036c/src/lib/pools/Pool.sol#L163


# Vulnerability details

## Vulnerability details

`getMinTimeBetweenWithdrawalQueues` is very important for `Pool`.

If `getMinTimeBetweenWithdrawalQueues` is too small, `pendingQueues` will be overwritten too early, and when `Loan` pays off, it won't be able to find the corresponding `queues`.

So we will calculate `getMinTimeBetweenWithdrawalQueues` by `MaxDuration + _LOAN_BUFFER_TIME` to make sure it won't be overwritten too early.

Currently `_LOAN_BUFFER_TIME = 7 days'
Similarly: LiquidationHandler.MAX_AUCTION_DURATION = 7 days

So that `getMinTimeBetweenWithdrawalQueues` is sufficient if the bidding is done within the time period

However, less consideration is given to the presence of `AuctionLoanLiquidator._MIN_NO_ACTION_MARGIN` and the bidding can be delayed.

```solidity
    function placeBid(address _nftAddress, uint256 _tokenId, Auction memory _auction, uint256 _bid)
        external
        nonReentrant
        returns (Auction memory)
    {
...
        uint256 currentTime = block.timestamp;
        uint96 expiration = _auction.startTime + _auction.duration;
        uint96 withMargin = _auction.lastBidTime + _MIN_NO_ACTION_MARGIN;
@>      uint96 max = withMargin > expiration ? withMargin : expiration;
        if (max < currentTime && currentHighestBid > 0) {
            revert AuctionOverError(max);
        }
```
If the bidding is intense, it may be delayed > `_MIN_NO_ACTION_MARGIN =10 minutes`, or even longer.

So `getMinTimeBetweenWithdrawalQueues` may not be enough.

Suggest adding an extra day: `MaxDuration + _LOAN_BUFFER_TIME + 3 day`. 


## Impact

`getMinTimeBetweenWithdrawalQueues` is not large enough causing `pendingQueues` to be overwritten prematurely.

## Recommended Mitigation

```diff
contract Pool is ERC4626, InputChecker, IPool, IPoolWithWithdrawalQueues, LoanManager, ReentrancyGuard {
...

    /// @dev Used in case loans might have a liquidation, then the extension is upper bounded by maxDuration + liq time.
-   uint256 private constant _LOAN_BUFFER_TIME = 7 days;
+   uint256 private constant _LOAN_BUFFER_TIME = 10 days;
```



## Assessed type

Context
confirmBaseInterestAllocator() change BaseInterestAllocator may pay large getReallocationBonus
Audit: 2024-04-gondi-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e0917e18b036c/src/lib/pools/Pool.sol#L211


# Vulnerability details

## Vulnerability details

`owner` can submit `getPendingBaseInterestAllocator` first, and then anyone can enable it by `confirmBaseInterestAllocator()`.
 
```solidity
    function confirmBaseInterestAllocator(address _newBaseInterestAllocator) external {
        address cachedAllocator = getBaseInterestAllocator;
        if (cachedAllocator != address(0)) {
            if (getPendingBaseInterestAllocatorSetTime + UPDATE_WAITING_TIME > block.timestamp) {
                revert TooSoonError();
            }
            if (getPendingBaseInterestAllocator != _newBaseInterestAllocator) {
                revert InvalidInputError();
            }
@>          IBaseInterestAllocator(cachedAllocator).transferAll();
            asset.approve(cachedAllocator, 0);
        }
        asset.approve(_newBaseInterestAllocator, type(uint256).max);

        getBaseInterestAllocator = _newBaseInterestAllocator;
        getPendingBaseInterestAllocator = address(0);
        getPendingBaseInterestAllocatorSetTime = type(uint256).max;

        emit BaseInterestAllocatorSet(_newBaseInterestAllocator);
    }
```

The current logic is
1. take all the balance of the old `BaseInterestAllocator` and put it in `Pool`. 
2. change getBaseInterestAllocator to the new BaseInterestAllocator.

If the old BaseInterestAllocator already has a large balance, the balance of the Pool will increase dramatically.

Subsequent users executing `reallocate()` will get a big bonus `getReallocationBonus`.

```solidity
    function reallocate() external nonReentrant returns (uint256) {
        (uint256 currentBalance, uint256 targetIdle) = _reallocate();
        uint256 delta = currentBalance > targetIdle ? currentBalance - targetIdle : targetIdle - currentBalance;
@>      uint256 shares = delta.mulDivDown(totalSupply * getReallocationBonus, totalAssets() * _BPS);

        _mint(msg.sender, shares);

        emit Reallocated(delta, shares);

        return shares;
    }
```

Assuming old `BaseInterestAllocator` balance:1 M

shares = 1 M * (1 - optimalIdleRange.mid) * totalSupply * getReallocationBonus / totalAssets()

## Impact

after  change BaseInterestAllocator,  may pay large getReallocationBonus

## Recommended Mitigation

Execute `_reallocate()` in the `confirmBaseInterestAllocator()` method without paying any `getReallocationBonus`.

```diff
    function confirmBaseInterestAllocator(address _newBaseInterestAllocator) external {
        address cachedAllocator = getBaseInterestAllocator;
        if (cachedAllocator != address(0)) {
            if (getPendingBaseInterestAllocatorSetTime + UPDATE_WAITING_TIME > block.timestamp) {
                revert TooSoonError();
            }
            if (getPendingBaseInterestAllocator != _newBaseInterestAllocator) {
                revert InvalidInputError();
            }
            IBase
validateOffer() give the wrong _targetIdle to reallocate()
Audit: 2024-04-gondi-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e0917e18b036c/src/lib/pools/Pool.sol#L411


# Vulnerability details

## Vulnerability details

when `validateOffer()`,  if principalAmount > currentBalance , we need to get back enough from `getBaseInterestAllocator` to avoid not having enough balance to pay the `offer`.

```solidity
    function validateOffer(bytes calldata _offer, uint256 _protocolFee) external override onlyAcceptedCallers {
        if (!isActive) {
            revert PoolStatusError();
        }
        uint256 currentBalance = asset.balanceOf(address(this)) - getAvailableToWithdraw;
        uint256 baseRateBalance = IBaseInterestAllocator(getBaseInterestAllocator).getAssetsAllocated();
        uint256 undeployedAssets = currentBalance + baseRateBalance;
        (uint256 principalAmount, uint256 apr) = IPoolOfferHandler(getUnderwriter).validateOffer(
            IBaseInterestAllocator(getBaseInterestAllocator).getBaseAprWithUpdate(), _offer
        );

        /// @dev Since the balance of the pool includes capital that is waiting to be claimed by the queues,
        ///      we need to check if the pool has enough capital to fund the loan.
        ///      If that's not the case, and the principal is larger than the currentBalance, the we need to reallocate
        ///      part of it.
        if (principalAmount > undeployedAssets) {
            revert InsufficientAssetsError();
        } else if (principalAmount > currentBalance) {
            IBaseInterestAllocator(getBaseInterestAllocator).reallocate(
@>              currentBalance, principalAmount - currentBalance, true
            );
        }
        /// @dev If the txn doesn't revert, we can assume the loan was executed.
        _outstandingValues = _getNewLoanAccounting(principalAmount, _netApr(apr, _protocolFee));
    }
```

The above call to `IBaseInterestAllocator(getBaseInterestAllocator).reallocate(_currentIdle, _targetIdle)` with the second argument `_targetIdle` is wrong.
Instead of passing `_targetIdle = principalAmount - currentBalance`, we should pass `_targetIdle = principalAmount` directly

The `reallocate()` method calculates the `delta` itself internally.

Take `AaveUsdcBaseInterestAllocator` as an example:
```solidity
contract AaveUsdcBaseInterestAllocator is IBaseInterestAllocator, Owned {
...
    function reallocate(uint256 _currentIdle, uint256 _targetIdle, bool) external {
        address pool = _onlyPool();
        if (_currentIdle > _targetIdle) {
            uint256 delta = _currentIdle - _targetIdle;
            ERC20(_usdc).transferFrom(pool, address(this), delta);
            IAaveLendingPool(_aavePool).deposit(_usdc, delta, address(this), 0);
        } else {
@>          uint256 delta = _targetIdle - _currentIdle;
            IAaveLendingPool(_aavePool).withdraw(_usdc, delta, address(this));
            ERC20(_usdc).transfer(pool, delta);
        }

        emit Reallocated(_currentIdle, _targetIdle)
loanLiquidation() calculation of interest is not accurate
Audit: 2024-04-gondi-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e0917e18b036c/src/lib/pools/Pool.sol#L460


# Vulnerability details

## Vulnerability details

`loanLiquidation()` The calculated interest codes are as follows：
```solidity
    function loanLiquidation(
        uint256 _loanId,
        uint256 _principalAmount,
        uint256 _apr,
        uint256,
        uint256 _protocolFee,
        uint256 _received,
        uint256 _startTime
    ) external override onlyAcceptedCallers {
        uint256 netApr = _netApr(_apr, _protocolFee);
        uint256 interestEarned = _principalAmount.getInterest(netApr, block.timestamp - _startTime);
@>      uint256 fees = IFeeManager(getFeeManager).processFees(_received, 0);
        getCollectedFees += fees;
        _loanTermination(msg.sender, _loanId, _principalAmount, netApr, interestEarned, _received - fees);
    }
...


contract FeeManager is IFeeManager, TwoStepOwned {
...
    function processFees(uint256 _principal, uint256 _interest) external view returns (uint256) {
        /// @dev cached
        Fees memory __fees = _fees;
        return _principal.mulDivDown(__fees.managementFee, PRECISION)
            + _interest.mulDivDown(__fees.performanceFee, PRECISION);
    }
```

The above code takes all of `_received` as the principal and gives it to `IFeeManager` to calculate.

But the amount received from the bidding is not always less than the principal, it may be more than the principal, and this part should be calculated as `interest`.

## Impact

When `received` is greater than the principal, `fees` is not correct.

## Recommended Mitigation

```diff
    function loanLiquidation(
        uint256 _loanId,
        uint256 _principalAmount,
        uint256 _apr,
        uint256,
        uint256 _protocolFee,
        uint256 _received,
        uint256 _startTime
    ) external override onlyAcceptedCallers {
        uint256 netApr = _netApr(_apr, _protocolFee);
        uint256 interestEarned = _principalAmount.getInterest(netApr, block.timestamp - _startTime);
-       uint256 fees = IFeeManager(getFeeManager).processFees(_received, 0);
+       uint256 fees;
+       if (_received > _principalAmount) {
+           fees = IFeeManager(getFeeManager).processFees(_principalAmount, _received - _principalAmount);
+       }else {
+           fees = IFeeManager(getFeeManager).processFees(_received, 0);
+       }
        getCollectedFees += fees;
        _loanTermination(msg.sender, _loanId, _principalAmount, netApr, interestEarned, _received - fees);
    }


## Assessed type

Context
Pool.getCollectedFees Lack of method for claim
Audit: 2024-04-gondi-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e0917e18b036c/src/lib/pools/Pool.sol#L53


# Vulnerability details

## Vulnerability details

when `loanRepayment()` / `loanLiquidation()`

Fees accumulated to `getCollectedFees`

```solidity
    function loanRepayment(
        uint256 _loanId,
        uint256 _principalAmount,
        uint256 _apr,
        uint256,
        uint256 _protocolFee,
        uint256 _startTime
    ) external override onlyAcceptedCallers {
        uint256 netApr = _netApr(_apr, _protocolFee);
        uint256 interestEarned = _principalAmount.getInterest(netApr, block.timestamp - _startTime);
        uint256 received = _principalAmount + interestEarned;
        uint256 fees = IFeeManager(getFeeManager).processFees(_principalAmount, interestEarned);
@>      getCollectedFees += fees;
        _loanTermination(msg.sender, _loanId, _principalAmount, netApr, interestEarned, received - fees);
    }

    /// @inheritdoc LoanManager
    function loanLiquidation(
        uint256 _loanId,
        uint256 _principalAmount,
        uint256 _apr,
        uint256,
        uint256 _protocolFee,
        uint256 _received,
        uint256 _startTime
    ) external override onlyAcceptedCallers {
        uint256 netApr = _netApr(_apr, _protocolFee);
        uint256 interestEarned = _principalAmount.getInterest(netApr, block.timestamp - _startTime);
        uint256 fees = IFeeManager(getFeeManager).processFees(_received, 0);
@>      getCollectedFees += fees;
        _loanTermination(msg.sender, _loanId, _principalAmount, netApr, interestEarned, _received - fees);
    }
```

But currently `Pool.sol` does not provide a method to claim and reduce `getCollectedFees`

## Impact

`Pool.getCollectedFees` can't be claimed.

## Recommended Mitigation

Add a method so that administrators can claim `getCollectedFees`.


## Assessed type

Context
confirmUnderwriter() need to recalculate getMinTimeBetweenWithdrawalQueues
Audit: 2024-04-gondi-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e0917e18b036c/src/lib/loans/LoanManager.sol#L127


# Vulnerability details

## Vulnerability details

`getMinTimeBetweenWithdrawalQueues` is very important for `Pool`.

If `getMinTimeBetweenWithdrawalQueues` is too small, `pendingQueues` will be overwritten too early, and when `Loan` pays off, it won't be able to find the corresponding `queues`.

So we will calculate `getMinTimeBetweenWithdrawalQueues` by `MaxDuration + _LOAN_BUFFER_TIME` to make sure it won't be overwritten too early.

```solidity
    constructor(
        address _feeManager,
        address _offerHandler,
        uint256 _waitingTimeBetweenUpdates,
        OptimalIdleRange memory _optimalIdleRange,
        uint256 _maxTotalWithdrawalQueues,
        uint256 _reallocationBonus,
        ERC20 _asset,
        string memory _name,
        string memory _symbol
    ) ERC4626(_asset, _name, _symbol) LoanManager(tx.origin, _offerHandler, _waitingTimeBetweenUpdates) {

....

@>      getMinTimeBetweenWithdrawalQueues = (IPoolOfferHandler(_offerHandler).getMaxDuration() + _LOAN_BUFFER_TIME)
            .mulDivUp(1, _maxTotalWithdrawalQueues);
```

But switching the new `getUnderwriter/_offerHandler` doesn't recalculate the `getMinTimeBetweenWithdrawalQueues`.

```solidity
    function confirmUnderwriter(address __underwriter) external onlyOwner {
        if (getPendingUnderwriterSetTime + UPDATE_WAITING_TIME > block.timestamp) {
            revert TooSoonError();
        }
        if (getPendingUnderwriter != __underwriter) {
            revert InvalidInputError();
        }

@>      getUnderwriter = __underwriter;
        getPendingUnderwriter = address(0);
        getPendingUnderwriterSetTime = type(uint256).max;

        emit UnderwriterSet(__underwriter);
    }
```

This may break the expectation of `getMinTimeBetweenWithdrawalQueues`, and the new `getUnderwriter.getMaxDuration` is larger than the old one, which may cause `pendingQueues` to be overwritten prematurely

## Impact

The new `getUnderwriter.getMaxDuration` is larger than the old one, which may cause `pendingQueues` to be overwritten prematurely.

## Recommended Mitigation

`Pool` overrides `confirmUnderwriter()` with an additional recalculation of `getMinTimeBetweenWithdrawalQueues` and must not be smaller than the old one, to avoid premature overwriting of the previous one.

```diff
contract Pool is ERC4626, InputChecker, IPool, IPoolWithWithdrawalQueues, LoanManager, ReentrancyGuard {
-   uint256 public immutable getMinTimeBetweenWithdrawalQueues;
+   uint256 public getMinTimeBetweenWithdrawalQueues;
...
+   function confirmUnderwriter(address __underwriter) external override onlyOwner {
+           super.confirmUnderwriter(__underwriter);
+           uint256 newMinTime = (IPoolOfferHandler(__underwriter).getMaxDuration() + _LOAN_BUFFER_TIME)
+            .mulDivUp(1, _maxTotalWithdrawalQueues);
+           require(newMinTime >=