Beatland Festival

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

`burnFrom()` in BeatToken allows FestivalPass to burn any user's tokens without approval

Description

Root + Impact

  • The burnFrom() function in BeatToken only checks that the caller is festivalContract. It does not check any ERC20 allowance or explicit user consent for the burn.

  • This means the FestivalPass contract (or any future function added to it) can burn arbitrary amounts of BEAT tokens from any user's balance without the user ever approving the burn. Currently redeemMemorabilia() uses this correctly (user calls it themselves), but the design is unsafe — any added function in FestivalPass that calls burnFrom with an attacker-controlled from address would drain victims.

function burnFrom(address from, uint256 amount) external {
require(msg.sender == festivalContract, "Only_Festival_Burn");
_burn(from, amount); // @> No allowance check — burns from ANY address
}

Compare with OpenZeppelin's standard ERC20Burnable.burnFrom():

function burnFrom(address account, uint256 value) public virtual {
_spendAllowance(account, _msgSender(), value); // @> Checks allowance first
_burn(account, value);
}

Risk

Likelihood:

  • Currently FestivalPass only calls burnFrom(msg.sender, ...) inside redeemMemorabilia(), so the user is always burning their own tokens

  • The risk materializes when any new function is added to FestivalPass (or a new festival contract is set) that passes a different from address

Impact:

  • Any upgrade or addition to the FestivalPass contract could enable burning tokens from any user without consent

  • Violates the ERC20 allowance model that users and integrators expect


Proof of Concept

function test_BurnFromNoApproval() public {
// Give user BEAT tokens
vm.prank(address(festivalPass));
beatToken.mint(user1, 1000e18);
assertEq(beatToken.balanceOf(user1), 1000e18);
// FestivalPass can burn user's tokens WITHOUT user approval
vm.prank(address(festivalPass));
beatToken.burnFrom(user1, 1000e18);
assertEq(beatToken.balanceOf(user1), 0);
console.log("Burned 1000 BEAT from user without any allowance or approval");
}

Recommended Mitigation

function burnFrom(address from, uint256 amount) external {
require(msg.sender == festivalContract, "Only_Festival_Burn");
+ _spendAllowance(from, msg.sender, amount);
_burn(from, amount);
}

Then in FestivalPass.redeemMemorabilia(), users would need to first approve the FestivalPass contract to spend their BEAT, or use a permit pattern.

Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 2 days 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!