diff --git a/src/liquidations.ts b/src/liquidations.ts index 42d43c7..f275388 100644 --- a/src/liquidations.ts +++ b/src/liquidations.ts @@ -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 = new Map(); + let effectiveLiabilities: [number, number][] = []; + let rawLiabilities: Map = 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; } @@ -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] diff --git a/test/helpers/utils.ts b/test/helpers/utils.ts index c1fc545..c2a5693 100644 --- a/test/helpers/utils.ts +++ b/test/helpers/utils.ts @@ -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. @@ -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]; +} diff --git a/test/liquidations.test.ts b/test/liquidations.test.ts index fb366b4..578992b 100644 --- a/test/liquidations.test.ts +++ b/test/liquidations.test.ts @@ -7,7 +7,7 @@ import { } from '@blend-capital/blend-sdk'; import { Keypair } from '@stellar/stellar-sdk'; import { - calculateLiquidationPercent, + calculateLiquidation, checkUsersForLiquidationsAndBadDebt, isBadDebt, isLiquidatable, @@ -18,6 +18,7 @@ import { AuctioneerDatabase } from '../src/utils/db.js'; import { PoolUserEst, SorobanHelper } from '../src/utils/soroban_helper.js'; import { WorkSubmissionType } from '../src/work_submitter.js'; import { + AQUA, AQUA_ID, EURC, EURC_ID, @@ -29,6 +30,7 @@ import { XLM, XLM_ID, } from './helpers/mocks.js'; +import { buildAuction } from './helpers/utils.js'; jest.mock('../src/utils/soroban_helper.js'); jest.mock('../src/utils/logger.js', () => ({ @@ -104,39 +106,376 @@ describe('isBadDebt', () => { }); }); -describe('calculateLiquidationPercent', () => { +describe('calculateLiquidation', () => { let userEstimate: PositionsEstimate; + let userPositions: Positions; beforeEach(() => { userEstimate = new PositionsEstimate(0, 0, 0, 0, 0, 0, 0, 0, 0); }); - it('should calculate the correct liquidation percent for typical values', () => { - userEstimate.totalEffectiveCollateral = 1000; - userEstimate.totalEffectiveLiabilities = 1100; - userEstimate.totalBorrowed = 1500; - userEstimate.totalSupplied = 2000; - const result = calculateLiquidationPercent(userEstimate); - expect(Number(result)).toBe(56); + it('should find auction subset with partial auction', () => { + let userPositions = new Positions( + new Map([ + [USDC_ID, BigInt(2000e7)], + [EURC_ID, BigInt(1000e7)], + ]), + new Map([ + [XLM_ID, BigInt(38000e7)], + [EURC_ID, BigInt(500e7)], + ]), + new Map([]) + ); + userEstimate = PositionsEstimate.build(mockPool, mockPoolOracle, userPositions); + + const result = calculateLiquidation(mockPool, userPositions, userEstimate, mockPoolOracle); + + let [auction, auctionEstimate] = buildAuction( + userPositions, + result.auctionPercent, + result.bid, + result.lot, + mockPool, + mockPoolOracle + ); + let postAuctionUser = userPositions; + + for (let [index, amount] of auction.collateral) { + postAuctionUser.collateral.set(index, postAuctionUser.collateral.get(index)! - amount); + } + + for (let [index, amount] of auction.liabilities) { + postAuctionUser.liabilities.set(index, postAuctionUser.liabilities.get(index)! - amount); + } + + const userEstimateAfterAuction = PositionsEstimate.build( + mockPool, + mockPoolOracle, + postAuctionUser + ); + + expect(result.auctionPercent).toBe(46); + expect(result.bid).toContain(USDC); + expect(result.lot).toContain(XLM); + expect(auctionEstimate.totalSupplied > auctionEstimate.totalBorrowed).toBe(true); + expect(userEstimateAfterAuction.totalEffectiveCollateral).toBeGreaterThan( + userEstimateAfterAuction.totalEffectiveLiabilities + ); + + // Check the assets are sorted by value (highest first) + const collateralValues = result.lot.map((assetId) => { + const index = mockPool.metadata.reserveList.indexOf(assetId); + const amount = userPositions.collateral.get(index) || BigInt(0); + const price = mockPoolOracle.getPriceFloat(assetId) || 0; + const reserve = mockPool.reserves.get(assetId); + if (!reserve) return 0; + return reserve.toEffectiveAssetFromBTokenFloat(amount) * price; + }); + + // Check that values are in descending order + for (let i = 1; i < collateralValues.length; i++) { + expect(collateralValues[i - 1]).toBeGreaterThanOrEqual(collateralValues[i]); + } + + const liabilityValues = result.bid.map((assetId) => { + const index = mockPool.metadata.reserveList.indexOf(assetId); + const amount = userPositions.liabilities.get(index) || BigInt(0); + const price = mockPoolOracle.getPriceFloat(assetId) || 0; + const reserve = mockPool.reserves.get(assetId); + if (!reserve) return 0; + return reserve.toEffectiveAssetFromDTokenFloat(amount) * price; + }); + + // Check that liability values are in descending order + for (let i = 1; i < liabilityValues.length; i++) { + expect(liabilityValues[i - 1]).toBeGreaterThanOrEqual(liabilityValues[i]); + } + }); + + it('should return 100% liquidation when health factor is very poor', () => { + // Create positions with very high liabilities compared to collateral + userPositions = new Positions( + new Map([ + [USDC_ID, BigInt(5000e7)], + [XLM_ID, BigInt(1e7)], + [AQUA_ID, BigInt(1e7)], + ]), + new Map([[XLM_ID, BigInt(500e7)]]), + new Map([]) + ); + + userEstimate = PositionsEstimate.build(mockPool, mockPoolOracle, userPositions); + + const result = calculateLiquidation(mockPool, userPositions, userEstimate, mockPoolOracle); + expect(result.auctionPercent).toBe(100); + expect(result.bid).toContain(USDC); + expect(result.bid).toContain(XLM); + expect(result.bid).toContain(AQUA); + expect(result.lot).toContain(XLM); + }); + + it('should calculate partial liquidation for marginally unhealthy position with no valid subsets', () => { + userPositions = new Positions( + new Map([ + [USDC_ID, BigInt(1200e7)], + [AQUA_ID, BigInt(800000e7)], + ]), + new Map([ + [XLM_ID, BigInt(13050e7)], + [EURC_ID, BigInt(1150e7)], + ]), + new Map([]) + ); + + // Setup for a position just slightly below health threshold + userEstimate = PositionsEstimate.build(mockPool, mockPoolOracle, userPositions); + const result = calculateLiquidation(mockPool, userPositions, userEstimate, mockPoolOracle); + let [auction, auctionEstimate] = buildAuction( + userPositions, + result.auctionPercent, + result.bid, + result.lot, + mockPool, + mockPoolOracle + ); + + let postAuctionUser = userPositions; + + for (let [index, amount] of auction.collateral) { + postAuctionUser.collateral.set(index, postAuctionUser.collateral.get(index)! - amount); + } + + for (let [index, amount] of auction.liabilities) { + postAuctionUser.liabilities.set(index, postAuctionUser.liabilities.get(index)! - amount); + } + + const userEstimateAfterAuction = PositionsEstimate.build( + mockPool, + mockPoolOracle, + postAuctionUser + ); + + expect(result.auctionPercent).toBe(45); + expect(result.bid).toContain(USDC); + expect(result.bid).toContain(AQUA); + expect(result.lot).toContain(XLM); + expect(result.lot).toContain(EURC); + expect(auctionEstimate.totalSupplied > auctionEstimate.totalBorrowed).toBe(true); + expect(userEstimateAfterAuction.totalEffectiveCollateral).toBeGreaterThan( + userEstimateAfterAuction.totalEffectiveLiabilities + ); + + // Check the assets are sorted by value (highest first) + const collateralValues = result.lot.map((assetId) => { + const index = mockPool.metadata.reserveList.indexOf(assetId); + const amount = userPositions.collateral.get(index) || BigInt(0); + const price = mockPoolOracle.getPriceFloat(assetId) || 0; + const reserve = mockPool.reserves.get(assetId); + if (!reserve) return 0; + return reserve.toEffectiveAssetFromBTokenFloat(amount) * price; + }); + + // Check that values are in descending order + for (let i = 1; i < collateralValues.length; i++) { + expect(collateralValues[i - 1]).toBeGreaterThanOrEqual(collateralValues[i]); + } + + const liabilityValues = result.bid.map((assetId) => { + const index = mockPool.metadata.reserveList.indexOf(assetId); + const amount = userPositions.liabilities.get(index) || BigInt(0); + const price = mockPoolOracle.getPriceFloat(assetId) || 0; + const reserve = mockPool.reserves.get(assetId); + if (!reserve) return 0; + return reserve.toEffectiveAssetFromDTokenFloat(amount) * price; + }); + + // Check that liability values are in descending order + for (let i = 1; i < liabilityValues.length; i++) { + expect(liabilityValues[i - 1]).toBeGreaterThanOrEqual(liabilityValues[i]); + } + }); + + it('should find auction with single collateral and multiple liabilities', () => { + userPositions = new Positions( + new Map([ + [USDC_ID, BigInt(1100e7)], + [AQUA_ID, BigInt(850000e7)], + [XLM_ID, BigInt(9000e7)], + ]), + new Map([[EURC_ID, BigInt(3000e7)]]), + new Map([]) + ); + + // Setup for a position just slightly below health threshold + userEstimate = PositionsEstimate.build(mockPool, mockPoolOracle, userPositions); + const result = calculateLiquidation(mockPool, userPositions, userEstimate, mockPoolOracle); + let [auction, auctionEstimate] = buildAuction( + userPositions, + result.auctionPercent, + result.bid, + result.lot, + mockPool, + mockPoolOracle + ); + + let postAuctionUser = userPositions; + + for (let [index, amount] of auction.collateral) { + postAuctionUser.collateral.set(index, postAuctionUser.collateral.get(index)! - amount); + } + + for (let [index, amount] of auction.liabilities) { + postAuctionUser.liabilities.set(index, postAuctionUser.liabilities.get(index)! - amount); + } + + const userEstimateAfterAuction = PositionsEstimate.build( + mockPool, + mockPoolOracle, + postAuctionUser + ); + + expect(result.auctionPercent).toBe(71); + expect(result.bid).toContain(USDC); + expect(result.bid).toContain(AQUA); + expect(result.bid).toContain(XLM); + expect(result.lot).toContain(EURC); + expect(auctionEstimate.totalSupplied > auctionEstimate.totalBorrowed).toBe(true); + expect(userEstimateAfterAuction.totalEffectiveCollateral).toBeGreaterThan( + userEstimateAfterAuction.totalEffectiveLiabilities + ); + }); + + it('should find auction with single liability and multiple collaterals', () => { + userPositions = new Positions( + new Map([[XLM_ID, BigInt(21000e7)]]), + new Map([ + [USDC_ID, BigInt(1000e7)], + [XLM_ID, BigInt(8000e7)], + [EURC_ID, BigInt(1000e7)], + ]), + new Map([]) + ); + + // Setup for a position just slightly below health threshold + userEstimate = PositionsEstimate.build(mockPool, mockPoolOracle, userPositions); + const result = calculateLiquidation(mockPool, userPositions, userEstimate, mockPoolOracle); + let [auction, auctionEstimate] = buildAuction( + userPositions, + result.auctionPercent, + result.bid, + result.lot, + mockPool, + mockPoolOracle + ); + + let postAuctionUser = userPositions; + + for (let [index, amount] of auction.collateral) { + postAuctionUser.collateral.set(index, postAuctionUser.collateral.get(index)! - amount); + } + + for (let [index, amount] of auction.liabilities) { + postAuctionUser.liabilities.set(index, postAuctionUser.liabilities.get(index)! - amount); + } + + const userEstimateAfterAuction = PositionsEstimate.build( + mockPool, + mockPoolOracle, + postAuctionUser + ); + + expect(result.auctionPercent).toBe(56); + expect(result.lot).toContain(USDC); + expect(result.lot).toContain(EURC); + expect(result.bid).toContain(XLM); + expect(auctionEstimate.totalSupplied > auctionEstimate.totalBorrowed).toBe(true); + expect(userEstimateAfterAuction.totalEffectiveCollateral).toBeGreaterThan( + userEstimateAfterAuction.totalEffectiveLiabilities + ); }); - it('should calculate max of 100 percent liquidation size', () => { - userEstimate.totalEffectiveCollateral = 1700; - userEstimate.totalEffectiveLiabilities = 2200; - userEstimate.totalBorrowed = 1900; - userEstimate.totalSupplied = 2000; - const result = calculateLiquidationPercent(userEstimate); + it('should find partial auction with single liability and single collateral', () => { + userPositions = new Positions( + new Map([[XLM_ID, BigInt(74e7)]]), + new Map([[USDC_ID, BigInt(10e7)]]), + new Map([]) + ); + + // Setup for a position just slightly below health threshold + userEstimate = PositionsEstimate.build(mockPool, mockPoolOracle, userPositions); + const result = calculateLiquidation(mockPool, userPositions, userEstimate, mockPoolOracle); + let [auction, auctionEstimate] = buildAuction( + userPositions, + result.auctionPercent, + result.bid, + result.lot, + mockPool, + mockPoolOracle + ); + + let postAuctionUser = userPositions; - expect(Number(result)).toBe(100); + for (let [index, amount] of auction.collateral) { + postAuctionUser.collateral.set(index, postAuctionUser.collateral.get(index)! - amount); + } + + for (let [index, amount] of auction.liabilities) { + postAuctionUser.liabilities.set(index, postAuctionUser.liabilities.get(index)! - amount); + } + + const userEstimateAfterAuction = PositionsEstimate.build( + mockPool, + mockPoolOracle, + postAuctionUser + ); + + expect(result.auctionPercent).toBe(33); + expect(result.lot).toContain(USDC); + expect(result.bid).toContain(XLM); + expect(auctionEstimate.totalSupplied > auctionEstimate.totalBorrowed).toBe(true); + expect(userEstimateAfterAuction.totalEffectiveCollateral).toBeGreaterThan( + userEstimateAfterAuction.totalEffectiveLiabilities + ); }); - it('should calculate the smallest possible liquidation size', () => { - userEstimate.totalEffectiveCollateral = 2199; - userEstimate.totalEffectiveLiabilities = 2200; - userEstimate.totalBorrowed = 1900; - userEstimate.totalSupplied = 10000000000000; - const result = calculateLiquidationPercent(userEstimate); + it('should find full auction with single liability and single collateral', () => { + userPositions = new Positions( + new Map([[XLM_ID, BigInt(89e7)]]), + new Map([[USDC_ID, BigInt(10e7)]]), + new Map([]) + ); + + // Setup for a position just slightly below health threshold + userEstimate = PositionsEstimate.build(mockPool, mockPoolOracle, userPositions); + const result = calculateLiquidation(mockPool, userPositions, userEstimate, mockPoolOracle); + let [auction, auctionEstimate] = buildAuction( + userPositions, + result.auctionPercent, + result.bid, + result.lot, + mockPool, + mockPoolOracle + ); + + let postAuctionUser = userPositions; + + for (let [index, amount] of auction.collateral) { + postAuctionUser.collateral.set(index, postAuctionUser.collateral.get(index)! - amount); + } + + for (let [index, amount] of auction.liabilities) { + postAuctionUser.liabilities.set(index, postAuctionUser.liabilities.get(index)! - amount); + } - expect(Number(result)).toBe(6); + const userEstimateAfterAuction = PositionsEstimate.build( + mockPool, + mockPoolOracle, + postAuctionUser + ); + + expect(result.auctionPercent).toBe(100); + expect(result.lot).toContain(USDC); + expect(result.bid).toContain(XLM); + expect(auctionEstimate.totalSupplied > auctionEstimate.totalBorrowed).toBe(true); }); }); @@ -167,10 +506,16 @@ describe('scanUsers', () => { }); it('should create a work submission for liquidatable users', async () => { - mockPoolUserEstimate.totalEffectiveCollateral = 1000; - mockPoolUserEstimate.totalEffectiveLiabilities = 1100; - mockPoolUserEstimate.totalBorrowed = 1500; - mockPoolUserEstimate.totalSupplied = 2000; + mockPoolUser.positions = new Positions( + new Map([[USDC_ID, BigInt(300e7)]]), + new Map([[XLM_ID, BigInt(3000e7)]]), + new Map() + ); + mockPoolUserEstimate = PositionsEstimate.build( + mockPool, + mockPoolOracle, + mockPoolUser.positions + ); db.setUserEntry({ pool_id: 'pool1', user_id: mockPoolUser.userId, @@ -207,6 +552,7 @@ describe('scanUsers', () => { return Promise.resolve({ estimate: {}, user: {} } as PoolUserEst); } ); + mockedSorobanHelper.loadPoolOracle.mockResolvedValue(mockPoolOracle); mockedSorobanHelper.loadAuction.mockResolvedValue(undefined); let liquidations = await scanUsers(db, mockedSorobanHelper); @@ -364,7 +710,7 @@ describe('checkUsersForLiquidationsAndBadDebt', () => { user: mockUser, }); mockedSorobanHelper.loadAuction.mockResolvedValue(undefined); - + mockedSorobanHelper.loadPoolOracle.mockResolvedValue(mockPoolOracle); const result = await checkUsersForLiquidationsAndBadDebt( db, mockedSorobanHelper, @@ -379,7 +725,7 @@ describe('checkUsersForLiquidationsAndBadDebt', () => { poolId: mockPool.id, auctionType: AuctionType.Liquidation, user: 'user1', - auctionPercent: 56, + auctionPercent: 100, bid: [USDC], lot: [XLM], },