xanoutas/solidity-security-findings huggingface dataset:
Spam function `refinancePartial()` could DOS refinance by passing empty `_renegotiationOffer.trancheIndex` list
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#L257


# Vulnerability details

## Impact
The Gondi protocol has created an open market where anyone can refinance a loan, provided the new loan term is better than the previous one by a certain percentage (5-10%). To interact with an existing loan (either by refinancing or renegotiating), users need to input the `loanId`. The refinanced or renegotiated loan is then tracked by a new loan ID.

The function `refinancePartial()` allows anyone to refinance some tranches of an existing loan. The caller specifies which tranches to refinance by providing a `_renegotiationOffer.trancheIndex` list. This list can be empty. If the `_renegotiationOffer.trancheIndex` list is empty, a new loan ID is simply created to track the loan without altering any parameters of the existing loan.

This could be exploited by an attacker to prevent other users from interacting with a specific loan. The attacker could call `refinancePartial()` with an empty trancheIndex list, incurring no cost beyond the gas cost. If the attacker sees a transaction in the mempool attempting to refinance or renegotiate a loan, they can front-run the transaction and call the `refinancePartial()` function. This changes the loan ID, invalidating the offer in the other transaction and causing it to revert.

```solidity
function refinancePartial(RenegotiationOffer calldata _renegotiationOffer, Loan memory _loan)
    external
    returns (uint256, Loan memory)
{
    if (msg.sender != _renegotiationOffer.lender) {
        revert InvalidCallerError();
    }
    if (_isLoanLocked(_loan.startTime, _loan.startTime + _loan.duration)) {
        revert LoanLockedError();
    }

    uint256 loanId = _renegotiationOffer.loanId;
    _baseLoanChecks(loanId, _loan);
    _baseRenegotiationChecks(_renegotiationOffer, _loan);

    uint256 newLoanId = _getAndSetNewLoanId();
    uint256 totalProtocolFee;
    uint256 totalAnnualInterest;
    uint256 totalRefinanced;
    /// @dev bring to mem
    ImprovementMinimum memory minimum = _minimum;
    /// @dev We iterate over all tranches to execute repayments.
    for (uint256 i; i < _renegotiationOffer.trancheIndex.length;) {
        ...
    }

    /// @dev A partial refinance cannot reduce principal. If it's higher, we added as the most junior tranche.
    if (_renegotiationOffer.principalAmount < totalRefinanced) {
        revert InvalidRenegotiationOfferError();
    } else if (_renegotiationOffer.principalAmount > totalRefinanced) {
        _addTrancheFromPartial(_loan, _renegotiationOffer, totalRefinanced, totalAnnualInterest);
    }

    _handleProtocolFeeForFee(
        _loan.principalAddress, _renegotiationOffer.lender, totalProtocolFee, _protocolFee.recipient
    );

    _loans[newLoanId] = _loan.hash();
    delete _loans[loanId];

    /// @dev Here reneg fee is always 0
    emit LoanRefinanced(_renegotiationOffer.renegotiati
Function `refinanceFromLoanExecutionData()` does not check `executionData.tokenId == loan.nftCollateralTokenId`
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#L306


# Vulnerability details

## Impact
The `refinanceFromLoanExecutionData()` function is used to refinance a loan from LoanExecutionData. It allows borrowers to use outstanding offers for new loans to refinance their current loan. This function essentially combines two actions: it processes the repayment for the previous loan and then emits a new loan.

The key difference is that the same NFT is used as collateral for the new loan, so it does not need to be transferred out of the protocol and then transferred back in. However, there is no check to ensure that the NFT id of the old loan matches the NFT id of the new execution data.

Therefore, the new loan may have a collateral NFT that does not match the NFT that the lender requested in their offers.

```solidity
/// @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, // @audit No check if matched with loan.nftCollateralTokenId
    executionData.duration,
    offerExecution
);
```

## Proof of Concept
As we can see, `executionData.tokenId` is passed to the `_processOffersFromExecutionData()` function instead of `loan.nftCollateralTokenId`. This function performs all the checks to ensure that the lenders' offers accept this NFT.

```solidity
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) {
  ...
  _validateOfferExecution(
      thisOfferExecution,
      _tokenId,
      offer.lender,
      offer.lender,
      thisOfferExecution.lenderOfferSignature,
      protocolFee.fraction,
      totalAmount
  );
  ...
}
```

Eventually, it calls the `_checkValidators()` function to check the NFT token ID.
```solidity
function _checkValidators(LoanOffer calldata _loanOffer, uint256 _tokenId) private {
    uint256 offerTokenId = _loanOffer.nftCollateralTokenId;
    if (_loanOffer.nftCollateralTokenId != 0) {
        if (offerTokenId != _tokenId) {
            revert InvalidCollateralIdError();
        }
    } else {
        uint256 totalValidators = _loanOffer.validators.length;
        if (totalValidators == 0 && _tokenId != 0) {
            revert InvalidCollateralIdError();
        } else if ((totalValidators == 1) && (_loanOffer.validators[0].validator == address(0))) {
            return;
        }
        for (uint256 i = 0; i < tota
Attacker could exploit function `addNewTranche()` to increase principal amount without borrower's permission
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#L349


# Vulnerability details

## Impact
The `addNewTranche()` function allows the addition of more tranches to an existing loan. It appends a new tranche at the end of the loan's current tranche list.

```solidity
function _addNewTranche(
    uint256 _newLoanId,
    IMultiSourceLoan.Loan memory _loan,
    IMultiSourceLoan.RenegotiationOffer calldata _renegotiationOffer
) private view returns (IMultiSourceLoan.Loan memory) {
    uint256 newTrancheIndex = _loan.tranche.length;
    IMultiSourceLoan.Tranche[] memory tranches = new IMultiSourceLoan.Tranche[](newTrancheIndex + 1);

    /// @dev Copy old tranches
    for (uint256 i = 0; i < newTrancheIndex;) {
        tranches[i] = _loan.tranche[i];
        unchecked {
            ++i;
        }
    }

    tranches[newTrancheIndex] = IMultiSourceLoan.Tranche(
        _newLoanId,
        _loan.principalAmount,
        _renegotiationOffer.principalAmount,
        _renegotiationOffer.lender,
        0,
        block.timestamp,
        _renegotiationOffer.aprBps
    );
    _loan.tranche = tranches;
    _loan.principalAmount += _renegotiationOffer.principalAmount;
    return _loan;
}
```

There are no constraints on the new tranche, unlike the refinancing case. The new tranche doesn't need to have a lower APR or longer duration. Normally, the borrower should approve this, or only the borrower should be able to call the function. However, the `addNewTranche()` function lacks such checks. Consequently, anyone could call `addNewTranche()` and force the borrower to accept a take more principal with a high APR.

## Proof of Concept
The `addNewTranche()` function's only related check is the lender's signature, which in this case is the attacker. There is no check to ensure the borrower has approved the action to add a new tranche to their existing loan.
```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);
    ...
}
```

## Tools Used
Manual Review

## Recommended Mitigation Steps
Add a check to ensure that only the borrower of the loan can add a new tranche or borrower has sign a message to approve the new tranche.



## Assessed type

Access Control
`triggerFee` is stolen from other auctions during `settleWithBuyout()`
Audit: 2024-04-gondi-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
The function `settleWithBuyout()` is used to settle an auction with a buyout from the main lender. This lender needs to repay all other lenders and will receive the NFT collateral. Near the end of the function, the `triggerFee` is also paid to the auction originator. However, the funds used to pay this fee are taken directly from the contract balance, even though the main lender doesn't transfer these funds into the contract.
```solidity
function settleWithBuyout(
    ...
) external nonReentrant {
    ...
    // @note Repay other lenders
    ERC20 asset = ERC20(_auction.asset); 
    uint256 totalOwed;
    for (uint256 i; i < _loan.tranche.length;) {
        ...
    }
    IMultiSourceLoan(_auction.loanAddress).loanLiquidated(_auction.loanId, _loan);
    
    // @audit There is no fund in this contract to pay triggerFee
    asset.safeTransfer(_auction.originator, totalOwed.mulDivDown(_auction.triggerFee, _BPS)); 
    ...
}
```

As a result, if the auction contract balance is insufficient to cover the fee, the function will simply revert and prevent the main lender from buying out. In other cases where multiple auctions are running in parallel, the `triggerFee` will be deducted from the other auctions. This could lead to the last auctions being unable to settle due to insufficient balance.

## Proof of Concept
The function `settleWithBuyout()` is called before any `placeBid()` so the funds is only from main lender. In the `settleWithBuyout()`, there are 2 transfers asset. One is to pay other lenders and one is to pay the `triggerFee`. As you can see in the code snippet, there is no `triggerFee` transfer from sender to originator.

## Tools Used
Manual Review

## Recommended Mitigation Steps
Consider using `safeTransferFrom()` to pay the `triggerFee` from the sender's address, rather than using `safeTransfer()` to pay the `triggerFee` from the contract balance.



## Assessed type

Other
Function `settleWithBuyout()` does not call `LoanManager.loanLiquidation()` during a buyout
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


# Vulnerability details

## Impact
Lenders in the Gondi protocol could be EOA and Gondi Pool. Gondi Pool, an ERC4626, allows anyone to deposit funds and earn yield from lending on Gondi. Gondi Pool implemented the `LoanManager` interfaces, which include the `validateOffer()`, `loanRepayment()`, and `loanLiquidation()` functions. The functions `loanRepayment()` and `loanLiquidation()` are called when a borrower repays the loan or the loan is liquidated, i.e., when the Pool receives funds back from `MultiSourceLoan`. Both functions is used to update the queue accounting and the outstanding values of the Pool.

```solidity
ERC20 asset = ERC20(_auction.asset); 
uint256 totalOwed;
// @audit Repay lender but not call LoanManager.loanLiquidation()
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);
    }
    unchecked {
        ++i;
    }
}
IMultiSourceLoan(_auction.loanAddress).loanLiquidated(_auction.loanId, _loan);
```

In the `settleWithBuyout()` function, the main lender buys out the loan by repaying all other lenders directly. However, `loanLiquidation()` is not called, leading to incorrect accounting in the Pool.


## Proof of Concept
The `loanLiquidation()` function handles accounting in the pool.
https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e0917e18b036c/src/lib/pools/Pool.sol#L449-L463
```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);
}
```

## Tools Used
Manual Review

## Recommended Mitigation Steps
Consider checking and calling `loanLiquidation()` in `settleWithBuyout()` to ensure accurate accounting in the pool.



## Assessed type

Other
deployWithdrawalQueue() need clear _queueAccounting[lastQueueIndex]
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#L378


# Vulnerability details

## Vulnerability details

in `deployWithdrawalQueue()` only clear `_queueOutstandingValues[lastQueueIndex]` and `_outstandingValues`
but not clear `_queueAccounting[lastQueueIndex]`
```solidity
    function deployWithdrawalQueue() external nonReentrant {
...

        /// @dev We move outstaning values from the pool to the queue that was just deployed.
        _queueOutstandingValues[pendingQueueIndex] = _outstandingValues;
        /// @dev We clear values of the new pending queue.
        delete _queueOutstandingValues[lastQueueIndex];
        delete _outstandingValues;
@>      //@audit miss delete _queueAccounting[lastQueueIndex]

        _updateLoanLastIds();

@>      _pendingQueueIndex = lastQueueIndex;

        // Cannot underflow because the sum of all withdrawals is never larger than totalSupply.
        unchecked {
            totalSupply -= sharesPendingWithdrawal;
        }
    }

```

after this method , anyone call `queueClaimAll()` will use this stale data `_queueAccounting[lastQueueIndex]`

`queueClaimAll()` -> `_queueClaimAll(_pendingQueueIndex)`-> `_updatePendingWithdrawalWithQueue(_pendingQueueIndex)`
```solidity
    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;
            }
            uint256 pendingForQueue = totalReceived.mulDivDown(queueAccounting.thisQueueFraction, PRINCIPAL_PRECISION);
            totalReceived -= pendingForQueue;

            _pendingWithdrawal[secondIdx] = pendingForQueue;
            unchecked {
                ++i;
            }
        }
        return _pendingWithdrawal;
Incorrect circular array check in _updatePendingWithdrawalWithQueue flow , causing received funds added to the wrong queues
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#L643


# Vulnerability details

## Impact
Incorrect circular array check in _updatePendingWithdrawalWithQueue flow , causing received funds to be added to the wrong queues

## Proof of Concept
In Pool.sol, queueClaimAll flow will transfer received funds(returned funds from loans) for each queue to newer queues. 

Received funds for a given queue are intended to be [distributed to newer queues](https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e0917e18b036c/src/lib/pools/Pool.sol#L656-L661):
>        /// @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.

This logic is implemented in `_updatePendingWithdrawalWithQueue()`. Due to queue arrays are circular, the array index never exceeds `getMaxTotalWithdrawalQueues` and will restart from 0. ` % totalQueues` should be used when checking array indexes in most cases.

However, [in the queue index for-loop](https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e0917e18b036c/src/lib/pools/Pool.sol#L672C17-L672C57), `if (secondIdx == _cachedPendingQueueIndex + 1) {break;}` is used to break the loop instead of `secondIdx == (_cachedPendingQueueIndex + 1)%totalQueues`.  

This is problematic in some cases:

(1) When `_cachedPendingQueueIndex` < `getMaxTotalWithdrawalQueues`.
`_updatePendingWithdrawalWithQueue()` will always skip the oldest queue when distributing `getTotalReceived[_idx]` funds. 
In _`queueClaimAll()`, [the first for-loop start with the oldestQueueIndex](https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e0917e18b036c/src/lib/pools/Pool.sol#L693) ((_cachedPendingQueueIndex + 1) % totalQueues + 0)%totalQueues). When (_cachedPendingQueueIndex + 1) % totalQueues== _cachedPendingQueueIndex + 1, this first iteration will always result in a break in the [second for-loop](https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e0917e18b036c/src/lib/pools/Pool.sol#L662), where secondIdx == oldestQueueIndex == _cachedPendingQueueIndex + 1. 

As a result, any received funds from the oldesQueueIndex ( [getTotalReceived[oldestQueueIndex]](https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e0917e18b036c/src/lib/pools/Pool.sol#L648)) will not be distributed and directly [deleted](https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e0917e18b036c/src/lib/pools/Pool.sol#L654) (` getTotalReceived[_idx] = 0;`).

(2) When `_cachedPendingQueueIndex` == `getMaxTotalWithdrawalQueues`
The [second for-loop](https://g
Incorrect accounting of _pendingWithdrawal in queueClaiming flow
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

## Impact
Incorrect accounting of _pendingWithdrawal in queueClaiming flow, funds received from a previous queue index will be lost. 

## Proof of Concept
In Pool.sol - `queueClaimAll()`, each queue's received funds `getTotalReceived[_idx]` (total returned funds from loans for that queue) will be distributed to all newer queues in a for-loop.

There are two for-loops in this flow. [First for-loop](https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e0917e18b036c/src/lib/pools/Pool.sol#L692) iterate through each `pendingWithdrawal` index to get the received funds for that queue index (`getTotalReceived[_idx]`). [Second for-loop](https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e0917e18b036c/src/lib/pools/Pool.sol#L662) iterates through each queue index again to distribute funds from `_idx`. 

The problem is in the second for-loop, `_pendingWithdrawal[secondIdx]` will not accumulate distributed funds from previous queue indexes, instead it erases the value from previous loops and only records the last queue's received funds.

```solidity
//src/lib/pools/Pool.sol
    function _updatePendingWithdrawalWithQueue(
        uint256 _idx,
        uint256 _cachedPendingQueueIndex,
        uint256[] memory _pendingWithdrawal
    ) private returns (uint256[] memory) {
        uint256 totalReceived = getTotalReceived[_idx];
        uint256 totalQueues = getMaxTotalWithdrawalQueues + 1;
...
        getTotalReceived[_idx] = 0;
...
        for (uint256 i; i < totalQueues;) {
...
              //@audit this should be _pendingWithdraw[secondIdx] += pendingForQueue; Current implementation directly erases `pendingForQueue` value distributed from other queues. 
|>            _pendingWithdrawal[secondIdx] = pendingForQueue;
...
```
(https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e0917e18b036c/src/lib/pools/Pool.sol#L678)
Note that `getTotalReceived[_idx]` will be cleared before the for-loop(`getTotalReceived[_idx] = 0`), meaning that the erased pendingForQueue values from previous loops cannot be recovered.  _pendingWithdrawal will be incorrect.

## Tools Used
Manual

## Recommended Mitigation Steps
Change into `_pendingWithdrawal[secondIdx] + = pendingForQueue;`


## Assessed type

Error