From c9004173264865dad51a5a3fc0dee394d8c9b24a Mon Sep 17 00:00:00 2001 From: Ikenna Omekam Date: Fri, 6 Oct 2023 17:11:23 -0500 Subject: [PATCH] test: add concurrent liquidation tests --- .../boot/test/bootstrapTests/liquidation.ts | 182 ++++++- .../test/bootstrapTests/test-liquidation-1.ts | 135 +---- .../bootstrapTests/test-liquidation-2b.ts | 93 +--- .../test-liquidation-concurrent-1.ts | 489 ++++++++++++++++++ .../test-liquidation-concurrent-2b.ts | 413 +++++++++++++++ 5 files changed, 1114 insertions(+), 198 deletions(-) create mode 100644 packages/boot/test/bootstrapTests/test-liquidation-concurrent-1.ts create mode 100644 packages/boot/test/bootstrapTests/test-liquidation-concurrent-2b.ts diff --git a/packages/boot/test/bootstrapTests/liquidation.ts b/packages/boot/test/bootstrapTests/liquidation.ts index 40f42d21652..1a5ea74d689 100644 --- a/packages/boot/test/bootstrapTests/liquidation.ts +++ b/packages/boot/test/bootstrapTests/liquidation.ts @@ -7,6 +7,8 @@ import { AgoricNamesRemotes, makeAgoricNamesRemotesFromFakeStorage, } from '@agoric/vats/tools/board-utils.js'; +import { Offers } from '@agoric/inter-protocol/src/clientSupport.js'; +import { ExecutionContext } from 'ava'; import { makeGovernanceDriver, makePriceFeedDriver, @@ -14,6 +16,40 @@ import { } from './drivers.ts'; import { makeSwingsetTestKit } from './supports.ts'; +export type LiquidationSetup = { + vaults: { + atom: number; + ist: number; + debt: number; + }[]; + bids: ( + | { + give: string; + discount: number; + price?: undefined; + } + | { + give: string; + price: number; + discount?: undefined; + } + )[]; + price: { + starting: number; + trigger: number; + }; + auction: { + start: { + collateral: number; + debt: number; + }; + end: { + collateral: number; + debt: number; + }; + }; +}; + export const scale6 = x => BigInt(Math.round(x * 1_000_000)); const DebtLimitValue = scale6(100_000); @@ -85,9 +121,11 @@ export const makeLiquidationTestContext = async t => { const setupStartingState = async ({ collateralBrandKey, managerIndex, + price, }: { collateralBrandKey: string; managerIndex: number; + price: number; }) => { const managerPath = `published.vaultFactory.managers.manager${managerIndex}`; const { advanceTimeBy, readLatest } = swingsetTestKit; @@ -111,7 +149,7 @@ export const makeLiquidationTestContext = async t => { // price feed logic treats zero time as "unset" so advance to nonzero await advanceTimeBy(1, 'seconds'); - await priceFeedDrivers[collateralBrandKey].setPrice(12.34); + await priceFeedDrivers[collateralBrandKey].setPrice(price); // raise the VaultFactory DebtLimit await governanceDriver.changeParams( @@ -205,6 +243,89 @@ export const makeLiquidationTestContext = async t => { }, }; + const setupVaults = async ( + collateralBrandKey: string, + managerIndex: number, + setup: LiquidationSetup, + ) => { + await setupStartingState({ + collateralBrandKey, + managerIndex, + price: setup.price.starting, + }); + + const minter = + await walletFactoryDriver.provideSmartWallet('agoric1minter'); + + for (let i = 0; i < setup.vaults.length; i += 1) { + const offerId = `open-${collateralBrandKey}-vault${i}`; + await minter.executeOfferMaker(Offers.vaults.OpenVault, { + offerId, + collateralBrandKey, + wantMinted: setup.vaults[i].ist, + giveCollateral: setup.vaults[i].atom, + }); + t.like(minter.getLatestUpdateRecord(), { + updated: 'offerStatus', + status: { id: offerId, numWantsSatisfied: 1 }, + }); + } + + // Verify starting balances + for (let i = 0; i < setup.vaults.length; i += 1) { + check.vaultNotification(managerIndex, i, { + debtSnapshot: { + debt: { value: scale6(setup.vaults[i].debt) }, + }, + locked: { value: scale6(setup.vaults[i].atom) }, + vaultState: 'active', + }); + } + }; + + const placeBids = async ( + collateralBrandKey: string, + buyerWalletAddress: string, + setup: LiquidationSetup, + ) => { + const buyer = + await walletFactoryDriver.provideSmartWallet(buyerWalletAddress); + + await buyer.sendOffer( + Offers.psm.swap( + agoricNamesRemotes, + agoricNamesRemotes.instance['psm-IST-USDC_axl'], + { + offerId: `print-${collateralBrandKey}-ist`, + wantMinted: 1_000, + pair: ['IST', 'USDC_axl'], + }, + ), + ); + + const maxBuy = `10000${collateralBrandKey}`; + + for (let i = 0; i < setup.bids.length; i += 1) { + const offerId = `${collateralBrandKey}-bid${i + 1}`; + // bids are long-lasting offers so we can't wait here for completion + await buyer.sendOfferMaker(Offers.auction.Bid, { + offerId, + ...setup.bids[i], + maxBuy, + }); + t.like( + swingsetTestKit.readLatest(`published.wallet.${buyerWalletAddress}`), + { + status: { + id: offerId, + result: 'Your bid has been accepted', + payouts: undefined, + }, + }, + ); + } + }; + return { ...swingsetTestKit, agoricNamesRemotes, @@ -212,11 +333,68 @@ export const makeLiquidationTestContext = async t => { governanceDriver, priceFeedDrivers, refreshAgoricNamesRemotes, - setupStartingState, walletFactoryDriver, + setupVaults, + placeBids, }; }; export type LiquidationTestContext = Awaited< ReturnType >; + +const addSTARsCollateral = async ( + t: ExecutionContext, +) => { + const { controller, buildProposal } = t.context; + + t.log('building proposal'); + const proposal = await buildProposal({ + package: 'builders', + packageScriptName: 'build:add-STARS-proposal', + }); + + for await (const bundle of proposal.bundles) { + await controller.validateAndInstallBundle(bundle); + } + t.log('installed', proposal.bundles.length, 'bundles'); + + t.log('launching proposal'); + const bridgeMessage = { + type: 'CORE_EVAL', + evals: proposal.evals, + }; + t.log({ bridgeMessage }); + + const { EV } = t.context.runUtils; + /** @type {ERef} */ + const coreEvalBridgeHandler = await EV.vat('bootstrap').consumeItem( + 'coreEvalBridgeHandler', + ); + await EV(coreEvalBridgeHandler).fromBridge(bridgeMessage); + + t.context.refreshAgoricNamesRemotes(); + + t.log('add-STARS proposal executed'); +}; + +export const ensureVaultCollateral = async ( + collateralBrandKey: string, + t: ExecutionContext, +) => { + // TODO: we'd like to have this work on any brand + const SUPPORTED_BRANDS = ['ATOM', 'STARS']; + + if (!SUPPORTED_BRANDS.includes(collateralBrandKey)) { + throw Error('Unsupported brand type'); + } + + if (collateralBrandKey === 'ATOM') { + return; + } + + if (collateralBrandKey === 'STARS') { + // eslint-disable-next-line @jessie.js/safe-await-separator + await addSTARsCollateral(t); + } +}; diff --git a/packages/boot/test/bootstrapTests/test-liquidation-1.ts b/packages/boot/test/bootstrapTests/test-liquidation-1.ts index 9af2c9ca477..0c88382cf70 100644 --- a/packages/boot/test/bootstrapTests/test-liquidation-1.ts +++ b/packages/boot/test/bootstrapTests/test-liquidation-1.ts @@ -2,22 +2,22 @@ import { test as anyTest } from '@agoric/zoe/tools/prepare-test-env-ava.js'; import { NonNullish } from '@agoric/assert'; -import { Offers } from '@agoric/inter-protocol/src/clientSupport.js'; import process from 'process'; import type { ExecutionContext, TestFn } from 'ava'; import type { ScheduleNotification } from '@agoric/inter-protocol/src/auction/scheduler.js'; -import { BridgeHandler } from '@agoric/vats'; import { + ensureVaultCollateral, LiquidationTestContext, likePayouts, makeLiquidationTestContext, scale6, + LiquidationSetup, } from './liquidation.ts'; const test = anyTest as TestFn; //#region Product spec -const setup = { +const setup: LiquidationSetup = { vaults: [ { atom: 15, @@ -58,8 +58,12 @@ const setup = { collateral: 45, debt: 309.54, }, + end: { + collateral: 9.659301, + debt: 0, + }, }, -} as const; +}; const outcome = { bids: [ @@ -139,105 +143,20 @@ const checkFlow1 = async ( const { advanceTimeBy, advanceTimeTo, - agoricNamesRemotes, check, - setupStartingState, priceFeedDrivers, readLatest, walletFactoryDriver, + setupVaults, + placeBids, } = t.context; const metricsPath = `published.vaultFactory.managers.manager${managerIndex}.metrics`; - await setupStartingState({ - collateralBrandKey, - managerIndex, - }); - - const minter = await walletFactoryDriver.provideSmartWallet('agoric1minter'); - - for (let i = 0; i < setup.vaults.length; i += 1) { - const offerId = `open-${collateralBrandKey}-vault${i}`; - await minter.executeOfferMaker(Offers.vaults.OpenVault, { - offerId, - collateralBrandKey, - wantMinted: setup.vaults[i].ist, - giveCollateral: setup.vaults[i].atom, - }); - t.like(minter.getLatestUpdateRecord(), { - updated: 'offerStatus', - status: { id: offerId, numWantsSatisfied: 1 }, - }); - } - - // Verify starting balances - for (let i = 0; i < setup.vaults.length; i += 1) { - check.vaultNotification(managerIndex, i, { - debtSnapshot: { debt: { value: scale6(setup.vaults[i].debt) } }, - locked: { value: scale6(setup.vaults[i].atom) }, - vaultState: 'active', - }); - } + await setupVaults(collateralBrandKey, managerIndex, setup); const buyer = await walletFactoryDriver.provideSmartWallet('agoric1buyer'); - { - // --------------- - // Place bids - // --------------- - - await buyer.sendOffer( - Offers.psm.swap( - agoricNamesRemotes, - agoricNamesRemotes.instance['psm-IST-USDC_axl'], - { - offerId: `print-${collateralBrandKey}-ist`, - wantMinted: 1_000, - pair: ['IST', 'USDC_axl'], - }, - ), - ); - - const maxBuy = `10000${collateralBrandKey}`; - - // bids are long-lasting offers so we can't wait here for completion - await buyer.sendOfferMaker(Offers.auction.Bid, { - offerId: `${collateralBrandKey}-bid1`, - ...setup.bids[0], - maxBuy, - }); - - t.like(readLatest('published.wallet.agoric1buyer'), { - status: { - id: `${collateralBrandKey}-bid1`, - result: 'Your bid has been accepted', - payouts: undefined, - }, - }); - await buyer.sendOfferMaker(Offers.auction.Bid, { - offerId: `${collateralBrandKey}-bid2`, - ...setup.bids[1], - maxBuy, - }); - t.like(readLatest('published.wallet.agoric1buyer'), { - status: { - id: `${collateralBrandKey}-bid2`, - result: 'Your bid has been accepted', - payouts: undefined, - }, - }); - await buyer.sendOfferMaker(Offers.auction.Bid, { - offerId: `${collateralBrandKey}-bid3`, - ...setup.bids[2], - maxBuy, - }); - t.like(readLatest('published.wallet.agoric1buyer'), { - status: { - id: `${collateralBrandKey}-bid3`, - result: 'Your bid has been accepted', - payouts: undefined, - }, - }); - } + await placeBids(collateralBrandKey, 'agoric1buyer', setup); { // --------------- @@ -389,35 +308,7 @@ test.serial( ); test.serial('add STARS collateral', async t => { - const { controller, buildProposal } = t.context; - - t.log('building proposal'); - const proposal = await buildProposal({ - package: 'builders', - packageScriptName: 'build:add-STARS-proposal', - }); - - for await (const bundle of proposal.bundles) { - await controller.validateAndInstallBundle(bundle); - } - t.log('installed', proposal.bundles.length, 'bundles'); - - t.log('launching proposal'); - const bridgeMessage = { - type: 'CORE_EVAL', - evals: proposal.evals, - }; - t.log({ bridgeMessage }); - - const { EV } = t.context.runUtils; - const coreEvalBridgeHandler: ERef = await EV.vat( - 'bootstrap', - ).consumeItem('coreEvalBridgeHandler'); - await EV(coreEvalBridgeHandler).fromBridge(bridgeMessage); - - t.context.refreshAgoricNamesRemotes(); - - t.log('add-STARS proposal executed'); + await ensureVaultCollateral('STARS', t); t.pass(); // reached here without throws }); diff --git a/packages/boot/test/bootstrapTests/test-liquidation-2b.ts b/packages/boot/test/bootstrapTests/test-liquidation-2b.ts index 1256b7e8a0f..2225e165e1a 100644 --- a/packages/boot/test/bootstrapTests/test-liquidation-2b.ts +++ b/packages/boot/test/bootstrapTests/test-liquidation-2b.ts @@ -13,6 +13,7 @@ import { Offers } from '@agoric/inter-protocol/src/clientSupport.js'; import type { TestFn } from 'ava'; import { ScheduleNotification } from '@agoric/inter-protocol/src/auction/scheduler.js'; import { + LiquidationSetup, LiquidationTestContext, makeLiquidationTestContext, scale6, @@ -24,7 +25,7 @@ const test = anyTest as TestFn; const collateralBrandKey = 'ATOM'; //#region Product spec -const setup = { +const setup: LiquidationSetup = { vaults: [ { atom: 15, @@ -61,8 +62,12 @@ const setup = { collateral: 45, debt: 309.54, }, + end: { + collateral: 31.414987, + debt: 209.54, + }, }, -} as const; +}; const outcome = { bids: [ @@ -125,92 +130,32 @@ test.serial('scenario: Flow 2b', async t => { const { advanceTimeBy, advanceTimeTo, - agoricNamesRemotes, check, - setupStartingState, priceFeedDrivers, readLatest, - walletFactoryDriver, + setupVaults, + placeBids, } = t.context; - await setupStartingState({ collateralBrandKey: 'ATOM', managerIndex: 0 }); - - const minter = await walletFactoryDriver.provideSmartWallet('agoric1minter'); - - for (let i = 0; i < setup.vaults.length; i += 1) { - const offerId = `open-vault${i}`; - await minter.executeOfferMaker(Offers.vaults.OpenVault, { - offerId, - collateralBrandKey, - wantMinted: setup.vaults[i].ist, - giveCollateral: setup.vaults[i].atom, - }); - t.like(minter.getLatestUpdateRecord(), { - updated: 'offerStatus', - status: { id: offerId, numWantsSatisfied: 1 }, - }); - } + const managerIndex = 0; + const metricPath = `published.vaultFactory.managers.manager${managerIndex}.metrics`; - // Verify starting balances - for (let i = 0; i < setup.vaults.length; i += 1) { - check.vaultNotification(0, i, { - debtSnapshot: { debt: { value: scale6(setup.vaults[i].debt) } }, - locked: { value: scale6(setup.vaults[i].atom) }, - vaultState: 'active', - }); - } - - const buyer = await walletFactoryDriver.provideSmartWallet('agoric1buyer'); - { - // --------------- - // Place bids - // --------------- - - await buyer.sendOffer( - Offers.psm.swap( - agoricNamesRemotes, - agoricNamesRemotes.instance['psm-IST-USDC_axl'], - { - offerId: 'print-ist', - wantMinted: 1_000, - pair: ['IST', 'USDC_axl'], - }, - ), - ); - - const maxBuy = '10000ATOM'; - - for (let i = 0; i < setup.bids.length; i += 1) { - const offerId = `bid${i}`; - // bids are long-lasting offers so we can't wait here for completion - await buyer.sendOfferMaker(Offers.auction.Bid, { - offerId, - ...setup.bids[i], - maxBuy, - }); - t.like(readLatest('published.wallet.agoric1buyer'), { - status: { - id: offerId, - result: 'Your bid has been accepted', - payouts: undefined, - }, - }); - } - } + await setupVaults(collateralBrandKey, managerIndex, setup); + await placeBids(collateralBrandKey, 'agoric1buyer', setup); { // --------------- // Change price to trigger liquidation // --------------- - await priceFeedDrivers.ATOM.setPrice(9.99); + await priceFeedDrivers.ATOM.setPrice(setup.price.trigger); // check nothing liquidating yet const liveSchedule: ScheduleNotification = readLatest( 'published.auction.schedule', ); t.is(liveSchedule.activeStartTime, null); - t.like(readLatest('published.vaultFactory.managers.manager0.metrics'), { + t.like(readLatest(metricPath), { numActiveVaults: setup.vaults.length, numLiquidatingVaults: 0, }); @@ -218,7 +163,7 @@ test.serial('scenario: Flow 2b', async t => { // advance time to start an auction console.log('step 0 of 10'); await advanceTimeTo(NonNullish(liveSchedule.nextDescendingStepTime)); - t.like(readLatest('published.vaultFactory.managers.manager0.metrics'), { + t.like(readLatest(metricPath), { numActiveVaults: 0, numLiquidatingVaults: setup.vaults.length, liquidatingCollateral: { @@ -229,7 +174,7 @@ test.serial('scenario: Flow 2b', async t => { console.log('step 1 of 10'); await advanceTimeBy(3, 'minutes'); - t.like(readLatest('published.auction.book0'), { + t.like(readLatest(`published.auction.book${managerIndex}`), { collateralAvailable: { value: scale6(setup.auction.start.collateral) }, startCollateral: { value: scale6(setup.auction.start.collateral) }, startProceedsGoal: { value: scale6(setup.auction.start.debt) }, @@ -246,7 +191,7 @@ test.serial('scenario: Flow 2b', async t => { console.log('step 5 of 10'); await advanceTimeBy(3, 'minutes'); - t.like(readLatest('published.auction.book0'), { + t.like(readLatest(`published.auction.book${managerIndex}`), { collateralAvailable: { value: scale6(45) }, }); @@ -300,7 +245,7 @@ test.serial('scenario: Flow 2b', async t => { shortfallBalance: { value: scale6(outcome.reserve.shortfall) }, }); - t.like(readLatest('published.vaultFactory.managers.manager0.metrics'), { + t.like(readLatest(metricPath), { // reconstituted numActiveVaults: 2, numLiquidationsCompleted: 1, diff --git a/packages/boot/test/bootstrapTests/test-liquidation-concurrent-1.ts b/packages/boot/test/bootstrapTests/test-liquidation-concurrent-1.ts new file mode 100644 index 00000000000..c9aee5d2268 --- /dev/null +++ b/packages/boot/test/bootstrapTests/test-liquidation-concurrent-1.ts @@ -0,0 +1,489 @@ +// @ts-check +/** @file Bootstrap test of liquidation across multiple collaterals */ +import { test as anyTest } from '@agoric/zoe/tools/prepare-test-env-ava.js'; + +import { NonNullish } from '@agoric/assert'; +import process from 'process'; +import { TestFn } from 'ava'; +import { + LiquidationSetup, + LiquidationTestContext, + ensureVaultCollateral, + likePayouts, + makeLiquidationTestContext, + scale6, +} from './liquidation.ts'; + +const test = anyTest as TestFn; + +const atomSetup: LiquidationSetup = { + vaults: [ + { + atom: 15, + ist: 100, + debt: 100.5, + }, + { + atom: 15, + ist: 103, + debt: 103.515, + }, + { + atom: 15, + ist: 105, + debt: 105.525, + }, + ], + bids: [ + { + give: '80IST', + discount: 0.1, + }, + { + give: '90IST', + price: 9.0, + }, + { + give: '150IST', + discount: 0.15, + }, + ], + price: { + starting: 12.34, + trigger: 9.99, + }, + auction: { + start: { + collateral: 45, + debt: 309.54, + }, + end: { + collateral: 9.659301, + debt: 0, + }, + }, +}; + +const starsSetup: LiquidationSetup = { + vaults: [ + { + atom: 15, + ist: 110, + debt: 110.55, + }, + { + atom: 15, + ist: 112, + debt: 112.56, + }, + { + atom: 15, + ist: 113, + debt: 113.565, + }, + ], + bids: [ + { + give: '80IST', + discount: 0.1, + }, + { + give: '90IST', + price: 10.0, + }, + { + give: '166.675IST', + discount: 0.15, + }, + ], + price: { + starting: 13.34, + trigger: 10.99, + }, + auction: { + start: { + collateral: 45, + debt: 336.675, + }, + end: { + collateral: 9.970236, + debt: 0, + }, + }, +}; + +const setups = { + ATOM: atomSetup, + STARS: starsSetup, +}; + +const atomOutcome = /** @type {const} */ { + bids: [ + { + payouts: { + Bid: 0, + Collateral: 8.897786, + }, + }, + { + payouts: { + Bid: 0, + Collateral: 10.01001, + }, + }, + { + payouts: { + Bid: 10.46, + Collateral: 16.432903, + }, + }, + ], + reserve: { + allocations: { + ATOM: 0.309852, + }, + shortfall: 0, + }, + vaultsSpec: [ + { + locked: 3.373, + }, + { + locked: 3.024, + }, + { + locked: 2.792, + }, + ], + vaultsActual: [ + { + locked: 3.525747, + }, + { + locked: 3.181519, + }, + { + locked: 2.642185, + }, + ], +}; + +const starsOutcome = /** @type {const} */ { + bids: [ + { + payouts: { + Bid: 0, + Collateral: 8.08, + }, + }, + { + payouts: { + Bid: 0, + Collateral: 9.099181, + }, + }, + { + payouts: { + Bid: 0, + Collateral: 17.48, + }, + }, + ], + reserve: { + allocations: { + STARS: 0.306349, + }, + shortfall: 0, + }, + vaultsSpec: [ + { + locked: 3.373, + }, + { + locked: 3.024, + }, + { + locked: 2.792, + }, + ], + vaultsActual: [ + { + locked: 3.525747, + }, + { + locked: 3.181519, + }, + { + locked: 2.642185, + }, + ], +}; + +const outcomes = { + ATOM: atomOutcome, + STARS: starsOutcome, +}; + +test.before(async t => { + t.context = await makeLiquidationTestContext(t); +}); + +test.after.always(t => { + return t.context.shutdown && t.context.shutdown(); +}); + +// Reference: Flow 1 from https://github.com/Agoric/agoric-sdk/issues/7123 +test('concurrent flow 1', async t => { + // fail if there are any unhandled rejections + process.on('unhandledRejection', (error: Error) => { + t.fail(error.message); + }); + + const { + advanceTimeBy, + advanceTimeTo, + check, + priceFeedDrivers, + readLatest, + walletFactoryDriver, + setupVaults, + placeBids, + } = t.context; + + const cases = [ + { collateralBrandKey: 'ATOM', managerIndex: 0 }, + { collateralBrandKey: 'STARS', managerIndex: 1 }, + ]; + + await Promise.all( + cases.map(({ collateralBrandKey }) => + ensureVaultCollateral(collateralBrandKey, t), + ), + ); + + const metricsPaths = cases.map( + ({ managerIndex }) => + `published.vaultFactory.managers.manager${managerIndex}.metrics`, + ); + + const buyer = await walletFactoryDriver.provideSmartWallet('agoric1buyer'); + + const { + collateralBrandKey: collateralBrandKeyA, + managerIndex: managerIndexA, + } = cases[0]; + const { + collateralBrandKey: collateralBrandKeySt, + managerIndex: managerIndexSt, + } = cases[1]; + + await setupVaults( + collateralBrandKeyA, + managerIndexA, + setups[collateralBrandKeyA], + ); + await setupVaults( + collateralBrandKeySt, + managerIndexSt, + setups[collateralBrandKeySt], + ); + + await Promise.all( + cases.map(({ collateralBrandKey }) => + placeBids(collateralBrandKey, 'agoric1buyer', setups[collateralBrandKey]), + ), + ); + + // --------------- + // Change price to trigger liquidation + // --------------- + console.log('Change prices'); + await priceFeedDrivers[collateralBrandKeyA].setPrice( + setups[collateralBrandKeyA].price.trigger, + ); + await priceFeedDrivers[collateralBrandKeySt].setPrice( + setups[collateralBrandKeySt].price.trigger, + ); + + const liveSchedule = readLatest('published.auction.schedule'); + + for (const { collateralBrandKey, managerIndex } of cases) { + // check nothing liquidating yet + /** @type {import('@agoric/inter-protocol/src/auction/scheduler.js').ScheduleNotification} */ + t.is(liveSchedule.activeStartTime, null); + t.like(readLatest(metricsPaths[managerIndex]), { + numActiveVaults: setups[collateralBrandKey].vaults.length, + numLiquidatingVaults: 0, + }); + } + + // advance time to start an auction + console.log('step 1 of 10'); + await advanceTimeTo(NonNullish(liveSchedule.nextDescendingStepTime)); + + for (const { collateralBrandKey, managerIndex } of cases) { + t.like(readLatest(metricsPaths[managerIndex]), { + numActiveVaults: 0, + numLiquidatingVaults: setups[collateralBrandKey].vaults.length, + liquidatingCollateral: { + value: scale6(setups[collateralBrandKey].auction.start.collateral), + }, + liquidatingDebt: { + value: scale6(setups[collateralBrandKey].auction.start.debt), + }, + lockedQuote: null, + }); + } + + console.log('step 2 of 10'); + await advanceTimeBy(3, 'minutes'); + + for (const { collateralBrandKey, managerIndex } of cases) { + t.like(readLatest(`published.auction.book${managerIndex}`), { + collateralAvailable: { + value: scale6(setups[collateralBrandKey].auction.start.collateral), + }, + startCollateral: { + value: scale6(setups[collateralBrandKey].auction.start.collateral), + }, + startProceedsGoal: { + value: scale6(setups[collateralBrandKey].auction.start.debt), + }, + }); + } + + console.log('step 3 of 10'); + await advanceTimeBy(3, 'minutes'); + + console.log('step 4 of 10'); + await advanceTimeBy(3, 'minutes'); + + // updates for bid1 and bid2 are appended in the same turn so readLatest gives bid2 + // updates for ATOM and STARS are appended in the same turn so readLatest gives STARS + t.like(readLatest('published.wallet.agoric1buyer'), { + status: { + id: `${collateralBrandKeySt}-bid2`, + payouts: likePayouts(outcomes[collateralBrandKeySt].bids[1].payouts), + }, + }); + + console.log('step 5 of 10'); + await advanceTimeBy(3, 'minutes'); + + console.log('step 6 of 10'); + await advanceTimeBy(3, 'minutes'); + + for (const { collateralBrandKey, managerIndex } of cases) { + t.like(readLatest(`published.auction.book${managerIndex}`), { + collateralAvailable: { + value: scale6(setups[collateralBrandKey].auction.end.collateral), + }, + }); + } + + console.log('step 7 of 10'); + await advanceTimeBy(3, 'minutes'); + + console.log('step 8 of 10'); + await advanceTimeBy(3, 'minutes'); + + console.log('step 9 of 10'); + await advanceTimeBy(3, 'minutes'); + + console.log('step 10 of 10'); + // continuing after now would start a new auction + { + /** + * @type {Record< + * string, + * import('@agoric/time/src/types.js').TimestampRecord + * >} + */ + const { nextDescendingStepTime, nextStartTime } = readLatest( + 'published.auction.schedule', + ); + t.is(nextDescendingStepTime.absValue, nextStartTime.absValue); + } + + for (const { collateralBrandKey, managerIndex } of cases) { + check.vaultNotification(managerIndex, 0, { + debt: undefined, + vaultState: 'liquidated', + locked: { + value: scale6(outcomes[collateralBrandKey].vaultsActual[0].locked), + }, + }); + check.vaultNotification(managerIndex, 1, { + debt: undefined, + vaultState: 'liquidated', + locked: { + value: scale6(outcomes[collateralBrandKey].vaultsActual[1].locked), + }, + }); + + // check reserve balances + t.like(readLatest('published.reserve.metrics'), { + allocations: { + [collateralBrandKey]: { + value: scale6( + outcomes[collateralBrandKey].reserve.allocations[ + collateralBrandKey + ], + ), + }, + }, + shortfallBalance: { + value: scale6(outcomes[collateralBrandKey].reserve.shortfall), + }, + }); + } + + const metricsPathA = metricsPaths[managerIndexA]; + const metricsPathSt = metricsPaths[managerIndexSt]; + + // ATOM + t.like(readLatest(metricsPathA), { + numActiveVaults: 0, + numLiquidationsCompleted: setups[collateralBrandKeyA].vaults.length, + numLiquidatingVaults: 0, + retainedCollateral: { value: 0n }, + totalCollateral: { value: 0n }, + totalDebt: { value: 0n }, + totalOverageReceived: { value: 0n }, + totalProceedsReceived: { + value: scale6(setups[collateralBrandKeyA].auction.start.debt), + }, + totalShortfallReceived: { + value: scale6(outcomes[collateralBrandKeyA].reserve.shortfall), + }, + }); + + // bid3 still live because it's not fully satisfied + const { liveOffers } = readLatest('published.wallet.agoric1buyer.current'); + t.is(liveOffers[0][1].id, `${collateralBrandKeyA}-bid3`); + // exit to get payouts + await buyer.tryExitOffer(`${collateralBrandKeyA}-bid3`); + t.like(readLatest('published.wallet.agoric1buyer'), { + status: { + id: `${collateralBrandKeyA}-bid3`, + payouts: likePayouts(outcomes[collateralBrandKeyA].bids[2].payouts), + }, + }); + + // STARS + t.like(readLatest(metricsPathSt), { + numActiveVaults: 0, + numLiquidationsCompleted: setups[collateralBrandKeySt].vaults.length, + numLiquidatingVaults: 0, + retainedCollateral: { value: 0n }, + totalCollateral: { value: 0n }, + totalDebt: { value: 0n }, + totalOverageReceived: { value: 0n }, + totalProceedsReceived: { + value: scale6(setups[collateralBrandKeySt].auction.start.debt), + }, + totalShortfallReceived: { + value: scale6(outcomes[collateralBrandKeySt].reserve.shortfall), + }, + }); +}); diff --git a/packages/boot/test/bootstrapTests/test-liquidation-concurrent-2b.ts b/packages/boot/test/bootstrapTests/test-liquidation-concurrent-2b.ts new file mode 100644 index 00000000000..62f7c597644 --- /dev/null +++ b/packages/boot/test/bootstrapTests/test-liquidation-concurrent-2b.ts @@ -0,0 +1,413 @@ +// @ts-check +/** + * @file Bootstrap test integration vaults with smart-wallet + * + * Forks test-liquidation to test another scenario, but with a clean vault + * manager state. TODO is there a way to _reset_ the vaultmanager to make the + * two tests run faster? + */ +import { test as anyTest } from '@agoric/zoe/tools/prepare-test-env-ava.js'; + +import { NonNullish } from '@agoric/assert'; +import { ExecutionContext, TestFn } from 'ava'; +import { + LiquidationTestContext, + ensureVaultCollateral, + makeLiquidationTestContext, + scale6, +} from './liquidation.ts'; + +const test = anyTest as TestFn; + +const atomSetup = { + vaults: [ + { + atom: 15, + ist: 100, + debt: 100.5, + }, + { + atom: 15, + ist: 103, + debt: 103.515, + }, + { + atom: 15, + ist: 105, + debt: 105.525, + }, + ], + bids: [ + { + give: '25IST', + discount: 0.3, + }, + { + give: '75IST', + discount: 0.22, + }, + ], + price: { + starting: 12.34, + trigger: 9.99, + }, + auction: { + start: { + collateral: 45, + debt: 309.54, + }, + end: { + collateral: 31.414987, + debt: 209.54, + }, + }, +}; + +const starsSetup = /** @type {const} */ { + vaults: [ + { + atom: 15, + ist: 110, + debt: 110.55, + }, + { + atom: 15, + ist: 112, + debt: 112.56, + }, + { + atom: 15, + ist: 113, + debt: 113.565, + }, + ], + bids: [ + { + give: '25IST', + discount: 0.3, + }, + { + give: '75IST', + discount: 0.22, + }, + ], + price: { + starting: 13.34, + trigger: 10.99, + }, + auction: { + start: { + collateral: 45, + debt: 336.675, + }, + end: { + collateral: 32.65, + debt: 236.675, + }, + }, +}; + +const setups = { + ATOM: atomSetup, + STARS: starsSetup, +}; + +const atomOutcome = /** @type {const} */ { + bids: [ + { + payouts: { + Bid: 0, + Collateral: 10.01, + }, + }, + { + payouts: { + Bid: 0, + Collateral: 3.575, + }, + }, + ], + reserve: { + allocations: { + ATOM: 1.619207, + }, + shortfall: 5.525, + }, + vaultsActual: [ + { + debt: 100.5, + locked: 14.998993, + }, + { + debt: 103.515, + locked: 14.998963, + }, + { + locked: 0, + }, + ], +}; + +const starsOutcome = /** @type {const} */ { + bids: [ + { + payouts: { + Bid: 0, + Collateral: 9.099, + }, + }, + { + payouts: { + Bid: 0, + Collateral: 3.2497, + }, + }, + ], + reserve: { + allocations: { + ATOM: 2.87192, + }, + shortfall: 13.565, + }, + vaultsActual: [ + { + debt: 100.5, + locked: 14.998993, + }, + { + debt: 103.515, + locked: 14.998963, + }, + { + locked: 0, + }, + ], +}; + +const outcomes = { + ATOM: atomOutcome, + STARS: starsOutcome, +}; + +test.before(async t => { + t.context = await makeLiquidationTestContext(t); +}); +test.after.always(t => { + return t.context.shutdown && t.context.shutdown(); +}); + +test.serial( + 'concurrent flow 2', + async (t: ExecutionContext) => { + const { + advanceTimeBy, + advanceTimeTo, + check, + priceFeedDrivers, + readLatest, + setupVaults, + placeBids, + } = t.context; + + const cases = [ + { collateralBrandKey: 'ATOM', managerIndex: 0 }, + { collateralBrandKey: 'STARS', managerIndex: 1 }, + ]; + + await Promise.all( + cases.map(({ collateralBrandKey }) => + ensureVaultCollateral(collateralBrandKey, t), + ), + ); + + const metricsPaths = cases.map( + ({ managerIndex }) => + `published.vaultFactory.managers.manager${managerIndex}.metrics`, + ); + + const { + collateralBrandKey: collateralBrandKeyA, + managerIndex: managerIndexA, + } = cases[0]; + const { + collateralBrandKey: collateralBrandKeySt, + managerIndex: managerIndexSt, + } = cases[1]; + + await setupVaults( + collateralBrandKeyA, + managerIndexA, + setups[collateralBrandKeyA], + ); + await setupVaults( + collateralBrandKeySt, + managerIndexSt, + setups[collateralBrandKeySt], + ); + + await Promise.all( + cases.map(({ collateralBrandKey }) => + placeBids( + collateralBrandKey, + 'agoric1buyer', + setups[collateralBrandKey], + ), + ), + ); + + // --------------- + // Change price to trigger liquidation + // --------------- + console.log('Change prices'); + await priceFeedDrivers[collateralBrandKeyA].setPrice( + setups[collateralBrandKeyA].price.trigger, + ); + await priceFeedDrivers[collateralBrandKeySt].setPrice( + setups[collateralBrandKeySt].price.trigger, + ); + + const liveSchedule = readLatest('published.auction.schedule'); + + for (const { collateralBrandKey, managerIndex } of cases) { + // check nothing liquidating yet + /** @type {import('@agoric/inter-protocol/src/auction/scheduler.js').ScheduleNotification} */ + t.is(liveSchedule.activeStartTime, null); + t.like(readLatest(metricsPaths[managerIndex]), { + numActiveVaults: setups[collateralBrandKey].vaults.length, + numLiquidatingVaults: 0, + }); + } + + console.log('step 0 of 11'); + await advanceTimeTo(NonNullish(liveSchedule.nextDescendingStepTime)); + + for (const { collateralBrandKey, managerIndex } of cases) { + t.like(readLatest(metricsPaths[managerIndex]), { + numActiveVaults: 0, + numLiquidatingVaults: setups[collateralBrandKey].vaults.length, + liquidatingCollateral: { + value: scale6(setups[collateralBrandKey].auction.start.collateral), + }, + liquidatingDebt: { + value: scale6(setups[collateralBrandKey].auction.start.debt), + }, + }); + } + + console.log('step 1 of 11'); + await advanceTimeBy(3, 'minutes'); + + for (const { collateralBrandKey, managerIndex } of cases) { + t.like(readLatest(`published.auction.book${managerIndex}`), { + collateralAvailable: { + value: scale6(setups[collateralBrandKey].auction.start.collateral), + }, + startCollateral: { + value: scale6(setups[collateralBrandKey].auction.start.collateral), + }, + startProceedsGoal: { + value: scale6(setups[collateralBrandKey].auction.start.debt), + }, + }); + } + + console.log('step 2 of 11'); + await advanceTimeBy(3, 'minutes'); + + console.log('step 3 of 11'); + await advanceTimeBy(3, 'minutes'); + + console.log('step 4 of 11'); + await advanceTimeBy(3, 'minutes'); + + console.log('step 5 of 11'); + await advanceTimeBy(3, 'minutes'); + + for (const { collateralBrandKey, managerIndex } of cases) { + t.like(readLatest(`published.auction.book${managerIndex}`), { + collateralAvailable: { + value: scale6(setups[collateralBrandKey].auction.start.collateral), + }, + }); + } + + console.log('step 6 of 11'); + await advanceTimeBy(3, 'minutes'); + + console.log('step 7 of 11'); + await advanceTimeBy(3, 'minutes'); + + console.log('step 8 of 11'); + await advanceTimeBy(3, 'minutes'); + + console.log('step 9 of 11'); + await advanceTimeBy(3, 'minutes'); + + console.log('step 10 of 11'); + await advanceTimeBy(3, 'minutes'); + + console.log('step 11 of 11'); + await advanceTimeBy(3, 'minutes'); + + for (const { collateralBrandKey, managerIndex } of cases) { + check.vaultNotification(managerIndex, 0, { + debt: undefined, + vaultState: 'active', + locked: { + value: scale6(outcomes[collateralBrandKey].vaultsActual[0].locked), + }, + }); + check.vaultNotification(managerIndex, 1, { + debt: undefined, + vaultState: 'active', + locked: { + value: scale6(outcomes[collateralBrandKey].vaultsActual[1].locked), + }, + }); + check.vaultNotification(managerIndex, 2, { + debt: undefined, + vaultState: 'liquidated', + locked: { + value: scale6(outcomes[collateralBrandKey].vaultsActual[2].locked), + }, + }); + } + + const metricsPathA = metricsPaths[managerIndexA]; + const metricsPathSt = metricsPaths[managerIndexSt]; + + // ATOM + t.like(readLatest(metricsPathA), { + // reconstituted + numActiveVaults: 2, + numLiquidationsCompleted: 1, + numLiquidatingVaults: 0, + retainedCollateral: { value: 0n }, + totalCollateral: { value: 29795782n }, + totalCollateralSold: { value: 13585013n }, + totalDebt: { value: 204015000n }, + totalOverageReceived: { value: 0n }, + totalProceedsReceived: { value: 100000000n }, + totalShortfallReceived: { + value: scale6(outcomes[collateralBrandKeyA].reserve.shortfall), + }, + }); + + // STARS + t.like(readLatest(metricsPathSt), { + // reconstituted + numActiveVaults: 2, + numLiquidationsCompleted: 1, + numLiquidatingVaults: 0, + retainedCollateral: { value: 0n }, + totalCollateral: { value: 29796989n }, + totalCollateralSold: { value: 12348888n }, + totalDebt: { value: 223110000n }, + totalOverageReceived: { value: 0n }, + totalProceedsReceived: { value: 100000000n }, + totalShortfallReceived: { + value: scale6(outcomes[collateralBrandKeySt].reserve.shortfall), + }, + }); + }, +);