@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,445 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.0;
3
+
4
+ import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol";
5
+ import {IJuiceDollar} from "../interface/IJuiceDollar.sol";
6
+ import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
7
+ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
8
+ import {ILeadrate} from "../interface/ILeadrate.sol";
9
+ import {IMintingHub} from "./interface/IMintingHub.sol";
10
+ import {IPositionFactory} from "./interface/IPositionFactory.sol";
11
+ import {IPosition} from "./interface/IPosition.sol";
12
+ import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
13
+ import {PositionRoller} from "./PositionRoller.sol";
14
+
15
+ /**
16
+ * @title Minting Hub
17
+ * @notice The central hub for creating, cloning, and challenging collateralized JuiceDollar positions.
18
+ * @dev Only one instance of this contract is required, whereas every new position comes with a new position
19
+ * contract. Pending challenges are stored as structs in an array.
20
+ */
21
+ contract MintingHub is IMintingHub, ERC165 {
22
+ /**
23
+ * @notice Irrevocable fee in JUSD when proposing a new position (but not when cloning an existing one).
24
+ */
25
+ uint256 public constant OPENING_FEE = 1000 * 10 ** 18;
26
+
27
+ /**
28
+ * @notice The challenger reward in parts per million (ppm) relative to the challenged amount, whereas
29
+ * challenged amount is defined as the challenged collateral amount times the liquidation price.
30
+ */
31
+ uint256 public constant CHALLENGER_REWARD = 20000; // 2%
32
+ uint256 public constant EXPIRED_PRICE_FACTOR = 10;
33
+
34
+ IPositionFactory private immutable POSITION_FACTORY; // position contract to clone
35
+
36
+ IJuiceDollar public immutable JUSD; // currency
37
+ PositionRoller public immutable ROLLER; // helper to roll positions
38
+ ILeadrate public immutable RATE; // to determine the interest rate
39
+
40
+ Challenge[] public challenges; // list of open challenges
41
+
42
+ /**
43
+ * @notice Map to remember pending postponed collateral returns.
44
+ * @dev It maps collateral => beneficiary => amount.
45
+ */
46
+ mapping(address collateral => mapping(address owner => uint256 amount)) public pendingReturns;
47
+
48
+ struct Challenge {
49
+ address challenger; // the address from which the challenge was initiated
50
+ uint40 start; // the start of the challenge
51
+ IPosition position; // the position that was challenged
52
+ uint256 size; // how much collateral the challenger provided
53
+ }
54
+
55
+ event PositionOpened(address indexed owner, address indexed position, address original, address collateral);
56
+ event ChallengeStarted(address indexed challenger, address indexed position, uint256 size, uint256 number);
57
+ event ChallengeAverted(address indexed position, uint256 number, uint256 size);
58
+ event ChallengeSucceeded(
59
+ address indexed position,
60
+ uint256 number,
61
+ uint256 bid,
62
+ uint256 acquiredCollateral,
63
+ uint256 challengeSize
64
+ );
65
+ event PostponedReturn(address collateral, address indexed beneficiary, uint256 amount);
66
+ event ForcedSale(address pos, uint256 amount, uint256 priceE36MinusDecimals);
67
+
68
+ error UnexpectedPrice();
69
+ error InvalidPos();
70
+ error IncompatibleCollateral();
71
+ error InsufficientCollateral();
72
+ error LeaveNoDust(uint256 amount);
73
+ error InvalidRiskPremium();
74
+ error InvalidReservePPM();
75
+ error InvalidCollateralDecimals();
76
+ error ChallengeTimeTooShort();
77
+ error InitPeriodTooShort();
78
+
79
+ modifier validPos(address position) {
80
+ if (JUSD.getPositionParent(position) != address(this)) revert InvalidPos();
81
+ _;
82
+ }
83
+
84
+ constructor(address _jusd, address _leadrate, address _roller, address _factory) {
85
+ JUSD = IJuiceDollar(_jusd);
86
+ RATE = ILeadrate(_leadrate);
87
+ POSITION_FACTORY = IPositionFactory(_factory);
88
+ ROLLER = PositionRoller(_roller);
89
+ }
90
+
91
+ /**
92
+ * @notice Open a collateralized loan position. See also https://docs.JUSD.com/positions/open .
93
+ * @dev For a successful call, you must set an allowance for the collateral token, allowing
94
+ * the minting hub to transfer the initial collateral amount to the newly created position and to
95
+ * withdraw the fees.
96
+ *
97
+ * @param _collateralAddress address of collateral token
98
+ * @param _minCollateral minimum collateral required to prevent dust amounts
99
+ * @param _initialCollateral amount of initial collateral to be deposited
100
+ * @param _mintingMaximum maximal amount of JUSD that can be minted by the position owner
101
+ * @param _initPeriodSeconds initial period in seconds
102
+ * @param _expirationSeconds position tenor in seconds from 'now'
103
+ * @param _challengeSeconds challenge period. Longer for less liquid collateral.
104
+ * @param _riskPremium ppm of minted amount that is added to the applicable minting fee as a risk premium
105
+ * @param _liqPrice Liquidation price with (36 - token decimals) decimals,
106
+ * e.g. 18 decimals for an 18 dec collateral, 36 decs for a 0 dec collateral.
107
+ * @param _reservePPM ppm of minted amount that is locked as borrower's reserve, e.g. 20%
108
+ * @return address address of created position
109
+ */
110
+ function openPosition(
111
+ address _collateralAddress,
112
+ uint256 _minCollateral,
113
+ uint256 _initialCollateral,
114
+ uint256 _mintingMaximum,
115
+ uint40 _initPeriodSeconds,
116
+ uint40 _expirationSeconds,
117
+ uint40 _challengeSeconds,
118
+ uint24 _riskPremium,
119
+ uint256 _liqPrice,
120
+ uint24 _reservePPM
121
+ ) public returns (address) {
122
+ {
123
+ if (_riskPremium > 1_000_000) revert InvalidRiskPremium();
124
+ if (CHALLENGER_REWARD > _reservePPM || _reservePPM > 1_000_000) revert InvalidReservePPM();
125
+ if (IERC20Metadata(_collateralAddress).decimals() > 24) revert InvalidCollateralDecimals(); // leaves 12 digits for price
126
+ if (_challengeSeconds < 1 days) revert ChallengeTimeTooShort();
127
+ if (_initPeriodSeconds < 6 hours) revert InitPeriodTooShort();
128
+ uint256 invalidAmount = IERC20(_collateralAddress).totalSupply() + 1;
129
+ // TODO: Improve for older tokens that revert with assert,
130
+ // which consumes all gas and makes the entire tx fail (uncatchable)
131
+ try IERC20(_collateralAddress).transfer(address(0x123), invalidAmount) {
132
+ revert IncompatibleCollateral(); // we need a collateral that reverts on failed transfers
133
+ } catch Error(string memory /*reason*/) {} catch Panic(uint /*errorCode*/) {} catch (
134
+ bytes memory /*lowLevelData*/
135
+ ) {}
136
+ if (_initialCollateral < _minCollateral) revert InsufficientCollateral();
137
+ // must start with at least 5000 JUSD worth of collateral
138
+ if (_minCollateral * _liqPrice < 5000 ether * 10 ** 18) revert InsufficientCollateral();
139
+ }
140
+ IPosition pos = IPosition(
141
+ POSITION_FACTORY.createNewPosition(
142
+ msg.sender,
143
+ address(JUSD),
144
+ _collateralAddress,
145
+ _minCollateral,
146
+ _mintingMaximum,
147
+ _initPeriodSeconds,
148
+ _expirationSeconds,
149
+ _challengeSeconds,
150
+ _riskPremium,
151
+ _liqPrice,
152
+ _reservePPM
153
+ )
154
+ );
155
+ JUSD.registerPosition(address(pos));
156
+ JUSD.collectProfits(msg.sender, OPENING_FEE);
157
+ IERC20(_collateralAddress).transferFrom(msg.sender, address(pos), _initialCollateral); // TODO: Use SafeERC20
158
+
159
+ emit PositionOpened(msg.sender, address(pos), address(pos), _collateralAddress);
160
+ return address(pos);
161
+ }
162
+
163
+ function clone(
164
+ address parent,
165
+ uint256 _initialCollateral,
166
+ uint256 _initialMint,
167
+ uint40 expiration
168
+ ) public returns (address) {
169
+ return clone(msg.sender, parent, _initialCollateral, _initialMint, expiration);
170
+ }
171
+
172
+ /**
173
+ * @notice Clones an existing position and immediately tries to mint the specified amount using the given collateral.
174
+ * @dev This needs an allowance to be set on the collateral contract such that the minting hub can get the collateral.
175
+ */
176
+ function clone(
177
+ address owner,
178
+ address parent,
179
+ uint256 _initialCollateral,
180
+ uint256 _initialMint,
181
+ uint40 expiration
182
+ ) public validPos(parent) returns (address) {
183
+ address pos = POSITION_FACTORY.clonePosition(parent);
184
+ IPosition child = IPosition(pos);
185
+ child.initialize(parent, expiration);
186
+ JUSD.registerPosition(pos);
187
+ IERC20 collateral = child.collateral();
188
+ if (_initialCollateral < child.minimumCollateral()) revert InsufficientCollateral();
189
+ collateral.transferFrom(msg.sender, pos, _initialCollateral); // collateral must still come from sender for security
190
+ emit PositionOpened(owner, address(pos), parent, address(collateral));
191
+ child.mint(owner, _initialMint);
192
+ Ownable(address(child)).transferOwnership(owner);
193
+ return address(pos);
194
+ }
195
+
196
+ /**
197
+ * @notice Launch a challenge (Dutch auction) on a position
198
+ * @param _positionAddr address of the position we want to challenge
199
+ * @param _collateralAmount amount of the collateral we want to challenge
200
+ * @param minimumPrice guards against the minter front-running with a price change
201
+ * @return index of the challenge in the challenge-array
202
+ */
203
+ function challenge(
204
+ address _positionAddr,
205
+ uint256 _collateralAmount,
206
+ uint256 minimumPrice
207
+ ) external validPos(_positionAddr) returns (uint256) {
208
+ IPosition position = IPosition(_positionAddr);
209
+ // challenger should be ok if front-run by owner with a higher price
210
+ // in case owner front-runs challenger with small price decrease to prevent challenge,
211
+ // the challenger should set minimumPrice to market price
212
+ uint256 liqPrice = position.virtualPrice();
213
+ if (liqPrice < minimumPrice) revert UnexpectedPrice();
214
+ IERC20(position.collateral()).transferFrom(msg.sender, address(this), _collateralAmount);
215
+ uint256 pos = challenges.length;
216
+ challenges.push(Challenge(msg.sender, uint40(block.timestamp), position, _collateralAmount));
217
+ position.notifyChallengeStarted(_collateralAmount, liqPrice);
218
+ emit ChallengeStarted(msg.sender, address(position), _collateralAmount, pos);
219
+ return pos;
220
+ }
221
+
222
+ /**
223
+ * @notice Post a bid in JUSD given an open challenge.
224
+ *
225
+ * @dev In case that the collateral cannot be transferred back to the challenger (i.e. because the collateral token
226
+ * has a blacklist and the challenger is on it), it is possible to postpone the return of the collateral.
227
+ *
228
+ * @param _challengeNumber index of the challenge as broadcast in the event
229
+ * @param size how much of the collateral the caller wants to bid for at most
230
+ * (automatically reduced to the available amount)
231
+ * @param postponeCollateralReturn To postpone the return of the collateral to the challenger. Usually false.
232
+ */
233
+ function bid(uint32 _challengeNumber, uint256 size, bool postponeCollateralReturn) external {
234
+ Challenge memory _challenge = challenges[_challengeNumber];
235
+ (uint256 liqPrice, uint40 phase) = _challenge.position.challengeData();
236
+ size = _challenge.size < size ? _challenge.size : size; // cannot bid for more than the size of the challenge
237
+
238
+ if (block.timestamp <= _challenge.start + phase) {
239
+ _avertChallenge(_challenge, _challengeNumber, liqPrice, size);
240
+ emit ChallengeAverted(address(_challenge.position), _challengeNumber, size);
241
+ } else {
242
+ _returnChallengerCollateral(_challenge, _challengeNumber, size, postponeCollateralReturn);
243
+ (uint256 transferredCollateral, uint256 offer) = _finishChallenge(_challenge, size);
244
+ emit ChallengeSucceeded(address(_challenge.position), _challengeNumber, offer, transferredCollateral, size);
245
+ }
246
+ }
247
+
248
+ function _finishChallenge(
249
+ Challenge memory _challenge,
250
+ uint256 size
251
+ ) internal returns (uint256, uint256) {
252
+ // Repayments depend on what was actually minted, whereas bids depend on the available collateral
253
+ (address owner, uint256 collateral, uint256 repayment, uint256 interest, uint32 reservePPM) = _challenge
254
+ .position
255
+ .notifyChallengeSucceeded(size);
256
+
257
+ // No overflow possible thanks to invariant (col * price <= limit * 10**18)
258
+ // enforced in Position.setPrice and knowing that collateral <= col.
259
+ uint256 offer = _calculateOffer(_challenge, collateral);
260
+
261
+ JUSD.transferFrom(msg.sender, address(this), offer); // get money from bidder
262
+ uint256 reward = (offer * CHALLENGER_REWARD) / 1_000_000;
263
+ JUSD.transfer(_challenge.challenger, reward); // pay out the challenger reward
264
+ uint256 fundsAvailable = offer - reward; // funds available after reward
265
+
266
+ // Example: available funds are 90, repayment is 50, reserve 20%. Then 20%*(90-50)=16 are collected as profits
267
+ // and the remaining 34 are sent to the position owner. If the position owner maxed out debt before the challenge
268
+ // started and the liquidation price was 100, they would be slightly better off as they would get away with 80
269
+ // instead of 40+36 = 76 in this example.
270
+ if (fundsAvailable > repayment + interest) {
271
+ // The excess amount is distributed between the system and the owner using the reserve ratio
272
+ // At this point, we cannot rely on the liquidation price because the challenge might have been started as a
273
+ // response to an unreasonable increase of the liquidation price, such that we have to use this heuristic
274
+ // for excess fund distribution, which make position owners that maxed out their positions slightly better
275
+ // off in comparison to those who did not.
276
+ uint256 profits = (reservePPM * (fundsAvailable - repayment - interest)) / 1_000_000;
277
+ JUSD.collectProfits(address(this), profits);
278
+ JUSD.transfer(owner, fundsAvailable - repayment - interest - profits);
279
+ } else if (fundsAvailable < repayment + interest) {
280
+ JUSD.coverLoss(address(this), repayment + interest - fundsAvailable); // ensure we have enough to pay everything
281
+ }
282
+ JUSD.burnWithoutReserve(repayment, reservePPM); // Repay the challenged part, example: 50 deur leading to 10 deur in implicit profits
283
+ JUSD.collectProfits(address(this), interest); // Collect interest as profits
284
+ _challenge.position.transferChallengedCollateral(msg.sender, collateral); // transfer the collateral to the bidder
285
+ return (collateral, offer);
286
+ }
287
+
288
+ function _avertChallenge(Challenge memory _challenge, uint32 number, uint256 liqPrice, uint256 size) internal {
289
+ require(block.timestamp != _challenge.start); // do not allow to avert the challenge in the same transaction, see CS-ZCHF-037
290
+ if (msg.sender == _challenge.challenger) {
291
+ // allow challenger to cancel challenge without paying themselves
292
+ } else {
293
+ JUSD.transferFrom(msg.sender, _challenge.challenger, (size * liqPrice) / (10 ** 18));
294
+ }
295
+
296
+ _challenge.position.notifyChallengeAverted(size);
297
+ _challenge.position.collateral().transfer(msg.sender, size);
298
+ if (size < _challenge.size) {
299
+ challenges[number].size = _challenge.size - size;
300
+ } else {
301
+ require(size == _challenge.size);
302
+ delete challenges[number];
303
+ }
304
+ }
305
+
306
+ /**
307
+ * @notice Returns 'amount' of the collateral to the challenger and reduces or deletes the relevant challenge.
308
+ */
309
+ function _returnChallengerCollateral(
310
+ Challenge memory _challenge,
311
+ uint32 number,
312
+ uint256 amount,
313
+ bool postpone
314
+ ) internal {
315
+ _returnCollateral(_challenge.position.collateral(), _challenge.challenger, amount, postpone);
316
+ if (_challenge.size == amount) {
317
+ // bid on full amount
318
+ delete challenges[number];
319
+ } else {
320
+ // bid on partial amount
321
+ challenges[number].size -= amount;
322
+ }
323
+ }
324
+
325
+ /**
326
+ * @notice Calculates the current Dutch auction price.
327
+ * @dev Starts at the full price at time 'start' and linearly goes to 0 as 'phase2' passes.
328
+ */
329
+ function _calculatePrice(uint40 start, uint40 phase2, uint256 liqPrice) internal view returns (uint256) {
330
+ uint40 timeNow = uint40(block.timestamp);
331
+ if (timeNow <= start) {
332
+ return liqPrice;
333
+ } else if (timeNow >= start + phase2) {
334
+ return 0;
335
+ } else {
336
+ uint256 timeLeft = phase2 - (timeNow - start);
337
+ return (liqPrice * timeLeft) / phase2;
338
+ }
339
+ }
340
+
341
+ /**
342
+ * @notice Calculates the offer amount for the given challenge.
343
+ * @dev The offer is calculated as the current price times the collateral amount.
344
+ */
345
+ function _calculateOffer(Challenge memory _challenge, uint256 collateral) internal view returns (uint256) {
346
+ (uint256 liqPrice, uint40 phase) = _challenge.position.challengeData();
347
+ return (_calculatePrice(_challenge.start + phase, phase, liqPrice) * collateral) / 10 ** 18;
348
+ }
349
+
350
+ /**
351
+ * @notice Get the price per unit of the collateral for the given challenge.
352
+ * @dev The price comes with (36 - collateral.decimals()) digits, so multiplying it with the raw collateral amount
353
+ * always yields a price with 36 digits, or 18 digits after dividing by 10**18 again.
354
+ */
355
+ function price(uint32 challengeNumber) public view returns (uint256) {
356
+ Challenge memory _challenge = challenges[challengeNumber];
357
+ if (_challenge.challenger == address(0x0)) {
358
+ return 0;
359
+ } else {
360
+ (uint256 liqPrice, uint40 phase) = _challenge.position.challengeData();
361
+ return _calculatePrice(_challenge.start + phase, phase, liqPrice);
362
+ }
363
+ }
364
+
365
+ /**
366
+ * @notice Challengers can call this method to withdraw collateral whose return was postponed.
367
+ */
368
+ function returnPostponedCollateral(address collateral, address target) external {
369
+ uint256 amount = pendingReturns[collateral][msg.sender];
370
+ delete pendingReturns[collateral][msg.sender];
371
+ IERC20(collateral).transfer(target, amount);
372
+ }
373
+
374
+ function _returnCollateral(IERC20 collateral, address recipient, uint256 amount, bool postpone) internal {
375
+ if (postpone) {
376
+ // Postponing helps in case the challenger was blacklisted or otherwise cannot receive at the moment.
377
+ pendingReturns[address(collateral)][recipient] += amount;
378
+ emit PostponedReturn(address(collateral), recipient, amount);
379
+ } else {
380
+ collateral.transfer(recipient, amount); // return the challenger's collateral
381
+ }
382
+ }
383
+
384
+ /**
385
+ * The applicable purchase price when forcing the sale of collateral of an expired position.
386
+ *
387
+ * The price starts at 10x the liquidation price at the expiration time, linearly declines to
388
+ * 1x liquidation price over the course of one challenge period, and then linearly declines
389
+ * less steeply to 0 over the course of another challenge period.
390
+ */
391
+ function expiredPurchasePrice(IPosition pos) public view returns (uint256) {
392
+ uint256 liqprice = pos.virtualPrice();
393
+ uint256 expiration = pos.expiration();
394
+ if (block.timestamp <= expiration) {
395
+ return EXPIRED_PRICE_FACTOR * liqprice;
396
+ } else {
397
+ uint256 challengePeriod = pos.challengePeriod();
398
+ uint256 timePassed = block.timestamp - expiration;
399
+ if (timePassed <= challengePeriod) {
400
+ // from 10x liquidation price to 1x in first phase
401
+ uint256 timeLeft = challengePeriod - timePassed;
402
+ return liqprice + (((EXPIRED_PRICE_FACTOR - 1) * liqprice * timeLeft) / challengePeriod);
403
+ } else if (timePassed < 2 * challengePeriod) {
404
+ // from 1x liquidation price to 0 in second phase
405
+ uint256 timeLeft = 2 * challengePeriod - timePassed;
406
+ return (liqprice * timeLeft) / challengePeriod;
407
+ } else {
408
+ // get collateral for free after both phases passed
409
+ return 0;
410
+ }
411
+ }
412
+ }
413
+
414
+ /**
415
+ * Buys up to the desired amount of the collateral asset from the given expired position using
416
+ * the applicable 'expiredPurchasePrice' at that instant.
417
+ *
418
+ * To prevent dust either the remaining collateral needs to be bought or collateral with a value
419
+ * of at least OPENING_FEE (1000 JUSD) needs to remain in the position for a different buyer
420
+ */
421
+ function buyExpiredCollateral(IPosition pos, uint256 upToAmount) external returns (uint256) {
422
+ uint256 max = pos.collateral().balanceOf(address(pos));
423
+ uint256 amount = upToAmount > max ? max : upToAmount;
424
+ uint256 forceSalePrice = expiredPurchasePrice(pos);
425
+
426
+ uint256 costs = (forceSalePrice * amount) / 10 ** 18;
427
+
428
+ if (max - amount > 0 && ((forceSalePrice * (max - amount)) / 10 ** 18) < OPENING_FEE) {
429
+ revert LeaveNoDust(max - amount);
430
+ }
431
+
432
+ pos.forceSale(msg.sender, amount, costs);
433
+ emit ForcedSale(address(pos), amount, forceSalePrice);
434
+ return amount;
435
+ }
436
+
437
+ /**
438
+ * @dev See {IERC165-supportsInterface}.
439
+ */
440
+ function supportsInterface(bytes4 interfaceId) public view override virtual returns (bool) {
441
+ return
442
+ interfaceId == type(IMintingHub).interfaceId ||
443
+ super.supportsInterface(interfaceId);
444
+ }
445
+ }