Beatland Festival

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

`encodeTokenId` and `decodeTokenId` are inconsistent for valid `uint256` inputs

Root + Impact

Description

The normal behavior should allow a token ID encoded from (collectionId, itemId) to decode back to the same (collectionId, itemId). The interface exposes both values as uint256.

encodeTokenId() accepts full uint256 values but decodeTokenId() only preserves the lower 128 bits for itemId. When itemId exceeds type(uint128).max, the encoded value spills into the collection portion and decoding returns different values.

uint256 constant COLLECTION_ID_SHIFT = 128;
function encodeTokenId(uint256 collectionId, uint256 itemId) public pure returns (uint256) {
// @> Accepts uint256 itemId and adds it directly into the lower 128-bit region
return (collectionId << COLLECTION_ID_SHIFT) + itemId;
}
function decodeTokenId(uint256 tokenId) public pure returns (uint256 collectionId, uint256 itemId) {
collectionId = tokenId >> COLLECTION_ID_SHIFT;
// @> Only the lower 128 bits are returned as itemId
itemId = uint256(uint128(tokenId));
}

Risk

Likelihood:

  • This occurs when callers use encodeTokenId() with an itemId larger than type(uint128).max.

  • This occurs because the public API accepts uint256 values while the encoding format only safely supports 128-bit components.

Impact:

  • decodeTokenId(encodeTokenId(collectionId, itemId)) can return a different collection and item than the original inputs.

  • Integrations relying on these public helpers can misidentify memorabilia tokens.

Proof of Concept

function testEncodeDecodeMismatchForLargeItemId() public view {
uint256 collectionId = 100;
uint256 itemId = uint256(type(uint128).max) + 1;
uint256 tokenId = festivalPass.encodeTokenId(collectionId, itemId);
(uint256 decodedCollectionId, uint256 decodedItemId) = festivalPass.decodeTokenId(tokenId);
assertNotEq(decodedCollectionId, collectionId);
assertNotEq(decodedItemId, itemId);
assertEq(decodedCollectionId, 101);
assertEq(decodedItemId, 0);
}

Recommended Mitigation

Make the supported 128-bit bound explicit in the API, or validate inputs before encoding.

-function encodeTokenId(uint256 collectionId, uint256 itemId) public pure returns (uint256) {
- return (collectionId << COLLECTION_ID_SHIFT) + itemId;
+function encodeTokenId(uint256 collectionId, uint256 itemId) public pure returns (uint256) {
+ require(collectionId <= type(uint128).max, "Collection ID too large");
+ require(itemId <= type(uint128).max, "Item ID too large");
+ return (collectionId << COLLECTION_ID_SHIFT) | itemId;
}

Or change the signature to accept uint128 collectionId, uint128 itemId.

Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 2 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!