xanoutas/solidity-security-findings huggingface dataset:
Losses are double counted if there currently locked profits
Audit: 2023-02-ethos-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact

Losses are double counted and vault participants lose funds

## Proof of Concept

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

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

        ...

        // Profit is locked and gradually released per block
        // NOTE: compute current locked profit and replace with sum of current and new
        vars.lockedProfitBeforeLoss = _calculateLockedProfit() + vars.gain - vars.fees;
        if (vars.lockedProfitBeforeLoss > vars.loss) {
            lockedProfit = vars.lockedProfitBeforeLoss - vars.loss; @audit-issue loss double counted
        } else {
            lockedProfit = 0;
        }

When a loss is reported by the strategy is the loss is first removed from totalAllocated via _reportLoss. Additionally the loss is also removed from the lockedProfits. This is problematic because if there is locked profit then the it will unfairly be double counted and the share accounting will be incorrect. 

## Tools Used

Manual Review

## Recommended Mitigation Steps

_reportLoss should first remove the loss from the locked profit then only remove it from totalAllocated if there isn't enough locked profits to cover the entire loss.
_computeRewardsPerUnitStaked() may overflow
Audit: 2023-02-ethos-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
no judgment that lastLUSDLossError_Offset!=0, but _debtToOffset is 0
_computeRewardsPerUnitStaked() may overflow

## Proof of Concept
Use `lastLUSDLossError_Offset` in the _computeRewardsPerUnitStaked() method to store the last excess
The code is as follows:

```solidity
    function _computeRewardsPerUnitStaked(
        address _collateral,
        uint _collToAdd,
        uint _debtToOffset,
        uint _totalLUSDDeposits
    )
        internal
        returns (uint collGainPerUnitStaked, uint LUSDLossPerUnitStaked)
    {
...

       if (_debtToOffset == _totalLUSDDeposits) {
            LUSDLossPerUnitStaked = DECIMAL_PRECISION;  // When the Pool depletes to 0, so does each deposit 
            lastLUSDLossError_Offset = 0;
        } else {

            uint LUSDLossNumerator = _debtToOffset.mul(DECIMAL_PRECISION).sub(lastLUSDLossError_Offset);//<----------If _debtToOffset==0,but lastLUSDLossError_Offset!=0 will overflow
            /*    
```

Here is a problem, there is no judgment that lastLUSDLossError_Offset!=0, but _debtToOffset is 0, so when this method is executed will overflow
is it possible for _debtToOffset to be 0?
This is possible
`updateRewardSum()` is to pass _debtToOffset=0
The code is as follows:

```solidity
    function updateRewardSum(address _collateral, uint _collToAdd) external override {
        _requireCallerIsActivePool();
        uint totalLUSD = totalLUSDDeposits; // cached to save an SLOAD
        if (totalLUSD == 0) { return; }

        _triggerLQTYIssuance(communityIssuance);

        (uint collGainPerUnitStaked, ) = _computeRewardsPerUnitStaked(_collateral, _collToAdd, 0, totalLUSD);//<---------_debtToOffset==0


    }
```

In ActivePool._rebalance() will call StabilityPool.updateRewardSum()

This will cause the ActivePool to overflow and fail if there is a residual `lastLUSDLossError_Offset` after the execution of StabilityPool.offset().


## Tools Used

## Recommended Mitigation Steps

_debtToOffset < lastLUSDLossError_Offset then return LUSDLossPerUnitStaked =0
Users would lose some shares during withdrawal in `ReaperVaultV2._withdraw()`.
Audit: 2023-02-ethos-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
`ReaperVaultV2._withdraw()` burns 100% of shares even if the vault balance is less than the required underlying amount.

As a result, users would lose some shares during withdrawal.

## Proof of Concept
Users can receive underlying tokens by burning their shares using `_withdraw()`.

If the vault doesn't have enough underlying balance, it withdraws from strategies inside `withdrawalQueue`.

```solidity
File: ReaperVaultV2.sol
359:     function _withdraw(
360:         uint256 _shares,
361:         address _receiver,
362:         address _owner
363:     ) internal nonReentrant returns (uint256 value) {
364:         require(_shares != 0, "Invalid amount");
365:         value = (_freeFunds() * _shares) / totalSupply();
366:         _burn(_owner, _shares);
367: 
368:         if (value > token.balanceOf(address(this))) { 
398:             ....
399:             vaultBalance = token.balanceOf(address(this));
400:             if (value > vaultBalance) {
401:                 value = vaultBalance; //@audit should reduce shares accordingly
402:             }
403: 
404:             require(
405:                 totalLoss <= ((value + totalLoss) * withdrawMaxLoss) / PERCENT_DIVISOR,
406:                 "Withdraw loss exceeds slippage"
407:             );
408:         }
409: 
410:         token.safeTransfer(_receiver, value);
411:         emit Withdraw(msg.sender, _receiver, _owner, value, _shares);
412:     }
```

After withdrawing from the strategies of `withdrawalQueue`, it applies the max cap at L401.

But as we can see from [setWithdrawalQueue()](https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Vault/contracts/ReaperVaultV2.sol#L258), `withdrawalQueue` wouldn't contain all of the active strategies and the above condition at L400 will be true.

In this case, users will get fewer underlying amounts after burning the whole shares that they requested.

As a reference, it recalculates the shares for the above case in [Yearn vault](https://github.com/yearn/yearn-vaults/blob/master/contracts/Vault.vy#L1151).

```solidity
    if value > vault_balance:
        value = vault_balance
        # NOTE: Burn # of shares that corresponds to what Vault has on-hand,
        #       including the losses that were incurred above during withdrawals
        shares = self._sharesForAmount(value + totalLoss)
```

## Tools Used
Manual Review

## Recommended Mitigation Steps
We should recalculate the shares and burn them rather than burn all shares.
Ethos stil using LUSD (not ERN) will lead to confusion and ambiguity of the protocol resulting uncountable risk for the project
Audit: 2023-02-ethos-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
Ethos stil using LUSD (not ERN) will lead to confusion and ambiguity of the protocol resulting uncountable risk for the project

## Proof of Concept
Ethos is a fork project of Liquity with additional changes, supporting multi collateral tokens. There are some other protocol already running and fork this Liquity, such as :
- Vesta Finance
- Yeti Finance

more complete list is on https://defillama.com/forks/Liquity

Those two sample are forks of Liquity, but replacing the Liquity terms (and token) with their own terms.

Current Ethos codebase heavily use the Liquity terms, including LUSD terms. In fact major protocol ERC20 token is still using LUSD name for its contract, including the name and symbol.

```js
File: LUSDToken.sol
28: contract LUSDToken is CheckContract, ILUSDToken {
29:     using SafeMath for uint256;
30:     
31:     uint256 private _totalSupply;
32:     string constant internal _NAME = "LUSD Stablecoin";
33:     string constant internal _SYMBOL = "LUSD";
34:     string constant internal _VERSION = "1";
35:     uint8 constant internal _DECIMALS = 18;
```

Why this is high issue, because the main ERC20 token protocol, supposed to be the ERN (Ethos Reserve Note), but currently is using LUSD Stablecoin.

Even though no direct loss of the protocol, this ERC20 metadata ambiguity may lead to protocol confusion, because people will think the Ethos is using the original LUSD token from Liquity, which is not.

If Ethos deploy current codebase, and still use the `LUSDToken.sol`, since it's not an upgradable contract, it will stay as is, unforseable issue will raise.

## Tools Used

Manual Analysis

## Recommended Mitigation Steps

Replace all LUSD signature and use Ethos own definition of names including contract name, ERC20 metadata name, and any LUSD reference functions.
Strategist can break the whole protocol
Audit: 2023-02-ethos-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Vault/contracts/ReaperVaultV2.sol#L319
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Vault/contracts/ReaperVaultV2.sol#L359
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/ActivePool.sol#L239


# Vulnerability details

## Background

There is known attack to ERC4626 vaults, called [inflation attacks](https://mixbytes.io/blog/overview-of-the-inflation-attack). 

It seems that the ReaperVault is not affected since the only depositor will be the ActivePool contract. But what actually the internal function `_deposit(uint256 amount, address _receiver)` validates is that the `msg.sender` has at least the role of `DEPOSITOR`. The internal function `_cascadingAccessRoles()` states that the roles `DEFAULT_ADMIN_ROLE`, `ADMIN`, `GUARDIAN` and `STRATEGIST` are roles above `DEPOSITOR`. This allows any strategist to deposit to the vault, and hence being able to carry on an inflation attack to the ActivePool contract.

## Impact

To know the impact lets analyze the state of the protocol after a successful inflation attack. Alice is about to open the first trove using WETH as colleteral. To not lose generality we assume ActivePool.sol will deposit 10 Ether to the ReaperVault taken from the total amount of collateral of Alice. A malicious strategist sees this, and front-run the Alice transaction by depositing 1 wei and transfering to the contract 10 ether. After the strategist transaction the shares for the ActivePool is computed as:

```
shares = (10 ether * 1) / 10 ether + 1
```

This will result to 0 shares for the ActivePool. Up to this point the Strategist can redeem its share and steal the 10 ether, this will reset the state of the vault to zero shares and zero assets. The admin can revoke the strategist, deposit 10 ether, get 10 shares and transfer them to the ActivePool contract, and the protocol can continue normally.

But what happens if the attacker decides to not redeem its share?. 

1.- Alice cannot get her collateral back (anything). Every time an operation involves the movement of collateral, the ActivePool tries to do a rebalance. The next code snippet is take from the `_rebalance` function at Active pool:

```
LocalVariables_rebalance memory vars;

// how much has been allocated as per our internal records?
vars.currentAllocated = yieldingAmount[_collateral];
        
// what is the present value of our shares?
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);
```
The transaction will revert due to `vars.sharesToAssets` been zero, because the ActivePool has zero shares.

2.- No one can open another trove. For the same reason above.

Basically, the borrower operations for WETH as collateral are DoSed. Trying to fix it
Lack of logic to mint OATH within the CommunityIssuance#`issueOath()`, which results in that entire OATH insurance mechanism will not work properly
Audit: 2023-02-ethos-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/StabilityPool.sol#L330
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/StabilityPool.sol#L374
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/StabilityPool.sol#L417
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/LQTY/CommunityIssuance.sol#L83-L95


# Vulnerability details

## Impact
OATH will be never minted (issued) when the the CommunityIssuance#`issueOath()` would be called. As a result, OATH will be never provided to the market. This results in that the treasury will be never able to buy the OATH from the market. This lead to a bad situation that entire OATH insurance mechanism will not work properly.


## Proof of Concept
Within the StabilityPool#`provideToSP()` and the StabilityPool#`withdrawFromSP()`,
the StabilityPool#`_triggerLQTYIssuance()` would be called like this:
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/StabilityPool.sol#L330
```solidity
    // --- External Depositor Functions ---

    /*  provideToSP():
    *
    * - Triggers a LQTY issuance, based on time passed since the last issuance. The LQTY issuance is shared between *all* depositors
    * - Sends depositor's accumulated gains to depositor
    * - Increases depositor's deposit, and takes new snapshot.
    */
    function provideToSP(uint _amount) external override {
        ...
        ICommunityIssuance communityIssuanceCached = communityIssuance;

        _triggerLQTYIssuance(communityIssuanceCached);  /// @audit 
        ...
```
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/StabilityPool.sol#L374
```solidity
    /*  withdrawFromSP():
    *
    * - Triggers a LQTY issuance, based on time passed since the last issuance. The LQTY issuance is shared between *all* depositors
    * - Sends all depositor's accumulated gains to depositor
    * - Decreases depositor's deposit, and takes new snapshot.
    *
    * If _amount > userDeposit, the user withdraws all of their compounded deposit.
    */
    function withdrawFromSP(uint _amount) external override {
        ...
        ICommunityIssuance communityIssuanceCached = communityIssuance;

        _triggerLQTYIssuance(communityIssuanceCached);  /// @audit 
        ...
```

Within the StabilityPool#`_triggerLQTYIssuance()`,
the CommunityIssuance#`issueOath()` would be called like this:
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/StabilityPool.sol#L417
```solidity
    // --- LQTY issuance functions ---

    function _triggerLQTYIssuance(ICommunityIssuance _communityIssuance) internal {
        uint LQTYIssuance = _communityIssuance.issueOath();  /// @audit
       _updateG(LQTYIssuance);
    }
```

Within the CommunityIssuance#`issueOath()`, the code would be written like this:
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/LQTY/CommunityIssuan
Using _freefunds() to calculate share price lead to shares miscalculations and could benefit treasury
Audit: 2023-02-ethos-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
Function `_freeFunds()`  calculates the amount of free funds available after profit locking and it's used to for calculating share price, issuing shares during deposit, or burning shares during withdrawal.

Value returned by `_freeFunds()` is time-dependant, because value returned by `_calculateLockedProfit()` depends on **WHEN** it is called.

    function _freeFunds() internal view returns (uint256) {
        return balance() - _calculateLockedProfit();
    }

Function `_calculateLockedProfit()` calculates the amount of locked profit from recent harvests and it returns a value that depends on time from `lastReport`.

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

`lastReport` is updated when a strategy interacts with the vault during its harvest by calling function `report()` profit/loss as well as any repayment of debt.

This is the snippet from `report()` function where `lockedProfit` is updated:

        vars.lockedProfitBeforeLoss = _calculateLockedProfit() + vars.gain - vars.fees;
        if (vars.lockedProfitBeforeLoss > vars.loss) {
            lockedProfit = vars.lockedProfitBeforeLoss - vars.loss;
        } else {
            lockedProfit = 0;
        }
        strategy.lastReport = block.timestamp;
        lastReport = block.timestamp;

In case there's no gain `lockedProfit` will be **DECREASED** because of fees, losses or lockedProfit degradation.

For instance `_freefunds()` is used to calculate shares amount to be minted in function `_deposit()` which is called by ActivePool:
        
        uint256 freeFunds = _freeFunds();
        if (totalSupply() == 0) {
            shares = _amount;
        } else {
            shares = (_amount * totalSupply()) / freeFunds; 
        }

That means that shares minted for ActivePool are based also on **WHEN** the deposit is carried out instead only on balances deposited.
For instance an issue that can rise is that in case ActivePool (which accounts fund of investors)  deposit an amount just after `lastReport` updating will receive less shares than when depositing the same amount after updated.
Function `report(int256 _roi, uint256 _repayment)` is called by function `harvest()` of strategies contract, which allowed callers are restricted by `_atLeastRole(KEEPER);` therefore updating of `lockedProfit` depends on KEEPER.

This would be in benefit or detrimental of the other ent
`closeTrove` never nulls `trove.stake`
Audit: 2023-02-ethos-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

// Auditor's note: not 100% sure if this is intentional, but I have reason to believe it's a mistake.

## Description
When a trove gets liquidated, its stake gets set to 0 (through `_removeStake`, called eg [here](https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Core/contracts/TroveManager.sol#L339)). However, when a trove gets closed gratiously through `_closeTrove`, the stake is never nulled.

## Exploit scenario
Not nulling a stake could cause unforseen consequences if other areas of the code assume that a non-existent (closed) trove doesn't have a stake.

## Recommendation
Either ensure all data relating to a trove gets nulled on closure, or provide documentation (code comments) for why th following invariant doesn't hold:

- opening a trove and closing it leaves the system unchanged (except for the order of elements in `TroveOwners`).
Callback tokens can cause re-entrancy
Audit: 2023-02-ethos-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Core/contracts/ActivePool.sol#L210
https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Core/contracts/BorrowerOperations.sol#L231
https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Vault/contracts/ReaperVaultV2.sol#L328


# Vulnerability details

## Description
There are many places in the code where token transfers are in the middle of a function (more specifically, both after and before state changes). It is very important not to pass control to untrusted code in these calls, as they could potentially re-enter and cause havoc, since the system is at present in a "dirty state" (i.e. a yet-uncommitted temporary state where important system invariants might not hold).

## Exploit scenario
Mallory is a malicious hacker. She interacts with the system in such a way that she is at either the receiving or sending end of a transfer of a token with callback support. Since system invariants don't hold at this time, she is able to exploit the situation to make a profit, potentially at the expense of other users.

## Recommendation
Review every token that the system interacts with to ensure it does not hand control to untrusted code, in particular callback tokens. Callback tokens that are also compliant with ERC20 include ERC223 and 777. ERC721 and 1155 have slightly different semantics and interface, but also allow for callbacks. Additionally:

- ensure protocol tokens (Ern and Oath) do not allow callbacks. This was the cause of the Siren hack [link](https://medium.com/siren/siren-incident-report-264e57f16d7).
- ensure the user may not submit a token that is untrusted. This was the precipitate of the Origin Dollar hack [link](https://peckshield.medium.com/origin-dollar-incident-root-cause-analysis-f27e11988c90).
A Malicious Borrower can drain ActivePool
Audit: 2023-02-ethos-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
Any Borrower can take any amount of collateral and the pool gets drained, in this case, the system's TCR will go way down the CCR. putting the whole system at risk of a dangerous recovery mode and may even cause a total crash to the system.

## Proof of Concept
https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Core/contracts/ActivePool.sol#L171-L188

 function sendCollateral(address _collateral, address _account, uint _amount) external override {
        _requireValidCollateralAddress(_collateral);
        _requireCallerIsBOorTroveMorSP(); // in our case it is a malicious borrower (or TroveManager!)
   ....
else { // if the caller is a Borrower
            IERC20(_collateral).safeTransfer(_account, _amount); // transfer the collateral amount
        }
    }

As shown, the requirements cover only the collateral and role checks, not the amount of collateral so, Any borrower can monitor the pool to know how much collateral it has and then call sendCollateral to the _account leaving the pool with 0 balance.


## Tools Used
  VSCode

## Recommended Mitigation Steps
-Borrower interactions with the system should only be through BorrowerOperations with no direct contact with the pools to mitigate the risk of misused rights.
- If it is necessary to leave this action then, a check for the borrower position and requested collateral amount can help a bit.