diff --git a/src/common/constants.ts b/src/common/constants.ts index 5d1437f..df65df0 100644 --- a/src/common/constants.ts +++ b/src/common/constants.ts @@ -28,4 +28,7 @@ export const PYTH_USDC_USD_PRICE_ID_BETA = '0x41f3625971ca2ed2263e78573fe5ce23e1 export const PYTH_USDC_USD_PRICE_ID_STABLE = '0xeaa020c61cc479712813461ce153894a96a6c00b21ed0cfc2798d1f9a9e9c94a'; // Constants for Pimlico relayer -export const FACTORY_ADDRESS_SIMPLE_ACCOUNT = '0x91E60e0613810449d098b0b5Ec8b51A0FE8c8985' \ No newline at end of file +export const FACTORY_ADDRESS_SIMPLE_ACCOUNT = '0x91E60e0613810449d098b0b5Ec8b51A0FE8c8985' + +// Temporary constants +export const SUBGRAPH_HELPER_ADDRESS = "0xf012d32505df6853187170F00C7b789A8ecC41c2" \ No newline at end of file diff --git a/src/common/helpers.ts b/src/common/helpers.ts index bf64061..2ee9ca8 100644 --- a/src/common/helpers.ts +++ b/src/common/helpers.ts @@ -10,6 +10,10 @@ export const getDiff = (a: Decimal, b: Decimal): Decimal => { return a.gt(b) ? a.minus(b) : b.minus(a); }; +export const addDelay = async (ms: number): Promise => { + return new Promise((resolve) => setTimeout(resolve, ms)); +}; + export const getUniqueValuesFromArray = (originalArray: string[]): string[] => { const uniqueArray: string[] = []; const seenValues = new Set(); diff --git a/src/common/subgraphMapper.ts b/src/common/subgraphMapper.ts index cdc507c..a08ae7c 100644 --- a/src/common/subgraphMapper.ts +++ b/src/common/subgraphMapper.ts @@ -322,6 +322,7 @@ export const mapVaultPositionToInterface = (response: any): VaultPosition | unde avgMintPriceDec: response.avgMintPriceDec, realizedPNL: response.realizedPNL, realizedPNLInUsd: response.realizedPNLInUsd, + unrealizedPNL: response.unrealizedPNL, timestamp: response.timestamp, cooldownInitiatedTimestamp: response.cooldownInitiatedTimestamp, cooldownEnd: response.cooldownEnd, diff --git a/src/core/index.ts b/src/core/index.ts index 245e35b..24ec3ac 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -14,6 +14,7 @@ import { canBeSettled, canBeSettledPriceId, checkIfOrderCanBeSettledId, + getLiquidationPrice, getNetProfitOrLossInCollateral, getOrderManagerInstance, getProfitOrLossInUsd, @@ -233,6 +234,15 @@ export class Core { ); }; + getLiquidationPrice = ( + position: Position, + market: Market, + normalizedMarketPrice: Decimal, + normalizedCollateralPrice: Decimal, + ): Decimal => { + return getLiquidationPrice(position, market, normalizedMarketPrice, normalizedCollateralPrice); + }; + //////////////////////////////////////////////////////////////// ////////////////////// PARIFI UTILS ////////////////////// //////////////////////////////////////////////////////////////// diff --git a/src/core/order-manager/index.ts b/src/core/order-manager/index.ts index cace7eb..ab6ac32 100644 --- a/src/core/order-manager/index.ts +++ b/src/core/order-manager/index.ts @@ -9,12 +9,17 @@ import { PRECISION_MULTIPLIER, } from '../../common/constants'; import { getAccruedBorrowFeesInMarket, getMarketUtilization } from '../data-fabric'; -import { convertMarketAmountToCollateral } from '../price-feed'; +import { convertCollateralAmountToUsd, convertMarketAmountToCollateral, convertMarketAmountToUsd } from '../price-feed'; import { Chain } from '@parifi/references'; import { contracts as parifiContracts } from '@parifi/references'; import { Contract, ethers } from 'ethers'; import { AxiosInstance } from 'axios'; -import { getOrderById, getPythPriceIdsForOrderIds, getPythPriceIdsForPositionIds } from '../../subgraph'; +import { + getOrderById, + getPythPriceIdsForOrderIds, + getPythPriceIdsForPositionIds, + getTotalUnrealizedPnlInUsd, +} from '../../subgraph'; import { getLatestPricesFromPyth, getVaaPriceUpdateData, normalizePythPriceForParifi } from '../../pyth/pyth'; import { getPriceIdsForCollaterals } from '../../common'; import { executeTxUsingGelato } from '../../relayers/gelato/gelato-function'; @@ -451,3 +456,50 @@ export const settleOrderUsingGelato = async ( console.log('Task ID:', taskId); return { gelatoTaskId: taskId }; }; + +// Returns the liquidation price of a Position +// A position is liquidated when the loss of a position in USD goes above the USD value +// of liquidation threshold times the deposited collateral value +export const getLiquidationPrice = ( + position: Position, + market: Market, + normalizedMarketPrice: Decimal, + normalizedCollateralPrice: Decimal, +): Decimal => { + const collateral = new Decimal(position.positionCollateral ?? '0'); + + // Decimal digits for market and collateral token + const collateralDecimals = new Decimal(market.depositToken?.decimals ?? '18'); + const marketDecimals = new Decimal(market.marketDecimals ?? '18'); + + // Total fees for the position taking into account closing fee and liquidation fee + const accruedBorrowFeesInMarket = getAccruedBorrowFeesInMarket(position, market); + const fixedFeesInMarket = new Decimal(position.positionSize ?? '0') + .times(new Decimal(market.openingFee ?? '0').add(market.liquidationFee ?? '0')) + .div(MAX_FEE); + + const totalFeesInUsd = convertMarketAmountToUsd( + accruedBorrowFeesInMarket.add(fixedFeesInMarket), + marketDecimals, + normalizedMarketPrice, + ); + + const collateralInUsd = convertCollateralAmountToUsd(collateral, collateralDecimals, normalizedCollateralPrice); + const maxLossLimitInUsd = collateralInUsd.times(market.liquidationThreshold ?? '0').div(PRECISION_MULTIPLIER); + + const lossLimitAfterFees = maxLossLimitInUsd.sub(totalFeesInUsd); + + // @todo Revisit this + // If loss is already more than the max loss, the position can be liquidated at the current price + if (lossLimitAfterFees.lessThan(DECIMAL_ZERO)) return normalizedMarketPrice; + + const lossPerToken = lossLimitAfterFees + .times(new Decimal('10').pow(marketDecimals)) + .div(position.positionSize ?? '1'); + + if (position.isLong) { + return new Decimal(position.avgPrice ?? 0).sub(lossPerToken); + } else { + return new Decimal(position.avgPrice ?? 0).add(lossPerToken); + } +}; diff --git a/src/core/pages/poolPage.ts b/src/core/pages/poolPage.ts index 5d68176..3d98752 100644 --- a/src/core/pages/poolPage.ts +++ b/src/core/pages/poolPage.ts @@ -1,9 +1,9 @@ import Decimal from 'decimal.js'; import { getAllVaults, getUserVaultData } from '../../subgraph/vaults'; -import { MAX_FEE } from '../../common'; +import { DECIMAL_10, MAX_FEE } from '../../common'; import { UserVaultData } from '../../interfaces/sdkTypes'; -// Add Vault and Position data for a user address. If the user has no vault deposits/withdrawals, it will +// Add Vault and Position data for a user address. If the user has no vault deposits/withdrawals, it will // return the user specific fields with 0 values. Other global values for vaults will be populated export const getPoolPageData = async (subgraphEndpoint: string, userAddress: string): Promise => { const userVaultData: UserVaultData[] = []; @@ -17,6 +17,14 @@ export const getPoolPageData = async (subgraphEndpoint: string, userAddress: str (BigInt(vaultPosition?.sharesBalance ?? 0) * BigInt(vault.assetsPerShare ?? 0)) / BigInt(10 ** (vault.vaultDecimals ?? 1)); + // Calculate the PNL for each token based on the average mint price and the current price + // of assets per share + const pnlPerToken = new Decimal(vault.assetsPerShare ?? 0).minus(vaultPosition?.avgMintPrice ?? 0); + + const currentUnrealizedPNL = new Decimal(vaultPosition?.sharesBalance ?? 0) + .times(pnlPerToken) + .div(DECIMAL_10.pow(vault.vaultDecimals ?? 0)); + const userData: UserVaultData = { vaultId: vault.id ?? '0x', vaultSymbol: vault.vaultSymbol ?? 'pfERC20', @@ -32,9 +40,9 @@ export const getPoolPageData = async (subgraphEndpoint: string, userAddress: str cooldownStarted: BigInt(vaultPosition?.cooldownInitiatedTimestamp ?? 0), cooldownWindowInSeconds: BigInt(vault.cooldownPeriod ?? '0'), withdrawalWindowInSeconds: BigInt(vault.withdrawalWindow ?? '0'), - totalAssetsGain: new Decimal(vaultPosition?.realizedPNL ?? 0).add(vaultPosition?.unrealizedPNL ?? 0), + totalAssetsGain: currentUnrealizedPNL.add(vaultPosition?.realizedPNL ?? 0), realizedPNL: new Decimal(vaultPosition?.realizedPNL ?? 0), - unrealizedPNL: new Decimal(vaultPosition?.unrealizedPNL ?? 0), + unrealizedPNL: currentUnrealizedPNL, realizedPNLInUsd: new Decimal(vaultPosition?.realizedPNLInUsd ?? 0), }; diff --git a/src/core/subgraph-helper/index.ts b/src/core/subgraph-helper/index.ts new file mode 100644 index 0000000..24c0a91 --- /dev/null +++ b/src/core/subgraph-helper/index.ts @@ -0,0 +1,43 @@ +import { Contract, ethers } from 'ethers'; +import { Chain } from '@parifi/references'; +import { contracts as parifiContracts } from '@parifi/references'; + +import { SUBGRAPH_HELPER_ADDRESS } from '../../common'; + +const subgraphHelperAbi = [ + { + anonymous: false, + inputs: [{ indexed: false, internalType: 'bytes32[]', name: 'orderIds', type: 'bytes32[]' }], + name: 'OrderUpdateRequest', + type: 'event', + }, + { + anonymous: false, + inputs: [{ indexed: false, internalType: 'bytes32[]', name: 'positionIds', type: 'bytes32[]' }], + name: 'PositionUpdateRequest', + type: 'event', + }, + { + inputs: [{ internalType: 'bytes32[]', name: 'orderIds', type: 'bytes32[]' }], + name: 'triggerOrderUpdate', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'bytes32[]', name: 'positionIds', type: 'bytes32[]' }], + name: 'triggerPositionUpdate', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, +]; + +// Returns Subgraph Helper contract instance without signer +export const getSubgraphHelperInstance = (chain: Chain): Contract => { + try { + return new ethers.Contract(SUBGRAPH_HELPER_ADDRESS, subgraphHelperAbi); + } catch (error) { + throw error; + } +}; diff --git a/src/subgraph/accounts/subgraphQueries.ts b/src/subgraph/accounts/subgraphQueries.ts index 196cc13..f33517c 100644 --- a/src/subgraph/accounts/subgraphQueries.ts +++ b/src/subgraph/accounts/subgraphQueries.ts @@ -4,7 +4,7 @@ import { gql } from 'graphql-request'; // for vaults and positions export const fetchRealizedPnlData = (userAddress: string) => gql` { - account(id: "${userAddress}") { + account(id: "${userAddress.toLowerCase()}") { id totalRealizedPnlPositions totalRealizedPnlVaults @@ -20,7 +20,7 @@ export const fetchRealizedPnlData = (userAddress: string) => gql` /// 4. Deposited collateral of all open positions - positions.positionCollateral export const fetchPortfolioData = (userAddresses: string[]) => gql` { - accounts(where: {id_in: [${userAddresses.map((id) => `"${id}"`).join(', ')}]}) { + accounts(where: {id_in: [${userAddresses.map((id) => `"${id.toLowerCase()}"`).join(', ')}]}) { id totalRealizedPnlPositions totalRealizedPnlVaults @@ -30,7 +30,7 @@ export const fetchPortfolioData = (userAddresses: string[]) => gql` orderBy: positionCollateral orderDirection: desc where: { - user_in: [${userAddresses.map((id) => `"${id}"`).join(', ')}], + user_in: [${userAddresses.map((id) => `"${id.toLowerCase()}"`).join(', ')}], status: OPEN } ) { @@ -54,7 +54,7 @@ export const fetchPortfolioData = (userAddresses: string[]) => gql` first: 1000 orderBy: sharesBalance orderDirection: desc - where: {user_in: [${userAddresses.map((id) => `"${id}"`).join(', ')}]} + where: {user_in: [${userAddresses.map((id) => `"${id.toLowerCase()}"`).join(', ')}]} ) { user { id diff --git a/src/subgraph/markets/subgraphQueries.ts b/src/subgraph/markets/subgraphQueries.ts index d486675..e168066 100644 --- a/src/subgraph/markets/subgraphQueries.ts +++ b/src/subgraph/markets/subgraphQueries.ts @@ -71,7 +71,7 @@ export const fetchAllMarketsDataQuery = gql` // Fetch all details of a market by ID export const fetchMarketByIdQuery = (marketId: string) => gql` { - market(id: "${marketId}") { + market(id: "${marketId.toLowerCase()}") { id vaultAddress depositToken { diff --git a/src/subgraph/orders/subgraphQueries.ts b/src/subgraph/orders/subgraphQueries.ts index 8bbcc42..fd454fc 100644 --- a/src/subgraph/orders/subgraphQueries.ts +++ b/src/subgraph/orders/subgraphQueries.ts @@ -7,7 +7,7 @@ export const fetchOrdersByUserQuery = (userAddress: string, count: number = 10, orders( first: ${count} skip: ${skip} - where: {user: "${userAddress}"} + where: {user: "${userAddress.toLowerCase()}"} orderBy: createdTimestamp orderDirection: desc ) { @@ -156,7 +156,7 @@ export const fetchPartnerRewards = (partnerAddress: string, count: number = 20, skip: ${skip} orderBy: timestamp orderDirection: desc - where: { partner: "${partnerAddress}" } + where: { partner: "${partnerAddress.toLowerCase()}" } ) { id partner { id } diff --git a/src/subgraph/positions/subgraphQueries.ts b/src/subgraph/positions/subgraphQueries.ts index 5144d66..54d93bc 100644 --- a/src/subgraph/positions/subgraphQueries.ts +++ b/src/subgraph/positions/subgraphQueries.ts @@ -9,7 +9,7 @@ export const fetchPositionsByUserQuery = (userAddress: string, count: number = 1 skip: ${skip} orderBy: createdTimestamp orderDirection: desc - where: {user: "${userAddress}"} + where: {user: "${userAddress.toLowerCase()}"} ) { id market { @@ -61,7 +61,7 @@ export const fetchPositionsByUserQueryAndStatus = ( orderBy: createdTimestamp orderDirection: desc where: { - user: "${userAddress}" + user: "${userAddress.toLowerCase()}" status: "${status}" } ) { @@ -204,7 +204,7 @@ export const fetchAllPositionsUnrealizedPnl = (userAddress: string) => gql` first: 1000 orderBy: positionCollateral orderDirection: desc - where: { user: "${userAddress}", status: OPEN } + where: { user: "${userAddress.toLowerCase()}", status: OPEN } ) { id netUnrealizedPnlInUsd diff --git a/src/subgraph/vaults/subgraphQueries.ts b/src/subgraph/vaults/subgraphQueries.ts index b922737..3a62e79 100644 --- a/src/subgraph/vaults/subgraphQueries.ts +++ b/src/subgraph/vaults/subgraphQueries.ts @@ -43,7 +43,7 @@ export const fetchUserVaultPositionsQuery = (user: string) => gql` first: 1000 orderBy: sharesBalance orderDirection: desc - where: {user: "${user}"} + where: {user: "${user.toLowerCase()}"} ) { id user { @@ -85,7 +85,7 @@ export const fetchVaultAprDetails = (vaultId: string) => gql` first: 30 orderBy: startTimestamp orderDirection: desc - where: { vault: "${vaultId}" } + where: { vault: "${vaultId.toLowerCase()}" } ) { vault { allTimeApr } apr @@ -99,7 +99,7 @@ export const fetchCooldownDetails = (user: string) => gql` orderBy: timestamp orderDirection: desc first: 10 - where: {user: "${user}"} + where: {user: "${user.toLowerCase()}"} ) { id user { diff --git a/test/core/orderManager.test.ts b/test/core/orderManager.test.ts index 988fbe2..4407e88 100644 --- a/test/core/orderManager.test.ts +++ b/test/core/orderManager.test.ts @@ -1,6 +1,7 @@ import { ethers } from 'ethers'; import { getParifiSdkInstanceForTesting } from '..'; -import { TEST_SETTLE_ORDER_ID } from '../common/constants'; +import { TEST_MARKET_ID1, TEST_POSITION_ID1, TEST_SETTLE_ORDER_ID } from '../common/constants'; +import { DECIMAL_ZERO, getNormalizedPriceByIdFromPriceIdArray } from '../../src'; describe('Order Manager tests', () => { it('should liquidate a single position', async () => { @@ -38,4 +39,41 @@ describe('Order Manager tests', () => { const tx = await parifiSdk.core.batchSettleOrdersUsingWallet(orderIds, priceUpdateData, wallet); console.log(tx); }); + + it('should return valid liquidation price', async () => { + const parifiSdk = await getParifiSdkInstanceForTesting(); + const position = await parifiSdk.subgraph.getPositionById( + '0x9a1b314246e76d5f912020961f95d44077df4be8450d25e2c7dddd98582b5b66', + ); + const market = await parifiSdk.subgraph.getMarketById(position.market?.id ?? TEST_MARKET_ID1); + + const normalizedPrice = await parifiSdk.pyth.getLatestPricesNormalized([ + market.depositToken?.pyth?.id ?? '0x', + market.pyth?.id ?? '0x', + ]); + + const normalizedCollateralPrice = normalizedPrice.find( + (p) => p.priceId === market.depositToken?.pyth?.id, + )?.normalizedPrice; + + const normalizedMarketPrice = + normalizedPrice.find((p) => p.priceId === market.pyth?.id)?.normalizedPrice ?? DECIMAL_ZERO; + + console.log('normalizedCollateralPrice', normalizedCollateralPrice); + console.log('normalizedMarketPrice', normalizedMarketPrice); + + const liquidationPrice = await parifiSdk.core.getLiquidationPrice( + position, + market, + normalizedMarketPrice ?? DECIMAL_ZERO, + normalizedCollateralPrice ?? DECIMAL_ZERO, + ); + + console.log('liquidationPrice', liquidationPrice); + if (position.isLong) { + expect(liquidationPrice.toNumber()).toBeLessThan(Number(position.avgPrice)); + } else { + expect(liquidationPrice.toNumber()).toBeGreaterThan(Number(position.avgPrice)); + } + }); }); diff --git a/test/core/poolPage.test.ts b/test/core/poolPage.test.ts index 558fd72..f82e6c4 100644 --- a/test/core/poolPage.test.ts +++ b/test/core/poolPage.test.ts @@ -18,7 +18,6 @@ describe('Stats tests', () => { const parifiSdk = await getParifiSdkInstanceForTesting(); const userPoolData = await parifiSdk.core.getPoolPageData(ethers.ZeroAddress); - console.log(userPoolData); expect(userPoolData.length).not.toBe(0); userPoolData.forEach((data) => { expect(data.assetBalance).toBe(BIGINT_ZERO); diff --git a/test/index.ts b/test/index.ts index 5aa0810..f92e7cf 100644 --- a/test/index.ts +++ b/test/index.ts @@ -5,7 +5,7 @@ export const getParifiSdkInstanceForTesting = async (): Promise => { const chain = Chain.ARBITRUM_MAINNET; const rpcConfig: RpcConfig = { chainId: chain, - rpcEndpointUrl: process.env.RPC_ARBITRUM + rpcEndpointUrl: process.env.RPC_ARBITRUM, }; const subgraphConfig: SubgraphConfig = { @@ -25,6 +25,7 @@ export const getParifiSdkInstanceForTesting = async (): Promise => { const pimlicoConfig: RelayerI = { apiKey: process.env.PIMLICO_API_KEY, + password: process.env.PRIVATE_KEY, }; const relayerConfig: RelayerConfig = { diff --git a/test/relayers/pimlico.test.ts b/test/relayers/pimlico.test.ts index 1905840..b06b776 100644 --- a/test/relayers/pimlico.test.ts +++ b/test/relayers/pimlico.test.ts @@ -1,5 +1,7 @@ +import { Chain } from '@parifi/references'; import { getParifiSdkInstanceForTesting } from '..'; -import { OrderStatus } from '../../src'; +import { OrderStatus, SUBGRAPH_HELPER_ADDRESS } from '../../src'; +import { getSubgraphHelperInstance } from '../../src/core/subgraph-helper'; import { TEST_SETTLE_ORDER_ID } from '../common/constants'; describe('Pimlico test cases', () => { @@ -26,4 +28,27 @@ describe('Pimlico test cases', () => { console.log(`User operation included: https://arbiscan.io/tx/${txHash}`); }); + + it.skip('should refresh positions using Pimlico', async () => { + const parifiSdk = await getParifiSdkInstanceForTesting(); + + const positionsToRefresh = await parifiSdk.subgraph.getPositionsToRefresh(50); + console.log('positionsToRefresh', positionsToRefresh); + + const subgraphHelper = getSubgraphHelperInstance(Chain.ARBITRUM_MAINNET); + const { data: encodedTxData } = await subgraphHelper.triggerPositionUpdate.populateTransaction(positionsToRefresh); + + const { txHash } = await parifiSdk.relayer.pimlico.executeTxUsingPimlico(SUBGRAPH_HELPER_ADDRESS, encodedTxData); + console.log(`Tx submitted: https://arbiscan.io/tx/${txHash}`); + }); + + it('should liquidate positions using Pimlico', async () => { + const parifiSdk = await getParifiSdkInstanceForTesting(); + + const positionIds = await parifiSdk.subgraph.getPositionsToLiquidate(); + if (positionIds.length !== 0) { + const { txHash } = await parifiSdk.relayer.pimlico.batchLiquidatePositionsUsingPimlico(positionIds); + console.log(`User operation included: https://arbiscan.io/tx/${txHash}`); + } + }); });