OrderBook

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

Mitigating Front-Running Vulnerabilities in DeFi

Root + Impact

Description

  • Describe the normal behavior in one or more sentences -

    Attackers can exploit the public mempool to front-run amendSellOrder or cancelSellOrder transactions by submitting buyOrder transactions with higher gas prices, buying assets at outdated prices or before cancellation.
    This undermines the seller’s ability to update or cancel orders reliably.

  • Root Cause:

    Blockchain transactions are visible in the public mempool before confirmation, allowing attackers to observe and outpace amendSellOrder or cancelSellOrder calls.
    The contract lacks mechanisms like time-locks or commit-reveal to obscure or delay these actions.


  • Explain the specific issue or problem in one or more sentences

// Root cause in the codebase with @> marks to highlight the relevant section

Risk

Likelihood:

  • Reason 1 - High likelihood due to easy mempool monitoring, automated MEV bots, and strong financial incentives in volatile markets.

  • Reason 2 - No built-in protections make successful front-running attacks highly probable.

Impact:

  • Impact 1 - Sellers face financial losses by selling at unintended prices or losing assets they meant to cancel, potentially in the thousands of USDC.

  • Impact 2 - User trust and platform reputation suffer, risking reduced adoption and market inefficiency.


Recommended Mitigation -

Use time lock mechanism

//updated code
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";
contract OrderBook is Ownable {
using SafeERC20 for IERC20;
using Strings for uint256;
struct Order {
uint256 id;
address seller;
address tokenToSell;
uint256 amountToSell;
uint256 priceInUSDC;
uint256 deadlineTimestamp;
bool isActive;
}
// --- New State Variables for Time-Lock ---
struct PendingAmendment {
uint256 newAmountToSell;
uint256 newPriceInUSDC;
uint256 newDeadlineTimestamp;
uint256 requestTimestamp;
}
mapping(uint256 => PendingAmendment) public pendingAmendments;
mapping(uint256 => uint256) public pendingCancellations;
uint256 public constant TIME_LOCK_DELAY = 60; // 60 seconds delay
// --- Existing State Variables (unchanged) ---
uint256 public constant MAX_DEADLINE_DURATION = 3 days;
uint256 public constant FEE = 3;
uint256 public constant PRECISION = 100;
IERC20 public immutable iWETH;
IERC20 public immutable iWBTC;
IERC20 public immutable iWSOL;
IERC20 public immutable iUSDC;
mapping(address => bool) public allowedSellToken;
mapping(uint256 => Order) public orders;
uint256 private _nextOrderId;
uint256 public totalFees;
// --- Existing Events (unchanged) ---
event OrderCreated(uint256 indexed orderId, address indexed seller, address indexed tokenToSell, uint256 amountToSell, uint256 priceInUSDC, uint256 deadlineTimestamp);
event OrderAmended(uint256 indexed orderId, uint256 newAmountToSell, uint256 newPriceInUSDC, uint256 newDeadlineTimestamp);
event OrderCancelled(uint256 indexed orderId, address indexed seller);
event OrderFilled(uint256 indexed orderId, address indexed buyer, address indexed seller);
event TokenAllowed(address indexed token, bool indexed status);
event EmergencyWithdrawal(address indexed token, uint256 indexed amount, address indexed receiver);
event FeesWithdrawn(address indexed receiver);
// --- New Events for Time-Lock ---
event AmendmentRequested(uint256 indexed orderId, uint256 newAmountToSell, uint256 newPriceInUSDC, uint256 newDeadlineTimestamp, uint256 requestTimestamp);
event CancellationRequested(uint256 indexed orderId, uint256 requestTimestamp);
// --- Existing Errors (unchanged) ---
error OrderNotFound();
error NotOrderSeller();
error OrderNotActive();
error OrderExpired();
error OrderAlreadyInactive();
error InvalidToken();
error InvalidAmount();
error InvalidPrice();
error InvalidDeadline();
error InvalidAddress();
// --- New Error for Time-Lock ---
error TimeLockNotElapsed();
// --- Constructor (unchanged) ---
constructor(address _weth, address _wbtc, address _wsol, address _usdc, address _owner) Ownable(_owner) {
if (_weth == address(0) || _wbtc == address(0) || _wsol == address(0) || _usdc == address(0)) revert InvalidToken();
if (_owner == address(0)) revert InvalidAddress();
iWETH = IERC20(_weth);
allowedSellToken[_weth] = true;
iWBTC = IERC20(_wbtc);
allowedSellToken[_wbtc] = true;
iWSOL = IERC20(_wsol);
allowedSellToken[_wsol] = true;
iUSDC = IERC20(_usdc);
_nextOrderId = 1;
}
// --- Modified amendSellOrder: Split into Request and Confirm ---
function requestAmendSellOrder(
uint256 _orderId,
uint256 _newAmountToSell,
uint256 _newPriceInUSDC,
uint256 _newDeadlineDuration
) external {
Order storage order = orders[_orderId];
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();
if (_newDeadlineDuration == 0 || _newDeadlineDuration > MAX_DEADLINE_DURATION) revert InvalidDeadline();
uint256 newDeadlineTimestamp = block.timestamp + _newDeadlineDuration;
pendingAmendments[_orderId] = PendingAmendment({
newAmountToSell: _newAmountToSell,
newPriceInUSDC: _newPriceInUSDC,
newDeadlineTimestamp: newDeadlineTimestamp,
requestTimestamp: block.timestamp
});
emit AmendmentRequested(_orderId, _newAmountToSell, _newPriceInUSDC, newDeadlineTimestamp, block.timestamp);
}
function confirmAmendSellOrder(uint256 _orderId) external {
Order storage order = orders[_orderId];
PendingAmendment memory amendment = pendingAmendments[_orderId];
if (order.seller == address(0)) revert OrderNotFound();
if (order.seller != msg.sender) revert NotOrderSeller();
if (amendment.requestTimestamp == 0) revert("No pending amendment");
if (block.timestamp < amendment.requestTimestamp + TIME_LOCK_DELAY) revert TimeLockNotElapsed();
if (!order.isActive) revert OrderAlreadyInactive();
if (block.timestamp >= order.deadlineTimestamp) revert OrderExpired();
IERC20 token = IERC20(order.tokenToSell);
if (amendment.newAmountToSell > order.amountToSell) {
uint256 diff = amendment.newAmountToSell - order.amountToSell;
token.safeTransferFrom(msg.sender, address(this), diff);
} else if (amendment.newAmountToSell < order.amountToSell) {
uint256 diff = order.amountToSell - amendment.newAmountToSell;
token.safeTransfer(order.seller, diff);
}
order.amountToSell = amendment.newAmountToSell;
order.priceInUSDC = amendment.newPriceInUSDC;
order.deadlineTimestamp = amendment.newDeadlineTimestamp;
// Clear pending amendment
delete pendingAmendments[_orderId];
emit OrderAmended(_orderId, amendment.newAmountToSell, amendment.newPriceInUSDC, amendment.newDeadlineTimestamp);
}
// --- Modified cancelSellOrder: Split into Request and Confirm ---
function requestCancelSellOrder(uint256 _orderId) external {
Order storage order = orders[_orderId];
if (order.seller == address(0)) revert OrderNotFound();
if (order.seller != msg.sender) revert NotOrderSeller();
if (!order.isActive) revert OrderAlreadyInactive();
pendingCancellations[_orderId] = block.timestamp;
emit CancellationRequested(_orderId, block.timestamp);
}
function confirmCancelSellOrder(uint256 _orderId) external {
Order storage order = orders[_orderId];
if (order.seller == address(0)) revert OrderNotFound();
if (order.seller != msg.sender) revert NotOrderSeller();
if (pendingCancellations[_orderId] == 0) revert("No pending cancellation");
if (block.timestamp < pendingCancellations[_orderId] + TIME_LOCK_DELAY) revert TimeLockNotElapsed();
if (!order.isActive) revert OrderAlreadyInactive();
order.isActive = false;
IERC20(order.tokenToSell).safeTransfer(order.seller, order.amountToSell);
// Clear pending cancellation
delete pendingCancellations[_orderId];
emit OrderCancelled(_orderId, order.seller);
}
// --- Modified buyOrder to Prevent Buying Pending Orders ---
function buyOrder(uint256 _orderId) public {
Order storage order = orders[_orderId];
if (order.seller == address(0)) revert OrderNotFound();
if (!order.isActive) revert OrderNotActive();
if (block.timestamp >= order.deadlineTimestamp) revert OrderExpired();
// Check for pending amendment or cancellation
if (pendingAmendments[_orderId].requestTimestamp != 0 || pendingCancellations[_orderId] != 0) {
revert("Order has pending amendment or cancellation");
}
order.isActive = false;
uint256 protocolFee = (order.priceInUSDC * FEE) / PRECISION;
uint256 sellerReceives = order.priceInUSDC - protocolFee;
iUSDC.safeTransferFrom(msg.sender, address(this), protocolFee);
iUSDC.safeTransferFrom(msg.sender, order.seller, sellerReceives);
IERC20(order.tokenToSell).safeTransfer(msg.sender, order.amountToSell);
totalFees += protocolFee;
emit OrderFilled(_orderId, msg.sender, order.seller);
}
// --- Other Functions (Unchanged) ---
// Include createSellOrder, getOrder, getOrderDetailsString, setAllowedSellToken, emergencyWithdrawERC20, withdrawFees as in the original contract
}
Updates

Lead Judging Commences

yeahchibyke Lead Judge 10 days ago
Submission Judgement Published
Validated
Assigned finding tags:

Amends or cancellation of sell orders can be front-run

When a seller wants to amend or cancel their sell orders, a malicious entity can front-run their transactions and buy out the orders. This can be especially harmful when real-world prices of listed assets fluctuate and sellers want to adjust the prices listed in their orders.

Support

FAQs

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