High-Precision Fixed-Point 128 Bit Math
Dealing with decimals is a notorious issue for most developers on other chains, especially when working with decentralized finance (DeFi). Blockchains are deterministic systems and floating-point arithmetic is non-deterministic across different compilers and architectures, which is why blockchains use fixed-point arithmetic via integers (scaling numbers by a fixed factor).
The issue with this is that these fixed-point integers tend to be very imprecise when using various mathematical operations on them. The more operations you apply to these numbers, the more imprecise these numbers become. However DeFiActionsMathUtils provides a standardized library for high-precision mathematical operations in DeFi applications on Flow. The contract extends Cadence's native 8-decimal precision (UFix64) to 24 decimals using UInt128 for intermediate calculations, ensuring accuracy in complex financial computations while maintaining deterministic results across the network.
Through integration of this math utility library, developers can ensure that their DeFi protocols perform precise calculations for liquidity pools, yield farming, token swaps, and other financial operations without accumulating rounding errors.
While this documentation focuses on DeFi use cases, you can use these mathematical utilities for any application requiring high-precision decimal arithmetic beyond the native 8-decimal limitation of UFix64.
The Precision Problem
DeFi applications often require multiple sequential calculations, and each operation can introduce rounding errors. When these errors compound over multiple operations, they can lead to:
- Price manipulation vulnerabilities
- Incorrect liquidity calculations
- Unfair token distributions
- Arbitrage opportunities from precision loss
Consider a simple example:
_10// Native UFix64 with 8 decimals_10let price: UFix64 = 1.23456789 // Actually stored as 1.23456789_10let amount: UFix64 = 1000000.0_10let fee: UFix64 = 0.003 // 0.3%_10_10// Multiple operations compound rounding errors_10let afterFee = amount * (1.0 - fee)     // Some precision lost_10let output = afterFee * price            // More precision lost_10let finalAmount = output / someRatio     // Even more precision lost
After three-to-four sequential operations, significant cumulative rounding errors can occur, especially when dealing with large amounts. Assuming a rounding error with eight decimals (1.234567885 rounds up to 1.23456789, causing a rounding error of 0.000000005), then after 100 operations with this error and dealing with one million dollars USDF, the protocol loses $0.5 in revenue from this lack of precision. This might not seem like a lot, but if we consider the TVL of Aave, which is around 40 billion USD, then that loss results in $20,000 USD!
The Solution: 24-Decimal Precision
DeFiActionsMathUtils solves this with UInt128 to represent fixed-point numbers with 24 decimal places (scaling factor of 10^24). This provides 16 additional decimal places for intermediate calculations, dramatically reducing precision loss.
There is still some precision loss occurring, but it is much smaller than with eight decimals.
The Three-Tier Precision System
The contract implements a precision sandwich pattern:
- Input Layer: UFix64(8 decimals) - User-facing values
- Processing Layer: UInt128(24 decimals) - Internal calculations
- Output Layer: UFix64(8 decimals) - Final results with smart rounding
_13// Import the contract_13import DeFiActionsMathUtils from 'ContractAddress'_13_13// Convert UFix64 to high-precision UInt128_13let inputAmount: UFix64 = 1000.12345678_13let highPrecision = DeFiActionsMathUtils.toUInt128(inputAmount)_13// highPrecision now represents 1000.123456780000000000000000 (24 decimals)_13_13// Perform calculations at 24-decimal precision_13let result = DeFiActionsMathUtils.mul(highPrecision, anotherValue)_13_13// Convert back to UFix64 with rounding_13let output = DeFiActionsMathUtils.toUFix64Round(result)
Core Constants
The contract defines several key constants:
_10access(all) let e24: UInt128  // 10^24 = 1,000,000,000,000,000,000,000,000_10access(all) let e8: UInt128   // 10^8 = 100,000,000_10access(all) let decimals: UInt8 // 24
These constants ensure consistent scaling across all operations.
Rounding Modes
Smart rounding is the strategic selection of rounding strategies based on the financial context of your calculation. After performing high-precision calculations at 24 decimals, you must convert the final results back to UFix64 (8 decimals). How you handle this conversion can protect your protocol from losses, ensure fairness to users, and reduce systematic bias.
DeFiActionsMathUtils provides four rounding modes, each optimized for specific financial scenarios:
_13access(all) enum RoundingMode: UInt8 {_13    /// Rounds down (floor) - use for payouts_13    access(all) case RoundDown_13_13    /// Rounds up (ceiling) - use for fees/liabilities_13    access(all) case RoundUp_13_13    /// Standard rounding: < 0.5 down, >= 0.5 up_13    access(all) case RoundHalfUp_13_13    /// Banker's rounding: ties round to even number_13    access(all) case RoundEven_13}
When to Use Each Mode
RoundDown - Choose this when you calculate user payouts, withdrawals, or rewards. When you round down, your protocol retains any fractional amounts, which protects against losses from accumulated rounding errors. This is the conservative choice when funds leave your protocol.
_10// When you calculate how much to pay out to users_10let userReward = DeFiActionsMathUtils.toUFix64RoundDown(calculatedReward)
RoundUp - Use this for protocol fees, transaction costs, or amounts owed to your protocol. Rounding up ensures your protocol collects slightly more, compensating for precision loss and preventing systematic under-collection of fees over many transactions.
_10// When calculating fees the protocol collects_10let protocolFee = DeFiActionsMathUtils.toUFix64RoundUp(calculatedFee)
RoundHalfUp - Apply this for general-purpose calculations, display values, or when presenting prices to users. This is the familiar rounding method (values 0.5 and above round up, below 0.5 round down) that users expect in traditional finance.
_10// For display values or general calculations_10let displayValue = DeFiActionsMathUtils.toUFix64Round(calculatedValue)
RoundEven - Select this for scenarios with many repeated calculations where you want to minimize systematic bias. Also known as "banker's rounding", this mode rounds ties (exactly 0.5) to the nearest even number, which statistically balances out over many operations, making it ideal for large-scale distributions or statistical calculations.
_10// For repeated operations where bias matters_10let unbiasedValue = DeFiActionsMathUtils.toUFix64(calculatedValue, DeFiActionsMathUtils.RoundingMode.RoundEven)
Core Functions
Conversion Functions
Converting UFix64 to UInt128
_10access(all) view fun toUInt128(_ value: UFix64): UInt128
Converts a UFix64 value to UInt128 with 24-decimal precision.
Example:
_10import DeFiActionsMathUtils from 'ContractAddress'_10_10let price: UFix64 = 123.45678900_10let highPrecisionPrice = DeFiActionsMathUtils.toUInt128(price)_10// highPrecisionPrice = 123456789000000000000000000 (represents 123.45678900... with 24 decimals)
Converting UInt128 to UFix64
_10access(all) view fun toUFix64(_ value: UInt128, _ roundingMode: RoundingMode): UFix64_10access(all) view fun toUFix64Round(_ value: UInt128): UFix64_10access(all) view fun toUFix64RoundDown(_ value: UInt128): UFix64_10access(all) view fun toUFix64RoundUp(_ value: UInt128): UFix64
Converts a UInt128 value back to UFix64, applying the specified rounding strategy.
Example:
_10let highPrecisionValue: UInt128 = 1234567890123456789012345678_10let roundedValue = DeFiActionsMathUtils.toUFix64Round(highPrecisionValue)_10// roundedValue = 1234567.89012346 (rounded to 8 decimals using RoundHalfUp)_10_10let flooredValue = DeFiActionsMathUtils.toUFix64RoundDown(highPrecisionValue)_10// flooredValue = 1234567.89012345 (truncated to 8 decimals)_10_10let ceilingValue = DeFiActionsMathUtils.toUFix64RoundUp(highPrecisionValue)_10// ceilingValue = 1234567.89012346 (rounded up to 8 decimals)
High-Precision Arithmetic
Multiplication
_10access(all) view fun mul(_ x: UInt128, _ y: UInt128): UInt128
Multiplies two 24-decimal fixed-point numbers, maintaining precision.
Example:
_10let amount = DeFiActionsMathUtils.toUInt128(1000.0)_10let price = DeFiActionsMathUtils.toUInt128(1.5)_10_10let totalValue = DeFiActionsMathUtils.mul(amount, price)_10let result = DeFiActionsMathUtils.toUFix64Round(totalValue)_10// result = 1500.0
Important: The multiplication uses UInt256 internally to prevent overflow:
_10// Internal implementation prevents overflow_10return UInt128(UInt256(x) * UInt256(y) / UInt256(e24))
Division
_10access(all) view fun div(_ x: UInt128, _ y: UInt128): UInt128
Divides two 24-decimal fixed-point numbers, maintaining precision.
Example:
_10let totalValue = DeFiActionsMathUtils.toUInt128(1500.0)_10let shares = DeFiActionsMathUtils.toUInt128(3.0)_10_10let pricePerShare = DeFiActionsMathUtils.div(totalValue, shares)_10let result = DeFiActionsMathUtils.toUFix64Round(pricePerShare)_10// result = 500.0
UFix64 Division with Rounding
For convenience, the contract provides direct division functions that handle conversion and rounding in one call:
_10access(all) view fun divUFix64WithRounding(_ x: UFix64, _ y: UFix64): UFix64_10access(all) view fun divUFix64WithRoundingUp(_ x: UFix64, _ y: UFix64): UFix64_10access(all) view fun divUFix64WithRoundingDown(_ x: UFix64, _ y: UFix64): UFix64
Example:
_14let totalAmount: UFix64 = 1000.0_14let numberOfUsers: UFix64 = 3.0_14_14// Standard rounding_14let perUserStandard = DeFiActionsMathUtils.divUFix64WithRounding(totalAmount, numberOfUsers)_14// perUserStandard = 333.33333333_14_14// Round down (conservative for payouts)_14let perUserSafe = DeFiActionsMathUtils.divUFix64WithRoundingDown(totalAmount, numberOfUsers)_14// perUserSafe = 333.33333333_14_14// Round up (conservative for fees)_14let perUserFee = DeFiActionsMathUtils.divUFix64WithRoundingUp(totalAmount, numberOfUsers)_14// perUserFee = 333.33333334
Common DeFi Use Cases
Liquidity Pool Pricing (Constant Product AMM)
Automated Market Makers like Uniswap use the formula x * y = k. Here's how to calculate swap outputs with high precision:
_35import DeFiActionsMathUtils from 'ContractAddress'_35import FungibleToken from 'FungibleTokenAddress'_35_35access(all) fun calculateSwapOutput(_35    inputAmount: UFix64,_35    inputReserve: UFix64,_35    outputReserve: UFix64,_35    feeBasisPoints: UFix64  // e.g., 30 for 0.3%_35): UFix64 {_35    // Convert to high precision_35    let input = DeFiActionsMathUtils.toUInt128(inputAmount)_35    let reserveIn = DeFiActionsMathUtils.toUInt128(inputReserve)_35    let reserveOut = DeFiActionsMathUtils.toUInt128(outputReserve)_35    let fee = DeFiActionsMathUtils.toUInt128(feeBasisPoints)_35    let basisPoints = DeFiActionsMathUtils.toUInt128(10000.0)_35_35    // Calculate: inputWithFee = inputAmount * (10000 - fee)_35    let feeMultiplier = DeFiActionsMathUtils.div(_35        basisPoints - fee,_35        basisPoints_35    )_35    let inputWithFee = DeFiActionsMathUtils.mul(input, feeMultiplier)_35_35    // Calculate: numerator = inputWithFee * outputReserve_35    let numerator = DeFiActionsMathUtils.mul(inputWithFee, reserveOut)_35_35    // Calculate: denominator = inputReserve + inputWithFee_35    let denominator = reserveIn + inputWithFee_35_35    // Calculate output: numerator / denominator_35    let output = DeFiActionsMathUtils.div(numerator, denominator)_35_35    // Return with conservative rounding (round down for user protection)_35    return DeFiActionsMathUtils.toUFix64RoundDown(output)_35}
Compound Interest Calculations
Calculate compound interest for yield farming rewards:
_30import DeFiActionsMathUtils from 0xYourAddress_30_30access(all) fun calculateCompoundInterest(_30    principal: UFix64,_30    annualRate: UFix64,  // e.g., 0.05 for 5%_30    periodsPerYear: UInt64,_30    numberOfYears: UFix64_30): UFix64 {_30    // Convert to high precision_30    let p = DeFiActionsMathUtils.toUInt128(principal)_30    let r = DeFiActionsMathUtils.toUInt128(annualRate)_30    let n = DeFiActionsMathUtils.toUInt128(UFix64(periodsPerYear))_30    let t = DeFiActionsMathUtils.toUInt128(numberOfYears)_30    let one = DeFiActionsMathUtils.toUInt128(1.0)_30_30    // Calculate: rate per period = r / n_30    let ratePerPeriod = DeFiActionsMathUtils.div(r, n)_30_30    // Calculate: (1 + rate per period)_30    let onePlusRate = one + ratePerPeriod_30_30    // Calculate: number of periods = n * t_30    let totalPeriods = DeFiActionsMathUtils.mul(n, t)_30_30    // Note: For production, you'd need to implement a power function_30    // This is simplified for demonstration_30_30    // Calculate final amount with rounding_30    return DeFiActionsMathUtils.toUFix64Round(finalAmount)_30}
Proportional Distribution
Distribute rewards proportionally among stakeholders:
_19import DeFiActionsMathUtils from 0xYourAddress_19_19access(all) fun calculateProportionalShare(_19    totalRewards: UFix64,_19    userStake: UFix64,_19    totalStaked: UFix64_19): UFix64 {_19    // Convert to high precision_19    let rewards = DeFiActionsMathUtils.toUInt128(totalRewards)_19    let stake = DeFiActionsMathUtils.toUInt128(userStake)_19    let total = DeFiActionsMathUtils.toUInt128(totalStaked)_19_19    // Calculate: (userStake / totalStaked) * totalRewards_19    let proportion = DeFiActionsMathUtils.div(stake, total)_19    let userReward = DeFiActionsMathUtils.mul(proportion, rewards)_19_19    // Round down for conservative payout_19    return DeFiActionsMathUtils.toUFix64RoundDown(userReward)_19}
Price Impact Calculation
Calculate the price impact of a large trade:
_29import DeFiActionsMathUtils from 0xYourAddress_29_29access(all) fun calculatePriceImpact(_29    inputAmount: UFix64,_29    inputReserve: UFix64,_29    outputReserve: UFix64_29): UFix64 {_29    // Convert to high precision_29    let input = DeFiActionsMathUtils.toUInt128(inputAmount)_29    let reserveIn = DeFiActionsMathUtils.toUInt128(inputReserve)_29    let reserveOut = DeFiActionsMathUtils.toUInt128(outputReserve)_29_29    // Calculate initial price: outputReserve / inputReserve_29    let initialPrice = DeFiActionsMathUtils.div(reserveOut, reserveIn)_29_29    // Calculate new reserves after trade_29    let newReserveIn = reserveIn + input_29    let k = DeFiActionsMathUtils.mul(reserveIn, reserveOut)_29    let newReserveOut = DeFiActionsMathUtils.div(k, newReserveIn)_29_29    // Calculate final price: newOutputReserve / newInputReserve_29    let finalPrice = DeFiActionsMathUtils.div(newReserveOut, newReserveIn)_29_29    // Calculate impact: (initialPrice - finalPrice) / initialPrice_29    let priceDiff = initialPrice - finalPrice_29    let impact = DeFiActionsMathUtils.div(priceDiff, initialPrice)_29_29    return DeFiActionsMathUtils.toUFix64Round(impact)_29}
Benefits of High-Precision Math
Precision Preservation
The 24-decimal precision provides headroom for complex calculations:
_10// Chain multiple operations without significant precision loss_10let step1 = DeFiActionsMathUtils.mul(valueA, valueB)_10let step2 = DeFiActionsMathUtils.div(step1, valueC)_10let step3 = DeFiActionsMathUtils.mul(step2, valueD)_10let step4 = DeFiActionsMathUtils.div(step3, valueE)_10// Still maintains 24 decimals of precision until final conversion
Overflow Protection
The contract uses UInt256 for intermediate multiplication to prevent overflow:
_10// Internal implementation protects against overflow_10access(all) view fun mul(_ x: UInt128, _ y: UInt128): UInt128 {_10    return UInt128(UInt256(x) * UInt256(y) / UInt256(self.e24))_10}
And includes explicit bounds checking when converting to UFix64:
_10access(all) view fun assertWithinUFix64Bounds(_ value: UInt128) {_10    let MAX_1E24: UInt128 = 184_467_440_737_095_516_150_000_000_000_000_000_10    assert(_10        value <= MAX_1E24,_10        message: "Value exceeds UFix64.max"_10    )_10}
Best Practices
Always Use High Precision for Intermediate Calculations.
❌ Low precision (loses ~$0.50 per 1M USDC):
_10let fee: UFix64 = amount * 0.003_10let afterFee: UFix64 = amount - fee_10let output: UFix64 = afterFee * price
✅ High precision (safe and accurate):
_11// Convert once at the start_11let amountHP = DeFiActionsMathUtils.toUInt128(amount)_11let feeRate = DeFiActionsMathUtils.toUInt128(0.003)_11let priceHP = DeFiActionsMathUtils.toUInt128(price)_11_11// Perform all calculations at high precision_11let afterFeeHP = DeFiActionsMathUtils.mul(amountHP, DeFiActionsMathUtils.toUInt128(1.0) - feeRate)_11let outputHP = DeFiActionsMathUtils.mul(afterFeeHP, priceHP)_11_11// Convert once at the end with smart rounding_11let output = DeFiActionsMathUtils.toUFix64RoundDown(outputHP)
The pattern is simple: convert → calculate → convert back. The extra lines give you production-grade precision that protects your protocol from financial losses.
Always validate that inputs are within acceptable ranges:
_10access(all) fun swap(inputAmount: UFix64) {_10    pre {_10        inputAmount > 0.0: "Amount must be positive"_10        inputAmount <= 1000000.0: "Amount exceeds maximum"_10    }_10_10    let inputHP = DeFiActionsMathUtils.toUInt128(inputAmount)_10    // ... perform calculations_10}
More Resources
- View the DeFiActionsMathUtils source code
- Flow DeFi Actions Documentation
- Cadence Fixed-Point Numbers
Key takeaways
- Use high precision (24 decimals) for all intermediate calculations.
- Convert to UFix64only for final results.
- Choose appropriate rounding modes based on your use case.
- Always validate inputs and test edge cases.
- Document your rounding decisions for maintainability.
Conclusion
DeFiActionsMathUtils gives Flow developers a significant advantage in building DeFi applications. With 24-decimal precision, it is much more accurate than typical blockchain implementations (which use 6-18 decimals). The standardized library eliminates the need to build custom math implementations.
The simple convert → calculate → convert back pattern, combined with strategic rounding modes and built-in overflow protection, means you can focus on your protocol's business logic instead of low-level precision handling. At scale, this protection prevents thousands of dollars in losses from accumulated rounding errors.