NFT Dealers

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

5% Fee Tier in `_calculateFees` is Unreachable and 3% Tier is Only Partially Reachable Due to `uint32` Price Cap

Author Revealed upon completion

Summary

The _calculateFees function implements a three-tier progressive fee structure (1%, 3%, 5%), but the uint32 type used for the listing price caps the maximum value at ~$4,294.97 USDC. This renders the 5% fee tier (HIGH_FEE_BPS) completely unreachable and limits the 3% tier (MID_FEE_BPS) to prices between $1,000 and ~$4,294.97 instead of the intended $1,000-$10,000 range. The protocol will never collect fees at the intended higher rates.

Impact

The protocol owner deploys with the expectation of collecting 5% fees on high-value sales above $10,000 USDC and 3% fees on sales up to $10,000. In reality, the maximum fee rate achievable is 3% and only on sales between $1,000 and ~$4,294 USDC.

Fee revenue comparison at maximum listable price (~$4,294.97 USDC):

  • Actual fee (3%): ($4,294.97 * 300) / 10,000 = ~$128.85 USDC

  • **Intended fee if $10,000 sale were possible (3%):** ($10,000 * 300) / 10,000 = $300 USDC

  • **Intended fee if $15,000 sale were possible (5%):** ($15,000 * 500) / 10,000 = $750 USDC

The protocol permanently loses all fee revenue above the $128.85 cap per transaction, and the entire HIGH_FEE_BPS code path is dead code.

Additionally, there is a fee cliff at the $1,000 boundary: listing at $1,000 incurs a 1% fee ($10), but listing at $1,001 jumps to 3% ($30.03) — a $20 increase in fees for $1 more in price. This creates a perverse incentive for sellers to price just under $1,000 to avoid the fee jump.

Vulnerability Details

The _calculateFees function operates on uint256 and defines thresholds that exceed uint32 capacity:

// 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 — exceeds uint32 max
// NFTDealers.sol:233-240
function _calculateFees(uint256 _price) internal pure returns (uint256) {
if (_price <= LOW_FEE_THRESHOLD) { // <= $1,000: 1%
return (_price * LOW_FEE_BPS) / MAX_BPS;
} else if (_price <= MID_FEE_THRESHOLD) { // <= $10,000: 3% (partially reachable)
return (_price * MID_FEE_BPS) / MAX_BPS;
}
return (_price * HIGH_FEE_BPS) / MAX_BPS; // > $10,000: 5% (UNREACHABLE)
}

Since listing.price is uint32 (max 4,294,967,295), and MID_FEE_THRESHOLD is 10,000,000,000:

  • The else if branch is reachable only for prices $1,000.01 to ~$4,294.97

  • The final return branch is never reachable_price can never exceed MID_FEE_THRESHOLD

Code Location

  • src/NFTDealers.sol:233-240_calculateFees() with unreachable 5% branch

  • src/NFTDealers.sol:28-29 — Fee thresholds exceeding uint32 capacity

  • src/NFTDealers.sol:60uint32 price in Listing struct (root cause)

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_unreachableFeeTiers_POC -vvv

Test Code

Location: test/NFTDealersTest.t.sol

function test_unreachableFeeTiers_POC() public {
uint256 maxUint32Price = type(uint32).max; // 4,294,967,295
emit log_named_uint("uint32 max (max listable price raw)", maxUint32Price);
emit log_named_uint("Max listable price in USD", maxUint32Price / 1e6);
// Show all three fee tiers and which are reachable
uint256 feeAtMax = nftDealers.calculateFees(maxUint32Price);
emit log_named_uint("Fee at max uint32 price (3% tier)", feeAtMax);
// 1% tier - fully reachable
uint256 feeAt500 = nftDealers.calculateFees(500e6);
emit log_named_uint("Fee at $500 (1% tier)", feeAt500);
uint256 feeAt1000 = nftDealers.calculateFees(1000e6);
emit log_named_uint("Fee at $1,000 (1% tier boundary)", feeAt1000);
// 3% tier - partially reachable
uint256 feeAt1001 = nftDealers.calculateFees(1001e6);
emit log_named_uint("Fee at $1,001 (3% tier)", feeAt1001);
uint256 feeAt4294 = nftDealers.calculateFees(4294e6);
emit log_named_uint("Fee at $4,294 (3% tier - near max)", feeAt4294);
// 5% tier - completely unreachable via listing
// But calculateFees is uint256 so we can call it directly to show what WOULD happen
uint256 feeAt10001 = nftDealers.calculateFees(10_001e6);
emit log_named_uint("Fee at $10,001 (5% tier - UNREACHABLE via listing)", feeAt10001);
uint256 feeAt50000 = nftDealers.calculateFees(50_000e6);
emit log_named_uint("Fee at $50,000 (5% tier - UNREACHABLE via listing)", feeAt50000);
// Prove 5% tier price exceeds uint32 max
assertGt(10_001e6, maxUint32Price, "5% tier threshold exceeds uint32 max");
// Prove max listable price hits 3% not 5%
uint256 expectedMidFee = (maxUint32Price * 300) / 10_000;
assertEq(feeAtMax, expectedMidFee, "Max price hits 3% tier, never 5%");
// Prove fee cliff: $1 increase = $20+ more in fees
uint256 feeCliff = feeAt1001 - feeAt1000;
emit log_named_uint("Fee cliff ($1,000 -> $1,001)", feeCliff);
assertGt(feeCliff, 20e6, "Fee cliff exceeds $20 for $1 price increase");
}

Output

Ran 1 test for test/NFTDealersTest.t.sol:NFTDealersTest
[PASS] test_unreachableFeeTiers_POC() (gas: 49312)
Logs:
uint32 max (max listable price raw): 4294967295
Max listable price in USD: 4294
Fee at max uint32 price (3% tier): 128849018
Fee at $500 (1% tier): 5000000
Fee at $1,000 (1% tier boundary): 10000000
Fee at $1,001 (3% tier): 30030000
Fee at $4,294 (3% tier - near max): 128820000
Fee at $10,001 (5% tier - UNREACHABLE via listing): 500050000
Fee at $50,000 (5% tier - UNREACHABLE via listing): 2500000000
Fee cliff ($1,000 -> $1,001): 20030000
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.60ms (183.34us CPU time)
Fee Tier Price Fee Reachable via Listing?
1% $500 $5.00 Yes
1% (boundary) $1,000 $10.00 Yes
3% $1,001 $30.03 Yes
3% (near max) $4,294 $128.82 Yes (near uint32 cap)
5% $10,001 $500.05 NO - exceeds uint32 max
5% $50,000 $2,500.00 NO - exceeds uint32 max

The fee cliff at $1,000 boundary: $1 price increase causes a $20.03 fee jump.

Mitigation

Change the price field in the Listing struct from uint32 to uint256 (see H-01 mitigation). This allows prices to reach all fee tiers as intended. Additionally, consider implementing marginal/progressive fee rates instead of flat per-bracket rates to eliminate the fee cliff at tier boundaries.

Support

FAQs

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

Give us feedback!