False “sale settled” event emitted during bidding.
Description
-
The contract should emit AuctionSettled only when a sale actually settles—that is, when _executeSale transfers the NFT to the winner and distributes the proceeds (either via takeHighestBid, settleAuction, or the buy‑now branch inside placeBid that immediately finalizes and returns).
-
placeBid emits AuctionSettled during normal bidding, before any settlement occurs and while the NFT remains escrowed in the marketplace. This creates a false positive sale event that can mislead off‑chain indexers, analytics, or automation that react to settlement events.
function placeBid(uint256 tokenId) external payable isListed(tokenId) {
Listing storage listing = listings[tokenId];
address previousBidder = bids[tokenId].bidder;
uint256 previousBidAmount = bids[tokenId].amount;
require(msg.sender != previousBidder, "Already highest bidder");
@> emit AuctionSettled(tokenId, msg.sender, listing.seller, msg.value);
uint256 requiredAmount;
if (previousBidAmount == 0) {
requiredAmount = listing.minPrice;
require(msg.value > requiredAmount, "First bid must be > min price");
listing.auctionEnd = block.timestamp + S_AUCTION_EXTENSION_DURATION;
emit AuctionExtended(tokenId, listing.auctionEnd);
} else {
}
bids[tokenId] = Bid(msg.sender, msg.value);
if (previousBidder != address(0)) {
_payout(previousBidder, previousBidAmount);
}
emit BidPlaced(tokenId, msg.sender, msg.value);
}
Risk
Likelihood:
-
This occurs on every non–buy‑now bid placed via placeBid, since the erroneous emit is reached in the normal bidding path.
-
Typical auction flows involve many bids; thus, multiple spurious settlement events are produced throughout the auction’s lifetime.
Impact:
-
Off‑chain consumers (indexers, bots, subgraphs, accounting systems) that rely on AuctionSettled to trigger asset handover, accounting, or notifications will record false settlements, causing operational errors or premature actions.
-
Data integrity & monitoring are compromised: analytics, revenue reporting, and alerting pipelines may reflect incorrect sale counts and volumes; incident response tools may misfire on bogus “settlements.”
Proof of Concept
A minimal Foundry test that shows AuctionSettled is emitted during a regular bid, while no settlement happens (NFT remains owned by the marketplace and listing stays active).
pragma solidity 0.8.20;
import "forge-std/Test.sol";
import "../src/BidBeasts_NFT_ERC721.sol";
import "../src/BidBeastsNFTMarketPlace.sol";
contract FalseSettlementEventTest is Test {
BidBeasts nft;
BidBeastsNFTMarket market;
address seller = address(0xA11CE);
address bidder1 = address(0xB1D01);
uint256 tokenId;
event AuctionSettled(uint256 tokenId, address winner, address seller, uint256 price);
function setUp() public {
nft = new BidBeasts();
market = new BidBeastsNFTMarket(address(nft));
tokenId = nft.mint(seller);
vm.startPrank(seller);
nft.approve(address(market), tokenId);
market.listNFT(tokenId, 0.01 ether, 0);
vm.stopPrank();
vm.deal(bidder1, 10 ether);
}
function test_AuctionSettledEmittedDuringBid_NoActualSettlement() public {
vm.expectEmit(true, true, true, true);
emit AuctionSettled(tokenId, bidder1, seller, 0.02 ether);
vm.prank(bidder1);
market.placeBid{value: 0.02 ether}(tokenId);
assertEq(nft.ownerOf(tokenId), address(market), "NFT should remain in escrow");
BidBeastsNFTMarket.Listing memory L = market.getListing(tokenId);
assertTrue(L.listed, "Listing should remain active after a normal bid");
BidBeastsNFTMarket.Bid memory B = market.getHighestBid(tokenId);
assertEq(B.bidder, bidder1);
assertEq(B.amount, 0.02 ether);
}
}
Recommended Mitigation
Emit AuctionSettled only from the true settlement path(s) (i.e., _executeSale and the buy‑now branch that calls it).
function placeBid(uint256 tokenId) external payable isListed(tokenId) {
Listing storage listing = listings[tokenId];
address previousBidder = bids[tokenId].bidder;
uint256 previousBidAmount = bids[tokenId].amount;
// ... checks and buy-now branch ...
- require(msg.sender != previousBidder, "Already highest bidder");
- emit AuctionSettled(tokenId, msg.sender, listing.seller, msg.value); // ❌ remove
+ require(msg.sender != previousBidder, "Already highest bidder");
// proceed with regular bidding logic...
}