NFT Dealers

First Flight #58
Beginner FriendlyFoundry
100 EXP
Submission Details
Impact: low
Likelihood: high

`updatePrice` bypasses `MIN_PRICE`

Author Revealed upon completion

list() enforces MIN_PRICE only at listing creation, but updatePrice() later allows any value greater than zero. As a result, active listings can remain on the marketplace below the configured minimum price.

Description

  • Normal behavior: the configured minimum price should remain enforced throughout the full listing lifecycle, not only when a listing is first created.

function list(uint256 _tokenId, uint32 _price) external onlyWhitelisted {
@> require(_price >= MIN_PRICE, "Price must be at least 1 USDC");
...
}
  • Issue: updatePrice() does not preserve the same rule. It only checks that the new price is greater than zero:

function updatePrice(uint256 _listingId, uint32 _newPrice) external onlySeller(_listingId) {
Listing memory listing = s_listings[_listingId];
uint256 oldPrice = listing.price;
if (!listing.isActive) revert ListingNotActive(_listingId);
@> require(_newPrice > 0, "Price must be greater than 0");
s_listings[_listingId].price = _newPrice;
emit NFT_Dealers_Price_Updated(_listingId, oldPrice, _newPrice);
}

Because of this inconsistency, a seller can create a valid listing and later reduce the price below MIN_PRICE, breaking the stated minimum-price policy for active listings.

Risk

Likelihood:

  • Every active seller can access updatePrice() after creating a valid listing.

  • The update path explicitly allows sub-floor values as long as the price is non-zero.

Impact:

  • Active listings can exist below the configured MIN_PRICE, violating marketplace pricing rules.

  • Integrators and users may rely on an invariant that the protocol does not actually preserve across listing updates.

Proof of Concept

The PoC demonstrates that a seller can create a listing at a valid price and then update it to a value below the configured minimum.

function test_UpdatePrice_AllowsValueBelowMinPrice() public revealed {
uint256 tokenId = 1;
uint32 initialPrice = 1000e6;
uint32 newPrice = 1e5;
mintAndListNFTForTesting(tokenId, initialPrice);
vm.prank(userWithCash);
nftDealers.updatePrice(tokenId, newPrice);
(, uint32 currentPrice,,,) = nftDealers.s_listings(tokenId);
assertEq(currentPrice, newPrice);
assertLt(currentPrice, nftDealers.MIN_PRICE());
}

PoC rationale:
The test shows that the minimum-price rule is enforced only at listing creation, not across the full active-listing lifecycle.

Recommended Mitigation

The same price floor enforced in list() should also be enforced in updatePrice().

function updatePrice(uint256 _listingId, uint32 _newPrice) external onlySeller(_listingId) {
Listing memory listing = s_listings[_listingId];
uint256 oldPrice = listing.price;
if (!listing.isActive) revert ListingNotActive(_listingId);
- require(_newPrice > 0, "Price must be greater than 0");
+ require(_newPrice >= MIN_PRICE, "Price must be at least 1 USDC");
s_listings[_listingId].price = _newPrice;
emit NFT_Dealers_Price_Updated(_listingId, oldPrice, _newPrice);
}

Mitigation rationale:
Applying the same floor check on both creation and update preserves pricing invariants for all active listings.

Support

FAQs

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

Give us feedback!