Description
The createPerformance() function in the FestivalPass contract creates a new performance and returns performanceCount which counts the total no. of performances that have been created but the return value is 1 less than the performanceCount actually is. Due to the use of post-increment on performanceCount (initialized to 0), the first performance created returns 0 instead of 1, the second returns 1, and so forth.
This can lead to confusion in frontends, off-chain indexing, event emission parsing, and user-facing displays.
Impact/Risk
-
Low / No direct security risk: No loss of funds, no exploitation vector, no incorrect reward distribution.
-
User experience issue: Frontends may display "Performance #0" or require special handling for ID 0.
-
Potential for off-by-one errors in integrations or future extensions assuming 1-based IDs.
-
Inconsistency with other parts of the contract (e.g., memorabilia currentItemId starts at 1).
Proof of Concept
The following Foundry test demonstrates the behavior:
solidity
pragma solidity ^0.8.25;
import "forge-std/Test.sol";
import "../src/FestivalPass.sol";
import "../src/BeatToken.sol";
contract FestivalPassTest is Test {
FestivalPass festival;
BeatToken beatToken;
address organizer = address(0x1111);
address owner = address(this);
function setUp() public {
beatToken = new BeatToken();
festival = new FestivalPass(address(beatToken), organizer);
beatToken.setFestivalContract(address(festival));
vm.prank(owner);
festival.setOrganizer(organizer);
}
function test_FirstPerformanceReturnsId1() public {
vm.prank(organizer);
uint256 performanceId = festival.createPerformance(
block.timestamp + 1 hours,
2 hours,
10 ether
);
assertEq(performanceId, 0, "First performance created and returned value is 0 instead of 1");
}
}
[PASS] test_FirstPerformanceReturnsId1() (gas: 105986)
Traces:
[105986] FestivalPassTest::test_FirstPerformanceReturnsId1()
├─ [0] VM::prank(0x0000000000000000000000000000000000001111)
│ └─ ← [Return]
├─ [94535] FestivalPass::createPerformance(3601, 7200, 10000000000000000000 [1e19])
│ ├─ emit PerformanceCreated(performanceId: 0, startTime: 3601, endTime: 10801 [1.08e4])
│ └─ ← [Return] 0
└─ ← [Stop]
Running this test confirms the returned ID is 0 for the first created performance.
Relevant code snippet:
solidity
uint256 public performanceCount;
performances[performanceCount] = Performance({...});
emit PerformanceCreated(performanceCount, startTime, startTime + duration);
return performanceCount++;
Recommended Mitigation
unction createPerformance(
uint256 startTime,
uint256 duration,
uint256 reward
) external onlyOrganizer returns (uint256) {
require(startTime > block.timestamp, "Start time must be in the future");// @audit - low - it can also start in the current block
require(duration > 0, "Duration must be greater than 0");
// Set start/end times
performances[performanceCount] = Performance({
startTime: startTime,
endTime: startTime + duration,
baseReward: reward
});
emit PerformanceCreated(performanceCount, startTime, startTime + duration);
- return performanceCount++;
+ return performanceCount++;
}
This change:
-
Makes the first performance return ID 1
-
Maintains sequential IDs (1, 2, 3...)
-
Requires no other contract modifications
-
Improves UX without affecting gas or security
Alternative: Explicitly return performanceCount + 1 if keeping zero-based storage is preferred.