@juicedollar/jusd 1.0.3 → 1.0.5
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/contracts/MintingHubV2/MintingHub.sol +123 -19
- package/contracts/MintingHubV2/PositionRoller.sol +160 -43
- package/contracts/MintingHubV2/interface/IMintingHub.sol +9 -1
- package/contracts/gateway/MintingHubGateway.sol +1 -1
- package/contracts/test/PositionExpirationTest.sol +1 -1
- package/contracts/test/PositionRollingTest.sol +1 -1
- package/dist/index.d.mts +213 -18
- package/dist/index.d.ts +213 -18
- package/dist/index.js +282 -32
- package/dist/index.mjs +282 -32
- package/exports/abis/MintingHubV2/PositionRoller.ts +90 -18
- package/exports/abis/core/MintingHubGateway.ts +91 -2
- package/exports/abis/utils/MintingHubV2.ts +91 -2
- package/exports/address.config.ts +10 -10
- package/package.json +1 -1
|
@@ -47,6 +47,12 @@ contract MintingHub is IMintingHub, ERC165 {
|
|
|
47
47
|
*/
|
|
48
48
|
mapping(address collateral => mapping(address owner => uint256 amount)) public pendingReturns;
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* @notice Tracks whether the first position has been created.
|
|
52
|
+
* @dev The first position (genesis) can skip the 3-day init period requirement.
|
|
53
|
+
*/
|
|
54
|
+
bool private _genesisPositionCreated;
|
|
55
|
+
|
|
50
56
|
struct Challenge {
|
|
51
57
|
address challenger; // the address from which the challenge was initiated
|
|
52
58
|
uint40 start; // the start of the challenge
|
|
@@ -79,13 +85,14 @@ contract MintingHub is IMintingHub, ERC165 {
|
|
|
79
85
|
error InitPeriodTooShort();
|
|
80
86
|
error NativeOnlyForWCBTC();
|
|
81
87
|
error ValueMismatch();
|
|
88
|
+
error NativeTransferFailed();
|
|
82
89
|
|
|
83
90
|
modifier validPos(address position) {
|
|
84
91
|
if (JUSD.getPositionParent(position) != address(this)) revert InvalidPos();
|
|
85
92
|
_;
|
|
86
93
|
}
|
|
87
94
|
|
|
88
|
-
constructor(address _jusd, address _leadrate, address _roller, address _factory, address _wcbtc) {
|
|
95
|
+
constructor(address _jusd, address _leadrate, address payable _roller, address _factory, address _wcbtc) {
|
|
89
96
|
JUSD = IJuiceDollar(_jusd);
|
|
90
97
|
RATE = ILeadrate(_leadrate);
|
|
91
98
|
POSITION_FACTORY = IPositionFactory(_factory);
|
|
@@ -129,7 +136,12 @@ contract MintingHub is IMintingHub, ERC165 {
|
|
|
129
136
|
if (CHALLENGER_REWARD > _reservePPM || _reservePPM > 1_000_000) revert InvalidReservePPM();
|
|
130
137
|
if (IERC20Metadata(_collateralAddress).decimals() > 24) revert InvalidCollateralDecimals(); // leaves 12 digits for price
|
|
131
138
|
if (_challengeSeconds < 1 days) revert ChallengeTimeTooShort();
|
|
132
|
-
|
|
139
|
+
// First position (genesis) can skip init period, all others require 3 days minimum
|
|
140
|
+
if (_genesisPositionCreated) {
|
|
141
|
+
if (_initPeriodSeconds < 3 days) revert InitPeriodTooShort();
|
|
142
|
+
} else {
|
|
143
|
+
_genesisPositionCreated = true;
|
|
144
|
+
}
|
|
133
145
|
uint256 invalidAmount = IERC20(_collateralAddress).totalSupply() + 1;
|
|
134
146
|
// TODO: Improve for older tokens that revert with assert,
|
|
135
147
|
// which consumes all gas and makes the entire tx fail (uncatchable)
|
|
@@ -225,6 +237,7 @@ contract MintingHub is IMintingHub, ERC165 {
|
|
|
225
237
|
|
|
226
238
|
/**
|
|
227
239
|
* @notice Launch a challenge (Dutch auction) on a position
|
|
240
|
+
* @dev For native coin positions (WcBTC), send msg.value equal to _collateralAmount.
|
|
228
241
|
* @param _positionAddr address of the position we want to challenge
|
|
229
242
|
* @param _collateralAmount amount of the collateral we want to challenge
|
|
230
243
|
* @param minimumPrice guards against the minter front-running with a price change
|
|
@@ -234,14 +247,24 @@ contract MintingHub is IMintingHub, ERC165 {
|
|
|
234
247
|
address _positionAddr,
|
|
235
248
|
uint256 _collateralAmount,
|
|
236
249
|
uint256 minimumPrice
|
|
237
|
-
) external validPos(_positionAddr) returns (uint256) {
|
|
250
|
+
) external payable validPos(_positionAddr) returns (uint256) {
|
|
238
251
|
IPosition position = IPosition(_positionAddr);
|
|
239
252
|
// challenger should be ok if front-run by owner with a higher price
|
|
240
253
|
// in case owner front-runs challenger with small price decrease to prevent challenge,
|
|
241
254
|
// the challenger should set minimumPrice to market price
|
|
242
255
|
uint256 liqPrice = position.virtualPrice();
|
|
243
256
|
if (liqPrice < minimumPrice) revert UnexpectedPrice();
|
|
244
|
-
|
|
257
|
+
|
|
258
|
+
// Transfer collateral (handles native coin positions)
|
|
259
|
+
address collateralAddr = address(position.collateral());
|
|
260
|
+
if (msg.value > 0) {
|
|
261
|
+
if (collateralAddr != WCBTC) revert NativeOnlyForWCBTC();
|
|
262
|
+
if (msg.value != _collateralAmount) revert ValueMismatch();
|
|
263
|
+
IWrappedNative(WCBTC).deposit{value: msg.value}();
|
|
264
|
+
} else {
|
|
265
|
+
IERC20(collateralAddr).transferFrom(msg.sender, address(this), _collateralAmount);
|
|
266
|
+
}
|
|
267
|
+
|
|
245
268
|
uint256 pos = challenges.length;
|
|
246
269
|
challenges.push(Challenge(msg.sender, uint40(block.timestamp), position, _collateralAmount));
|
|
247
270
|
position.notifyChallengeStarted(_collateralAmount, liqPrice);
|
|
@@ -259,25 +282,36 @@ contract MintingHub is IMintingHub, ERC165 {
|
|
|
259
282
|
* @param size how much of the collateral the caller wants to bid for at most
|
|
260
283
|
* (automatically reduced to the available amount)
|
|
261
284
|
* @param postponeCollateralReturn To postpone the return of the collateral to the challenger. Usually false.
|
|
285
|
+
* @param returnCollateralAsNative If true, return collateral as native coin (only for WcBTC positions).
|
|
286
|
+
* In phase 1 (aversion): bidder receives native. In phase 2 (liquidation): both
|
|
287
|
+
* challenger refund and bidder acquisition are returned as native.
|
|
262
288
|
*/
|
|
263
|
-
function bid(uint32 _challengeNumber, uint256 size, bool postponeCollateralReturn)
|
|
289
|
+
function bid(uint32 _challengeNumber, uint256 size, bool postponeCollateralReturn, bool returnCollateralAsNative) public {
|
|
264
290
|
Challenge memory _challenge = challenges[_challengeNumber];
|
|
265
291
|
(uint256 liqPrice, uint40 phase) = _challenge.position.challengeData();
|
|
266
292
|
size = _challenge.size < size ? _challenge.size : size; // cannot bid for more than the size of the challenge
|
|
267
293
|
|
|
268
294
|
if (block.timestamp <= _challenge.start + phase) {
|
|
269
|
-
_avertChallenge(_challenge, _challengeNumber, liqPrice, size);
|
|
295
|
+
_avertChallenge(_challenge, _challengeNumber, liqPrice, size, returnCollateralAsNative);
|
|
270
296
|
emit ChallengeAverted(address(_challenge.position), _challengeNumber, size);
|
|
271
297
|
} else {
|
|
272
|
-
_returnChallengerCollateral(_challenge, _challengeNumber, size, postponeCollateralReturn);
|
|
273
|
-
(uint256 transferredCollateral, uint256 offer) = _finishChallenge(_challenge, size);
|
|
298
|
+
_returnChallengerCollateral(_challenge, _challengeNumber, size, postponeCollateralReturn, returnCollateralAsNative);
|
|
299
|
+
(uint256 transferredCollateral, uint256 offer) = _finishChallenge(_challenge, size, returnCollateralAsNative);
|
|
274
300
|
emit ChallengeSucceeded(address(_challenge.position), _challengeNumber, offer, transferredCollateral, size);
|
|
275
301
|
}
|
|
276
302
|
}
|
|
277
303
|
|
|
304
|
+
/**
|
|
305
|
+
* @notice Post a bid in JUSD given an open challenge (backward compatible version).
|
|
306
|
+
*/
|
|
307
|
+
function bid(uint32 _challengeNumber, uint256 size, bool postponeCollateralReturn) external {
|
|
308
|
+
bid(_challengeNumber, size, postponeCollateralReturn, false);
|
|
309
|
+
}
|
|
310
|
+
|
|
278
311
|
function _finishChallenge(
|
|
279
312
|
Challenge memory _challenge,
|
|
280
|
-
uint256 size
|
|
313
|
+
uint256 size,
|
|
314
|
+
bool asNative
|
|
281
315
|
) internal returns (uint256, uint256) {
|
|
282
316
|
// Repayments depend on what was actually minted, whereas bids depend on the available collateral
|
|
283
317
|
(address owner, uint256 collateral, uint256 repayment, uint256 interest, uint32 reservePPM) = _challenge
|
|
@@ -311,11 +345,21 @@ contract MintingHub is IMintingHub, ERC165 {
|
|
|
311
345
|
}
|
|
312
346
|
JUSD.burnWithoutReserve(repayment, reservePPM); // Repay the challenged part, example: 50 deur leading to 10 deur in implicit profits
|
|
313
347
|
JUSD.collectProfits(address(this), interest); // Collect interest as profits
|
|
314
|
-
|
|
348
|
+
|
|
349
|
+
// Transfer collateral to bidder (handles native coin if requested)
|
|
350
|
+
if (asNative && address(_challenge.position.collateral()) == WCBTC) {
|
|
351
|
+
_challenge.position.transferChallengedCollateral(address(this), collateral);
|
|
352
|
+
IWrappedNative(WCBTC).withdraw(collateral);
|
|
353
|
+
(bool success, ) = msg.sender.call{value: collateral}("");
|
|
354
|
+
if (!success) revert NativeTransferFailed();
|
|
355
|
+
} else {
|
|
356
|
+
_challenge.position.transferChallengedCollateral(msg.sender, collateral);
|
|
357
|
+
}
|
|
358
|
+
|
|
315
359
|
return (collateral, offer);
|
|
316
360
|
}
|
|
317
361
|
|
|
318
|
-
function _avertChallenge(Challenge memory _challenge, uint32 number, uint256 liqPrice, uint256 size) internal {
|
|
362
|
+
function _avertChallenge(Challenge memory _challenge, uint32 number, uint256 liqPrice, uint256 size, bool asNative) internal {
|
|
319
363
|
require(block.timestamp != _challenge.start); // do not allow to avert the challenge in the same transaction, see CS-ZCHF-037
|
|
320
364
|
if (msg.sender == _challenge.challenger) {
|
|
321
365
|
// allow challenger to cancel challenge without paying themselves
|
|
@@ -324,13 +368,22 @@ contract MintingHub is IMintingHub, ERC165 {
|
|
|
324
368
|
}
|
|
325
369
|
|
|
326
370
|
_challenge.position.notifyChallengeAverted(size);
|
|
327
|
-
|
|
371
|
+
|
|
328
372
|
if (size < _challenge.size) {
|
|
329
373
|
challenges[number].size = _challenge.size - size;
|
|
330
374
|
} else {
|
|
331
375
|
require(size == _challenge.size);
|
|
332
376
|
delete challenges[number];
|
|
333
377
|
}
|
|
378
|
+
|
|
379
|
+
// Transfer collateral to bidder (handles native coin if requested)
|
|
380
|
+
if (asNative && address(_challenge.position.collateral()) == WCBTC) {
|
|
381
|
+
IWrappedNative(WCBTC).withdraw(size);
|
|
382
|
+
(bool success, ) = msg.sender.call{value: size}("");
|
|
383
|
+
if (!success) revert NativeTransferFailed();
|
|
384
|
+
} else {
|
|
385
|
+
_challenge.position.collateral().transfer(msg.sender, size);
|
|
386
|
+
}
|
|
334
387
|
}
|
|
335
388
|
|
|
336
389
|
/**
|
|
@@ -340,9 +393,9 @@ contract MintingHub is IMintingHub, ERC165 {
|
|
|
340
393
|
Challenge memory _challenge,
|
|
341
394
|
uint32 number,
|
|
342
395
|
uint256 amount,
|
|
343
|
-
bool postpone
|
|
396
|
+
bool postpone,
|
|
397
|
+
bool asNative
|
|
344
398
|
) internal {
|
|
345
|
-
_returnCollateral(_challenge.position.collateral(), _challenge.challenger, amount, postpone);
|
|
346
399
|
if (_challenge.size == amount) {
|
|
347
400
|
// bid on full amount
|
|
348
401
|
delete challenges[number];
|
|
@@ -350,6 +403,7 @@ contract MintingHub is IMintingHub, ERC165 {
|
|
|
350
403
|
// bid on partial amount
|
|
351
404
|
challenges[number].size -= amount;
|
|
352
405
|
}
|
|
406
|
+
_returnCollateral(_challenge.position.collateral(), _challenge.challenger, amount, postpone, asNative);
|
|
353
407
|
}
|
|
354
408
|
|
|
355
409
|
/**
|
|
@@ -394,18 +448,39 @@ contract MintingHub is IMintingHub, ERC165 {
|
|
|
394
448
|
|
|
395
449
|
/**
|
|
396
450
|
* @notice Challengers can call this method to withdraw collateral whose return was postponed.
|
|
451
|
+
* @param collateral The collateral token address
|
|
452
|
+
* @param target The address to receive the collateral
|
|
453
|
+
* @param asNative If true and collateral is WcBTC, unwrap and send as native coin
|
|
397
454
|
*/
|
|
398
|
-
function returnPostponedCollateral(address collateral, address target)
|
|
455
|
+
function returnPostponedCollateral(address collateral, address target, bool asNative) public {
|
|
399
456
|
uint256 amount = pendingReturns[collateral][msg.sender];
|
|
400
457
|
delete pendingReturns[collateral][msg.sender];
|
|
401
|
-
|
|
458
|
+
if (asNative && collateral == WCBTC) {
|
|
459
|
+
IWrappedNative(WCBTC).withdraw(amount);
|
|
460
|
+
(bool success, ) = target.call{value: amount}("");
|
|
461
|
+
if (!success) revert NativeTransferFailed();
|
|
462
|
+
} else {
|
|
463
|
+
IERC20(collateral).transfer(target, amount);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* @notice Challengers can call this method to withdraw collateral whose return was postponed (backward compatible).
|
|
469
|
+
*/
|
|
470
|
+
function returnPostponedCollateral(address collateral, address target) external {
|
|
471
|
+
returnPostponedCollateral(collateral, target, false);
|
|
402
472
|
}
|
|
403
473
|
|
|
404
|
-
function _returnCollateral(IERC20 collateral, address recipient, uint256 amount, bool postpone) internal {
|
|
474
|
+
function _returnCollateral(IERC20 collateral, address recipient, uint256 amount, bool postpone, bool asNative) internal {
|
|
405
475
|
if (postpone) {
|
|
406
476
|
// Postponing helps in case the challenger was blacklisted or otherwise cannot receive at the moment.
|
|
407
477
|
pendingReturns[address(collateral)][recipient] += amount;
|
|
408
478
|
emit PostponedReturn(address(collateral), recipient, amount);
|
|
479
|
+
} else if (asNative && address(collateral) == WCBTC) {
|
|
480
|
+
// Unwrap and return as native coin
|
|
481
|
+
IWrappedNative(WCBTC).withdraw(amount);
|
|
482
|
+
(bool success, ) = recipient.call{value: amount}("");
|
|
483
|
+
if (!success) revert NativeTransferFailed();
|
|
409
484
|
} else {
|
|
410
485
|
collateral.transfer(recipient, amount); // return the challenger's collateral
|
|
411
486
|
}
|
|
@@ -447,8 +522,12 @@ contract MintingHub is IMintingHub, ERC165 {
|
|
|
447
522
|
*
|
|
448
523
|
* To prevent dust either the remaining collateral needs to be bought or collateral with a value
|
|
449
524
|
* of at least OPENING_FEE (1000 JUSD) needs to remain in the position for a different buyer
|
|
525
|
+
*
|
|
526
|
+
* @param pos The expired position to buy collateral from
|
|
527
|
+
* @param upToAmount Maximum amount of collateral to buy
|
|
528
|
+
* @param receiveAsNative If true and collateral is WcBTC, receive as native coin
|
|
450
529
|
*/
|
|
451
|
-
function buyExpiredCollateral(IPosition pos, uint256 upToAmount)
|
|
530
|
+
function buyExpiredCollateral(IPosition pos, uint256 upToAmount, bool receiveAsNative) public returns (uint256) {
|
|
452
531
|
uint256 max = pos.collateral().balanceOf(address(pos));
|
|
453
532
|
uint256 amount = upToAmount > max ? max : upToAmount;
|
|
454
533
|
uint256 forceSalePrice = expiredPurchasePrice(pos);
|
|
@@ -459,11 +538,31 @@ contract MintingHub is IMintingHub, ERC165 {
|
|
|
459
538
|
revert LeaveNoDust(max - amount);
|
|
460
539
|
}
|
|
461
540
|
|
|
462
|
-
pos.
|
|
541
|
+
address collateralAddr = address(pos.collateral());
|
|
542
|
+
if (receiveAsNative && collateralAddr == WCBTC) {
|
|
543
|
+
// Pull JUSD from user to Hub, then approve Position to spend it
|
|
544
|
+
JUSD.transferFrom(msg.sender, address(this), costs);
|
|
545
|
+
IERC20(address(JUSD)).approve(address(pos), costs);
|
|
546
|
+
// Route through hub to unwrap
|
|
547
|
+
pos.forceSale(address(this), amount, costs);
|
|
548
|
+
IWrappedNative(WCBTC).withdraw(amount);
|
|
549
|
+
(bool success, ) = msg.sender.call{value: amount}("");
|
|
550
|
+
if (!success) revert NativeTransferFailed();
|
|
551
|
+
} else {
|
|
552
|
+
pos.forceSale(msg.sender, amount, costs);
|
|
553
|
+
}
|
|
554
|
+
|
|
463
555
|
emit ForcedSale(address(pos), amount, forceSalePrice);
|
|
464
556
|
return amount;
|
|
465
557
|
}
|
|
466
558
|
|
|
559
|
+
/**
|
|
560
|
+
* Buys up to the desired amount of the collateral asset from the given expired position (backward compatible).
|
|
561
|
+
*/
|
|
562
|
+
function buyExpiredCollateral(IPosition pos, uint256 upToAmount) external returns (uint256) {
|
|
563
|
+
return buyExpiredCollateral(pos, upToAmount, false);
|
|
564
|
+
}
|
|
565
|
+
|
|
467
566
|
/**
|
|
468
567
|
* @dev See {IERC165-supportsInterface}.
|
|
469
568
|
*/
|
|
@@ -472,4 +571,9 @@ contract MintingHub is IMintingHub, ERC165 {
|
|
|
472
571
|
interfaceId == type(IMintingHub).interfaceId ||
|
|
473
572
|
super.supportsInterface(interfaceId);
|
|
474
573
|
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* @notice Required to receive native coin when unwrapping WcBTC.
|
|
577
|
+
*/
|
|
578
|
+
receive() external payable {}
|
|
475
579
|
}
|
|
@@ -8,6 +8,7 @@ import {IMintingHubGateway} from "../gateway/interface/IMintingHubGateway.sol";
|
|
|
8
8
|
import {IMintingHub} from "./interface/IMintingHub.sol";
|
|
9
9
|
import {IPosition} from "./interface/IPosition.sol";
|
|
10
10
|
import {IReserve} from "../interface/IReserve.sol";
|
|
11
|
+
import {IWrappedNative} from "../interface/IWrappedNative.sol";
|
|
11
12
|
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
|
|
12
13
|
|
|
13
14
|
/**
|
|
@@ -15,13 +16,19 @@ import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
|
|
|
15
16
|
*
|
|
16
17
|
* Helper to roll over a debt from an old position to a new one.
|
|
17
18
|
* Both positions should have the same collateral. Otherwise, it does not make much sense.
|
|
19
|
+
*
|
|
20
|
+
* For standard ERC20 positions, use roll/rollFully/rollFullyWithExpiration.
|
|
21
|
+
* Collateral flows through the user's wallet and requires approval.
|
|
22
|
+
*
|
|
23
|
+
* For native coin positions (e.g., WCBTC), use rollNative/rollFullyNative/rollFullyNativeWithExpiration.
|
|
24
|
+
* Collateral flows through the roller, no approval needed, and excess is returned as native coin.
|
|
18
25
|
*/
|
|
19
26
|
contract PositionRoller {
|
|
20
27
|
IJuiceDollar private jusd;
|
|
21
28
|
|
|
22
29
|
error NotOwner(address pos);
|
|
23
30
|
error NotPosition(address pos);
|
|
24
|
-
error
|
|
31
|
+
error NativeTransferFailed();
|
|
25
32
|
|
|
26
33
|
event Roll(address source, uint256 collWithdraw, uint256 repay, address target, uint256 collDeposit, uint256 mint);
|
|
27
34
|
|
|
@@ -32,8 +39,8 @@ contract PositionRoller {
|
|
|
32
39
|
/**
|
|
33
40
|
* Convenience method to roll an old position into a new one.
|
|
34
41
|
*
|
|
35
|
-
* Pre-condition: an allowance for the roller to spend the collateral asset
|
|
36
|
-
* i.e.,
|
|
42
|
+
* Pre-condition: an allowance for the roller to spend the collateral asset
|
|
43
|
+
* on behalf of the caller, i.e., collateral.approve(roller, collateral.balanceOf(sourcePosition)).
|
|
37
44
|
*
|
|
38
45
|
* The following is assumed:
|
|
39
46
|
* - If the limit of the target position permits, the user wants to roll everything.
|
|
@@ -49,21 +56,12 @@ contract PositionRoller {
|
|
|
49
56
|
*/
|
|
50
57
|
function rollFullyWithExpiration(IPosition source, IPosition target, uint40 expiration) public {
|
|
51
58
|
require(source.collateral() == target.collateral());
|
|
52
|
-
uint256
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
uint256 depositAmount = (mintAmount * 10 ** 18 + targetPrice - 1) / targetPrice; // round up
|
|
59
|
-
if (depositAmount > collateralToWithdraw) {
|
|
60
|
-
// If we need more collateral than available from the old position, we opt for taking
|
|
61
|
-
// the missing funds from the caller instead of requiring additional collateral.
|
|
62
|
-
depositAmount = collateralToWithdraw;
|
|
63
|
-
mintAmount = (depositAmount * target.price()) / 10 ** 18; // round down, rest will be taken from caller
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
roll(source, principal + interest, collateralToWithdraw, target, mintAmount, depositAmount, expiration);
|
|
59
|
+
(uint256 repay, uint256 collWithdraw, uint256 mint, uint256 collDeposit) = _calculateRollParams(
|
|
60
|
+
source,
|
|
61
|
+
target,
|
|
62
|
+
0
|
|
63
|
+
);
|
|
64
|
+
roll(source, repay, collWithdraw, target, mint, collDeposit, expiration);
|
|
67
65
|
}
|
|
68
66
|
|
|
69
67
|
/**
|
|
@@ -72,12 +70,12 @@ contract PositionRoller {
|
|
|
72
70
|
* It is the responsibility of the caller to ensure that both positions are valid contracts.
|
|
73
71
|
*
|
|
74
72
|
* @param source The source position, must be owned by the msg.sender.
|
|
75
|
-
* @param repay The amount of principal to repay from the source position using a flash loan
|
|
76
|
-
* @param collWithdraw Collateral to
|
|
73
|
+
* @param repay The amount of principal to repay from the source position using a flash loan.
|
|
74
|
+
* @param collWithdraw Collateral to withdraw from the source position.
|
|
77
75
|
* @param target The target position. If not owned by msg.sender or if it does not have the desired expiration,
|
|
78
76
|
* it is cloned to create a position owned by the msg.sender.
|
|
79
|
-
* @param mint The amount to be minted from the target position
|
|
80
|
-
* @param collDeposit The amount of collateral to
|
|
77
|
+
* @param mint The amount to be minted from the target position.
|
|
78
|
+
* @param collDeposit The amount of collateral to deposit into the target position.
|
|
81
79
|
* @param expiration The desired expiration date for the target position.
|
|
82
80
|
*/
|
|
83
81
|
function roll(
|
|
@@ -94,9 +92,10 @@ contract PositionRoller {
|
|
|
94
92
|
source.withdrawCollateral(msg.sender, collWithdraw);
|
|
95
93
|
if (mint > 0) {
|
|
96
94
|
IERC20 targetCollateral = IERC20(target.collateral());
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
targetCollateral.
|
|
95
|
+
bool needsClone = Ownable(address(target)).owner() != msg.sender || expiration != target.expiration();
|
|
96
|
+
if (needsClone) {
|
|
97
|
+
targetCollateral.transferFrom(msg.sender, address(this), collDeposit);
|
|
98
|
+
targetCollateral.approve(target.hub(), collDeposit);
|
|
100
99
|
target = _cloneTargetPosition(target, source, collDeposit, mint, expiration);
|
|
101
100
|
} else {
|
|
102
101
|
// We can roll into the provided existing position.
|
|
@@ -116,10 +115,125 @@ contract PositionRoller {
|
|
|
116
115
|
emit Roll(address(source), collWithdraw, repay, address(target), collDeposit, mint);
|
|
117
116
|
}
|
|
118
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Convenience method to roll a native coin position into a new one.
|
|
120
|
+
*
|
|
121
|
+
* No collateral approval is needed - collateral flows through the roller
|
|
122
|
+
* and excess is returned as native coin.
|
|
123
|
+
*
|
|
124
|
+
* Additional collateral can be provided via msg.value.
|
|
125
|
+
*/
|
|
126
|
+
function rollFullyNative(IPosition source, IPosition target) external payable {
|
|
127
|
+
rollFullyNativeWithExpiration(source, target, target.expiration());
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Like rollFullyNative, but with a custom expiration date for the new position.
|
|
132
|
+
*/
|
|
133
|
+
function rollFullyNativeWithExpiration(IPosition source, IPosition target, uint40 expiration) public payable {
|
|
134
|
+
require(source.collateral() == target.collateral());
|
|
135
|
+
(uint256 repay, uint256 collWithdraw, uint256 mint, uint256 collDeposit) = _calculateRollParams(
|
|
136
|
+
source,
|
|
137
|
+
target,
|
|
138
|
+
msg.value
|
|
139
|
+
);
|
|
140
|
+
rollNative(source, repay, collWithdraw, target, mint, collDeposit, expiration);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Rolls a native coin position into the target position using a flash loan.
|
|
145
|
+
* Collateral is routed through the roller and returned as native coin,
|
|
146
|
+
* eliminating the need for users to interact with wrapped native tokens directly.
|
|
147
|
+
*
|
|
148
|
+
* If additional collateral is needed (collDeposit > collWithdraw), it can be provided
|
|
149
|
+
* as native coin via msg.value.
|
|
150
|
+
*
|
|
151
|
+
* @param source The source position, must be owned by the msg.sender.
|
|
152
|
+
* @param repay The amount of principal to repay from the source position using a flash loan.
|
|
153
|
+
* @param collWithdraw Collateral to withdraw from the source position.
|
|
154
|
+
* @param target The target position. If not owned by msg.sender or if it does not have the desired expiration,
|
|
155
|
+
* it is cloned to create a position owned by the msg.sender.
|
|
156
|
+
* @param mint The amount to be minted from the target position.
|
|
157
|
+
* @param collDeposit The amount of collateral to deposit into the target position.
|
|
158
|
+
* @param expiration The desired expiration date for the target position.
|
|
159
|
+
*/
|
|
160
|
+
function rollNative(
|
|
161
|
+
IPosition source,
|
|
162
|
+
uint256 repay,
|
|
163
|
+
uint256 collWithdraw,
|
|
164
|
+
IPosition target,
|
|
165
|
+
uint256 mint,
|
|
166
|
+
uint256 collDeposit,
|
|
167
|
+
uint40 expiration
|
|
168
|
+
) public payable valid(source) valid(target) own(source) {
|
|
169
|
+
address collateral = address(source.collateral());
|
|
170
|
+
|
|
171
|
+
jusd.mint(address(this), repay); // take a flash loan
|
|
172
|
+
uint256 used = source.repay(repay);
|
|
173
|
+
source.withdrawCollateral(address(this), collWithdraw);
|
|
174
|
+
if (msg.value > 0) {
|
|
175
|
+
IWrappedNative(collateral).deposit{value: msg.value}();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (mint > 0) {
|
|
179
|
+
IERC20 targetCollateral = IERC20(collateral);
|
|
180
|
+
bool needsClone = Ownable(address(target)).owner() != msg.sender || expiration != target.expiration();
|
|
181
|
+
if (needsClone) {
|
|
182
|
+
targetCollateral.approve(target.hub(), collDeposit);
|
|
183
|
+
target = _cloneTargetPosition(target, source, collDeposit, mint, expiration);
|
|
184
|
+
} else {
|
|
185
|
+
targetCollateral.transfer(address(target), collDeposit);
|
|
186
|
+
target.mint(msg.sender, mint);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Transfer remaining flash loan to caller for repayment
|
|
191
|
+
if (repay > used) {
|
|
192
|
+
jusd.transfer(msg.sender, repay - used);
|
|
193
|
+
}
|
|
194
|
+
jusd.burnFrom(msg.sender, repay); // repay the flash loan
|
|
195
|
+
|
|
196
|
+
// Return excess as native coin
|
|
197
|
+
uint256 remaining = IERC20(collateral).balanceOf(address(this));
|
|
198
|
+
if (remaining > 0) {
|
|
199
|
+
IWrappedNative(collateral).withdraw(remaining);
|
|
200
|
+
(bool success, ) = msg.sender.call{value: remaining}("");
|
|
201
|
+
if (!success) revert NativeTransferFailed();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
emit Roll(address(source), collWithdraw, repay, address(target), collDeposit, mint);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Calculates the parameters for a roll operation.
|
|
209
|
+
* @param extraCollateral Additional collateral provided via msg.value (for native rolls).
|
|
210
|
+
*/
|
|
211
|
+
function _calculateRollParams(
|
|
212
|
+
IPosition source,
|
|
213
|
+
IPosition target,
|
|
214
|
+
uint256 extraCollateral
|
|
215
|
+
) internal view returns (uint256 repay, uint256 collWithdraw, uint256 mint, uint256 collDeposit) {
|
|
216
|
+
uint256 principal = source.principal();
|
|
217
|
+
uint256 interest = source.getInterest();
|
|
218
|
+
uint256 usableMint = source.getUsableMint(principal) + interest;
|
|
219
|
+
uint256 mintAmount = target.getMintAmount(usableMint);
|
|
220
|
+
uint256 collateralAvailable = IERC20(source.collateral()).balanceOf(address(source));
|
|
221
|
+
uint256 totalAvailable = collateralAvailable + extraCollateral;
|
|
222
|
+
uint256 targetPrice = target.price();
|
|
223
|
+
uint256 depositAmount = (mintAmount * 10 ** 18 + targetPrice - 1) / targetPrice;
|
|
224
|
+
|
|
225
|
+
if (depositAmount > totalAvailable) {
|
|
226
|
+
depositAmount = totalAvailable;
|
|
227
|
+
mintAmount = (depositAmount * target.price()) / 10 ** 18;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return (principal + interest, collateralAvailable, mintAmount, depositAmount);
|
|
231
|
+
}
|
|
232
|
+
|
|
119
233
|
/**
|
|
120
234
|
* Clones the target position and mints the specified amount using the given collateral.
|
|
121
235
|
*/
|
|
122
|
-
function _cloneTargetPosition
|
|
236
|
+
function _cloneTargetPosition(
|
|
123
237
|
IPosition target,
|
|
124
238
|
IPosition source,
|
|
125
239
|
uint256 collDeposit,
|
|
@@ -127,24 +241,24 @@ contract PositionRoller {
|
|
|
127
241
|
uint40 expiration
|
|
128
242
|
) internal returns (IPosition) {
|
|
129
243
|
if (IERC165(target.hub()).supportsInterface(type(IMintingHubGateway).interfaceId)) {
|
|
130
|
-
bytes32 frontendCode = IMintingHubGateway(target.hub()).GATEWAY().getPositionFrontendCode(
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
)
|
|
143
|
-
);
|
|
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
|
+
);
|
|
144
257
|
} else {
|
|
145
|
-
return
|
|
146
|
-
|
|
147
|
-
|
|
258
|
+
return
|
|
259
|
+
IPosition(
|
|
260
|
+
IMintingHub(target.hub()).clone(msg.sender, address(target), collDeposit, mint, expiration, 0)
|
|
261
|
+
);
|
|
148
262
|
}
|
|
149
263
|
}
|
|
150
264
|
|
|
@@ -157,4 +271,7 @@ contract PositionRoller {
|
|
|
157
271
|
if (jusd.getPositionParent(address(pos)) == address(0x0)) revert NotPosition(address(pos));
|
|
158
272
|
_;
|
|
159
273
|
}
|
|
274
|
+
|
|
275
|
+
/// @notice Required to receive native coin when unwrapping
|
|
276
|
+
receive() external payable {}
|
|
160
277
|
}
|
|
@@ -10,17 +10,25 @@ interface IMintingHub {
|
|
|
10
10
|
|
|
11
11
|
function ROLLER() external view returns (PositionRoller);
|
|
12
12
|
|
|
13
|
+
function WCBTC() external view returns (address);
|
|
14
|
+
|
|
13
15
|
function challenge(
|
|
14
16
|
address _positionAddr,
|
|
15
17
|
uint256 _collateralAmount,
|
|
16
18
|
uint256 minimumPrice
|
|
17
|
-
) external returns (uint256);
|
|
19
|
+
) external payable returns (uint256);
|
|
18
20
|
|
|
19
21
|
function bid(uint32 _challengeNumber, uint256 size, bool postponeCollateralReturn) external;
|
|
20
22
|
|
|
23
|
+
function bid(uint32 _challengeNumber, uint256 size, bool postponeCollateralReturn, bool returnCollateralAsNative) external;
|
|
24
|
+
|
|
21
25
|
function returnPostponedCollateral(address collateral, address target) external;
|
|
22
26
|
|
|
27
|
+
function returnPostponedCollateral(address collateral, address target, bool asNative) external;
|
|
28
|
+
|
|
23
29
|
function buyExpiredCollateral(IPosition pos, uint256 upToAmount) external returns (uint256);
|
|
24
30
|
|
|
31
|
+
function buyExpiredCollateral(IPosition pos, uint256 upToAmount, bool receiveAsNative) external returns (uint256);
|
|
32
|
+
|
|
25
33
|
function clone(address owner, address parent, uint256 _initialCollateral, uint256 _initialMint, uint40 expiration, uint256 _liqPrice) external payable returns (address);
|
|
26
34
|
}
|
|
@@ -14,7 +14,7 @@ contract PositionExpirationTest {
|
|
|
14
14
|
IJuiceDollar public jusd;
|
|
15
15
|
bytes32 public frontendCode;
|
|
16
16
|
|
|
17
|
-
constructor(address hub_) {
|
|
17
|
+
constructor(address payable hub_) {
|
|
18
18
|
hub = MintingHubGateway(hub_);
|
|
19
19
|
col = new TestToken("Some Collateral", "COL", uint8(0));
|
|
20
20
|
jusd = hub.JUSD();
|
|
@@ -20,7 +20,7 @@ contract PositionRollingTest {
|
|
|
20
20
|
IPosition public p1;
|
|
21
21
|
IPosition public p2;
|
|
22
22
|
|
|
23
|
-
constructor(address hub_) {
|
|
23
|
+
constructor(address payable hub_) {
|
|
24
24
|
hub = MintingHub(hub_);
|
|
25
25
|
col = new TestToken("Some Collateral", "COL", uint8(0));
|
|
26
26
|
jusd = hub.JUSD();
|