Beatland Festival

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

[L-04] `setOrganizer` and `withdraw` do not emit events, breaking off-chain monitoring and audit trails

Description

setOrganizer changes a privileged role without emitting an event. withdraw moves ETH out of the contract but never emits the FundsWithdrawn event that is already defined in IFestivalPass. Off-chain monitoring tools, block explorers, and frontends cannot detect these state changes without parsing transaction traces.

Vulnerability Details

// src/FestivalPass.sol, lines 50-52
function setOrganizer(address _organizer) public onlyOwner {
organizer = _organizer; // @> no event emitted — silent role change
}

The IFestivalPass interface defines a FundsWithdrawn event, but withdraw never emits it:

// src/FestivalPass.sol, lines 147-149
function withdraw(address target) external onlyOwner {
payable(target).transfer(address(this).balance); // @> FundsWithdrawn event defined but never emitted
}

For setOrganizer, this is a security concern because the organizer role controls pass configuration, performance creation, and memorabilia collection management. A silent organizer change could go unnoticed by monitoring systems. For withdraw, the event is already defined in the interface but simply never used, suggesting it was intended but forgotten.

Risk

Likelihood:

  • Both functions will be called during normal protocol operation. setOrganizer is called during deployment (constructor calls it) and whenever the organizer needs to change. withdraw is called to collect pass sale revenue.

Impact:

  • No direct fund loss. Off-chain systems (The Graph indexers, monitoring bots, block explorer event tabs) miss critical state transitions. Security dashboards won't detect unauthorized organizer changes. Accounting systems won't track ETH outflows.

Proof of Concept

function testExploit_NoEventOnOrganizerChange() public {
// Record all events
vm.recordLogs();
// Change organizer
vm.prank(festivalPass.owner());
festivalPass.setOrganizer(address(0xBEEF));
// Check: no events emitted for this critical role change
Vm.Log[] memory logs = vm.getRecordedLogs();
// Only Ownable events may fire, but no OrganizerChanged event exists
// Off-chain systems have no way to detect this change via events
assertEq(festivalPass.organizer(), address(0xBEEF), "Organizer changed silently");
}
function testExploit_NoEventOnWithdraw() public {
// Setup: sell a pass to put ETH in contract
vm.prank(organizer);
festivalPass.configurePass(1, 1 ether, 10);
vm.deal(user, 1 ether);
vm.prank(user);
festivalPass.buyPass{value: 1 ether}(1);
vm.recordLogs();
// Withdraw ETH
vm.prank(festivalPass.owner());
festivalPass.withdraw(festivalPass.owner());
// Despite IFestivalPass defining FundsWithdrawn, no event is emitted
Vm.Log[] memory logs = vm.getRecordedLogs();
bool foundWithdrawEvent = false;
for (uint i = 0; i < logs.length; i++) {
if (logs[i].topics[0] == keccak256("FundsWithdrawn(address,uint256)")) {
foundWithdrawEvent = true;
}
}
assertFalse(foundWithdrawEvent, "FundsWithdrawn event was NOT emitted");
}

Output:

Organizer changed to 0xBEEF: no event emitted
1 ETH withdrawn: FundsWithdrawn event NOT emitted despite being defined in interface

Recommendations

Emit events for both functions:

+ event OrganizerChanged(address indexed previousOrganizer, address indexed newOrganizer);
function setOrganizer(address _organizer) public onlyOwner {
+ emit OrganizerChanged(organizer, _organizer);
organizer = _organizer;
}
function withdraw(address target) external onlyOwner {
+ uint256 amount = address(this).balance;
payable(target).transfer(address(this).balance);
+ emit FundsWithdrawn(target, amount);
}
Updates

Lead Judging Commences

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