diff --git a/packages/boot/test/bootstrapTests/liquidation.ts b/packages/boot/test/bootstrapTests/liquidation.ts index 40f42d216529..dfe6d4c7dc01 100644 --- a/packages/boot/test/bootstrapTests/liquidation.ts +++ b/packages/boot/test/bootstrapTests/liquidation.ts @@ -85,9 +85,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 +113,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( @@ -217,6 +219,39 @@ export const makeLiquidationTestContext = async t => { }; }; +export const addSTARsCollateral = 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; + /** @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 type LiquidationTestContext = Awaited< ReturnType >; diff --git a/packages/boot/test/bootstrapTests/test-liquidation-1.ts b/packages/boot/test/bootstrapTests/test-liquidation-1.ts index 9af2c9ca477e..9663b6f80c98 100644 --- a/packages/boot/test/bootstrapTests/test-liquidation-1.ts +++ b/packages/boot/test/bootstrapTests/test-liquidation-1.ts @@ -8,6 +8,7 @@ import type { ExecutionContext, TestFn } from 'ava'; import type { ScheduleNotification } from '@agoric/inter-protocol/src/auction/scheduler.js'; import { BridgeHandler } from '@agoric/vats'; import { + addSTARsCollateral, LiquidationTestContext, likePayouts, makeLiquidationTestContext, @@ -152,6 +153,7 @@ const checkFlow1 = async ( await setupStartingState({ collateralBrandKey, managerIndex, + price: setup.price.starting, }); const minter = await walletFactoryDriver.provideSmartWallet('agoric1minter'); @@ -389,35 +391,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 addSTARsCollateral(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 1256b7e8a0f7..87469d772c42 100644 --- a/packages/boot/test/bootstrapTests/test-liquidation-2b.ts +++ b/packages/boot/test/bootstrapTests/test-liquidation-2b.ts @@ -133,7 +133,11 @@ test.serial('scenario: Flow 2b', async t => { walletFactoryDriver, } = t.context; - await setupStartingState({ collateralBrandKey: 'ATOM', managerIndex: 0 }); + await setupStartingState({ + collateralBrandKey: 'ATOM', + managerIndex: 0, + price: setup.price.starting, + }); const minter = await walletFactoryDriver.provideSmartWallet('agoric1minter'); 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 000000000000..bdf2f69ace70 --- /dev/null +++ b/packages/boot/test/bootstrapTests/test-liquidation-concurrent-1.ts @@ -0,0 +1,543 @@ +// @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 { Offers } from '@agoric/inter-protocol/src/clientSupport.js'; +import process from 'process'; +import { + LiquidationTestContext, + addSTARsCollateral, + likePayouts, + makeLiquidationTestContext, + scale6, +} from './liquidation.ts'; +import { TestFn } from 'ava'; + +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: '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 = /** @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: '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); + await addSTARsCollateral(t); +}); +test.after.always(t => { + return t.context.shutdown && t.context.shutdown(); +}); + +// Reference: Flow 1 from https://github.com/Agoric/agoric-sdk/issues/7123 +const checkFlow1 = async (t: ExecutionContext) => { + // fail if there are any unhandled rejections + process.on('unhandledRejection', (error: Error) => { + t.fail(error.message); + }); + + const { + advanceTimeBy, + advanceTimeTo, + agoricNamesRemotes, + check, + setupStartingState, + priceFeedDrivers, + readLatest, + walletFactoryDriver, + } = t.context; + + const cases = [ + { collateralBrandKey: 'ATOM', managerIndex: 0 }, + { collateralBrandKey: 'STARS', managerIndex: 1 } + ] + + const metricsPaths = cases.map( + ({ managerIndex }) => + `published.vaultFactory.managers.manager${managerIndex}.metrics`, + ); + + const buyer = await walletFactoryDriver.provideSmartWallet('agoric1buyer'); + + const setupVault = async (collateralBrandKey: string, managerIndex: number) => { + await setupStartingState({ + collateralBrandKey, + managerIndex, + price: setups[collateralBrandKey].price.starting, + }); + + const minter = await walletFactoryDriver.provideSmartWallet( + 'agoric1minter', + ); + + for (let i = 0; i < setups[collateralBrandKey].vaults.length; i += 1) { + const offerId = `open-${collateralBrandKey}-vault${i}`; + await minter.executeOfferMaker(Offers.vaults.OpenVault, { + offerId, + collateralBrandKey, + wantMinted: setups[collateralBrandKey].vaults[i].ist, + giveCollateral: setups[collateralBrandKey].vaults[i].atom, + }); + t.like(minter.getLatestUpdateRecord(), { + updated: 'offerStatus', + status: { id: offerId, numWantsSatisfied: 1 }, + }); + } + + // Verify starting balances + for (let i = 0; i < setups[collateralBrandKey].vaults.length; i += 1) { + check.vaultNotification(managerIndex, i, { + debtSnapshot: { + debt: { value: scale6(setups[collateralBrandKey].vaults[i].debt) }, + }, + locked: { value: scale6(setups[collateralBrandKey].vaults[i].atom) }, + vaultState: 'active', + }); + } + } + + const placeBid = async (collateralBrandKey: string) => { + 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 < setups[collateralBrandKey].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, + ...setups[collateralBrandKey].bids[i], + maxBuy, + }); + t.like(readLatest('published.wallet.agoric1buyer'), { + status: { + id: offerId, + result: 'Your bid has been accepted', + payouts: undefined, + }, + }); + } + } + + const { collateralBrandKey: collateralBrandKeyA, managerIndex: managerIndexA } = cases[0] + const { collateralBrandKey: collateralBrandKeySt, managerIndex: managerIndexSt } = cases[1] + + await setupVault(collateralBrandKeyA, managerIndexA) + await setupVault(collateralBrandKeySt, managerIndexSt) + + await Promise.all(cases.map(({ collateralBrandKey }) => { + placeBid(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'); + + cases.map(async ({ collateralBrandKey, managerIndex }) => { + const metricsPath = metricsPaths[managerIndex]; + + // check nothing liquidating yet + /** @type {import('@agoric/inter-protocol/src/auction/scheduler.js').ScheduleNotification} */ + t.is(liveSchedule.activeStartTime, null); + t.like(readLatest(metricsPath), { + numActiveVaults: setups[collateralBrandKey].vaults.length, + numLiquidatingVaults: 0, + }); + }) + + // advance time to start an auction + console.log('step 1 of 10'); + await advanceTimeTo(NonNullish(liveSchedule.nextDescendingStepTime)); + + cases.map(async ({ collateralBrandKey, managerIndex }) => { + const metricsPath = metricsPaths[managerIndex]; + + t.like(readLatest(metricsPath), { + 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'); + + cases.map(async ({ collateralBrandKey, managerIndex }) => { + 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'); + + cases.map(async ({ collateralBrandKey, managerIndex }) => { + 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); + } + + cases.map(async ({ collateralBrandKey, managerIndex }) => { + 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 + + // Not part of product spec + 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) }, + }); +}; + +test('concurrent', async t => { + await checkFlow1(t); +}); 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 000000000000..3191ec94ecc7 --- /dev/null +++ b/packages/boot/test/bootstrapTests/test-liquidation-concurrent-2b.ts @@ -0,0 +1,456 @@ +// @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 { Offers } from '@agoric/inter-protocol/src/clientSupport.js'; +import { LiquidationTestContext, addSTARsCollateral, makeLiquidationTestContext, scale6 } from './liquidation.ts'; +import { ExecutionContext, TestFn } from 'ava'; + +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); + await addSTARsCollateral(t); +}); +test.after.always(t => { + return t.context.shutdown && t.context.shutdown(); +}); + +const checkFlow2b = async (t: ExecutionContext) => { + const { + advanceTimeBy, + advanceTimeTo, + agoricNamesRemotes, + check, + setupStartingState, + priceFeedDrivers, + readLatest, + walletFactoryDriver, + } = t.context; + + const cases = [ + { collateralBrandKey: 'ATOM', managerIndex: 0 }, + { collateralBrandKey: 'STARS', managerIndex: 1 } + ] + + const metricsPaths = cases.map( + ({ managerIndex }) => + `published.vaultFactory.managers.manager${managerIndex}.metrics`, + ); + + const buyer = await walletFactoryDriver.provideSmartWallet('agoric1buyer'); + const minter = await walletFactoryDriver.provideSmartWallet('agoric1minter'); + + const setupVault = async (collateralBrandKey: string, managerIndex: number) => { + await setupStartingState({ + collateralBrandKey, + managerIndex, + price: setups[collateralBrandKey].price.starting, + }); + + for (let i = 0; i < setups[collateralBrandKey].vaults.length; i += 1) { + const offerId = `open-${collateralBrandKey}-vault${i}`; + await minter.executeOfferMaker(Offers.vaults.OpenVault, { + offerId, + collateralBrandKey, + wantMinted: setups[collateralBrandKey].vaults[i].ist, + giveCollateral: setups[collateralBrandKey].vaults[i].atom, + }); + t.like(minter.getLatestUpdateRecord(), { + updated: 'offerStatus', + status: { id: offerId, numWantsSatisfied: 1 }, + }); + } + + // Verify starting balances + for (let i = 0; i < setups[collateralBrandKey].vaults.length; i += 1) { + check.vaultNotification(managerIndex, i, { + debtSnapshot: { + debt: { value: scale6(setups[collateralBrandKey].vaults[i].debt) }, + }, + locked: { value: scale6(setups[collateralBrandKey].vaults[i].atom) }, + vaultState: 'active', + }); + } + } + + const placeBid = async (collateralBrandKey: string) => { + 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 < setups[collateralBrandKey].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, + ...setups[collateralBrandKey].bids[i], + maxBuy, + }); + t.like(readLatest('published.wallet.agoric1buyer'), { + status: { + id: offerId, + result: 'Your bid has been accepted', + payouts: undefined, + }, + }); + } + } + + const { collateralBrandKey: collateralBrandKeyA, managerIndex: managerIndexA } = cases[0] + const { collateralBrandKey: collateralBrandKeySt, managerIndex: managerIndexSt } = cases[1] + + await setupVault(collateralBrandKeyA, managerIndexA) + await setupVault(collateralBrandKeySt, managerIndexSt) + + await Promise.all(cases.map(({ collateralBrandKey }) => { + placeBid(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'); + + cases.map(async ({ collateralBrandKey, managerIndex }) => { + const metricsPath = metricsPaths[managerIndex]; + + // check nothing liquidating yet + /** @type {import('@agoric/inter-protocol/src/auction/scheduler.js').ScheduleNotification} */ + t.is(liveSchedule.activeStartTime, null); + t.like(readLatest(metricsPath), { + numActiveVaults: setups[collateralBrandKey].vaults.length, + numLiquidatingVaults: 0, + }); + }) + + console.log('step 0 of 10',); + await advanceTimeTo(NonNullish(liveSchedule.nextDescendingStepTime)); + + cases.map(async ({ collateralBrandKey, managerIndex }) => { + const metricsPath = metricsPaths[managerIndex]; + console.log("ETV", collateralBrandKey, await readLatest(metricsPath), { + numActiveVaults: 0, + numLiquidatingVaults: setups[collateralBrandKey].vaults.length, + liquidatingCollateral: { + value: scale6(setups[collateralBrandKey].auction.start.collateral), + }, + liquidatingDebt: { value: scale6(setups[collateralBrandKey].auction.start.debt) }, + }) + + t.like(readLatest(metricsPath), { + 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 10'); + await advanceTimeBy(3, 'minutes'); + + cases.map(async ({ collateralBrandKey, managerIndex }) => { + 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 10'); + await advanceTimeBy(3, 'minutes'); + + console.log('step 3 of 10'); + await advanceTimeBy(3, 'minutes'); + + console.log('step 4 of 10'); + await advanceTimeBy(3, 'minutes'); + + console.log('step 5 of 10'); + await advanceTimeBy(3, 'minutes'); + + cases.map(async ({ collateralBrandKey, managerIndex }) => { + t.like(readLatest(`published.auction.book${managerIndex}`), { + collateralAvailable: { + value: scale6(setups[collateralBrandKey].auction.start.collateral), + }, + }); + }); + + console.log('step 6 of 10'); + await advanceTimeBy(3, 'minutes'); + + 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'); + await advanceTimeBy(3, 'minutes'); + + console.log('step 11 of 10'); + await advanceTimeBy(3, 'minutes'); + + cases.map(async ({ collateralBrandKey, managerIndex }) => { + 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) }, + }); +} + +// Reference: Flow 2b from https://github.com/Agoric/agoric-sdk/issues/7123 +test.serial('scenario: Flow 2b', async t => { + await checkFlow2b(t); +}); \ No newline at end of file