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
text/typescript
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}`);
}
}