xanoutas/solidity-security-findings huggingface dataset:
It is possible that strategy cannot be harvested and its loss fails to be reported or realized
Audit: 2023-02-ethos-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Vault/contracts/abstract/ReaperBaseStrategyv4.sol#L109-L138
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Vault/contracts/ReaperStrategyGranarySupplyOnly.sol#L114-L142
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Vault/contracts/ReaperVaultV2.sol#L493-L560
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Vault/contracts/ReaperVaultV2.sol#L432-L453


# Vulnerability details

## Impact
When calling the `ReaperBaseStrategyv4.harvest` function, the `debt` can be positive because the `availableCapital` returned by calling `IVault(vault).availableCapital()` can be negative. Then, the `debt` is used to call the `ReaperStrategyGranarySupplyOnly._harvestCore` function.

https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Vault/contracts/abstract/ReaperBaseStrategyv4.sol#L109-L138
```solidity
    function harvest() external override returns (int256 roi) {
        _atLeastRole(KEEPER);
        int256 availableCapital = IVault(vault).availableCapital();
        uint256 debt = 0;
        if (availableCapital < 0) {
            debt = uint256(-availableCapital);
        }

        uint256 repayment = 0;
        if (emergencyExit) {
            ...
        } else {
            (roi, repayment) = _harvestCore(debt);
        }

        debt = IVault(vault).report(roi, repayment);
        ...
    }
```

When calling the `ReaperStrategyGranarySupplyOnly._harvestCore` function, if `totalAssets < allocated` is true, then the `roi` is first set to `-int256(allocated - totalAssets)` and is negative. The  `amountFreed` and `loss` returned by calling `_liquidatePosition(toFree)`, where `toFree` is the `_debt`, would add up to the `_debt`. Since `loss` can be positive, `roi` is further reduced by the `loss` after executing `roi -= int256(loss)`. However, this can cause the `-_roi` to be larger than the vault's token amount that is allocated to the strategy.

https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Vault/contracts/ReaperStrategyGranarySupplyOnly.sol#L114-L142
```solidity
    function _harvestCore(uint256 _debt) internal override returns (int256 roi, uint256 repayment) {
        ...

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

        if (totalAssets > allocated) {
            ...
        } else if (totalAssets < allocated) {
            roi = -int256(allocated - totalAssets);
        }

        (uint256 amountFreed, uint256 loss) = _liquidatePosition(toFree);
        repayment = MathUpgradeable.min(_debt, amountFreed);
        roi -= int256(loss);
    }
```

Calling the `ReaperVaultV2.report` function then calls the `ReaperVaultV2._reportLoss` function because the `_roi` is negative. Since the `roi` was further reduced by the `loss` in the `ReaperStrategyGranarySupplyOnly._harvestCore` function that caused the the `-_roi`
`upgradeProtocol` can create Peg Risk via Oracle Price Arbitrage
Audit: 2023-02-ethos-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

### Impact
`upgradeProtocol` is meant to enable a new version of the protocol while retaining the same LUSD token

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

```solidity
    function upgradeProtocol(
```

In case of a migration, with the same collateral, but a new oracle, the system could open up to arbitrage between the two oracles via redemptions, allowing to extract value from the difference between the 2 prices.

This is because each oracle (e.g. chainlink), can change it's price based on two aspects:
- Hearbeat -> Maximum amount of time before the feed is updated
- Threshold -> % change at which the price is changed no matter what

In case of the oracle being different, for example having a different heartbeat setting, or simply having a different cadence (e.g. one refreshes at noon the other at 3 pm), the difference can open up to Arbitrage Strategies that can potentially increase risk to the system


### Arbitrage through Redemptions Explanation

The fact that that older version of the protocol can burn means they could allow for redemption arbitrage, leaking value.

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

```solidity
        // old versions of the protocol may still burn

```

Burning of tokens can be performed via two operations:
- Repayment
- Redemptions

Repayment seems to be safest options and it's hard to imagine a scenario for exploit.

If the oracle offers a different price for redemptions, that can crate an incentive to go redeem against the older system, and since the older system cannot create new Troves, the CR for it could suffer.

The way in which this get's problematic is if there's positions that risk becoming under-collateralized in the old system and the debt from those positions is used to redeem against better collateralized positions on the new migrated system

This would create an economic incentive to leave the bad debt in older system as the new one is offering a more profitable opportunity


### Additional Resources

An example of desynch is what happened to a Gearbox Ninja, that got liquidated due to hearbeat differences

https://twitter.com/gearbox_intern/status/1587957233605918721

### Remediation Steps

It will be best to ensure that a collateral is either in the old system, or on the new system, and if the same collateral is in both version, I believe the Oracle must be the same as to avoid inconsistent pricing.

It may also be best to change the migration pattern to one based on Zaps, which will offer good UX but reduce risk to the LUSD peg dynamic
Calling `ReaperBaseStrategyv4.harvest` function loses accrued rewards if `emergencyExit` is true
Audit: 2023-02-ethos-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Vault/contracts/ReaperStrategyGranarySupplyOnly.sol#L114-L142
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Vault/contracts/abstract/ReaperBaseStrategyv4.sol#L109-L138


# Vulnerability details

## Impact
When the `ReaperStrategyGranarySupplyOnly._harvestCore` function is called, the `ReaperStrategyGranarySupplyOnly._claimRewards` function is further called to claim the rewards, and the claimed reward tokens are then swapped to the underlying tokens; then, the `ReaperStrategyGranarySupplyOnly._liquidatePosition` function is called to withdraw tokens from the corresponding Granary lending pool.

https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Vault/contracts/ReaperStrategyGranarySupplyOnly.sol#L114-L142
```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;

        ...

        (uint256 amountFreed, uint256 loss) = _liquidatePosition(toFree);
        repayment = MathUpgradeable.min(_debt, amountFreed);
        roi -= int256(loss);
    }
```

However, unlike the `ReaperStrategyGranarySupplyOnly._harvestCore` function, if `emergencyExit` is true, calling the following `ReaperBaseStrategyv4.harvest` function would only call the `ReaperStrategyGranarySupplyOnly._liquidateAllPositions` function to withdraw the strategy's token balance from the corresponding Granary lending pool without claiming any rewards. In this case, the accrued rewards that should belong to the strategy are lost; because the vault's strategy does not claim such deserved rewards, the owners of the vault's shares lose these rewards that they are entitled to.

https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Vault/contracts/abstract/ReaperBaseStrategyv4.sol#L109-L138
```solidity
    function harvest() external override returns (int256 roi) {
        _atLeastRole(KEEPER);
        int256 availableCapital = IVault(vault).availableCapital();
        uint256 debt = 0;
        if (availableCapital < 0) {
            debt = uint256(-availableCapital);
        }

        uint256 repayment = 0;
        if (emergencyExit) {
            uint256 amountFreed = _liquidateAllPositions();
            if (amountFreed < debt) {
                roi = -int256(debt - amountFreed);
            } else if (amountFreed > debt) {
`lockedProfit` can become inaccurate when calling `ReaperVaultV2.report` function if vault's underlying token is a fee-on-transfer token
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#L306-L338
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Vault/contracts/ReaperVaultV2.sol#L493-L560


# Vulnerability details

## Impact
The comment of the `ReaperVaultV2.deposit` function and the implementation of the `ReaperVaultV2._deposit` function indicate that the vault wants to support fee-on-transfer tokens. However, the accounting for `lockedProfit` can become inaccurate when calling the `ReaperVaultV2.report` function if the vault's underlying token is a fee-on-transfer token.

https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Vault/contracts/ReaperVaultV2.sol#L306-L338
```solidity
    /**
     ...
     * @notice the _before and _after variables are used to account properly for
     * 'burn-on-transaction' tokens.
     ...
     */
    function deposit(uint256 _amount) external {
        _deposit(_amount, msg.sender);
    }

    ...
    function _deposit(uint256 _amount, address _receiver) internal nonReentrant returns (uint256 shares) {
        ...
        uint256 balBefore = token.balanceOf(address(this));
        token.safeTransferFrom(msg.sender, address(this), _amount);
        uint256 balAfter = token.balanceOf(address(this));
        ...
    }
```

When calling the following `ReaperVaultV2.report` function with a positive `_roi`, `vars.gain` is set to this positive `_roi`. If `vars.credit < vars.freeWantInStrat` is true, where `vars.freeWantInStrat` includes the `vars.gain`, `token.safeTransferFrom(vars.stratAddr, address(this), vars.freeWantInStrat - vars.credit)` is executed for transferring the corresponding token amount from the strategy to the vault. Then, if `vars.lockedProfitBeforeLoss > vars.loss` is true, where `vars.lockedProfitBeforeLoss` also includes the `vars.gain`, `lockedProfit = vars.lockedProfitBeforeLoss - vars.loss` is executed for locking the profit.

https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Vault/contracts/ReaperVaultV2.sol#L493-L560
```solidity
    function report(int256 _roi, uint256 _repayment) external returns (uint256) {
        LocalVariables_report memory vars;
        vars.stratAddr = msg.sender;
        StrategyParams storage strategy = strategies[vars.stratAddr];
        ...

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

        ...

        vars.freeWantInStrat = vars.gain + _repayment;
        if (vars.credit > vars.freeWantInStrat) {
            ...
        } else if (vars.credit < vars.freeWantInStrat) {
            token.safeTransferFrom(vars.stratAddr, address(this), vars.freeWantInStrat - vars.credit);
        }

        ...
        vars.lockedProfitBeforeLoss = _calculateLockedProfit() + vars.gain - vars.fees;
        if (vars.lockedProfitBeforeLoss > vars.loss) {
wrong value is passed in withdrawing from the yield generator when `ActivePool` is rebalanced 
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#L282
https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Core/contracts/ActivePool.sol#L302


# Vulnerability details

In `ActivePool#_rebalance()` after the code determines the `netAssetMovement` value for withdrawing the wrong value is used with the yield generator `withdraw` method.

## Impact
In rebalancing when withdrawing from yield generator the `ActivePool` contract extracts less collateral asset then calculated. In case of profit distribution this can possible result in a unintended revert in case the contract entire balance end with less collateral that we can distribute.

## Proof of Concept

In this line we are using the wrong value to withdraw assets from the yield generator. The correct value to use with the withdraw functions is the value of shares and not assets:

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

A possible revert can happen when the `ActivePool` ends having less collateral asset in its balance then calculated for splitting on the next line:

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

Resulting in a unintended revert

## Tools Used
Manual review

## Recommended Mitigation Steps
Convert calculated asset value to shares and then withdraw

```diff
} else if (vars.netAssetMovement < 0) {
+   uint256 shares = IERC4626(yieldGenerator[_collateral]).convertToShares(uint256(-vars.netAssetMovement));
    IERC4626(yieldGenerator[_collateral]).withdraw(shares, address(this), address(this));
}

```
value can be burned in `burnLPToken` but nothing withdrawn
Audit: 2022-11-stakehouse-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2022-11-stakehouse/blob/23c3cf65975cada7fd2255a141b359a6b31c2f9c/contracts/liquid-staking/SavETHVault.sol#L174


# Vulnerability details

## value can be burned in `burnLPToken` but nothing withdrawn
### Summary
If initial amount is not big enough, value can get truncated by division of 24 ether and therefore be 0 the `redemptionValue`. Therefore, this value to be withdrawn would be lost.

### PoC
https://github.com/code-423n4/2022-11-stakehouse/blob/23c3cf65975cada7fd2255a141b359a6b31c2f9c/contracts/liquid-staking/SavETHVault.sol#L174
```
// redeem savETH from the vault
            redemptionValue = (dETHDetails.savETHBalance * _amount) / 24 ether;

            // withdraw dETH (after burning the savETH)
            getSavETHRegistry().withdraw(msg.sender, uint128(redemptionValue));

            uint256 dETHRedeemed = getSavETHRegistry().savETHToDETH(redemptionValue);

            emit DETHRedeemed(msg.sender, dETHRedeemed);
            return redemptionValue;

```


### Mitigation
Check the value is big enough and revert if it doesn't work as expected
Users of `StakingFundsVault` can never redeem their `LP token` once staking has started
Audit: 2022-11-stakehouse-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2022-11-stakehouse/blob/4b6828e9c807f2f7c569e6d721ca1289f7cf7112/contracts/liquid-staking/StakingFundsVault.sol#L180-L183


# Vulnerability details

The `burnLPToken` of a Fees & MEV vault allow users to burn LP tokens in exchange of ETH.

Quoting the documentation 

```
Every user has a right to leave the LSD network at anytime. A depositor/staker can simply sell their LP tokens to someone else or burn to redeem ETH or dETH
```

But there is a check in `StakingFundsVault.sol` reverting `burnLPToken` unless the Lifecycle of the validator is `INITIALS_REGISTERED`

```solidity
180: require(
181:             getAccountManager().blsPublicKeyToLifecycleStatus(blsPublicKeyOfKnot) == IDataStructures.LifecycleStatus.INITIALS_REGISTERED,
182:             "Cannot burn LP tokens"
183:         );
```

This means once the derivatives have been minted for a validator, a user will not be able to redeem their `LP token` for ETH. Even after staking withdrawal, when the lifecycle status has been updated to `EXITED`.



## Impact

High

## Proof of Concept


Add this test to `StakingFundsVault.t.sol`, showing that when the lifecycle is `EXITED`, the user cannot retrieve their `ETH`:

```solidity
function testBurnLPRevert() public {
    // @audit - user deposits ETH in vault
    maxETHDeposit(accountFive, getBytesArrayFromBytes(blsPubKeyOne));

    // warp speed ahead
    vm.warp(block.timestamp + 3 hours);

    // @audit - Move lifecycle straight to exited 
    vault.accountMan().setLifecycleStatus(blsPubKeyOne, 4);

    vm.startPrank(accountFive);
    LPToken token = vault.lpTokenForKnot(blsPubKeyOne);

    // @audit - The call reverts
    vm.expectRevert("Cannot burn LP tokens");
    vault.burnLPForETH(token, maxStakingAmountPerValidator);
    vm.stopPrank();
}
```

You also need to make the following adjustments in the test file:

```diff
7: import {
8:     TestUtils,
9:     MockLSDNFactory,
10:     MockLiquidStakingManager,
11:     MockAccountManager,
+12:     MockStakingFundsVault,
13:     IDataStructures
14: } from "../utils/TestUtils.sol";
15: 
16: contract StakingFundsVaultTest is TestUtils {
17: 
18:     address operations = accountOne;
19: 
20:     MockLiquidStakingManager liquidStakingManager;
+21:     MockStakingFundsVault vault; //@audit - change to access account manager
22: 
23:     uint256 maxStakingAmountPerValidator;
...
...
52:     function setUp() public {
53:         factory = createMockLSDNFactory();
54:         liquidStakingManager = deployDefaultLiquidStakingNetwork(factory, admin);
55:         manager = liquidStakingManager;
+56:         vault = MockStakingFundsVault(payable(address(liquidStakingManager.stakingFundsVault()))); //@audit - vault 
57:         maxStakingAmountPerValidator = vault.maxStakingAmountPerValidator();
```

## Tools Used

Manual Analysis, Foundry

## Mitigation

The vault should allow users to redeem their LP tokens for ETH when the validator lifecycle has come to an end
HIGH: Vault or pool funds can be stolen by any user who holds LP tokens.
Audit: 2022-11-stakehouse-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2022-11-stakehouse/blob/4b6828e9c807f2f7c569e6d721ca1289f7cf7112/contracts/liquid-staking/SyndicateRewardsProcessor.sol#L63


# Vulnerability details

## Description

The \_distributeETHRewardsToUserForToken is implemented in SyndicateRewardsProcessor and used in derived contracts: GiantMevAndFeePool and StakingFundsVault.
It sends out rewards to the given user according to the current accumulatedETHPerLPShare and the previous user's claimed value:

```
function _distributeETHRewardsToUserForToken(
    address _user,
    address _token,
    uint256 _balance,
    address _recipient
) internal {
    require(_recipient != address(0), "Zero address");
    uint256 balance = _balance;
    if (balance > 0) {
        // Calculate how much ETH rewards the address is owed / due 
        uint256 due = ((accumulatedETHPerLPShare * balance) / PRECISION) - claimed[_user][_token];
        if (due > 0) {
            claimed[_user][_token] = due;
            totalClaimed += due;
            (bool success, ) = _recipient.call{value: due}("");
            require(success, "Failed to transfer");
            emit ETHDistributed(_user, _recipient, due);
        }
    }
}
```

The issue is that claimed[\_user][\_token] is *overwritten* with due instead of adding due to current amount. This means the previous claimed value is totally discarded and the delta from the previous one is the total claimed. Interestingly, in the first invocation of \_distributeETHRewardsToUserForToken, when claimed[\_user][\_token] = 0, claimed is calculated correctly (since = and += are the same when left side is 0). 

This can be exploited to steal from the vault or Giant pool an arbitrary amount because the distribute function is called every token transfer by a hook.

The beforeTransferHook called:
```
function beforeTokenTransfer(address _from, address _to, uint256) external override {
    address syndicate = liquidStakingNetworkManager.syndicate();
    if (syndicate != address(0)) {
        LPToken token = LPToken(msg.sender);
        bytes memory blsPubKey = KnotAssociatedWithLPToken[token];
        require(blsPubKey.length > 0, "Invalid token");
        if (getAccountManager().blsPublicKeyToLifecycleStatus(blsPubKey) == IDataStructures.LifecycleStatus.TOKENS_MINTED) {
            // Claim any ETH for the BLS key mapped to this token
            bytes[] memory keys = new bytes[](1);
            keys[0] = blsPubKey;
            _claimFundsFromSyndicateForDistribution(syndicate, keys);
            // Update the accumulated ETH per minted derivative LP share
            updateAccumulatedETHPerLP();
            // distribute any due rewards for the `from` user
            if (_from != address(0)) {
                _distributeETHRewardsToUserForToken(_from, address(token), token.balanceOf(_from), _from);
            }
            // in case the new user has existing rewards - give it to them so that the after transfer hook does not wipe pending