@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 +1 -2
- package/contracts/MintingHubV2/MintingHub.sol +49 -19
- package/contracts/MintingHubV2/Position.sol +190 -24
- package/contracts/MintingHubV2/PositionFactory.sol +2 -2
- package/contracts/MintingHubV2/PositionRoller.sol +2 -1
- package/contracts/MintingHubV2/interface/IMintingHub.sol +1 -1
- package/contracts/MintingHubV2/interface/IPosition.sol +12 -2
- package/contracts/StartUSD.sol +2 -2
- package/contracts/gateway/MintingHubGateway.sol +13 -18
- package/contracts/gateway/interface/IMintingHubGateway.sol +2 -2
- package/contracts/interface/IWrappedNative.sol +10 -0
- package/contracts/test/PositionExpirationTest.sol +3 -3
- package/contracts/test/ReentrantAttacker.sol +74 -0
- package/contracts/test/RejectNative.sol +17 -0
- package/dist/index.d.mts +240 -511
- package/dist/index.d.ts +240 -511
- package/dist/index.js +246 -598
- package/dist/index.mjs +246 -597
- package/exports/abis/MintingHubV2/PositionV2.ts +125 -8
- package/exports/abis/core/MintingHubGateway.ts +40 -75
- package/exports/abis/utils/MintingHubV2.ts +35 -36
- package/exports/address.config.ts +10 -13
- package/exports/index.ts +0 -1
- package/package.json +2 -1
- package/contracts/gateway/CoinLendingGateway.sol +0 -223
- package/contracts/gateway/interface/ICoinLendingGateway.sol +0 -73
- package/exports/abis/core/CoinLendingGateway.ts +0 -427
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:
|
|
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 <
|
|
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
|
|
138
|
-
if (_minCollateral * _liqPrice <
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
621
|
-
*
|
|
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
|
|
624
|
-
if (token == address(collateral))
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
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
|
|
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);
|
package/contracts/StartUSD.sol
CHANGED
|
@@ -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
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
}
|