|
| 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