Beatland Festival

First Flight #44
Beginner FriendlyFoundrySolidityNFT
100 EXP
View results
Submission Details
Impact: high
Likelihood: medium
Invalid

Error arising from 128-bit shift operation

Root + Impact

Description

  • Describe the normal behavior:
    When we use collectionId, it won't exceed 2^128-1 normally and encoding method won't make any problem.


  • Explain the specific issue or problem:
    If collectionId exceeds 128 bits, the encodeTokenId function may lose the upper bits during the left shift operation. This causes the resulting tokenId to be miscalculated—for example, a collectionId of 2^128 - 1 + k may be interpreted simply as k.

// Root cause in the codebase with @> marks to highlight the relevant section
function encodeTokenId(uint256 collectionId, uint256 itemId) public pure returns (uint256) {
@> return (collectionId << COLLECTION_ID_SHIFT) + itemId;
}
function uri(uint256 tokenId) public view override returns (string memory) {
// Decode collection and item IDs
@> (uint256 collectionId, uint256 itemId) = decodeTokenId(tokenId);
}
function redeemMemorabilia(uint256 collectionId) external {
// Generate unique token ID
uint256 itemId = collection.currentItemId++;
@> uint256 tokenId = encodeTokenId(collectionId, itemId);
// Store edition number
@> tokenIdToEdition[tokenId] = itemId;
// Mint the unique NFT
@> _mint(msg.sender, tokenId, 1, "");
emit MemorabiliaRedeemed(msg.sender, tokenId, collectionId, itemId);
}
function getMemorabiliaDetails(uint256 tokenId) external view returns (
uint256 collectionId,
uint256 itemId,
string memory collectionName,
uint256 editionNumber,
uint256 maxSupply,
string memory tokenUri
) {
(collectionId, itemId) = decodeTokenId(tokenId);
@> MemorabiliaCollection memory collection = collections[collectionId];
require(collection.priceInBeat > 0, "Invalid token");
return (
collectionId,
itemId,
collection.name,
itemId, // Edition number is the item ID
collection.maxSupply,
uri(tokenId)
);
}

Risk

Likelihood:

  • This issue occurs when the collectionId variable of type uint256 is used without proper validation, and the collectionId exceeds 2^128 - 1, causing no exception or error handling.

  • Incorrect usage of bit shifting leads to misinterpretation of the collectionId.

Resolution:

  • Add a require check inside relevant functions to ensure that collectionId does not exceed 128 bits.

Impact:

  • Users may end up purchasing memorabilia items different from what they intended.

  • Functions like uri, getMemorabiliaDetails, and redeemMemorabilia will use outdated or incorrect **collectionId**s, resulting in the provision of wrong information.

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;
import "./FestivalPass.sol";
// @notice PoC contract to demonstrate a potential overflow issue when using large collectionId values in encodeTokenId logic.
contract PoC_EncodeTokenIdOverflow {
// Reference to the deployed FestivalPass contract
FestivalPass public festivalPass;
constructor(address _festivalPass) {
// Initialize the FestivalPass instance
festivalPass = FestivalPass(_festivalPass);
}
// @notice Demonstrates a silent overflow issue when using collectionId ≥ 2^128
function testOverflowRedeem() external {
// set bigCollectionID to 2^128
uint256 bigCollectionId = (1 << 128);
// This value, when left-shifted (collectionId << 128), will overflow a 256-bit uint
// This line would revert in many implementations if decode fails or returns invalid data
// Uncomment to test actual redeem behavior (if implemented)
// festivalPass.redeemMemorabilia(bigCollectionId); // Potential revert
// using encodeTokenId with bigCollectedId, itemId
uint256 itemId = 1;
// tokenId == (bigCollectionId << 128) + itemId;
uint256 tokenId = festivalPass.encodeTokenId(bigCollectionId, itemId);
// decoding that tokenId(will be different from intention)
(uint256 decodedCollectionId, uint256 decodedItemId) = festivalPass.decodeTokenId(tokenId);
// decodedCollectionId will be zero, cause of bitshift.
require(decodedCollectionId == 0, "decodedCollectionId should overflow to 0");
// itemId remains intact as it is in the lower 128 bits
require(decodedItemId == itemId, "itemId should remain same");
}
// @notice Demonstrates unintended URI resolution due to overflowed collectionId
function testUriMisbehavior() external view returns (string memory) {
// Use a collectionId slightly over 2^128
uint256 bigCollectionId = (1 << 128) + 42;
uint256 itemId = 7;
// Encode a tokenId with the large collectionId and given itemId
uint256 tokenId = festivalPass.encodeTokenId(bigCollectionId, itemId);
// Decode the tokenId to see the actual interpreted values
(uint256 decodedCollectionId, uint256 decodedItemId) = festivalPass.decodeTokenId(tokenId);
// Expectation: decodedCollectionId is different from input due to overflow
require(decodedCollectionId != bigCollectionId, "No problems!"); // This should fail if overflow happens
// Call uri function with malformed tokenId; this will likely return an incorrect or unintended URI
return festivalPass.uri(tokenId);
}
}

Recommended Mitigation

- remove this code
// change all nextCollectionId type to uint128 or check if uint128 overflows
uint256 public nextCollectionId = 100;
function buyPass(uint256 collectionId) external payable
function encodeTokenId(uint256 collectionId, uint256 itemId) public pure returns (uint256)
function decodeTokenId(uint256 tokenId) public pure returns (uint256 collectionId, uint256 itemId)
function redeemMemorabilia(uint256 collectionId) external
function getUserMemorabiliaDetailed(address user) external view returns (
uint256[] memory tokenIds,
uint256[] memory collectionIds,
uint256[] memory itemIds
)
+ add this code
uint128 public nextCollectionId = 100;
function buyPass(uint128 collectionId) external payable
function encodeTokenId(uint128 collectionId, uint256 itemId) public pure returns (uint256) {
require(collectionId <= uint128.max(), "Need to choose another collectionId!")
}
function decodeTokenId(uint128 tokenId) public pure returns (uint256 collectionId, uint256 itemId)
function redeemMemorabilia(uint128 collectionId) external
function getUserMemorabiliaDetailed(address user) external view returns (
uint256[] memory tokenIds,
uint128[] memory collectionIds,
uint256[] memory itemIds
)
Updates

Lead Judging Commences

inallhonesty Lead Judge about 2 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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