NFT Dealers

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

`uint32` price type caps listings at ~4294 USDC, making the 5% fee tier permanently unreachable

Author Revealed upon completion

Description

The protocol defines three fee tiers based on sale price: 1% below 1,000 USDC, 3% between 1,000 and 10,000 USDC, and 5% above 10,000 USDC. High-value sales should pay the highest fee rate to generate more revenue for the protocol.

Listing.price is declared as uint32, which has a maximum value of ~4294 USDC (4,294,967,295 with 6 decimals). The MID_FEE_THRESHOLD that triggers the 5% tier is 10,000 USDC. Since no uint32 value can reach 10,000 USDC, the HIGH_FEE_BPS tier is dead code and can never be applied.

@> uint256 private constant MID_FEE_THRESHOLD = 10_000e6; // 10,000 USDC — unreachable
struct Listing {
address seller;
@> uint32 price; // max ~4294 USDC — cannot reach MID_FEE_THRESHOLD
address nft;
uint256 tokenId;
bool isActive;
}
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% — always hits this
}
@> return (_price * HIGH_FEE_BPS) / MAX_BPS; // 5% — dead code
}

Risk

Likelihood:

  • Every listing is constrained to uint32 max (~4294 USDC). No seller can set a price above this, regardless of the NFT's actual value. The 5% fee tier is unreachable on every sale.

  • The list() function accepts uint32 _price as a parameter, so the truncation happens at the contract interface level — there is no workaround.

Impact:

  • The protocol loses 2% in fees on every high-value sale. On a ~4294 USDC sale, the fee is 128 USDC (3%) instead of 214 USDC (5%) — a loss of ~85 USDC per sale.

  • NFTs worth more than ~4294 USDC cannot be listed at their actual value, forcing sellers to use external channels or accept underpriced listings.

Proof of Concept

The maximum possible listing price is type(uint32).max (~4294 USDC). Calling calculateFees with this value returns the 3% fee (MID_FEE_BPS), not 5% (HIGH_FEE_BPS). The 5% tier requires a price above 10,000 USDC, but no uint32 can represent that value. The HIGH_FEE_BPS branch is dead code that can never execute.

function test_poc_highFeeTierUnreachable() public view {
// uint32 max is ~4294 USDC — well below the 10,000 USDC threshold
uint32 maxPrice = type(uint32).max;
assertLt(uint256(maxPrice), 10_000e6);
// Fee at absolute maximum price
uint256 feeAtMaxPrice = nftDealers.calculateFees(maxPrice);
// Always 3%, never 5%
uint256 expectedMidFee = (uint256(maxPrice) * 300) / 10_000; // 3%
uint256 expectedHighFee = (uint256(maxPrice) * 500) / 10_000; // 5%
assertEq(feeAtMaxPrice, expectedMidFee); // always 3%
assertFalse(feeAtMaxPrice == expectedHighFee); // never 5%
// Revenue loss: ~85 USDC per max-price sale
uint256 revenueLoss = expectedHighFee - expectedMidFee;
assertGt(revenueLoss, 85e6);
}

Recommended Mitigation

Change Listing.price from uint32 to uint256. This allows listings at any price and makes all fee tiers reachable. The list() parameter type must also be updated to match.

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 {

Support

FAQs

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

Give us feedback!