xanoutas/solidity-security-findings huggingface dataset:
Staking contract can distribute rewards to multisig without activity
Audit: 2024-05-olas-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-05-olas/blob/e2a8bc31d2769bfb578a06cc64919ad369a82c08/registries/contracts/staking/StakingFactory.sol#L168-L216
https://github.com/code-423n4/2024-05-olas/blob/e2a8bc31d2769bfb578a06cc64919ad369a82c08/registries/contracts/staking/StakingBase.sol#L278-L361
https://github.com/code-423n4/2024-05-olas/blob/3ce502ec8b475885b90668e617f3983cea3ae29f/registries/contracts/staking/StakingBase.sol#L406-L422


# Vulnerability details

Relevant code: [StakingFactory::createStakingInstance](https://github.com/code-423n4/2024-05-olas/blob/e2a8bc31d2769bfb578a06cc64919ad369a82c08/registries/contracts/staking/StakingFactory.sol#L168-L216), [StakingBase::\_initialize](https://github.com/code-423n4/2024-05-olas/blob/e2a8bc31d2769bfb578a06cc64919ad369a82c08/registries/contracts/staking/StakingBase.sol#L278-L361), [StakingBase::\_checkRatioPass](https://github.com/code-423n4/2024-05-olas/blob/3ce502ec8b475885b90668e617f3983cea3ae29f/registries/contracts/staking/StakingBase.sol#L406-L422)

### Description
A `Staking` contract can be created permissionlessly by using the `StakingFactory`:
```javascript
function createStakingInstance(
    address implementation,
    bytes memory initPayload
) external returns (address payable instance)
```
The first parameter is a whitelisted staking implementation and the second one encodes a function call to an `initialize()` function containing `StakingParams`.

One of the vital parameters for the proper work of the `Staking` contract is `activityChecker`. It's responsibility is to make sure the staked service has been active throughout the last `epoch`. 
```javascript
// Get the ratio pass activity check
activityData = abi.encodeCall(IActivityChecker.isRatioPass, (currentNonces, lastNonces, ts));
(success, returnData) = activityChecker.staticcall(activityData);
```

While other params are checked for their validity, `activityChecker` can be arbitrarily set by the caller. This opens up the possibility for a malicious actor to give their own implementation of `activityChecker` that can behave in any way they like. Since all the validity checks will pass, the newly created `Staking` contract can be voted for and used in the protocol.

### Root Cause
Lack of validation for the `activityChecker` address when creating `Staking` contracts.

### Impact
A malicious actor can **permissionlessly** create a `Staking` contract in which certain multisigs can **always** pass the activity check. Thus, allowing them to collect **rewards** without engaging in any blockchain activity. This **breaks the core protocol invariant** for paying only services that do real work.

### PoC
The following PoC demonstrates how a malicious `ActivityChecker` contract can be inserted in `StakingParam` and manipulate the `Staking` contract to issue rewards, even though no transactions were done by the `multisig`.

Place the following contract at `contracts/test/MaliciousActivityChecker.sol`:
<details>

```javascript
`pointsSum.slope` Not Updated After Nominee Removal and Votes Revocation
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-L637
https://github.com/code-423n4/2024-05-olas/blob/main/governance/contracts/VoteWeighting.sol#L223-L249


# Vulnerability details

## Issue Description

The `removeNominee` and `revokeRemovedNomineeVotingPower` functions are part of the governance mechanism in the `VoteWeighting.sol` smart contract.

- The `removeNominee` function removes a nominee from the voting system.
- The `revokeRemovedNomineeVotingPower` function revokes the voting power that a user has delegated to a nominee who has been removed.

When a user votes for a nominee using the `voteForNomineeWeights` function, both `pointsSum.slope` and `pointsSum.bias` are updated to reflect the new voting weights:

```solidity
pointsWeight[nomineeHash][nextTime].bias = _maxAndSub(_getWeight(account, chainId) + newBias, oldBias);
pointsSum[nextTime].bias = _maxAndSub(_getSum() + newBias, oldBias);
if (oldSlope.end > nextTime) {
    pointsWeight[nomineeHash][nextTime].slope =
        _maxAndSub(pointsWeight[nomineeHash][nextTime].slope + newSlope.slope, oldSlope.slope);
    pointsSum[nextTime].slope = _maxAndSub(pointsSum[nextTime].slope + newSlope.slope, oldSlope.slope);
} else {
    pointsWeight[nomineeHash][nextTime].slope += newSlope.slope;
    pointsSum[nextTime].slope += newSlope.slope;
}
```

However, when a nominee is removed by the owner through the `removeNominee` function, only `pointsSum.bias` is updated. `pointsSum.slope` is not updated, nor is it updated when users revoke their votes through the `revokeRemovedNomineeVotingPower` function. As a result, the `pointsSum.slope` for the next timestamp will be incorrect as it still includes the removed nominee's slope.

When the `_getSum()` function is called later to perform a checkpoint, the incorrect slope will be used for the calculation, causing the voting weight logic to become inaccurate:

```solidity
function _getSum() internal returns (uint256) {
    // t is always > 0 as it is set in the constructor
    uint256 t = timeSum;
    Point memory pt = pointsSum[t];
    for (uint256 i = 0; i < MAX_NUM_WEEKS; i++) {
        if (t > block.timestamp) {
            break;
        }
        t += WEEK;
        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;
        }

        pointsSum[t] = pt;
        if (t > block.timestamp) {
            timeSum = t;
        }
    }
    return pt.bias;
}
```

## Impact

The voting weight logic will be inaccurate after a nominee is removed, potentially compromising the integrity of the governance mechanism.

## Tools Used

Manual review, VS Code

## Recommended Mitigation

To address this issue, ensure that `pointsSum.slope` is updated when a nominee is removed. This can be done in either the `removeNom
Arbitrary tokens and data can be bridged to `GnosisTargetDispenserL2` to manipulate staking incentives
Audit: 2024-05-olas-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-05-olas/blob/3ce502ec8b475885b90668e617f3983cea3ae29f/tokenomics/contracts/staking/GnosisTargetDispenserL2.sol#L99-L107


# Vulnerability details

The [`GnosisTargetDispenserL2`](https://github.com/code-423n4/2024-05-olas/blob/3ce502ec8b475885b90668e617f3983cea3ae29f/tokenomics/contracts/staking/GnosisTargetDispenserL2.sol) contract receives OLAS tokens and data from L1 to L2 via the Omnibridge, or just data via the AMB. When tokens are bridged, the `onTokenBridged()` callback is invoked on the contract. This callback processes the received tokens and associated data by calling the internal `_receiveMessage()` function.

However, the `onTokenBridged()` callback does not verify the sender of the message on the L1 side (or the token received).

This allows anyone to send any token to the `GnosisTargetDispenserL2` contract on L2 along with arbitrary data. The [`_receiveMessage()`](https://github.com/code-423n4/2024-05-olas/blob/3ce502ec8b475885b90668e617f3983cea3ae29f/tokenomics/contracts/staking/DefaultTargetDispenserL2.sol#L224) function in `DefaultTargetDispenserL2` will then process this data, assuming it represents valid staking targets and incentives.

## Impact
An attacker can bridge any tokens to the `GnosisTargetDispenserL2` contract on L2 with fake data for staking incentives. If the contract holds any withheld funds, these funds can be redistributed to arbitrary targets as long as they pass the [checks](https://github.com/code-423n4/2024-05-olas/blob/3ce502ec8b475885b90668e617f3983cea3ae29f/tokenomics/contracts/staking/DefaultTargetDispenserL2.sol#L160-L186) in `_processData()`. 

Even if the contract doesn't hold any withheld funds, the attacker can cause the amounts to be [stored](https://github.com/code-423n4/2024-05-olas/blob/3ce502ec8b475885b90668e617f3983cea3ae29f/tokenomics/contracts/staking/DefaultTargetDispenserL2.sol#L199) in `stakingQueueingNonces` and redeem them at a later point.

## Proof of Concept
1. Attacker calls `relayTokensAndCall()` on the Omnibridge on L1 to send any tokens to the `GnosisTargetDispenserL2` contract on L2
2. Attacker includes malicious staking data in the `payload` parameter
3. `onTokenBridged()` is called on `GnosisTargetDispenserL2`, which invokes `_receiveMessage()` to process the data
4. Since the L1 sender is not validated, the malicious data is accepted and fake staking incentives are distributed

## Tools Used
Manual review

## Recommended Mitigation Steps
The Omnibridge contracts do not seem to provide a way to access the original sender's address on L1 when [executing the receiver callback](https://github.com/omni/omnibridge/blob/master/contracts/upgradeable_contracts/BasicOmnibridge.sol#L471) upon receiving tokens. Hence the most sensible mitigation may be to send the bridged tokens and associated staking data separately:
1. When bridging tokens, only send the token amount without any data via `relayTokens()` 
2. Always transmit the staking
No limit on maximum number of `stakingTargets` for `claimStakingIncentivesBatch`
Audit: 2024-05-olas-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

There is no maximum limit for the number of `stakingTargets` for the permissionless `claimStakingIncentivesBatch` when sending the cross-chain transaction.

When the `claimStakingIncentivesBatch` data is received on the L2, the gas required grows linearly with the number of `stakingTargets` as seen in the `for` loop below

[DefaultTargetDispenserL2.sol#L154-L213](https://github.com/code-423n4/2024-05-olas/blob/main/tokenomics/contracts/staking/DefaultTargetDispenserL2.sol#L154-L213)
```solidity
    function _processData(bytes memory data) internal {
        ...
        // Traverse all the targets
        for (uint256 i = 0; i < targets.length; ++i) {
            ...
        }
        ...
    }
```

## Impact

Therefore, a user can purposely cause the cross-chain message to fail by putting in so many staking targets such that it exceeds the block gas limit in the L2, causing the cross-chain message to fail, because of a lack of a maximum limit of staking targets check in `claimStakingIncentivesBatch` - https://github.com/code-423n4/2024-05-olas/blob/main/tokenomics/contracts/Dispenser.sol#L1058


## Tools Used

Manual Review

## Recommended Mitigation Steps

Enforce a reasonable maximum limit for the number of staking targets that can be passed in `claimStakingIncentivesBatch`





## Assessed type

Other
Incorrect staking incentives can be sent to `GnosisTargetDispenserL2` and cause OLAS tokens to be allocated to incorrect staking targets
Audit: 2024-05-olas-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

`GnosisTargetDispenserL2.onTokenBridged` does not take steps to verify that the L1 deposit processor really sent the message when receiving a cross-chain token transfer in the `onTokenBridged` function.

[GnosisTargetDispenserL2.sol#L99-L107](https://github.com/code-423n4/2024-05-olas/blob/main/tokenomics/contracts/staking/GnosisTargetDispenserL2.sol#L99-L107)
```solidity
    function onTokenBridged(address, uint256, bytes calldata data) external {
        // Check for the message to come from the L2 token relayer
        if (msg.sender != l2TokenRelayer) {
            revert TargetRelayerOnly(msg.sender, l2TokenRelayer);
        }

        // Process the data
        _receiveMessage(l2MessageRelayer, l1DepositProcessor, data);
    }
```
It calls `_receiveMessage` which will process the `data`. Here the first parameter is set to the default `l1DepositProcessor` which means that the contract does not verify which address on the L1 performed the cross-chain transmission.

So a message with incorrect `stakingIncentives` can be sent by performing the same call on the `l1TokenRelayer` from a malicious contract.

[GnosisDepositProcessorL1.sol#L63-L71](https://github.com/code-423n4/2024-05-olas/blob/main/tokenomics/contracts/staking/GnosisDepositProcessorL1.sol#L63-L71)
```solidity
        // Transfer OLAS together with message, or just a message
        if (transferAmount > 0) {
            // Approve tokens for the bridge contract
            IToken(olas).approve(l1TokenRelayer, transferAmount);

            bytes memory data = abi.encode(targets, stakingIncentives);
            IBridge(l1TokenRelayer).relayTokensAndCall(olas, l2TargetDispenser, transferAmount, data);

            sequence = stakingBatchNonce;
        } 
```

## Impact

Incorrect staking incentives can be passed to the L2. This can cause the dispenser to allocate OLAS to the incorrect staking targets via `redeem`.

A malicious user for example, can call the `relayTokensAndCall` function with the incorrect staking target and staking incentive equal to the entire OLAS balance of the L2 Dispenser which will allow the `redeem` function on the L2 Dispenser to send its entire OLAS balance to the single staking target draining it of its funds.

## Tools Used

Manual

## Recommended Mitigation Steps

Perform the token transmission separately from the message transmission so that don't need to call `_receiveMessage` in `onTokenBridged`.

















## Assessed type

Other
Upgraded Q -> 2 from #115 [1720340805863]
Audit: 2024-05-olas-findings
Severity: MEDIUM

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

 ### [L-19] Some functions do not work correctly with fee-on-transfer tokens
Some tokens take a transfer fee (e.g. `STA`, `PAXG`), some do not currently charge a fee but may do so in the future (e.g. `USDT`, `USDC`).

Should a fee-on-transfer token be added as an asset and deposited, it could be abused, as the accounting is wrong. In the current implementation the following function calls do not work well with fee-on-transfer tokens as the amount variable is the pre-fee amount, including the fee, whereas the final balance do not include the fee anymore.

*There are <b>4</b> instances of this issue:*
```solidity
File: tokenomics/contracts/Dispenser.sol

420:             IToken(olas).transfer(depositProcessor, transferAmount);

456:                 IToken(olas).transfer(depositProcessor, transferAmounts[i]);

```
*Github:* [[420](https://github.com/code-423n4/2024-05-olas/blob/e2a8bc31d2769bfb578a06cc64919ad369a82c08/tokenomics/contracts/Dispenser.sol#L420), [456](https://github.com/code-423n4/2024-05-olas/blob/e2a8bc31d2769bfb578a06cc64919ad369a82c08/tokenomics/contracts/Dispenser.sol#L456)]


```solidity
File: tokenomics/contracts/staking/DefaultTargetDispenserL2.sol

443:             bool success = IToken(olas).transfer(newL2TargetDispenser, amount);

```
*Github:* [[443](https://github.com/code-423n4/2024-05-olas/blob/e2a8bc31d2769bfb578a06cc64919ad369a82c08/tokenomics/contracts/staking/DefaultTargetDispenserL2.sol#L443)]


```solidity
File: tokenomics/contracts/staking/EthereumDepositProcessor.sol

112:                 IToken(olas).transfer(timelock, refundAmount);

```
*Github:* [[112](https://github.com/code-423n4/2024-05-olas/blob/e2a8bc31d2769bfb578a06cc64919ad369a82c08/tokenomics/contracts/staking/EthereumDepositProcessor.sol#L112)]
Lack of support for specific 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#L108
https://github.com/code-423n4/2024-05-olas/blob/e2a8bc31d2769bfb578a06cc64919ad369a82c08/registries/contracts/staking/StakingToken.sol#L125


# Vulnerability details

## Impact
Olas is expected to support all kinds of ERC20 tokens, if one such tokens is fee-on-transfer[balances changes during transfer) tokens, user's may loose their expected funds.
Since, if the balance changed later, the returned value will be inaccurate. And the amount used before and after that specific operation might differ.

## Proof of Concept
Here is one such instances in StakingToken.sol in deposit & _withdraw
```
// Add to the overall balance
        SafeTransferLib.safeTransferFrom(stakingToken, msg.sender, address(this), amount);
```
```
        SafeTransferLib.safeTransfer(stakingToken, to, amount);
```
Users who deposit/ withdraw tokens will not see their exact amount deposited or withdrawn from the protocol

## Tools Used
ERC20 tokens

## Recommended Mitigation Steps
Compute the balance before and after transfer and subtract them to get the real amount.


## Assessed type

ERC20
Upgraded Q -> 2 from #114 [1720183356196]
Audit: 2024-05-olas-findings
Severity: MEDIUM

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

 ## [QA-17] Potential Precision Loss in `syncWithheldAmountMaintenance` Function Due to Normalization

### Impact
The normalization process for the `amount` parameter in the `syncWithheldAmountMaintenance` function can lead to a loss of precision. This can result in a smaller `amount` being stored in `mapChainIdWithheldAmounts` than intended, which might not accurately reflect the withheld amount that needs to be synced. 

### Proof of Concept
[Here's the concerned part:](https://github.com/code-423n4/2024-05-olas/blob/9b812d9d8cade3ecbd0043ae2fbd70a9d969b901/tokenomics/contracts/Dispenser.sol#L1219-L1225)

```solidity
if (bridgingDecimals < 18) {
    uint256 normalizedAmount = amount / (10 ** (18 - bridgingDecimals));
    normalizedAmount *= 10 ** (18 - bridgingDecimals);
    // Downsize staking incentive to a specified number of bridging decimals
    amount = normalizedAmount;
}
```

##### Example Scenario
Suppose `amount` is `1234567890123456789` and `bridgingDecimals` is `6`. The normalization process would be:

1. `normalizedAmount = amount / (10 ** (18 - 6)) = 1234567890123456789 / 10**12 = 1234567`
2. `normalizedAmount *= 10 ** (18 - 6) = 1234567 * 10**12 = 1234567000000000000`

The final `amount` after normalization is `1234567000000000000`, which is less than the original `1234567890123456789`. 

### Recommended Mitigation Steps:
Try to ensure that the `amount` is correctly rounded to the nearest value that can be represented with the specified `bridgingDecimals` before performing the normalization. This can be achieved by using a more precise method for normalization or adding a check to ensure that the `amount` is already in the correct format.
Upgraded Q -> 2 from #108 [1720182372513]
Audit: 2024-05-olas-findings
Severity: MEDIUM

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

 ## 5. Blocklisting tokens can leave rewards locked and unclaimable if multisig gets blocklisted

Links to affected code *

https://github.com/code-423n4/2024-05-olas/blob/3ce502ec8b475885b90668e617f3983cea3ae29f/registries/contracts/staking/StakingBase.sol#L508

https://github.com/code-423n4/2024-05-olas/blob/3ce502ec8b475885b90668e617f3983cea3ae29f/registries/contracts/staking/StakingToken.sol#L108

### Impact

The `_claim` function allows the owner to claim his tokens. The reward tokens are transferred to the services's multisig. SInce all sorts of ERC20 tokens are to be used, tokens that offer a blocklist can be used and will render the rewards unclaimable if the multisig is ever changed. 
```solidity
    function _claim(uint256 serviceId, bool execCheckPoint) internal returns (uint256 reward) {
        ServiceInfo storage sInfo = mapServiceInfo[serviceId];
        // Check for the service ownership
        if (msg.sender != sInfo.owner) {
            revert OwnerOnly(msg.sender, sInfo.owner);
        }

        // Call the checkpoint, if required
        if (execCheckPoint) {
            checkpoint();
        }

        // Get the claimed service data
        reward = sInfo.reward;

        // Check for the zero reward
        if (reward == 0) {
            revert ZeroValue();
        }

        // Zero the reward field
        sInfo.reward = 0;

        // Transfer accumulated rewards to the service multisig
        // Note that the reentrancy is not possible since the reward is set to zero
        address multisig = sInfo.multisig;
        _withdraw(multisig, reward);

        emit RewardClaimed(epochCounter, serviceId, msg.sender, multisig, sInfo.nonces, reward);
    }
```
### Recommended Mitigation Steps
Consider introducing a way for owners to change multisig addresses in their service struct.
Upgraded Q -> 2 from #108 [1720182273590]
Audit: 2024-05-olas-findings
Severity: MEDIUM

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

 ## 7. `syncWithheldAmount` doesn't normalize the amount to sync based on bridging decimals

Links to affected code *

https://github.com/code-423n4/2024-05-olas/blob/3ce502ec8b475885b90668e617f3983cea3ae29f/tokenomics/contracts/Dispenser.sol#L1174

### Impact

`syncWithheldAmount` is missing the downsizing of staking incentive to the specified number of bridging decimals which can lead to potential overinflation of the witheld amount for the chainid to be synced.

### Recommended Mitigation Steps

Introduce the normalizer check for bridging decimals.
```solidity
        if (bridgingDecimals < 18) {
            uint256 normalizedAmount = amount / (10 ** (18 - bridgingDecimals));
            normalizedAmount *= 10 ** (18 - bridgingDecimals);
            // Downsize staking incentive to a specified number of bridging decimals
            amount = normalizedAmount;
        }
```
Upgraded Q -> 2 from #7 [1720019193236]
Audit: 2024-05-olas-findings
Severity: MEDIUM

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

 ### 3. `syncWithheldAmount` needs to normalize according to bridging decimals.

The `withheldAmount` received from L2 -> L1 transaction can be unnormalized according to the bridging decimals. 

For example for the Wormhole bridge, if the `limitAmount` for a target on the L2 is not a multiple of 1e10, the `withheldAmount` sent from L2 -> L1 will also not be multiple of 1e10, so it should be normalized here:
```
    function syncWithheldAmount(uint256 chainId, uint256 amount) external {
        address depositProcessor = mapChainIdDepositProcessors[chainId];

        // Check L1 deposit processor address
        if (msg.sender != depositProcessor) {
            revert DepositProcessorOnly(msg.sender, depositProcessor);
        }

        // The overall amount is bound by the OLAS projected maximum amount for years to come
        uint256 withheldAmount = mapChainIdWithheldAmounts[chainId] + amount;
        if (withheldAmount > type(uint96).max) {
            revert Overflow(withheldAmount, type(uint96).max);
        }

        // Update the withheld amount
        mapChainIdWithheldAmounts[chainId] = withheldAmount;

        emit WithheldAmountSynced(chainId, amount, withheldAmount);
    }
```