No Minimum Token Amount on Creation May Lead to Denial-of-Service
Description
-
While a user cannot create a sell order with a zero amount, the contract does not enforce a minimum amount.
-
This allows a malicious user to create a very large number of sell orders with an insignificant amount (e.g., 1 wei), which bloats the contract's storage and can lead to a denial-of-service condition.
function createSellOrder(
address _tokenToSell,
uint256 _amountToSell,
uint256 _priceInUSDC,
uint256 _deadlineDuration
) public returns (uint256) {
if (!allowedSellToken[_tokenToSell]) revert InvalidToken();
@> if (_amountToSell == 0) revert InvalidAmount();
if (_priceInUSDC == 0) revert InvalidPrice();
if (_deadlineDuration == 0 || _deadlineDuration > MAX_DEADLINE_DURATION) revert InvalidDeadline();
uint256 deadlineTimestamp = block.timestamp + _deadlineDuration;
uint256 orderId = _nextOrderId++;
IERC20(_tokenToSell).safeTransferFrom(msg.sender, address(this), _amountToSell);
orders[orderId] = Order({
id: orderId,
seller: msg.sender,
tokenToSell: _tokenToSell,
amountToSell: _amountToSell,
priceInUSDC: _priceInUSDC,
deadlineTimestamp: deadlineTimestamp,
isActive: true
});
emit OrderCreated(orderId, msg.sender, _tokenToSell, _amountToSell, _priceInUSDC, deadlineTimestamp);
return orderId;
}
Risk
Likelihood: High
-
A malicious actor can easily automate the creation of thousands of near-worthless orders in a script.
-
The cost of this attack is very low, as each transaction only requires a minimal token amount and gas.
Impact: Medium
-
The contract's storage becomes bloated with economically insignificant orders, increasing the state size on the blockchain.
-
This can degrade the user experience by making it more difficult to parse through legitimate orders and may cause transactions that create many orders to run out of gas, as shown in the PoC.
Proof of Concept
The following test demonstrates how a user can create a large number of orders with a minimal amount (1 wei), eventually causing the transaction to fail due to running out of gas. This highlights the potential for storage bloat and DoS.
function test_createLotsOrders() public {
vm.startPrank(bob);
weth.approve(address(book), type(uint256).max);
for (uint256 i = 0; i < 10000; i++) {
book.createSellOrder(address(weth), 1, 100_000, 1 days);
}
vm.stopPrank();
}
The following trace shows the transaction failing with an OutOfGas
error, proving that this vector can be used to disrupt the normal operation of the contract.
├─ [74048] OrderBook::createSellOrder(MockWETH: [0xF62849F9A0B5Bf2913b396098F7c7019b51A820a], 1, 100000 [1e5], 86400 [8.64e4])
│ ├─ [4466] MockWETH::transferFrom(will_sell_weth_order: [0x5836Fb2f9DE86916F726F675AA83Ced224C0E7B3], OrderBook: [0xd0398c02CB8A3Ea728E6e32EBC6030E8B3cCcA59], 1)
│ │ ├─ emit Transfer(from: will_sell_weth_order: [0x5836Fb2f9DE86916F726F675AA83Ced224C0E7B3], to: OrderBook: [0xd0398c02CB8A3Ea728E6e32EBC6030E8B3cCcA59], value: 1)
│ │ └─ ← [Return] true
│ └─ ← [OutOfGas] EvmError: OutOfGas
└─ ← [Revert] EvmError: Revert
Recommended Mitigation
Introduce a minimum sell amount to prevent users from creating economically insignificant orders. This should be enforced in both the createSellOrder
and amendSellOrder
functions.
// In OrderBook.sol
// --- Constants ---
+ uint256 public constant MIN_SELL_AMOUNT = 1e4; // Example value, adjust based on token decimals and desired minimum value.
// ...
function createSellOrder(
address _tokenToSell,
uint256 _amountToSell,
uint256 _priceInUSDC,
uint256 _deadlineDuration
) public returns (uint256) {
if (!allowedSellToken[_tokenToSell]) revert InvalidToken();
- if (_amountToSell == 0) revert InvalidAmount();
+ if (_amountToSell < MIN_SELL_AMOUNT) revert InvalidAmount();
if (_priceInUSDC < 34) revert InvalidPrice();
if (_deadlineDuration == 0 || _deadlineDuration > MAX_DEADLINE_DURATION) revert InvalidDeadline();
// ...
}
function amendSellOrder(
uint256 _orderId,
uint256 _newAmountToSell,
uint256 _newPriceInUSDC,
uint256 _newDeadlineDuration
) public {
// ...
if (block.timestamp >= order.deadlineTimestamp) revert OrderExpired(); // Cannot amend expired order
- if (_newAmountToSell == 0) revert InvalidAmount();
+ if (_newAmountToSell < MIN_SELL_AMOUNT) revert InvalidAmount();
if (_newPriceInUSDC < 34) revert InvalidPrice();
if (_newDeadlineDuration == 0 || _newDeadlineDuration > MAX_DEADLINE_DURATION) revert InvalidDeadline();
// ...
}