Mystery Box

First Flight #25
Beginner FriendlyFoundry
100 EXP
View results
Submission Details
Severity: medium
Valid

Deletion in a Struct Containing a Mapping Does Not Remove Mapping Entries, Leading to Inefficiencies and Confusion

Summary

The MysteryBox contract uses the delete operation to remove rewards from the rewardsOwned mapping, which is stored as an array inside a struct. While the delete keyword resets the entry to its default value (an empty reward), it does not remove the slot or shift the array elements. This leads to inefficiencies due to empty slots left in the array, which increases gas costs and causes potential confusion when users or external systems interact with the rewards array.

Vulnerability Details

The rewardsOwned mapping is an array structure where user rewards are stored (mapping(address => Reward[]) public rewardsOwned). Functions such as transferReward and claimSingleReward delete entries from this mapping using the delete keyword:

function transferReward(address _to, uint256 _index) public {
require(_index < rewardsOwned[msg.sender].length, "Invalid index");
rewardsOwned[_to].push(rewardsOwned[msg.sender][_index]);
delete rewardsOwned[msg.sender][_index]; // Deletes the reward but leaves an empty slot
}

The delete keyword resets the reward entry at the specified index but does not remove the slot, leaving empty entries (default Reward("", 0)) in the array. These gaps cause inefficiencies during iterations and can confuse users when they view or interact with their rewards.

For example, in the claimAllRewards() function, the array is looped through to sum the values of all rewards:

function claimAllRewards() public {
uint256 totalValue = 0;
for (uint256 i = 0; i < rewardsOwned[msg.sender].length; i++) {
totalValue += rewardsOwned[msg.sender][i].value; // Empty slots increase gas cost
}
require(totalValue > 0, "No rewards to claim");
(bool success,) = payable(msg.sender).call{value: totalValue}("");
require(success, "Transfer failed");
delete rewardsOwned[msg.sender];
}

These empty slots remain in the array after deletion, leading to increased gas costs and inefficiencies when looping through the array. This behavior could also cause confusion for users who expect a contiguous array of rewards, as empty or default values may remain in the array.

Impact

  • Increased Gas Costs: Iterating through arrays with empty slots leads to higher gas costs, as the contract unnecessarily processes default values.

  • Potential Confusion: Users or external systems may be confused by the presence of empty slots when viewing or interacting with the rewards, leading to unexpected behavior.

  • Unexpected Behavior: If the contract expects a contiguous array, these gaps can cause problems in functionality, especially in functions that rely on iterating over all rewards.

Tools Used

Manual code review

Recommendations

  • Shift Array Elements After Deletion:
    When deleting an entry from the rewards array, shift the remaining elements to fill the gap. This ensures that the array remains contiguous and does not contain empty slots.

    Example:

function transferReward(address _to, uint256 _index) public {
require(_index < rewardsOwned[msg.sender].length, "Invalid index");
rewardsOwned[_to].push(rewardsOwned[msg.sender][_index]);
// Shift the array elements to close the gap
for (uint256 i = _index; i < rewardsOwned[msg.sender].length - 1; i++) {
rewardsOwned[msg.sender][i] = rewardsOwned[msg.sender][i + 1];
}
rewardsOwned[msg.sender].pop(); // Remove the last empty element
}
  • Use Mappings with Unique Keys Instead of Arrays:
    Consider using mappings with unique keys rather than arrays to manage user rewards. Mappings allow for better flexibility and prevent gaps, as each reward can be accessed and removed individually without affecting the overall structure.

  • Implement a Marker for Removed Entries:
    Instead of using the delete keyword, consider marking rewards as “inactive” or “claimed,” and filter out these entries during iterations. This maintains clarity over all rewards while avoiding the creation of gaps.

By implementing these changes, the contract will reduce gas costs, improve efficiency, and prevent confusion, ensuring smoother user interactions and cleaner array management.

Updates

Appeal created

inallhonesty Lead Judge 11 months ago
Submission Judgement Published
Validated
Assigned finding tags:

A user can poison the `rewardsOwned` of another user via `transferReward` of an empty reward index

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.