OrderBook

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

Inconsistent Decimal Handling Leads to Pricing Errors and Token Amount Misrepresentation

Root + Impact

Description

The OrderBook contract supports selling multiple ERC20 tokens — wETH, wBTC, and wSOL — all priced in USDC. Under expected behavior, sellers list tokens (like 1 ETH or 1 BTC) for a fixed USDC price (e.g. $2,000), and buyers receive the correctly corresponding token amount.

However, the contract does not normalize token decimals. This means:

  • wBTC (8 decimals), wETH/wSOL (18 decimals), and USDC (6 decimals) are stored and processed as raw uint256 amounts.

  • No conversions are done to account for decimal mismatch.

This creates two major issues:

  1. User Confusion: Different tokens appear equal in amountToSell but represent vastly different values.

  2. Exploitable Underpayment/Overpayment: Malicious sellers can list dust-sized amounts using misaligned decimals, tricking buyers into paying full USDC price for tiny token amounts.

// Root cause in the codebase with @> marks to highlight the relevant section
solidity// OrderBook.sol - Constructor accepts tokens with different decimals
constructor(address _weth, address _wbtc, address _wsol, address _usdc, address _owner) {
iWETH = IERC20(_weth); // @> 18 decimals standard
iWBTC = IERC20(_wbtc); // @> 8 decimals standard
iWSOL = IERC20(_wsol); // @> 18 decimals standard
iUSDC = IERC20(_usdc); // @> 6 decimals standard
}
function createSellOrder(
address _tokenToSell,
uint256 _amountToSell, // @> Raw amount - no decimal normalization
uint256 _priceInUSDC, // @> Raw USDC amount - inconsistent scaling
uint256 _deadlineDuration
) public returns (uint256) {
// @> No decimal validation or normalization performed
IERC20(_tokenToSell).safeTransferFrom(msg.sender, address(this), _amountToSell);
}

Risk

Likelihood:

  • Sellers can easily craft misleading listings with dust amounts.

  • Buyers have no on-chain way to verify price-per-token.

  • OrderBook assumes all tokens use 18 decimals, but wBTC only uses 8.

Impact:

  • Pricing confusion where identical raw amounts represent vastly different economic values (1e8 wBTC vs 1e18 wETH)

  • Cross-token pricing comparisons (e.g., ETH vs BTC) are meaningless without decimal normalization.

  • Protocol trust and user experience is harmed.

  • UIs must handle decimal scaling off-chain, increasing inconsistency risks.


Proof of Concept

function test_overpaymentDueToDecimalMismatch() public {
// Seller lists 0.0000000001 WETH (1e8 wei) for full $2000
vm.startPrank(alice);
weth.mint(alice, 1e8);
weth.approve(address(book), 1e8);
uint256 orderId = book.createSellOrder(address(weth), 1e8, 2000e6, 1 days);
vm.stopPrank();
// Buyer pays $2000 thinking it's 1 full ETH
vm.startPrank(dan);
usdc.mint(dan, 2000e6);
usdc.approve(address(book), 2000e6);
book.buyOrder(orderId);
vm.stopPrank();
// Buyer receives only 1e8 wei = 0.0000000001 ETH
assertEq(weth.balanceOf(dan), 1e8);
console2.log("Dan received:", weth.balanceOf(dan)); // 1e8 wei
}
// Test Output
Ran 1 test for test/TestOrderBook.t.sol:TestOrderBook
[PASS] test_overpaymentDueToDecimalMismatch() (gas: 360602)
Logs:
Dan received: 100000000

Recommended Mitigation

diff+ import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
contract OrderBook is Ownable {
+ using SafeERC20 for IERC20;
+
+ uint8 public constant STANDARD_DECIMALS = 18;
+ mapping(address => uint8) public tokenDecimals;
constructor(address _weth, address _wbtc, address _wsol, address _usdc, address _owner) Ownable(_owner) {
// ... existing validation ...
iWETH = IERC20(_weth);
iWBTC = IERC20(_wbtc);
iWSOL = IERC20(_wsol);
iUSDC = IERC20(_usdc);
+ // Store and validate token decimals
+ tokenDecimals[_weth] = IERC20Metadata(_weth).decimals();
+ tokenDecimals[_wbtc] = IERC20Metadata(_wbtc).decimals();
+ tokenDecimals[_wsol] = IERC20Metadata(_wsol).decimals();
+ tokenDecimals[_usdc] = IERC20Metadata(_usdc).decimals();
+
+ require(tokenDecimals[_weth] <= STANDARD_DECIMALS, "Unsupported decimals");
+ require(tokenDecimals[_wbtc] <= STANDARD_DECIMALS, "Unsupported decimals");
+ require(tokenDecimals[_wsol] <= STANDARD_DECIMALS, "Unsupported decimals");
}
+ function normalizeAmount(address token, uint256 amount) internal view returns (uint256) {
+ uint8 decimals = tokenDecimals[token];
+ return amount * (10 ** (STANDARD_DECIMALS - decimals));
+ }
+
+ function denormalizeAmount(address token, uint256 normalizedAmount) internal view returns (uint256) {
+ uint8 decimals = tokenDecimals[token];
+ return normalizedAmount / (10 ** (STANDARD_DECIMALS - decimals));
+ }
function createSellOrder(
address _tokenToSell,
uint256 _amountToSell,
uint256 _priceInUSDC,
uint256 _deadlineDuration
) public returns (uint256) {
// ... existing validation ...
+ // Normalize amounts for consistent storage
+ uint256 normalizedAmount = normalizeAmount(_tokenToSell, _amountToSell);
+ uint256 normalizedPrice = normalizeAmount(address(iUSDC), _priceInUSDC);
// Store the order with normalized amounts
orders[orderId] = Order({
id: orderId,
seller: msg.sender,
tokenToSell: _tokenToSell,
- amountToSell: _amountToSell,
- priceInUSDC: _priceInUSDC,
+ amountToSell: normalizedAmount,
+ priceInUSDC: normalizedPrice,
deadlineTimestamp: deadlineTimestamp,
isActive: true
});
}
Updates

Lead Judging Commences

yeahchibyke Lead Judge
4 months ago
yeahchibyke Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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