xanoutas/solidity-security-findings huggingface dataset:
`_processLockChange` fails to account for `boostedValue` when incrementing `permanentTotalSupply`
Audit: 2024-09-fenix-finance-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
This could lead to an inaccurate balance between the actual locked amount and the **permanentTotalSupply**. In the worst case, users might not be able to unlock their permanent locks due to over-deduction by previous users with boosted balances.  
## Proof-of-Concept
The `_proccessLockChange` function is responsible for updating the locked balance of an NFT. When processing a lock change, the function checks if the lock is eligible for a boost reward from the VeBoost contract.  

If the lock is eligible, the function calculates the boost amount and adds it to the total locked amount.  

However, when updating `permanentTotalSupply` (for permanent lock case), the function only adds the original deposit amount, excluding the boosted amount. This can lead to an inaccurate permanent total supply calculation, as shown in the following code snippet:

```solidity
function _proccessLockChange(
    uint256 tokenId_,
    uint256 amount_,
    uint256 unlockTimestamp_,
    LockedBalance memory oldLocked_,
    DepositType depositType_,
    bool shouldBoosted_
) internal {

    ...
    ... sniped...
    ...

    uint256 boostedValue;
    IVeBoost veBoostCached = IVeBoost(veBoost);
    if (address(veBoostCached) != address(0) && shouldBoosted_) {
        if (depositType_ == DepositType.CREATE_LOCK_TYPE || depositType_ == DepositType.DEPOSIT_FOR_TYPE) {
            if (
                (LibVotingEscrowUtils.roundToWeek(block.timestamp + veBoostCached.getMinLockedTimeForBoost()) <= newLocked.end ||
                    newLocked.isPermanentLocked) && amount_ >= veBoostCached.getMinFNXAmountForBoost()
            ) {
                uint256 calculatedBoostValue = veBoostCached.calculateBoostFNXAmount(amount_);
                uint256 availableFNXBoostAmount = veBoostCached.getAvailableBoostFNXAmount();
                boostedValue = calculatedBoostValue < availableFNXBoostAmount ? calculatedBoostValue : availableFNXBoostAmount;
            }
        }
    }
    newLocked.amount += LibVotingEscrowUtils.toInt128(boostedValue); // <-- @audit boostedValue is added to total locked amount
    uint256 diff = LibVotingEscrowUtils.toUint256(newLocked.amount - oldLocked_.amount);
    uint256 supplyBefore = supply;
    supply += diff;
    if (newLocked.isPermanentLocked) {
        permanentTotalSupply += amount_; // <-- @audit this does not include boostedValue that is also locked
    }

    _updateNftLocked(tokenId_, newLocked);

    ...
    ... snipped ...
    ...
```

In the code above, `permanentTotalSupply` is only incremented by the original `amount_` value, while `boostedValue` is added to `newLocked.amount`. This means that when a user withdraws their locked tokens, the contract will deduct more than was originally added to `permanentTotalSupply`, leading to an underestimation of the permanent total suppl
`dettachFromManagedNFT` might revert and temporarily prevent users from detaching in certain situation
Audit: 2024-09-fenix-finance-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
Users' veNFT might be temporarily undetachable, preventing users from performing action on their own veNFT.  
## Proof-of-Concept
When users invoke `dettachFromManagedNFT` to get their veNFT back from **ManagedNFT**, `_poke` is called at the end of the function to update voting power across gauges voted by this **ManagedNFT**.  

```solidity
function dettachFromManagedNFT(uint256 tokenId_) external nonReentrant onlyNftApprovedOrOwner(tokenId_) {
    _checkVoteDelay(tokenId_);
    _checkVoteWindow();
    IManagedNFTManager managedNFTManagerCache = IManagedNFTManager(managedNFTManager);
    uint256 managedTokenId = managedNFTManagerCache.getAttachedManagedTokenId(tokenId_);
    managedNFTManagerCache.onDettachFromManagedNFT(tokenId_);
    uint256 weight = IVotingEscrowV2(votingEscrow).balanceOfNftIgnoreOwnershipChange(managedTokenId);
    if (weight == 0) {
        _reset(managedTokenId);
        delete lastVotedTimestamps[managedTokenId];
    } else {
        _poke(managedTokenId);
    }
    emit DettachFromManagedNFT(tokenId_);
}

function _poke(uint256 tokenId_) internal {
    address[] memory _poolVote = poolVote[tokenId_];
    uint256[] memory _weights = new uint256[](_poolVote.length);

    for (uint256 i; i < _poolVote.length; ) {
        _weights[i] = votes[tokenId_][_poolVote[i]];
        unchecked {
            i++;
        }
    }
    _vote(tokenId_, _poolVote, _weights);
    _updateLastVotedTimestamp(tokenId_);
}

function _vote(uint256 tokenId_, address[] memory pools_, uint256[] memory weights_) internal {
    _reset(tokenId_);
    uint256 nftVotePower = IVotingEscrowV2(votingEscrow).balanceOfNFT(tokenId_);
    uint256 totalVotesWeight;
    uint256 totalVoterPower;
    for (uint256 i; i < pools_.length; i++) {
        GaugeState memory state = gaugesState[poolToGauge[pools_[i]]];
        if (!state.isAlive) {
            revert GaugeAlreadyKilled();
        }
        totalVotesWeight += weights_[i];
    }
    ...
    ... snipped ...
    ...
}
```
`_poke` loads a list of pools and weights voted by **ManagedNFT** then recast votes again to the same set of pools and weights via calling into `_vote`.  

However, `_vote` reverts when one of the pool/gauge has already been killed.  

Now consider this situation:  
1. Bob attaches his veNFT with **ManagedNFT**.    
2. **ManagedNFT** votes for [gaugeA, gaugeB].  
3. gaugeB is killed.  
4. Bob decides to detach his veNFT from **ManagedNFT**.  
5. Bob's transaction reverts because `_poke` will attempt to recast the vote on gaugeB.  
6. Bob can't detach his veNFT until **ManagedNFT** notices and recast the vote excluding gaugeB.  

As a result, users' veNFT might be temporarily undetachable when the described scenario happens.  

## Recommended Mitigations  
Users are expected to only include active pools in normal `vote` flow.  
If one of
Potential incorrect index update in revived gauge under specific conditions
Audit: 2024-09-fenix-finance-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
This vulnerability could allow revived gauges to claim more rewards than intended under specific circumstances, potentially leading to unfair distribution of rewards.

## Description
The `reviveGauge` function fails to update the gauge's index to the current global index when reviving a previously killed gauge. While this issue is mitigated in most scenarios by the `distributeAll` function, which updates all gauges' indices to the global index on each epoch, a vulnerability still exists under specific conditions.

Relevant code snippet:
```solidity
function reviveGauge(address gauge_) external onlyRole(_GOVERNANCE_ROLE) {
    if (gaugesState[gauge_].isAlive) {
        revert GaugeNotKilled();
    }
    gaugesState[gauge_].isAlive = true;
    emit GaugeRevived(gauge_);
}
function _distribute(address gauge_) internal {
    GaugeState memory state = gaugesState[gauge_];
    uint256 currentTimestamp = _epochTimestamp();
    if (state.lastDistributionTimestamp < currentTimestamp) {
        uint256 totalVotesWeight = weightsPerEpoch[currentTimestamp - _WEEK][state.pool];
        if (totalVotesWeight > 0) {
            uint256 delta = index - state.index; // @contest-info outdated index can cause problem here
            if (delta > 0) {
                uint256 amount = (totalVotesWeight * delta) / 1e18;
                if (state.isAlive) {
                    gaugesState[gauge_].claimable += amount;
                } else {
                    IERC20Upgradeable(token).safeTransfer(minter, amount);
                }
            }
        }
        gaugesState[gauge_].index = index;
        uint256 claimable = gaugesState[gauge_].claimable;
        if (claimable > 0 && state.isAlive) {
            gaugesState[gauge_].claimable = 0;
            gaugesState[gauge_].lastDistributionTimestamp = currentTimestamp;
            IGauge(gauge_).notifyRewardAmount(token, claimable);
            emit DistributeReward(_msgSender(), gauge_, claimable);
        }
    }
}
```

The vulnerability arises in scenarios where:

1. There's a large number of gauges in the protocol.  
2. Due to gas limitations, `distributeAll` cannot update all gauges in a single transaction.  
3. Manual iteration through gauges is required.  
4. A killed gauge might not be updated before it's revived as there is no incentive to call `distribute` function for a killed gauge.  

In this specific scenario, a revived gauge could retain an outdated index, leading to incorrect reward calculations.  

### Example scenario
Epoch x:
- Gauge A is active with an index of 100
- Global index is 100

Epoch x+1:
- Gauge A is killed, its index stays at 100
- Global index updates to 150
- `distributeAll` fails to update all gauges due to gas limitations  

Epoch x+2:
- Before manual updates reach Gauge A, it is revived with index still at 100
- Globa
When removing nominees, the slope of pointsSum is not updated.
Audit: 2024-05-olas-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-05-olas/blob/main/governance/contracts/VoteWeighting.sol#L586-L636


# Vulnerability details

## Impact
Since the slope is not updated and the bias is reduced, the bias will be lower than the correct value when the points are updated later.

## Detail
In the function `removeNominee`, the contract will zeros nominees' weights and change the pointsSum at the same time.
```solidity
// Set nominee weight to zero
uint256 oldWeight = _getWeight(account, chainId);
uint256 oldSum = _getSum();
uint256 nextTime = (block.timestamp + WEEK) / WEEK * WEEK;
pointsWeight[nomineeHash][nextTime].bias = 0; 
timeWeight[nomineeHash] = nextTime; 

// Account for the the sum weight change
uint256 newSum = oldSum - oldWeight;
pointsSum[nextTime].bias = newSum; // @@audit: high, don't update slope
timeSum = nextTime;
```
As we can see, the bias of pointsSum is reduced by oldWeight(i.e., the nominee's remaining weight). However, the slope is not updated, which means when pointsSum  is updated next time, the bias will be reduced by the excess value.
## Tools Used
VScode

## Recommended Mitigation Steps
```solidity
pointsSum[nextTime].slope -= pointsWeight[nomineeHash][nextTime].slope;
```


## Assessed type

Math
When revoking user voting power, the update of changesSum is not synchronized with pointsSum.
Audit: 2024-05-olas-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-05-olas/blob/main/governance/contracts/VoteWeighting.sol#L657-L661


# Vulnerability details

## Impact
pointsSum will be incorrect.

## Detail
In the function `revokeRemovedNomineeVotingPower`, protocol will update the changesWeight and changesSum. However, this operation should be synchronized with the update of pointsSum. If the changesSum is reduced, the slope of pointsSum also should be reduced, or else the slope of pointsSum will be larger than expected after oldSlope.end.
On the other hand, the function `revokeRemovedNomineeVotingPower` doesn't update the slope of pointsSum in any case, but changesSum may be changed. Then, two situations will occur. The first situation is that changesSum is updated (decreased), so the slope of pointsSum will be reduced by a smaller value. The other situation is that changesSum is not updated, then the slope of pointsSum will be reduced by a larger value. The final result is that after `oldSlope.end`, pointsSum will have two possible values, which is obviously incorrect.

## Tools Used
VScode

## Recommended Mitigation Steps
When updating changesSum, the slope of pointsSum needs to be updated at the same time.


## Assessed type

Math
Not decreasing the global slope when removing a nominee causes double subtraction of the bias in upcoming weeks.
Audit: 2024-05-olas-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-05-olas/blob/3ce502ec8b475885b90668e617f3983cea3ae29f/governance/contracts/VoteWeighting.sol#L610


# Vulnerability details

## Impact
When a nominee is removed its left over bias/weight is subtracted from the global weights but the amount of slope contributed by the removed nominee is not removed from the global value of total slope due to which in upcoming weeks the total left over weight of the removed nominee will be once again reduced from the global/total weights thus causing double subtraction of weights of removed nominee. This causes wrong calculation of weights and relative weights of nominee and ultimately affecting staking incentives of each nominee.

## Proof of Concept
Following is vote for nominee weights function
```solidity
 function voteForNomineeWeights(bytes32 account, uint256 chainId, uint256 weight) public {
        // Get the nominee hash
        bytes32 nomineeHash = keccak256(abi.encode(Nominee(account, chainId)));

        // Check for the previously removed nominee
        if (mapRemovedNominees[nomineeHash] > 0) {
            revert NomineeRemoved(account, chainId);
        }

        // Get user veOLAS slope and check its value
        int128 userSlope = IVEOLAS(ve).getLastUserPoint(msg.sender).slope;
        if (userSlope < 0) {
            revert NegativeSlope(msg.sender, userSlope);
        }

        uint256 lockEnd = IVEOLAS(ve).lockedEnd(msg.sender);
        uint256 nextTime = (block.timestamp + WEEK) / WEEK * WEEK;

        // Check for the lock end expiration
        if (nextTime >= lockEnd) {
            revert LockExpired(msg.sender, lockEnd, nextTime);
        }

        // Check for the weight number
        if (weight > MAX_WEIGHT) {
            revert Overflow(weight, MAX_WEIGHT);
        }

        // Check for the last voting time
        uint256 nextAllowedVotingTime = lastUserVote[msg.sender][nomineeHash] + WEIGHT_VOTE_DELAY;
        if (nextAllowedVotingTime > block.timestamp) {
            revert VoteTooOften(msg.sender, block.timestamp, nextAllowedVotingTime);
        }

        // Prepare old and new slopes and biases
        VotedSlope memory oldSlope = voteUserSlopes[msg.sender][nomineeHash];
        uint256 oldBias;
        if (oldSlope.end > nextTime) {
            oldBias = oldSlope.slope * (oldSlope.end - nextTime);
        }

        VotedSlope memory newSlope = VotedSlope({
            slope: uint256(uint128(userSlope)) * weight / MAX_WEIGHT,
            end: lockEnd,
            power: weight
        });

        uint256 newBias = newSlope.slope * (lockEnd - nextTime);

        uint256 powerUsed = voteUserPower[msg.sender];
        powerUsed = powerUsed + newSlope.power - oldSlope.power;
        voteUserPower[msg.sender] = powerUsed;
        if (powerUsed > MAX_WEIGHT) {
            revert Overflow(powerUsed, MAX_WEIGHT);
        }

        // Remove old and schedule new slope changes
        // Remove slope changes for old slopes
Removing a nominee can brick the VoteWeighting.sol
Audit: 2024-05-olas-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-05-olas/blob/3ce502ec8b475885b90668e617f3983cea3ae29f/governance/contracts/VoteWeighting.sol#L603-L612
https://github.com/code-423n4/2024-05-olas/blob/3ce502ec8b475885b90668e617f3983cea3ae29f/governance/contracts/VoteWeighting.sol#L232-L240


# Vulnerability details

## Impact

Slopes are not handled correctly when a nominee is removed. This can cause incorrect accounting for sum weights and lead to bricking the protocol in certain scenarios.

## Proof of Concept

Points are accounted using bias, slope and end time. According to the formula, the bias is gradually decreased by its slope eventually reaching 0. In case the bias is suddenly reduced without handling/removing its slope, there will reach a time before the slope end time where the bias will drop below 0. `pointsSum` uses points to calculate the sum of individual nominee weights of at any time. The above case of bias dropping below 0 is handled as follows in `_getSum`:

```solidity
            uint256 dBias = pt.slope * WEEK;
            if (pt.bias > dBias) {
                pt.bias -= dBias;
                uint256 dSlope = changesSum[t];
                pt.slope -= dSlope;
            } else {
                pt.bias = 0;
                pt.slope = 0;
            }
```
Whenever bias drops below 0 the slope is also made 0 without removing the slope end accounting in `changesSum`. This is what usually occurs when nominee is removed.

```solidity
        // Set nominee weight to zero
        uint256 oldWeight = _getWeight(account, chainId);
        uint256 oldSum = _getSum();
        uint256 nextTime = (block.timestamp + WEEK) / WEEK * WEEK;
        pointsWeight[nomineeHash][nextTime].bias = 0;
        timeWeight[nomineeHash] = nextTime;

        // Account for the the sum weight change
        uint256 newSum = oldSum - oldWeight;
        pointsSum[nextTime].bias = newSum;
        timeSum = nextTime;
```
Afterwards, if a scenario occurs where the pt.bias regains a value greater than the previous `dBias` which is `pt.slope * WEEK`, the `pt.bias > dBias` part of the if condition executes which can cause it to revert if at any point `changesSum[t]` is greater than the current `pt.slope` due to underflow.

Once bias goes to 0, it can regain a value greater than the pt.slope * WEEK if another nominee is added or a a user votes for a nominee.

All this will lead to incorrect wight values and potential bricking of the protcool.

## Tools Used
Manual code review

## Recommended Mitigation Steps

Refactor the function to be like this.
```solidity
            uint256 dBias = pt.slope * WEEK;
            if (pt.bias > dBias) {
                pt.bias -= dBias;
                uint256 dSlope = changesSum[t];
-           pt.slope -= d_slope;
+           if(pt.slope >= d.slope) {
+               pt.slope -= d_slope;
+           } else {
+               pt.slope = 0;
+           }
            } else {
                pt.bias = 0;
                pt.slo
Forcing the protocol to distribute staking incentives to staking instances two times
Audit: 2024-05-olas-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-05-olas/blob/main/tokenomics/contracts/staking/DefaultTargetDispenserL2.sol#L311


# Vulnerability details

## Impact
An attacker can force the protocol to distribute staking incentives to staking instances two times by engineering the staking instances and misusing the replayability of bridging protocols.

## Proof of Concept

The attack scenario is as follows:

 - The attacker creates two staking instances (instanceA and instanceB) by calling the function `createStakingInstance`. Note that both are valid and honest contracts, so the malicious intention of the attacker is not recognizable. Morevoer, the attacker creates these two instances with two different addresses, so that the `deployer` of these instances will be different from each other.
 ```solidity
     function createStakingInstance(
        address implementation,
        bytes memory initPayload
    ) external returns (address payable instance) {
        //....

        bytes memory deploymentData = abi.encodePacked(type(StakingProxy).creationCode,
            uint256(uint160(implementation)));

        // solhint-disable-next-line no-inline-assembly
        assembly {
            instance := create2(0x0, add(0x20, deploymentData), mload(deploymentData), salt)
        }

        //....
    }
 ```
 https://github.com/code-423n4/2024-05-olas/blob/main/registries/contracts/staking/StakingFactory.sol#L168

  - The implementation of instanceA is similar exactly to `StakingToken` contract. But the instanceB is a customized version of `StakingToken`. The only difference of instanceB from the instanceA is that the attacker is able to pause the depositing mechanism in the instanceB. So, the following modification is implemented in instanceB:
  ```solidity

    address public immutable owner = address(1); // The address of the attacker

    function pause(bool _enabled) public{
        require(msg.sender == owner, "only owner is allowed to pause!");
        paused = _enabled;
    }

    function deposit(uint256 amount) external {
        require(!paused, "it is paused, so depositing is not allowed.");

        //...
    }
  ```
  https://github.com/code-423n4/2024-05-olas/blob/main/registries/contracts/staking/StakingToken.sol#L115

  - Both instances work properly and honestly. Until the time that staking incentives are claimed on L1, and they are supposed to be distributed to these contracts on L2. Suppose 100e18 incentives are going to be distributed on the target chain to those instances (90e18 to instanceA and 10e18 to instanceB). This can be triggered by calling the function `claimStakingIncentivesBatch` on L1.
  https://github.com/code-423n4/2024-05-olas/blob/main/tokenomics/contracts/Dispenser.sol#L1058

  - Before the message is received on L2, the attacker calls the function `pause(true)` on instanceB.
  - When the `receiveMessage` on L2 is called, the bridged data will be processed.
  ```solidity
    function receiveMessage(bytes m
inflation recalculation return incorrect inflationPerEpoch
Audit: 2024-05-olas-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-05-olas/blob/3ce502ec8b475885b90668e617f3983cea3ae29f/tokenomics/contracts/Tokenomics.sol#L1122-L1143


# Vulnerability details

# **Vulnerability Detail**

During the checkpoint, if the year changes during the epoch, function does handle the `inflationPerEpoch` incorrectly. 

# **Proof of Concept**

The critical flow begins with checking the current year

```solidity
uint256 numYears = (block.timestamp - timeLaunch) / ONE_YEAR;
```

It worth to mention that numYears is updated only when it equals to whole number. If `timeLaunch` was 3 years ago, we need full year to be based, so `numYears` updates to 4. Because 3.1-3.9 year would be rounded. It is important edge, because below, during the `yearEndTime` calculation, it will return the current `timestamp` 

It calculates the yearEndTime by taking the timeLaunch + numYears(current one we switched to) * ONE_YEAR, and it returns the current `block.timestamp`

```solidity
uint256 yearEndTime = timeLaunch + numYears * ONE_YEAR
```

Then, It returns current inflation that start from `prevEpochTime` till `yearEndTime` (current timestamp) and multiply by curInflation

```solidity
inflationPerEpoch = (yearEndTime - prevEpochTime) * curInflationPerSecond
```

After, the new `curInflationPerSecond` is taken for the new year 

```solidity
curInflationPerSecond = getInflationForYear(numYears) / ONE_YEAR;
```

Eventually, the `inflationPerEpoch` is recalculated via 

```solidity
inflationPerEpoch += (block.timestamp - yearEndTime) * curInflationPerSecond;
```

Here is the problem, because `yearEndTime` is equal to the `block.timestamp`
and first part (deduction) would return 0 and the final result would be 0. It means that inflationPerEpoch would fail to add new time-frame with new `curInflationPerSecond`.

It means that incorrect `inflationPerEpoch` (past one) would be added into this part of the code below

```solidity
//Bonding and top-ups in OLAS are recalculated based on the inflation schedule per epoch
tp.epochPoint.totalTopUpsOLAS = uint96(inflationPerEpoch);
incentives[4] = (inflationPerEpoch * tp.epochPoint.maxBondFraction) / 100;
```

If we proceed below we could note that topUps would be calculated with incorrect `inflationPerEpoch`.

```
incentives[5] = (inflationPerEpoch * tp.unitPoints[0].topUpUnitFraction) / 100; //e: component ownerTopUps calculus
// Owner top-ups: epoch incentives for agent owners funded with the inflation
incentives[6] = (inflationPerEpoch * tp.unitPoints[1].topUpUnitFraction) / 100;
```

# **Impact**

Incorrect `inflationPerEpoch` would used during the recalculation.

# **Recommendation**
Adjust the calculation, ensure that correct inflation is returned from the calculation


## Assessed type

Math