Skip to content

Commit

Permalink
Merge pull request #8398 from Agoric/8347-timing
Browse files Browse the repository at this point in the history
refactor: delay addition of issuer to auctioneer until a good time
  • Loading branch information
mergify[bot] authored Oct 6, 2023
2 parents a0c5f45 + 6d52bcb commit 61383f9
Show file tree
Hide file tree
Showing 3 changed files with 288 additions and 5 deletions.
187 changes: 187 additions & 0 deletions packages/boot/test/bootstrapTests/test-addAssets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
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';
import { makeProposalExtractor } from './supports.ts';

const test = anyTest as TestFn<
LiquidationTestContext & {
getCollateralProposal: (
name: string,
id: string,
) => Awaited<ReturnType<ReturnType<typeof makeProposalExtractor>>>;
}
>;

const auctioneerPath = 'published.auction';

test.before(async t => {
const context = await makeLiquidationTestContext(t);
const proposal = await 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 context.controller.validateAndInstallBundle(bundle);
}
t.log('installed', proposal.bundles.length, 'bundles');

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');
});

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 } = t.context;

const proposal = t.context.getCollateralProposal('COMETS', 'A');
const bridgeMessage = {
type: 'CORE_EVAL',
evals: proposal.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('proposal executed');

t.like(readLatest(`${auctioneerPath}.book1`), {
currentPriceLevel: null,
});
});

test('addAsset to active auction', async t => {
const { advanceTimeTo, readLatest } = 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 proposal = t.context.getCollateralProposal('PLANETS', 'B');
const bridgeMessage = {
type: 'CORE_EVAL',
evals: proposal.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('proposal executed');

const schedulesAfter = await EV(auctioneerKit.creatorFacet).getSchedule();
// TimeMath.compareAbs() can't handle brands processed by kmarshall
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, 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 proposal = t.context.getCollateralProposal('MOONS', 'C');
t.log('launching proposal');
const bridgeMessage = {
type: 'CORE_EVAL',
evals: proposal.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('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 });
});
1 change: 1 addition & 0 deletions packages/inter-protocol/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
105 changes: 100 additions & 5 deletions packages/inter-protocol/src/proposals/addAssetToVault.js
Original file line number Diff line number Diff line change
@@ -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]
Expand All @@ -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
Expand Down Expand Up @@ -216,7 +226,74 @@ 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;

/**
* 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<import('@agoric/time/src/types').TimerService>} timer
* @param {() => void} thunk
*/
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,
);
}
}

// cases 1, 3, and 4 fall through to here.
console.warn(`Add Asset immediately`, thunk);
return thunk();
};

/**
* @param {EconomyBootstrapPowers} powers
Expand All @@ -228,7 +305,12 @@ export const registerScaledPriceAuthority = async (
*/
export const addAssetToVault = async (
{
consume: { vaultFactoryKit, agoricNamesAdmin, auctioneerKit },
consume: {
vaultFactoryKit,
agoricNamesAdmin,
auctioneerKit,
chainTimerService,
},
brand: {
consume: { [Stable.symbol]: stableP },
},
Expand Down Expand Up @@ -263,6 +345,20 @@ 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(undefined);
});

// 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, {
Expand All @@ -277,8 +373,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 = (
Expand Down Expand Up @@ -338,6 +432,7 @@ export const getManifestForAddAssetToVault = (
auctioneerKit: 'auctioneer',
vaultFactoryKit: 'vaultFactory',
agoricNamesAdmin: true,
chainTimerService: true,
},
brand: {
consume: { [Stable.symbol]: true },
Expand Down

0 comments on commit 61383f9

Please sign in to comment.