Skip to content

Commit

Permalink
auction scheduler robustness (#9759)
Browse files Browse the repository at this point in the history
_incidental_

## Description
Refactors the auction amount math out of the auction book and adds unit tests. I noticed an inconsistency so this adds an assertion to prevent that and a fix. Also a bunch of refactorings and docs to try to make this area of the code more clear.

Reviewers, review by commit is recommended.

### Security Considerations
The auction book now calls out to another module to do the math. Module from its own package though.

### Scaling Considerations
no change

### Documentation Considerations
not user facing

### Testing Considerations
regression test

### Upgrade Considerations

This won't go out until VaultFactory gets a new auctioneer. See,
- #8735
- #8981

@Chris-Hibbert is working on the CoreEval and its test to deploy this
  • Loading branch information
mergify[bot] authored Aug 13, 2024
2 parents b9f5667 + e9e637c commit 13c10cc
Show file tree
Hide file tree
Showing 5 changed files with 333 additions and 39 deletions.
57 changes: 21 additions & 36 deletions packages/inter-protocol/src/auction/auctionBook.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import { assertAllDefined, makeTracer } from '@agoric/internal';
import {
atomicRearrange,
ceilMultiplyBy,
floorDivideBy,
makeRatioFromAmounts,
makeRecorderTopic,
multiplyRatios,
Expand All @@ -21,6 +20,7 @@ import {
import { observeNotifier } from '@agoric/notifier';

import { makeNatAmountShape } from '../contractSupport.js';
import { amountsToSettle } from './auctionMath.js';
import { preparePriceBook, prepareScaledBidBook } from './offerBook.js';
import {
isScaledBidPriceHigher,
Expand Down Expand Up @@ -282,39 +282,30 @@ export const prepareAuctionBook = (baggage, zcf, makeRecorderKit) => {
return makeEmpty(collateralBrand);
}

/** @type {Amount<'nat'>} */
const initialCollateralTarget = AmountMath.min(
collateralWanted,
collateralAvailable,
);

const { curAuctionPrice, bidHoldingSeat, remainingProceedsGoal } =
this.state;
curAuctionPrice !== null ||
Fail`auctionPrice must be set before each round`;
assert(curAuctionPrice);

const proceedsNeeded = ceilMultiplyBy(
initialCollateralTarget,
curAuctionPrice,
);
if (AmountMath.isEmpty(proceedsNeeded)) {
const { proceedsExpected, proceedsTarget, collateralTarget } =
amountsToSettle(
{
bidAlloc,
collateralWanted,
collateralAvailable,
curAuctionPrice,
remainingProceedsGoal,
},
trace,
);

if (proceedsExpected === null) {
seat.fail(Error('price fell to zero'));
return makeEmpty(collateralBrand);
}

const minProceedsTarget = AmountMath.min(proceedsNeeded, bidAlloc);
const proceedsLimit = remainingProceedsGoal
? AmountMath.min(remainingProceedsGoal, minProceedsTarget)
: minProceedsTarget;
const isRaiseLimited =
remainingProceedsGoal ||
!AmountMath.isGTE(proceedsLimit, proceedsNeeded);

const [proceedsTarget, collateralTarget] = isRaiseLimited
? [proceedsLimit, floorDivideBy(proceedsLimit, curAuctionPrice)]
: [minProceedsTarget, initialCollateralTarget];

// check that the requested amount could be satisfied
const { Collateral } = seat.getProposal().want;
if (Collateral && AmountMath.isGTE(Collateral, collateralTarget)) {
seat.exit('unable to satisfy want');
Expand All @@ -335,7 +326,7 @@ export const prepareAuctionBook = (baggage, zcf, makeRecorderKit) => {
);
}

trace('settle', {
trace('settled', {
collateralTarget,
proceedsTarget,
remainingProceedsGoal: this.state.remainingProceedsGoal,
Expand Down Expand Up @@ -611,20 +602,14 @@ export const prepareAuctionBook = (baggage, zcf, makeRecorderKit) => {
const pricedOffers = priceBook.offersAbove(curAuctionPrice);
const scaledBidOffers = scaledBidBook.offersAbove(reduction);

const compareValues = (v1, v2) => {
if (v1 < v2) {
return -1;
} else if (v1 === v2) {
return 0;
} else {
return 1;
}
};
trace(`settling`, pricedOffers.length, scaledBidOffers.length);
// requested price or BidScaling gives no priority beyond specifying which
// round the order will be serviced in.
const prioritizedOffers = [...pricedOffers, ...scaledBidOffers].sort(
(a, b) => compareValues(a[1].seqNum, b[1].seqNum),
(a, b) => Number(a[1].seqNum - b[1].seqNum),
);

trace(
`settling ${prioritizedOffers.length} offers at ${curAuctionPrice} (priced ${pricedOffers.length}, scaled ${scaledBidOffers.length}) `,
);

const { remainingProceedsGoal } = state;
Expand Down
81 changes: 81 additions & 0 deletions packages/inter-protocol/src/auction/auctionMath.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { AmountMath } from '@agoric/ertp';
import {
ceilMultiplyBy,
floorDivideBy,
} from '@agoric/zoe/src/contractSupport/index.js';

/**
* @import {Amount} from '@agoric/ertp/src/types.js';
*/

/**
* @param {object} p
* @param {Amount<'nat'>} p.bidAlloc current allocation of the bidding seat
* @param {Amount<'nat'>} p.collateralWanted want of the offer
* @param {Amount<'nat'>} p.collateralAvailable available to auction
* @param {Ratio} p.curAuctionPrice current auction price
* @param {Amount<'nat'> | null} p.remainingProceedsGoal amount still needing
* liquidating over multiple rounds; null indicates no limit
* @param {(...msgs: any[]) => void} [log]
*/
export const amountsToSettle = (
{
bidAlloc,
collateralWanted,
collateralAvailable,
curAuctionPrice,
remainingProceedsGoal,
},
log = () => {},
) => {
log('amountsToSettle', {
bidAlloc,
collateralWanted,
collateralAvailable,
curAuctionPrice,
remainingProceedsGoal,
});
const initialCollateralTarget = AmountMath.min(
collateralWanted,
collateralAvailable,
);

const proceedsExpected = ceilMultiplyBy(
initialCollateralTarget,
curAuctionPrice,
);
if (AmountMath.isEmpty(proceedsExpected)) {
return { proceedsExpected: null };
}

const targetByProceeds = proceedsLimit =>
AmountMath.min(
collateralAvailable,
floorDivideBy(proceedsLimit, curAuctionPrice),
);

const [proceedsTarget, collateralTarget] = (() => {
// proceeds cannot exceed what is needed or being offered
const proceedsBidded = AmountMath.min(proceedsExpected, bidAlloc);
if (remainingProceedsGoal) {
const goalProceeds = AmountMath.min(
remainingProceedsGoal,
proceedsBidded,
);
return [goalProceeds, targetByProceeds(goalProceeds)];
} else if (AmountMath.isGTE(proceedsBidded, proceedsExpected)) {
// initial collateral suffices
return [proceedsBidded, initialCollateralTarget];
} else {
return [proceedsBidded, targetByProceeds(proceedsBidded)];
}
})();

assert(
AmountMath.isGTE(collateralAvailable, collateralTarget),
'target cannot exceed available',
);

return { proceedsExpected, proceedsTarget, collateralTarget };
};
harden(amountsToSettle);
204 changes: 204 additions & 0 deletions packages/inter-protocol/test/auction/auctionMath.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js';

import { AmountMath } from '@agoric/ertp';
import { makeRatioFromAmounts } from '@agoric/zoe/src/contractSupport/ratio.js';
import { Far } from '@endo/far';
import { amountsToSettle } from '../../src/auction/auctionMath.js';

/**
* @import {Amount, Brand} from '@agoric/ertp/src/types.js';
*/

const brand = /** @type {any} */ (Far('fungible brand', {}));

const testAmounts = test.macro(
/**
* @type {(
* t: import('ava').ExecutionContext<unknown>,
* input: any,
* output: any,
* ) => Promise<void>}
*/
async (
t,
{ bid, want, avail, price, goal },
{ expected, procTarget, collTarget },
) => {
/** @type {(n: number) => Amount<'nat'>} */
const amt = n => AmountMath.make(brand, BigInt(n));

const result = amountsToSettle({
bidAlloc: amt(bid),
collateralWanted: amt(want),
collateralAvailable: amt(avail),
curAuctionPrice: makeRatioFromAmounts(amt(price[0]), amt(price[1])),
remainingProceedsGoal: goal ? amt(goal) : null,
});

t.deepEqual(result, {
proceedsExpected: amt(expected),
proceedsTarget: amt(procTarget),
collateralTarget: amt(collTarget),
});
},
);

// These were observed in other tests
test(
'observed 1',
testAmounts,
{ bid: 578, want: 500, avail: 1000, price: [1155, 1000] },
{
expected: 578,
procTarget: 578,
collTarget: 500,
},
);

test(
'observed 2 - with remaining proceeds goal',
testAmounts,
{ bid: 125, want: 100, avail: 400, price: [525, 1000], goal: 200 },
{
expected: 53,
procTarget: 53,
collTarget: 100,
},
);

test(
'observed 3',
testAmounts,
{ bid: 231, want: 200, avail: 1000, price: [1155, 1000] },
{
expected: 231,
procTarget: 231,
collTarget: 200,
},
);

test(
'observed 4',
testAmounts,
{ bid: 232, want: 200, avail: 100, price: [1155, 1000] },
{
expected: 116,
procTarget: 116,
collTarget: 100,
},
);

test(
'observed 5',
testAmounts,
{ bid: 19, want: 300, avail: 300, price: [625, 10000] },
{
expected: 19,
procTarget: 19,
collTarget: 300,
},
);

test(
'observed 6',
testAmounts,
{ bid: 23, want: 200, avail: 500, price: [1125, 10000] },
{
expected: 23,
procTarget: 23,
collTarget: 200,
},
);

test(
'observed 7',
testAmounts,
{ bid: 500, want: 2000, avail: 717, price: [715, 1000] },
{
expected: 513,
procTarget: 500,
collTarget: 699,
},
);

test(
'observed 8',
testAmounts,
{ bid: 240, want: 200, avail: 20, price: [1155, 1000] },
{
expected: 24,
procTarget: 24,
collTarget: 20,
},
);

test(
'observed 9',
testAmounts,
{ bid: 2000, want: 200, avail: 1000, price: [1155, 1000] },
{
expected: 231,
procTarget: 231,
collTarget: 200,
},
);

test(
'observed 10',
testAmounts,
{ bid: 2240, want: 200, avail: 1000, price: [1155, 1000] },
{
expected: 231,
procTarget: 231,
collTarget: 200,
},
);

test(
'want exceeeds avail',
testAmounts,
{ bid: 2000, want: 2000, avail: 1000, price: [1, 1] },
{
expected: 1000,
procTarget: 1000,
collTarget: 1000,
},
);

test(
'want exceeeds avail at half price',
testAmounts,
{ bid: 1999, want: 2000, avail: 1000, price: [201, 1] },
{
expected: 201000,
procTarget: 1999,
collTarget: 9,
},
);
test(
'want exceeeds avail at half price with goal',
testAmounts,
{ bid: 1999, want: 2000, avail: 1000, price: [201, 1], goal: 301 },
{
expected: 201000,
procTarget: 301,
collTarget: 1,
},
);

test(
'observed in production',
testAmounts,
{
bid: 3000,
want: 2000,
avail: 1000,
price: [4914, 10000], // "currentPriceLevel": "0.4914 IST/stOSMO",
goal: 1254_886835, // "remainingProceedsGoal": "1254.886835 IST",
},
{
expected: 492,
procTarget: 492,
collTarget: 1000,
},
);
Loading

0 comments on commit 13c10cc

Please sign in to comment.