# audit-math-precision

Audits Solidity smart contracts for arithmetic precision vulnerabilities including division-before-multiplication causing value loss, small amounts rounding to zero enabling fee bypass, token decimal mismatches in multi-asset pools, unsafe downcasts truncating storage values, incorrect rounding direction leaking protocol fees, inverted oracle price pairs, hardcoded decimal assumptions, and time unit confusion in interest calculations

- **Kind:** skill
- **Source:** https://github.com/auditmos/skills
- **Page:** https://forefy.com/skills/95cb1f8d-a521-4036-9e29-91075a87d140
- **API (JSON + files):** https://forefy.com/api/asr/95cb1f8d-a521-4036-9e29-91075a87d140

---

## SKILL.md

---
name: audit-math-precision
description: Audits Solidity smart contracts for arithmetic precision vulnerabilities including division-before-multiplication causing value loss, small amounts rounding to zero enabling fee bypass, token decimal mismatches in multi-asset pools, unsafe downcasts truncating storage values, incorrect rounding direction leaking protocol fees, inverted oracle price pairs, hardcoded decimal assumptions, and time unit confusion in interest calculations
allowed-tools: Read, Grep, Glob
license: MIT
compatibility: Designed for Claude Code (or similar products)
metadata:
  author: Tomasz Kowalczyk (tom@auditmos.com)
  version: "1.0"
---

# Math Precision Auditor

## When to Use
- Auditing Solidity contracts for arithmetic vulnerabilities
- User mentions: precision, decimals, rounding, overflow, downcasting, division, multiplication, fees, rewards
- Analyzing token protocols, DeFi systems, AMMs, lending protocols
- Reviewing calculations involving multiple tokens with different decimals

## Audit Workflow

**IMPORTANT: Announce skill usage at the start of analysis**

Begin with: "I'm using the **audit-math-precision** skill to analyze this contract for arithmetic and precision vulnerabilities..."

1. **Scan for arithmetic operations**
   - Search: `* / % **` operators, type casting, `unchecked` blocks
   - Focus: fee calculations, reward distributions, token conversions, oracle price usage

2. **Check against vulnerability patterns**
   - Reference `reference.md` for complete checklist
   - Compare code against `example.md`

3. **Validate findings**
   - **Check access control first** - grep for `onlyOwner|onlyAdmin|onlyGovernance` modifiers
   - Verify exploitability by non-privileged actors (not just style issues)
   - Calculate impact (% loss or USD value)
   - Check for compensating protections
   - Downgrade severity if admin-only unless severe impact

4. **Generate report**
   - Use deliverable template below
   - Include PoC for each finding
   - Rank by severity

## Core Vulnerability Patterns

See `reference.md` for full checklist. Key patterns:

1. Division before multiplication → precision loss
2. Small amounts round to zero → fee bypass
3. Token decimal mismatches → magnitude errors
4. Unsafe downcasting → truncation
5. Wrong rounding direction → protocol value leak
6. Inverted oracle pairs → incorrect calculations
7. Hardcoded decimal assumptions → breaks with different tokens
8. Time unit confusion → interest calculation errors
9. Unchecked arithmetic → silent overflows
10. Exponentiation precision loss → compound calculation errors

**Code examples:** See `example.md`

## Severity Criteria

**Critical:** Direct fund extraction, >10% value loss, no preconditions required
**High:** 1-10% value leakage, specific but achievable conditions, affects protocol solvency, **MUST be exploitable by non-privileged actors**
**Medium:** <1% precision loss in edge cases, requires privileged actors or specific conditions
**Low:** Gas inefficiency, view function issues, admin-only precision issues, no security impact

**IMPORTANT:** Admin-only functions (onlyOwner, onlyAdmin, onlyGovernance) with precision issues are **LOW severity** unless:
- Precision loss is severe (>10% of intended value)
- Function is called frequently in normal operations
- Error cascades to affect user funds directly

## False Positives - Do NOT Flag

- Division-before-multiplication in view functions (display only)
- Zero-rounding with explicit `require(fee > 0, ...)`
- Different decimals in isolated modules (no cross-interaction)
- Downcasts with explicit bounds check: `require(value <= type(uint96).max)`
- Documented "favor user" rounding policies
- **Admin-only functions** (onlyOwner, onlyAdmin, onlyGovernance modifiers) with minor precision issues
- Setter functions with division-before-multiplication where values are large enough to avoid practical loss
- One-time initialization functions with precision quirks (not called in normal operations)

## Deliverable Format

**MANDATORY:** Before deliverable, verify each `checklist.md` item against codebase. Flag violations as findings.

Use template: `templates/report-template.md`

Each finding includes: severity, pattern #, file/lines, description, vulnerable code, impact analysis, PoC, remediation, gas impact.

## Key Principles

- **Multiply first, divide last** - minimize truncation impact
- **Protocol-favoring rounding** - round fees up, withdrawals down
- **Explicit decimals** - never assume 18 decimals
- **Validate minimums** - prevent rounding-to-zero attacks

## Output Guidelines

**DO:**
- Reference specific lines and functions
- Provide executable PoCs
- Quantify potential loss
- Group similar issues

**DON'T:**
- Report style issues
- Flag intentional designs without exploit path
- Use vague terms ("might be vulnerable")
- Ignore context

## checklist.md

* [ ] Multiplication always performed before division
* [ ] Checks for rounding to zero with appropriate reverts
* [ ] Token amounts scaled to common precision before calculations
* [ ] No double-scaling of already scaled values
* [ ] Consistent precision scaling across all modules
* [ ] SafeCast used for all downcasting operations
* [ ] Protocol fees round up, user amounts round down
* [ ] Decimal assumptions documented and validated
* [ ] Interest calculations use correct time units
* [ ] Token pair directions consistent across calculations

## example.md

# Code Examples: Math Precision Vulnerabilities

## Vulnerable Examples

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract VulnerableMathExamples {
    uint256 constant PRECISION = 1e18;
    uint256 constant FEE_BPS = 500; // 5%

    // Pattern #1: Division Before Multiplication
    function calculateReward_VULNERABLE(uint256 amount, uint256 rate) public pure returns (uint256) {
        return (amount / PRECISION) * rate; // Loses precision
    }

    // Pattern #2: Rounding Down to Zero
    function collectFee_VULNERABLE(uint256 amount) public pure returns (uint256) {
        uint256 fee = (amount * FEE_BPS) / 10000;
        // If amount < 20, fee = 0 (free transactions)
        return fee;
    }

    // Pattern #3: No Precision Scaling (Mixed Decimals)
    function addLiquidity_VULNERABLE(uint256 amountUSDC, uint256 amountDAI) public pure returns (uint256) {
        // USDC is 6 decimals, DAI is 18 decimals
        return amountUSDC + amountDAI; // Magnitude error!
    }

    // Pattern #4: Excessive Precision Scaling
    function convertToken_VULNERABLE(uint256 amount) public pure returns (uint256) {
        // amount is already 1e18
        return amount * 1e18; // Inflates by 10^18!
    }

    // Pattern #5: Mismatched Precision Scaling
    function scaleAmount_VULNERABLE(uint256 amount) public pure returns (uint256) {
        // Assumes 18 decimals but token might be WBTC (8 decimals)
        return amount * 1e18;
    }

    // Pattern #6: Downcast Overflow
    struct Checkpoint {
        uint96 votes;
        uint32 blockNumber;
    }

    function setVotes_VULNERABLE(uint256 amount) public pure returns (uint96) {
        // Silent overflow if amount > type(uint96).max
        return uint96(amount);
    }

    // Pattern #7: Rounding Leaks Value (Protocol Fees)
    function calculateProtocolFee_VULNERABLE(uint256 amount, uint256 bps) public pure returns (uint256) {
        return (amount * bps) / 10000; // Rounds down, user pays less
    }

    // Pattern #8: Inverted Oracle Pairs
    function swapTokens_VULNERABLE(uint256 tokenAAmount, uint256 priceAinB) public pure returns (uint256) {
        // If we want B but oracle gives price of A in terms of B, this is wrong
        return tokenAAmount * priceAinB; // Should divide for inversion
    }

    // Pattern #9: Decimal Assumption Errors
    function calculateOneToken_VULNERABLE() public pure returns (uint256) {
        uint256 oneToken = 1 ether; // Assumes 18 decimals
        // Breaks if used with USDC (6) or WBTC (8)
        return oneToken;
    }

    // Pattern #10: Interest Calculation Time Unit Confusion
    function calculateInterest_VULNERABLE(
        uint256 principal,
        uint256 ratePerYear,
        uint256 lastUpdate
    ) public view returns (uint256) {
        uint256 timeElapsed = block.timestamp - lastUpdate;
        // ratePerYear is annual but timeElapsed is seconds - missing conversion!
        return principal * ratePerYear * timeElapsed;
    }

    // Pattern #11: Phantom Overflow (Unchecked Blocks)
    function addBalance_VULNERABLE(uint256 balance, uint256 amount) public pure returns (uint256) {
        unchecked {
            return balance + amount; // Can wrap to 0
        }
    }

    // Pattern #12: Loss of Precision in Exponentiation
    function compoundInterest_VULNERABLE(uint256 principal, uint256 rate, uint256 years)
        public pure returns (uint256)
    {
        return principal * (rate ** years); // Loses precision each iteration
    }

    // Pattern #13: Percentage Calculation Base Confusion
    function calculateFeeConfused_VULNERABLE(uint256 amount) public pure returns (uint256) {
        // Meant 5 bps (0.05%) but calculated 5%
        return (amount * 5) / 100;
    }
}
```

## Fixed Examples

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol";

interface IERC20Extended {
    function decimals() external view returns (uint8);
}

contract FixedMathExamples {
    using SafeCast for uint256;

    uint256 constant PRECISION = 1e18;
    uint256 constant FEE_BPS = 500; // 5%
    uint256 constant SECONDS_PER_YEAR = 31536000;

    // Pattern #1 FIXED: Multiply First, Divide Last
    function calculateReward_FIXED(uint256 amount, uint256 rate) public pure returns (uint256) {
        return (amount * rate) / PRECISION; // Multiply first preserves precision
    }

    // Pattern #2 FIXED: Prevent Rounding to Zero
    function collectFee_FIXED(uint256 amount) public pure returns (uint256) {
        uint256 fee = (amount * FEE_BPS) / 10000;
        require(fee > 0, "Amount too small"); // Explicit minimum check
        return fee;
    }

    // Pattern #3 FIXED: Normalize Decimals Before Math
    function addLiquidity_FIXED(uint256 amountUSDC, uint256 amountDAI) public pure returns (uint256) {
        // Normalize USDC (6 decimals) to 18 decimals before adding
        uint256 normalizedUSDC = amountUSDC * 1e12; // 6 + 12 = 18
        return normalizedUSDC + amountDAI; // Both 18 decimals now
    }

    // Pattern #4 FIXED: Don't Double-Scale
    function convertToken_FIXED(uint256 amount) public pure returns (uint256) {
        // amount is already 1e18, use as-is
        return amount;
    }

    // Pattern #5 FIXED: Query Decimals Dynamically
    function scaleAmount_FIXED(address token, uint256 amount) public view returns (uint256) {
        uint8 decimals = IERC20Extended(token).decimals();
        if (decimals < 18) {
            return amount * (10 ** (18 - decimals));
        }
        return amount;
    }

    // Pattern #6 FIXED: Safe Downcast with Validation
    struct Checkpoint {
        uint96 votes;
        uint32 blockNumber;
    }

    function setVotes_FIXED(uint256 amount) public pure returns (uint96) {
        // Explicit bounds check before downcast
        require(amount <= type(uint96).max, "Amount exceeds uint96");
        return uint96(amount);

        // Or use OpenZeppelin SafeCast:
        // return amount.toUint96();
    }

    // Pattern #7 FIXED: Round Fees UP (Ceiling Math)
    function calculateProtocolFee_FIXED(uint256 amount, uint256 bps) public pure returns (uint256) {
        // Add (denominator - 1) before dividing to round up
        return (amount * bps + 9999) / 10000;
    }

    // Pattern #8 FIXED: Invert Oracle Price Correctly
    function swapTokens_FIXED(uint256 tokenAAmount, uint256 priceAinB) public pure returns (uint256) {
        // To get B from A when price is "A in terms of B", divide
        return (tokenAAmount * PRECISION) / priceAinB;
    }

    // Pattern #9 FIXED: Use Explicit Decimals Per Token
    uint256 constant USDC_DECIMALS = 6;
    uint256 constant WBTC_DECIMALS = 8;
    uint256 constant DAI_DECIMALS = 18;

    function calculateOneToken_FIXED(address token) public view returns (uint256) {
        uint8 decimals = IERC20Extended(token).decimals();
        return 10 ** decimals;
    }

    // Pattern #10 FIXED: Convert Time Units for Interest
    function calculateInterest_FIXED(
        uint256 principal,
        uint256 ratePerYear,
        uint256 lastUpdate
    ) public view returns (uint256) {
        uint256 timeElapsed = block.timestamp - lastUpdate;
        // Convert annual rate to per-second rate
        return (principal * ratePerYear * timeElapsed) / SECONDS_PER_YEAR;
    }

    // Pattern #11 FIXED: Only Use Unchecked When Provably Safe
    function addBalance_FIXED(uint256 balance, uint256 amount) public pure returns (uint256) {
        // Use checked arithmetic (default in 0.8+)
        return balance + amount; // Reverts on overflow

        // Only use unchecked for guaranteed-safe operations like:
        // unchecked { for (uint256 i = 0; i < array.length; ++i) { ... } }
    }

    // Pattern #12 FIXED: Iterative Multiplication with Scaling
    function compoundInterest_FIXED(uint256 principal, uint256 rate, uint256 years)
        public pure returns (uint256)
    {
        uint256 amount = principal;
        for (uint256 i = 0; i < years; i++) {
            amount = (amount * rate) / PRECISION;
        }
        return amount;
    }

    // Pattern #13 FIXED: Standardize on Basis Points
    function calculateFee_FIXED(uint256 amount) public pure returns (uint256) {
        // Use 10000 BPS consistently, document clearly
        uint256 FEE_5_BPS = 5; // 0.05% = 5 basis points
        return (amount * FEE_5_BPS) / 10000;
    }

    // Bonus: Helper function for ceiling division
    function ceilDiv(uint256 a, uint256 b) internal pure returns (uint256) {
        require(b > 0, "Division by zero");
        return (a + b - 1) / b;
    }

    // Bonus: Safe multi-decimal normalization helper
    function normalizeDecimals(
        uint256 amount,
        uint8 fromDecimals,
        uint8 toDecimals
    ) internal pure returns (uint256) {
        if (fromDecimals == toDecimals) return amount;

        if (fromDecimals < toDecimals) {
            return amount * (10 ** (toDecimals - fromDecimals));
        } else {
            return amount / (10 ** (fromDecimals - toDecimals));
        }
    }
}
```

## reference.md

# Precision & Mathematical Vulnerabilities

1. **Division Before Multiplication** - Always multiply before dividing to minimize rounding errors
2. **Rounding Down To Zero** - Small values can round to 0, allowing state changes without proper accounting
3. **No Precision Scaling** - Mixing tokens with different decimals without scaling causes calculation errors
4. **Excessive Precision Scaling** - Re-scaling already scaled values leads to inflated amounts
5. **Mismatched Precision Scaling** - Different modules using different scaling methods (decimals vs hardcoded 1e18)
6. **Downcast Overflow** - Downcasting can silently overflow, breaking pre-downcast invariant checks
7. **Rounding Leaks Value From Protocol** - Fee calculations should round in favor of the protocol, not users
8. **Inverted Base/Rate Token Pairs** - Using opposite token pairs in calculations (e.g., WETH/DAI vs DAI/ETH)
9. **Decimal Assumption Errors** - Assuming all tokens have 18 decimals when some have 6, 8, or 2
10. **Interest Calculation Time Unit Confusion** - Mixing per-second and per-year rates without proper conversion

## templates

```

```

## templates/report-template.md

# Mathematical Precision Audit Report

**Contract:** [Contract Name]
**Files Analyzed:** [List of .sol files]
**Vulnerabilities Found:** Critical: X | High: Y | Medium: Z | Low: W

---

## [SEVERITY] Vulnerability Title
**Pattern:** #[1-13]
**File:** `path/to/Contract.sol`
**Lines:** [line numbers]
**Function:** `functionName()`

### Description
[2-3 sentences explaining the issue and why it matters]

### Vulnerable Code
```solidity
// Actual code from contract with line numbers
145: function calculateRewards(uint256 amount) public {
146:     uint256 reward = (amount / PRECISION) * rewardRate; // Division before multiplication
147:     userRewards[msg.sender] += reward;
148: }
```

### Impact
- **Loss Magnitude:** Up to [X]% precision loss per transaction
- **Exploitability:** [High/Medium/Low] - [Explanation of preconditions]
- **Affected Functions:** [List if multiple functions share this pattern]

### Proof of Concept
```solidity
function testExploit() public {
    // Setup
    uint256 smallAmount = 999; // Below PRECISION threshold

    // Execute vulnerable function
    contract.calculateRewards(smallAmount);

    // Demonstrate loss
    // Expected: X, Actual: 0 (rounded down)
    assert(userRewards[msg.sender] == 0); // User receives nothing
}
```

### Remediation
```solidity
// Fixed code
function calculateRewards(uint256 amount) public {
    uint256 reward = (amount * rewardRate) / PRECISION; // Multiply first
    require(reward > 0, "Amount too small");
    userRewards[msg.sender] += reward;
}
```

**Gas Impact:** +[X] gas per call (negligible vs security improvement)

---

## Summary

### Critical Issues Requiring Immediate Attention
1. [Issue #X] - [Brief description] - [Estimated loss potential]
2. [Issue #Y] - [Brief description] - [Estimated loss potential]

### Recommendations
- Standardize scaling: Use consistent WAD (1e18) or RAY (1e27) throughout
- Add minimum amount thresholds for all fee/reward calculations
- Implement SafeCast for all downcast operations
- Document rounding direction policy in natspec comments
- Query token decimals dynamically; avoid hardcoded assumptions

