Beatland Festival

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

Missing onlyOwner Modifier Vulnerability

Root + Impact

The BeatToken contract contains NO access control on critical functions. The require(msg.sender == festivalContract) check is completely useless because:

The Core Problem

solidity

function mint(address to, uint256 amount) external {
require(msg.sender == festivalContract, "Only_Festival_Mint");
_mint(to, amount);
}
function burnFrom(address from, uint256 amount) external {
require(msg.sender == festivalContract, "Only_Festival_Burn");
_burn(from, amount);
}

THERE IS NO onlyOwner MODIFIER!

The contract allows ANY contract that becomes festivalContract to mint and burn unlimited tokens. This is NOT proper access control.

function mint(address to, uint256 amount) external {
require(msg.sender == festivalContract, "Only_Festival_Mint");
_mint(to, amount);
}
function burnFrom(address from, uint256 amount) external {
require(msg.sender == festivalContract, "Only_Festival_Burn");
_burn(from, amount);
}

Risk

Stop minting
Burn tokens themselves
Mint unlimited tokens
Burn anyone's tokens
Destroy token value instantly

Likelihood:

Impact:

The vulnerability allows complete takeover of the token's supply and can destroy all token value instantly.

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity 0.8.25;
import "forge-std/Test.sol";
import "../src/BeatToken.sol";
contract MissingAccessControlTest is Test {
BeatToken public token;
address public owner;
address public attacker;
address public maliciousContract;
function setUp() public {
owner = makeAddr("owner");
attacker = makeAddr("attacker");
maliciousContract = makeAddr("maliciousContract");
vm.prank(owner);
token = new BeatToken();
}
function test_NoAccessControl_AnyoneCanBecomeFestival() public {
// Step 1: Owner sets festivalContract (only legitimate operation)
vm.prank(owner);
token.setFestivalContract(maliciousContract);
console.log("Owner set festivalContract to:", maliciousContract);
// Step 2: Malicious contract now has FULL CONTROL
vm.prank(maliciousContract);
token.mint(attacker, 1_000_000 ether);
uint256 attackerBalance = token.balanceOf(attacker);
console.log("Attacker received:", attackerBalance);
assertEq(attackerBalance, 1_000_000 ether);
// Step 3: Owner has NO POWER to stop this
vm.prank(owner);
vm.expectRevert("Festival contract already set");
token.setFestivalContract(address(0)); // CANNOT revoke!
console.log("Owner CANNOT revoke malicious contract!");
// Step 4: Malicious contract continues abusing
vm.prank(maliciousContract);
token.mint(attacker, 100_000_000 ether);
attackerBalance = token.balanceOf(attacker);
console.log("Attacker now has:", attackerBalance);
// Step 5: Malicious contract can burn ANYONE's tokens
address victim = makeAddr("victim");
vm.prank(owner);
token.transfer(victim, 5000 ether);
console.log("Victim received 5000 BEAT");
vm.prank(maliciousContract);
token.burnFrom(victim, 5000 ether);
assertEq(token.balanceOf(victim), 0);
console.log("Malicious contract burned ALL victim tokens!");
}
function test_NoAccessControl_MintWithoutLimit() public {
vm.prank(owner);
token.setFestivalContract(maliciousContract);
// Malicious contract can mint UNLIMITED tokens
for(uint i = 0; i < 10; i++) {
vm.prank(maliciousContract);
token.mint(attacker, 1_000_000_000 ether);
}
uint256 finalBalance = token.balanceOf(attacker);
console.log("Attacker balance after unlimited minting:", finalBalance);
// This would crash the token value to ZERO
assertTrue(finalBalance > 10_000_000_000 ether);
}
function test_NoAccessControl_OwnerCannotMint() public {
vm.prank(owner);
token.setFestivalContract(maliciousContract);
// Owner tries to mint (for legitimate purposes)
vm.prank(owner);
vm.expectRevert("Only_Festival_Mint");
token.mint(owner, 1000 ether);
console.log("Owner CANNOT mint tokens - only malicious contract can!");
// Owner tries to burn
vm.prank(owner);
vm.expectRevert("Only_Festival_Burn");
token.burnFrom(owner, 100 ether);
console.log("Owner CANNOT burn tokens - only malicious contract can!");
}
function test_NoAccessControl_FestivalContractNotEvenSet() public {
// Before setFestivalContract is called
// No one can mint (good) but contract is USELESS
vm.prank(owner);
vm.expectRevert("Only_Festival_Mint");
token.mint(owner, 1000 ether);
console.log("No one can mint before festivalContract is set");
console.log("But contract cannot function at all!");
// The contract is completely USELESS without setting festivalContract
// But once set, there's NO PROTECTION
}
}
// Malicious contract that exploits the missing access control
contract ExploitContract {
BeatToken public token;
constructor(BeatToken _token) {
token = _token;
}
// Anyone can deploy this and if it becomes festivalContract
function destroyTokenValue() external {
// Mint infinite tokens
token.mint(address(this), type(uint256).max);
// Burn all tokens from largest holders
// (Would need holder addresses)
}
function stealFromUsers(address[] memory victims) external {
for(uint i = 0; i < victims.length; i++) {
uint256 balance = token.balanceOf(victims[i]);
token.burnFrom(victims[i], balance);
}
}
}

Recommended Mitigation

Add onlyOwner to Critical Functions
solidity
function mint(address to, uint256 amount) external onlyOwner {
_mint(to, amount);
}
function burnFrom(address from, uint256 amount) external onlyOwner {
_burn(from, amount);
}
// festivalContract can have LIMITED permissions
function festivalMint(address to, uint256 amount) external {
require(msg.sender == festivalContract, "Only festival");
require(amount <= mintLimit, "Exceeds limit");
_mint(to, amount);
}
Updates

Lead Judging Commences

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