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

Give us feedback!