diff --git a/packages/inter-protocol/src/stakeFactory/stakeFactoryKit.js b/packages/inter-protocol/src/stakeFactory/stakeFactoryKit.js index a0567da2b4a2..44855b1c151a 100644 --- a/packages/inter-protocol/src/stakeFactory/stakeFactoryKit.js +++ b/packages/inter-protocol/src/stakeFactory/stakeFactoryKit.js @@ -275,7 +275,12 @@ const helperBehavior = { const want = proposal.want.Debt || emptyDebt; const giveDebtOnly = matches( proposal, - harden({ give: { [KW.Debt]: M.record() }, want: {}, exit: M.any() }), + harden({ + give: { [KW.Debt]: M.record() }, + want: {}, + multiples: 1n, + exit: M.any(), + }), ); // Calculate the fee, the amount to mint and the resulting debt. We'll diff --git a/packages/wallet/api/test/test-lib-wallet.js b/packages/wallet/api/test/test-lib-wallet.js index 6e57c81ca301..5224194525e1 100644 --- a/packages/wallet/api/test/test-lib-wallet.js +++ b/packages/wallet/api/test/test-lib-wallet.js @@ -1356,6 +1356,7 @@ test('lib-wallet can give attestations in offers', async t => { give: { Attestation: AmountMath.make(attestationBrand, 30n), }, + multiples: 1n, want: {}, }, { @@ -1365,6 +1366,7 @@ test('lib-wallet can give attestations in offers', async t => { give: { Attestation: AmountMath.make(attestationBrand, 30n), }, + multiples: 1n, want: {}, }, ]); @@ -1428,6 +1430,7 @@ test('lib-wallet can want attestations in offers', async t => { want: { Attestation: AmountMath.make(attestationBrand, 65n), }, + multiples: 1n, give: {}, }, ]); @@ -1444,6 +1447,7 @@ test('lib-wallet can want attestations in offers', async t => { give: { Attestation: AmountMath.make(attestationBrand, 65n), }, + multiples: 1n, want: {}, }, ]); @@ -1551,6 +1555,7 @@ test('addOffer invitationQuery', async t => { value: 1n, }, }, + multiples: 1n, exit: { onDemand: null, }, @@ -1672,6 +1677,7 @@ test('addOffer offer.invitation', async t => { value: 1n, }, }, + multiples: 1n, exit: { onDemand: null, }, diff --git a/packages/zoe/src/cleanProposal.js b/packages/zoe/src/cleanProposal.js index 6a8ca885305e..7ce7eea00d6a 100644 --- a/packages/zoe/src/cleanProposal.js +++ b/packages/zoe/src/cleanProposal.js @@ -4,10 +4,12 @@ import { assertRecord } from '@endo/marshal'; import { assertKey, assertPattern, fit, isKey } from '@agoric/store'; import { FullProposalShape } from './typeGuards.js'; import { arrayToObj } from './objArrayConversion.js'; +import { natSafeMath } from './contractSupport/safeMath.js'; import '../exported.js'; import './internal-types.js'; +const { values } = Object; const { ownKeys } = Reflect; export const MAX_KEYWORD_LENGTH = 100; @@ -147,6 +149,7 @@ export const cleanProposal = (proposal, getAssetKindByBrand) => { const { want = harden({}), give = harden({}), + multiples = 1n, exit = harden({ onDemand: null }), ...rest } = proposal; @@ -164,10 +167,41 @@ export const cleanProposal = (proposal, getAssetKindByBrand) => { const cleanedProposal = harden({ want: cleanedWant, give: cleanedGive, + multiples, exit, }); fit(cleanedProposal, FullProposalShape, 'proposal'); + if (multiples > 1n) { + for (const amount of values(cleanedGive)) { + assert.typeof( + amount.value, + 'bigint', + X`multiples > 1 not yet implemented for non-fungibles: ${multiples} * ${amount}`, + ); + } + } assertExit(exit); assertKeywordNotInBoth(cleanedWant, cleanedGive); return cleanedProposal; }; + +/** + * + * @param {Amount} amount + * @param {bigint} multiples + * @returns {Amount} + */ +export const scaleAmount = (amount, multiples) => { + if (multiples === 1n) { + return amount; + } + const { brand, value } = amount; + assert(value >= 1n); + assert.typeof( + value, + 'bigint', + X`multiples > 1 not yet implemented for non-fungibles: ${multiples} * ${amount}`, + ); + return harden({ brand, value: natSafeMath.multiply(value, multiples) }); +}; +harden(scaleAmount); diff --git a/packages/zoe/src/contractFacet/offerSafety.js b/packages/zoe/src/contractFacet/offerSafety.js index 6171b00287d7..d9001778d02c 100644 --- a/packages/zoe/src/contractFacet/offerSafety.js +++ b/packages/zoe/src/contractFacet/offerSafety.js @@ -1,34 +1,57 @@ import { AmountMath } from '@agoric/ertp'; +import { natSafeMath } from '../contractSupport/safeMath.js'; + +const { details: X } = assert; +const { entries } = Object; /** - * Helper to perform satisfiesWant and satisfiesGive. Is - * allocationAmount greater than or equal to requiredAmount for every - * keyword of giveOrWant? + * Helper to perform numWantsSatisfied and satisfiesGive. How many times + * does the `allocation` satisfy the `giveOrWant`? * - * To prepare for multiples, satisfiesWant and satisfiesGive return 0 or 1. - * isOfferSafe will still be boolean. When we have Multiples, satisfiesWant and + * To prepare for multiples, numWantsSatisfied and satisfiesGive return 0 or 1. + * isOfferSafe will still be boolean. When we have Multiples, numWantsSatisfied and * satisfiesGive will tell how many times the offer was matched. * * @param {AmountKeywordRecord} giveOrWant * @param {AmountKeywordRecord} allocation - * @returns {0|1} + * @returns {number} If the giveOrWant is empty, then any allocation satisfies + * it an `Infinity` number of times. */ -const satisfiesInternal = (giveOrWant = {}, allocation) => { - const isGTEByKeyword = ([keyword, requiredAmount]) => { - // If there is no allocation for a keyword, we know the giveOrWant - // is not satisfied without checking further. +const numSatisfied = (giveOrWant = {}, allocation) => { + let multiples = Infinity; + for (const [keyword, requiredAmount] of entries(giveOrWant)) { if (allocation[keyword] === undefined) { return 0; } const allocationAmount = allocation[keyword]; - return AmountMath.isGTE(allocationAmount, requiredAmount) ? 1 : 0; - }; - return Object.entries(giveOrWant).every(isGTEByKeyword) ? 1 : 0; + if (!AmountMath.isGTE(allocationAmount, requiredAmount)) { + return 0; + } + if (typeof requiredAmount.value !== 'bigint') { + multiples = 1; + } else if (requiredAmount.value > 0n) { + assert.typeof(allocationAmount.value, 'bigint'); + const howMany = natSafeMath.floorDivide( + allocationAmount.value, + requiredAmount.value, + ); + if (multiples > howMany) { + assert( + howMany <= Number.MAX_SAFE_INTEGER, + X`numSatisfied ${howMany} out of safe integer range`, + ); + multiples = Number(howMany); + } + } + } + return multiples; }; /** * For this allocation to satisfy what the user wanted, their * allocated amounts must be greater than or equal to proposal.want. + * Even if multiples > 1n, this succeeds if it satisfies just one + * unit of want. * * @param {ProposalRecord} proposal - the rules that accompanied the * escrow of payments that dictate what the user expected to get back @@ -39,9 +62,12 @@ const satisfiesInternal = (giveOrWant = {}, allocation) => { * @param {AmountKeywordRecord} allocation - a record with keywords * as keys and amounts as values. These amounts are the reallocation * to be given to a user. + * @returns {number} If the want is empty, then any allocation satisfies + * it an `Infinity` number of times. */ -const satisfiesWant = (proposal, allocation) => - satisfiesInternal(proposal.want, allocation); +export const numWantsSatisfied = (proposal, allocation) => + numSatisfied(proposal.want, allocation); +harden(numWantsSatisfied); /** * For this allocation to count as a full refund, the allocated @@ -57,9 +83,12 @@ const satisfiesWant = (proposal, allocation) => * @param {AmountKeywordRecord} allocation - a record with keywords * as keys and amounts as values. These amounts are the reallocation * to be given to a user. + * @returns {number} If the give is empty, then any allocation satisfies + * it an `Infinity` number of times. */ -const satisfiesGive = (proposal, allocation) => - satisfiesInternal(proposal.give, allocation); +// Commented out because not currently used +// const satisfiesGive = (proposal, allocation) => +// satisfiesInternal(proposal.give, allocation); /** * `isOfferSafe` checks offer safety for a single offer. @@ -78,13 +107,10 @@ const satisfiesGive = (proposal, allocation) => * as keys and amounts as values. These amounts are the reallocation * to be given to a user. */ -function isOfferSafe(proposal, allocation) { - return ( - satisfiesGive(proposal, allocation) > 0 || - satisfiesWant(proposal, allocation) > 0 - ); -} - +export const isOfferSafe = (proposal, allocation) => { + const { give, want, multiples } = proposal; + const howMany = + numSatisfied(give, allocation) + numSatisfied(want, allocation); + return howMany >= multiples; +}; harden(isOfferSafe); -harden(satisfiesWant); -export { isOfferSafe, satisfiesWant }; diff --git a/packages/zoe/src/contractSupport/zoeHelpers.js b/packages/zoe/src/contractSupport/zoeHelpers.js index 4edbaf0e8379..2ff5303827fd 100644 --- a/packages/zoe/src/contractSupport/zoeHelpers.js +++ b/packages/zoe/src/contractSupport/zoeHelpers.js @@ -6,7 +6,7 @@ import { E } from '@endo/eventual-send'; import { makePromiseKit } from '@endo/promise-kit'; import { AssetKind } from '@agoric/ertp'; import { fromUniqueEntries } from '@agoric/internal'; -import { satisfiesWant } from '../contractFacet/offerSafety.js'; +import { numWantsSatisfied } from '../contractFacet/offerSafety.js'; export const defaultAcceptanceMsg = `The offer has been accepted. Once the contract has been completed, please check your payout`; @@ -35,20 +35,25 @@ export const assertIssuerKeywords = (zcf, expected) => { * check; whether the allocation constitutes a refund is not * checked. The update is merged with currentAllocation * (update's values prevailing if the keywords are the same) - * to produce the newAllocation. The return value is 0 for - * false and 1 for true. When multiples are introduced, any - * positive return value will mean true. + * to produce the newAllocation. The return value indicates the + * number of times the want was satisfied. + * + * There are some calls to `satisfies` dating from when it returned a + * boolean rather than a number. Manual inspection verifies that these + * are only sensitive to whether the result is truthy or falsy. + * Since `0` is falsy and any positive number (including `Infinity`) + * is truthy, all these callers still operate correctly. * * @param {ZCF} zcf * @param {ZcfSeatPartial} seat * @param {AmountKeywordRecord} update - * @returns {0|1} + * @returns {number} */ export const satisfies = (zcf, seat, update) => { const currentAllocation = seat.getCurrentAllocation(); const newAllocation = { ...currentAllocation, ...update }; const proposal = seat.getProposal(); - return satisfiesWant(proposal, newAllocation); + return numWantsSatisfied(proposal, newAllocation); }; /** @type {Swap} */ @@ -162,6 +167,16 @@ export const assertProposalShape = (seat, expected) => { assertKeys(actual.give, expected.give); assertKeys(actual.want, expected.want); assertKeys(actual.exit, expected.exit); + if ('multiples' in expected) { + // Not sure what to do with the value of expected.multiples. Probably + // nothing until we convert all this to use proper patterns + } else { + // multiples other than 1n need to be opted into + assert( + actual.multiples === 1n, + X`Only 1n multiples expected: ${actual.multiples}`, + ); + } }; /* Given a brand, assert that brand is AssetKind.NAT. */ diff --git a/packages/zoe/src/typeGuards.js b/packages/zoe/src/typeGuards.js index 8fa14f7a9d4a..d9a4d5a9e978 100644 --- a/packages/zoe/src/typeGuards.js +++ b/packages/zoe/src/typeGuards.js @@ -27,6 +27,7 @@ export const TimerShape = makeHandleShape('timer'); export const FullProposalShape = harden({ want: AmountPatternKeywordRecordShape, give: AmountKeywordRecordShape, + multiples: M.gte(1n), // To accept only one, we could use M.or rather than M.partial, // but the error messages would have been worse. Rather, // cleanProposal's assertExit checks that there's exactly one. diff --git a/packages/zoe/src/zoeService/escrowStorage.js b/packages/zoe/src/zoeService/escrowStorage.js index 76cfd0db653d..34b4876e6166 100644 --- a/packages/zoe/src/zoeService/escrowStorage.js +++ b/packages/zoe/src/zoeService/escrowStorage.js @@ -7,7 +7,7 @@ import { provideDurableWeakMapStore } from '@agoric/vat-data'; import './types.js'; import './internal-types.js'; -import { cleanKeywords } from '../cleanProposal.js'; +import { cleanKeywords, scaleAmount } from '../cleanProposal.js'; import { arrayToObj } from '../objArrayConversion.js'; /** @@ -76,7 +76,7 @@ export const makeEscrowStorage = baggage => { /** @type {DepositPayments} */ const depositPayments = async (proposal, payments) => { - const { give, want } = proposal; + const { give, want, multiples } = proposal; const giveKeywords = Object.keys(give); const wantKeywords = Object.keys(want); const paymentKeywords = cleanKeywords(payments); @@ -114,7 +114,8 @@ export const makeEscrowStorage = baggage => { paymentKeywords, )}`, ); - return doDepositPayment(payments[keyword], give[keyword]); + const giveAmount = scaleAmount(give[keyword], multiples); + return doDepositPayment(payments[keyword], giveAmount); }), ); diff --git a/packages/zoe/src/zoeService/offer/offer.js b/packages/zoe/src/zoeService/offer/offer.js index 4776e5bfdfa6..5f65583f6256 100644 --- a/packages/zoe/src/zoeService/offer/offer.js +++ b/packages/zoe/src/zoeService/offer/offer.js @@ -54,7 +54,14 @@ export const makeOfferMethod = ( const proposal = cleanProposal(uncleanProposal, getAssetKindByBrand); const proposalShape = getProposalShapeForInvitation(invitationHandle); - if (proposalShape !== undefined) { + if (proposalShape === undefined) { + // For the contract to opt into accepting a multiples value other than + // `1n`, it must provide `makeInvitation` with a proposalShape. + assert( + proposal.multiples === 1n, + X`Contract not willing to accept multiples for this invitation: ${proposal}`, + ); + } else { fit(proposal, proposalShape, `${q(description)} proposal`); } diff --git a/packages/zoe/src/zoeService/types.js b/packages/zoe/src/zoeService/types.js index d53829adae4c..8e9f86d36d49 100644 --- a/packages/zoe/src/zoeService/types.js +++ b/packages/zoe/src/zoeService/types.js @@ -214,7 +214,7 @@ * interact with the contract. * @property {() => Promise} hasExited * Returns true if the seat has exited, false if it is still active. - * @property {() => Promise<0|1>} numWantsSatisfied + * @property {() => Promise} numWantsSatisfied * * @property {() => Promise} getCurrentAllocationJig * Labelled "Jig" because it *should* only be used for tests, though @@ -233,6 +233,7 @@ * * @typedef {{give: AmountKeywordRecord, * want: AmountKeywordRecord, + * multiples: bigint, * exit: ExitRule * }} ProposalRecord */ diff --git a/packages/zoe/src/zoeService/zoeSeat.js b/packages/zoe/src/zoeService/zoeSeat.js index c81ce80c0a8c..ab45bdc63a8a 100644 --- a/packages/zoe/src/zoeService/zoeSeat.js +++ b/packages/zoe/src/zoeService/zoeSeat.js @@ -4,7 +4,7 @@ import { E } from '@endo/eventual-send'; import { Far } from '@endo/marshal'; import { handlePKitWarning } from '../handleWarning.js'; -import { satisfiesWant } from '../contractFacet/offerSafety.js'; +import { numWantsSatisfied } from '../contractFacet/offerSafety.js'; import '../types.js'; import '../internal-types.js'; @@ -99,7 +99,7 @@ export const makeZoeSeatAdminKit = ( numWantsSatisfied: async () => { return E.when(payoutPromiseKit.promise, () => - satisfiesWant(proposal, currentAllocation), + numWantsSatisfied(proposal, currentAllocation), ); }, }); diff --git a/packages/zoe/test/unitTests/contractSupport/test-offerTo.js b/packages/zoe/test/unitTests/contractSupport/test-offerTo.js index 291f0df81d16..d816e7a1b735 100644 --- a/packages/zoe/test/unitTests/contractSupport/test-offerTo.js +++ b/packages/zoe/test/unitTests/contractSupport/test-offerTo.js @@ -106,7 +106,7 @@ test(`offerTo - basic usage`, async t => { want: { TokenL: moolaIssuer.getBrand().getAmountShape(), }, - // multiples: 1n, + multiples: 1n, exit: { onDemand: null, }, @@ -222,7 +222,7 @@ test(`offerTo - violates offer safety of fromSeat`, async t => { ), { message: - /Offer safety was violated by the proposed allocation: {"Token[JK]":{"brand":"\[Alleged: .* brand]","value":"\[0n]"},"Token[KJ]":{"brand":"\[Alleged: .* brand]","value":"\[0n]"}}. Proposal was/, + 'Offer safety was violated by the proposed allocation: {"TokenJ":{"brand":"[Alleged: moola brand]","value":"[0n]"},"TokenK":{"brand":"[Alleged: bucks brand]","value":"[0n]"}}. Proposal was {"exit":{"onDemand":null},"give":{"TokenK":{"brand":"[Alleged: bucks brand]","value":"[5n]"}},"multiples":"[1n]","want":{"TokenJ":{"brand":"[Alleged: moola brand]","value":"[3n]"}}}', }, ); diff --git a/packages/zoe/test/unitTests/contracts/test-priceAggregator.js b/packages/zoe/test/unitTests/contracts/test-priceAggregator.js index aa23d894fd34..1063ef4cdac9 100644 --- a/packages/zoe/test/unitTests/contracts/test-priceAggregator.js +++ b/packages/zoe/test/unitTests/contracts/test-priceAggregator.js @@ -559,7 +559,7 @@ test('oracle continuing invitation', async t => { const invPrice = await E(invitationMakers).makePushPriceInvitation('1234'); const invPriceResult = await E(zoe).offer(invPrice); - t.deepEqual(await E(invPriceResult).numWantsSatisfied(), 1); + t.deepEqual(await E(invPriceResult).numWantsSatisfied(), Infinity); await E(oracleTimer).tick(); await E(oracleTimer).tick(); diff --git a/packages/zoe/test/unitTests/test-cleanProposal.js b/packages/zoe/test/unitTests/test-cleanProposal.js index 9579034c7710..461e165662b6 100644 --- a/packages/zoe/test/unitTests/test-cleanProposal.js +++ b/packages/zoe/test/unitTests/test-cleanProposal.js @@ -30,6 +30,7 @@ test('cleanProposal test', t => { { give: { Asset: simoleans(1n) }, want: { Price: moola(3n) }, + multiples: 1n, exit: { onDemand: null }, }, ); @@ -39,6 +40,7 @@ test('cleanProposal - all empty', t => { proposeGood(t, {}, 'nat', { give: harden({}), want: harden({}), + multiples: 1n, exit: { onDemand: null }, }); @@ -53,6 +55,7 @@ test('cleanProposal - all empty', t => { { give: harden({}), want: harden({}), + multiples: 1n, exit: { waived: null }, }, ); @@ -73,6 +76,7 @@ test('cleanProposal - repeated brands', t => { { want: { Asset2: simoleans(1n) }, give: { Price2: moola(3n) }, + multiples: 1n, exit: { afterDeadline: { timer, deadline: 100n } }, }, ); @@ -109,6 +113,7 @@ test('cleanProposal - want patterns', t => { { want: { Asset2: M.any() }, give: { Price2: moola(3n) }, + multiples: 1n, exit: { afterDeadline: { timer, deadline: 100n } }, }, ); @@ -188,9 +193,10 @@ test('cleanProposal - other wrong stuff', t => { /keyword "Not Ident" must be an ascii identifier starting with upper case./, ); proposeGood(t, { give: { ['A'.repeat(100)]: simoleans(1n) } }, 'nat', { - exit: { onDemand: null }, - give: { ['A'.repeat(100)]: simoleans(1n) }, want: {}, + give: { ['A'.repeat(100)]: simoleans(1n) }, + multiples: 1n, + exit: { onDemand: null }, }); proposeBad( t, diff --git a/packages/zoe/test/unitTests/test-offerSafety-multiples.js b/packages/zoe/test/unitTests/test-offerSafety-multiples.js new file mode 100644 index 000000000000..ada1f9d7abb4 --- /dev/null +++ b/packages/zoe/test/unitTests/test-offerSafety-multiples.js @@ -0,0 +1,193 @@ +// @ts-check + +// eslint-disable-next-line import/no-extraneous-dependencies +import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; + +import { isOfferSafe } from '../../src/contractFacet/offerSafety.js'; +import { setup } from './setupBasicMints.js'; + +// Potential outcomes: +// 1. Users can get what they wanted, get back what they gave, both, or +// neither +// 2. Users can either get more than, less than, or equal to what they +// wanted or gave + +// possible combinations to test: +// more than want, more than give -> isOfferSafe() = true +// more than want, less than give -> true +// more than want, equal to give -> true +// less than want, more than give -> true +// less than want, less than give -> false +// less than want, equal to give -> true +// equal to want, more than give -> true +// equal to want, less than give -> true +// equal to want, equal to give -> true + +// more than want, more than give -> isOfferSafe() = true +test('isOfferSafe - more than want, more than give', t => { + const { moola, simoleans, bucks } = setup(); + const proposal = harden({ + give: { A: moola(8n) }, + want: { B: simoleans(6n), C: bucks(7n) }, + multiples: 5n, + exit: { waived: null }, + }); + t.truthy( + isOfferSafe( + proposal, + harden({ A: moola(50n), B: simoleans(35n), C: bucks(40n) }), + ), + ); +}); + +// more than want, less than give -> true +test('isOfferSafe - more than want, less than give', t => { + const { moola, simoleans, bucks } = setup(); + const proposal = harden({ + give: { A: moola(8n) }, + want: { B: simoleans(6n), C: bucks(7n) }, + multiples: 5n, + exit: { waived: null }, + }); + t.truthy( + isOfferSafe( + proposal, + harden({ A: moola(1n), B: simoleans(35n), C: bucks(40n) }), + ), + ); +}); + +// more than want, equal to give -> true +test('isOfferSafe - more than want, equal to give', t => { + const { moola, simoleans, bucks } = setup(); + const proposal = harden({ + want: { A: moola(8n) }, + give: { B: simoleans(6n), C: bucks(7n) }, + multiples: 5n, + exit: { waived: null }, + }); + t.truthy( + isOfferSafe( + proposal, + harden({ A: moola(45n), B: simoleans(30n), C: bucks(35n) }), + ), + ); +}); + +// less than want, more than give -> true +test('isOfferSafe - less than want, more than give', t => { + const { moola, simoleans, bucks } = setup(); + const proposal = harden({ + want: { A: moola(8n) }, + give: { B: simoleans(6n), C: bucks(7n) }, + multiples: 5n, + exit: { waived: null }, + }); + t.truthy( + isOfferSafe( + proposal, + harden({ A: moola(7n), B: simoleans(45n), C: bucks(95n) }), + ), + ); +}); + +// less than want, less than give -> false +test('isOfferSafe - less than want, less than give', t => { + const { moola, simoleans, bucks } = setup(); + const proposal = harden({ + want: { A: moola(8n) }, + give: { B: simoleans(6n), C: bucks(7n) }, + multiples: 5n, + exit: { waived: null }, + }); + t.falsy( + isOfferSafe( + proposal, + harden({ A: moola(7n), B: simoleans(5n), C: bucks(6n) }), + ), + ); +}); + +// less than want, equal to give -> true +test('isOfferSafe - less than want, equal to give', t => { + const { moola, simoleans, bucks } = setup(); + const proposal = harden({ + want: { B: simoleans(6n) }, + give: { A: moola(1n), C: bucks(7n) }, + multiples: 5n, + exit: { waived: null }, + }); + t.truthy( + isOfferSafe( + proposal, + harden({ A: moola(5n), B: simoleans(5n), C: bucks(35n) }), + ), + ); +}); + +// equal to want, more than give -> true +test('isOfferSafe - equal to want, more than give', t => { + const { moola, simoleans, bucks } = setup(); + const proposal = harden({ + want: { B: simoleans(6n) }, + give: { A: moola(1n), C: bucks(7n) }, + multiples: 5n, + exit: { waived: null }, + }); + t.truthy( + isOfferSafe( + proposal, + harden({ A: moola(10n), B: simoleans(30n), C: bucks(40n) }), + ), + ); +}); + +// equal to want, less than give -> true +test('isOfferSafe - equal to want, less than give', t => { + const { moola, simoleans, bucks } = setup(); + const proposal = harden({ + want: { B: simoleans(6n) }, + give: { A: moola(1n), C: bucks(7n) }, + multiples: 5n, + exit: { waived: null }, + }); + t.truthy( + isOfferSafe( + proposal, + harden({ A: moola(0n), B: simoleans(30n), C: bucks(0n) }), + ), + ); +}); + +// equal to want, equal to give -> true +test('isOfferSafe - equal to want, equal to give', t => { + const { moola, simoleans, bucks } = setup(); + const proposal = harden({ + want: { B: simoleans(6n) }, + give: { A: moola(1n), C: bucks(7n) }, + multiples: 5n, + exit: { waived: null }, + }); + t.truthy( + isOfferSafe( + proposal, + harden({ A: moola(5n), B: simoleans(30n), C: bucks(35n) }), + ), + ); +}); + +test('isOfferSafe - empty proposal', t => { + const { moola, simoleans, bucks } = setup(); + const proposal = harden({ + give: {}, + want: {}, + multiples: 5n, + exit: { waived: null }, + }); + t.truthy( + isOfferSafe( + proposal, + harden({ A: moola(1n), B: simoleans(6n), C: bucks(7n) }), + ), + ); +}); diff --git a/packages/zoe/test/unitTests/test-offerSafety.js b/packages/zoe/test/unitTests/test-offerSafety.js index 26c085417e66..918d1d0c2b5f 100644 --- a/packages/zoe/test/unitTests/test-offerSafety.js +++ b/packages/zoe/test/unitTests/test-offerSafety.js @@ -3,7 +3,7 @@ import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; import { isOfferSafe, - satisfiesWant, + numWantsSatisfied, } from '../../src/contractFacet/offerSafety.js'; import { setup } from './setupBasicMints.js'; @@ -30,12 +30,27 @@ test('isOfferSafe - more than want, more than give', t => { const proposal = harden({ give: { A: moola(8n) }, want: { B: simoleans(6n), C: bucks(7n) }, + multiples: 1n, exit: { waived: null }, }); const amounts = harden({ A: moola(10n), B: simoleans(7n), C: bucks(8n) }); t.truthy(isOfferSafe(proposal, amounts)); - t.is(satisfiesWant(proposal, amounts), 1); + t.is(numWantsSatisfied(proposal, amounts), 1); +}); + +test('isOfferSafe - much more than want, much more than give', t => { + const { moola, simoleans, bucks } = setup(); + const proposal = harden({ + give: { A: moola(8n) }, + want: { B: simoleans(6n), C: bucks(7n) }, + multiples: 1n, + exit: { waived: null }, + }); + const amounts = harden({ A: moola(100n), B: simoleans(70n), C: bucks(80n) }); + + t.truthy(isOfferSafe(proposal, amounts)); + t.is(numWantsSatisfied(proposal, amounts), 11); }); // more than want, less than give -> true @@ -44,12 +59,13 @@ test('isOfferSafe - more than want, less than give', t => { const proposal = harden({ give: { A: moola(8n) }, want: { B: simoleans(6n), C: bucks(7n) }, + multiples: 1n, exit: { waived: null }, }); const amounts = harden({ A: moola(1n), B: simoleans(7n), C: bucks(8n) }); t.truthy(isOfferSafe(proposal, amounts)); - t.is(satisfiesWant(proposal, amounts), 1); + t.is(numWantsSatisfied(proposal, amounts), 1); }); // more than want, equal to give -> true @@ -58,12 +74,13 @@ test('isOfferSafe - more than want, equal to give', t => { const proposal = harden({ want: { A: moola(8n) }, give: { B: simoleans(6n), C: bucks(7n) }, + multiples: 1n, exit: { waived: null }, }); const amounts = harden({ A: moola(9n), B: simoleans(6n), C: bucks(7n) }); t.truthy(isOfferSafe(proposal, amounts)); - t.is(satisfiesWant(proposal, amounts), 1); + t.is(numWantsSatisfied(proposal, amounts), 1); }); // less than want, more than give -> true @@ -72,12 +89,13 @@ test('isOfferSafe - less than want, more than give', t => { const proposal = harden({ want: { A: moola(8n) }, give: { B: simoleans(6n), C: bucks(7n) }, + multiples: 1n, exit: { waived: null }, }); const amounts = harden({ A: moola(7n), B: simoleans(9n), C: bucks(19n) }); t.truthy(isOfferSafe(proposal, amounts)); - t.is(satisfiesWant(proposal, amounts), 0); + t.is(numWantsSatisfied(proposal, amounts), 0); }); // less than want, less than give -> false @@ -86,12 +104,13 @@ test('isOfferSafe - less than want, less than give', t => { const proposal = harden({ want: { A: moola(8n) }, give: { B: simoleans(6n), C: bucks(7n) }, + multiples: 1n, exit: { waived: null }, }); const amounts = harden({ A: moola(7n), B: simoleans(5n), C: bucks(6n) }); t.falsy(isOfferSafe(proposal, amounts)); - t.is(satisfiesWant(proposal, amounts), 0); + t.is(numWantsSatisfied(proposal, amounts), 0); }); // less than want, equal to give -> true @@ -100,12 +119,13 @@ test('isOfferSafe - less than want, equal to give', t => { const proposal = harden({ want: { B: simoleans(6n) }, give: { A: moola(1n), C: bucks(7n) }, + multiples: 1n, exit: { waived: null }, }); const amounts = harden({ A: moola(1n), B: simoleans(5n), C: bucks(7n) }); t.truthy(isOfferSafe(proposal, amounts)); - t.is(satisfiesWant(proposal, amounts), 0); + t.is(numWantsSatisfied(proposal, amounts), 0); }); // equal to want, more than give -> true @@ -114,12 +134,13 @@ test('isOfferSafe - equal to want, more than give', t => { const proposal = harden({ want: { B: simoleans(6n) }, give: { A: moola(1n), C: bucks(7n) }, + multiples: 1n, exit: { waived: null }, }); const amounts = harden({ A: moola(2n), B: simoleans(6n), C: bucks(8n) }); t.truthy(isOfferSafe(proposal, amounts)); - t.is(satisfiesWant(proposal, amounts), 1); + t.is(numWantsSatisfied(proposal, amounts), 1); }); // equal to want, less than give -> true @@ -128,12 +149,13 @@ test('isOfferSafe - equal to want, less than give', t => { const proposal = harden({ want: { B: simoleans(6n) }, give: { A: moola(1n), C: bucks(7n) }, + multiples: 1n, exit: { waived: null }, }); const amounts = harden({ A: moola(0n), B: simoleans(6n), C: bucks(0n) }); t.truthy(isOfferSafe(proposal, amounts)); - t.is(satisfiesWant(proposal, amounts), 1); + t.is(numWantsSatisfied(proposal, amounts), 1); }); // equal to want, equal to give -> true @@ -142,19 +164,25 @@ test('isOfferSafe - equal to want, equal to give', t => { const proposal = harden({ want: { B: simoleans(6n) }, give: { A: moola(1n), C: bucks(7n) }, + multiples: 1n, exit: { waived: null }, }); const amounts = harden({ A: moola(1n), B: simoleans(6n), C: bucks(7n) }); t.truthy(isOfferSafe(proposal, amounts)); - t.is(satisfiesWant(proposal, amounts), 1); + t.is(numWantsSatisfied(proposal, amounts), 1); }); test('isOfferSafe - empty proposal', t => { const { moola, simoleans, bucks } = setup(); - const proposal = harden({ give: {}, want: {}, exit: { waived: null } }); + const proposal = harden({ + give: {}, + want: {}, + multiples: 1n, + exit: { waived: null }, + }); const amounts = harden({ A: moola(1n), B: simoleans(6n), C: bucks(7n) }); t.truthy(isOfferSafe(proposal, amounts)); - t.is(satisfiesWant(proposal, amounts), 1); + t.is(numWantsSatisfied(proposal, amounts), Infinity); }); diff --git a/packages/zoe/test/unitTests/zcf/test-zcf.js b/packages/zoe/test/unitTests/zcf/test-zcf.js index 97f1aa59dfdd..9fc75393a2df 100644 --- a/packages/zoe/test/unitTests/zcf/test-zcf.js +++ b/packages/zoe/test/unitTests/zcf/test-zcf.js @@ -727,6 +727,7 @@ test(`zcfSeat.getProposal from zcf.makeEmptySeatKit`, async t => { }, give: {}, want: {}, + multiples: 1n, }); }); @@ -958,6 +959,7 @@ test(`userSeat.getProposal from zcf.makeEmptySeatKit`, async t => { }, give: {}, want: {}, + multiples: 1n, }); }); diff --git a/packages/zoe/test/unitTests/zoe/test-escrowStorage.js b/packages/zoe/test/unitTests/zoe/test-escrowStorage.js index 84442c8cc511..188ca3c83991 100644 --- a/packages/zoe/test/unitTests/zoe/test-escrowStorage.js +++ b/packages/zoe/test/unitTests/zoe/test-escrowStorage.js @@ -52,6 +52,7 @@ test('makeEscrowStorage', async t => { GameTicket: gameTicketAmount, Money: currencyAmount, }, + multiples: 1n, exit: { onDemand: null, }, @@ -156,6 +157,7 @@ test('payments without matching give keywords', async t => { GameTicket: gameTicketAmount, Money: currencyAmount, }, + multiples: 1n, exit: { onDemand: null, }, @@ -191,6 +193,7 @@ test(`give keywords without matching payments`, async t => { GameTicket: gameTicketAmount, Money: currencyAmount, }, + multiples: 1n, exit: { onDemand: null, },