NFT Dealers

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

M04. `uint32` price field caps the maximum sale price at \~4,294 USDC, making the HIGH fee tier permanently unreachable

Author Revealed upon completion

Root + Impact

Description

  • The protocol defines three fee tiers: 1% for sales up to 1,000 USDC, 3% for sales up to 10,000 USDC, and 5% for sales above 10,000 USDC.

  • The price field in the Listing struct is declared as uint32. The maximum value of a uint32 is 4,294,967,295, which at 6 decimal USDC precision corresponds to approximately 4,294 USDC. MID_FEE_THRESHOLD is 10,000e6 = 10,000,000,000, which exceeds the uint32 maximum by more than 2x. It is therefore impossible to set a listing price above ~4,294 USDC, so the HIGH fee tier (5%) is never applied.

struct Listing {
address seller;
// @> uint32 max = 4,294,967,295 (~4,294 USDC with 6 decimals)
uint32 price;
address nft;
uint256 tokenId;
bool isActive;
}
// @> MID_FEE_THRESHOLD = 10,000,000,000 > uint32 max — HIGH fee branch is dead code
uint256 private constant MID_FEE_THRESHOLD = 10_000e6;
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;
}
// @> unreachable: no uint32 price can exceed MID_FEE_THRESHOLD
return (_price * HIGH_FEE_BPS) / MAX_BPS;
}

Risk

Likelihood: High

  • This applies to every listing created through the protocol. No listing can ever trigger the HIGH fee tier because list() accepts a uint32 price.

Impact: Medium

  • No user funds are at risk, but the protocol systematically under-collects fees on every sale above 1,000 USDC: it receives 3% instead of the intended maximum of 5%.

  • The HIGH fee tier and the HIGH_FEE_BPS constant are permanently dead code, making the fee schedule specified in the protocol documentation impossible to achieve.

Proof of Concept

The test confirms that uint32 max is strictly less than MID_FEE_THRESHOLD, making the HIGH fee branch unreachable at the type level.

function test_highFeeTierUnreachable() public pure {
uint256 uint32Max = type(uint32).max; // 4,294,967,295 (~4,294 USDC)
uint256 midFeeThreshold = 10_000e6; // 10,000,000,000 (10,000 USDC)
// uint32 max < MID_FEE_THRESHOLD
// Therefore no price stored as uint32 can ever enter the HIGH fee branch
assertLt(uint32Max, midFeeThreshold);
}

The fuzz test below confirms that the fee rate never exceeds 3% (MID tier) for any valid uint32 price by checking that fees are bounded by 3% of the price. If the HIGH tier were reachable, fees could reach 5%.

function testFuzz_feeNeverExceedsMidRate(uint32 price) public view {
price = uint32(bound(price, 1e6, type(uint32).max));
uint256 fees = nftDealers.calculateFees(price);
uint256 midRateCap = (uint256(price) * 300) / 10_000; // 3% of price
// If HIGH fee were reachable, fees could exceed midRateCap for prices > 10,000 USDC.
// Since uint32 prevents that, fees are always <= midRateCap.
assertLe(fees, midRateCap);
}

Recommended Mitigation

Change Listing.price from uint32 to uint256 in list() and updatePrice() so that the full fee tier range can be expressed.

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!