xanoutas/solidity-security-findings huggingface dataset:
Users with expired locks who haven't withdrawn their tokens will receive the same benefits as if their tokens are locked
Audit: 2024-05-munchables-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-05-munchables/blob/57dff486c3cd905f21b330c2157fe23da2a4807d/src/managers/LockManager.sol#L461-L487
https://github.com/code-423n4/2024-05-munchables/blob/57dff486c3cd905f21b330c2157fe23da2a4807d/src/managers/LockManager.sol#L349-L371
https://github.com/code-423n4/2024-05-munchables/blob/57dff486c3cd905f21b330c2157fe23da2a4807d/src/managers/LockManager.sol#L410-L411


# Vulnerability details

## Impact
Users whose lock has expired but they're still holding/keeping the tokens in the LockManager will receive the same benefits as if their tokens are locked.

This opens up the possibility for malicious users to take advantage of this mechanics and game the system by locking their tokens for the minimum amount of time, while collecting all of the rewards and being able to unlock/withdraw their tokens at any time.

## Proof of Concept
There are two ways in which users are rewarded for locking their tokens inside the LockManager: 

- The first one is whenever there is a lockdrop event, the amount of tokens which is being locked will equal a certain amount of NFTs:

```
 if (lockdrop.start <= uint32(block.timestamp) && lockdrop.end >= uint32(block.timestamp)) {
            if (
                _lockDuration < lockdrop.minLockDuration
                    || _lockDuration > uint32(configStorage.getUint(StorageKey.MaxLockDuration))
            ) revert InvalidLockDurationError();
            if (msg.sender != address(migrationManager)) {

                remainder = quantity % configuredToken.nftCost;
                numberNFTs = (quantity - remainder) / configuredToken.nftCost;

                if (numberNFTs > type(uint16).max) revert TooManyNFTsError();

                // Tell nftOverlord that the player has new unopened Munchables
                nftOverlord.addReveal(_lockRecipient, uint16(numberNFTs));
```
- The second one is related to collecting Schnibbles. The calculation on how many Schnibbles a user has collected is based on two things, the `getLockedWeightedValue()` function, and the the time between `block.timestamp` and the last harvest of said Schnibbles (either the user manually harvesting it by calling the function `harvest()` on the accounting manager, or automatically doing so when locking/unlocking tokens by `forceHarvest()` being called).

If there is a lockdrop event, the amount of NFTs that a user will receive is calculated immediately when the lock is made and `addReveal()` on the NFT Overlord is called. 

During or outside of lock events, users can stake so that they accrue daily Schnibbles, which is another form of reward. As explained above this reward is dependent on the `getLockedWeightedValue()` and the time from the last harvest of said Schnibbles.

If we take a look at the `getLockedWeightedValue()`:

```
   function getLockedWeightedValue(address _player) external view returns (uint256 _lockedWeightedValue) {
        uint256 lockedWeighted = 0;
        uint256 configuredToke
Incorrect state change in `_lock` function allows user to steal tokens.
Audit: 2024-05-munchables-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-05-munchables/blob/main/src/managers/LockManager.sol#L380


# Vulnerability details

The `lock` function is responsible for locking the user's tokens for a particular `_duration`. However, the `_lock` function adds the previous `remainder` each time when a user locks the token again to his provided `quantity`.

```javascript
uint256 quantity = _quantity + lockedToken.remainder;
```

And also adds whole provided `quantity` to user to `unlock` at a particular `unlockTime`.

```javascript
lockedToken.quantity += _quantity;
```

So the problem is here that a user can `unlock` the entire provided `quantity` after first `lock`, along with the previous `remainder` amount, during their second locking action.


## Impact
As much as `configuredToken.nftCost` grows user will be able to steal more tokens according to below statement:

The highest remainder can be calculated as:
`nftCost - 1`

## Proof of Concept
Scenario:

1) Let's say the user locks `199` tokens which nftCost = `100`.
2) Now remainder will be `99` according to this:

   ```javascript
   remainder = quantity % configuredToken.nftCost;
   ```

3) So, due to the below statements in the `_lock` function, that users are able to unlock all the provided tokens, 
   after the `unlockTime` finishes.

   ```javascript
   lockedToken.quantity += _quantity
   ```

4) User unlocks `199` when the `unlockTime` elapsed.
5) After that, the user again locks `1` more token, and since the `remainder` is now `99`, the `quantity` will be  
   `100` according to the below statement.

   ```javascript
     uint256 quantity = _quantity + lockedToken.remainder;
   ```

6) He Waits until the `unlockTime` finishes, call the `unlock` function again, and gather `100` more tokens.
7) In this way, the user drains `99` tokens.

## Tools Used
Manual review.

## Recommended Mitigation Steps
Consider implement this on `unlock` function.

```diff
+   lockedToken.quantity += _quantity - remainder;
```



## Assessed type

Other
Players ETH token could be locked forever in the contract
Audit: 2024-05-munchables-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-05-munchables/blob/main/src/managers/LockManager.sol#L420


# Vulnerability details

## Description
This has been reported by [4naly3er](https://github.com/code-423n4/2024-05-munchables/blob/main/4naly3er-report.md#M-2), but the impact would be more severe then Medium, which is why I still submit this report providing richer details about the issue.

Whenever a player wants to get their locked token back they will call `LockManager::unlock`. The current implementation present a flaw for players who locked `native ETH token`, as the contract will send those token back using `payable.transfer` function which restrict the operation with a limited amount of gas (Stipend currently at [2300](
https://github.com/blast-io/blast/blob/master/blast-geth/params/protocol_params.go#L53)). 

Interacting with a smart contract is a little more expensive than an EOA, so if the locked token owner is a smart contract, there is always a chance in the future that if gas cost change on `Blast` which could make this operation to revert, preventing the player to unlock their ETH token which would be `stuck forever` (for all the players who locked ETH) in the contract, which seems to warrant `High` severity.

This is usuallly a Medium severity issue, but the fact that token would be stuck forever in the contract, which is a `user funds loss`, seems to justify the High severity.

## Impacts
Player unable to unlock their ETH token if gas cost for `opCall` changes unfavorably

## Recommended Mitigation Steps
Use `call` (and confirm the return flag) instead of `transfer` to send the ETH token back to the player. Reentrancy isn't a concern since the contract is already using `nonReentrant` modifier.

```diff
        if (_tokenContract == address(0)) {
-           payable(msg.sender).transfer(_quantity);
+           // solhint-disable-next-line avoid-low-level-calls
+           (bool sent, ) = msg.sender.call{value: _quantity}("");
+           require(sent, "Transfer failed");
        }
```











## Assessed type

ETH-Transfer
Use of payable.transfer and inability to specify a recipient will cause tokens to be locked forever if unlocker is a contract or multisig wallet
Audit: 2024-05-munchables-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-05-munchables/blob/57dff486c3cd905f21b330c2157fe23da2a4807d/src/managers/LockManager.sol#L420


# Vulnerability details

## Impact

The `unlock` function allows users to unlock only for themselves to themselves i.e the unlocked tokens are on `msg.sender`'s account and transferred to `msg.sender`. This is very important as when trying to unlock their tokens, the `.transfer` method is used if `tokenContract` is address(0). The `transfer` method only forwards 2300 gas, which is not enough for the recipient to execute any non-trivial logic in a receive() or fallback function. If the locker is a contract or a multisig wallet, the gas forwarded is not going to be enough, causing that the `unlock` function will always revert. Now, since there's no option to provide a recipient, the user will permanently not be able to withdraw his tokens.

## Proof of Concept

Compared to the [4naly3er report](https://github.com/code-423n4/2024-05-munchables/blob/57dff486c3cd905f21b330c2157fe23da2a4807d/4naly3er-report.md#m-2-call-should-be-used-instead-of-transfer-on-an-address-payable) which only discusses how the call may fail, here in this case, there is a notable absence of the ability to specify a recipient when unlocking which means a user can only claim for himself. This makes the severity much higher as whenever the user calls the `unlock` function from his contract account like a multisig or smart contract wallet that has a `receive` function requiring >2300 gas, the call will always fail, and as there's no way to specify a different recipient, the tokens will be locked in the the contract forever, leading to loss of funds.


```solidity
    function unlock(
        address _tokenContract,
        uint256 _quantity
    ) external notPaused nonReentrant {
        LockedToken storage lockedToken = lockedTokens[msg.sender][
            _tokenContract
        ];
        if (lockedToken.quantity < _quantity)
            revert InsufficientLockAmountError();
        if (lockedToken.unlockTime > uint32(block.timestamp))
            revert TokenStillLockedError();

        // force harvest to make sure that they get the schnibbles that they are entitled to
        accountManager.forceHarvest(msg.sender);

        lockedToken.quantity -= _quantity;

        // send token
        if (_tokenContract == address(0)) {
            payable(msg.sender).transfer(_quantity);
        } else {
            IERC20 token = IERC20(_tokenContract);
            token.transfer(msg.sender, _quantity);
        }

        emit Unlocked(msg.sender, _tokenContract, _quantity);
    }

```

## Tools Used
Manual code review

## Recommended Mitigation Steps

Use `call` instead of `transfer` to send ETH instead as it forwards all available gas and allows contract recipients to execute arbitrary logic.
```solidity
        if (_tokenContract == address(0)) {
            payable(msg.sender).transfer(_quantity);
```
Or introduce the ability to speci
If the user executes the lock function not during the lockdrop period, the retainer may be lost.
Audit: 2024-05-munchables-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-05-munchables/blob/57dff486c3cd905f21b330c2157fe23da2a4807d/src/managers/LockManager.sol#L345
https://github.com/code-423n4/2024-05-munchables/blob/57dff486c3cd905f21b330c2157fe23da2a4807d/src/managers/LockManager.sol#L379


# Vulnerability details

## Impact
Users may lose part of the remainder funds.

## Proof of Concept
remainder represents the non-divisible part of the user's pledge, and quantity represents the divisible part of the user's pledge.

If the user locks during the lockdrop, and the remainder is not equal to 0. After the lockdrop ends, and the user locks again, [the contract will directly set the remainder to 0, and will not add the previous remainder to the quantity](https://github.com/code-423n4/2024-05-munchables/blob/57dff486c3cd905f21b330c2157fe23da2a4807d/src/managers/LockManager.sol#L379-L380). In this way, the user loses the remainder of locks.

```solidity
        uint256 quantity = _quantity + lockedToken.remainder;
        uint256 remainder;
···
        lockedToken.remainder = remainder;
        lockedToken.quantity += _quantity;
```

## Tools Used
manual

## Recommended Mitigation Steps
Remainder should be calculated correctly even if addReveal is not called.





## Assessed type

Math
Users can miss out on some rewards
Audit: 2024-05-munchables-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-05-munchables/blob/57dff486c3cd905f21b330c2157fe23da2a4807d/src/managers/LockManager.sol#L311-L398


# Vulnerability details

## Impact
Users can receive less rewards than they were entitled to during a lockdrop event because of wrong code structure.

## Proof of Concept
When a user calls `lock` during a lockdrop event the amount they deposit is used in order to mint a certain number of NFTs and all of the unused funds are saved in `remainder` in order to be used on the next transaction.

If the user wants to increase their lock amount not during a lockdrop event the variable `remainder` is set to 0 even if it held funds before. The variable is declared and is left with its default value of 0 and is only changed during a lockdrop event. Then it is used to set the user's actual `remainder` `lockedToken.remainder = remainder;`

https://github.com/code-423n4/2024-05-munchables/blob/57dff486c3cd905f21b330c2157fe23da2a4807d/src/managers/LockManager.sol#L344-L379

After consulting with a member of the contract's team they confirmed that this behavior is unintended and `remainder` is supposed to remain the same when there is no lockdrop event.

The issue is also not that uncommon since anyone can add 0 funds to your lock by calling `lockOnBehalf` and reset your `remainder` value back to 0 



## Tools Used
Manual review


## Recommended Mitigation Steps
set remainder to `uint256 remainder = lockedToken.remainder` to ensure that the value remains.





## Assessed type

Context
User can lose his remainder for future lockdrop events
Audit: 2024-05-munchables-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-05-munchables/blob/main/src/managers/LockManager.sol#L345-L379


# Vulnerability details

In `LockManager.sol` users can lock their tokens at any point. However if there is a lockdrop event, depending on the amount of tokens they lock they will get a number of nft reveals:

```solidity
        if (lockdrop.start <= uint32(block.timestamp) &&
            lockdrop.end >= uint32(block.timestamp)) {
            if (
                _lockDuration < lockdrop.minLockDuration ||
                _lockDuration >
                uint32(configStorage.getUint(StorageKey.MaxLockDuration))
            ) revert InvalidLockDurationError();
            if (msg.sender != address(migrationManager)) {
                // calculate number of nfts
                remainder = quantity % configuredToken.nftCost;
                numberNFTs = (quantity - remainder) / configuredToken.nftCost;

                if (numberNFTs > type(uint16).max) revert TooManyNFTsError();

                // Tell nftOverlord that the player has new unopened Munchables
                nftOverlord.addReveal(_lockRecipient, uint16(numberNFTs));
            }
        }
```
As you can see from the code snippet we get a remainder from the modulo operation between the quantity of tokens we lock and the cost of a single nft:
```solidity
remainder = quantity % configuredToken.nftCost;
```
This remainder is added to future locks:
```solidity
uint256 quantity = _quantity + lockedToken.remainder;
```

The issue is that if the user locks tokens when there is no lockdrop event this remainder will get reset to 0. He won't be able to use it for future lockdrop events so he just loses funds.


## Impact
Loss of funds for the user.

## Proof of Concept
```solidity
    function _lock(address _tokenContract, uint256 _quantity, address 
    _tokenOwner, address _lockRecipient) private {
...
        uint256 remainder; //default value = 0
...
        if (lockdrop.start <= uint32(block.timestamp) &&
            lockdrop.end >= uint32(block.timestamp)) {
...
                remainder = quantity % configuredToken.nftCost;
...
            }
        }
...
        lockedToken.remainder = remainder;
```
As you can see if there is no lockdrop event we don't enter the if statement so the remainder variable doesn't get updated to `quantity % configuredToken.nftCost` which means it keeps its default value of 0. After that `lockedToken.remainder` will get updated to zero in the last line of the code snippet.

Here is an example of how an user can lose his funds:
1. Alice decides to lock 2005e18 DAI
2. The NFT associated with this token costs 1000e18 DAI
3. Alice gets 2 NFT reveals and a remainder of 5e18 DAI which gets stored in `lockedToken.remainder`
4. After the lockdrop event has ended alice decides to lock 995e18 DAI but because of this her `lockedToken.remainder = 5e18` gets reset do 0
5. A new lockdrop event starts and she deposits 1000e18 more DAI and expects to get 2
`lockedToken.reminder` of a Previous Lock Resets to 0 If User/Attacker Locks Funds Between Two Lockdrop Events
Audit: 2024-05-munchables-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-05-munchables/blob/57dff486c3cd905f21b330c2157fe23da2a4807d/src/managers/LockManager.sol#L379


# Vulnerability details

The `LockManager` contract has a mechanism to retain the remaining funds after calculating the number of NFTs during a lockdrop event. This reminder is used to calculate the number of NFTs in the next lock, ensuring the maximum utilization of funds to generate Munchables. However, if a user locks some funds between two lockdrops, the reminder resets to 0, making it ineffective for creating Munchables in the next lockdrop event. Additionally, a malicious user can exploit this vulnerability to reset the reminders of other users through the `lockOnBehalf()` function.

## Impact
This vulnerability allows an attacker to delete the reminders of any users after a lockdrop event, thereby preventing them from using their remaining funds effectively in the next lockdrop event.

## Proof of Concept
The vulnerability lies in the `_lock()` function. At line 344, the function uses the reminder from the previous lock, and at line 345, it resets the `reminder` to 0. However, if there is no active lockdrop event at this moment, the `quantity` is not used even though `lockedToken.remainder` is reset to 0 at line 379.

```solidity
343:    // add remainder from any previous lock
344:    uint256 quantity = _quantity + lockedToken.remainder;
345:    uint256 remainder;
346:    uint256 numberNFTs;
347:    uint32 _lockDuration = playerSettings[_lockRecipient].lockDuration;
348:
349:    if (_lockDuration == 0) {
350:        _lockDuration = lockdrop.minLockDuration;
351:    }
352:    if (
353:        lockdrop.start <= uint32(block.timestamp) &&
354:        lockdrop.end >= uint32(block.timestamp)
355:    ) {
.           // --- disabling code snippet ----
361:        if (msg.sender != address(migrationManager)) {
362:            // calculate number of nfts
363:            remainder = quantity % configuredToken.nftCost;
364:            numberNFTs = (quantity - remainder) / configuredToken.nftCost;
.               // --- disabling code snippet ----
370:        }
371:    }
.       // --- disabling code snippet ----
.
379:    lockedToken.remainder = remainder;
```
https://github.com/code-423n4/2024-05-munchables/blob/57dff486c3cd905f21b330c2157fe23da2a4807d/src/managers/LockManager.sol#L379

The previous `reminder` of a lock is intended to be used in the next lock, even between two distinct lockdrop events. However, if there is a lock between two lockdrops, the previous `reminder` is removed. This can be exploited by malicious users to remove reminders of other users after each lockdrop event.

## Tools Used
Manual Review

## Recommended Mitigation Steps
Update the lockedToken.remainder only if there is an active lockdrop event.

Sample Code Snippet:

```diff
    function _lock(
        address _tokenContract,
        uint256 _quantity,
        address _tokenOwner,
        address _lockRecipient
    ) private
When a player calls the lock() method outside the lockdrop timeframe, he/she loses the remainder amount
Audit: 2024-05-munchables-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-05-munchables/blob/main/src/managers/LockManager.sol#L345
https://github.com/code-423n4/2024-05-munchables/blob/main/src/managers/LockManager.sol#L352-L355
https://github.com/code-423n4/2024-05-munchables/blob/main/src/managers/LockManager.sol#L379


# Vulnerability details

## Impact
According to Documentation, 

```
the lockdrop period refers to a specific timeframe 
during which participants can lock up their tokens 
to receive rewards and benefits.
```

However, the [_lock() method](https://github.com/code-423n4/2024-05-munchables/blob/main/src/managers/LockManager.sol#L311-L398) can still be successfully called outside the lockdrop timeframe. In this case, the value of `remainder` is set to 0, leading to a loss of the player's funds.

## Proof of Concept
In line [LockManager.sol#L345](https://github.com/code-423n4/2024-05-munchables/blob/main/src/managers/LockManager.sol#L345) the local variable remainder is not initialized.
When the `_lock() method` is called outside the lockdrop timeframe, the lines [LockManager.sol#L352-L371](https://github.com/code-423n4/2024-05-munchables/blob/main/src/managers/LockManager.sol#L352-L371) are not executed. This is the right behavior because a player shouldn't mint a munchable outside the lockdrop timeframe. However, in this case, the value of `remainder` is not updated, and that variable remains uninitialized.

In line [LockManager.sol#L379](https://github.com/code-423n4/2024-05-munchables/blob/main/src/managers/LockManager.sol#L379) that variable is used to update the `lockedToken.remainder` value:

```
LockManager.sol#L379

379  lockedToken.remainder = remainder;
```

In case there was a remainder that the player accrued during the last `lockdrop` period, he/she will entirely lose it. We want to stress this: because the `lockdrop.start` and the `lockdrop.end` values are `uint32`, a player could wrongly call the `lock()` method outside the `lockdrop` timeframe just because his/her transaction has been processed 1 second later than the end of `lockdrop` period. So, we consider this an unwanted behavior that leads to losses of player's funds. 

## Tools Used
Visual inspection. Due to the lack of time and the simplicity of the issue, we don't report a Coded PoC.
However, if it is needed, we will provide it.

## Recommended Mitigation Steps

```diff
LockManager.sol#L352-L377

    if (
      lockdrop.start <= uint32(block.timestamp) &&
      lockdrop.end >= uint32(block.timestamp)
    ) {
      if (
        _lockDuration < lockdrop.minLockDuration ||
        _lockDuration >
        uint32(configStorage.getUint(StorageKey.MaxLockDuration))
      ) revert InvalidLockDurationError();
      if (msg.sender != address(migrationManager)) {
        // calculate number of nfts
        remainder = quantity % configuredToken.nftCost;
        numberNFTs = (quantity - remainder) / configuredToken.nftCost;


        if (numberNFTs > type(uint16).max) revert TooManyNFTsEr