From 412cd9eb51fee004ae0fdc016f6fd93df9a20c01 Mon Sep 17 00:00:00 2001 From: Chris Hibbert Date: Wed, 4 Oct 2023 15:17:42 -0700 Subject: [PATCH 1/5] refactor: delay addition of issuer to auctioneer until a good time --- packages/inter-protocol/package.json | 1 + .../src/proposals/addAssetToVault.js | 97 ++++++++++++++++++- 2 files changed, 93 insertions(+), 5 deletions(-) diff --git a/packages/inter-protocol/package.json b/packages/inter-protocol/package.json index 36c428ed53e..5d88e536104 100644 --- a/packages/inter-protocol/package.json +++ b/packages/inter-protocol/package.json @@ -46,6 +46,7 @@ "@endo/far": "^0.2.21", "@endo/marshal": "^0.8.8", "@endo/nat": "^4.1.30", + "@endo/promise-kit": "^0.2.59", "jessie.js": "^0.3.2" }, "devDependencies": { diff --git a/packages/inter-protocol/src/proposals/addAssetToVault.js b/packages/inter-protocol/src/proposals/addAssetToVault.js index 3f15fc73dba..73752081e77 100644 --- a/packages/inter-protocol/src/proposals/addAssetToVault.js +++ b/packages/inter-protocol/src/proposals/addAssetToVault.js @@ -1,16 +1,24 @@ // @jessie-check +// @ts-check +import { ToFarFunction } from '@endo/captp'; +import { Far } from '@endo/marshal'; import { AmountMath, AssetKind } from '@agoric/ertp'; import { deeplyFulfilledObject } from '@agoric/internal'; import { makeRatio } from '@agoric/zoe/src/contractSupport/index.js'; import { parseRatio } from '@agoric/zoe/src/contractSupport/ratio.js'; import { E } from '@endo/far'; import { Stable } from '@agoric/internal/src/tokens.js'; +import { TimeMath } from '@agoric/time/src/timeMath.js'; +import { makePromiseKit } from '@endo/promise-kit'; + import { instanceNameFor } from './price-feed-proposal.js'; import { reserveThenGetNames } from './utils.js'; export * from './startPSM.js'; +const { quote: q } = assert; + /** * @typedef {object} InterchainAssetOptions * @property {string} [issuerBoardId] @@ -24,8 +32,10 @@ export * from './startPSM.js'; * @property {number} [initialPrice] */ +/** @typedef {import('./econ-behaviors.js').EconomyBootstrapPowers} EconomyBootstrapPowers */ + /** - * @param {EconomyBootstrapPowers} powers + * @param {BootstrapPowers} powers * @param {object} config * @param {object} config.options * @param {InterchainAssetOptions} config.options.interchainAssetOptions @@ -216,7 +226,65 @@ export const registerScaledPriceAuthority = async ( ); }; -/** @typedef {import('./econ-behaviors.js').EconomyBootstrapPowers} EconomyBootstrapPowers */ +// wait a short while after end to allow things to settle +const BUFFER = 5n * 60n; +// let's insist on 20 minutes leeway for running the scripts +const COMPLETION = 20n * 60n; + +// If there is a liveSchedule, 1) run now if start is far enough away, +// otherwise, 2) run after endTime. +// If neither liveSchedule nor nextSchedule is defined, 3) run now. +// If there is only a nextSchedule, 4) run now if startTime is far enough away, +// else 5) run after endTime +const whenQuiescent = async (schedules, timer, thunk) => { + const { nextAuctionSchedule, liveAuctionSchedule } = schedules; + const now = await E(timer).getCurrentTimestamp(); + + const waker = Far('addAssetWaker', { wake: () => thunk() }); + + if (liveAuctionSchedule) { + const safeStart = TimeMath.subtractAbsRel( + liveAuctionSchedule.startTime, + COMPLETION, + ); + + if (TimeMath.compareAbs(safeStart, now) < 0) { + // case 2 + console.warn( + `Add Asset after live schedule's endtime: ${q( + liveAuctionSchedule.endTime, + )}`, + ); + + return E(timer).setWakeup( + TimeMath.addAbsRel(liveAuctionSchedule.endTime, BUFFER), + waker, + ); + } + } + + if (!liveAuctionSchedule && nextAuctionSchedule) { + const safeStart = TimeMath.subtractAbsRel( + nextAuctionSchedule.startTime, + COMPLETION, + ); + if (TimeMath.compareAbs(safeStart, now) < 0) { + // case 5 + console.warn( + `Add Asset after next schedule's endtime: ${q( + nextAuctionSchedule.endTime, + )}`, + ); + return E(timer).setWakeup( + TimeMath.addAbsRel(nextAuctionSchedule.endTime, BUFFER), + waker, + ); + } + } + + console.warn(`Add Asset immediately`, thunk); + return thunk(); +}; /** * @param {EconomyBootstrapPowers} powers @@ -228,7 +296,12 @@ export const registerScaledPriceAuthority = async ( */ export const addAssetToVault = async ( { - consume: { vaultFactoryKit, agoricNamesAdmin, auctioneerKit }, + consume: { + vaultFactoryKit, + agoricNamesAdmin, + auctioneerKit, + chainTimerService, + }, brand: { consume: { [Stable.symbol]: stableP }, }, @@ -263,6 +336,21 @@ export const addAssetToVault = async ( // eslint-disable-next-line no-restricted-syntax -- allow this computed property await consumeInstance[oracleInstanceName]; + const auctioneerCreator = E.get(auctioneerKit).creatorFacet; + const schedules = await E(auctioneerCreator).getSchedule(); + + const finishPromiseKit = makePromiseKit(); + const addBrandThenResolve = ToFarFunction('addBrandThenResolve', async () => { + await E(auctioneerCreator).addBrand(interchainIssuer, keyword); + finishPromiseKit.resolve(true); + return true; + }); + + // schedules actions on a timer (or does it immediately). + // finishPromiseKit signals completion. + void whenQuiescent(schedules, chainTimerService, addBrandThenResolve); + await finishPromiseKit.promise; + const stable = await stableP; const vaultFactoryCreator = E.get(vaultFactoryKit).creatorFacet; await E(vaultFactoryCreator).addVaultType(interchainIssuer, keyword, { @@ -277,8 +365,6 @@ export const addAssetToVault = async ( mintFee: makeRatio(50n, stable, 10_000n), liquidationPenalty: makeRatio(1n, stable), }); - const auctioneerCreator = E.get(auctioneerKit).creatorFacet; - await E(auctioneerCreator).addBrand(interchainIssuer, keyword); }; export const getManifestForAddAssetToVault = ( @@ -338,6 +424,7 @@ export const getManifestForAddAssetToVault = ( auctioneerKit: 'auctioneer', vaultFactoryKit: 'vaultFactory', agoricNamesAdmin: true, + chainTimerService: true, }, brand: { consume: { [Stable.symbol]: true }, From d589ef3eba57244c807825a500f481ffd7d39698 Mon Sep 17 00:00:00 2001 From: Chris Hibbert Date: Wed, 4 Oct 2023 15:19:36 -0700 Subject: [PATCH 2/5] test: test for adding assets at various times --- .../test/bootstrapTests/test-addAssets.ts | 196 ++++++++++++++++++ 1 file changed, 196 insertions(+) create mode 100644 packages/boot/test/bootstrapTests/test-addAssets.ts diff --git a/packages/boot/test/bootstrapTests/test-addAssets.ts b/packages/boot/test/bootstrapTests/test-addAssets.ts new file mode 100644 index 00000000000..940f370f3e0 --- /dev/null +++ b/packages/boot/test/bootstrapTests/test-addAssets.ts @@ -0,0 +1,196 @@ +import { test as anyTest } from '@agoric/zoe/tools/prepare-test-env-ava.js'; + +import type { TestFn } from 'ava'; + +import { TimeMath } from '@agoric/time'; +import { + LiquidationTestContext, + makeLiquidationTestContext, +} from './liquidation.ts'; + +const test = anyTest as TestFn; + +const auctioneerPath = 'published.auction'; + +test.before(async t => { + t.context = await makeLiquidationTestContext(t); + const proposal = await t.context.buildProposal({ + package: 'builders', + packageScriptName: 'build:add-STARS-proposal', + }); + + t.log('installing proposal'); + // share a single proposal so tests don't stomp on each other's files; It has + // to be edited by each so as not to re-use keywords. + for await (const bundle of proposal.bundles) { + await t.context.controller.validateAndInstallBundle(bundle); + } + t.log('installed', proposal.bundles.length, 'bundles'); + // @ts-expect-error override + t.context.proposal = proposal; + t.log('installed', proposal.bundles.length, 'bundles'); +}); + +test.after.always(t => { + // This will fail if a subset of tests are run. It detects that three + // collaterals were added to the auction after ATOM. + t.like(t.context.readLatest(`${auctioneerPath}.book3`), { + currentPriceLevel: null, + }); + return t.context.shutdown && t.context.shutdown(); +}); + +test('addAsset to quiescent auction', async t => { + const { + advanceTimeTo, + readLatest, + // @ts-expect-error override + proposal, + } = t.context; + + // stringify, modify and parse because modifying a deep copy was fragile. + const proposalJSON = JSON.stringify(proposal); + const proposalMod = proposalJSON.replaceAll('STARS', 'COMETS'); + const proposalNew = JSON.parse(proposalMod); + + const bridgeMessage = { + type: 'CORE_EVAL', + evals: proposalNew.evals, + }; + + const { EV } = t.context.runUtils; + + const auctioneerKit = await EV.vat('bootstrap').consumeItem('auctioneerKit'); + const schedules = await EV(auctioneerKit.creatorFacet).getSchedule(); + const { liveAuctionSchedule, nextAuctionSchedule } = schedules; + const nextEndTime = liveAuctionSchedule + ? liveAuctionSchedule.endTime + : nextAuctionSchedule.endTime; + const fiveMinutes = harden({ + relValue: 5n * 60n, + timerBrand: nextEndTime.timerBrand, + }); + const nextQuiescentTime = TimeMath.addAbsRel(nextEndTime, fiveMinutes); + await advanceTimeTo(nextQuiescentTime); + + const coreEvalBridgeHandler = await EV.vat('bootstrap').consumeItem( + 'coreEvalBridgeHandler', + ); + await EV(coreEvalBridgeHandler).fromBridge(bridgeMessage); + t.log('add-STARS proposal executed'); + + t.like(readLatest(`${auctioneerPath}.book1`), { + currentPriceLevel: null, + }); +}); + +test('addAsset to active auction', async t => { + const { + advanceTimeTo, + readLatest, + // @ts-expect-error override + proposal, + } = t.context; + const { EV } = t.context.runUtils; + + t.like(readLatest(`${auctioneerPath}.book0`), { startPrice: null }); + + const auctioneerKit = await EV.vat('bootstrap').consumeItem('auctioneerKit'); + const schedules = await EV(auctioneerKit.creatorFacet).getSchedule(); + const { nextAuctionSchedule } = schedules; + t.truthy(nextAuctionSchedule); + const nextStartTime = nextAuctionSchedule.startTime; + const fiveMinutes = harden({ + relValue: 5n * 60n, + timerBrand: nextStartTime.timerBrand, + }); + const futureBusyTime = TimeMath.addAbsRel(nextStartTime, fiveMinutes); + + await advanceTimeTo(futureBusyTime); + + t.log('launching proposal'); + + const proposalJSON = JSON.stringify(proposal); + const proposalMod = proposalJSON + .replaceAll('STARS', 'PLANETS') + .replaceAll('ibc/987C17B1', 'ibc/987C17B2'); + const proposalNew = JSON.parse(proposalMod); + + const bridgeMessage = { + type: 'CORE_EVAL', + evals: proposalNew.evals, + }; + t.log({ bridgeMessage }); + + const coreEvalBridgeHandler = await EV.vat('bootstrap').consumeItem( + 'coreEvalBridgeHandler', + ); + EV(coreEvalBridgeHandler).fromBridge(bridgeMessage); + + const nextEndTime = nextAuctionSchedule.endTime; + const afterEndTime = TimeMath.addAbsRel(nextEndTime, fiveMinutes); + await advanceTimeTo(afterEndTime); + + const schedulesAfter = await EV(auctioneerKit.creatorFacet).getSchedule(); + // brand equality is broken + t.truthy( + schedules.nextAuctionSchedule.endTime.absValue < + schedulesAfter.nextAuctionSchedule.endTime.absValue, + ); + + t.like(readLatest(`${auctioneerPath}.book1`), { currentPriceLevel: null }); +}); + +test('addAsset to auction starting soon', async t => { + const { + advanceTimeTo, + // @ts-expect-error override + proposal, + readLatest, + } = t.context; + const { EV } = t.context.runUtils; + + const auctioneerKit = await EV.vat('bootstrap').consumeItem('auctioneerKit'); + const schedules = await EV(auctioneerKit.creatorFacet).getSchedule(); + const { nextAuctionSchedule } = schedules; + t.truthy(nextAuctionSchedule); + const nextStartTime = nextAuctionSchedule.startTime; + const fiveMinutes = harden({ + relValue: 5n * 60n, + timerBrand: nextStartTime.timerBrand, + }); + const tooCloseTime = TimeMath.subtractAbsRel(nextStartTime, fiveMinutes); + + await advanceTimeTo(tooCloseTime); + + const proposalJSON = JSON.stringify(proposal); + const proposalMod = proposalJSON + .replaceAll('STARS', 'MOONS') + .replaceAll('ibc/987C17B1', 'ibc/987C17B3'); + const proposalNew = JSON.parse(proposalMod); + + t.log('launching proposal'); + const bridgeMessage = { + type: 'CORE_EVAL', + evals: proposalNew.evals, + }; + t.log({ bridgeMessage }); + + const coreEvalBridgeHandler = await EV.vat('bootstrap').consumeItem( + 'coreEvalBridgeHandler', + ); + EV(coreEvalBridgeHandler).fromBridge(bridgeMessage); + + const nextEndTime = nextAuctionSchedule.endTime; + const afterEndTime = TimeMath.addAbsRel(nextEndTime, fiveMinutes); + await advanceTimeTo(afterEndTime); + + t.log('add-STARS proposal executed'); + + const schedulesAfter = await EV(auctioneerKit.creatorFacet).getSchedule(); + t.truthy( + schedules.nextAuctionSchedule.endTime.absValue < + schedulesAfter.nextAuctionSchedule.endTime.absValue, + ); + t.like(readLatest(`${auctioneerPath}.book1`), { currentPriceLevel: null }); +}); From db080ce2d300e5a01da2cd740d24ef54668d12f7 Mon Sep 17 00:00:00 2001 From: Chris Hibbert Date: Thu, 5 Oct 2023 10:42:39 -0700 Subject: [PATCH 3/5] chore: clean-ups and clarifications --- .../test/bootstrapTests/test-addAssets.ts | 2 +- .../src/proposals/addAssetToVault.js | 22 +++++++++++++------ 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/packages/boot/test/bootstrapTests/test-addAssets.ts b/packages/boot/test/bootstrapTests/test-addAssets.ts index 940f370f3e0..c57dd110e72 100644 --- a/packages/boot/test/bootstrapTests/test-addAssets.ts +++ b/packages/boot/test/bootstrapTests/test-addAssets.ts @@ -132,7 +132,7 @@ test('addAsset to active auction', async t => { await advanceTimeTo(afterEndTime); const schedulesAfter = await EV(auctioneerKit.creatorFacet).getSchedule(); - // brand equality is broken + // TimeMath.compareAbs() complains that the brands don't match t.truthy( schedules.nextAuctionSchedule.endTime.absValue < schedulesAfter.nextAuctionSchedule.endTime.absValue, diff --git a/packages/inter-protocol/src/proposals/addAssetToVault.js b/packages/inter-protocol/src/proposals/addAssetToVault.js index 73752081e77..fce52bb9fde 100644 --- a/packages/inter-protocol/src/proposals/addAssetToVault.js +++ b/packages/inter-protocol/src/proposals/addAssetToVault.js @@ -231,11 +231,19 @@ const BUFFER = 5n * 60n; // let's insist on 20 minutes leeway for running the scripts const COMPLETION = 20n * 60n; -// If there is a liveSchedule, 1) run now if start is far enough away, -// otherwise, 2) run after endTime. -// If neither liveSchedule nor nextSchedule is defined, 3) run now. -// If there is only a nextSchedule, 4) run now if startTime is far enough away, -// else 5) run after endTime +/** + * This function works around an issue identified in #8307 and #8296, and fixed + * in #8301. The fix is needed until #8301 makes it into production. + * + * If there is a liveSchedule, 1) run now if start is far enough away, + * otherwise, 2) run after endTime. If neither liveSchedule nor nextSchedule is + * defined, 3) run now. If there is only a nextSchedule, 4) run now if startTime + * is far enough away, else 5) run after endTime + * + * @param {import('../auction/scheduler.js').FullSchedule} schedules + * @param {ERef} timer + * @param {() => void} thunk + */ const whenQuiescent = async (schedules, timer, thunk) => { const { nextAuctionSchedule, liveAuctionSchedule } = schedules; const now = await E(timer).getCurrentTimestamp(); @@ -282,6 +290,7 @@ const whenQuiescent = async (schedules, timer, thunk) => { } } + // cases 1, 3, and 4 fall through to here. console.warn(`Add Asset immediately`, thunk); return thunk(); }; @@ -342,8 +351,7 @@ export const addAssetToVault = async ( const finishPromiseKit = makePromiseKit(); const addBrandThenResolve = ToFarFunction('addBrandThenResolve', async () => { await E(auctioneerCreator).addBrand(interchainIssuer, keyword); - finishPromiseKit.resolve(true); - return true; + finishPromiseKit.resolve(undefined); }); // schedules actions on a timer (or does it immediately). From 539e3dd1e673fcc6447481260f66dcb5c0e11fc7 Mon Sep 17 00:00:00 2001 From: Chris Hibbert Date: Thu, 5 Oct 2023 13:45:10 -0700 Subject: [PATCH 4/5] test: refactor test context setup to share propsoal mutation. --- .../test/bootstrapTests/test-addAssets.ts | 85 +++++++++---------- 1 file changed, 38 insertions(+), 47 deletions(-) diff --git a/packages/boot/test/bootstrapTests/test-addAssets.ts b/packages/boot/test/bootstrapTests/test-addAssets.ts index c57dd110e72..ad785dea91b 100644 --- a/packages/boot/test/bootstrapTests/test-addAssets.ts +++ b/packages/boot/test/bootstrapTests/test-addAssets.ts @@ -7,14 +7,22 @@ import { LiquidationTestContext, makeLiquidationTestContext, } from './liquidation.ts'; - -const test = anyTest as TestFn; +import { makeProposalExtractor } from './supports.ts'; + +const test = anyTest as TestFn< + LiquidationTestContext & { + getCollateralProposal: ( + name: string, + id: string, + ) => Awaited>>; + } +>; const auctioneerPath = 'published.auction'; test.before(async t => { - t.context = await makeLiquidationTestContext(t); - const proposal = await t.context.buildProposal({ + const context = await makeLiquidationTestContext(t); + const proposal = await context.buildProposal({ package: 'builders', packageScriptName: 'build:add-STARS-proposal', }); @@ -23,11 +31,22 @@ test.before(async t => { // share a single proposal so tests don't stomp on each other's files; It has // to be edited by each so as not to re-use keywords. for await (const bundle of proposal.bundles) { - await t.context.controller.validateAndInstallBundle(bundle); + await context.controller.validateAndInstallBundle(bundle); } t.log('installed', proposal.bundles.length, 'bundles'); - // @ts-expect-error override - t.context.proposal = proposal; + + const getCollateralProposal = (name, id) => { + // stringify, modify and parse because modifying a deep copy was fragile. + const proposalJSON = JSON.stringify(proposal); + const proposalMod = proposalJSON + .replaceAll('STARS', name) + .replaceAll('ibc/987C17B1', `ibc/987C17B1${id}`); + return JSON.parse(proposalMod); + }; + t.context = { + ...context, + getCollateralProposal, + }; t.log('installed', proposal.bundles.length, 'bundles'); }); @@ -41,21 +60,12 @@ test.after.always(t => { }); test('addAsset to quiescent auction', async t => { - const { - advanceTimeTo, - readLatest, - // @ts-expect-error override - proposal, - } = t.context; - - // stringify, modify and parse because modifying a deep copy was fragile. - const proposalJSON = JSON.stringify(proposal); - const proposalMod = proposalJSON.replaceAll('STARS', 'COMETS'); - const proposalNew = JSON.parse(proposalMod); + const { advanceTimeTo, readLatest } = t.context; + const proposal = t.context.getCollateralProposal('COMETS', 'A'); const bridgeMessage = { type: 'CORE_EVAL', - evals: proposalNew.evals, + evals: proposal.evals, }; const { EV } = t.context.runUtils; @@ -77,7 +87,7 @@ test('addAsset to quiescent auction', async t => { 'coreEvalBridgeHandler', ); await EV(coreEvalBridgeHandler).fromBridge(bridgeMessage); - t.log('add-STARS proposal executed'); + t.log('proposal executed'); t.like(readLatest(`${auctioneerPath}.book1`), { currentPriceLevel: null, @@ -85,12 +95,7 @@ test('addAsset to quiescent auction', async t => { }); test('addAsset to active auction', async t => { - const { - advanceTimeTo, - readLatest, - // @ts-expect-error override - proposal, - } = t.context; + const { advanceTimeTo, readLatest } = t.context; const { EV } = t.context.runUtils; t.like(readLatest(`${auctioneerPath}.book0`), { startPrice: null }); @@ -110,15 +115,10 @@ test('addAsset to active auction', async t => { t.log('launching proposal'); - const proposalJSON = JSON.stringify(proposal); - const proposalMod = proposalJSON - .replaceAll('STARS', 'PLANETS') - .replaceAll('ibc/987C17B1', 'ibc/987C17B2'); - const proposalNew = JSON.parse(proposalMod); - + const proposal = t.context.getCollateralProposal('PLANETS', 'B'); const bridgeMessage = { type: 'CORE_EVAL', - evals: proposalNew.evals, + evals: proposal.evals, }; t.log({ bridgeMessage }); @@ -130,6 +130,7 @@ test('addAsset to active auction', async t => { const nextEndTime = nextAuctionSchedule.endTime; const afterEndTime = TimeMath.addAbsRel(nextEndTime, fiveMinutes); await advanceTimeTo(afterEndTime); + t.log('proposal executed'); const schedulesAfter = await EV(auctioneerKit.creatorFacet).getSchedule(); // TimeMath.compareAbs() complains that the brands don't match @@ -142,12 +143,7 @@ test('addAsset to active auction', async t => { }); test('addAsset to auction starting soon', async t => { - const { - advanceTimeTo, - // @ts-expect-error override - proposal, - readLatest, - } = t.context; + const { advanceTimeTo, readLatest } = t.context; const { EV } = t.context.runUtils; const auctioneerKit = await EV.vat('bootstrap').consumeItem('auctioneerKit'); @@ -163,16 +159,11 @@ test('addAsset to auction starting soon', async t => { await advanceTimeTo(tooCloseTime); - const proposalJSON = JSON.stringify(proposal); - const proposalMod = proposalJSON - .replaceAll('STARS', 'MOONS') - .replaceAll('ibc/987C17B1', 'ibc/987C17B3'); - const proposalNew = JSON.parse(proposalMod); - + const proposal = t.context.getCollateralProposal('MOONS', 'C'); t.log('launching proposal'); const bridgeMessage = { type: 'CORE_EVAL', - evals: proposalNew.evals, + evals: proposal.evals, }; t.log({ bridgeMessage }); @@ -185,7 +176,7 @@ test('addAsset to auction starting soon', async t => { const afterEndTime = TimeMath.addAbsRel(nextEndTime, fiveMinutes); await advanceTimeTo(afterEndTime); - t.log('add-STARS proposal executed'); + t.log('proposal executed'); const schedulesAfter = await EV(auctioneerKit.creatorFacet).getSchedule(); t.truthy( From 6d52bcb4f1b8c192414f8d522edf789c3e577471 Mon Sep 17 00:00:00 2001 From: Chris Hibbert Date: Thu, 5 Oct 2023 13:49:58 -0700 Subject: [PATCH 5/5] chore: update comment on object comparison breakage --- packages/boot/test/bootstrapTests/test-addAssets.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/boot/test/bootstrapTests/test-addAssets.ts b/packages/boot/test/bootstrapTests/test-addAssets.ts index ad785dea91b..12510dd810d 100644 --- a/packages/boot/test/bootstrapTests/test-addAssets.ts +++ b/packages/boot/test/bootstrapTests/test-addAssets.ts @@ -133,7 +133,7 @@ test('addAsset to active auction', async t => { t.log('proposal executed'); const schedulesAfter = await EV(auctioneerKit.creatorFacet).getSchedule(); - // TimeMath.compareAbs() complains that the brands don't match + // TimeMath.compareAbs() can't handle brands processed by kmarshall t.truthy( schedules.nextAuctionSchedule.endTime.absValue < schedulesAfter.nextAuctionSchedule.endTime.absValue,