xanoutas/solidity-security-findings huggingface dataset:
Incorrect ID assignment in `removeNominee` when the last nominee is removed.
Audit: 2024-05-olas-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact

Contract : voteWeighting 
When the last item in the `setNominees` array is removed via the `removeNominee` function, 
there is an accounting error in the `mapNomineeIds` mapping,
wherein the removed Nominee gets reassigned the same ID, instead of 0.

This same ID is assigned to the subsequent nominee that gets added.
Resulting in 2 Nominees sharing the same ID.

This incorrect Nominee ID Tracking results in unexpected outcomes
in the following functions relying on `mapNomineeIds` mapping.
- `getNextAllowedVotingTimes()`
- `getNomineeId()`
- `removeNominee()`
- `_getWeight()`

## Proof of Concept

Contract : VoteWeighting

1. Assume the following initial state

```
setNominees = [0, Nominee1, Nominee2, Nominee3]

mapNomineeIds:
mapNomineeIds[hash(nominee1)] = 1
mapNomineeIds[hash(nominee2)] = 2
mapNomineeIds[hash(nominee3)] = 3
```

2. Owner removes Nominee3, the last item in the  `setNominees ` array.

```
uint256 beforeRemovalId =   VoteWeightingContract.getNomineeId(Nominee3Addr, 1);
vm.prank(owner);
VoteWeightingContract.removeNominee(Nominee3Addr, 1);
uint256 afterRemovalId = VoteWeightingContract.getNomineeId(Nominee3Addr, 1);
assertEq(beforeRemovalId , afterRemovalId);
```
3. This results in, 

- `Nominee3` still having the same ID (3), instead of being set to 0.

- The same ID (3) is assigned to the next Nominee (say Nominee4) that gets added as well.

## Tools Used

Manual Review, Foundry

## Recommended Mitigation Steps

One potential solution is to move the `mapNomineeIds[nomineeHash] = 0` to the end.

```
        // 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();
        
        // Remove nominee from the map
        mapNomineeIds[nomineeHash] = 0;
```


## Assessed type

Other
Wormhole uses Dispenser contract address as a refund address on l2 chain, resulting in a loss of funds
Audit: 2024-05-olas-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact

Wormhole has a refund mechanism that refunds on the destination chain instead of the source as the `refundChain` passed by default is L2, but `WormholeDepositProcessorL1::_sendMessage` passes `msg.sender` as a refunder, resulting in all the refunds being sent to the wrong recipient.

## Proof of Concept

In case no explicit `refundAccount` has been provided to the `bridgePayload` `msg.sender` , which is the `Dispenser` contract, will be used by default. The problem is that since `Dispenser` is a smart contract, it won’t have the same address on the L2 chains where the refund will happen. This will result in all the ever-refunded tokens being sent to a random recipient on the L2 chain when the refund happens.

```solidity
/// @inheritdoc DefaultDepositProcessorL1
  function _sendMessage(
      address[] memory targets,
      uint256[] memory stakingIncentives,
      bytes memory bridgePayload,
      uint256 transferAmount
  ) internal override returns (uint256 sequence) {
...MORE CODE
      // If refundAccount is zero, default to msg.sender
      if (refundAccount == address(0)) {
          refundAccount = msg.sender;
      }

      sequence = sendTokenWithPayloadToEvm(
          uint16(wormholeTargetChainId), 
          l2TargetDispenser, 
          data, 
          0,
          gasLimitMessage, 
          olas,//REVIEW this should be set to the address of Olas on the destination chain
          transferAmount, 
          uint16(l2TargetChainId), 
          refundAccount);//@audit wrong address
  }
```

Refunds in Wormhole are for the unused gas on the destination chain execution. We say that refunds will be happening often as the  `gasLimitMessage` is an arbitrary parameter provided from the caller as well, although having better validation.

## Tools Used

Manual Review

## Recommended Mitigation Steps

Revert the transaction in case `address(0)` is passed as `refundAddress` to the `bridgePayload`.


## Assessed type

Token-Transfer
Service owner can fail to receive reward when `stakingToken` is a fee-on-transfer token
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#L115-L128
https://github.com/code-423n4/2024-05-olas/blob/main/registries/contracts/staking/StakingBase.sol#L482-L511
https://github.com/code-423n4/2024-05-olas/blob/main/registries/contracts/staking/StakingBase.sol#L805-L872
https://github.com/code-423n4/2024-05-olas/blob/main/registries/contracts/staking/StakingToken.sol#L104-L111


# Vulnerability details

## Impact
According to https://github.com/code-423n4/2024-05-olas?tab=readme-ov-file#erc20-token-behaviors-in-scope, fee-on-transfer tokens are in scope and should be supported by this protocol. When this kind of token is `stakingToken`, calling the following `deposit` function to deposit funds for staking would increase `balance` and `availableRewards` by `amount` but the balance of such token held by the corresponding `StakingToken` contract would be increased by a value that equals such `amount` minus the fee of such token transfer. In this situation, the corresponding `StakingToken` contract's balance of such token is less than `balance` or `availableRewards`.

https://github.com/code-423n4/2024-05-olas/blob/main/registries/contracts/staking/StakingToken.sol#L115-L128
```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);
        ...
    }
```

After the first deposit, both `balance` and `availableRewards` are the same so it is possible that the reward accumulates to equal such `availableRewards`, which also equals such `balance`. When the accumulated reward equals `balance`, calling the following `_claim` or `unstake` function, which further calls the `_withdraw` function below, would reduce `balance` to 0 and attempt to transfer such accumulated reward from the corresponding `StakingToken` contract to the service owner. However, because the `StakingToken` contract's balance of such fee-on-transfer token is less than `balance`, such transfer reverts. As a result, the service owner fails to receive the reward that is entitled to her or him.

https://github.com/code-423n4/2024-05-olas/blob/main/registries/contracts/staking/StakingBase.sol#L482-L511
```solidity
    function _claim(uint256 serviceId, bool execCheckPoint) internal returns (uint256 reward) {
        ServiceInfo storage sInfo = mapServiceInfo[serviceId];
        ...
        // Get the claimed service data
        reward = sInfo.reward;
        ...
        // Transfer accumulated rewards to the service multisig
        // Note that the reentrancy is not possible since the
checkpoint function is not called before staking which can cause loss of rewards for already staked services.
Audit: 2024-05-olas-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
Before allowing anyone to stake a service id checkpoint function should be  called in order to distribute already present rewards in the contract and thus update the availableRewards because if this doesn't happen and a new service id is staked then after some time the rewards are then shared among the new service id as well thus causing the service ids which were staked before the new service id to receive less rewards. Further as there is no mention who calls checkpoint function and when is it called likelihood of this error is high and impact is loss of rewards thus impact is also high.

## Proof of Concept
Following is stake function 
```solidity
function stake(uint256 serviceId) external {
        // Check if there available rewards
        if (availableRewards == 0) {
            revert NoRewardsAvailable();
        }

        // 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);
        }

        // Check for the maximum number of staking services
        uint256 numStakingServices = setServiceIds.length;
        if (numStakingServices == maxNumServices) {
            revert MaxNumServicesReached(maxNumServices);
        }

        // Check the service conditions for staking
        IService.Service memory service = IService(serviceRegistry).getService(serviceId);

        // Check the number of agent instances
        if (numAgentInstances != service.maxNumAgentInstances) {
            revert WrongServiceConfiguration(serviceId);
        }

        // Check the configuration hash, if applicable
        if (configHash != 0 && configHash != service.configHash) {
            revert WrongServiceConfiguration(serviceId);
        }
        // Check the threshold, if applicable
        if (threshold > 0 && threshold != service.threshold) {
            revert WrongServiceConfiguration(serviceId);
        }
        // The service must be deployed
        if (service.state != IService.ServiceState.Deployed) {
            revert WrongServiceState(uint256(service.state), serviceId);
        }

        // Check that the multisig address corresponds to the authorized multisig proxy bytecode hash
        bytes32 multisigProxyHash = keccak256(service.multisig.code);
        if (proxyHash != multisigProxyHash) {
            revert UnauthorizedMultisig(service.multisig);
        }

        // Check the agent Ids requirement, if applicable
        uint256 size = agentIds.length;
        if (size > 0) {
            uint256 numAgents = service.agentIds.length;

            if (size != numAgents) {
                re
Excess token refund spent is not refunded back to senders in multiple places
Audit: 2024-05-olas-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-05-olas/blob/3ce502ec8b475885b90668e617f3983cea3ae29f/tokenomics/contracts/staking/WormholeTargetDispenserL2.sol#L115-L117
https://github.com/code-423n4/2024-05-olas/blob/3ce502ec8b475885b90668e617f3983cea3ae29f/tokenomics/contracts/staking/OptimismTargetDispenserL2.sol#L71-L74
https://github.com/code-423n4/2024-05-olas/blob/3ce502ec8b475885b90668e617f3983cea3ae29f/tokenomics/contracts/staking/OptimismDepositProcessorL1.sol#L131-L133
https://github.com/code-423n4/2024-05-olas/blob/3ce502ec8b475885b90668e617f3983cea3ae29f/tokenomics/contracts/staking/ArbitrumDepositProcessorL1.sol#L168-L170


# Vulnerability details


## Impact

Excess native tokens sent when sending messages on various depositProcessors and targetDispensers is not refunded to the sender leading to loss of funds.

## Proof of Concept
Looking through the various `_sendMessage` functions 

In WormholeTargetDispenserL2.sol

```solidity
    function _sendMessage(uint256 amount, bytes memory bridgePayload) internal override {
    ......
        // Check that provided msg.value is enough to cover the cost
        if (cost > msg.value) {
            revert LowerThan(msg.value, cost);
        }

        // Send the message to L1
        uint64 sequence = IBridge(l2MessageRelayer).sendPayloadToEvm{value: cost}(uint16(l1SourceChainId),
            l1DepositProcessor, abi.encode(amount), 0, gasLimitMessage, uint16(l1SourceChainId), refundAccount);

        emit MessagePosted(sequence, msg.sender, l1DepositProcessor, amount);
    }
```
In ArbitrumDepositProcessorL1.sol

```solidity
    function _sendMessage(
        address[] memory targets,
        uint256[] memory stakingIncentives,
        bytes memory bridgePayload,
        uint256 transferAmount
    ) internal override returns (uint256 sequence) {
        .....
        // Get the total cost
        uint256 totalCost = cost[0] + cost[1];

        // Check fot msg.value to cover the total cost
        if (totalCost > msg.value) {
            revert LowerThan(msg.value, totalCost);
        }

        if (transferAmount > 0) {
            // Approve tokens for the bridge contract
            IToken(olas).approve(l1ERC20Gateway, transferAmount);

            // Construct the data for IBridge consisting of 2 pieces:
            // uint256 maxSubmissionCost: Max gas deducted from user's L2 balance to cover base submission fee
            // bytes memory extraData: empty data
            bytes memory submissionCostData = abi.encode(maxSubmissionCostToken, "");

            // Transfer OLAS to the staking dispenser contract across the bridge
            IBridge(l1TokenRelayer).outboundTransferCustomRefund{value: cost[0]}(olas, refundAccount,
                l2TargetDispenser, transferAmount, TOKEN_GAS_LIMIT, gasPriceBid, submissionCostData);
        }

        // Assemble message data payload
        bytes memory data = abi.encodeWithSelector(RECEIVE_MESSAGE, abi.encode(targets, stakingIncentive
Incorrect callValueRefundAddress in createRetryableTicket function leads to cross-chain message not delivered
Audit: 2024-05-olas-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-05-olas/blob/3ce502ec8b475885b90668e617f3983cea3ae29f/tokenomics/contracts/staking/ArbitrumDepositProcessorL1.sol#L189-L191


# Vulnerability details

# **Vulnerability Detail**

The `createRetryableTicket` delivers/propagate the Information to the Arbitrum L2 about targets, stakingIncentives. However, incorrect `callValueRefundAddress` is set, which could result in the information not to be delivered and lost.

According to the Technical documentation we see that during the `createRetryableTicket` following params should hold:

1. `excessFeeRefundAddress`: The L2 address to which the excess fee is
credited. This is going to be set as the Alias Timelock address on Arbitrum
2. `callValueRefundAddress`: The L2 address to which the l2CallValue is
credited if the ticket times out or gets cancelled. This is going to be set as
the Alias Timelock address on Arbitrum

However, both these params are controlled by the user, who initiated the tx. These params are encoded in the bridgePayload and right before cross-chain transfer they are decoded to be passed into the the `createRetryableTicket` function. Let’s take a look

```solidity
// Decode the staking contract supplemental payload required for bridging tokens
        (address refundAccount, uint256 gasPriceBid, uint256 maxSubmissionCostToken, uint256 gasLimitMessage,
            uint256 maxSubmissionCostMessage) = abi.decode(bridgePayload, (address, uint256, uint256, uint256, uint256));

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

So, from the bridgePayload, the refundAccount could be any address, if it is set to address(0) , it is set to msg.sender.

According to the Arbitrum docs, the `callValueRefundAddress` (which is set to any arbitrary address in our case) got a critical permission to cancel the ticket. 

# **Proof of Concept**

1. Malicious user calls `claimStakingIncentives` fn in the Dispenser.sol with malicious address encoded into the `bridgePayload` parameter.
2. calculation of staking incentives happens + Refund returned amount back to tokenomics inflation
3. The internal fn `_distributeStakingIncentives` is called which will execute the cross-chain tx.
4. Following the tx flow we reach the ArbitrumDepositProcessorL1, the _sendMessage function. At this step the `bridgePayload` is decoded with malicious `refundAccount`
5. `createRetryableTicket` function doesn’t differentiate between `excessFeeRefundAddress` and `callValueRefundAddress` and simply use refundAccount for both.
    
    ```solidity
    sequence = IBridge(l1MessageRelayer).createRetryableTicket{value: cost[1]}(l2TargetDispenser, 0,
                maxSubmissionCostMessage, refundAccount, refundAccount, gasLimitMessage, gasPriceBid, data);
    ```
    
6. Once the ticket is created, the malicious “`refundAccount`” manually cancel the ticket and on Arbitrum the **Tick
StakingBase.sol can’t handle fee-on-transfer token
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#L115-L125


# Vulnerability details

# **Vulnerability Detail**

Olas Protocol allows to utilise “fee-on-transfer” (PAXG, STA) tokens as stakingToken. However, the deposit function isn’t implemented in such way, so it could handle the “fee-on-transfer” token.

```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;
        //@audit
        // Add to the overall balance
        SafeTransferLib.safeTransferFrom(stakingToken, msg.sender, address(this), amount);

        emit Deposit(msg.sender, amount, newBalance, newAvailableRewards);
    }
```

# **Proof of Concept**

1. Assume user deposit 10 PAXG tokens. The `newBalance` is incremented with passed amount exactly, while in reality the `safeTransferFrom` amount will be less. Let’s assume that 9.5 PAXG tokens will be transferred. It means, that `availableRewards` will be incremented incorrectly 
2. When the owner of the StakingBase contract claim the reward, the rewards will be based on the total `availableRewards` . It means reward calculation will be based on the 10 PAXG tokens instead of actual amount transferred which is 9.5 PAXG
3. The `claim` function, as well as `unstake` will fail, because it will try to `_withdraw`  more than the contract hold.

# **Impact**

Owner can’t `claim` / `unstake`  the rewards.

# **Recommendation**

In `deposit` function , add only the actual transferred amounts (computed by PostTransferBalance - PreTransferBalance


## Assessed type

ERC20
Dispenser is unable to use excess gas refund for Arbitrum bridging 
Audit: 2024-05-olas-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-05-olas/blob/3ce502ec8b475885b90668e617f3983cea3ae29f/tokenomics/contracts/staking/ArbitrumDepositProcessorL1.sol#L134-L137


# Vulnerability details

Relevant code: [ArbitrumDepositProcessorL1::\_sendMessage](https://github.com/code-423n4/2024-05-olas/blob/3ce502ec8b475885b90668e617f3983cea3ae29f/tokenomics/contracts/staking/ArbitrumDepositProcessorL1.sol#L134-L137)
### 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
address stakingTargetEVM = address(uint160(uint256(stakingTarget)));
IDepositProcessor(depositProcessor).sendMessage{value:msg.value}(stakingTargetEVM, stakingIncentive,
        bridgePayload, transferAmount);
```

In every `IDepositProcessor`, the only contract able to call `sendMessage` is the `Dispenser`:
```javascript
if (msg.sender != l1Dispenser) {
	revert ManagerOnly(l1Dispenser, msg.sender);
}
```

When bridging funds to Arbitrum, one of the parameters encoded in the `bridgePayload` is `refundAccount`. This refund address is going to receive all of the excess gas that was sent during bridging. Here's the Arbitrum natspec about the bridging function:
```javascript
/**
 * @notice Deposit ERC20 token from Ethereum into Arbitrum. If L2 side hasn't been deployed yet, includes name/symbol/decimals data for initial L2 deploy. Initiate by GatewayRouter.
 * @dev L2 address alias will not be applied to the following types of addresses on L1:
 *      - an externally-owned account
 *      - a contract in construction
 *      - an address where a contract will be created
 *      - an address where a contract lived, but was destroyed
 * @param _l1Token L1 address of ERC20
 * @param _refundTo Account, or its L2 alias if it have code in L1, to be credited with excess gas refund in L2
 * @param _to Account to be credited with the tokens in the L2 (can be the user's L2 account or a contract), not subject to L2 aliasing
              This account, or its L2 alias if it have code in L1, will also be able to cancel the retryable ticket and receive callvalue refund
 * @param _amount Token Amount
 * @param _maxGas Max gas deducted from user's L2 balance to cover L2 execution
 * @param _gasPriceBid Gas price for L2 execution
 * @param _data encoded data from router and user
 * @return res abi encoded inbox sequence number
 */
//  * @param maxSubmissionCost Max gas deducted from user's L2 balance to cover base submission fee
function outboundTransferCustomRefund(
    address _l1Token,
    address _refundTo,
    address _to,
    uint256 _amount,
    uint256 _maxGas,
    uint256 _gasPriceBid,
    bytes calldata _data
) public payable virtual override returns (bytes memory res)
```


In case such address is not given in the payload, a default address is