xanoutas/solidity-security-findings huggingface dataset:
[H1]  Low data feed frequency from Tellor makes you protocol vulnerable to flash loan attacks
Audit: 2023-02-ethos-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Core/contracts/Dependencies/TellorCaller.sol#L44


# Vulnerability details

# [H1]  Low data feed frequency from Tellor makes you protocol vulnerable to flash loan attacks

 

## Impact 

​          An attacker can stale Tellor Oracle for several hours cheaply and perform a flash loan attack to profit.      

## PoC

To explain this issue I will first compare Chainlink to Tellor.

Most ERC-20 tokens are in general much more volatile than ETH and BTC.     In Chainlink,  there are triggers of 0,5% for BTC and ETH and 1% for other assets.     This is to ensure that you are cutting error by those values.

Tellor, on the other hand, is an *optimistic oracle*.    Stakers use the oracle system to put data on chain  `submitValue(..)` that are directly shown in the oracle.     The security lies the fact that data consumers should  wait some  **dispute windows** in order to give time to others to dispute data and remove incorrect or malicious data.

This is what happened in a Liquity bug found last year, they were reading instant data.   [2] 

Being explained this and back to your code you have essentially two bugs

 

## First bug :  Default disputetime  = 20 minutes

In `TellorCaller.sol` you have the following statement

   `(bytes memory data, uint256 timestamp) = getDataBefore(_queryId, block.timestamp - 20 minutes)`

Maybe you are using 20 minutes because this is the default value in Tellor documentation.    However, in Liquity they are using 15 minutes for ETH because they say that have been made an analysis of  ETH  volatility behaviour.[2]

Basically there is a tradeoff between the volatility of an asset and the dispute time.    More time is safer to have time to dipute but more likely to read a so old value.     Less dispute time you have less error but no time to dispute can put you at risk of reading a manipulated value.

​     In your case, you are using more volatile assets so in theory, if Liquity analysis is correct, you should be using less time for ERC20 assets.  

​	Of course this requires a deeper analysis but I am not doing it because the second bug makes this unnecessary as it has a higher impact.



## Second bug :   Data feed frequency in Tellor is very low so it is cheap to break




 I will briefly explain some Tellor security designs.

Tellor bases his security design in an exponential cost to dispute.       They have a several-round voting to dispute  a single value but we are interested in the *Cost of Stalling* (CoS) the System.   

To stale the system we need  to dipute every single value for a given period, for a given asset.

According to whitepaper cost starts at  `baseFee`  and increase with the following formula



​                           𝑑𝑖𝑠𝑝𝑢𝑡𝑒𝐹𝑒𝑒𝑖𝑑,𝑡,𝑟>1 = 𝑑𝑖𝑠𝑝𝑢𝑡𝑒𝐹𝑒𝑒𝑖 × 2 𝑑𝑖𝑠𝑝𝑢𝑡𝑒𝑅𝑜𝑢𝑛𝑑𝑠𝑖𝑑,𝑡−1  

Where 

​                                𝑑𝑖𝑠𝑝𝑢𝑡𝑒𝐹𝑒𝑒𝑖 is the initial dispute fee (`baseFee`) 

​
UNABLE TO CLOSE THE LAST TROVE
Audit: 2023-02-ethos-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/TroveManager.sol#L1282
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/TroveManager.sol#L1538-L1540


# Vulnerability details

## Impact
When the current size of the list for the specified collateral equals `1`, attempting to close the last trove is going to be denied as it requires more than one trove in the system.

## Proof of Concept
When a borrower calls [closeTrove()](https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/BorrowerOperations.sol#L397) in BorrowerOperations.sol, its external call, `troveManagerCached.closeTrove()`, is going to revert as can be seen in line 1282 of the function logic below:

[TroveManager.sol#L1273-L1293](https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/TroveManager.sol#L1273-L1293)

```
    function closeTrove(address _borrower, address _collateral, uint256 _closedStatusNum) external override {
        _requireCallerIsBorrowerOperationsOrRedemptionHelper();
        return _closeTrove(_borrower, _collateral, Status(_closedStatusNum));
    }

    function _closeTrove(address _borrower, address _collateral, Status closedStatus) internal {
        assert(closedStatus != Status.nonExistent && closedStatus != Status.active);

        uint TroveOwnersArrayLength = TroveOwners[_collateral].length;
1282:        _requireMoreThanOneTroveInSystem(TroveOwnersArrayLength, _collateral);

        Troves[_borrower][_collateral].status = closedStatus;
        Troves[_borrower][_collateral].coll = 0;
        Troves[_borrower][_collateral].debt = 0;

        rewardSnapshots[_borrower][_collateral].collAmount = 0;
        rewardSnapshots[_borrower][_collateral].LUSDDebt = 0;

        _removeTroveOwner(_borrower, _collateral, TroveOwnersArrayLength);
        sortedTroves.remove(_collateral, _borrower);
    }
```
[TroveManager.sol#L1538-L1540](https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/TroveManager.sol#L1538-L1540)

Notice that both array lengths, `TroveOwnersArrayLength` and `sortedTroves.getSize(_collateral)`, need to be greater than `1`: 

```
    function _requireMoreThanOneTroveInSystem(uint TroveOwnersArrayLength, address _collateral) internal view {
        require (TroveOwnersArrayLength > 1 && sortedTroves.getSize(_collateral) > 1);
    }
```

Consequently, a borrower will be denied this call either if s/he is the:
1. newest and only borrower since the system started and will have to wait for more user participations before the call can be functionally successful, or
2. last borrower in the system in a mass exodus due to a super late response. 

## Recommended Mitigation Steps
It is recommended that `_requireMoreThanOneTroveInSystem()` be totally or at least conditionally removed from `_closeTrove()`. For instance, the protocol can make it such that this specific function is only invoked when the trove is not closed by the owner
`_rebalance()` in `ActivePool` fails if a vault is at a deficit
Audit: 2023-02-ethos-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/ActivePool.sol#L248-L251


# Vulnerability details

In the `ActivePool` contract, the `_rebalance()` function subtracts the amount of collateral currently in the vault (`vars.sharesToAssets`) from the amount originally allocated (`vars.currentAllocated`) to calculate its profit. This can be seen in [`ActivePool.sol#L248-L251`](https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/ActivePool.sol#L248-L251):
```solidity
vars.sharesToAssets = vars.yieldGenerator.convertToAssets(vars.ownedShares);

// if we have profit that's more than the threshold, record it for withdrawal and redistribution
vars.profit = vars.sharesToAssets.sub(vars.currentAllocated);
```

As seen from above, the `sub()` function from [`SafeMath`](https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/Dependencies/SafeMath.sol#L47-L49) is used to do the calculation. Hence, whenever the vault is at a loss, `_rebalance()` will always revert as `vars.currentAllocated` is less than `vars.sharesToAssets`, leading to an arithmetic underflow.

This affects the following functions as they call the `_rebalance()` function:

* `BorrowOperations.sol`
  * `openTrove`
  * `closeTrove`
  * `addColl`
  * `withdrawColl`
  * `withdrawLUSD`
  * `repayLUSD`
  * `adjustTrove`
* `TroveManager.sol`
  * `redeemCollateral`
  * `liquidate`
  * `batchLiquidateTroves`
  * `liquidateTroves`

## Impact

Whenever a vault containing one type of collateral is at a loss, all the functions listed above will revert when called with the same type of collateral, massively affecting the protocol's functionality. 

Moreover, since all the functions used to withdraw/transfer collateral are affected, all the borrowers who opened troves with the same type of collateral will have their collateral stuck in the protocol until the vault has a non-negative profit.

## Proof of Concept

The test in [this gist](https://gist.github.com/MiloTruck/5a6928720cfd8eb51a8c1214d5a1a250#file-rebalancedostest-js) demonstrates how a vault at a loss causes all the functions listed above to revert. 

To run the test:

* Copy [`MockBorrowerOperations.sol`](https://gist.github.com/MiloTruck/5a6928720cfd8eb51a8c1214d5a1a250#file-mockborroweroperations-sol) into `Ethos-Core/contracts/TestContracts`.
* Copy [`RebalanceDOSTest.js`](https://gist.github.com/MiloTruck/5a6928720cfd8eb51a8c1214d5a1a250#file-rebalancedostest-js) into `Ethos-Core/test`.
* Run the following command: `npx hardhat test ./test/RebalanceDOSTest.js`

## Recommended Mitigation Steps

Consider declaring `vars.profit` as a `int256`, and changing [`ActivePool.sol#L250-L254`](https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/ActivePool.sol#L250-L254) to the following:
```js
// if we have profit that's more than the threshold, record it for withdrawal and redistribution
if (vars.sharesToAssets < vars.currentAllocated && vars.cur
Unable to update distribution timeframe for communityIssuance without depositing more $OATH
Audit: 2023-02-ethos-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Core/contracts/LQTY/CommunityIssuance.sol#L97-L123


# Vulnerability details

## Impact
Cannot change the distribution payout period -- without additionally funding the contract

## Proof of Concept
If there is a necessity to change the distribution details, it cannot be done without adding OATH to the contract
fund(uint amount)  updates the distribution details
fund() cannot be called with an amount of zero
https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Core/contracts/LQTY/CommunityIssuance.sol#L101-L102

## Tools Used
Visual inspection

## Recommended Mitigation Steps
break the unissued OATH code in fund() and move to its own function updateDistributionDetails()
https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Core/contracts/LQTY/CommunityIssuance.sol#L105-L116
Then updateDistributionPeriod(..) and fund(..) can call updateDistributionDetails()
_withdraw() totalLoss may be smaller than expected
Audit: 2023-02-ethos-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Vault/contracts/ReaperVaultV2.sol#L399-L402


# Vulnerability details

## Impact
user execute withdraw(), but loss exceed the `withdrawMaxLoss`

## Proof of Concept
The _withdraw() is allowed to be lost and the validation code is as follows:

```solidity
    function _withdraw(
        uint256 _shares,
        address _receiver,
        address _owner
    ) internal nonReentrant returns (uint256 value) {
...    

    function _withdraw(
        uint256 _shares,
        address _receiver,
        address _owner
    ) internal nonReentrant returns (uint256 value) {
        require(_shares != 0, "Invalid amount");
        value = (_freeFunds() * _shares) / totalSupply();
        _burn(_owner, _shares);

        if (value > token.balanceOf(address(this))) {
            uint256 totalLoss = 0;
            uint256 queueLength = withdrawalQueue.length;
            uint256 vaultBalance = 0;
            for (uint256 i = 0; i < queueLength; i = i.uncheckedInc()) {
                vaultBalance = token.balanceOf(address(this));
                if (value <= vaultBalance) {
                    break;
                }

                address stratAddr = withdrawalQueue[i];
                uint256 strategyBal = strategies[stratAddr].allocated;
                if (strategyBal == 0) {
                    continue;
                }

                uint256 remaining = value - vaultBalance;
                uint256 loss = IStrategy(stratAddr).withdraw(Math.min(remaining, strategyBal));
                uint256 actualWithdrawn = token.balanceOf(address(this)) - vaultBalance;

                // Withdrawer incurs any losses from withdrawing as reported by strat
                if (loss != 0) {
                    value -= loss;
                    totalLoss += loss;
                    _reportLoss(stratAddr, loss);
                }

                strategies[stratAddr].allocated -= actualWithdrawn;
                totalAllocated -= actualWithdrawn;
            }

            vaultBalance = token.balanceOf(address(this));
            if (value > vaultBalance) {
                value = vaultBalance;
                //<----------------There is a problem with this place, totalLoss does not add the difference
            }

            require(
                totalLoss <= ((value + totalLoss) * withdrawMaxLoss) / PERCENT_DIVISOR,
                "Withdraw loss exceeds slippage"
            );
        }

        token.safeTransfer(_receiver, value);
        emit Withdraw(msg.sender, _receiver, _owner, value, _shares);
    }
```
where the variable totalLoss is used to calculate the current loss, but in the end `value > vaultBalance`, there may still be a loss, but totalLoss does not increase
```solidity
            vaultBalance = token.balanceOf(address(this));
            if (value > vaultBalance) {
                value = vaultBalance;
                 //<---
_harvestCore() roi calculation error
Audit: 2023-02-ethos-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Vault/contracts/ReaperStrategyGranarySupplyOnly.sol#L141


# Vulnerability details

## Impact
_harvestCore() roi calculation error,may double

## Proof of Concept
The _harvestCore() will calculate the roi and repayment values
The implementation code is as follows:
```solidity
    function _harvestCore(uint256 _debt) internal override returns (int256 roi, uint256 repayment) {
        _claimRewards();
        uint256 numSteps = steps.length;
        for (uint256 i = 0; i < numSteps; i = i.uncheckedInc()) {
            address[2] storage step = steps[i];
            IERC20Upgradeable startToken = IERC20Upgradeable(step[0]);
            uint256 amount = startToken.balanceOf(address(this));
            if (amount == 0) {
                continue;
            }
            _swapVelo(step[0], step[1], amount, VELO_ROUTER);
        }

        uint256 allocated = IVault(vault).strategies(address(this)).allocated;
        uint256 totalAssets = balanceOf();
        uint256 toFree = _debt;

        if (totalAssets > allocated) {
            uint256 profit = totalAssets - allocated;
            toFree += profit;
            roi = int256(profit);
        } else if (totalAssets < allocated) {
            roi = -int256(allocated - totalAssets);
        }

        (uint256 amountFreed, uint256 loss) = _liquidatePosition(toFree);
        repayment = MathUpgradeable.min(_debt, amountFreed);
        roi -= int256(loss);//<------这个地方可能会导致重复
    }
```
The last line may cause double counting of losses
For example, the current:
vault.allocated = 9
vault.strategy.allocBPS = 9000
strategy.totalAssets = 9

Suppose that after some time, strategy loses 2, then:
strategy.totalAssets = 9 - 2 = 7
Also the administrator sets vault.strategy.allocBPS = 0

This executes harvest()->_harvestCore(9) to get
roi = 4
repayment = 7

The actual loss of 2, but roi = 4 (double), test code as follows:

add to test/starter-test.js 'Vault Tests'

```solidity
    it.only('test_roi', async function () {
      const {vault, strategy, wantHolder, strategist} = await loadFixture(deployVaultAndStrategyAndGetSigners);
      const depositAmount = toWantUnit('10');
      await vault.connect(wantHolder)['deposit(uint256)'](depositAmount);
      await strategy.harvest();

      const balanceOf = await strategy.balanceOf();
      console.log(`strategy balanceOf: ${balanceOf}`);
      // allocated = 9
      // 1.loss 2, left 7
      await strategy.lossFortest(toWantUnit('2'));
      // 2.modify bps=>0
      await vault.connect(strategist).updateStrategyAllocBPS(strategy.address, 0);
      // 3.so debt = 9
      await strategy.harvest();

      const {allocated, losses, allocBPS} = await vault.strategies(strategy.address);
      console.log(`losses: ${losses}`);
      console.log(`allocated: ${allocated}`);
      console.log(`allocBPS: ${allocBPS}`);
    });

```
add to ReaperStrategyGranarySupply
Logical omission in `report()`
Audit: 2023-02-ethos-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Vault/contracts/ReaperVaultV2.sol#L524-L529


# Vulnerability details

## Impact

In `ReaperVaultV2.sol` we have [report()](https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Vault/contracts/ReaperVaultV2.sol#L493) function. This function is main contact point where each strategy interacts with the vault during its harvest to report profit/loss as well as any repayment of debt.  But this function has a logic error when the credit is checked.

## Proof of Concept

```solidity
524: vars.freeWantInStrat = vars.gain + _repayment;
525: if (vars.credit > vars.freeWantInStrat) {
526:     token.safeTransfer(vars.stratAddr, vars.credit - vars.freeWantInStrat);
527: } else if (vars.credit < vars.freeWantInStrat) {
528:     token.safeTransferFrom(vars.stratAddr, address(this), vars.freeWantInStrat - vars.credit);
529: }
```
From line 525 the function checks if `vars.credit > vars.freeWantInStrat` but nowhere checks if `vars.credit == vars.freeWantInStrat`.  This causes an omission exactly when `credit == freeWantInStrat`.

## Tools Used

Manual Review

## Recommended Mitigation Steps

Change:
```solidity
525: if (vars.credit > vars.freeWantInStrat)
```

To:
```solidity
525: if (vars.credit >= vars.freeWantInStrat)
```
_rebalance() may overflow
Audit: 2023-02-ethos-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Core/contracts/ActivePool.sol#L251


# Vulnerability details

## Impact
There may be a loss of precision in the vault deposit, resulting in _rebalance overflow

## Proof of Concept
After executing ActivePool.rebalance() it will first calculate the current profit
The code is as follows:
```solidity
    function _rebalance(address _collateral, uint256 _amountLeavingPool) internal {
...
        vars.currentAllocated = yieldingAmount[_collateral];
        

        vars.yieldGenerator = IERC4626(yieldGenerator[_collateral]);
        vars.ownedShares = vars.yieldGenerator.balanceOf(address(this));
        vars.sharesToAssets = vars.yieldGenerator.convertToAssets(vars.ownedShares);

        vars.profit = vars.sharesToAssets.sub(vars.currentAllocated); //<-------vars.profit is uint256
        if (vars.profit < yieldClaimThreshold[_collateral]) {
            vars.profit = 0;
        }    
```
where vars.fit is uint256, the default is not negative
The documentation is described below:
`
The vault will be farming the lowest-risk yield possible, so you can assume that the principal will be protected from loss
`

But there is a problem, if the vault is not exclusive (only one depositor), when making a deposit is generally a loss of precision
If you deposit 100 (* e18), you will get 99.99999999999999999999 (*e18) if you take it immediately
Simplify ReaperVaultV2.sol as follows:

```solidity
  function test() external {
    uint256 totalSupply = 333*10**18;
    uint256 totalAsset = 340*10**18;
    uint256 depositAsset = 100*10**18;

    //1.get shares
    uint256 getShares = depositAsset * totalSupply / totalAsset; //round down
    totalAsset  += depositAsset;
    totalSupply += getShares;
    //2.convertToAssets use round down
    uint256 getAsset = getShares * totalAsset / totalSupply; //round down
    console.log("depositAsset:",depositAsset);
    console.log("getAsset:",getAsset);
  }
```
```console
$ forge test -vvv

[PASS] test() (gas: 4593)
Logs:
  depositAsset: 100000000000000000000
  getAsset: 99999999999999999999
```
This will result in `vars.profit = vars.sharesToAssets.sub(vars.currentAllocated);` overflow
ActivePool.sendCollateral()/ActivePool.pullCollateralFromBorrowerOperationsOrDefaultPool() will revert
Until there are gains or other depositors

## Tools Used

## Recommended Mitigation Steps

`vars.sharesToAssets.sub(vars.currentAllocated) `If less than 0, then set profit=0
Adversary can frontrun harvest call that would cause loss to vault to avoid loss
Audit: 2023-02-ethos-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Vault/contracts/ReaperVaultV2.sol#L359-L412


# Vulnerability details

## Impact

Adversary can avoid and losses to share value

## Proof of Concept

https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Vault/contracts/ReaperVaultV2.sol#L432-L453

    function _reportLoss(address strategy, uint256 loss) internal {
        StrategyParams storage stratParams = strategies[strategy];
        // Loss can only be up the amount of capital allocated to the strategy
        uint256 allocation = stratParams.allocated;
        require(loss <= allocation, "Strategy loss cannot be greater than allocation");


        if (totalAllocBPS != 0) {
            // reduce strat's allocBPS proportional to loss
            uint256 bpsChange = Math.min((loss * totalAllocBPS) / totalAllocated, stratParams.allocBPS);


            // If the loss is too small, bpsChange will be 0
            if (bpsChange != 0) {
                stratParams.allocBPS -= bpsChange;
                totalAllocBPS -= bpsChange;
            }
        }


        // Finally, adjust our strategy's parameters by the loss
        stratParams.losses += loss;
        stratParams.allocated -= loss;
        totalAllocated -= loss;
    }

When a loss is reported by the strategy to the vault it immediately decreases the share value of all vault participants. An adversary can frontrun any harvest call that would cause a loss to withdraw before the loss takes place then deposit again after to avoid the loss.

## Tools Used

Manual Review

## Recommended Mitigation Steps

I see two solutions possible:

1) Create a withdraw cooldown, i.e. withdraws must first be initiated then after a cooldown can be withdrawn
2) (A bit gas intensive) Forcing a harvest call on all strategies
OUT OF GAS
Audit: 2023-02-ethos-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/CollateralConfig.sol#L59


# Vulnerability details

## Impact
There is no upper limit on collaterals array, it increments each time when a new collateral is added. Eventually, as the count of collateral increases, gas cost of smart contract calls will raise and that there is no implemented function to reduce the array size.

`the gas consumption can be more expensive each time that a new collateral address is appended to the array, until reaching an “Out of Gas” error or a “Block Gas Limit” in the worst scenario.`.

## Proof of Concept
- [line of code in CollateralConfig](https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/CollateralConfig.sol#L59)

## Tools Used
Manual Review

## Recommended Mitigation Steps
Array’s length should be checked.