Unsafe NFT Transfers Locking Tokens in Non-Compliant Contracts
Description
-
NFT transfers to/from marketplace use transferFrom for listing, unlisting, and settlement.
-
Lacks safeTransferFrom, so tokens sent to contracts without onERC721Received hook become irretrievable.
function listNFT(uint256 tokenId, uint256 _minPrice, uint256 _buyNowPrice) external {
@>BBERC721.transferFrom(msg.sender, address(this), tokenId);@>
}
function unlistNFT(uint256 tokenId) external {
@>BBERC721.transferFrom(address(this), msg.sender, tokenId);@>
}
function _executeSale(uint256 tokenId) internal {
@>BBERC721.transferFrom(address(this), bid.bidder, tokenId);@>
}
Risk
Likelihood:
Impact:
Proof of Concept
Deploys contract without receiver hook, bids via it and settles auction, asserts NFT owned by contract but can't transfer out due to no functions, showing lock.
contract NonReceiver {
}
function testNFTLockedInNonCompliantReceiver() public {
_mintNFT();
_listNFT();
NonReceiver nonReceiver = new NonReceiver();
vm.deal(address(nonReceiver), MIN_PRICE + 0.01 ether);
vm.prank(address(nonReceiver));
market.placeBid{value: MIN_PRICE + 0.01 ether}(TOKEN_ID);
vm.warp(block.timestamp + 16 minutes);
market.settleAuction(TOKEN_ID);
assertEq(nft.ownerOf(TOKEN_ID), address(nonReceiver));
}
Recommended Mitigation
Swaps transferFrom to safeTransferFrom for reverts on non-compliant; adds pending winner mapping and claim function with safeTransferFrom for user-controlled receipt.
function listNFT(uint256 tokenId, uint256 _minPrice, uint256 _buyNowPrice) external {
// ...
- BBERC721.transferFrom(msg.sender, address(this), tokenId);
+ BBERC721.safeTransferFrom(msg.sender, address(this), tokenId);
}
function unlistNFT(uint256 tokenId) external {
// ...
- BBERC721.transferFrom(address(this), msg.sender, tokenId);
+ BBERC721.safeTransferFrom(address(this), msg.sender, tokenId);
}
function _executeSale(uint256 tokenId) internal {
// ...
- BBERC721.transferFrom(address(this), bid.bidder, tokenId);
+ // Defer to claim; set pending winner
+ pendingWinners[tokenId] = bid.bidder;
}
+// Add claim function
+mapping(uint256 => address) public pendingWinners;
+
+function claimNFT(uint256 tokenId, address receiver) external {
+ require(pendingWinners[tokenId] == msg.sender, "Not winner");
+ delete pendingWinners[tokenId];
+ BBERC721.safeTransferFrom(address(this), receiver, tokenId);
+}