Beatland Festival

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

Unnecessary use of storage for MemorabiliaCollection in redeemMemorabilia() increases gas costs

Description

In the redeemMemorabilia() function, the MemorabiliaCollection struct is loaded into storage:

solidity

MemorabiliaCollection storage collection = collections[collectionId];

The function then reads several fields (priceInBeat, isActive, maxSupply, currentItemId) and performs one write (currentItemId++).

While using storage is correct and necessary for the increment operation, the multiple reads from storage are expensive. Each SLOAD operation costs significant gas.

A common gas optimization pattern is to copy the struct to memory, perform all reads from memory, and only write back the modified field (currentItemId) at the end.

This reduces the number of SLOADs from multiple to just one or two, saving gas on every redemption.

Impact

  • Low severity / Gas optimization: No security risk, no functional bug.

  • Gas savings: Approximately 2–4 extra SLOADs are avoided per call (each ~2,100 gas in post-London EIP-2929 pricing).

  • User impact: Higher gas fees for redeeming memorabilia NFTs — noticeable in high-volume collections.

  • Scalability: As the protocol grows and memorabilia redemptions become frequent, cumulative gas waste adds up.

Classified as Low / Gas / QA per CodeHawks guidelines.

Proof of Concept / Gas Comparison

Current vulnerable pattern:

solidity

MemorabiliaCollection storage collection = collections[collectionId];
require(collection.priceInBeat > 0, ...); // SLOAD
require(collection.isActive, ...); // SLOAD
require(collection.currentItemId < collection.maxSupply, ...); // 2 SLOADs
uint256 itemId = collection.currentItemId++; // SLOAD + SSTORE

Optimized pattern:

solidity

MemorabiliaCollection memory collection = collections[collectionId]; // 1 compound SLOAD
// All reads from cheap memory
collection.currentItemId++; // Increment in memory
collections[collectionId].currentItemId = collection.currentItemId; // 1 SSTORE

Estimated savings per call: ~4,000–6,000 gas (depending on access list warm/cold status).

Recommended Mitigation

Refactor to use memory for reads and minimize storage access:

solidity

function redeemMemorabilia(uint256 collectionId) external {
// Load into memory once
MemorabiliaCollection memory 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");
// Burn BEAT tokens
BeatToken(beatToken).burnFrom(msg.sender, collection.priceInBeat);
// Increment in memory
uint256 itemId = collection.currentItemId;
collection.currentItemId++;
// Write back only the incremented value
collections[collectionId].currentItemId = collection.currentItemId;
uint256 tokenId = encodeTokenId(collectionId, itemId + 1); // Note: itemId starts from 1
tokenIdToEdition[tokenId] = itemId + 1;
_mint(msg.sender, tokenId, 1, "");
emit MemorabiliaRedeemed(msg.sender, tokenId, collectionId, itemId + 1);
}

Note: Since currentItemId starts at 1, itemId is correct as the edition number.

Alternative (even cleaner):

solidity

uint256 nextItemId = collections[collectionId].currentItemId;
require(nextItemId < collections[collectionId].maxSupply, "Collection sold out");
// ... other checks using collections[collectionId].field if needed ...
collections[collectionId].currentItemId = nextItemId + 1;
uint256 itemId = nextItemId; // or nextItemId + 1 depending on display
Updates

Lead Judging Commences

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