From 561dadf412c43c49d59709a4625db76c5eb16278 Mon Sep 17 00:00:00 2001 From: Chris Hibbert Date: Thu, 8 Jun 2023 14:02:25 -0700 Subject: [PATCH] refactor: move atomicRearrange into Zcf This is the next step of #6577 (atomic reallocations for contracts). It adds native support in ZCF for atomicReallocate, which makes it possible to update contracts from the helper to use the ZCF-native approach, which is a prerequisite for removing the old hazardous staging-based approach. This PR only affects ZCF, though it updates types referenced from inter-protocol contracts. This is required in order to keep the mono-repo consistent, and has no run-time impact on the contracts, even if changes are pushed to some live contracts even before the ZOE change takes effect. The sample contracts in Zoe have also been updated to use the new API. These do not affect the interprotocol contracts. The only zoe contract referenced by inter-protocol contracts is scaledPriceAuthority, which is not changed here. The new code was tested with versions of the inter-protocol contracts updated to use the ZCF-native API. Those contracts were reverted to their old form before merging this PR since the contracts should be updated separately from Zoe. None of the contracts should be updated to use the new reallocation API until this PR is running on chain. They can be individually updated once this code is in place. --- .../inter-protocol/src/auction/auctioneer.js | 4 +- .../src/vaultFactory/liquidation.js | 2 +- .../inter-protocol/src/vaultFactory/types.js | 2 +- .../inter-protocol/src/vaultFactory/vault.js | 2 +- .../src/vaultFactory/vaultDirector.js | 2 +- .../src/vaultFactory/vaultManager.js | 2 +- .../vaultFactory/vault-contract-wrapper.js | 6 +- packages/zoe/src/contractFacet/reallocate.js | 104 +++++++ packages/zoe/src/contractFacet/types.js | 25 +- packages/zoe/src/contractFacet/zcfSeat.js | 102 ++++++- packages/zoe/src/contractFacet/zcfZygote.js | 1 + .../zoe/src/contractSupport/atomicTransfer.js | 13 +- .../zoe/src/contractSupport/zoeHelpers.js | 13 +- .../src/contracts/auction/firstPriceLogic.js | 4 +- .../src/contracts/auction/secondPriceLogic.js | 4 +- packages/zoe/src/contracts/autoswap.js | 10 +- packages/zoe/src/contracts/barterExchange.js | 5 +- .../contracts/callSpread/fundedCallSpread.js | 4 +- .../contracts/callSpread/pricedCallSpread.js | 4 +- packages/zoe/src/contracts/loan/borrow.js | 4 +- packages/zoe/src/contracts/loan/close.js | 8 +- packages/zoe/src/contracts/sellItems.js | 4 +- .../contracts/loan/test-liquidate.js | 4 +- .../unitTests/zcf/test-atomicRearrange.js | 269 ++++++++++++++++++ 24 files changed, 525 insertions(+), 73 deletions(-) create mode 100644 packages/zoe/src/contractFacet/reallocate.js create mode 100644 packages/zoe/test/unitTests/zcf/test-atomicRearrange.js 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..b7cc1b16cd56 --- /dev/null +++ b/packages/zoe/test/unitTests/zcf/test-atomicRearrange.js @@ -0,0 +1,269 @@ +// 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/ }, + ); + + 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/ }, + ); +});