OrderBook

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

Fee Precision Truncation Leads to Systematic Undercharging and Lost Protocol Revenue

Root + Impact

Description

  • In normal operation, the protocol charges a 3% fee on the sale price of each order. This fee should accumulate to totalFees and be withdrawable by the protocol owner.

    However, when the priceInUSDC of an order is small (especially below 34), the fee computation using integer division truncates the result, potentially to zero. This means that small orders avoid paying any fees, or pay less than the expected 3%, leading to systematic loss of protocol revenue. Even for some number that are greater than 34, the protocol get some fee but still lost some due to rounding off error as solidity dosent accomadate decimals.

solidity
// OrderBook.sol - #L39-L40
uint256 protocolFee = (order.priceInUSDC * FEE) / PRECISION;
// @> Integer division truncates towards zero
// @> For priceInUSDC = 33: (33 * 3) / 100 = 99 / 100 = 0
// @> This results in protocolFee = 0 → fee fully lost

Risk

Likelihood:

  • Occurs whenever users submit orders priced under 34 USDC, which is trivially done.

  • Easy to automate or script many low-value orders that systematically evade fees.

Impact:

  • Protocol loses revenue it should have accrued from fees.

  • Could be used as a fee evasion strategy to exploit the system.

  • Over time, this creates a financial imbalance between the protocol and users.

Proof of Concept

solidity
function test_feeEvasionBySplitting() public {
vm.startPrank(alice);
weth.mint(alice, 1e17); // 0.1 ETH
weth.approve(address(book), type(uint256).max);
vm.stopPrank();
vm.startPrank(dan);
usdc.mint(dan, 3300); // 100 orders × 33 USDC each
usdc.approve(address(book), type(uint256).max);
vm.stopPrank();
for (uint i = 0; i < 100; i++) {
vm.prank(alice);
uint256 orderId = book.createSellOrder(address(weth), 1e15, 33, 1 days);
vm.prank(dan);
book.buyOrder(orderId);
}
console2.log("Total fees after 100 micro orders:", book.totalFees());
assertEq(book.totalFees(), 0); // Fails: No fee collected!
}
> Expected fees: 99 USDC
> **Actual fees**: 0 USDC
## Test ouput
Ran 1 test for test/TestOrderBook.t.sol:TestOrderBook
[PASS] test_feeEvasionBySplitting() (gas: 17188534)
Logs:
Total fees after 100 micro orders: 0
function test_feeCalculationExamples() public {
// Test various price points to show truncation
uint256[] memory prices = new uint256[](5);
prices[0] = 33; // (33 * 3) / 100 = 0
prices[1] = 34; // (34 * 3) / 100 = 1 (should be 1.02)
prices[2] = 66; // (66 * 3) / 100 = 1 (should be 1.98)
prices[3] = 67; // (67 * 3) / 100 = 2 (should be 2.01)
prices[4] = 100; // (100 * 3) / 100 = 3 (correct)
for (uint i = 0; i < prices.length; i++) {
uint256 expectedFee = (prices[i] * 3) / 100;
console2.log("Price:", prices[i], "USDC, Fee:", expectedFee);
}
}

Recommended Mitigation

function buyOrder(uint256 _orderId) public {
Order storage order = orders[_orderId];
// ... existing validation ...
order.isActive = false;
- uint256 protocolFee = (order.priceInUSDC * FEE) / PRECISION;
+ uint256 protocolFee = (order.priceInUSDC * FEE + PRECISION - 1) / PRECISION; // Round up
+ require(protocolFee > 0, "Fee cannot be zero"); // Optional: enforce minimum fee
uint256 sellerReceives = order.priceInUSDC - protocolFee;
// ... rest of function
}
Updates

Lead Judging Commences

yeahchibyke Lead Judge
about 2 months ago
yeahchibyke Lead Judge about 2 months 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.