Root + Impact
Description
The attendPerformance
function mints BEAT tokens to the user. However, it lacks a nonReentrant
modifier or similar protection. If the BeatToken
contract is replaced with a malicious or ERC777-compliant contract, reentrancy into attendPerformance
may be possible, which could bypass hasAttended
or lastCheckIn
guards before state is updated, allowing users to mint BEAT repeatedly in a single transaction.
function attendPerformance(uint256 performanceId) external {
require(isPerformanceActive(performanceId), "Performance is not active");
require(hasPass(msg.sender), "Must own a pass");
require(!hasAttended[performanceId][msg.sender], "Already attended this performance");
require(block.timestamp >= lastCheckIn[msg.sender] + COOLDOWN, "Cooldown period not met");
hasAttended[performanceId][msg.sender] = true;
lastCheckIn[msg.sender] = block.timestamp;
uint256 multiplier = getMultiplier(msg.sender);
@> BeatToken(beatToken).mint(msg.sender, performances[performanceId].baseReward * multiplier);
}
Risk
Likelihood:
Requires BEAT to be ERC777-compliant or a malicious contract.
Requires performance to be active and cooldown met.
Needs a crafted contract as msg.sender
to reenter.
Impact:
Proof of Concept
Calls attendPerformance normally.
In the callback of the BEAT mint (e.g., tokensReceived if ERC777), reenters attendPerformance again before the first call finishes.
Since state changes (e.g., hasAttended) may not be enforced properly under reentrancy, the second call may also succeed.
This cycle continues, minting more BEAT than allowed.
This is only possible if BeatToken is an ERC777 token or a user-supplied contract that calls back into FestivalPass during minting.
Recommended Mitigation
+ import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
+ contract FestivalPass is ERC1155, Ownable2Step, IFestivalPass, ReentrancyGuard {
- function attendPerformance(uint256 performanceId) external {
+ function attendPerformance(uint256 performanceId) external nonReentrant {