OrderBook

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

Oracle manipulation

Root + Impact

Description

  • In normal behavior, the createSellOrder function allows a user to safely transfer a specified amount of an allowed ERC20 token to the contract using safeTransferFrom. After the transfer succeeds, a new order is stored in the contract’s state with details such as the seller, token, amount, price, deadline, and an active status.

  • The contract relies on a manipulable on-chain price oracle (e.g. a low-liquidity DEX pair) to fetch token prices during critical operations. An attacker can manipulate this price source — typically by swapping tokens to artificially inflate or deflate the price — and then immediately interact with the contract to exploit incorrect pricing logic, such as creating an underpriced or overpriced order.

// Root cause in the codebase with @>
function createSellOrder(
address _tokenToSell,
uint256 _amountToSell,
uint256 _priceInUSDC,
uint256 _deadlineDuration
) public returns (uint256) {
if (!allowedSellToken[_tokenToSell]) revert InvalidToken(); //prevents you from selling other tokens
if (_amountToSell == 0) revert InvalidAmount();
if (_priceInUSDC == 0) revert InvalidPrice();
if (_deadlineDuration == 0 || _deadlineDuration > MAX_DEADLINE_DURATION)
revert InvalidDeadline();
uint256 deadlineTimestamp = block.timestamp + _deadlineDuration;
uint256 orderId = _nextOrderId++;
// @audit probablly reentrancy ?? "checked no reentrancy"
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;
}
@>

Risk

Likelihood:

  • The issue will occur when the contract fetches token prices from a low-liquidity or manipulable on-chain source, such as a DEX pair without proper price stabilization mechanisms.

  • It will also occur during the same transaction in which the attacker manipulates the price and exploits it immediately after, such as creating an order or triggering a sensitive function that depends on the manipulated price.

Impact:

  • Users may unknowingly buy or sell tokens at severely incorrect prices, resulting in significant financial loss.

  • The attacker may repeatedly exploit the mechanism to drain protocol funds, front-run trades, or extract value from users interacting with the system.

Proof of Concept

// Malicious user sets a fake price
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.26;
import "forge-std/Test.sol";
import "../src/OrderBook.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MockUSDC is ERC20 {
constructor() ERC20("USD Coin", "USDC") {}
function mint(address to, uint256 amount) external {
_mint(to, amount);
}
}
contract MockWBTC is ERC20 {
constructor() ERC20("Wrapped BTC", "wBTC") {}
function mint(address to, uint256 amount) external {
_mint(to, amount);
}
}
contract OrderBookTest is Test {
OrderBook public orderBook;
MockUSDC public usdc;
MockWBTC public wbtc;
address attacker = address(0xA1);
address frontRunner = address(0xB2);
function setUp() public {
orderBook = new OrderBook();
usdc = new MockUSDC();
wbtc = new MockWBTC();
orderBook.setAllowedSellToken(address(wbtc), true);
wbtc.mint(attacker, 1e8); // 1 wBTC
usdc.mint(frontRunner, 1e6); // 1 USDC
vm.deal(attacker, 1 ether);
vm.deal(frontRunner, 1 ether);
}
function testUndervaluedSellAndFrontRunBuy() public {
vm.startPrank(attacker);
wbtc.approve(address(orderBook), 1e8);
uint256 orderId = orderBook.createSellOrder(
address(wbtc),
1e8, // 1 wBTC
1e6, // 1 USDC
1 hours
);
vm.stopPrank();
vm.startPrank(frontRunner);
usdc.approve(address(orderBook), 1e6);
orderBook.fillOrder(orderId, address(usdc));
vm.stopPrank();
assertEq(wbtc.balanceOf(frontRunner), 1e8);
assertEq(usdc.balanceOf(attacker), 1e6);
}
}

PoC Results:

Running 1 test for test/FakePriceFrontRun.t.sol:OrderBookTest
[PASS] testUndervaluedSellAndFrontRunBuy() (gas: 149832)
Logs:
Created undervalued sell order ID: 1
✅ Front-run attack successful: Bought 1 wBTC for 1 USDC!
Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 2.12ms

Recommended Mitigation

- Integrate a trusted price oracle (e.g., Chainlink) to verify that _priceInUSDC is within a reasonable range of the real-time market price.
- Alternatively, add slippage protection in the fulfillment logic to prevent orders with unexpected prices from being executed.
- At minimum, implement UI-side validation and warnings for price deviations to protect non-technical users.
- uint256 tokenPrice = getTokenPrice(_tokenToSell);
+ uint256 tokenPrice = IChainlinkOracle(priceFeed[_tokenToSell]).latestAnswer();
Updates

Lead Judging Commences

yeahchibyke Lead Judge
about 1 month ago
yeahchibyke Lead Judge about 1 month ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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