xanoutas/solidity-security-findings huggingface dataset:
Any UniStaker fees accruing when no one is staking would be lost
Audit: 2024-02-uniswap-foundation-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-02-uniswap-foundation/blob/5a2761c8277541a24bc551fbd624413b384bea94/src/UniStaker.sol#L755


# Vulnerability details

## Description

When fees are added to the UniStaker, `notifyRewardAmount()` is called.
```
function notifyRewardAmount(uint256 _amount) external {
  if (!isRewardNotifier[msg.sender]) revert UniStaker__Unauthorized("not notifier", msg.sender);
  // We checkpoint the accumulator without updating the timestamp at which it was updated, because
  // that second operation will be done after updating the reward rate.
  rewardPerTokenAccumulatedCheckpoint = rewardPerTokenAccumulated();
  if (block.timestamp >= rewardEndTime) {
    scaledRewardRate = (_amount * SCALE_FACTOR) / REWARD_DURATION;
  } else {
    uint256 _remainingReward = scaledRewardRate * (rewardEndTime - block.timestamp);
    scaledRewardRate = (_remainingReward + _amount * SCALE_FACTOR) / REWARD_DURATION;
  }
  rewardEndTime = block.timestamp + REWARD_DURATION;
  lastCheckpointTime = block.timestamp;
```

It sets `scaledRewardRate` so that the total pending fees would be split uniformally over the next 30 days. 
`rewardEndTime` is set to `now + 30 days`, while `lastCheckpointTime = now`

Suppose there's no stakers when the first rewards are sent to the UniStaker, for example since yield is zero right now. Some time passes and the first staker joins. All interactions with beneficiaries including `stake()` run the following line to update global rewards:
```
_checkpointGlobalReward();
```
Which is:
```
/// @notice Checkpoints the global reward per token accumulator.
function _checkpointGlobalReward() internal {
  rewardPerTokenAccumulatedCheckpoint = rewardPerTokenAccumulated();
  lastCheckpointTime = lastTimeRewardDistributed();
}
```

The first line updates the total unlocked reward/token:
```
function rewardPerTokenAccumulated() public view returns (uint256) {
  if (totalStaked == 0) return rewardPerTokenAccumulatedCheckpoint;
  return rewardPerTokenAccumulatedCheckpoint
    + (scaledRewardRate * (lastTimeRewardDistributed() - lastCheckpointTime)) / totalStaked;
}
```
In case `totalStaked` is zero, it returns the original checkpoint without accounting for the new rewards.

The second line updates `lastCheckpointTime`:
```
function lastTimeRewardDistributed() public view returns (uint256) {
  if (rewardEndTime <= block.timestamp) return rewardEndTime;
  else return block.timestamp;
}
```
Note that it would update regardless if `totalStaked==0`. Therefore, it would store the current timestamp in `lastCheckpointTime`.

At this point, the tokens weren't distributed, but `lastCheckpointTime` is updated. So in the future, the accumulation of tokens in `rewardPerTokenAccumulated()` will never include the days when there weren't any stakers. 

This results in loss of fees which are permanently stuck in the contract, whenver there are no stakers. That could be at any part of the contract's lifetime.

## Impact

Permanent freeze
Callers of claimFees() will lose value due to unavoidable race condition
Audit: 2024-02-uniswap-foundation-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-02-uniswap-foundation/blob/5a2761c8277541a24bc551fbd624413b384bea94/src/V3FactoryOwner.sol#L181


# Vulnerability details

## Description

The new fee claiming method relies on bots / searchers to call `claimFees()` when it becomes profitable. Callers pass `amount0Requested`, `amount1Requested`, and are guaranteed to receive those amounts in fees, in exchange for the global `payoutAmount`.

Note that admin is expected to adjust the `payoutAmount` using the function below:
```
/// @notice Update the payout amount to a new value. Must be called by admin.
/// @param _newPayoutAmount The value that will be the new payout amount.
function setPayoutAmount(uint256 _newPayoutAmount) external {
  _revertIfNotAdmin();
  if (_newPayoutAmount == 0) revert V3FactoryOwner__InvalidPayoutAmount();
  emit PayoutAmountSet(payoutAmount, _newPayoutAmount);
  payoutAmount = _newPayoutAmount;
}
```

Consider the facts below:
- Searchers are independent entities and don't know when `payoutAmount` will change.
- There are thousands of Pools which `claimFees()` can be called on at any moment.
- The admin doesn't know if and when searchers will call `claimFees()`

As a result, there is a critical race condition when `payoutAmount` is adjusted upwards. The searcher intended to trade:
`(amount0 of token0) + (amount1 of token1) > (payoutAmount of payoutToken)`
However when the admin call is executed first, this will result in:
`(amount0 of token0) + (amount1 of token1) < (newPayoutAmount of payoutToken)`

This will result in a loss of value for a searcher that at moment of TX generation is not able to anticipate the change in costs.

## Impact

Callers of claimFees() will lose value due to unavoidable race condition

## Proof of Concept

1. `claimFees()` just became profitable
2. Searcher sends a TX to `claimFees()`
3. Meanwhile, admin updates `payoutAmount` to a higher value
4. The `claimFees()` function executes whilst losing value for the trader.

## Tools Used

Manual audit

## Recommended Mitigation Steps

Consider adding a `maxPayoutAmount` argument to `claimFees()`. If `payoutAmount` at runtime is higher than `maxPayoutAmount`, revert the TX.


## Assessed type

Timing
Loss of yield in a large variety of scenarios
Audit: 2024-02-uniswap-foundation-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-02-uniswap-foundation/blob/5a2761c8277541a24bc551fbd624413b384bea94/src/V3FactoryOwner.sol#L181


# Vulnerability details

## Description

The new fee collection design allows fees to be claimed only through `claimFees()` on the FactoryOwner. The function requires the caller to pay a set `payoutAmount` of WETH and in exchange for all fees accrued in the pool. The new design opens an option for fees to be essentially locked in the Pool. Consider the simple case:

- WETH/TOK pair has generated $400 in fees
- The `payoutAmount` is set to $700
- WETH/TOK is no longer being traded for any reason (e.g. no liquidity because LPs moved to a higher yield pool, TOK rugged, people lost interest in the project, etc)

At this point, one of the following two outcomes will happen:
- Someone (perhaps a UNI staker) will take a bullet for the team and call `claimFees()`. They will pay $700 and receive $400 in tokens, losing $300. UNI stakers gain $300.
- No one calls `claimFees()`. UNI stakers lose access to the $400 in fees trapped in the Pool.

Note that `setPayoutAmount()` could theoretically be used to manually lower the payout amount to $400 and then a user can safely call `claimFees()`. However, the idea collapses because the payout is shared between all Pools in existence. If fees are reduced to $400, a Pool that currently stores $600 in fees will introduce a $200 arbitrage opportunity, which will be directly lost by UNI stakers. A script could be written to drop `payoutAmount` in small increments to decrease the arb opportunities, then bump it back to the original. However a bit of napkin math shows that gas for such a large amount of TXs, plus all the extra yield that will be lost by the `payoutAmount` decrease, will translate to many thousands of dollars lost.

## Impact

Loss of yield in a large variety of scenarios.

## Proof of Concept

- WETH/TOK pair has generated $400 in fees
- The `payoutAmount` is set to $700
- WETH/TOK is no longer being traded for any reason (e.g. no liquidity because LPs moved to a higher yield pool, TOK rugged, people lost interest in the project, etc)
- There is no incentive to call `claimFees()`. 
- UNI stakers lose access to the $400 in fees trapped in the Pool.

## Tools Used

Manual audit

## Recommended Mitigation Steps

Owner (Governance) should be able to perform an OTC trade with the pool fees. They will pay the same value of WETH and receive the fees in the Pool.


## Assessed type

Other
Exploitable Delegation in Stake Function
Audit: 2024-02-uniswap-foundation-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-02-uniswap-foundation/blob/5a2761c8277541a24bc551fbd624413b384bea94/src/UniStaker.sol#L256
https://github.com/code-423n4/2024-02-uniswap-foundation/blob/5a2761c8277541a24bc551fbd624413b384bea94/src/UniStaker.sol#L271


# Vulnerability details

## Impact
The current implementation of the stake function allows an attacker to exploit the delegation mechanism by making multiple small deposits to the same delegatee address. Since the voting power is determined by the delegatee address rather than the deposited amount, an attacker can potentially hijack the protocol governance by spreading small deposits across multiple addresses but directing them to the same delegatee. This could lead to centralized control of governance by the attacker or their designated delegatee, undermining the decentralization and fairness of the governance process.

## Proof of Concept
```javascript   
//@audit no-bound no how much a depositor can deposit
function stake(uint256 _amount, address _delegatee, address _beneficiary)
    external
    returns (DepositIdentifier _depositId)
  {
    _depositId = _stake(msg.sender, _amount, _delegatee, _beneficiary);
  }
 function stake(uint256 _amount, address _delegatee)
    external
    returns (DepositIdentifier _depositId)
  {
    _depositId = _stake(msg.sender, _amount, _delegatee, msg.sender);
  }
```

## Tools Used
Manual review

## Recommended Mitigation Steps
Consider implementing a voting power calculation that takes into account the deposited amount rather than solely relying on the delegatee address.


## Assessed type

Governance
Non-Compliance with EIP-2612 in permitAndStake Function Limits Delegated Staking Flexibility
Audit: 2024-02-uniswap-foundation-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-02-uniswap-foundation/blob/main/src/UniStaker.sol#L292


# Vulnerability details

## Impact
The `permitAndStake` function uses the `STAKE_TOKEN.permit` method to allow staking with a permit. However, the function might not be fully leveraging the permit functionality if it implicitly assumes that the `msg.sender` (the entity calling `permitAndStake`) is the same as the permit signer (the token owner). This assumption could inadvertently restrict the functionality, making it so that only the permit signer can initiate the staking, rather than enabling any party to stake on behalf of the permit signer, provided they have a valid permit.

## Proof of Concept
```solidity
function permitAndStake(
    uint256 _amount,
    address _delegatee,
    address _beneficiary,
    uint256 _deadline,
    uint8 _v,
    bytes32 _r,
    bytes32 _s
) external returns (DepositIdentifier _depositId) {
    STAKE_TOKEN.permit(msg.sender, address(this), _amount, _deadline, _v, _r, _s);
    _depositId = _stake(msg.sender, _amount, _delegatee, _beneficiary);
}
```

## Tools Used
Manual Review

## Recommended Mitigation Steps

- Modify the `permitAndStake` function to ensure that it allows any caller to perform the staking operation on behalf of the token owner, as long as they possess a valid permit. This involves verifying the permit signature against the token owner (the signer) and not necessarily the `msg.sender`.

- Ensure that the `permit` validation checks that the `_owner` specified in the permit parameters is the same as the signer of the permit. This validation confirms that the staking action is indeed authorized by the rightful token owner.


## Assessed type

Access Control
In function _stakeMore the earningPower[deposit.beneficiary] is incorrectly updated by adding only _amount
Audit: 2024-02-uniswap-foundation-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-02-uniswap-foundation/blob/main/src/UniStaker.sol#L679


# Vulnerability details

## Impact
The earningPower[deposit.beneficiary] is incorrectly updated by adding _amount directly to it without considering the unclaimedRewards. 

## Proof of Concept

earningPower[deposit.beneficiary] += _amount;
This result in an incorrect calculation of rewards for the beneficiary.

## Manual review

## Recommended Mitigation Steps
Update the earningPower[deposit.beneficiary] by adding the accrued rewards along with the _amount being staked.

uint256 totalAmount = _amount + unclaimedReward(deposit.beneficiary);
earningPower[deposit.beneficiary] += totalAmount;


## Assessed type

Token-Transfer
Incorrect Reward Calculation in rewardPerTokenAccumulated function
Audit: 2024-02-uniswap-foundation-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-02-uniswap-foundation/blob/main/src/UniStaker.sol#L229


# Vulnerability details

## Impact
There is a problem in the way the `rewardPerTokenAccumulated` function is calculated, which is used to determine the global reward accumulation per token.

## Proof of Concept
This calculation assumes that the `totalStaked` amount remains constant between checkpoints. However, in reality, the `totalStaked` amount can change as stakers deposit or withdraw their stakes.

```solidity
function rewardPerTokenAccumulated() public view returns (uint256) {
    if (totalStaked == 0) return rewardPerTokenAccumulatedCheckpoint;

    return rewardPerTokenAccumulatedCheckpoint
      + (scaledRewardRate * (lastTimeRewardDistributed() - lastCheckpointTime)) / totalStaked;
}
```

If the `totalStaked` amount changes between checkpoints, the reward accumulation calculation will be incorrect. Specifically:

1. If `totalStaked` increases due to new stakes, the calculated reward accumulation will be lower than it should be, resulting in underpayment of rewards to existing stakers.

2. If `totalStaked` decreases due to withdrawals, the calculated reward accumulation will be higher than it should be, resulting in overpayment of rewards to remaining stakers.


## Tools Used
Manual Review

## Recommended Mitigation Steps

The contract should track the changes in `totalStaked` between checkpoints and adjust the reward accumulation calculation accordingly.

One possible approach is to introduce an additional variable that tracks the sum of the staked balances multiplied by the time they were staked. This variable would be updated whenever stakes are deposited, withdrawn, or rewards are checkpointed.

Then, the `rewardPerTokenAccumulated` function would be modified to use this variable, along with the current `totalStaked`, to calculate the correct reward accumulation.


## Assessed type

Context
Unclaimed Reward is not calculated properly.
Audit: 2024-02-uniswap-foundation-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-02-uniswap-foundation/blob/5a2761c8277541a24bc551fbd624413b384bea94/src/UniStaker.sol#L755


# Vulnerability details

## Impact
Unclaimed Rewards is not calculated properly.

## Proof of Concept
``_checkpointGlobalReward()`` function is called upon every action like ``stake``, ``withdraw``, ``claim``, etc.

The problem is, with every ``_checkpointGlobalReward()`` function call, ``lastCheckpointTime`` value is also updated to the value of ``lastTimeRewardDistributed()``. 
```solidity
./Unistaker.sol

  function _checkpointGlobalReward() internal {
    rewardPerTokenAccumulatedCheckpoint = rewardPerTokenAccumulated();
    lastCheckpointTime = lastTimeRewardDistributed();
  }
```

With every ``_checkpointGlobalReward()``, ``_checkpointReward()`` is also called in the sequence throughout the contract. Now, let's look at the ``_checkpointReward()`` function:
```solidity
./Unistaker.sol

  function _checkpointReward(address _beneficiary) internal {
    unclaimedRewardCheckpoint[_beneficiary] = unclaimedReward(_beneficiary);
    beneficiaryRewardPerTokenCheckpoint[_beneficiary] = rewardPerTokenAccumulatedCheckpoint;
  }
```

It then calls the ``unclaimedReward()`` function for the beneficiary for ``unclaimedRewardCheckpoint[_beneficiary]`` value. In the ``unclaimedReward()`` function, 
```solidity
./Unistaker.sol

  function unclaimedReward(address _beneficiary) public view returns (uint256) {
    return unclaimedRewardCheckpoint[_beneficiary]
      + (
        earningPower[_beneficiary]
          * (rewardPerTokenAccumulated() - beneficiaryRewardPerTokenCheckpoint[_beneficiary])
      ) / SCALE_FACTOR;
  }
```

Now, the main issue comes here. Let's take a look at the ``rewardPerTokenAccumulated()`` function that is being called inside the ``unclaimedReward()`` function.

```solidity
./Unistaker.sol

  function rewardPerTokenAccumulated() public view returns (uint256) {
    if (totalStaked == 0) return rewardPerTokenAccumulatedCheckpoint;

    return rewardPerTokenAccumulatedCheckpoint
      + (scaledRewardRate * (lastTimeRewardDistributed() - lastCheckpointTime)) / totalStaked;
  }
```

This uses ``lastTimeRewardDistributed() - lastCheckpointTime``. Here, at the beginning of this transaction sequence, ``lastCheckpointTime`` was set to the value of ``lastTimeRewardDistributed()``. Now, It is being subtracted again. Thus, cancelling out each other.

 Thus, the ``rewardPerTokenAccumulated`` when called in sequence of ``_checkpointGlobalReward()`` and ``_checkpointReward()`` will always amount to the value calculated after new rewards have been notified and ``lastCheckpointTime`` and ``lastTimeRewardDistributed()`` differ. Meaning, ``UnclaimedReward()`` will also never update the value.

PoC:
When ``notifyRewardAmount()`` function is called to notify rewards, ``lastCheckpointTime`` gets updated to current ``block.timestamp`` and ``rewardEndTime`` is set to ``block.timestamp + REWARD_DURATION``.

Let's ass
Rewards may get locked in contract if the time between reward notifcations exceeds `REWARD_DURATION`.
Audit: 2024-02-uniswap-foundation-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-02-uniswap-foundation/blob/main/src/UniStaker.sol#L570-L582


# Vulnerability details

## Impact
When the time between two reward notifications exceeds the `REWARD_DURATION`, the `scaledRewardRate` is not updated correctly. The reward of the last notification is locked in the `UniStaker.sol` contract, and will not be able be claimed by user.

## Proof of Concept
In function `notifyRewardAmount`, if `block.timestamp >= rewardEndTime` is true, it means the time duration between two reward notifications is greater than or equal to `REWARD_DURATION`. In such condition, `scaledRewardRate` is only relelated to current notified `_amount`. But the reward of the last `REWARD_DURATION` is not added to the `scaledRewardRate` and that part of reward will not be claimed by user.
```solidity
570:  function notifyRewardAmount(uint256 _amount) external {
571:    if (!isRewardNotifier[msg.sender]) revert UniStaker__Unauthorized("not notifier", msg.sender);
572:
573:    // We checkpoint the accumulator without updating the timestamp at which it was updated, because
574:    // that second operation will be done after updating the reward rate.
575:    rewardPerTokenAccumulatedCheckpoint = rewardPerTokenAccumulated();
576:
577:    if (block.timestamp >= rewardEndTime) {
578:->    scaledRewardRate = (_amount * SCALE_FACTOR) / REWARD_DURATION;
579:    } else {
580:      uint256 _remainingReward = scaledRewardRate * (rewardEndTime - block.timestamp); 
581:      scaledRewardRate = (_remainingReward + _amount * SCALE_FACTOR) / REWARD_DURATION;
582:    }
```
https://github.com/code-423n4/2024-02-uniswap-foundation/blob/main/src/UniStaker.sol#L570-L582

## Tools Used
Manual Review

## Recommended Mitigation Steps
Add a state variable `lastAmount` to track the last notified reward amount, and fix the code as follows.
```solidity
570:  function notifyRewardAmount(uint256 _amount) external {
571:    if (!isRewardNotifier[msg.sender]) revert UniStaker__Unauthorized("not notifier", msg.sender);
572:
573:    // We checkpoint the accumulator without updating the timestamp at which it was updated, because
574:    // that second operation will be done after updating the reward rate.
575:    rewardPerTokenAccumulatedCheckpoint = rewardPerTokenAccumulated();
576:
577:    if (block.timestamp >= rewardEndTime) {
578:-     scaledRewardRate = (_amount * SCALE_FACTOR) / REWARD_DURATION;
578:+     scaledRewardRate = (lastAmount + _amount) * SCALE_FACTOR / REWARD_DURATION;
579:    } else {
580:      uint256 _remainingReward = scaledRewardRate * (rewardEndTime - block.timestamp); 
581:      scaledRewardRate = (_remainingReward + _amount * SCALE_FACTOR) / REWARD_DURATION;
582:    }
583:+   lastAmount = _amount;



## Assessed type

Math
There is an error in the unistaker.sol contract in the _useDepositId function.
Audit: 2024-02-uniswap-foundation-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-02-uniswap-foundation/blob/main/src/UniStaker.sol#L630


# Vulnerability details

## Impact
In this function, _depositId is assigned the value of nextDepositId, and then nextDepositId is incremented by 1. However, the function is missing a return statement, causing a compilation error.

## Proof of Concept
function _useDepositId() internal returns (DepositIdentifier _depositId) {
    _depositId = nextDepositId;
    nextDepositId = DepositIdentifier.wrap(DepositIdentifier.unwrap(_depositId) + 1);
}

## Manual review

## function _useDepositId() internal returns (DepositIdentifier _depositId) {
    _depositId = nextDepositId;
    nextDepositId = DepositIdentifier.wrap(DepositIdentifier.unwrap(_depositId) + 1);
   + return _depositId;
}


## Assessed type

Error