NFT Dealers

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

[C-3] uint32 Price Field Silently Truncates — High-Value NFTs Mispriced and High Fee Tier Unreachable

Author Revealed upon completion

Root + Impact

Description

  • The fee structure defines three tiers based on price thresholds, with the highest tier activating above 10,000 USDC. The Listing struct stores price as uint32, and list() accepts uint32 _price.

  • uint32 has a maximum value of 4,294,967,295, which at USDC's 6-decimal precision equals ~4,294 USDC. Any NFT priced above this silently truncates when cast to uint32. Additionally, the MID and HIGH fee thresholds (10,000 USDC = 10,000,000,000 in base units) exceed uint32 max, making the 5% high fee tier permanently unreachable and the 3% mid fee tier partially unreachable.

// src/NFTDealers.sol
@> uint256 private constant MID_FEE_THRESHOLD = 10_000e6; // 10B — exceeds uint32 max
struct Listing {
address seller;
@> uint32 price; // max ~4,294 USDC with 6 decimals
...
}
@> function list(uint256 _tokenId, uint32 _price) external onlyWhitelisted {

Risk

Likelihood:

  • Any NFT priced above ~4,294 USDC triggers truncation silently. Solidity does not revert on uint narrowing in explicit casts.

  • The HIGH_FEE tier is never applied, regardless of price, permanently reducing protocol revenue.

Impact:

  • Sellers believe they listed at a high price; buyers pay the truncated (much lower) price, and the seller loses funds.

  • Protocol collects lower fees than designed, and the 5% tier is dead code.

Proof of Concept

A seller intends to list at 5,000 USDC (5_000_000_000 base units). Passing this as uint32 silently wraps it to ~705 USDC. The stored price is far below the intended amount.

function testPriceTruncation() public revealed {
vm.prank(owner);
nftDealers.whitelistWallet(userWithCash);
vm.startPrank(userWithCash);
usdc.approve(address(nftDealers), 20e6);
nftDealers.mintNft();
// Intended price: 5000 USDC = 5_000_000_000
// uint32 max = 4_294_967_295
// 5_000_000_000 as uint32 = 705_032_704 (~705 USDC) — silently truncated
uint32 truncatedPrice = uint32(5_000e6);
nftDealers.list(1, truncatedPrice);
vm.stopPrank();
(, uint32 storedPrice,,,) = nftDealers.s_listings(1);
// storedPrice is 705_032_704, not 5_000_000_000
assert(storedPrice != uint32(5_000e6));
}

Recommended Mitigation

Change the price field and all related function parameters to uint256 to match the full USDC amount space and make all fee tiers reachable.

struct Listing {
address seller;
- uint32 price;
+ uint256 price;
address nft;
uint256 tokenId;
bool isActive;
}
-function list(uint256 _tokenId, uint32 _price) external onlyWhitelisted {
+function list(uint256 _tokenId, uint256 _price) external onlyWhitelisted {
-function updatePrice(uint256 _listingId, uint32 _newPrice) external onlySeller(_listingId) {
+function updatePrice(uint256 _listingId, uint256 _newPrice) external onlySeller(_listingId) {

Support

FAQs

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

Give us feedback!