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.
orders[orderId] = Order({
id: orderId,
seller: msg.sender,
tokenToSell: _tokenToSell,
@> amountToSell: _amountToSell,
priceInUSDC: _priceInUSDC,
deadlineTimestamp: deadlineTimestamp,
isActive: true
});
Risk
Likelihood:
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 ===");
uint256 priceInUSDC = 50000e6;
uint256 wbtcAmount = 1e8;
vm.startPrank(alice);
wbtc.approve(address(book), wbtcAmount);
uint256 wbtcOrderId = book.createSellOrder(
address(wbtc),
wbtcAmount,
priceInUSDC,
3600
);
vm.stopPrank();
uint256 wethAmount = 1e18;
vm.startPrank(bob);
weth.approve(address(book),wethAmount);
uint256 wethOrderId = book.createSellOrder(
address(weth),
wethAmount,
priceInUSDC,
3600
);
vm.stopPrank();
(,,,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);
uint256 wbtcPricePerUnit = wbtcPrice / storedWbtcAmount;
uint256 wethPricePerUnit = wethPrice / storedWethAmount;
console2.log("WBTC Price per unit:", wbtcPricePerUnit);
console2.log("WETH Price per unit:", wethPricePerUnit);
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(),