xanoutas/solidity-security-findings huggingface dataset:
Stakers who are evicted cannot stake again because their mapServiceInfo[serviceId] is not deleted
Audit: 2024-05-olas-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-05-olas/blob/e2a8bc31d2769bfb578a06cc64919ad369a82c08/registries/contracts/staking/StakingBase.sol#L430-L476


# Vulnerability details

## Impact

Stakers who are inactive and evicted cannot stake again.

## Proof of Concept

When staking, `mapServiceInfo[serviceId]` is queried and if `sInfo.tsStart > 0`, the service cannot stake.

```
// Check if the evicted service has not yet unstaked
        ServiceInfo storage sInfo = mapServiceInfo[serviceId];
        // tsStart being greater than zero means that the service was not yet unstaked: still staking or evicted
>       if (sInfo.tsStart > 0) {
            revert ServiceNotUnstaked(serviceId);
        }

```

The staker is added to the array of services.

```
        // Add the service Id to the set of staked services
        setServiceIds.push(serviceId);
```

When unstaking, mapServiceInfo[serviceId] is deleted. This means the `ts` is also reset to zero.

```
        // Clear all the data about the unstaked service
        // Delete the service info struct
        delete mapServiceInfo[serviceId];
```

The staker is removed from the set of services.

```
 // Update the set of staked service Ids
        // If the index was not found, the service was evicted and is not part of staked services set
        if (inSet) {
            setServiceIds[idx] = setServiceIds[setServiceIds.length - 1];
            setServiceIds.pop();
        }
```

However, when the service staker is evicted because of inactivity, the `mapServiceInfo[serviceId]` is not deleted. This means the service cannot re-stake again.

```
 // Evict services from the global set of staked services
        for (uint256 i = numEvictServices; i > 0; --i) {
            // Decrease the number of services
            totalNumServices--;
            // Get the evicted service index
            uint256 idx = serviceIndexes[i - 1];
            // Assign last service Id to the index that points to the evicted service Id
            setServiceIds[idx] = setServiceIds[totalNumServices];
            // Pop the last element
            setServiceIds.pop();
        }
```

## Tools Used

Manual Review

## Recommended Mitigation Steps

Ensure that the mapping is also deleted for the inactive staker so that they can restake again.

```
 // Evict services from the global set of staked services
        for (uint256 i = numEvictServices; i > 0; --i) {
            // Decrease the number of services
            totalNumServices--;
            delete mapServiceInfo[serviceId[i - 1]];
            // Get the evicted service index
            uint256 idx = serviceIndexes[i - 1];
            // Assign last service Id to the index that points to the evicted service Id
            setServiceIds[idx] = setServiceIds[totalNumServices];
            // Pop the last element
            setServiceIds.pop();
        }
```


## Assessed type

Context
Leftover rewards is wrongly accounted in StakingBase, leaving the leftover rewards stuck in the contract instead
Audit: 2024-05-olas-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-05-olas/blob/e2a8bc31d2769bfb578a06cc64919ad369a82c08/registries/contracts/staking/StakingBase.sol#L620-L643


# Vulnerability details

## Impact

Reward accounting error. The first eligible service will not get the excess amounts and the excess amount will still get stuck in the contract.

## Proof of Concept

When calculating rewards in `StakingBase.checkpoint()`, the function calls `_calculateStakingRewards()` and gets back an array of `eligibleServiceRewards`.

If the `totalRewards > lastAvailableRewards`, meaning that if the total rewards for eligible stakers is more than the available rewards, then the rewards will be proportioned to the percentage of `lastAvailableRewards / totalRewards`.

For example, if there are 4 stakers with 10 tokens as reward each. The `totalRewards` is 40 tokens, but the available rewards is only 39 tokens. Each staker will get `10 * 39/40` tokens instead (9.75 tokens each)

```
if (totalRewards > lastAvailableRewards) {
                // Traverse all the eligible services and adjust their rewards proportional to leftovers
                uint256 updatedReward;
                uint256 updatedTotalRewards;
                for (uint256 i = 1; i < numServices; ++i) {
                    // Calculate the updated reward
>                   updatedReward = (eligibleServiceRewards[i] * lastAvailableRewards) / totalRewards;
                    // Add to the total updated reward
>                   updatedTotalRewards += updatedReward;
                    // Add reward to the overall service reward
                    curServiceId = eligibleServiceIds[i];
                    finalEligibleServiceIds[i] = eligibleServiceIds[i];
                    finalEligibleServiceRewards[i] = updatedReward;
                    mapServiceInfo[curServiceId].reward += updatedReward;
                }
```

If there are dust values still avaible after the division (due to truncation), then the remaining dust value is given to the first staker.

```
  // Process the first service in the set
                updatedReward = (eligibleServiceRewards[0] * lastAvailableRewards) / totalRewards;
                updatedTotalRewards += updatedReward;
                curServiceId = eligibleServiceIds[0];
                finalEligibleServiceIds[0] = eligibleServiceIds[0];
                // If the reward adjustment happened to have small leftovers, add it to the first service
                if (lastAvailableRewards > updatedTotalRewards) {
                    updatedReward += lastAvailableRewards - updatedTotalRewards;
                }
                finalEligibleServiceRewards[0] = updatedReward;
                // Add reward to the overall service reward
                mapServiceInfo[curServiceId].reward += updatedReward;
                // Set available rewards to zero
                lastAvailableRewards = 0;
```

This calculation of giving the first staker the dust value is incorrectly calculate
Less active nominees can be left without rewards after an year of inactivity
Audit: 2024-05-olas-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact

The `VoteWeighting` within the protocol requires some sort of activity for nominees at least every 53 weeks to maintain its functionality properly. However, some nominees can be less active. Particularly when users lock their veOLAS for long periods (e.g., 4 years) and vote in a set-and-forget manner. Due to the current one-year lookbehind period, the `nomineeRelativeWeight()` function will break for less active nominees after one year (it will return `0`), causing users with longer lock periods to essentially lose their voting power. This undermines the fairness and intended functionality of the protocol, as these users will no longer be able to influence governance or earn rewards despite their significant long-term commitment.

An attempt to address this issue with keepers would not be effective. A malicious user can create an arbitrarily large number of nominees. This would make iterating over all nominees on a regular basis to ensure checkpoints are made extremely gas-expensive, rendering the keepers model impractical and ineffective.

## Proof of Concept

The vulnerability can be observed in the following scenario:

1. A user locks their veOLAS tokens for 4 years and votes for a nominee.
2. The user does not interact with the nominee for over 53 weeks. No other users interact with it either.
3. After this period, when attempting to call the `nomineeRelativeWeight()` function, it will return `0` even though the user has almost 3 years of lock period remaining.
4. As a result, the voting power associated with the user's locked veOLAS is lost.

This issue can be traced to the code handling the lookbehind period for nominees, which does not retain historical data beyond 53 weeks:

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

The following coded PoC demonstrates the issue. Add the following test case to `VoteWeighting.js`:

```js
        it.only("Nominee can get bigger share", async function () {
            const oneMillionOLAS = ethers.utils.parseEther("1000000");

            // Add nominees
            const numNominees = 2;
            let nominees = [signers[1].address, signers[2].address];
            const chainIds = new Array(numNominees).fill(chainId);
            for (let i = 0; i < numNominees; i++) {
                await vw.addNomineeEVM(nominees[i], chainIds[i]);
            }

            nominees = [convertAddressToBytes32(nominees[0]), convertAddressToBytes32(nominees[1])];

            // Actors
            let charlie = signers[0];
            let alice = signers[10];
            let bob = signers[11];
Loss of reward incentives if ETHFromService is less than the expected rewards
Audit: 2024-05-olas-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-05-olas/blob/main/tokenomics/contracts/Treasury.sol#L387


# Vulnerability details

## Impact
When claiming the reward incentives, if treasury does not have enough `ETHFromServices`, then the reward transfer will be skipped silently leading to a situation that the user does not receive any reward but the mapping resets it to zero.

## Proof of Concept
To claim incentives for the owner of components or agents, the function `claimOwnerIncentives` should be called where it invokes the external function `accountOwnerIncentives` to calculate the amount of incentives in terms of reward (ETH) and topUp (OLAS). Then, the external function `withdrawToAccount` is called to transfer the calculated incentives to the `msg.sender`.
```solidity
    function claimOwnerIncentives(
        uint256[] memory unitTypes,
        uint256[] memory unitIds
    ) external returns (uint256 reward, uint256 topUp) {
        //....

        // Calculate incentives
        (reward, topUp) = ITokenomics(tokenomics).accountOwnerIncentives(msg.sender, unitTypes, unitIds);

        bool success;
        // Request treasury to transfer funds to msg.sender if reward > 0 or topUp > 0
        if ((reward + topUp) > 0) {
            // Get the current OLAS balance
            uint256 olasBalance;
            if (topUp > 0) {
                olasBalance = IToken(olas).balanceOf(msg.sender);
            }

            success = ITreasury(treasury).withdrawToAccount(msg.sender, reward, topUp);

            // Check the balance after the OLAS mint, if applicable
            if (topUp > 0){
                olasBalance = IToken(olas).balanceOf(msg.sender) - olasBalance;
                if (olasBalance != topUp) {
                    revert WrongAmount(olasBalance, topUp);
                }
            }
        }

        // Check if the claim is successful and has at least one non-zero incentive.
        if (!success) {
            revert ClaimIncentivesFailed(msg.sender, reward, topUp);
        }

        //.....
    }
```
https://github.com/code-423n4/2024-05-olas/blob/main/tokenomics/contracts/Dispenser.sol#L789

In the function `withdrawToAccount`, to transfer the reward, it checks whether `ETHFromServices` is higher than the reward or not. If it is not, then reward transfer is skipped, and it continues to transfer the topUp. If topUp transfer is successful, the `true` boolean is returned.
```solidity
    function withdrawToAccount(address account, uint256 accountRewards, uint256 accountTopUps) external
        returns (bool success)
    {
        // Check if the contract is paused
        if (paused == 2) {
            revert Paused();
        }

        // Check for the dispenser access
        if (dispenser != msg.sender) {
            revert ManagerOnly(msg.sender, dispenser);
        }

        uint256 amountETHFromServices = ETHFromServices;
        // Send ETH rewards, if any
        if (accountRewards > 0 && amountETHFromServic
Adding staking instance as nominee before it is created
Audit: 2024-05-olas-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
It is possible to add a staking instance as a nominee before it is being created. This can lead to a situation that when this instance is going to claim its staking incentives, it should go over all the epochs from the time it was added as nominee to the epoch it was created. This range of epochs is useless for this instance, because no incentives can be assigned to that instance during the epoch it is added as nominee until the epoch it is created. By doing so, the attacker can force users (whoever claim the staking incentives for an instance) to consume much more gas.

## Proof of Concept
When creating a proxy staking instance, the address of the to-be-created proxy instance is predictable as follows. 
```solidity
    function getProxyAddressWithNonce(address implementation, uint256 localNonce) public view returns (address) {
        // Get salt based on chain Id and nonce values
        bytes32 salt = keccak256(abi.encodePacked(block.chainid, localNonce));

        // Get the deployment data based on the proxy bytecode and the implementation address
        bytes memory deploymentData = abi.encodePacked(type(StakingProxy).creationCode,
            uint256(uint160(implementation)));

        // Get the hash forming the contract address
        bytes32 hash = keccak256(
            abi.encodePacked(
                bytes1(0xff), address(this), salt, keccak256(deploymentData)
            )
        );

        return address(uint160(uint256(hash)));
    }
```
https://github.com/code-423n4/2024-05-olas/blob/main/registries/contracts/staking/StakingFactory.sol#L141

It shows that the address is dependent on two dynamic parameters `nonce` as well as the address of the `implementation`. 

The `nonce` is incremented by one each time an instance is created, so it can be tracked easily. 

The `implementation` address is assumed to be the address of the contract `StakingToken` or `StakingNativeToken` for most instances. In other words, it is more probable that users create the proxy instance based on those default implementations instead of a customized one, and users usually only change the `initPayload` to create an instance with different configurations. As can be seen in the following code, the address of an instance is independent of `initPayload`.

```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
Loss of incentives if total weight in an epoch is zero
Audit: 2024-05-olas-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-05-olas/blob/main/tokenomics/contracts/Dispenser.sol#L911


# Vulnerability details

## Impact
If during an epoch, the total weight is zero, then the staking incentives related to that epoch will be lost, while it is expected that they will be refunded to tokenomics inflation.

## Proof of Concept
During claiming the staking incentives through `claimStakingIncentives`, the function `calculateStakingIncentives` is called to calculate the `totalStakingIncentive` and `totalReturnAmount` which means the total staking incentives that should be distributed to staking instances and total incentives that should be refunded to tokenomics inflation.

```solidity
    function claimStakingIncentives(
        uint256 numClaimedEpochs,
        uint256 chainId,
        bytes32 stakingTarget,
        bytes memory bridgePayload
    ) external payable {
        //.....

        (uint256 stakingIncentive, uint256 returnAmount, uint256 lastClaimedEpoch, bytes32 nomineeHash) =
            calculateStakingIncentives(numClaimedEpochs, chainId, stakingTarget, bridgingDecimals);

        //....

        if (returnAmount > 0) {
            ITokenomics(tokenomics).refundFromStaking(returnAmount);
        }

        //....

    }
```
https://github.com/code-423n4/2024-05-olas/blob/main/tokenomics/contracts/Dispenser.sol#L953

In this function, the staking weight and total weight for each epoch is calculated. If available staking incentives is higher than total weight, then the extra part (`diff`) will be refunded to tokenomics inflation.

```solidity
            uint256 availableStakingAmount = stakingPoint.stakingIncentive;

            uint256 stakingDiff;
            if (availableStakingAmount > totalWeightSum) {
                stakingDiff = availableStakingAmount - totalWeightSum;
                availableStakingAmount = totalWeightSum;
            }

            //.....

            returnAmount = (stakingDiff * stakingWeight) / 1e18;
```
https://github.com/code-423n4/2024-05-olas/blob/main/tokenomics/contracts/Dispenser.sol#L903

Then, the rest of the available staking incentives will be distributed to the staking instance based on the staking weight assigned to it.
```solidity
stakingIncentive = (availableStakingAmount * stakingWeight) / 1e18;
```
https://github.com/code-423n4/2024-05-olas/blob/main/tokenomics/contracts/Dispenser.sol#L916

The issue is that if the total weight for an epoch is equal to zero, no staking incentives will be refunded to tokenomics inflation. While, it is expected that unused/extra staking incentives be returned to tokenomics inflation. The reason is that when `totalWeightSum` is zero (obviously the `stakingWeight` will be zero either), the following if-clause will be executed.
```solidity
    if (stakingWeight < uint256(stakingPoint.minStakingWeight) * 1e14) {
        // If vote weighting staking weight is lower than the defined threshold - return the staking incentive
        returnAmo
Changing VoteWeighting contract can result in lost staking incentives
Audit: 2024-05-olas-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
The Dispenser contract provides functionality for the owner to update manager accunts at any time, including changing the VoteWeighting contract. 

```solidity
function changeManagers(address _tokenomics, address _treasury, address _voteWeighting) external {
        // Check for the contract ownership
        if (msg.sender != owner) {
            revert OwnerOnly(msg.sender, owner);
        }

        // Change Tokenomics contract address
        if (_tokenomics != address(0)) {
            tokenomics = _tokenomics;
            emit TokenomicsUpdated(_tokenomics);
        }

        // Change Treasury contract address
        if (_treasury != address(0)) {
            treasury = _treasury;
            emit TreasuryUpdated(_treasury);
        }

        // Change Vote Weighting contract address
        if (_voteWeighting != address(0)) {
            voteWeighting = _voteWeighting;
            emit VoteWeightingUpdated(_voteWeighting);
        }
    }
```
Changing the VoteWeighting contract mid-flight can lead to lost staking incentives due to mapLastClaimedStakingEpochs state reset to the current epoch.  

## Proof of Concept
Consider the following scenario:
1. Dispenser owner decides to swap the current VoteWeighting contract for some reason, eg a critical vulnerability has been found with the VoteWeighting contract.
2. The new VoteWeighting contract does not have Nominee X, a popular staking program that has  earned huge rewards since the protocol was deployed. Nominee X has not yet claimed staking rewards for 3 epochs.
3. Bob notices that a new VoteWeighting contract has been set, and decides to disrupt the party by registering staking program X **again** by calling VoteWeighting::addNominee(X, chainId). Since the Dispenser contract does not check if Nominee X has already been set, this results in a reset to the current epoch
```solidity 
mapLastClaimedStakingEpochs[X] = ITokenomics(tokenomics).epochCounter().  

```
Now when `claimStakingIncentives` is called for Nominee X, staking incentives for 2 epochs will have been lost.
For a coded POC, 

1. Add these lines to Tokenomics/test/Dispenser.t.sol::Ln 24:

```solidity

    MockVoteWeighting internal vw;
    MockVoteWeighting internal vw2;
```
2. Add these lines to Tokenomics/test/Dispenser.t.sol::Ln 64:
```solidity
vw = new MockVoteWeighting(address(dispenser));
vw2 = new MockVoteWeighting(address(dispenser));
```
3. Add this test to Tokenomics/test/Dispenser.t.sol::DispenserTest Ln 100:
```solidity


    function testChangeVoteWeightLostIncenctives() public{
        dispenser.setPauseState(Pause.Unpaused);
        address deal = makeAddr("deal");
        vw.addNominee(deal,1);
        uint256 epoch = dispenser.mapLastClaimedStakingEpochs(keccak256(abi.encode(Nominee(bytes32(uint256(uint160(deal))),1))));