xanoutas/solidity-security-findings huggingface dataset:
Upgraded Q -> 2 from #34 [1720018773609]
Audit: 2024-05-olas-findings
Severity: MEDIUM

Judge has assessed an item in Issue #34 as 2 risk. The relevant finding follows:

 ### [L-11] Inaccurate nominee weights may be returned by `_getWeight()` for unused nominees

The `_getWeight()` function fills in historic nominee weights week-over-week for up to `MAX_NUM_WEEKS` (53 weeks). However, any number of nominees can be permissionlessly added via `addNomineeEVM()` or `addNomineeNonEVM()` and remain unused for over 53 weeks. 

In this scenario, it is not feasible to ensure that every nominee is checkpointed within that 53 week period. If a nominee is not checkpointed for over 53 weeks, `_getWeight()` would return a nominee weight value that is higher than the actual current bias, as the loop exits after 53 iterations.

This inaccurate weight value is then used throughout the protocol, as the return value of `_getWeight()` is considered accurate under all circumstances. This could lead to incorrect relative weights and sums being used.

Consider implementing logic that allows the calling function to revert if the `_getWeight()` return value is from a nominee that hasn't been checkpointed in over 53 weeks, instead of unconditionally using a potentially stale value.
Upgraded Q -> 2 from #34 [1720018599046]
Audit: 2024-05-olas-findings
Severity: MEDIUM

Judge has assessed an item in Issue #34 as 2 risk. The relevant finding follows:

 ### [L-03] Excess ETH sent when sending staking incentives to Arbitrum will remain in `ArbitrumDepositProcessorL1`

The `msg.value` sent to `ArbitrumDepositProcessorL1.sendMessageBatch()` from the dispenser is only checked to be at least as large as `cost[0] + cost[1]`:

```solidity
File: ArbitrumDepositProcessorL1.sol
164:         // Get the total cost
165:         uint256 totalCost = cost[0] + cost[1];
166: 
167:         // Check fot msg.value to cover the total cost
168:         if (totalCost > msg.value) {
169:             revert LowerThan(msg.value, totalCost);
170:         }
```

However, excess funds are not returned to the user and will remain in the `ArbitrumDepositProcessorL1`. It would be more sensible to check for strict equality.

This is also the case in the `_sendMessage()` functions of the Wormhole target dispenser on L2 as well as in the Optimism contracts on both sides, although those contracts should not accept any ETH in the first place as reported in a separate finding.
Desynchronized Nominee Data After Nominee Removal.
Audit: 2024-05-olas-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

In the `removeNominee` function there are inconsistencies between the `setNominees` array and the `mapNomineeIds` mapping. This inconsistency can cause incorrect behavior when retrieving nominees or their weights, as the mapping will point to the wrong nominee after the shift operation.

## Impact
The `mapNomineeIds` mapping is used to associate a nominee's hash (derived from their account and chain ID) with their corresponding ID in the `setNominees` array.
When a nominee is removed, the last nominee in the `setNominees` array is shifted to the position of the removed nominee, but the `mapNomineeIds` mapping is not updated for the shifted nominee.
This means that the `mapNomineeIds` mapping will still point to the old nominee hash for the shifted nominee's ID, even though the nominee at that position has changed.
As a result, any function that relies on the `mapNomineeIds` mapping to retrieve nominee information or weights will return incorrect data for the shifted nominee.
This can lead to various issues, such as incorrect weight calculations, voting power assignments, or other operations that depend on the accurate mapping between nominee hashes and IDs.
[nance](https://github.com/code-423n4/2024-05-olas/tree/main/governance)/[contracts](https://github.com/code-423n4/2024-05-olas/tree/main/governance/contracts)

[VoteWeighting#removeNominee](https://github.com/code-423n4/2024-05-olas/blob/e2a8bc31d2769bfb578a06cc64919ad369a82c08/governance/contracts/VoteWeighting.sol#L586-L627)
```solidity
function removeNominee(bytes32 account, uint256 chainId) external {
    // ...

    // Get the nominee id in the nominee set
    uint256 id = mapNomineeIds[nomineeHash];
    if (id == 0) {
        revert NomineeDoesNotExist(account, chainId);
    }

    // ...

    // Remove nominee from the map
    mapNomineeIds[nomineeHash] = 0;

    // Shuffle the current last nominee id in the set to be placed to the removed one
    nominee = setNominees[setNominees.length - 1];
    bytes32 replacedNomineeHash = keccak256(abi.encode(nominee));
    mapNomineeIds[replacedNomineeHash] = id; // <-- This line updates the mapping for the removed nominee
    setNominees[id] = nominee; // <-- This line shifts the last nominee to the position of the removed nominee
    // Pop the last element from the set
    setNominees.pop();

    // ...
}
```
The line `mapNomineeIds[replacedNomineeHash] = id;` updates the `mapNomineeIds` mapping for the removed nominee correctly.

However, the line `setNominees[id] = nominee;` shifts the last nominee in the `setNominees` array to the position of the removed nominee.

**`The problem is that there is no corresponding update to the `mapNomineeIds` mapping for the shifted nominee.`**

As a result, the `mapNomineeIds` mapping will still point to the old nominee hash for
`stakingIncentive` is not transffered onto the next epoch which is a deviation from the spec
Audit: 2024-05-olas-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact

In the `Tokenomics` smart contract, there is a `stakingIncentive` param inside of `StakingPoint` struct that represents the staking incentives for each epoch. Additionally, it's supposed to keep refund amounts after `claimStakingIncentives()` in `Dispenser` is called. So, as per specification, if there are some amounts remained after refunds, they'll be transferred in the next epoch. However, this logic is not implemented in the current functionality and the incentives stay in the same epoch.

## Proof of Concept

Let's say there were some refunds happen during the current epoch:

https://github.com/code-423n4/2024-05-olas/blob/main/tokenomics/contracts/Tokenomics.sol#L826-833
```
function refundFromStaking(uint256 amount) external {
        // Check for the dispenser access
        if (dispenser != msg.sender) {
            revert ManagerOnly(msg.sender, depository);
        }

        uint256 eCounter = epochCounter;
        uint256 stakingIncentive = mapEpochStakingPoints[eCounter].stakingIncentive + amount;
```

So now `stakingIncentives` accumulates some excessive amounts that were not distributed during the current epoch. So, as per spec, they should be transferred onto the next epoch when calling `checkpoint()` in `Tokenomics` smart contract:

```
Specifically, this logic ensures that OLAS emissions, which have been
decided to be retained by veOLAS holders through voting, are returned to the staking inflation pool for the next epoch
```

https://github.com/code-423n4/2024-05-olas/blob/main/tokenomics/contracts/Tokenomics.sol#L1235-1238
```
incentives[7] = mapEpochStakingPoints[eCounter].stakingIncentive;
        // Adding service staking top-ups amount based on a current epoch inflation
        incentives[8] = incentives[7] + (inflationPerEpoch * mapEpochStakingPoints[eCounter].stakingFraction) / 100;
        mapEpochStakingPoints[eCounter].stakingIncentive = uint96(incentives[8]);
```

As you can see here, the function just takes parameter for this epoch and then updates it for the same epoch. The `epochCounter` is only increased at the end of the checkpoint:

```
  epochCounter = uint32(eCounter + 1);
```

So this creates a deviation from the spec as the rewards are left in the same epoch instead of being transferred onto the next one.

## Tools Used

Manual review.

## Recommended Mitigation Steps

Make sure the refunds from the current epoch are transferred onto the next one.





## Assessed type

Other
`timeSum` update make it impossible to update `getSum()` in the next week
Audit: 2024-05-olas-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact

`VoteWeighting` smart contract is used by the protocol to enable Olas DAO members (via veOLAS) to vote on staking programs, assigning weights according to their preferences. It has individual weight params and also the cumulative ones. In the `_getSum()` function we update the bias and the slope of the point. However, if we remove a nominee, it becomes impossible to update those params in the next week.

## Proof of Concept

According to the spec:

```
To ensure compatibility with veOLAS, changes in vote weights per staking contract are scheduled for the next week
```

`_getSum()` function takes the current value of the `timeSum` and loops over `MAX_NUM_WEEKS`. And if the `t > block.timestamp` condition becomes true, the loop simply breaks:

https://github.com/code-423n4/2024-05-olas/blob/main/governance/contracts/VoteWeighting.sol#L225-230
```
  uint256 t = timeSum;
        Point memory pt = pointsSum[t];
        for (uint256 i = 0; i < MAX_NUM_WEEKS; i++) {
            if (t > block.timestamp) {
                break;
            }

```

So let's say `_getSum()` was called and the next time it can be called is 1 week later. At the same time, at the end of this week (say, day 6), a nominee is removed. Now `timeSum` value is updated:

https://github.com/code-423n4/2024-05-olas/blob/main/governance/contracts/VoteWeighting.sol#L605
```
 uint256 nextTime = (block.timestamp + WEEK) / WEEK * WEEK;
```

https://github.com/code-423n4/2024-05-olas/blob/main/governance/contracts/VoteWeighting.sol#L612
```
timeSum = nextTime;
```

There is also a call to the `_getSum()` inside of `removeNominee()`. However, it's day 6 and nothing is still updated. So basically the `timeSum` is assigned to the week later and now if we call `_getSum()`, the loop will break. So it basically can take far more than 1 week and it breaks compatibility with veOLAS.


## Tools Used

Manual review.

## Recommended Mitigation Steps

Make sure `timeSum` param is correctly updated.


## Assessed type

Other
If the underlying asset is a fee on transfer token,  the `_withdraw` may fail
Audit: 2024-05-olas-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
The `deposit` is used to deposit funds for staking. It will transfer tokens to the contract and record the new actual balance and available rewards:
```solidity
        // Record the new actual balance and available rewards
        balance = newBalance;
        availableRewards = newAvailableRewards;

        // Add to the overall balance
        SafeTransferLib.safeTransferFrom(stakingToken, msg.sender, address(this), amount);
```
The issue here is if the `stakingToken` is a fee on transfer token then the actual received amount will be less than input parameter `amount` and the recorded balance is wrong. For example, A funder deposits 100 tokens to the contract but the actual received amount is 90. However, the `balance` is recorded to 100. When the staked service tries to withdraw all 100 tokens, there will not be enough tokens.

Impact: The same issue found in other [C4 report](https://code4rena.com/reports/2023-07-pooltogether#m-01-if-the-underlying-asset-is-a-fee-on-transfer-token-it-could-break-the-internal-accounting-of-the-vault) is defined as Medium

## Proof of Concept
```solidity
function deposit(uint256 amount) external {
        // Add to the contract and available rewards balances
        uint256 newBalance = balance + amount;
        uint256 newAvailableRewards = availableRewards + amount;

        // Record the new actual balance and available rewards
        balance = newBalance;
        availableRewards = newAvailableRewards;

        // Add to the overall balance
        SafeTransferLib.safeTransferFrom(stakingToken, msg.sender, address(this), amount);

        emit Deposit(msg.sender, amount, newBalance, newAvailableRewards);
    }
```
https://github.com/code-423n4/2024-05-olas/blob/main/registries/contracts/staking/StakingToken.sol#L115-L128

```solidity
function _withdraw(address to, uint256 amount) internal override {
        // Update the contract balance
        balance -= amount;

        SafeTransferLib.safeTransfer(stakingToken, to, amount);

        emit Withdraw(to, amount);
    }
```
https://github.com/code-423n4/2024-05-olas/blob/main/registries/contracts/staking/StakingToken.sol#L104C5-L111C6

## Tools Used
Manual Review

## Recommended Mitigation Steps
It is recommended to record actual received amount:
```solidity
function deposit(uint256 amount) external {
        uint256 before = IERC20(stakingToken).balanceOf(address(this));
        // Add to the overall balance
        SafeTransferLib.safeTransferFrom(stakingToken, msg.sender, address(this), amount);
        uint256 amount = IERC20(stakingToken).balanceOf(address(this)) - before;
        // Add to the contract and available rewards balances
        uint256 newBalance = balance + amount;
        uint256 newAvailableRewards = availableRewards + amount;

        // Record the new actual balance and ava
The StakingToken is incompatible with fee-on-transfer tokens
Audit: 2024-05-olas-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-05-olas/blob/e2a8bc31d2769bfb578a06cc64919ad369a82c08/registries/contracts/staking/StakingToken.sol#L117-L125


# Vulnerability details

## Impact
The `[registries/contracts/staking/]StakingToken.sol` (https://github.com/code-423n4/2024-05-olas/blob/main/registries/contracts/staking/StakingToken.sol#L117-L125) contract is incompatible with fee-on-transfer ERC20 tokens. The `StakingToken` contract assumes that the `amount` transferred during the `deposit` is received by `address(this)` **in-full**.

However, if the `stakingToken` is a **fee-on-transfer** ERC20 token, a percentage of the `amount` during the `deposit` will be consumed by the token contract as a charged fee.

Currently, the `rewardToken` is the Olas token, but, as per my clarification with @kupermind on Discord: *"but obviously can be extended to any token"*.


Note that `StakingToken` is an extension for the `StakingBase` contract.

Due to the fact that the *rewards accumulated for eligible stakers* are calculated using the following formula, this problem with **fee-on-transfer** tokens being unsupported by the `StakingToken` extension contract (in case the `rewardToken` in `StakingBase` is one of them), will give incorrect results.


#### The precise damage might differ: *for instance, for large `StakingBase` pools, keeping track of an improper `totalRewards` value can lead to serious calculation problems, whilst for smaller pools the calculation problem will not be **that significant**.*

```solidity
/** 623: **/         updatedReward = (eligibleServiceRewards[i] * lastAvailableRewards) / totalRewards;
/** 624: **/         // Add to the total updated reward
/** 625: **/         updatedTotalRewards += updatedReward;
/** 626: **/         // Add reward to the overall service reward
/** 627: **/         curServiceId = eligibleServiceIds[i];
/** 628: **/         finalEligibleServiceIds[i] = eligibleServiceIds[i];
/** 629: **/         finalEligibleServiceRewards[i] = updatedReward;
/** 630: **/         mapServiceInfo[curServiceId].reward += updatedReward;
```

## Details
As a result of an improper calculation, the stakers will receive less rewards than they expect.

Taking into account the case when the `rewardToken` is a fee-on-transfer token, the user will also be charged a fee when `claim`ing, so the losses will be significant for stakers.

```solidity
// Get the unstaked service data
        reward = sInfo.reward;
        uint256[] memory nonces = sInfo.nonces;
        address multisig = sInfo.multisig;

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

        // 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();
        }
There is no way to get the refund through the Wormhole Bridge from L2 - L1
Audit: 2024-05-olas-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact

Refunded tokens will not be accepted by L1, will be lost in contract.

## Proof of Concept

When calling `WormholeTargetDispenserL2._sendMessage()` to send a message from L2 to L1, the refund account is set to `msg.sender if it is set at address(0).

```
  function _sendMessage(uint256 amount, bytes memory bridgePayload) internal override {
        // Check for the bridge payload length
        if (bridgePayload.length != BRIDGE_PAYLOAD_LENGTH) {
            revert IncorrectDataLength(BRIDGE_PAYLOAD_LENGTH, bridgePayload.length);
        }

        // Extract refundAccount and gasLimitMessage from bridgePayload
        (address refundAccount, uint256 gasLimitMessage) = abi.decode(bridgePayload, (address, uint256));
        // If refundAccount is zero, default to msg.sender
        if (refundAccount == address(0)) {
            refundAccount = msg.sender;
        }
```

`msg.sender` means the `DefaultTargetDispenserL2()` is the msg.sender, and in L1, the refund address will be the Dispenser contract in L1. 

However, the dispenser contract has no way of receiving the native tokens and withdrawing them.

Looking at the L1 -> L2 transfer, if there is a refund, the owner can call `drain()` in the DefaultTargetDispenserL2.sol to retrieve refunds

```
 /// @dev Drains contract native funds.
    /// @notice For cross-bridge leftovers and incorrectly sent funds.
    /// @return amount Drained amount to the owner address.
    function drain() external returns (uint256 amount) {
        // Reentrancy guard
```

## Tools Used

Manual Review

## Recommended Mitigation Steps

Since the refund address is a contract if msg.sender is set, it should have a function in Dispenser.sol to retrieve the refunds.

https://docs.wormhole.com/wormhole/quick-start/tutorials/hello-wormhole/beyond-hello-wormhole#feature-receive-refunds


## Assessed type

Context
Refund address on Wormhole that is set as msg.sender may be different for different chains
Audit: 2024-05-olas-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-05-olas/blob/e2a8bc31d2769bfb578a06cc64919ad369a82c08/tokenomics/contracts/staking/WormholeDepositProcessorL1.sol#L82


# Vulnerability details

## Impact

Refunds will be lost.

## Proof of Concept

In both L1 and L2 WormholeDepositProcessor contracts, if the refundAccount is not set, set it to the `msg.sender`.

```
        // If refundAccount is zero, default to msg.sender
        if (refundAccount == address(0)) {
            refundAccount = msg.sender;
        }
```

In this case, the `msg.sender` is eith the Dispenser or the DefaultTargetDispenserL2 contract. The function assumes that the L1 and L2 addresses is the same, so that the refund can go to the same address, just on different chain.

However, that might not be the case as the same contract may have a different address on a different chain. 0x123 on L1 may be 0x456 on L2, which means that the `msg.sender` will be of a different address. 

## Tools Used

Manual Review

## Recommended Mitigation Steps

Best is to not assume that contracts on a different chain have the same address, and make sure `refundAccount` is always set by the caller. It will be ideal if the `refundAccount` is reverted if it is address(0), instead of being the msg.sender.


## Assessed type

Context
Fee-on-transfer problem in the `StakingToken` contract
Audit: 2024-05-olas-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
Fee-on-transfer tokens may mess up the accounting in the staking mechanism

## Proof of Concept
After conversation with the sponsor I came to the conclution, that the `stakingToken` address in the logic of `StakingToken` contract, can be a token different than `Olas`. This means that a token with Fee-On-Transfer functionality, can mess up the tokens accounting in the staking mechanism

## Tools Used
Manual review 
## Recommended Mitigation Steps
After importing the `IERC20` interface in the contract, change the transfer in the deposit function like this:
```javascript
  uint256 balanceBefore = IERC20(stakingToken).balanceOf(address(this));
        SafeTransferLib.safeTransferFrom(
            stakingToken,
            msg.sender,
            address(this),
            amount
        );
        amount = IERC20(stakingToken).balanceOf(address(this)) - balanceBefore;
```


## Assessed type

ERC20
Removing last nominee in setNominees makes mapNomineeIds persist causing getter functions to err
Audit: 2024-05-olas-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-05-olas/blob/3ce502ec8b475885b90668e617f3983cea3ae29f/governance/contracts/VoteWeighting.sol#L622-L627
https://github.com/code-423n4/2024-05-olas/blob/3ce502ec8b475885b90668e617f3983cea3ae29f/governance/contracts/VoteWeighting.sol#L725
https://github.com/code-423n4/2024-05-olas/blob/3ce502ec8b475885b90668e617f3983cea3ae29f/governance/contracts/VoteWeighting.sol#L799-L801


# Vulnerability details

## Impact
Getter functions like `getNomineeId` and `getNextAllowedVotingTimes` return corrupted values from a removed nominee that will err off chain calculations instead of rightfully returning 0 for `getNomineeId` and reverting with `NomineeDoesNotExist` error in `getNextAllowedVotingTimes`.

## Proof of Concept

In removeNominee, we see that 
```
    function removeNominee(bytes32 account, uint256 chainId) external {
    .
    .

        // Get the nominee id in the nominee set
        uint256 id = mapNomineeIds[nomineeHash];
    .
        // Remove nominee from the map
@>      mapNomineeIds[nomineeHash] = 0;

        // Shuffle the current last nominee id in the set to be placed to the removed one
        nominee = setNominees[setNominees.length - 1];
        bytes32 replacedNomineeHash = keccak256(abi.encode(nominee));
@>      mapNomineeIds[replacedNomineeHash] = id;
        setNominees[id] = nominee;
        // Pop the last element from the set
        setNominees.pop();
    .
    .
    .
}
```

* Here, in the code. the `mapNomineeIds` for the nomineeHash to remove is set 0 at the start.

* `mapNomineesIds` for the last element is overwritten with the id of the just removed nomineeHash.

* Then the last nominee is taken from `setNominees` and it's set to the id position in the array of `setNominees`

* The `setNominees` pops the last element

This approach works when the element to remove is not the last element in `setNominees`, However when the element to remove is the last element then the nominee is removed from `setNominees`. The id it has lingers in the value of `mapNomineeIds` as we're overwriting it with the id of the removed element.

This errs getter functionalities since mapNomineeIds is assumed to be zero for removed nominees but it need not be the case, confusing users and off chain processors.

## Tools Used
Manual analysis

## Recommended Mitigation Steps
Have `mapNomineeIds[replacedNomineeHash] = id` only when id of nominee to remove isn't the last


## Assessed type

Other