OrderBook

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

Protocol fee can be bypassed for low-value orders.

Protocol fee can be bypassed for low-value orders.

Description

Each time someone buys an order, the contract calculate fees and subtract them from the order's price. However, for order's where the priceInUSDC <= 33 the contract subtract 0 fees because of integer division truncation issue.

The protocol calculates protocol fees using this formula in OrderBook.sol::buyOrder()

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

FEE = 3
PRECISION = 100

Since solidity doesn't support floating point or decimal division, any fee calculation that results in a fractional number less than 1 will be truncated to .

Risk

Likelihood: High

  • Any user can create as many orders with the desired price.

Impact: High

  • Although the impact per order is small, it affects the core protocol revenue mechanism and can be abused by malicious users to entirely bypass fees.

Proof of Concept

Replace the test_buyOrder() in TestOrderBook.t.sol with this:

function test_buyOrder() public {
// alice creates sell order for wbtc
vm.startPrank(alice);
wbtc.approve(address(book), 2e8);
uint256 aliceId = book.createSellOrder(address(wbtc), 2e8, 33, 2 days);
vm.stopPrank();
assert(aliceId == 1);
assert(wbtc.balanceOf(alice) == 0);
assert(wbtc.balanceOf(address(book)) == 2e8);
// bob creates sell order for weth
vm.startPrank(bob);
weth.approve(address(book), 2e18);
uint256 bobId = book.createSellOrder(address(weth), 2e18, 33, 2 days);
vm.stopPrank();
assert(bobId == 2);
assert(weth.balanceOf(bob) == 0);
assert(weth.balanceOf(address(book)) == 2e18);
// clara creates sell order for wsol
vm.startPrank(clara);
wsol.approve(address(book), 2e18);
uint256 claraId = book.createSellOrder(address(wsol), 2e18, 33, 2 days);
vm.stopPrank();
vm.startPrank(dan);
usdc.approve(address(book), 200_000e6);
book.buyOrder(aliceId); // dan buys alice wbtc order
book.buyOrder(bobId); // dan buys bob weth order
book.buyOrder(claraId); // dan buys clara wsol order
vm.stopPrank();
assert(book.totalFees() == 0);
vm.prank(owner);
vm.expectRevert();
book.withdrawFees(owner);
assert(usdc.balanceOf(owner) == 0);
}
forge test --match-test buyOrder

Recommended Mitigation

Easiest fix is to implement minimum price > 33 policy

function createSellOrder(
address _tokenToSell,
uint256 _amountToSell,
uint256 _priceInUSDC,
uint256 _deadlineDuration
) public returns (uint256) {
if (!allowedSellToken[_tokenToSell]) revert InvalidToken();
if (_amountToSell == 0) revert InvalidAmount();
- if (_priceInUSDC == 0) revert InvalidPrice();
+ if (_priceInUSDC <= 33) revert InvalidPrice();
if (_deadlineDuration == 0 || _deadlineDuration > MAX_DEADLINE_DURATION) revert InvalidDeadline();
uint256 deadlineTimestamp = block.timestamp + _deadlineDuration;
uint256 orderId = _nextOrderId++;
IERC20(_tokenToSell).safeTransferFrom(msg.sender, address(this), _amountToSell);
// Store the order
orders[orderId] = Order({
id: orderId,
seller: msg.sender,
tokenToSell: _tokenToSell,
amountToSell: _amountToSell,
priceInUSDC: _priceInUSDC,
deadlineTimestamp: deadlineTimestamp,
isActive: true
});
emit OrderCreated(orderId, msg.sender, _tokenToSell, _amountToSell, _priceInUSDC, deadlineTimestamp);
return orderId;
}
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.