Bid Beasts

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

Medium: Using transferFrom can lock NFTs in non-receiver contracts

Medium: Using transferFrom can lock NFTs in non-receiver contracts

Description

  • Normal behavior: When transferring NFTs to potentially-contract recipients, use safeTransferFrom to enforce onERC721Received compatibility or revert.

  • Issue: The marketplace uses transferFrom for escrow and distribution, which succeeds even when the recipient contract cannot handle ERC721. This can lock NFTs irrecoverably.

75:2025-09-bid-beasts/src/BidBeastsNFTMarketPlace.sol

BBERC721.transferFrom(msg.sender, address(this), tokenId);

97:2025-09-bid-beasts/src/BidBeastsNFTMarketPlace.sol

BBERC721.transferFrom(address(this), msg.sender, tokenId);

213:2025-09-bid-beasts/src/BidBeastsNFTMarketPlace.sol

BBERC721.transferFrom(address(this), bid.bidder, tokenId);

Risk

Likelihood:

  • Occurs when a bidder or seller is a contract without ERC721 receiver hook

  • Common in composable or testing setups

Impact:

  • NFT becomes stuck at recipient address with no ability to transfer out

  • User loss or platform liability

Proof of Concept

Foundry-style PoC: a non-receiver contract buys via buy-now and becomes the owner.
The marketplace uses transferFrom, so no receiver check is performed and the NFT gets stuck.

// Foundry-style PoC: a non-receiver contract buys via buy-now and becomes the owner.
// The marketplace uses transferFrom, so no receiver check is performed and the NFT gets stuck.
contract BuyerNoReceiver {
BidBeastsNFTMarket immutable market;
constructor(BidBeastsNFTMarket m) { market = m; }
function buy(uint256 tokenId, uint256 amount) external payable {
require(msg.value == amount, "bad value");
market.placeBid{value: amount}(tokenId); // triggers buy-now path if >= buyNowPrice
}
}
// Test sketch:
// 1) Seller lists with buyNowPrice.
// 2) BuyerNoReceiver calls buy() sending buyNowPrice.
// 3) Owner updates to BuyerNoReceiver, which cannot transfer/approve → NFT stuck.
function test_NFT_stuck_when_sold_to_nonReceiver() public {
BidBeasts nft = new BidBeasts();
BidBeastsNFTMarket market = new BidBeastsNFTMarket(address(nft));
address seller = address(0xA11CE);
vm.startPrank(nft.owner()); // contract owner mints
nft.mint(seller);
vm.stopPrank();
vm.startPrank(seller);
nft.approve(address(market), 0);
market.listNFT(0, 1 ether, 2 ether); // set buy-now
vm.stopPrank();
BuyerNoReceiver buyer = new BuyerNoReceiver(market);
vm.deal(address(buyer), 10 ether);
vm.prank(address(buyer));
buyer.buy{value: 2 ether}(0, 2 ether); // buy-now reached
assertEq(nft.ownerOf(0), address(buyer)); // Buyer is a non-receiver contract
// No function exists in BuyerNoReceiver to transfer/approve the NFT → asset is trapped.
}

Recommended Mitigation

Use safeTransferFrom to avoid this issue.

- BBERC721.transferFrom(msg.sender, address(this), tokenId);
+ BBERC721.safeTransferFrom(msg.sender, address(this), tokenId);
- BBERC721.transferFrom(address(this), msg.sender, tokenId);
+ BBERC721.safeTransferFrom(address(this), msg.sender, tokenId);
- BBERC721.transferFrom(address(this), bid.bidder, tokenId);
+ BBERC721.safeTransferFrom(address(this), bid.bidder, tokenId);
Updates

Lead Judging Commences

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

BidBeasts Marketplace: Risk of Locked NFTs

Non-safe transferFrom calls can send NFTs to non-compliant contracts, potentially locking them permanently.

Support

FAQs

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