Bid Beasts

First Flight #49
Beginner FriendlyFoundrySolidityNFT
100 EXP
View results
Submission Details
Impact: medium
Likelihood: high
Invalid

Auction Plutocracy: Buy-Now Feature Replaces Competition with Wealth-Based Advantage

Description

Normal Behavior

The documentation describes a fair, open market where the highest bidder wins through competitive bidding, enabling true price discovery and equal opportunity for all participants.

Issue

The implementation in code allows leveraging _buyNowPrice in the function `listNft()`, which sabotages "Price Discovery". The final price is no longer discovered through the competition of bidders and does not reflect the true maximum a buyer was willing to pay. Additionally, the function `placeBid()` allows bidders to buy instantly at an already fixed final price. Therefore, any participant can bypass the entire competitive bidding process at any time by paying a fixed price aka _buyNowPrice.

struct Listing {
address seller;
uint256 minPrice;
@> uint256 buyNowPrice;
uint256 auctionEnd;
bool listed;
}
@> function listNFT(uint256 tokenId, uint256 _minPrice, uint256 _buyNowPrice) external {
require(BBERC721.ownerOf(tokenId) == msg.sender, "Not the owner");
require(_minPrice >= S_MIN_NFT_PRICE, "Min price too low");
@> if (_buyNowPrice > 0) {
@> require(_minPrice <= _buyNowPrice, "Min price cannot exceed buy now price");
@> }
BBERC721.transferFrom(msg.sender, address(this), tokenId);
listings[tokenId] = Listing({
seller: msg.sender,
minPrice: _minPrice,
@> buyNowPrice: _buyNowPrice,
auctionEnd: 0, // Timer starts only after the first valid bid.
listed: true
});
emit NftListed(tokenId, msg.sender, _minPrice, _buyNowPrice);
}
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");
// auctionEnd == 0 => no bids yet => allowed
// auctionEnd > 0 and block.timestamp >= auctionEnd => auction ended => block
require(listing.auctionEnd == 0 || block.timestamp < listing.auctionEnd, "Auction ended");
@> // --- 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);
@> }
// NOTE: using internal finalize to do transfer/payouts. _executeSale will assume bids[tokenId] is the final winner.
_executeSale(tokenId);
@> // Refund overpay (if any) to buyer
@> if (overpay > 0) {
@> _payout(msg.sender, overpay);
@> }
@> return;
}

Risk

Likelihood:

  • The function listNft() with `_buyNowPrice` feature is a core function, not an edge case. This scenario will occur in every auction where:

    1. A seller sets a `_buyNowPrice`

    2. Any bidder is willing and able to pay that price before auction end This will happen frequently on an active platform, systematically short-circuiting the competitive bidding process.

Impact:

  • Sabotages Price Discovery: The final price no longer reflects true market demand.

  • Creates Auction Plutocracy: The mechanism allows the wealthiest participant to instantly win, rendering the competitive bidding process meaningless for others.

Recommended Mitigation

Elimination of Buy-Now Feature The proposed code changes completely remove the buy-now functionality to restore the auction system's core principles of competitive bidding and fair price discovery.

  1. Data Structure Simplification
    Removed: `buyNowPrice` from the `Listing` struct

  2. function listNft() modification

    Modified:`listNFT` function signature

    Removed: `_buyNowPrice` parameter

  3. Core Logic Removal in function placeBid()

    Deleted: Entire buy-now conditional block in `placeBid` function

    Removed Logic: Instant purchase capability when `msg.value >= listing.buyNowPrice`


// Remove the buyNowPrice variable
struct Listing {
address seller;
uint256 minPrice;
- uint256 buyNowPrice;
uint256 auctionEnd;
bool listed;
}
// Remove the leverage to set the final price
+ function listNFT(uint256 tokenId, uint256 _minPrice) external {
- function listNFT(uint256 tokenId, uint256 _minPrice, uint256 _buyNowPrice) external {
require(BBERC721.ownerOf(tokenId) == msg.sender, "Not the owner");
require(_minPrice >= S_MIN_NFT_PRICE, "Min price too low");
- if (_buyNowPrice > 0) {
- require(_minPrice <= _buyNowPrice, "Min price cannot exceed buy now price");
- }
BBERC721.transferFrom(msg.sender, address(this), tokenId);
listings[tokenId] = Listing({
seller: msg.sender,
minPrice: _minPrice,
- buyNowPrice: _buyNowPrice,
auctionEnd: 0, // Timer starts only after the first valid bid.
listed: true
});
+ emit NftListed(tokenId, msg.sender, _minPrice);
- emit NftListed(tokenId, msg.sender, _minPrice, _buyNowPrice);
}
// Remove the leverage for immidiate winning
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");
// auctionEnd == 0 => no bids yet => allowed
// auctionEnd > 0 and block.timestamp >= auctionEnd => auction ended => block
require(listing.auctionEnd == 0 || block.timestamp < listing.auctionEnd, "Auction ended");
// --- 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);
- }
// NOTE: using internal finalize to do transfer/payouts. _executeSale will assume bids[tokenId] is the final winner.
- _executeSale(tokenId);
- // Refund overpay (if any) to buyer
- if (overpay > 0) {
- _payout(msg.sender, overpay);
- }
- return;
- }
require(msg.sender != previousBidder, "Already highest bidder");
emit AuctionSettled(tokenId, msg.sender, listing.seller, msg.value);
// --- Regular Bidding Logic ---
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 {
requiredAmount = (previousBidAmount / 100) * (100 + S_MIN_BID_INCREMENT_PERCENTAGE);
require(msg.value >= requiredAmount, "Bid not high enough");
uint256 timeLeft = 0;
if (listing.auctionEnd > block.timestamp) {
timeLeft = listing.auctionEnd - block.timestamp;
}
if (timeLeft < S_AUCTION_EXTENSION_DURATION) {
listing.auctionEnd = listing.auctionEnd + S_AUCTION_EXTENSION_DURATION;
emit AuctionExtended(tokenId, listing.auctionEnd);
}
}
// EFFECT: update highest bid
bids[tokenId] = Bid(msg.sender, msg.value);
if (previousBidder != address(0)) {
_payout(previousBidder, previousBidAmount);
}
emit BidPlaced(tokenId, msg.sender, msg.value);
}
Updates

Lead Judging Commences

cryptoghost Lead Judge about 1 month ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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