Description
The fee calculation uses integer division which truncates fractional values. For prices not divisible by 100, this results in:
-
Protocol receiving less than the intended 3% fee
-
Seller receiving marginally more than intended
-
Small value leakage that compounds over many transactions
function buyOrder(uint256 _orderId) public {
uint256 protocolFee = (order.priceInUSDC * FEE) / PRECISION;
uint256 sellerReceives = order.priceInUSDC - protocolFee;
}
Risk
Likelihood: High
-
Occurs in every transaction where price % 100 ≠ 0
-
Affects 99% of possible price points
-
Magnified with high transaction volume
Impact: Medium
-
Protocol loses expected revenue from fees
-
Sellers receive unintended windfall gains
-
Fee inaccuracy violates protocol specifications
-
Value leakage compounds significantly over time
Proof of Concept
function test_feeTruncationGivesDustToSeller() public {
vm.startPrank(alice);
wbtc.approve(address(book), 1);
uint256 orderId = book.createSellOrder(address(wbtc), 1, 1, 1 days);
vm.stopPrank();
uint256 protocolFeesBefore = book.totalFees();
vm.startPrank(dan);
usdc.approve(address(book), 1);
book.buyOrder(orderId);
vm.stopPrank();
uint256 protocolFeesAfter = book.totalFees();
console2.log("before", protocolFeesBefore);
console2.log("after", protocolFeesAfter);
assertEq(protocolFeesAfter - protocolFeesBefore, 0, "Protocol gets 0 fees");
assertEq(usdc.balanceOf(alice), 1, "Seller keeps full amount");
}
Recommended Mitigation
Increase precision and implement rounding up:
- uint256 public constant FEE = 3; // 3%
- uint256 public constant PRECISION = 100;
+ uint256 public constant FEE = 300; // 3% with 10000 precision
+ uint256 public constant PRECISION = 10000; // 0.01% granularity
function buyOrder(uint256 _orderId) public {
// ...
- uint256 protocolFee = (order.priceInUSDC * FEE) / PRECISION;
+ uint256 protocolFee = (order.priceInUSDC * FEE + PRECISION - 1) / PRECISION;
// ...
}