Smart Contract
DeFiMath
A.ca7ee55e4fc3251a.DeFiMath
1/// DeFiMath: High-precision DeFi math utilities using 128-bit fixed-point arithmetic
2///
3/// This contract provides utilities for:
4/// - Slippage calculations using basis points (bps)
5/// - Weighted average price tracking using FP128
6/// - Safe fixed-point arithmetic operations
7///
8/// Educational Notes:
9/// - Basis Points (bps): 1 bps = 0.01%, 100 bps = 1%
10/// - Fixed-Point 128: Uses 128-bit integers to represent decimals with high precision
11/// - All price calculations maintain precision to avoid rounding errors in DCA
12access(all) contract DeFiMath {
13
14 /// Basis points scale: 10000 bps = 100%
15 access(all) let BPS_SCALE: UInt64
16
17 /// Fixed-point 128 scale factor (2^64 for 64.64 fixed point representation)
18 access(all) let FP128_SCALE: UInt128
19
20 init() {
21 self.BPS_SCALE = 10000
22 self.FP128_SCALE = 18446744073709551616 // 2^64
23 }
24
25 /// Calculate minimum output amount after applying slippage protection
26 ///
27 /// @param amountIn: The input amount (in native token units)
28 /// @param expectedPrice: Expected output per input unit (FP128 format)
29 /// @param slippageBps: Maximum acceptable slippage in basis points
30 /// @return minAmountOut: Minimum acceptable output amount
31 ///
32 /// Example:
33 /// - amountIn = 10.0 FLOW (10_00000000 in UFix64)
34 /// - expectedPrice = 2.5 BEAVER per FLOW (in FP128)
35 /// - slippageBps = 100 (1% slippage)
36 /// - Result: minAmountOut = 24.75 BEAVER (2.5 * 10 * 0.99)
37 access(all) fun calculateMinOutWithSlippage(
38 amountIn: UFix64,
39 expectedPriceFP128: UInt128,
40 slippageBps: UInt64
41 ): UFix64 {
42 pre {
43 slippageBps <= self.BPS_SCALE: "Slippage cannot exceed 100%"
44 expectedPriceFP128 > 0: "Expected price must be positive"
45 }
46
47 // Calculate expected output in FP128
48 let amountInScaled = UInt128(amountIn * 100000000.0) // Convert UFix64 to UInt128 (8 decimals)
49 let expectedOutFP128 = (amountInScaled * expectedPriceFP128) / self.FP128_SCALE
50
51 // Apply slippage: minOut = expectedOut * (BPS_SCALE - slippageBps) / BPS_SCALE
52 let slippageMultiplier = UInt128(self.BPS_SCALE - slippageBps)
53 let minOutFP128 = (expectedOutFP128 * slippageMultiplier) / UInt128(self.BPS_SCALE)
54
55 // Convert back to UFix64 (round down for safety)
56 return UFix64(minOutFP128) / 100000000.0
57 }
58
59 /// Update weighted average price using new execution data
60 ///
61 /// Formula: newAvg = (prevAvg * totalPrevIn + executionPrice * newIn) / (totalPrevIn + newIn)
62 ///
63 /// @param previousAvgPriceFP128: Previous weighted average price (FP128)
64 /// @param totalPreviousIn: Total amount previously invested
65 /// @param newAmountIn: New investment amount in this execution
66 /// @param newAmountOut: Output amount received in this execution
67 /// @return Updated weighted average price in FP128 format
68 ///
69 /// Educational Note:
70 /// This calculation maintains a running weighted average of execution prices,
71 /// which is crucial for DCA performance tracking. Each purchase is weighted
72 /// by the amount invested.
73 access(all) fun updateWeightedAveragePriceFP128(
74 previousAvgPriceFP128: UInt128,
75 totalPreviousIn: UFix64,
76 newAmountIn: UFix64,
77 newAmountOut: UFix64
78 ): UInt128 {
79 pre {
80 newAmountIn > 0.0: "New amount in must be positive"
81 newAmountOut > 0.0: "New amount out must be positive"
82 }
83
84 // If this is the first execution, return the execution price directly
85 if totalPreviousIn == 0.0 {
86 return self.calculatePriceFP128(amountIn: newAmountIn, amountOut: newAmountOut)
87 }
88
89 // Calculate new execution price in FP128
90 let newPriceFP128 = self.calculatePriceFP128(amountIn: newAmountIn, amountOut: newAmountOut)
91
92 // Convert amounts to UInt128 for high-precision math
93 let prevInScaled = UInt128(totalPreviousIn * 100000000.0)
94 let newInScaled = UInt128(newAmountIn * 100000000.0)
95 let totalInScaled = prevInScaled + newInScaled
96
97 // Weighted average: (prevAvg * prevIn + newPrice * newIn) / totalIn
98 let prevWeighted = (previousAvgPriceFP128 * prevInScaled) / self.FP128_SCALE
99 let newWeighted = (newPriceFP128 * newInScaled) / self.FP128_SCALE
100
101 return ((prevWeighted + newWeighted) * self.FP128_SCALE) / totalInScaled
102 }
103
104 /// Calculate price as output/input in FP128 format
105 ///
106 /// @param amountIn: Input amount
107 /// @param amountOut: Output amount
108 /// @return Price in FP128 format (output per unit input)
109 access(all) fun calculatePriceFP128(amountIn: UFix64, amountOut: UFix64): UInt128 {
110 pre {
111 amountIn > 0.0: "Amount in must be positive"
112 amountOut > 0.0: "Amount out must be positive"
113 }
114
115 // Price = amountOut / amountIn, scaled to FP128
116 let amountInScaled = UInt128(amountIn * 100000000.0)
117 let amountOutScaled = UInt128(amountOut * 100000000.0)
118
119 return (amountOutScaled * self.FP128_SCALE) / amountInScaled
120 }
121
122 /// Convert FP128 price to human-readable UFix64
123 ///
124 /// @param priceFP128: Price in FP128 format
125 /// @return Price as UFix64 for display purposes
126 ///
127 /// Note: This is for display/logging only. Use FP128 for all calculations.
128 access(all) view fun fp128ToUFix64(priceFP128: UInt128): UFix64 {
129 // Divide by FP128_SCALE and convert to UFix64
130 let scaled = priceFP128 / (self.FP128_SCALE / 100000000)
131 return UFix64(scaled) / 100000000.0
132 }
133
134 /// Validate slippage basis points are within acceptable range
135 ///
136 /// @param slippageBps: Slippage in basis points
137 /// @return true if valid, false otherwise
138 access(all) view fun isValidSlippage(slippageBps: UInt64): Bool {
139 // Typically DCA should use 0.1% - 5% slippage (10 - 500 bps)
140 // But we allow up to 100% for flexibility
141 return slippageBps <= self.BPS_SCALE
142 }
143}
144