NFT Dealers

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

`uint32` price type caps listings at ~4294 USDC and makes 5% fee tier unreachable

Author Revealed upon completion

Description

  • The protocol implements a tiered fee structure: 1% for prices up to 1,000 USDC, 3% for up to 10,000 USDC, and 5% above 10,000 USDC. USDC uses 6 decimal places, so 10_000 USDC = 10_000e6 = 10_000_000_000.

  • Listing.price is declared as uint32, whose maximum value is 4,294,967,295 (~4294 USDC with 6 decimals). This is less than MID_FEE_THRESHOLD (10_000e6 = 10_000_000_000), making the HIGH_FEE_BPS (5%) tier permanently unreachable. Furthermore, list() and updatePrice() accept uint32 _price, silently truncating any value above uint32.max passed by the caller. The marketplace cannot support NFT sales above ~4294 USDC.

uint256 private constant MID_FEE_THRESHOLD = 10_000e6; // 10_000_000_000
uint32 private constant HIGH_FEE_BPS = 500; // 5%, unreachable
struct Listing {
address seller;
@> uint32 price; // max 4_294_967_295 < MID_FEE_THRESHOLD (10_000_000_000)
address nft;
uint256 tokenId;
bool isActive;
}
@> function list(uint256 _tokenId, uint32 _price) external onlyWhitelisted { ... }
@> function updatePrice(uint256 _listingId, uint32 _newPrice) external onlySeller(_listingId) { ... }
function _calculateFees(uint256 _price) internal pure returns (uint256) {
if (_price <= LOW_FEE_THRESHOLD) {
return (_price * LOW_FEE_BPS) / MAX_BPS;
} else if (_price <= MID_FEE_THRESHOLD) {
return (_price * MID_FEE_BPS) / MAX_BPS;
}
@> return (_price * HIGH_FEE_BPS) / MAX_BPS; // dead code, never reached
}

Risk

Likelihood:

  • This is a constant design flaw present from deployment — it affects every listing on the platform from day one.

  • Any seller trying to list an NFT for more than ~4294 USDC is silently limited or reverted, depending on how the caller encodes the value.

Impact:

  • The marketplace cannot support high-value NFT sales above ~4294 USDC, severely limiting the platform's use case.

  • The 5% fee tier is permanently dead code — the protocol consistently under-collects fees on all sales in the 1000–4294 USDC range (charged 3% instead of up to 5%).

Proof of Concept

Paste this function inside NFTDealersTest in test/NFTDealersTest.t.sol and run:
forge test --match-test testPoC_Uint32PriceHighFeeTierUnreachable -vvvv

function testPoC_Uint32PriceHighFeeTierUnreachable() public view {
uint256 maxListablePrice = uint256(type(uint32).max); // 4_294_967_295 ~ 4294 USDC
uint256 midFeeThreshold = 10_000e6; // 10_000_000_000
// 1. The maximum price storable in Listing.price is below MID_FEE_THRESHOLD
assertLt(maxListablePrice, midFeeThreshold);
// 2. At the highest possible listing price, fees are still 3% (MID), never 5% (HIGH)
uint256 fees = nftDealers.calculateFees(maxListablePrice);
uint256 expectedMidFee = (maxListablePrice * 300) / 10_000; // 3%
uint256 expectedHighFee = (maxListablePrice * 500) / 10_000; // 5%
assertEq(fees, expectedMidFee); // 3% applied
assertNotEq(fees, expectedHighFee); // 5% never reached
// 3. Attempting to pass 15_000 USDC as uint32 silently truncates
uint256 desiredPrice = 15_000e6; // 15_000_000_000
uint32 truncatedPrice = uint32(desiredPrice); // silent wrap: 15_000_000_000 % 2^32 = 1_215_752_192
assertNotEq(uint256(truncatedPrice), desiredPrice); // stored price is wrong
}

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 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!