This repository has been archived by the owner on Dec 10, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1
/
SwapUtils.sol
357 lines (311 loc) · 13.4 KB
/
SwapUtils.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
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;
import "../adl/AdlUtils.sol";
import "../data/DataStore.sol";
import "../event/EventEmitter.sol";
import "../oracle/Oracle.sol";
import "../pricing/SwapPricingUtils.sol";
import "../token/TokenUtils.sol";
import "../fee/FeeUtils.sol";
/**
* @title SwapUtils
* @dev Library for swap functions
*/
library SwapUtils {
using SafeCast for uint256;
using SafeCast for int256;
using Price for Price.Props;
using EventUtils for EventUtils.AddressItems;
using EventUtils for EventUtils.UintItems;
using EventUtils for EventUtils.IntItems;
using EventUtils for EventUtils.BoolItems;
using EventUtils for EventUtils.Bytes32Items;
using EventUtils for EventUtils.BytesItems;
using EventUtils for EventUtils.StringItems;
/**
* @param dataStore The contract that provides access to data stored on-chain.
* @param eventEmitter The contract that emits events.
* @param oracle The contract that provides access to price data from oracles.
* @param bank The contract providing the funds for the swap.
* @param key An identifying key for the swap.
* @param tokenIn The address of the token that is being swapped.
* @param amountIn The amount of the token that is being swapped.
* @param swapPathMarkets An array of market properties, specifying the markets in which the swap should be executed.
* @param minOutputAmount The minimum amount of tokens that should be received as part of the swap.
* @param receiver The address to which the swapped tokens should be sent.
* @param uiFeeReceiver The address of the ui fee receiver.
* @param shouldUnwrapNativeToken A boolean indicating whether the received tokens should be unwrapped from the wrapped native token (WNT) if they are wrapped.
*/
struct SwapParams {
DataStore dataStore;
EventEmitter eventEmitter;
Oracle oracle;
Bank bank;
bytes32 key;
address tokenIn;
uint256 amountIn;
Market.Props[] swapPathMarkets;
uint256 minOutputAmount;
address receiver;
address uiFeeReceiver;
bool shouldUnwrapNativeToken;
}
/**
* @param market The market in which the swap should be executed.
* @param tokenIn The address of the token that is being swapped.
* @param amountIn The amount of the token that is being swapped.
* @param receiver The address to which the swapped tokens should be sent.
* @param shouldUnwrapNativeToken A boolean indicating whether the received tokens should be unwrapped from the wrapped native token (WNT) if they are wrapped.
*/
struct _SwapParams {
Market.Props market;
address tokenIn;
uint256 amountIn;
address receiver;
bool shouldUnwrapNativeToken;
}
/**
* @param tokenOut The address of the token that is being received as part of the swap.
* @param tokenInPrice The price of the token that is being swapped.
* @param tokenOutPrice The price of the token that is being received as part of the swap.
* @param amountIn The amount of the token that is being swapped.
* @param amountOut The amount of the token that is being received as part of the swap.
* @param poolAmountOut The total amount of the token that is being received by all users in the swap pool.
*/
struct SwapCache {
address tokenOut;
Price.Props tokenInPrice;
Price.Props tokenOutPrice;
uint256 amountIn;
uint256 amountOut;
uint256 poolAmountOut;
}
event SwapReverted(string reason, bytes reasonBytes);
/**
* @dev Swaps a given amount of a given token for another token based on a
* specified swap path.
* @param params The parameters for the swap.
* @return A tuple containing the address of the token that was received as
* part of the swap and the amount of the received token.
*/
function swap(SwapParams memory params) external returns (address, uint256) {
if (params.swapPathMarkets.length == 0) {
if (params.amountIn < params.minOutputAmount) {
revert Errors.InsufficientOutputAmount(params.amountIn, params.minOutputAmount);
}
if (address(params.bank) != params.receiver) {
params.bank.transferOut(
params.tokenIn,
params.receiver,
params.amountIn,
params.shouldUnwrapNativeToken
);
}
return (params.tokenIn, params.amountIn);
}
if (address(params.bank) != params.swapPathMarkets[0].marketToken) {
params.bank.transferOut(
params.tokenIn,
params.swapPathMarkets[0].marketToken,
params.amountIn,
false
);
}
address tokenOut = params.tokenIn;
uint256 outputAmount = params.amountIn;
for (uint256 i; i < params.swapPathMarkets.length; i++) {
Market.Props memory market = params.swapPathMarkets[i];
bool flagExists = params.dataStore.getBool(Keys.swapPathMarketFlagKey(market.marketToken));
if (flagExists) {
revert Errors.DuplicatedMarketInSwapPath(market.marketToken);
}
params.dataStore.setBool(Keys.swapPathMarketFlagKey(market.marketToken), true);
uint256 nextIndex = i + 1;
address receiver;
if (nextIndex < params.swapPathMarkets.length) {
receiver = params.swapPathMarkets[nextIndex].marketToken;
} else {
receiver = params.receiver;
}
_SwapParams memory _params = _SwapParams(
market,
tokenOut,
outputAmount,
receiver,
i == params.swapPathMarkets.length - 1 ? params.shouldUnwrapNativeToken : false // only convert ETH on the last swap if needed
);
(tokenOut, outputAmount) = _swap(params, _params);
}
for (uint256 i; i < params.swapPathMarkets.length; i++) {
Market.Props memory market = params.swapPathMarkets[i];
params.dataStore.setBool(Keys.swapPathMarketFlagKey(market.marketToken), false);
}
if (outputAmount < params.minOutputAmount) {
revert Errors.InsufficientSwapOutputAmount(outputAmount, params.minOutputAmount);
}
return (tokenOut, outputAmount);
}
/**
* Performs a swap on a single market.
*
* @param params The parameters for the swap.
* @param _params The parameters for the swap on this specific market.
* @return The token and amount that was swapped.
*/
function _swap(SwapParams memory params, _SwapParams memory _params) internal returns (address, uint256) {
SwapCache memory cache;
if (_params.tokenIn != _params.market.longToken && _params.tokenIn != _params.market.shortToken) {
revert Errors.InvalidTokenIn(_params.tokenIn, _params.market.marketToken);
}
MarketUtils.validateSwapMarket(_params.market);
cache.tokenOut = MarketUtils.getOppositeToken(_params.tokenIn, _params.market);
cache.tokenInPrice = params.oracle.getLatestPrice(_params.tokenIn);
cache.tokenOutPrice = params.oracle.getLatestPrice(cache.tokenOut);
SwapPricingUtils.SwapFees memory fees = SwapPricingUtils.getSwapFees(
params.dataStore,
_params.market.marketToken,
_params.amountIn,
params.uiFeeReceiver
);
FeeUtils.incrementClaimableFeeAmount(
params.dataStore,
params.eventEmitter,
_params.market.marketToken,
_params.tokenIn,
fees.feeReceiverAmount,
Keys.SWAP_FEE
);
FeeUtils.incrementClaimableUiFeeAmount(
params.dataStore,
params.eventEmitter,
params.uiFeeReceiver,
_params.market.marketToken,
_params.tokenIn,
fees.uiFeeAmount,
Keys.UI_SWAP_FEE
);
int256 priceImpactUsd = SwapPricingUtils.getPriceImpactUsd(
SwapPricingUtils.GetPriceImpactUsdParams(
params.dataStore,
_params.market,
_params.tokenIn,
cache.tokenOut,
cache.tokenInPrice.midPrice(),
cache.tokenOutPrice.midPrice(),
(fees.amountAfterFees * cache.tokenInPrice.midPrice()).toInt256(),
-(fees.amountAfterFees * cache.tokenInPrice.midPrice()).toInt256()
)
);
if (priceImpactUsd > 0) {
// when there is a positive price impact factor, additional tokens from the swap impact pool
// are withdrawn for the user
// for example, if 50,000 USDC is swapped out and there is a positive price impact
// an additional 100 USDC may be sent to the user
// the swap impact pool is decreased by the used amount
cache.amountIn = fees.amountAfterFees;
// round amountOut down
cache.amountOut = cache.amountIn * cache.tokenInPrice.min / cache.tokenOutPrice.max;
cache.poolAmountOut = cache.amountOut;
int256 positiveImpactAmount = MarketUtils.applySwapImpactWithCap(
params.dataStore,
params.eventEmitter,
_params.market.marketToken,
cache.tokenOut,
cache.tokenOutPrice,
priceImpactUsd
);
cache.amountOut += positiveImpactAmount.toUint256();
} else {
// when there is a negative price impact factor,
// less of the input amount is sent to the pool
// for example, if 10 ETH is swapped in and there is a negative price impact
// only 9.995 ETH may be swapped in
// the remaining 0.005 ETH will be stored in the swap impact pool
int256 negativeImpactAmount = MarketUtils.applySwapImpactWithCap(
params.dataStore,
params.eventEmitter,
_params.market.marketToken,
_params.tokenIn,
cache.tokenInPrice,
priceImpactUsd
);
cache.amountIn = fees.amountAfterFees - (-negativeImpactAmount).toUint256();
cache.amountOut = cache.amountIn * cache.tokenInPrice.min / cache.tokenOutPrice.max;
cache.poolAmountOut = cache.amountOut;
}
// the amountOut value includes the positive price impact amount
if (_params.receiver != _params.market.marketToken) {
MarketToken(payable(_params.market.marketToken)).transferOut(
cache.tokenOut,
_params.receiver,
cache.amountOut,
_params.shouldUnwrapNativeToken
);
}
MarketUtils.applyDeltaToPoolAmount(
params.dataStore,
params.eventEmitter,
_params.market.marketToken,
_params.tokenIn,
(cache.amountIn + fees.feeAmountForPool).toInt256()
);
// the poolAmountOut excludes the positive price impact amount
// as that is deducted from the swap impact pool instead
MarketUtils.applyDeltaToPoolAmount(
params.dataStore,
params.eventEmitter,
_params.market.marketToken,
cache.tokenOut,
-cache.poolAmountOut.toInt256()
);
MarketUtils.MarketPrices memory prices = MarketUtils.MarketPrices(
params.oracle.getLatestPrice(_params.market.indexToken),
_params.tokenIn == _params.market.longToken ? cache.tokenInPrice : cache.tokenOutPrice,
_params.tokenIn == _params.market.shortToken ? cache.tokenInPrice : cache.tokenOutPrice
);
MarketUtils.validatePoolAmount(
params.dataStore,
_params.market,
_params.tokenIn
);
// for single token markets cache.tokenOut will always equal _params.market.longToken
// so only the reserve for longs will be validated
// swaps should be disabled for single token markets so this should not be an issue
MarketUtils.validateReserve(
params.dataStore,
_params.market,
prices,
cache.tokenOut == _params.market.longToken
);
MarketUtils.validateMaxPnl(
params.dataStore,
_params.market,
prices,
_params.tokenIn == _params.market.longToken ? Keys.MAX_PNL_FACTOR_FOR_DEPOSITS : Keys.MAX_PNL_FACTOR_FOR_WITHDRAWALS,
cache.tokenOut == _params.market.shortToken ? Keys.MAX_PNL_FACTOR_FOR_WITHDRAWALS : Keys.MAX_PNL_FACTOR_FOR_DEPOSITS
);
SwapPricingUtils.emitSwapInfo(
params.eventEmitter,
params.key,
_params.market.marketToken,
_params.receiver,
_params.tokenIn,
cache.tokenOut,
cache.tokenInPrice.min,
cache.tokenOutPrice.max,
_params.amountIn,
cache.amountIn,
cache.amountOut,
priceImpactUsd
);
SwapPricingUtils.emitSwapFeesCollected(
params.eventEmitter,
_params.market.marketToken,
_params.tokenIn,
cache.tokenInPrice.min,
"swap",
fees
);
return (cache.tokenOut, cache.amountOut);
}
}