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
13 changes: 7 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "auctioneer-bot",
"version": "2.0.0-beta",
"version": "2.0.1-beta",
"main": "index.js",
"type": "module",
"scripts": {
Expand All @@ -25,7 +25,7 @@
"typescript": "^5.5.4"
},
"dependencies": {
"@blend-capital/blend-sdk": "3.0.0",
"@blend-capital/blend-sdk": "3.0.1",
"@stellar/stellar-sdk": "13.2.0",
"better-sqlite3": "^11.1.2",
"winston": "^3.13.1",
Expand Down
6 changes: 6 additions & 0 deletions src/bidder_submitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,12 @@ export class BidderSubmitter extends SubmissionQueue<BidderSubmission> {

if (nextLedger >= fill.block) {
const pool = new PoolContractV2(auctionBid.auctionEntry.pool_id);
const est_profit = fill.lotValue - fill.bidValue;
// include high inclusion fee if the esimated profit is over $10
if (est_profit > 10) {
// this object gets recreated every time, so no need to reset the fee level
sorobanHelper.setFeeLevel('high');
}

const result = await sorobanHelper.submitTransaction(
pool.submit({
Expand Down
11 changes: 11 additions & 0 deletions src/interest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,17 @@ export async function checkPoolForInterestAuction(
const pool = await sorobanHelper.loadPool(poolId);
const poolOracle = await sorobanHelper.loadPoolOracle(poolId);

// check if there is an existing interest auction
const interestAuction = await sorobanHelper.loadAuction(
poolId,
APP_CONFIG.backstopAddress,
AuctionType.Interest
);
if (interestAuction !== undefined) {
logger.info(`Interest auction already exists for pool ${poolId}`);
return undefined;
}

// use the pools max auction lot size or at most 3 lot assets
let maxLotAssets = Math.min(pool.metadata.maxPositions - 1, 3);
let totalInterest = 0;
Expand Down
184 changes: 122 additions & 62 deletions src/liquidations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,27 @@ import { logger } from './utils/logger.js';
import { SorobanHelper } from './utils/soroban_helper.js';
import { WorkSubmission, WorkSubmissionType } from './work_submitter.js';
import { sendSlackNotification } from './utils/slack_notifier.js';
import { stringify } from './utils/json.js';

/**
* A representation of a position taking into account the oracle price.
*/
interface PricedPosition {
assetId: string;
index: number;
effectiveAmount: number;
baseAmount: number;
}

/**
* The result of a liquidation calculation.
* Contains the auction percent, lot and bid asset ids.
*/
interface LiquidationCalc {
auctionPercent: number;
lot: string[];
bid: string[];
}

/**
* Check if a user is liquidatable
Expand Down Expand Up @@ -45,15 +66,9 @@ export function calculateLiquidation(
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();
): LiquidationCalc {
let collateral: PricedPosition[] = [];
let liabilities: PricedPosition[] = [];

for (let [index, amount] of user.collateral) {
let assetId = pool.metadata.reserveList[index];
Expand All @@ -63,9 +78,13 @@ export function calculateLiquidation(
continue;
}
let effectiveAmount = reserve.toEffectiveAssetFromBTokenFloat(amount) * oraclePrice;
let rawAmount = reserve.toAssetFromBTokenFloat(amount) * oraclePrice;
effectiveCollaterals.push([index, effectiveAmount]);
rawCollaterals.set(assetId, rawAmount);
let baseAmount = reserve.toAssetFromBTokenFloat(amount) * oraclePrice;
collateral.push({
assetId,
index,
effectiveAmount,
baseAmount,
});
}
for (let [index, amount] of user.liabilities) {
let assetId = pool.metadata.reserveList[index];
Expand All @@ -75,84 +94,125 @@ export function calculateLiquidation(
continue;
}
let effectiveAmount = reserve.toEffectiveAssetFromDTokenFloat(amount) * oraclePrice;
let rawAmount = reserve.toAssetFromDTokenFloat(amount) * oraclePrice;
effectiveLiabilities.push([index, effectiveAmount]);
rawLiabilities.set(assetId, rawAmount);
let baseAmount = reserve.toAssetFromDTokenFloat(amount) * oraclePrice;
liabilities.push({
assetId,
index,
effectiveAmount,
baseAmount,
});
}

effectiveCollaterals.sort((a, b) => a[1] - b[1]);
effectiveLiabilities.sort((a, b) => a[1] - b[1]);
let firstCollateral = effectiveCollaterals.pop();
let firstLiability = effectiveLiabilities.pop();
// sort ascending by effective amount
collateral.sort((a, b) => a.effectiveAmount - b.effectiveAmount);
liabilities.sort((a, b) => a.effectiveAmount - b.effectiveAmount);
let largestCollateral = collateral.pop();
let largestLiability = liabilities.pop();

if (firstCollateral === undefined || firstLiability === undefined) {
if (largestCollateral === undefined || largestLiability === 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);

let liabilitesToReduce = Math.max(
0,
estimate.totalEffectiveLiabilities * 1.06 - estimate.totalEffectiveCollateral
let liabilitesToReduce =
estimate.totalEffectiveLiabilities * 1.06 - estimate.totalEffectiveCollateral;
if (liabilitesToReduce <= 0) {
throw new Error('No liabilities to reduce for liquidation calculation');
}

let effectiveCollateral = largestCollateral.effectiveAmount;
let baseCollateral = largestCollateral.baseAmount;
let effectiveLiabilities = largestLiability.effectiveAmount;
let baseLiabilities = largestLiability.baseAmount;

let bid: string[] = [largestLiability.assetId];
let lot: string[] = [largestCollateral.assetId];
let liqPercent = calculateLiqPercent(
effectiveCollateral,
baseCollateral,
effectiveLiabilities,
baseLiabilities,
liabilitesToReduce
);
let liqPercent = calculateLiqPercent(auctionEstimate, liabilitesToReduce);
while (liqPercent > 100 || liqPercent === 0) {
if (liqPercent > 100) {
let nextLiability = effectiveLiabilities.pop();
let nextLiability = liabilities.pop();
if (nextLiability === undefined) {
let nextCollateral = effectiveCollaterals.pop();
let nextCollateral = collateral.pop();
if (nextCollateral === undefined) {
// full liquidation required
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]),
lot: Array.from(user.collateral).map(([index]) => pool.metadata.reserveList[index]),
bid: Array.from(user.liabilities).map(([index]) => pool.metadata.reserveList[index]),
};
}
auction.collateral.set(nextCollateral[0], user.collateral.get(nextCollateral[0])!);
effectiveCollateral += nextCollateral.effectiveAmount;
baseCollateral += nextCollateral.baseAmount;
lot.push(nextCollateral.assetId);
} else {
auction.liabilities.set(nextLiability[0], user.liabilities.get(nextLiability[0])!);
effectiveLiabilities += nextLiability.effectiveAmount;
baseLiabilities += nextLiability.baseAmount;
bid.push(nextLiability.assetId);
}
} else if (liqPercent == 0) {
let nextCollateral = effectiveCollaterals.pop();
let nextCollateral = collateral.pop();
if (nextCollateral === undefined) {
// No more collaterals to liquidate
// full liquidation required
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])),
lot: Array.from(user.collateral).map(([index]) => pool.metadata.reserveList[index]),
bid: Array.from(user.liabilities).map(([index]) => pool.metadata.reserveList[index]),
};
}
auction.collateral.set(nextCollateral[0], user.collateral.get(nextCollateral[0])!);
effectiveCollateral += nextCollateral.effectiveAmount;
baseCollateral += nextCollateral.baseAmount;
lot.push(nextCollateral.assetId);
}
auctionEstimate = PositionsEstimate.build(pool, oracle, auction);
liqPercent = calculateLiqPercent(auctionEstimate, liabilitesToReduce);
liqPercent = calculateLiqPercent(
effectiveCollateral,
baseCollateral,
effectiveLiabilities,
baseLiabilities,
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]),
lot,
bid,
};
}

function calculateLiqPercent(positions: PositionsEstimate, excessLiabilities: number) {
let avgCF = positions.totalEffectiveCollateral / positions.totalSupplied;
let avgLF = positions.totalEffectiveLiabilities / positions.totalBorrowed;
/**
* Calculate the liquidation percent to bring the user back to a 1.06 HF
* @param effectiveCollateral - The effective collateral of the position to liquidate, in the pool's oracle denomination
* @param baseCollateral - The base collateral of the position to liquidate, in the pool's oracle denomination
* @param effectiveLiabilities - The effective liabilities of the position to liquidate, in the pool's oracle denomination
* @param baseLiabilities - The base liabilities of the position to liquidate, in the pool's oracle denomination
* @param excessLiabilities - The excess liabilities over the borrow limit, in the pool's oracle denomination
* @returns A percentage of the borrow limit that needs to be liquidated.
* A percentage of 0 means there is not enough collateral to cover the liquidated liabilities.
* A percentage over 100 means there is not enough liabilities being liquidated to cover the excess.
*/
function calculateLiqPercent(
effectiveCollateral: number,
baseCollateral: number,
effectiveLiabilities: number,
baseLiabilities: number,
excessLiabilities: number
) {
let avgCF = effectiveCollateral / baseCollateral;
let avgLF = effectiveLiabilities / baseLiabilities;
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 totalBorrowLimitRecovered = borrowLimitFactor * baseLiabilities;
let liqPercent = Math.round((excessLiabilities / totalBorrowLimitRecovered) * 100);
let requiredRawCollateral = (liqPercent / 100) * positions.totalBorrowed * estIncentive;
let requiredBaseCollateral = (liqPercent / 100) * baseLiabilities * estIncentive;

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

Expand Down Expand Up @@ -215,15 +275,19 @@ export async function checkUsersForLiquidationsAndBadDebt(
isBadDebt(backstopPostionsEstimate) &&
(await sorobanHelper.loadAuction(poolId, user, AuctionType.BadDebt)) === undefined
) {
let backstopLiabilities = Array.from(backstop.positions.liabilities.keys()).map(
(index) => pool.metadata.reserveList[index]
);
if (backstopLiabilities.length >= pool.metadata.maxPositions) {
backstopLiabilities = backstopLiabilities.slice(0, pool.metadata.maxPositions - 1);
}
submissions.push({
type: WorkSubmissionType.AuctionCreation,
poolId,
user: APP_CONFIG.backstopAddress,
auctionType: AuctionType.BadDebt,
auctionPercent: 100,
bid: Array.from(backstop.positions.liabilities.keys()).map(
(index) => pool.metadata.reserveList[index]
),
bid: backstopLiabilities,
lot: [APP_CONFIG.backstopTokenAddress],
});
}
Expand All @@ -242,12 +306,8 @@ export async function checkUsersForLiquidationsAndBadDebt(
user,
auctionPercent: newLiq.auctionPercent,
auctionType: AuctionType.Liquidation,
bid: Array.from(poolUser.positions.liabilities.keys()).map(
(index) => pool.metadata.reserveList[index]
),
lot: Array.from(poolUser.positions.collateral.keys()).map(
(index) => pool.metadata.reserveList[index]
),
bid: newLiq.bid,
lot: newLiq.lot,
});
} else if (isBadDebt(poolUserEstimate)) {
submissions.push({
Expand Down
6 changes: 5 additions & 1 deletion src/utils/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ export interface AppConfig {
priceSources: PriceSource[] | undefined;
profits: AuctionProfit[] | undefined;
slackWebhook: string | undefined;
highBaseFee: number | undefined;
baseFee: number | undefined;
}

let APP_CONFIG: AppConfig;
Expand Down Expand Up @@ -96,7 +98,9 @@ export function validateAppConfig(config: any): boolean {
(config.horizonURL !== undefined && typeof config.horizonURL !== 'string') ||
(config.priceSources !== undefined && !Array.isArray(config.priceSources)) ||
(config.profits !== undefined && !Array.isArray(config.profits)) ||
(config.slackWebhook !== undefined && typeof config.slackWebhook !== 'string')
(config.slackWebhook !== undefined && typeof config.slackWebhook !== 'string') ||
(config.highBaseFee !== undefined && typeof config.highBaseFee !== 'number') ||
(config.baseFee !== undefined && typeof config.baseFee !== 'number')
) {
console.log('Invalid app config');
return false;
Expand Down
Loading