xanoutas/solidity-security-findings huggingface dataset:
Dispenser is unable to control refunded tokens when bridging via Wormhole
Audit: 2024-05-olas-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-05-olas/blob/3ce502ec8b475885b90668e617f3983cea3ae29f/tokenomics/contracts/staking/WormholeDepositProcessorL1.sol#L83-L97


# Vulnerability details

Relevant code: [WormholeDepositProcessorL1::\_sendMessage](https://github.com/code-423n4/2024-05-olas/blob/3ce502ec8b475885b90668e617f3983cea3ae29f/tokenomics/contracts/staking/WormholeDepositProcessorL1.sol#L83-L97)
### Description
The `Dispenser` contract is responsible for distributing token incentives across `Staking` contracts deployed on various blockchains. Every chain has a corresponding `depositProcessor` contract, which facilitates the bridging and handles chain-specific transfers/messaging:
```javascript
if (chainId <= MAX_EVM_CHAIN_ID) {
    address stakingTargetEVM = address(uint160(uint256(stakingTarget)));
    IDepositProcessor(depositProcessor).sendMessage{value:msg.value}(stakingTargetEVM, stakingIncentive,
        bridgePayload, transferAmount);
} else {
    // Send to non-EVM
	IDepositProcessor(depositProcessor).sendMessageNonEVM{value:msg.value}(stakingTarget,
        stakingIncentive, bridgePayload, transferAmount);
}
```

The `WormholeDepositProcessorL1`, makes sure `sendMessage` is called only by the `Dispenser`:
```javascript
if (msg.sender != l1Dispenser) {
	revert ManagerOnly(l1Dispenser, msg.sender);
}
```

When bridging funds using **Wormhole**, one of the parameters encoded in the `bridgePayload` is `refundAccount`.  When such address is not set in the payload, the `Dispenser` is set as default:
```javascript
// If refundAccount is zero, default to msg.sender
if (refundAccount == address(0)) {
	refundAccount = msg.sender; // This can only be Dispenser
}
```

According to the docs, `refundAccount` [can receive](https://docs.wormhole.com/wormhole/quick-start/tutorials/hello-wormhole/beyond-hello-wormhole#feature-receive-refunds) the **unspent gas**:
> **If refundChain is equal to targetChain**, a refund of `targetChainRefundPerGasUnused * (gasLimit - gasUsed)` will be sent to address `refundAddress` on the target chain.

When this address is inappropriately set, the refund will be lost forever.
### Root Cause
Lack of appropriate `refundAccount` for L2 target chain.

### Impact
Wormhole bridging will lose gas refunds if the default value for `refundAccount` is used. In a protocol which depends on bridging tokens, therefore paying gas fees regularly, these refunds will accrue to a substantial amount over time.

### Suggested Mitigation
The default refund address should be configurable and deployed on the corresponding target chain. The best candidate would be the corresponding `WormholeTargetDispenserL2`.


## Assessed type

Other
if amountETHFromServices< accountRewards and topup is non zero, account doesn't receive rewards but it gets deducted from rewards balance
Audit: 2024-05-olas-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-05-olas/blob/main/tokenomics/contracts/Dispenser.sol#L809-L832
https://github.com/code-423n4/2024-05-olas/blob/main/tokenomics/contracts/Treasury.sol#L402-L419


# Vulnerability details

## Impact
When claiming owner incentives in the Dispenser contract, if amountETHFromServices< accountRewards, the user's reward balance will get updated, but he won't receive the reward

## Proof of Concept
Dispenser#claimOwnerIncentives can be used to claim incentives for the owner of components/agents:

```solidity
    function claimOwnerIncentives(
        uint256[] memory unitTypes,
        uint256[] memory unitIds
    ) external returns (uint256 reward, uint256 topUp) {
        ...
        (reward, topUp) = ITokenomics(tokenomics).accountOwnerIncentives(
            msg.sender,
            unitTypes,
            unitIds
        );

        bool success;
        if ((reward + topUp) > 0) {
            ...
            success = ITreasury(treasury).withdrawToAccount(
                msg.sender,
                reward,
                topUp
            );
            ...
        }

        if (!success) {
            revert ClaimIncentivesFailed(msg.sender, reward, topUp);
        }
        ...
    }
```

Here is Treasury#withdrawToAccount:

```solidity
    function withdrawToAccount(
        address account,
        uint256 accountRewards,
        uint256 accountTopUps
    ) external returns (bool success) {
        ...
        if (accountRewards > 0 && amountETHFromServices >= accountRewards) {
            amountETHFromServices -= accountRewards;
            ETHFromServices = uint96(amountETHFromServices);
            emit Withdraw(ETH_TOKEN_ADDRESS, account, accountRewards);
            (success, ) = account.call{value: accountRewards}("");
            if (!success) {
                revert TransferFailed(
                    address(0),
                    address(this),
                    account,
                    accountRewards
                );
            }
        }
        if (accountTopUps > 0) {
            // Tokenomics has already accounted for the account's top-up amount,
            // thus the the mint does not break the inflation schedule
            IOLAS(olas).mint(account, accountTopUps);
            success = true;
            emit Withdraw(olas, account, accountTopUps);
        }
    }
```

We can see that only if accountRewards>0 && amountETHFromServices>=accountRewards, the if block gets executed.
If amountETHFromServices < accountRewards, the if block, which contains logic for sending the rewards won't be executed.

Still looking at the withdrawAccount function, if the topup>0, olas gets minted, and the `success` return parameter gets set to true.

claimOwnerIncentives would revert if !success. But in our case, it won't revert cos success is true.

This is a problem because in claimOwnerIncentives function, the rewards balance of the user has aleady been updated when calling Token
In ArbitrumDepositProcessorL1#_sendMessage, if refundAccount is 0, it is set to Dispenser contract, but the funds can't be used
Audit: 2024-05-olas-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-05-olas/blob/main/tokenomics/contracts/staking/ArbitrumDepositProcessorL1.sol#L136
https://github.com/code-423n4/2024-05-olas/blob/main/tokenomics/contracts/staking/ArbitrumDepositProcessorL1.sol#L182
https://github.com/code-423n4/2024-05-olas/blob/main/tokenomics/contracts/staking/ArbitrumDepositProcessorL1.sol#L190


# Vulnerability details

## Impact
There's no logic to spend funds in the Dispenser alias on Arbitrum. Setting the recipient of a createRetryableTicket call to Dispenser contract would lead to loss of the refund

## Proof of Concept
In ArbitrumDepositProcessorL1#\_sendMessage, if refundAccount==address(0), it is set to msg.sender i.e. Dispenser contract address

```solidity
function _sendMessage(
    address[] memory targets,
    uint256[] memory stakingIncentives,
    bytes memory bridgePayload,
    uint256 transferAmount
) internal override returns (uint256 sequence) {
    ...
    if (refundAccount == address(0)) {
        refundAccount = msg.sender;
    }
    ...
    sequence = IBridge(l1MessageRelayer).createRetryableTicket{
        value: cost[1]
    }(
        l2TargetDispenser,
        0,
        maxSubmissionCostMessage,
        refundAccount,
        refundAccount,
        gasLimitMessage,
        gasPriceBid,
        data
    );
}
```

But there is no logic to use the refund that gets sent to the Dispenser alias on Arbitrum. Hence, refunds are lost

## Tools Used
Manual Review

## Recommended Mitigation Steps
- ArbRetyableTx can be queried to get the current cost for transaction execution
- Include logic in Dispenser contract to be able to spend funds sent to its L2 alias






## Assessed type

ETH-Transfer
BlackListed multisig will not be able to withdraw the funds.
Audit: 2024-05-olas-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-05-olas/blob/3ce502ec8b475885b90668e617f3983cea3ae29f/registries/contracts/staking/StakingToken.sol#L104-L110


# Vulnerability details

# **Vulnerability Detail**

User could create the staking contract with the USDC || USDT as a stakingToken. However after some time, if the user's multisig wallet will be blacklisted by USDC || USDT, he will not be able to claim/unstake the staking reward, which will be accumulated and stuck forever in the Staking contract.

# **Proof of Concept**
https://github.com/code-423n4/2024-05-olas/blob/3ce502ec8b475885b90668e617f3983cea3ae29f/registries/contracts/staking/StakingToken.sol#L104-L110

# **Impact**

USDC or USDT funds of the black listed account will be stuck forever in the Olas Staking contract.

# **Recommendation**

There are 2 options. Ensure that owner could withdraw the funds to the any address that he pass into the unstake/claim params. Or, create the withdraw mechanism which would allow to withdraw the blacklisted funds and put it into the Olas DAO “vault.”


## Assessed type

Context
Incorrect Mapping Update During Nominee Removal Leads to Inconsistent State
Audit: 2024-05-olas-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Summary 
When removing a nominee, the function `removeNominee` in the provided code attempts to replace the nominee being removed with the last nominee in the `setNominees` array and then remove the last nominee from the array. However, the current implementation updates the `mapNomineeIds` mapping for the replaced nominee before actually moving the last nominee into the removed nominee's slot. This sequence leads to an incorrect update of `mapNomineeIds`.
It also does not handle the case where the nominee being removed is the last one in the array
## Impact
This issue can lead to inconsistencies and incorrect states within the contract's data structures. Specifically, the mapping `mapNomineeIds` may contain incorrect nominee IDs, potentially causing errors in subsequent operations that rely on accurate nominee data. These inconsistencies can disrupt the proper functioning of the voting and nomination processes, possibly leading to erroneous removal, replacement, or querying of nominees.

## Proof of Concept
Here's the problematic part of the `removeNominee` function:

```
// 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();
```
Issue Breakdown
The code prematurely updates `mapNomineeIds` for the replaced nominee before moving the last nominee to the removed nominee's slot.
This can lead to an incorrect state in `mapNomineeIds`, where the mapping does not reflect the correct positions of the nominees.
Here's a step-by-step description of the problematic sequence:

Assume `setNominees` contains `[N1, N2, N3]` and we are removing `N2`.
The `removeNominee` function sets `mapNomineeIds[hash(N2)]` to 0.
It then fetches `N3` and updates `mapNomineeIds[hash(N3)]` to point to the index of `N2` before actually moving `N3` into the slot previously occupied by `N2`.

## Tools Used
Manual Code Review
## Recommended Mitigation Steps
To correct this issue, the nominee should be moved first before updating the mapping. Here is the revised code:
```
// Get the last index of the nominees array
uint256 lastIndex = setNominees.length - 1;
if (id != lastIndex) {
    // Shuffle the current last nominee id in the set to be placed to the removed one
    Nominee memory lastNominee = setNominees[lastIndex];
    bytes32 lastNomineeHash = keccak256(abi.encode(lastNominee));
    // Update the mapping after moving the nominee
    setNominees[id] = lastNominee;
    mapNomineeIds[lastNomineeHash] = id;
}
// Pop the last element
setNominees.pop();
// Finally, remove the nomine
The `_processData` function is not compatible with duplicate target
Audit: 2024-05-olas-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
In the `_processData` function, `data` is parsed into `targets` and `amounts`. `targets` is an array of staking target addresses and `amounts` is an array of staking incentives.
The issue is that the `_processData` function does not check for duplicate target. If the `targets` array contains duplicate data, the `stakingQueueingNonces[queueHash]` will be set to `true` multiple times for the same `queueHash` when there is not enough `Olas` tokens in the contract. The function `redeem` is used to redeem queued staking incentive when there is enough `Olas` tokens in the contract. However, the function `redeem` can only be called for once for each `queueHash`.  As a result, the staking incentives of the duplicate target will be ignored.

## Proof of Concept
When there is not enough tokens in the contract, it will go to the `else` clause:
```solidity
else {
                // Hash of target + amount + batchNonce
                bytes32 queueHash = keccak256(abi.encode(target, amount, batchNonce));
                // Queue the hash for further redeem
                stakingQueueingNonces[queueHash] = true;

                emit StakingRequestQueued(queueHash, target, amount, batchNonce, localPaused);
            }
```
https://github.com/code-423n4/2024-05-olas/blob/main/tokenomics/contracts/staking/DefaultTargetDispenserL2.sol#L189-L202

If the `target` is duplicate, the `queueHash` will be the same.

In `redeem` function, the `stakingQueueingNonces[queueHash]` will be set to false:
```solidity
if (olasBalance >= amount) {
            // Approve and transfer OLAS to the service staking target
            IToken(olas).approve(target, amount);
            IStaking(target).deposit(amount);

            emit StakingTargetDeposited(target, amount);

            // Remove processed queued nonce
            stakingQueueingNonces[queueHash] = false;
        } 
```
https://github.com/code-423n4/2024-05-olas/blob/main/tokenomics/contracts/staking/DefaultTargetDispenserL2.sol#L288-L297


if `stakingQueueingNonces[queueHash]` is set to false, the `redeem` function can not be called again:
```solidity
bool queued = stakingQueueingNonces[queueHash];
        // Check if the target and amount are queued
        if (!queued) {
            revert TargetAmountNotQueued(target, amount, batchNonce);
        }

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

## Tools Used
Manual review

## Recommended Mitigation Steps
Consider using nonce for each target, for example:
```solidity
// Traverse all the targets
        for (uint256 i = 0; i < targets.length; ++i) {
            uint256 batchNonce = stakingBatchNonce;
            address target = targets[i];
            uint256 amount = amounts[i];

            // Check the tar
`VoteWeighting` Users cannot vote for nominees during the last week of their lock period
Audit: 2024-05-olas-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact

Users who have locked their OLAS tokens cannot vote during the last voting period (i.e. last week) of their lock period due to an incorrect check in the `VoteWeighting` contract.

Despite having available voting power for the entire last voting period, a strict inequality check prevents users from voting. For example, if a user locks their OLAS for 1 week, they lose the ability to vote for the entire lock period. If they lock it for 2 weeks, they lose the ability to vote for 50% of their lock period, and so on.

As a result of this issue, users miss out on the opportunity to vote, potentially affecting critical decisions and reducing the effectiveness of their locked tokens. Additionally, their OLAS tokens remain locked and illiquid during a period in which they cannot vote.

## Proof of Concept

The issue lies in the `voteForNomineeWeights()` function of the `VoteWeighting` contract, specifically in the following check:

```sol
        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);
        }
```

This code calculates the start time of the next voting period (`nextTime`) and checks that the user's lock end time (`lockEnd`) is strictly less than `nextTime`.

However, as shown in the `_createLockFor()` function in `veOLAS`, all unlock times are rounded down to weeks:

```sol
        // Lock time is rounded down to weeks
        // Cannot practically overflow because block.timestamp + unlockTime (max 4 years) << 2^64-1
        unchecked {
            unlockTime = ((block.timestamp + unlockTime) / WEEK) * WEEK;
        }
```

Due to this rounding down, the user's `lockEnd` will be equal to `nextTime`, causing the voting function to revert and preventing the user from voting during their last nominee voting period.

### Example Scenario

1. Alice locks her OLAS tokens in `veOLAS` for 2 weeks.
2. A week later, Alice attempts to vote since at this point she has 50% of her voting power (due to decay).

**Expected:** Alice should be able to successfully cast her vote for nominees.

**Actual:** Alice's transaction is reverted. Due to the above-mentioned issue, she cannot cast her vote, while her tokens remain locked and illiquid.

### Coded PoC

The following is a coded PoC that demonstrates the issue. Add the following test case to `VoteWeighting.js` in the `Voting` context:

```js
        it.only("User loses half of their voting power", async function () {
            // Add a nominee
            let nominee = signers[1].address;
            await vw.addNomineeEVM(nominee, chainId);
            nominee = convertAddressToBytes32(nominee);
WRONG? StakingBase::_evict() overwrites the wrong index, leading to wrong serice being evicted
Audit: 2024-05-olas-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
Detailed description of the impact of this finding.

## Proof of Concept
`serviceIds` is retrieved from [StakingBase::L600-L603](https://github.com/code-423n4/2024-05-olas/blob/main/registries/contracts/staking/StakingBase.sol#L600-L603)
```solidity
        (uint256 lastAvailableRewards, uint256 numServices, uint256 totalRewards,
            uint256[] memory eligibleServiceIds, uint256[] memory eligibleServiceRewards,
>           uint256[] memory serviceIds, uint256[][] memory serviceNonces,
            uint256[] memory serviceInactivity) = _calculateStakingRewards();
```
It is a direct copy of `setServiceIds`:
[StakingBase::L522-L550](https://github.com/code-423n4/2024-05-olas/blob/main/registries/contracts/staking/StakingBase.sol#L522-L550)
```solidity
            for (uint256 i = 0; i < size; ++i) {
                // Get current service Id
>               serviceIds[i] = setServiceIds[i];
```

```solidity
        if (serviceIds.length > 0) {
            uint256 eCounter = epochCounter;
            numServices = 0;
            // Record service inactivities and updated current service nonces
            for (uint256 i = 0; i < serviceIds.length; ++i) {
                // Get the current service Id
>               curServiceId = serviceIds[i];
...SKIP...
                    if (serviceInactivity[i] > maxInactivityDuration) {
                        // Evict a service if it has been inactive for more than a maximum allowed inactivity time
>                       evictServiceIds[i] = curServiceId;
                        // Increase number of evicted services
                        numServices++;
```
It is then used to know which inactive `serviceIds` should be removed:
[StakingBase::_evict()](https://github.com/code-423n4/2024-05-olas/blob/main/registries/contracts/staking/StakingBase.sol#L449-L473)
```solidity
    function _evict(
        uint256[] memory evictServiceIds,
        uint256[] memory serviceInactivity,
        uint256 numEvictServices
    ) internal {
        // Get the total number of staked services
        // All the passed arrays have the length of the number of staked services
        uint256 totalNumServices = evictServiceIds.length;

        // Get arrays of exact sizes
        uint256[] memory serviceIds = new uint256[](numEvictServices);
        address[] memory owners = new address[](numEvictServices);
        address[] memory multisigs = new address[](numEvictServices);
        uint256[] memory inactivity = new uint256[](numEvictServices);
        uint256[] memory serviceIndexes = new uint256[](numEvictServices);

        // Fill in arrays
        uint256 sCounter;
        uint256 serviceId;
        for (uint256 i = 0; i < totalNumServices; ++i) {
            if (evictServiceIds[i] > 0) {
                serviceId = evictServiceIds[i];
                serviceId