xanoutas/solidity-security-findings huggingface dataset:
getLoanManager.updateOfferHandler() should be executed inside confirmOfferHandler()
Audit: 2024-05-gondi-mitigation-findings
Severity: MEDIUM

# Lines of code

https://github.com/pixeldaogg/florida-contracts/blob/b83b37bbe69325b12e3b8119dfd86eb86f16fc73/src/lib/loans/LoanManagerParameterSetter.sol#L70


# Vulnerability details

## Vulnerability details
https://github.com/pixeldaogg/florida-contracts/pull/387
This PR adds that the new `__offerHandler.getMaxDuration` can't be larger than the old one, and already avoids the problem of `getMinTimeBetweenWithdrawalQueues` being too large.

Also added using `LoanManagerParameterSetter.sol` to set `offerHandler`.

In two steps:
1. setOfferHandler() => set `ProposedOfferHandler = new offerHandler`
2. after the `UPDATE_WAITING_TIME` time expires, execute `confirmOfferHandler()` to make the `ProposedOfferHandler` effective.

But currently it works immediately, not after `UPDATE_WAITING_TIME`.

```solidity
    function setOfferHandler(address __offerHandler) external onlyOwner {
        __offerHandler.checkNotZero();

        if (IPoolOfferHandler(__offerHandler).getMaxDuration() > IPoolOfferHandler(getOfferHandler).getMaxDuration()) {
            revert InvalidInputError();
        }

        getProposedOfferHandler = __offerHandler;
        getProposedOfferHandlerSetTime = block.timestamp;
@>      ILoanManager(getLoanManager).updateOfferHandler(__offerHandler); //@audit call in  confirmOfferHandler()

        emit ProposedOfferHandlerSet(__offerHandler);
    }

    /// @notice Confirm the OfferHandler contract.
    /// @param __offerHandler The new OfferHandler address.
    function confirmOfferHandler(address __offerHandler) external onlyOwner {
        if (getProposedOfferHandlerSetTime + UPDATE_WAITING_TIME > block.timestamp) {
            revert TooSoonError();
        }
        if (getProposedOfferHandler != __offerHandler) {
            revert InvalidInputError();
        }

        getOfferHandler = __offerHandler;
        getProposedOfferHandler = address(0);
        getProposedOfferHandlerSetTime = type(uint256).max;

        emit OfferHandlerSet(__offerHandler);
    }

```
## Recommended Mitigation

```diff
    function setOfferHandler(address __offerHandler) external onlyOwner {
        __offerHandler.checkNotZero();

        if (IPoolOfferHandler(__offerHandler).getMaxDuration() > IPoolOfferHandler(getOfferHandler).getMaxDuration()) {
            revert InvalidInputError();
        }

        getProposedOfferHandler = __offerHandler;
        getProposedOfferHandlerSetTime = block.timestamp;
-       ILoanManager(getLoanManager).updateOfferHandler(__offerHandler);

        emit ProposedOfferHandlerSet(__offerHandler);
    }

    /// @notice Confirm the OfferHandler contract.
    /// @param __offerHandler The new OfferHandler address.
    function confirmOfferHandler(address __offerHandler) external onlyOwner {
        if (getProposedOfferHandlerSetTime + UPDATE_WAITING_TIME > block.timestamp) {
            revert TooSoonError();
        }
        if (getProposedOfferHandler != __offerHandler) {
            revert InvalidInputError();
M-12 Unmitigated
Audit: 2024-05-gondi-mitigation-findings
Severity: MEDIUM

# Lines of code

https://github.com/pixeldaogg/florida-contracts/blob/ac51cc6102fcf5ab274f8812eb585539332431f4/src/lib/LiquidationHandler.sol#L72


# Vulnerability details

https://github.com/pixeldaogg/florida-contracts/pull/384
This PR adds `getMaxExtension` to address the issue of auction times being indefinitely postponed.

The maximum time is: `block.timestamp` + _liquidationAuctionDuration + `getMaxExtension`

However, `_liquidationAuctionDuration` can be modified, with the current constraint being `_liquidationAuctionDuration < MAX_AUCTION_DURATION (7 days)`

```solidity
    function updateLiquidationAuctionDuration(uint48 _newDuration) external override onlyOwner {
@>      if (_newDuration < MIN_AUCTION_DURATION || _newDuration > MAX_AUCTION_DURATION) {
            revert InvalidDurationError();
        }
        _liquidationAuctionDuration = _newDuration;

        emit LiquidationAuctionDurationUpdated(_newDuration);
    }
```

Therefore, it's still possible that: `block.timestamp` + _liquidationAuctionDuration + `getMaxExtension` could exceed `7 Days`

## Recommended Mitigation

It is recommended to restrict `_liquidationAuctionDuration + ILoanLiquidator(liquidator).getMaxExtension < MAX_AUCTION_DURATION`

```diff
    function updateLiquidationAuctionDuration(uint48 _newDuration) external override onlyOwner {
-       if (_newDuration < MIN_AUCTION_DURATION || _newDuration > MAX_AUCTION_DURATION) {
+       if (_newDuration < MIN_AUCTION_DURATION || _newDuration + ILoanLiquidator(liquidator).getMaxExtension  > MAX_AUCTION_DURATION) {    
            revert InvalidDurationError();
        }
        _liquidationAuctionDuration = _newDuration;

        emit LiquidationAuctionDurationUpdated(_newDuration);
    }
```



## Assessed type

Context
H-17 Unmitigated
Audit: 2024-05-gondi-mitigation-findings
Severity: MEDIUM

# Lines of code




# Vulnerability details

https://github.com/pixeldaogg/florida-contracts/pull/390

This PR adds two methods to differentiate between the two:
```solidity
    function _checkAddNewTrancheOffer(RenegotiationOffer calldata _renegotiationOffer, uint256 _totalTranches)
        private
        pure
    {
        if (_renegotiationOffer.trancheIndex.length != 1 || _renegotiationOffer.trancheIndex[0] != _totalTranches) {
            revert InvalidRenegotiationOfferError();
        }
    }

    function _checkRefinanceFullRenegotiationOffer(
        RenegotiationOffer calldata _renegotiationOffer,
        uint256 _totalTranches
    ) private pure {
        if (_renegotiationOffer.trancheIndex.length != _totalTranches) {
            revert InvalidRenegotiationOfferError();
        }
    }
```

1. `addNewTranche()` need :
` _renegotiationOffer.trancheIndex.length == 1 && _renegotiationOffer.trancheIndex[0] == _loan.tranche.length`
2. `refinanceFull()` need:
`_renegotiationOffer.trancheIndex.length == _loan.tranche.length`

The above conditions all pass the check if `_loan.tranche.length==1`, it still conflicts.

Exmaple: user sign "_renegotiationOffer.trancheIndex = [1]" will pass both

## Recommended Mitigation

The best way is add `type`, eg:  `_renegotiationOffer.type = add | full | part`

simple way limit `_checkRefinanceFullRenegotiationOffer()` like:

```diff
    function _checkRefinanceFullRenegotiationOffer(
        RenegotiationOffer calldata _renegotiationOffer,
        uint256 _totalTranches
    ) private pure {
-       if (_renegotiationOffer.trancheIndex.length != _totalTranches) {
+       if (_renegotiationOffer.trancheIndex.length != 0 || _renegotiationOffer.trancheIndex[0] != 0) {    
            revert InvalidRenegotiationOfferError();
        }
    }
```


## Assessed type

Context
Upgraded Q -> 3 from #35 [1713794205670]
Audit: 2024-04-gondi-findings
Severity: HIGH

Judge has assessed an item in Issue #35 as 3 risk. The relevant finding follows:

 # Lines of code
https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e0917e18b036c/src/lib/loans/MultiSourceLoan.sol#L405

# Vulnerability details
## Impact
The attackers make it impossible for borrowers to repay their debts, and the collateral is liquidated when the debts mature.

## Proof of Concept
`repayLoan`, need to check the `loanId`, if the id is inconsistent will revert.

```solidity
    function repayLoan(LoanRepaymentData calldata _repaymentData) external override nonReentrant {
        uint256 loanId = _repaymentData.data.loanId;
        Loan calldata loan = _repaymentData.loan;
        .....
@>      _baseLoanChecks(loanId, loan);
        .....
    }
    
    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 problem is that `_loans[_loanId]` can change, for example, when `mergeTranches` delete the old `loanId` and write the new one.

```solidity
    _loans[loanId] = loanMergedTranches.hash();
    delete _loans[_loanId];
```

An attacker can use the `front-running` attack method, when `repayLoan` is called, execute the `mergeTranches` function in advance, and make the id in `_loans` updated. In this case, the `repayLoan` execution will fail due to inconsistent `_loanId`.

If the attacker keeps using this attack, the borrower's debt will not be repaid, eventually causing the collateral to be liquidated.

In addition to the `mergeTranches` function, the attacker can also call `addNewTranche`, and the borrower can also call the refinance-related function, again causing `_loanId` to be updated.

An attacker can also use the same method to attack `refinance` related functions, making refinance unable to execute.

An attacker can also use the same method to attack the `liquidateLoan` function, making it impossible for debts to be cleared.

## Tools Used
vscode, manual

## Recommended Mitigation Steps
Do not delete `_loanId`

## Assessed type
DoS
Upgraded Q -> 3 from #70 [1713692031143]
Audit: 2024-04-gondi-findings
Severity: HIGH

Judge has assessed an item in Issue #70 as 3 risk. The relevant finding follows:

 # L-9. Race condition when `block.timestamp == expirationTime`

[LiquidationHandler.sol#L91-L93](https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e0917e18b036c/src/lib/LiquidationHandler.sol#L91-L93)

```solidity
if (expirationTime > block.timestamp) { // @audit at expirationTime, loan can still be interacted
    revert LoanNotDueError(expirationTime);
}
```

[MultiSourceLoan.sol#L649-L651](https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e0917e18b036c/src/lib/loans/MultiSourceLoan.sol#L649-L651)

```solidity
// @audit can liquidate when block.timestamp == endTime as well, user could be the largest tranche and buyout in 1 TX
if (_loan.startTime + _loan.duration < block.timestamp) { 
    revert LoanExpiredError();
}
```

As shown, when `block.timestamp == expirationTime`, the loan could be liquidated but it is still be able to be interacted (refinance, renegotiation, repay, add more tranches, ...).

One of the possible impact is that if one user want to buyout the NFT, he can be the largest tranche (main lender) and buyout through auction. Both actions can be finished in 1 transaction.

---
A borrower can use a wrong collateral nft token to refinance from a new loan offer
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#L334
https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e0917e18b036c/src/lib/loans/MultiSourceLoan.sol#L322


# Vulnerability details

## Impact
A borrower can use a wrong collateral nft token to refinance from a new loan offer

## Proof of Concept
In MultiSourceLoan.sol, `refinanceFromLoanExecutionData()` flow allows an existing loan to be refinanced from a [new loan executionData](https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e0917e18b036c/src/lib/loans/MultiSourceLoan.sol#L309). 

However, there's no check on the new loan executionData requires the [same collateral nft tokenId](https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e0917e18b036c/src/lib/loans/MultiSourceLoan.sol#L334) from the existing loan(_loan.nftCollateralTokenId).

The vulnerability is that the check only ensure the principalAddress and nftCollateralAddress from the new executionData are the same as the existing loan, but didn't check for tokenId.

```solidity
    function refinanceFromLoanExecutionData(
        uint256 _loanId,
        Loan calldata _loan,
        LoanExecutionData calldata _loanExecutionData
    ) external nonReentrant returns (uint256, Loan memory) {
...
 |>       if (_loan.principalAddress != principalAddress || _loan.nftCollateralAddress != nftCollateralAddress) {
            revert InvalidAddressesError();
        }
...
```
(https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e0917e18b036c/src/lib/loans/MultiSourceLoan.sol#L322-L323)

## Tools Used

Manual

## Recommended Mitigation Steps
Add check to ensure that _loan.nftCollateralTokenId == _loanExecutionData.tokenId.


## Assessed type

Other
Lender can refinance a worse loan for the borrower through addNewTranche, due to missing checks
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-L353


# Vulnerability details

## Impact
Lender can refinance a worse loan for the borrower through addNewTranche, due to missing checks

## Proof of Concept
During refinance flows, when [a lender initiates](https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e0917e18b036c/src/lib/loans/MultiSourceLoan.sol#L177) the refinance call (refinancefull() or refinancePartial()), it is checked that the refinance offer is [better than existing loan](https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e0917e18b036c/src/lib/loans/MultiSourceLoan.sol#L181). This is checked in either _checkStrictlyBetter() or [_checkTranchesStrictly](https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e0917e18b036c/src/lib/loans/MultiSourceLoan.sol#L263) or in [addTrancheFromPartial()](https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e0917e18b036c/src/lib/loans/MultiSourceLoan.sol#L716), when [a new tranche](https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e0917e18b036c/src/lib/loans/MultiSourceLoan.sol#L288-L289) is appended.

When a borrower initiates the refinance call, the checks are relaxed. For example, in refinanceFromLoanExecutionData(), [borrower is checked to be the caller or signed the executiondata](https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e0917e18b036c/src/lib/loans/MultiSourceLoan.sol#L792-L794), in which case strictly better checks are not needed.

There is one exception in `addNewTranche()` flow, where the caller is not checked and can be initiated by either lender or borrower. However, if lender initiated the call, there is no check to ensure the added tranche or the combined loan will be better than before. Borrowers can be forced to take on a worse loan.

```solidity
//src/lib/loans/MultiSourceLoan.sol
    function addNewTranche(
        RenegotiationOffer calldata _renegotiationOffer,
        Loan memory _loan,
        bytes calldata _renegotiationOfferSignature
    ) external nonReentrant returns (uint256, Loan memory) {
...
          //@audit this check lender's signature, but it doesn't check if borrower makes the call or have approved the execution
|>        _checkSignature(_renegotiationOffer.lender, _renegotiationOffer.hash(), _renegotiationOfferSignature);
...
        uint256 newLoanId = _getAndSetNewLoanId();
        //@audit this add a newTranche based on the renegotiaonOffer, but doesn't check whether the combined new loan is better than before
|>      Loan memory loanWithTranche = _addNewTranche(newLoanId, _loan, _renegotiationOffer);
        _loans[newLoanId] = loanWithTranche.hash();
...
```
(https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fcdd2337dc80a8b5e0917e18b036c/src/lib/loans/MultiSourceLoan.sol#L364)
```solidity
    functi
Merging tranches could make `_loanTermination()` accounting incorrect 
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#L516


# Vulnerability details

## Impact
In the `Pool` contract, when a loan is repaid or liquidated, a call to the Pool is made for accounting. The `_loanTermination()` function is eventually invoked. This function uses the `loanId` to determine the withdrawal queue to which the loan belongs. If the loan was issued after the last queue, it belongs entirely to the pool, and `_outstandingValues` is updated. If not, it updates the queue accounting, queue outstanding values, `getTotalReceived` and `getAvailableToWithdraw`.


```solidity
function _loanTermination(
    ...
) private {
    uint256 pendingIndex = _pendingQueueIndex;
    uint256 totalQueues = getMaxTotalWithdrawalQueues + 1;
    uint256 idx;
    /// @dev oldest queue is the one after pendingIndex
    uint256 i;
    for (i = 1; i < totalQueues;) {
        idx = (pendingIndex + i) % totalQueues;
        if (getLastLoanId[idx][_loanContract] >= _loanId) {
            break;
        }
        unchecked {
            ++i;
        }
    }
    /// @dev We iterated through all queues and never broke, meaning it was issued after the newest one.
    if (i == totalQueues) {
        _outstandingValues =
            _updateOutstandingValuesOnTermination(_outstandingValues, _principalAmount, _apr, _interestEarned);
        return;
    } else {
        uint256 pendingToQueue =
            _received.mulDivDown(PRINCIPAL_PRECISION - _queueAccounting[idx].netPoolFraction, PRINCIPAL_PRECISION);
        getTotalReceived[idx] += _received;
        getAvailableToWithdraw += pendingToQueue;
        _queueOutstandingValues[idx] = _updateOutstandingValuesOnTermination(
            _queueOutstandingValues[idx], _principalAmount, _apr, _interestEarned
        );
    }
}
```

However, the `mergeTranches()` function is permissionless and only requires the merged tranches to be contiguous. Once tranches are merged, the `loanId` of the new tranche changes, which can lead to incorrect accounting in the `Pool`.


## Proof of Concept
Consider the following scenario:

1. A borrower opens a loan and takes liquidity from multiple offers of the same Pool. The loan has the parameters `loanId = 100`, with two tranches, both having `lender = pool_address`.
2. In the Pool, assume `getLastLoanId[1][loan] = 100`, indicating that queue index 1 points to the latest loanId in the `loan` contract.
3. An attacker calls `mergeTranches()` to merge the two tranches of `loanId = 100` with the same lender, which is the pool address. The new `newLoanId = 101` is used in the new tranche.
4. Now, when the loan is repaid, the `_loanTermination()` function is invoked with `_loanId = 101`. The loop returns `i == totalQueues`, making the loan belong entirely to the pool, while it should belong to withdrawal queue index 1.


[MultiSourceLoan.sol#L1132-L1140](https://github.com/code-423n4/2024-04-gondi/blob/b9863d73c08fc
Division before multiplication could lead to users losing 50% in WithdrawalQueue
Audit: 2024-04-gondi-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
In the `_getAvailable()` function, the calculation performs division before multiplication, which could result in precision loss. The consequence is that users may not be able to withdraw the amount they should receive, leaving some funds locked in the WithdrawalQueue.

```solidity
// @audit division before multiplication
function _getAvailable(uint256 _tokenId) private view returns (uint256) {
    return getShares[_tokenId] * _getWithdrawablePerShare() - getWithdrawn[_tokenId]; 
}

/// @notice Get the amount that can be withdrawn per share.
function _getWithdrawablePerShare() private view returns (uint256) {
    return (_totalWithdrawn + _asset.balanceOf(address(this))) / getTotalShares;
}
```


## Proof of Concept
Consider the following scenario:
```solidity
getShares[_tokenId] = 1e8
getWithdrawn[_tokenId] = 0
_totalWithdrawn = 0
_asset.balanceOf(address(this)) = 1e9 (1000 USDC)
getTotalShares = 5e8 + 1

```

The current calculation will yield
```solidity
_getWithdrawablePerShare() = 1e9 / (5e8 + 1) = 1
_getAvailable() = 1e8 * 1 - 0 = 1e8 = 100000000
```

However, the users should actually receive

```solidity
getShares[_tokenId] * _asset.balanceOf(address(this)) / getTotalShares
= 1e8 * 1e9 / (5e8 + 1) = 199999999
```

As shown, the users lose almost 50% of what they should receive.

## Tools Used
Manual Review

## Recommended Mitigation Steps
Change the order of calculation to multiply before division.



## Assessed type

Math
Function `distribute()` lacks access control allowing anyone to spam and disrupt the pool's accounting
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
The `LiquidationDistributor` contract manages the distribution of funds after a liquidation auction is settled. It distributes the received funds to the lenders of the loan. If the lender has implemented the `LoanManager` interface, it will also call `loanLiquidation()` on the lender's address. The Pool, when `loanLiquidation()` is called, will conduct an accounting process to ensure that the received funds are fairly distributed to the depositors.

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

However, the `distribute()` function lacks access control. Consequently, an attacker could directly call it with malicious data, leading to incorrect accounting in the Pool.


## Proof of Concept
Observe how the `loanLiquidation()` function is called
```solidity
function _handleLoanManagerCall(IMultiSourceLoan.Tranche calldata _tranche, uint256 _sent) private {
    if (getLoanManagerRegistry.isLoanManager(_tranche.lender)) {
        LoanManager(_tranche.lender).loanLiquidation(
            _tranche.loanId,
            _tranche.principalAmount,
            _tranche.aprBps,
            _tranche.accruedInterest,
            0,
            _sent,
            _tranche.startTime
        );
    }
}
```

As shown above, the `principalAddress` is not passed in, meaning it will not be validated by the Pool. Therefore, an attacker can simply call the `distribute()` function with `loan.principalAddress` set to a random ERC20 token. This token will still be transferred to the Pool. However, the Pool will mistake this token as its asset token (USDC / WETH) and perform the accounting accordingly.

## Tools Used
Manual Review

## Recommended Mitigation Steps
Only allow Loan contracts to call the `distribute()` function.



## Assessed type

Access Control