@juicedollar/jusd 3.0.1 → 4.0.1
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/Leadrate.sol +1 -1
- package/contracts/{MintingHubV2 → MintingHubV3}/MintingHub.sol +93 -100
- package/contracts/{MintingHubV2 → MintingHubV3}/Position.sol +20 -13
- package/contracts/{MintingHubV2 → MintingHubV3}/PositionRoller.sol +17 -37
- package/contracts/{MintingHubV2 → MintingHubV3}/interface/IMintingHub.sol +2 -2
- package/contracts/{MintingHubV2 → MintingHubV3}/interface/IPosition.sol +3 -3
- package/contracts/Savings.sol +36 -6
- package/contracts/TeamMinter.sol +68 -0
- package/contracts/interface/ISavingsJUSD.sol +10 -1
- package/contracts/test/PositionExpirationTest.sol +17 -38
- package/contracts/test/PositionRollingTest.sol +3 -3
- package/contracts/test/ReentrantAttacker.sol +1 -1
- package/dist/index.d.mts +5609 -3568
- package/dist/index.d.ts +5609 -3568
- package/dist/index.js +9380 -6699
- package/dist/index.mjs +9370 -6693
- package/exports/abis/shared/Equity.ts +1286 -0
- package/exports/abis/shared/JuiceDollar.ts +1366 -0
- package/exports/abis/shared/StablecoinBridge.ts +279 -0
- package/exports/abis/utils/StartUSD.ts +213 -213
- package/exports/abis/{core → v2}/FrontendGateway.ts +1 -1
- package/exports/abis/{core → v2}/MintingHubGateway.ts +1 -1
- package/exports/abis/{MintingHubV2 → v2}/PositionRoller.ts +1 -1
- package/exports/abis/{core → v2}/SavingsGateway.ts +1 -1
- package/exports/abis/{core → v2}/SavingsVaultJUSD.ts +1 -1
- package/exports/abis/v3/MintingHub.ts +1024 -0
- package/exports/abis/v3/Position.ts +1142 -0
- package/exports/abis/v3/PositionFactory.ts +90 -0
- package/exports/abis/v3/PositionRoller.ts +255 -0
- package/exports/abis/v3/Savings.ts +553 -0
- package/exports/abis/v3/SavingsVaultJUSD.ts +925 -0
- package/exports/address.config.ts +63 -35
- package/exports/index.ts +22 -14
- package/package.json +7 -9
- package/contracts/gateway/FrontendGateway.sol +0 -224
- package/contracts/gateway/MintingHubGateway.sol +0 -82
- package/contracts/gateway/SavingsGateway.sol +0 -51
- package/contracts/gateway/interface/IFrontendGateway.sol +0 -49
- package/contracts/gateway/interface/IMintingHubGateway.sol +0 -12
- package/exports/abis/core/Equity.ts +0 -1286
- package/exports/abis/core/JuiceDollar.ts +0 -1366
- package/exports/abis/utils/MintingHubV2.ts +0 -888
- package/exports/abis/utils/Savings.ts +0 -453
- package/exports/abis/utils/StablecoinBridge.ts +0 -279
- /package/contracts/{MintingHubV2 → MintingHubV3}/PositionFactory.sol +0 -0
- /package/contracts/{MintingHubV2 → MintingHubV3}/interface/IPositionFactory.sol +0 -0
- /package/exports/abis/{MintingHubV2 → v2}/PositionFactoryV2.ts +0 -0
- /package/exports/abis/{MintingHubV2 → v2}/PositionV2.ts +0 -0
package/contracts/Leadrate.sol
CHANGED
|
@@ -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(
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
306
|
+
uint256 _challengeNumber,
|
|
302
307
|
uint256 size,
|
|
303
308
|
bool postponeCollateralReturn,
|
|
304
309
|
bool returnCollateralAsNative
|
|
305
|
-
)
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
459
|
-
* @dev
|
|
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
|
|
478
|
+
function _challengeUnitPrice(Challenge memory _challenge) internal view returns (uint256) {
|
|
462
479
|
(uint256 liqPrice, uint40 phase) = _challenge.position.challengeData();
|
|
463
|
-
return
|
|
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(
|
|
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)
|
|
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)
|
|
569
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
735
|
+
Position(original).notifyRepaid(amount);
|
|
727
736
|
principal -= amount;
|
|
728
737
|
}
|
|
729
738
|
|
|
730
739
|
/**
|
|
731
|
-
* @notice Updates outstanding interest
|
|
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,
|
|
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
|
-
|
|
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(
|
|
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,
|
|
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 =
|
|
192
|
+
uint256 remaining = collateralToken.balanceOf(address(this));
|
|
198
193
|
if (remaining > 0) {
|
|
199
|
-
IWrappedNative(
|
|
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 *
|
|
222
|
+
mintAmount = (depositAmount * targetPrice) / 10 ** 18;
|
|
228
223
|
}
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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(
|
|
31
|
+
function bid(uint256 _challengeNumber, uint256 size, bool postponeCollateralReturn) external;
|
|
32
32
|
|
|
33
33
|
function bid(
|
|
34
|
-
|
|
34
|
+
uint256 _challengeNumber,
|
|
35
35
|
uint256 size,
|
|
36
36
|
bool postponeCollateralReturn,
|
|
37
37
|
bool returnCollateralAsNative
|