Bid Beasts

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

Misleading settlement event: placeBid() emits AuctionSettled during bidding (pre-settlement), corrupting off-chain logic and accounting

Root + Impact

Description

  • Normal behavior: While bidding, contracts should emit bid-phase events only (BidPlaced, AuctionExtended). A single settlement event must be emitted exactly once when the auction is finalized (after NFT transfer and payment distribution).

Issue: placeBid() emits AuctionSettled before any finalization and even before updating bids[tokenId]. Off-chain consumers treating AuctionSettled as authoritative will record false settlements, trigger premature automations, and end up with duplicate/conflicting settlement records once _executeSale() emits the real AuctionSettled.

function placeBid(uint256 tokenId) external payable isListed(tokenId) {
Listing storage listing = listings[tokenId];
address previousBidder = bids[tokenId].bidder;
uint256 previousBidAmount = bids[tokenId].amount;
require(listing.seller != msg.sender, "Seller cannot bid");
require(listing.auctionEnd == 0 || block.timestamp < listing.auctionEnd, "Auction ended");
// @> BUG: Settlement event during bidding (pre-finalization)
emit AuctionSettled(tokenId, msg.sender, listing.seller, msg.value);
// ... bidding logic continues, not a real settlement ...
// later, _executeSale(tokenId) emits AuctionSettled again (the real one)
}

Risk

Likelihood:

  • Happens on every call to placeBid(); extremely frequent in normal usage.

Indexers/bots that subscribe to AuctionSettled will process false positives consistently.

Impact:

  • Incorrect off-chain state & accounting: Early “settlements” at non-final prices; double entries when _executeSale emits the real event.

Operational/UX breakage: Premature payouts, wrong ownership displays, data reconciliation headaches for integrators.

Proof of Concept

What this proves

  1. A normal bid triggers AuctionSettled even though no settlement happened.

  2. Later, when the auction actually finalizes, _executeSale() emits another AuctionSettled, causing duplicate/conflicting settlement logs


    ### Test 1 — “Settlement” emitted during a regular bid (should never happen)
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
import {Test, console} from "forge-std/Test.sol";
import {BidBeastsNFTMarket} from "../src/BidBeastsNFTMarketPlace.sol";
import {BidBeasts} from "../src/BidBeasts_NFT_ERC721.sol";
contract MisleadingSettlementEvent_BidPhase_PoC is Test {
BidBeastsNFTMarket market;
BidBeasts nft;
address OWNER = address(0xA1);
address SELLER = address(0xB2);
address BIDDER = address(0xC3);
uint256 constant TOKEN_ID = 0;
uint256 constant MIN_PRICE = 1 ether;
// Signature of the settlement event in market
event AuctionSettled(uint256 tokenId, address winner, address seller, uint256 price);
function setUp() public {
// Deploy NFT + market
vm.startPrank(OWNER);
nft = new BidBeasts();
market = new BidBeastsNFTMarket(address(nft));
vm.stopPrank();
// Fund & mint & list
vm.deal(SELLER, 100 ether);
vm.deal(BIDDER, 100 ether);
vm.prank(OWNER);
nft.mint(SELLER);
vm.startPrank(SELLER);
nft.approve(address(market), TOKEN_ID);
market.listNFT(TOKEN_ID, MIN_PRICE, 0);
vm.stopPrank();
}
function test_SettlementEventShouldNotFireDuringPlaceBid_preFix() public {
// PRE-FIX BEHAVIOR: placeBid wrongly emits AuctionSettled
vm.prank(BIDDER);
// Expect the (wrong) settlement event to fire during bidding
vm.expectEmit(true, true, true, true);
emit AuctionSettled(TOKEN_ID, BIDDER, SELLER, MIN_PRICE); // emitted inside placeBid()
market.placeBid{value: MIN_PRICE}(TOKEN_ID);
}
}

Test 2 — Duplicate/conflicting settlement logs (one fake from bidding, one real at settlement)

// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
import {Test} from "forge-std/Test.sol";
import {BidBeastsNFTMarket} from "../src/BidBeastsNFTMarketPlace.sol";
import {BidBeasts} from "../src/BidBeasts_NFT_ERC721.sol";
contract MisleadingSettlementEvent_DuplicateLogs_PoC is Test {
BidBeastsNFTMarket market;
BidBeasts nft;
address OWNER = address(0x11);
address SELLER = address(0x22);
address B1 = address(0x33);
address B2 = address(0x44);
uint256 constant TOKEN_ID = 0;
uint256 constant MIN_PRICE = 1 ether;
event AuctionSettled(uint256 tokenId, address winner, address seller, uint256 price);
function setUp() public {
vm.startPrank(OWNER);
nft = new BidBeasts();
market = new BidBeastsNFTMarket(address(nft));
vm.stopPrank();
vm.deal(SELLER, 100 ether);
vm.deal(B1, 100 ether);
vm.deal(B2, 100 ether);
vm.prank(OWNER);
nft.mint(SELLER);
vm.startPrank(SELLER);
nft.approve(address(market), TOKEN_ID);
market.listNFT(TOKEN_ID, MIN_PRICE, 0);
vm.stopPrank();
}
function test_DuplicateSettlementLogs_preFix() public {
// 1) First bid — wrongly emits AuctionSettled
vm.prank(B1);
vm.expectEmit(true, true, true, true);
emit AuctionSettled(TOKEN_ID, B1, SELLER, MIN_PRICE);
market.placeBid{value: MIN_PRICE}(TOKEN_ID);
// 2) Second bid — wrongly emits AuctionSettled again
uint256 incBid = (MIN_PRICE * 105) / 100; // +5%
vm.prank(B2);
vm.expectEmit(true, true, true, true);
emit AuctionSettled(TOKEN_ID, B2, SELLER, incBid);
market.placeBid{value: incBid}(TOKEN_ID);
// 3) Time travel and settle — real settlement emit (third settlement log)
(, , , uint256 end, ) = market.getListing(TOKEN_ID);
vm.warp(end + 1);
vm.expectEmit(true, true, true, true);
emit AuctionSettled(TOKEN_ID, B2, SELLER, incBid);
market.settleAuction(TOKEN_ID);
// Off-chain systems now observe 3 "settlements": two fake (from bids), one real (from settle)
}
}


Recommended Mitigation

Why this is wrong

  • Event semantics: AuctionSettled should represent the finalization of an auction (NFT transferred, payouts computed). Emitting it in placeBid() lies to indexers, bots, and UIs that treat this event as canonical.

  • Operational risk: Off-chain automations might prematurely pay out, close positions, or update ownership due to a false settlement.

Goals of the fix

  1. Never emit AuctionSettled during bidding.

  2. Emit exactly one AuctionSettled, at the true settlement point inside _executeSale().

  3. Keep bid-phase events (BidPlaced, AuctionExtended) for real-time UX updates.

--- a/src/BidBeastsNFTMarketPlace.sol
+++ b/src/BidBeastsNFTMarketPlace.sol
@@ function placeBid(uint256 tokenId) external payable isListed(tokenId) {
- // Misleading: emits settlement during bidding
- emit AuctionSettled(tokenId, msg.sender, listing.seller, msg.value);
+ // Do NOT emit settlement here; this is still the bidding phase.
// --- Buy Now Logic ---
if (listing.buyNowPrice > 0 && msg.value >= listing.buyNowPrice) {
uint256 salePrice = listing.buyNowPrice;
uint256 overpay = msg.value - salePrice;
// EFFECT: set winner bid to exact sale price (keep consistent)
bids[tokenId] = Bid(msg.sender, salePrice);
listing.listed = false;
if (previousBidder != address(0)) {
_payout(previousBidder, previousBidAmount);
}
// Finalize here — this is the correct place that emits AuctionSettled
_executeSale(tokenId);
if (overpay > 0) {
_payout(msg.sender, overpay);
}
return;
}
// ... rest of bidding logic ...
bids[tokenId] = Bid(msg.sender, msg.value);
if (previousBidder != address(0)) {
_payout(previousBidder, previousBidAmount);
}
- // keep bid-phase telemetry here
+ // Bid-phase telemetry only
emit BidPlaced(tokenId, msg.sender, msg.value);
}
@@ function _executeSale(uint256 tokenId) internal {
- BBERC721.transferFrom(address(this), bid.bidder, tokenId);
+ // (Optional hardening) Consider safeTransferFrom; left as transferFrom per current design
+ 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);
// Single authoritative settlement signal
emit AuctionSettled(tokenId, bid.bidder, listing.seller, bid.amount);
}
Updates

Lead Judging Commences

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

BidBeasts Marketplace: Incorrect Event Emission

placeBid emits AuctionSettled even though the auction hasn’t ended, causing misleading event logs.

Support

FAQs

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