Beatland Festival

First Flight #44
Beginner FriendlyFoundrySolidityNFT
100 EXP
View results
Submission Details
Severity: medium
Valid

Last Item can never be minted due to logic error of `collection.maxSupply` check

Root + Impact

Description

  • Each MemorabiliaCollection struct contains a maxSupply field specifying the maximum number of mintable items in the collection

  • However, due to an incorrect check of maxSupply inside redeemMemorabilia() the last item in the collection can never be minted

function redeemMemorabilia(uint256 collectionId) external {
MemorabiliaCollection storage collection = collections[collectionId];
require(collection.priceInBeat > 0, "Collection does not exist");
require(collection.isActive, "Collection not active");
@> // below check misuse `<`, should be `<=`
require(collection.currentItemId < collection.maxSupply, "Collection sold out");
BeatToken(beatToken).burnFrom(msg.sender, collection.priceInBeat);
uint256 itemId = collection.currentItemId++;
uint256 tokenId = encodeTokenId(collectionId, itemId);
tokenIdToEdition[tokenId] = itemId;
_mint(msg.sender, tokenId, 1, "");
emit MemorabiliaRedeemed(msg.sender, tokenId, collectionId, itemId);
}

Risk

Likelihood:

  • It always happen on every collection

Impact:

  • Undermines the integrity of the collection’s supply logic

  • Undermines user expectation and trust

Proof of Concept

Add the following test and run this command: forge test -vv --match-test test_RedeemMemorabilia_LastItemFailed

function test_RedeemMemorabilia_LastItemFailed() public {
// Setup collection
vm.prank(organizer);
uint256 collectionId = festivalPass.createMemorabiliaCollection(
"Limited Shirts",
"ipfs://QmShirts",
50e18,
3,
true
);
(string memory name, , , uint256 maxSupply, ,) = festivalPass.collections(collectionId);
console.log("Collection name: %s, max supply: ", name, maxSupply);
// Give users BEAT tokens
vm.prank(address(festivalPass));
beatToken.mint(user1, 200e18);
// User1 tries to redeem all items
console.log("Try to mint %s items", maxSupply);
vm.startPrank(user1);
for (uint i = 0; i < maxSupply; i++) {
if (i == maxSupply - 1) {
// Last item should fail
vm.expectRevert("Collection sold out");
}
festivalPass.redeemMemorabilia(collectionId);
}
vm.stopPrank();
console.log("Failed to mint last item");
}

PoC Results:

forge test -vv --match-test test_RedeemMemorabilia_LastItemFailed
[⠊] Compiling...
[⠘] Compiling 1 files with Solc 0.8.25
[⠃] Solc 0.8.25 finished in 757.13ms
Ran 1 test for test/FestivalPass.t.sol:FestivalPassTest
[PASS] test_RedeemMemorabilia_LastItemFailed() (gas: 356357)
Logs:
Collection name: Limited Shirts, max supply: 3
Try to mint 3 items
Failed to mint last item
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 7.84ms (1.09ms CPU time)
Ran 1 test suite in 243.65ms (7.84ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

Recommended Mitigation

Use <= rather than < in the maxSupply check

function redeemMemorabilia(uint256 collectionId) external {
...
+ require(collection.currentItemId <= collection.maxSupply, "Collection sold out");
- require(collection.currentItemId < collection.maxSupply, "Collection sold out");
...
}
Updates

Lead Judging Commences

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

Off by one error in redeemMemorabilia

Support

FAQs

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