-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathPerpetual.sol
181 lines (147 loc) · 6.68 KB
/
Perpetual.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
/*
Copyright 2022 JOJO Exchange
SPDX-License-Identifier: BUSL-1.1
*/
pragma solidity ^0.8.19;
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/math/SafeCast.sol";
import "./interfaces/IDealer.sol";
import "./interfaces/IPerpetual.sol";
import "./libraries/SignedDecimalMath.sol";
contract Perpetual is Ownable, IPerpetual {
using SignedDecimalMath for int256;
// ========== storage ==========
/*
We use int128 to store paper and reduced credit,
so that we could store balance in a single slot.
This trick can help us saving gas.
int128 can support size of 1.7E38, which is enough
for most transactions. But other than storing paper
and reduced credit values, we use int256 to achieve
higher accuracy of calculation.
Normally, paper amount will be a 1e18 based decimal.
*/
struct balance {
int128 paper;
int128 reducedCredit;
}
mapping(address => balance) balanceMap;
int256 fundingRate;
// ========== events ==========
event BalanceChange(address indexed trader, int256 paperChange, int256 creditChange);
event UpdateFundingRate(int256 oldFundingRate, int256 newFundingRate);
// ========== constructor ==========
constructor(address _owner) Ownable() {
transferOwnership(_owner);
}
// ========== balance related ==========
/*
We store "reducedCredit" instead of credit itself.
So that after funding rate is updated, the credit values will be
updated without any extra storage write.
credit = (paper * fundingRate) + reducedCredit
FundingRate here is a little different from what it means at CEX.
FundingRate is a cumulative value. Its absolute value doesn't mean
anything and only the changes (due to funding updates) matter.
e.g. If the fundingRate increases by 5 at a certain update,
then you will receive 5 credit for every paper you long.
And you will be charged 5 credit for every paper you short.
*/
/// @inheritdoc IPerpetual
function balanceOf(address trader) external view returns (int256 paper, int256 credit) {
paper = int256(balanceMap[trader].paper);
credit = paper.decimalMul(fundingRate) + int256(balanceMap[trader].reducedCredit);
}
function updateFundingRate(int256 newFundingRate) external onlyOwner {
int256 oldFundingRate = fundingRate;
fundingRate = newFundingRate;
emit UpdateFundingRate(oldFundingRate, newFundingRate);
}
function getFundingRate() external view returns (int256) {
return fundingRate;
}
// ========== trade ==========
/// @inheritdoc IPerpetual
function trade(bytes calldata tradeData) external {
(address[] memory traderList, int256[] memory paperChangeList, int256[] memory creditChangeList) =
IDealer(owner()).approveTrade(msg.sender, tradeData);
for (uint256 i = 0; i < traderList.length;) {
_settle(traderList[i], paperChangeList[i], creditChangeList[i]);
unchecked {
++i;
}
}
require(IDealer(owner()).isAllSafe(traderList), "TRADER_NOT_SAFE");
}
// ========== liquidation ==========
/// @inheritdoc IPerpetual
function liquidate(
address liquidator,
address liquidatedTrader,
int256 requestPaper,
int256 expectCredit
)
external
returns (int256 liqtorPaperChange, int256 liqtorCreditChange)
{
// liqed => liquidated trader, who faces the risk of liquidation.
// liqtor => liquidator, who takes over the trader's position.
int256 liqedPaperChange;
int256 liqedCreditChange;
(liqtorPaperChange, liqtorCreditChange, liqedPaperChange, liqedCreditChange) =
IDealer(owner()).requestLiquidation(msg.sender, liquidator, liquidatedTrader, requestPaper);
// expected price = expectCredit/requestPaper * -1
// execute price = liqtorCreditChange/liqtorPaperChange * -1
if (liqtorPaperChange < 0) {
// open short, execute price >= expected price
// liqtorCreditChange/liqtorPaperChange * -1 >= expectCredit/requestPaper * -1
// liqtorCreditChange/liqtorPaperChange <= expectCredit/requestPaper
// liqtorCreditChange*requestPaper <= expectCredit*liqtorPaperChange
require(
liqtorCreditChange * requestPaper <= expectCredit * liqtorPaperChange, "LIQUIDATION_PRICE_PROTECTION"
);
} else {
// open long, execute price <= expected price
// liqtorCreditChange/liqtorPaperChange * -1 <= expectCredit/requestPaper * -1
// liqtorCreditChange/liqtorPaperChange >= expectCredit/requestPaper
// liqtorCreditChange*requestPaper >= expectCredit*liqtorPaperChange
require(
liqtorCreditChange * requestPaper >= expectCredit * liqtorPaperChange, "LIQUIDATION_PRICE_PROTECTION"
);
}
_settle(liquidatedTrader, liqedPaperChange, liqedCreditChange);
_settle(liquidator, liqtorPaperChange, liqtorCreditChange);
require(IDealer(owner()).isSafe(liquidator), "LIQUIDATOR_NOT_SAFE");
if (balanceMap[liquidatedTrader].paper == 0) {
IDealer(owner()).handleBadDebt(liquidatedTrader);
}
}
// ========== settlement ==========
/*
Remember the fomula?
credit = (paper * fundingRate) + reducedCredit
So we have...
reducedCredit = credit - (paper * fundingRate)
When you update the balance, you need to first calculate the credit,
and then calculate and store the reducedCredit.
*/
function _settle(address trader, int256 paperChange, int256 creditChange) internal {
bool isNewPosition = balanceMap[trader].paper == 0;
int256 rate = fundingRate; // gas saving
int256 credit =
int256(balanceMap[trader].paper).decimalMul(rate) + int256(balanceMap[trader].reducedCredit) + creditChange;
int128 newPaper = balanceMap[trader].paper + SafeCast.toInt128(paperChange);
int128 newReducedCredit = SafeCast.toInt128(credit - int256(newPaper).decimalMul(rate));
balanceMap[trader].paper = newPaper;
balanceMap[trader].reducedCredit = newReducedCredit;
emit BalanceChange(trader, paperChange, creditChange);
if (isNewPosition) {
IDealer(owner()).openPosition(trader);
}
if (newPaper == 0) {
// realize PNL
IDealer(owner()).realizePnl(trader, balanceMap[trader].reducedCredit);
balanceMap[trader].reducedCredit = 0;
}
}
}