NFT Dealers

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

Fee Cliff at Tier Boundaries Creates Perverse Pricing Incentive — Sellers Net Less by Pricing Higher

Author Revealed upon completion

Root + Impact

Fee Cliff at Tier Boundaries Creates Perverse Pricing Incentive ,Sellers Net Less by Pricing Higher

Description

  • The _calculateFees function uses a flat-tier fee structure: 1% for prices ≤ 1,000 USDC, 3% for 1,001–10,000 USDC, and 5% above 10,000 USDC. The fee rate jumps abruptly at each boundary.

  • At the 1,000 → 1,001 USDC boundary, a seller pricing at 1,001 USDC pays 30.03 USDC in fees (3%) and nets 970.97 USDC, while pricing at 1,000 USDC costs only 10 USDC (1%) and nets 990 USDC. The seller loses ~19 USDC by pricing 1 USDC higher.

function _calculateFees(uint256 _price) internal view returns (uint256) {
@> if (_price <= LOW_FEE_THRESHOLD) { // ≤ 1,000 USDC: 1%
return (_price * LOW_FEE_BPS) / BPS;
@> } else if (_price <= MID_FEE_THRESHOLD) { // 1,001–10,000 USDC: 3% on FULL price
return (_price * MID_FEE_BPS) / BPS;
} else { // > 10,000 USDC: 5% on FULL price
return (_price * HIGH_FEE_BPS) / BPS;
}
}

Risk

Likelihood:

  • Rational sellers will discover the fee cliff through experimentation and intentionally price at exactly 1,000 USDC to avoid the 3% tier

Market prices naturally cluster around round numbers, making boundary collisions frequent

Impact:

  • Sellers in the 1,001–1,020 USDC range net less than sellers at exactly 1,000 USDC , creating a ~20 USDC dead zone where rational listing is economically disadvantageous

Protocol fee revenue is suboptimal as sellers artificially price below tier boundaries

  • Same pattern exists at the 10,000 USDC boundary (unreachable per NM-004, but applies once the uint32 price is fixed)

Proof of Concept

function test_NM007_FeeCliffPerverseIncentive() public {
// At 1,000 USDC: 1% fee = 10 USDC, seller nets 990 USDC
uint256 feesAt1000 = nftDealers.calculateFees(1_000e6);
uint256 sellerNetsAt1000 = 1_000e6 - feesAt1000;
assertEq(feesAt1000, 10e6);
assertEq(sellerNetsAt1000, 990e6);
// At 1,001 USDC: 3% fee = 30.03 USDC, seller nets 970.97 USDC
uint256 feesAt1001 = nftDealers.calculateFees(1_001e6);
uint256 sellerNetsAt1001 = 1_001e6 - feesAt1001;
assertEq(feesAt1001, 30_030_000);
assertEq(sellerNetsAt1001, 970_970_000);
// Seller nets ~19 USDC LESS by pricing 1 USDC higher
assertGt(sellerNetsAt1000, sellerNetsAt1001);
// Breakeven: need to price above ~1,021 USDC to net more than 990 USDC
uint256 feesAt1021 = nftDealers.calculateFees(1_021e6);
uint256 sellerNetsAt1021 = 1_021e6 - feesAt1021;
assertGt(sellerNetsAt1021, sellerNetsAt1000);
}

Recommended Mitigation

function _calculateFees(uint256 _price) internal view returns (uint256) {
- if (_price <= LOW_FEE_THRESHOLD) {
- return (_price * LOW_FEE_BPS) / BPS;
- } else if (_price <= MID_FEE_THRESHOLD) {
- return (_price * MID_FEE_BPS) / BPS;
- } else {
- return (_price * HIGH_FEE_BPS) / BPS;
- }
+ // Progressive fee: apply each rate only to the portion within its bracket
+ if (_price <= LOW_FEE_THRESHOLD) {
+ return (_price * LOW_FEE_BPS) / BPS;
+ } else if (_price <= MID_FEE_THRESHOLD) {
+ uint256 lowPortion = (LOW_FEE_THRESHOLD * LOW_FEE_BPS) / BPS;
+ uint256 midPortion = ((_price - LOW_FEE_THRESHOLD) * MID_FEE_BPS) / BPS;
+ return lowPortion + midPortion;
+ } else {
+ uint256 lowPortion = (LOW_FEE_THRESHOLD * LOW_FEE_BPS) / BPS;
+ uint256 midPortion = ((MID_FEE_THRESHOLD - LOW_FEE_THRESHOLD) * MID_FEE_BPS) / BPS;
+ uint256 highPortion = ((_price - MID_FEE_THRESHOLD) * HIGH_FEE_BPS) / BPS;
+ return lowPortion + midPortion + highPortion;
+ }
}

Support

FAQs

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

Give us feedback!