@juicedollar/jusd 1.0.6 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -26
- package/contracts/MintingHubV2/MintingHub.sol +80 -16
- package/contracts/MintingHubV2/Position.sol +80 -24
- package/contracts/MintingHubV2/interface/IMintingHub.sol +24 -2
- package/contracts/SavingsVaultJUSD.sol +14 -21
- package/contracts/StablecoinBridge.sol +49 -4
- package/contracts/interface/IReserve.sol +2 -0
- package/contracts/test/PositionExpirationTest.sol +2 -2
- package/contracts/test/PositionRollingTest.sol +2 -2
- package/dist/index.d.mts +269 -0
- package/dist/index.d.ts +269 -0
- package/dist/index.js +350 -12
- package/dist/index.mjs +350 -12
- package/exports/abis/MintingHubV2/PositionV2.ts +21 -0
- package/exports/abis/core/MintingHubGateway.ts +118 -0
- package/exports/abis/core/SavingsVaultJUSD.ts +5 -0
- package/exports/abis/utils/MintingHubV2.ts +118 -0
- package/exports/abis/utils/StablecoinBridge.ts +70 -0
- package/exports/address.config.ts +28 -14
- package/exports/index.ts +0 -3
- package/package.json +3 -5
package/README.md
CHANGED
|
@@ -213,32 +213,6 @@ USE_FORK=true BRIDGE_KEY=<KEY> npx hardhat run scripts/deployment/deploy/deployB
|
|
|
213
213
|
|
|
214
214
|
Bridge configurations: `scripts/deployment/config/stablecoinBridgeConfig.ts`
|
|
215
215
|
|
|
216
|
-
### Hardhat Ignition Deployment
|
|
217
|
-
|
|
218
|
-
```bash
|
|
219
|
-
# Deploy single module with verification
|
|
220
|
-
npm run deploy ignition/modules/MODULE --network citrea --verify --deployment-id MODULE_ID_01
|
|
221
|
-
|
|
222
|
-
# Deploy all modules
|
|
223
|
-
npm run deploy -- --network citrea --verify
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
**Output:**
|
|
227
|
-
- `ignition/deployments/[deployment]/deployed_addresses.json`
|
|
228
|
-
- `ignition/deployments/[deployment]/journal.jsonl`
|
|
229
|
-
- `ignition/constructor-args/*.js`
|
|
230
|
-
|
|
231
|
-
### Manual Verification
|
|
232
|
-
|
|
233
|
-
```bash
|
|
234
|
-
npx hardhat verify --network citrea \
|
|
235
|
-
--constructor-args ./ignition/constructor-args/$FILE.js \
|
|
236
|
-
$ADDRESS
|
|
237
|
-
|
|
238
|
-
# Verify unrelated contracts
|
|
239
|
-
npx hardhat ignition verify $DEPLOYMENT --include-unrelated-contracts
|
|
240
|
-
```
|
|
241
|
-
|
|
242
216
|
---
|
|
243
217
|
|
|
244
218
|
## NPM Package
|
|
@@ -32,6 +32,14 @@ contract MintingHub is IMintingHub, ERC165 {
|
|
|
32
32
|
uint256 public constant CHALLENGER_REWARD = 20000; // 2%
|
|
33
33
|
uint256 public constant EXPIRED_PRICE_FACTOR = 10;
|
|
34
34
|
|
|
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
|
+
|
|
35
43
|
IPositionFactory private immutable POSITION_FACTORY; // position contract to clone
|
|
36
44
|
|
|
37
45
|
IJuiceDollar public immutable JUSD; // currency
|
|
@@ -49,7 +57,7 @@ contract MintingHub is IMintingHub, ERC165 {
|
|
|
49
57
|
|
|
50
58
|
/**
|
|
51
59
|
* @notice Tracks whether the first position has been created.
|
|
52
|
-
* @dev The first position (genesis) can skip the
|
|
60
|
+
* @dev The first position (genesis) can skip the 14-day init period requirement.
|
|
53
61
|
*/
|
|
54
62
|
bool private _genesisPositionCreated;
|
|
55
63
|
|
|
@@ -72,6 +80,7 @@ contract MintingHub is IMintingHub, ERC165 {
|
|
|
72
80
|
);
|
|
73
81
|
event PostponedReturn(address collateral, address indexed beneficiary, uint256 amount);
|
|
74
82
|
event ForcedSale(address pos, uint256 amount, uint256 priceE36MinusDecimals);
|
|
83
|
+
// Note: PositionUpdate and PositionDeniedByGovernance events are defined in IMintingHub interface
|
|
75
84
|
|
|
76
85
|
error UnexpectedPrice();
|
|
77
86
|
error InvalidPos();
|
|
@@ -86,6 +95,8 @@ contract MintingHub is IMintingHub, ERC165 {
|
|
|
86
95
|
error NativeOnlyForWCBTC();
|
|
87
96
|
error ValueMismatch();
|
|
88
97
|
error NativeTransferFailed();
|
|
98
|
+
error MessageTooLong(uint256 length, uint256 maxLength);
|
|
99
|
+
error EmptyMessage();
|
|
89
100
|
|
|
90
101
|
modifier validPos(address position) {
|
|
91
102
|
if (JUSD.getPositionParent(position) != address(this)) revert InvalidPos();
|
|
@@ -136,9 +147,9 @@ contract MintingHub is IMintingHub, ERC165 {
|
|
|
136
147
|
if (CHALLENGER_REWARD > _reservePPM || _reservePPM > 1_000_000) revert InvalidReservePPM();
|
|
137
148
|
if (IERC20Metadata(_collateralAddress).decimals() > 24) revert InvalidCollateralDecimals(); // leaves 12 digits for price
|
|
138
149
|
if (_challengeSeconds < 1 days) revert ChallengeTimeTooShort();
|
|
139
|
-
// First position (genesis) can skip init period, all others require
|
|
150
|
+
// First position (genesis) can skip init period, all others require 14 days minimum
|
|
140
151
|
if (_genesisPositionCreated) {
|
|
141
|
-
if (_initPeriodSeconds <
|
|
152
|
+
if (_initPeriodSeconds < 14 days) revert InitPeriodTooShort();
|
|
142
153
|
} else {
|
|
143
154
|
_genesisPositionCreated = true;
|
|
144
155
|
}
|
|
@@ -286,7 +297,12 @@ contract MintingHub is IMintingHub, ERC165 {
|
|
|
286
297
|
* In phase 1 (aversion): bidder receives native. In phase 2 (liquidation): both
|
|
287
298
|
* challenger refund and bidder acquisition are returned as native.
|
|
288
299
|
*/
|
|
289
|
-
function bid(
|
|
300
|
+
function bid(
|
|
301
|
+
uint32 _challengeNumber,
|
|
302
|
+
uint256 size,
|
|
303
|
+
bool postponeCollateralReturn,
|
|
304
|
+
bool returnCollateralAsNative
|
|
305
|
+
) public {
|
|
290
306
|
Challenge memory _challenge = challenges[_challengeNumber];
|
|
291
307
|
(uint256 liqPrice, uint40 phase) = _challenge.position.challengeData();
|
|
292
308
|
size = _challenge.size < size ? _challenge.size : size; // cannot bid for more than the size of the challenge
|
|
@@ -295,8 +311,18 @@ contract MintingHub is IMintingHub, ERC165 {
|
|
|
295
311
|
_avertChallenge(_challenge, _challengeNumber, liqPrice, size, returnCollateralAsNative);
|
|
296
312
|
emit ChallengeAverted(address(_challenge.position), _challengeNumber, size);
|
|
297
313
|
} else {
|
|
298
|
-
_returnChallengerCollateral(
|
|
299
|
-
|
|
314
|
+
_returnChallengerCollateral(
|
|
315
|
+
_challenge,
|
|
316
|
+
_challengeNumber,
|
|
317
|
+
size,
|
|
318
|
+
postponeCollateralReturn,
|
|
319
|
+
returnCollateralAsNative
|
|
320
|
+
);
|
|
321
|
+
(uint256 transferredCollateral, uint256 offer) = _finishChallenge(
|
|
322
|
+
_challenge,
|
|
323
|
+
size,
|
|
324
|
+
returnCollateralAsNative
|
|
325
|
+
);
|
|
300
326
|
emit ChallengeSucceeded(address(_challenge.position), _challengeNumber, offer, transferredCollateral, size);
|
|
301
327
|
}
|
|
302
328
|
}
|
|
@@ -322,8 +348,8 @@ contract MintingHub is IMintingHub, ERC165 {
|
|
|
322
348
|
// enforced in Position.setPrice and knowing that collateral <= col.
|
|
323
349
|
uint256 offer = _calculateOffer(_challenge, collateral);
|
|
324
350
|
|
|
325
|
-
JUSD.transferFrom(msg.sender, address(this), offer); // get money from bidder
|
|
326
|
-
uint256 reward = (offer * CHALLENGER_REWARD) / 1_000_000;
|
|
351
|
+
JUSD.transferFrom(msg.sender, address(this), offer); // get money from bidder
|
|
352
|
+
uint256 reward = (offer * CHALLENGER_REWARD) / 1_000_000;
|
|
327
353
|
JUSD.transfer(_challenge.challenger, reward); // pay out the challenger reward
|
|
328
354
|
uint256 fundsAvailable = offer - reward; // funds available after reward
|
|
329
355
|
|
|
@@ -359,7 +385,13 @@ contract MintingHub is IMintingHub, ERC165 {
|
|
|
359
385
|
return (collateral, offer);
|
|
360
386
|
}
|
|
361
387
|
|
|
362
|
-
function _avertChallenge(
|
|
388
|
+
function _avertChallenge(
|
|
389
|
+
Challenge memory _challenge,
|
|
390
|
+
uint32 number,
|
|
391
|
+
uint256 liqPrice,
|
|
392
|
+
uint256 size,
|
|
393
|
+
bool asNative
|
|
394
|
+
) internal {
|
|
363
395
|
require(block.timestamp != _challenge.start); // do not allow to avert the challenge in the same transaction, see CS-ZCHF-037
|
|
364
396
|
if (msg.sender == _challenge.challenger) {
|
|
365
397
|
// allow challenger to cancel challenge without paying themselves
|
|
@@ -471,7 +503,13 @@ contract MintingHub is IMintingHub, ERC165 {
|
|
|
471
503
|
returnPostponedCollateral(collateral, target, false);
|
|
472
504
|
}
|
|
473
505
|
|
|
474
|
-
function _returnCollateral(
|
|
506
|
+
function _returnCollateral(
|
|
507
|
+
IERC20 collateral,
|
|
508
|
+
address recipient,
|
|
509
|
+
uint256 amount,
|
|
510
|
+
bool postpone,
|
|
511
|
+
bool asNative
|
|
512
|
+
) internal {
|
|
475
513
|
if (postpone) {
|
|
476
514
|
// Postponing helps in case the challenger was blacklisted or otherwise cannot receive at the moment.
|
|
477
515
|
pendingReturns[address(collateral)][recipient] += amount;
|
|
@@ -508,7 +546,7 @@ contract MintingHub is IMintingHub, ERC165 {
|
|
|
508
546
|
} else if (timePassed < 2 * challengePeriod) {
|
|
509
547
|
// from 1x liquidation price to 0 in second phase
|
|
510
548
|
uint256 timeLeft = 2 * challengePeriod - timePassed;
|
|
511
|
-
return (liqprice
|
|
549
|
+
return (liqprice * timeLeft) / challengePeriod;
|
|
512
550
|
} else {
|
|
513
551
|
// get collateral for free after both phases passed
|
|
514
552
|
return 0;
|
|
@@ -527,7 +565,7 @@ contract MintingHub is IMintingHub, ERC165 {
|
|
|
527
565
|
* @param upToAmount Maximum amount of collateral to buy
|
|
528
566
|
* @param receiveAsNative If true and collateral is WcBTC, receive as native coin
|
|
529
567
|
*/
|
|
530
|
-
function buyExpiredCollateral(IPosition pos, uint256 upToAmount, bool receiveAsNative) public returns (uint256) {
|
|
568
|
+
function buyExpiredCollateral(IPosition pos, uint256 upToAmount, bool receiveAsNative) public validPos(address(pos)) returns (uint256) {
|
|
531
569
|
uint256 max = pos.collateral().balanceOf(address(pos));
|
|
532
570
|
uint256 amount = upToAmount > max ? max : upToAmount;
|
|
533
571
|
uint256 forceSalePrice = expiredPurchasePrice(pos);
|
|
@@ -566,10 +604,36 @@ contract MintingHub is IMintingHub, ERC165 {
|
|
|
566
604
|
/**
|
|
567
605
|
* @dev See {IERC165-supportsInterface}.
|
|
568
606
|
*/
|
|
569
|
-
function supportsInterface(bytes4 interfaceId) public view override
|
|
570
|
-
return
|
|
571
|
-
|
|
572
|
-
|
|
607
|
+
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
|
|
608
|
+
return interfaceId == type(IMintingHub).interfaceId || super.supportsInterface(interfaceId);
|
|
609
|
+
}
|
|
610
|
+
|
|
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);
|
|
573
637
|
}
|
|
574
638
|
|
|
575
639
|
/**
|
|
@@ -22,6 +22,15 @@ contract Position is Ownable, IPosition, MathUtil {
|
|
|
22
22
|
* the constant and immutable fields, but have their own values for the other fields.
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* @notice Maximum allowed message length for denial messages (prevents gas griefing attacks).
|
|
27
|
+
* @dev This constant is intentionally duplicated in MintingHub.sol for defense-in-depth.
|
|
28
|
+
* Both contracts validate independently since Position contracts are deployed separately
|
|
29
|
+
* and must enforce limits even if the hub validation is bypassed or modified.
|
|
30
|
+
* If changing this value, update MintingHub.MAX_MESSAGE_LENGTH as well.
|
|
31
|
+
*/
|
|
32
|
+
uint256 private constant MAX_MESSAGE_LENGTH = 500;
|
|
33
|
+
|
|
25
34
|
/**
|
|
26
35
|
* @notice The JUSD price per unit of the collateral below which challenges succeed, (36 - collateral.decimals) decimals
|
|
27
36
|
*/
|
|
@@ -126,6 +135,29 @@ contract Position is Ownable, IPosition, MathUtil {
|
|
|
126
135
|
event MintingUpdate(uint256 collateral, uint256 price, uint256 principal);
|
|
127
136
|
event PositionDenied(address indexed sender, string message); // emitted if closed by governance
|
|
128
137
|
|
|
138
|
+
/**
|
|
139
|
+
* @dev Emits MintingUpdate both locally and to the hub for centralized monitoring.
|
|
140
|
+
* This allows monitoring systems to track all position updates from a single address (the hub).
|
|
141
|
+
* @notice Adds ~3,800 gas overhead for the hub call.
|
|
142
|
+
*/
|
|
143
|
+
function _emitUpdate(uint256 _collateral, uint256 _price, uint256 _principal) internal {
|
|
144
|
+
emit MintingUpdate(_collateral, _price, _principal);
|
|
145
|
+
IMintingHub(hub).emitPositionUpdate(_collateral, _price, _principal);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* @dev Emits PositionDenied both locally and to the hub for centralized monitoring.
|
|
150
|
+
* @param sender The address that triggered the denial
|
|
151
|
+
* @param message Reason for denial (1-500 bytes, prevents gas griefing and ensures meaningful messages)
|
|
152
|
+
*/
|
|
153
|
+
function _emitDenied(address sender, string memory message) internal {
|
|
154
|
+
uint256 messageLength = bytes(message).length;
|
|
155
|
+
if (messageLength == 0) revert EmptyMessage();
|
|
156
|
+
if (messageLength > MAX_MESSAGE_LENGTH) revert MessageTooLong(messageLength, MAX_MESSAGE_LENGTH);
|
|
157
|
+
emit PositionDenied(sender, message);
|
|
158
|
+
IMintingHub(hub).emitPositionDenied(sender, message);
|
|
159
|
+
}
|
|
160
|
+
|
|
129
161
|
error InsufficientCollateral(uint256 needed, uint256 available);
|
|
130
162
|
error TooLate();
|
|
131
163
|
error RepaidTooMuch(uint256 excess);
|
|
@@ -140,6 +172,8 @@ contract Position is Ownable, IPosition, MathUtil {
|
|
|
140
172
|
error NotOriginal();
|
|
141
173
|
error InvalidExpiration();
|
|
142
174
|
error AlreadyInitialized();
|
|
175
|
+
error MessageTooLong(uint256 length, uint256 maxLength);
|
|
176
|
+
error EmptyMessage();
|
|
143
177
|
error PriceTooHigh(uint256 newPrice, uint256 maxPrice);
|
|
144
178
|
error InvalidPriceReference();
|
|
145
179
|
error NativeTransferFailed();
|
|
@@ -208,7 +242,7 @@ contract Position is Ownable, IPosition, MathUtil {
|
|
|
208
242
|
reserveContribution = _reservePPM;
|
|
209
243
|
minimumCollateral = _minCollateral;
|
|
210
244
|
challengePeriod = _challengePeriod;
|
|
211
|
-
start = uint40(block.timestamp) + _initPeriod; // at least
|
|
245
|
+
start = uint40(block.timestamp) + _initPeriod; // at least 14 days time to deny the position
|
|
212
246
|
cooldown = start;
|
|
213
247
|
expiration = start + _duration;
|
|
214
248
|
limit = _initialLimit;
|
|
@@ -222,7 +256,8 @@ contract Position is Ownable, IPosition, MathUtil {
|
|
|
222
256
|
*/
|
|
223
257
|
function initialize(address parent, uint40 _expiration) external onlyHub {
|
|
224
258
|
if (expiration != 0) revert AlreadyInitialized();
|
|
225
|
-
if (_expiration < block.timestamp || _expiration > Position(payable(original)).expiration())
|
|
259
|
+
if (_expiration < block.timestamp || _expiration > Position(payable(original)).expiration())
|
|
260
|
+
revert InvalidExpiration(); // expiration must not be later than original
|
|
226
261
|
expiration = _expiration;
|
|
227
262
|
price = Position(payable(parent)).price();
|
|
228
263
|
_fixRateToLeadrate(Position(payable(parent)).riskPremiumPPM());
|
|
@@ -282,7 +317,7 @@ contract Position is Ownable, IPosition, MathUtil {
|
|
|
282
317
|
if (block.timestamp >= start) revert TooLate();
|
|
283
318
|
IReserve(jusd.reserve()).checkQualified(msg.sender, helpers);
|
|
284
319
|
_close();
|
|
285
|
-
|
|
320
|
+
_emitDenied(msg.sender, message);
|
|
286
321
|
}
|
|
287
322
|
|
|
288
323
|
/**
|
|
@@ -322,7 +357,12 @@ contract Position is Ownable, IPosition, MathUtil {
|
|
|
322
357
|
* @param newPrice The new liquidation price
|
|
323
358
|
* @param withdrawAsNative If true, withdraw collateral as native coin instead of wrapped token
|
|
324
359
|
*/
|
|
325
|
-
function adjust(
|
|
360
|
+
function adjust(
|
|
361
|
+
uint256 newPrincipal,
|
|
362
|
+
uint256 newCollateral,
|
|
363
|
+
uint256 newPrice,
|
|
364
|
+
bool withdrawAsNative
|
|
365
|
+
) external payable onlyOwner {
|
|
326
366
|
_adjust(newPrincipal, newCollateral, newPrice, address(0), withdrawAsNative);
|
|
327
367
|
}
|
|
328
368
|
|
|
@@ -337,7 +377,13 @@ contract Position is Ownable, IPosition, MathUtil {
|
|
|
337
377
|
* @param referencePosition Reference position for cooldown-free price increase (address(0) for normal logic with cooldown)
|
|
338
378
|
* @param withdrawAsNative If true, withdraw collateral as native coin instead of wrapped token
|
|
339
379
|
*/
|
|
340
|
-
function adjustWithReference(
|
|
380
|
+
function adjustWithReference(
|
|
381
|
+
uint256 newPrincipal,
|
|
382
|
+
uint256 newCollateral,
|
|
383
|
+
uint256 newPrice,
|
|
384
|
+
address referencePosition,
|
|
385
|
+
bool withdrawAsNative
|
|
386
|
+
) external payable onlyOwner {
|
|
341
387
|
_adjust(newPrincipal, newCollateral, newPrice, referencePosition, withdrawAsNative);
|
|
342
388
|
}
|
|
343
389
|
|
|
@@ -349,7 +395,13 @@ contract Position is Ownable, IPosition, MathUtil {
|
|
|
349
395
|
* @param referencePosition Reference position for cooldown-free price increase (address(0) for normal logic)
|
|
350
396
|
* @param withdrawAsNative If true and withdrawing collateral, unwrap to native coin
|
|
351
397
|
*/
|
|
352
|
-
function _adjust(
|
|
398
|
+
function _adjust(
|
|
399
|
+
uint256 newPrincipal,
|
|
400
|
+
uint256 newCollateral,
|
|
401
|
+
uint256 newPrice,
|
|
402
|
+
address referencePosition,
|
|
403
|
+
bool withdrawAsNative
|
|
404
|
+
) internal {
|
|
353
405
|
// Handle native coin deposit first (wraps to WCBTC)
|
|
354
406
|
if (msg.value > 0) {
|
|
355
407
|
IWrappedNative(address(collateral)).deposit{value: msg.value}();
|
|
@@ -378,7 +430,7 @@ contract Position is Ownable, IPosition, MathUtil {
|
|
|
378
430
|
if (newPrice != price) {
|
|
379
431
|
_adjustPrice(newPrice, referencePosition);
|
|
380
432
|
}
|
|
381
|
-
|
|
433
|
+
_emitUpdate(newCollateral, newPrice, newPrincipal);
|
|
382
434
|
}
|
|
383
435
|
|
|
384
436
|
/**
|
|
@@ -389,7 +441,7 @@ contract Position is Ownable, IPosition, MathUtil {
|
|
|
389
441
|
*/
|
|
390
442
|
function adjustPrice(uint256 newPrice) public onlyOwner {
|
|
391
443
|
_adjustPrice(newPrice, address(0));
|
|
392
|
-
|
|
444
|
+
_emitUpdate(_collateralBalance(), price, principal);
|
|
393
445
|
}
|
|
394
446
|
|
|
395
447
|
/**
|
|
@@ -403,7 +455,7 @@ contract Position is Ownable, IPosition, MathUtil {
|
|
|
403
455
|
*/
|
|
404
456
|
function adjustPriceWithReference(uint256 newPrice, address referencePosition) external onlyOwner {
|
|
405
457
|
_adjustPrice(newPrice, referencePosition);
|
|
406
|
-
|
|
458
|
+
_emitUpdate(_collateralBalance(), price, principal);
|
|
407
459
|
}
|
|
408
460
|
|
|
409
461
|
/**
|
|
@@ -499,7 +551,7 @@ contract Position is Ownable, IPosition, MathUtil {
|
|
|
499
551
|
function mint(address target, uint256 amount) public ownerOrRoller {
|
|
500
552
|
uint256 collateralBalance = _collateralBalance();
|
|
501
553
|
_mint(target, amount, collateralBalance);
|
|
502
|
-
|
|
554
|
+
_emitUpdate(collateralBalance, price, principal);
|
|
503
555
|
}
|
|
504
556
|
|
|
505
557
|
/**
|
|
@@ -511,7 +563,7 @@ contract Position is Ownable, IPosition, MathUtil {
|
|
|
511
563
|
|
|
512
564
|
/**
|
|
513
565
|
* @notice Computes the virtual price of the collateral in JUSD, which is the minimum collateral
|
|
514
|
-
* price required to cover the entire debt with interest overcollateralization, lower bounded by the floor price.
|
|
566
|
+
* price required to cover the entire debt with interest overcollateralization, lower bounded by the floor price.
|
|
515
567
|
* Returns the challenged price if a challenge is active.
|
|
516
568
|
* @param colBalance The collateral balance of the position.
|
|
517
569
|
* @param floorPrice The minimum price of the collateral in JUSD.
|
|
@@ -519,9 +571,9 @@ contract Position is Ownable, IPosition, MathUtil {
|
|
|
519
571
|
function _virtualPrice(uint256 colBalance, uint256 floorPrice) internal view returns (uint256) {
|
|
520
572
|
if (challengedAmount > 0) return challengedPrice;
|
|
521
573
|
if (colBalance == 0) return floorPrice;
|
|
522
|
-
|
|
574
|
+
|
|
523
575
|
uint256 virtPrice = (_getCollateralRequirement() * ONE_DEC18) / colBalance;
|
|
524
|
-
return virtPrice < floorPrice ? floorPrice: virtPrice;
|
|
576
|
+
return virtPrice < floorPrice ? floorPrice : virtPrice;
|
|
525
577
|
}
|
|
526
578
|
|
|
527
579
|
/**
|
|
@@ -564,8 +616,9 @@ contract Position is Ownable, IPosition, MathUtil {
|
|
|
564
616
|
// Single-division optimization for gas efficiency and precision. Equivalent formula:
|
|
565
617
|
// uint256 usablePrincipal = (principal * (1_000_000 - reserveContribution)) / 1_000_000;
|
|
566
618
|
// newInterest += (usablePrincipal * fixedAnnualRatePPM * delta) / (365 days * 1_000_000);
|
|
567
|
-
newInterest +=
|
|
568
|
-
|
|
619
|
+
newInterest +=
|
|
620
|
+
(principal * (1_000_000 - reserveContribution) * fixedAnnualRatePPM * delta) /
|
|
621
|
+
(365 days * 1_000_000 * 1_000_000);
|
|
569
622
|
}
|
|
570
623
|
|
|
571
624
|
return newInterest;
|
|
@@ -594,7 +647,7 @@ contract Position is Ownable, IPosition, MathUtil {
|
|
|
594
647
|
function getDebt() public view returns (uint256) {
|
|
595
648
|
return _getDebt();
|
|
596
649
|
}
|
|
597
|
-
|
|
650
|
+
|
|
598
651
|
/**
|
|
599
652
|
* @notice Public function to calculate current debt with overcollateralized interest
|
|
600
653
|
* @return The total debt including overcollateralized interest
|
|
@@ -656,7 +709,7 @@ contract Position is Ownable, IPosition, MathUtil {
|
|
|
656
709
|
*/
|
|
657
710
|
function repay(uint256 amount) public returns (uint256) {
|
|
658
711
|
uint256 used = _payDownDebt(amount);
|
|
659
|
-
|
|
712
|
+
_emitUpdate(_collateralBalance(), price, principal);
|
|
660
713
|
return used;
|
|
661
714
|
}
|
|
662
715
|
|
|
@@ -713,7 +766,7 @@ contract Position is Ownable, IPosition, MathUtil {
|
|
|
713
766
|
if (proceeds > 0) {
|
|
714
767
|
jusd.transferFrom(buyer, owner(), proceeds);
|
|
715
768
|
}
|
|
716
|
-
|
|
769
|
+
_emitUpdate(_collateralBalance(), price, principal);
|
|
717
770
|
return;
|
|
718
771
|
}
|
|
719
772
|
|
|
@@ -736,7 +789,7 @@ contract Position is Ownable, IPosition, MathUtil {
|
|
|
736
789
|
jusd.transferFrom(buyer, owner(), proceeds);
|
|
737
790
|
}
|
|
738
791
|
|
|
739
|
-
|
|
792
|
+
_emitUpdate(_collateralBalance(), price, principal);
|
|
740
793
|
}
|
|
741
794
|
|
|
742
795
|
/**
|
|
@@ -762,7 +815,7 @@ contract Position is Ownable, IPosition, MathUtil {
|
|
|
762
815
|
*/
|
|
763
816
|
function withdrawCollateral(address target, uint256 amount) public ownerOrRoller {
|
|
764
817
|
uint256 balance = _withdrawCollateral(target, amount);
|
|
765
|
-
|
|
818
|
+
_emitUpdate(balance, price, principal);
|
|
766
819
|
}
|
|
767
820
|
|
|
768
821
|
/**
|
|
@@ -773,7 +826,7 @@ contract Position is Ownable, IPosition, MathUtil {
|
|
|
773
826
|
*/
|
|
774
827
|
function withdrawCollateralAsNative(address target, uint256 amount) public onlyOwner {
|
|
775
828
|
uint256 balance = _withdrawCollateralAsNative(target, amount);
|
|
776
|
-
|
|
829
|
+
_emitUpdate(balance, price, principal);
|
|
777
830
|
}
|
|
778
831
|
|
|
779
832
|
function _withdrawCollateral(address target, uint256 amount) internal noCooldown noChallenge returns (uint256) {
|
|
@@ -786,7 +839,10 @@ contract Position is Ownable, IPosition, MathUtil {
|
|
|
786
839
|
* @dev Internal helper for native coin withdrawal. Used by withdrawCollateralAsNative() and _adjust().
|
|
787
840
|
* Does NOT emit MintingUpdate - callers are responsible for emitting.
|
|
788
841
|
*/
|
|
789
|
-
function _withdrawCollateralAsNative(
|
|
842
|
+
function _withdrawCollateralAsNative(
|
|
843
|
+
address target,
|
|
844
|
+
uint256 amount
|
|
845
|
+
) internal noCooldown noChallenge returns (uint256) {
|
|
790
846
|
if (amount > 0) {
|
|
791
847
|
IWrappedNative(address(collateral)).withdraw(amount);
|
|
792
848
|
(bool success, ) = target.call{value: amount}("");
|
|
@@ -807,7 +863,7 @@ contract Position is Ownable, IPosition, MathUtil {
|
|
|
807
863
|
*/
|
|
808
864
|
function transferChallengedCollateral(address target, uint256 amount) external onlyHub {
|
|
809
865
|
uint256 newBalance = _sendCollateral(target, amount);
|
|
810
|
-
|
|
866
|
+
_emitUpdate(newBalance, price, principal);
|
|
811
867
|
}
|
|
812
868
|
|
|
813
869
|
function _sendCollateral(address target, uint256 amount) internal returns (uint256) {
|
|
@@ -830,7 +886,7 @@ contract Position is Ownable, IPosition, MathUtil {
|
|
|
830
886
|
function _checkCollateral(uint256 collateralReserve, uint256 atPrice) internal view {
|
|
831
887
|
uint256 relevantCollateral = collateralReserve < minimumCollateral ? 0 : collateralReserve;
|
|
832
888
|
uint256 collateralRequirement = _getCollateralRequirement();
|
|
833
|
-
|
|
889
|
+
|
|
834
890
|
if (relevantCollateral * atPrice < collateralRequirement * ONE_DEC18) {
|
|
835
891
|
revert InsufficientCollateral(relevantCollateral * atPrice, collateralRequirement * ONE_DEC18);
|
|
836
892
|
}
|
|
@@ -6,12 +6,22 @@ import {IPosition} from "./IPosition.sol";
|
|
|
6
6
|
import {PositionRoller} from "../PositionRoller.sol";
|
|
7
7
|
|
|
8
8
|
interface IMintingHub {
|
|
9
|
+
// Events for centralized position monitoring (emitted by hub, not individual positions)
|
|
10
|
+
event PositionUpdate(address indexed position, uint256 collateral, uint256 price, uint256 principal);
|
|
11
|
+
|
|
12
|
+
event PositionDeniedByGovernance(address indexed position, address indexed denier, string message);
|
|
13
|
+
|
|
9
14
|
function RATE() external view returns (ILeadrate);
|
|
10
15
|
|
|
11
16
|
function ROLLER() external view returns (PositionRoller);
|
|
12
17
|
|
|
13
18
|
function WCBTC() external view returns (address);
|
|
14
19
|
|
|
20
|
+
// Position event forwarding functions
|
|
21
|
+
function emitPositionUpdate(uint256 collateral, uint256 price, uint256 principal) external;
|
|
22
|
+
|
|
23
|
+
function emitPositionDenied(address denier, string calldata message) external;
|
|
24
|
+
|
|
15
25
|
function challenge(
|
|
16
26
|
address _positionAddr,
|
|
17
27
|
uint256 _collateralAmount,
|
|
@@ -20,7 +30,12 @@ interface IMintingHub {
|
|
|
20
30
|
|
|
21
31
|
function bid(uint32 _challengeNumber, uint256 size, bool postponeCollateralReturn) external;
|
|
22
32
|
|
|
23
|
-
function bid(
|
|
33
|
+
function bid(
|
|
34
|
+
uint32 _challengeNumber,
|
|
35
|
+
uint256 size,
|
|
36
|
+
bool postponeCollateralReturn,
|
|
37
|
+
bool returnCollateralAsNative
|
|
38
|
+
) external;
|
|
24
39
|
|
|
25
40
|
function returnPostponedCollateral(address collateral, address target) external;
|
|
26
41
|
|
|
@@ -30,5 +45,12 @@ interface IMintingHub {
|
|
|
30
45
|
|
|
31
46
|
function buyExpiredCollateral(IPosition pos, uint256 upToAmount, bool receiveAsNative) external returns (uint256);
|
|
32
47
|
|
|
33
|
-
function clone(
|
|
48
|
+
function clone(
|
|
49
|
+
address owner,
|
|
50
|
+
address parent,
|
|
51
|
+
uint256 _initialCollateral,
|
|
52
|
+
uint256 _initialMint,
|
|
53
|
+
uint40 expiration,
|
|
54
|
+
uint256 _liqPrice
|
|
55
|
+
) external payable returns (address);
|
|
34
56
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
pragma solidity ^0.8.20;
|
|
3
3
|
|
|
4
|
-
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
|
|
5
4
|
import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol";
|
|
6
5
|
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
|
7
6
|
import {ERC4626, ERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol";
|
|
@@ -11,18 +10,19 @@ import {ISavingsJUSD} from "./interface/ISavingsJUSD.sol";
|
|
|
11
10
|
/**
|
|
12
11
|
* @title SavingsVaultJUSD
|
|
13
12
|
* @notice ERC-4626-compatible vault adapter for the JUSD Savings module.
|
|
14
|
-
* This vault tracks interest-bearing deposits
|
|
15
|
-
*
|
|
13
|
+
* This vault tracks interest-bearing deposits where share value increases over time
|
|
14
|
+
* as interest accrues in the underlying SAVINGS contract.
|
|
16
15
|
*
|
|
17
|
-
* @dev
|
|
18
|
-
*
|
|
19
|
-
*
|
|
16
|
+
* @dev Inherits OpenZeppelin's ERC4626 virtual shares pattern for inflation attack mitigation.
|
|
17
|
+
* The base _convertToShares/_convertToAssets functions add virtual shares (+10**_decimalsOffset())
|
|
18
|
+
* and virtual assets (+1) to prevent first-depositor manipulation attacks.
|
|
20
19
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
20
|
+
* Only totalAssets() is overridden to return the vault's balance in the SAVINGS contract
|
|
21
|
+
* plus any accrued interest. The base conversion functions use this through polymorphism.
|
|
22
|
+
*
|
|
23
|
+
* Interest is recognized through `_accrueInterest()` calls during deposits/withdrawals.
|
|
23
24
|
*/
|
|
24
25
|
contract SavingsVaultJUSD is ERC4626 {
|
|
25
|
-
using Math for uint256;
|
|
26
26
|
using SafeCast for uint256;
|
|
27
27
|
|
|
28
28
|
ISavingsJUSD public immutable SAVINGS;
|
|
@@ -30,6 +30,8 @@ contract SavingsVaultJUSD is ERC4626 {
|
|
|
30
30
|
|
|
31
31
|
event InterestClaimed(uint256 interest, uint256 totalClaimed);
|
|
32
32
|
|
|
33
|
+
error ZeroShares();
|
|
34
|
+
|
|
33
35
|
constructor(
|
|
34
36
|
IERC20 _coin,
|
|
35
37
|
ISavingsJUSD _savings,
|
|
@@ -50,11 +52,9 @@ contract SavingsVaultJUSD is ERC4626 {
|
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
/// @notice Returns the current price per share of the contract
|
|
53
|
-
/// @dev
|
|
55
|
+
/// @dev Uses virtual shares pattern for consistency with _convertToShares/_convertToAssets
|
|
54
56
|
function price() public view returns (uint256) {
|
|
55
|
-
|
|
56
|
-
if (totalShares == 0) return 1 ether;
|
|
57
|
-
return (totalAssets() * 1 ether) / totalShares;
|
|
57
|
+
return ((totalAssets() + 1) * 1 ether) / (totalSupply() + 10 ** _decimalsOffset());
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
/// @notice Calculates the accrued interest for this contract
|
|
@@ -69,17 +69,10 @@ contract SavingsVaultJUSD is ERC4626 {
|
|
|
69
69
|
return SAVINGS.savings(address(this)).saved + _interest();
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
function _convertToShares(uint256 assets, Math.Rounding rounding) internal view virtual override returns (uint256) {
|
|
73
|
-
return assets.mulDiv(1 ether, price(), rounding);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function _convertToAssets(uint256 shares, Math.Rounding rounding) internal view virtual override returns (uint256) {
|
|
77
|
-
return shares.mulDiv(price(), 1 ether, rounding);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
72
|
// ---------------------------------------------------------------------------------------
|
|
81
73
|
|
|
82
74
|
function _deposit(address caller, address receiver, uint256 assets, uint256 shares) internal virtual override {
|
|
75
|
+
if (shares == 0) revert ZeroShares();
|
|
83
76
|
_accrueInterest();
|
|
84
77
|
|
|
85
78
|
// If _asset is ERC-777, `transferFrom` can trigger a reentrancy BEFORE the transfer happens through the
|