NFT Dealers

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

Listing `price` Stored as `uint32` Caps Maximum Sale Price at ~$4,294 USDC, Breaking Protocol Fee Structure

Author Revealed upon completion

Summary

The price field in the Listing struct is declared as uint32, which has a maximum value of 4,294,967,295. Since USDC uses 6 decimals, this limits the maximum listing price to approximately $4,294.97 USDC. This directly breaks the protocol's progressive fee logic, as the fee thresholds extend to $10,000 USDC — a value that can never be represented in a uint32.

Impact

The protocol's fee structure defines three tiers: 1% for prices up to $1,000, 3% for prices up to $10,000, and 5% for prices above $10,000. The uint32 price cap at ~$4,294.97 means the 5% fee tier is completely unreachable and the 3% tier is only partially usable (capped at $4,294.97 instead of $10,000).

Consider a high-value NFT collection where pieces are worth $5,000-$50,000 USDC. No seller can list above ~$4,294 USDC. If they attempt to pass a larger value, Solidity 0.8+ will silently revert at the function signature level with no meaningful error message. The protocol permanently loses all fee revenue that would have come from higher-priced sales, and the owner's intended fee structure is broken from deployment.

Additionally, in Solidity 0.8+, passing a value exceeding uint32 max to the list() or updatePrice() functions causes a revert at the ABI decoding level — providing no useful error message to the user.

Vulnerability Details

The Listing struct uses uint32 for the price field:

// NFTDealers.sol:58-64
struct Listing {
address seller;
uint32 price; // Max value: 4,294,967,295 = ~$4,294.97 USDC
address nft;
uint256 tokenId;
bool isActive;
}

But the fee thresholds are defined as uint256 with values far exceeding the uint32 cap:

// NFTDealers.sol:28-29
uint256 private constant LOW_FEE_THRESHOLD = 1000e6; // $1,000 USDC
uint256 private constant MID_FEE_THRESHOLD = 10_000e6; // $10,000 USDC

The list() function signature enforces the uint32 constraint:

// NFTDealers.sol:131
function list(uint256 _tokenId, uint32 _price) external onlyWhitelisted {

Any call with _price > 4,294,967,295 reverts before the function body executes due to Solidity 0.8+ overflow protection on type casting.

Code Location

  • src/NFTDealers.sol:60uint32 price in the Listing struct

  • src/NFTDealers.sol:28-29 — Fee thresholds defined as uint256 up to $10,000

  • src/NFTDealers.sol:131list() function with uint32 _price parameter

  • src/NFTDealers.sol:189updatePrice() function with uint32 _newPrice parameter

  • src/NFTDealers.sol:233-240_calculateFees() with unreachable branches

Proof of Concept

POC Guide

  1. Copy the test code below

  2. Paste it into test/NFTDealersTest.t.sol

  3. Run: forge test --mt test_uint32PriceCap_POC -vvv

Test Code

Location: test/NFTDealersTest.t.sol

function test_uint32PriceCap_POC() public revealed whitelisted {
// Demonstrate that uint32 price cap prevents listings above ~$4,294 USDC
uint256 maxUint32 = type(uint32).max; // 4,294,967,295
uint256 maxPriceInDollars = maxUint32 / 1e6;
emit log_named_uint("uint32 max value", maxUint32);
emit log_named_uint("Max listable price in USD", maxPriceInDollars);
// Mint an NFT to list
vm.startBroadcast(userWithCash);
usdc.approve(address(nftDealers), 20e6);
nftDealers.mintNft();
vm.stopBroadcast();
// Listing at max uint32 succeeds
vm.prank(userWithCash);
nftDealers.list(1, type(uint32).max);
(, uint32 price,,,) = nftDealers.s_listings(1);
emit log_named_uint("Listed price (raw)", price);
emit log_named_uint("Listed price in USD", uint256(price) / 1e6);
// Cancel so we can try again
vm.prank(userWithCash);
nftDealers.cancelListing(1);
// Attempt to list at $5,000 USDC (5000e6 = 5,000,000,000) which exceeds uint32 max
// Must use low-level call since compiler won't allow uint256 -> uint32 directly
vm.prank(userWithCash);
uint256 priceAboveCap = 5000e6; // $5,000 USDC - exceeds uint32 max of 4,294,967,295
emit log_named_uint("Attempting to list at price", priceAboveCap);
emit log_named_uint("uint32 max", type(uint32).max);
emit log_named_uint("Price exceeds uint32 max by", priceAboveCap - type(uint32).max);
(bool success,) = address(nftDealers).call(
abi.encodeWithSelector(NFTDealers.list.selector, uint256(1), priceAboveCap)
);
assertFalse(success, "Listing at $5,000 USDC reverted - price exceeds uint32 max");
emit log_string("CONFIRMED: Listing at $5,000 USDC reverts - uint32 overflow");
// Show fee tier impact
uint256 feeAtMax = nftDealers.calculateFees(maxUint32);
uint256 feeAt1000 = nftDealers.calculateFees(1000e6);
uint256 feeAt1001 = nftDealers.calculateFees(1001e6);
emit log_named_uint("Fee at max uint32 price (3% tier)", feeAtMax);
emit log_named_uint("Fee at $1,000 (1% tier)", feeAt1000);
emit log_named_uint("Fee at $1,001 (3% tier)", feeAt1001);
// MID_FEE_THRESHOLD ($10,000) is unreachable
assertGt(10_000e6, maxUint32, "MID_FEE_THRESHOLD exceeds uint32 max - 5% tier unreachable");
// Max price falls in 3% tier, not 5%
uint256 expectedMidFee = (maxUint32 * 300) / 10_000;
assertEq(feeAtMax, expectedMidFee, "Max price hits 3% tier, never 5%");
}

Output

Ran 1 test for test/NFTDealersTest.t.sol:NFTDealersTest
[PASS] test_uint32PriceCap_POC() (gas: 288564)
Logs:
uint32 max value: 4294967295
Max listable price in USD: 4294
Listed price (raw): 4294967295
Listed price in USD: 4294
Attempting to list at price: 5000000000
uint32 max: 4294967295
Price exceeds uint32 max by: 705032705
CONFIRMED: Listing at $5,000 USDC reverts - uint32 overflow
Fee at max uint32 price (3% tier): 128849018
Fee at $1,000 (1% tier): 10000000
Fee at $1,001 (3% tier): 30030000
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 2.25ms (795.44us CPU time)
Metric Value
uint32 max value 4,294,967,295
Max listable price $4,294 USDC
Listing at $5,000 USDC REVERTS - exceeds uint32 max by 705,032,705
Fee at max price (3% tier) $128.85 USDC
Fee at $1,000 (1% tier) $10.00 USDC
Fee at $1,001 (3% tier - fee cliff) $30.03 USDC
MID_FEE_THRESHOLD ($10,000) Exceeds uint32 max - 5% tier unreachable

Mitigation

Change the price field in the Listing struct to uint256, and update all function signatures accordingly:

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!