Root + Impact
Description
-
The market uses transferFrom rather than safeTransferFrom for outgoing transfers
-
If bidder is a contract that cannot handle ERC721, tokens can be effectively lost for that bidder
function _executeSale(uint256 tokenId) internal {
Listing storage listing = listings[tokenId];
Bid memory bid = bids[tokenId];
listing.listed = false;
delete bids[tokenId];
@>
BBERC721.transferFrom(address(this), bid.bidder, tokenId);
uint256 fee = (bid.amount * S_FEE_PERCENTAGE) / 100;
s_totalFee += fee;
uint256 sellerProceeds = bid.amount - fee;
_payout(listing.seller, sellerProceeds);
emit AuctionSettled(tokenId, bid.bidder, listing.seller, bid.amount);
}
Risk
Likelihood: Medium
Impact: High
Proof of Concept
Add the following test, then run this command: forge test --match-test test_unsafeERC721Transfer
function test_unsafeERC721Transfer() public {
vm.deal(address(rejector), STARTING_BALANCE);
_mintNFT();
_listNFT();
vm.prank(address(rejector));
market.placeBid{value: MIN_PRICE + 1 ether}(TOKEN_ID);
vm.prank(SELLER);
market.takeHighestBid(TOKEN_ID);
assertEq(nft.ownerOf(TOKEN_ID), address(rejector), "NFT should be held by the rejector");
assertEq(market.getListing(TOKEN_ID).listed, false, "Listing should be marked as unlisted");
}
Recommended Mitigation
Use safeTransferFrom(address(this), bid.bidder, tokenId) for outgoing trasnfer, so it will revert if the recipient is non-IERC721Receiver
function _executeSale(uint256 tokenId) internal {
Listing storage listing = listings[tokenId];
Bid memory bid = bids[tokenId];
listing.listed = false;
delete bids[tokenId];
+ BBERC721.safeTransferFrom(address(this), bid.bidder, tokenId);
- BBERC721.transferFrom(address(this), bid.bidder, tokenId);
uint256 fee = (bid.amount * S_FEE_PERCENTAGE) / 100;
s_totalFee += fee;
uint256 sellerProceeds = bid.amount - fee;
_payout(listing.seller, sellerProceeds);
emit AuctionSettled(tokenId, bid.bidder, listing.seller, bid.amount);
}