-
Notifications
You must be signed in to change notification settings - Fork 50
/
FeeUtil.sol
339 lines (298 loc) · 13.2 KB
/
FeeUtil.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
//SPDX-License-Identifier: MIT
pragma solidity >=0.8.11 <0.9.0;
import "@synthetixio/core-contracts/contracts/utils/SafeCast.sol";
import "@synthetixio/core-contracts/contracts/utils/DecimalMath.sol";
import "../storage/SpotMarketFactory.sol";
import "../storage/FeeConfiguration.sol";
library FeeUtil {
using SpotMarketFactory for SpotMarketFactory.Data;
using SafeCastU256 for uint256;
using SafeCastI256 for int256;
using DecimalMath for uint256;
using DecimalMath for int256;
/**
* @dev Calculates fees then runs the fees through a fee collector before returning the computed data.
*/
function processFees(
uint128 marketId,
address transactor,
uint256 usdAmount,
SpotMarketFactory.TransactionType transactionType
) internal returns (uint256 amountUsable, int256 totalFees, uint collectedFees) {
(amountUsable, totalFees) = calculateFees(marketId, transactor, usdAmount, transactionType);
// TODO: negative fees are ignored. Verify this.
if (totalFees > 0) {
collectedFees = collectFees(marketId, totalFees.toUint());
}
}
/**
* @dev Calculates fees for a given transaction type.
*/
function calculateFees(
uint128 marketId,
address transactor,
uint256 usdAmount,
SpotMarketFactory.TransactionType transactionType
) internal returns (uint256 amountUsable, int256 feesCollected) {
FeeConfiguration.Data storage feeConfiguration = FeeConfiguration.load(marketId);
if (
transactionType == SpotMarketFactory.TransactionType.BUY ||
transactionType == SpotMarketFactory.TransactionType.ASYNC_BUY
) {
(amountUsable, feesCollected) = calculateBuyFees(
feeConfiguration,
transactor,
marketId,
usdAmount,
transactionType == SpotMarketFactory.TransactionType.ASYNC_BUY
);
} else if (
transactionType == SpotMarketFactory.TransactionType.SELL ||
transactionType == SpotMarketFactory.TransactionType.ASYNC_SELL
) {
(amountUsable, feesCollected) = calculateSellFees(
feeConfiguration,
transactor,
marketId,
usdAmount,
transactionType == SpotMarketFactory.TransactionType.ASYNC_SELL
);
} else if (transactionType == SpotMarketFactory.TransactionType.WRAP) {
(amountUsable, feesCollected) = calculateWrapFees(feeConfiguration, usdAmount);
} else if (transactionType == SpotMarketFactory.TransactionType.UNWRAP) {
(amountUsable, feesCollected) = calculateUnwrapFees(feeConfiguration, usdAmount);
} else {
amountUsable = usdAmount;
feesCollected = 0;
}
}
/**
* @dev Calculates wrap fees based on the wrapFixedFee.
*/
function calculateWrapFees(
FeeConfiguration.Data storage feeConfiguration,
uint256 amount
) internal view returns (uint amountUsable, int feesCollected) {
(amountUsable, feesCollected) = _applyFees(amount, feeConfiguration.wrapFixedFee);
}
/**
* @dev Calculates wrap fees based on the unwrapFixedFee.
*/
function calculateUnwrapFees(
FeeConfiguration.Data storage feeConfiguration,
uint256 amount
) internal view returns (uint amountUsable, int feesCollected) {
(amountUsable, feesCollected) = _applyFees(amount, feeConfiguration.unwrapFixedFee);
}
/**
* @dev Calculates fees for a buy transaction.
*
* Fees are calculated as follows:
*
* 1. Utilization fee (bips): The utilization fee is a fee that's applied based on the ratio of delegated collateral to total outstanding synth exposure.
* 2. Skew fee (bips): The skew fee is a fee that's applied based on the ratio of outstanding synths to the skew scale.
* 3. Fixed fee (bips): The fixed fee is a fee that's applied to every transaction.
*/
function calculateBuyFees(
FeeConfiguration.Data storage feeConfiguration,
address transactor,
uint128 marketId,
uint256 amount,
bool async
) internal returns (uint amountUsable, int feesCollected) {
uint utilizationFee = calculateUtilizationRateFee(
feeConfiguration,
marketId,
amount,
SpotMarketFactory.TransactionType.BUY
);
int skewFee = calculateSkewFee(
feeConfiguration,
marketId,
amount,
SpotMarketFactory.TransactionType.BUY
);
uint fixedFee = async
? feeConfiguration.asyncFixedFee
: _getAtomicFixedFee(feeConfiguration, transactor);
int totalFees = utilizationFee.toInt() + skewFee + fixedFee.toInt();
(amountUsable, feesCollected) = _applyFees(amount, totalFees);
}
/**
* @dev Calculates fees for a sell transaction.
*
* Fees are calculated as follows:
*
* 1. Skew fee (bips): The skew fee is a fee that's applied based on the ratio of outstanding synths to the skew scale.
* When a sell trade is executed, the skew fee is applied as a negative value to create incentive to bring market to equilibrium.
* 3. Fixed fee (bips): The fixed fee is a fee that's applied to every transaction.
*/
function calculateSellFees(
FeeConfiguration.Data storage feeConfiguration,
address transactor,
uint128 marketId,
uint256 amount,
bool async
) internal returns (uint amountUsable, int feesCollected) {
int skewFee = calculateSkewFee(
feeConfiguration,
marketId,
amount,
SpotMarketFactory.TransactionType.SELL
);
uint fixedFee = async
? feeConfiguration.asyncFixedFee
: _getAtomicFixedFee(feeConfiguration, transactor);
int totalFees = skewFee + fixedFee.toInt();
(amountUsable, feesCollected) = _applyFees(amount, totalFees);
}
/**
* @dev Calculates skew fee
*
* If no skewScale is set, then the fee is 0
* The skew fee is determined based on the ratio of outstanding synth value to the skew scale value.
* Example:
* Skew scale set to 1000 snxETH
* Before fill outstanding snxETH (minus any wrapped collateral): 100 snxETH
* If buy trade:
* - user is buying 10 ETH
* - skew fee = (100 / 1000 + 110 / 1000) / 2 = 0.105 = 10.5% = 1005 bips
* sell trade would be the same, except -10.5% fee would be applied incentivizing user to sell which brings market closer to 0 skew.
*/
function calculateSkewFee(
FeeConfiguration.Data storage feeConfiguration,
uint128 marketId,
uint amount,
SpotMarketFactory.TransactionType transactionType
) internal returns (int skewFee) {
if (feeConfiguration.skewScale == 0) {
return 0;
}
bool isBuyTrade = transactionType == SpotMarketFactory.TransactionType.BUY ||
transactionType == SpotMarketFactory.TransactionType.ASYNC_BUY;
bool isSellTrade = transactionType == SpotMarketFactory.TransactionType.SELL ||
transactionType == SpotMarketFactory.TransactionType.ASYNC_SELL;
if (!isBuyTrade && !isSellTrade) {
return 0;
}
uint collateralPrice = Price.getCurrentPrice(marketId, transactionType);
uint skewScaleValue = feeConfiguration.skewScale.mulDecimal(collateralPrice);
uint totalSynthValue = SynthUtil.getToken(marketId).totalSupply().mulDecimal(
collateralPrice
);
Wrapper.Data storage wrapper = Wrapper.load(marketId);
uint wrappedMarketCollateral = 0;
if (wrapper.wrappingEnabled) {
wrappedMarketCollateral = IMarketCollateralModule(SpotMarketFactory.load().synthetix)
.getMarketCollateralAmount(marketId, wrapper.collateralType)
.mulDecimal(collateralPrice);
}
uint initialSkew = totalSynthValue - wrappedMarketCollateral;
uint initialSkewAdjustment = initialSkew.divDecimal(skewScaleValue);
uint skewAfterFill = initialSkew;
// TODO: when the Adjustment after fill is calculated, does it take into account the Adjustments collected for the trade?
if (isBuyTrade) {
skewAfterFill += amount;
} else if (isSellTrade) {
skewAfterFill -= amount;
}
uint skewAfterFillAdjustment = skewAfterFill.divDecimal(skewScaleValue);
int skewAdjustmentAveragePercentage = (skewAfterFillAdjustment.toInt() +
initialSkewAdjustment.toInt()) / 2;
skewFee = isSellTrade
? skewAdjustmentAveragePercentage * -1
: skewAdjustmentAveragePercentage;
}
/**
* @dev Calculates utilization rate fee
*
* If no utilizationFeeRate is set, then the fee is 0
* The utilization rate fee is determined based on the ratio of outstanding synth value to the delegated collateral to the market.
* Example:
* Utilization fee rate set to 0.1%
* Total delegated collateral value: $1000
* Total outstanding synth value = $1100
* User buys $100 worth of synths
* Before fill utilization rate: 1100 / 1000 = 110%
* After fill utilization rate: 1200 / 1000 = 120%
* Utilization Rate Delta = 120 - 110 = 10% delta
* Fee charged = 10 * 0.001 (0.1%) = 1%
*
* TODO: verify this calculation with the team
* TODO: utilization fee should be average of before and after fill
*/
function calculateUtilizationRateFee(
FeeConfiguration.Data storage feeConfiguration,
uint128 marketId,
uint amount,
SpotMarketFactory.TransactionType transactionType
) internal view returns (uint utilFee) {
if (feeConfiguration.utilizationFeeRate == 0) {
return 0;
}
AsyncOrderConfiguration.Data storage asyncOrderConfiguration = AsyncOrderConfiguration.load(
marketId
);
uint delegatedCollateral = IMarketManagerModule(SpotMarketFactory.load().synthetix)
.getMarketCollateral(marketId);
uint totalBalance = (SynthUtil.getToken(marketId).totalSupply().toInt() +
asyncOrderConfiguration.asyncUtilizationDelta).toUint();
uint totalValueBeforeFill = totalBalance.mulDecimal(
Price.getCurrentPrice(marketId, transactionType)
);
uint totalValueAfterFill = totalValueBeforeFill + amount;
// utilization is below 100%
if (delegatedCollateral > totalValueAfterFill) {
return 0;
} else {
uint preUtilization = totalValueBeforeFill.divDecimal(delegatedCollateral);
// use 100% utilization if pre-fill utilization was less than 100%
// no fees charged below 100% utilization
uint preUtilizationDelta = preUtilization > 1e18 ? preUtilization - 1e18 : 0;
uint postUtilization = totalValueAfterFill.divDecimal(delegatedCollateral);
uint postUtilizationDelta = postUtilization - 1e18;
// utilization is represented as the # of percentage points above 100%
uint utilization = (preUtilizationDelta + postUtilizationDelta).mulDecimal(100e18) / 2;
utilFee = utilization.mulDecimal(feeConfiguration.utilizationFeeRate);
}
}
/**
* @dev Runs the calculated fees through the Fee collector if it exists.
*
* The rest of the fees not collected by fee collector is deposited into the market manager
* If no fee collector is specified, all fees are deposited into the market manager to help staker c-ratios.
*
*/
function collectFees(uint128 marketId, uint totalFees) internal returns (uint collectedFees) {
IFeeCollector feeCollector = FeeConfiguration.load(marketId).feeCollector;
SpotMarketFactory.Data storage store = SpotMarketFactory.load();
if (address(feeCollector) != address(0)) {
store.usdToken.approve(address(feeCollector), totalFees);
uint previousUsdBalance = store.usdToken.balanceOf(address(this));
feeCollector.collectFees(marketId, totalFees);
uint currentUsdBalance = store.usdToken.balanceOf(address(this));
collectedFees = previousUsdBalance - currentUsdBalance;
store.usdToken.approve(address(feeCollector), 0);
}
uint feesToDeposit = totalFees - collectedFees;
store.depositToMarketManager(marketId, feesToDeposit);
}
function _applyFees(
uint amount,
int fees // 18 decimals
) private pure returns (uint amountUsable, int feesCollected) {
feesCollected = fees.mulDecimal(amount.toInt());
amountUsable = (amount.toInt() - feesCollected).toUint();
}
function _getAtomicFixedFee(
FeeConfiguration.Data storage feeConfiguration,
address transactor
) private view returns (uint) {
// TODO: add to readme, can't set transactor's value to zero
// talk to afif
return
feeConfiguration.atomicFixedFeeOverrides[transactor] > 0
? feeConfiguration.atomicFixedFeeOverrides[transactor]
: feeConfiguration.atomicFixedFee;
}
}