-
Notifications
You must be signed in to change notification settings - Fork 16
/
Copy pathPosition.sol
328 lines (287 loc) · 13.9 KB
/
Position.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;
import "@openzeppelin/contracts/utils/math/Math.sol";
import "v3-core/libraries/FullMath.sol";
import "./FixedCast.sol";
import "./FixedPoint.sol";
import "./Tick.sol";
library Position {
using FixedCast for uint16;
using FixedCast for uint256;
using FixedPoint for uint256;
uint256 internal constant ONE = 1e18;
/// @dev immutables: notionalInitial, debtInitial, midTick, entryTick, isLong
/// @dev mutables: liquidated, oiShares, fractionRemaining
struct Info {
uint96 notionalInitial; // initial notional = collateral * leverage
uint96 debtInitial; // initial debt = notional - collateral
int24 midTick; // midPrice = 1.0001 ** midTick at build
int24 entryTick; // entryPrice = 1.0001 ** entryTick at build
bool isLong; // whether long or short
bool liquidated; // whether has been liquidated (mutable)
uint240 oiShares; // current shares of aggregate open interest on side (mutable)
uint16 fractionRemaining; // fraction of initial position remaining (mutable)
}
/*///////////////////////////////////////////////////////////////
POSITIONS MAPPING FUNCTIONS
//////////////////////////////////////////////////////////////*/
/// @notice Retrieves a position from positions mapping
function get(mapping(bytes32 => Info) storage self, address owner, uint256 id)
internal
view
returns (Info memory position_)
{
position_ = self[keccak256(abi.encodePacked(owner, id))];
}
/// @notice Stores a position in positions mapping
function set(
mapping(bytes32 => Info) storage self,
address owner,
uint256 id,
Info memory position
) internal {
self[keccak256(abi.encodePacked(owner, id))] = position;
}
/*///////////////////////////////////////////////////////////////
POSITION CAST GETTER FUNCTIONS
//////////////////////////////////////////////////////////////*/
/// @notice Computes the position's initial notional cast to uint256
function _notionalInitial(Info memory self) private pure returns (uint256) {
return uint256(self.notionalInitial);
}
/// @notice Computes the position's initial debt cast to uint256
function _debtInitial(Info memory self) private pure returns (uint256) {
return uint256(self.debtInitial);
}
/// @notice Computes the position's current shares of open interest
/// @notice cast to uint256
function _oiShares(Info memory self) private pure returns (uint256) {
return uint256(self.oiShares);
}
/// @notice Computes the fraction remaining of the position cast to uint256
function _fractionRemaining(Info memory self) private pure returns (uint256) {
return self.fractionRemaining.toUint256Fixed();
}
/*///////////////////////////////////////////////////////////////
POSITION EXISTENCE FUNCTIONS
//////////////////////////////////////////////////////////////*/
/// @notice Whether the position exists
/// @dev Is false if position has been liquidated or fraction remaining == 0
function exists(Info memory self) internal pure returns (bool exists_) {
return (self.fractionRemaining > 0);
}
/*///////////////////////////////////////////////////////////////
POSITION FRACTION REMAINING FUNCTIONS
//////////////////////////////////////////////////////////////*/
/// @notice Gets the current fraction remaining of the initial position
function getFractionRemaining(Info memory self) internal pure returns (uint256) {
return _fractionRemaining(self);
}
/// @notice Computes an updated fraction remaining of the initial position
/// @notice given fractionRemoved unwound/liquidated from remaining position
function updatedFractionRemaining(Info memory self, uint256 fractionRemoved)
internal
pure
returns (uint16)
{
uint256 fractionRemaining = _fractionRemaining(self).mulDown(ONE - fractionRemoved);
return fractionRemaining.toUint16Fixed();
}
/*///////////////////////////////////////////////////////////////
POSITION PRICE FUNCTIONS
//////////////////////////////////////////////////////////////*/
/// @notice Computes the midPrice of the position at entry cast to uint256
/// @dev Will be slightly different (tol of 1bps) vs actual
/// @dev midPrice at build given tick resolution limited to 1bps
/// @dev Only affects value() calc below and thus PnL slightly
function midPriceAtEntry(Info memory self) internal pure returns (uint256 midPrice_) {
midPrice_ = Tick.tickToPrice(self.midTick);
}
/// @notice Computes the entryPrice of the position cast to uint256
/// @dev Will be slightly different (tol of 1bps) vs actual
/// @dev entryPrice at build given tick resolution limited to 1bps
/// @dev Only affects value() calc below and thus PnL slightly
function entryPrice(Info memory self) internal pure returns (uint256 entryPrice_) {
entryPrice_ = Tick.tickToPrice(self.entryTick);
}
/*///////////////////////////////////////////////////////////////
POSITION OI FUNCTIONS
//////////////////////////////////////////////////////////////*/
/// @notice Computes the amount of shares of open interest to issue
/// @notice a newly built position
/// @dev use mulDiv
function calcOiShares(uint256 oi, uint256 oiTotalOnSide, uint256 oiTotalSharesOnSide)
internal
pure
returns (uint256 oiShares_)
{
oiShares_ = (oiTotalOnSide == 0 || oiTotalSharesOnSide == 0)
? oi
: FullMath.mulDiv(oi, oiTotalSharesOnSide, oiTotalOnSide);
}
/// @notice Computes the position's initial open interest cast to uint256
/// @dev oiInitial = Q / midPriceAtEntry
/// @dev Will be slightly different (tol of 1bps) vs actual oi at build
/// @dev given midTick resolution limited to 1bps
/// @dev Only affects value() calc below and thus PnL slightly
function _oiInitial(Info memory self) private pure returns (uint256) {
uint256 q = _notionalInitial(self);
uint256 mid = midPriceAtEntry(self);
return q.divDown(mid);
}
/*///////////////////////////////////////////////////////////////
POSITION FRACTIONAL GETTER FUNCTIONS
//////////////////////////////////////////////////////////////*/
/// @notice Computes the initial notional of position when built
/// @notice accounting for amount of position remaining
/// @dev use mulUp to avoid rounding leftovers on unwind
function notionalInitial(Info memory self, uint256 fraction) internal pure returns (uint256) {
uint256 notionalForRemaining = _notionalInitial(self).mulUp(_fractionRemaining(self));
return notionalForRemaining.mulUp(fraction);
}
/// @notice Computes the initial open interest of position when built
/// @notice accounting for amount of position remaining
/// @dev use mulUp to avoid rounding leftovers on unwind
function oiInitial(Info memory self, uint256 fraction) internal pure returns (uint256) {
uint256 oiInitialForRemaining = _oiInitial(self).mulUp(_fractionRemaining(self));
return oiInitialForRemaining.mulUp(fraction);
}
/// @notice Computes the current shares of open interest position holds
/// @notice on pos.isLong side of the market
/// @dev use mulDown to avoid giving excess shares to pos owner on unwind
function oiSharesCurrent(Info memory self, uint256 fraction) internal pure returns (uint256) {
uint256 oiSharesForRemaining = _oiShares(self);
// WARNING: must mulDown to avoid giving excess oi shares
return oiSharesForRemaining.mulDown(fraction);
}
/// @notice Computes the current debt position holds accounting
/// @notice for amount of position remaining
/// @dev use mulUp to avoid rounding leftovers on unwind
function debtInitial(Info memory self, uint256 fraction) internal pure returns (uint256) {
uint256 debtForRemaining = _debtInitial(self).mulUp(_fractionRemaining(self));
return debtForRemaining.mulUp(fraction);
}
/// @notice Computes the current open interest of remaining position accounting for
/// @notice potential funding payments between long/short sides
/// @dev returns zero when oiShares = oiTotalOnSide = oiTotalSharesOnSide = 0 to avoid
/// @dev div by zero errors
/// @dev use mulDiv
function oiCurrent(
Info memory self,
uint256 fraction,
uint256 oiTotalOnSide,
uint256 oiTotalSharesOnSide
) internal pure returns (uint256) {
uint256 oiShares = oiSharesCurrent(self, fraction);
if (oiShares == 0 || oiTotalOnSide == 0 || oiTotalSharesOnSide == 0) return 0;
return FullMath.mulDiv(oiShares, oiTotalOnSide, oiTotalSharesOnSide);
}
/// @notice Computes the remaining position's cost cast to uint256
function cost(Info memory self, uint256 fraction) internal pure returns (uint256) {
uint256 posNotionalInitial = notionalInitial(self, fraction);
uint256 posDebt = debtInitial(self, fraction);
// should always be > 0 but use subFloor to be safe w reverts
uint256 posCost = posNotionalInitial;
posCost = posCost.subFloor(posDebt);
return posCost;
}
/*///////////////////////////////////////////////////////////////
POSITION CALC FUNCTIONS
//////////////////////////////////////////////////////////////*/
/// @notice Computes the value of remaining position
/// @dev Floors to zero, so won't properly compute if self is underwater
function value(
Info memory self,
uint256 fraction,
uint256 oiTotalOnSide,
uint256 oiTotalSharesOnSide,
uint256 currentPrice,
uint256 capPayoff
) internal pure returns (uint256 val_) {
uint256 posOiInitial = oiInitial(self, fraction);
uint256 posNotionalInitial = notionalInitial(self, fraction);
uint256 posDebt = debtInitial(self, fraction);
uint256 posOiCurrent = oiCurrent(self, fraction, oiTotalOnSide, oiTotalSharesOnSide);
uint256 posEntryPrice = entryPrice(self);
// NOTE: PnL = +/- oiCurrent * [currentPrice - entryPrice]; ... (w/o capPayoff)
// NOTE: fundingPayments = notionalInitial * ( oiCurrent / oiInitial - 1 )
// NOTE: value = collateralInitial + PnL + fundingPayments
// NOTE: = notionalInitial - debt + PnL + fundingPayments
if (self.isLong) {
// val = notionalInitial * oiCurrent / oiInitial
// + oiCurrent * min[currentPrice, entryPrice * (1 + capPayoff)]
// - oiCurrent * entryPrice - debt
val_ = posNotionalInitial.mulUp(posOiCurrent).divUp(posOiInitial)
+ Math.min(
posOiCurrent.mulUp(currentPrice),
posOiCurrent.mulUp(posEntryPrice).mulUp(ONE + capPayoff)
);
// floor to 0
val_ = val_.subFloor(posDebt + posOiCurrent.mulUp(posEntryPrice));
} else {
// NOTE: capPayoff >= 1, so no need to include w short
// val = notionalInitial * oiCurrent / oiInitial + oiCurrent * entryPrice
// - oiCurrent * currentPrice - debt
val_ = posNotionalInitial.mulUp(posOiCurrent).divUp(posOiInitial)
+ posOiCurrent.mulUp(posEntryPrice);
// floor to 0
val_ = val_.subFloor(posDebt + posOiCurrent.mulUp(currentPrice));
}
}
/// @notice Computes the current notional of remaining position including PnL
/// @dev Floors to debt if value <= 0
function notionalWithPnl(
Info memory self,
uint256 fraction,
uint256 oiTotalOnSide,
uint256 oiTotalSharesOnSide,
uint256 currentPrice,
uint256 capPayoff
) internal pure returns (uint256 notionalWithPnl_) {
uint256 posValue =
value(self, fraction, oiTotalOnSide, oiTotalSharesOnSide, currentPrice, capPayoff);
uint256 posDebt = debtInitial(self, fraction);
notionalWithPnl_ = posValue + posDebt;
}
/// @notice Computes the trading fees to be imposed on remaining position
/// @notice for build/unwind
function tradingFee(
Info memory self,
uint256 fraction,
uint256 oiTotalOnSide,
uint256 oiTotalSharesOnSide,
uint256 currentPrice,
uint256 capPayoff,
uint256 tradingFeeRate
) internal pure returns (uint256 tradingFee_) {
uint256 posNotional = notionalWithPnl(
self, fraction, oiTotalOnSide, oiTotalSharesOnSide, currentPrice, capPayoff
);
tradingFee_ = posNotional.mulUp(tradingFeeRate);
}
/// @notice Whether a position can be liquidated
/// @dev is true when value * (1 - liq fee rate) < maintenance margin
/// @dev liq fees are reward given to liquidator
function liquidatable(
Info memory self,
uint256 oiTotalOnSide,
uint256 oiTotalSharesOnSide,
uint256 currentPrice,
uint256 capPayoff,
uint256 maintenanceMarginFraction,
uint256 liquidationFeeRate
) internal pure returns (bool can_) {
uint256 fraction = ONE;
uint256 posNotionalInitial = notionalInitial(self, fraction);
if (self.fractionRemaining == 0) {
// already been liquidated or doesn't exist
// latter covers edge case of val == 0 and MM + liq fee == 0
return false;
}
uint256 val =
value(self, fraction, oiTotalOnSide, oiTotalSharesOnSide, currentPrice, capPayoff);
uint256 maintenanceMargin = posNotionalInitial.mulUp(maintenanceMarginFraction);
uint256 liquidationFee = val.mulDown(liquidationFeeRate);
can_ = val < maintenanceMargin + liquidationFee;
}
}