diff --git a/package-lock.json b/package-lock.json index a3013b0..ed78c80 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "auctioneer-bot", - "version": "3.1.2", + "version": "3.1.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "auctioneer-bot", - "version": "3.1.2", + "version": "3.1.3", "license": "MIT", "dependencies": { "@blend-capital/blend-sdk": "3.2.2", @@ -1171,13 +1171,13 @@ } }, "node_modules/axios": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", - "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", + "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, @@ -2184,10 +2184,11 @@ } }, "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -2261,9 +2262,9 @@ } }, "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -3590,10 +3591,11 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, diff --git a/package.json b/package.json index 9ac08a5..35012e3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "auctioneer-bot", - "version": "3.1.2", + "version": "3.1.3", "main": "index.js", "type": "module", "scripts": { diff --git a/src/auction.ts b/src/auction.ts index 2b6ed90..5a79855 100644 --- a/src/auction.ts +++ b/src/auction.ts @@ -176,6 +176,7 @@ export async function calculateBlockFillAndPercent( ); // attempt to repay any liabilities the filler has took on from the bids + let allBidLiabilitiesRepaid = true; for (const [assetId, amount] of loopScaledAuction.data.bid) { const balance = loopFillerBalances.get(assetId) ?? 0n; if (balance > 0n && amount > 0n) { @@ -185,6 +186,9 @@ export async function calculateBlockFillAndPercent( // 100n prevents dust positions from being created, and is deducted from the repaid liability const amountAsUnderlying = reserve.toAssetFromDToken(amount) + 100n; const repaidLiability = amountAsUnderlying <= balance ? amountAsUnderlying : balance; + if (amountAsUnderlying > balance) { + allBidLiabilitiesRepaid = false; + } const effectiveLiability = FixedMath.toFloat(repaidLiability - 100n, reserve.config.decimals) * reserve.getLiabilityFactor() * @@ -197,7 +201,11 @@ export async function calculateBlockFillAndPercent( address: assetId, amount: repaidLiability, }); + } else { + allBidLiabilitiesRepaid = false; } + } else if (amount > 0n) { + allBidLiabilitiesRepaid = false; } } @@ -215,12 +223,13 @@ export async function calculateBlockFillAndPercent( } } - if (limitToHF < 0) { + if (limitToHF < 0 && !allBidLiabilitiesRepaid) { // if we still are under the health factor, we need to try and add more of the fillers primary asset as collateral const primaryBalance = loopFillerBalances.get(poolConfig.primaryAsset) ?? 0n; const primaryReserve = pool.reserves.get(poolConfig.primaryAsset); const primaryOraclePrice = poolOracle.getPriceFloat(poolConfig.primaryAsset); if ( + pool.metadata.status <= 3 && // don't add collateral if pool is frozen primaryReserve !== undefined && primaryOraclePrice !== undefined && primaryBalance > 0n diff --git a/test/auction.test.ts b/test/auction.test.ts index 8b5a9de..8495c9d 100644 --- a/test/auction.test.ts +++ b/test/auction.test.ts @@ -84,6 +84,7 @@ describe('auctions', () => { supplyApy: 0, borrowApy: 0, }; + mockPool.metadata.status = 1; // active mockedSorobanHelper.loadPool.mockResolvedValue(mockPool); mockedSorobanHelper.loadPoolOracle.mockResolvedValue(mockPoolOracle); mockedSorobanHelper.loadUserPositionEstimate.mockResolvedValue({ @@ -423,7 +424,7 @@ describe('auctions', () => { ); }); - it('calcs fill for liquidation auction and repays incoming liabilties and withdraws 0 CF collateral', async () => { + it('calcs fill for liquidation auction and repays incoming liabilities and withdraws 0 CF collateral', async () => { let user = Keypair.random().publicKey(); let nextLedger = MOCK_LEDGER + 1; let auction = new Auction(user, AuctionType.Liquidation, { @@ -482,6 +483,65 @@ describe('auctions', () => { expectRelApproxEqual(fill.bidValue, 29.73769976, 0.005); }); + it('calcs fill for liquidation auction no existing positions and repays incoming liabilities and withdraws 0 CF collateral', async () => { + let user = Keypair.random().publicKey(); + let nextLedger = MOCK_LEDGER + 1; + let auction = new Auction(user, AuctionType.Liquidation, { + lot: new Map([ + [USDC, FixedMath.toFixed(15.93)], + [EURC, FixedMath.toFixed(16.211)], + [AQUA, FixedMath.toFixed(750)], + ]), + bid: new Map([[XLM, FixedMath.toFixed(300.21)]]), + block: MOCK_LEDGER, + }); + positionEstimate.totalEffectiveLiabilities = 0; + positionEstimate.totalEffectiveCollateral = 0; + + mockedSorobanHelper.loadUserPositionEstimate.mockResolvedValue({ + user: {} as PoolUser, + estimate: positionEstimate, + }); + mockedGetFillerProfitPct.mockReturnValue(0.1); + mockedGetFilledAvailableBalances.mockResolvedValue( + new Map([ + [USDC, FixedMath.toFixed(100)], + [XLM, FixedMath.toFixed(500)], + ]) + ); + + let fill = await calculateAuctionFill( + poolConfig, + auction, + nextLedger, + mockedSorobanHelper, + db + ); + + let expectedRequests: Request[] = [ + { + request_type: 6, + address: user, + amount: 100n, + }, + { + request_type: 5, + address: XLM, + amount: 3003808157n, + }, + { + request_type: 3, + address: AQUA, + amount: BigInt('9223372036854775807'), + }, + ]; + expect(fill.block).toEqual(MOCK_LEDGER + 191); + expect(fill.percent).toEqual(100); + expect(fill.requests).toEqual(expectedRequests); + expectRelApproxEqual(fill.lotValue, 32.7722, 0.005); + expectRelApproxEqual(fill.bidValue, 29.73769976, 0.005); + }); + it('calcs fill for liquidation auction adds primary collateral', async () => { let user = Keypair.random().publicKey(); let nextLedger = MOCK_LEDGER + 186; @@ -542,6 +602,61 @@ describe('auctions', () => { expectRelApproxEqual(fill.bidValue, 8378.033243, 0.005); }); + it('calcs fill for liquidation auction scales and does not add collateral when pool frozen', async () => { + let user = Keypair.random().publicKey(); + let nextLedger = MOCK_LEDGER + 186; + let auction = new Auction(user, AuctionType.Liquidation, { + lot: new Map([[XLM, FixedMath.toFixed(100000)]]), + bid: new Map([ + [USDC, FixedMath.toFixed(100)], + [EURC, FixedMath.toFixed(7500)], + ]), + block: MOCK_LEDGER, + }); + positionEstimate.totalEffectiveLiabilities = 0; + positionEstimate.totalEffectiveCollateral = 1000; + mockPool.metadata.status = 4; // frozen + + mockedSorobanHelper.loadUserPositionEstimate.mockResolvedValue({ + user: {} as PoolUser, + estimate: positionEstimate, + }); + mockedGetFillerProfitPct.mockReturnValue(0.1); + mockedGetFilledAvailableBalances.mockResolvedValue( + new Map([ + [USDC, FixedMath.toFixed(5000)], + [XLM, FixedMath.toFixed(500)], + ]) + ); + + let fill = await calculateAuctionFill( + poolConfig, + auction, + nextLedger, + mockedSorobanHelper, + db + ); + + let expectedRequests: Request[] = [ + { + request_type: 6, + address: user, + amount: 19n, + }, + // repays any incoming primary liabilities first + { + request_type: 5, + address: USDC, + amount: 19_1934786n, + }, + ]; + expect(fill.block).toEqual(MOCK_LEDGER + 187); + expect(fill.percent).toEqual(19); + expect(fill.requests).toEqual(expectedRequests); + expectRelApproxEqual(fill.lotValue, 1758.8892, 0.005); + expectRelApproxEqual(fill.bidValue, 1591.826316, 0.005); + }); + it('calcs fill for liquidation auction scales fill percent down', async () => { let user = Keypair.random().publicKey(); let nextLedger = MOCK_LEDGER + 188; @@ -622,6 +737,50 @@ describe('auctions', () => { expectRelApproxEqual(fill.bidValue, 4209.893874, 0.005); }); + it('calcs fill for liquidation auction delays fill block if filler not healthy and pool frozen', async () => { + let user = Keypair.random().publicKey(); + let nextLedger = MOCK_LEDGER + 123; + let auction = new Auction(user, AuctionType.Liquidation, { + lot: new Map([[XLM, FixedMath.toFixed(100000)]]), + bid: new Map([[XLM, FixedMath.toFixed(85000)]]), + block: MOCK_LEDGER, + }); + positionEstimate.totalEffectiveLiabilities = 750; + positionEstimate.totalEffectiveCollateral = 1000; + mockPool.metadata.status = 4; // frozen + + mockedSorobanHelper.loadUserPositionEstimate.mockResolvedValue({ + user: {} as PoolUser, + estimate: positionEstimate, + }); + mockedGetFillerProfitPct.mockReturnValue(0.1); + // can't be used as pool is frozen + mockedGetFilledAvailableBalances.mockResolvedValue( + new Map([[USDC, FixedMath.toFixed(5000)]]) + ); + + let fill = await calculateAuctionFill( + poolConfig, + auction, + nextLedger, + mockedSorobanHelper, + db + ); + + let expectedRequests: Request[] = [ + { + request_type: 6, + address: user, + amount: 100n, + }, + ]; + expect(fill.block).toEqual(MOCK_LEDGER + 300); + expect(fill.percent).toEqual(100); + expect(fill.requests).toEqual(expectedRequests); + expectRelApproxEqual(fill.lotValue, 9900.8679, 0.005); + expectRelApproxEqual(fill.bidValue, 4209.893874, 0.005); + }); + it('calcs fill for liquidation auction with repayment, additional collateral, and scaling minor', async () => { let user = Keypair.random().publicKey(); let nextLedger = MOCK_LEDGER + 123; @@ -632,6 +791,7 @@ describe('auctions', () => { }); positionEstimate.totalEffectiveLiabilities = 0; positionEstimate.totalEffectiveCollateral = 1000; + mockPool.metadata.status = 3; // on-ice, can still supply mockedSorobanHelper.loadUserPositionEstimate.mockResolvedValue({ user: {} as PoolUser, @@ -804,5 +964,69 @@ describe('auctions', () => { mockedSorobanHelper ); }); + + it('calcs fill for bad debt auction with no collateral', async () => { + let user = Keypair.random().publicKey(); + let nextLedger = MOCK_LEDGER + 1; + let auction = new Auction(user, AuctionType.BadDebt, { + lot: new Map([[BACKSTOP_TOKEN, FixedMath.toFixed(4200)]]), + bid: new Map([ + [XLM, FixedMath.toFixed(10000)], + [USDC, FixedMath.toFixed(500)], + ]), + block: MOCK_LEDGER, + }); + positionEstimate.totalEffectiveLiabilities = 0; + positionEstimate.totalEffectiveCollateral = 0; + + mockedSorobanHelper.loadUserPositionEstimate.mockResolvedValue({ + user: {} as PoolUser, + estimate: positionEstimate, + }); + mockedGetFillerProfitPct.mockReturnValue(0.1); + mockedGetFilledAvailableBalances.mockResolvedValue( + new Map([ + [USDC, FixedMath.toFixed(4200)], + [XLM, FixedMath.toFixed(20000)], + [EURC, FixedMath.toFixed(10000)], + ]) + ); + + let fill = await calculateAuctionFill( + poolConfig, + auction, + nextLedger, + mockedSorobanHelper, + db + ); + + let expectedRequests: Request[] = [ + { + request_type: 7, + address: user, + amount: 100n, + }, + { + request_type: 5, + address: XLM, + amount: 100056895500n, + }, + { + request_type: 5, + address: USDC, + amount: 5050912865n, + }, + ]; + expect(fill.block).toEqual(MOCK_LEDGER + 157); + expect(fill.percent).toEqual(100); + expect(fill.requests).toEqual(expectedRequests); + expectRelApproxEqual(fill.lotValue, 1648.5, 0.005); + expectRelApproxEqual(fill.bidValue, 1495.503014, 0.005); + + expect(mockedGetFilledAvailableBalances).toHaveBeenCalledWith( + [XLM, USDC], + mockedSorobanHelper + ); + }); }); });