OrderBook

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

Hidden Transfer Fees

Root + Impact

The createSellOrder() function assumes that safeTransferFrom() will transfer the full _amountToSell without deductions. However, many ERC20 tokens implement fee-on-transfer mechanics, where a portion of the tokens is automatically deducted during transfers. This leads the contract to overestimate the amount received, recording incorrect order data.

As a result, the contract may promise buyers more tokens than it holds, causing fund loss, trading failures, or griefing attacks. This also makes the system incompatible with fee-on-transfer tokens, which are common on mainnet.

Description

  • The createSellOrder() function originally assumed that safeTransferFrom() would always transfer the full _amountToSell of tokens from the seller to the contract. However, this assumption is not valid for fee-on-transfer or deflationary tokens (e.g., RFI, SafeMoon, etc.).

    Such tokens deduct a fee during transfers, meaning the contract receives less than _amountToSell. If not accounted for, this leads to the contract overestimating its balance, storing incorrect order data, and ultimately misleading buyers about the amount they are purchasing.

function createSellOrder(...) public returns (uint256) {
...
@> IERC20(_tokenToSell).safeTransferFrom(msg.sender, address(this), _amountToSell);
@> // No validation of actual tokens received
...
orders[orderId] = Order({
...
amountToSell: _amountToSell, // @> Incorrectly assumes full transfer
});
}

Risk

Likelihood:

  • This will occur when a fee-on-transfer or deflationary token is added to the allowed token list.

  • Many tokens in production (e.g., SafeMoon, EverRise, RFI) have non-standard transfer behavior, making this a realistic threat on mainnet.

Impact:

  • The order book will misrepresent how many tokens the contract actually holds, causing buyers to receive less than expected.

  • This may break downstream logic or protocols relying on accurate token amounts and result in financial loss for users.

Proof of Concept

This test simulates a common fee-on-transfer scenario where a token deducts a 10% fee during transfers. Although the seller intends to transfer 100 tokens, only 90 arrive at the contract. Since the original logic stores the full _amountToSell, the order is overstated. If a buyer later purchases the order expecting 100 tokens, they receive only 90 — leading to fund loss and inconsistent contract state. This highlights how dangerous the incorrect assumption is in real-world use.

// Simulate adding a fee-on-transfer token with 10% fee logic
feeToken.mint(alice, 100);
feeToken.setTransferFee(10); // 10%
vm.prank(alice);
feeToken.approve(address(orderBook), 100);
orderBook.createSellOrder(address(feeToken), 100, 1000e6, 1 days);
// Order records 100 tokens for sale, but contract only received 90
// Buyer purchases 100 tokens — receives only 90 — loss of funds

Recommended Mitigation

This fix replaces the assumption-based logic with a pre- and post-balance check, ensuring the contract records only the tokens it actually receives. This approach is robust against fee-on-transfer, burn-on-transfer, or deflationary tokens. The optional revert on actualReceived == 0 protects the system from dust or unexpected zero-value transfers. By storing actualReceived instead of _amountToSell, the system guarantees accurate accounting and prevents buyer-side loss or orderbook inconsistencies.

function createSellOrder(...) public returns (uint256) {
...
+ uint256 balanceBefore = IERC20(_tokenToSell).balanceOf(address(this));
IERC20(_tokenToSell).safeTransferFrom(msg.sender, address(this), _amountToSell);
+ uint256 balanceAfter = IERC20(_tokenToSell).balanceOf(address(this));
+ uint256 actualReceived = balanceAfter - balanceBefore;
+ if (actualReceived == 0) revert TokenTransferFeeDetected();
- amountToSell: _amountToSell,
+ amountToSell: actualReceived,
...
}
Updates

Lead Judging Commences

yeahchibyke Lead Judge
about 2 months 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.