xanoutas/solidity-security-findings huggingface dataset:
Stolen erc721 collateral to `msg.sender` instead of the bidder in `IsolateLogic::executeIsolateLiquidate()`
Audit: 2024-07-benddao-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
The caller of `IsolateLogic::executeIsolateLiquidate()` receives the liquidated erc721 collateral, stealing it from the bidder who repaid the debt assets.

## Proof of Concept
In `IsolateLogic::executeIsolateLiquidate()`, the ids of the tokens liquidated are https://github.com/code-423n4/2024-07-benddao/blob/main/src/libraries/logic/IsolateLogic.sol#L472-L478 to the `msg.sender`. However, the debt was https://github.com/code-423n4/2024-07-benddao/blob/main/src/libraries/logic/IsolateLogic.sol#L269 by the bidder in `IsolateLogic::executeIsolateBorrow()`. Thus, the caller of `IsolateLogic::executeIsolateLiquidate()` steals these tokens, which should have gone to the bidder instead.

Place the following test in `TestIntIsolateLiquidate.t.sol`, showing that `tsLiquidator2` steals the tokens without providing any assets.
```solidity
function test_POC_Should_LiquidateWETH_DoesNotTransferCollateralToBidder() public {
  TestCaseLocalVars memory testVars;

  // deposit
  prepareWETH(tsDepositor1);
  uint256[] memory tokenIds = prepareIsolateBAYC(tsBorrower1);

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

  // make some interest
  advanceTimes(365 days);

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

  uint256 initialLiquidator2Balance = tsWETH.balanceOf(address(tsLiquidator2));

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

  // end the auction
  advanceTimes(25 hours);

  uint256[] memory liquidateAmounts = new uint256[](tokenIds.length);
  testVars.loanDataBefore = getIsolateLoanData(tsCommonPoolId, address(tsBAYC), tokenIds);
  for (uint256 i = 0; i < tokenIds.length; i++) {
    testVars.totalBidAmount += testVars.loanDataBefore[i].bidAmount;
    testVars.totalBidFine += testVars.loanDataBefore[i].bidFine;
    testVars.totalRedeemAmount += testVars.loanDataBefore[i].redeemAmount;
    testVars.totalBorrowAmount += testVars.loanDataBefore[i].borrowAmount;
  }

  testVars.poolBalanceBefore = tsWETH.balanceOf(address(tsPoolManager));
  testVars.walletBalanceBefore1 = tsWETH.balanceOf(address(tsLiquidator1));
  testVars.walletBalanceBefore2 = tsWETH.balanceOf(address(tsBorrower1));
  testVars.erc721BalanceBefore1 = tsBAYC.balanceOf(address(tsLiquidator1));

  // liquidate
  tsLiquidator2.isolateLiquidate(tsCommonPoolId, address(tsBAYC), tokenIds, address(tsWETH), liquidateAmounts, false);
  testVars.txAuctionTimestamp = block.timestamp;

  // check results
  testVars.loanDataAfter = getIsolateLoanData(tsCommonPoolId, address(tsBAYC), tokenIds);
  for (uint256 i = 0; i < tokenIds.length; i++) {
    assertEq(testVars.loanDataAfter[i].bidStartTimestamp, 0, 'bidStartTimestamp');
    assertEq(testVars.loanDataAfter[i].firstBidder, address(0), 'firstBidder');
    assertEq(testVars.loanDataAfter[i].la
Users cannot unstake from YiedlETHStakingEtherfi.sol, because YieldAccount.sol is incompatible with ether.fi's WithdrawRequestNFT.sol 
Audit: 2024-07-benddao-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-07-benddao/blob/117ef61967d4b318fc65170061c9577e674fffa1/src/yield/YieldAccount.sol#L15


# Vulnerability details

## Impact
Users cannot unstake from YiedlETHStakingEtherfi.sol, because YieldAccount.sol is incompatible with ether.fi's WithdrawRequestNFT.sol.

## Proof of Concept
In YiedlETHStakingEtherfi::stake, users yieldBorrow WETH, convert it to ETH, and stake in Ether.fi's LiquidityPool. Ether.fi's LiquidityPool will mint eETH (protocol token) to the user's yieldAccount.

Users will `unstake()` by requesting withdrawal of staked ETH from ether.fi's WithdrawRequestNFT::requestWithdraw. 

The problem is WithdrawRequestNFT::requestWithdraw will `_safeMint` an NFT token to the user's yieldAccount as proof of request withdrawal. But the user's yieldAccount is not compatible with `_safeMint` because the implementation YieldAcount.sol doesn't have `onERC721Received` method required by `_safeMint`.
```solidity
//src/yield/YieldAccount.sol
...
//@audit YieldAcount doesn't inherit openzeppelin's ERC721Holder.sol, neither does it implement onERC721Received. Not compatible with safeMint method required by ether.fi's WithdrawRequestNFT.
contract YieldAccount is IYieldAccount, Initializable {
...
```
(https://github.com/code-423n4/2024-07-benddao/blob/117ef61967d4b318fc65170061c9577e674fffa1/src/yield/YieldAccount.sol#L15)

Flows: [YieldStakingBase::unstake](https://github.com/code-423n4/2024-07-benddao/blob/117ef61967d4b318fc65170061c9577e674fffa1/src/yield/YieldStakingBase.sol#L343) → [YieldEthStakingEtherfi::protocolRequestWithdrawal](https://github.com/code-423n4/2024-07-benddao/blob/117ef61967d4b318fc65170061c9577e674fffa1/src/yield/etherfi/YieldEthStakingEtherfi.sol#L87) → [liquidityPool::requestWithdraw](https://github.com/etherfi-protocol/smart-contracts/blob/7c66e571df4fe7ec502a3c325b623bc52349ef9d/src/LiquidityPool.sol#L209) → [WithdrawRequestNFT::requestWithdraw](https://github.com/etherfi-protocol/smart-contracts/blob/7c66e571df4fe7ec502a3c325b623bc52349ef9d/src/WithdrawRequestNFT.sol#L63) → [_safeMint(recipient, requestId)](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/4764ea50750d8bda9096e833706beba86918b163/contracts/token/ERC721/ERC721.sol#L315)
```solidity
//https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/ERC721.sol#L315
    function _safeMint(address to, uint256 tokenId, bytes memory data) internal virtual {
        _mint(to, tokenId);
        require(
            _checkOnERC721Received(address(0), to, tokenId, data),
            "ERC721: transfer to non ERC721Receiver implementer"
        );
    }
```
(https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/ERC721.sol#L315)

Because yieldAcount.sol is cloned for every user and atomically used as caller address for ether.fi interactions, no one can unstake from YiedlETHStakingEtherfi.sol. User funds, yield and nft collateral's will be locked. When
Anyone can get the NFT collateral token after an Auction without bidding due to missing check on msg.sender
Audit: 2024-07-benddao-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-07-benddao/blob/117ef61967d4b318fc65170061c9577e674fffa1/src/libraries/logic/IsolateLogic.sol#L473
https://github.com/code-423n4/2024-07-benddao/blob/117ef61967d4b318fc65170061c9577e674fffa1/src/libraries/logic/IsolateLogic.sol#L477


# Vulnerability details

## Impact
Anyone can get the NFT collateral token after an auction without bidding due to a missing check on `msg.sender`.

## Proof of Concept
In IsolateLogic.sol, liquidation of an isolate loan can only be placed after the auction period is passed with bidding. 

The problem is that there is a missing check on `msg.sender` in isolate liquidation flow, which allows anyone to take the collateral NFT token. 
```solidity
//src/libraries/logic/IsolateLogic.sol
  function executeIsolateLiquidate(InputTypes.ExecuteIsolateLiquidateParams memory params) internal {
  ...
    if (params.supplyAsCollateral) {
        //@audit due to no check on msg.sender, anyone calls isolateLiquidate will get the collateral nft
 |>     VaultLogic.erc721TransferIsolateSupplyOnLiquidate(nftAssetData, params.msgSender, params.nftTokenIds);
    } else {
      VaultLogic.erc721DecreaseIsolateSupplyOnLiquidate(nftAssetData, params.nftTokenIds);

 |>     VaultLogic.erc721TransferOutLiquidity(nftAssetData, params.msgSender, params.nftTokenIds);
    }
  ...
```
(https://github.com/code-423n4/2024-07-benddao/blob/117ef61967d4b318fc65170061c9577e674fffa1/src/libraries/logic/IsolateLogic.sol#L473)


Flows: IsolateLiquidation::isolateLiquidate -> IsolateLogic.executeIsolateLiquidate(). Note that msg.sender is passed from isolateLiquidate() to the end of executeIsolateLiquidate() without any checks.

POC:
See added unit test `test_Anyone_Can_LiquidateWETH()`. Only `tsLiquidator1` auctioned. But `tsBorrower2` can liquidate and receive collaterals.
```solidity
//test/integration/TestIntIsolateLiquidate.t.sol
...
  function test_Anyone_Can_LiquidateWETH() public {
    TestCaseLocalVars memory testVars;

    // deposit
    prepareWETH(tsDepositor1);
    uint256[] memory tokenIds = prepareIsolateBAYC(tsBorrower1);

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

    // make some interest
    advanceTimes(365 days);

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

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

    // end the auction
    advanceTimes(25 hours);

    uint256[] memory liquidateAmounts = new uint256[](tokenIds.length);
    testVars.loanDataBefore = getIsolateLoanData(tsCommonPoolId, address(tsBAYC), tokenIds);
    for (uint256 i = 0; i < tokenIds.length; i++) {
      testVars.totalBidAmount += testVars.loanDataBefore[i].bidAmount;
      testVars.totalBidFine += testVars.loanDataBefore[i].bidFine;
      testVars.totalRedeemAmount += testVars.loanDataBefore[i].redeemAmount;
      testVars.totalBorrowAmount += testVars.loanDataBefore[i].borrowAmount;
    }
Vulnerable erc20TransferOutBidAmountToLiqudity() will cause incorrect accounting of assetData or DOS liquidation.
Audit: 2024-07-benddao-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-07-benddao/blob/117ef61967d4b318fc65170061c9577e674fffa1/src/libraries/logic/VaultLogic.sol#L497


# Vulnerability details

## Impact
Vulnerable `VaultLogic::erc20TransferOutBidAmountToLiqudity` will cause incorrect accounting of `assetData` or DOS liquidation.

## Proof of Concept
In IsolateLogic.sol's auctions, when a bid is placed, `assetData.totalBidAmount` is increased with the bid amount [erc20TransferInBidAmount()](https://github.com/code-423n4/2024-07-benddao/blob/117ef61967d4b318fc65170061c9577e674fffa1/src/libraries/logic/VaultLogic.sol#L472). When a bid is outbidded, the old bid is transferred out and in [erc20TransferOutBidAmount()](https://github.com/code-423n4/2024-07-benddao/blob/117ef61967d4b318fc65170061c9577e674fffa1/src/libraries/logic/VaultLogic.sol#L488), `assetData.totalBidAmount` is subtracted by the old bidAmount.

The problem is `VaultLogic::erc20TransferOutBidAmountToLiqudity` will most likely cause `assetData.totalBidAmount` to be incorrect because it falsely assumes the final debt amount (vars.totalBorrowAmount) will equal to the total bid amount(`vars.totalBidAmount`).

When a loan is liquidated post-auction, due to debt compounding and due to minBidDelta requirements, vars.totalBorrowAmount and vars.totalBidAmount will differ.
```solidity
//src/libraries/logic/IsolateLogic.sol
  function executeIsolateLiquidate(InputTypes.ExecuteIsolateLiquidateParams memory params) internal {
...
    for (vars.nidx = 0; vars.nidx < params.nftTokenIds.length; vars.nidx++) {
...
      // Last bid can not cover borrow amount and liquidator need pay the extra amount
      //@audit-info note: this condition means vars.totalBorrowAmount != vars.totalBidAmount
|>    if (loanData.bidAmount < vars.borrowAmount) {
        vars.extraBorrowAmounts[vars.nidx] = vars.borrowAmount - loanData.bidAmount;
      }

      // Last bid exceed borrow amount and the remain part belong to borrower
      //@audit-info note: this condition means vars.totalBorrowAmount != vars.totalBidAmount
|>    if (loanData.bidAmount > vars.borrowAmount) {
        vars.remainBidAmounts[vars.nidx] = loanData.bidAmount - vars.borrowAmount;
      }
...
      vars.totalBorrowAmount += vars.borrowAmount;
      vars.totalBidAmount += loanData.bidAmount;
...
    }
...
    // bid already in pool and now repay the borrow but need to increase liquidity
|>  VaultLogic.erc20TransferOutBidAmountToLiqudity(debtAssetData, vars.totalBorrowAmount);
...
```
(https://github.com/code-423n4/2024-07-benddao/blob/117ef61967d4b318fc65170061c9577e674fffa1/src/libraries/logic/IsolateLogic.sol#L430)

In `VaultLogic.erc20TransferOutBidAmountToLiqudity`, `vars.totalBorrowAmount` will be subtracted from `assetData.totalBidAmount`.
```solidity
//src/libraries/logic/VaultLogic.sol
  function erc20TransferOutBidAmountToLiqudity(DataTypes.AssetData storage assetData, uint amount) internal {
    //@audit amount is likely not a bid amount when called from Is
The bot won't be able to unstake or repay risky positions in the yield contract
Audit: 2024-07-benddao-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-07-benddao/blob/main/src/yield/YieldStakingBase.sol#L312
https://github.com/code-423n4/2024-07-benddao/blob/main/src/yield/YieldStakingBase.sol#L384


# Vulnerability details

## Impact
When `botAdmin` attempts to unstake and repay risky positions, wrong account address will be used, causing the transaction to fail.

## Proof of Concept
In the `YieldStakingBase.sol` contract, `unstake` and `repay` transaction can only be initiated by either the NFT owner or `botAdmin`. Unfortunately, there is a bug in which `msg.sender` is used to fetch borrower yield account address from the `yieldAccounts` mapping:
```solidity
  function _unstake(uint32 poolId, address nft, uint256 tokenId, uint256 unstakeFine) internal virtual {
    UnstakeLocalVars memory vars;

>>  vars.yieldAccout = IYieldAccount(yieldAccounts[msg.sender]);
    require(address(vars.yieldAccout) != address(0), Errors.YIELD_ACCOUNT_NOT_EXIST);

  function _repay(uint32 poolId, address nft, uint256 tokenId) internal virtual {
    RepayLocalVars memory vars;

>>  vars.yieldAccout = IYieldAccount(yieldAccounts[msg.sender]);
    require(address(vars.yieldAccout) != address(0), Errors.YIELD_ACCOUNT_NOT_EXIST);
```
If `botAdmin` calls one of these functions, the protocol will attempt to use `botAdmin` yield account address instead of the borrower account, resulting in incorrect account data being used and transaction failure. Since `botAdmin` is the only address that can force close risky positions, this bug poses a high threat to the protocol. 

## Tools Used
Manual review

## Recommended Mitigation Steps
Allow specifying `user` address in `unstake` and `repay` functions
```diff
+ function _unstake(uint32 poolId, address nft, address user, uint256 tokenId, uint256 unstakeFine) internal virtual {
    UnstakeLocalVars memory vars;

+   vars.yieldAccout = IYieldAccount(yieldAccounts[user]);
+   require(user == msg.sender || botAdmin == msg.sender, 
Errors.INVALID_CALLER);
    require(address(vars.yieldAccout) != address(0), Errors.YIELD_ACCOUNT_NOT_EXIST);
```





## Assessed type

Other
PriceOracle has invalid checks on price staleness. 
Audit: 2024-07-benddao-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
PriceOracle has invalid checks on price staleness.

## Proof of Concept
There are two checks on price staleness in PriceOracle::getAssetPriceFromChainlink. But both checks are invalid.
(1) `updatedAt != 0`
In chainlink aggregator, the price is updated at a set heartbeat and a threshold of deviation. `updatedAt` should be used to check if the answer is within the hearbeat or acceptable time limits. See [doc](https://docs.chain.link/data-feeds#check-the-timestamp-of-the-latest-answer).

(2) `answeredInRound >= roundId`
`answeredInRound` is deprecated and shouldn't be used. see [doc](https://docs.chain.link/data-feeds/api-reference#getrounddata).

```solidity
//src/PriceOracle.sol
  /// @notice Query the price of asset from chainlink oracle
  function getAssetPriceFromChainlink(address asset) public view returns (uint256) {
    AggregatorV2V3Interface sourceAgg = assetChainlinkAggregators[asset];
    require(address(sourceAgg) != address(0), Errors.ASSET_AGGREGATOR_NOT_EXIST);

    (uint80 roundId, int256 answer, , uint256 updatedAt, uint80 answeredInRound) = sourceAgg.latestRoundData();
    require(answer > 0, Errors.ASSET_PRICE_IS_ZERO);
|>  require(updatedAt != 0, Errors.ORACLE_PRICE_IS_STALE);
|>  require(answeredInRound >= roundId, Errors.ORACLE_PRICE_IS_STALE);

    return uint256(answer);
  }
```
(https://github.com/code-423n4/2024-07-benddao/blob/117ef61967d4b318fc65170061c9577e674fffa1/src/PriceOracle.sol#L124-L125)


## Tools Used

Manual

## Recommended Mitigation Steps
Consider using asset-specific hearbeat(e.g. ETH/USD has 1 hour hearbeat) and check against (block.timestamp - updatedAt). 


## Assessed type

Oracle
Risk of mass liquidation after pool/asset pause and unpause, due to borrow interest compounding implemenation
Audit: 2024-07-benddao-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-07-benddao/blob/117ef61967d4b318fc65170061c9577e674fffa1/src/libraries/logic/InterestLogic.sol#L281-L285


# Vulnerability details

## Impact
Risk of mass liquidation after pool/asset pause and unpause, due to borrow interest compounding implementation.

## Proof of Concept
Current pausing features allow a pool to be globally paused (all assets borrow, repay, liquidation pausing) and asset pausing (specific asset borrow, repay, liquidation pausing).

See [validatePoolBasic()](https://github.com/code-423n4/2024-07-benddao/blob/117ef61967d4b318fc65170061c9577e674fffa1/src/libraries/logic/ValidateLogic.sol#L29) and [validateAssetBasic()](https://github.com/code-423n4/2024-07-benddao/blob/117ef61967d4b318fc65170061c9577e674fffa1/src/libraries/logic/ValidateLogic.sol#L43) which are checked in borrow, [repay](https://github.com/code-423n4/2024-07-benddao/blob/117ef61967d4b318fc65170061c9577e674fffa1/src/libraries/logic/ValidateLogic.sol#L281-L282), and will revert if the pool or the asset is paused.

The problem is the borrow interest compounding implementation(InterestLogic::_updateBorrowIndex) will not factor in the duration of pausing when a user cannot repay a loan. 
```solidity
//src/libraries/logic/InterestLogic.sol
  function _updateBorrowIndex(DataTypes.AssetData storage assetData, DataTypes.GroupData storage groupData) internal {
    // borrow index only gets updated if there is any variable debt.
    // groupData.borrowRate != 0 is not a correct validation,
    // because a positive base variable rate can be stored on
    // groupData.borrowRate, but the index should not increase
    if ((groupData.totalScaledCrossBorrow != 0) || (groupData.totalScaledIsolateBorrow != 0)) {
        //@audit Case: asset paused and unpaused, assetData.lastUpdateTimestamp will be a timestamp before paused, interests compound during pausing 
|>      uint256 cumulatedBorrowInterest = MathUtils.calculateCompoundedInterest(
        groupData.borrowRate,
        assetData.lastUpdateTimestamp
      );
      uint256 nextBorrowIndex = cumulatedBorrowInterest.rayMul(groupData.borrowIndex);
      groupData.borrowIndex = nextBorrowIndex.toUint128();
    }
  }
```
(https://github.com/code-423n4/2024-07-benddao/blob/117ef61967d4b318fc65170061c9577e674fffa1/src/libraries/logic/InterestLogic.sol#L281)

Interests continuously compound with no repayment allowed. When unpaused, many accounts are vulnerable to immediate liquidation out of users' control.

## Tools Used

Manual

## Recommended Mitigation Steps
Consider storing pause / unpause timestamp to allow skipping borrow interests compounding for the duration of the pause. 



## Assessed type

Other
assetFee calculation will be incorrect when there is a feeFactor change due to vulnerable implementation
Audit: 2024-07-benddao-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-07-benddao/blob/117ef61967d4b318fc65170061c9577e674fffa1/src/modules/Configurator.sol#L186-L187


# Vulnerability details

## Impact
`assetFee` calculation will be incorrect when there is a feeFactor change due to vulnerable implementation.

## Proof of Concept
When there is a change in feeFactor, Configurator::setAssetProtocolFee will directly update assetData.feeFactor without first settling previously accrued fees.
```solidity
//src/modules/Configurator.sol

  function setAssetProtocolFee(uint32 poolId, address asset, uint16 feeFactor) public nonReentrant {
    address msgSender = unpackTrailingParamMsgSender();
    //@audit this directly reset feeFactor without first settling previously accrued fees.
|>  ConfigureLogic.executeSetAssetProtocolFee(msgSender, poolId, asset, feeFactor);
  }
```
(https://github.com/code-423n4/2024-07-benddao/blob/117ef61967d4b318fc65170061c9577e674fffa1/src/modules/Configurator.sol#L186-L187)

Because fee accrual implementation is always based on a passed time delta, this means a future fee factor will be used for a historical period of interest accumulation, which is incorrect.
```solidity
//src/libraries/logic/InterestLogic.sol
  function _accrueFeeToTreasury(
    DataTypes.AssetData storage assetData,
    DataTypes.GroupData storage groupData,
    uint256 prevGroupBorrowIndex
  ) internal {
...
  ...
      vars.totalDebtAccrued = vars.currTotalBorrow - vars.prevTotalBorrow;
     //@audit-info note: fee is always accrued based previous time delta (block.timestamp - lastUpdateTimestamp)
|>   vars.amountToMint = vars.totalDebtAccrued.percentMul(assetData.feeFactor);
        if (vars.amountToMint != 0) {
              assetData.accruedFee += vars.amountToMint.rayDiv(assetData.supplyIndex).toUint128();
    }
...
```
(https://github.com/code-423n4/2024-07-benddao/blob/117ef61967d4b318fc65170061c9577e674fffa1/src/libraries/logic/InterestLogic.sol#L248)

Suppose a case where `feeFactor` was 10% and now changed to 30%. Previously unsettled time delta will be charged a 30% fee but it was only a 10% `feeFactor` when those borrow interests accumulated.

## Tools Used

Manual

## Recommended Mitigation Steps
In configurator::setAssetProtocolFee, consider atomically settling previously time delta fees before setting the new fee factor.



## Assessed type

Other