diff --git a/packages/inter-protocol/src/auction/auctioneer.js b/packages/inter-protocol/src/auction/auctioneer.js index 9eb4d101eb4..7a705398dc3 100644 --- a/packages/inter-protocol/src/auction/auctioneer.js +++ b/packages/inter-protocol/src/auction/auctioneer.js @@ -98,7 +98,7 @@ const distributeProportionalShares = ( const collShare = makeRatioFromAmounts(unsoldCollateral, totalCollDeposited); const currShare = makeRatioFromAmounts(proceeds, totalCollDeposited); - /** @type {import('@agoric/zoe/src/contractSupport/atomicTransfer.js').TransferPart[]} */ + /** @type {TransferPart[]} */ const transfers = []; let proceedsLeft = proceeds; let collateralLeft = unsoldCollateral; @@ -258,7 +258,7 @@ export const distributeProportionalSharesWithLimits = ( // collateral to reach their share. Then see what's left, and allocate it // among the remaining depositors. Escape to distributeProportionalShares if // anything doesn't work. - /** @type {import('@agoric/zoe/src/contractSupport/atomicTransfer.js').TransferPart[]} */ + /** @type {TransferPart[]} */ const transfers = []; let proceedsLeft = proceeds; let collateralLeft = unsoldCollateral; diff --git a/packages/inter-protocol/src/vaultFactory/liquidation.js b/packages/inter-protocol/src/vaultFactory/liquidation.js index f511befd4a1..cf0241696dc 100644 --- a/packages/inter-protocol/src/vaultFactory/liquidation.js +++ b/packages/inter-protocol/src/vaultFactory/liquidation.js @@ -280,7 +280,7 @@ export const getLiquidatableVaults = ( const { zcfSeat: liqSeat } = zcf.makeEmptySeatKit(); let totalDebt = AmountMath.makeEmpty(debtBrand); let totalCollateral = AmountMath.makeEmpty(collateralBrand); - /** @type {import('@agoric/zoe/src/contractSupport/atomicTransfer.js').TransferPart[]} */ + /** @type {TransferPart[]} */ const transfers = []; for (const vault of vaultsToLiquidate.values()) { diff --git a/packages/inter-protocol/src/vaultFactory/types.js b/packages/inter-protocol/src/vaultFactory/types.js index ecde8bf4f24..c2a6a07c53c 100644 --- a/packages/inter-protocol/src/vaultFactory/types.js +++ b/packages/inter-protocol/src/vaultFactory/types.js @@ -70,7 +70,7 @@ * @param {ZCFSeat} mintReceiver * @param {Amount<'nat'>} toMint * @param {Amount<'nat'>} fee - * @param {import('@agoric/zoe/src/contractSupport/atomicTransfer.js').TransferPart[]} transfers + * @param {TransferPart[]} transfers * @returns {void} */ diff --git a/packages/inter-protocol/src/vaultFactory/vault.js b/packages/inter-protocol/src/vaultFactory/vault.js index 9877f4c9f1f..646271ec8ac 100644 --- a/packages/inter-protocol/src/vaultFactory/vault.js +++ b/packages/inter-protocol/src/vaultFactory/vault.js @@ -549,7 +549,7 @@ export const prepareVault = (baggage, makeRecorderKit, zcf) => { const giveMintedTaken = AmountMath.subtract(fp.give.Minted, surplus); - /** @type {import('@agoric/zoe/src/contractSupport/atomicTransfer.js').TransferPart[]} */ + /** @type {TransferPart[]} */ const transfers = harden([ [clientSeat, vaultSeat, { Collateral: fp.give.Collateral }], [vaultSeat, clientSeat, { Collateral: fp.want.Collateral }], diff --git a/packages/inter-protocol/src/vaultFactory/vaultDirector.js b/packages/inter-protocol/src/vaultFactory/vaultDirector.js index 47a872c0475..c60b8b2d544 100644 --- a/packages/inter-protocol/src/vaultFactory/vaultDirector.js +++ b/packages/inter-protocol/src/vaultFactory/vaultDirector.js @@ -198,7 +198,7 @@ const prepareVaultDirector = ( mintAndTransfer: (mintReceiver, toMint, fee, nonMintTransfers) => { const kept = AmountMath.subtract(toMint, fee); debtMint.mintGains(harden({ Minted: toMint }), mintSeat); - /** @type {import('@agoric/zoe/src/contractSupport/atomicTransfer.js').TransferPart[]} */ + /** @type {TransferPart[]} */ const transfers = [ ...nonMintTransfers, [mintSeat, rewardPoolSeat, { Minted: fee }], diff --git a/packages/inter-protocol/src/vaultFactory/vaultManager.js b/packages/inter-protocol/src/vaultFactory/vaultManager.js index 4c84e140bbe..dd9daf9efee 100644 --- a/packages/inter-protocol/src/vaultFactory/vaultManager.js +++ b/packages/inter-protocol/src/vaultFactory/vaultManager.js @@ -699,7 +699,7 @@ export const prepareVaultManagerKit = ( if (plan.transfersToVault.length > 0) { const transfers = plan.transfersToVault.map( ([vaultIndex, amounts]) => - /** @type {import('@agoric/zoe/src/contractSupport/atomicTransfer.js').TransferPart} */ ([ + /** @type {TransferPart} */ ([ liqSeat, vaultsInPlan[vaultIndex].getVaultSeat(), amounts, diff --git a/packages/inter-protocol/test/vaultFactory/vault-contract-wrapper.js b/packages/inter-protocol/test/vaultFactory/vault-contract-wrapper.js index d79d9b5a16b..e403f71029f 100644 --- a/packages/inter-protocol/test/vaultFactory/vault-contract-wrapper.js +++ b/packages/inter-protocol/test/vaultFactory/vault-contract-wrapper.js @@ -1,14 +1,12 @@ /** @file DEPRECATED use the vault test driver instead */ import { AmountMath, makeIssuerKit } from '@agoric/ertp'; -import { assert } from '@agoric/assert'; import { makePublishKit, observeNotifier } from '@agoric/notifier'; import { makeFakeMarshaller, makeFakeStorage, } from '@agoric/notifier/tools/testSupports.js'; import { - atomicRearrange, prepareRecorderKit, unitAmount, } from '@agoric/zoe/src/contractSupport/index.js'; @@ -101,14 +99,14 @@ export async function start(zcf, privateArgs, baggage) { const mintAndTransfer = (mintReceiver, toMint, fee, nonMintTransfers) => { const kept = AmountMath.subtract(toMint, fee); stableMint.mintGains(harden({ Minted: toMint }), mintSeat); - /** @type {import('@agoric/zoe/src/contractSupport/atomicTransfer.js').TransferPart[]} */ + /** @type {TransferPart[]} */ const transfers = [ ...nonMintTransfers, [mintSeat, vaultFactorySeat, { Minted: fee }], [mintSeat, mintReceiver, { Minted: kept }], ]; try { - atomicRearrange(zcf, harden(transfers)); + zcf.atomicRearrange(harden(transfers)); } catch (e) { console.error('mintAndTransfer caught', e); stableMint.burnLosses(harden({ Minted: toMint }), mintSeat); diff --git a/packages/zoe/package.json b/packages/zoe/package.json index 53d426bd435..3b5e125c6b2 100644 --- a/packages/zoe/package.json +++ b/packages/zoe/package.json @@ -104,6 +104,7 @@ "test/unitTests/zcf/test-reallocate-empty.js", "test/unitTests/zcf/test-zoeHelpersWZcf.js", "test/unitTests/zcf/test-reallocateForZCFMint.js", + "test/unitTests/zcf/test-atomicRearrange.js", "test/unitTests/zcf/test-zcf.js", "test/unitTests/zcf/test-allStagedSeatsUsed.js", "# ManualTimer.setWakeup: no function", diff --git a/packages/zoe/src/contractFacet/reallocate.js b/packages/zoe/src/contractFacet/reallocate.js new file mode 100644 index 00000000000..f09b7f031c4 --- /dev/null +++ b/packages/zoe/src/contractFacet/reallocate.js @@ -0,0 +1,94 @@ +import { makeScalarMapStore } from '@agoric/vat-data'; + +import { assertRightsConserved } from './rightsConservation.js'; +import { addToAllocation, subtractFromAllocation } from './allocationMath.js'; + +const { Fail } = assert; + +/** @typedef {Array} TransactionList */ + +/** + * Convert from a list of transfer descriptions ([fromSeat, toSeat, fromAmount, + * toAmount], with many parts optional) to a list of resulting allocations for + * each of the seats mentioned. + * + * @param {Array} transfers + * @returns {[ZCFSeat, AmountKeywordRecord][]} + */ +export const makeAllocationMap = transfers => { + /** @type {MapStore} */ + const allocations = makeScalarMapStore(); + + const getAllocations = seat => { + if (allocations.has(seat)) { + return allocations.get(seat); + } + + /** @type {[TransactionList, TransactionList]} */ + const pair = [[], []]; + allocations.init(seat, pair); + return pair; + }; + + const decrementAllocation = (seat, decrement) => { + const [incr, decr] = getAllocations(seat); + + const newDecr = [...decr, decrement]; + allocations.set(seat, [incr, newDecr]); + }; + + const incrementAllocation = (seat, increment) => { + const [incr, decr] = getAllocations(seat); + + const newIncr = [...incr, increment]; + allocations.set(seat, [newIncr, decr]); + }; + + for (const [fromSeat, toSeat, fromAmounts, toAmounts] of transfers) { + if (fromSeat) { + if (!fromAmounts) { + throw Fail`Transfer from ${fromSeat} must say how much`; + } + decrementAllocation(fromSeat, fromAmounts); + if (toSeat) { + // Conserved transfer between seats + if (toAmounts) { + // distinct amounts, so we check conservation. + assertRightsConserved( + Object.values(fromAmounts), + Object.values(toAmounts), + ); + incrementAllocation(toSeat, toAmounts); + } else { + // fromAmounts will be used for toAmounts as well + incrementAllocation(toSeat, fromAmounts); + } + } else { + // Transfer only from fromSeat + !toAmounts || + Fail`Transfer without toSeat cannot have toAmounts ${toAmounts}`; + } + } else { + toSeat || Fail`Transfer must have at least one of fromSeat or toSeat`; + // Transfer only to toSeat + !fromAmounts || + Fail`Transfer without fromSeat cannot have fromAmounts ${fromAmounts}`; + toAmounts || Fail`Transfer to ${toSeat} must say how much`; + incrementAllocation(toSeat, toAmounts); + } + } + + /** @type {[ZCFSeat,AmountKeywordRecord][]} */ + const resultingAllocations = []; + for (const [seat, [incrList, decrList]] of allocations.entries()) { + let newAlloc = seat.getCurrentAllocation(); + for (const incr of incrList) { + newAlloc = addToAllocation(newAlloc, incr); + } + for (const decr of decrList) { + newAlloc = subtractFromAllocation(newAlloc, decr); + } + resultingAllocations.push([seat, newAlloc]); + } + return resultingAllocations; +}; diff --git a/packages/zoe/src/contractFacet/types.js b/packages/zoe/src/contractFacet/types.js index dd86a4c1a04..f6277e641eb 100644 --- a/packages/zoe/src/contractFacet/types.js +++ b/packages/zoe/src/contractFacet/types.js @@ -26,6 +26,7 @@ * synchronously from within the contract, and usually is referred to * in code as zcf. * + * @property {(transfers: TransferPart[]) => void} atomicRearrange - atomically reallocate amounts among seats. * @property {Reallocate} reallocate - reallocate amounts among seats. * Deprecated: Use atomicRearrange instead. * @property {(keyword: Keyword) => void} assertUniqueKeyword - check @@ -59,6 +60,8 @@ */ /** + * @deprecated reallocate(). Use zcf.atomicRearrange() instead + * * @typedef {(seat1: ZCFSeat, seat2: ZCFSeat, ...seatRest: * Array) => void} Reallocate * @@ -83,6 +86,15 @@ * effect offer safety for seats whose allocations change. */ +/** + * @typedef {[ + * fromSeat?: ZCFSeat, + * toSeat?: ZCFSeat, + * fromAmounts?: AmountKeywordRecord, + * toAmounts?: AmountKeywordRecord + * ]} TransferPart + */ + /** * @callback SaveIssuer * diff --git a/packages/zoe/src/contractFacet/zcfSeat.js b/packages/zoe/src/contractFacet/zcfSeat.js index a7caa360852..006ea2840cb 100644 --- a/packages/zoe/src/contractFacet/zcfSeat.js +++ b/packages/zoe/src/contractFacet/zcfSeat.js @@ -1,10 +1,10 @@ import { makeScalarBigWeakMapStore, - provideDurableMapStore, - provideDurableWeakMapStore, + prepareExoClass, prepareExoClassKit, provide, - prepareExoClass, + provideDurableMapStore, + provideDurableWeakMapStore, } from '@agoric/vat-data'; import { E } from '@endo/eventual-send'; import { AmountMath } from '@agoric/ertp'; @@ -19,6 +19,8 @@ import { SeatDataShape, SeatShape, } from '../typeGuards.js'; +import { makeAllocationMap } from './reallocate.js'; +import { TransferPartShape } from '../contractSupport/atomicTransfer.js'; const { Fail } = assert; @@ -214,6 +216,10 @@ export const createSeatManager = ( return isOfferSafe(state.proposal, reallocation); }, + /** + * @deprecated switch to zcf.atomicRearrange() + * @param {AmountKeywordRecord} amountKeywordRecord + */ incrementBy(amountKeywordRecord) { const { self } = this; assertActive(self); @@ -227,6 +233,10 @@ export const createSeatManager = ( ); return amountKeywordRecord; }, + /** + * @deprecated switch to zcf.atomicRearrange() + * @param {AmountKeywordRecord} amountKeywordRecord + */ decrementBy(amountKeywordRecord) { const { self } = this; assertActive(self); @@ -265,6 +275,7 @@ export const createSeatManager = ( const ZcfSeatManagerIKit = harden({ seatManager: M.interface('ZcfSeatManager', { makeZCFSeat: M.call(SeatDataShape).returns(M.remotable('zcfSeat')), + atomicRearrange: M.call(M.arrayOf(TransferPartShape)).returns(), reallocate: M.call(M.remotable('zcfSeat'), M.remotable('zcfSeat')) .rest(M.arrayOf(M.remotable('zcfSeat'))) .returns(), @@ -289,6 +300,96 @@ export const createSeatManager = ( return zcfSeat; }, + /** + * Rearrange the allocations according to the transfer descriptions. + * This is a set of changes to allocations that must satisfy several + * constraints. If these constraints are all met, then the reallocation + * happens atomically. Otherwise, it does not happen at all. + * + * The conditions + * * All the mentioned seats are still live, + * * No outstanding stagings for any of the mentioned seats. Stagings + * have been deprecated in favor or atomicRearrange. To prevent + * confusion, for each reallocation, it can only be expressed in + * the old way or the new way, but not a mixture. + * * Offer safety + * * Overall conservation + * + * The overall transfer is expressed as an array of `TransferPart`. Each + * individual `TransferPart` is one of + * - A transfer from a `fromSeat` to a `toSeat`. Specify both toAmount + * and fromAmount to change keywords, otherwise only fromAmount is required. + * - A taking from a `fromSeat`'s allocation. See the `fromOnly` helper. + * - A giving into a `toSeat`'s allocation. See the `toOnly` helper. + * + * @param {TransferPart[]} transfers + */ + atomicRearrange(transfers) { + const newAllocations = makeAllocationMap(transfers); + + // ////// All Seats are active ///////////////////////////////// + for (const [seat] of newAllocations) { + assertActive(seat); + !seat.hasStagedAllocation() || + Fail`Cannot mix atomicRearrange with seat stagings: ${seat}`; + zcfSeatToSeatHandle.has(seat) || + Fail`The seat ${seat} was not recognized`; + } + + // ////// Ensure that rights are conserved overall ///////////// + + // convert array of keywordAmountRecords to 1-level array of Amounts + const flattenAmounts = allocations => + allocations.flatMap(Object.values); + const previousAmounts = flattenAmounts( + newAllocations.map(([seat]) => seat.getCurrentAllocation()), + ); + const newAmounts = flattenAmounts( + newAllocations.map(([_, allocation]) => allocation), + ); + assertRightsConserved(previousAmounts, newAmounts); + + // ////// Ensure that offer safety holds /////////////////////// + for (const [seat, allocation] of newAllocations) { + isOfferSafe(seat.getProposal(), allocation) || + Fail`Offer safety was violated by the proposed allocation: ${allocation}. Proposal was ${seat.getProposal()}`; + } + + const seatHandleAllocations = newAllocations.map( + ([seat, allocation]) => { + const seatHandle = zcfSeatToSeatHandle.get(seat); + return { allocation, seatHandle }; + }, + ); + try { + // No side effects above. All conditions checked which could have + // caused us to reject this reallocation. Notice that the current + // allocations are captured in seatHandleAllocations, so there must + // be no awaits between that assignment and here. + // + // COMMIT POINT + // + // The effects must succeed atomically. The call to + // replaceAllocations() will be processed in the order of updates + // from ZCF to Zoe. Its effects must occur immediately in Zoe on + // reception, and must not fail. + // + // Commit the new allocations (currentAllocation is replaced + // for each of the seats) and inform Zoe of the new allocation. + + for (const [seat, allocation] of newAllocations) { + activeZCFSeats.set(seat, allocation); + } + + // we don't wait for the results here. As described in + // docs/zoe-zcf.md, The initial allocation to a seat originates with + // Zoe, but *all subsequent updates come from ZCF to Zoe*. + void E(zoeInstanceAdmin).replaceAllocations(seatHandleAllocations); + } catch (err) { + shutdownWithFailure(err); + throw err; + } + }, reallocate(/** @type {ZCFSeat[]} */ ...seats) { seats.forEach(assertActive); seats.forEach(assertStagedAllocation); diff --git a/packages/zoe/src/contractFacet/zcfZygote.js b/packages/zoe/src/contractFacet/zcfZygote.js index cc09e66f095..8b444299dd2 100644 --- a/packages/zoe/src/contractFacet/zcfZygote.js +++ b/packages/zoe/src/contractFacet/zcfZygote.js @@ -251,6 +251,7 @@ export const makeZCFZygote = async ( // accept raw functions. assert cannot be a valid passable! (It's a function // and has members.) const zcf = Remotable('Alleged: zcf', undefined, { + atomicRearrange: transfers => seatManager.atomicRearrange(transfers), reallocate: (...seats) => seatManager.reallocate(...seats), assertUniqueKeyword: kwd => getInstanceRecHolder().assertUniqueKeyword(kwd), saveIssuer: async (issuerP, keyword) => { diff --git a/packages/zoe/src/contractSupport/atomicTransfer.js b/packages/zoe/src/contractSupport/atomicTransfer.js index fa27fe8adc7..befda09311c 100644 --- a/packages/zoe/src/contractSupport/atomicTransfer.js +++ b/packages/zoe/src/contractSupport/atomicTransfer.js @@ -1,23 +1,11 @@ -import { mustMatch, M } from '@agoric/store'; -import { assertRightsConserved } from '../contractFacet/rightsConservation.js'; +import { M } from '@agoric/store'; import { AmountKeywordRecordShape, SeatShape } from '../typeGuards.js'; -const { Fail, quote: q } = assert; - export const TransferPartShape = M.splitArray( harden([M.opt(SeatShape), M.opt(SeatShape), M.opt(AmountKeywordRecordShape)]), harden([M.opt(AmountKeywordRecordShape)]), ); -/** - * @typedef {[ - * fromSeat?: ZCFSeat, - * toSeat?: ZCFSeat, - * fromAmounts?: AmountKeywordRecord, - * toAmounts?: AmountKeywordRecord - * ]} TransferPart - */ - /** * Asks Zoe (via zcf) to rearrange the allocations among the seats * mentioned. This is a set of changes to allocations that must satisfy @@ -52,86 +40,13 @@ export const TransferPartShape = M.splitArray( * which will remain helpers. These helper are for convenience * in expressing atomic rearrangements clearly. * + * @deprecated use the zcf builtin instead + * * @param {ZCF} zcf * @param {TransferPart[]} transfers */ export const atomicRearrange = (zcf, transfers) => { - mustMatch(transfers, M.arrayOf(M.array()), 'transfers'); - const uniqueSeatSet = new Set(); - for (const [ - fromSeat = undefined, - toSeat = undefined, - fromAmounts = undefined, - toAmounts = undefined, - ] of transfers) { - if (fromSeat) { - if (!fromAmounts) { - throw Fail`Transfer from ${fromSeat} must say how much`; - } - uniqueSeatSet.add(fromSeat); - if (toSeat) { - // Conserved transfer between seats - if (toAmounts) { - // distinct amounts, so we check conservation. - assertRightsConserved( - Object.values(fromAmounts), - Object.values(toAmounts), - ); - } // else fromAmounts will be used as toAmounts - uniqueSeatSet.add(toSeat); - } else { - // Transfer only from fromSeat - !toAmounts || - Fail`Transfer without toSeat cannot have toAmounts ${toAmounts}`; - } - } else { - toSeat || Fail`Transfer must have at least one of fromSeat or toSeat`; - // Transfer only to toSeat - !fromAmounts || - Fail`Transfer without fromSeat cannot have fromAmounts ${fromAmounts}`; - toAmounts || Fail`Transfer to ${toSeat} must say how much`; - uniqueSeatSet.add(toSeat); - } - } - - const uniqueSeats = harden([...uniqueSeatSet.keys()]); - for (const seat of uniqueSeats) { - !seat.hasStagedAllocation() || - Fail`Cannot mix atomicRearrange with seat stagings: ${seat}`; - } - - // At this point the basic shape has been validated - - try { - for (const [ - fromSeat = undefined, - toSeat = undefined, - fromAmounts = undefined, - toAmounts = toSeat && fromAmounts, - ] of transfers) { - if (fromSeat && fromAmounts) { - // testing both just to satisfy the type checker - fromSeat.decrementBy(fromAmounts); - } - if (toSeat && toAmounts) { - // testing both just to satisfy the type checker - toSeat.incrementBy(toAmounts); - } - } - - // Perhaps deprecate this >= 2 restriction? - uniqueSeats.length >= 2 || - Fail`Can only commit a reallocation among at least 2 seats: ${q( - uniqueSeats.length, - )}`; - // Take it apart and put it back together to satisfy the type checker - const [seat0, seat1, ...restSeats] = uniqueSeats; - zcf.reallocate(seat0, seat1, ...restSeats); - } finally { - for (const seat of uniqueSeats) { - seat.clear(); - } - } + zcf.atomicRearrange(transfers); }; /** @@ -179,4 +94,4 @@ export const atomicTransfer = ( toSeat = undefined, fromAmounts = undefined, toAmounts = undefined, -) => atomicRearrange(zcf, harden([[fromSeat, toSeat, fromAmounts, toAmounts]])); +) => zcf.atomicRearrange(harden([[fromSeat, toSeat, fromAmounts, toAmounts]])); diff --git a/packages/zoe/src/contractSupport/zoeHelpers.js b/packages/zoe/src/contractSupport/zoeHelpers.js index 8eee34c1aa5..2287395c33f 100644 --- a/packages/zoe/src/contractSupport/zoeHelpers.js +++ b/packages/zoe/src/contractSupport/zoeHelpers.js @@ -4,12 +4,7 @@ import { makePromiseKit } from '@endo/promise-kit'; import { AssetKind } from '@agoric/ertp'; import { fromUniqueEntries } from '@agoric/internal'; import { satisfiesWant } from '../contractFacet/offerSafety.js'; -import { - atomicRearrange, - atomicTransfer, - fromOnly, - toOnly, -} from './atomicTransfer.js'; +import { atomicTransfer, fromOnly, toOnly } from './atomicTransfer.js'; export const defaultAcceptanceMsg = `The offer has been accepted. Once the contract has been completed, please check your payout`; @@ -57,8 +52,7 @@ export const satisfies = (zcf, seat, update) => { /** @type {Swap} */ export const swap = (zcf, leftSeat, rightSeat) => { try { - atomicRearrange( - zcf, + zcf.atomicRearrange( harden([ [rightSeat, leftSeat, leftSeat.getProposal().want], [leftSeat, rightSeat, rightSeat.getProposal().want], @@ -78,8 +72,7 @@ export const swap = (zcf, leftSeat, rightSeat) => { /** @type {SwapExact} */ export const swapExact = (zcf, leftSeat, rightSeat) => { try { - atomicRearrange( - zcf, + zcf.atomicRearrange( harden([ fromOnly(rightSeat, rightSeat.getProposal().give), fromOnly(leftSeat, leftSeat.getProposal().give), diff --git a/packages/zoe/src/contracts/auction/firstPriceLogic.js b/packages/zoe/src/contracts/auction/firstPriceLogic.js index 294a3837ed4..d44239b37c6 100644 --- a/packages/zoe/src/contracts/auction/firstPriceLogic.js +++ b/packages/zoe/src/contracts/auction/firstPriceLogic.js @@ -1,5 +1,4 @@ import { AmountMath } from '@agoric/ertp'; -import { atomicRearrange } from '../../contractSupport/index.js'; /** * @param {ZCF} zcf @@ -42,8 +41,7 @@ export const calcWinnerAndClose = (zcf, sellSeat, bidSeats) => { } // Everyone else gets a refund so their values remain the same. - atomicRearrange( - zcf, + zcf.atomicRearrange( harden([ [highestBidSeat, sellSeat, { Bid: highestBid }, { Ask: highestBid }], [sellSeat, highestBidSeat, { Asset: assetAmount }], diff --git a/packages/zoe/src/contracts/auction/secondPriceLogic.js b/packages/zoe/src/contracts/auction/secondPriceLogic.js index 29d1faca8fc..7d08cbc1b20 100644 --- a/packages/zoe/src/contracts/auction/secondPriceLogic.js +++ b/packages/zoe/src/contracts/auction/secondPriceLogic.js @@ -1,5 +1,4 @@ import { AmountMath } from '@agoric/ertp'; -import { atomicRearrange } from '../../contractSupport/index.js'; /** * @param {ZCF} zcf @@ -48,8 +47,7 @@ export const calcWinnerAndClose = (zcf, sellSeat, bidSeats) => { } // Everyone else gets a refund so their values remain the same. - atomicRearrange( - zcf, + zcf.atomicRearrange( harden([ [ highestBidSeat, diff --git a/packages/zoe/src/contracts/autoswap.js b/packages/zoe/src/contracts/autoswap.js index 6d717ae153c..1b9f58f40a3 100644 --- a/packages/zoe/src/contracts/autoswap.js +++ b/packages/zoe/src/contracts/autoswap.js @@ -11,7 +11,6 @@ import { assertProposalShape, assertNatAssetKind, calcSecondaryRequired, - atomicRearrange, } from '../contractSupport/index.js'; /** @@ -88,8 +87,7 @@ const start = async zcf => { }; function consummate(tradeAmountIn, tradeAmountOut, swapSeat) { - atomicRearrange( - zcf, + zcf.atomicRearrange( harden([ [ swapSeat, @@ -205,8 +203,7 @@ const start = async zcf => { Central: AmountMath.make(brands.Central, centralIn), Secondary: secondaryAmount, }; - atomicRearrange( - zcf, + zcf.atomicRearrange( harden([ [seat, poolSeat, liquidityDeposited], [poolSeat, seat, { Liquidity: liquidityAmountOut }], @@ -308,8 +305,7 @@ const start = async zcf => { Secondary: newUserSecondaryAmount, }; - atomicRearrange( - zcf, + zcf.atomicRearrange( harden([ [removeLiqSeat, poolSeat, { Liquidity: userAllocation.Liquidity }], [poolSeat, removeLiqSeat, liquidityRemoved], diff --git a/packages/zoe/src/contracts/barterExchange.js b/packages/zoe/src/contracts/barterExchange.js index 2ed71f86de1..5320359e212 100644 --- a/packages/zoe/src/contracts/barterExchange.js +++ b/packages/zoe/src/contracts/barterExchange.js @@ -1,7 +1,7 @@ import { Far } from '@endo/marshal'; import { makeLegacyMap } from '@agoric/store'; // Eventually will be importable from '@agoric/zoe-contract-support' -import { satisfies, atomicRearrange } from '../contractSupport/index.js'; +import { satisfies } from '../contractSupport/index.js'; /** * This Barter Exchange accepts offers to trade arbitrary goods for other @@ -63,8 +63,7 @@ const start = zcf => { const matchingTrade = findMatchingTrade(offerDetails, orders); if (matchingTrade) { // reallocate by giving each side what it wants - atomicRearrange( - zcf, + zcf.atomicRearrange( harden([ [ offerDetails.seat, diff --git a/packages/zoe/src/contracts/callSpread/fundedCallSpread.js b/packages/zoe/src/contracts/callSpread/fundedCallSpread.js index 28d965687b9..c15ba721f34 100644 --- a/packages/zoe/src/contracts/callSpread/fundedCallSpread.js +++ b/packages/zoe/src/contracts/callSpread/fundedCallSpread.js @@ -7,7 +7,6 @@ import { assertProposalShape, depositToSeat, assertNatAssetKind, - atomicRearrange, } from '../../contractSupport/index.js'; import { makePayoffHandler } from './payoffHandler.js'; import { Position } from './position.js'; @@ -112,8 +111,7 @@ const start = async zcf => { give: { Collateral: null }, want: { LongOption: null, ShortOption: null }, }); - atomicRearrange( - zcf, + zcf.atomicRearrange( harden([ [creatorSeat, collateralSeat, { Collateral: settlementAmount }], [ diff --git a/packages/zoe/src/contracts/callSpread/pricedCallSpread.js b/packages/zoe/src/contracts/callSpread/pricedCallSpread.js index 10add08117a..9b06e16fd25 100644 --- a/packages/zoe/src/contracts/callSpread/pricedCallSpread.js +++ b/packages/zoe/src/contracts/callSpread/pricedCallSpread.js @@ -10,7 +10,6 @@ import { assertNatAssetKind, makeRatio, ceilMultiplyBy, - atomicRearrange, } from '../../contractSupport/index.js'; import { makePayoffHandler } from './payoffHandler.js'; import { Position } from './position.js'; @@ -132,8 +131,7 @@ const start = zcf => { 'wanted option not a match', ); - atomicRearrange( - zcf, + zcf.atomicRearrange( harden([ [collateralSeat, depositSeat, spreadAmount], [depositSeat, collateralSeat, { Collateral: newCollateral }], diff --git a/packages/zoe/src/contracts/loan/borrow.js b/packages/zoe/src/contracts/loan/borrow.js index a50285d6c37..f24a039ed26 100644 --- a/packages/zoe/src/contracts/loan/borrow.js +++ b/packages/zoe/src/contracts/loan/borrow.js @@ -9,7 +9,6 @@ import { getAmountOut, ceilMultiplyBy, getTimestamp, - atomicRearrange, } from '../../contractSupport/index.js'; import { scheduleLiquidation } from './scheduleLiquidation.js'; @@ -72,8 +71,7 @@ export const makeBorrowInvitation = (zcf, config) => { const { zcfSeat: collateralSeat } = zcf.makeEmptySeatKit(); - atomicRearrange( - zcf, + zcf.atomicRearrange( harden([ // Transfer the wanted Loan amount to the borrower [lenderSeat, borrowerSeat, { Loan: loanWanted }], diff --git a/packages/zoe/src/contracts/loan/close.js b/packages/zoe/src/contracts/loan/close.js index 2351817758b..2d7060befb8 100644 --- a/packages/zoe/src/contracts/loan/close.js +++ b/packages/zoe/src/contracts/loan/close.js @@ -3,10 +3,7 @@ import './types.js'; import { Fail } from '@agoric/assert'; import { AmountMath } from '@agoric/ertp'; -import { - assertProposalShape, - atomicRearrange, -} from '../../contractSupport/index.js'; +import { assertProposalShape } from '../../contractSupport/index.js'; // The debt, the amount which must be repaid, is just the amount // loaned plus interest (aka stability fee). All debt must be repaid @@ -44,8 +41,7 @@ export const makeCloseLoanInvitation = (zcf, config) => { 'Collateral', collateralBrand, ); - atomicRearrange( - zcf, + zcf.atomicRearrange( harden([ [collateralSeat, repaySeat, { Collateral: collateralAmount }], [repaySeat, lenderSeat, { Loan: debt }], diff --git a/packages/zoe/src/contracts/sellItems.js b/packages/zoe/src/contracts/sellItems.js index 77b43ade259..7ec99d2da9b 100644 --- a/packages/zoe/src/contracts/sellItems.js +++ b/packages/zoe/src/contracts/sellItems.js @@ -11,7 +11,6 @@ import { defaultAcceptanceMsg, assertProposalShape, assertNatAssetKind, - atomicRearrange, } from '../contractSupport/index.js'; const { Fail } = assert; @@ -107,8 +106,7 @@ const start = zcf => { Fail`More money (${totalCost}) is required to buy these items`; // Reallocate. - atomicRearrange( - zcf, + zcf.atomicRearrange( harden([ [buyerSeat, sellerSeat, { Money: providedMoney }], [sellerSeat, buyerSeat, { Items: wantedItems }], diff --git a/packages/zoe/test/unitTests/contracts/loan/test-liquidate.js b/packages/zoe/test/unitTests/contracts/loan/test-liquidate.js index d6f2e79c90d..4a321559beb 100644 --- a/packages/zoe/test/unitTests/contracts/loan/test-liquidate.js +++ b/packages/zoe/test/unitTests/contracts/loan/test-liquidate.js @@ -11,7 +11,6 @@ import { checkNoNewOffers, checkPayouts, } from './helpers.js'; -import { atomicRearrange } from '../../../../src/contractSupport/atomicTransfer.js'; test('test doLiquidation with mocked autoswap', async t => { const { zcf, collateralKit, loanKit } = await setupLoanUnitTest(); @@ -43,8 +42,7 @@ test('test doLiquidation with mocked autoswap', async t => { const swapHandler = swapSeat => { // swapSeat gains 20 loan tokens from fakePoolSeat, loses all collateral - atomicRearrange( - zcf, + zcf.atomicRearrange( harden([ [swapSeat, fakePoolSeat, { In: collateral }, { Secondary: collateral }], [fakePoolSeat, swapSeat, { Central: price }, { Out: price }], diff --git a/packages/zoe/test/unitTests/zcf/test-atomicRearrange.js b/packages/zoe/test/unitTests/zcf/test-atomicRearrange.js new file mode 100644 index 00000000000..80de4f34aba --- /dev/null +++ b/packages/zoe/test/unitTests/zcf/test-atomicRearrange.js @@ -0,0 +1,268 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; + +import { AmountMath } from '@agoric/ertp'; +import { + fromOnly, + toOnly, +} from '../../../src/contractSupport/atomicTransfer.js'; + +import { setupZCFTest } from './setupZcfTest.js'; +import { makeOffer } from '../makeOffer.js'; +import { setup } from '../setupBasicMints.js'; + +const zcfMintUtils = async (zcf, name) => { + const mint = await zcf.makeZCFMint(name); + const { brand } = mint.getIssuerRecord(); + + return { + make: v => AmountMath.make(brand, v), + makeEmpty: () => AmountMath.makeEmpty(brand), + brand: () => brand, + mintGains: (seat, v, t) => + mint.mintGains(harden({ [t]: AmountMath.make(brand, v) }), seat), + }; +}; + +const assertAllocations = (t, seat, expectedAllocations) => { + const allocations = seat.getCurrentAllocation(); + t.deepEqual(allocations, expectedAllocations, 'allocations did not match'); +}; + +test(`zcf.atomicRearrange all Legal Combinations`, async t => { + const { zcf } = await setupZCFTest(); + const moola = await zcfMintUtils(zcf, 'Moola'); + const bucks = await zcfMintUtils(zcf, 'Bucks'); + + const { zcfSeat: alice } = zcf.makeEmptySeatKit(); + const { zcfSeat: bob } = zcf.makeEmptySeatKit(); + const { zcfSeat: carol } = zcf.makeEmptySeatKit(); + + moola.mintGains(alice, 100n, 'M'); + bucks.mintGains(bob, 50n, 'B'); + + zcf.atomicRearrange([ + [alice, bob, { M: moola.make(20n) }], + [bob, carol, { B: bucks.make(30n) }], + [alice, carol, { M: moola.make(15n) }, { C: moola.make(15n) }], + fromOnly(bob, { B: bucks.make(12n) }), + toOnly(alice, { N: bucks.make(12n) }), + ]); + assertAllocations(t, alice, { M: moola.make(65n), N: bucks.make(12n) }); + assertAllocations(t, bob, { B: bucks.make(8n), M: moola.make(20n) }); + assertAllocations(t, carol, { B: bucks.make(30n), C: moola.make(15n) }); +}); + +test(`zcf.atomicRearrange missing to amount`, async t => { + const { zcf } = await setupZCFTest(); + const moola = await zcfMintUtils(zcf, 'Moola'); + const bucks = await zcfMintUtils(zcf, 'Bucks'); + + const { zcfSeat: alice } = zcf.makeEmptySeatKit(); + const { zcfSeat: bob } = zcf.makeEmptySeatKit(); + const { zcfSeat: carol } = zcf.makeEmptySeatKit(); + + moola.mintGains(alice, 100n, 'M'); + bucks.mintGains(bob, 50n, 'B'); + + t.throws(() => zcf.atomicRearrange([[undefined, carol, undefined]]), { + message: /must say how much/, + }); +}); + +test(`zcf.atomicRearrange too few argumemnts`, async t => { + const { zcf } = await setupZCFTest(); + const moola = await zcfMintUtils(zcf, 'Moola'); + const bucks = await zcfMintUtils(zcf, 'Bucks'); + + const { zcfSeat: alice } = zcf.makeEmptySeatKit(); + const { zcfSeat: bob } = zcf.makeEmptySeatKit(); + + moola.mintGains(alice, 100n, 'M'); + bucks.mintGains(bob, 50n, 'B'); + + t.throws( + () => + zcf.atomicRearrange([ + [alice, bob, { M: moola.make(20n) }], + // @ts-expect-error Testing + [bob, { B: bucks.make(30n) }], + ]), + { message: /Expected at least 3 arguments/ }, + ); +}); + +test(`zcf.atomicRearrange no seats`, async t => { + const { zcf } = await setupZCFTest(); + const moola = await zcfMintUtils(zcf, 'Moola'); + const bucks = await zcfMintUtils(zcf, 'Bucks'); + + const { zcfSeat: alice } = zcf.makeEmptySeatKit(); + const { zcfSeat: bob } = zcf.makeEmptySeatKit(); + + moola.mintGains(alice, 100n, 'M'); + bucks.mintGains(bob, 50n, 'B'); + + t.throws( + () => + zcf.atomicRearrange([ + [alice, bob, { M: moola.make(20n) }], + [undefined, undefined, { B: bucks.make(30n) }], + ]), + { message: /Transfer must have at least one of fromSeat or toSeat/ }, + ); +}); + +test(`zcf.atomicRearrange no transfers`, async t => { + const { zcf } = await setupZCFTest(); + const moola = await zcfMintUtils(zcf, 'Moola'); + const bucks = await zcfMintUtils(zcf, 'Bucks'); + + const { zcfSeat: alice } = zcf.makeEmptySeatKit(); + const { zcfSeat: bob } = zcf.makeEmptySeatKit(); + + moola.mintGains(alice, 100n, 'M'); + bucks.mintGains(bob, 50n, 'B'); + + t.throws( + () => + zcf.atomicRearrange([ + [alice, bob, { M: moola.make(20n) }], + // @ts-expect-error Testing + [bob, alice, bob], + ]), + { message: /Must be a copyRecord/ }, + ); + + t.throws( + () => + zcf.atomicRearrange([ + [alice, bob, { M: moola.make(20n) }], + [bob, alice, undefined, undefined], + ]), + { message: / must say how much/ }, + ); +}); + +test(`zcf.atomicRearrange no FromAmount`, async t => { + const { zcf } = await setupZCFTest(); + const moola = await zcfMintUtils(zcf, 'Moola'); + const bucks = await zcfMintUtils(zcf, 'Bucks'); + + const { zcfSeat: alice } = zcf.makeEmptySeatKit(); + const { zcfSeat: bob } = zcf.makeEmptySeatKit(); + + moola.mintGains(alice, 100n, 'M'); + bucks.mintGains(bob, 50n, 'B'); + + t.throws( + () => + zcf.atomicRearrange([ + [alice, bob, { M: moola.make(20n) }], + [alice, bob, undefined, { M: moola.make(20n) }], + ]), + { message: /must say how much/ }, + ); +}); + +test(`zcf.atomicRearrange FromAmount w/o fromSeat`, async t => { + const { zcf } = await setupZCFTest(); + const moola = await zcfMintUtils(zcf, 'Moola'); + const bucks = await zcfMintUtils(zcf, 'Bucks'); + + const { zcfSeat: alice } = zcf.makeEmptySeatKit(); + const { zcfSeat: bob } = zcf.makeEmptySeatKit(); + + moola.mintGains(alice, 100n, 'M'); + bucks.mintGains(bob, 50n, 'B'); + + t.throws( + () => + zcf.atomicRearrange([ + [alice, bob, { M: moola.make(20n) }], + [undefined, bob, { B: bucks.make(37n) }, { M: moola.make(20n) }], + ]), + { message: /Transfer without fromSeat cannot have fromAmounts/ }, + ); +}); + +test(`zcf.atomicRearrange toAmount w/o toSeat`, async t => { + const { zcf } = await setupZCFTest(); + const moola = await zcfMintUtils(zcf, 'Moola'); + const bucks = await zcfMintUtils(zcf, 'Bucks'); + + const { zcfSeat: alice } = zcf.makeEmptySeatKit(); + const { zcfSeat: bob } = zcf.makeEmptySeatKit(); + + moola.mintGains(alice, 100n, 'M'); + bucks.mintGains(bob, 50n, 'B'); + + t.throws( + () => + zcf.atomicRearrange([ + [alice, bob, { M: moola.make(20n) }], + [alice, undefined, { B: bucks.make(37n) }, { M: moola.make(20n) }], + ]), + { message: /Transfer without toSeat cannot have toAmounts/ }, + ); +}); + +test(`zcf.atomicRearrange breaks conservation`, async t => { + const { zcf } = await setupZCFTest(); + const moola = await zcfMintUtils(zcf, 'Moola'); + const bucks = await zcfMintUtils(zcf, 'Bucks'); + + const { zcfSeat: alice } = zcf.makeEmptySeatKit(); + const { zcfSeat: bob } = zcf.makeEmptySeatKit(); + + moola.mintGains(alice, 100n, 'M'); + bucks.mintGains(bob, 50n, 'B'); + + t.throws( + () => + zcf.atomicRearrange([ + [alice, bob, { M: moola.make(20n) }], + toOnly(bob, { C: bucks.make(27n) }), + ]), + { message: /rights were not conserved for brand / }, + ); + t.throws( + () => + zcf.atomicRearrange([ + [alice, bob, { M: moola.make(20n) }], + fromOnly(bob, { B: bucks.make(10n) }), + ]), + { message: /rights were not conserved for brand / }, + ); +}); + +test(`zcf.atomicRearrange breaks offerSafety`, async t => { + const { moolaKit, moola, bucksKit, bucks } = setup(); + const { zoe, zcf } = await setupZCFTest({ + M: moolaKit.issuer, + B: bucksKit.issuer, + }); + + const { zcfSeat: alice } = await makeOffer( + zoe, + zcf, + harden({ give: { M: moola(30n) }, want: { B: bucks(200n) } }), + harden({ M: moolaKit.mint.mintPayment(moola(30n)) }), + ); + + const { zcfSeat: bob } = await makeOffer( + zoe, + zcf, + harden({ give: { B: bucks(20n) } }), + harden({ B: bucksKit.mint.mintPayment(bucks(20n)) }), + ); + + t.throws( + () => + zcf.atomicRearrange([ + [bob, alice, { B: bucks(20n) }], + [alice, bob, { M: moola(30n) }], + ]), + { message: /Offer safety was violated/ }, + ); +});