Smart Contract

CompoundInterest

A.76a9b420a331b9f0.CompoundInterest

Deployed

14h ago
Feb 28, 2026, 02:29:56 AM UTC

Dependents

0 imports
1// Compound interest, periodic compounding with 1 second period.
2//
3// This contract mostly reinvents the wheel -- waiting for https://github.com/onflow/cadence/issues/1151
4// This all will be one-liner with exponentiaion that support fractions.
5//
6// Math here is based on the work of this guy and his articles. Kudos to him:
7//   * https://medium.com/coinmonks/math-in-solidity-part-4-compound-interest-512d9e13041b
8//   * https://medium.com/coinmonks/math-in-solidity-part-5-exponent-and-logarithm-9aef8515136e
9//
10// Math is adapted from base 2 to base 10. And taking into account Cadence UFix64 low precision.
11//
12// The formula is: 10^(t * log10(1+r)), where t is amount of periods and r is per-second interest ratio.
13//
14// We consider that one year has 31556952 seconds, and, for example, r for 3% APY is 0.000000000936681155.
15// So that 10^(31556952 * log10(1+0.000000000936681155) = 1.0300000007092494.
16// Here CompoundInterest.generatedCompoundInterest(seconds: 31556952.0, k: CompoundInterest.getK(3)) = 1.02999994
17// Which is enough for practical application.
18//
19// The problem is that Cadence does not have neither exponentiation nor logarithm, and also lacks needed precision.
20//   * base 10 exponentiation that support fractions is implemented here
21//   * logarithms log10(1+r) are pre-computed and we denote it as k
22//   * we use scaling factor of 10^7 to increase precision
23access(all) contract CompoundInterest {
24    access(self) let magic_factors: [[UFix64; 10]; 8]
25    access(self) let powers_of_10: [UFix64; 12]
26    access(self) let ks: [UFix64; 101]
27    access(all) let k15: UFix64 // k for 15% APY
28    access(all) let k100: UFix64 // k for 100% APY
29    access(all) let k200: UFix64 // k for 200% APY
30    access(all) let k2000: UFix64 // k for 2000% APY
31
32    init() {
33
34        // Magic factors can be generated using this Python script:
35        //
36        // import math
37        // for i in range(1,9):
38        //   print('[' + ', '.join(str("{:1.8f}".format(math.pow(10, 1/(10**i))**j)) for j in range(10)) + '],')
39        self.magic_factors = [
40            [1.00000000, 1.25892541, 1.58489319, 1.99526231, 2.51188643, 3.16227766, 3.98107171, 5.01187234, 6.30957344, 7.94328235],
41            [1.00000000, 1.02329299, 1.04712855, 1.07151931, 1.09647820, 1.12201845, 1.14815362, 1.17489755, 1.20226443, 1.23026877],
42            [1.00000000, 1.00230524, 1.00461579, 1.00693167, 1.00925289, 1.01157945, 1.01391139, 1.01624869, 1.01859139, 1.02093948],
43            [1.00000000, 1.00023029, 1.00046062, 1.00069101, 1.00092146, 1.00115196, 1.00138251, 1.00161311, 1.00184377, 1.00207448],
44            [1.00000000, 1.00002303, 1.00004605, 1.00006908, 1.00009211, 1.00011514, 1.00013816, 1.00016119, 1.00018422, 1.00020725],
45            [1.00000000, 1.00000230, 1.00000461, 1.00000691, 1.00000921, 1.00001151, 1.00001382, 1.00001612, 1.00001842, 1.00002072],
46            [1.00000000, 1.00000023, 1.00000046, 1.00000069, 1.00000092, 1.00000115, 1.00000138, 1.00000161, 1.00000184, 1.00000207],
47            [1.00000000, 1.00000002, 1.00000005, 1.00000007, 1.00000009, 1.00000012, 1.00000014, 1.00000016, 1.00000018, 1.00000021]
48        ]
49
50        // UFix64 -- max is 184467440737.09551615, so max 11-th power of 10
51        self.powers_of_10 = [
52            1.0,
53            10.0,
54            100.0,
55            1000.0,
56            10000.0,
57            100000.0,
58            1000000.0,
59            10000000.0,
60            100000000.0,
61            1000000000.0,
62            10000000000.0,
63            100000000000.0
64        ]
65
66        // For Starly we want APY to be 15%. So the numbers below are for that number.
67        //
68        // With given parameters if you put 1 $STARLY for 1 year (31556952 seconds), you would get 1.14999997 at the end.
69        // Precision should be enough. Same math (using math.pow and math.log10) would give 1.150000001904556 in Python.
70        //
71        // Python snippets to get the numbers:
72        // import math
73        // interest = 1.15 # equivalent of annual percent
74        // t = 31556952 # seconds in year
75        // r = math.pow(interest, 1/t) - 1 # 4.428879707418787e-09 -- per-second interest ratio
76        // k = math.log10(1+r) # 1.9234380136859298e-09
77        //
78        // Cadence UFix64 lacks precision, so we will do some scaling
79        //
80        // doing ks for 0-100%, calculated using this Python snippet:
81        // import math
82        // t = 31556952
83        // for i in range(41):
84        //   interest = 1 + i/100
85        //   r = math.pow(interest, 1/t) - 1
86        //   k = math.log10(1+r)
87        //   k_scaled = k * 10**7 # multiple k by scaling factor 10^7
88        //   print("{:1.8f},".format(k_scaled))
89        self.ks = [
90            0.00000000,
91            0.00136939,
92            0.00272529,
93            0.00406795, // 3% APY
94            0.00539765,
95            0.00671462,
96            0.00801911,
97            0.00931135,
98            0.01059157,
99            0.01185998,
100            0.01311682,
101            0.01436228,
102            0.01559657,
103            0.01681989,
104            0.01803243,
105            0.01923438, // 15% APY
106            0.02042592,
107            0.02160724,
108            0.02277850,
109            0.02393988,
110            0.02509154,
111            0.02623364,
112            0.02736634,
113            0.02848980,
114            0.02960415,
115            0.03070956,
116            0.03180616,
117            0.03289409,
118            0.03397349,
119            0.03504448,
120            0.03610721, // 30% APY
121            0.03716179,
122            0.03820836,
123            0.03924702,
124            0.04027791,
125            0.04130113,
126            0.04231680,
127            0.04332502,
128            0.04432592,
129            0.04531959,
130            0.04630613,
131            0.04728565,
132            0.04825825,
133            0.04922403,
134            0.05018308,
135            0.05113548,
136            0.05208135,
137            0.05302075,
138            0.05395379,
139            0.05488054,
140            0.05580110,
141            0.05671554,
142            0.05762394,
143            0.05852638,
144            0.05942295,
145            0.06031371,
146            0.06119875,
147            0.06207813,
148            0.06295192,
149            0.06382021,
150            0.06468305,
151            0.06554051,
152            0.06639266,
153            0.06723957,
154            0.06808131,
155            0.06891792,
156            0.06974948,
157            0.07057604,
158            0.07139767,
159            0.07221442,
160            0.07302636,
161            0.07383353,
162            0.07463599,
163            0.07543381,
164            0.07622702,
165            0.07701569,
166            0.07779987,
167            0.07857960,
168            0.07935494,
169            0.08012594,
170            0.08089264,
171            0.08165509,
172            0.08241334,
173            0.08316744,
174            0.08391743,
175            0.08466335,
176            0.08540525,
177            0.08614318,
178            0.08687716,
179            0.08760726,
180            0.08833350,
181            0.08905593,
182            0.08977459,
183            0.09048951,
184            0.09120074,
185            0.09190831,
186            0.09261226,
187            0.09331263,
188            0.09400946,
189            0.09470277,
190            0.09539261 // 100% APY
191        ]
192
193        self.k15 = self.ks[15]
194        self.k100 = self.ks[100]
195        self.k200 = 0.15119371
196        self.k2000 = 0.41899461
197    }
198
199    access(all)
200    fun getK(_ i: UInt8): UFix64 {
201        return self.ks[i]
202    }
203
204    // Exponentiation with base 10
205    access(all)
206    fun pow10(_ x: UFix64): UFix64 {
207        pre {
208            // log10(184467440737.09551615) = 11.26591972249479
209            x <= 11.26591972: "Cannot exceed Cadence UFix64 limit 184467440737.09551615"
210        }
211        // e.g for x = 3.14159265, we want:
212        // * x_integer = 3
213        // * f1 = 1
214        // * f2 = 4
215        // * f3 = 1
216        // * f4 = 5
217        // * etc
218        // and multiply corresponding magic factors from the table
219        let x_m1 = x % 1.0
220        let x_m2 = x % 0.1
221        let x_m3 = x % 0.01
222        let x_m4 = x % 0.001
223        let x_m5 = x % 0.0001
224        let x_m6 = x % 0.00001
225        let x_m7 = x % 0.000001
226        let x_m8 = x % 0.0000001
227        let x_integer = UInt8(x - x_m1);
228        let f1 = UInt8((x_m1 - x_m2) * 10.0)
229        let f2 = UInt8((x_m2 - x_m3) * 100.0)
230        let f3 = UInt8((x_m3 - x_m4) * 1000.0)
231        let f4 = UInt8((x_m4 - x_m5) * 10000.0)
232        let f5 = UInt8((x_m5 - x_m6) * 100000.0)
233        let f6 = UInt8((x_m6 - x_m7) * 1000000.0)
234        let f7 = UInt8((x_m7 - x_m8) * 10000000.0)
235        let f8 = UInt8(x_m8 * 100000000.0)
236        return self.powers_of_10[x_integer]
237            * self.magic_factors[0][f1]
238            * self.magic_factors[1][f2]
239            * self.magic_factors[2][f3]
240            * self.magic_factors[3][f4]
241            * self.magic_factors[4][f5]
242            * self.magic_factors[5][f6]
243            * self.magic_factors[6][f7]
244            * self.magic_factors[7][f8]
245    }
246
247    // Get the generated compound interest for given number of seconds (periods) and k.
248    // k = log10(1+r) * 10^7, where r is per-second interest ratio, e.g for 3% r = 0.000000000936681155,
249    // since those numbers are very tiny for Cadence UFix64 precision, we use scaling factor of 10^7 to compensate it.
250    // Use CompoundInterest.ks for precomputed values, e.k ks[1] = 1% APY, ks[8] = 8% APY.
251    access(all)
252    fun generatedCompoundInterest(seconds: UFix64, k: UFix64): UFix64 {
253        // Applying same inverse scaling factor 10^7 for seconds, in the end those scaling factors will cancel out.
254        let seconds_scaled = seconds / 10000000.0
255        return self.pow10(seconds_scaled * k)
256    }
257}
258