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) {
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;
}
}
Risk
Likelihood:
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
Proof of Concept
function test_NM007_FeeCliffPerverseIncentive() public {
uint256 feesAt1000 = nftDealers.calculateFees(1_000e6);
uint256 sellerNetsAt1000 = 1_000e6 - feesAt1000;
assertEq(feesAt1000, 10e6);
assertEq(sellerNetsAt1000, 990e6);
uint256 feesAt1001 = nftDealers.calculateFees(1_001e6);
uint256 sellerNetsAt1001 = 1_001e6 - feesAt1001;
assertEq(feesAt1001, 30_030_000);
assertEq(sellerNetsAt1001, 970_970_000);
assertGt(sellerNetsAt1000, sellerNetsAt1001);
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;
+ }
}