@juicedollar/jusd 1.0.1 → 1.0.3

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.
package/README.md CHANGED
@@ -139,9 +139,8 @@ yarn install
139
139
  Create a `.env` file (see `.env.example`):
140
140
 
141
141
  ```bash
142
- # Required: Use EITHER private key OR seed phrase
142
+ # Required: Deployer wallet private key
143
143
  DEPLOYER_PRIVATE_KEY=your_private_key_here
144
- # DEPLOYER_ACCOUNT_SEED="twelve word seed phrase goes here"
145
144
 
146
145
  # Optional: For contract verification on Citrea explorer
147
146
  # CITREA_EXPLORER_API_KEY=your_api_key_here
@@ -2,6 +2,7 @@
2
2
  pragma solidity ^0.8.0;
3
3
 
4
4
  import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol";
5
+ import {IWrappedNative} from "../interface/IWrappedNative.sol";
5
6
  import {IJuiceDollar} from "../interface/IJuiceDollar.sol";
6
7
  import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
7
8
  import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
@@ -36,6 +37,7 @@ contract MintingHub is IMintingHub, ERC165 {
36
37
  IJuiceDollar public immutable JUSD; // currency
37
38
  PositionRoller public immutable ROLLER; // helper to roll positions
38
39
  ILeadrate public immutable RATE; // to determine the interest rate
40
+ address public immutable WCBTC; // wrapped native token (cBTC) address
39
41
 
40
42
  Challenge[] public challenges; // list of open challenges
41
43
 
@@ -75,17 +77,20 @@ contract MintingHub is IMintingHub, ERC165 {
75
77
  error InvalidCollateralDecimals();
76
78
  error ChallengeTimeTooShort();
77
79
  error InitPeriodTooShort();
80
+ error NativeOnlyForWCBTC();
81
+ error ValueMismatch();
78
82
 
79
83
  modifier validPos(address position) {
80
84
  if (JUSD.getPositionParent(position) != address(this)) revert InvalidPos();
81
85
  _;
82
86
  }
83
87
 
84
- constructor(address _jusd, address _leadrate, address _roller, address _factory) {
88
+ constructor(address _jusd, address _leadrate, address _roller, address _factory, address _wcbtc) {
85
89
  JUSD = IJuiceDollar(_jusd);
86
90
  RATE = ILeadrate(_leadrate);
87
91
  POSITION_FACTORY = IPositionFactory(_factory);
88
92
  ROLLER = PositionRoller(_roller);
93
+ WCBTC = _wcbtc;
89
94
  }
90
95
 
91
96
  /**
@@ -118,15 +123,15 @@ contract MintingHub is IMintingHub, ERC165 {
118
123
  uint24 _riskPremium,
119
124
  uint256 _liqPrice,
120
125
  uint24 _reservePPM
121
- ) public returns (address) {
126
+ ) public payable returns (address) {
122
127
  {
123
128
  if (_riskPremium > 1_000_000) revert InvalidRiskPremium();
124
129
  if (CHALLENGER_REWARD > _reservePPM || _reservePPM > 1_000_000) revert InvalidReservePPM();
125
130
  if (IERC20Metadata(_collateralAddress).decimals() > 24) revert InvalidCollateralDecimals(); // leaves 12 digits for price
126
131
  if (_challengeSeconds < 1 days) revert ChallengeTimeTooShort();
127
- if (_initPeriodSeconds < 6 hours) revert InitPeriodTooShort();
132
+ if (_initPeriodSeconds < 3 days) revert InitPeriodTooShort();
128
133
  uint256 invalidAmount = IERC20(_collateralAddress).totalSupply() + 1;
129
- // TODO: Improve for older tokens that revert with assert,
134
+ // TODO: Improve for older tokens that revert with assert,
130
135
  // which consumes all gas and makes the entire tx fail (uncatchable)
131
136
  try IERC20(_collateralAddress).transfer(address(0x123), invalidAmount) {
132
137
  revert IncompatibleCollateral(); // we need a collateral that reverts on failed transfers
@@ -134,9 +139,10 @@ contract MintingHub is IMintingHub, ERC165 {
134
139
  bytes memory /*lowLevelData*/
135
140
  ) {}
136
141
  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();
142
+ // must start with at least 100 JUSD worth of collateral
143
+ if (_minCollateral * _liqPrice < 100 ether * 10 ** 18) revert InsufficientCollateral();
139
144
  }
145
+
140
146
  IPosition pos = IPosition(
141
147
  POSITION_FACTORY.createNewPosition(
142
148
  msg.sender,
@@ -154,41 +160,65 @@ contract MintingHub is IMintingHub, ERC165 {
154
160
  );
155
161
  JUSD.registerPosition(address(pos));
156
162
  JUSD.collectProfits(msg.sender, OPENING_FEE);
157
- IERC20(_collateralAddress).transferFrom(msg.sender, address(pos), _initialCollateral); // TODO: Use SafeERC20
163
+
164
+ // Transfer collateral (handles native coin positions)
165
+ if (msg.value > 0) {
166
+ if (_collateralAddress != WCBTC) revert NativeOnlyForWCBTC();
167
+ if (msg.value != _initialCollateral) revert ValueMismatch();
168
+ IWrappedNative(WCBTC).deposit{value: msg.value}();
169
+ IERC20(WCBTC).transfer(address(pos), _initialCollateral);
170
+ } else {
171
+ IERC20(_collateralAddress).transferFrom(msg.sender, address(pos), _initialCollateral); // TODO: Use SafeERC20
172
+ }
158
173
 
159
174
  emit PositionOpened(msg.sender, address(pos), address(pos), _collateralAddress);
160
175
  return address(pos);
161
176
  }
162
177
 
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
178
  /**
173
179
  * @notice Clones an existing position and immediately tries to mint the specified amount using the given collateral.
174
180
  * @dev This needs an allowance to be set on the collateral contract such that the minting hub can get the collateral.
181
+ * For native coin positions (WcBTC), send msg.value equal to _initialCollateral.
182
+ * @param owner The owner of the cloned position
183
+ * @param parent The parent position to clone from
184
+ * @param _initialCollateral Amount of collateral to deposit
185
+ * @param _initialMint Amount of JUSD to mint
186
+ * @param expiration Expiration timestamp for the clone
187
+ * @param _liqPrice The liquidation price. If 0, inherits from parent.
175
188
  */
176
189
  function clone(
177
190
  address owner,
178
191
  address parent,
179
192
  uint256 _initialCollateral,
180
193
  uint256 _initialMint,
181
- uint40 expiration
182
- ) public validPos(parent) returns (address) {
194
+ uint40 expiration,
195
+ uint256 _liqPrice
196
+ ) public payable validPos(parent) returns (address) {
183
197
  address pos = POSITION_FACTORY.clonePosition(parent);
184
198
  IPosition child = IPosition(pos);
185
199
  child.initialize(parent, expiration);
186
200
  JUSD.registerPosition(pos);
187
201
  IERC20 collateral = child.collateral();
188
202
  if (_initialCollateral < child.minimumCollateral()) revert InsufficientCollateral();
189
- collateral.transferFrom(msg.sender, pos, _initialCollateral); // collateral must still come from sender for security
203
+
204
+ // Transfer collateral (handles native coin positions)
205
+ if (msg.value > 0) {
206
+ if (address(collateral) != WCBTC) revert NativeOnlyForWCBTC();
207
+ if (msg.value != _initialCollateral) revert ValueMismatch();
208
+ IWrappedNative(WCBTC).deposit{value: msg.value}();
209
+ collateral.transfer(pos, _initialCollateral);
210
+ } else {
211
+ collateral.transferFrom(msg.sender, pos, _initialCollateral); // collateral must still come from sender for security
212
+ }
213
+
190
214
  emit PositionOpened(owner, address(pos), parent, address(collateral));
191
215
  child.mint(owner, _initialMint);
216
+
217
+ // Adjust price if requested, incurs cooldown on price increase
218
+ if (_liqPrice > 0 && _liqPrice != child.price()) {
219
+ child.adjustPrice(_liqPrice);
220
+ }
221
+
192
222
  Ownable(address(child)).transferOwnership(owner);
193
223
  return address(pos);
194
224
  }
@@ -2,6 +2,7 @@
2
2
  pragma solidity ^0.8.0;
3
3
 
4
4
  import {IMintingHubGateway} from "../gateway/interface/IMintingHubGateway.sol";
5
+ import {IWrappedNative} from "../interface/IWrappedNative.sol";
5
6
  import {IJuiceDollar} from "../interface/IJuiceDollar.sol";
6
7
  import {IReserve} from "../interface/IReserve.sol";
7
8
  import {MathUtil} from "../utils/MathUtil.sol";
@@ -140,6 +141,9 @@ contract Position is Ownable, IPosition, MathUtil {
140
141
  error InvalidExpiration();
141
142
  error AlreadyInitialized();
142
143
  error PriceTooHigh(uint256 newPrice, uint256 maxPrice);
144
+ error InvalidPriceReference();
145
+ error NativeTransferFailed();
146
+ error CannotRescueCollateral();
143
147
 
144
148
  modifier alive() {
145
149
  if (block.timestamp >= expiration) revert Expired(uint40(block.timestamp), expiration);
@@ -204,7 +208,7 @@ contract Position is Ownable, IPosition, MathUtil {
204
208
  reserveContribution = _reservePPM;
205
209
  minimumCollateral = _minCollateral;
206
210
  challengePeriod = _challengePeriod;
207
- start = uint40(block.timestamp) + _initPeriod; // at least six hours time to deny the position
211
+ start = uint40(block.timestamp) + _initPeriod; // at least three days time to deny the position
208
212
  cooldown = start;
209
213
  expiration = start + _duration;
210
214
  limit = _initialLimit;
@@ -218,10 +222,10 @@ contract Position is Ownable, IPosition, MathUtil {
218
222
  */
219
223
  function initialize(address parent, uint40 _expiration) external onlyHub {
220
224
  if (expiration != 0) revert AlreadyInitialized();
221
- if (_expiration < block.timestamp || _expiration > Position(original).expiration()) revert InvalidExpiration(); // expiration must not be later than original
225
+ if (_expiration < block.timestamp || _expiration > Position(payable(original)).expiration()) revert InvalidExpiration(); // expiration must not be later than original
222
226
  expiration = _expiration;
223
- price = Position(parent).price();
224
- _fixRateToLeadrate(Position(parent).riskPremiumPPM());
227
+ price = Position(payable(parent)).price();
228
+ _fixRateToLeadrate(Position(payable(parent)).riskPremiumPPM());
225
229
  _transferOwnership(hub);
226
230
  }
227
231
 
@@ -267,7 +271,7 @@ contract Position is Ownable, IPosition, MathUtil {
267
271
  if (address(this) == original) {
268
272
  return limit - totalMinted;
269
273
  } else {
270
- return Position(original).availableForClones();
274
+ return Position(payable(original)).availableForClones();
271
275
  }
272
276
  }
273
277
 
@@ -311,8 +315,46 @@ contract Position is Ownable, IPosition, MathUtil {
311
315
  /**
312
316
  * @notice "All in one" function to adjust the principal, the collateral amount,
313
317
  * and the price in one transaction.
318
+ * @dev For native coin positions (WCBTC), msg.value will be wrapped and added as collateral.
319
+ * If withdrawAsNative is true, collateral withdrawals will be unwrapped to native coin.
320
+ * @param newPrincipal The new principal amount
321
+ * @param newCollateral The new collateral amount
322
+ * @param newPrice The new liquidation price
323
+ * @param withdrawAsNative If true, withdraw collateral as native coin instead of wrapped token
314
324
  */
315
- function adjust(uint256 newPrincipal, uint256 newCollateral, uint256 newPrice) external onlyOwner {
325
+ function adjust(uint256 newPrincipal, uint256 newCollateral, uint256 newPrice, bool withdrawAsNative) external payable onlyOwner {
326
+ _adjust(newPrincipal, newCollateral, newPrice, address(0), withdrawAsNative);
327
+ }
328
+
329
+ /**
330
+ * @notice "All in one" function to adjust the principal, the collateral amount,
331
+ * and the price in one transaction, with optional reference position for cooldown-free price increase.
332
+ * @dev For native coin positions (WCBTC), msg.value will be wrapped and added as collateral.
333
+ * If withdrawAsNative is true, collateral withdrawals will be unwrapped to native coin.
334
+ * @param newPrincipal The new principal amount
335
+ * @param newCollateral The new collateral amount
336
+ * @param newPrice The new liquidation price
337
+ * @param referencePosition Reference position for cooldown-free price increase (address(0) for normal logic with cooldown)
338
+ * @param withdrawAsNative If true, withdraw collateral as native coin instead of wrapped token
339
+ */
340
+ function adjustWithReference(uint256 newPrincipal, uint256 newCollateral, uint256 newPrice, address referencePosition, bool withdrawAsNative) external payable onlyOwner {
341
+ _adjust(newPrincipal, newCollateral, newPrice, referencePosition, withdrawAsNative);
342
+ }
343
+
344
+ /**
345
+ * @dev Internal implementation of adjust() - handles collateral, principal, and price adjustments.
346
+ * @param newPrincipal The new principal amount
347
+ * @param newCollateral The new collateral amount
348
+ * @param newPrice The new liquidation price
349
+ * @param referencePosition Reference position for cooldown-free price increase (address(0) for normal logic)
350
+ * @param withdrawAsNative If true and withdrawing collateral, unwrap to native coin
351
+ */
352
+ function _adjust(uint256 newPrincipal, uint256 newCollateral, uint256 newPrice, address referencePosition, bool withdrawAsNative) internal {
353
+ // Handle native coin deposit first (wraps to WCBTC)
354
+ if (msg.value > 0) {
355
+ IWrappedNative(address(collateral)).deposit{value: msg.value}();
356
+ }
357
+
316
358
  uint256 colbal = _collateralBalance();
317
359
  if (newCollateral > colbal) {
318
360
  collateral.transferFrom(msg.sender, address(this), newCollateral - colbal);
@@ -323,14 +365,18 @@ contract Position is Ownable, IPosition, MathUtil {
323
365
  _payDownDebt(debt - newPrincipal);
324
366
  }
325
367
  if (newCollateral < colbal) {
326
- _withdrawCollateral(msg.sender, colbal - newCollateral);
368
+ if (withdrawAsNative) {
369
+ _withdrawCollateralAsNative(msg.sender, colbal - newCollateral);
370
+ } else {
371
+ _withdrawCollateral(msg.sender, colbal - newCollateral);
372
+ }
327
373
  }
328
374
  // Must be called after collateral withdrawal
329
375
  if (newPrincipal > principal) {
330
376
  _mint(msg.sender, newPrincipal - principal, newCollateral);
331
377
  }
332
378
  if (newPrice != price) {
333
- _adjustPrice(newPrice);
379
+ _adjustPrice(newPrice, referencePosition);
334
380
  }
335
381
  emit MintingUpdate(newCollateral, newPrice, newPrincipal);
336
382
  }
@@ -339,21 +385,98 @@ contract Position is Ownable, IPosition, MathUtil {
339
385
  * @notice Allows the position owner to adjust the liquidation price as long as there is no pending challenge.
340
386
  * Lowering the liquidation price can be done with immediate effect, given that there is enough collateral.
341
387
  * Increasing the liquidation price triggers a cooldown period of 3 days, during which minting is suspended.
388
+ * @param newPrice The new liquidation price
342
389
  */
343
390
  function adjustPrice(uint256 newPrice) public onlyOwner {
344
- _adjustPrice(newPrice);
391
+ _adjustPrice(newPrice, address(0));
392
+ emit MintingUpdate(_collateralBalance(), price, principal);
393
+ }
394
+
395
+ /**
396
+ * @notice Adjusts the liquidation price without cooldown if a valid reference position is provided.
397
+ * @dev The reference position must be active (not in cooldown, not expired, not challenged, not closed),
398
+ * have the same collateral, and have a price >= newPrice.
399
+ * Note: For price decreases (newPrice <= current price), the reference position is ignored
400
+ * and only the collateral check is performed, as price decreases don't require cooldown protection.
401
+ * @param newPrice The new liquidation price
402
+ * @param referencePosition An active position with the same collateral and at least this price (only used for price increases)
403
+ */
404
+ function adjustPriceWithReference(uint256 newPrice, address referencePosition) external onlyOwner {
405
+ _adjustPrice(newPrice, referencePosition);
345
406
  emit MintingUpdate(_collateralBalance(), price, principal);
346
407
  }
347
408
 
348
- function _adjustPrice(uint256 newPrice) internal noChallenge alive backed noCooldown {
409
+ /**
410
+ * @dev Unified internal price adjustment logic.
411
+ * @param newPrice The new liquidation price
412
+ * @param referencePosition For price increases: address(0) triggers 3-day cooldown,
413
+ * valid reference allows cooldown-free adjustment.
414
+ * For price decreases: ignored (only collateral check performed).
415
+ * Price decreases are allowed even during cooldown since they make the position safer.
416
+ */
417
+ function _adjustPrice(uint256 newPrice, address referencePosition) internal noChallenge alive backed {
349
418
  if (newPrice > price) {
350
- _restrictMinting(3 days);
419
+ if (block.timestamp <= cooldown) revert Hot();
420
+ if (referencePosition == address(0)) {
421
+ _restrictMinting(3 days);
422
+ } else if (!_isValidPriceReference(referencePosition, newPrice)) {
423
+ revert InvalidPriceReference();
424
+ }
351
425
  } else {
352
426
  _checkCollateral(_collateralBalance(), newPrice);
353
427
  }
354
428
  _setPrice(newPrice, principal + availableForMinting());
355
429
  }
356
430
 
431
+ /**
432
+ * @notice Checks if a reference position is valid for a cooldown-free price increase.
433
+ * @param referencePosition The address of the reference position to validate
434
+ * @param newPrice The new price that should be validated against the reference
435
+ * @return True if the reference position is valid, false otherwise
436
+ */
437
+ function _isValidPriceReference(address referencePosition, uint256 newPrice) internal view returns (bool) {
438
+ // 1. Reference must be registered in the same hub
439
+ if (jusd.getPositionParent(referencePosition) != hub) return false;
440
+
441
+ IPosition ref = IPosition(referencePosition);
442
+
443
+ // 2. Reference must not be this position itself
444
+ if (referencePosition == address(this)) return false;
445
+
446
+ // 3. Same collateral
447
+ if (address(ref.collateral()) != address(collateral)) return false;
448
+
449
+ // 4. Reference must be active (not in cooldown)
450
+ if (block.timestamp <= ref.cooldown()) return false;
451
+
452
+ // 5. Reference must not be expired
453
+ if (block.timestamp >= ref.expiration()) return false;
454
+
455
+ // 6. Reference must not be challenged
456
+ if (ref.challengedAmount() > 0) return false;
457
+
458
+ // 7. Reference must not be closed
459
+ if (ref.isClosed()) return false;
460
+
461
+ // 8. New price must be <= reference price
462
+ if (newPrice > ref.price()) return false;
463
+
464
+ // 9. Reference must have principal > 0 (actively used)
465
+ if (ref.principal() == 0) return false;
466
+
467
+ return true;
468
+ }
469
+
470
+ /**
471
+ * @notice Checks if a reference position is valid for a cooldown-free price increase.
472
+ * @param referencePosition The address of the reference position to validate
473
+ * @param newPrice The new price that should be validated against the reference
474
+ * @return True if the reference position is valid, false otherwise
475
+ */
476
+ function isValidPriceReference(address referencePosition, uint256 newPrice) external view returns (bool) {
477
+ return _isValidPriceReference(referencePosition, newPrice);
478
+ }
479
+
357
480
  function _setPrice(uint256 newPrice, uint256 bounds) internal {
358
481
  uint256 colBalance = _collateralBalance();
359
482
  if (block.timestamp >= start && newPrice > 2 * price) {
@@ -493,7 +616,7 @@ contract Position is Ownable, IPosition, MathUtil {
493
616
  _accrueInterest(); // accrue interest
494
617
  _fixRateToLeadrate(riskPremiumPPM); // sync interest rate with leadrate
495
618
 
496
- Position(original).notifyMint(amount);
619
+ Position(payable(original)).notifyMint(amount);
497
620
  jusd.mintWithReserve(target, amount, reserveContribution);
498
621
 
499
622
  principal += amount;
@@ -547,7 +670,7 @@ contract Position is Ownable, IPosition, MathUtil {
547
670
  */
548
671
  function _notifyRepaid(uint256 amount) internal {
549
672
  if (amount > principal) revert RepaidTooMuch(amount - principal);
550
- Position(original).notifyRepaid(amount);
673
+ Position(payable(original)).notifyRepaid(amount);
551
674
  principal -= amount;
552
675
  }
553
676
 
@@ -617,36 +740,68 @@ contract Position is Ownable, IPosition, MathUtil {
617
740
  }
618
741
 
619
742
  /**
620
- * @notice Withdraw any ERC20 token that might have ended up on this address.
621
- * Withdrawing collateral is subject to the same restrictions as withdrawCollateral(...).
743
+ * @notice Rescue ERC20 tokens that were accidentally sent to this address.
744
+ * @dev Cannot be used for collateral - use withdrawCollateral() instead.
745
+ * @param token The ERC20 token to rescue
746
+ * @param target The address to send rescued tokens to
747
+ * @param amount The amount of tokens to rescue
622
748
  */
623
- function withdraw(address token, address target, uint256 amount) external onlyOwner {
624
- if (token == address(collateral)) {
625
- withdrawCollateral(target, amount);
626
- } else {
627
- uint256 balance = _collateralBalance();
628
- IERC20(token).transfer(target, amount);
629
- require(balance == _collateralBalance()); // guard against double-entry-point tokens
630
- }
749
+ function rescueToken(address token, address target, uint256 amount) external onlyOwner {
750
+ if (token == address(collateral)) revert CannotRescueCollateral();
751
+ uint256 balance = _collateralBalance();
752
+ IERC20(token).transfer(target, amount);
753
+ require(balance == _collateralBalance()); // guard against double-entry-point tokens
631
754
  }
632
755
 
633
756
  /**
634
757
  * @notice Withdraw collateral from the position up to the extent that it is still well collateralized afterwards.
635
758
  * Not possible as long as there is an open challenge or the contract is subject to a cooldown.
636
- *
637
759
  * Withdrawing collateral below the minimum collateral amount formally closes the position.
760
+ * @param target Address to receive the collateral
761
+ * @param amount Amount of collateral to withdraw
638
762
  */
639
763
  function withdrawCollateral(address target, uint256 amount) public ownerOrRoller {
640
764
  uint256 balance = _withdrawCollateral(target, amount);
641
765
  emit MintingUpdate(balance, price, principal);
642
766
  }
643
767
 
768
+ /**
769
+ * @notice Withdraw collateral as native coin (unwrapped).
770
+ * @dev Only works for wrapped native collateral tokens.
771
+ * @param target Address to receive the native coin
772
+ * @param amount Amount of collateral to withdraw and unwrap
773
+ */
774
+ function withdrawCollateralAsNative(address target, uint256 amount) public onlyOwner {
775
+ uint256 balance = _withdrawCollateralAsNative(target, amount);
776
+ emit MintingUpdate(balance, price, principal);
777
+ }
778
+
644
779
  function _withdrawCollateral(address target, uint256 amount) internal noCooldown noChallenge returns (uint256) {
645
780
  uint256 balance = _sendCollateral(target, amount);
646
781
  _checkCollateral(balance, price);
647
782
  return balance;
648
783
  }
649
784
 
785
+ /**
786
+ * @dev Internal helper for native coin withdrawal. Used by withdrawCollateralAsNative() and _adjust().
787
+ * Does NOT emit MintingUpdate - callers are responsible for emitting.
788
+ */
789
+ function _withdrawCollateralAsNative(address target, uint256 amount) internal noCooldown noChallenge returns (uint256) {
790
+ if (amount > 0) {
791
+ IWrappedNative(address(collateral)).withdraw(amount);
792
+ (bool success, ) = target.call{value: amount}("");
793
+ if (!success) revert NativeTransferFailed();
794
+ }
795
+
796
+ uint256 balance = _collateralBalance();
797
+ if (balance < minimumCollateral) {
798
+ _close();
799
+ }
800
+
801
+ _checkCollateral(balance, price);
802
+ return balance;
803
+ }
804
+
650
805
  /**
651
806
  * @notice Transfer the challenged collateral to the bidder. Only callable by minting hub.
652
807
  */
@@ -807,4 +962,15 @@ contract Position is Ownable, IPosition, MathUtil {
807
962
 
808
963
  return (owner(), _size, principalToPay, interestToPay, reserveContribution);
809
964
  }
965
+
966
+ /**
967
+ * @notice Receive native coin and auto-wrap to collateral.
968
+ * @dev Reverts for non-native positions to prevent stuck funds.
969
+ * Checks if sender is collateral to prevent unwrap loops.
970
+ */
971
+ receive() external payable {
972
+ if (msg.sender != address(collateral)) {
973
+ IWrappedNative(address(collateral)).deposit{value: msg.value}();
974
+ }
975
+ }
810
976
  }
@@ -48,9 +48,9 @@ contract PositionFactory {
48
48
  * @return address of the newly created clone position
49
49
  */
50
50
  function clonePosition(address _parent) external returns (address) {
51
- Position parent = Position(_parent);
51
+ Position parent = Position(payable(_parent));
52
52
  parent.assertCloneable();
53
- Position clone = Position(_createClone(parent.original()));
53
+ Position clone = Position(payable(_createClone(parent.original())));
54
54
  return address(clone);
55
55
  }
56
56
 
@@ -137,12 +137,13 @@ contract PositionRoller {
137
137
  collDeposit,
138
138
  mint,
139
139
  expiration,
140
+ 0, // inherit price from parent
140
141
  frontendCode // use the same frontend code
141
142
  )
142
143
  );
143
144
  } else {
144
145
  return IPosition(
145
- IMintingHub(target.hub()).clone(msg.sender, address(target), collDeposit, mint, expiration)
146
+ IMintingHub(target.hub()).clone(msg.sender, address(target), collDeposit, mint, expiration, 0)
146
147
  );
147
148
  }
148
149
  }
@@ -22,5 +22,5 @@ interface IMintingHub {
22
22
 
23
23
  function buyExpiredCollateral(IPosition pos, uint256 upToAmount) external returns (uint256);
24
24
 
25
- function clone(address owner, address parent, uint256 _initialCollateral, uint256 _initialMint, uint40 expiration) external returns (address);
25
+ function clone(address owner, address parent, uint256 _initialCollateral, uint256 _initialMint, uint40 expiration, uint256 _liqPrice) external payable returns (address);
26
26
  }
@@ -56,10 +56,18 @@ interface IPosition {
56
56
 
57
57
  function getMintAmount(uint256 usableMint) external view returns (uint256);
58
58
 
59
- function adjust(uint256 newMinted, uint256 newCollateral, uint256 newPrice) external;
59
+ function adjust(uint256 newMinted, uint256 newCollateral, uint256 newPrice, bool withdrawAsNative) external payable;
60
60
 
61
61
  function adjustPrice(uint256 newPrice) external;
62
62
 
63
+ function adjustPriceWithReference(uint256 newPrice, address referencePosition) external;
64
+
65
+ function adjustWithReference(uint256 newMinted, uint256 newCollateral, uint256 newPrice, address referencePosition, bool withdrawAsNative) external payable;
66
+
67
+ function isValidPriceReference(address referencePosition, uint256 newPrice) external view returns (bool);
68
+
69
+ function isClosed() external view returns (bool);
70
+
63
71
  function mint(address target, uint256 amount) external;
64
72
 
65
73
  function getDebt() external view returns (uint256);
@@ -72,10 +80,12 @@ interface IPosition {
72
80
 
73
81
  function forceSale(address buyer, uint256 colAmount, uint256 proceeds) external;
74
82
 
75
- function withdraw(address token, address target, uint256 amount) external;
83
+ function rescueToken(address token, address target, uint256 amount) external;
76
84
 
77
85
  function withdrawCollateral(address target, uint256 amount) external;
78
86
 
87
+ function withdrawCollateralAsNative(address target, uint256 amount) external;
88
+
79
89
  function transferChallengedCollateral(address target, uint256 amount) external;
80
90
 
81
91
  function challengeData() external view returns (uint256 liqPrice, uint40 phase);
@@ -6,11 +6,11 @@ import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
6
6
  /**
7
7
  * @title StartUSD
8
8
  * @notice A minimal genesis stablecoin used to bootstrap the JuiceDollar protocol.
9
- * @dev Mints 10,000 SUSD to the deployer. Used to initialize the protocol with initial
9
+ * @dev Mints 100,000,000 SUSD to the deployer. Used to initialize the protocol with initial
10
10
  * JUSD supply through a StablecoinBridge, which then creates the initial JUICE tokens.
11
11
  */
12
12
  contract StartUSD is ERC20 {
13
13
  constructor() ERC20("StartUSD", "SUSD") {
14
- _mint(msg.sender, 10_000 * 10 ** 18);
14
+ _mint(msg.sender, 100_000_000 * 10 ** 18);
15
15
  }
16
16
  }
@@ -13,8 +13,9 @@ contract MintingHubGateway is MintingHub, IMintingHubGateway {
13
13
  address _leadrate,
14
14
  address _roller,
15
15
  address _factory,
16
- address _gateway
17
- ) MintingHub(_jusd, _leadrate, _roller, _factory) {
16
+ address _gateway,
17
+ address _wcbtc
18
+ ) MintingHub(_jusd, _leadrate, _roller, _factory, _wcbtc) {
18
19
  GATEWAY = IFrontendGateway(_gateway);
19
20
  }
20
21
 
@@ -30,8 +31,8 @@ contract MintingHubGateway is MintingHub, IMintingHubGateway {
30
31
  uint256 _liqPrice,
31
32
  uint24 _reservePPM,
32
33
  bytes32 _frontendCode
33
- ) public returns (address) {
34
- address position = openPosition(
34
+ ) public payable returns (address) {
35
+ address position = MintingHub.openPosition(
35
36
  _collateralAddress,
36
37
  _minCollateral,
37
38
  _initialCollateral,
@@ -47,19 +48,12 @@ contract MintingHubGateway is MintingHub, IMintingHubGateway {
47
48
  return position;
48
49
  }
49
50
 
50
- function clone(
51
- address parent,
52
- uint256 _initialCollateral,
53
- uint256 _initialMint,
54
- uint40 expiration,
55
- bytes32 frontendCode
56
- ) public returns (address) {
57
- return clone(msg.sender, parent, _initialCollateral, _initialMint, expiration, frontendCode);
58
- }
59
-
60
51
  /**
61
- * @notice Clones an existing position and immediately tries to mint the specified amount using the given collateral.
62
- * @dev This needs an allowance to be set on the collateral contract such that the minting hub can get the collateral.
52
+ * @notice Clones an existing position and immediately tries to mint the specified amount using the given collateral.
53
+ * @dev For native coin positions (WcBTC), send msg.value equal to _initialCollateral.
54
+ * For ERC20 collateral, ensure prior approval for the minting hub to transfer _initialCollateral.
55
+ * @param _liqPrice Optionally adjust price of new position after minting. Set to 0 to inherit parent's price.
56
+ * @param frontendCode Optionally register the position with a frontend code.
63
57
  */
64
58
  function clone(
65
59
  address owner,
@@ -67,9 +61,10 @@ contract MintingHubGateway is MintingHub, IMintingHubGateway {
67
61
  uint256 _initialCollateral,
68
62
  uint256 _initialMint,
69
63
  uint40 expiration,
64
+ uint256 _liqPrice,
70
65
  bytes32 frontendCode
71
- ) public returns (address) {
72
- address position = clone(owner, parent, _initialCollateral, _initialMint, expiration);
66
+ ) public payable returns (address) {
67
+ address position = MintingHub.clone(owner, parent, _initialCollateral, _initialMint, expiration, _liqPrice);
73
68
  GATEWAY.registerPosition(position, frontendCode);
74
69
  return position;
75
70
  }
@@ -7,6 +7,6 @@ import {IFrontendGateway} from "./IFrontendGateway.sol";
7
7
  interface IMintingHubGateway {
8
8
  function GATEWAY() external view returns (IFrontendGateway);
9
9
  function notifyInterestPaid(uint256 amount) external;
10
- function openPosition(address _collateralAddress, uint256 _minCollateral, uint256 _initialCollateral, uint256 _mintingMaximum, uint40 _initPeriodSeconds, uint40 _expirationSeconds, uint40 _challengeSeconds, uint24 _riskPremium, uint256 _liqPrice, uint24 _reservePPM, bytes32 _frontendCode) external returns (address);
11
- function clone(address owner, address parent, uint256 _initialCollateral, uint256 _initialMint, uint40 expiration, bytes32 frontendCode) external returns (address);
10
+ function openPosition(address _collateralAddress, uint256 _minCollateral, uint256 _initialCollateral, uint256 _mintingMaximum, uint40 _initPeriodSeconds, uint40 _expirationSeconds, uint40 _challengeSeconds, uint24 _riskPremium, uint256 _liqPrice, uint24 _reservePPM, bytes32 _frontendCode) external payable returns (address);
11
+ function clone(address owner, address parent, uint256 _initialCollateral, uint256 _initialMint, uint40 expiration, uint256 _liqPrice, bytes32 frontendCode) external payable returns (address);
12
12
  }
@@ -0,0 +1,10 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.0;
3
+
4
+ /**
5
+ * @notice Interface for wrapped native tokens (e.g., WcBTC, WETH)
6
+ */
7
+ interface IWrappedNative {
8
+ function deposit() external payable;
9
+ function withdraw(uint256) external;
10
+ }