Beatland Festival

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

[M-1]: Improper Input Validation in `encodeTokenId` Leads to Token ID Collision and Impersonation

[M-1]: Improper Input Validation in encodeTokenId Leads to Token ID Collision and Impersonation

Description

  • The protocol uses bitmasking to pack a collectionId and an itemId into a single uint256 tokenId. The decodeTokenId function assumes that the itemId occupies the lower 128 bits and the collectionId occupies the upper 128 bits.

    The vulnerability lies in the encodeTokenId function, which accepts itemId as a uint256 without validating that it fits within the designated 128 bits. The function uses addition (+) . If an itemId larger than 2**128 - 1 is provided, the addition "carries over" the excess bits, which corrupts the upper 128 bits reserved for the collectionId.

    This breaks the critical invariant decodeTokenId(encodeTokenId(c, i)) == (c, i) and allows an attacker to predictably generate a tokenId that collides with a legitimate one.


Code: https://github.com/CodeHawks-Contests/2025-07-beatland-festival/blob/5034ccf16e4c0be96de2b91d19c69963ec7e3ee3/src/FestivalPass.sol#L152

function encodeTokenId(uint256 collectionId, uint256 itemId) public pure returns (uint256) {
@> return (collectionId << COLLECTION_ID_SHIFT) + itemId;
}
function decodeTokenId(uint256 tokenId) public pure returns (uint256 collectionId, uint256 itemId) {
collectionId = tokenId >> COLLECTION_ID_SHIFT;
// @audit - data corruption due to conversion of uint256 `tokenId` to uint128 leads to incorrect decoding
@> itemId = uint256(uint128(tokenId));
}

Risk

Likelihood:

  • Reason 1 : The exploit occurs whenever the itemId becomes greater than or equal to 2**128 to any public-facing function that uses encodeTokenId to generate a token's unique identifier.


Impact:

  • Improper input validation in the encodeTokenId function allows a malicious actor to craft inputs that produce a tokenId identical to that of a separate, legitimate token. This collision enables an attacker to mint a token that impersonates another existing asset.

Proof of Concept

Add the following to your test suite , the test will fail at assertEq(decodedItem, itemId)

function test_EncodeDecodeTokenIdVuln() public {
uint256 collectionId = 100;
uint256 itemId = 2**128;
uint256 encoded = festivalPass.encodeTokenId(collectionId, itemId);
(uint256 decodedCollection, uint256 decodedItem) = festivalPass.decodeTokenId(encoded);
assertEq(decodedCollection, collectionId);
}

Recommended Mitigation

It is strongly recommended to enforce size constraints on the inputs to encodeTokenId. The most robust and gas-efficient solution is to change the function's input parameters from uint256 to uint128.

- function encodeTokenId(uint256 collectionId, uint256 itemId) public pure returns (uint256) {
- return (collectionId << COLLECTION_ID_SHIFT) + itemId;
- }
-
- function decodeTokenId(uint256 tokenId) public pure returns (uint256 collectionId, uint256 itemId) {
- collectionId = tokenId >> COLLECTION_ID_SHIFT;
- itemId = uint256(uint128(tokenId));
- }
+ function encodeTokenId(uint128 collectionId, uint128 itemId) public pure returns (uint256) {
+ return (uint256(collectionId) << COLLECTION_ID_SHIFT) | uint256(itemId);
+ }
+
+ function decodeTokenId(uint256 tokenId) public pure returns (uint128 collectionId, uint128 itemId) {
+ collectionId = uint128(tokenId >> COLLECTION_ID_SHIFT);
+ itemId = uint128(tokenId);
+ }
Updates

Lead Judging Commences

inallhonesty Lead Judge 3 months ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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