@juicedollar/jusd 3.0.0 → 4.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 (47) hide show
  1. package/contracts/Leadrate.sol +1 -1
  2. package/contracts/{MintingHubV2 → MintingHubV3}/MintingHub.sol +93 -100
  3. package/contracts/{MintingHubV2 → MintingHubV3}/Position.sol +20 -13
  4. package/contracts/{MintingHubV2 → MintingHubV3}/PositionRoller.sol +17 -37
  5. package/contracts/{MintingHubV2 → MintingHubV3}/interface/IMintingHub.sol +2 -2
  6. package/contracts/{MintingHubV2 → MintingHubV3}/interface/IPosition.sol +3 -3
  7. package/contracts/Savings.sol +36 -6
  8. package/contracts/interface/ISavingsJUSD.sol +10 -1
  9. package/contracts/test/PositionExpirationTest.sol +17 -38
  10. package/contracts/test/PositionRollingTest.sol +3 -3
  11. package/contracts/test/ReentrantAttacker.sol +1 -1
  12. package/dist/index.d.mts +5609 -3568
  13. package/dist/index.d.ts +5609 -3568
  14. package/dist/index.js +9360 -6681
  15. package/dist/index.mjs +9350 -6675
  16. package/exports/abis/shared/Equity.ts +1286 -0
  17. package/exports/abis/shared/JuiceDollar.ts +1366 -0
  18. package/exports/abis/shared/StablecoinBridge.ts +279 -0
  19. package/exports/abis/utils/StartUSD.ts +213 -213
  20. package/exports/abis/{core → v2}/FrontendGateway.ts +1 -1
  21. package/exports/abis/{core → v2}/MintingHubGateway.ts +1 -1
  22. package/exports/abis/{MintingHubV2 → v2}/PositionRoller.ts +1 -1
  23. package/exports/abis/{core → v2}/SavingsGateway.ts +1 -1
  24. package/exports/abis/{core → v2}/SavingsVaultJUSD.ts +1 -1
  25. package/exports/abis/v3/MintingHub.ts +1024 -0
  26. package/exports/abis/v3/Position.ts +1142 -0
  27. package/exports/abis/v3/PositionFactory.ts +90 -0
  28. package/exports/abis/v3/PositionRoller.ts +255 -0
  29. package/exports/abis/v3/Savings.ts +553 -0
  30. package/exports/abis/v3/SavingsVaultJUSD.ts +925 -0
  31. package/exports/address.config.ts +43 -15
  32. package/exports/index.ts +22 -14
  33. package/package.json +9 -9
  34. package/contracts/gateway/FrontendGateway.sol +0 -224
  35. package/contracts/gateway/MintingHubGateway.sol +0 -82
  36. package/contracts/gateway/SavingsGateway.sol +0 -51
  37. package/contracts/gateway/interface/IFrontendGateway.sol +0 -49
  38. package/contracts/gateway/interface/IMintingHubGateway.sol +0 -12
  39. package/exports/abis/core/Equity.ts +0 -1286
  40. package/exports/abis/core/JuiceDollar.ts +0 -1366
  41. package/exports/abis/utils/MintingHubV2.ts +0 -888
  42. package/exports/abis/utils/Savings.ts +0 -453
  43. package/exports/abis/utils/StablecoinBridge.ts +0 -279
  44. /package/contracts/{MintingHubV2 → MintingHubV3}/PositionFactory.sol +0 -0
  45. /package/contracts/{MintingHubV2 → MintingHubV3}/interface/IPositionFactory.sol +0 -0
  46. /package/exports/abis/{MintingHubV2 → v2}/PositionFactoryV2.ts +0 -0
  47. /package/exports/abis/{MintingHubV2 → v2}/PositionV2.ts +0 -0
@@ -56,7 +56,7 @@ contract Leadrate {
56
56
  if (currentRatePPM == nextRatePPM) revert NoPendingChange();
57
57
  uint40 timeNow = uint40(block.timestamp);
58
58
  if (timeNow < nextChange) revert ChangeNotReady();
59
- ticksAnchor += (timeNow - anchorTime) * currentRatePPM;
59
+ ticksAnchor += uint64(timeNow - anchorTime) * currentRatePPM;
60
60
  anchorTime = timeNow;
61
61
  currentRatePPM = nextRatePPM;
62
62
  emit RateChanged(currentRatePPM);
@@ -1,16 +1,18 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  pragma solidity ^0.8.0;
3
3
 
4
- import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol";
5
4
  import {IWrappedNative} from "../interface/IWrappedNative.sol";
6
5
  import {IJuiceDollar} from "../interface/IJuiceDollar.sol";
6
+ import {IReserve} from "../interface/IReserve.sol";
7
7
  import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
8
8
  import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
9
9
  import {ILeadrate} from "../interface/ILeadrate.sol";
10
+ import {Leadrate} from "../Leadrate.sol";
10
11
  import {IMintingHub} from "./interface/IMintingHub.sol";
11
12
  import {IPositionFactory} from "./interface/IPositionFactory.sol";
12
13
  import {IPosition} from "./interface/IPosition.sol";
13
14
  import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
15
+ import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol";
14
16
  import {PositionRoller} from "./PositionRoller.sol";
15
17
 
16
18
  /**
@@ -18,8 +20,12 @@ import {PositionRoller} from "./PositionRoller.sol";
18
20
  * @notice The central hub for creating, cloning, and challenging collateralized JuiceDollar positions.
19
21
  * @dev Only one instance of this contract is required, whereas every new position comes with a new position
20
22
  * contract. Pending challenges are stored as structs in an array.
23
+ *
24
+ * Unsupported collateral token types (enforced via governance):
25
+ * - Fee-on-transfer tokens: break collateral accounting (actual balance < recorded amount)
26
+ * - Rebasing tokens: break challenge accounting (challengedAmount becomes stale after rebase)
21
27
  */
22
- contract MintingHub is IMintingHub, ERC165 {
28
+ contract MintingHub is IMintingHub, ERC165, Leadrate {
23
29
  /**
24
30
  * @notice Irrevocable fee in JUSD when proposing a new position (but not when cloning an existing one).
25
31
  */
@@ -32,20 +38,11 @@ contract MintingHub is IMintingHub, ERC165 {
32
38
  uint256 public constant CHALLENGER_REWARD = 20000; // 2%
33
39
  uint256 public constant EXPIRED_PRICE_FACTOR = 10;
34
40
 
35
- /**
36
- * @dev Maximum allowed message length for denial messages (prevents gas griefing attacks).
37
- * This constant is intentionally duplicated in Position.sol for defense-in-depth.
38
- * Hub validates as a second layer of protection; Position validates first to fail early.
39
- * If changing this value, update Position.MAX_MESSAGE_LENGTH as well.
40
- */
41
- uint256 private constant MAX_MESSAGE_LENGTH = 500;
42
-
43
41
  IPositionFactory private immutable POSITION_FACTORY; // position contract to clone
44
42
 
45
43
  IJuiceDollar public immutable JUSD; // currency
46
- PositionRoller public immutable ROLLER; // helper to roll positions
47
- ILeadrate public immutable RATE; // to determine the interest rate
48
44
  address public immutable WCBTC; // wrapped native token (cBTC) address
45
+ PositionRoller public immutable ROLLER; // helper to roll positions
49
46
 
50
47
  Challenge[] public challenges; // list of open challenges
51
48
 
@@ -55,12 +52,6 @@ contract MintingHub is IMintingHub, ERC165 {
55
52
  */
56
53
  mapping(address collateral => mapping(address owner => uint256 amount)) public pendingReturns;
57
54
 
58
- /**
59
- * @notice Tracks whether the first position has been created.
60
- * @dev The first position (genesis) can skip the 14-day init period requirement.
61
- */
62
- bool private _genesisPositionCreated;
63
-
64
55
  struct Challenge {
65
56
  address challenger; // the address from which the challenge was initiated
66
57
  uint40 start; // the start of the challenge
@@ -95,24 +86,43 @@ contract MintingHub is IMintingHub, ERC165 {
95
86
  error NativeOnlyForWCBTC();
96
87
  error ValueMismatch();
97
88
  error NativeTransferFailed();
98
- error MessageTooLong(uint256 length, uint256 maxLength);
99
- error EmptyMessage();
100
89
 
101
90
  modifier validPos(address position) {
102
91
  if (JUSD.getPositionParent(position) != address(this)) revert InvalidPos();
103
92
  _;
104
93
  }
105
94
 
106
- constructor(address _jusd, address _leadrate, address payable _roller, address _factory, address _wcbtc) {
95
+ constructor(
96
+ address _jusd,
97
+ uint24 _initialRatePPM,
98
+ address payable _roller,
99
+ address _factory,
100
+ address _wcbtc
101
+ ) Leadrate(IReserve(IJuiceDollar(_jusd).reserve()), _initialRatePPM) {
107
102
  JUSD = IJuiceDollar(_jusd);
108
- RATE = ILeadrate(_leadrate);
109
103
  POSITION_FACTORY = IPositionFactory(_factory);
110
- ROLLER = PositionRoller(_roller);
111
104
  WCBTC = _wcbtc;
105
+ ROLLER = PositionRoller(_roller);
106
+ }
107
+
108
+ /**
109
+ * @notice Backward-compatible view returning this contract as the ILeadrate implementation.
110
+ */
111
+ function RATE() public view returns (ILeadrate) {
112
+ return ILeadrate(address(this));
113
+ }
114
+
115
+ // Events for centralized position monitoring
116
+ function emitPositionUpdate(uint256 _collateral, uint256 _price, uint256 _principal) external validPos(msg.sender) {
117
+ emit PositionUpdate(msg.sender, _collateral, _price, _principal);
118
+ }
119
+
120
+ function emitPositionDenied(address denier, string calldata message) external validPos(msg.sender) {
121
+ emit PositionDeniedByGovernance(msg.sender, denier, message);
112
122
  }
113
123
 
114
124
  /**
115
- * @notice Open a collateralized loan position. See also https://docs.JUSD.com/positions/open .
125
+ * @notice Open a collateralized loan position.
116
126
  * @dev For a successful call, you must set an allowance for the collateral token, allowing
117
127
  * the minting hub to transfer the initial collateral amount to the newly created position and to
118
128
  * withdraw the fees.
@@ -147,12 +157,7 @@ contract MintingHub is IMintingHub, ERC165 {
147
157
  if (CHALLENGER_REWARD > _reservePPM || _reservePPM > 1_000_000) revert InvalidReservePPM();
148
158
  if (IERC20Metadata(_collateralAddress).decimals() > 24) revert InvalidCollateralDecimals(); // leaves 12 digits for price
149
159
  if (_challengeSeconds < 1 days) revert ChallengeTimeTooShort();
150
- // First position (genesis) can skip init period, all others require 14 days minimum
151
- if (_genesisPositionCreated) {
152
- if (_initPeriodSeconds < 14 days) revert InitPeriodTooShort();
153
- } else {
154
- _genesisPositionCreated = true;
155
- }
160
+ if (_initPeriodSeconds < 14 days) revert InitPeriodTooShort();
156
161
  uint256 invalidAmount = IERC20(_collateralAddress).totalSupply() + 1;
157
162
  // TODO: Improve for older tokens that revert with assert,
158
163
  // which consumes all gas and makes the entire tx fail (uncatchable)
@@ -298,11 +303,27 @@ contract MintingHub is IMintingHub, ERC165 {
298
303
  * challenger refund and bidder acquisition are returned as native.
299
304
  */
300
305
  function bid(
301
- uint32 _challengeNumber,
306
+ uint256 _challengeNumber,
302
307
  uint256 size,
303
308
  bool postponeCollateralReturn,
304
309
  bool returnCollateralAsNative
305
- ) public {
310
+ ) external {
311
+ _bid(_challengeNumber, size, postponeCollateralReturn, returnCollateralAsNative);
312
+ }
313
+
314
+ /**
315
+ * @notice Post a bid in JUSD given an open challenge (backward compatible version).
316
+ */
317
+ function bid(uint256 _challengeNumber, uint256 size, bool postponeCollateralReturn) external {
318
+ _bid(_challengeNumber, size, postponeCollateralReturn, false);
319
+ }
320
+
321
+ function _bid(
322
+ uint256 _challengeNumber,
323
+ uint256 size,
324
+ bool postponeCollateralReturn,
325
+ bool returnCollateralAsNative
326
+ ) internal {
306
327
  Challenge memory _challenge = challenges[_challengeNumber];
307
328
  (uint256 liqPrice, uint40 phase) = _challenge.position.challengeData();
308
329
  size = _challenge.size < size ? _challenge.size : size; // cannot bid for more than the size of the challenge
@@ -327,18 +348,14 @@ contract MintingHub is IMintingHub, ERC165 {
327
348
  }
328
349
  }
329
350
 
330
- /**
331
- * @notice Post a bid in JUSD given an open challenge (backward compatible version).
332
- */
333
- function bid(uint32 _challengeNumber, uint256 size, bool postponeCollateralReturn) external {
334
- bid(_challengeNumber, size, postponeCollateralReturn, false);
335
- }
336
-
337
351
  function _finishChallenge(
338
352
  Challenge memory _challenge,
339
353
  uint256 size,
340
354
  bool asNative
341
355
  ) internal returns (uint256, uint256) {
356
+ // Read challenge price BEFORE state mutation
357
+ uint256 unitPrice = _challengeUnitPrice(_challenge);
358
+
342
359
  // Repayments depend on what was actually minted, whereas bids depend on the available collateral
343
360
  (address owner, uint256 collateral, uint256 repayment, uint256 interest, uint32 reservePPM) = _challenge
344
361
  .position
@@ -346,7 +363,7 @@ contract MintingHub is IMintingHub, ERC165 {
346
363
 
347
364
  // No overflow possible thanks to invariant (col * price <= limit * 10**18)
348
365
  // enforced in Position.setPrice and knowing that collateral <= col.
349
- uint256 offer = _calculateOffer(_challenge, collateral);
366
+ uint256 offer = (unitPrice * collateral) / 10 ** 18;
350
367
 
351
368
  JUSD.transferFrom(msg.sender, address(this), offer); // get money from bidder
352
369
  uint256 reward = (offer * CHALLENGER_REWARD) / 1_000_000;
@@ -369,7 +386,7 @@ contract MintingHub is IMintingHub, ERC165 {
369
386
  } else if (fundsAvailable < repayment + interest) {
370
387
  JUSD.coverLoss(address(this), repayment + interest - fundsAvailable); // ensure we have enough to pay everything
371
388
  }
372
- JUSD.burnWithoutReserve(repayment, reservePPM); // Repay the challenged part, example: 50 deur leading to 10 deur in implicit profits
389
+ JUSD.burnWithoutReserve(repayment, reservePPM); // Repay the challenged part, example: 50 JUSD leading to 10 JUSD in implicit profits
373
390
  JUSD.collectProfits(address(this), interest); // Collect interest as profits
374
391
 
375
392
  // Transfer collateral to bidder (handles native coin if requested)
@@ -387,7 +404,7 @@ contract MintingHub is IMintingHub, ERC165 {
387
404
 
388
405
  function _avertChallenge(
389
406
  Challenge memory _challenge,
390
- uint32 number,
407
+ uint256 number,
391
408
  uint256 liqPrice,
392
409
  uint256 size,
393
410
  bool asNative
@@ -423,7 +440,7 @@ contract MintingHub is IMintingHub, ERC165 {
423
440
  */
424
441
  function _returnChallengerCollateral(
425
442
  Challenge memory _challenge,
426
- uint32 number,
443
+ uint256 number,
427
444
  uint256 amount,
428
445
  bool postpone,
429
446
  bool asNative
@@ -455,12 +472,12 @@ contract MintingHub is IMintingHub, ERC165 {
455
472
  }
456
473
 
457
474
  /**
458
- * @notice Calculates the offer amount for the given challenge.
459
- * @dev The offer is calculated as the current price times the collateral amount.
475
+ * @notice Returns the current unit price for the given challenge's Dutch auction.
476
+ * @dev Must be called before notifyChallengeSucceeded() which mutates challengedAmount/principal.
460
477
  */
461
- function _calculateOffer(Challenge memory _challenge, uint256 collateral) internal view returns (uint256) {
478
+ function _challengeUnitPrice(Challenge memory _challenge) internal view returns (uint256) {
462
479
  (uint256 liqPrice, uint40 phase) = _challenge.position.challengeData();
463
- return (_calculatePrice(_challenge.start + phase, phase, liqPrice) * collateral) / 10 ** 18;
480
+ return _calculatePrice(_challenge.start + phase, phase, liqPrice);
464
481
  }
465
482
 
466
483
  /**
@@ -468,7 +485,7 @@ contract MintingHub is IMintingHub, ERC165 {
468
485
  * @dev The price comes with (36 - collateral.decimals()) digits, so multiplying it with the raw collateral amount
469
486
  * always yields a price with 36 digits, or 18 digits after dividing by 10**18 again.
470
487
  */
471
- function price(uint32 challengeNumber) public view returns (uint256) {
488
+ function price(uint256 challengeNumber) public view returns (uint256) {
472
489
  Challenge memory _challenge = challenges[challengeNumber];
473
490
  if (_challenge.challenger == address(0x0)) {
474
491
  return 0;
@@ -484,7 +501,18 @@ contract MintingHub is IMintingHub, ERC165 {
484
501
  * @param target The address to receive the collateral
485
502
  * @param asNative If true and collateral is WcBTC, unwrap and send as native coin
486
503
  */
487
- function returnPostponedCollateral(address collateral, address target, bool asNative) public {
504
+ function returnPostponedCollateral(address collateral, address target, bool asNative) external {
505
+ _returnPostponedCollateral(collateral, target, asNative);
506
+ }
507
+
508
+ /**
509
+ * @notice Challengers can call this method to withdraw collateral whose return was postponed (backward compatible).
510
+ */
511
+ function returnPostponedCollateral(address collateral, address target) external {
512
+ _returnPostponedCollateral(collateral, target, false);
513
+ }
514
+
515
+ function _returnPostponedCollateral(address collateral, address target, bool asNative) internal {
488
516
  uint256 amount = pendingReturns[collateral][msg.sender];
489
517
  delete pendingReturns[collateral][msg.sender];
490
518
  if (asNative && collateral == WCBTC) {
@@ -496,13 +524,6 @@ contract MintingHub is IMintingHub, ERC165 {
496
524
  }
497
525
  }
498
526
 
499
- /**
500
- * @notice Challengers can call this method to withdraw collateral whose return was postponed (backward compatible).
501
- */
502
- function returnPostponedCollateral(address collateral, address target) external {
503
- returnPostponedCollateral(collateral, target, false);
504
- }
505
-
506
527
  function _returnCollateral(
507
528
  IERC20 collateral,
508
529
  address recipient,
@@ -565,8 +586,20 @@ contract MintingHub is IMintingHub, ERC165 {
565
586
  * @param upToAmount Maximum amount of collateral to buy
566
587
  * @param receiveAsNative If true and collateral is WcBTC, receive as native coin
567
588
  */
568
- function buyExpiredCollateral(IPosition pos, uint256 upToAmount, bool receiveAsNative) public validPos(address(pos)) returns (uint256) {
569
- uint256 max = pos.collateral().balanceOf(address(pos));
589
+ function buyExpiredCollateral(IPosition pos, uint256 upToAmount, bool receiveAsNative) external validPos(address(pos)) returns (uint256) {
590
+ return _buyExpiredCollateral(pos, upToAmount, receiveAsNative);
591
+ }
592
+
593
+ /**
594
+ * Buys up to the desired amount of the collateral asset from the given expired position (backward compatible).
595
+ */
596
+ function buyExpiredCollateral(IPosition pos, uint256 upToAmount) external validPos(address(pos)) returns (uint256) {
597
+ return _buyExpiredCollateral(pos, upToAmount, false);
598
+ }
599
+
600
+ function _buyExpiredCollateral(IPosition pos, uint256 upToAmount, bool receiveAsNative) internal returns (uint256) {
601
+ address collateralAddr = address(pos.collateral());
602
+ uint256 max = IERC20(collateralAddr).balanceOf(address(pos));
570
603
  uint256 amount = upToAmount > max ? max : upToAmount;
571
604
  uint256 forceSalePrice = expiredPurchasePrice(pos);
572
605
 
@@ -575,12 +608,10 @@ contract MintingHub is IMintingHub, ERC165 {
575
608
  if (max - amount > 0 && ((forceSalePrice * (max - amount)) / 10 ** 18) < OPENING_FEE) {
576
609
  revert LeaveNoDust(max - amount);
577
610
  }
578
-
579
- address collateralAddr = address(pos.collateral());
580
611
  if (receiveAsNative && collateralAddr == WCBTC) {
581
612
  // Pull JUSD from user to Hub, then approve Position to spend it
582
613
  JUSD.transferFrom(msg.sender, address(this), costs);
583
- IERC20(address(JUSD)).approve(address(pos), costs);
614
+ JUSD.approve(address(pos), costs);
584
615
  // Route through hub to unwrap
585
616
  pos.forceSale(address(this), amount, costs);
586
617
  IWrappedNative(WCBTC).withdraw(amount);
@@ -594,48 +625,10 @@ contract MintingHub is IMintingHub, ERC165 {
594
625
  return amount;
595
626
  }
596
627
 
597
- /**
598
- * Buys up to the desired amount of the collateral asset from the given expired position (backward compatible).
599
- */
600
- function buyExpiredCollateral(IPosition pos, uint256 upToAmount) external returns (uint256) {
601
- return buyExpiredCollateral(pos, upToAmount, false);
602
- }
603
-
604
- /**
605
- * @dev See {IERC165-supportsInterface}.
606
- */
607
- function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
628
+ function supportsInterface(bytes4 interfaceId) public view override virtual returns (bool) {
608
629
  return interfaceId == type(IMintingHub).interfaceId || super.supportsInterface(interfaceId);
609
630
  }
610
631
 
611
- /**
612
- * @notice Allows Position contracts to emit state updates through the hub for centralized monitoring.
613
- * @dev Only callable by registered positions. Emits PositionUpdate event with the caller as position.
614
- * @param _collateral Current collateral balance of the position
615
- * @param _price Current liquidation price of the position
616
- * @param _principal Current principal (debt) of the position
617
- */
618
- function emitPositionUpdate(
619
- uint256 _collateral,
620
- uint256 _price,
621
- uint256 _principal
622
- ) external virtual validPos(msg.sender) {
623
- emit PositionUpdate(msg.sender, _collateral, _price, _principal);
624
- }
625
-
626
- /**
627
- * @notice Allows Position contracts to emit governance denial events through the hub.
628
- * @dev Only callable by registered positions. Emits PositionDeniedByGovernance event.
629
- * @param denier Address of the governance participant who denied the position
630
- * @param message Reason for denial (max 500 bytes to prevent gas exhaustion attacks)
631
- */
632
- function emitPositionDenied(address denier, string calldata message) external virtual validPos(msg.sender) {
633
- uint256 messageLength = bytes(message).length;
634
- if (messageLength == 0) revert EmptyMessage();
635
- if (messageLength > MAX_MESSAGE_LENGTH) revert MessageTooLong(messageLength, MAX_MESSAGE_LENGTH);
636
- emit PositionDeniedByGovernance(msg.sender, denier, message);
637
- }
638
-
639
632
  /**
640
633
  * @notice Required to receive native coin when unwrapping WcBTC.
641
634
  */
@@ -1,7 +1,6 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  pragma solidity ^0.8.0;
3
3
 
4
- import {IMintingHubGateway} from "../gateway/interface/IMintingHubGateway.sol";
5
4
  import {IWrappedNative} from "../interface/IWrappedNative.sol";
6
5
  import {IJuiceDollar} from "../interface/IJuiceDollar.sol";
7
6
  import {IReserve} from "../interface/IReserve.sol";
@@ -10,7 +9,6 @@ import {IMintingHub} from "./interface/IMintingHub.sol";
10
9
  import {IPosition} from "./interface/IPosition.sol";
11
10
  import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
12
11
  import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
13
- import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
14
12
 
15
13
  /**
16
14
  * @title Position
@@ -80,7 +78,7 @@ contract Position is Ownable, IPosition, MathUtil {
80
78
  /**
81
79
  * @notice The original position to help identify clones.
82
80
  */
83
- address public immutable original;
81
+ address payable public immutable original;
84
82
 
85
83
  /**
86
84
  * @notice Pointer to the minting hub.
@@ -150,7 +148,7 @@ contract Position is Ownable, IPosition, MathUtil {
150
148
  * @param sender The address that triggered the denial
151
149
  * @param message Reason for denial (1-500 bytes, prevents gas griefing and ensures meaningful messages)
152
150
  */
153
- function _emitDenied(address sender, string memory message) internal {
151
+ function _emitDenied(address sender, string calldata message) internal {
154
152
  uint256 messageLength = bytes(message).length;
155
153
  if (messageLength == 0) revert EmptyMessage();
156
154
  if (messageLength > MAX_MESSAGE_LENGTH) revert MessageTooLong(messageLength, MAX_MESSAGE_LENGTH);
@@ -177,6 +175,7 @@ contract Position is Ownable, IPosition, MathUtil {
177
175
  error PriceTooHigh(uint256 newPrice, uint256 maxPrice);
178
176
  error InvalidPriceReference();
179
177
  error NativeTransferFailed();
178
+ error NativeOnlyForWCBTC();
180
179
  error CannotRescueCollateral();
181
180
 
182
181
  modifier alive() {
@@ -234,7 +233,7 @@ contract Position is Ownable, IPosition, MathUtil {
234
233
  uint256 _liqPrice,
235
234
  uint24 _reservePPM
236
235
  ) Ownable(_owner) {
237
- original = address(this);
236
+ original = payable(address(this));
238
237
  hub = _hub;
239
238
  jusd = IJuiceDollar(_jusd);
240
239
  collateral = IERC20(_collateral);
@@ -256,7 +255,7 @@ contract Position is Ownable, IPosition, MathUtil {
256
255
  */
257
256
  function initialize(address parent, uint40 _expiration) external onlyHub {
258
257
  if (expiration != 0) revert AlreadyInitialized();
259
- if (_expiration < block.timestamp || _expiration > Position(payable(original)).expiration())
258
+ if (_expiration < block.timestamp || _expiration > Position(original).expiration())
260
259
  revert InvalidExpiration(); // expiration must not be later than original
261
260
  expiration = _expiration;
262
261
  price = Position(payable(parent)).price();
@@ -306,7 +305,7 @@ contract Position is Ownable, IPosition, MathUtil {
306
305
  if (address(this) == original) {
307
306
  return limit - totalMinted;
308
307
  } else {
309
- return Position(payable(original)).availableForClones();
308
+ return Position(original).availableForClones();
310
309
  }
311
310
  }
312
311
 
@@ -404,6 +403,7 @@ contract Position is Ownable, IPosition, MathUtil {
404
403
  ) internal {
405
404
  // Handle native coin deposit first (wraps to WCBTC)
406
405
  if (msg.value > 0) {
406
+ if (address(collateral) != IMintingHub(hub).WCBTC()) revert NativeOnlyForWCBTC();
407
407
  IWrappedNative(address(collateral)).deposit{value: msg.value}();
408
408
  }
409
409
 
@@ -516,6 +516,15 @@ contract Position is Ownable, IPosition, MathUtil {
516
516
  // 9. Reference must have principal > 0 (actively used)
517
517
  if (ref.principal() == 0) return false;
518
518
 
519
+ // 10. Reference principal >= 1000 JUSD (meaningful skin-in-the-game)
520
+ if (ref.principal() < 1000 * 10 ** 18) return false;
521
+
522
+ // 11. Reference has been out of cooldown for >= challengePeriod
523
+ if (ref.cooldown() + ref.challengePeriod() > block.timestamp) return false;
524
+
525
+ // 12. Reference has meaningful remaining life (can still be challenged)
526
+ if (ref.expiration() <= block.timestamp + ref.challengePeriod()) return false;
527
+
519
528
  return true;
520
529
  }
521
530
 
@@ -669,7 +678,7 @@ contract Position is Ownable, IPosition, MathUtil {
669
678
  _accrueInterest(); // accrue interest
670
679
  _fixRateToLeadrate(riskPremiumPPM); // sync interest rate with leadrate
671
680
 
672
- Position(payable(original)).notifyMint(amount);
681
+ Position(original).notifyMint(amount);
673
682
  jusd.mintWithReserve(target, amount, reserveContribution);
674
683
 
675
684
  principal += amount;
@@ -723,18 +732,15 @@ contract Position is Ownable, IPosition, MathUtil {
723
732
  */
724
733
  function _notifyRepaid(uint256 amount) internal {
725
734
  if (amount > principal) revert RepaidTooMuch(amount - principal);
726
- Position(payable(original)).notifyRepaid(amount);
735
+ Position(original).notifyRepaid(amount);
727
736
  principal -= amount;
728
737
  }
729
738
 
730
739
  /**
731
- * @notice Updates outstanding interest and notifies the minting hub gateway that interest has been paid.
740
+ * @notice Updates outstanding interest tracking when interest is paid.
732
741
  */
733
742
  function _notifyInterestPaid(uint256 amount) internal {
734
743
  if (amount > interest) revert RepaidTooMuch(amount - interest);
735
- if (IERC165(hub).supportsInterface(type(IMintingHubGateway).interfaceId)) {
736
- IMintingHubGateway(hub).notifyInterestPaid(amount);
737
- }
738
744
  interest -= amount;
739
745
  }
740
746
 
@@ -1026,6 +1032,7 @@ contract Position is Ownable, IPosition, MathUtil {
1026
1032
  */
1027
1033
  receive() external payable {
1028
1034
  if (msg.sender != address(collateral)) {
1035
+ if (address(collateral) != IMintingHub(hub).WCBTC()) revert NativeOnlyForWCBTC();
1029
1036
  IWrappedNative(address(collateral)).deposit{value: msg.value}();
1030
1037
  }
1031
1038
  }
@@ -2,12 +2,9 @@
2
2
  pragma solidity ^0.8.0;
3
3
 
4
4
  import {IJuiceDollar} from "../interface/IJuiceDollar.sol";
5
- import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
6
5
  import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
7
- import {IMintingHubGateway} from "../gateway/interface/IMintingHubGateway.sol";
8
6
  import {IMintingHub} from "./interface/IMintingHub.sol";
9
7
  import {IPosition} from "./interface/IPosition.sol";
10
- import {IReserve} from "../interface/IReserve.sol";
11
8
  import {IWrappedNative} from "../interface/IWrappedNative.sol";
12
9
  import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
13
10
 
@@ -55,7 +52,6 @@ contract PositionRoller {
55
52
  * Like rollFully, but with a custom expiration date for the new position.
56
53
  */
57
54
  function rollFullyWithExpiration(IPosition source, IPosition target, uint40 expiration) public {
58
- require(source.collateral() == target.collateral());
59
55
  (uint256 repay, uint256 collWithdraw, uint256 mint, uint256 collDeposit) = _calculateRollParams(
60
56
  source,
61
57
  target,
@@ -96,7 +92,7 @@ contract PositionRoller {
96
92
  if (needsClone) {
97
93
  targetCollateral.transferFrom(msg.sender, address(this), collDeposit);
98
94
  targetCollateral.approve(target.hub(), collDeposit);
99
- target = _cloneTargetPosition(target, source, collDeposit, mint, expiration);
95
+ target = _cloneTargetPosition(target, collDeposit, mint, expiration);
100
96
  } else {
101
97
  // We can roll into the provided existing position.
102
98
  // We do not verify whether the target position was created by the known minting hub in order
@@ -131,7 +127,6 @@ contract PositionRoller {
131
127
  * Like rollFullyNative, but with a custom expiration date for the new position.
132
128
  */
133
129
  function rollFullyNativeWithExpiration(IPosition source, IPosition target, uint40 expiration) public payable {
134
- require(source.collateral() == target.collateral());
135
130
  (uint256 repay, uint256 collWithdraw, uint256 mint, uint256 collDeposit) = _calculateRollParams(
136
131
  source,
137
132
  target,
@@ -166,21 +161,21 @@ contract PositionRoller {
166
161
  uint256 collDeposit,
167
162
  uint40 expiration
168
163
  ) public payable valid(source) valid(target) own(source) {
169
- address collateral = address(source.collateral());
164
+ IERC20 collateralToken = source.collateral();
170
165
 
171
166
  jusd.mint(address(this), repay); // take a flash loan
172
167
  uint256 used = source.repay(repay);
173
168
  source.withdrawCollateral(address(this), collWithdraw);
174
169
  if (msg.value > 0) {
175
- IWrappedNative(collateral).deposit{value: msg.value}();
170
+ IWrappedNative(address(collateralToken)).deposit{value: msg.value}();
176
171
  }
177
172
 
178
173
  if (mint > 0) {
179
- IERC20 targetCollateral = IERC20(collateral);
174
+ IERC20 targetCollateral = IERC20(target.collateral());
180
175
  bool needsClone = Ownable(address(target)).owner() != msg.sender || expiration != target.expiration();
181
176
  if (needsClone) {
182
177
  targetCollateral.approve(target.hub(), collDeposit);
183
- target = _cloneTargetPosition(target, source, collDeposit, mint, expiration);
178
+ target = _cloneTargetPosition(target, collDeposit, mint, expiration);
184
179
  } else {
185
180
  targetCollateral.transfer(address(target), collDeposit);
186
181
  target.mint(msg.sender, mint);
@@ -194,9 +189,9 @@ contract PositionRoller {
194
189
  jusd.burnFrom(msg.sender, repay); // repay the flash loan
195
190
 
196
191
  // Return excess as native coin
197
- uint256 remaining = IERC20(collateral).balanceOf(address(this));
192
+ uint256 remaining = collateralToken.balanceOf(address(this));
198
193
  if (remaining > 0) {
199
- IWrappedNative(collateral).withdraw(remaining);
194
+ IWrappedNative(address(collateralToken)).withdraw(remaining);
200
195
  (bool success, ) = msg.sender.call{value: remaining}("");
201
196
  if (!success) revert NativeTransferFailed();
202
197
  }
@@ -213,6 +208,7 @@ contract PositionRoller {
213
208
  IPosition target,
214
209
  uint256 extraCollateral
215
210
  ) internal view returns (uint256 repay, uint256 collWithdraw, uint256 mint, uint256 collDeposit) {
211
+ require(source.collateral() == target.collateral());
216
212
  uint256 principal = source.principal();
217
213
  uint256 interest = source.getInterest();
218
214
  uint256 usableMint = source.getUsableMint(principal) + interest;
@@ -221,13 +217,14 @@ contract PositionRoller {
221
217
  uint256 totalAvailable = collateralAvailable + extraCollateral;
222
218
  uint256 targetPrice = target.price();
223
219
  uint256 depositAmount = (mintAmount * 10 ** 18 + targetPrice - 1) / targetPrice;
224
-
225
220
  if (depositAmount > totalAvailable) {
226
221
  depositAmount = totalAvailable;
227
- mintAmount = (depositAmount * target.price()) / 10 ** 18;
222
+ mintAmount = (depositAmount * targetPrice) / 10 ** 18;
228
223
  }
229
-
230
- return (principal + interest, collateralAvailable, mintAmount, depositAmount);
224
+ repay = principal + interest;
225
+ collWithdraw = collateralAvailable;
226
+ mint = mintAmount;
227
+ collDeposit = depositAmount;
231
228
  }
232
229
 
233
230
  /**
@@ -235,31 +232,14 @@ contract PositionRoller {
235
232
  */
236
233
  function _cloneTargetPosition(
237
234
  IPosition target,
238
- IPosition source,
239
235
  uint256 collDeposit,
240
236
  uint256 mint,
241
237
  uint40 expiration
242
238
  ) internal returns (IPosition) {
243
- if (IERC165(target.hub()).supportsInterface(type(IMintingHubGateway).interfaceId)) {
244
- bytes32 frontendCode = IMintingHubGateway(target.hub()).GATEWAY().getPositionFrontendCode(address(source));
245
- return
246
- IPosition(
247
- IMintingHubGateway(target.hub()).clone(
248
- msg.sender,
249
- address(target),
250
- collDeposit,
251
- mint,
252
- expiration,
253
- 0, // inherit price from parent
254
- frontendCode // use the same frontend code
255
- )
256
- );
257
- } else {
258
- return
259
- IPosition(
260
- IMintingHub(target.hub()).clone(msg.sender, address(target), collDeposit, mint, expiration, 0)
261
- );
262
- }
239
+ return
240
+ IPosition(
241
+ IMintingHub(target.hub()).clone(msg.sender, address(target), collDeposit, mint, expiration, 0)
242
+ );
263
243
  }
264
244
 
265
245
  modifier own(IPosition pos) {
@@ -28,10 +28,10 @@ interface IMintingHub {
28
28
  uint256 minimumPrice
29
29
  ) external payable returns (uint256);
30
30
 
31
- function bid(uint32 _challengeNumber, uint256 size, bool postponeCollateralReturn) external;
31
+ function bid(uint256 _challengeNumber, uint256 size, bool postponeCollateralReturn) external;
32
32
 
33
33
  function bid(
34
- uint32 _challengeNumber,
34
+ uint256 _challengeNumber,
35
35
  uint256 size,
36
36
  bool postponeCollateralReturn,
37
37
  bool returnCollateralAsNative