xanoutas/solidity-security-findings huggingface dataset:
`withdrawLPTokens` in `GiantPoolBase` can be used to trick users out of their `lpTokenETH`
Audit: 2022-11-stakehouse-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details


## Impact
Users can have their `lpTokenETH` burned but get no real `LPTokens` in return if tricked into supplying fake _lpTokens addresses to `withdrawLPTokens`.
https://github.com/code-423n4/2022-11-stakehouse/blob/4b6828e9c807f2f7c569e6d721ca1289f7cf7112/contracts/liquid-staking/GiantPoolBase.sol#L69

## Proof of Concept
In `withdrawLPTokens`, `_lpTokens` are checked for their validity through
```
_assertUserHasEnoughGiantLPToClaimVaultLP(token, amount);
```
which, in turn, checks
```
require(_amount >= MIN_STAKING_AMOUNT, "Invalid amount");
require(_token.balanceOf(address(this)) >= _amount, "Pool does not own specified LP");

```
However, if the website gets hacked and when users interact with `withdrawLPTokens`, they might be supplying fake `_lpTokens` as specified by the attacker. 
These fake contracts can return anything needed to pass the above checks easily. 
After theses checks, users' legitimate `lpTokenETH` will be burned:
```
lpTokenETH.burn(msg.sender, amount); 
```
but get fake `_lpTokens` in return.

## Tools Used
manual

## Recommended Mitigation Steps
Always passing liquid staking manager address, checking its real and then requesting either the savETH vault or staking funds vault is a good idea to prove the validity of vaults. From vault, one can then verify lpTokens.
Users can `withdrawDETH` in `GiantSavETHVaultPool.sol` without burning his lpTokens if they supply fake vault and lpToken
Audit: 2022-11-stakehouse-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
User's `lpTokenETH` and `_lpTokens` will be out-of-sync
https://github.com/code-423n4/2022-11-stakehouse/blob/4b6828e9c807f2f7c569e6d721ca1289f7cf7112/contracts/liquid-staking/GiantSavETHVaultPool.sol#66

## Proof of Concept
In `withdrawDETH`, `vault` and `token` are checked for their validity through
```
_assertUserHasEnoughGiantLPToClaimVaultLP(token, amount);
```
which, in turn, checks
```
require(_amount >= MIN_STAKING_AMOUNT, "Invalid amount");
require(_token.balanceOf(address(this)) >= _amount, "Pool does not own specified LP");

```
and 
```
require(vault.isDETHReadyForWithdrawal(address(token)), "dETH is not ready for withdrawal");
```
However, if the website gets hacked and when users interact with `withdrawDETH`, they might be supplying fake `_savETHVaults` and `_lpTokens` as specified by the attacker. 
These fake contracts can return anything needed to pass the above checks easily. 
After theses checks, users' legitimate `lpTokenETH` will be burned:
```
lpTokenETH.burn(msg.sender, amount); 
```
but only fake `lpTokens` will be burned:
`getDETH().transfer(msg.sender, dETHReceivedFromAllSavETHVaults); `

## Tools Used
manual

## Recommended Mitigation Steps
Always passing liquid staking manager address, checking its real and then requesting either the savETH vault or staking funds vault is a good idea to prove the validity of vaults.
`rotateNodeRunnerOfSmartWallet()` should be more solidified
Audit: 2022-11-stakehouse-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2022-11-stakehouse/blob/main/contracts/liquid-staking/LiquidStakingManager.sol#L356-L377


# Vulnerability details

## Impact
`rotateNodeRunnerOfSmartWallet()` is used to change the current node runner. However, if the `_new` address is a non-zero non-existent account due to typo or other reasons, the nodeRunner will be tranfserred to an inaccessible non-existent account. Although it can be rotated to a right one by the `dao`, it will create unfriendly customer experience.

## Proof of Concept
`rotateNodeRunnerOfSmartWallet()` is called with a non-zero non-existent `_new` account, the call will succeed as `_new` will pass all tests. However, the nodeRunner is transferred to a non-accessible account.

## Tools Used
Manual audit.

## Recommended Mitigation Steps
Take a two-step role change pattern. Step 1: `nodeRunner` or `dao` register the potential nodeRunner in the contract; Step2: implement a function, which can be called only by the potential nodeRunner, for the potential nodeRunner to replace the current nodeRunner with itself.
Error when calculating total accumulated ETH in the `previewAccumulatedETH` function 
Audit: 2022-11-stakehouse-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2022-11-stakehouse/blob/main/contracts/liquid-staking/GiantMevAndFeesPool.sol#L82-L98


# Vulnerability details

## Impact

In the `previewAccumulatedETH` function from the `GiantMevAndFeesPool` contract, the function is supposed to calculate the total ETH accumulated by an address for a given staking vaults array, but when calculating the value of `accumulated` only ETH accumulated in the last vault of the array will be accounted for as the `accumulated` value is replaced in each iteration instead of being summed with previous value, this will result in a wrong value for the calculated 'total accumulated ETH" as it contains only the ETH accumulated from the last vault instead of the total ETH accumulated in all the vaults.

The impact of this is a potential loss of funds for the user or the protocol because the `accumulated` value is later used when calling `_previewAccumulatedETH` so the function will return the wrong ETH accumulated by the user.

## Proof of Concept

The issue occurs in the `previewAccumulatedETH` function below :

File: contracts/liquid-staking/GiantMevAndFeesPool.sol [Line 82-98](https://github.com/code-423n4/2022-11-stakehouse/blob/main/contracts/liquid-staking/GiantMevAndFeesPool.sol#L82-L98)
```
function previewAccumulatedETH(
        address _user,
        address[] calldata _stakingFundsVaults,
        LPToken[][] calldata _lpTokens
    ) external view returns (uint256) {
        require(_stakingFundsVaults.length == _lpTokens.length, "Inconsistent array lengths");

        uint256 accumulated;
        for (uint256 i; i < _stakingFundsVaults.length; ++i) {
            /* 
                @audit it should be : accumulated += 
                       instead of : accumulated =
            */
            accumulated = StakingFundsVault(payable(_stakingFundsVaults[i])).batchPreviewAccumulatedETH(
                address(this),
                _lpTokens[i]
            );
        }

        return _previewAccumulatedETH(_user, address(lpTokenETH), lpTokenETH.balanceOf(_user), lpTokenETH.totalSupply(), accumulated);
    }
```

As you can see in the code above, the for loop is used to go through all the staking vaults in the `_stakingFundsVaults` array, and it is supposed to add at each iteration the value accumulated by the vault to the `accumulated` variable but the `accumulated` variable is replaced at each iteration with the new value which in the end will give `accumulated` variable the value of the ETH accumulated in the last staking vault only instead of being equal to the sum of ETH accumulated in all vaults.

And because the `accumulated` value is later used when calling `_previewAccumulatedETH`, the function will return the wrong ETH accumulated by the user which may lead to the loss of user (or protocol) funds.

## Tools Used

Manual review

## Recommended Mitigation Steps

To fix this issue the `previewAccumulatedETH` function should be modified as follow :

```
function
The `killGauge` function should set the `weightsPerEpoch` value to zero
Audit: 2024-09-fenix-finance-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-09-fenix-finance/blob/main/contracts/core/VotingEscrowUpgradeableV2.sol#L227-L241


# Vulnerability details

## Impact

Rewards cannot be distributed to some gauges.

## Proof of Concept

In the `VoterUpgradeableV2.killGauge` function, `totalWeightsPerEpoch` is decreased, but `weightsPerEpoch` is not initialized.

```solidity
239:         totalWeightsPerEpoch[epochTimestamp] -= weightsPerEpoch[epochTimestamp][state.pool];
```

In the `notifyRewardAmount` function, killed gauges are not considered because the `index` is calculated using the decreased `totalWeightsPerEpoch`.

```solidity
File: contracts\core\VoterUpgradeableV2.sol
384:         uint256 weightAt = totalWeightsPerEpoch[_epochTimestamp() - _WEEK];
385:         if (weightAt > 0) {
386:             index += (amount_ * 1e18) / weightAt;
387:         }
```

However, the `_distribute` function refunds rewards to the minter for the killed gauges using this `index`.

```solidity
File: contracts\core\VoterUpgradeableV2.sol
632:         uint256 delta = index - state.index;
633:         if (delta > 0) {
634:             uint256 amount = (totalVotesWeight * delta) / 1e18;
635:             if (state.isAlive) {
636:                 gaugesState[gauge_].claimable += amount;
637:             } else {
638:@>               IERC20Upgradeable(token).safeTransfer(minter, amount);
639:             }
640:         }
```

In other words, the `_distribute` function attempts to distribute rewards that exceed what was actually distributed from the minter. As a result, if the killed gauges received rewards previously, the non-killed gauges cannot receive their rewards.

Let's consider the following scenario:

1. There are two pools (`poolA` and `poolB`), with corresponding gauges `gaugeA` and `gaugeB`.
2. Users vote 100 for each pool individually in the first week, resulting in individual `weightsPerEpoch` of 100 and a `totalWeightsPerEpoch` of 200.
3. The governors kill `gaugeA` in the first week, reducing `totalWeightsPerEpoch` to 100.

4. An attacker calls the `distribute` function for the killed gauge (`gaugeA`) in the second week:
    - The minter distributes 100 FNX rewards: `index = 100 * 1e18 / 100 = 1e18`.
    - 100 FNX is transferred to the minter:

        ```solidity
        amount = (totalVotesWeight * delta) / 1e18 = 100 * 1e18 / 1e18 = 100
        ```

5. When users attempt to distribute the rewards to `gaugeB`, the operation is reverted. Since the `VoterUpgradeableV2` contract receives 100 FNX and refunds 100 FNX to the minter, `gaugeB` cannot receive the 100 FNX rewards.

There is another vulnerability: if the governor continuously kills, revives, and kills a gauge again during one epoch, `weightsPerEpoch` is subtracted twice from `totalWeightsPerEpoch`.

Let me know if you need any further adjustments!

## Tools Used

Manual Review

## Recommended Mitigation Steps

It is recommended to change the code as following:

```diff
    function killGaug
The `VotingEscrowUpgradeableV2.onDetachFromManagedNFT` function can lead to incorrect calculations of voting power
Audit: 2024-09-fenix-finance-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-09-fenix-finance/blob/main/contracts/core/VotingEscrowUpgradeableV2.sol#L295-L314


# Vulnerability details

## Impact

The users' voting power is calculated incorrectly.

## Proof of Concept

In the `VotingEscrowUpgradeableV2.onDetachFromManagedNFT` function, `newBalance_` is subtracted from both `permanentTotalSupply` and `newManagedLocked.amount`.

```solidity
File: contracts\core\VotingEscrowUpgradeableV2.sol
307:         permanentTotalSupply -= (newBalance_ < permanentTotalSupply ? newBalance_ : permanentTotalSupply);
312:         newManagedLocked.amount -= amount < newManagedLocked.amount ? amount : newManagedLocked.amount;
```

`newBalance_` is greater than the actual deposited amount because it includes `lockedRewards` received from `managedNFTManager` at L218.

```solidity
File: contracts\nest\ManagedNFTManagerUpgradeable.sol
215:         IVotingEscrowV1_2(votingEscrow).onDettachFromManagedNFT(
216:             tokenId_,
217:             tokenInfo.attachedManagedTokenId,
218:@>           tokenInfo.amount + lockedRewards
219:         );
```

As a result, this causes incorrect balance of NFT.

Let's consider the following scenario:

1. Alice locks 100 FNX permanently: `permanentTotalSupply` = 100.
2. Bob creates a lock with 100 FNX and attaches it to the `managedToken`: `permanentTotalSupply` = 200.
3. After some time, Alice detaches from the `managedToken` and receives 10 FNX as `lockedRewards` from `managedNFTManager`: `permanentTotalSupply` = 200 - 100 - 10 = 90.
4. When Bob tries to vote, his NFT balance is reported as 90, even though he locked 100 FNX permanently.

## Tools Used

Manual Review

## Recommended Mitigation Steps

It is recommended to change the code as following:

```diff
    function onDettachFromManagedNFT(
        uint256 tokenId_,
        uint256 managedTokenId_,
        uint256 newBalance_
    ) external override nonReentrant onlyManagedNFTManager {
        [...]
        int128 amount = LibVotingEscrowUtils.toInt128(newBalance_);
+       uint256 oldBalance = newBalance_ - IManagedNFTManager(managedNFTManager).tokensInfo[tokenId_];
-       permanentTotalSupply -= (newBalance_ < permanentTotalSupply ? newBalance_ : permanentTotalSupply);
+       permanentTotalSupply -= oldBalance;
        nftStates[tokenId_].isAttached = false;
        _updateNftLocked(tokenId_, LockedBalance(amount, LibVotingEscrowUtils.maxUnlockTimestamp(), false));
        LockedBalance memory newManagedLocked = nftStates[managedTokenId_].locked;
-       newManagedLocked.amount -= amount < newManagedLocked.amount ? amount : newManagedLocked.amount;
+       newManagedLocked.amount -= oldBalance;
        _updateNftLocked(managedTokenId_, newManagedLocked);
    }
```


## Assessed type

Other
In the `VotingEscrowUpgradeableV2.onDetachFromManagedNFT` function, `supply` should be increased by `lockedRewards`
Audit: 2024-09-fenix-finance-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-09-fenix-finance/blob/main/contracts/core/VotingEscrowUpgradeableV2.sol#L295-L314


# Vulnerability details

## Impact

Withdrawing can be DoSed.

## Proof of Concept

In the `VotingEscrowUpgradeableV2.onDetachFromManagedNFT` function, the locked amount is updated as `newBalance_` at L309.

```solidity
File: contracts\core\VotingEscrowUpgradeableV2.sol
305:         int128 amount = LibVotingEscrowUtils.toInt128(newBalance_);
309:         _updateNftLocked(tokenId_, LockedBalance(amount, LibVotingEscrowUtils.maxUnlockTimestamp(), false));
```

`newBalance_` is greater than the actual deposited amount because it includes `lockedRewards` received from `managedNFTManager` at L218.

```solidity
File: contracts\nest\ManagedNFTManagerUpgradeable.sol
210:         uint256 lockedRewards = IManagedNFTStrategy(IVotingEscrow(votingEscrow).ownerOf(tokenInfo.attachedManagedTokenId)).onDettach(
211:             tokenId_,
212:             tokenInfo.amount
213:         );
214: 
215:         IVotingEscrowV1_2(votingEscrow).onDettachFromManagedNFT(
216:             tokenId_,
217:             tokenInfo.attachedManagedTokenId,
218:@>           tokenInfo.amount + lockedRewards
219:         );
```

When users withdraw their tokens, the locked amount including `lockedRewards` is subtracted from `supply` at L510, but `supply` only cumulated actual deposited amount, not including `lockedRewards`.
When users withdraw their tokens, the locked amount, including `lockedRewards`, is subtracted from `supply` at L510. However, `supply` only accumulates the actual deposited amount and does not account for `lockedRewards`.

```solidity
File: contracts\core\VotingEscrowUpgradeableV2.sol
508:         uint256 amount = LibVotingEscrowUtils.toUint256(state_.locked.amount);
509:         uint256 supplyBefore = supply;
510:@>       supply -= amount;
```

As a result, this can cause withdrawing DoSed.

Let's consider the following scenario:

1. Alice and Bob each create locks with 100 FNX individually, and Bob attaches his lock to the managed token: `supply = 200`.
2. Bob withdraws his tokens: `supply = 200 - 100 = 100`.
3. After some time, Alice detaches from the `managedToken` and receives 1 FNX as `lockedRewards` from `managedNFTManager`. Her locked amount is then updated to 101 FNX, and the locking end time is updated to `maxUnlockTimestamp`.
4. After `maxUnlockTimestamp`, Alice tries to withdraw her tokens, but the operation is reverted due to an underflow: `supply = 100 < 101`.

## Tools Used

Manual Review

## Recommended Mitigation Steps

It is recommended to change the code as following:

```diff
    function onDettachFromManagedNFT(
        uint256 tokenId_,
        uint256 managedTokenId_,
        uint256 newBalance_
    ) external override nonReentrant onlyManagedNFTManager {
        [...]
        int128 amount = LibVotingEscrowUtils.toInt128(newBalance_);
+       supply = supply + newBalance_ - tokensInfo[tokenId_];
        perm
When users lock their tokens permanently, they cannot receive the boosted amount of FNX for the locked amount
Audit: 2024-09-fenix-finance-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-09-fenix-finance/blob/main/contracts/core/VotingEscrowUpgradeableV2.sol#L205-L211


# Vulnerability details

## Impact

Users cannot receive the boosted amount of FNX fairly, which results in a loss of funds.

## Proof of Concept

The boosted FNX serves as a reward to encourage users to lock their tokens for a longer duration and in larger amounts.
Therefore, permanent lockers should also receive the boosted FNX.
However, when users lock their tokens permanently, they cannot receive the boosted amount of FNX for the locked tokens.

Let's consider the following scenario:

1. Alice creates a lock with 101 FNX for a 1-week duration and decides to lock it permanently after some time.
2. Alice locks her tokens permanently but does not receive the `boostedValue` amount of FNX.
3. Bob creates a lock with 1 FNX for a 1-week duration and also locks his tokens permanently.
4. Bob deposits 100 FNX for his token and receives the `boostedValue` amount of FNX. If `_boostFNXPercentage` is 1000 (10%), then `boostedValue` is 10.

Even though Alice and Bob both lock the same 101 FNX permanently, only Bob receives the 10 FNX as a boosted amount.
If `_boostFNXPercentage` is 1000 (10%), Alice effectively loses 10% of her funds.

## Tools Used

Manual Review

## Recommended Mitigation Steps

Implement a mechanism to allocate the boosted FNX to users who lock their tokens permanently.


## Assessed type

Other
`killGauge()` will lead to wrong calculation of emission   
Audit: 2024-09-fenix-finance-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-09-fenix-finance/blob/main/contracts/core/VoterUpgradeableV2.sol#L239
https://github.com/code-423n4/2024-09-fenix-finance/blob/main/contracts/core/VoterUpgradeableV2.sol#L630-L639


# Vulnerability details

## Description

The `VoterUpgradeableV2.sol` contract has `killGauge()` that disables the gauge to prevent it from further rewards distribution, only the address with **GOVERNANCE_ROLE** role can call it.
the `killGauge()` only updates three state variables 
```solidity
File: VoterUpgradeableV2.sol
227:     function killGauge(address gauge_) external onlyRole(_GOVERNANCE_ROLE) {
...
232:         delete gaugesState[gauge_].isAlive;
...
236:             delete gaugesState[gauge_].claimable;
...
240:         totalWeightsPerEpoch[epochTimestamp] -= weightsPerEpoch[epochTimestamp][state.pool];
```
The `distribute()` function will distribute rewards to pools managed by the `VoterUpgradeableV2.sol` contract and it will call the Minter contract by triggering `update_period()` function before distributing rewards.

The timeline looks like this 
```
    Epoch_x                      Epoch_x+1  
    |-----------x-------------------|-x---------------------  
       call `killGauge()`             call `distribute()` 
```
When `distribute()` gets invoked in the timeline it will distribute the rewards of Epoch_x, The killed gauge has no weight in this epoch because its weight gets subtracted from `totalWeightsPerEpoch[]` in `killGauge()`.

When the Minter invokes `VoterUpgradeableV2.sol#notifyRewardAmount()` to notify the contract of the reward amount to be distributed for Epoch_x, we can also find in the same function how the `index` value gets increased 
```solidity
File: VoterUpgradeableV2.sol
382:     function notifyRewardAmount(uint256 amount_) external {
...
387:         uint256 weightAt = totalWeightsPerEpoch[_epochTimestamp() - _WEEK]; 
388:         if (weightAt > 0) {
389:             index += (amount_ * 1e18) / weightAt;
390:         }
```
the `index` is updated as the reward amount divided by the total weights of Epoch_x 
we know the weight of the disabled gauge is not included in `totalWeightsPerEpoch[Epoch_x]`

So, back to [_distribute()](https://github.com/code-423n4/2024-09-fenix-finance/blob/main/contracts/core/VoterUpgradeableV2.sol#L626-L651) 
```solidity
File: VoterUpgradeableV2.sol
671:     function _distribute(address gauge_) internal {
...
677:             uint256 totalVotesWeight = weightsPerEpoch[currentTimestamp - _WEEK][state.pool];
678: 
679:             if (totalVotesWeight > 0) {
...
684:                     if (state.isAlive) {
685:                         gaugesState[gauge_].claimable += amount;
686:                     } else {
687:                         IERC20Upgradeable(token).safeTransfer(minter, amount);
}

```
Because `killGauge()` doesn't delete the values of ` weightsPerEpoch[]`, it will send back `amount` of emissions back to Minter, which actually should get distrib