UNPKG

liquidops-test-liquidations

Version:

LiquidOps is an over-collateralised lending and borrowing protocol built on Arweave's L2 AO.

312 lines (282 loc) 9.88 kB
import { getData } from "../../ao/messaging/getData"; import { TokenInput } from "../../ao/utils/tokenInput"; import { collateralEnabledTickers, tokens, tokenData, SupportedTokensTickers, controllerAddress, } from "../../ao/utils/tokenAddressData"; import { redstoneOracleAddress } from "../../ao/utils/tokenAddressData"; import { getAllPositions } from "../protocolData/getAllPositions"; import { DryRunResult } from "@permaweb/aoconnect/dist/lib/dryrun" export interface GetLiquidationsRes { liquidations: Map<string, QualifyingPosition>; prices: RedstonePrices; } export interface QualifyingPosition { /* The tokens that can be liquidated */ debts: { ticker: string; quantity: BigInt; }[]; /* The available collaterals that can be received for the liquidation */ collaterals: { ticker: string; quantity: BigInt; }[]; /** The current discount percentage for this liquidation (multiplied by the precision factor) */ discount: BigInt; } export type RedstonePrices = Record< string, { t: number; a: string; v: number } >; interface Tag { name: string; value: string; } // Base token position with the core metrics interface TokenPosition { borrowBalance: BigInt; capacity: BigInt; collateralization: BigInt; liquidationLimit: BigInt; } // Extended token position with USD values interface TokenPositionUSD extends TokenPosition { borrowBalanceUSD: BigInt; capacityUSD: BigInt; collateralizationUSD: BigInt; liquidationLimitUSD: BigInt; } // Global position across all tokens interface GlobalPosition { borrowBalanceUSD: BigInt; capacityUSD: BigInt; collateralizationUSD: BigInt; liquidationLimitUSD: BigInt; tokenPositions: { [token: string]: TokenPosition; }; } export async function getLiquidations( precisionFactor: number, ): Promise<GetLiquidationsRes> { try { if (!Number.isInteger(precisionFactor)) { throw new Error("The precision factor has to be an integer"); } // Get list of tokens to process const tokensList = Object.keys(tokens); /*const [redstonePriceFeedRes, positionsList, auctionsRes] = await Promise.all([ // Make a request to RedStone oracle process for prices (same used onchain) getData({ Target: redstoneOracleAddress, Action: "v2.Request-Latest-Data", Tickers: JSON.stringify( collateralEnabledTickers.map((ticker) => ticker === "QAR" ? "AR" : ticker, ), ), }), // Get positions for each token Promise.all( tokensList.map(async (token) => ({ token, positions: await getAllPositions({ token }), })), ), // get discovered liquidations getData({ Target: controllerAddress, Action: "Get-Auctions", }) ]);*/ /* console.log( JSON.stringify(redstonePriceFeedRes.Messages, null, 2), JSON.stringify(positionsList, null, 2), JSON.stringify(auctionsRes.Messages, null, 2) )*/ const [redstonePriceFeedRes, positionsList, auctionsRes] = [ { Messages: [{ Data: JSON.stringify({ "AR": { "a": "0x0000000000000000000000000000000000000000", "v": 2.1, "t": 1742900720937 }, "USDC": { "a": "0x0000000000000000000000000000000000000000", "v": 1, "t": 1742900720937 } }) }] }, [ { token: "QAR", positions: { "-CZI5_c-SA_0gnwyXxE9zJyBvEvy5gxEoTwtxLID9UE": { "collateralization": BigInt("20000007272774"), "capacity": BigInt("10000003636387"), "borrowBalance": BigInt("0"), "liquidationLimit": BigInt("10200003709114") }, "ljvCPN31XCLPkBo9FUeB7vAK0VC6-eY52-CS-6Iho8U": { "collateralization": BigInt("0"), "capacity": BigInt("0"), "borrowBalance": BigInt("2500007272774"), "liquidationLimit": BigInt("0") } } as { [a: string]: TokenPosition } }, { token: "USDC", positions: { "ljvCPN31XCLPkBo9FUeB7vAK0VC6-eY52-CS-6Iho8U": { "borrowBalance": BigInt("0"), "collateralization": BigInt("10000000000000"), "liquidationLimit": BigInt("5100000000000"), "capacity": BigInt("5000000000000") } } as { [a: string]: TokenPosition } } ], { Messages: [{ Tags: [ { name: "Initial-Discount", value: "5" }, { name: "Discount-Interval", value: (1000 * 60 * 60).toString() } ], Data: JSON.stringify({ "ljvCPN31XCLPkBo9FUeB7vAK0VC6-eY52-CS-6Iho8U": Date.now() - 1000 * 60 * 0 }) }] } ] // parse prices and auctions const prices: RedstonePrices = JSON.parse( redstonePriceFeedRes.Messages[0].Data, ); const auctions: Record<string, number> = JSON.parse( auctionsRes.Messages[0].Data, ); // maximum discount percentage and discount period const auctionTags = Object.fromEntries( auctionsRes.Messages[0].Tags.map((tag: Tag) => [tag.name, tag.value]), ); const maxDiscount = parseFloat(auctionTags["Initial-Discount"] || "0"); const discountInterval = parseInt(auctionTags["Discount-Interval"] || "0"); // Create a map to store global positions by wallet address const globalPositions = new Map<string, GlobalPosition>(); // Calculate global positions for all wallets across all tokens for (const { token, positions: localPositions } of positionsList) { // token data const tokenPrice = prices[token === "QAR" ? "AR" : token].v; const tokenDenomination = tokenData[token as SupportedTokensTickers].denomination; // Use the token's specific denomination for scaling const scale = BigInt(10) ** tokenDenomination; const priceScaled = BigInt(Math.round(tokenPrice * Number(scale))); // loop through all positions, add them to the global positions for (const [walletAddress, position] of Object.entries<TokenPosition>( localPositions, )) { const posValueUSD = { borrowBalanceUSD: ((position.borrowBalance as bigint) * priceScaled) / scale, capacityUSD: ((position.capacity as bigint) * priceScaled) / scale, collateralizationUSD: ((position.collateralization as bigint) * priceScaled) / scale, liquidationLimitUSD: ((position.liquidationLimit as bigint) * priceScaled) / scale, }; if (!globalPositions.has(walletAddress)) { // no global position calculated for this user yet globalPositions.set(walletAddress, { ...posValueUSD, tokenPositions: { [token]: position }, }); } else { // update existing global position const globalPos = globalPositions.get(walletAddress); // @ts-expect-error globalPos!.borrowBalanceUSD += posValueUSD.borrowBalanceUSD; // @ts-expect-error globalPos!.capacityUSD += posValueUSD.capacityUSD; // @ts-expect-error globalPos!.collateralizationUSD += posValueUSD.collateralizationUSD; // @ts-expect-error globalPos!.liquidationLimitUSD += posValueUSD.liquidationLimitUSD; globalPos!.tokenPositions[token] = position; } } } // Initialize available liquidations object const res = new Map<string, QualifyingPosition>(); // Initialize liquidations object for each supported token for (const [walletAddress, position] of globalPositions) { // Check if the position is eligible for liquidation // A position is eligible if borrowBalanceUSD > liquidationLimitUSD if (position.borrowBalanceUSD <= position.liquidationLimitUSD) continue; // time calculations for the discount const currentTime = Date.now(); let timeSinceDiscovery = currentTime - (auctions[walletAddress] || currentTime); // maximum price reached, no discount applied if (timeSinceDiscovery > discountInterval) { timeSinceDiscovery = discountInterval; } // calculate the discount for this user const discount = BigInt( Math.max( Math.floor( ((discountInterval - timeSinceDiscovery) * maxDiscount * precisionFactor) / discountInterval, ), 0, ), ); // the final position that can be liquidated, with rewards and collaterals const qualifyingPos: QualifyingPosition = { debts: [], collaterals: [], discount, }; // find rewards and debt for (const [token, localPosition] of Object.entries<TokenPosition>( position.tokenPositions, )) { // found a debt if ((localPosition.borrowBalance as bigint) > BigInt(0)) { qualifyingPos.debts.push({ ticker: token, quantity: localPosition.borrowBalance, }); } // found a reward if ((localPosition.collateralization as bigint) > BigInt(0)) { qualifyingPos.collaterals.push({ ticker: token, quantity: localPosition.collateralization, }); } } // add qualifying position as an opportunity to liquidate res.set(walletAddress, qualifyingPos); } return { liquidations: res, prices, }; } catch (error) { throw new Error(`Error in getLiquidations function: ${error}`); } }