OrderBook

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

Improper handling of token decimals causes massive pricing errors between tokens

Improper handling of token decimals without normalization causes massive pricing errors between tokens meaning tokens with different decimal places are treated identically in pricing calculations

Description

  • Tokens with different decimal places—like WBTC (8) and WETH (18 )—are stored and treated uniformly in the contract without adjusting for their precision.

  • Since sellers set the total price in USDC, a mismatch in decimals can result in orders where the actual value of the token is either massively underpriced or overpriced.

//tokens are stored without normalizing their decimals
orders[orderId] = Order({
id: orderId,
seller: msg.sender,
tokenToSell: _tokenToSell,
@> amountToSell: _amountToSell,
priceInUSDC: _priceInUSDC,
deadlineTimestamp: deadlineTimestamp,
isActive: true
});

Risk

Likelihood:

  • This will occur everytime a user creates a sell order.

Impact:

  • Tokens with high decimals appear artificially cheap. This can lead to unintentional giveaways, unfair trades, and potential exploitation by buyers who recognize the imbalance.

  • The protocol fee is calculated incorrectly.

Proof of Concept

This creates broken pricing where WETH appears free per unit while WBTC has a meaningful price where 1 WBTC priced at $50,000 USDC and 1 WETH priced at $50,000 USDC too.

function test_Decimal_Precision_Bug() public {
console2.log("=== DEMONSTRATING DECIMAL PRECISION ===");
//Alice creates order for 1 WBTC priced at $50,000 USDC
uint256 priceInUSDC = 50000e6; // $50,000 USDC
//WBTC order: 1 WBTC = 100000000 (8 decimals)
uint256 wbtcAmount = 1e8;
vm.startPrank(alice);
wbtc.approve(address(book), wbtcAmount);
uint256 wbtcOrderId = book.createSellOrder(
address(wbtc),
wbtcAmount,
priceInUSDC,
3600
);
vm.stopPrank();
//WETH order: 1 WETH = 1000000000000000000 (18 decimals)
uint256 wethAmount = 1e18;
//Bob creates order for 1 WETH priced at $50,000 USDC
vm.startPrank(bob);
weth.approve(address(book),wethAmount);
uint256 wethOrderId = book.createSellOrder(
address(weth),
wethAmount,
priceInUSDC,
3600
);
vm.stopPrank();
// Get order details
(,,,uint256 storedWbtcAmount, uint256 wbtcPrice,,) = book.orders(wbtcOrderId);
(,,,uint256 storedWethAmount, uint256 wethPrice,,) = book.orders(wethOrderId);
console2.log("WBTC Order - Amount:", storedWbtcAmount);
console2.log("WBTC Order - Price:", wbtcPrice);
console2.log("WETH Order - Amount:", storedWethAmount);
console2.log("WETH Order - Price:", wethPrice);
// Calculate price per token unit (this shows the bug)
// Note: WETH calculation will result in 0 due to integer division
uint256 wbtcPricePerUnit = wbtcPrice / storedWbtcAmount;
uint256 wethPricePerUnit = wethPrice / storedWethAmount; // This will be 0!
console2.log("WBTC Price per unit:", wbtcPricePerUnit);
console2.log("WETH Price per unit:", wethPricePerUnit);
// The bug: WETH price per unit becomes 0 due to integer division
// while WBTC has a meaningful price per unit
assertEq(wethPricePerUnit, 0);
assertGt(wbtcPricePerUnit, 0);
console2.log("WETH price per unit is 0, while WBTC has meaningful price!");
}

Recommended Mitigation

To ensure consistent value representation across tokens with varying decimal places (e.g., 6, 8, or 18 decimals), it's recommended to introduce normalization and denormalization logic within the contract to enhance accuracy and prevent mispricing, fee miscalculations, or potential exploits.

+ uint256 constant NORMALIZED_DECIMALS = 18;
+ mapping(address => uint8) public tokenDecimals; // Store token decimals
+ // Normalize token amount to 18 decimals for internal calculations
+ function normalizeTokenAmount(address token, uint256 amount) internal view returns (uint256) {
+ uint8 decimals = tokenDecimals[token];
+ if (decimals == NORMALIZED_DECIMALS) {
+ return amount;
+ } else if (decimals < NORMALIZED_DECIMALS) {
+ return amount * (10 ** (NORMALIZED_DECIMALS - decimals));
+ } else {
+ return amount / (10 ** (decimals - NORMALIZED_DECIMALS));
+ }
+ }
+ // Denormalize token amount back to original decimals
+ function denormalizeTokenAmount(address token, uint256 normalizedAmount) internal view returns (uint256) {
+ uint8 decimals = tokenDecimals[token];
+ if (decimals == NORMALIZED_DECIMALS) {
+ return normalizedAmount;
+ } else if (decimals < NORMALIZED_DECIMALS) {
+ return normalizedAmount / (10 ** (NORMALIZED_DECIMALS - decimals));
+ } else {
+ return normalizedAmount * (10 ** (decimals - NORMALIZED_DECIMALS));
+ }
+ }
+ //while creating sellOrder
- amountToSell: _amountToSell,
+ amountToSell: normalizeTokenAmount(_tokenToSell, _amountToSell);
+ //while ammending order
- order.amountToSell = _newAmountToSell;
+ rder.amountToSell = normalizeTokenAmount(_tokenToSell, _newAmountToSell);
+ //while buying order
- IERC20(order.tokenToSell).safeTransfer(msg.sender, order.amountToSell);
+ IERC20(order.tokenToSell).safeTransfer(msg.sender, denormalizeTokenAmount(order.tokenToSell, order.amountToSell));
+ //while fetching order
+ orders[_orderId].amountToSell = denormalizeTokenAmount(orders[_orderId].tokenToSell, orders[_orderId].amountToSell);
+ //while feching order details string
- order.amountToSell.toString(),
+ denormalizeTokenAmount(order.tokenToSell, order.amountToSell).toString(),
Updates

Lead Judging Commences

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.