diff --git a/packages/inter-protocol/src/auction/auctioneer.js b/packages/inter-protocol/src/auction/auctioneer.js index 8469ee521071..61df583c5d0d 100644 --- a/packages/inter-protocol/src/auction/auctioneer.js +++ b/packages/inter-protocol/src/auction/auctioneer.js @@ -97,7 +97,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; @@ -255,7 +255,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 b2efde016c07..e3180fecb1c9 100644 --- a/packages/inter-protocol/src/vaultFactory/liquidation.js +++ b/packages/inter-protocol/src/vaultFactory/liquidation.js @@ -267,7 +267,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 f7f9ae5e4080..4674a1ab52d4 100644 --- a/packages/inter-protocol/src/vaultFactory/types.js +++ b/packages/inter-protocol/src/vaultFactory/types.js @@ -58,7 +58,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 7b0ae4b4fb24..5e161b788af9 100644 --- a/packages/inter-protocol/src/vaultFactory/vault.js +++ b/packages/inter-protocol/src/vaultFactory/vault.js @@ -543,7 +543,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 86aff7c1b113..97d44c5781a1 100644 --- a/packages/inter-protocol/src/vaultFactory/vaultDirector.js +++ b/packages/inter-protocol/src/vaultFactory/vaultDirector.js @@ -196,7 +196,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 6c41b45ac762..0a5d4fe29739 100644 --- a/packages/inter-protocol/src/vaultFactory/vaultManager.js +++ b/packages/inter-protocol/src/vaultFactory/vaultManager.js @@ -687,7 +687,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 2dff1adfb68f..d4e11db4baba 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/src/contractFacet/reallocate.js b/packages/zoe/src/contractFacet/reallocate.js new file mode 100644 index 000000000000..43f56d4a912b --- /dev/null +++ b/packages/zoe/src/contractFacet/reallocate.js @@ -0,0 +1,104 @@ +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 updateAllocations = (seat, newAllocation) => { + allocations.set(seat, newAllocation); + }; + + const decrementAllocation = (seat, decrement) => { + const [incr, decr] = getAllocations(seat); + + const newDecr = [...decr, decrement]; + updateAllocations(seat, [incr, newDecr]); + }; + + const incrementAllocation = (seat, increment) => { + const [incr, decr] = getAllocations(seat); + + const newIncr = [...incr, increment]; + updateAllocations(seat, [newIncr, decr]); + }; + + 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`; + } + 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 of allocations.keys()) { + const [incrList, decrList] = getAllocations(seat); + 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 843ff6988ff8..00577ce9f567 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,9 @@ */ /** + * @deprecated reallocate() will be supported until at least 2023/09/01. It may + * be removed without further warning any time after 2023/11/01. + * * @typedef {(seat1: ZCFSeat, seat2: ZCFSeat, ...seatRest: * Array) => void} Reallocate * @@ -83,6 +87,15 @@ * effect offer safety for seats whose allocations change. */ +/** + * @typedef {[ + * fromSeat?: ZCFSeat, + * toSeat?: ZCFSeat, + * fromAmounts?: AmountKeywordRecord, + * toAmounts?: AmountKeywordRecord + * ]} TransferPart + */ + /** * @callback SaveIssuer * @@ -184,6 +197,14 @@ * @returns {Amount} */ +/** + * @deprecated Use atomicRearrange instead + * + * @callback DeprecatedIncrementDecrementBy + * @param {AmountKeywordRecord} amountKeywordRecord + * @returns {AmountKeywordRecord} + */ + /** * @typedef {object} ZCFSeat * @property {(completion?: Completion) => void} exit @@ -198,9 +219,9 @@ * @property {() => boolean} hasStagedAllocation * Deprecated: Use atomicRearrange instead * @property {(newAllocation: Allocation) => boolean} isOfferSafe - * @property {(amountKeywordRecord: AmountKeywordRecord) => AmountKeywordRecord} incrementBy + * @property {DeprecatedIncrementDecrementBy} incrementBy * Deprecated: Use atomicRearrange instead - * @property {(amountKeywordRecord: AmountKeywordRecord) => AmountKeywordRecord} decrementBy + * @property {DeprecatedIncrementDecrementBy} decrementBy * Deprecated: Use atomicRearrange instead * @property {() => void} clear * Deprecated: Use atomicRearrange instead diff --git a/packages/zoe/src/contractFacet/zcfSeat.js b/packages/zoe/src/contractFacet/zcfSeat.js index a7caa3608522..3e4c7d897093 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,91 @@ 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 ///////////////////////////////// + newAllocations.forEach(([seat]) => { + 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 ///////////// + const flattenAllocations = allocations => + allocations.flatMap(Object.values); + const previousAmounts = flattenAllocations( + newAllocations.map(([seat]) => seat.getCurrentAllocation()), + ); + const newAmounts = flattenAllocations( + newAllocations.map(([_, allocation]) => allocation), + ); + assertRightsConserved(previousAmounts, newAmounts); + + // ////// Ensure that offer safety holds /////////////////////// + newAllocations.forEach(([seat, allocation]) => { + 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 { seatHandle, allocation }; + }, + ); + 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. + + newAllocations.map(([seat, allocation]) => + activeZCFSeats.set(seat, allocation), + ); + + 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 6bad81fd1763..501de00db139 100644 --- a/packages/zoe/src/contractFacet/zcfZygote.js +++ b/packages/zoe/src/contractFacet/zcfZygote.js @@ -245,6 +245,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 fa27fe8adc7a..2af443d5fe8f 100644 --- a/packages/zoe/src/contractSupport/atomicTransfer.js +++ b/packages/zoe/src/contractSupport/atomicTransfer.js @@ -9,15 +9,6 @@ export const TransferPartShape = M.splitArray( 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,6 +43,8 @@ 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 */ @@ -179,4 +172,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 19dfd1ac4fc2..36f2cc9497ab 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 294a3837ed44..d44239b37c6b 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 29d1faca8fc2..7d08cbc1b204 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 7697c2ed7490..a6fd1b1a9c69 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, @@ -204,8 +202,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 }], @@ -307,8 +304,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 2ed71f86de19..5320359e212d 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 28d965687b9f..c15ba721f344 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 10add08117a1..9b06e16fd257 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 a50285d6c37e..f24a039ed265 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 2351817758bb..2d7060befb84 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 77b43ade259c..7ec99d2da9bf 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 4b559072c034..23151eed91a1 100644 --- a/packages/zoe/test/unitTests/contracts/loan/test-liquidate.js +++ b/packages/zoe/test/unitTests/contracts/loan/test-liquidate.js @@ -12,7 +12,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(); @@ -44,8 +43,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 000000000000..2df22e83333b --- /dev/null +++ b/packages/zoe/test/unitTests/zcf/test-atomicRearrange.js @@ -0,0 +1,260 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; + +import { makeIssuerKit, 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'; +import { E } from '@endo/eventual-send'; + +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/ }, + ); +}); + +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/ }, + ); +});