Root + Impact
Description
-
The `redeemMemorabilia` function uses strict less-than comparison (`<`) against `maxSupply`, but `currentItemId` is initialized to 1 in `createMemorabiliaCollection`. This causes the last item to be unredeemable.
-
For a collection with `maxSupply = 10`, only 9 items can ever be minted because when `currentItemId` reaches 10, the check `10 < 10` fails.
function redeemMemorabilia(uint256 collectionId) external {
MemorabiliaCollection storage collection = collections[collectionId];
require(collection.priceInBeat > 0, "Collection does not exist");
require(collection.isActive, "Collection not active");
require(collection.currentItemId < collection.maxSupply, "Collection sold out"); @> should be <=
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:
Impact:
-
Users cannot redeem the last item of any collection despite having BEAT tokens
-
Protocol breaks its advertised supply guarantees (potential fraud)
-
User BEAT tokens wasted on failed transactions
Proof of Concept
Below PoC demonstrate that the last item cannot be redeemed.
function test_OffByOne_LastItemUnredeemable() public {
uint256 maxSupply = 10;
uint256 priceInBeat = 100e18;
vm.prank(organizer);
uint256 collectionId = festivalPass.createMemorabiliaCollection(
"Limited Poster", "ipfs://poster", priceInBeat, maxSupply, true
);
vm.prank(address(festivalPass));
beatToken.mint(user, priceInBeat * maxSupply);
vm.startPrank(user);
for (uint256 i = 0; i < 9; i++) {
festivalPass.redeemMemorabilia(collectionId);
}
vm.expectRevert("Collection sold out");
festivalPass.redeemMemorabilia(collectionId);
vm.stopPrank();
assertEq(beatToken.balanceOf(user), priceInBeat);
}
Recommended Mitigation
Change currentItemId < maxSupply to currentItemId <= maxSupply, so that the last item can be redeemed.
function redeemMemorabilia(uint256 collectionId) external {
MemorabiliaCollection storage collection = collections[collectionId];
require(collection.priceInBeat > 0, "Collection does not exist");
require(collection.isActive, "Collection not active");
- require(collection.currentItemId < collection.maxSupply, "Collection sold out");
+ require(collection.currentItemId <= collection.maxSupply, "Collection sold out");