Beatland Festival

AI First Flight #4
Beginner FriendlyFoundrySolidityNFT
EXP
View results
Submission Details
Impact: medium
Likelihood: medium
Invalid

Unsafe truncation of tokenId to uint128 causes itemId collisions and state inconsistency

Root + Impact

Description

  • Under normal behavior, tokenId is expected to be uniquely preserved when deriving itemId, ensuring a 1:1 mapping between tokens and their associated items.

    However, the contract downcasts a uint256 value to uint128 and then upcasts it back to uint256:

    itemId = uint256(uint128(tokenId));


    This operation truncates the upper 128 bits of tokenId, meaning the original value is not preserved. As a result, different tokenId values can map to the same itemId.

// Root cause in the codebase with @> marks to highlight the relevant section
function decodeTokenId(uint256 tokenId) public pure returns (uint256 collectionId, uint256 itemId) {
collectionId = tokenId >> COLLECTION_ID_SHIFT;
@> itemId = uint256(uint128(tokenId));
}

Risk

Likelihood:

  • tokenId values grow beyond the uint128 range during normal minting or system usage in long-running deployments or high-volume collections

  • External systems (ERC-721 / ERC-1155 patterns) naturally use full uint256 IDs, making large values realistic and expected

Impact:

  • Multiple distinct tokenIds can collapse into the same itemId, breaking uniqueness guarantees

  • Attackers can deliberately craft high tokenId values that collide with existing itemIds, leading to state manipulation or unauthorized interactions

Proof of Concept

Step 1: Create two different tokenIds

  • tokenId A = 1

  • tokenId B = 2^128 + 1


Step 2: Apply the conversion

  • itemId A = uint128(1) = 1

  • itemId B = uint128(2^128 + 1) = 1


Step 3: Collision occurs

  • Both different tokenIds map to the same itemId

  • Contract cannot distinguish between:

    • original token A

    • forged token B


Step 4: State impact (typical scenarios)

If itemId is used in:

  • mappings → values get overwritten

  • ownership tracking → users share same entry

  • redemption logic → one token can affect another

  • uniqueness checks → become unreliable

function toItemId(uint256 tokenId) internal pure returns (uint256) {
return uint256(uint128(tokenId));
}
function test_CollisionFromTruncation() public {
uint256 tokenIdA = 1;
uint256 tokenIdB = (uint256(1) << 128) + 1;
uint256 itemIdA = toItemId(tokenIdA);
uint256 itemIdB = toItemId(tokenIdB);
// Both map to same itemId due to truncation
assertEq(itemIdA, itemIdB);
// Show actual values (both = 1)
emit log_named_uint("itemIdA", itemIdA);
emit log_named_uint("itemIdB", itemIdB);
}

Recommended Mitigation

1. **Avoid truncation entirely **

If tokenId is already uint256, do not downcast:

itemId = tokenId;

2. Enforce safe bounds before casting (if compression is required)

Only allow values that fit in uint128:

require(tokenId <= type(uint128).max, "tokenId too large");````itemId = uint256(uint128(tokenId));

3. Use explicit design separation (recommended in complex systems)

If the goal is to derive a different identifier:

itemId = uint256(keccak256(abi.encode(tokenId)));

- itemId = uint256(uint128(tokenId));
+ itemId = tokenId;
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 3 hours ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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

Give us feedback!