Skip to content
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
129 changes: 116 additions & 13 deletions src/liquidations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,20 +40,122 @@ export function isBadDebt(user: PositionsEstimate): boolean {
* @param user - The positions estimate of the user
* @returns The liquidation percent
*/
export function calculateLiquidationPercent(user: PositionsEstimate): number {
const avgInverseLF = user.totalEffectiveLiabilities / user.totalBorrowed;
const avgCF = user.totalEffectiveCollateral / user.totalSupplied;
const estIncentive = 1 + (1 - avgCF / avgInverseLF) / 2;
const numerator = user.totalEffectiveLiabilities * 1.06 - user.totalEffectiveCollateral;
const denominator = avgInverseLF * 1.06 - avgCF * estIncentive;
const liqPercent = Math.min(
Math.round((numerator / denominator / user.totalBorrowed) * 100),
100
export function calculateLiquidation(
pool: Pool,
user: Positions,
estimate: PositionsEstimate,
oracle: PoolOracle
): {
auctionPercent: number;
lot: string[];
bid: string[];
} {
let effectiveCollaterals: [number, number][] = [];
let rawCollaterals: Map<string, number> = new Map();
let effectiveLiabilities: [number, number][] = [];
let rawLiabilities: Map<string, number> = new Map();

for (let [index, amount] of user.collateral) {
let assetId = pool.metadata.reserveList[index];
let oraclePrice = oracle.getPriceFloat(assetId);
let reserve = pool.reserves.get(assetId);
if (oraclePrice === undefined || reserve === undefined) {
continue;
}
let effectiveAmount = reserve.toEffectiveAssetFromBTokenFloat(amount) * oraclePrice;
let rawAmount = reserve.toAssetFromBTokenFloat(amount) * oraclePrice;
effectiveCollaterals.push([index, effectiveAmount]);
rawCollaterals.set(assetId, rawAmount);
}
for (let [index, amount] of user.liabilities) {
let assetId = pool.metadata.reserveList[index];
let oraclePrice = oracle.getPriceFloat(assetId);
let reserve = pool.reserves.get(assetId);
if (oraclePrice === undefined || reserve === undefined) {
continue;
}
let effectiveAmount = reserve.toEffectiveAssetFromDTokenFloat(amount) * oraclePrice;
let rawAmount = reserve.toAssetFromDTokenFloat(amount) * oraclePrice;
effectiveLiabilities.push([index, effectiveAmount]);
rawLiabilities.set(assetId, rawAmount);
}

effectiveCollaterals.sort((a, b) => a[1] - b[1]);
effectiveLiabilities.sort((a, b) => a[1] - b[1]);
let firstCollateral = effectiveCollaterals.pop();
let firstLiability = effectiveLiabilities.pop();

if (firstCollateral === undefined || firstLiability === undefined) {
throw new Error('No collaterals or liabilities found for liquidation calculation');
}
let auction = new Positions(
new Map([[firstLiability[0], user.liabilities.get(firstLiability[0])!]]),
new Map([[firstCollateral[0], user.collateral.get(firstCollateral[0])!]]),
new Map()
);
let auctionEstimate = PositionsEstimate.build(pool, oracle, auction);

logger.info(
`Calculated liquidation percent ${liqPercent} with est incentive ${estIncentive} numerator ${numerator} and denominator ${denominator} for user ${user}.`
let liabilitesToReduce = Math.max(
0,
estimate.totalEffectiveLiabilities * 1.06 - estimate.totalEffectiveCollateral
);
let liqPercent = calculateLiqPercent(auctionEstimate, liabilitesToReduce);
while (liqPercent > 100 || liqPercent === 0) {
if (liqPercent > 100) {
let nextLiability = effectiveLiabilities.pop();
if (nextLiability === undefined) {
let nextCollateral = effectiveCollaterals.pop();
if (nextCollateral === undefined) {
return {
auctionPercent: 100,
lot: Array.from(auction.collateral).map(([index]) => pool.metadata.reserveList[index]),
bid: Array.from(auction.liabilities).map(([index]) => pool.metadata.reserveList[index]),
};
}
auction.collateral.set(nextCollateral[0], user.collateral.get(nextCollateral[0])!);
} else {
auction.liabilities.set(nextLiability[0], user.liabilities.get(nextLiability[0])!);
}
} else if (liqPercent == 0) {
let nextCollateral = effectiveCollaterals.pop();
if (nextCollateral === undefined) {
// No more collaterals to liquidate
return {
auctionPercent: 100,
lot: Array.from(auction.collateral).map(([index]) => pool.metadata.reserveList[index]),
bid: Array.from(auction.liabilities)
.map(([index]) => pool.metadata.reserveList[index])
.concat(effectiveLiabilities.map(([index]) => pool.metadata.reserveList[index])),
};
}
auction.collateral.set(nextCollateral[0], user.collateral.get(nextCollateral[0])!);
}
auctionEstimate = PositionsEstimate.build(pool, oracle, auction);
liqPercent = calculateLiqPercent(auctionEstimate, liabilitesToReduce);
}

return {
auctionPercent: liqPercent,
lot: Array.from(auction.collateral).map(([index]) => pool.metadata.reserveList[index]),
bid: Array.from(auction.liabilities).map(([index]) => pool.metadata.reserveList[index]),
};
}

function calculateLiqPercent(positions: PositionsEstimate, excessLiabilities: number) {
let avgCF = positions.totalEffectiveCollateral / positions.totalSupplied;
let avgLF = positions.totalEffectiveLiabilities / positions.totalBorrowed;
let estIncentive = 1 + (1 - avgCF / avgLF) / 2;
// The factor by which the effective liabilities are reduced per raw liability
let borrowLimitFactor = avgLF * 1.06 - estIncentive * avgCF;

let totalBorrowLimitRecovered = borrowLimitFactor * positions.totalBorrowed;
let liqPercent = Math.round((excessLiabilities / totalBorrowLimitRecovered) * 100);
let requiredRawCollateral = (liqPercent / 100) * positions.totalBorrowed * estIncentive;

if (requiredRawCollateral > positions.totalSupplied) {
return 0; // Not enough collateral to cover the liquidation
}

return liqPercent;
}

Expand Down Expand Up @@ -130,14 +232,15 @@ export async function checkUsersForLiquidationsAndBadDebt(
) {
const { estimate: poolUserEstimate, user: poolUser } =
await sorobanHelper.loadUserPositionEstimate(poolId, user);
const oracle = await sorobanHelper.loadPoolOracle(poolId);
updateUser(db, pool, poolUser, poolUserEstimate);
if (isLiquidatable(poolUserEstimate)) {
const auctionPercent = calculateLiquidationPercent(poolUserEstimate);
const newLiq = calculateLiquidation(pool, poolUser.positions, poolUserEstimate, oracle);
submissions.push({
type: WorkSubmissionType.AuctionCreation,
poolId,
user,
auctionPercent,
auctionPercent: newLiq.auctionPercent,
auctionType: AuctionType.Liquidation,
bid: Array.from(poolUser.positions.liabilities.keys()).map(
(index) => pool.metadata.reserveList[index]
Expand Down
61 changes: 61 additions & 0 deletions test/helpers/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
import {
FixedMath,
PoolOracle,
PoolV2,
Positions,
PositionsEstimate,
} from '@blend-capital/blend-sdk';
import { mockPool, mockPoolOracle } from './mocks';

/**
* Assert that a and b are approximately equal, relative to the smaller of the two,
* within epsilon as a percentage.
Expand All @@ -8,3 +17,55 @@
export function expectRelApproxEqual(a: number, b: number, epsilon = 0.001) {
expect(Math.abs(a - b) / Math.min(a, b)).toBeLessThanOrEqual(epsilon);
}

export function buildAuction(
userPositions: Positions,
auctionPercent: number,
bid: string[],
lot: string[],
pool: PoolV2,
oracle: PoolOracle
): [Positions, PositionsEstimate] {
let positionsToAuction = new Positions(new Map([]), new Map(), new Map());
bid.map((asset) => {
let index = mockPool.metadata.reserveList.indexOf(asset);
let amount = userPositions.liabilities.get(index)!;
positionsToAuction.liabilities.set(index, amount);
});

lot.map((asset) => {
let index = mockPool.metadata.reserveList.indexOf(asset);
let amount = userPositions.collateral.get(index)!;
positionsToAuction.collateral.set(index, amount);
});

let auctionPositionsEstimate = PositionsEstimate.build(
mockPool,
mockPoolOracle,
positionsToAuction
);
let auctionPositionCF =
auctionPositionsEstimate.totalEffectiveCollateral / auctionPositionsEstimate.totalSupplied;
let auctionPositionLF =
auctionPositionsEstimate.totalEffectiveLiabilities / auctionPositionsEstimate.totalBorrowed;
let auctionIncentive = 1 + (1 - auctionPositionCF / auctionPositionLF) / 2;
let withdrawnCollateralPct = Math.ceil(
((((auctionPositionsEstimate.totalBorrowed * auctionPercent) / 100) * auctionIncentive) /
auctionPositionsEstimate.totalSupplied) *
100
);

let auction = new Positions(new Map([]), new Map(), new Map());
for (let [index, amount] of positionsToAuction.liabilities) {
auction.liabilities.set(index, FixedMath.mulCeil(amount, BigInt(auctionPercent), BigInt(100)));
}
for (let [index, amount] of positionsToAuction.collateral) {
auction.collateral.set(
index,
FixedMath.mulCeil(amount, BigInt(withdrawnCollateralPct), BigInt(100))
);
}

const auctionEstimate = PositionsEstimate.build(mockPool, mockPoolOracle, auction);
return [auction, auctionEstimate];
}
Loading