xanoutas/solidity-security-findings huggingface dataset:
PriceOracle missing checks for whether L2 Sequencer is active
Audit: 2024-07-benddao-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-07-benddao/blob/117ef61967d4b318fc65170061c9577e674fffa1/src/PriceOracle.sol#L118


# Vulnerability details

## Vulnerability details
The protocol will be deployed in L2
> Chains the protocol will be deployed on : Ethereum, Arbitrum, Optimism, Polygon

But currently `PriceOracle.sol` does not check that `L2 Sequencer is active`

Chainlink recommends that users using price oracles, check whether the l2 sequencer is active

https://docs.chain.link/data-feeds#l2-sequencer-uptime-feeds

If the sequencer goes down, the index oracles may have stale prices, since L2-submitted transactions (i.e. by the aggregating oracles) will not be processed.

## Impact

Stale prices, e.g. if token were to de-peg while the sequencer is offline, stale price is used and can result in false liquidation or over-borrowing.

## Recommended Mitigation

Use sequencer oracle to determine whether the sequencer is offline or not, and don't allow orders to be executed while the sequencer is offline.


## Assessed type

Oracle
Fee-on-Transfer tokens cause problems in multiple places 
Audit: 2024-07-benddao-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-07-benddao/blob/main/src/libraries/logic/VaultLogic.sol#L428-L438


# Vulnerability details


## Description

The protocol is designed to ensure it is compatible with fee-on-transfer tokens (Fee-on-transfer is in scope). For example, the `erc20TransferInLiquidity()` function checks the balance before and after, and calculates the difference between these values to measure the tokens received

```solidity
File: VaultLogic.sol

448:   function erc20TransferInBidAmount(DataTypes.AssetData storage assetData, address from, uint256 amount) internal {
449:     address asset = assetData.underlyingAsset;
450:     uint256 poolSizeBefore = IERC20Upgradeable(asset).balanceOf(address(this));
451: 
452:     assetData.totalBidAmout += amount;
453: 
454:     IERC20Upgradeable(asset).safeTransferFrom(from, address(this), amount);
455: 
456:     uint256 poolSizeAfter = IERC20Upgradeable(asset).balanceOf(address(this));
457:     require(poolSizeAfter == (poolSizeBefore + amount), Errors.INVALID_TRANSFER_AMOUNT);
458:   }
```
But at the end of the logic, it has a required check if the contract received the exact intended amount 
 these two features are not compatible If the `asset` is Fee-on-transfer tokens the requirement will keep revert

Example:
1- Contract balanceOf is 2000 of Fee-on-transfer tokens
2- Contract transfer from user `amount == 1000` 
3- Fee is collected, so the contract received only 950 tokens, But the require will check ** 2000 + 1000 ==  2950 ** and revert

## Impact

Any function/feature that calls 
- `erc20TransferInLiquidity()`
- `erc20TransferOutLiquidity()`
- `erc20TransferBetweenWallets()`
- `erc20TransferOutBidAmount()`
- `erc20TransferInBidAmount()`
in `VaultLogic.sol`
will revert if the asset is Fee-on-transfer tokens

## Recommended Mitigation Steps

When fee-on-transfer tokens should be supported, you need to check the actual balance differences but don't expect to receive the exact amount. If they are not supported, this should be documented.


## Assessed type

ERC20
User are forced to borrow again in order to unlock their NFTs from `IsolateLending.sol`
Audit: 2024-07-benddao-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-07-benddao/blob/main/src/libraries/logic/IsolateLogic.sol#L356-L357


# Vulnerability details


## Description

Isolated Margin Lending the BendDAO protocol will calculate the heath factor based on a single collateral (one NFT), the liquidation of collateral will be achieved by selling the NFT through an auction mechanism.

However, the borrower who gets liquidated (his NFT is in the auction state) still able to repay his loan and close the auction, by triggering `IsolateLiquidation.sol#isolateRedeem()`.  

Each asset has a redemption threshold `redeemThreshold` to define the minimum debt repayment amount by the borrower

```solidity
Filse: IsolateLogic.sol#executeIsolateRedeem()

vars.redeemAmounts[vars.nidx] = vars.borrowAmount.percentMul(nftAssetData.redeemThreshold);
```
The `redeemThreshold` is updateable from `Configurator.sol#setAssetAuctionParams()` with a max value of **10_000**

In case the user calls `IsolateLiquidation.sol#isolateRedeem()`
and `nftAssetData.redeemThreshold` is 10_000 **(MAX_REDEEM_THRESHOLD)**
this will be set `scaledAmount ` to zero

```solidity
File: IsolateLogic.sol#executeIsolateRedeem()

       loanData.scaledAmount -= vars.amountScaled;
```
This means the debt is fully paid 
However, 
1- the `loanData.loanStatus` will still be in active state `Constants.LOAN_STATUS_ACTIVE`
2- the `erc721TokenData.LockerAddr` is not updated to address(0), 

So, the user is not able to withdraw his NFT from `BVault.sol` or even update the loan state and `LockerAddr` values through `IsolateLending.sol#isolateRepay()` due to [this check](https://github.com/code-423n4/2024-07-benddao/blob/main/src/libraries/logic/ValidateLogic.sol#L498-L500)

Note: the only way the user can unlock his NFT is by borrowing again, this will increase `loanData.scaledAmount` again to greater than zero, and then he needs to repay that loan  `IsolateLending.sol#isolateRepay()` to update the `erc721TokenData.LockerAddr` to address(0).

and this will expose the user to the unnecessary risk of being liquidated again.

## Impact

Users are forced to borrow again to unlock their NFTs After Isolate Redeem

## Proof of Concept

Foundry PoC:
Please copy the following POC in `TestIntIsolateRedeem.t.sol`
```solidity
  function test_Withdraw_After_Full_Redeem_Should_Revert() public {
    TestCaseLocalVars memory testVars;

    tsHEVM.startPrank(tsPoolAdmin);
    tsConfigurator.setAssetAuctionParams(tsCommonPoolId, address(tsBAYC), 10000, 500, 2000, 1 days);
    tsHEVM.stopPrank();
    // deposit
    prepareUSDT(tsDepositor1);
    uint256[] memory tokenIds = prepareIsolateBAYC(tsBorrower1);

    // borrow
    prepareBorrow(tsBorrower1, address(tsBAYC), tokenIds, address(tsUSDT));

    // make some interest
    advanceTimes(365 days);

    // drop down nft price
    actionSetNftPrice(address(tsBAYC), 5000);

    // auction
    prepareAuction(tsLiquidator1, address(tsBAYC), tokenIds, address(tsUSDT));

    uint256[] mem
`isolateRedeem()` revert in case Revert-on-zero-value-transfers tokens
Audit: 2024-07-benddao-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-07-benddao/blob/main/src/libraries/logic/IsolateLogic.sol#L346-L354


# Vulnerability details


## Description

One of the features of this protocol is that the borrower can redeem his loan (under the **Isolate Lending**) after his loan goes into auction state (Before the end of the auction) by simply invoking `IsolateLiquidation.sol#isolateRedeem()`, However in order to keep the liquidators incentivized to lunch the auction for any bad debt, the borrower could get forced to pay them some fee (called `bidFine`)

The `bidFine` is defined by two factors `bidFineFactor` and `minBidFineFactor`, both of them are updatable from `Configurator.sol#setAssetAuctionParams()`, 
So, in case admin set them to zero

 
When the  borrower tries to redeem his loan this logic from `IsolateLogic.sol#executeIsolateRedeem()`
```solidity
File: IsolateLogic.sol#executeIsolateRedeem()

328:       (, vars.bidFines[vars.nidx]) = GenericLogic.calculateNftLoanBidFine(
329:         debtAssetData,
330:         debtGroupData,
331:         nftAssetData,
332:         loanData,
333:         vars.priceOracle
334:       );

```
will set the value of `vars.bidFines[vars.nidx]` to zero

After that, the flow will enter this IF block to transfer the `bidFine` to the liquidator who launched the auction.

```solidity
File: IsolateLogic.sol#executeIsolateRedeem()

346:       if (loanData.firstBidder != address(0)) {
347:         // transfer bid fine from borrower to the first bidder
348:         VaultLogic.erc20TransferBetweenWallets(
349:           params.asset,
350:           params.msgSender,
351:           loanData.firstBidder,
352:           vars.bidFines[vars.nidx]
353:         );
354:       }
```
but in case `params.asset` is one of Revert-on-Zero-Value-Transfers tokens (are in scope)
the transaction will revert because it is trying to transfer zero value `vars.bidFines[vars.nidx] == 0`  


## Impact

The borrower will never be able to redeem his loan after his loan goes into auction state

## Proof of Concept

## Tools Used

## Recommended Mitigation Steps
```diff
File: IsolateLogic.sol#executeIsolateRedeem()

- 346:       if (loanData.firstBidder != address(0)) {
+ 346:       if (loanData.firstBidder != address(0) && vars.bidFines[vars.nidx] > 0) {
347:         // transfer bid fine from borrower to the first bidder
348:         VaultLogic.erc20TransferBetweenWallets(
349:           params.asset,
350:           params.msgSender,
351:           loanData.firstBidder,
352:           vars.bidFines[vars.nidx]
353:         );
354:       }
```


## Assessed type

ERC20
Malicious liquidator can guarantee to gain the Isolate Auction
Audit: 2024-07-benddao-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-07-benddao/blob/main/src/libraries/logic/IsolateLogic.sol#L261-L263


# Vulnerability details


## Description
This protocol gives liquidators to liquidate loans backed by NFTs (called isolate lending) using English auction in a predefined auction. 

with [Blocklists](https://github.com/d-xo/weird-erc20#tokens-with-blocklists) tokens In scope 
```
Some tokens (e.g. USDC, USDT) have a contract level admin controlled address blocklist. If an address is blocked, then transfers to and from that address are forbidden.
```

Any malicious liquidator can trigger `IsolateLiquidation.sol#isolateAuction()` function to set him-selfe the `lastBidder`

```solidity
File: IsolateLogic.sol#isolateAuction()

257:       // record last bid state
258:       loanData.lastBidder = params.msgSender;

```
Then he can get maliciously enters the blacklist in `underlyingAsset` e.g.,USDC by doing wired things (this depends on the mechanism of Blacklising on every token)

Now, no one can call `IsolateLiquidation.sol#isolateAuction()` to participate in the auction. Also, the borrower can't call `IsolateLiquidation.sol#isolateRedeem()`.

The liquidator needs just sets and waits until the end of the auction to call `IsolateLiquidation.sol#isolateLiquidate()`. 
the only thing he needs to make sure that the flow is not to enter this IF block 

```solidity
       File: IsolateLogic.sol#executeIsolateLiquidate()

        if (vars.totalExtraAmount > 0) {
          // transfer underlying asset from liquidator to pool
          VaultLogic.erc20TransferInLiquidity(debtAssetData, params.msgSender, vars.totalExtraAmount);
        }
```

## Impact

The malicious liquidator will guarantee his win of the Isolate Auction

## Proof of Concept

## Tools Used

Manual Review

## Recommended Mitigation Steps

In case **Blacklists tokens** don't directly transfer the token to the liquidator, consider storing the number of tokens in storage and having the liquidator claim it at a later time.
Or implement your own `safeTransfer()` function and only if it reverts store the number of tokens in storage otherwise send it.



## Assessed type

ERC20
The new owner of NFT can't trigger `delegateERC721()` to re-set the delegation
Audit: 2024-07-benddao-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-07-benddao/blob/main/src/libraries/logic/LiquidationLogic.sol#L433-L441
https://github.com/code-423n4/2024-07-benddao/blob/main/src/libraries/logic/IsolateLogic.sol#L474-L478


# Vulnerability details


## Description

The protocol is designed to accept NFTs as collateral due to that, The BendDAO gives the users the ability to delegate their NFTs for ownership checks in third-party projects e.g. airdrop claims and token ownership by calling The `BVault.sol#delegateERC721()` (This feature needs third-party projects to support it)

When a user supplies an NFT as collateral by invoking `BVault.sol#depositERC721()` the below logic will set the `owner` to the user address (to keep tracking it internally) in both supply modes (CROSS & ISOLATE)

```solidity
File: VaultLogic.sol

for (uint256 i = 0; i < tokenIds.length; i++) {
    DataTypes.ERC721TokenData storage tokenData = assetData.erc721TokenData[tokenIds[i]];
     tokenData.owner = user;
```

However, in case the user gets liquidated or just withdraws his NFT from the protocol and sells it

the `delegateRegistryV2` contract will not get updated. this will lead third-party projects to use outdated data from `delegateRegistryV2`

The new owner of the NFT can't update the delegation due to this check-in 
```solidity
File: PoolLogic.sol#executeDelegateERC721()

86:       DataTypes.ERC721TokenData storage tokenData = VaultLogic.erc721GetTokenData(assetData, params.tokenIds[i]);
87:       require(tokenData.owner == params.msgSender, Errors.INVALID_TOKEN_OWNER);
```
So, they are forced to deposit the NFT in this protocol in order to delete the delegation in their NFTs.

## Impact

Provide wrong information to the third-party e.g., it can affect airdrop claims (the old owner can claim them), especially when the user re-uses the NFT as collateral in the BendDAO. 

## Proof of Concept

## Tools Used

Manual Review

## Recommended Mitigation Steps
re-set  the delegation in `delegateRegistryV2` when liquidation or withdrawal of ERC721 happens on:
- `IsolateLiquidation.sol#isolateLiquidate()`
- `CrossLiquidation.sol#crossLiquidateERC721()`
- `BVault.sol#withdrawERC721()`


## Assessed type

Access Control
Updating asset collateral params can lead to liquidate borrowers arbitrarily
Audit: 2024-07-benddao-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-07-benddao/blob/main/src/modules/Configurator.sol#L147-L163


# Vulnerability details


## Description

One of the key concepts in this protocol is **Cross Lending** when the contract will calculate the health-factor of the account,
If it is unhealthy the liquidator can repay the debt on behalf of the borrower and take their collateral assets at a certain discount price.

The protocol has two main factors to calculate the health-factor for users, The `collateralFactor` and `liquidationThreshold` values are unique for each pool and asset. Also, The Pool Admin can update them at any time by triggering `Configurator.sol#setAssetCollateralParams()`

The responsibility for computing health-factor is `GenericLogic.sol#calculateUserAccountData()`, which Calculates the user data across the reserves.

However, the current logic uses `liquidationThreshold` value also to check if the asset will not be used as collateral (but still, users can supply/lend it to be borrowed and earn interest)

```solidity
File: GenericLogic.sol#calculateUserAccountData()

      if (currentAssetData.liquidationThreshold != 0) {
       /***/
        result.totalCollateralInBaseCurrency += vars.userBalanceInBaseCurrency;
       /***/
      }
```
The issue is the PoolAdmin can call `Configurator.sol#setAssetCollateralParams()` at any time to disable accepting an asset as collateral by updating `collateralFactor` and `liquidationThreshold` to zero value. 
So, users suddenly will get liquidated directly by MEV bots.

## Impact

- The Pool Admin can update the asset collateral params, this will leave borrowers with bad debt and allow the liquidators to liquidate all the loans.  
- MEV bots will just back-run the `setAssetCollateralParams()` transaction to liquidate users, They have no chance to update their positions.  

## Proof of Concept
Foundry PoC:
- Please copy the following POC in `TestIntCrossLiquidateERC20.t.sol`
```solidity
function test_Should_LiquidateUSDT_executeSetAssetCollateralParams() public {
    prepareUSDT(tsDepositor1);
    //? 1- borrower supply tsWETH as collateral
    prepareWETH(tsBorrower1);

    TestUserAccountData memory accountDataBeforeBorrow = getUserAccountData(address(tsBorrower1), tsCommonPoolId);

    // borrow some eth
    uint8[] memory borrowGroups = new uint8[](1);
    borrowGroups[0] = tsLowRateGroupId;

    uint256[] memory borrowAmounts = new uint256[](1);
    uint256 usdtCurPrice = tsPriceOracle.getAssetPrice(address(tsUSDT));
    borrowAmounts[0] = (accountDataBeforeBorrow.availableBorrowInBase * (10 ** tsUSDT.decimals())) / usdtCurPrice;

    //? 2- the borrower call `crossBorrowERC20()` to borrow `tsUSDT`
    actionCrossBorrowERC20(
      address(tsBorrower1),
      tsCommonPoolId,
      address(tsUSDT),
      borrowGroups,
      borrowAmounts,
      new bytes(0)
    );

    // make some interest
    advanceTimes(1 days);

    //? 3- set `liquidationThreshold` to zero
    tsHEVM.startPra
Liquidator can repay the debt and take non-collateral assets from the user
Audit: 2024-07-benddao-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-07-benddao/blob/main/src/modules/Configurator.sol#L147-L163


# Vulnerability details


## Description

The protocol has `liquidationThreshold` factor to calculate the health-factor for users, it is a unique value for each pool and asset. Also, The Pool Admin can update them at any time by triggering `Configurator.sol#setAssetCollateralParams()`

However, the current logic uses `liquidationThreshold` value also to check if the asset will not be used as collateral. So The Pool Admin can set `liquidationThreshold` to zero to only let users supply/lend an asset to be used in the borrow system to earn interest for lenders, This will make the asset not accepted as collateral (check the code snap)

```solidity
File: GenericLogic.sol#calculateUserAccountData()

if (currentAssetData.liquidationThreshold != 0) {
/***/
result.totalCollateralInBaseCurrency += vars.userBalanceInBaseCurrency;
/***/
}
```
But, Maliseus liquidators can take those assets (assets only for lending), in order to liquidate user loans.

From [Docs](https://github.com/BendDAO/bend-gitbook-portal-v2/blob/main/concepts/cross-margin-lending.md) in Cross Margin Lending model
```
 The liquidator will repay the debt on behalf of the borrower and take the user's collateral assets at a certain discount price.
```
So, liquidators can break this invariant (take the user's collateral assets), check the coded POC below.

## Impact

-  In case `liquidation threshold == 0` the asset will not be used as collateral, but liquidators still can take it in return for the paid debt (break protocol invariant)
- in normal cases, the `liquidationBonus` will be also zero so the liquidator will have no discount. So, no profit motive for an attacker here, but he damages the user's positions  

## Proof of Concept
Foundry PoC:
- Please copy the following POC in `TestIntCrossLiquidateERC20.t.sol`
```solidity
 function test_Should_LiquidateUSDT_HasDAI_SupplyAsNotCollateral() public {
    //? 1- borrower supply
    //  1.1- tsWETH as collateral
    //  1.2- tsDAI as supply nb: can't used as collateral, bec its `liquidationThreshold == 0`
    //? 2- the borrower call `crossBorrowERC20()` to borrow `tsUSDT`
    // nb: tsUSDT => debt || tsWETH => collateral
    //? 3- drop down eth price to lower heath factor
    //? 4- liquidator call `crossLiquidateERC20()` with `collateralAsset == tsDAI`
    /*----------------------------- */
    tsHEVM.startPrank(tsPoolAdmin);
    tsConfigurator.setAssetCollateralParams(tsCommonPoolId, address(tsDAI), 0, 0, 0);
    tsHEVM.stopPrank();

    prepareUSDT(tsDepositor1);
    //? 1- borrower supply
    //  1.1- tsWETH as collateral
    prepareWETH(tsBorrower1);

    //  1.2- tsDAI as supply nb: can't used as collateral, bec its `liquidationThreshold == 0`
    prepareDAI(tsBorrower1);

    TestUserAccountData memory accountDataBeforeBorrow = getUserAccountData(address(tsBorrower1), tsCommonPoolId);

    // borrow some eth
    uint8[] me
No check if Arbitrum/Optimism L2 sequencer is down in Chainlink feeds `PriceOracle.sol` 
Audit: 2024-07-benddao-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-07-benddao/blob/main/src/PriceOracle.sol#L122


# Vulnerability details


## Description

Chainlink recommends that all Optimistic L2 oracles consult the Sequencer Uptime Feed to ensure that the sequencer is live before trusting the data returned by the oracle.

If the Arbitrum Sequencer goes down, oracle data will not be kept up to date, and thus could become stale. However, users are able to continue to interact with the protocol directly through the L1 optimistic rollup contract. You can review Chainlink docs on [L2 Sequencer Uptime Feeds](https://docs.chain.link/docs/data-feeds/l2-sequencer-feeds/) for more details on this.

This protocol uses `getPriceOracle()` everywhere and it sub call to [PriceOracle.sol#latestRoundData()](https://github.com/code-423n4/2024-07-benddao/blob/main/src/PriceOracle.sol#L122)
As a result, users may be able to use the protocol while Oracle feeds are stale. This could cause many problems,
example:

A user has a loan with 100 tokens, valued at 1 ETH each, and no borrows
The Arbitrum sequencer goes down temporarily
While it's down, the price of the token falls to 0.5 ETH each
The current value of the user's loan is 50 ETH, so the liquidators should be able to liquidate this position because it is unhealthy
However, due to the stale price, the protocol lets the user keep borrowing.

## Impact

If the Arbitrum sequencer goes down, the protocol will allow users to continue to operate at the previous (stale) prices.
this will brack all the core functionality in the protocol. 

## Tools Used

Manual Review - Solodit

## Recommended Mitigation Steps
It is recommended to follow the code example of Chainlink: https://docs.chain.link/data-feeds/l2-sequencer-feeds#example-code
or just add theis check:
```solidity
File: PriceOracle.sol

function getAssetPriceFromChainlink(address asset) public view returns (uint256) {
   AggregatorV2V3Interface sourceAgg = assetChainlinkAggregators[asset];
   require(address(sourceAgg) != address(0), Errors.ASSET_AGGREGATOR_NOT_EXIST);
   if (!isSequencerActive()) revert Errors.L2SequencerUnavailable();

    ...
}

function isSequencerActive() internal view returns (bool) {
    (, int256 answer, uint256 startedAt,,) = sequencer.latestRoundData();
    if (block.timestamp - startedAt <= GRACE_PERIOD_TIME || answer == 1)
        return false;
    return true;
}
```



## Assessed type

Oracle