xanoutas/solidity-security-findings huggingface dataset:
_baseLoanChecks() check errors for expire
Audit: 2024-04-gondi-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Vulnerability details

`_baseLoanChecks()` is used to check whether Loan has expired.

```solidity
    function _baseLoanChecks(uint256 _loanId, Loan memory _loan) private view {
        if (_loan.hash() != _loans[_loanId]) {
            revert InvalidLoanError(_loanId);
        }
@>      if (_loan.startTime + _loan.duration < block.timestamp) {
            revert LoanExpiredError();
        }
    }
```

The expiration checks in liquidation are as follows:

```solidity
    function _liquidateLoan(uint256 _loanId, IMultiSourceLoan.Loan calldata _loan, bool _canClaim)
        internal
        returns (bool liquidated, bytes memory liquidation)
    {
...

        uint256 expirationTime = _loan.startTime + _loan.duration;
@>      if (expirationTime > block.timestamp) {
            revert LoanNotDueError(expirationTime);
        }
```

This way,  both checks pass when `block.timestamp == _loan.startTime + _loan.duration`

This leads to the problem that a malicious attacker can perform the following step
when `block.timestamp == _loan.startTime + _loan.duration`
1. Alice call liquidateLoan(loandId =1) -> success
   - LoanLiquidator generates an auction
   - _loans[loandId = 1] is still valid , and will only be cleared when the auction is over.
2. Alice call addNewTranche(loandId = 1) -> success
   - _baseLoanChecks(loandId = 1) will pass
   - delete _loans[1]; 
   - _loans[2] = newLoan.hash()
3. bidding ends, call `loanLiquidated(loandId = 1)` will fail , because `_loans[1]` has been cleared

## Impact

Maliciously disrupting the end of the bidding, causing the NFT/funds to be locked

## Recommended Mitigation

```diff

    function _baseLoanChecks(uint256 _loanId, Loan memory _loan) private view {
        if (_loan.hash() != _loans[_loanId]) {
            revert InvalidLoanError(_loanId);
        }
-       if (_loan.startTime + _loan.duration < block.timestamp) {
+      if (_loan.startTime + _loan.duration <= block.timestamp) {
            revert LoanExpiredError();
        }
    }
```


## Assessed type

Context
validateOffer()  reentry to manipulate exchangeRate
Audit: 2024-04-gondi-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Vulnerability details

The current mechanism of `validateOffer()` is to first book `_outstandingValues` to increase, but `assets.balanceOf(address(this))` doesn't decrease immediately

```solidity
    function validateOffer(bytes calldata _offer, uint256 _protocolFee) external override onlyAcceptedCallers {
..

        /// @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));
    }
```

I.e.: After this method is called, `_getUndeployedAssets()` is unchanged, but `_getTotalOutstandingValue()` is increased, so `totalAssets()` is increased, but `totalSupply` is unchanged, so `exchangeRate` is get bigger.

Originally, it was expected that after that, the Pool balance would be transferred at `MultiSourceLoan`, so `_getUndeployedAssets()` becomes smaller and `exchangeRate` returns to normal.

But if it's possible to do callback malicious logic before `MultiSourceLoan` transfers away the Pool balance, it's possible to take advantage of this `exchangeRate` that becomes larger

Example:
Suppose:  _getUndeployedAssets() = 1000   _getTotalOutstandingValue() = 1000  totalSupply = 2000
so: 
totalAssets() =  2000
exchangeRate = 1:1


1. alice call `MultiSourceLoan.emitLoan()`
   - offer.lender = pool
   - offer.principalAmount = 500
   - offer.validators = CustomContract  -> for callback
2. emitLoan() -> Pool.validateOffer()
   - _getUndeployedAssets() = 1000  (no change)
   - _getTotalOutstandingValue() = 1000 + 500 = 1500  (more 500)
   - totalAssets() =  2500
   - exchangeRate = 1.25 : 1
3. emitLoan() -> _checkValidators() -> CustomContract.validateOffer()
   - in `CustomContract.validateOffer()` call pool.redeem(shares) use `exchangeRate = 1.25 : 1` to get more assets
4. emitLoan() -> asset.safeTransferFrom(pool,receiver,500)
   - _getUndeployedAssets() -= 500
   - exchangeRate Expect to return to normal


## Impact

Manipulating the exchangeRate to redeem additional assets

## Recommended Mitigation

In `validateOffer()`, restrict `offer.validators` to be an empty array to avoid callbacks.


## Assessed type

Context
_updatePendingWithdrawalWithQueue() should accumulate pendingForQueue
Audit: 2024-04-gondi-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Vulnerability details

`_queueClaimAll()` is used to allocate `getTotalReceived[]`

The main calculation is in the `_updatePendingWithdrawalWithQueue()` method

This method has an important mechanism to calculate `All future queues/pools also have a piece of it`.

```solidity
    function _queueClaimAll(uint256 _totalToBeWithdrawn, uint256 _cachedPendingQueueIndex) private {
        _reallocateOnWithdrawal(_totalToBeWithdrawn);
        uint256 totalQueues = (getMaxTotalWithdrawalQueues + 1);
        uint256 oldestQueueIdx = (_cachedPendingQueueIndex + 1) % totalQueues;
        uint256[] memory pendingWithdrawal = new uint256[](totalQueues);
        for (uint256 i; i < pendingWithdrawal.length;) {
            uint256 idx = (oldestQueueIdx + i) % totalQueues;
@>          _updatePendingWithdrawalWithQueue(idx, _cachedPendingQueueIndex, pendingWithdrawal);
            unchecked {
                ++i;
            }
        }
        getAvailableToWithdraw = 0;

        for (uint256 i; i < pendingWithdrawal.length;) {
            if (pendingWithdrawal[i] == 0) {
                unchecked {
                    ++i;
                }
                continue;
            }
            address queueAddr = _deployedQueues[i].contractAddress;
            uint256 amount = pendingWithdrawal[i];

            asset.safeTransfer(queueAddr, amount);
            emit QueueClaimed(queueAddr, amount);
            unchecked {
                ++i;
            }
        }
    }
....

    function _updatePendingWithdrawalWithQueue(
        uint256 _idx,
        uint256 _cachedPendingQueueIndex,
        uint256[] memory _pendingWithdrawal
    ) private returns (uint256[] memory) {
        uint256 totalReceived = getTotalReceived[_idx];
        uint256 totalQueues = getMaxTotalWithdrawalQueues + 1;
        /// @dev Nothing to be returned
        if (totalReceived == 0) {
            return _pendingWithdrawal;
        }
        getTotalReceived[_idx] = 0;

        /// @dev We go from idx to newer queues. Each getTotalReceived is the total
        /// returned from loans for that queue. All future queues/pool also have a piece of it.
        /// X_i: Total received for queue `i`
        /// X_1  = Received * shares_1 / totalShares_1
        /// X_2 = (Received - (X_1)) * shares_2 / totalShares_2 ...
        /// Remainder goes to the pool.
        for (uint256 i; i < totalQueues;) {
            uint256 secondIdx = (_idx + i) % totalQueues;
            QueueAccounting memory queueAccounting = _queueAccounting[secondIdx];
            if (queueAccounting.thisQueueFraction == 0) {
                unchecked {
                    ++i;
                }
                continue;
            }
            /// @dev We looped around.
            if (secondIdx == _cachedPendingQueueIndex + 1) {
                break;
refinanceFromLoanExecutionData() lack of check if tokenId is equal
Audit: 2024-04-gondi-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Vulnerability details

`refinanceFromLoanExecutionData()` is used to pay off the old `Loan` and generate a new `Loan` with `LoanExecutionData`.

```solidity
    function refinanceFromLoanExecutionData(
        uint256 _loanId,
        Loan calldata _loan,
        LoanExecutionData calldata _loanExecutionData
    ) external nonReentrant returns (uint256, Loan memory) {
        _baseLoanChecks(_loanId, _loan);

        ExecutionData calldata executionData = _loanExecutionData.executionData;
        address borrower = _loanExecutionData.borrower;
        (address principalAddress, address nftCollateralAddress) = _getAddressesFromExecutionData(executionData);

        OfferExecution[] calldata offerExecution = executionData.offerExecution;

        _validateExecutionData(_loanExecutionData, _loan.borrower);
        _checkWhitelists(principalAddress, nftCollateralAddress);

@>      //@audit miss check if tokenId is equal
        if (_loan.principalAddress != principalAddress || _loan.nftCollateralAddress != nftCollateralAddress) {
            revert InvalidAddressesError();
        }

        /// @dev We first process the incoming offers so borrower gets the capital. After that, we process repayments.
        ///      NFT doesn't need to be transfered (it was already in escrow)
        (uint256 newLoanId, uint256[] memory offerIds, Loan memory loan, uint256 totalFee) =
        _processOffersFromExecutionData(
            borrower,
            executionData.principalReceiver,
            principalAddress,
            nftCollateralAddress,
            executionData.tokenId,
            executionData.duration,
            offerExecution
        );
        _processRepayments(_loan);

        emit LoanRefinancedFromNewOffers(_loanId, newLoanId, loan, offerIds, totalFee);

        _loans[newLoanId] = loan.hash();
        delete _loans[_loanId];

        return (newLoanId, loan);
    }
```
The above code checks that `principalAddress` and `nftCollateralAddress` must be equal

but does not restrict the `tokenId` to be equal.

This way the user can use the old `Loan` with the cheap `NFT`, but generate a new `Loan` with the expensive `NFT` (this new NFT could belong to another Loan, and the lender could be his own account, no loss of repay).

It is not necessary to transfer the new NFT during the execution of this method.

The NFT is then stolen through `repayLoan()`.


## Impact

lack of checking whether tokenId is equal , can be used to steal other people's NFTs.

## Recommended Mitigation

```diff
    function refinanceFromLoanExecutionData(
        uint256 _loanId,
        Loan calldata _loan,
        LoanExecutionData calldata _loanExecutionData
    ) external nonReentrant returns (uint256, Loan memory) {
        _baseLoanChecks(_loanId, _loan);
...
-       if (_loan.principalAddress
refinanceFull/addNewTranche reusing a lender's signature leads to unintended behavior
Audit: 2024-04-gondi-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Vulnerability details

in `MultiSourceLoan`
`refinanceFull()` and `addNewTranche()` use the same signature 
```solidity
    function refinanceFull(
        RenegotiationOffer calldata _renegotiationOffer,
        Loan memory _loan,
        bytes calldata _renegotiationOfferSignature
    ) external nonReentrant returns (uint256, Loan memory) {
...
        if (lenderInitiated) {
            if (_isLoanLocked(_loan.startTime, _loan.startTime + _loan.duration)) {
                revert LoanLockedError();
            }
            _checkStrictlyBetter(
                _renegotiationOffer.principalAmount,
                _loan.principalAmount,
                _renegotiationOffer.duration + block.timestamp,
                _loan.duration + _loan.startTime,
                _renegotiationOffer.aprBps,
                totalAnnualInterest / _loan.principalAmount,
                _renegotiationOffer.fee
            );
        } else if (msg.sender != _loan.borrower) {
            revert InvalidCallerError();
        } else {
            /// @notice Borrowers clears interest
@>          _checkSignature(_renegotiationOffer.lender, _renegotiationOffer.hash(), _renegotiationOfferSignature);
            netNewLender -= totalAccruedInterest;
            totalAccruedInterest = 0;
        }

```

```solidity
    function addNewTranche(
        RenegotiationOffer calldata _renegotiationOffer,
        Loan memory _loan,
        bytes calldata _renegotiationOfferSignature
    ) external nonReentrant returns (uint256, Loan memory) {
...
        uint256 loanId = _renegotiationOffer.loanId;

        _baseLoanChecks(loanId, _loan);
        _baseRenegotiationChecks(_renegotiationOffer, _loan);
@>      _checkSignature(_renegotiationOffer.lender, _renegotiationOffer.hash(), _renegotiationOfferSignature);
        if (_loan.tranche.length == getMaxTranches) {
            revert TooManyTranchesError();
        }
```

So when `lender` signs `RenegotiationOffer`, it is meant to replace `tranche`, i.e. execute `refinanceFull()`.

But a malicious user can use this sign and front-run execute `addNewTranche()`.
`addNewTranche()` doesn't limit the `RenegotiationOffer` too much.
The newly generated `Loan` will be approximately twice the total amount borrowed, and the risk of borrowing against the `lender` will increase dramatically.

## Impact

Maliciously using the signature of `refinanceFull()` to execute `addNewTranche()` will result in approximately double the borrowed amount, and the risk of borrowing will increase dramatically.

## Recommended Mitigation

`RenegotiationOffer` Add a type field to differentiate between signatures.


## Assessed type

Context
mergeTranches() If the lender is a LoanManager it will break the Pool accounting
Audit: 2024-04-gondi-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Vulnerability details

Anyone can run `mergeTranches()` to merge similar `tranches` with the same `lender` and `startTime` into a new `tranche`.
```solidity
    function mergeTranches(uint256 _loanId, Loan memory _loan, uint256 _minTranche, uint256 _maxTranche)
        external
        returns (uint256, Loan memory)
    {
        _baseLoanChecks(_loanId, _loan);
        uint256 loanId = _getAndSetNewLoanId();
        Loan memory loanMergedTranches = _mergeTranches(loanId, _loan, _minTranche, _maxTranche);
        _loans[loanId] = loanMergedTranches.hash();
        delete _loans[_loanId];

        emit TranchesMerged(loanMergedTranches, _minTranche, _maxTranche);

        return (loanId, loanMergedTranches);
    }

    function _mergeTranches(
        uint256 _newLoanId,
        IMultiSourceLoan.Loan memory _loan,
        uint256 _minTranche,
        uint256 _maxTranche
    ) private pure returns (IMultiSourceLoan.Loan memory) {
        /// @dev if the diff is also just 1, then we wouldn't be merging anything
        if (_minTranche >= _maxTranche - 1) {
            revert InvalidParametersError();
        }

        IMultiSourceLoan.Tranche[] memory tranche =
            new IMultiSourceLoan.Tranche[](_loan.tranche.length - (_maxTranche - _minTranche) + 1);

        uint256 originalIndex = 0;
        /// @dev Copy tranches before
        for (; originalIndex < _minTranche;) {
            tranche[originalIndex] = _loan.tranche[originalIndex];
            unchecked {
                ++originalIndex;
            }
        }

        /// @dev Merge tranches. Just picking one, they must all match.
        address lender = _loan.tranche[_minTranche].lender;
        uint256 startTime = _loan.tranche[_minTranche].startTime;

        uint256 principalAmount;
        uint256 cumAprBps;
        uint256 accruedInterest;
        /// @dev Use to validate _totalTranches
        for (; originalIndex < _maxTranche;) {
            IMultiSourceLoan.Tranche memory thisTranche = _loan.tranche[originalIndex];
            if (lender != thisTranche.lender || startTime != thisTranche.startTime) {
                revert MismatchError();
            }
            principalAmount += thisTranche.principalAmount;
            cumAprBps += thisTranche.aprBps * thisTranche.principalAmount;
            accruedInterest += thisTranche.accruedInterest;
            unchecked {
                ++originalIndex;
            }
        }
        /// @dev Output of merged tranches.
        tranche[_minTranche] = IMultiSourceLoan.Tranche(
@>          _newLoanId,
            _loan.tranche[_minTranche].floor,
            principalAmount,
            lender,
            accruedInterest,
            startTime,
            cumAprBps / principalAmount
        );

        /// @dev Copy remaining ones
        uint256 remaini
settleWithBuyout() lack of call LoanManager.loanRepayment()
Audit: 2024-04-gondi-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e0917e18b036c/src/lib/AuctionWithBuyoutLoanLiquidator.sol#L83-L94


# Vulnerability details

## Vulnerability details

in `AuctionWithBuyoutLoanLiquidator`
The `lender` who has lent out the most can purchase the NFT through `settleWithBuyout()`
```solidity
    function settleWithBuyout(
        address _nftAddress,
        uint256 _tokenId,
        Auction calldata _auction,
        IMultiSourceLoan.Loan calldata _loan
    ) external nonReentrant {
        /// TODO: Originator fee
        _checkAuction(_nftAddress, _tokenId, _auction);
        uint256 timeLimit = _auction.startTime + _timeForMainLenderToBuy;
        if (timeLimit < block.timestamp) {
            revert OptionToBuyExpiredError(timeLimit);
        }
        uint256 largestTrancheIdx;
        uint256 largestPrincipal;
        for (uint256 i = 0; i < _loan.tranche.length;) {
            if (_loan.tranche[i].principalAmount > largestPrincipal) {
                largestPrincipal = _loan.tranche[i].principalAmount;
                largestTrancheIdx = i;
            }
            unchecked {
                ++i;
            }
        }
        if (msg.sender != _loan.tranche[largestTrancheIdx].lender) {
            revert NotMainLenderError();
        }
        ERC20 asset = ERC20(_auction.asset); 
        uint256 totalOwed;
        for (uint256 i; i < _loan.tranche.length;) {
            if (i != largestTrancheIdx) {
                IMultiSourceLoan.Tranche calldata thisTranche = _loan.tranche[i];
                uint256 owed = thisTranche.principalAmount + thisTranche.accruedInterest
                    + thisTranche.principalAmount.getInterest(thisTranche.aprBps, block.timestamp - thisTranche.startTime);
                totalOwed += owed; 
                asset.safeTransferFrom(msg.sender, thisTranche.lender, owed);
@>              //@audit miss notice LoanManager if lender is LoanManager
            }
            unchecked {
                ++i;
            }
        }
        IMultiSourceLoan(_auction.loanAddress).loanLiquidated(_auction.loanId, _loan);

        asset.safeTransfer(_auction.originator, totalOwed.mulDivDown(_auction.triggerFee, _BPS));

        ERC721(_loan.nftCollateralAddress).transferFrom(address(this), msg.sender, _tokenId);

        delete _auctions[_nftAddress][_tokenId];

        emit AuctionSettledWithBuyout(_auction.loanAddress, _auction.loanId, _nftAddress, _tokenId, largestTrancheIdx);
    }
```
The above code repays other lenders, but it lacks notice the lender If the `lender` is a `LoanManager`, notify it to accounting using `ILoanManager(tranche.lender).loanRepayment()`  like in `MultiSourceLoan` or `LiquidationDistributor`.

It's crucial to notify the `LoanManager` for accounting. 
Failure to do so will result in an incorrect allocation of repaid assets to the correct `WithdrawalQueue`.

## Impact

The lack of notification to the `LoanManager` for accounting wil
LiquidationDistributor#distribute function lacks permission control
Audit: 2024-04-gondi-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e0917e18b036c/src/lib/LiquidationDistributor.sol#L32


# Vulnerability details

## Impact
An attacker can use the `distribute` function to call `Pool#loanLiquidation` to add any number of interest/fee to the Pool.

## Proof of Concept

The `loanLiquidation` function in Pool requires permission to access:

```solidity
    function loanLiquidation(
       ....
@>  ) 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);
    }

    modifier onlyAcceptedCallers() {
        if (!_acceptedCallers.contains(msg.sender)) {
            revert CallerNotAccepted();
        }
        _;
    }

```

modifier `onlyAcceptedCallers` indicates that` msg.sender` needs to be added to the trust list before it can be called.

The `distribute` function in the `LiquidationDistributor` contract is a public function and calls `Pool#loanLiquidation`:

distribute->_handleTrancheExcess->_handleLoanManagerCall:

```solidity
  function distribute(uint256 _proceeds, IMultiSourceLoan.Loan calldata _loan) external {
        ......
        if (_proceeds > totalPrincipalAndPaidInterestOwed + totalPendingInterestOwed) {
            for (uint256 i = 0; i < _loan.tranche.length;) {
                IMultiSourceLoan.Tranche calldata thisTranche = _loan.tranche[i];
@>                _handleTrancheExcess(
                    _loan.principalAddress,
                    thisTranche,
                    msg.sender,
                    _proceeds,
                    totalPrincipalAndPaidInterestOwed + totalPendingInterestOwed
                );
                unchecked {
                    ++i;
                }
            }
        } else {
            for (uint256 i = 0; i < _loan.tranche.length && _proceeds > 0;) {
                IMultiSourceLoan.Tranche calldata thisTranche = _loan.tranche[i];
@>              _proceeds = _handleTrancheInsufficient(
                    _loan.principalAddress, thisTranche, msg.sender, _proceeds, owedPerTranche[i]
                );
                unchecked {
                    ++i;
                }
            }
        }
    }

    function _handleTrancheExcess(....) private {
        uint256 excess = _proceeds - _totalOwed;
        /// Total = principal + accruedInterest +  pendingInterest + pro-rata remainder
        uint256 owed = _tranche.principalAmount + _tranche.accruedInterest
            + _tranche.principalAmount.getInterest(_tranche.aprBps, block.timestamp - _tranche.startTime);
        uint256 total = owed + excess.mulDivDown(owed, _totalOwed);
@>      _handleLoanManagerCall(_tranche, total);