NFT Dealers

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

updatePrice Missing MIN_PRICE Validation Allows Sub-Minimum Listings

Author Revealed upon completion

updatePrice Missing MIN_PRICE Validation Allows Sub-Minimum Listings

Description

The protocol enforces a minimum listing price of MIN_PRICE as a procol-wide invariant at creation time. The updatePrice function is the only post-creation mutation point for the price field in the Listing struct, yet it validates only price above zero and completely omits the MIN_PRICE check. As a result a seller can reduce the listing price to as little as 1 wei of USDC after it has been created.

// updatePrice() — only validates > 0, MIN_PRICE invariant broken
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);
// @> missing: require(_newPrice >= MIN_PRICE, "Price must be at least 1 USDC");
require(_newPrice > 0, "Price must be greater than 0");
s_listings[_listingId].price = _newPrice;
emit NFT_Dealers_Price_Updated(_listingId, oldPrice, _newPrice);
}

Risk

Likelihood: High

  • updatePrice() is a standard seller action, callbacl at any time on any active listing with no preconditions beyond being the seller.

Impact: High

  • Every downstream function that reads the price, silently inherits the broken invariant with no guards of their own. This affects protocol trust and protocol fees.

Proof of Concept

function test_UpdatePrice_CanSetPriceBelowMin() public {
// Initialize test
vm.startBroadcast(owner);
nftDealers.whitelistWallet(seller);
nftDealers.revealCollection();
vm.stopBroadcast();
// Assert state after initialization
assertEq(nftDealers.whitelistedUsers(seller), true);
assertEq(nftDealers.isCollectionRevealed(), true);
// Mint
vm.startBroadcast(seller);
usdc.approve(address(nftDealers), USDC_COLLATERAL);
nftDealers.mintNft();
vm.stopBroadcast();
// Assert state after mint
uint256 tokenId = 1;
assertEq(nftDealers.ownerOf(tokenId), seller);
assertEq(nftDealers.collateralForMinting(tokenId), USDC_COLLATERAL);
assertEq(usdc.balanceOf(address(nftDealers)), INITIAL_USER_BALANCE + USDC_COLLATERAL);
assertEq(usdc.balanceOf(seller), INITIAL_USER_BALANCE - USDC_COLLATERAL);
// List NFT
uint32 sellingPrice = 40e6;
vm.prank(seller);
nftDealers.list(tokenId, sellingPrice);
// Assert state after list
(address _seller, uint32 _price, address _nft, uint256 _tokenId, bool _isActive) =
nftDealers.s_listings(tokenId);
assertEq(_seller, seller);
assertEq(_price, sellingPrice);
assertEq(_nft, address(nftDealers));
assertEq(_tokenId, tokenId);
assertEq(_isActive, true);
assertEq(nftDealers.ownerOf(tokenId), seller);
// Update the listing price
uint32 priceBelowMin = 1e5;
vm.prank(seller);
nftDealers.updatePrice(tokenId, priceBelowMin);
// Assert state after price update
(, uint32 _newPrice,,,) = nftDealers.s_listings(tokenId);
// Price updated
assertGt(_price, _newPrice);
// Price is below protocol min price
assertGt(nftDealers.MIN_PRICE(), _newPrice);
}

Recommended Mitigation

Replace the > 0 check in updatePrice() with the same >= MIN_PRICE guard already used in list(). This restores the invariant while updating the listing price.

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);
}

Support

FAQs

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

Give us feedback!