xanoutas/solidity-security-findings huggingface dataset:
distribute() Use the wrong end time to break maxSeniorRepayment's expectations
Audit: 2024-04-gondi-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Vulnerability details

When the bid amount is not enough, the `lender` will be repaid in order of `tranche[]`.
In order to minimize the risk, the user can specify `maxSeniorRepayment` to avoid the risk to some extent, and put himself in a position of higher repayment priority.
At the same time `emitLoan()` checks `maxSeniorRepayment` for the `emitLoan()`

`emitLoan()` -> `_processOffersFromExecutionData()` -> `_checkOffer()`
```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) {
...
            uint256 amount = thisOfferExecution.amount;
            address lender = offer.lender;
            /// @dev Please note that we can now have many tranches with same `loanId`.
            tranche[i] = Tranche(loanId, totalAmount, amount, lender, 0, block.timestamp, offer.aprBps);
            totalAmount += amount;
@>          totalAmountWithMaxInterest += amount + amount.getInterest(offer.aprBps, _duration);
...

    function _checkOffer(
        LoanOffer calldata _offer,
        address _principalAddress,
        address _nftCollateralAddress,
        uint256 _amountWithInterestAhead
    ) private pure {
        if (_offer.principalAddress != _principalAddress || _offer.nftCollateralAddress != _nftCollateralAddress) {
            revert InvalidAddressesError();
        }
@>      if (_amountWithInterestAhead > _offer.maxSeniorRepayment) {
            revert InvalidTrancheError();
        }
    }
```

Note: `totalAmountWithMaxInterest` is computed using `loan._duration`

But when the bidding ends and the distribution is done `LiquidationDistributor.distribute()`
the current time is used to calculate `Interest`.
```solidity
    function distribute(uint256 _proceeds, IMultiSourceLoan.Loan calldata _loan) external {
        uint256[] memory owedPerTranche = new uint256[](_loan.tranche.length);
        uint256 totalPrincipalAndPaidInterestOwed = _loan.principalAmount;
        uint256 totalPendingInterestOwed = 0;
        for (uint256 i = 0; i < _loan.tranche.length;) {
            IMultiSourceLoan.Tranche calldata thisTranche = _loan.tranche[i];
            uint256 pendingInterest =
@>              thisTranche.principalAmount.getInterest(thisTranche.aprBps, block.timestamp - thisTranche.startTime);
            totalPrincipalAndPaidInterestOwed += thisTranche.accruedInterest;
            totalPendingInterestOwed += pendingInterest;
            owedPerTranche[i] += thisTranche.principalAmount + thisTranche.accruedInterest + pendingInterest;
            unchecked {
loan.hash() does not contain protocolFee
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#L117


# Vulnerability details

## Vulnerability details

The current `IMultiSourceLoan.loop.hash()` does not contain `protocolFee`

```solidity
    function emitLoan(LoanExecutionData calldata _loanExecutionData)
        external
        nonReentrant
        returns (uint256, Loan memory)
    {
...
@>      _loans[loanId] = loan.hash();
        emit LoanEmitted(loanId, offerIds, loan, totalFee);

        return (loanId, loan);
    }

    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,
@>              //@audit miss protocolFee
                keccak256(trancheHashes)
            )
        );
    }

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


```

Then you can specify `protocolFee` arbitrarily in many methods, but the `_baseLoanChecks()` security check doesn't `revert`.

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

Example:`repayLoan(loadn.protocolFee=0)` to escape `fees` and cause a `LoanManager` accounting error
`refinancePartial()/refinanceFull()` can also specify the wrong `fees` to skip the fees

## Impact
The loan hash does not contain a `protocolFee`, leading to an arbitrary `protocolFee` that can be specified to escape `fees` or cause a accounting error

## Recommended Mitigation

```diff
    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,
                _
confirmTerms() DOS
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

## Vulnerability details
in `PoolOfferHandler`
If new `_terms[]` need to be set up
First, the administrator needs to submit `_pendingTerms[]`
```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);
    }
```

Then wait for `NEW_TERMS_WAITING_TIME`.
Anyone can then execute `confirmTerms()` to make `_pendingTerms[]` effective
```solidity
    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 above method does not have permission control and does not limit the length of `__terms`.
So if a malicious user front-run execute `confirmTerms()`, submits `__terms.length==0`, `pendingTermsSetTime` will be invalidated, and `_pendingTerms[]` will be invalidated as well.
The administrator needs to resubmit `_pendingTerms[]`, which is also possible by DOS.

## Impact
`confirmTerms()` can be DOS , and `_terms[]` can never be added.

## Recommended Mitigation

Only administrators can execute `confirmTerms()`.

```diff
-   function confirmTerms(TermsKey[] calldata _termKeys, Terms[] calldata __terms) external {
+   function confirmTerms(TermsKey[] calldata _termKeys, Terms[] calldata __terms) extern
distribute() when can't repay all lenders, may lack of notification to LoanManager for accounting
Audit: 2024-04-gondi-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Vulnerability details

The `LiquidationDistributor` is used to distribute funds after an auction. 
When the auction amount is insufficient, lenders are repaid in sequence.

```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;
                }
            }
        }
    }
```
The code snippet above introduces a condition `_proceeds > 0` to terminate the loop when there's no remaining balance in `_proceeds`, thereby preventing further execution of `_handleTrancheInsufficient()`.

However, this approach creates an issue: if the subsequent lender is a `LoanManager`, it won't be notified for accounting via `_handleTrancheInsufficient()`->`_handleLoanManagerCall()` -> `LoanManager(_tranche.lender).loanLiquidation()`

Although no funds can be repaid, accounting is still necessary to notice and prevent incorrect accounting. causing inaccuracies in `totalAssets()` and continued accumulation of interest.
This outstanding debt should be shared among current users and prevent it from persisting as bad debt.

## Impact

Failure to notify `LoanManager.loanLiquidation()` may result in accounting inaccuracies.

## Recommended Mitigation

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


## Asses
placeBid() malicious low bidding
Audit: 2024-04-gondi-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Vulnerability details

 Users who need to bid can do so with the method: `AuctionLoanLiquidator.placeBid()`

```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;
        if (_bid == 0 || (currentHighestBid.mulDivDown(_BPS + MIN_INCREMENT_BPS, _BPS) >= _bid)) {
            revert MinBidError(_bid);
        }
...

@>      _auctions[_nftAddress][_tokenId] = _auction.hash();

        emit BidPlaced(_nftAddress, _tokenId, newBidder, _bid, _auction.loanAddress, _auction.loanId);
        return _auction;
    }
```

 There are two important checks
1. auctions[_nftAddress][_tokenId] == _auction.hash() ( whether the param `_auction` is legal or not)
2. this bid amount `bid` is > 5% over `highestBid` ( bid > highestBid * 10500 / 10000)

These two conditions can be easily underbid by a user
Users just need to monitor `mempool`, front-run `placeBid()` starting from bid=1.
Example:
1. block = 1 , front-run `placeBid(_auction, bid=1)`  --> pass   ( 1 > (0 * 10500 / 10000))

2. block = 2 , front-run `placeBid(_auction, bid=2)`  --> pass   ( 2 > (1 * 10500 / 10000))

3. block = 3 , front-run `placeBid(_auction, bid=3)`  --> pass   ( 3 > (2 * 10500 / 10000))
...

Each front-run, causes subsequent bids on the same `block` to fail, regardless of the user's bid amount, because the first one changes the `hash` : `_auctions[_nftAddress][_tokenId] = _auction.hash();`, resulting in the other users not being able to pass the `hash` check

## Impact
Malicious front-run low bidding

## Recommended Mitigation

Suggestions:
1. `Auction` is saved in `storage`
2. `placeBid()` pass in `auction id`


## Assessed type

DoS
Bidders might lose funds due to possible racing condition between settleWithBuyout and placeBid
Audit: 2024-04-gondi-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
Bidder might lose funds due to possible racing condition between settleWithBuyout and placeBid.

## Proof of Concept
In AuctionWithBuyoutLoanLiquidator.sol, settleWithBuyout and placeBid are allowed at an overlapping timestamp (`_auction.startTime + _timeForMainLenderToBuy`). This allows settleWithBuyout and placeBid to be settled at the same block.

When placeBid tx settles at `_auction.startTime + _timeForMainLenderToBuy` before `settleWithBuyout` tx, the bidder will lose their funds. Because `settleWithBuyout` will always assume no bids are placed, it will directly transfer out the collateral NFT token and [delete the auction data](https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e0917e18b036c/src/lib/AuctionWithBuyoutLoanLiquidator.sol#L101) from storage. 

```solidity
    function settleWithBuyout(
        address _nftAddress,
        uint256 _tokenId,
        Auction calldata _auction,
        IMultiSourceLoan.Loan calldata _loan
    ) external nonReentrant {
...
        uint256 timeLimit = _auction.startTime + _timeForMainLenderToBuy;
 |>       if (timeLimit < block.timestamp) {
            revert OptionToBuyExpiredError(timeLimit);
        }
...
```
(https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e0917e18b036c/src/lib/AuctionWithBuyoutLoanLiquidator.sol#L63C1-L66C10)

```solidity
    function _placeBidChecks(address _nftAddress, uint256 _tokenId, Auction memory _auction, uint256 _bid)
        internal
        view
        override
    {
...
        uint256 timeLimit = _auction.startTime + _timeForMainLenderToBuy;
|>        if (timeLimit > block.timestamp) {
            revert OptionToBuyStilValidError(timeLimit);
        }
```
(https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e0917e18b036c/src/lib/AuctionWithBuyoutLoanLiquidator.sol#L129)

## Tools Used
Manual

## Recommended Mitigation Steps

Consider only allow buyout strictly before the timeLimit ` if (timeLimit <= block.timestamp) {//revert`.


## Assessed type

Other
An udpated liquidationAuctionDuration parameter might DOS placing bid in AuctionWithBuyoutLoanLiquidator.sol
Audit: 2024-04-gondi-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
An udpated `liquidationAuctionDuration` parameter might DOS placing bid in AuctionWithBuyoutLoanLiquidator.sol

## Proof of Concept
In AuctionWithBuyoutLoanLiquidator.sol, placing bid is only allowed [after the settleWithBuyout window](https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e0917e18b036c/src/lib/AuctionWithBuyoutLoanLiquidator.sol#L128). 

The vulnerability is that (1) AuctionWithBuyoutLoanLiquidator.sol is intended to work with [multiple loan contracts](https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e0917e18b036c/src/lib/AuctionLoanLiquidator.sol#L126) with potentially different [liquidationAuctionDuration](https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e0917e18b036c/src/lib/LiquidationHandler.sol#L72) parameter set-up; (2) There is no way to ensure any `liquidationAuctionDuration` will be strictly greater than the buyout window.

This means that if any loan contract's `liquidationAuctionDuration` is updated to be lower than `_timeForMainLenderToBuy`. No bids can be placed. After the buyout window, aution [would have already expired](https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e0917e18b036c/src/lib/AuctionLoanLiquidator.sol#L238-L239).

```solidity
//src/lib/AuctionWithBuyoutLoanLiquidator.sol
    function _placeBidChecks(address _nftAddress, uint256 _tokenId, Auction memory _auction, uint256 _bid)
        internal
        view
        override
    {
...
        //@audit placeBid check is against the buyout window limit, when the auction duration is smaller than the buyout window, user cannot place bid.
|>      uint256 timeLimit = _auction.startTime + _timeForMainLenderToBuy;
        if (timeLimit > block.timestamp) {
            revert OptionToBuyStilValidError(timeLimit);
        }

```
(https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e0917e18b036c/src/lib/AuctionWithBuyoutLoanLiquidator.sol#L128C1-L128C74)
```solidity
//
    function placeBid(address _nftAddress, uint256 _tokenId, Auction memory _auction, uint256 _bid)
...
//@audit _auction.duration is _liquidationAuctionDuration, auction would expired before bidding is allowed after the buyout window.
        uint96 expiration = _auction.startTime + _auction.duration;
...
```
(https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e0917e18b036c/src/lib/AuctionLoanLiquidator.sol#L235)

## Tools Used
Manual
## Recommended Mitigation Steps
In AuctionWithBuyoutLoanLiquidator.sol, consider adding liquidationAuctionDuration in addition to the buy-out window _timeForMainLenderToBuy, because during the buy-out no bids can be placed anyway.


## Assessed type

Other
Hardcoded incorrect getLidoData timestamp, resulting in incorrect base point Apr. Loans can be validated with a substantially low baseRate interest 
Audit: 2024-04-gondi-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
Hardcoded incorrect getLidoData timestamp, resulting in incorrect base point Apr. Loans can be validated with a substantially low baseRate interest 

## Proof of Concept
In LidoEthBaseInterestAllocator.sol, [getLidoData](https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e0917e18b036c/src/lib/pools/LidoEthBaseInterestAllocator.sol#L62) is initialized with an incorrect timestamp, causing subsequent baseApr to be incorrect.

In constructor, `getLidoData` is initialized with `0` timestamp. This should be block.timestamp instead. As a result, baseApr updates will be much lower.
```solidity
//src/lib/pools/LidoEthBaseInterestAllocator.sol

    struct LidoData {
        uint96 lastTs;
        uint144 shareRate;
        uint16 aprBps;
    }
...
    constructor(
        address _pool,
        address payable __curvePool,
        address payable __weth,
        address __lido,
        uint256 _currentBaseAprBps,
        uint96 _lidoUpdateTolerance
    ) Owned(tx.origin) {
...
          //@audit 0-> block.timestamp
|>        getLidoData = LidoData(0, uint144(_currentShareRate()), uint16(_currentBaseAprBps));
...
```
(https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e0917e18b036c/src/lib/pools/LidoEthBaseInterestAllocator.sol#L62)

For example, in `_updateLidoValue()`, `_lidoData.aprBps` is [calculated based on delta shareRate, divided by delta timespan](https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e0917e18b036c/src/lib/pools/LidoEthBaseInterestAllocator.sol#L162-L164). (`_BPS * _SECONDS_PER_YEAR * (shareRate - _lidoData.shareRate) / _lidoData.shareRate
                / (block.timestamp - _lidoData.lastTs)`) `shareRate` and `_lidoData.shareRate` are associated with current timestamp, and deployment timestamp respectively. But timespan would be (block.timestamp - 0). This deflated _lidoData.aprBps value, which is used to [validate Loan offers](https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e0917e18b036c/src/lib/pools/Pool.sol#L400) in Pool.sol during loan initiation. 

In PoolOfferHandler.sol, this allows loan offers with substantially low `aprBps` to pass the [minimal apr check](https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e0917e18b036c/src/lib/pools/PoolOfferHandler.sol#L165).

```solidity
//src/lib/pools/PoolOfferHandler.sol
    function validateOffer(uint256 _baseRate, bytes calldata _offer)
        external
        view
        override
        returns (uint256 principalAmount, uint256 aprBps)
    {
...
        if (offerExecution.offer.aprBps < _baseRate + aprPremium || aprPremium == 0) {
            revert InvalidAprError();
        }
...
```
(https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e