From f195783d7062d1a774ce86a6fa7239d687bef6a8 Mon Sep 17 00:00:00 2001 From: Chris Hibbert Date: Wed, 1 Mar 2023 17:55:40 -0800 Subject: [PATCH] chore: responses to reviews --- .../inter-protocol/src/auction/auctionBook.js | 264 ++++++++++-------- .../inter-protocol/src/auction/auctioneer.js | 116 +++++--- .../inter-protocol/src/auction/offerBook.js | 36 ++- packages/inter-protocol/src/auction/util.js | 16 +- .../test/auction/test-auctionBook.js | 80 +++++- .../test/auction/test-proportionalDist.js | 168 +++++++++++ .../test/smartWallet/test-amm-integration.js | 6 - packages/internal/src/utils.js | 2 + 8 files changed, 503 insertions(+), 185 deletions(-) create mode 100644 packages/inter-protocol/test/auction/test-proportionalDist.js delete mode 100644 packages/inter-protocol/test/smartWallet/test-amm-integration.js diff --git a/packages/inter-protocol/src/auction/auctionBook.js b/packages/inter-protocol/src/auction/auctionBook.js index 40793749a3b2..d72de4552028 100644 --- a/packages/inter-protocol/src/auction/auctionBook.js +++ b/packages/inter-protocol/src/auction/auctionBook.js @@ -3,7 +3,7 @@ import '@agoric/zoe/src/contracts/exported.js'; import '@agoric/governance/exported.js'; import { M, makeScalarBigMapStore, provide } from '@agoric/vat-data'; -import { AmountMath, AmountShape } from '@agoric/ertp'; +import { AmountMath } from '@agoric/ertp'; import { Far } from '@endo/marshal'; import { mustMatch } from '@agoric/store'; import { observeNotifier } from '@agoric/notifier'; @@ -21,7 +21,6 @@ import { makeTracer } from '@agoric/internal'; import { makeScaledBidBook, makePriceBook } from './offerBook.js'; import { - AuctionState, isScaledBidPriceHigher, makeBrandedRatioPattern, priceFrom, @@ -29,6 +28,8 @@ import { const { Fail } = assert; +const DEFAULT_DECIMALS = 9n; + /** * @file The book represents the collateral-specific state of an ongoing * auction. It holds the book, the lockedPrice, and the collateralSeat that has @@ -37,17 +38,17 @@ const { Fail } = assert; * The book contains orders for the collateral. It holds two kinds of * orders: * - Prices express the bid in terms of a Currency amount - * - Scaled bid express the bid in terms of a discount (or markup) from the + * - Scaled bids express the bid in terms of a discount (or markup) from the * most recent oracle price. * - * Offers can be added in three ways. When the auction is not active, prices are - * automatically added to the appropriate collection. If a new offer is at or - * above the current price of an active auction, it will be settled immediately. - * If the offer is below the current price, it will be added, and settled when - * the price reaches that level. + * Offers can be added in three ways. 1) When the auction is not active, prices + * are automatically added to the appropriate collection. When the auction is + * active, 2) if a new offer is at or above the current price, it will be + * settled immediately; 2) If the offer is below the current price, it will be + * added in the appropriate place and settled when the price reaches that level. */ -const trace = makeTracer('AucBook', false); +const trace = makeTracer('AucBook', true); /** @typedef {import('@agoric/vat-data').Baggage} Baggage */ @@ -62,72 +63,124 @@ export const makeAuctionBook = async ( AmountMath.makeEmpty(currencyBrand), AmountMath.make(collateralBrand, 1n), ); + const [currencyAmountShape, collateralAmountShape] = await Promise.all([ + E(currencyBrand).getAmountShape(), + E(collateralBrand).getAmountShape(), + ]); const BidSpecShape = M.or( { - want: AmountShape, - offerPrice: makeBrandedRatioPattern(currencyBrand, collateralBrand), + want: collateralAmountShape, + offerPrice: makeBrandedRatioPattern( + currencyAmountShape, + collateralAmountShape, + ), }, { - want: AmountShape, - offerBidScaling: makeBrandedRatioPattern(currencyBrand, currencyBrand), + want: collateralAmountShape, + offerBidScaling: makeBrandedRatioPattern( + currencyAmountShape, + currencyAmountShape, + ), }, ); let assetsForSale = AmountMath.makeEmpty(collateralBrand); + + // these don't have to be durable, since we're currently assuming that upgrade + // from a quiescent state is sufficient. When the auction is quiescent, there + // may be offers in the book, but these seats will be empty, with all assets + // returned to the funders. const { zcfSeat: collateralSeat } = zcf.makeEmptySeatKit(); const { zcfSeat: currencySeat } = zcf.makeEmptySeatKit(); let lockedPriceForRound = zeroRatio; let updatingOracleQuote = zeroRatio; - E.when(E(collateralBrand).getDisplayInfo(), ({ decimalPlaces = 9n }) => { - // TODO(#6946) use this to keep a current price that can be published in state. - const quoteNotifier = E(priceAuthority).makeQuoteNotifier( - AmountMath.make(collateralBrand, 10n ** decimalPlaces), - currencyBrand, - ); + E.when( + E(collateralBrand).getDisplayInfo(), + ({ decimalPlaces = DEFAULT_DECIMALS }) => { + // TODO(#6946) use this to keep a current price that can be published in state. + const quoteNotifier = E(priceAuthority).makeQuoteNotifier( + AmountMath.make(collateralBrand, 10n ** decimalPlaces), + currencyBrand, + ); - observeNotifier(quoteNotifier, { - updateState: quote => { - trace( - `BOOK notifier ${priceFrom(quote).numerator.value}/${ - priceFrom(quote).denominator.value - }`, - ); - return (updatingOracleQuote = priceFrom(quote)); - }, - fail: reason => { - throw Error(`auction observer of ${collateralBrand} failed: ${reason}`); - }, - finish: done => { - throw Error(`auction observer for ${collateralBrand} died: ${done}`); - }, - }); - }); + observeNotifier(quoteNotifier, { + updateState: quote => { + trace( + `BOOK notifier ${priceFrom(quote).numerator.value}/${ + priceFrom(quote).denominator.value + }`, + ); + return (updatingOracleQuote = priceFrom(quote)); + }, + fail: reason => { + throw Error( + `auction observer of ${collateralBrand} failed: ${reason}`, + ); + }, + finish: done => { + throw Error(`auction observer for ${collateralBrand} died: ${done}`); + }, + }); + }, + ); let curAuctionPrice = zeroRatio; const scaledBidBook = provide(baggage, 'scaledBidBook', () => { + const ratioPattern = makeBrandedRatioPattern( + currencyAmountShape, + currencyAmountShape, + ); const scaledBidStore = makeScalarBigMapStore('scaledBidBookStore', { durable: true, }); - return makeScaledBidBook(scaledBidStore, currencyBrand, collateralBrand); + return makeScaledBidBook(scaledBidStore, ratioPattern, collateralBrand); }); const priceBook = provide(baggage, 'sortedOffers', () => { + const ratioPattern = makeBrandedRatioPattern( + currencyAmountShape, + collateralAmountShape, + ); + const priceStore = makeScalarBigMapStore('sortedOffersStore', { durable: true, }); - return makePriceBook(priceStore, currencyBrand, collateralBrand); + return makePriceBook(priceStore, ratioPattern, collateralBrand); }); - const removeFromOneBook = (isPriceBook, key) => { - if (isPriceBook) { + /** + * remove the key from the appropriate book, indicated by whether the price + * is defined. + * + * @param {string} key + * @param {Ratio | undefined} price + */ + const removeFromItsBook = (key, price) => { + if (price) { priceBook.delete(key); } else { scaledBidBook.delete(key); } }; + /** + * Update the entry in the appropriate book, indicated by whether the price + * is defined. + * + * @param {string} key + * @param {Amount} collateralSold + * @param {Ratio | undefined} price + */ + const updateItsBook = (key, collateralSold, price) => { + if (price) { + priceBook.updateReceived(key, collateralSold); + } else { + scaledBidBook.updateReceived(key, collateralSold); + } + }; + // Settle with seat. The caller is responsible for updating the book, if any. const settle = (seat, collateralWanted) => { const { Currency: currencyAvailable } = seat.getCurrentAllocation(); @@ -149,42 +202,30 @@ export const makeAuctionBook = async ( return AmountMath.makeEmptyFromAmount(collateralWanted); } - let collateralRecord; - let currencyRecord; - if (AmountMath.isGTE(currencyAvailable, currencyNeeded)) { - collateralRecord = { - Collateral: collateralTarget, - }; - currencyRecord = { - Currency: currencyNeeded, - }; - } else { - const affordableCollateral = floorDivideBy( - currencyAvailable, - curAuctionPrice, - ); - collateralRecord = { - Collateral: affordableCollateral, - }; - currencyRecord = { - Currency: currencyAvailable, - }; - } - - trace('settle', { currencyRecord, collateralRecord }); + const affordableAmounts = () => { + if (AmountMath.isGTE(currencyAvailable, currencyNeeded)) { + return [collateralTarget, currencyNeeded]; + } else { + const affordableCollateral = floorDivideBy( + currencyAvailable, + curAuctionPrice, + ); + return [affordableCollateral, currencyAvailable]; + } + }; + const [collateralAmount, currencyAmount] = affordableAmounts(); + trace('settle', { collateralAmount, currencyAmount }); atomicRearrange( zcf, harden([ - [collateralSeat, seat, collateralRecord], - [seat, currencySeat, currencyRecord], + [collateralSeat, seat, { Collateral: collateralAmount }], + [seat, currencySeat, { Currency: currencyAmount }], ]), ); - return collateralRecord.Collateral; + return collateralAmount; }; - const isActive = auctionState => auctionState === AuctionState.ACTIVE; - /** * Accept an offer expressed as a price. If the auction is active, attempt to * buy collateral. If any of the offer remains add it to the book. @@ -192,28 +233,26 @@ export const makeAuctionBook = async ( * @param {ZCFSeat} seat * @param {Ratio} price * @param {Amount} want - * @param {AuctionState} auctionState + * @param {boolean} trySettle */ - const acceptPriceOffer = (seat, price, want, auctionState) => { + const acceptPriceOffer = (seat, price, want, trySettle) => { trace('acceptPrice'); // Offer has ZcfSeat, offerArgs (w/price) and timeStamp - let collateralSold = AmountMath.makeEmptyFromAmount(want); - if (isActive(auctionState) && ratioGTE(price, curAuctionPrice)) { - collateralSold = settle(seat, want); - - if (AmountMath.isEmpty(seat.getCurrentAllocation().Currency)) { - seat.exit(); - return; - } - } + const collateralSold = + trySettle && ratioGTE(price, curAuctionPrice) + ? settle(seat, want) + : AmountMath.makeEmptyFromAmount(want); const stillWant = AmountMath.subtract(want, collateralSold); - if (!AmountMath.isEmpty(stillWant)) { + if ( + AmountMath.isEmpty(stillWant) || + AmountMath.isEmpty(seat.getCurrentAllocation().Currency) + ) { + seat.exit(); + } else { trace('added Offer ', price, stillWant.value); priceBook.add(seat, price, stillWant); - } else { - seat.exit(); } }; @@ -225,28 +264,24 @@ export const makeAuctionBook = async ( * @param {ZCFSeat} seat * @param {Ratio} bidScaling * @param {Amount} want - * @param {AuctionState} auctionState + * @param {boolean} trySettle */ - const acceptScaledBidOffer = (seat, bidScaling, want, auctionState) => { + const acceptScaledBidOffer = (seat, bidScaling, want, trySettle) => { trace('accept scaled bid offer'); - let collateralSold = AmountMath.makeEmptyFromAmount(want); - - if ( - isActive(auctionState) && + const collateralSold = + trySettle && isScaledBidPriceHigher(bidScaling, curAuctionPrice, lockedPriceForRound) - ) { - collateralSold = settle(seat, want); - if (AmountMath.isEmpty(seat.getCurrentAllocation().Currency)) { - seat.exit(); - return; - } - } + ? settle(seat, want) + : AmountMath.makeEmptyFromAmount(want); const stillWant = AmountMath.subtract(want, collateralSold); - if (!AmountMath.isEmpty(stillWant)) { - scaledBidBook.add(seat, bidScaling, stillWant); - } else { + if ( + AmountMath.isEmpty(stillWant) || + AmountMath.isEmpty(seat.getCurrentAllocation().Currency) + ) { seat.exit(); + } else { + scaledBidBook.add(seat, bidScaling, stillWant); } }; @@ -263,16 +298,16 @@ export const makeAuctionBook = async ( curAuctionPrice = multiplyRatios(reduction, lockedPriceForRound); const pricedOffers = priceBook.offersAbove(curAuctionPrice); - const discOffers = scaledBidBook.offersAbove(reduction); + const scaledBidOffers = scaledBidBook.offersAbove(reduction); + trace(`settling`, pricedOffers.length, scaledBidOffers.length); // requested price or bid scaling gives no priority beyond specifying which - // round the order will be service in. - const prioritizedOffers = [...pricedOffers, ...discOffers].sort(); + // round the order will be serviced in. + const prioritizedOffers = [...pricedOffers, ...scaledBidOffers].sort(); - trace(`settling`, pricedOffers.length, discOffers.length); for (const [key, { seat, price: p, wanted }] of prioritizedOffers) { if (seat.hasExited()) { - removeFromOneBook(p, key); + removeFromItsBook(key, p); } else { const collateralSold = settle(seat, wanted); @@ -281,13 +316,9 @@ export const makeAuctionBook = async ( AmountMath.isGTE(seat.getCurrentAllocation().Collateral, wanted) ) { seat.exit(); - removeFromOneBook(p, key); + removeFromItsBook(key, p); } else if (!AmountMath.isGTE(collateralSold, wanted)) { - if (p) { - priceBook.updateReceived(key, collateralSold); - } else { - scaledBidBook.updateReceived(key, collateralSold); - } + updateItsBook(key, collateralSold, p); } } } @@ -307,22 +338,29 @@ export const makeAuctionBook = async ( trace('set startPrice', lockedPriceForRound); curAuctionPrice = multiplyRatios(lockedPriceForRound, rate); }, - addOffer(bidSpec, seat, auctionState) { + addOffer(bidSpec, seat, trySettle) { mustMatch(bidSpec, BidSpecShape); + const { give } = seat.getProposal(); + mustMatch( + give.Currency, + currencyAmountShape, + 'give must include "Currency"', + ); if (bidSpec.offerPrice) { + give.C; return acceptPriceOffer( seat, bidSpec.offerPrice, bidSpec.want, - auctionState, + trySettle, ); } else if (bidSpec.offerBidScaling) { return acceptScaledBidOffer( seat, bidSpec.offerBidScaling, bidSpec.want, - auctionState, + trySettle, ); } else { throw Fail`Offer was neither a price nor a scaled bid`; @@ -337,3 +375,5 @@ export const makeAuctionBook = async ( }, }); }; + +/** @typedef {Awaited>} AuctionBook */ diff --git a/packages/inter-protocol/src/auction/auctioneer.js b/packages/inter-protocol/src/auction/auctioneer.js index 6a2dcbeafe42..a51a92ee8d06 100644 --- a/packages/inter-protocol/src/auction/auctioneer.js +++ b/packages/inter-protocol/src/auction/auctioneer.js @@ -19,11 +19,11 @@ import { provideEmptySeat, } from '@agoric/zoe/src/contractSupport/index.js'; import { handleParamGovernance } from '@agoric/governance'; -import { makeTracer } from '@agoric/internal'; +import { makeTracer, BASIS_POINTS } from '@agoric/internal'; import { FullProposalShape } from '@agoric/zoe/src/typeGuards.js'; import { makeAuctionBook } from './auctionBook.js'; -import { BASIS_POINTS } from './util.js'; +import { AuctionState } from './util.js'; import { makeScheduler } from './scheduler.js'; import { auctioneerParamTypes } from './params.js'; @@ -36,9 +36,69 @@ const trace = makeTracer('Auction', false); const makeBPRatio = (rate, currencyBrand, collateralBrand = currencyBrand) => makeRatioFromAmounts( AmountMath.make(currencyBrand, rate), - AmountMath.make(collateralBrand, 10000n), + AmountMath.make(collateralBrand, BASIS_POINTS), ); +/** + * Return a set of transfers for atomicRearrange() that distribute + * collateralRaised and currencyRaised proportionally to each seat's deposited + * amount. Any uneven split should be allocated to the reserve. + * + * @param {Amount} collateralRaised + * @param {Amount} currencyRaised + * @param {{seat: ZCFSeat, amount: Amount<"nat">}[]} deposits + * @param {ZCFSeat} collateralSeat + * @param {ZCFSeat} currencySeat + * @param {string} collateralKeyword + * @param {ZCFSeat} reserveSeat + * @param {Brand} brand + */ +export const distributeProportionalShares = ( + collateralRaised, + currencyRaised, + deposits, + collateralSeat, + currencySeat, + collateralKeyword, + reserveSeat, + brand, +) => { + const totalCollDeposited = deposits.reduce((prev, { amount }) => { + return AmountMath.add(prev, amount); + }, AmountMath.makeEmpty(brand)); + + const collShare = makeRatioFromAmounts(collateralRaised, totalCollDeposited); + const currShare = makeRatioFromAmounts(currencyRaised, totalCollDeposited); + /** @type {import('@agoric/zoe/src/contractSupport/atomicTransfer.js').TransferPart[]} */ + const transfers = []; + let currencyLeft = currencyRaised; + let collateralLeft = collateralRaised; + + // each depositor gets a share that equals their amount deposited + // divided by the total deposited multiplied by the currency and + // collateral being distributed. + for (const { seat, amount } of deposits.values()) { + const currPortion = floorMultiplyBy(amount, currShare); + currencyLeft = AmountMath.subtract(currencyLeft, currPortion); + const collPortion = floorMultiplyBy(amount, collShare); + collateralLeft = AmountMath.subtract(collateralLeft, collPortion); + transfers.push([currencySeat, seat, { Currency: currPortion }]); + transfers.push([collateralSeat, seat, { Collateral: collPortion }]); + } + + // TODO The leftovers should go to the reserve, and should be visible. + transfers.push([currencySeat, reserveSeat, { Currency: currencyLeft }]); + + // There will be multiple collaterals, so they can't all use the same keyword + transfers.push([ + collateralSeat, + reserveSeat, + { Collateral: collateralLeft }, + { [collateralKeyword]: collateralLeft }, + ]); + return transfers; +}; + /** * @param {ZCF { timer || Fail`Timer must be in Auctioneer terms`; const timerBrand = await E(timer).getTimerBrand(); + /** @type {MapStore} */ const books = provideDurableMapStore(baggage, 'auctionBooks'); + /** @type {MapStore}>>} */ const deposits = provideDurableMapStore(baggage, 'deposits'); + /** @type {MapStore} */ const brandToKeyword = provideDurableMapStore(baggage, 'brandToKeyword'); const reserveFunds = provideEmptySeat(zcf, baggage, 'collateral'); @@ -100,45 +163,18 @@ export const start = async (zcf, privateArgs, baggage) => { liqSeat.exit(); deposits.set(brand, []); } else if (depositsForBrand.length > 1) { - const totCollDeposited = depositsForBrand.reduce((prev, { amount }) => { - return AmountMath.add(prev, amount); - }, AmountMath.makeEmpty(brand)); - const collatRaise = collateralSeat.getCurrentAllocation().Collateral; const currencyRaise = currencySeat.getCurrentAllocation().Currency; - - const collShare = makeRatioFromAmounts(collatRaise, totCollDeposited); - const currShare = makeRatioFromAmounts(currencyRaise, totCollDeposited); - /** @type {import('@agoric/zoe/src/contractSupport/atomicTransfer.js').TransferPart[]} */ - const transfers = []; - let currencyLeft = currencyRaise; - let collateralLeft = collatRaise; - - // each depositor gets as share that equals their amount deposited - // divided by the total deposited multplied by the currency and - // collateral being distributed. - for (const { seat, amount } of deposits.get(brand).values()) { - const currPortion = floorMultiplyBy(amount, currShare); - currencyLeft = AmountMath.subtract(currencyLeft, currPortion); - const collPortion = floorMultiplyBy(amount, collShare); - collateralLeft = AmountMath.subtract(collateralLeft, collPortion); - transfers.push([currencySeat, seat, { Currency: currPortion }]); - transfers.push([collateralSeat, seat, { Collateral: collPortion }]); - } - - // TODO The leftovers should go to the reserve, and should be visible. - const keyword = brandToKeyword.get(brand); - transfers.push([ - currencySeat, - reserveFunds, - { Currency: currencyLeft }, - ]); - transfers.push([ + const transfers = distributeProportionalShares( + collatRaise, + currencyRaise, + depositsForBrand, collateralSeat, + currencySeat, + brandToKeyword.get(brand), reserveFunds, - { Collateral: collateralLeft }, - { [keyword]: collateralLeft }, - ]); + brand, + ); atomicRearrange(zcf, harden(transfers)); for (const { seat } of depositsForBrand) { @@ -205,6 +241,8 @@ export const start = async (zcf, privateArgs, baggage) => { // @ts-expect-error types are correct. How to convince TS? const scheduler = await makeScheduler(driver, timer, params, timerBrand); + const isActive = () => scheduler.getAuctionState() === AuctionState.ACTIVE; + const depositOfferHandler = zcfSeat => { const { Collateral: collateralAmount } = zcfSeat.getCurrentAllocation(); const book = books.get(collateralAmount.brand); @@ -222,7 +260,7 @@ export const start = async (zcf, privateArgs, baggage) => { const newBidHandler = (zcfSeat, bidSpec) => { if (books.has(collateralBrand)) { const auctionBook = books.get(collateralBrand); - auctionBook.addOffer(bidSpec, zcfSeat, scheduler.getAuctionState()); + auctionBook.addOffer(bidSpec, zcfSeat, isActive()); return 'Your offer has been received'; } else { zcfSeat.exit(`No book for brand ${collateralBrand}`); diff --git a/packages/inter-protocol/src/auction/offerBook.js b/packages/inter-protocol/src/auction/offerBook.js index 0c54b1cc456b..aa45661ee54e 100644 --- a/packages/inter-protocol/src/auction/offerBook.js +++ b/packages/inter-protocol/src/auction/offerBook.js @@ -11,7 +11,8 @@ import { toPartialOfferKey, toPriceOfferKey, } from './sortedOffers.js'; -import { makeBrandedRatioPattern } from './util.js'; + +/** @typedef {import('@agoric/vat-data').Baggage} Baggage */ // multiple offers might be provided at the same time (since the time // granularity is limited to blocks), so we increment a sequenceNumber with each @@ -22,12 +23,22 @@ const nextSequenceNumber = () => { return latestSequenceNumber; }; -// prices in this book are expressed as percentage of the full oracle price -// snapshot taken when the auction started. .4 is 60% off. 1.1 is 10% above par. -export const makeScaledBidBook = (store, currencyBrand, collateralBrand) => { +/** + * Prices in this book are expressed as percentage of the full oracle price + * snapshot taken when the auction started. .4 is 60% off. 1.1 is 10% above par. + * + * @param {Baggage} store + * @param {Pattern} bidScalingPattern + * @param {Brand} collateralBrand + */ +export const makeScaledBidBook = ( + store, + bidScalingPattern, + collateralBrand, +) => { return Far('scaledBidBook ', { add(seat, bidScaling, wanted) { - // XXX mustMatch(bidScaling, BID_SCALING_PATTERN); + mustMatch(bidScaling, bidScalingPattern); const seqNum = nextSequenceNumber(); const key = toScaledRateOfferKey(bidScaling, seqNum); @@ -68,13 +79,18 @@ export const makeScaledBidBook = (store, currencyBrand, collateralBrand) => { }); }; -// prices in this book are actual prices expressed in terms of currency amount -// and collateral amount. -export const makePriceBook = (store, currencyBrand, collateralBrand) => { - const RATIO_PATTERN = makeBrandedRatioPattern(currencyBrand, collateralBrand); +/** + * Prices in this book are actual prices expressed in terms of currency amount + * and collateral amount. + * + * @param {Baggage} store + * @param {Pattern} ratioPattern + * @param {Brand} collateralBrand + */ +export const makePriceBook = (store, ratioPattern, collateralBrand) => { return Far('priceBook ', { add(seat, price, wanted) { - mustMatch(price, RATIO_PATTERN); + mustMatch(price, ratioPattern); const seqNum = nextSequenceNumber(); const key = toPriceOfferKey(price, seqNum); diff --git a/packages/inter-protocol/src/auction/util.js b/packages/inter-protocol/src/auction/util.js index 5f0440241def..5462ca3a0c37 100644 --- a/packages/inter-protocol/src/auction/util.js +++ b/packages/inter-protocol/src/auction/util.js @@ -1,12 +1,9 @@ -import { M } from '@agoric/store'; import { makeRatioFromAmounts, multiplyRatios, ratioGTE, } from '@agoric/zoe/src/contractSupport/index.js'; -export const BASIS_POINTS = 10000n; - /** * Constants for Auction State. * @@ -17,10 +14,17 @@ export const AuctionState = { WAITING: 'waiting', }; -export const makeBrandedRatioPattern = (nBrand, dBrand) => { +/** + * @param {Pattern} numeratorAmountShape + * @param {Pattern} denominatorAmountShape + */ +export const makeBrandedRatioPattern = ( + numeratorAmountShape, + denominatorAmountShape, +) => { return harden({ - numerator: { brand: nBrand, value: M.nat() }, - denominator: { brand: dBrand, value: M.nat() }, + numerator: numeratorAmountShape, + denominator: denominatorAmountShape, }); }; diff --git a/packages/inter-protocol/test/auction/test-auctionBook.js b/packages/inter-protocol/test/auction/test-auctionBook.js index ade8786dedae..1c4e30df5d72 100644 --- a/packages/inter-protocol/test/auction/test-auctionBook.js +++ b/packages/inter-protocol/test/auction/test-auctionBook.js @@ -14,7 +14,6 @@ import { eventLoopIteration } from '@agoric/notifier/tools/testSupports.js'; import { setup } from '../../../zoe/test/unitTests/setupBasicMints.js'; import { makeAuctionBook } from '../../src/auction/auctionBook.js'; -import { AuctionState } from '../../src/auction/util.js'; const buildManualPriceAuthority = initialPrice => makeManualPriceAuthority({ @@ -62,21 +61,21 @@ const makeSeatWithAssets = async (zoe, zcf, giveAmount, giveKwd, issuerKit) => { return zcfSeat; }; -test('acceptOffer fakeSeat', async t => { +test('simple addOffer', async t => { const { moolaKit, moola, simoleans, simoleanKit } = setup(); const { zoe, zcf } = await setupZCFTest(); await zcf.saveIssuer(moolaKit.issuer, 'Moola'); await zcf.saveIssuer(simoleanKit.issuer, 'Sim'); - const payment = moolaKit.mint.mintPayment(moola(100n)); - - const { zcfSeat } = await makeOffer( + const zcfSeat = await makeSeatWithAssets( zoe, zcf, - { give: { Bid: moola(100n) }, want: { Ask: simoleans(0n) } }, - { Bid: payment }, + moola(100n), + 'Currency', + moolaKit, ); + const baggage = makeScalarBigMapStore('zcfBaggage', { durable: true }); const donorSeat = await makeSeatWithAssets( zoe, @@ -109,7 +108,7 @@ test('acceptOffer fakeSeat', async t => { want: simoleans(50n), }), zcfSeat, - AuctionState.ACTIVE, + true, ); t.true(book.hasOrders()); @@ -150,7 +149,7 @@ test('getOffers to a price limit', async t => { zoe, zcf, moola(100n), - 'Bid', + 'Currency', moolaKit, ); @@ -163,13 +162,13 @@ test('getOffers to a price limit', async t => { want: simoleans(50n), }), zcfSeat, - AuctionState.ACTIVE, + true, ); t.true(book.hasOrders()); }); -test('getOffers w/discount', async t => { +test('Bad keyword', async t => { const { moolaKit, moola, simoleanKit, simoleans } = setup(); const { zoe, zcf } = await setupZCFTest(); @@ -212,13 +211,70 @@ test('getOffers w/discount', async t => { moolaKit, ); + t.throws( + () => + book.addOffer( + harden({ + offerBidScaling: makeRatioFromAmounts(moola(10n), moola(100n)), + want: simoleans(50n), + }), + zcfSeat, + true, + ), + { message: /give must include "Currency".*/ }, + ); +}); + +test('getOffers w/discount', async t => { + const { moolaKit, moola, simoleanKit, simoleans } = setup(); + + const { zoe, zcf } = await setupZCFTest(); + await zcf.saveIssuer(moolaKit.issuer, 'Moola'); + await zcf.saveIssuer(simoleanKit.issuer, 'Sim'); + + const baggage = makeScalarBigMapStore('zcfBaggage', { durable: true }); + + const donorSeat = await makeSeatWithAssets( + zoe, + zcf, + simoleans(500n), + 'Collateral', + simoleanKit, + ); + + const initialPrice = makeRatioFromAmounts(moola(20n), simoleans(100n)); + const pa = buildManualPriceAuthority(initialPrice); + + const book = await makeAuctionBook( + baggage, + zcf, + moolaKit.brand, + simoleanKit.brand, + pa, + ); + + pa.setPrice(makeRatioFromAmounts(moola(11n), simoleans(10n))); + await eventLoopIteration(); + book.addAssets(AmountMath.make(simoleanKit.brand, 123n), donorSeat); + + book.lockOraclePriceForRound(); + book.setStartingRate(makeRatio(50n, moolaKit.brand, 100n)); + + const zcfSeat = await makeSeatWithAssets( + zoe, + zcf, + moola(100n), + 'Currency', + moolaKit, + ); + book.addOffer( harden({ offerBidScaling: makeRatioFromAmounts(moola(10n), moola(100n)), want: simoleans(50n), }), zcfSeat, - AuctionState.ACTIVE, + true, ); t.true(book.hasOrders()); diff --git a/packages/inter-protocol/test/auction/test-proportionalDist.js b/packages/inter-protocol/test/auction/test-proportionalDist.js new file mode 100644 index 000000000000..c98134a62bb5 --- /dev/null +++ b/packages/inter-protocol/test/auction/test-proportionalDist.js @@ -0,0 +1,168 @@ +import { test as anyTest } from '@agoric/zoe/tools/prepare-test-env-ava.js'; + +import '@agoric/zoe/exported.js'; + +import { makeIssuerKit } from '@agoric/ertp'; +import { makeTracer } from '@agoric/internal'; + +import { setUpZoeForTest, withAmountUtils } from '../supports.js'; +import { distributeProportionalShares } from '../../src/auction/auctioneer.js'; + +/** @type {import('ava').TestFn>>} */ +const test = anyTest; + +const trace = makeTracer('Test AuctContract', false); + +const makeTestContext = async () => { + const { zoe } = await setUpZoeForTest(); + + const currency = withAmountUtils(makeIssuerKit('Currency')); + const collateral = withAmountUtils(makeIssuerKit('Collateral')); + + trace('makeContext'); + return { + zoe: await zoe, + currency, + collateral, + }; +}; + +test.before(async t => { + t.context = await makeTestContext(); +}); + +const checkProportions = ( + t, + amountsReturned, + rawDeposits, + rawExpected, + kwd = 'ATOM', +) => { + const { collateral, currency } = t.context; + + const rawExp = rawExpected[0]; + t.is(rawDeposits.length, rawExp.length); + + const [collateralReturned, currencyReturned] = amountsReturned; + const fakeCollateralSeat = harden({}); + const fakeCurrencySeat = harden({}); + const fakeReserveSeat = harden({}); + + debugger; + const deposits = []; + const expectedXfer = []; + for (let i = 0; i < rawDeposits.length; i += 1) { + const seat = harden({}); + deposits.push({ seat, amount: collateral.make(rawDeposits[i]) }); + const currencyRecord = { Currency: currency.make(rawExp[i][1]) }; + expectedXfer.push([fakeCurrencySeat, seat, currencyRecord]); + const collateralRecord = { Collateral: collateral.make(rawExp[i][0]) }; + expectedXfer.push([fakeCollateralSeat, seat, collateralRecord]); + } + const expectedLeftovers = rawExpected[1]; + const leftoverCurrency = { Currency: currency.make(expectedLeftovers[1]) }; + expectedXfer.push([fakeCurrencySeat, fakeReserveSeat, leftoverCurrency]); + expectedXfer.push([ + fakeCollateralSeat, + fakeReserveSeat, + { Collateral: collateral.make(expectedLeftovers[0]) }, + { [kwd]: collateral.make(expectedLeftovers[0]) }, + ]); + + const transfers = distributeProportionalShares( + collateral.make(collateralReturned), + currency.make(currencyReturned), + // @ts-expect-error mocks for test + deposits, + fakeCollateralSeat, + fakeCurrencySeat, + 'ATOM', + fakeReserveSeat, + collateral.brand, + ); + + t.deepEqual(transfers, expectedXfer); +}; + +test('distributeProportionalShares', t => { + // received 0 Collateral and 20 Currency from the auction to distribute to one + // vaultManager. Expect the one to get 0 and 20, and no leftovers + checkProportions(t, [0n, 20n], [100n], [[[0n, 20n]], [0n, 0n]]); +}); + +test('proportional simple', t => { + // received 100 Collateral and 2000 Currency from the auction to distribute to + // two depositors in a ratio of 6:1. expect leftovers + checkProportions( + t, + [100n, 2000n], + [100n, 600n], + [ + [ + [14n, 285n], + [85n, 1714n], + ], + [1n, 1n], + ], + ); +}); + +test('proportional three way', t => { + // received 100 Collateral and 2000 Currency from the auction to distribute to + // three depositors in a ratio of 1:3:1. expect no leftovers + checkProportions( + t, + [100n, 2000n], + [100n, 300n, 100n], + [ + [ + [20n, 400n], + [60n, 1200n], + [20n, 400n], + ], + [0n, 0n], + ], + ); +}); + +test('proportional odd ratios, no collateral', t => { + // received 0 Collateral and 2001 Currency from the auction to distribute to + // five depositors in a ratio of 20, 36, 17, 83, 42. expect leftovers + // sum = 198 + checkProportions( + t, + [0n, 2001n], + [20n, 36n, 17n, 83n, 42n], + [ + [ + [0n, 202n], + [0n, 363n], + [0n, 171n], + [0n, 838n], + [0n, 424n], + ], + [0n, 3n], + ], + ); +}); + +test('proportional, no currency', t => { + // received 0 Collateral and 2001 Currency from the auction to distribute to + // five depositors in a ratio of 20, 36, 17, 83, 42. expect leftovers + // sum = 198 + checkProportions( + t, + [20n, 0n], + [20n, 36n, 17n, 83n, 42n], + [ + [ + [2n, 0n], + [3n, 0n], + [1n, 0n], + [8n, 0n], + [4n, 0n], + ], + [2n, 0n], + ], + ); +}); diff --git a/packages/inter-protocol/test/smartWallet/test-amm-integration.js b/packages/inter-protocol/test/smartWallet/test-amm-integration.js deleted file mode 100644 index ef25b243a5f9..000000000000 --- a/packages/inter-protocol/test/smartWallet/test-amm-integration.js +++ /dev/null @@ -1,6 +0,0 @@ -import test from 'ava'; - -// defer to after ps0 -test.todo('trade amm'); -// make a smart wallet -// suggestIssuer diff --git a/packages/internal/src/utils.js b/packages/internal/src/utils.js index 3c754e7177a9..d12004455eaa 100644 --- a/packages/internal/src/utils.js +++ b/packages/internal/src/utils.js @@ -10,6 +10,8 @@ const { ownKeys } = Reflect; const { details: X, quote: q, Fail } = assert; +export const BASIS_POINTS = 10_000n; + /** @template T @typedef {import('@endo/eventual-send').ERef} ERef */ /**