The buyOrder
function facilitates the exchange of tokens by a buyer for a seller's listed asset, handling the transfer of tokens and calculation of protocol fees. The function updates the totalFees
state variable after performing multiple external token transfers to the contract, the seller, and the buyer.
The specific issue is that the totalFees
accumulation, which is a critical state update, occurs as the last step in the buyOrder
function. This violates the "Checks-Effects-Interactions" pattern, a fundamental security best practice in Solidity.
Likelihood: Medium
This will occur when the buyOrder
function is called and an unexpected revert happens after the token transfers have completed but before totalFees
is incremented. This could be due to an out-of-gas condition if the preceding transfers consume a large amount of gas, or an unexpected revert from a hook in a non-standard ERC20 (though SafeERC20
mitigates for standard ones, the general principle of CEI still applies).
This will occur if a malicious actor finds a way to re-enter the buyOrder
function after tokens have been transferred but before totalFees
is updated. While SafeERC20
significantly reduces re-entrancy risks for compliant ERC20 tokens, the vulnerability exists if non-standard tokens or complex re-entrancy patterns are employed (e.g., if a token's transferFrom
or transfer
function itself triggers a re-entrant call, or if a malicious msg.sender
or order.seller
contract could re-enter through a fallback
/receive
function triggered by the external token transfer after state changes).
Impact: Medium
Inaccurate Fee Accounting: If a revert occurs post-transfers but pre-totalFees
update, the protocol will fail to accurately track accumulated fees, leading to a loss of revenue for the protocol owner when withdrawFees
is called.
Loss of Funds (Theoretical): In a worst-case re-entrancy scenario, a malicious actor might exploit the window between token transfers and the totalFees
update to manipulate state or potentially execute unauthorized actions, although direct loss of user funds is more mitigated by SafeERC20
for standard tokens. The primary impact here is on the protocol's accounting.
Given the current setup, we need to simulate a revert after transfers but before the totalFees
update. This is tricky to demonstrate with a standard ERC20 and SafeERC20
unless we introduce a custom mock for USDC or the sell token that includes a re-entrancy hook or a conditional revert.
However, the core of the bug is the violation of the pattern itself, which opens the door to potential issues, even if a direct exploit path isn't immediately obvious with standard components.
A simplified PoC for the inconsistent state on revert (without full re-entrancy) could look like this:
Deploy OrderBook
and MockUSDC
(with a forced revert)
setUp
: Initialize contracts, mint tokens, create a sell order.
Malicious buyOrder
Scenario:
dan
calls buyOrder
.
Inside buyOrder
:
order.isActive
is set to false
.
iUSDC.safeTransferFrom(msg.sender, address(this), protocolFee);
executes successfully.
iUSDC.safeTransferFrom(msg.sender, order.seller, sellerReceives);
executes successfully.
IERC20(order.tokenToSell).safeTransfer(msg.sender, order.amountToSell);
executes successfully.
Now, just before totalFees += protocolFee;
, imagine a forced revert (e.g., by modifying MockUSDC
to revert on a subsequent unrelated call, or by consuming all gas if that was possible).
The transaction reverts.
Observation:
order.isActive
is false
(state was changed).
Tokens have been transferred (from buyer to contract, from buyer to seller, from contract to buyer).
totalFees
has not been updated (it remains its previous value).
This leads to an inconsistent state: the order is marked filled, tokens are moved, but fees are not accounted for.
Adhere strictly to the Checks-Effects-Interactions pattern. All state changes should occur before any external calls. This ensures that if any external call reverts, the entire transaction is rolled back, preserving a consistent state.
Alternative (Simpler) Mitigation for Current safeTransferFrom
pattern:
If the goal is to keep safeTransferFrom(msg.sender, ...)
directly, the key is to ensure totalFees
is updated before any external calls.
https://github.com/CodeHawks-Contests/2025-07-orderbook/blob/main/src/OrderBook.sol
.../src/OrderBook.sol
I was unable to select scope in scope
-section !
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.