xanoutas/solidity-security-findings huggingface dataset:
Attacker can front-run and pass in empty terms, making it impossible to `confirmTerms()`
Audit: 2024-04-gondi-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
In PoolOfferHandler, setting new terms requires two steps (`setTerms()` and `confirmTerms()`). The `setTerm()` function is `onlyOwner`, but anyone can call the `confirmTerms()` function. At the end of the `confirmTerms()` function, `pendingTermsSetTime` is set to `type(uint256).max`, preventing the function from being called again.

Since `confirmTerms()` uses the caller's input to execute the setup, an attacker could input empty `__terms` to prevent the owner from setting up new terms.


## Proof of Concept
```solidity
// @audit Anyone can front-run and pass in empty terms, making it impossible to confirmTerms
function confirmTerms(TermsKey[] calldata _termKeys, Terms[] calldata __terms) external { 
    if (block.timestamp - pendingTermsSetTime < NEW_TERMS_WAITING_TIME) {
        revert TooSoonError();
    }
    for (uint256 i = 0; i < __terms.length; i++) {
        if (_termKeys[i].duration > getMaxDuration) {
            revert InvalidDurationError();
        }
        uint256 pendingAprPremium = _pendingTerms[_termKeys[i].collection][_termKeys[i].duration][_termKeys[i]
            .maxSeniorRepayment][__terms[i].principalAmount];
        if (pendingAprPremium != __terms[i].aprPremium) {
            revert InvalidTermsError();
        }
        _terms[_termKeys[i].collection][_termKeys[i].duration][_termKeys[i].maxSeniorRepayment][__terms[i]
            .principalAmount] = __terms[i].aprPremium;
        delete _pendingTerms[_termKeys[i].collection][_termKeys[i].duration][_termKeys[i]
            .maxSeniorRepayment][__terms[i].principalAmount];
    }
    pendingTermsSetTime = type(uint256).max;

    emit TermsSet(_termKeys, __terms);
}
```

In this function, it loops through the input `__terms.length` list. If an attacker calls `confirmTerms()` with an empty `__terms` list, there will be no terms set, and `pendingTermsSetTime` is still set to `type(uint256).max`.

As a result, the owner will never be able to set up new terms for the pool because the attacker keeps spamming `confirmTerms()` when the `NEW_TERMS_WAITING_TIME` passes after the owner calls `setTerms()`.

## Tools Used
Manual Review

## Recommended Mitigation Steps
Record the `__terms` list in the `setTerms()` function to confirm terms instead of using input from caller.



## Assessed type

DoS
`Loan` struct hash does not include protocol fee
Audit: 2024-04-gondi-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e0917e18b036c/src/lib/utils/Hash.sol#L136


# Vulnerability details

## Impact
In Gondi, when a new loan is issued, all the loan parameters are represented in the `Loan` struct. Only the hash value of this struct is stored on-chain. Therefore, when users want to interact with an existing loan, they need to input the entire `Loan` struct. This is necessary when users want to refinance, or borrowers want to repay the loan.

However, the `protocolFee` is not included in the `Loan` struct hash. As a result, users could input an arbitrary `protocolFee` value when they interact with an existing loan to avoid the protocol fee. For example, when a borrower repays the loan, the `protocolFee` is taken from the interest paid by the borrower. But since the `loan.protocolFee` could be input as `0`, they can avoid this fee.

```solidity
if (withProtocolFee) {
    thisProtocolFee = newInterest.mulDivUp(loan.protocolFee, _PRECISION);
    totalProtocolFee += thisProtocolFee;
}
```

## Proof of Concept
Observe the function used to hash the `Loan` struct and the `Loan` struct itself. It is clear that the `protocolFee` is not included in the `Loan` struct hash.

```solidity
struct Loan {
    address borrower;
    uint256 nftCollateralTokenId;
    address nftCollateralAddress;
    address principalAddress;
    uint256 principalAmount;
    uint256 startTime;
    uint256 duration;
    Tranche[] tranche;
    uint256 protocolFee;
}

function hash(IMultiSourceLoan.Loan memory _loan) internal pure returns (bytes32) {
    bytes memory trancheHashes;
    for (uint256 i; i < _loan.tranche.length;) {
        trancheHashes = abi.encodePacked(trancheHashes, _hashTranche(_loan.tranche[i]));
        unchecked {
            ++i;
        }
    }
    return keccak256(
        abi.encode(
            _MULTI_SOURCE_LOAN_HASH,
            _loan.borrower,
            _loan.nftCollateralTokenId,
            _loan.nftCollateralAddress,
            _loan.principalAddress,
            _loan.principalAmount,
            _loan.startTime,
            _loan.duration,
            keccak256(trancheHashes)
        ) // @audit protocolFee is missing
    );
}
```

## Tools Used
Manual Review

## Recommended Mitigation Steps
Include `protocolFee` in the `Loan` struct hash.




## Assessed type

Other
Borrower signature could be reused in `emitLoan()`
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#L135


# Vulnerability details

## Impact
The function `emitLoan()` is used to issue a new loan. This function could be called directly by the borrower or by a random address if the borrower has signed the `LoanExecutionData`.

```solidity
function emitLoan(LoanExecutionData calldata _loanExecutionData)
    external
    nonReentrant
    returns (uint256, Loan memory)
{
    address borrower = _loanExecutionData.borrower;
    ExecutionData calldata executionData = _loanExecutionData.executionData;
    (address principalAddress, address nftCollateralAddress) = _getAddressesFromExecutionData(executionData);

    OfferExecution[] calldata offerExecution = executionData.offerExecution;

    // @audit Check borrower signature or borrower is caller
    _validateExecutionData(_loanExecutionData, borrower); 
    ...
}


function _validateExecutionData(LoanExecutionData calldata _executionData, address _borrower) private view {
    if (msg.sender != _borrower) {
        _checkSignature(
            _executionData.borrower, _executionData.executionData.hash(), _executionData.borrowerOfferSignature
        );
    }
    if (block.timestamp > _executionData.executionData.expirationTime) {
        revert ExpiredOfferError(_executionData.executionData.expirationTime);
    }
}
```

However, there isn't a check to ensure the signature for the same `LoanExecutionData` can't be used to execute `emitLoan()` more than once. As a result, if the borrower repays the loan, an attacker could call `emitLoan()` again to initiate a new loan.


## Proof of Concept
Consider this scenario:

1. Lender Alice has an offer with a capacity of 50 ETH.
2. Borrower Bob signs a signature to take a 10 ETH loan with his NFT.
3. After Bob repays the loan, anyone can call `emitLoan()` using the previous signature to force Bob to take the 10 ETH loan again. Since the capacity of Alice's offer is 50 ETH, the signature can be reused up to 5 times.


## Tools Used
Manual Review

## Recommended Mitigation Steps
Add a nonce to ensure a signature cannot be reused.



## Assessed type

Other
Inconsistent accounting of undeployedAssets might result in undesired optimal range in the pool
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#L398
https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e0917e18b036c/src/lib/pools/Pool.sol#L564-L565
https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e0917e18b036c/src/lib/pools/Pool.sol#L572


# Vulnerability details

## Impact
Inconsistent accounting of undeployedAssets might result in undesired optimal range in the pool

## Proof of Concept
undeployedAssets is calculated inconsistently. Currently in `_getUndeployedAssets()` the protocol collected fees are subtracted, however, in `validateOffer`, the protocol collected fees are not subtracted.

(1)`_getUndeployedAssets()`:This is called in `deployWithdrawalQueue()` to [calculate proRata liquid assets](https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e0917e18b036c/src/lib/pools/Pool.sol#L339) to the queue.contractAddress.
```solidity
    function _getUndeployedAssets() private view returns (uint256) {
        return asset.balanceOf(address(this)) + IBaseInterestAllocator(getBaseInterestAllocator).getAssetsAllocated()
|>            - getAvailableToWithdraw - getCollectedFees;
    }
```
(2)`uint256 undeployedAssets`: this is manually calculated in `validateOffer` flow, which is used check whether the pool has [enough undeployed Assets to cover loan.principalAmount](https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e0917e18b036c/src/lib/pools/Pool.sol#L407).
```solidity
    function validateOffer(bytes calldata _offer, uint256 _protocolFee) external override onlyAcceptedCallers {
...
        uint256 currentBalance = asset.balanceOf(address(this)) - getAvailableToWithdraw;
        uint256 baseRateBalance = IBaseInterestAllocator(getBaseInterestAllocator).getAssetsAllocated();
         //@audit getCollectedFees is not subtracted
|>        uint256 undeployedAssets = currentBalance + baseRateBalance;
        (uint256 principalAmount, uint256 apr) = IPoolOfferHandler(getUnderwriter).validateOffer(
            IBaseInterestAllocator(getBaseInterestAllocator).getBaseAprWithUpdate(), _offer
        );
...
```
(https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e0917e18b036c/src/lib/pools/Pool.sol#L398)
Note that in (2), undeployedAssets are inflated because `getCollectedFees` are fees protocol collected from liquidation/repayment flows and shouldn't be considered as liquid assets to cover the loan principal amount. 

(3)`_reallocate()`: This also manually calculate total undeployedassets amount, but again didn't account for `getCollectedFees`. `_reaalocate()` balances optimal target idle assets ratio by checking `currentBalance` / `total` ratio. Here `currentBalance` should be additionally subtracted by `getCollectedFees` because fees are set aside and shouldn't be considered idle. This affects [optimal range check](https://github.com/code-423n4/202
Target idle amount is incorrect in validateOffer flow, which can result in insufficient liquid asset in pool 
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

## Impact
Target idle amount is incorrect in `validateOffer()` flow, which can result in insufficient liquid asset in pool 

## Proof of Concept
In Pool:validateOffer, reallocating undeployed assets from BaseInterestAllocator to the pool is required if the pool doesn't have enough asset balance. 

However, the [target idle amount input](https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e0917e18b036c/src/lib/pools/Pool.sol#L411) for the reallocate call is incorrect. The reallocate will transfer the [delta of current idle and target idle](https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e0917e18b036c/src/lib/pools/AaveUsdcBaseInterestAllocator.sol#L70), which means target idle should be input as the desired amount of liquid assets in the pool. Current implementation input `principalAmount - currentBalance`, but it should be `principalAmount`.

```solidity
//src/lib/pools/Pool.sol
    function validateOffer(bytes calldata _offer, uint256 _protocolFee) external override onlyAcceptedCallers {
...
        } else if (principalAmount > currentBalance) {
//@audit principalAmount - currentBalance -> principalAmount
            IBaseInterestAllocator(getBaseInterestAllocator).reallocate(
|>                currentBalance, principalAmount - currentBalance, true
            );
        }
...
```
(https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e0917e18b036c/src/lib/pools/Pool.sol#L411)

When the pool needs more liquid asset, this may transfer liquid asset out of the pool, resulting in insufficient pool balance.

## Tools Used
Manual
## Recommended Mitigation Steps
Change to correct target idle amount



## Assessed type

Error
Any liquidators can pretend to be a loan contract to validate offers, due to insufficient validation
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#L392


# Vulnerability details

## Impact
Any liquidators can pretend to be a loan contract to validate offers, due to insufficient validation

## Proof of Concept
Accepted callers in a loan manager (e.g. Pool.sol) can be [either liquidators or loan contracts](https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e0917e18b036c/src/lib/loans/LoanManager.sol#L73). 

And only loan contracts should validate offers during a loan creation. However, the current access control check is insufficient in [pool::validateOffer](https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e0917e18b036c/src/lib/pools/Pool.sol#L392), which allows liquidators to pretend to be a loan contract, and [directly modify storage (`__outstandingValues`)](https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e0917e18b036c/src/lib/pools/Pool.sol#L414-L415) bypassing additional checks and accounting in a loan contract.
```solidity
//src/lib/pools/Pool.sol
    //@audit onlyAcceptedCallers only doesn't ensure caller is a loan contract
|>    function validateOffer(bytes calldata _offer, uint256 _protocolFee) external override onlyAcceptedCallers {
        if (!isActive) {
            revert PoolStatusError();
        }
...
```
(https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e0917e18b036c/src/lib/pools/Pool.sol#L392)

Current access control check([onlyAcceptedCallers](https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e0917e18b036c/src/lib/loans/LoanManager.sol#L58)) only ensures caller is accepted caller but doesn't verify the caller is a loan contract (`_isLoanContract(caller)`==true). 
```solidity
//src/lib/loans/LoanManager.sol
    modifier onlyAcceptedCallers() {
        if (!_acceptedCallers.contains(msg.sender)) {
            revert CallerNotAccepted();
        }
        _;
    }
```

When a liquidator calls `validateOffer`, they can provide a fabricated `bytes calldata _offer` and `uint256 _protocolFee` bypassing additional checks and state accounting in a loan contract. For example, in MultiSourceLoan.sol- `emitLoan`, [extra checks are implemented](https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e0917e18b036c/src/lib/loans/MultiSourceLoan.sol#L124) on LoanExecutionData to verify borrower and lender signatures and offer expiration timestamp as well as [transfer collateral NFT tokens](https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e0917e18b036c/src/lib/loans/MultiSourceLoan.sol#L153) and [recoding loan to storage](https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e0917e18b036c/src/lib/loans/MultiSourceLoan.sol#L155). All of the above can be skipped if a liquidator directly call `validateOffer` and modify `__outstandingValues` without token transfer.

#
The setTerms/confirmTerms in PoolOfferHandler can be attack
Audit: 2024-04-gondi-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
An attacker calls `confirmTerms` by passing only one `_termKeys`, leaving the other `_termKeys` unset.

## Proof of Concept
In `PoolOfferHandler`, `setTerms` is a function that can only have administrative calls,`confirmTerms` can be called anywhere.

The administrator calls `setTerms` and puts the argument in `_pendingTerms` with a time lock,

After the time lock expires, anyone can call `confirmTerms` to set the actual `_terms`.

```solidity
    function setTerms(TermsKey[] calldata _termKeys, Terms[] calldata __terms) external onlyOwner {
        if (_termKeys.length != __terms.length) {
            revert InvalidInputError();
        }
        for (uint256 i = 0; i < __terms.length; i++) {
            if (_termKeys[i].duration > getMaxDuration) {
                revert InvalidDurationError();
            }
            _pendingTerms[_termKeys[i].collection][_termKeys[i].duration][_termKeys[i].maxSeniorRepayment][__terms[i]
                .principalAmount] = __terms[i].aprPremium;
        }
        uint256 ts = block.timestamp;
        pendingTermsSetTime = ts;

        emit PendingTermsSet(_termKeys, __terms, ts);
    }

        function confirmTerms(TermsKey[] calldata _termKeys, Terms[] calldata __terms) external {
        if (block.timestamp - pendingTermsSetTime < NEW_TERMS_WAITING_TIME) {
            revert TooSoonError();
        }
        for (uint256 i = 0; i < __terms.length; i++) {
            if (_termKeys[i].duration > getMaxDuration) {
                revert InvalidDurationError();
            }
            uint256 pendingAprPremium = _pendingTerms[_termKeys[i].collection][_termKeys[i].duration][_termKeys[i]
                .maxSeniorRepayment][__terms[i].principalAmount];
            if (pendingAprPremium != __terms[i].aprPremium) {
                revert InvalidTermsError();
            }
@>          _terms[_termKeys[i].collection][_termKeys[i].duration][_termKeys[i].maxSeniorRepayment][__terms[i]
                .principalAmount] = __terms[i].aprPremium;
            delete _pendingTerms[_termKeys[i].collection][_termKeys[i].duration][_termKeys[i]
                .maxSeniorRepayment][__terms[i].principalAmount];
        }
@>      pendingTermsSetTime = type(uint256).max;

        emit TermsSet(_termKeys, __terms);
    }
```

The problem here is that a malicious caller, when calling confirmTerms`, can pass only one element in an array of parameters and ignore the rest.
`pendingTermsSetTime` will be set to `type(uint256).max` when the for loop is over,

The `setTerms` operation will fail, only a `_term` is set correctly, requiring the administrator to call `setTerms` again and wait for the time lock to end. After the administrator is reset, the attacker can still attack again.

## Tools Used
vscode, manual

## Recommended Mitigation Steps
```diff
-
In WithdrawalQueue, the user may not be able to withdraw token
Audit: 2024-04-gondi-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
In some cases, users are unable to withdraw token, resulting in the loss of user funds.

## Proof of Concept

`WithdrawalQueue` uses `_getAvailable` to calculate the token that a certain `_tokenId` can withdraw.

```solidity
    function _getAvailable(uint256 _tokenId) private view returns (uint256) {
        return getShares[_tokenId] * _getWithdrawablePerShare() - getWithdrawn[_tokenId];
    }
    
    function _getWithdrawablePerShare() private view returns (uint256) {
        return (_totalWithdrawn + _asset.balanceOf(address(this))) / getTotalShares;
    }
```

This function will get a negative number in some cases, but since it is uint256, it will revert.

`_getWithdrawablePerShare` gets a ratio based on balanceOf(this) and getTotalShares.

withdrawablePerShare is a variable value,
Suppose the value is 2 when the user first withdraw, and becomes 1 when the user again withdraw.
Since `getWithdrawn` is also calculated based on userShare * withdrawablePerShare,
getWithdrawn[_tokenId] is the number of tokens that have been withdraw.

```solidity
    uint256 available = _getAvailable(_tokenId);
    getWithdrawn[_tokenId] += available;
```

For the first withdraw, `getWithdrawn[_tokenId]` may be a larger value,
When withdraw again, `withdrawablePerShare` is reduced, so `userShare * withdrawablePerShare` is likely to be smaller than `getWithdrawn[_tokenId]`.

It will be revert, as a result, users can't get rewards.

## Tools Used
vscode, manual

## Recommended Mitigation Steps
Record the difference in share when two withdrawals are made,
Instead of recording the number of tokens that have been extracted


## Assessed type

Other
AuctionLoanLiquidator#placeBid can be DoS
Audit: 2024-04-gondi-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
The attacker performs a DoS attack on the `Bid` function, causing other users to be unable to participate and eventually obtaining the NFT at a low price.

## Proof of Concept

The placeBid function requires each bid to increase by 5% from the original, locking in for a period of time after each bid.

```solidity
function placeBid(address _nftAddress, uint256 _tokenId, Auction memory _auction, uint256 _bid)
        external
        nonReentrant
        returns (Auction memory)
    {
        _placeBidChecks(_nftAddress, _tokenId, _auction, _bid);

        uint256 currentHighestBid = _auction.highestBid;
        // MIN_INCREMENT_BPS = 10000, _BPS = 500 , add 5%
        if (_bid == 0 || (currentHighestBid.mulDivDown(_BPS + MIN_INCREMENT_BPS, _BPS) >= _bid)) {
            revert MinBidError(_bid);
        }

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

The problem here is that if the initial price increases from a very small value, the second increase in percentage only needs to be a very small amount.
For example: 100 wei -> 105 wei -> 110 wei

So an attacker can start with a small bid and keep growing slowly,
Because of the time lock, other users cannot participate for a period of time,
Normal users must wait until the time lock is over and the transaction needs to be executed before the attacker.

If normal users are unable to participate in the bidding, the attacker can obtain the auction item(NFT) at a very low price.

## Tools Used
vscode, manual

## Recommended Mitigation Steps
```diff
function placeBid(.....){
+       require (_bid > MIN_BID);
}
```


## Assessed type

DoS