forked from overlay-market/v1-core
-
Notifications
You must be signed in to change notification settings - Fork 0
/
OverlayV1Market.sol
875 lines (758 loc) · 35.9 KB
/
OverlayV1Market.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
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;
import "@openzeppelin/contracts/utils/math/Math.sol";
import "./interfaces/IOverlayV1Factory.sol";
import "./interfaces/IOverlayV1Market.sol";
import "./interfaces/IOverlayV1Token.sol";
import "./interfaces/feeds/IOverlayV1Feed.sol";
import "./libraries/FixedPoint.sol";
import "./libraries/Oracle.sol";
import "./libraries/Position.sol";
import "./libraries/Risk.sol";
import "./libraries/Roller.sol";
contract OverlayV1Market is IOverlayV1Market {
using FixedPoint for uint256;
using Oracle for Oracle.Data;
using Position for mapping(bytes32 => Position.Info);
using Position for Position.Info;
using Risk for uint256[15];
using Roller for Roller.Snapshot;
// internal constants
uint256 internal constant ONE = 1e18; // 18 decimal places
// cap for euler exponent powers; SEE: ./libraries/LogExpMath.sol::pow
// using ~ 1/2 library max for substantial padding
uint256 internal constant MAX_NATURAL_EXPONENT = 20e18;
// immutables
IOverlayV1Token public immutable ovl; // ovl token
address public immutable feed; // oracle feed
address public immutable factory; // factory that deployed this market
// risk params
uint256[15] public params; // params.idx order based on Risk.Parameters enum
// aggregate oi quantities
uint256 public oiLong;
uint256 public oiShort;
uint256 public oiLongShares;
uint256 public oiShortShares;
// rollers
Roller.Snapshot public override snapshotVolumeBid; // snapshot of recent volume on bid
Roller.Snapshot public override snapshotVolumeAsk; // snapshot of recent volume on ask
Roller.Snapshot public override snapshotMinted; // snapshot of recent PnL minted/burned
// positions
mapping(bytes32 => Position.Info) public override positions;
uint256 private _totalPositions;
// data from last call to update
uint256 public timestampUpdateLast;
// cached risk calcs
uint256 public dpUpperLimit; // e**(+priceDriftUpperLimit * macroWindow)
// factory modifier for governance sensitive functions
modifier onlyFactory() {
require(msg.sender == factory, "OVLV1: !factory");
_;
}
// events for core functions
event Build(
address indexed sender, // address that initiated build (owns position)
uint256 positionId, // id of built position
uint256 oi, // oi of position at build
uint256 debt, // debt of position at build
bool isLong, // whether is long or short
uint256 price // entry price
);
event Unwind(
address indexed sender, // address that initiated unwind (owns position)
uint256 positionId, // id of unwound position
uint256 fraction, // fraction of position unwound
int256 mint, // total amount minted/burned (+/-) at unwind
uint256 price // exit price
);
event Liquidate(
address indexed sender, // address that initiated liquidate
address indexed owner, // address that owned the liquidated position
uint256 positionId, // id of the liquidated position
int256 mint, // total amount burned (-) at liquidate
uint256 price // liquidation price
);
constructor() {
(address _ovl, address _feed, address _factory) = IOverlayV1Deployer(msg.sender)
.parameters();
ovl = IOverlayV1Token(_ovl);
feed = _feed;
factory = _factory;
}
/// @notice initializes the market and its risk params
/// @notice called only once by factory on deployment
function initialize(uint256[15] memory _params) external onlyFactory {
// initialize update data
Oracle.Data memory data = IOverlayV1Feed(feed).latest();
require(_midFromFeed(data) > 0, "OVLV1:!data");
timestampUpdateLast = block.timestamp;
// check risk params valid
uint256 _capLeverage = _params[uint256(Risk.Parameters.CapLeverage)];
uint256 _delta = _params[uint256(Risk.Parameters.Delta)];
uint256 _maintenanceMarginFraction = _params[
uint256(Risk.Parameters.MaintenanceMarginFraction)
];
uint256 _liquidationFeeRate = _params[uint256(Risk.Parameters.LiquidationFeeRate)];
require(
_capLeverage <=
ONE.divDown(
2 * _delta + _maintenanceMarginFraction.divDown(ONE - _liquidationFeeRate)
),
"OVLV1: max lev immediately liquidatable"
);
uint256 _priceDriftUpperLimit = _params[uint256(Risk.Parameters.PriceDriftUpperLimit)];
require(
_priceDriftUpperLimit * data.macroWindow < MAX_NATURAL_EXPONENT,
"OVLV1: price drift exceeds max exp"
);
_cacheRiskCalc(Risk.Parameters.PriceDriftUpperLimit, _priceDriftUpperLimit);
// set the risk params
for (uint256 i = 0; i < _params.length; i++) {
params[i] = _params[i];
}
}
/// @dev builds a new position
function build(
uint256 collateral,
uint256 leverage,
bool isLong,
uint256 priceLimit
) external returns (uint256 positionId_) {
require(leverage >= ONE, "OVLV1:lev<min");
require(leverage <= params.get(Risk.Parameters.CapLeverage), "OVLV1:lev>max");
require(collateral >= params.get(Risk.Parameters.MinCollateral), "OVLV1:collateral<min");
uint256 oi;
uint256 debt;
uint256 price;
uint256 tradingFee;
// avoids stack too deep
{
// call to update before any effects
Oracle.Data memory data = update();
// calculate notional, oi, and trading fees. fees charged on notional
// and added to collateral transferred in
uint256 notional = collateral.mulUp(leverage);
uint256 midPrice = _midFromFeed(data);
oi = oiFromNotional(notional, midPrice);
// check have more than zero number of contracts built
require(oi > 0, "OVLV1:oi==0");
// calculate debt and trading fees. fees charged on notional
// and added to collateral transferred in
debt = notional - collateral;
tradingFee = notional.mulUp(params.get(Risk.Parameters.TradingFeeRate));
// calculate current notional cap adjusted for circuit breaker *then* adjust
// for front run and back run bounds (order matters)
// finally, transform into a cap on open interest
uint256 capOi = oiFromNotional(
capNotionalAdjustedForBounds(
data,
capNotionalAdjustedForCircuitBreaker(params.get(Risk.Parameters.CapNotional))
),
midPrice
);
// longs get the ask and shorts get the bid on build
// register the additional volume on either the ask or bid
// where volume = oi / capOi
price = isLong
? ask(data, _registerVolumeAsk(data, oi, capOi))
: bid(data, _registerVolumeBid(data, oi, capOi));
// check price hasn't changed more than max slippage specified by trader
require(isLong ? price <= priceLimit : price >= priceLimit, "OVLV1:slippage>max");
// add new position's open interest to the side's aggregate oi value
// and increase number of oi shares issued. assemble position for storage
// cache for gas savings
uint256 oiTotalOnSide = isLong ? oiLong : oiShort;
uint256 oiTotalSharesOnSide = isLong ? oiLongShares : oiShortShares;
// check new total oi on side does not exceed capOi
oiTotalOnSide += oi;
oiTotalSharesOnSide += oi;
require(oiTotalOnSide <= capOi, "OVLV1:oi>cap");
// update total aggregate oi and oi shares
if (isLong) {
oiLong = oiTotalOnSide;
oiLongShares = oiTotalSharesOnSide;
} else {
oiShort = oiTotalOnSide;
oiShortShares = oiTotalSharesOnSide;
}
// assemble position info data
// check position is not immediately liquidatable prior to storing
Position.Info memory pos = Position.Info({
notional: uint96(notional), // won't overflow as capNotional max is 8e24
debt: uint96(debt),
isLong: isLong,
entryToMidRatio: Position.calcEntryToMidRatio(price, midPrice),
liquidated: false,
oiShares: oi
});
require(
!pos.liquidatable(
oiTotalOnSide,
oiTotalSharesOnSide,
midPrice, // mid price used on liquidations
params.get(Risk.Parameters.CapPayoff),
params.get(Risk.Parameters.MaintenanceMarginFraction),
params.get(Risk.Parameters.LiquidationFeeRate)
),
"OVLV1:liquidatable"
);
// store the position info data
positionId_ = _totalPositions;
positions.set(msg.sender, positionId_, pos);
_totalPositions++;
}
// emit build event
emit Build(msg.sender, positionId_, oi, debt, isLong, price);
// transfer in the OVL collateral needed to back the position + fees
// trading fees charged as a percentage on notional size of position
ovl.transferFrom(msg.sender, address(this), collateral + tradingFee);
// send trading fees to trading fee recipient
ovl.transfer(IOverlayV1Factory(factory).feeRecipient(), tradingFee);
}
/// @dev unwinds fraction of an existing position
function unwind(
uint256 positionId,
uint256 fraction,
uint256 priceLimit
) external {
require(fraction > 0, "OVLV1:fraction<min");
require(fraction <= ONE, "OVLV1:fraction>max");
uint256 value;
uint256 cost;
uint256 price;
uint256 tradingFee;
// avoids stack too deep
{
// call to update before any effects
Oracle.Data memory data = update();
// check position exists
Position.Info memory pos = positions.get(msg.sender, positionId);
require(pos.exists(), "OVLV1:!position");
// cache for gas savings
uint256 oiTotalOnSide = pos.isLong ? oiLong : oiShort;
uint256 oiTotalSharesOnSide = pos.isLong ? oiLongShares : oiShortShares;
// check position not liquidatable otherwise can't unwind
require(
!pos.liquidatable(
oiTotalOnSide,
oiTotalSharesOnSide,
_midFromFeed(data), // mid price used on liquidations
params.get(Risk.Parameters.CapPayoff),
params.get(Risk.Parameters.MaintenanceMarginFraction),
params.get(Risk.Parameters.LiquidationFeeRate)
),
"OVLV1:liquidatable"
);
// longs get the bid and shorts get the ask on unwind
// register the additional volume on either the ask or bid
// where volume = oi / capOi
// current cap only adjusted for bounds (no circuit breaker so traders
// don't get stuck in a position)
uint256 capOi = oiFromNotional(
capNotionalAdjustedForBounds(data, params.get(Risk.Parameters.CapNotional)),
_midFromFeed(data)
);
price = pos.isLong
? bid(
data,
_registerVolumeBid(
data,
pos.oiCurrent(fraction, oiTotalOnSide, oiTotalSharesOnSide),
capOi
)
)
: ask(
data,
_registerVolumeAsk(
data,
pos.oiCurrent(fraction, oiTotalOnSide, oiTotalSharesOnSide),
capOi
)
);
// check price hasn't changed more than max slippage specified by trader
require(pos.isLong ? price >= priceLimit : price <= priceLimit, "OVLV1:slippage>max");
// calculate the value and cost of the position for pnl determinations
// and amount to transfer
uint256 capPayoff = params.get(Risk.Parameters.CapPayoff);
value = pos.value(fraction, oiTotalOnSide, oiTotalSharesOnSide, price, capPayoff);
cost = pos.cost(fraction);
// calculate the trading fee as % on notional
uint256 tradingFeeRate = params.get(Risk.Parameters.TradingFeeRate);
tradingFee = pos.tradingFee(
fraction,
oiTotalOnSide,
oiTotalSharesOnSide,
price,
capPayoff,
tradingFeeRate
);
tradingFee = Math.min(tradingFee, value); // if value < tradingFee
// subtract unwound open interest from the side's aggregate oi value
// and decrease number of oi shares issued
// use subFloor to avoid reverts with rounding issues
if (pos.isLong) {
oiLong = oiLong.subFloor(
pos.oiCurrent(fraction, oiTotalOnSide, oiTotalSharesOnSide)
);
oiLongShares = oiLongShares.subFloor(pos.oiSharesCurrent(fraction));
} else {
oiShort = oiShort.subFloor(
pos.oiCurrent(fraction, oiTotalOnSide, oiTotalSharesOnSide)
);
oiShortShares = oiShortShares.subFloor(pos.oiSharesCurrent(fraction));
}
// register the amount to be minted/burned
// capPayoff prevents overflow reverts with int256 cast
_registerMintOrBurn(int256(value) - int256(cost));
// store the updated position info data
// use subFloor to avoid reverts with rounding issues
pos.notional = uint96(uint256(pos.notional).subFloor(pos.notionalInitial(fraction)));
pos.debt = uint96(uint256(pos.debt).subFloor(pos.debtCurrent(fraction)));
pos.oiShares = pos.oiShares.subFloor(pos.oiSharesCurrent(fraction));
positions.set(msg.sender, positionId, pos);
}
// emit unwind event
emit Unwind(msg.sender, positionId, fraction, int256(value) - int256(cost), price);
// mint or burn the pnl for the position
if (value >= cost) {
ovl.mint(address(this), value - cost);
} else {
ovl.burn(cost - value);
}
// transfer out the unwound position value less fees to trader
ovl.transfer(msg.sender, value - tradingFee);
// send trading fees to trading fee recipient
ovl.transfer(IOverlayV1Factory(factory).feeRecipient(), tradingFee);
}
/// @dev liquidates a liquidatable position
function liquidate(address owner, uint256 positionId) external {
uint256 value;
uint256 cost;
uint256 price;
uint256 liquidationFee;
uint256 marginToBurn;
uint256 marginRemaining;
// avoids stack too deep
{
// check position exists
Position.Info memory pos = positions.get(owner, positionId);
require(pos.exists(), "OVLV1:!position");
// call to update before any effects
Oracle.Data memory data = update();
// cache for gas savings
uint256 oiTotalOnSide = pos.isLong ? oiLong : oiShort;
uint256 oiTotalSharesOnSide = pos.isLong ? oiLongShares : oiShortShares;
uint256 capPayoff = params.get(Risk.Parameters.CapPayoff);
// entire position should be liquidated
uint256 fraction = ONE;
// Use mid price without volume for liquidation (oracle price effectively) to
// prevent market impact manipulation from causing unneccessary liquidations
price = _midFromFeed(data);
// check position is liquidatable
require(
pos.liquidatable(
oiTotalOnSide,
oiTotalSharesOnSide,
price,
capPayoff,
params.get(Risk.Parameters.MaintenanceMarginFraction),
params.get(Risk.Parameters.LiquidationFeeRate)
),
"OVLV1:!liquidatable"
);
// calculate the value and cost of the position for pnl determinations
// and amount to transfer
value = pos.value(fraction, oiTotalOnSide, oiTotalSharesOnSide, price, capPayoff);
cost = pos.cost(fraction);
// calculate the liquidation fee as % on remaining value
// sent as reward to liquidator
liquidationFee = value.mulDown(params.get(Risk.Parameters.LiquidationFeeRate));
marginRemaining = value - liquidationFee;
// Reduce burn amount further by the mm burn rate, as insurance
// for cases when not liquidated in time
marginToBurn = marginRemaining.mulDown(
params.get(Risk.Parameters.MaintenanceMarginBurnRate)
);
marginRemaining -= marginToBurn;
// subtract liquidated open interest from the side's aggregate oi value
// and decrease number of oi shares issued
// use subFloor to avoid reverts with rounding issues
if (pos.isLong) {
oiLong = oiLong.subFloor(
pos.oiCurrent(fraction, oiTotalOnSide, oiTotalSharesOnSide)
);
oiLongShares = oiLongShares.subFloor(pos.oiSharesCurrent(fraction));
} else {
oiShort = oiShort.subFloor(
pos.oiCurrent(fraction, oiTotalOnSide, oiTotalSharesOnSide)
);
oiShortShares = oiShortShares.subFloor(pos.oiSharesCurrent(fraction));
}
// register the amount to be burned
_registerMintOrBurn(int256(value) - int256(cost) - int256(marginToBurn));
// store the updated position info data. mark as liquidated
pos.notional = 0;
pos.debt = 0;
pos.oiShares = 0;
pos.liquidated = true;
pos.entryToMidRatio = 0;
positions.set(owner, positionId, pos);
}
// emit liquidate event
emit Liquidate(
msg.sender,
owner,
positionId,
int256(value) - int256(cost) - int256(marginToBurn),
price
);
// burn the pnl for the position + insurance margin
ovl.burn(cost - value + marginToBurn);
// transfer out the liquidation fee to liquidator for reward
ovl.transfer(msg.sender, liquidationFee);
// send remaining margin to trading fee recipient
ovl.transfer(IOverlayV1Factory(factory).feeRecipient(), marginRemaining);
}
/// @dev updates market: pays funding and fetches freshest data from feed
/// @dev update is called every time market is interacted with
function update() public returns (Oracle.Data memory) {
// apply funding if at least one block has passed
uint256 timeElapsed = block.timestamp - timestampUpdateLast;
if (timeElapsed > 0) {
// calculate adjustments to oi due to funding
bool isLongOverweight = oiLong > oiShort;
uint256 oiOverweight = isLongOverweight ? oiLong : oiShort;
uint256 oiUnderweight = isLongOverweight ? oiShort : oiLong;
(oiOverweight, oiUnderweight) = oiAfterFunding(
oiOverweight,
oiUnderweight,
timeElapsed
);
// pay funding
oiLong = isLongOverweight ? oiOverweight : oiUnderweight;
oiShort = isLongOverweight ? oiUnderweight : oiOverweight;
// refresh last update data
timestampUpdateLast = block.timestamp;
}
// fetch new oracle data from feed
// applies sanity check in case of data manipulation
Oracle.Data memory data = IOverlayV1Feed(feed).latest();
require(dataIsValid(data), "OVLV1:!data");
// return the latest data from feed
return data;
}
/// @dev sanity check on data fetched from oracle in case of manipulation
/// @dev rough check that log price bounded by +/- priceDriftUpperLimit * dt
/// @dev when comparing priceMacro(now) vs priceMacro(now - macroWindow)
function dataIsValid(Oracle.Data memory data) public view returns (bool) {
// upper and lower limits are e**(+/- priceDriftUpperLimit * dt)
uint256 _dpUpperLimit = dpUpperLimit;
uint256 _dpLowerLimit = ONE.divDown(_dpUpperLimit);
// compare current price over macro window vs price over macro window
// one macro window in the past
uint256 priceNow = data.priceOverMacroWindow;
uint256 priceLast = data.priceOneMacroWindowAgo;
if (priceLast == 0 || priceNow == 0) {
// data is not valid if price is zero
return false;
}
// price is valid if within upper and lower limits on drift given
// time elapsed over one macro window
uint256 dp = priceNow.divUp(priceLast);
return (dp >= _dpLowerLimit && dp <= _dpUpperLimit);
}
/// @notice Current open interest after funding payments transferred
/// @notice from overweight oi side to underweight oi side
/// @dev The value of oiOverweight must be >= oiUnderweight
function oiAfterFunding(
uint256 oiOverweight,
uint256 oiUnderweight,
uint256 timeElapsed
) public view returns (uint256, uint256) {
uint256 oiTotal = oiOverweight + oiUnderweight;
uint256 oiImbalance = oiOverweight - oiUnderweight;
uint256 oiInvariant = oiUnderweight.mulUp(oiOverweight);
// If no OI or imbalance, no funding occurs. Handles div by zero case below
if (oiTotal == 0 || oiImbalance == 0) {
return (oiOverweight, oiUnderweight);
}
// draw down the imbalance by factor of e**(-2*k*t)
// but min to zero if pow = 2*k*t exceeds MAX_NATURAL_EXPONENT
uint256 fundingFactor;
uint256 pow = 2 * params.get(Risk.Parameters.K) * timeElapsed;
if (pow < MAX_NATURAL_EXPONENT) {
fundingFactor = ONE.divDown(pow.expUp()); // e**(-pow)
}
// Decrease total aggregate open interest (i.e. oiLong + oiShort)
// to compensate protocol for pro-rata share of imbalance liability
// OI_tot(t) = OI_tot(0) * \
// sqrt( 1 - (OI_imb(0)/OI_tot(0))**2 * (1 - e**(-4*k*t)) )
// Guaranteed 0 <= underRoot <= 1
uint256 oiImbFraction = oiImbalance.divDown(oiTotal);
uint256 underRoot = ONE -
oiImbFraction.mulDown(oiImbFraction).mulDown(
ONE - fundingFactor.mulDown(fundingFactor)
);
// oiTotalNow guaranteed <= oiTotalBefore (burn happens)
oiTotal = oiTotal.mulDown(underRoot.powDown(ONE / 2));
// Time decay imbalance: OI_imb(t) = OI_imb(0) * e**(-2*k*t)
// oiImbalanceNow guaranteed <= oiImbalanceBefore
oiImbalance = oiImbalance.mulDown(fundingFactor);
// overweight pays underweight
// use oiOver * oiUnder = invariant for oiUnderNow to avoid any
// potential overflow reverts
oiOverweight = (oiTotal + oiImbalance) / 2;
if (oiOverweight != 0) {
oiUnderweight = oiInvariant.divUp(oiOverweight);
}
return (oiOverweight, oiUnderweight);
}
/// @dev current notional cap with adjustments to lower
/// @dev cap in the event market has printed a lot in recent past
function capNotionalAdjustedForCircuitBreaker(uint256 cap) public view returns (uint256) {
// Adjust cap downward for circuit breaker. Use snapshotMinted
// but transformed to account for decay in magnitude of minted since
// last snapshot taken
Roller.Snapshot memory snapshot = snapshotMinted;
uint256 circuitBreakerWindow = params.get(Risk.Parameters.CircuitBreakerWindow);
snapshot = snapshot.transform(block.timestamp, circuitBreakerWindow, 0);
cap = circuitBreaker(snapshot, cap);
return cap;
}
/// @dev bound on notional cap from circuit breaker
/// @dev Three cases:
/// @dev 1. minted < 1x target amount over circuitBreakerWindow: return cap
/// @dev 2. minted > 2x target amount over last circuitBreakerWindow: return 0
/// @dev 3. minted between 1x and 2x target amount: return cap * (2 - minted/target)
function circuitBreaker(Roller.Snapshot memory snapshot, uint256 cap)
public
view
returns (uint256)
{
int256 minted = int256(snapshot.cumulative());
uint256 circuitBreakerMintTarget = params.get(Risk.Parameters.CircuitBreakerMintTarget);
if (minted <= int256(circuitBreakerMintTarget)) {
return cap;
} else if (minted >= 2 * int256(circuitBreakerMintTarget)) {
return 0;
}
// case 3 (circuit breaker adjustment downward)
uint256 adjustment = 2 * ONE - uint256(minted).divDown(circuitBreakerMintTarget);
return cap.mulDown(adjustment);
}
/// @dev current notional cap with adjustments to prevent
/// @dev front-running trade and back-running trade
function capNotionalAdjustedForBounds(Oracle.Data memory data, uint256 cap)
public
view
returns (uint256)
{
if (data.hasReserve) {
// Adjust cap downward if exceeds bounds from front run attack
cap = Math.min(cap, frontRunBound(data));
// Adjust cap downward if exceeds bounds from back run attack
cap = Math.min(cap, backRunBound(data));
}
return cap;
}
/// @dev bound on notional cap to mitigate front-running attack
/// @dev bound = lmbda * reserveInOvl
function frontRunBound(Oracle.Data memory data) public view returns (uint256) {
uint256 lmbda = params.get(Risk.Parameters.Lmbda);
return lmbda.mulDown(data.reserveOverMicroWindow);
}
/// @dev bound on notional cap to mitigate back-running attack
/// @dev bound = macroWindowInBlocks * reserveInOvl * 2 * delta
function backRunBound(Oracle.Data memory data) public view returns (uint256) {
uint256 averageBlockTime = params.get(Risk.Parameters.AverageBlockTime);
uint256 window = (data.macroWindow * ONE) / averageBlockTime;
uint256 delta = params.get(Risk.Parameters.Delta);
return delta.mulDown(data.reserveOverMicroWindow).mulDown(window).mulDown(2 * ONE);
}
/// @dev Returns the open interest in number of contracts for a given notional
/// @dev Uses _midFromFeed(data) price to calculate oi: OI = Q / P
function oiFromNotional(uint256 notional, uint256 midPrice) public view returns (uint256) {
return notional.divDown(midPrice);
}
/// @dev bid price given oracle data and recent volume
function bid(Oracle.Data memory data, uint256 volume) public view returns (uint256 bid_) {
bid_ = Math.min(data.priceOverMicroWindow, data.priceOverMacroWindow);
// add static spread (delta) and market impact (lmbda * volume)
uint256 delta = params.get(Risk.Parameters.Delta);
uint256 lmbda = params.get(Risk.Parameters.Lmbda);
uint256 pow = delta + lmbda.mulUp(volume);
require(pow < MAX_NATURAL_EXPONENT, "OVLV1:slippage>max");
bid_ = bid_.mulDown(ONE.divDown(pow.expUp())); // bid * e**(-pow)
}
/// @dev ask price given oracle data and recent volume
function ask(Oracle.Data memory data, uint256 volume) public view returns (uint256 ask_) {
ask_ = Math.max(data.priceOverMicroWindow, data.priceOverMacroWindow);
// add static spread (delta) and market impact (lmbda * volume)
uint256 delta = params.get(Risk.Parameters.Delta);
uint256 lmbda = params.get(Risk.Parameters.Lmbda);
uint256 pow = delta + lmbda.mulUp(volume);
require(pow < MAX_NATURAL_EXPONENT, "OVLV1:slippage>max");
ask_ = ask_.mulUp(pow.expUp()); // ask * e**(pow)
}
/// @dev mid price without impact/spread given oracle data and recent volume
/// @dev used for gas savings to avoid accessing storage for delta, lmbda
function _midFromFeed(Oracle.Data memory data) private view returns (uint256 mid_) {
mid_ = Math.average(data.priceOverMicroWindow, data.priceOverMacroWindow);
}
/// @dev Rolling volume adjustments on bid side to be used for market impact.
/// @dev Volume values are normalized with respect to cap
function _registerVolumeBid(
Oracle.Data memory data,
uint256 volume,
uint256 cap
) private returns (uint256) {
// save gas with snapshot in memory
Roller.Snapshot memory snapshot = snapshotVolumeBid;
int256 value = int256(volume.divUp(cap));
// calculates the decay in the rolling volume since last snapshot
// and determines new window to decay over
snapshot = snapshot.transform(block.timestamp, data.microWindow, value);
// store the transformed snapshot
snapshotVolumeBid = snapshot;
// return the cumulative volume
return uint256(snapshot.cumulative());
}
/// @dev Rolling volume adjustments on ask side to be used for market impact.
/// @dev Volume values are normalized with respect to cap
function _registerVolumeAsk(
Oracle.Data memory data,
uint256 volume,
uint256 cap
) private returns (uint256) {
// save gas with snapshot in memory
Roller.Snapshot memory snapshot = snapshotVolumeAsk;
int256 value = int256(volume.divUp(cap));
// calculates the decay in the rolling volume since last snapshot
// and determines new window to decay over
snapshot = snapshot.transform(block.timestamp, data.microWindow, value);
// store the transformed snapshot
snapshotVolumeAsk = snapshot;
// return the cumulative volume
return uint256(snapshot.cumulative());
}
/// @notice Rolling mint accumulator to be used for circuit breaker
/// @dev value > 0 registers a mint, value <= 0 registers a burn
function _registerMintOrBurn(int256 value) private returns (int256) {
// save gas with snapshot in memory
Roller.Snapshot memory snapshot = snapshotMinted;
// calculates the decay in the rolling amount minted since last snapshot
// and determines new window to decay over
uint256 circuitBreakerWindow = params.get(Risk.Parameters.CircuitBreakerWindow);
snapshot = snapshot.transform(block.timestamp, circuitBreakerWindow, value);
// store the transformed snapshot
snapshotMinted = snapshot;
// return the cumulative mint amount
int256 minted = snapshot.cumulative();
return minted;
}
/// @notice Sets the governance per-market risk parameter
function setRiskParam(Risk.Parameters name, uint256 value) external onlyFactory {
_checkRiskParam(name, value);
_cacheRiskCalc(name, value);
params.set(name, value);
}
/// @notice Checks the governance per-market risk parameter is valid
function _checkRiskParam(Risk.Parameters name, uint256 value) private {
// checks delta won't cause position to be immediately
// liquidatable given current leverage cap (capLeverage),
// liquidation fee rate (liquidationFeeRate), and
// maintenance margin fraction (maintenanceMarginFraction)
if (name == Risk.Parameters.Delta) {
uint256 _delta = value;
uint256 capLeverage = params.get(Risk.Parameters.CapLeverage);
uint256 maintenanceMarginFraction = params.get(
Risk.Parameters.MaintenanceMarginFraction
);
uint256 liquidationFeeRate = params.get(Risk.Parameters.LiquidationFeeRate);
require(
capLeverage <=
ONE.divDown(
2 * _delta + maintenanceMarginFraction.divDown(ONE - liquidationFeeRate)
),
"OVLV1: max lev immediately liquidatable"
);
}
// checks capLeverage won't cause position to be immediately
// liquidatable given current spread (delta),
// liquidation fee rate (liquidationFeeRate), and
// maintenance margin fraction (maintenanceMarginFraction)
if (name == Risk.Parameters.CapLeverage) {
uint256 _capLeverage = value;
uint256 delta = params.get(Risk.Parameters.Delta);
uint256 maintenanceMarginFraction = params.get(
Risk.Parameters.MaintenanceMarginFraction
);
uint256 liquidationFeeRate = params.get(Risk.Parameters.LiquidationFeeRate);
require(
_capLeverage <=
ONE.divDown(
2 * delta + maintenanceMarginFraction.divDown(ONE - liquidationFeeRate)
),
"OVLV1: max lev immediately liquidatable"
);
}
// checks maintenanceMarginFraction won't cause position
// to be immediately liquidatable given current spread (delta),
// liquidation fee rate (liquidationFeeRate),
// and leverage cap (capLeverage)
if (name == Risk.Parameters.MaintenanceMarginFraction) {
uint256 _maintenanceMarginFraction = value;
uint256 delta = params.get(Risk.Parameters.Delta);
uint256 capLeverage = params.get(Risk.Parameters.CapLeverage);
uint256 liquidationFeeRate = params.get(Risk.Parameters.LiquidationFeeRate);
require(
capLeverage <=
ONE.divDown(
2 * delta + _maintenanceMarginFraction.divDown(ONE - liquidationFeeRate)
),
"OVLV1: max lev immediately liquidatable"
);
}
// checks liquidationFeeRate won't cause position
// to be immediately liquidatable given current spread (delta),
// leverage cap (capLeverage), and
// maintenance margin fraction (maintenanceMarginFraction)
if (name == Risk.Parameters.LiquidationFeeRate) {
uint256 _liquidationFeeRate = value;
uint256 delta = params.get(Risk.Parameters.Delta);
uint256 capLeverage = params.get(Risk.Parameters.CapLeverage);
uint256 maintenanceMarginFraction = params.get(
Risk.Parameters.MaintenanceMarginFraction
);
require(
capLeverage <=
ONE.divDown(
2 * delta + maintenanceMarginFraction.divDown(ONE - _liquidationFeeRate)
),
"OVLV1: max lev immediately liquidatable"
);
}
// checks priceDriftUpperLimit won't cause pow() call in dataIsValid
// to exceed max
if (name == Risk.Parameters.PriceDriftUpperLimit) {
Oracle.Data memory data = IOverlayV1Feed(feed).latest();
uint256 _priceDriftUpperLimit = value;
require(
_priceDriftUpperLimit * data.macroWindow < MAX_NATURAL_EXPONENT,
"OVLV1: price drift exceeds max exp"
);
}
}
/// @notice Caches risk param calculations used in market contract
/// @notice for gas savings
function _cacheRiskCalc(Risk.Parameters name, uint256 value) private {
// caches calculations for dpUpperLimit
// = e**(priceDriftUpperLimit * data.macroWindow)
if (name == Risk.Parameters.PriceDriftUpperLimit) {
Oracle.Data memory data = IOverlayV1Feed(feed).latest();
uint256 _priceDriftUpperLimit = value;
uint256 pow = _priceDriftUpperLimit * data.macroWindow;
dpUpperLimit = pow.expUp(); // e**(pow)
}
}
}