OrderBook

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

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

Finding Title

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

Summary

The protocol's fee calculation, which uses integer division with low precision (/ 100), creates a rounding error that can be exploited. For any trade priced at 33 wei of USDC or less, the calculated 3% fee rounds down to zero, allowing the trade to be processed fee-free. While the high gas cost of performing many small transactions makes a large-scale economic attack impractical today, this represents a fundamental design flaw that causes a verifiable and permanent leakage of protocol revenue. This flaw undermines the economic model and should be remediated as a matter of protocol robustness and best practice.

Finding Description

The buyOrder function calculates the protocol fee using the formula (order.priceInUSDC * 3) / 100. Due to Solidity's integer division, any result with a remainder is truncated. Consequently, if the numerator (order.priceInUSDC * 3) is less than 100, the resulting protocolFee is 0. This is true for any priceInUSDC value between 1 and 33.

// src/OrderBook.sol:203
uint256 protocolFee = (order.priceInUSDC * FEE) / PRECISION; // FEE = 3, PRECISION = 100

This creates a scenario where users can intentionally price their orders just below the 34 wei threshold to avoid fees. Although a single such transaction has a negligible impact, it establishes a pattern of value leakage that is built into the protocol's core logic.

Impact

The primary impact is a direct, albeit small, loss of protocol revenue on certain trades. While the economic viability of a large-scale attack is questionable due to gas costs, the existence of this flaw has several negative consequences:

  • Protocol Value Leak: The protocol fails to capture fees it is entitled to, creating a small but persistent drain on its treasury.

  • Design Flaw: It demonstrates a weakness in the handling of financial calculations. In DeFi, even minor rounding errors can be aggregated or combined with other exploits to cause significant issues.

  • Future Risk: A reduction in L2 gas fees or the introduction of new protocol features could potentially make this exploit more economically viable in the future.

Likelihood

Medium. From a technical standpoint, the flaw is easy to trigger. Any user can create a low-priced order. However, the economic incentive to do so at scale is currently low, which reduces the practical likelihood of a major exploit.

Proof of Concept

The following test demonstrates that an order priced at 33 wei of USDC results in zero fees being collected by the protocol, confirming the rounding vulnerability.

Test File: test/FeeRoundingVulnerabilityV2.t.sol

// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.0;
import {Test, console2} from "forge-std/Test.sol";
import {OrderBook} from "../src/OrderBook.sol";
import {MockUSDC} from "./mocks/MockUSDC.sol";
import {MockWETH} from "./mocks/MockWETH.sol";
/**
* @title Fee Rounding Vulnerability PoC
* @notice Demonstrates how integer division in fee calculations leads to revenue loss for the protocol.
*/
contract FeeRoundingExploitTest is Test {
OrderBook book;
MockWETH weth;
MockUSDC usdc;
address owner = makeAddr("owner");
address seller = makeAddr("seller");
address buyer = makeAddr("buyer");
function setUp() public {
weth = new MockWETH(18);
usdc = new MockUSDC(6);
vm.prank(owner);
book = new OrderBook(address(weth), address(weth), address(weth), address(usdc), owner);
// Mint tokens to participants
weth.mint(seller, 10e18); // 10 WETH for multiple orders
usdc.mint(buyer, 1000e6); // 1000 USDC
}
/// @notice This test proves that a single order with a low price (e.g., 33 wei of USDC)
/// results in a calculated fee of zero, allowing a trade to occur fee-free.
function test_PoC_SingleOrderFeeEvasion() public {
// A price of 33 will result in a fee calculation of (33 * 3) / 100, which rounds down to 0.
uint256 exploitablePrice = 33;
// --- Execution ---
vm.startPrank(seller);
weth.approve(address(book), 1e18);
uint256 orderId = book.createSellOrder(address(weth), 1e18, exploitablePrice, 1 days);
vm.stopPrank();
uint256 feesBefore = book.totalFees();
assertEq(feesBefore, 0, "Initial fees should be zero");
vm.startPrank(buyer);
usdc.approve(address(book), exploitablePrice);
book.buyOrder(orderId);
vm.stopPrank();
// --- Assertion ---
uint256 feesAfter = book.totalFees();
console2.log("Price per order:", exploitablePrice);
console2.log("Protocol fees collected for this trade:", feesAfter - feesBefore);
// The key assertion: The protocol failed to collect any fee for this transaction.
assertEq(feesAfter, 0, "VULNERABILITY: Protocol should have collected a fee, but it rounded down to zero.");
}
/// @notice This test demonstrates how an attacker can exploit the rounding error repeatedly
/// by splitting a large sale into multiple small, fee-free orders, causing
/// a cumulative loss of revenue for the protocol.
function test_PoC_CumulativeFeeLoss() public {
uint256 numOrders = 20;
uint256 exploitablePrice = 33; // This price results in a fee of 0
// --- Execution ---
for (uint256 i = 0; i < numOrders; i++) {
vm.startPrank(seller);
weth.approve(address(book), 1e17); // Sell 0.1 WETH per order
uint256 orderId = book.createSellOrder(address(weth), 1e17, exploitablePrice, 1 days);
vm.stopPrank();
vm.startPrank(buyer);
usdc.approve(address(book), exploitablePrice);
book.buyOrder(orderId);
vm.stopPrank();
}
// --- Assertion ---
uint256 totalFeesCollected = book.totalFees();
console2.log("Number of fee-free orders processed:", numOrders);
console2.log("Total fees collected by protocol:", totalFeesCollected);
// The key assertion: After 20 trades, the protocol has still earned nothing.
assertEq(totalFeesCollected, 0, "VULNERABILITY: Protocol revenue remains zero after multiple trades due to rounding exploit.");
}
}

Successful Test Output:

[PASS] test_PoC_SingleOrderFeeEvasion()
Logs:
Price per order: 33
Protocol fees collected for this trade: 0

The successful test confirms that it is possible to execute a trade without paying any fees, validating the existence of the revenue leakage flaw.

Recommended Mitigation

The standard industry practice to prevent such rounding issues is to increase the precision of the calculation by using basis points (1 bp = 0.01%).

// src/OrderBook.sol
- uint256 public constant FEE = 3; // 3%
- uint256 public constant PRECISION = 100;
+ uint256 public constant FEE = 300; // 300 bps = 3.00%
+ uint256 public constant PRECISION = 10000; // Represents 100.00%

Impact of the Fix:
With this change, the fee calculation becomes significantly more precise. While a price of 33 wei would still result in a zero fee ((33 * 300) / 10000 = 0), the threshold for earning a fee is much lower. For a more realistic low-value transaction of 1 USDC (1,000,000 wei), the fee would be:
(1,000,000 * 300) / 10000 = 30,000 wei (or 0.03 USDC).
This ensures that fees are collected fairly and consistently across almost all non-trivial trades, patching the revenue leak.

Updates

Lead Judging Commences

yeahchibyke Lead Judge 15 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.