NFT Dealers

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

uint32 price type in Listing struct truncates prices and makes the high fee tier unreachable

Author Revealed upon completion

uint32 price type in Listing struct truncates prices above ~4,294 USDC and makes the 5% fee tier permanently unreachable

Description

The NFTDealers protocol implements a progressive fee system with three tiers: 1% for prices ≤ 1,000 USDC, 3% for prices ≤ 10,000 USDC, and 5% for prices above 10,000 USDC. Listing prices are expected to support the full USDC range to enable high-value NFT sales and collect appropriate fees at each tier.

The price field in the Listing struct is declared as uint32, which has a maximum value of 4,294,967,295. With USDC's 6 decimals, this caps any listing price at approximately 4,294 USDC. Since the MID_FEE_THRESHOLD is 10,000 USDC (10,000,000,000) — which exceeds uint32 max — the 5% HIGH_FEE_BPS tier is permanently unreachable. Additionally, any price value above ~4,294 USDC passed to list() or updatePrice() will silently truncate due to the uint32 cast, storing a drastically wrong price.

struct Listing {
address seller;
@> uint32 price; // max value: 4,294,967,295 (~4,294 USDC with 6 decimals)
address nft;
uint256 tokenId;
bool isActive;
}
@> function list(uint256 _tokenId, uint32 _price) external onlyWhitelisted {
// ...
}
@> function updatePrice(uint256 _listingId, uint32 _newPrice) external onlySeller(_listingId) {
// ...
}
uint256 private constant LOW_FEE_THRESHOLD = 1000e6; // 1,000,000,000 — fits uint32
@> uint256 private constant MID_FEE_THRESHOLD = 10_000e6; // 10,000,000,000 — EXCEEDS uint32 max
function _calculateFees(uint256 _price) internal pure returns (uint256) {
if (_price <= LOW_FEE_THRESHOLD) {
return (_price * LOW_FEE_BPS) / MAX_BPS; // 1%
} else if (_price <= MID_FEE_THRESHOLD) {
return (_price * MID_FEE_BPS) / MAX_BPS; // 3% — max reachable tier
}
@> return (_price * HIGH_FEE_BPS) / MAX_BPS; // 5% — UNREACHABLE via listings
}

Risk

** Likelihood**:

  • Any seller listing an NFT priced above ~4,294 USDC will have their price silently truncated. A 5,000 USDC intended price becomes ~705 USDC after uint32 wrapping (5,000,000,000 mod 2^32 = 705,032,704), creating an immediate arbitrage opportunity for buyers.

  • The protocol's 5% fee tier is dead code from deployment — every single listing, regardless of price, will pay at most 3% fees, reducing protocol revenue on every high-value sale permanently.


    Impact:

  • Sellers lose funds directly through silent price truncation — an NFT intended at 5,000 USDC gets listed at ~705 USDC, an 86% loss that a bot will snipe instantly.

The protocol permanently loses revenue from the 5% fee tier. All sales above 1,000 USDC pay 3% instead of the intended 5% for high-value transactions, compounding revenue loss over the protocol's lifetime.

Proof of Concept

function test_H02_price_silent_truncation() public {
// Alice mints tokenId=1
vm.startPrank(alice);
usdc.approve(address(nftDealers), 20e6);
nftDealers.mintNft();
vm.stopPrank();
// 5,000,000,000 mod 2^32 = 705,032,704 (~705 USDC instead of 5000!)
uint256 intendedPrice = 5000e6;
uint32 truncatedPrice = uint32(intendedPrice);
vm.prank(alice);
nftDealers.list(1, truncatedPrice);
(, uint32 actualPrice,,,) = nftDealers.s_listings(1);
// NFT listed at ~705 USDC instead of intended 5000 USDC
assertTrue(actualPrice < intendedPrice);
assertEq(actualPrice, 705032704);
// High fee tier is permanently unreachable
uint256 feeAtMaxUint32 = nftDealers.calculateFees(uint256(type(uint32).max));
uint256 expectedMidFee = (uint256(type(uint32).max) * 300) / 10_000;
assertEq(feeAtMaxUint32, expectedMidFee); // Still 3%, never 5%
}
test_H02_uint32_price_max — Demonstrates that uint32 max (~4,294 USDC) falls within the MID fee tier, making the HIGH 5% tier permanently unreachable through any listing.
test_H02_price_silent_truncation — Alice intends to list at 5,000 USDC (5,000,000,000), but casting to uint32 wraps it to 705,032,704 (~705 USDC). Her NFT is listed at 86% below her intended price.

Recommended Mitigation

Change the price field to uint256 in the Listing struct, and update the function signatures for list()
and updatePrice() accordinglystruct 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!