F-2025-0001·decimal-precision

Decimal Precision Issues in Fee Calculation

Acknowledgedescrowpaymentserc-20
TL;DR

The fee calculation uses a fixed divisor (100,000) without normalizing for token decimal precision, so the effective fee rate varies dramatically between tokens (0.0003% USDC vs 0.3% WETH at the same nominal amount).

Severity
MEDIUM
Impact
MEDIUM
Likelihood
MEDIUM
Method
MManual review
CAT.
Complexity
LOW
Exploitability
MEDIUM
02Section · Description

Description

The protocol's fee calculation mechanism fails to account for varying decimal places across different ERC-20 tokens, resulting in inconsistent fee implementation. The core issue exists in the _processFee function:

solidity
function _createERC20Payment(...) internal {
(uint256 feeAmount, uint256 paymentAmount) = _processFee(amount);
// feeAmount could be 0 for very small payments
SafeERC20.safeTransfer(token, feeRecipient, feeAmount);
}

This function calculates fees using a fixed divisor (100,000) without normalizing for token decimal precision, causing the effective fee rate to vary dramatically between tokens with different decimal standards.

Consider two common tokens with different decimal representations:

  • USDC (6 decimals): 1000 USDC = 1000_000000
  • WETH (18 decimals): 1000 WETH = 1000_000000000000000000

When processing a fee of 0.3% (where feeValue = 300):

solidity
// USDC (6 decimals)
feeAmount = 1000_000000 * 300 / 100000 = 3000 (0.003 USDC)
// WETH (18 decimals)
feeAmount = 1000_000000000000000000 * 300 / 100000 = 3000_000000000000000 (3 WETH)

This calculation shows that the effective fee rate is 0.0003% for USDC but 0.3% for WETH when using the same nominal amount (1000 tokens).

03Section · Impact

Impact

  • Tokens with fewer decimals pay much lower fees than intended or even none at all.
  • Tokens with more decimals pay correct fees.
  • Protocol earns significantly less fees from low-decimal tokens.
  • Unfair fee distribution among users of different tokens.
  • Protocol economics are affected as users might prefer low-decimal tokens.
04Section · Recommendation

Recommendation

  1. Scale amounts to 18 decimals before fee calculation:
solidity
function _processFee(uint256 amount, address tokenAddress) internal returns (uint256 feeAmount, uint256 paymentAmount) {
uint8 decimals = ERC20(tokenAddress).decimals();
uint256 scaledAmount = amount * 10**(18 - decimals);
uint256 scaledFee = scaledAmount * feeValue / 100000;
feeAmount = scaledFee / 10**(18 - decimals);
paymentAmount = amount - feeAmount;
}
  1. Store different fee values per token decimals:
solidity
mapping(uint8 => uint256) public decimalToFeeValue;
function setFeeValue(uint256 newValue, uint8 decimals) external onlyOwner {
decimalToFeeValue[decimals] = newValue;
emit FeeValueChanged(newValue, decimals);
}
function _processFee(uint256 amount, address tokenAddress) internal returns (uint256 feeAmount, uint256 paymentAmount) {
uint8 decimals = ERC20(tokenAddress).decimals();
feeAmount = amount * decimalToFeeValue[decimals] / 100000;
paymentAmount = amount - feeAmount;
}
  1. Enforce minimum fee amounts based on decimals:
solidity
function _processFee(uint256 amount, address tokenAddress) internal returns (uint256 feeAmount, uint256 paymentAmount) {
uint8 decimals = ERC20(tokenAddress).decimals();
feeAmount = amount * feeValue / 100000;
uint256 minFee = 10**decimals / 1000; // Minimum 0.001 tokens
if (feeAmount < minFee) {
feeAmount = minFee;
}
paymentAmount = amount - feeAmount;
}

Each approach has its tradeoffs:

  1. Most accurate but higher gas costs.
  2. Flexible but requires more maintenance.
  3. Simplest but might overcharge small amounts.
F-2025-0001

oog
zealynx

Smart Contract Security Digest

Monthly exploit breakdowns, audit checklists, and DeFi security research — straight to your inbox

© 2026 Zealynx