OrderBook

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

Missing Reentrancy Protection + Potential Fund Drainage

Description

  • Smart contracts should protect against reentrancy attacks where external contracts can call back into the contract during execution to manipulate state.

  • The contract lacks explicit reentrancy guards and relies only on SafeERC20, which may not be sufficient protection against malicious tokens with hooks or callback mechanisms.

contract OrderBook is Ownable {
using SafeERC20 for IERC20;
function createSellOrder(...) public returns (uint256) {
IERC20(_tokenToSell).safeTransferFrom(msg.sender, address(this), _amountToSell);
// State changes after external call
orders[orderId] = Order({...});
}
function amendSellOrder(...) public {
token.safeTransferFrom(msg.sender, address(this), diff);
// State changes after external calls
}
}

Risk

Likelihood:

  • When malicious ERC20 tokens with transfer hooks are added to the allowlist

  • When tokens implement callbacks or notifications on transfer

  • When users interact with contracts that have fallback functions

Impact:

  • Malicious tokens could reenter functions and manipulate order state

  • Multiple orders could be created or modified in a single transaction

  • Contract state could become inconsistent leading to fund loss

Proof of Concept

Reentrancy Attack Vector: This demonstrates how a malicious token can exploit the lack of reentrancy protection to manipulate order state.

// Malicious token that reenters on transfer
contract MaliciousToken is ERC20 {
OrderBook public orderBook;
address public attacker;
bool public attacking;
function transfer(address to, uint256 amount) public override returns (bool) {
if (attacking && to == address(orderBook)) {
// Reenter during amendSellOrder
orderBook.amendSellOrder(1, 1e18, 5000e6, 1 days);
}
return super.transfer(to, amount);
}
}
function testReentrancyAttack() public {
MaliciousToken maliciousToken = new MaliciousToken();
// Owner adds malicious token to allowlist
vm.startPrank(owner);
book.setAllowedSellToken(address(maliciousToken), true);
vm.stopPrank();
// Attacker creates order with malicious token
vm.startPrank(attacker);
maliciousToken.mint(attacker, 10e18);
maliciousToken.approve(address(book), 10e18);
uint256 orderId = book.createSellOrder(address(maliciousToken), 2e18, 1000e6, 1 days);
// Trigger reentrancy during amendment
maliciousToken.attacking = true;
book.amendSellOrder(orderId, 1e18, 500e6, 1 days); // This will reenter
vm.stopPrank();
}

Attack Flow:

  1. Attacker deploys malicious ERC20 token with transfer hook

  2. Owner unknowingly adds malicious token to allowlist

  3. Attacker creates initial order with malicious token

  4. During amendSellOrder, malicious token's transfer hook is triggered

  5. Hook calls back into amendSellOrder again, potentially with different parameters

  6. Order state becomes inconsistent due to nested modifications

  7. Attack could drain funds or manipulate order parameters maliciously

Real-world risks:

  • ERC777 tokens have built-in transfer hooks

  • Some deflationary tokens have complex transfer logic

  • Upgradeable tokens could add hooks in future versions

Recommended Mitigation

Solution: Add OpenZeppelin's ReentrancyGuard to all functions that interact with external tokens.

+ import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol";
- contract OrderBook is Ownable {
+ contract OrderBook is Ownable, ReentrancyGuard {
- function createSellOrder(...) public returns (uint256) {
+ function createSellOrder(...) public nonReentrant returns (uint256) {
- function amendSellOrder(...) public {
+ function amendSellOrder(...) public nonReentrant {
- function cancelSellOrder(uint256 _orderId) public {
+ function cancelSellOrder(uint256 _orderId) public nonReentrant {
- function buyOrder(uint256 _orderId) public {
+ function buyOrder(uint256 _orderId) public nonReentrant {

Why this works:

  • ReentrancyGuard: Uses a simple state variable to prevent nested calls

  • Gas efficient: Only 2,300 gas overhead per protected function call

  • Battle-tested: Used by thousands of protocols, well-audited by OpenZeppelin

  • Comprehensive protection: Prevents both cross-function and same-function reentrancy

Alternative/Additional protections:

  1. Checks-Effects-Interactions pattern: Modify state before external calls (already needed for H-01)

  2. Token allowlist vetting: Carefully audit tokens before adding to allowlist

  3. Pull payment pattern: Let users withdraw funds rather than pushing payments

  4. Function-specific guards: Custom reentrancy protection for specific functions if needed

Updates

Lead Judging Commences

yeahchibyke Lead Judge 4 days ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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