xanoutas/solidity-security-findings huggingface dataset:
DID NOT APPROVE TO ZERO FIRST 
Audit: 2023-02-ethos-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/BorrowerOperations.sol#L506
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Vault/contracts/abstract/ReaperBaseStrategyv4.sol#L74
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Vault/contracts/ReaperStrategyGranarySupplyOnly.sol#L179


# Vulnerability details

## Impact
Some tokens like `USDT` do not work when changing the allowance from an existing non-zero allowance value.
They must first be approved by zero and then the actual allowance must be approved.

`A number of features within the vaults will not work if the approve function reverts`.

## Proof of Concept
- [line of code in BorrowerOperations](https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/BorrowerOperations.sol#L506)
- [line of code in ReaperBaseStrategyv4](https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Vault/contracts/abstract/ReaperBaseStrategyv4.sol#L74)
- [line of code in ReaperStrategyGranarySupplyOnly](https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Vault/contracts/ReaperStrategyGranarySupplyOnly.sol#L179)

## Tools Used
Manual Review

## Recommended Mitigation Steps
It is recommended to set the `allowance to zero before increasing` the allowance and use `safeIncreaseAllowance`.
The vault might charge more underlying tokens from strategies because `ReaperVaultV2.report()` uses `repayment` wrongly.
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#L524


# Vulnerability details

## Impact
The strategies would be bankrupt as the vault charges more repayments than the real amount from strategies.

## Proof of Concept
Strategies can repay debts to the vault using `report()`.

```solidity
    function report(int256 _roi, uint256 _repayment) external returns (uint256) {
        LocalVariables_report memory vars;
        vars.stratAddr = msg.sender;
        StrategyParams storage strategy = strategies[vars.stratAddr];
        require(strategy.activation != 0, "Unauthorized strategy");

        if (_roi < 0) {
            vars.loss = uint256(-_roi);
            _reportLoss(vars.stratAddr, vars.loss);
        } else if (_roi > 0) {
            vars.gain = uint256(_roi);
            vars.fees = _chargeFees(vars.stratAddr, vars.gain);
            strategy.gains += vars.gain;
        }

        vars.available = availableCapital();
        if (vars.available < 0) {
            vars.debt = uint256(-vars.available);
            vars.debtPayment = Math.min(vars.debt, _repayment); //@audit-info capped with current debt

            if (vars.debtPayment != 0) {
                strategy.allocated -= vars.debtPayment;
                totalAllocated -= vars.debtPayment;
                vars.debt -= vars.debtPayment; // tracked for return value
            }
        } else if (vars.available > 0) { //@audit-info don't use _repayment at all
            vars.credit = uint256(vars.available);
            strategy.allocated += vars.credit;
            totalAllocated += vars.credit;
        }

        vars.freeWantInStrat = vars.gain + _repayment; //@audit should use vars.debtPayment instead of _repayment
        if (vars.credit > vars.freeWantInStrat) {
            token.safeTransfer(vars.stratAddr, vars.credit - vars.freeWantInStrat);
        } else if (vars.credit < vars.freeWantInStrat) {
            token.safeTransferFrom(vars.stratAddr, address(this), vars.freeWantInStrat - vars.credit);
        }

        ...
    }
```

Each strategy calls this function with the `_repayment` param that is less than the current `debt` of the strategy.

After calling `_reportLoss()` in case `_roi < 0`, the strategy's `debt` might be less than `_repayment` and it uses the minimum value of `debt` and `_repayment`.

But when tracks `freeWantInStrat` for token transfers, it uses `_repayment` instead of the capped amount so it should transfer more amounts to the vault or receive fewer amounts from the vault.

Also, with the profitable strategy, it doesn't use `_repayment` to track the strategy's state but uses it during token transfer.

With the current implementation in `ReaperStrategyGranarySupplyOnly.sol`, I couldn't find any problems as it validates `repayment <= debt` in `harvest()`.

But I think other strategies might be added later that don't validate `repayment` properly and ther
`ReaperBaseStrategyv4.harvest()` might revert in an emergency.
Audit: 2023-02-ethos-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
`ReaperBaseStrategyv4.harvest()` might revert in an emergency if there is no position on the lending pool.

As a result, the funds might be locked inside the strategy.

## Proof of Concept
The main problem is that [Aave lending pool doesn't allow 0 withdrawals](https://github.com/aave/protocol-v2/blob/554a2ed7ca4b3565e2ceaea0c454e5a70b3a2b41/contracts/protocol/libraries/logic/ValidationLogic.sol#L60-L70).

```solidity
  function validateWithdraw(
    address reserveAddress,
    uint256 amount,
    uint256 userBalance,
    mapping(address => DataTypes.ReserveData) storage reservesData,
    DataTypes.UserConfigurationMap storage userConfig,
    mapping(uint256 => address) storage reserves,
    uint256 reservesCount,
    address oracle
  ) external view {
    require(amount != 0, Errors.VL_INVALID_AMOUNT);
```

So the below scenario would be possible.

1. After depositing and withdrawing from the Aave lending pool, the current position is 0 and the strategy is in debt.
2. It's possible that the strategy has some want balance in the contract but no position on the lending pool.
It's because `_adjustPosition()` remains the debt during reinvesting and also, there is an `authorizedWithdrawUnderlying()` for `STRATEGIST` to withdraw from the lending pool.
3. If the strategy is in an emergency, `harvest()` tries to liquidate all positions(=0 actually) and it will revert because of 0 withdrawal from Aave.
4. Also, `withdraw()` will revert at [L98](https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Vault/contracts/abstract/ReaperBaseStrategyv4.sol#L98) as the strategy is in the debt.

As a result, the funds might be locked inside the strategy unless the `emergency` mode is canceled.

## Tools Used
Manual Review

## Recommended Mitigation Steps
We should check 0 withdrawal in `_withdrawUnderlying()`.

```solidity
    function _withdrawUnderlying(uint256 _withdrawAmount) internal {
        uint256 withdrawable = balanceOfPool();
        _withdrawAmount = MathUpgradeable.min(_withdrawAmount, withdrawable);

        if(_withdrawAmount != 0) {
            ILendingPool(ADDRESSES_PROVIDER.getLendingPool()).withdraw(address(want), _withdrawAmount, address(this));
        }
    }
```
REENTRANCY IN CONTRACTS/BORROWEROPERATIONS.SOL
Audit: 2023-02-ethos-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/BorrowerOperations.sol#L242-L245
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/BorrowerOperations.sol#L254-L256


# Vulnerability details

### Impact
There are several potential re-entrant functions in `Ethos-Core/contracts/BorrowerOperations.sol` in the following [lines of code in addColl function](https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/BorrowerOperations.sol#L242-L245) and [withdrawColl function](https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/BorrowerOperations.sol#L254-L256) is potentially re-entrant as it is external but has no re-entrancy guard declared. This function invokes _adjustTrove() which potentially impacts user debt, collateral top-ups or withdrawals.

### PROOF OF CONCEPT

- [lines of code in addColl function](https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/BorrowerOperations.sol#L242-L245)
- [withdrawColl function](https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/BorrowerOperations.sol#L254-L256)

### MITIGATION
```solidity
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
```

Contract module that helps prevent reentrant calls to a function.

Inheriting from ReentrancyGuard will make the nonReentrant modifier available, which can be applied to functions to make sure there are no nested (reentrant) calls to them.

Note that because there is a single nonReentrant guard, functions marked as nonReentrant may not call one another. This can be worked around by making those functions private, and then adding external nonReentrant entry points to them.
In `ReaperVaultV2`, we should update `lockedProfit` and `lastReport` before changing `lockedProfitDegradation`.
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#L617


# Vulnerability details

## Impact
The locked profit degradation for the past will be changed with the new `lockedProfitDegradation`.

As a result, malicious users can steal others' rewards by frontrunning.

## Proof of Concept
`setLockedProfitDegradation()` is used to change `lockedProfitDegradation` by admin.

```solidity
    function setLockedProfitDegradation(uint256 degradation) external {
        _atLeastRole(ADMIN);
        require(degradation <= DEGRADATION_COEFFICIENT, "Degradation cannot be more than 100%");
        lockedProfitDegradation = degradation;
        emit LockedProfitDegradationUpdated(degradation);
    }
```

And `lockedProfitDegradation` is used to calculate the locked profit.

```solidity
    function _calculateLockedProfit() internal view returns (uint256) {
        uint256 lockedFundsRatio = (block.timestamp - lastReport) * lockedProfitDegradation;
        if (lockedFundsRatio < DEGRADATION_COEFFICIENT) {
            return lockedProfit - ((lockedFundsRatio * lockedProfit) / DEGRADATION_COEFFICIENT);
        }

        return 0;
    }
```

But it doesn't update `lockedProfit` and `lastReport` before changing `lockedProfitDegradation` so the below scenario would be possible.

1. Let's assume `lockedProfit = 200, lastReport = block.timestamp` after calling `report()`, `lockedProfitDegradation` are `6 hours in blocks`.
2. 3 hours later, 100 tokens of `lockedProfit` are released and added to the free funds. We can assume `report()` wasn't called for 3 hours.
3. At that time, `lockedProfitDegradation` is changed to `4 hours in blocks` and it means `200 * 3 / 4 = 150` tokens are released. As a result, free funds are increased by 50 inside the same block.
4. So a malicious user(it should be a pool) can front run `deposit()` with huge amounts before `lockedProfitDegradation` is changed and charge most of the new rewards(=50).

Similarly, already unlocked funds will be treated as locked again if `lockedProfitDegradation` is decreased.

Even if there is no front run as all depositors are pools, it's not fair to change locked/unlocked amounts that are confirmed already.

## Tools Used
Manual Review

## Recommended Mitigation Steps
We should modify `setLockedProfitDegradation()` like below.

```solidity
    function setLockedProfitDegradation(uint256 degradation) external {
        _atLeastRole(ADMIN);
        require(degradation <= DEGRADATION_COEFFICIENT, "Degradation cannot be more than 100%");

        // update lockedProfit and lastReport
        lockedProfit = _calculateLockedProfit();
        lastReport = block.timestamp;

        lockedProfitDegradation = degradation;
        emit LockedProfitDegradationUpdated(degradation);
    }
```
Some tokens do not allow changing allowance from non-zero to non-zero
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#L180


# Vulnerability details

## Impact

For some ERC20 tokens, the collateral movements will fail due to insufficient approval.

## Proof of Concept

The protocol uses `yieldClaimThreshold` to prevent unnecessary transfer of dust collateral profit (maybe to save gas?).

```solidity
ActivePool.sol
252:         if (vars.profit < yieldClaimThreshold[_collateral]) {
253:             vars.profit = 0;//@audit-issue check how the dust remaining in the vault are processed in the end
254:         }
```

Some tokens (like USDT) do not work when changing the allowance from an existing non-zero allowance value.
They must first be approved by zero and then the actual allowance must be approved.

Reference [1](https://github.com/code-423n4/2022-12-tigris-findings/issues/104), [2](https://github.com/d-xo/weird-erc20#approval-race-protections).

## Tools Used

Manual Review

## Recommended Mitigation Steps

Approve zero first.
ActivePool.\_rebalance() does not consider the case when the vault's strategy gets loss
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
https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Core/contracts/ActivePool.sol#L282
https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Core/contracts/ActivePool.sol#L288


# Vulnerability details

## Impact

The \_rebalance() reverts if a strategy gets loss.
Because \_rebalance() is called on all important workflows, this leads to insolvency of the protocol.

## Proof of Concept

The protocol uses ReaperVaultERC4626 to manage the collateral assets and farm profit.
The vaults are connected to whitelisted strategies.

But it is not guaranteed that the strategies earn profit all the time.

On the other hand, in several places of ActivePool.\_rebalance(), the protocol assumes that it can get more than deposits all the time.

At L251, where the protocol calculates the profit by subtracting the stored `yieldingAmount` from the `sharesToAssets`, this will revert if the strategy got loss.

```solidity
ActivePool.sol
242:         // how much has been allocated as per our internal records?
243:         vars.currentAllocated = yieldingAmount[_collateral];//@audit-info current yield deposit
244:
245:         // what is the present value of our shares?
246:         vars.yieldGenerator = IERC4626(yieldGenerator[_collateral]);
247:         vars.ownedShares = vars.yieldGenerator.balanceOf(address(this));
248:         vars.sharesToAssets = vars.yieldGenerator.convertToAssets(vars.ownedShares);
249:
250:         // if we have profit that's more than the threshold, record it for withdrawal and redistribution
251:         vars.profit = vars.sharesToAssets.sub(vars.currentAllocated);//@audit-issue profit from the farming, this can revert!
252:         if (vars.profit < yieldClaimThreshold[_collateral]) {
253:             vars.profit = 0;
254:         }
```

At L282 where the protocol withdraws specifying the collateral amount to receive but it will revert if the strategy lost.

```solidity
ActivePool.sol
276:         // + means deposit, - means withdraw
277:         vars.netAssetMovement = int256(vars.toDeposit) - int256(vars.toWithdraw) - int256(vars.profit);
278:         if (vars.netAssetMovement > 0) {
279:             IERC20(_collateral).safeIncreaseAllowance(yieldGenerator[_collateral], uint256(vars.netAssetMovement));
280:             IERC4626(yieldGenerator[_collateral]).deposit(uint256(vars.netAssetMovement), address(this));
281:         } else if (vars.netAssetMovement < 0) {
282:             IERC4626(yieldGenerator[_collateral]).withdraw(uint256(-vars.netAssetMovement), address(this), address(this));//@audit-info coll received, this can revert
283:         }
```

Because \_rebalance() is called on all important workflows, this leads to insolvency of the protocol.

## Tools Used

Manual Review

## Recommended Mitigation St
Access Control Flaw
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#L192
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Vault/contracts/abstract/ReaperBaseStrategyv4.sol#L157


# Vulnerability details

## Impact

The Ethos Vault ecosystem contains an access control inconsistency whereby a lesser role can have a significant impact by bypassing the role requirement via a different contract.

The `ReaperBaseStrategyv4::setEmergencyExit` function is meant to set a strategy to "emergency" mode by setting its allocation to 0 and explicitly setting its emergency flag to `true`. This function applies the `GUARDIAN` role AC requirement which is higher than the `STRATEGIST` role as defined in `ReaperBaseStrategyv4::_cascadingAccessRoles`.

The `ReaperVaultV2::updateStrategyAllocBPS` function is meant to set a strategy's allocation, enabling it to be set to 0. This function applies the `STRATEGIST` role AC requirement which is lower than the `GUARDIAN` role as defined in `ReaperVaultV2::_cascadingAccessRoles`. This effectively permits a `STRATEGIST` to bypass the `GUARDIAN` requirement and enforce an effectual emergency mode on a strategy by setting its allocation to `0`.

## Proof of Concept

The following code illustrates the issue in action:

```sol
contract AccessControlFlaw {
    bytes32 public constant STRATEGIST = keccak256("STRATEGIST");

    function demonstrate(IStrategy strategy, IVault vault) external {
        // Pre-Requisites
        require(vault.hasRole(address(this), STRATEGIST);
        require(strategy.hasRole(address(this), STRATEGIST);
        
        try strategy.setEmergencyExit() {
            revert();
        } catch (bytes memory e) {
            // The `STRATEGIST` role is unable to invoke `ReaperBaseStrategyv4::setEmergencyExit` as expected
        }
        
        try vault.updateStrategyAllocBPS(address(strategy), 0) {
            revert("ISSUE: The `STRATEGIST` role was able to set the allocation of a strategy to 0, thereby simulating its emergency exit scenario");
        } catch (bytes memory e) {
        }
    }
}
```

## Tools Used

Manual review of the codebase.

## Recommended Mitigation Steps

The adjustment required for the codebase depends on the business requirements of the Reaper ecosystem. 

In our opinion, the current access control structure is meant to be retained in the code and the `ReaperVaultV2::updateStrategyAllocBPS` function should enforce a minimum BPS that each strategy must possess.

This will prevent a strategy from being removed unless a higher access role is possessed, similar to how strategy additions are guarded by the highest role possible.
Trove can not be closed if we have only one 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


# Vulnerability details

## Impact
This is an edge case when there is only one trove open, the owner can not close his/her trove. 
When the protocol in its early days after launch, the scenario where only one trove is open is likely. For example, Let's say we have 10 troves are open, it is possible that 9 get closed in a period where no new troves are open. In this period, the owner can not close his/her trove (if possible) till at least a new trove is open which is unfair.

## Proof of Concept

Have a look at **TroveManager**.`_closeTrove` function which is called to close a trove.

```sh
_requireMoreThanOneTroveInSystem(TroveOwnersArrayLength, _collateral);
```

It require more than one trove. This is due to the algorithm used to remove trove owners from the array. Check `_removeTroveOwner` function.
```sh
		uint128 index = Troves[_borrower][_collateral].arrayIndex;
        uint length = TroveOwnersArrayLength;
        uint idxLast = length.sub(1);

        assert(index <= idxLast);

        address addressToMove = TroveOwners[_collateral][idxLast];

        TroveOwners[_collateral][index] = addressToMove;
        Troves[addressToMove][_collateral].arrayIndex = index;
```


As explained in the comments:
> Removing owner 'B' does the following:
 [A B C D E] => [A E C D], and updates E's Trove struct to point to its new array index
 
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/TroveManager.sol#L1336-L1337

Thus, if we have only one element in the array, the algorithm above will not clear the Troves mapping for the owner (borrower) as the following line will always be executed :
```sh
 Troves[addressToMove][_collateral].arrayIndex = index;
```


## Tools Used
Manual analysis

## Recommended Mitigation Steps


- One possibility is to add one trove (that will never be removed) upon setting the addresses in **BorrowerOperations**.setAddresses function.
- Another possibility is to fix the algorithm above to allow for the only one trove to be closed by skipping updating the Troves mapping whenever there is only one trove open. However, other functional parts might need to be adjusted as well.