xanoutas/solidity-security-findings huggingface dataset:
Overflows in 'farmPlots' function can leads to incorrect calculations and severe financial losses
Audit: 2024-07-munchables-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-07-munchables/blob/94cf468aaabf526b7a8319f7eba34014ccebe7b9/src/managers/LandManager.sol#L232-L310


# Vulnerability details

## Impact
The calculation of 'finalBonus' by summing values from the REALM_BONUSES and RARITY_BONUSES arrays risks an overflow if the total exceeds the int16 limit.

The operations in 'schibblesTotal', specifically the multiplication and addition, are susceptible to overflow, particularly when 'timestamp - _toiler.lastToilDate' is substantial.

Similarly, the 'schibblesLandlord' calculation is prone to overflow, especially if 'schnibblesTotal' is a large value.

The impact of overflow vulnerabilities in the 'farmPlots' function can lead to incorrect calculations and severe financial losses.

## Proof of Concept
```solidity
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.25;

import "forge-std/Test.sol";
import "../src/LandManager.sol";
import "../src/interfaces/ILockManager.sol";
import "../src/interfaces/IAccountManager.sol";
import "../src/interfaces/INFTAttributesManager.sol";
import "openzeppelin-contracts/contracts/token/ERC721/IERC721.sol";

contract LandManagerTest is Test {
    LandManager landManager;
    ILockManager lockManager;
    IAccountManager accountManager;
    IERC721 munchNFT;
    INFTAttributesManager nftAttributesManager;

    address configStorage = address(0x1234);
    address landlord = address(0x5678);
    address renter = address(0x9ABC);
    uint256 tokenId = 1;
    uint256 plotId = 0;

    function setUp() public {
        landManager = new LandManager();
        lockManager = ILockManager(address(new MockLockManager()));
        accountManager = IAccountManager(address(new MockAccountManager()));
        munchNFT = IERC721(address(new MockERC721()));
        nftAttributesManager = INFTAttributesManager(address(new MockNFTAttributesManager()));

        vm.etch(configStorage, address(landManager).code);
        vm.etch(address(lockManager), address(lockManager).code);
        vm.etch(address(accountManager), address(accountManager).code);
        vm.etch(address(munchNFT), address(munchNFT).code);
        vm.etch(address(nftAttributesManager), address(nftAttributesManager).code);

        vm.mockCall(configStorage, abi.encodeWithSelector(IConfigStorage.getAddress.selector, StorageKey.LockManager), abi.encode(address(lockManager)));
        vm.mockCall(configStorage, abi.encodeWithSelector(IConfigStorage.getAddress.selector, StorageKey.AccountManager), abi.encode(address(accountManager)));
        vm.mockCall(configStorage, abi.encodeWithSelector(IConfigStorage.getAddress.selector, StorageKey.MunchNFT), abi.encode(address(munchNFT)));
        vm.mockCall(configStorage, abi.encodeWithSelector(IConfigStorage.getAddress.selector, StorageKey.NFTAttributesManager), abi.encode(address(nftAttributesManager)));

        landManager.initialize(configStorage);

        vm.startPrank(renter);
        munchNFT.approve(address(landManager), token
Incorrect Schnibble Calculation Severely Impacts Game Economy
Audit: 2024-07-munchables-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-07-munchables/blob/94cf468aaabf526b7a8319f7eba34014ccebe7b9/src/managers/LandManager.sol#L283


# Vulnerability details

## Description
The current implementation of Schnibble calculation in the `_farmPlots` function is incorrect, leading to drastically reduced Schnibble generation and inverted bonus effects. This error significantly impacts the game's economy and core mechanics.

## Code snippet

```solidity
schnibblesTotal = uint256(
                (int256(schnibblesTotal) +
                    (int256(schnibblesTotal) * finalBonus)) / 100
            );
```

## Problem
The current calculation:
1. Incorrectly applies bonuses
2. Divides the result by 100, leading to a ~99% reduction in Schnibble output
3. Inverts the effect of bonuses, causing positive bonuses to decrease output and negative bonuses to increase it

## Impact
1. Severe deflation in the game economy: Players receive only about 1% of the intended Schnibbles
2. Stunted player progression due to drastically reduced rewards
3. Broken bonus system: Positive bonuses reduce output, negative bonuses increase it
4. Core gameplay mechanics are undermined, potentially leading to player frustration and churn

## Example Scenario
Initial Schnibble amount: 1000, Bonus: 10% (finalBonus = 10)

Current (incorrect) calculation:
(1000 + (1000 * 10)) / 100 = 11000 / 100 = 110

Correct calculation:
(1000 * (100 + 10)) / 100 = 110000 / 100 = 1100

Result: Players receive 110 Schnibbles instead of 1100, a 90% reduction

## Suggested Fix
Replace the current calculation with:
```solidity
schnibblesTotal = uint256((int256(schnibblesTotal) * (100 + finalBonus)) / 100);
```


## Assessed type

Math
`LandManager::transferToUnoccupiedPlot` doesn't update the `toilerState[tokenId].plotId` variable
Audit: 2024-07-munchables-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-07-munchables/blob/94cf468aaabf526b7a8319f7eba34014ccebe7b9/src/managers/LandManager.sol#L199-L226


# Vulnerability details

## Impact
`LandManager::transferToUnoccupiedPlot` is essentially useless and not doing its job and puts users at risk of having staked munchables on plots that no longer exist if the landlord has withdrawn funds. Another issue that arises is the fact that a user can override another user's staking and steal their plot.

## Proof of Concept

Inside the `transferToUnoccupiedPlot` function the state of the `toilerState[tokenId].plotId` variable is never changed

```solidity
    function transferToUnoccupiedPlot(
        uint256 tokenId,
        uint256 plotId
    ) external override forceFarmPlots(msg.sender) notPaused {
        (address mainAccount, ) = _getMainAccountRequireRegistered(msg.sender);
        ToilerState memory _toiler = toilerState[tokenId];
        uint256 oldPlotId = _toiler.plotId;
        uint256 totalPlotsAvail = _getNumPlots(_toiler.landlord);
        if (_toiler.landlord == address(0)) revert NotStakedError();
        if (munchableOwner[tokenId] != mainAccount) revert InvalidOwnerError();
        if (plotOccupied[_toiler.landlord][plotId].occupied)
            revert OccupiedPlotError(_toiler.landlord, plotId);
        if (plotId >= totalPlotsAvail) revert PlotTooHighError();

@>      toilerState[tokenId].latestTaxRate = plotMetadata[_toiler.landlord]
            .currentTaxRate;
        plotOccupied[_toiler.landlord][oldPlotId] = Plot({
            occupied: false,
            tokenId: 0
        });
        plotOccupied[_toiler.landlord][plotId] = Plot({
            occupied: true,
            tokenId: tokenId
        });

        emit FarmPlotLeave(_toiler.landlord, tokenId, oldPlotId);
        emit FarmPlotTaken(toilerState[tokenId], tokenId);
    }
```
The only update made to the `toilerState` mapping is on the `latestTaxRate`.

Now a possible attack vector would be for Alice to transfer to another plot and wait for another user (Bob) to stake on her previous plot. After that she can call `unstakeMunchable` and because her `plotId` was not updated correctly she will make Bob's plot unoccupied.

```solidity
        plotOccupied[_toiler.landlord][_toiler.plotId] = Plot({
            occupied: false,
            tokenId: 0
        });
```

After that she can simply call `stakeMunchable` and reclaim her plot from Bob because `plotOccupied[landlord][plotId].occupied` will be false.

## Tools Used

Manual review
VS Code

## Recommended Mitigation Steps
```diff
...
        if (plotOccupied[_toiler.landlord][plotId].occupied)
            revert OccupiedPlotError(_toiler.landlord, plotId);
        if (plotId >= totalPlotsAvail) revert PlotTooHighError();

  +     toilerState[tokenId].plotId = plotId;
        toilerState[tokenId].latestTaxRate = plotMetadata[_toiler.landlord]
            .currentTaxRate;
        plotOccupied[_toiler.landlord][oldPlotId] = Pl
`LandManager::transferToUnoccupiedPlot` doesn't set `toilerState[tokenId].dirty` to false which leads to a user being unable to receive rewards
Audit: 2024-07-munchables-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-07-munchables/blob/94cf468aaabf526b7a8319f7eba34014ccebe7b9/src/managers/LandManager.sol#L199-L226


# Vulnerability details

## Impact
If the landlord of the user ever withdraws enough funds to make the plot they are staking on invalid, transferring to another existing plot using `transferToUnoccupiedPlot` will not allow them to receive rewards.

## Proof of Concept

A plot becomes `dirty` when the landlord withdraws/unlocks funds and lowers the amount of plots they own making some of the plots currently being used invalid. The developers have decided to simply award the stakers and landlords once and set the plot as dirty by setting `toilerState[tokenId].dirty = true;`

When a user calls `transferToUnoccpiedPlot` the contract ensures that the new `plotId` is a valid one by checking `if (plotId >= totalPlotsAvail) revert PlotTooHighError();` but it never resets the `toilerState[tokenId].dirty` variable to signify the fact that the user is now staking on a valid plot. That means that when the user tries to farm the plot using `farmPlots` or through the modifier on all other functions `forceFarmPlots` the code will hit this check:
`if (_toiler.dirty) continue;` and skip over the plot and will not grand the user or the landlord any rewards.

## Tools Used

Manual review
VS Code

## Recommended Mitigation Steps

```diff
    function transferToUnoccupiedPlot(
        uint256 tokenId,
        uint256 plotId
    ) external override forceFarmPlots(msg.sender) notPaused {
        (address mainAccount, ) = _getMainAccountRequireRegistered(msg.sender);
        ToilerState memory _toiler = toilerState[tokenId];
        uint256 oldPlotId = _toiler.plotId;
        uint256 totalPlotsAvail = _getNumPlots(_toiler.landlord);
        if (_toiler.landlord == address(0)) revert NotStakedError();
        if (munchableOwner[tokenId] != mainAccount) revert InvalidOwnerError();
        if (plotOccupied[_toiler.landlord][plotId].occupied)
            revert OccupiedPlotError(_toiler.landlord, plotId);
        if (plotId >= totalPlotsAvail) revert PlotTooHighError();

+       toilerState[tokenId].dirty = false;
        toilerState[tokenId].latestTaxRate = plotMetadata[_toiler.landlord]
            .currentTaxRate;
        plotOccupied[_toiler.landlord][oldPlotId] = Plot({
            occupied: false,
            tokenId: 0
        });
        plotOccupied[_toiler.landlord][plotId] = Plot({
            occupied: true,
            tokenId: tokenId
        });

        emit FarmPlotLeave(_toiler.landlord, tokenId, oldPlotId);
        emit FarmPlotTaken(toilerState[tokenId], tokenId);
    }
```


## Assessed type

Other
Users can own one plot for free because of improper check in `LandManager::_farmPlots`
Audit: 2024-07-munchables-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-07-munchables/blob/94cf468aaabf526b7a8319f7eba34014ccebe7b9/src/managers/LandManager.sol#L258


# Vulnerability details

## Impact
A user can own a plot and receive rewards from farming without having any funds locked because of a wrong check in `LandManager::_farmPlots`.

## Proof of Concept

The contract team has outlined an edge case where the landlord has withdrawn/unlocked their funds and as a result own less plots than before and some of the users that have staked on them can no longer use them. They have decided that if a user were to be staking on a no longer existing plot they would give them the rewards once and mark it as `dirty` and stop giving both the landlord and user rewards. However this edge case is not handled properly and allows for a landlord to own a plot while having 0 funds locked and for a user to stake on it and receive rewards as if the plot existed.

If we take a look inside `LandManager::_farmPlots` we can see the check that takes care of the edge case:

```solidity
            // use last updated plot metadata time if the plot id doesn't fit
            // track a dirty bool to signify this was done once
            // the edge case where this doesnt work is if the user hasnt farmed in a while and the landlord
            // updates their plots multiple times. then the last updated time will be the last time they updated their plot details
            // instead of the first
@>          if (_getNumPlots(landlord) < _toiler.plotId) {
                timestamp = plotMetadata[landlord].lastUpdated;
                toilerState[tokenId].dirty = true;
            }
```

The problem arises from the fact that `plotId` are numbers starting from 0,1,2... , meaning that the first plot a landlord gets will have `plotId = 0`. This is confirmed from the check in `LandManager::stakeMunchable`

```solidity
        uint256 totalPlotsAvail = _getNumPlots(landlord);
        if (plotId >= totalPlotsAvail) revert PlotTooHighError();
```

The return value of `_getNumPlots` is the exact number of how many plots a landlord owns. For example if `_getNumPlots` returns 1, the landlord has 1 plot with `plotId = 0`. But if we again take a look at the check in `LandManager::_farmPlots` we can see that we will not enter the stated edge case even if we provide a `plotId` that shouldn't exist.

E.g. 

```solidity
//           1              <      1         => false
 if (_getNumPlots(landlord) < _toiler.plotId) {
```

That means we will not enter the edge case and mark the plot as dirty even though the landlord does not own the plot with `plotId = 1`.

## Tools Used

Manual review
VS Code

## Recommended Mitigation Steps

```diff
            // use last updated plot metadata time if the plot id doesn't fit
            // track a dirty bool to signify this was done once
            // the edge case where this doesnt work is if the user hasnt farmed in a while and the landlord
            // updates
A Malicious Staker Can Occupy His Landlord's All Plots With Only One Munchable
Audit: 2024-07-munchables-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-07-munchables/blob/94cf468aaabf526b7a8319f7eba34014ccebe7b9/src/managers/LandManager.sol#L199-L226


# Vulnerability details

## Impact

After staking one Munchable, the staker can make his landlord's all plots occupied by repeatedly transfering to unoccupied plots. As a result, staking Munchables to that landlord will be DoSed due to no unoccupied plot.

## Proof of Concept

In the `transferToUnoccupiedPlot()` function, `toilerState[tokenId]` is updated as follows and `latestTaxRate` is the only updated field.

```solidity
    function transferToUnoccupiedPlot(
        uint256 tokenId,
        uint256 plotId
    ) external override forceFarmPlots(msg.sender) notPaused {
        ... ...

        toilerState[tokenId].latestTaxRate = plotMetadata[_toiler.landlord]
            .currentTaxRate;
        .. ...
    }
```

Because `toilerState[tokenId].plotId` is not update, if a staker transfers one staked token multiple times, `oldPlotId` value in the function call never changes. Even after unstaking that token, the tracked plots remain occupied.

For example, let's assume a toiler stakes a token to `Plot 1`. And then he transfers his token to `Plot 2`, then transfers from `Plot 2` to `Plot 3`, from `Plot 3` to `Plot 4`, etc...
In this case, `oldPlotId` in the `transferToUnoccupiedPlot()` call is always 1.

Consequently, all plots that were previously occupied remain in that state, leading to DoS on the staking of Munchables.

### Test case of POC

A new test case is added to `transferToUnoccupiedPlot.test.ts` for PoC purpose.

```javascript
    it("DoSing staking munchables", async () => {
      for (let i = 2; i < 100; i ++) {
        const { request } =
          await testContracts.landManagerProxy.contract.simulate.transferToUnoccupiedPlot([1, i], {
            account: bob,
          });
        const txHash = await testClient.writeContract(request);
        await assertTxSuccess({ txHash: txHash });
      }

      // Plot 3 - 100 are in occupied state
      for (let i = 3; i < 100; i ++) {
        await assert.rejects(
          testContracts.landManagerProxy.contract.simulate.stakeMunchable([alice, 2, i], {
            account: bob,
          }),
          (err: Error) => assertContractFunctionRevertedError(err, "OccupiedPlotError")
        );
      }
    });
```

## Tools Used

Manual Review

## Recommended Mitigation Steps

`toilerState[tokenId].plotId` should be updated with new `plotId`.

```solidity
    function transferToUnoccupiedPlot(
        uint256 tokenId,
        uint256 plotId
    ) external override forceFarmPlots(msg.sender) notPaused {
        ... ...

        toilerState[tokenId].latestTaxRate = plotMetadata[_toiler.landlord]
            .currentTaxRate;
+       toilerState[tokenId].plotId = plotId;
        ... ...
    }
```



## Assessed type

DoS
Incorrect calculation of total Schnibbles in farming plots
Audit: 2024-07-munchables-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-07-munchables/blob/94cf468aaabf526b7a8319f7eba34014ccebe7b9/src/managers/LandManager.sol#L283-L286


# Vulnerability details

## Impact

Due to an incorrect calculation of total Schnibbles in farming plots, both toilers and landlords will harvest significantly fewer Schnibbles than anticipated. This discrepancy may adversely impact the growth of Munchables and the overall game strategy.

## Proof of Concept

In the `_farmPlots()` function, `schnibblesTotal` is calculated as follows:

```solidity
    function _farmPlots(address _sender) internal {
        ... ...
        for (uint8 i = 0; i < staked.length; i++) {
            ... ...
            schnibblesTotal =
                (timestamp - _toiler.lastToilDate) *
                BASE_SCHNIBBLE_RATE;
            schnibblesTotal = uint256(
                (int256(schnibblesTotal) +
                    (int256(schnibblesTotal) * finalBonus)) / 100 
            );
            ... ...
        }
        ... ...
    }
```

If we rewrite the above statement as formula:
$$
\text{schnibblesTotal} = \frac{(\text{schnibblesTotal} + \text{schnibblesTotal} \times \text{finalBonus})}{100}
$$

If we assume that `finalBonus` is 10%, `schnibblesTotal` is reduced to only 11% of its value. Therefore, the tailor and his landlord will receive much less Schnibbles.

I am sure the intended formula of the above statement is like the below:

$$
\text{schnibblesTotal} = \text{schnibblesTotal} + \frac{\text{schnibblesTotal} \times \text{finalBonus}}{100}
$$


## Tools Used

Manual Review

## Recommended Mitigation Steps

I would suggest changing the issued statement as follow:

```solidity
    function _farmPlots(address _sender) internal {
        ... ...
        for (uint8 i = 0; i < staked.length; i++) {
            ... ...
-           schnibblesTotal = uint256(
-               (int256(schnibblesTotal) +
-                   (int256(schnibblesTotal) * finalBonus)) / 100 
-           );

+           schnibblesTotal = uint256(
+               int256(schnibblesTotal) +
+                   (int256(schnibblesTotal) * finalBonus) / 100 
+           );
            ... ...
        }
        ... ...
    }
```



## Assessed type

Error
A toiler and its landlord can keep earning unintended Schnibbles from at most one dirty plot.
Audit: 2024-07-munchables-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-07-munchables/blob/94cf468aaabf526b7a8319f7eba34014ccebe7b9/src/managers/LandManager.sol#L258


# Vulnerability details

## Impact

After the available plots get reduced by withdrawing locks by the landlord, the toiler and its landlord can keep earning Schnibbles from the last plot 

## Proof of Concept

As `plotId` starts from `0`(the sponsor confirmed), the maximum value of `plotId` is `_getNumPlots() - 1`.

Let's assume this scenario:

A landlord has `6` available plots, and toilers stakes Munchables to all of them. After a while, the landlord withdraws his locked assets, so the available plots decreases to `4`.

But because of this check in the `_farmPlots()` function, `Plot 4` can successfully be escaped from being dirty and its toiler can keep earning Schnibbles.

```solidity
    if (_getNumPlots(landlord) < _toiler.plotId) {
        timestamp = plotMetadata[landlord].lastUpdated;
        toilerState[tokenId].dirty = true;
    }
```

## Tools Used

Manual Review

## Recommended Mitigation Steps

I think the conditional check should be changed like the below:

```solidity
-   if (_getNumPlots(landlord) < _toiler.plotId) {
+   if (_getNumPlots(landlord) <= _toiler.plotId) {
        timestamp = plotMetadata[landlord].lastUpdated;
        toilerState[tokenId].dirty = true;
    }
```



## Assessed type

Error
`transferToUnoccupiedPlot` is lacking updating `dirty field` which prevent player from earning when farming
Audit: 2024-07-munchables-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-07-munchables/blob/main/src/managers/LandManager.sol#L251
https://github.com/code-423n4/2024-07-munchables/blob/main/src/managers/LandManager.sol#L199-L226


# Vulnerability details

## Description
`transferToUnoccupiedPlot` is lacking updating `dirty field` during his operation which will prevent the player from earning during farming as even if the plot transfered to exist, the farming logic will believe such plot is gone (as dirty still set to true), which seems to warrant `Medium` severity.

## Impact
Player is not earning from farming as it should when transfering from a dirty plot, as the new plot is still being considered `dirty`, so [ignored](https://github.com/code-423n4/2024-07-munchables/blob/main/src/managers/LandManager.sol#L251) in the farming calculation.

## PoC
Alice and Bob are the players. Alice is a landlord with 121 plots [0,120] as locked 1 ETH in LockManager.
1) Bob's stake NFT 1 on Alice's plotId 120. 
2) After a week, Alice unlock 0.1 ETH, reducing his number of plot to 112 [0,111].
3) Bob note that he is not earning any rewards anymore for his NFT, so he decides to `transferToUnoccupiedPlot` his NFT 1 to plotId 100, which works fine (and call `forceFarmPlots` which will mark plotId 120 dirty if not yet done).
4) Unfortunatelly, Bob will not be earning point during farming afterwards either, as the transfer didn't overwrite the `dirty` field, so the farming accounting will still think the plot is invalid.
```solidity
        ...

        for (uint8 i = 0; i < staked.length; i++) {
            timestamp = block.timestamp;
            tokenId = staked[i];
            _toiler = toilerState[tokenId];
            if (_toiler.dirty) continue;     // <----------
            landlord = _toiler.landlord;
			
			...
```


## Tools Used
Manual review

## Recommended Mitigation Steps
Apply the following patch to resolve this issue.

```diff
    function transferToUnoccupiedPlot(
        uint256 tokenId,
        uint256 plotId
    ) external override forceFarmPlots(msg.sender) notPaused {
        (address mainAccount, ) = _getMainAccountRequireRegistered(msg.sender);
        ToilerState memory _toiler = toilerState[tokenId];
        uint256 oldPlotId = _toiler.plotId;
        uint256 totalPlotsAvail = _getNumPlots(_toiler.landlord);
        if (_toiler.landlord == address(0)) revert NotStakedError();
        if (munchableOwner[tokenId] != mainAccount) revert InvalidOwnerError();
        if (plotOccupied[_toiler.landlord][plotId].occupied)
            revert OccupiedPlotError(_toiler.landlord, plotId);
        if (plotId >= totalPlotsAvail) revert PlotTooHighError();

        toilerState[tokenId].latestTaxRate = plotMetadata[_toiler.landlord].currentTaxRate;
+	toilerState[tokenId].dirty = false;
        plotOccupied[_toiler.landlord][oldPlotId] = Plot({
            occupied: false,
            tokenId: 0
        });
        plotOccupied[_toiler.landlord][plotId] = Plot({
            o