Skip to content

Commit

Permalink
fix: multiples, for making divisible offers
Browse files Browse the repository at this point in the history
  • Loading branch information
erights committed Oct 19, 2022
1 parent 65d8e0a commit 48b0552
Show file tree
Hide file tree
Showing 16 changed files with 384 additions and 56 deletions.
7 changes: 6 additions & 1 deletion packages/inter-protocol/src/stakeFactory/stakeFactoryKit.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions packages/wallet/api/test/test-lib-wallet.js
Original file line number Diff line number Diff line change
Expand Up @@ -1356,6 +1356,7 @@ test('lib-wallet can give attestations in offers', async t => {
give: {
Attestation: AmountMath.make(attestationBrand, 30n),
},
multiples: 1n,
want: {},
},
{
Expand All @@ -1365,6 +1366,7 @@ test('lib-wallet can give attestations in offers', async t => {
give: {
Attestation: AmountMath.make(attestationBrand, 30n),
},
multiples: 1n,
want: {},
},
]);
Expand Down Expand Up @@ -1428,6 +1430,7 @@ test('lib-wallet can want attestations in offers', async t => {
want: {
Attestation: AmountMath.make(attestationBrand, 65n),
},
multiples: 1n,
give: {},
},
]);
Expand All @@ -1444,6 +1447,7 @@ test('lib-wallet can want attestations in offers', async t => {
give: {
Attestation: AmountMath.make(attestationBrand, 65n),
},
multiples: 1n,
want: {},
},
]);
Expand Down Expand Up @@ -1551,6 +1555,7 @@ test('addOffer invitationQuery', async t => {
value: 1n,
},
},
multiples: 1n,
exit: {
onDemand: null,
},
Expand Down Expand Up @@ -1672,6 +1677,7 @@ test('addOffer offer.invitation', async t => {
value: 1n,
},
},
multiples: 1n,
exit: {
onDemand: null,
},
Expand Down
34 changes: 34 additions & 0 deletions packages/zoe/src/cleanProposal.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,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;
Expand Down Expand Up @@ -149,6 +151,7 @@ export const cleanProposal = (proposal, getAssetKindByBrand) => {
const {
want = harden({}),
give = harden({}),
multiples = 1n,
exit = harden({ onDemand: null }),
...rest
} = proposal;
Expand All @@ -166,10 +169,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);
78 changes: 52 additions & 26 deletions packages/zoe/src/contractFacet/offerSafety.js
Original file line number Diff line number Diff line change
@@ -1,36 +1,59 @@
// @ts-check

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
Expand All @@ -41,9 +64,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
Expand All @@ -59,9 +85,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.
Expand All @@ -80,13 +109,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 };
27 changes: 21 additions & 6 deletions packages/zoe/src/contractSupport/zoeHelpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`;

Expand Down Expand Up @@ -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} */
Expand Down Expand Up @@ -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. */
Expand Down
1 change: 1 addition & 0 deletions packages/zoe/src/typeGuards.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
7 changes: 4 additions & 3 deletions packages/zoe/src/zoeService/escrowStorage.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,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';

/**
Expand Down Expand Up @@ -78,7 +78,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);
Expand Down Expand Up @@ -116,7 +116,8 @@ export const makeEscrowStorage = baggage => {
paymentKeywords,
)}`,
);
return doDepositPayment(payments[keyword], give[keyword]);
const giveAmount = scaleAmount(give[keyword], multiples);
return doDepositPayment(payments[keyword], giveAmount);
}),
);

Expand Down
9 changes: 8 additions & 1 deletion packages/zoe/src/zoeService/offer/offer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, 'proposal');
}

Expand Down
3 changes: 2 additions & 1 deletion packages/zoe/src/zoeService/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@
* interact with the contract.
* @property {() => Promise<boolean>} hasExited
* Returns true if the seat has exited, false if it is still active.
* @property {() => Promise<0|1>} numWantsSatisfied
* @property {() => Promise<number>} numWantsSatisfied
*
* @property {() => Promise<Allocation>} getCurrentAllocationJig
* Labelled "Jig" because it *should* only be used for tests, though
Expand All @@ -233,6 +233,7 @@
*
* @typedef {{give: AmountKeywordRecord,
* want: AmountKeywordRecord,
* multiples: bigint,
* exit: ExitRule
* }} ProposalRecord
*/
Expand Down
4 changes: 2 additions & 2 deletions packages/zoe/src/zoeService/zoeSeat.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,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';
Expand Down Expand Up @@ -101,7 +101,7 @@ export const makeZoeSeatAdminKit = (

numWantsSatisfied: async () => {
return E.when(payoutPromiseKit.promise, () =>
satisfiesWant(proposal, currentAllocation),
numWantsSatisfied(proposal, currentAllocation),
);
},
});
Expand Down
Loading

0 comments on commit 48b0552

Please sign in to comment.