Root + Impact
Description
-
The amendSellOrder()
function violates the CEI (Checks-Effects-Interactions) pattern by performing external token transfers before updating the order state.
-
This creates a reentrancy window where malicious tokens can call back into the contract while order state is in an intermediate state.
function amendSellOrder(uint256 _orderId, uint256 _newAmountToSell, ) public {
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);
}
order.amountToSell = _newAmountToSell;
order.priceInUSDC = _newPriceInUSDC;
order.deadlineTimestamp = newDeadlineTimestamp;
Risk
Likelihood:
The owner can add any ERC20 token via setAllowedSellToken()
, including tokens with transfer hooks or malicious implementations
Every call to amendSellOrder()
that changes token amounts triggers external transfers before state updates, creating reentrancy windows
Impact:
Malicious tokens can reenter and manipulate order parameters multiple times during a single amendment operation
Order state inconsistencies can be exploited where the contract operates on stale data during reentrancy calls
Proof of Concept
The following demonstrates how a malicious token can exploit the reentrancy window to manipulate order state during amendment. The malicious token's transfer hook reenters the contract while order state still reflects pre-amendment values.
contract MaliciousToken {
OrderBook orderBook;
uint256 targetOrderId;
bool attacking;
function transferFrom(address from, address to, uint256 amount) external returns (bool) {
if (!attacking && to == address(orderBook)) {
attacking = true;
orderBook.amendSellOrder(targetOrderId, 1, 1 ether, 1 days);
attacking = false;
}
return true;
}
}
Recommended Mitigation
The fix involves reordering the function to follow the CEI pattern: perform all state updates before any external interactions.
This prevents reentrancy by ensuring that all state changes are completed before any external calls occur. Even if a malicious token attempts to reenter, it will see the updated state and cannot manipulate the order during an intermediate state.
The CEI pattern is a fundamental security practice that eliminates reentrancy vulnerabilities by removing the window where external code can execute while internal state is inconsistent.
function amendSellOrder(
uint256 _orderId,
uint256 _newAmountToSell,
uint256 _newPriceInUSDC,
uint256 _newDeadlineDuration
) public {
Order storage order = orders[_orderId];
// ...validation checks (unchanged)...
uint256 newDeadlineTimestamp = block.timestamp + _newDeadlineDuration;
IERC20 token = IERC20(order.tokenToSell);
+
+ // Store original amount for transfer calculations
+ uint256 originalAmount = order.amountToSell;
+
+ // EFFECTS: Update order state BEFORE external interactions
+ order.amountToSell = _newAmountToSell;
+ order.priceInUSDC = _newPriceInUSDC;
+ order.deadlineTimestamp = newDeadlineTimestamp;
- // Handle token amount changes
- if (_newAmountToSell > order.amountToSell) {
+ // INTERACTIONS: Handle token transfers AFTER state updates
+ if (_newAmountToSell > originalAmount) {
- uint256 diff = _newAmountToSell - order.amountToSell;
+ uint256 diff = _newAmountToSell - originalAmount;
token.safeTransferFrom(msg.sender, address(this), diff);
- } else if (_newAmountToSell < order.amountToSell) {
+ } else if (_newAmountToSell < originalAmount) {
- uint256 diff = order.amountToSell - _newAmountToSell;
+ uint256 diff = originalAmount - _newAmountToSell;
token.safeTransfer(order.seller, diff);
}
- // Update order details
- order.amountToSell = _newAmountToSell;
- order.priceInUSDC = _newPriceInUSDC;
- order.deadlineTimestamp = newDeadlineTimestamp;
emit OrderAmended(_orderId, _newAmountToSell, _newPriceInUSDC, newDeadlineTimestamp);
}