Skip to content

Commit 35558ca

Browse files
committed
exercises
1 parent 83dd7ad commit 35558ca

File tree

13 files changed

+829
-11
lines changed

13 files changed

+829
-11
lines changed
Lines changed: 368 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,368 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity 0.8.26;
3+
4+
import {console} from "forge-std/Test.sol";
5+
import {IERC20} from "../../interfaces/IERC20.sol";
6+
import {IExchangeRouter} from "../../interfaces/IExchangeRouter.sol";
7+
import {IDataStore} from "../../interfaces/IDataStore.sol";
8+
import {IReader} from "../../interfaces/IReader.sol";
9+
import {Keys} from "../../lib/Keys.sol";
10+
import {Math} from "../../lib/Math.sol";
11+
import {Order} from "../../types/Order.sol";
12+
import {Position} from "../../types/Position.sol";
13+
import {MarketUtils} from "../../types/MarketUtils.sol";
14+
import {Price} from "../../types/Price.sol";
15+
import {ReaderPositionUtils} from "../../types/ReaderPositionUtils.sol";
16+
import {IBaseOrderUtils} from "../../types/IBaseOrderUtils.sol";
17+
import {Oracle} from "../../lib/Oracle.sol";
18+
import "../../Constants.sol";
19+
20+
abstract contract GmxHelper {
21+
IDataStore constant dataStore = IDataStore(DATA_STORE);
22+
IExchangeRouter constant exchangeRouter = IExchangeRouter(EXCHANGE_ROUTER);
23+
IReader constant reader = IReader(READER);
24+
// Note: both long and short token price must return 8 decimals (1e8 = 1 USD)
25+
uint256 private constant CHAINLINK_MULTIPLIER = 1e8;
26+
uint256 private constant CHAINLINK_DECIMALS = 8;
27+
28+
IERC20 public immutable marketToken;
29+
IERC20 public immutable longToken;
30+
IERC20 public immutable shortToken;
31+
uint256 public immutable longTokenDecimals;
32+
uint256 public immutable shortTokenDecimals;
33+
address public immutable chainlinkLongToken;
34+
address public immutable chainlinkShortToken;
35+
Oracle immutable oracle;
36+
37+
constructor(
38+
address _marketToken,
39+
address _longToken,
40+
address _shortToken,
41+
address _chainlinkLongToken,
42+
address _chainlinkShortToken,
43+
address _oracle
44+
) {
45+
marketToken = IERC20(_marketToken);
46+
longToken = IERC20(_longToken);
47+
shortToken = IERC20(_shortToken);
48+
49+
longTokenDecimals = uint256(longToken.decimals());
50+
shortTokenDecimals = uint256(shortToken.decimals());
51+
require(
52+
longTokenDecimals + CHAINLINK_DECIMALS <= 30,
53+
"long + chainlink decimals > 30"
54+
);
55+
require(
56+
shortTokenDecimals + CHAINLINK_DECIMALS <= 30,
57+
"short + chainlink decimals > 30"
58+
);
59+
60+
chainlinkLongToken = _chainlinkLongToken;
61+
chainlinkShortToken = _chainlinkShortToken;
62+
oracle = Oracle(_oracle);
63+
}
64+
65+
function getPositionKey() internal view returns (bytes32 positionKey) {
66+
return Position.getPositionKey({
67+
account: address(this),
68+
market: address(marketToken),
69+
collateralToken: address(longToken),
70+
isLong: false
71+
});
72+
}
73+
74+
function getPosition(bytes32 positionKey)
75+
internal
76+
view
77+
returns (Position.Props memory)
78+
{
79+
return reader.getPosition(address(dataStore), positionKey);
80+
}
81+
82+
// Returns collateral amount locked in the current position
83+
function getPositionCollateralAmount() internal view returns (uint256) {
84+
bytes32 positionKey = getPositionKey();
85+
Position.Props memory position = getPosition(positionKey);
86+
return position.numbers.collateralAmount;
87+
}
88+
89+
// Returns the max callback gas limit used for calling a callback contract
90+
// once a order is executed.
91+
function getMaxCallbackGasLimit() internal view returns (uint256) {
92+
return dataStore.getUint(Keys.MAX_CALLBACK_GAS_LIMIT);
93+
}
94+
95+
96+
// Returns position collateral amount + profit and loss of the position in terms of the collateral token
97+
function getPositionWithPnlInToken() internal view returns (int256) {
98+
bytes32 positionKey = getPositionKey();
99+
Position.Props memory position = getPosition(positionKey);
100+
101+
if (
102+
position.numbers.sizeInUsd == 0
103+
|| position.numbers.collateralAmount == 0
104+
) {
105+
return 0;
106+
}
107+
108+
uint256 longTokenPrice = oracle.getPrice(chainlinkLongToken);
109+
uint256 shortTokenPrice = oracle.getPrice(chainlinkShortToken);
110+
111+
// +/- 0.1% of current prices of the long token
112+
uint256 minLongTokenPrice = longTokenPrice
113+
* 10 ** (30 - CHAINLINK_DECIMALS - longTokenDecimals) * 999 / 1000;
114+
uint256 maxLongTokenPrice = longTokenPrice
115+
* 10 ** (30 - CHAINLINK_DECIMALS - longTokenDecimals) * 1001 / 1000;
116+
117+
require(minLongTokenPrice > 0, "min long token price = 0");
118+
require(maxLongTokenPrice > 0, "max long token price = 0");
119+
120+
MarketUtils.MarketPrices memory prices = MarketUtils.MarketPrices({
121+
indexTokenPrice: Price.Props({
122+
min: minLongTokenPrice,
123+
max: maxLongTokenPrice
124+
}),
125+
longTokenPrice: Price.Props({
126+
min: minLongTokenPrice,
127+
max: maxLongTokenPrice
128+
}),
129+
shortTokenPrice: Price.Props({
130+
min: shortTokenPrice
131+
* 10 ** (30 - CHAINLINK_DECIMALS - shortTokenDecimals) * 999 / 1000,
132+
max: shortTokenPrice
133+
* 10 ** (30 - CHAINLINK_DECIMALS - shortTokenDecimals) * 1001 / 1000
134+
})
135+
});
136+
137+
ReaderPositionUtils.PositionInfo memory info = reader.getPositionInfo({
138+
dataStore: address(dataStore),
139+
referralStorage: REFERRAL_STORAGE,
140+
positionKey: positionKey,
141+
prices: prices,
142+
// Use current position size for size delta
143+
sizeDeltaUsd: 0,
144+
uiFeeReceiver: address(0),
145+
usePositionSizeAsSizeDeltaUsd: true
146+
});
147+
148+
int256 collateralUsd =
149+
Math.toInt256(position.numbers.collateralAmount * minLongTokenPrice);
150+
int256 collateralCostUsd =
151+
Math.toInt256(info.fees.totalCostAmount * minLongTokenPrice);
152+
153+
int256 remainingCollateralUsd =
154+
collateralUsd + info.pnlAfterPriceImpactUsd - collateralCostUsd;
155+
156+
int256 remainingCollateral =
157+
remainingCollateralUsd / Math.toInt256(minLongTokenPrice);
158+
159+
return remainingCollateral;
160+
}
161+
162+
// Task 1: Calculate position size delta
163+
function getSizeDeltaUsd(
164+
// Long token price from Chainlink (1e8 = 1 USD)
165+
uint256 longTokenPrice,
166+
// Current position size
167+
uint256 sizeInUsd,
168+
// Current collateral amount locked in the position
169+
uint256 collateralAmount,
170+
// Long token amount to add or remove
171+
uint256 longTokenAmount,
172+
// True for market increase
173+
bool isIncrease
174+
) internal view returns (uint256 sizeDeltaUsd) {
175+
// Calculate sizeDeltaUsd so that new position's leverage to close to 1
176+
if (isIncrease) {
177+
// new position size = long token price * new collateral amount
178+
// new collateral amount = position.collateralAmount + longTokenAmount
179+
// sizeDeltaUsd = new position size - position.sizeInUsd
180+
uint256 newCollateralAmount = collateralAmount + longTokenAmount;
181+
uint256 newPositionSizeInUsd = newCollateralAmount * longTokenPrice
182+
* 10 ** (30 - longTokenDecimals - CHAINLINK_DECIMALS);
183+
if (newPositionSizeInUsd > sizeInUsd) {
184+
sizeDeltaUsd = newPositionSizeInUsd - sizeInUsd;
185+
}
186+
} else {
187+
// new position size = long token price * new collateral amount
188+
// new collateral amount = position.collateralAmount - longTokenAmount
189+
// sizeDeltaUsd = new position size - position.sizeInUsd
190+
uint256 newCollateralAmount = collateralAmount - longTokenAmount;
191+
uint256 newPositionSizeInUsd = newCollateralAmount * longTokenPrice
192+
* 10 ** (30 - longTokenDecimals - CHAINLINK_DECIMALS);
193+
if (sizeInUsd > newPositionSizeInUsd) {
194+
sizeDeltaUsd = sizeInUsd - newPositionSizeInUsd;
195+
}
196+
}
197+
}
198+
199+
// Task 2: Create market increase order
200+
function createIncreaseShortPositionOrder(
201+
// Execution fee to send to the order vault
202+
uint256 executionFee,
203+
// Long token amount to add to the current position
204+
uint256 longTokenAmount
205+
) internal returns (bytes32 orderKey) {
206+
uint256 longTokenPrice = oracle.getPrice(chainlinkLongToken);
207+
bytes32 positionKey = getPositionKey();
208+
Position.Props memory position = getPosition(positionKey);
209+
210+
// Task 2.1 - Calculate position size delta
211+
uint256 sizeDeltaUsd = getSizeDeltaUsd({
212+
longTokenPrice: longTokenPrice,
213+
sizeInUsd: position.numbers.sizeInUsd,
214+
collateralAmount: position.numbers.collateralAmount,
215+
longTokenAmount: longTokenAmount,
216+
isIncrease: true
217+
});
218+
219+
// Task 2.2 - Create market increase order
220+
221+
// 90% of current long price
222+
uint256 acceptablePrice =
223+
longTokenPrice * 1e12 / CHAINLINK_MULTIPLIER * 90 / 100;
224+
225+
exchangeRouter.sendWnt{value: executionFee}({
226+
receiver: ORDER_VAULT,
227+
amount: executionFee
228+
});
229+
230+
longToken.approve(ROUTER, longTokenAmount);
231+
exchangeRouter.sendTokens({
232+
token: address(longToken),
233+
receiver: ORDER_VAULT,
234+
amount: longTokenAmount
235+
});
236+
237+
return exchangeRouter.createOrder(
238+
IBaseOrderUtils.CreateOrderParams({
239+
addresses: IBaseOrderUtils.CreateOrderParamsAddresses({
240+
receiver: address(this),
241+
cancellationReceiver: address(0),
242+
callbackContract: address(0),
243+
uiFeeReceiver: address(0),
244+
market: address(marketToken),
245+
initialCollateralToken: address(longToken),
246+
swapPath: new address[](0)
247+
}),
248+
numbers: IBaseOrderUtils.CreateOrderParamsNumbers({
249+
sizeDeltaUsd: sizeDeltaUsd,
250+
// Set by amount of collateral sent to ORDER_VAULT
251+
initialCollateralDeltaAmount: 0,
252+
triggerPrice: 0,
253+
acceptablePrice: acceptablePrice,
254+
executionFee: executionFee,
255+
callbackGasLimit: 0,
256+
minOutputAmount: 0,
257+
validFromTime: 0
258+
}),
259+
orderType: Order.OrderType.MarketIncrease,
260+
decreasePositionSwapType: Order.DecreasePositionSwapType.NoSwap,
261+
isLong: false,
262+
shouldUnwrapNativeToken: false,
263+
autoCancel: false,
264+
referralCode: bytes32(uint256(0))
265+
})
266+
);
267+
}
268+
269+
// Task 3: Create market decrease order
270+
function createDecreaseShortPositionOrder(
271+
// Execution fee to send to the order vault
272+
uint256 executionFee,
273+
// Long token amount to remove from the current position
274+
uint256 longTokenAmount,
275+
// Receiver of long token
276+
address receiver,
277+
// Callback contract used to handle withdrawal from the vault
278+
address callbackContract,
279+
// Max gas to send to the callback contract
280+
uint256 callbackGasLimit
281+
) internal returns (bytes32 orderKey) {
282+
uint256 longTokenPrice = oracle.getPrice(chainlinkLongToken);
283+
bytes32 positionKey = getPositionKey();
284+
Position.Props memory position = getPosition(positionKey);
285+
286+
require(position.numbers.sizeInUsd > 0, "position size = 0");
287+
288+
longTokenAmount =
289+
Math.min(longTokenAmount, position.numbers.collateralAmount);
290+
require(longTokenAmount > 0, "long token amount = 0");
291+
292+
// Task 3.1 - Calculate position size delta
293+
uint256 sizeDeltaUsd = getSizeDeltaUsd({
294+
longTokenPrice: longTokenPrice,
295+
sizeInUsd: position.numbers.sizeInUsd,
296+
collateralAmount: position.numbers.collateralAmount,
297+
longTokenAmount: longTokenAmount,
298+
isIncrease: false
299+
});
300+
301+
// Withdrawing collateral should also decrease position size
302+
require(sizeDeltaUsd > 0, "size delta = 0");
303+
304+
// Task 3.2 - Send market decrease order
305+
306+
// 110% of current price
307+
uint256 acceptablePrice =
308+
longTokenPrice * 1e12 / CHAINLINK_MULTIPLIER * 110 / 100;
309+
310+
exchangeRouter.sendWnt{value: executionFee}({
311+
receiver: ORDER_VAULT,
312+
amount: executionFee
313+
});
314+
315+
// Decreasing position that results in small position size causes liquidation error
316+
return exchangeRouter.createOrder(
317+
IBaseOrderUtils.CreateOrderParams({
318+
addresses: IBaseOrderUtils.CreateOrderParamsAddresses({
319+
receiver: receiver,
320+
cancellationReceiver: address(0),
321+
callbackContract: callbackContract,
322+
uiFeeReceiver: address(0),
323+
market: address(marketToken),
324+
initialCollateralToken: address(longToken),
325+
swapPath: new address[](0)
326+
}),
327+
numbers: IBaseOrderUtils.CreateOrderParamsNumbers({
328+
sizeDeltaUsd: sizeDeltaUsd,
329+
initialCollateralDeltaAmount: longTokenAmount,
330+
triggerPrice: 0,
331+
acceptablePrice: acceptablePrice,
332+
executionFee: executionFee,
333+
callbackGasLimit: callbackGasLimit,
334+
minOutputAmount: 0,
335+
validFromTime: 0
336+
}),
337+
orderType: Order.OrderType.MarketDecrease,
338+
decreasePositionSwapType: Order
339+
.DecreasePositionSwapType
340+
.SwapPnlTokenToCollateralToken,
341+
isLong: false,
342+
shouldUnwrapNativeToken: false,
343+
autoCancel: false,
344+
referralCode: bytes32(uint256(0))
345+
})
346+
);
347+
}
348+
349+
// Task 4: Cancel order
350+
function cancelOrder(bytes32 orderKey) internal {
351+
exchangeRouter.cancelOrder(orderKey);
352+
}
353+
354+
// Task 5: Claim funding fees
355+
function claimFundingFees() internal {
356+
address[] memory markets = new address[](1);
357+
markets[0] = address(marketToken);
358+
359+
address[] memory tokens = new address[](1);
360+
tokens[0] = address(longToken);
361+
362+
exchangeRouter.claimFundingFees({
363+
markets: markets,
364+
tokens: tokens,
365+
receiver: address(this)
366+
});
367+
}
368+
}

0 commit comments

Comments
 (0)