Smart Contract
CompoundInterest
A.76a9b420a331b9f0.CompoundInterest
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