@juicedollar/jusd 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +356 -0
  3. package/contracts/Equity.sol +457 -0
  4. package/contracts/JuiceDollar.sol +363 -0
  5. package/contracts/Leadrate.sol +79 -0
  6. package/contracts/MintingHubV2/MintingHub.sol +445 -0
  7. package/contracts/MintingHubV2/Position.sol +810 -0
  8. package/contracts/MintingHubV2/PositionFactory.sol +69 -0
  9. package/contracts/MintingHubV2/PositionRoller.sol +159 -0
  10. package/contracts/MintingHubV2/interface/IMintingHub.sol +26 -0
  11. package/contracts/MintingHubV2/interface/IPosition.sol +90 -0
  12. package/contracts/MintingHubV2/interface/IPositionFactory.sol +20 -0
  13. package/contracts/Savings.sol +141 -0
  14. package/contracts/SavingsVaultJUSD.sol +140 -0
  15. package/contracts/StablecoinBridge.sol +109 -0
  16. package/contracts/StartUSD.sol +16 -0
  17. package/contracts/gateway/CoinLendingGateway.sol +223 -0
  18. package/contracts/gateway/FrontendGateway.sol +224 -0
  19. package/contracts/gateway/MintingHubGateway.sol +87 -0
  20. package/contracts/gateway/SavingsGateway.sol +51 -0
  21. package/contracts/gateway/interface/ICoinLendingGateway.sol +73 -0
  22. package/contracts/gateway/interface/IFrontendGateway.sol +49 -0
  23. package/contracts/gateway/interface/IMintingHubGateway.sol +12 -0
  24. package/contracts/impl/ERC3009.sol +171 -0
  25. package/contracts/interface/IJuiceDollar.sol +54 -0
  26. package/contracts/interface/ILeadrate.sol +7 -0
  27. package/contracts/interface/IReserve.sol +9 -0
  28. package/contracts/interface/ISavingsJUSD.sol +49 -0
  29. package/contracts/test/FreakToken.sol +25 -0
  30. package/contracts/test/Math.sol +339 -0
  31. package/contracts/test/MockEquity.sol +15 -0
  32. package/contracts/test/PositionExpirationTest.sol +75 -0
  33. package/contracts/test/PositionRollingTest.sol +65 -0
  34. package/contracts/test/TestFlashLoan.sol +84 -0
  35. package/contracts/test/TestFlashLoanGateway.sol +49 -0
  36. package/contracts/test/TestMathUtil.sol +40 -0
  37. package/contracts/test/TestToken.sol +45 -0
  38. package/contracts/test/TestWcBTC.sol +35 -0
  39. package/contracts/utils/MathUtil.sol +61 -0
  40. package/dist/index.d.mts +8761 -0
  41. package/dist/index.d.ts +8761 -0
  42. package/dist/index.js +11119 -0
  43. package/dist/index.mjs +11073 -0
  44. package/exports/abis/MintingHubV2/PositionFactoryV2.ts +90 -0
  45. package/exports/abis/MintingHubV2/PositionRoller.ts +183 -0
  46. package/exports/abis/MintingHubV2/PositionV2.ts +999 -0
  47. package/exports/abis/core/CoinLendingGateway.ts +427 -0
  48. package/exports/abis/core/Equity.ts +1286 -0
  49. package/exports/abis/core/FrontendGateway.ts +906 -0
  50. package/exports/abis/core/JuiceDollar.ts +1366 -0
  51. package/exports/abis/core/MintingHubGateway.ts +865 -0
  52. package/exports/abis/core/SavingsGateway.ts +559 -0
  53. package/exports/abis/core/SavingsVaultJUSD.ts +920 -0
  54. package/exports/abis/utils/ERC20.ts +310 -0
  55. package/exports/abis/utils/ERC20PermitLight.ts +520 -0
  56. package/exports/abis/utils/Leadrate.ts +175 -0
  57. package/exports/abis/utils/MintingHubV2.ts +682 -0
  58. package/exports/abis/utils/Ownable.ts +76 -0
  59. package/exports/abis/utils/Savings.ts +453 -0
  60. package/exports/abis/utils/StablecoinBridge.ts +209 -0
  61. package/exports/abis/utils/StartUSD.ts +315 -0
  62. package/exports/abis/utils/UniswapV3Pool.ts +638 -0
  63. package/exports/address.config.ts +48 -0
  64. package/exports/index.ts +28 -0
  65. package/package.json +87 -0
@@ -0,0 +1,363 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.0;
3
+
4
+ import {Equity} from "./Equity.sol";
5
+ import {IJuiceDollar} from "./interface/IJuiceDollar.sol";
6
+ import {IReserve} from "./interface/IReserve.sol";
7
+ import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
8
+ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
9
+ import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
10
+ import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol";
11
+ import {ERC3009} from "./impl/ERC3009.sol";
12
+
13
+ /**
14
+ * @title JuiceDollar
15
+ * @notice The JuiceDollar (JUSD) is an ERC-20 token that is designed to track the value of the Dollar.
16
+ * It is not upgradable, but open to arbitrary minting plugins. These are automatically accepted if none of the
17
+ * qualified pool shareholders casts a veto, leading to a flexible but conservative governance.
18
+ */
19
+ contract JuiceDollar is ERC20Permit, ERC3009, IJuiceDollar, ERC165 {
20
+ /**
21
+ * @notice Minimal fee and application period when suggesting a new minter.
22
+ */
23
+ uint256 public constant MIN_FEE = 1000 * (10 ** 18);
24
+ uint256 public immutable MIN_APPLICATION_PERIOD; // For example: 10 days
25
+
26
+ /**
27
+ * @notice The contract that holds the reserve.
28
+ */
29
+ IReserve public immutable override reserve;
30
+
31
+ /**
32
+ * @notice How much of the reserve belongs to the minters. Everything else belongs to the pool shareholders.
33
+ * Stored with 6 additional digits of accuracy so no rounding is necessary when dealing with parts per
34
+ * million (ppm) in reserve calculations.
35
+ */
36
+ uint256 private minterReserveE6;
37
+
38
+ /**
39
+ * @notice Map of minters to approval time stamps. If the time stamp is in the past, the minter contract is allowed
40
+ * to mint JuiceDollars.
41
+ */
42
+ mapping(address minter => uint256 validityStart) public minters;
43
+
44
+ /**
45
+ * @notice List of positions that are allowed to mint and the minter that registered them.
46
+ */
47
+ mapping(address position => address registeringMinter) public positions;
48
+
49
+ event MinterApplied(address indexed minter, uint256 applicationPeriod, uint256 applicationFee, string message);
50
+ event MinterDenied(address indexed minter, string message);
51
+ event Loss(address indexed reportingMinter, uint256 amount);
52
+ event Profit(address indexed reportingMinter, uint256 amount);
53
+ event ProfitDistributed(address indexed recipient, uint256 amount);
54
+
55
+ error PeriodTooShort();
56
+ error FeeTooLow();
57
+ error AlreadyRegistered();
58
+ error NotMinter();
59
+ error TooLate();
60
+
61
+ modifier minterOnly() {
62
+ if (!isMinter(msg.sender) && !isMinter(positions[msg.sender])) revert NotMinter();
63
+ _;
64
+ }
65
+
66
+ /**
67
+ * @notice Initiates the JuiceDollar with the provided minimum application period for new plugins
68
+ * in seconds, for example 10 days, i.e. 3600*24*10 = 864000
69
+ */
70
+ constructor(uint256 _minApplicationPeriod) ERC20Permit("Juice Dollar") ERC20("Juice Dollar", "JUSD") {
71
+ MIN_APPLICATION_PERIOD = _minApplicationPeriod;
72
+ reserve = new Equity(this);
73
+ }
74
+
75
+ function initialize(address _minter, string calldata _message) external {
76
+ require(totalSupply() == 0 && reserve.totalSupply() == 0);
77
+ minters[_minter] = block.timestamp;
78
+ emit MinterApplied(_minter, 0, 0, _message);
79
+ }
80
+
81
+ /**
82
+ * @notice Publicly accessible method to suggest a new way of minting JuiceDollar.
83
+ * @dev The caller has to pay an application fee that is irrevocably lost even if the new minter is vetoed.
84
+ * The caller must assume that someone will veto the new minter unless there is broad consensus that the new minter
85
+ * adds value to the JuiceDollar system. Complex proposals should have application periods and applications fees
86
+ * above the minimum. It is assumed that over time, informal ways to coordinate on new minters will emerge. The message
87
+ * parameter might be useful for initiating further communication. Maybe it contains a link to a website describing
88
+ * the proposed minter.
89
+ *
90
+ * @param _minter An address that is given the permission to mint JuiceDollars
91
+ * @param _applicationPeriod The time others have to veto the suggestion, at least MIN_APPLICATION_PERIOD
92
+ * @param _applicationFee The fee paid by the caller, at least MIN_FEE
93
+ * @param _message An optional human readable message to everyone watching this contract
94
+ */
95
+ function suggestMinter(
96
+ address _minter,
97
+ uint256 _applicationPeriod,
98
+ uint256 _applicationFee,
99
+ string calldata _message
100
+ ) external override {
101
+ if (_applicationPeriod < MIN_APPLICATION_PERIOD) revert PeriodTooShort();
102
+ if (_applicationFee < MIN_FEE) revert FeeTooLow();
103
+ if (minters[_minter] != 0) revert AlreadyRegistered();
104
+ _collectProfits(address(this), msg.sender, _applicationFee);
105
+ minters[_minter] = block.timestamp + _applicationPeriod;
106
+ emit MinterApplied(_minter, _applicationPeriod, _applicationFee, _message);
107
+ }
108
+
109
+ /**
110
+ * @notice Make the system more user friendly by skipping the allowance in many cases.
111
+ * @dev We trust minters and the positions they have created to mint and burn as they please, so
112
+ * giving them arbitrary allowances does not pose an additional risk.
113
+ */
114
+ function allowance(address owner, address spender) public view override(IERC20, ERC20) returns (uint256) {
115
+ uint256 explicit = super.allowance(owner, spender);
116
+ if (explicit > 0) {
117
+ return explicit; // don't waste gas checking minter
118
+ }
119
+
120
+ if (spender == address(reserve)) {
121
+ return type(uint256).max;
122
+ }
123
+
124
+ if (
125
+ (isMinter(spender) || isMinter(getPositionParent(spender))) &&
126
+ (isMinter(owner) || positions[owner] != address(0) || owner == address(reserve))
127
+ ) {
128
+ return type(uint256).max;
129
+ }
130
+
131
+ return 0;
132
+ }
133
+
134
+ /**
135
+ * @notice The reserve provided by the owners of collateralized positions.
136
+ * @dev The minter reserve can be used to cover losses after the equity holders have been wiped out.
137
+ */
138
+ function minterReserve() public view returns (uint256) {
139
+ return minterReserveE6 / 1_000_000;
140
+ }
141
+
142
+ /**
143
+ * @notice Allows minters to register collateralized debt positions, thereby giving them the ability to mint JuiceDollars.
144
+ * @dev It is assumed that the responsible minter that registers the position ensures that the position can be trusted.
145
+ */
146
+ function registerPosition(address _position) external override {
147
+ if (!isMinter(msg.sender)) revert NotMinter();
148
+ positions[_position] = msg.sender;
149
+ }
150
+
151
+ /**
152
+ * @notice The amount of equity of the JuiceDollar system in JUSD, owned by the holders of Juice Protocol (JUICE).
153
+ * @dev Note that the equity contract technically holds both the minter reserve as well as the equity, so the minter
154
+ * reserve must be subtracted. All fees and other kinds of income are added to the Equity contract and essentially
155
+ * constitute profits attributable to the pool shareholders.
156
+ */
157
+ function equity() public view returns (uint256) {
158
+ uint256 balance = balanceOf(address(reserve));
159
+ uint256 minReserve = minterReserve();
160
+ if (balance <= minReserve) {
161
+ return 0;
162
+ } else {
163
+ return balance - minReserve;
164
+ }
165
+ }
166
+
167
+ /**
168
+ * @notice Qualified pool shareholders can deny minters during the application period.
169
+ * @dev Calling this function is relatively cheap thanks to the deletion of a storage slot.
170
+ */
171
+ function denyMinter(address _minter, address[] calldata _helpers, string calldata _message) external override {
172
+ if (block.timestamp > minters[_minter]) revert TooLate();
173
+ reserve.checkQualified(msg.sender, _helpers);
174
+ delete minters[_minter];
175
+ emit MinterDenied(_minter, _message);
176
+ }
177
+
178
+ /**
179
+ * @notice Mints the provided amount of JUSD to the target address, automatically forwarding
180
+ * the minting fee and the reserve to the right place.
181
+ */
182
+ function mintWithReserve(address _target, uint256 _amount, uint32 _reservePPM) external override minterOnly {
183
+ uint256 usableMint = (_amount * (1_000_000 - _reservePPM)) / 1_000_000; // rounding down is fine
184
+ _mint(_target, usableMint);
185
+ _mint(address(reserve), _amount - usableMint); // rest goes to equity as reserves or as fees
186
+ minterReserveE6 += _amount * _reservePPM;
187
+ }
188
+
189
+ function mint(address _target, uint256 _amount) external override minterOnly {
190
+ _mint(_target, _amount);
191
+ }
192
+
193
+ /**
194
+ * Anyone is allowed to burn their JUSD.
195
+ */
196
+ function burn(uint256 _amount) external {
197
+ _burn(msg.sender, _amount);
198
+ }
199
+
200
+ /**
201
+ * @notice Burn someone else's JUSD.
202
+ */
203
+ function burnFrom(address _owner, uint256 _amount) external override minterOnly {
204
+ _spendAllowance(_owner, msg.sender, _amount);
205
+ _burn(_owner, _amount);
206
+ }
207
+
208
+ /**
209
+ * @notice Burn the amount without reclaiming the reserve, but freeing it up and thereby essentially donating it to the
210
+ * pool shareholders. This can make sense in combination with 'coverLoss', i.e. when it is the pool shareholders
211
+ * that bear the risk and depending on the outcome they make a profit or a loss.
212
+ *
213
+ * Design rule: Minters calling this method are only allowed to do so for token amounts they previously minted with
214
+ * the same _reservePPM amount.
215
+ *
216
+ * For example, if someone minted 50 JUSD earlier with a 20% reserve requirement (200000 ppm), they got 40 JUSD
217
+ * and paid 10 JUSD into the reserve. Now they want to repay the debt by burning 50 JUSD. When doing so using this
218
+ * method, 50 JUSD get burned and on top of that, 10 JUSD previously assigned to the minter's reserve are
219
+ * reassigned to the pool shareholders.
220
+ */
221
+ function burnWithoutReserve(uint256 amount, uint32 reservePPM) public override minterOnly {
222
+ _burn(msg.sender, amount);
223
+
224
+ uint256 equityBefore = equity();
225
+ uint256 reserveReduction = amount * reservePPM;
226
+ minterReserveE6 = minterReserveE6 > reserveReduction ? minterReserveE6 - reserveReduction : 0;
227
+ uint256 equityAfter = equity();
228
+
229
+ if (equityAfter > equityBefore) {
230
+ emit Profit(msg.sender, equityAfter - equityBefore);
231
+ }
232
+ }
233
+
234
+ /**
235
+ * @notice Burns the target amount taking the tokens to be burned from the payer and the payer's reserve.
236
+ * Only use this method for tokens also minted by the caller with the same reservePPM.
237
+ *
238
+ * Example: the calling contract has previously minted 100 JUSD with a reserve ratio of 20% (i.e. 200000 ppm).
239
+ * To burn half of that again, the minter calls burnFromWithReserve with a target amount of 50 JUSD. Assuming that reserves
240
+ * are only 90% covered, this call will deduct 41 JUSD from the payer's balance and 9 from the reserve, while
241
+ * reducing the minter reserve by 10.
242
+ */
243
+ function burnFromWithReserve(
244
+ address payer,
245
+ uint256 targetTotalBurnAmount,
246
+ uint32 reservePPM
247
+ ) public override minterOnly returns (uint256) {
248
+ uint256 assigned = calculateAssignedReserve(targetTotalBurnAmount, reservePPM);
249
+ _spendAllowance(payer, msg.sender, targetTotalBurnAmount - assigned); // spend amount excluding the reserve
250
+ _burn(address(reserve), assigned); // burn reserve amount from the reserve
251
+ _burn(payer, targetTotalBurnAmount - assigned); // burn remaining amount from the payer
252
+ minterReserveE6 -= targetTotalBurnAmount * reservePPM; // reduce reserve requirements by original ratio
253
+ return assigned;
254
+ }
255
+
256
+ /**
257
+ * @notice Calculates the assigned reserve for a given amount and reserve requirement, adjusted for reserve losses.
258
+ * @return `amountExcludingReserve` plus its share of the reserve.
259
+ */
260
+ function calculateFreedAmount(uint256 amountExcludingReserve, uint32 _reservePPM) public view returns (uint256) {
261
+ uint256 effectiveReservePPM = _effectiveReservePPM(_reservePPM);
262
+ return (1_000_000 * amountExcludingReserve) / (1_000_000 - effectiveReservePPM);
263
+ }
264
+
265
+ /**
266
+ * @notice Calculates the reserve attributable to someone who minted the given amount with the given reserve requirement.
267
+ * Under normal circumstances, this is just the reserve requirement multiplied by the amount. However, after a
268
+ * severe loss of capital that burned into the minter's reserve, this can also be less than that.
269
+ */
270
+ function calculateAssignedReserve(uint256 mintedAmount, uint32 _reservePPM) public view returns (uint256) {
271
+ uint256 effectiveReservePPM = _effectiveReservePPM(_reservePPM);
272
+ return (effectiveReservePPM * mintedAmount) / 1_000_000;
273
+ }
274
+
275
+ /**
276
+ * @notice Calculates the reserve ratio adjusted for any reserve shortfall
277
+ * @dev When there's a reserve shortfall (currentReserve < minterReserve), the effective reserve ratio is proportionally reduced.
278
+ * This ensures fair distribution of remaining reserves during repayment.
279
+ * @param reservePPM The nominal reserve ratio in parts per million
280
+ * @return The effective reserve ratio in parts per million, adjusted for any shortfall
281
+ */
282
+ function _effectiveReservePPM(uint32 reservePPM) internal view returns (uint256) {
283
+ uint256 minterReserve_ = minterReserve();
284
+ uint256 currentReserve = balanceOf(address(reserve));
285
+ return currentReserve < minterReserve_ ? (reservePPM * currentReserve) / minterReserve_ : reservePPM;
286
+ }
287
+
288
+ /**
289
+ * @notice Notify the JuiceDollar that a minter lost economic access to some coins. This does not mean that the coins are
290
+ * literally lost. It just means that some JUSD will likely never be repaid and that in order to bring the system
291
+ * back into balance, the lost amount of JUSD must be removed from the reserve instead.
292
+ *
293
+ * For example, if a minter printed 1 million JUSD for a mortgage and the mortgage turned out to be unsound with
294
+ * the house only yielding 800,000 in the subsequent auction, there is a loss of 200,000 that needs to be covered
295
+ * by the reserve.
296
+ */
297
+ function coverLoss(address source, uint256 _amount) external override minterOnly {
298
+ _withdrawFromReserve(source, _amount);
299
+ emit Loss(source, _amount);
300
+ }
301
+
302
+ /**
303
+ * @notice Distribute profits (e.g., savings interest) from the reserve to recipients.
304
+ *
305
+ * @param recipient The address receiving the payout.
306
+ * @param amount The amount of JUSD to distribute.
307
+ */
308
+ function distributeProfits(address recipient, uint256 amount) external override minterOnly {
309
+ _withdrawFromReserve(recipient, amount);
310
+ emit ProfitDistributed(recipient, amount);
311
+ }
312
+
313
+ function collectProfits(address source, uint256 _amount) external override minterOnly {
314
+ _collectProfits(msg.sender, source, _amount);
315
+ }
316
+
317
+ function _collectProfits(address minter, address source, uint256 _amount) internal {
318
+ _spendAllowance(source, minter, _amount);
319
+ _transfer(source, address(reserve), _amount);
320
+ emit Profit(minter, _amount);
321
+ }
322
+
323
+ /**
324
+ * @notice Transfers the specified amount from the reserve if possible; mints the remainder if necessary.
325
+ * @param recipient The address receiving the funds.
326
+ * @param amount The total amount to be paid.
327
+ */
328
+ function _withdrawFromReserve(address recipient, uint256 amount) internal {
329
+ uint256 reserveLeft = balanceOf(address(reserve));
330
+ if (reserveLeft >= amount) {
331
+ _transfer(address(reserve), recipient, amount);
332
+ } else {
333
+ _transfer(address(reserve), recipient, reserveLeft);
334
+ _mint(recipient, amount - reserveLeft);
335
+ }
336
+ }
337
+
338
+ /**
339
+ * @notice Returns true if the address is an approved minter.
340
+ */
341
+ function isMinter(address _minter) public view override returns (bool) {
342
+ return minters[_minter] != 0 && block.timestamp >= minters[_minter];
343
+ }
344
+
345
+ /**
346
+ * @notice Returns the address of the minter that created this position or null if the provided address is unknown.
347
+ */
348
+ function getPositionParent(address _position) public view override returns (address) {
349
+ return positions[_position];
350
+ }
351
+
352
+ /**
353
+ * @dev See {IERC165-supportsInterface}.
354
+ */
355
+ function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
356
+ return
357
+ interfaceId == type(IERC20).interfaceId ||
358
+ interfaceId == type(ERC20Permit).interfaceId ||
359
+ interfaceId == type(ERC3009).interfaceId ||
360
+ interfaceId == type(IJuiceDollar).interfaceId ||
361
+ super.supportsInterface(interfaceId);
362
+ }
363
+ }
@@ -0,0 +1,79 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.0;
3
+
4
+ import {IReserve} from "./interface/IReserve.sol";
5
+
6
+ /**
7
+ * @title Leadrate (attempt at translating the concise German term 'Leitzins')
8
+ *
9
+ * A module that can provide other modules with the leading interest rate for the system.
10
+ *
11
+ **/
12
+ contract Leadrate {
13
+ IReserve public immutable equity;
14
+
15
+ // The following five variables are less than 256 bits, so they should be stored
16
+ // in the same slot, making them cheaper to access together, right?
17
+
18
+ uint24 public currentRatePPM; // 24 bits allows rates of up to ~1670% per year
19
+ uint24 public nextRatePPM;
20
+ uint40 public nextChange;
21
+
22
+ uint40 private anchorTime; // 40 bits for time in seconds spans up to 1000 human generations
23
+ uint64 private ticksAnchor; // in bips * seconds
24
+
25
+ event RateProposed(address who, uint24 nextRate, uint40 nextChange);
26
+ event RateChanged(uint24 newRate);
27
+
28
+ error NoPendingChange();
29
+ error ChangeNotReady();
30
+
31
+ constructor(IReserve equity_, uint24 initialRatePPM) {
32
+ equity = equity_;
33
+ nextRatePPM = initialRatePPM;
34
+ currentRatePPM = initialRatePPM;
35
+ nextChange = uint40(block.timestamp);
36
+ anchorTime = nextChange;
37
+ ticksAnchor = 0;
38
+ emit RateChanged(initialRatePPM); // emit for initialization indexing, if desired
39
+ }
40
+
41
+ /**
42
+ * Proposes a new interest rate that will automatically be applied after seven days.
43
+ * To cancel a proposal, just overwrite it with a new one proposing the current rate.
44
+ */
45
+ function proposeChange(uint24 newRatePPM_, address[] calldata helpers) external {
46
+ equity.checkQualified(msg.sender, helpers);
47
+ nextRatePPM = newRatePPM_;
48
+ nextChange = uint40(block.timestamp + 7 days);
49
+ emit RateProposed(msg.sender, nextRatePPM, nextChange);
50
+ }
51
+
52
+ /**
53
+ * Setting a previously proposed interest rate change into force.
54
+ */
55
+ function applyChange() external {
56
+ if (currentRatePPM == nextRatePPM) revert NoPendingChange();
57
+ uint40 timeNow = uint40(block.timestamp);
58
+ if (timeNow < nextChange) revert ChangeNotReady();
59
+ ticksAnchor += (timeNow - anchorTime) * currentRatePPM;
60
+ anchorTime = timeNow;
61
+ currentRatePPM = nextRatePPM;
62
+ emit RateChanged(currentRatePPM);
63
+ }
64
+
65
+ /**
66
+ * Total accumulated 'interest ticks' since this contract was deployed.
67
+ * One 'tick' is a ppm-second, so one month of 12% annual interest is
68
+ * 120000*30*24*3600 = 311040000000 ticks.
69
+ * Two months of 6% annual interest would result in the same number of
70
+ * ticks. For simplicity, this is linear, so there is no "interest on interest".
71
+ */
72
+ function currentTicks() public view returns (uint64) {
73
+ return ticks(block.timestamp);
74
+ }
75
+
76
+ function ticks(uint256 timestamp) public view returns (uint64) {
77
+ return ticksAnchor + (uint64(timestamp) - anchorTime) * currentRatePPM;
78
+ }
79
+ }