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 3 months 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.

Give us feedback!