@juicedollar/jusd 1.0.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/LICENSE +21 -0
- package/README.md +356 -0
- package/contracts/Equity.sol +457 -0
- package/contracts/JuiceDollar.sol +363 -0
- package/contracts/Leadrate.sol +79 -0
- package/contracts/MintingHubV2/MintingHub.sol +445 -0
- package/contracts/MintingHubV2/Position.sol +810 -0
- package/contracts/MintingHubV2/PositionFactory.sol +69 -0
- package/contracts/MintingHubV2/PositionRoller.sol +159 -0
- package/contracts/MintingHubV2/interface/IMintingHub.sol +26 -0
- package/contracts/MintingHubV2/interface/IPosition.sol +90 -0
- package/contracts/MintingHubV2/interface/IPositionFactory.sol +20 -0
- package/contracts/Savings.sol +141 -0
- package/contracts/SavingsVaultJUSD.sol +140 -0
- package/contracts/StablecoinBridge.sol +109 -0
- package/contracts/StartUSD.sol +16 -0
- package/contracts/gateway/CoinLendingGateway.sol +223 -0
- package/contracts/gateway/FrontendGateway.sol +224 -0
- package/contracts/gateway/MintingHubGateway.sol +87 -0
- package/contracts/gateway/SavingsGateway.sol +51 -0
- package/contracts/gateway/interface/ICoinLendingGateway.sol +73 -0
- package/contracts/gateway/interface/IFrontendGateway.sol +49 -0
- package/contracts/gateway/interface/IMintingHubGateway.sol +12 -0
- package/contracts/impl/ERC3009.sol +171 -0
- package/contracts/interface/IJuiceDollar.sol +54 -0
- package/contracts/interface/ILeadrate.sol +7 -0
- package/contracts/interface/IReserve.sol +9 -0
- package/contracts/interface/ISavingsJUSD.sol +49 -0
- package/contracts/test/FreakToken.sol +25 -0
- package/contracts/test/Math.sol +339 -0
- package/contracts/test/MockEquity.sol +15 -0
- package/contracts/test/PositionExpirationTest.sol +75 -0
- package/contracts/test/PositionRollingTest.sol +65 -0
- package/contracts/test/TestFlashLoan.sol +84 -0
- package/contracts/test/TestFlashLoanGateway.sol +49 -0
- package/contracts/test/TestMathUtil.sol +40 -0
- package/contracts/test/TestToken.sol +45 -0
- package/contracts/test/TestWcBTC.sol +35 -0
- package/contracts/utils/MathUtil.sol +61 -0
- package/dist/index.d.mts +8761 -0
- package/dist/index.d.ts +8761 -0
- package/dist/index.js +11119 -0
- package/dist/index.mjs +11073 -0
- package/exports/abis/MintingHubV2/PositionFactoryV2.ts +90 -0
- package/exports/abis/MintingHubV2/PositionRoller.ts +183 -0
- package/exports/abis/MintingHubV2/PositionV2.ts +999 -0
- package/exports/abis/core/CoinLendingGateway.ts +427 -0
- package/exports/abis/core/Equity.ts +1286 -0
- package/exports/abis/core/FrontendGateway.ts +906 -0
- package/exports/abis/core/JuiceDollar.ts +1366 -0
- package/exports/abis/core/MintingHubGateway.ts +865 -0
- package/exports/abis/core/SavingsGateway.ts +559 -0
- package/exports/abis/core/SavingsVaultJUSD.ts +920 -0
- package/exports/abis/utils/ERC20.ts +310 -0
- package/exports/abis/utils/ERC20PermitLight.ts +520 -0
- package/exports/abis/utils/Leadrate.ts +175 -0
- package/exports/abis/utils/MintingHubV2.ts +682 -0
- package/exports/abis/utils/Ownable.ts +76 -0
- package/exports/abis/utils/Savings.ts +453 -0
- package/exports/abis/utils/StablecoinBridge.ts +209 -0
- package/exports/abis/utils/StartUSD.ts +315 -0
- package/exports/abis/utils/UniswapV3Pool.ts +638 -0
- package/exports/address.config.ts +48 -0
- package/exports/index.ts +28 -0
- package/package.json +87 -0
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.0;
|
|
3
|
+
|
|
4
|
+
import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol";
|
|
5
|
+
import {IJuiceDollar} from "../interface/IJuiceDollar.sol";
|
|
6
|
+
import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
|
|
7
|
+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
8
|
+
import {ILeadrate} from "../interface/ILeadrate.sol";
|
|
9
|
+
import {IMintingHub} from "./interface/IMintingHub.sol";
|
|
10
|
+
import {IPositionFactory} from "./interface/IPositionFactory.sol";
|
|
11
|
+
import {IPosition} from "./interface/IPosition.sol";
|
|
12
|
+
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
|
|
13
|
+
import {PositionRoller} from "./PositionRoller.sol";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @title Minting Hub
|
|
17
|
+
* @notice The central hub for creating, cloning, and challenging collateralized JuiceDollar positions.
|
|
18
|
+
* @dev Only one instance of this contract is required, whereas every new position comes with a new position
|
|
19
|
+
* contract. Pending challenges are stored as structs in an array.
|
|
20
|
+
*/
|
|
21
|
+
contract MintingHub is IMintingHub, ERC165 {
|
|
22
|
+
/**
|
|
23
|
+
* @notice Irrevocable fee in JUSD when proposing a new position (but not when cloning an existing one).
|
|
24
|
+
*/
|
|
25
|
+
uint256 public constant OPENING_FEE = 1000 * 10 ** 18;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @notice The challenger reward in parts per million (ppm) relative to the challenged amount, whereas
|
|
29
|
+
* challenged amount is defined as the challenged collateral amount times the liquidation price.
|
|
30
|
+
*/
|
|
31
|
+
uint256 public constant CHALLENGER_REWARD = 20000; // 2%
|
|
32
|
+
uint256 public constant EXPIRED_PRICE_FACTOR = 10;
|
|
33
|
+
|
|
34
|
+
IPositionFactory private immutable POSITION_FACTORY; // position contract to clone
|
|
35
|
+
|
|
36
|
+
IJuiceDollar public immutable JUSD; // currency
|
|
37
|
+
PositionRoller public immutable ROLLER; // helper to roll positions
|
|
38
|
+
ILeadrate public immutable RATE; // to determine the interest rate
|
|
39
|
+
|
|
40
|
+
Challenge[] public challenges; // list of open challenges
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @notice Map to remember pending postponed collateral returns.
|
|
44
|
+
* @dev It maps collateral => beneficiary => amount.
|
|
45
|
+
*/
|
|
46
|
+
mapping(address collateral => mapping(address owner => uint256 amount)) public pendingReturns;
|
|
47
|
+
|
|
48
|
+
struct Challenge {
|
|
49
|
+
address challenger; // the address from which the challenge was initiated
|
|
50
|
+
uint40 start; // the start of the challenge
|
|
51
|
+
IPosition position; // the position that was challenged
|
|
52
|
+
uint256 size; // how much collateral the challenger provided
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
event PositionOpened(address indexed owner, address indexed position, address original, address collateral);
|
|
56
|
+
event ChallengeStarted(address indexed challenger, address indexed position, uint256 size, uint256 number);
|
|
57
|
+
event ChallengeAverted(address indexed position, uint256 number, uint256 size);
|
|
58
|
+
event ChallengeSucceeded(
|
|
59
|
+
address indexed position,
|
|
60
|
+
uint256 number,
|
|
61
|
+
uint256 bid,
|
|
62
|
+
uint256 acquiredCollateral,
|
|
63
|
+
uint256 challengeSize
|
|
64
|
+
);
|
|
65
|
+
event PostponedReturn(address collateral, address indexed beneficiary, uint256 amount);
|
|
66
|
+
event ForcedSale(address pos, uint256 amount, uint256 priceE36MinusDecimals);
|
|
67
|
+
|
|
68
|
+
error UnexpectedPrice();
|
|
69
|
+
error InvalidPos();
|
|
70
|
+
error IncompatibleCollateral();
|
|
71
|
+
error InsufficientCollateral();
|
|
72
|
+
error LeaveNoDust(uint256 amount);
|
|
73
|
+
error InvalidRiskPremium();
|
|
74
|
+
error InvalidReservePPM();
|
|
75
|
+
error InvalidCollateralDecimals();
|
|
76
|
+
error ChallengeTimeTooShort();
|
|
77
|
+
error InitPeriodTooShort();
|
|
78
|
+
|
|
79
|
+
modifier validPos(address position) {
|
|
80
|
+
if (JUSD.getPositionParent(position) != address(this)) revert InvalidPos();
|
|
81
|
+
_;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
constructor(address _jusd, address _leadrate, address _roller, address _factory) {
|
|
85
|
+
JUSD = IJuiceDollar(_jusd);
|
|
86
|
+
RATE = ILeadrate(_leadrate);
|
|
87
|
+
POSITION_FACTORY = IPositionFactory(_factory);
|
|
88
|
+
ROLLER = PositionRoller(_roller);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* @notice Open a collateralized loan position. See also https://docs.JUSD.com/positions/open .
|
|
93
|
+
* @dev For a successful call, you must set an allowance for the collateral token, allowing
|
|
94
|
+
* the minting hub to transfer the initial collateral amount to the newly created position and to
|
|
95
|
+
* withdraw the fees.
|
|
96
|
+
*
|
|
97
|
+
* @param _collateralAddress address of collateral token
|
|
98
|
+
* @param _minCollateral minimum collateral required to prevent dust amounts
|
|
99
|
+
* @param _initialCollateral amount of initial collateral to be deposited
|
|
100
|
+
* @param _mintingMaximum maximal amount of JUSD that can be minted by the position owner
|
|
101
|
+
* @param _initPeriodSeconds initial period in seconds
|
|
102
|
+
* @param _expirationSeconds position tenor in seconds from 'now'
|
|
103
|
+
* @param _challengeSeconds challenge period. Longer for less liquid collateral.
|
|
104
|
+
* @param _riskPremium ppm of minted amount that is added to the applicable minting fee as a risk premium
|
|
105
|
+
* @param _liqPrice Liquidation price with (36 - token decimals) decimals,
|
|
106
|
+
* e.g. 18 decimals for an 18 dec collateral, 36 decs for a 0 dec collateral.
|
|
107
|
+
* @param _reservePPM ppm of minted amount that is locked as borrower's reserve, e.g. 20%
|
|
108
|
+
* @return address address of created position
|
|
109
|
+
*/
|
|
110
|
+
function openPosition(
|
|
111
|
+
address _collateralAddress,
|
|
112
|
+
uint256 _minCollateral,
|
|
113
|
+
uint256 _initialCollateral,
|
|
114
|
+
uint256 _mintingMaximum,
|
|
115
|
+
uint40 _initPeriodSeconds,
|
|
116
|
+
uint40 _expirationSeconds,
|
|
117
|
+
uint40 _challengeSeconds,
|
|
118
|
+
uint24 _riskPremium,
|
|
119
|
+
uint256 _liqPrice,
|
|
120
|
+
uint24 _reservePPM
|
|
121
|
+
) public returns (address) {
|
|
122
|
+
{
|
|
123
|
+
if (_riskPremium > 1_000_000) revert InvalidRiskPremium();
|
|
124
|
+
if (CHALLENGER_REWARD > _reservePPM || _reservePPM > 1_000_000) revert InvalidReservePPM();
|
|
125
|
+
if (IERC20Metadata(_collateralAddress).decimals() > 24) revert InvalidCollateralDecimals(); // leaves 12 digits for price
|
|
126
|
+
if (_challengeSeconds < 1 days) revert ChallengeTimeTooShort();
|
|
127
|
+
if (_initPeriodSeconds < 6 hours) revert InitPeriodTooShort();
|
|
128
|
+
uint256 invalidAmount = IERC20(_collateralAddress).totalSupply() + 1;
|
|
129
|
+
// TODO: Improve for older tokens that revert with assert,
|
|
130
|
+
// which consumes all gas and makes the entire tx fail (uncatchable)
|
|
131
|
+
try IERC20(_collateralAddress).transfer(address(0x123), invalidAmount) {
|
|
132
|
+
revert IncompatibleCollateral(); // we need a collateral that reverts on failed transfers
|
|
133
|
+
} catch Error(string memory /*reason*/) {} catch Panic(uint /*errorCode*/) {} catch (
|
|
134
|
+
bytes memory /*lowLevelData*/
|
|
135
|
+
) {}
|
|
136
|
+
if (_initialCollateral < _minCollateral) revert InsufficientCollateral();
|
|
137
|
+
// must start with at least 5000 JUSD worth of collateral
|
|
138
|
+
if (_minCollateral * _liqPrice < 5000 ether * 10 ** 18) revert InsufficientCollateral();
|
|
139
|
+
}
|
|
140
|
+
IPosition pos = IPosition(
|
|
141
|
+
POSITION_FACTORY.createNewPosition(
|
|
142
|
+
msg.sender,
|
|
143
|
+
address(JUSD),
|
|
144
|
+
_collateralAddress,
|
|
145
|
+
_minCollateral,
|
|
146
|
+
_mintingMaximum,
|
|
147
|
+
_initPeriodSeconds,
|
|
148
|
+
_expirationSeconds,
|
|
149
|
+
_challengeSeconds,
|
|
150
|
+
_riskPremium,
|
|
151
|
+
_liqPrice,
|
|
152
|
+
_reservePPM
|
|
153
|
+
)
|
|
154
|
+
);
|
|
155
|
+
JUSD.registerPosition(address(pos));
|
|
156
|
+
JUSD.collectProfits(msg.sender, OPENING_FEE);
|
|
157
|
+
IERC20(_collateralAddress).transferFrom(msg.sender, address(pos), _initialCollateral); // TODO: Use SafeERC20
|
|
158
|
+
|
|
159
|
+
emit PositionOpened(msg.sender, address(pos), address(pos), _collateralAddress);
|
|
160
|
+
return address(pos);
|
|
161
|
+
}
|
|
162
|
+
|
|
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
|
+
/**
|
|
173
|
+
* @notice Clones an existing position and immediately tries to mint the specified amount using the given collateral.
|
|
174
|
+
* @dev This needs an allowance to be set on the collateral contract such that the minting hub can get the collateral.
|
|
175
|
+
*/
|
|
176
|
+
function clone(
|
|
177
|
+
address owner,
|
|
178
|
+
address parent,
|
|
179
|
+
uint256 _initialCollateral,
|
|
180
|
+
uint256 _initialMint,
|
|
181
|
+
uint40 expiration
|
|
182
|
+
) public validPos(parent) returns (address) {
|
|
183
|
+
address pos = POSITION_FACTORY.clonePosition(parent);
|
|
184
|
+
IPosition child = IPosition(pos);
|
|
185
|
+
child.initialize(parent, expiration);
|
|
186
|
+
JUSD.registerPosition(pos);
|
|
187
|
+
IERC20 collateral = child.collateral();
|
|
188
|
+
if (_initialCollateral < child.minimumCollateral()) revert InsufficientCollateral();
|
|
189
|
+
collateral.transferFrom(msg.sender, pos, _initialCollateral); // collateral must still come from sender for security
|
|
190
|
+
emit PositionOpened(owner, address(pos), parent, address(collateral));
|
|
191
|
+
child.mint(owner, _initialMint);
|
|
192
|
+
Ownable(address(child)).transferOwnership(owner);
|
|
193
|
+
return address(pos);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* @notice Launch a challenge (Dutch auction) on a position
|
|
198
|
+
* @param _positionAddr address of the position we want to challenge
|
|
199
|
+
* @param _collateralAmount amount of the collateral we want to challenge
|
|
200
|
+
* @param minimumPrice guards against the minter front-running with a price change
|
|
201
|
+
* @return index of the challenge in the challenge-array
|
|
202
|
+
*/
|
|
203
|
+
function challenge(
|
|
204
|
+
address _positionAddr,
|
|
205
|
+
uint256 _collateralAmount,
|
|
206
|
+
uint256 minimumPrice
|
|
207
|
+
) external validPos(_positionAddr) returns (uint256) {
|
|
208
|
+
IPosition position = IPosition(_positionAddr);
|
|
209
|
+
// challenger should be ok if front-run by owner with a higher price
|
|
210
|
+
// in case owner front-runs challenger with small price decrease to prevent challenge,
|
|
211
|
+
// the challenger should set minimumPrice to market price
|
|
212
|
+
uint256 liqPrice = position.virtualPrice();
|
|
213
|
+
if (liqPrice < minimumPrice) revert UnexpectedPrice();
|
|
214
|
+
IERC20(position.collateral()).transferFrom(msg.sender, address(this), _collateralAmount);
|
|
215
|
+
uint256 pos = challenges.length;
|
|
216
|
+
challenges.push(Challenge(msg.sender, uint40(block.timestamp), position, _collateralAmount));
|
|
217
|
+
position.notifyChallengeStarted(_collateralAmount, liqPrice);
|
|
218
|
+
emit ChallengeStarted(msg.sender, address(position), _collateralAmount, pos);
|
|
219
|
+
return pos;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* @notice Post a bid in JUSD given an open challenge.
|
|
224
|
+
*
|
|
225
|
+
* @dev In case that the collateral cannot be transferred back to the challenger (i.e. because the collateral token
|
|
226
|
+
* has a blacklist and the challenger is on it), it is possible to postpone the return of the collateral.
|
|
227
|
+
*
|
|
228
|
+
* @param _challengeNumber index of the challenge as broadcast in the event
|
|
229
|
+
* @param size how much of the collateral the caller wants to bid for at most
|
|
230
|
+
* (automatically reduced to the available amount)
|
|
231
|
+
* @param postponeCollateralReturn To postpone the return of the collateral to the challenger. Usually false.
|
|
232
|
+
*/
|
|
233
|
+
function bid(uint32 _challengeNumber, uint256 size, bool postponeCollateralReturn) external {
|
|
234
|
+
Challenge memory _challenge = challenges[_challengeNumber];
|
|
235
|
+
(uint256 liqPrice, uint40 phase) = _challenge.position.challengeData();
|
|
236
|
+
size = _challenge.size < size ? _challenge.size : size; // cannot bid for more than the size of the challenge
|
|
237
|
+
|
|
238
|
+
if (block.timestamp <= _challenge.start + phase) {
|
|
239
|
+
_avertChallenge(_challenge, _challengeNumber, liqPrice, size);
|
|
240
|
+
emit ChallengeAverted(address(_challenge.position), _challengeNumber, size);
|
|
241
|
+
} else {
|
|
242
|
+
_returnChallengerCollateral(_challenge, _challengeNumber, size, postponeCollateralReturn);
|
|
243
|
+
(uint256 transferredCollateral, uint256 offer) = _finishChallenge(_challenge, size);
|
|
244
|
+
emit ChallengeSucceeded(address(_challenge.position), _challengeNumber, offer, transferredCollateral, size);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function _finishChallenge(
|
|
249
|
+
Challenge memory _challenge,
|
|
250
|
+
uint256 size
|
|
251
|
+
) internal returns (uint256, uint256) {
|
|
252
|
+
// Repayments depend on what was actually minted, whereas bids depend on the available collateral
|
|
253
|
+
(address owner, uint256 collateral, uint256 repayment, uint256 interest, uint32 reservePPM) = _challenge
|
|
254
|
+
.position
|
|
255
|
+
.notifyChallengeSucceeded(size);
|
|
256
|
+
|
|
257
|
+
// No overflow possible thanks to invariant (col * price <= limit * 10**18)
|
|
258
|
+
// enforced in Position.setPrice and knowing that collateral <= col.
|
|
259
|
+
uint256 offer = _calculateOffer(_challenge, collateral);
|
|
260
|
+
|
|
261
|
+
JUSD.transferFrom(msg.sender, address(this), offer); // get money from bidder
|
|
262
|
+
uint256 reward = (offer * CHALLENGER_REWARD) / 1_000_000;
|
|
263
|
+
JUSD.transfer(_challenge.challenger, reward); // pay out the challenger reward
|
|
264
|
+
uint256 fundsAvailable = offer - reward; // funds available after reward
|
|
265
|
+
|
|
266
|
+
// Example: available funds are 90, repayment is 50, reserve 20%. Then 20%*(90-50)=16 are collected as profits
|
|
267
|
+
// and the remaining 34 are sent to the position owner. If the position owner maxed out debt before the challenge
|
|
268
|
+
// started and the liquidation price was 100, they would be slightly better off as they would get away with 80
|
|
269
|
+
// instead of 40+36 = 76 in this example.
|
|
270
|
+
if (fundsAvailable > repayment + interest) {
|
|
271
|
+
// The excess amount is distributed between the system and the owner using the reserve ratio
|
|
272
|
+
// At this point, we cannot rely on the liquidation price because the challenge might have been started as a
|
|
273
|
+
// response to an unreasonable increase of the liquidation price, such that we have to use this heuristic
|
|
274
|
+
// for excess fund distribution, which make position owners that maxed out their positions slightly better
|
|
275
|
+
// off in comparison to those who did not.
|
|
276
|
+
uint256 profits = (reservePPM * (fundsAvailable - repayment - interest)) / 1_000_000;
|
|
277
|
+
JUSD.collectProfits(address(this), profits);
|
|
278
|
+
JUSD.transfer(owner, fundsAvailable - repayment - interest - profits);
|
|
279
|
+
} else if (fundsAvailable < repayment + interest) {
|
|
280
|
+
JUSD.coverLoss(address(this), repayment + interest - fundsAvailable); // ensure we have enough to pay everything
|
|
281
|
+
}
|
|
282
|
+
JUSD.burnWithoutReserve(repayment, reservePPM); // Repay the challenged part, example: 50 deur leading to 10 deur in implicit profits
|
|
283
|
+
JUSD.collectProfits(address(this), interest); // Collect interest as profits
|
|
284
|
+
_challenge.position.transferChallengedCollateral(msg.sender, collateral); // transfer the collateral to the bidder
|
|
285
|
+
return (collateral, offer);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function _avertChallenge(Challenge memory _challenge, uint32 number, uint256 liqPrice, uint256 size) internal {
|
|
289
|
+
require(block.timestamp != _challenge.start); // do not allow to avert the challenge in the same transaction, see CS-ZCHF-037
|
|
290
|
+
if (msg.sender == _challenge.challenger) {
|
|
291
|
+
// allow challenger to cancel challenge without paying themselves
|
|
292
|
+
} else {
|
|
293
|
+
JUSD.transferFrom(msg.sender, _challenge.challenger, (size * liqPrice) / (10 ** 18));
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
_challenge.position.notifyChallengeAverted(size);
|
|
297
|
+
_challenge.position.collateral().transfer(msg.sender, size);
|
|
298
|
+
if (size < _challenge.size) {
|
|
299
|
+
challenges[number].size = _challenge.size - size;
|
|
300
|
+
} else {
|
|
301
|
+
require(size == _challenge.size);
|
|
302
|
+
delete challenges[number];
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* @notice Returns 'amount' of the collateral to the challenger and reduces or deletes the relevant challenge.
|
|
308
|
+
*/
|
|
309
|
+
function _returnChallengerCollateral(
|
|
310
|
+
Challenge memory _challenge,
|
|
311
|
+
uint32 number,
|
|
312
|
+
uint256 amount,
|
|
313
|
+
bool postpone
|
|
314
|
+
) internal {
|
|
315
|
+
_returnCollateral(_challenge.position.collateral(), _challenge.challenger, amount, postpone);
|
|
316
|
+
if (_challenge.size == amount) {
|
|
317
|
+
// bid on full amount
|
|
318
|
+
delete challenges[number];
|
|
319
|
+
} else {
|
|
320
|
+
// bid on partial amount
|
|
321
|
+
challenges[number].size -= amount;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* @notice Calculates the current Dutch auction price.
|
|
327
|
+
* @dev Starts at the full price at time 'start' and linearly goes to 0 as 'phase2' passes.
|
|
328
|
+
*/
|
|
329
|
+
function _calculatePrice(uint40 start, uint40 phase2, uint256 liqPrice) internal view returns (uint256) {
|
|
330
|
+
uint40 timeNow = uint40(block.timestamp);
|
|
331
|
+
if (timeNow <= start) {
|
|
332
|
+
return liqPrice;
|
|
333
|
+
} else if (timeNow >= start + phase2) {
|
|
334
|
+
return 0;
|
|
335
|
+
} else {
|
|
336
|
+
uint256 timeLeft = phase2 - (timeNow - start);
|
|
337
|
+
return (liqPrice * timeLeft) / phase2;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* @notice Calculates the offer amount for the given challenge.
|
|
343
|
+
* @dev The offer is calculated as the current price times the collateral amount.
|
|
344
|
+
*/
|
|
345
|
+
function _calculateOffer(Challenge memory _challenge, uint256 collateral) internal view returns (uint256) {
|
|
346
|
+
(uint256 liqPrice, uint40 phase) = _challenge.position.challengeData();
|
|
347
|
+
return (_calculatePrice(_challenge.start + phase, phase, liqPrice) * collateral) / 10 ** 18;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* @notice Get the price per unit of the collateral for the given challenge.
|
|
352
|
+
* @dev The price comes with (36 - collateral.decimals()) digits, so multiplying it with the raw collateral amount
|
|
353
|
+
* always yields a price with 36 digits, or 18 digits after dividing by 10**18 again.
|
|
354
|
+
*/
|
|
355
|
+
function price(uint32 challengeNumber) public view returns (uint256) {
|
|
356
|
+
Challenge memory _challenge = challenges[challengeNumber];
|
|
357
|
+
if (_challenge.challenger == address(0x0)) {
|
|
358
|
+
return 0;
|
|
359
|
+
} else {
|
|
360
|
+
(uint256 liqPrice, uint40 phase) = _challenge.position.challengeData();
|
|
361
|
+
return _calculatePrice(_challenge.start + phase, phase, liqPrice);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* @notice Challengers can call this method to withdraw collateral whose return was postponed.
|
|
367
|
+
*/
|
|
368
|
+
function returnPostponedCollateral(address collateral, address target) external {
|
|
369
|
+
uint256 amount = pendingReturns[collateral][msg.sender];
|
|
370
|
+
delete pendingReturns[collateral][msg.sender];
|
|
371
|
+
IERC20(collateral).transfer(target, amount);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function _returnCollateral(IERC20 collateral, address recipient, uint256 amount, bool postpone) internal {
|
|
375
|
+
if (postpone) {
|
|
376
|
+
// Postponing helps in case the challenger was blacklisted or otherwise cannot receive at the moment.
|
|
377
|
+
pendingReturns[address(collateral)][recipient] += amount;
|
|
378
|
+
emit PostponedReturn(address(collateral), recipient, amount);
|
|
379
|
+
} else {
|
|
380
|
+
collateral.transfer(recipient, amount); // return the challenger's collateral
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* The applicable purchase price when forcing the sale of collateral of an expired position.
|
|
386
|
+
*
|
|
387
|
+
* The price starts at 10x the liquidation price at the expiration time, linearly declines to
|
|
388
|
+
* 1x liquidation price over the course of one challenge period, and then linearly declines
|
|
389
|
+
* less steeply to 0 over the course of another challenge period.
|
|
390
|
+
*/
|
|
391
|
+
function expiredPurchasePrice(IPosition pos) public view returns (uint256) {
|
|
392
|
+
uint256 liqprice = pos.virtualPrice();
|
|
393
|
+
uint256 expiration = pos.expiration();
|
|
394
|
+
if (block.timestamp <= expiration) {
|
|
395
|
+
return EXPIRED_PRICE_FACTOR * liqprice;
|
|
396
|
+
} else {
|
|
397
|
+
uint256 challengePeriod = pos.challengePeriod();
|
|
398
|
+
uint256 timePassed = block.timestamp - expiration;
|
|
399
|
+
if (timePassed <= challengePeriod) {
|
|
400
|
+
// from 10x liquidation price to 1x in first phase
|
|
401
|
+
uint256 timeLeft = challengePeriod - timePassed;
|
|
402
|
+
return liqprice + (((EXPIRED_PRICE_FACTOR - 1) * liqprice * timeLeft) / challengePeriod);
|
|
403
|
+
} else if (timePassed < 2 * challengePeriod) {
|
|
404
|
+
// from 1x liquidation price to 0 in second phase
|
|
405
|
+
uint256 timeLeft = 2 * challengePeriod - timePassed;
|
|
406
|
+
return (liqprice * timeLeft) / challengePeriod;
|
|
407
|
+
} else {
|
|
408
|
+
// get collateral for free after both phases passed
|
|
409
|
+
return 0;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Buys up to the desired amount of the collateral asset from the given expired position using
|
|
416
|
+
* the applicable 'expiredPurchasePrice' at that instant.
|
|
417
|
+
*
|
|
418
|
+
* To prevent dust either the remaining collateral needs to be bought or collateral with a value
|
|
419
|
+
* of at least OPENING_FEE (1000 JUSD) needs to remain in the position for a different buyer
|
|
420
|
+
*/
|
|
421
|
+
function buyExpiredCollateral(IPosition pos, uint256 upToAmount) external returns (uint256) {
|
|
422
|
+
uint256 max = pos.collateral().balanceOf(address(pos));
|
|
423
|
+
uint256 amount = upToAmount > max ? max : upToAmount;
|
|
424
|
+
uint256 forceSalePrice = expiredPurchasePrice(pos);
|
|
425
|
+
|
|
426
|
+
uint256 costs = (forceSalePrice * amount) / 10 ** 18;
|
|
427
|
+
|
|
428
|
+
if (max - amount > 0 && ((forceSalePrice * (max - amount)) / 10 ** 18) < OPENING_FEE) {
|
|
429
|
+
revert LeaveNoDust(max - amount);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
pos.forceSale(msg.sender, amount, costs);
|
|
433
|
+
emit ForcedSale(address(pos), amount, forceSalePrice);
|
|
434
|
+
return amount;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* @dev See {IERC165-supportsInterface}.
|
|
439
|
+
*/
|
|
440
|
+
function supportsInterface(bytes4 interfaceId) public view override virtual returns (bool) {
|
|
441
|
+
return
|
|
442
|
+
interfaceId == type(IMintingHub).interfaceId ||
|
|
443
|
+
super.supportsInterface(interfaceId);
|
|
444
|
+
}
|
|
445
|
+
}
|