-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathInterestPayments.sol
268 lines (239 loc) · 10.9 KB
/
InterestPayments.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.18;
// This library is actually maintained, gas efficient and well designed.
import "lib/BokkyPooBahsDateTimeLibrary/contracts/BokkyPooBahsDateTimeLibrary.sol";
/**
* @title InterestPayments
* @dev A smart contract for managing interest payments
*/
contract InterestPayments {
struct InterestEntry {
uint256 timestamp; // UNIX timestamp of the entry
uint256 capital; // Compounded capital amount.
uint256 daily; // Daily interest rate.
uint256 totalCashFlow; // Total cashflow including the interest.
}
enum CompoundingPeriod {
ANNUAL_COMPOUNDING,
QUARTERLY_COMPOUNDING
}
InterestEntry[] public interestEntries;
// Event declarations.
event InterestEntryAdded(uint256 timestamp, uint256 capital, uint256 dailyInterestRate, uint256 totalCashFlow);
/* ========== INTERNAL ========== */
/**
* @dev Adds an inflow of funds and updates the compounding interest calculations.
* @param amount Amount of the inflow
* @param scale Scale factor for calculating the daily interest rate
* @param interestRate Annual nominal interest rate in basis points
* @param time UNIX timestamp of the inflow occurrence
* @param cp Compounding period - either ANNUAL_COMPOUNDING or QUARTERLY_COMPOUNDING
* @return The updated total capital after the inflow
*/
function _addInflow(uint256 amount, uint8 scale, uint256 interestRate, uint256 time, CompoundingPeriod cp)
internal
returns (uint256)
{
(uint256 dt, uint256 capital, uint256 d, uint256 tcf) = _computeCompounding(cp, scale, interestRate, time);
if (dt > 0) {
uint256 od = _getDays(dt);
uint256 nd = _getDays(time);
uint256 delta = nd - od;
uint256 comp = d * delta;
tcf += comp;
}
tcf += amount;
d = ((tcf * interestRate) / scale) / 1000;
_addInterestEntry(time, capital + amount, d, tcf);
return tcf;
}
/**
* @dev Adds an outflow of funds and updates the compounding interest calculations.
* @param amount Amount of the outflow
* @param scale Scale factor for calculating the daily interest rate
* @param interestRate Annual nominal interest rate in basis points
* @param time UNIX timestamp of the outflow occurrence
* @param cp Compounding period - either ANNUAL_COMPOUNDING or QUARTERLY_COMPOUNDING
* @return A tuple containing the remaining amount to be withdrawn, the capital paid, and the interest paid.
*/
function _addOutflow(uint256 amount, uint8 scale, uint256 interestRate, uint32 time, CompoundingPeriod cp)
internal
returns (uint256, uint256, uint256)
{
(uint256 dt, uint256 capital, uint256 d, uint256 tcf) = _computeCompounding(cp, scale, interestRate, time);
uint256 capitalPaid = 0;
uint256 interestPaid = 0;
uint256 remainingAmount = amount;
if (dt > 0 && tcf > 0) {
uint256 delta = _getDays(time) - _getDays(dt);
tcf += d * delta;
if (amount <= capital) {
tcf -= amount;
d = ((tcf * interestRate) / scale) / 1000;
capital -= amount;
capitalPaid = amount;
remainingAmount = 0;
} else if (amount <= tcf) {
capitalPaid = capital;
capital = 0;
interestPaid = amount - capitalPaid;
tcf -= amount;
d = ((tcf * interestRate) / scale) / 1000;
remainingAmount = 0;
} else {
capitalPaid = capital;
interestPaid = tcf - capitalPaid;
remainingAmount -= tcf;
tcf = 0;
d = 0;
}
_addInterestEntry(time, capital, d, tcf);
}
return (remainingAmount, capitalPaid, interestPaid);
}
/* ========== PRIVATE ========== */
/**
* @dev Adds a new interest entry to the 'interestEntries' list and triggers the InterestEntryAdded event.
* @param timestamp The timestamp related to this interest entry.
* @param capital The compounded capital amount.
* @param dailyInterestRate The daily interest rate.
* @param totalCashFlow The total cashflow including the interest.
*/
function _addInterestEntry(uint256 timestamp, uint256 capital, uint256 dailyInterestRate, uint256 totalCashFlow)
private
{
interestEntries.push(InterestEntry(timestamp, capital, dailyInterestRate, totalCashFlow));
emit InterestEntryAdded(timestamp, capital, dailyInterestRate, totalCashFlow);
}
/**
* @notice Calculates the compounding interest for a specified compounding period.
* @dev Chooses between annual and quarterly compounding based on the `cp` parameter.
* Reverts if an invalid compounding period is passed.
* @param cp The compounding period (either ANNUAL_COMPOUNDING or QUARTERLY_COMPOUNDING).
* @param scale The scale factor to compute the daily interest rate.
* @param interestRate The yearly nominal interest rate, expressed in basis points.
* @return A tuple containing the last interest payment date, the capital, the daily interest, and the total capital.
*/
function _computeCompounding(CompoundingPeriod cp, uint8 scale, uint256 interestRate, uint256 timestamp)
private
returns (uint256, uint256, uint256, uint256)
{
if (cp == CompoundingPeriod.ANNUAL_COMPOUNDING) {
return _compoundAnnual(scale, interestRate, timestamp);
} else if (cp == CompoundingPeriod.QUARTERLY_COMPOUNDING) {
return _compoundQuarterly(scale, interestRate, timestamp);
} else {
revert("Invalid compounding period");
}
}
/**
* @notice Compound interest annually.
* @param scale The scale factor used to calculate the daily interest rate.
* @param interestRate The annual nominal interest rate, in basis points.
* @param timestamp The UNIX timestamp at which the compounding is performed.
* @return A tuple containing the date of the last interest payment, the capital, the daily interest, and the total capital.
*/
function _compoundAnnual(uint256 scale, uint256 interestRate, uint256 timestamp)
private
returns (uint256, uint256, uint256, uint256)
{
(uint256 dt, uint256 capital, uint256 d, uint256 tcf) = _getLastInterestEntry();
if (dt > 0) {
uint256 nextYear = BokkyPooBahsDateTimeLibrary.getYear(dt) + 1;
uint256 latestYear = BokkyPooBahsDateTimeLibrary.getYear(timestamp);
while (nextYear <= latestYear) {
uint256 nextYearTimestamp = BokkyPooBahsDateTimeLibrary.timestampFromDate(nextYear, 1, 1);
uint256 delta = BokkyPooBahsDateTimeLibrary.diffDays(dt, nextYearTimestamp);
uint256 ai = d * delta;
tcf = tcf + ai;
d = ((tcf * interestRate) / scale) / 1000;
_addInterestEntry(dt, capital, d, tcf);
dt = nextYearTimestamp;
++nextYear;
}
}
return (dt, capital, d, tcf);
}
/**
* @notice Compound interest quarterly.
* @param scale The scale factor used to calculate the daily interest rate.
* @param interestRate The annual nominal interest rate, in basis points.
* @param timestamp The UNIX timestamp at which the compounding is performed.
* @return A tuple containing the date of the last interest payment, the capital, the daily interest, and the total capital.
*/
function _compoundQuarterly(uint8 scale, uint256 interestRate, uint256 timestamp)
private
returns (uint256, uint256, uint256, uint256)
{
(uint256 dt, uint256 capital, uint256 d, uint256 tcf) = _getLastInterestEntry();
if (dt > 0) {
uint256 od = _getDays(dt);
uint256 oqt = _getDays(_nextQuarter(dt));
uint256 nqt = _getDays(_getQuarter(timestamp));
while (od <= nqt) {
uint256 delta = oqt - od;
uint256 ai = d * delta;
tcf = tcf + ai;
d = ((tcf * interestRate) / scale) / 1000;
dt = od * 86400; // Convert back to UNIX timestamp
_addInterestEntry(dt, capital, d, tcf);
od = _nextQuarter(od);
}
}
return (dt, capital, d, tcf);
}
/**
* @notice Returns the most recent interest entry.
* If there are no entries, it will return a tuple of four zeroes.
* @return time The timestamp at which the interest entry was made
* @return capital The principal amount for the interest calculation
* @return daily The daily interest applied to the capital
* @return total The total accumulated interest until the entry
*/
function _getLastInterestEntry() private view returns (uint256, uint256, uint256, uint256) {
if (interestEntries.length == 0) {
return (0, 0, 0, 0);
} else {
InterestEntry memory entry = interestEntries[interestEntries.length - 1];
return (entry.timestamp, entry.capital, entry.daily, entry.totalCashFlow);
}
}
/**
* @notice Convert a UNIX timestamp into the number of days since the UNIX epoch.
* @param timestamp The UNIX timestamp to be converted.
* @return The number of days since the UNIX epoch.
*/
function _getDays(uint256 timestamp) private pure returns (uint256) {
return timestamp / 86400;
}
/**
* @notice Determines the UNIX timestamp of the quarter in which the given timestamp occurs.
* @param timestamp The UNIX timestamp to compute the quarter for.
* @return The UNIX timestamp representing the beginning of the quarter.
*/
function _getQuarter(uint256 timestamp) private pure returns (uint256) {
uint256 year = BokkyPooBahsDateTimeLibrary.getYear(timestamp);
// Ugly but gas efficient
uint256 q2 = BokkyPooBahsDateTimeLibrary.timestampFromDate(year, 4, 1);
if (timestamp < q2) {
return BokkyPooBahsDateTimeLibrary.timestampFromDate(year, 1, 1);
}
uint256 q3 = BokkyPooBahsDateTimeLibrary.timestampFromDate(year, 7, 1);
if (timestamp < q3) {
return q2;
}
uint256 q4 = BokkyPooBahsDateTimeLibrary.timestampFromDate(year, 10, 1);
if (timestamp < q4) {
return q3;
}
return q4;
}
/**
* @notice Determines the UNIX timestamp of the next quarter following the given timestamp.
* @param timestamp The UNIX timestamp to compute the next quarter for.
* @return The UNIX timestamp representing the beginning of the next quarter.
*/
function _nextQuarter(uint256 timestamp) private pure returns (uint256) {
return _getQuarter(BokkyPooBahsDateTimeLibrary.addMonths(timestamp, 3));
}
}