OrderBook

First Flight #43
Beginner FriendlySolidity
100 EXP
Submission Details
Severity: low
Valid

Fee Precision Loss + Protocol Revenue Loss

Author Revealed upon completion

Description

  • The protocol should collect a 3% fee on all transactions to generate revenue for the platform.

  • The fee calculation uses integer division which rounds down to zero for small transactions, causing the protocol to lose fee revenue on orders below a certain threshold.

function buyOrder(uint256 _orderId) public {
// ...
uint256 protocolFee = (order.priceInUSDC * FEE) / PRECISION; // FEE=3, PRECISION=100
uint256 sellerReceives = order.priceInUSDC - protocolFee;
// ...
}

Risk

Likelihood:

  • When users place orders with priceInUSDC values less than 34 USDC (since 33 * 3 / 100 = 0)

  • When the contract is used for micro-transactions or testing with small amounts

  • When users deliberately place many small orders to avoid fees

Impact:

  • Protocol loses fee revenue on a significant portion of small transactions

  • Users can exploit this by splitting large orders into many small fee-free orders

  • Economic model of the protocol becomes unsustainable for small-value trading

Proof of Concept

Fee Avoidance Exploit: This demonstrates how users can avoid protocol fees by splitting large orders into many small ones.

function testZeroFeeExploit() public {
// Alice creates multiple small sell orders to avoid fees
vm.startPrank(alice);
weth.approve(address(book), 1e18);
// Create 10 orders of 33 USDC each (total 330 USDC)
uint256[] memory orderIds = new uint256[](10);
for (uint i = 0; i < 10; i++) {
orderIds[i] = book.createSellOrder(address(weth), 0.1e18, 33e6, 1 days);
}
vm.stopPrank();
// Dan buys all orders
vm.startPrank(dan);
usdc.approve(address(book), 330e6);
uint256 feesBefore = book.totalFees();
for (uint i = 0; i < 10; i++) {
book.buyOrder(orderIds[i]);
}
uint256 feesAfter = book.totalFees();
vm.stopPrank();
// No fees collected despite 330 USDC in transactions
assertEq(feesAfter - feesBefore, 0);
// Compare with single large order
vm.startPrank(alice);
uint256 largeOrderId = book.createSellOrder(address(weth), 1e18, 330e6, 1 days);
vm.stopPrank();
vm.startPrank(dan);
book.buyOrder(largeOrderId);
vm.stopPrank();
// Large order would generate 9.9 USDC in fees
assertEq(book.totalFees(), 9.9e6);
}

Mathematical Breakdown:

  • Fee calculation: protocolFee = (priceInUSDC * 3) / 100

  • For 33 USDC: (33 * 3) / 100 = 99 / 100 = 0 (integer division rounds down)

  • For 34 USDC: (34 * 3) / 100 = 102 / 100 = 1 (first value that generates fee)

  • By splitting 330 USDC into 10×33 USDC orders, user pays 0 fees instead of 9.9 USDC

  • This represents a 100% fee avoidance on substantial transaction volume

Recommended Mitigation

+ uint256 public constant MIN_FEE = 1e6; // Minimum 1 USDC fee
function buyOrder(uint256 _orderId) public {
// ...
uint256 protocolFee = (order.priceInUSDC * FEE) / PRECISION;
+ if (protocolFee < MIN_FEE) {
+ protocolFee = MIN_FEE;
+ }
uint256 sellerReceives = order.priceInUSDC - protocolFee;
// ...
}

Why this works:

  • Guarantees minimum revenue of 1 USDC per transaction regardless of order size

  • Eliminates fee avoidance through order splitting strategies

  • Maintains percentage-based fees for larger orders while protecting against small order exploitation

  • Simple implementation that doesn't require complex precision arithmetic

Alternative approaches:

  1. Higher precision: Use basis points (10000) instead of percentage (100) for more granular fees

  2. Minimum order size: Require orders above a certain threshold (e.g., 100 USDC minimum)

  3. Tiered fee structure: Different fee rates based on order size brackets

  4. Gas + percentage: Combine fixed gas cost coverage with percentage fees

Updates

Lead Judging Commences

yeahchibyke Lead Judge
8 days ago
yeahchibyke Lead Judge 2 days ago
Submission Judgement Published
Validated
Assigned finding tags:

Fee can be bypassed

Protocol Suffers Potential Revenue Leakage due to Precision Loss in Fee Calculation

Support

FAQs

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