NFT Dealers

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

`uint32` price type makes the 3% and 5% fee tiers permanently unreachable

Author Revealed upon completion

Description

The protocol defines three progressive fee tiers as its core economic mechanic: 1% for sale prices up to 1,000 USDC, 3% for prices up to 10,000 USDC, and 5% above 10,000 USDC. The Listing struct stores price as uint32, and list() accepts a uint32 _price parameter.

USDC has 6 decimals, so 1 USDC = 1,000,000 units. A uint32 holds a maximum value of 4,294,967,295 units, equivalent to approximately 4,294 USDC. The MID fee threshold is 10,000,000,000 units (10,000 USDC) and the HIGH threshold is anything above — both exceed uint32 maximum. Since the function parameter itself is uint32, any value above ~4,294 USDC cannot be passed by a caller. The 3% and 5% fee tiers are permanently dead code.

struct Listing {
address seller;
// @> uint32 max = 4,294,967,295 units ≈ 4,294 USDC (6 decimals)
// @> MID_FEE_THRESHOLD = 10,000e6 = 10,000,000,000 units → exceeds uint32 max
// @> HIGH fee tier: above 10,000 USDC → exceeds uint32 max
uint32 price;
address nft;
uint256 tokenId;
bool isActive;
}
uint256 private constant LOW_FEE_THRESHOLD = 1000e6; // 1,000 USDC — reachable
uint256 private constant MID_FEE_THRESHOLD = 10_000e6; // 10,000 USDC — UNREACHABLE
// HIGH tier: above 10,000 USDC — UNREACHABLE
// @> Parameter is uint32 — no caller can pass a value above ~4,294 USDC
function list(uint256 _tokenId, uint32 _price) external onlyWhitelisted { ... }
// @> Same truncation applies to updatePrice
function updatePrice(uint256 _listingId, uint32 _newPrice) external onlySeller(_listingId) { ... }

Root Cause

File: src/NFTDealers.sol

The root cause is a type mismatch between the fee threshold constants and the price storage type. The fee thresholds are defined as uint256 with values up to 10_000e6 (10 billion units), but Listing.price and both function parameters that write to it are declared as uint32, which saturates at 4,294,967,295 units (~4,294 USDC). The two halves of the fee system were designed independently without verifying that the price type can actually represent the values the thresholds require.

Risk

Likelihood:

  • Any user attempting to list an NFT for more than 4,294 USDC finds the call reverts at ABI encoding since the parameter type is uint32 and the value overflows.

  • The protocol operates at a perpetual 1% fee regardless of listing price for the entire lifetime of the deployment, as no value expressible in uint32 crosses either the MID or HIGH threshold.

Impact:

  • The 3% and 5% fee tiers are completely unreachable; the protocol collects only 1% fees on all transactions regardless of sale price, undermining its progressive fee design.

  • High-value NFT listings are hard-capped at approximately 4,294 USDC, severely limiting the protocol's utility for any valuable asset class.

Proof of Concept

function test_uint32PriceCapsMaxListingAndFees() public revealed {
vm.prank(owner);
nftDealers.whitelistWallet(alice);
usdc.mint(alice, 20e6);
vm.startPrank(alice);
usdc.approve(address(nftDealers), 20e6);
nftDealers.mintNft();
// Any price above uint32 max cannot be passed — reverts at ABI encoding
// 5_000e6 = 5_000_000_000 > 4_294_967_295 (uint32 max) — MID fee tier unreachable
// nftDealers.list(1, 5_000e6); // <-- cannot compile / always reverts
// Highest expressible price still only attracts 1% fee, never 3%
nftDealers.list(1, uint32(4_294e6));
vm.stopPrank();
(, uint32 price,,,) = nftDealers.s_listings(1);
uint256 fee = nftDealers.calculateFees(price);
// Fee is always LOW_FEE_BPS (1%) — MID and HIGH tiers can never be reached
assertEq(fee, (uint256(price) * 100) / 10_000);
}

Recommended Mitigation

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 {
require(_price >= MIN_PRICE, "Price must be at least 1 USDC");
...
}
- 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!