High-Precision Fixed-Point 128 Bit Math
Dealing with decimals is a notorious issue for most developers on other chains, especially when working with DeFi. Blockchains are deterministic systems and floating-point arithmetic is non-deterministic across different compilers and architectures, this 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, these mathematical utilities can be used 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 3-4 sequential operations, the cumulative rounding error can be significant, especially when dealing with large amounts. Assuming a rounding error with 8 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 1 million dollars USDF, the protocol is losing $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 by using 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.
Note: There is still some precision loss occurring, but it is orders of magnitud smaller than with 8 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, the final results must be converted back to UFix64
(8 decimals), and 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 calculating user payouts, withdrawals, or rewards. By rounding down, your protocol retains any fractional amounts, protecting against losses from accumulated rounding errors. This is the conservative choice when funds leave your protocol.
_10// When calculating 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 involving 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
UFix64
only 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 orders of magnitude 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.