OrderBook

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

Precision loss for small orders creation

Description

When order.priceInUSDC is very small (e.g., 1 wei), this formula causes rounding down to zero or rounding up that consumes the entire amount, due to Solidity’s lack of floating-point arithmetic.

This introduces precision loss, allows fee evasion, and may result in sellers receiving zero proceeds, effectively breaking micro-order fairness and opening doors for spam attacks or unintended gas waste.

protocolFee = (order.priceInUSDC * FEE) / PRECISION;

Risk

Likelihood:

High

  • Easy to trigger with a single call.

  • No special permissions or contracts required.

  • Any user can exploit it with createSellOrder() and buyOrder().

Impact:

  • Fees are skipped entirely on small orders, even when FEE > 0

  • Sellers might receive 0 if fee ever rounds up to 100%

  • Attackers can spam small orders with no economic penalty

  • Thousands of dust orders can bloat storage & waste gas

  • Protocol loses intended revenue flow from valid order activity

Proof of Concept

The POC simulates two order flows:

Minimal Order

  • alice creates an order to sell 1 wei WETH for 1 wei USDC.

  • Fee = (1 * 3) / 100 = 0 → no fee taken.

  • dan buys the order, paying 1 wei USDC.

  • Alice receives full 1 wei, and protocol gets no fee.

Precision loss confirmed (fee is lost), and seller can avoid fee intentionally by keeping price low.


Slightly Larger Order

  • alice creates another order to sell 10 wei WETH for 10 wei USDC.

  • Fee = (10 * 3) / 100 = 0.3, which truncates to 0.

  • Again, no protocol fee is collected.

  • Alice receives all 10 wei.

  • This shows fee logic is ineffective for small orders, and fee collection is dependent on order size, which breaks economic consistency.

function test_precisionLossForSmallOrders() public {
// Test creating and buying very small orders to check for precision loss
uint256 smallAmount = 1; // 1 wei (smallest possible for WETH)
uint256 smallPrice = 1; // 1 wei USDC (smallest possible)
uint256 deadline = 1 days;
// Mint and approve small amounts
weth.mint(alice, smallAmount);
usdc.mint(dan, smallPrice);
vm.startPrank(alice);
weth.approve(address(book), smallAmount);
uint256 orderId = book.createSellOrder(address(weth), smallAmount, smallPrice, deadline);
vm.stopPrank();
// Log order details
(OrderBook.Order memory order) = book.getOrder(orderId);
console2.log("Order amount:", order.amountToSell);
console2.log("Order price:", order.priceInUSDC);
// Buyer attempts to buy the small order
vm.startPrank(dan);
usdc.approve(address(book), smallPrice);
book.buyOrder(orderId);
vm.stopPrank();
// Check balances after purchase
uint256 danWeth = weth.balanceOf(dan);
uint256 aliceUsdc = usdc.balanceOf(alice);
uint256 protocolFees = book.totalFees();
console2.log("dan WETH:", danWeth);
console2.log("alice USDC:", aliceUsdc);
console2.log("protocol fees:", protocolFees);
// Assert that dan received the small amount, and alice received the small price minus protocol fee (may be zero due to rounding)
assertEq(danWeth, smallAmount);
// Depending on fee logic, aliceUsdc may be zero if fee rounds up
assert(aliceUsdc <= smallPrice);
// Protocol fees may be zero or one depending on rounding
assert(protocolFees <= smallPrice);
// --- Additional test for slightly larger amount with another user (eve) ---
address eve = makeAddr("eve");
uint256 biggerAmount = 10; // 10 wei WETH
uint256 biggerPrice = 10; // 10 wei USDC
weth.mint(alice, biggerAmount);
usdc.mint(eve, biggerPrice);
vm.startPrank(alice);
weth.approve(address(book), biggerAmount);
uint256 orderId2 = book.createSellOrder(address(weth), biggerAmount, biggerPrice, deadline);
vm.stopPrank();
(OrderBook.Order memory order2) = book.getOrder(orderId2);
console2.log("[Bigger] Order amount:", order2.amountToSell);
console2.log("[Bigger] Order price:", order2.priceInUSDC);
vm.startPrank(eve);
usdc.approve(address(book), biggerPrice);
book.buyOrder(orderId2);
vm.stopPrank();
uint256 eveWeth = weth.balanceOf(eve);
uint256 aliceUsdc2 = usdc.balanceOf(alice);
uint256 protocolFees2 = book.totalFees();
console2.log("eve WETH:", eveWeth);
console2.log("alice USDC (after bigger order):", aliceUsdc2);
console2.log("protocol fees (after bigger order):", protocolFees2);
// Assert that eve received the bigger amount, and alice received the bigger price minus protocol fee (may be less than biggerPrice due to fee rounding)
assertEq(eveWeth, biggerAmount);
assert(aliceUsdc2 <= smallPrice + biggerPrice); // total alice USDC from both orders
assert(protocolFees2 <= smallPrice + biggerPrice);
}

Recommended Mitigation

  • Use Higher Precision

  • Enforce Minimum Fee

  • Reject Dust Orders

Updates

Lead Judging Commences

yeahchibyke Lead Judge
about 1 month ago
yeahchibyke Lead Judge about 1 month 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.