Decimal Precision Issues in Fee Calculation
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).
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:
function _createERC20Payment(...) internal {(uint256 feeAmount, uint256 paymentAmount) = _processFee(amount);// feeAmount could be 0 for very small paymentsSafeERC20.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):
// 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).
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.
Recommendation
- Scale amounts to 18 decimals before fee calculation:
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;}
- Store different fee values per token decimals:
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;}
- Enforce minimum fee amounts based on decimals:
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 tokensif (feeAmount < minFee) {feeAmount = minFee;}paymentAmount = amount - feeAmount;}
Each approach has its tradeoffs:
- Most accurate but higher gas costs.
- Flexible but requires more maintenance.
- Simplest but might overcharge small amounts.

