Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PFT-26] [PFT-356] Add functionality to settle orders in batch #12

Merged
merged 3 commits into from
Feb 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
331 changes: 330 additions & 1 deletion package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,12 @@
"typescript": "^5.3.3"
},
"dependencies": {
"@gelatonetwork/relay-sdk": "^5.5.5",
"@parifi/references": "^0.2.4",
"axios": "^1.6.7",
"decimal.js": "^10.4.3",
"dotenv": "^16.4.1",
"ethers": "^6.10.0",
"graphql": "^16.8.1",
"graphql-request": "^6.1.0"
}
Expand Down
3 changes: 3 additions & 0 deletions src/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,6 @@ export const WAD = new Decimal(10).pow(18); // 10^18
export const EMPTY_BYTES32 = '0x0000000000000000000000000000000000000000000000000000000000000000';
export const PRICE_FEED_DECIMALS = 8;
export const DECIMAL_10 = new Decimal(10);
export const DECIMAL_ZERO = new Decimal(0);
export const DEFAULT_BATCH_COUNT = 10;

12 changes: 12 additions & 0 deletions src/contract-logic/order-manager/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,18 @@ import { Decimal } from 'decimal.js';
import { DEVIATION_PRECISION_MULTIPLIER, MAX_FEE, PRECISION_MULTIPLIER } from '../../common/constants';
import { getAccruedBorrowFeesInMarket, getMarketUtilization } from '../data-fabric';
import { convertMarketAmountToCollateral } from '../price-feed';
import { Chain } from '@parifi/references';
import { contracts as parifiContracts } from '@parifi/references';
import { Contract, ethers } from 'ethers';

// Returns an Order Manager contract instance without signer
export const getOrderManagerInstance = (chain: Chain): Contract => {
try {
return new ethers.Contract(parifiContracts[chain].OrderManager.address, parifiContracts[chain].OrderManager.abi);
} catch (error) {
throw error;
}
};

// Return the Profit or Loss for a position in USD
// `normalizedMarketPrice` is the price of market with 8 decimals
Expand Down
118 changes: 118 additions & 0 deletions src/contract-logic/order-manager/settlement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import Decimal from 'decimal.js';
import { BatchExecute, Order, getAllPendingOrders } from '../../subgraph';
import { Chain, DECIMAL_ZERO, DEFAULT_BATCH_COUNT, PRECISION_MULTIPLIER } from '../../common';
import { getLatestPricesFromPyth, getVaaPriceUpdateData, normalizePythPriceForParifi } from '../../pyth';
import { AxiosInstance } from 'axios';
import { getParifiUtilsInstance } from '../parifi-utils';
import { executeTxUsingGelato } from '../../gelato';
import { contracts as parifiContracts } from '@parifi/references';

// Returns true if the price of market is within the range configured in order struct
// The function can be used to check if a pending order can be settled or not
export const checkIfOrderCanBeSettled = (order: Order, normalizedMarketPrice: Decimal): boolean => {
const isLimitOrder = order.isLimitOrder;
const triggerAbove = order.triggerAbove;
const isLong = order.isLong;
// Return false if any of the fields is undefined
if (isLimitOrder === undefined || triggerAbove === undefined || isLong === undefined) {
return false;
}

// Return false if any of the fields is undefined
if (order.expectedPrice === undefined || order.maxSlippage === undefined) {
return false;
}
const expectedPrice = new Decimal(order.expectedPrice);
const maxSlippage = new Decimal(order.maxSlippage);

if (isLimitOrder) {
// If its a limit order, check if the limit price is reached, either above or below
// depending on the triggerAbove flag
if (
(triggerAbove && normalizedMarketPrice < expectedPrice) ||
(!triggerAbove && normalizedMarketPrice > expectedPrice)
) {
return false;
}
} else {
// Market Orders
// Check if current market price is within slippage range
if (expectedPrice != DECIMAL_ZERO) {
const upperLimit = expectedPrice.mul(PRECISION_MULTIPLIER.add(maxSlippage)).div(PRECISION_MULTIPLIER);
const lowerLimit = expectedPrice.mul(PRECISION_MULTIPLIER.sub(maxSlippage)).div(PRECISION_MULTIPLIER);

if ((isLong && normalizedMarketPrice > upperLimit) || (!isLong && normalizedMarketPrice < lowerLimit)) {
return false;
}
}
}
return true;
};

export const batchSettlePendingOrdersUsingGelato = async (
chainId: Chain,
gelatoKey: string,
pythClient: AxiosInstance,
): Promise<{ ordersCount: number }> => {
const currentTimestamp = Math.floor(Date.now() / 1000);
const pendingOrders = await getAllPendingOrders(chainId, currentTimestamp, DEFAULT_BATCH_COUNT);
if (pendingOrders.length == 0) return { ordersCount: 0 };

const priceIds: string[] = [];

// Populate the price ids array to fetch price update data
pendingOrders.forEach((order) => {
if (order.market?.pyth?.id) {
priceIds.push(order.market.pyth.id);
}
});

// Get Price update data and latest prices from Pyth
const priceUpdateData = await getVaaPriceUpdateData(priceIds, pythClient);
const pythLatestPrices = await getLatestPricesFromPyth(priceIds, pythClient);

// Populate batched orders for settlement for orders that can be settled
const batchedOrders: BatchExecute[] = [];

pendingOrders.forEach((order) => {
if (order.id) {
// Pyth returns price id without '0x' at the start, hence the price id from order
// needs to be formatted
const orderPriceId = order.market?.pyth?.id ?? '0x';
const formattedPriceId = orderPriceId.startsWith('0x') ? orderPriceId.substring(2) : orderPriceId;

const assetPrice = pythLatestPrices.find((pythPrice) => pythPrice.id === formattedPriceId);
const normalizedMarketPrice = normalizePythPriceForParifi(
parseInt(assetPrice?.price.price ?? '0'),
assetPrice?.price.expo ?? 0,
);

if (checkIfOrderCanBeSettled(order, normalizedMarketPrice)) {
batchedOrders.push({
id: order.id,
priceUpdateData: priceUpdateData,
});
// We need these console logs for feedback to Tenderly actions and other scripts
console.log('Order ID available for settlement:', order.id);
} else {
console.log('Order ID not available for settlement because of price mismatch:', order.id);
}
}
});

// Encode transaction data
if (batchedOrders.length != 0) {
const parifiUtils = getParifiUtilsInstance(chainId);
const { data: encodedTxData } = await parifiUtils.batchSettleOrders.populateTransaction(batchedOrders);

const taskId = await executeTxUsingGelato(
parifiContracts[chainId].ParifiUtils.address,
chainId,
gelatoKey,
encodedTxData,
);
// We need these console logs for feedback to Tenderly actions and other scripts
console.log('Task ID:', taskId);
}
return { ordersCount: batchedOrders.length };
};
12 changes: 12 additions & 0 deletions src/contract-logic/parifi-utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Contract, ethers } from 'ethers';
import { Chain } from '@parifi/references';
import { contracts as parifiContracts } from '@parifi/references';

// Returns an Order Manager contract instance without signer
export const getParifiUtilsInstance = (chain: Chain): Contract => {
try {
return new ethers.Contract(parifiContracts[chain].ParifiUtils.address, parifiContracts[chain].ParifiUtils.abi);
} catch (error) {
throw error;
}
};
19 changes: 19 additions & 0 deletions src/gelato/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { GelatoRelay, SponsoredCallRequest } from '@gelatonetwork/relay-sdk';
import { Chain } from '@parifi/references';

export const executeTxUsingGelato = async (
targetContractAddress: string,
chainId: Chain,
gelatoKey: string,
encodedTxData: string,
): Promise<string> => {
const request: SponsoredCallRequest = {
chainId: BigInt(chainId.toString()),
target: targetContractAddress,
data: encodedTxData,
};

const relay = new GelatoRelay();
const { taskId } = await relay.sponsoredCall(request, gelatoKey);
return taskId;
};
35 changes: 33 additions & 2 deletions src/pyth/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import axios, { AxiosInstance } from 'axios';
import { PRICE_FEED_DECIMALS, getUniqueValuesFromArray } from '../common';
import Decimal from 'decimal.js';
import { PythPriceResponse } from '../subgraph';
import { mapPythPriceResponseToInterface } from './pythMapper';

// Returns a Pyth client object based on the params provided
export const getPythClient = async (
Expand Down Expand Up @@ -46,7 +48,7 @@ export const getPythClient = async (

// The function accepts an array of priceIds and returns the priceUpdateData
// for them from Pyth
export async function getVaaPriceUpdateData(priceIds: string[], pythClient: AxiosInstance): Promise<string[]> {
export const getVaaPriceUpdateData = async (priceIds: string[], pythClient: AxiosInstance): Promise<string[]> => {
const uniquePriceIds = getUniqueValuesFromArray(priceIds);
let priceUpdateData: string[] = [];

Expand All @@ -64,7 +66,7 @@ export async function getVaaPriceUpdateData(priceIds: string[], pythClient: Axio
}
}
return priceUpdateData.map((vaa) => '0x' + Buffer.from(vaa, 'base64').toString('hex'));
}
};

// Pyth currently uses different exponents for supported assets. Parifi uses all price feeds with 8 decimals
// This function converts the price from Pyth to a format Parifi uses with 8 decimals.
Expand All @@ -77,3 +79,32 @@ export const normalizePythPriceForParifi = (pythPrice: number, pythExponent: num
return new Decimal(pythPrice).div(adjustedFactor);
}
};

// Get latest prices from Pyth for priceIds
// The prices returned are in Pyth structure which needs to be normalized
// before using for any Parifi functions
export const getLatestPricesFromPyth = async (
priceIds: string[],
pythClient: AxiosInstance,
): Promise<PythPriceResponse[]> => {
const uniquePriceIds = getUniqueValuesFromArray(priceIds);

const pythPriceResponses: PythPriceResponse[] = [];

if (pythClient) {
try {
const response = await pythClient.get('/api/latest_price_feeds', {
params: {
ids: uniquePriceIds,
verbose: false,
binary: false,
},
});
return mapPythPriceResponseToInterface(response.data);
} catch (error) {
console.log('Error fetching latest prices from Pyth', error);
throw error;
}
}
return pythPriceResponses;
};
20 changes: 20 additions & 0 deletions src/pyth/pythMapper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { PythPriceResponse } from '../subgraph';

// Function to map the pyth latest price response to interface
export const mapPythPriceResponseToInterface = (response: any[]): PythPriceResponse[] => {
return response.map((item) => ({
id: item.id || '',
price: {
price: item.price.price || '0',
conf: item.price.conf || '0',
expo: item.price.expo || 0,
publish_time: item.price.publish_time || 0,
},
ema_price: {
price: item.ema_price.price || '0',
conf: item.ema_price.conf || '0',
expo: item.ema_price.expo || 0,
publish_time: item.ema_price.publish_time || 0,
},
}));
};
28 changes: 28 additions & 0 deletions src/subgraph/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,29 @@ export interface Token {
////////////////////////////////////////////////////////////////
//////////////////////// PYTH ////////////////////////////
////////////////////////////////////////////////////////////////


// Pyth price data interface for prices received from Pyth
export interface PythPrice {
price: string

conf: string

expo: number

publish_time: number
}

// Interface for response received for Pyth Price data
export interface PythPriceResponse {
// Pyth Price ID
id? :string

price: PythPrice

ema_price: PythPrice
}

export interface PriceFeedSnapshot {
//" Price ID + Timestamp "
id?: string
Expand Down Expand Up @@ -417,4 +440,9 @@ export interface PythData {

// " Last updated timestamp "
lastUpdatedTimestamp?: string
}

export interface BatchExecute {
id: string;
priceUpdateData: string[];
}
18 changes: 18 additions & 0 deletions test/contract-logic/parifi-utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import 'dotenv/config';
import { Chain } from '../../src';
import { getPythClient } from '../../src/pyth';
import { batchSettlePendingOrdersUsingGelato } from '../../src/contract-logic/order-manager/settlement';

const chainId = Chain.ARBITRUM_SEPOLIA;

describe('Parifi Utils tests', () => {
it('should settle orders in batch using Parifi Utils', async () => {
// To test the batch settle functionality, create some orders manually using the interface
const pythClient = await getPythClient();

if (pythClient) {
const orderCount = await batchSettlePendingOrdersUsingGelato(chainId, process.env.GELATO_KEY ?? '', pythClient);
console.log('Orders processed: ', orderCount);
}
});
});