OrderBook

First Flight #43
Beginner FriendlySolidity
100 EXP
View results
Submission Details
Impact: high
Likelihood: high
Invalid

Lack of price reasonability checks exposes users to severe financial loss

Description

The OrderBook contract lacks validation for the _priceInUSDC parameter in the createSellOrder function, allowing users to accidentally sell valuable assets for a tiny fraction of their worth. The contract expects prices to be specified in the smallest unit of USDC (let's say 6 decimal places), but provides no safeguards against users inputting human-readable prices without the proper decimal conversion.

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(); // Only checks if price is zero, not if it's reasonable
if (_deadlineDuration == 0 || _deadlineDuration > MAX_DEADLINE_DURATION)
revert InvalidDeadline();
// Rest of the function...
}
// No validation that the price is reasonable or within expected ranges

Risk

Likelihood: High

  • Users naturally think in whole units (e.g., 2000 USDC) rather than smallest units (2,000,000,000), making this error extremely common in DeFi interfaces.

  • The contract provides no warnings, safeguards, or validation to prevent this type of input error.

Impact: High

  • Users could lose nearly all value of their assets by selling them at a tiny fraction of their intended price (e.g., selling 1 ETH worth $2000 for just $0.002).

  • Bots and frontrunners will quickly exploit these mispriced orders, making recovery impossible once the transaction is confirmed.

  • The protocol's reputation would be severely damaged after users experience significant financial losses.

Proof of Concept

The following test case demonstrates how a user can accidentally lose nearly all the value of their assets due to the lack of price reasonability checks:

// Test demonstrating the vulnerability
function testPriceInputVulnerability() public {
// Setup: Alice has 1 WETH worth approximately 2000 USDC
weth.mint(alice, 1e18); // 1 WETH
// Alice intends to sell her WETH for 2000 USDC
// But mistakenly inputs 2000 instead of 2000e6
vm.startPrank(alice);
weth.approve(address(book), 1e18);
uint256 orderId = book.createSellOrder(address(weth), 1e18, 2000, 1 days);
vm.stopPrank();
// Bob notices the severely underpriced WETH and buys it immediately
vm.startPrank(bob);
usdc.approve(address(book), 2000);
book.buyOrder(orderId);
vm.stopPrank();
// Result: Bob got 1 WETH (worth 2000 USDC) for just 0.002 USDC
// Alice lost approximately 99.9999% of her asset's value
assertEq(weth.balanceOf(bob), 1e18);
// Alice received only 1940 units of USDC (2000 - 60 fee)
// Which is 0.00194 USDC instead of 2000 USDC
assertEq(usdc.balanceOf(alice), 1940);
}

Explanation

  1. The Decimal Confusion:

    • USDC has 6 decimal places, meaning 1 USDC = 1,000,000 units

    • To sell 1 WETH for 2000 USDC, Alice should input 2,000,000,000 (2000e6)

    • Instead, she inputs just 2000, which the contract interprets as 0.002 USDC

  2. The Contract Accepts the Order:

    • The contract only validates that the price is non-zero (if (_priceInUSDC == 0) revert InvalidPrice();)

    • It doesn't check if the price is reasonable or within expected ranges

    • The order is created successfully with a price of 0.002 USDC for 1 WETH

  3. Immediate Exploitation:

    • Arbitrage bots or other users will quickly spot and exploit severely underpriced assets

    • In this example, Bob immediately buys the 1 WETH for just 0.002 USDC

    • The transaction is valid and cannot be reversed

  4. Devastating Financial Loss:

    • Alice receives only 1940 units (0.00194 USDC) after the protocol fee

    • This represents a 99.9999% loss compared to the intended 2000 USDC

    • The protocol also receives a much smaller fee than intended

Recommended Mitigation

Add a minimum price validation with a configurable threshold:

+ // State variable
+ uint256 public minimumPriceThreshold;
+ bool public minPriceCheckEnabled;
+
+ // Function to set minimum price threshold
+ function setMinimumPriceThreshold(uint256 _threshold, bool _enabled) external onlyOwner {
+ minimumPriceThreshold = _threshold;
+ minPriceCheckEnabled = _enabled;
+ }
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();
+ // Add minimum price check
+ if (minPriceCheckEnabled && _priceInUSDC < minimumPriceThreshold)
+ revert PriceTooLow();
if (_deadlineDuration == 0 || _deadlineDuration > MAX_DEADLINE_DURATION)
revert InvalidDeadline();
// Rest of the function...
}
Updates

Lead Judging Commences

yeahchibyke Lead Judge 10 days ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.