Skip to content

Commit

Permalink
feat(auction): allow orders to automatically exit after partial fill
Browse files Browse the repository at this point in the history
  • Loading branch information
Chris-Hibbert committed Mar 30, 2023
1 parent 3f9059a commit 493b816
Show file tree
Hide file tree
Showing 4 changed files with 130 additions and 32 deletions.
53 changes: 28 additions & 25 deletions packages/inter-protocol/src/auction/auctionBook.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,12 @@ const trace = makeTracer('AucBook', false);
/**
* @typedef {{
* want: Amount<'nat'>
* } & {
* exitAfterBuy?: boolean,
* } & ({
* offerPrice: Ratio,
* offerPrice: Ratio,
* } | {
* offerBidScaling: Ratio,
* offerBidScaling: Ratio,
* })} BidSpec
*/
/**
Expand All @@ -71,6 +73,7 @@ export const makeBidSpecShape = (currencyBrand, collateralBrand) => {
return M.splitRecord(
{ want: collateralAmountShape },
{
exitAfterBuy: true,
// xxx should have exactly one of these properties
offerPrice: makeBrandedRatioPattern(
currencyAmountShape,
Expand Down Expand Up @@ -267,8 +270,9 @@ export const prepareAuctionBook = (baggage, zcf) => {
* @param {Ratio} price
* @param {Amount<'nat'>} want
* @param {boolean} trySettle
* @param {boolean} [exitAfterBuy]
*/
acceptPriceOffer(seat, price, want, trySettle) {
acceptPriceOffer(seat, price, want, trySettle, exitAfterBuy = false) {
const { priceBook, curAuctionPrice } = this.state;
const { helper } = this.facets;
trace('acceptPrice');
Expand All @@ -281,13 +285,14 @@ export const prepareAuctionBook = (baggage, zcf) => {

const stillWant = AmountMath.subtract(want, collateralSold);
if (
(exitAfterBuy && !AmountMath.isEmpty(collateralSold)) ||
AmountMath.isEmpty(stillWant) ||
AmountMath.isEmpty(seat.getCurrentAllocation().Currency)
) {
seat.exit();
} else {
trace('added Offer ', price, stillWant.value);
priceBook.add(seat, price, stillWant);
priceBook.add(seat, price, stillWant, exitAfterBuy);
}
},

Expand All @@ -300,8 +305,15 @@ export const prepareAuctionBook = (baggage, zcf) => {
* @param {Ratio} bidScaling
* @param {Amount<'nat'>} want
* @param {boolean} trySettle
* @param {boolean} [exitAfterBuy]
*/
acceptScaledBidOffer(seat, bidScaling, want, trySettle) {
acceptScaledBidOffer(
seat,
bidScaling,
want,
trySettle,
exitAfterBuy = false,
) {
trace('accept scaled bid offer');
const { curAuctionPrice, lockedPriceForRound, scaledBidBook } =
this.state;
Expand All @@ -318,12 +330,13 @@ export const prepareAuctionBook = (baggage, zcf) => {

const stillWant = AmountMath.subtract(want, collateralSold);
if (
(exitAfterBuy && !AmountMath.isEmpty(collateralSold)) ||
AmountMath.isEmpty(stillWant) ||
AmountMath.isEmpty(seat.getCurrentAllocation().Currency)
) {
seat.exit();
} else {
scaledBidBook.add(seat, bidScaling, stillWant);
scaledBidBook.add(seat, bidScaling, stillWant, exitAfterBuy);
}
},
},
Expand Down Expand Up @@ -418,7 +431,8 @@ export const prepareAuctionBook = (baggage, zcf) => {

const { totalProceedsGoal } = this.state;
const { helper } = this.facets;
for (const [key, { seat, price: p, wanted }] of prioritizedOffers) {
for (const [key, seatRecord] of prioritizedOffers) {
const { seat, price: p, wanted, exitAfterBuy } = seatRecord;
if (totalProceedsGoal && AmountMath.isEmpty(totalProceedsGoal)) {
break;
} else if (seat.hasExited()) {
Expand All @@ -428,6 +442,7 @@ export const prepareAuctionBook = (baggage, zcf) => {

const alloc = seat.getCurrentAllocation();
if (
(exitAfterBuy && !AmountMath.isEmpty(collateralSold)) ||
AmountMath.isEmpty(alloc.Currency) ||
('Collateral' in alloc &&
AmountMath.isGTE(alloc.Collateral, wanted))
Expand Down Expand Up @@ -462,52 +477,40 @@ export const prepareAuctionBook = (baggage, zcf) => {
);
},
/**
*
* @param {BidSpec} bidSpec
* @param {ZCFSeat} seat
* @param {boolean} trySettle
*/
addOffer(bidSpec, seat, trySettle) {
const { collateralAmountShape, currencyAmountShape } = this.state;
const BidSpecShape = M.or(
{
want: collateralAmountShape,
offerPrice: makeBrandedRatioPattern(
currencyAmountShape,
collateralAmountShape,
),
},
{
want: collateralAmountShape,
offerBidScaling: makeBrandedRatioPattern(
currencyAmountShape,
currencyAmountShape,
),
},
);
const { currencyBrand, collateralBrand } = this.state;
const BidSpecShape = makeBidSpecShape(currencyBrand, collateralBrand);

mustMatch(bidSpec, BidSpecShape);
const { give } = seat.getProposal();
const { currencyAmountShape } = this.state;
mustMatch(
give.Currency,
currencyAmountShape,
'give must include "Currency"',
);

const { helper } = this.facets;
const exitAfterBuy = bidSpec.exitAfterBuy;
if ('offerPrice' in bidSpec) {
return helper.acceptPriceOffer(
seat,
bidSpec.offerPrice,
bidSpec.want,
trySettle,
exitAfterBuy,
);
} else if ('offerBidScaling' in bidSpec) {
return helper.acceptScaledBidOffer(
seat,
bidSpec.offerBidScaling,
bidSpec.want,
trySettle,
exitAfterBuy,
);
} else {
throw Fail`Offer was neither a price nor a scaled bid`;
Expand Down
4 changes: 3 additions & 1 deletion packages/inter-protocol/src/auction/auctioneer.js
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,9 @@ export const start = async (zcf, privateArgs, baggage) => {
deposits.set(brand, []);
} else if (depositsForBrand.length > 1) {
const collProceeds = collateralSeat.getCurrentAllocation().Collateral;
const currProceeds = currencySeat.getCurrentAllocation().Currency;
const currProceeds =
currencySeat.getCurrentAllocation().Currency ||
AmountMath.makeEmpty(brands.Currency);
const transfers = distributeProportionalSharesWithLimits(
collProceeds,
currProceeds,
Expand Down
15 changes: 12 additions & 3 deletions packages/inter-protocol/src/auction/offerBook.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const nextSequenceNumber = () => {
* wanted: Amount<'nat'>,
* seqNum: NatValue,
* received: Amount<'nat'>,
* } & ({ bidScaling: Pattern, price: undefined } | { bidScaling: undefined, price: Ratio})
* } & {exitAfterBuy: boolean} & ({ bidScaling: Pattern, price: undefined } | { bidScaling: undefined, price: Ratio})
* } BidderRecord
*/

Expand Down Expand Up @@ -60,8 +60,9 @@ export const prepareScaledBidBook = baggage =>
* @param {ZCFSeat} seat
* @param {Ratio} bidScaling
* @param {Amount<'nat'>} wanted
* @param {boolean} exitAfterBuy
*/
add(seat, bidScaling, wanted) {
add(seat, bidScaling, wanted, exitAfterBuy) {
const { bidScalingPattern, collateralBrand, records } = this.state;
mustMatch(bidScaling, bidScalingPattern);

Expand All @@ -76,6 +77,7 @@ export const prepareScaledBidBook = baggage =>
seat,
seqNum,
wanted,
exitAfterBuy,
};
records.init(key, harden(bidderRecord));
return key;
Expand Down Expand Up @@ -139,7 +141,13 @@ export const preparePriceBook = baggage =>
records: makeScalarBigMapStore('scaledBidRecords', { durable: true }),
}),
{
add(seat, price, wanted) {
/**
* @param {ZCFSeat} seat
* @param {Ratio} price
* @param {Amount<'nat'>} wanted
* @param {boolean} exitAfterBuy
*/
add(seat, price, wanted, exitAfterBuy) {
const { priceRatioPattern, collateralBrand, records } = this.state;
mustMatch(price, priceRatioPattern);

Expand All @@ -154,6 +162,7 @@ export const preparePriceBook = baggage =>
seat,
seqNum,
wanted,
exitAfterBuy,
};
records.init(key, harden(bidderRecord));
return key;
Expand Down
90 changes: 87 additions & 3 deletions packages/inter-protocol/test/auction/test-auctionContract.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ const makeAuctionDriver = async (t, customTerms, params = defaultParams) => {
* @param {Amount<'nat'>} giveCurrency
* @param {Amount<'nat'>} wantCollateral
* @param {Ratio} [discount]
* @param {ExitRule} [exitRule]
* @param {ExitRule | { onBuy: true }} [exitRule]
*/
const bidForCollateralSeat = async (
giveCurrency,
Expand All @@ -155,7 +155,8 @@ const makeAuctionDriver = async (t, customTerms, params = defaultParams) => {
// IF we had multiples, the buyer could express an offer-safe want.
// want: { Collateral: wantCollateral },
};
if (exitRule) {

if (exitRule && !('onBuy' in exitRule)) {
rawProposal.exit = exitRule;
}
const proposal = harden(rawProposal);
Expand All @@ -172,6 +173,10 @@ const makeAuctionDriver = async (t, customTerms, params = defaultParams) => {
discount ||
harden(makeRatioFromAmounts(giveCurrency, wantCollateral)),
};
if (exitRule && 'onBuy' in exitRule) {
offerArgs.exitAfterBuy = true;
}

return E(zoe).offer(bidInvitation, proposal, payment, harden(offerArgs));
};

Expand Down Expand Up @@ -376,6 +381,32 @@ test.serial('discount bid settled', async t => {
await assertPayouts(t, seat, currency, collateral, 250n - 231n, 200n);
});

test.serial('discount bid exit onBuy', async t => {
const { collateral, currency } = t.context;
const driver = await makeAuctionDriver(t);

await driver.setupCollateralAuction(collateral, collateral.make(1000n));
await driver.updatePriceAuthority(
makeRatioFromAmounts(currency.make(11n), collateral.make(10n)),
);

const schedules = await driver.getSchedule();
t.is(schedules.nextAuctionSchedule?.startTime.absValue, 170n);
await driver.advanceTo(170n);

const seat = await driver.bidForCollateralSeat(
currency.make(2000n),
collateral.make(200n),
makeRatioFromAmounts(currency.make(120n), currency.make(100n)),
{ onBuy: true },
);
t.is(await E(seat).getOfferResult(), 'Your bid has been accepted');
await driver.advanceTo(180n);

// 250 - 200 * (1.1 * 1.05)
await assertPayouts(t, seat, currency, collateral, 2000n - 231n, 200n);
});

// serial because dynamicConfig is shared across tests
test.serial('priced bid insufficient collateral added', async t => {
const { collateral, currency } = t.context;
Expand Down Expand Up @@ -627,7 +658,60 @@ test.serial('multiple Depositors, with goal', async t => {
await E(seat).tryExit();
t.true(await E(seat).hasExited());

// 1500 Collateral was put up for auction by two bidders (1000 and 500), so
// 1500 Collateral was put up for auction by two depositors (1000 and 500), so
// one seller gets 66% of the proceeds, and the other 33%. The price authority
// quote was 1.1, and the goods were sold in the first auction round at 105%.
// At those rates, 900 pays for 799 collateral. The sellers pro-rate 900 and
// the returned collateral. The auctioneer sets the remainder aside.
await assertPayouts(t, seat, currency, collateral, 600n, 779n);
await assertPayouts(t, liqSeatA, currency, collateral, 600n, 480n);
await assertPayouts(t, liqSeatB, currency, collateral, 300n, 239n);
});

// serial because dynamicConfig is shared across tests
test.serial('multiple Depositors, exit onBuy', async t => {
const { collateral, currency } = t.context;
const driver = await makeAuctionDriver(t);

const liqSeatA = await driver.setupCollateralAuction(
collateral,
collateral.make(1000n),
);
const liqSeatB = await driver.depositCollateral(
collateral.make(500n),
collateral,
{ goal: currency.make(300n) },
);
await driver.updatePriceAuthority(
makeRatioFromAmounts(currency.make(11n), collateral.make(10n)),
);

const result = await E(liqSeatA).getOfferResult();
t.is(result, 'deposited');

await driver.advanceTo(167n);
const seat = await driver.bidForCollateralSeat(
currency.make(1500n),
collateral.make(1000n),
undefined,
{ onBuy: true },
);

t.is(await E(seat).getOfferResult(), 'Your bid has been accepted');
t.false(await E(seat).hasExited());

await driver.advanceTo(170n);
await eventLoopIteration();
await driver.advanceTo(175n);
await eventLoopIteration();
await driver.advanceTo(180n);
await eventLoopIteration();
await driver.advanceTo(185n);
await eventLoopIteration();

t.true(await E(seat).hasExited());

// 1500 Collateral was put up for auction by two depositors (1000 and 500), so
// one seller gets 66% of the proceeds, and the other 33%. The price authority
// quote was 1.1, and the goods were sold in the first auction round at 105%.
// At those rates, 900 pays for 799 collateral. The sellers pro-rate 900 and
Expand Down

0 comments on commit 493b816

Please sign in to comment.