From 65d3f14c8102993168d2568eed5e6acbcba0c48a Mon Sep 17 00:00:00 2001 From: "Mark S. Miller" Date: Mon, 12 Dec 2022 19:32:15 -0800 Subject: [PATCH] fix: use atomicTransfers rather than stagings. (#6577) * refactor: use atomicRearrange rather than stagings * refactor: review suggestion * refactor: more review suggestions * refactpr: comment --- packages/inter-protocol/src/collectFees.js | 6 +- .../inter-protocol/src/contractSupport.js | 1 + packages/inter-protocol/src/interchainPool.js | 8 +- packages/inter-protocol/src/psm/psm.js | 65 +++---- .../src/reserve/assetReserve.js | 42 ++--- .../vaultFactory/liquidateIncrementally.js | 6 +- .../src/vaultFactory/liquidateMinimum.js | 6 +- .../inter-protocol/src/vaultFactory/vault.js | 9 +- .../src/vaultFactory/vaultManager.js | 10 +- .../src/vpool-xyk-amm/addPool.js | 16 +- .../src/vpool-xyk-amm/doublePool.js | 26 ++- .../inter-protocol/src/vpool-xyk-amm/pool.js | 26 +-- .../src/vpool-xyk-amm/singlePool.js | 41 ++-- packages/pegasus/src/pegasus.js | 15 +- .../store/src/stores/scalarWeakMapStore.js | 4 + .../store/src/stores/scalarWeakSetStore.js | 4 + packages/store/src/types.js | 8 +- packages/zoe/src/contractFacet/types.js | 8 +- .../zoe/src/contractSupport/atomicTransfer.js | 176 ++++++++++++++++++ packages/zoe/src/contractSupport/index.js | 8 +- .../zoe/src/contractSupport/zoeHelpers.js | 66 +++---- .../src/contracts/auction/firstPriceLogic.js | 14 +- .../src/contracts/auction/secondPriceLogic.js | 19 +- packages/zoe/src/contracts/autoswap.js | 57 +++--- packages/zoe/src/contracts/barterExchange.js | 25 ++- .../contracts/callSpread/fundedCallSpread.js | 25 +-- .../src/contracts/callSpread/payoffHandler.js | 11 +- .../contracts/callSpread/pricedCallSpread.js | 12 +- .../zoe/src/contracts/loan/addCollateral.js | 16 +- packages/zoe/src/contracts/loan/borrow.js | 18 +- packages/zoe/src/contracts/loan/close.js | 28 +-- .../src/contracts/loan/scheduleLiquidation.js | 14 +- packages/zoe/src/contracts/oracle.js | 16 +- packages/zoe/src/contracts/otcDesk.js | 11 +- packages/zoe/src/contracts/sellItems.js | 13 +- 35 files changed, 530 insertions(+), 300 deletions(-) create mode 100644 packages/zoe/src/contractSupport/atomicTransfer.js diff --git a/packages/inter-protocol/src/collectFees.js b/packages/inter-protocol/src/collectFees.js index aeed4c84f2e..e6fb1440df3 100644 --- a/packages/inter-protocol/src/collectFees.js +++ b/packages/inter-protocol/src/collectFees.js @@ -1,3 +1,5 @@ +import { atomicTransfer } from '@agoric/zoe/src/contractSupport'; + /** * Provide shared support for providing access to fees from a service contract. * @@ -14,9 +16,7 @@ export const makeMakeCollectFeesInvitation = ( ) => { const collectFees = seat => { const amount = feeSeat.getAmountAllocated(keyword, feeBrand); - feeSeat.decrementBy(harden({ [keyword]: amount })); - seat.incrementBy(harden({ Fee: amount })); - zcf.reallocate(seat, feeSeat); + atomicTransfer(zcf, feeSeat, seat, { [keyword]: amount }, { Fee: amount }); seat.exit(); return `paid out ${amount.value}`; diff --git a/packages/inter-protocol/src/contractSupport.js b/packages/inter-protocol/src/contractSupport.js index 05e4f2aa3ed..7dbf28d913a 100644 --- a/packages/inter-protocol/src/contractSupport.js +++ b/packages/inter-protocol/src/contractSupport.js @@ -46,6 +46,7 @@ export const assertOnlyKeys = (proposal, keys) => { * the gain and a loss on the `fromSeat`. The gain/loss are typically from the * give/want respectively of a proposal. The `key` is the allocation keyword. * + * @deprecated Use atomicRearrange instead * @param {ZCFSeat} fromSeat * @param {ZCFSeat} toSeat * @param {Amount} fromLoses diff --git a/packages/inter-protocol/src/interchainPool.js b/packages/inter-protocol/src/interchainPool.js index d22aec04c41..a18a89d00e4 100644 --- a/packages/inter-protocol/src/interchainPool.js +++ b/packages/inter-protocol/src/interchainPool.js @@ -1,6 +1,9 @@ import { E, Far } from '@endo/far'; import { AmountMath, AssetKind, makeIssuerKit } from '@agoric/ertp'; -import { offerTo } from '@agoric/zoe/src/contractSupport/index.js'; +import { + offerTo, + atomicTransfer, +} from '@agoric/zoe/src/contractSupport/index.js'; import { MIN_INITIAL_POOL_LIQUIDITY_KEY } from './vpool-xyk-amm/params.js'; const { Fail, quote: q } = assert; @@ -87,8 +90,7 @@ export const start = (zcf, { bankManager }) => { }, }); - seat2.incrementBy(seat.decrementBy(harden({ Central: centralAmt }))); - zcf.reallocate(seat, seat2); + atomicTransfer(zcf, seat, seat2, { Central: centralAmt }); const invitation = await E(ammPub).addPoolInvitation(); const { userSeatPromise, deposited } = await offerTo( diff --git a/packages/inter-protocol/src/psm/psm.js b/packages/inter-protocol/src/psm/psm.js index 9a0746f7c9a..aa69631b023 100644 --- a/packages/inter-protocol/src/psm/psm.js +++ b/packages/inter-protocol/src/psm/psm.js @@ -7,6 +7,7 @@ import { ceilMultiplyBy, floorDivideBy, floorMultiplyBy, + atomicRearrange, } from '@agoric/zoe/src/contractSupport/index.js'; import { Far } from '@endo/marshal'; import { @@ -49,21 +50,6 @@ const { Fail } = assert; * ever given by this contract */ -/** - * Stage a transfer of a single asset from one seat to another, with an optional - * remapping of the Keywords. Check that the remapping is for the same amount. - * - * @param {ZCFSeat} from - * @param {ZCFSeat} to - * @param {AmountKeywordRecord} txFrom - * @param {AmountKeywordRecord} txTo - */ -const stageTransfer = (from, to, txFrom, txTo = txFrom) => { - assert(AmountMath.isEqual(Object.values(txFrom)[0], Object.values(txTo)[0])); - from.decrementBy(txFrom); - to.incrementBy(txTo); -}; - /** @typedef {import('@agoric/vat-data').Baggage} Baggage */ /** @@ -177,24 +163,18 @@ export const start = async (zcf, privateArgs, baggage) => { const maxAnchor = floorMultiplyBy(afterFee, anchorPerMinted); AmountMath.isGTE(maxAnchor, wanted) || Fail`wanted ${wanted} is more than ${given} minus fees ${fee}`; - try { - stageTransfer(seat, stage, { In: afterFee }, { Minted: afterFee }); - stageTransfer(seat, feePool, { In: fee }, { Minted: fee }); - stageTransfer( - anchorPool, - seat, - { Anchor: maxAnchor }, - { Out: maxAnchor }, - ); - zcf.reallocate(seat, anchorPool, stage, feePool); - burnMinted(afterFee); - } catch (e) { - stage.clear(); - anchorPool.clear(); - feePool.clear(); - // TODO(#6116) someday, reallocate should guarantee that this case cannot happen - throw e; - } + atomicRearrange( + zcf, + harden([ + [seat, stage, { In: afterFee }, { Minted: afterFee }], + [seat, feePool, { In: fee }, { Minted: fee }], + [anchorPool, seat, { Anchor: maxAnchor }, { Out: maxAnchor }], + ]), + ); + // The treatment of `burnMinted` here is different than the + // one immediately below. This `burnMinted` + // happen only if the `atomicRearrange` does *not* throw. + burnMinted(afterFee); totalAnchorProvided = AmountMath.add(totalAnchorProvided, maxAnchor); }; @@ -212,15 +192,18 @@ export const start = async (zcf, privateArgs, baggage) => { Fail`wanted ${wanted} is more than ${given} minus fees ${fee}`; mintMinted(asStable); try { - stageTransfer(seat, anchorPool, { In: given }, { Anchor: given }); - stageTransfer(stage, seat, { Minted: afterFee }, { Out: afterFee }); - stageTransfer(stage, feePool, { Minted: fee }); - zcf.reallocate(seat, anchorPool, stage, feePool); + atomicRearrange( + zcf, + harden([ + [seat, anchorPool, { In: given }, { Anchor: given }], + [stage, seat, { Minted: afterFee }, { Out: afterFee }], + [stage, feePool, { Minted: fee }], + ]), + ); } catch (e) { - stage.clear(); - anchorPool.clear(); - feePool.clear(); - // TODO(#6116) someday, reallocate should guarantee that this case cannot happen + // The treatment of `burnMinted` here is different than the + // one immediately above. This `burnMinted` + // happens only if the `atomicRearrange` *does* throw. burnMinted(asStable); throw e; } diff --git a/packages/inter-protocol/src/reserve/assetReserve.js b/packages/inter-protocol/src/reserve/assetReserve.js index 20351450a16..1d23a7a24c0 100644 --- a/packages/inter-protocol/src/reserve/assetReserve.js +++ b/packages/inter-protocol/src/reserve/assetReserve.js @@ -1,7 +1,10 @@ import { E, Far } from '@endo/far'; import { AmountMath } from '@agoric/ertp'; import { handleParamGovernance, ParamTypes } from '@agoric/governance'; -import { offerTo } from '@agoric/zoe/src/contractSupport/index.js'; +import { + offerTo, + atomicTransfer, +} from '@agoric/zoe/src/contractSupport/index.js'; import { provideDurableMapStore, vivifyKindMulti } from '@agoric/vat-data'; import { AMM_INSTANCE } from './params.js'; @@ -253,10 +256,14 @@ const start = async (zcf, privateArgs, baggage) => { } = seat.getProposal(); const collateralKeyword = getKeywordForBrand(amountIn.brand); - seat.decrementBy(harden({ Collateral: amountIn })); - collateralSeat.incrementBy(harden({ [collateralKeyword]: amountIn })); - zcf.reallocate(collateralSeat, seat); + atomicTransfer( + zcf, + seat, + collateralSeat, + { Collateral: amountIn }, + { [collateralKeyword]: amountIn }, + ); seat.exit(); trace('received collateral', amountIn); @@ -333,14 +340,9 @@ const start = async (zcf, privateArgs, baggage) => { const offerToSeat = feeMint.mintGains(harden({ Fee: fee })); state.totalFeeMinted = AmountMath.add(state.totalFeeMinted, fee); - offerToSeat.incrementBy( - collateralSeat.decrementBy( - harden({ - [collateralKeyword]: collateral, - }), - ), - ); - zcf.reallocate(collateralSeat, offerToSeat); + atomicTransfer(zcf, collateralSeat, offerToSeat, { + [collateralKeyword]: collateral, + }); // Add Fee tokens and collateral to the AMM const invitation = await E( @@ -368,17 +370,13 @@ const start = async (zcf, privateArgs, baggage) => { const liquidityAmount = offerToSeat.getCurrentAllocation(); const liquidityKeyword = makeLiquidityKeyword(collateralKeyword); - offerToSeat.decrementBy( - harden({ - Liquidity: liquidityAmount.Liquidity, - }), - ); - collateralSeat.incrementBy( - harden({ - [liquidityKeyword]: liquidityAmount.Liquidity, - }), + atomicTransfer( + zcf, + offerToSeat, + collateralSeat, + { Liquidity: liquidityAmount.Liquidity }, + { [liquidityKeyword]: liquidityAmount.Liquidity }, ); - zcf.reallocate(offerToSeat, collateralSeat); updateMetrics({ state }); }; diff --git a/packages/inter-protocol/src/vaultFactory/liquidateIncrementally.js b/packages/inter-protocol/src/vaultFactory/liquidateIncrementally.js index c5bfbc9a449..0b76843627f 100644 --- a/packages/inter-protocol/src/vaultFactory/liquidateIncrementally.js +++ b/packages/inter-protocol/src/vaultFactory/liquidateIncrementally.js @@ -7,6 +7,7 @@ import { natSafeMath as NatMath, ceilMultiplyBy, oneMinus, + atomicTransfer, } from '@agoric/zoe/src/contractSupport/index.js'; import { AmountMath } from '@agoric/ertp'; import { Far } from '@endo/marshal'; @@ -321,10 +322,7 @@ const start = async zcf => { const penaltyPaid = AmountMath.min(penalty, debtPaid); // Allocate penalty portion of proceeds to a seat that will hold it for transfer to reserve - penaltyPoolSeat.incrementBy( - debtorSeat.decrementBy(harden({ Out: penaltyPaid })), - ); - zcf.reallocate(penaltyPoolSeat, debtorSeat); + atomicTransfer(zcf, debtorSeat, penaltyPoolSeat, { Out: penaltyPaid }); debtorSeat.exit(); trace('exit seat'); diff --git a/packages/inter-protocol/src/vaultFactory/liquidateMinimum.js b/packages/inter-protocol/src/vaultFactory/liquidateMinimum.js index fa6af16697b..9d9402464b0 100644 --- a/packages/inter-protocol/src/vaultFactory/liquidateMinimum.js +++ b/packages/inter-protocol/src/vaultFactory/liquidateMinimum.js @@ -2,6 +2,7 @@ import { E } from '@endo/eventual-send'; import { ceilMultiplyBy, offerTo, + atomicTransfer, } from '@agoric/zoe/src/contractSupport/index.js'; import { AmountMath } from '@agoric/ertp'; import { Far } from '@endo/marshal'; @@ -73,10 +74,7 @@ const start = async zcf => { const penaltyPaid = AmountMath.min(penalty, debtPaid); // Allocate penalty portion of proceeds to a seat that will hold it for transfer to reserve - penaltyPoolSeat.incrementBy( - debtorSeat.decrementBy(harden({ Out: penaltyPaid })), - ); - zcf.reallocate(penaltyPoolSeat, debtorSeat); + atomicTransfer(zcf, debtorSeat, penaltyPoolSeat, { Out: penaltyPaid }); debtorSeat.exit(); }; diff --git a/packages/inter-protocol/src/vaultFactory/vault.js b/packages/inter-protocol/src/vaultFactory/vault.js index e2ca76f53a4..087ce71d64a 100644 --- a/packages/inter-protocol/src/vaultFactory/vault.js +++ b/packages/inter-protocol/src/vaultFactory/vault.js @@ -5,6 +5,7 @@ import { makeRatioFromAmounts, ceilMultiplyBy, floorMultiplyBy, + atomicTransfer, } from '@agoric/zoe/src/contractSupport/index.js'; import { AmountMath } from '@agoric/ertp'; import { @@ -398,15 +399,15 @@ const helperBehavior = { Fail`Offer ${given} is not sufficient to pay off debt ${debt}`; // Return any overpayment - seat.incrementBy(vaultSeat.decrementBy(vaultSeat.getCurrentAllocation())); - zcf.reallocate(seat, vaultSeat); + atomicTransfer(zcf, vaultSeat, seat, vaultSeat.getCurrentAllocation()); + state.manager.burnAndRecord(debt, seat); } else if (phase === Phase.LIQUIDATED) { // Simply reallocate vault assets to the offer seat. // Don't take anything from the offer, even if vault is underwater. // TODO verify that returning Minted here doesn't mess up debt limits - seat.incrementBy(vaultSeat.decrementBy(vaultSeat.getCurrentAllocation())); - zcf.reallocate(seat, vaultSeat); + + atomicTransfer(zcf, vaultSeat, seat, vaultSeat.getCurrentAllocation()); } else { throw new Error('only active and liquidated vaults can be closed'); } diff --git a/packages/inter-protocol/src/vaultFactory/vaultManager.js b/packages/inter-protocol/src/vaultFactory/vaultManager.js index 2c9aa6f1988..ca69f2de299 100644 --- a/packages/inter-protocol/src/vaultFactory/vaultManager.js +++ b/packages/inter-protocol/src/vaultFactory/vaultManager.js @@ -31,6 +31,7 @@ import { getAmountOut, makeRatio, makeRatioFromAmounts, + atomicTransfer, } from '@agoric/zoe/src/contractSupport/index.js'; import { E } from '@endo/eventual-send'; import { unitAmount } from '@agoric/zoe/src/contractSupport/priceQuote.js'; @@ -537,12 +538,9 @@ const helperBehavior = { // it. We could hold it until it crosses some threshold, then sell it // to the AMM, or we could transfer it to the reserve. At least it's // visible in the accounting. - vaultSeat.decrementBy( - state.retainedCollateralSeat.incrementBy({ - Collateral: collateralPost, - }), - ); - zcf.reallocate(vaultSeat, state.retainedCollateralSeat); + atomicTransfer(zcf, vaultSeat, state.retainedCollateralSeat, { + Collateral: collateralPost, + }); } // Reduce totalCollateral by collateralPre, since all the collateral was diff --git a/packages/inter-protocol/src/vpool-xyk-amm/addPool.js b/packages/inter-protocol/src/vpool-xyk-amm/addPool.js index 0010ca52fb4..9b1ab5023de 100644 --- a/packages/inter-protocol/src/vpool-xyk-amm/addPool.js +++ b/packages/inter-protocol/src/vpool-xyk-amm/addPool.js @@ -1,6 +1,9 @@ import { E } from '@endo/eventual-send'; import { AmountMath, AssetKind } from '@agoric/ertp'; -import { assertProposalShape } from '@agoric/zoe/src/contractSupport/index.js'; +import { + assertProposalShape, + atomicTransfer, +} from '@agoric/zoe/src/contractSupport/index.js'; import { definePoolKind } from './pool.js'; @@ -173,10 +176,15 @@ export const makeAddPoolInvitation = ( // transfer minPoolLiquidity in tokens from the funder to the reserve. helper.addLiquidityInternal(seat, secondaryAmount, centralAmount); - seat.decrementBy({ Liquidity: minLiqAmount }); const { zcfSeat: reserveLiquidityTokenSeat } = zcf.makeEmptySeatKit(); - reserveLiquidityTokenSeat.incrementBy({ [liquidityKeyword]: minLiqAmount }); - zcf.reallocate(reserveLiquidityTokenSeat, seat); + atomicTransfer( + zcf, + seat, + reserveLiquidityTokenSeat, + { Liquidity: minLiqAmount }, + { [liquidityKeyword]: minLiqAmount }, + ); + seat.exit(); pool.updateState(); diff --git a/packages/inter-protocol/src/vpool-xyk-amm/doublePool.js b/packages/inter-protocol/src/vpool-xyk-amm/doublePool.js index dc47a8a2799..07333bcaba6 100644 --- a/packages/inter-protocol/src/vpool-xyk-amm/doublePool.js +++ b/packages/inter-protocol/src/vpool-xyk-amm/doublePool.js @@ -1,4 +1,9 @@ import { AmountMath } from '@agoric/ertp'; +import { + atomicRearrange, + fromOnly, + toOnly, +} from '@agoric/zoe/src/contractSupport/index.js'; import { Far } from '@endo/marshal'; import { makeFeeRatio } from './constantProduct/calcFees.js'; import { @@ -55,15 +60,20 @@ export const makeDoublePool = ( const inPoolSeat = collateralInPool.getPoolSeat(); const outPoolSeat = collateralOutPool.getPoolSeat(); - seat.decrementBy(harden({ In: prices.swapperGives })); - inPoolSeat.decrementBy(harden({ Central: prices.inPoolDecrement })); - outPoolSeat.decrementBy(harden({ Secondary: prices.outPoolDecrement })); - seat.incrementBy(harden({ Out: prices.swapperGets })); - inPoolSeat.incrementBy(harden({ Secondary: prices.inPoolIncrement })); - outPoolSeat.incrementBy(harden({ Central: prices.outPoolIncrement })); - feeSeat.incrementBy(harden({ Fee: prices.protocolFee })); + atomicRearrange( + zcf, + harden([ + fromOnly(seat, { In: prices.swapperGives }), + fromOnly(inPoolSeat, { Central: prices.inPoolDecrement }), + fromOnly(outPoolSeat, { Secondary: prices.outPoolDecrement }), + + toOnly(seat, { Out: prices.swapperGets }), + toOnly(inPoolSeat, { Secondary: prices.inPoolIncrement }), + toOnly(outPoolSeat, { Central: prices.outPoolIncrement }), + toOnly(feeSeat, { Fee: prices.protocolFee }), + ]), + ); - zcf.reallocate(outPoolSeat, inPoolSeat, feeSeat, seat); seat.exit(); collateralInPool.updateState(); collateralOutPool.updateState(); diff --git a/packages/inter-protocol/src/vpool-xyk-amm/pool.js b/packages/inter-protocol/src/vpool-xyk-amm/pool.js index cede2e0cbe7..b2222a7c784 100644 --- a/packages/inter-protocol/src/vpool-xyk-amm/pool.js +++ b/packages/inter-protocol/src/vpool-xyk-amm/pool.js @@ -7,6 +7,7 @@ import { calcLiqValueToMint, calcSecondaryRequired, calcValueToRemove, + atomicRearrange, } from '@agoric/zoe/src/contractSupport/index.js'; import { E } from '@endo/eventual-send'; @@ -252,18 +253,21 @@ export const definePoolKind = (baggage, ammPowers, storageNode, marshaller) => { ), ); - poolSeat.incrementBy( - userSeat.decrementBy(harden({ Liquidity: liquidityIn })), - ); - userSeat.incrementBy( - poolSeat.decrementBy( - harden({ - Central: centralTokenAmountOut, - Secondary: tokenKeywordAmountOut, - }), - ), + atomicRearrange( + ammPowers.zcf, + harden([ + [userSeat, poolSeat, { Liquidity: liquidityIn }], + [ + poolSeat, + userSeat, + { + Central: centralTokenAmountOut, + Secondary: tokenKeywordAmountOut, + }, + ], + ]), ); - ammPowers.zcf.reallocate(userSeat, poolSeat); + state.liqTokenSupply -= liquidityValueIn; userSeat.exit(); diff --git a/packages/inter-protocol/src/vpool-xyk-amm/singlePool.js b/packages/inter-protocol/src/vpool-xyk-amm/singlePool.js index d1049c502d4..c47f90252e4 100644 --- a/packages/inter-protocol/src/vpool-xyk-amm/singlePool.js +++ b/packages/inter-protocol/src/vpool-xyk-amm/singlePool.js @@ -1,3 +1,8 @@ +import { + atomicRearrange, + fromOnly, + toOnly, +} from '@agoric/zoe/src/contractSupport/index.js'; import { makeFeeRatio } from './constantProduct/calcFees.js'; import { pricesForStatedInput, @@ -17,20 +22,32 @@ export const makeSinglePool = ammPowers => ({ const { pool } = context.facets; const { poolSeat } = context.state; const { zcf, protocolSeat } = ammPowers; - seat.decrementBy(harden({ In: prices.swapperGives })); - seat.incrementBy(harden({ Out: prices.swapperGets })); - protocolSeat.incrementBy(harden({ Fee: prices.protocolFee })); - const inBrand = prices.swapperGives.brand; - if (inBrand === getSecondaryBrand(pool)) { - poolSeat.decrementBy(harden({ Central: prices.yDecrement })); - poolSeat.incrementBy(harden({ Secondary: prices.xIncrement })); - } else { - poolSeat.decrementBy(harden({ Secondary: prices.yDecrement })); - poolSeat.incrementBy(harden({ Central: prices.xIncrement })); - } - zcf.reallocate(poolSeat, seat, protocolSeat); + /** @type {import('@agoric/zoe/src/contractSupport/atomicTransfer.js').TransferPart[]} */ + const xfer = harden( + inBrand === getSecondaryBrand(pool) + ? [ + fromOnly(poolSeat, { Central: prices.yDecrement }), + toOnly(poolSeat, { Secondary: prices.xIncrement }), + ] + : [ + fromOnly(poolSeat, { Secondary: prices.yDecrement }), + toOnly(poolSeat, { Central: prices.xIncrement }), + ], + ); + + atomicRearrange( + zcf, + harden([ + fromOnly(seat, { In: prices.swapperGives }), + + toOnly(seat, { Out: prices.swapperGets }), + toOnly(protocolSeat, { Fee: prices.protocolFee }), + + ...xfer, + ]), + ); seat.exit(); pool.updateState(); return `Swap successfully completed.`; diff --git a/packages/pegasus/src/pegasus.js b/packages/pegasus/src/pegasus.js index 67311f48d1e..81f38416761 100644 --- a/packages/pegasus/src/pegasus.js +++ b/packages/pegasus/src/pegasus.js @@ -3,7 +3,10 @@ import { assert, details as X, Fail } from '@agoric/assert'; import { makeLegacyWeakMap, makeLegacyMap } from '@agoric/store'; import { E, Far } from '@endo/far'; -import { assertProposalShape } from '@agoric/zoe/src/contractSupport/index.js'; +import { + assertProposalShape, + atomicTransfer, +} from '@agoric/zoe/src/contractSupport/index.js'; import { makeSubscriptionKit } from '@agoric/notifier'; import '@agoric/vats/exported.js'; @@ -247,9 +250,13 @@ const makePegasus = (zcf, board, namesByAddress) => { winner, ) => { // Transfer the amount to our backing seat. - loser.decrementBy(harden({ [loserKeyword]: amount })); - winner.incrementBy(harden({ [winnerKeyword]: amount })); - zcf.reallocate(loser, winner); + atomicTransfer( + zcf, + loser, + winner, + { [loserKeyword]: amount }, + { [winnerKeyword]: amount }, + ); }; // Describe how to retain/redeem real local erights. diff --git a/packages/store/src/stores/scalarWeakMapStore.js b/packages/store/src/stores/scalarWeakMapStore.js index ed29fc75aab..0b0818072eb 100644 --- a/packages/store/src/stores/scalarWeakMapStore.js +++ b/packages/store/src/stores/scalarWeakMapStore.js @@ -1,4 +1,5 @@ import { Far, assertPassable, passStyleOf } from '@endo/marshal'; +import { getCopyMapEntries, isCopyMap } from '../keys/checkKey.js'; import { fit, assertPattern } from '../patterns/patternMatchers.js'; const { quote: q, Fail } = assert; @@ -57,6 +58,9 @@ export const makeWeakMapStoreMethods = ( }, addAll: entries => { + if (isCopyMap(entries)) { + entries = getCopyMapEntries(entries); + } for (const [key, value] of entries) { // Don't assert that the key either does or does not exist. assertKVOkToAdd(key, value); diff --git a/packages/store/src/stores/scalarWeakSetStore.js b/packages/store/src/stores/scalarWeakSetStore.js index 40ac8d1262b..9e8eaf460ad 100644 --- a/packages/store/src/stores/scalarWeakSetStore.js +++ b/packages/store/src/stores/scalarWeakSetStore.js @@ -1,4 +1,5 @@ import { Far, passStyleOf } from '@endo/marshal'; +import { getCopySetKeys, isCopySet } from '../keys/checkKey.js'; import { fit, assertPattern } from '../patterns/patternMatchers.js'; const { quote: q, Fail } = assert; @@ -41,6 +42,9 @@ export const makeWeakSetStoreMethods = ( }, addAll: keys => { + if (isCopySet(keys)) { + keys = getCopySetKeys(keys); + } for (const key of keys) { assertKeyOkToAdd(key); jsset.add(key); diff --git a/packages/store/src/types.js b/packages/store/src/types.js index 8f0971fb425..916d5d8877b 100644 --- a/packages/store/src/types.js +++ b/packages/store/src/types.js @@ -147,7 +147,7 @@ * allows primitives and remotables. * @property {(key: K) => void} delete * Remove the key. Throws if not found. - * @property {(keys: Iterable) => void} addAll + * @property {(keys: CopySet | Iterable) => void} addAll */ /** @@ -163,7 +163,7 @@ * allows primitives and remotables. * @property {(key: K) => void} delete * Remove the key. Throws if not found. - * @property {(keys: Iterable) => void} addAll + * @property {(keys: CopySet | Iterable) => void} addAll * @property {(keyPatt?: Pattern) => Iterable} keys * @property {(keyPatt?: Pattern) => Iterable} values * @property {(keyPatt?: Pattern) => CopySet} snapshot @@ -187,7 +187,7 @@ * Set the key. Throws if not found. * @property {(key: K) => void} delete * Remove the key. Throws if not found. - * @property {(entries: Iterable<[K,V]>) => void} addAll + * @property {(entries: CopyMap | Iterable<[K,V]>) => void} addAll */ /** @@ -206,7 +206,7 @@ * Set the key. Throws if not found. * @property {(key: K) => void} delete * Remove the key. Throws if not found. - * @property {(entries: Iterable<[K,V]>) => void} addAll + * @property {(entries: CopyMap | Iterable<[K,V]>) => void} addAll * @property {(keyPatt?: Pattern, valuePatt?: Pattern) => Iterable} keys * @property {(keyPatt?: Pattern, valuePatt?: Pattern) => Iterable} values * @property {( diff --git a/packages/zoe/src/contractFacet/types.js b/packages/zoe/src/contractFacet/types.js index c0087eda632..774f9bfeecf 100644 --- a/packages/zoe/src/contractFacet/types.js +++ b/packages/zoe/src/contractFacet/types.js @@ -22,7 +22,8 @@ * synchronously from within the contract, and usually is referred to * in code as zcf. * - * @property {Reallocate} reallocate - reallocate amounts among seats + * @property {Reallocate} reallocate - reallocate amounts among seats. + * Deprecated: Use atomicRearrange instead. * @property {(keyword: Keyword) => void} assertUniqueKeyword - check * whether a keyword is valid and unique and could be added in * `saveIssuer` @@ -193,11 +194,16 @@ * @property {ZCFGetAmountAllocated} getAmountAllocated * @property {() => Allocation} getCurrentAllocation * @property {() => Allocation} getStagedAllocation + * Deprecated: Use atomicRearrange instead * @property {() => boolean} hasStagedAllocation + * Deprecated: Use atomicRearrange instead * @property {(newAllocation: Allocation) => boolean} isOfferSafe * @property {(amountKeywordRecord: AmountKeywordRecord) => AmountKeywordRecord} incrementBy + * Deprecated: Use atomicRearrange instead * @property {(amountKeywordRecord: AmountKeywordRecord) => AmountKeywordRecord} decrementBy + * Deprecated: Use atomicRearrange instead * @property {() => void} clear + * Deprecated: Use atomicRearrange instead */ /** diff --git a/packages/zoe/src/contractSupport/atomicTransfer.js b/packages/zoe/src/contractSupport/atomicTransfer.js new file mode 100644 index 00000000000..49b20fd46ab --- /dev/null +++ b/packages/zoe/src/contractSupport/atomicTransfer.js @@ -0,0 +1,176 @@ +import { fit, M } from '@agoric/store'; +import { assertRightsConserved } from '../contractFacet/rightsConservation.js'; + +const { Fail, quote: q } = assert; + +/** + * @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 + * 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 -- enforced by ZCF. + * * No outstanding stagings for any of the mentioned seats. + * Stagings now 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 -- enforced by ZCF. + * * Overall conservation -- enforced by ZCF. + * * The overall transfer is expressed as an array of `TransferPart`. + * Each individual `TransferPart` is one of + * - A transfer from a `fromSeat` to a `toSeat`. + * This is not needed for Zoe's safety, as Zoe does + its own overall conservation check. Rather, it helps catch + and diagnose contract bugs earlier. + * - A taking from a `fromSeat`'s allocation. See the `fromOnly` + helper. + - A giving into a `toSeat`'s allocation. See the `toOnly` + helper. + * + * TODO Refactor `atomicRearrange`from being a helper into being + * zcf's replacement for reallocate. Is currently a helper during + * the transition, to avoid interference with progress on Zoe durability. + * + * See the helpers below, `fromOnly`, `toOnly`, and `atomicTransfer`, + * which will remain helpers. These helper are for convenience + * in expressing atomic rearragements clearly. + * + * @param {ZCF} zcf + * @param {TransferPart[]} transfers + */ +export const atomicRearrange = (zcf, transfers) => { + fit(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(); + } + } +}; + +/** + * Sometimes a TransferPart in an atomicRearrange only expresses what amounts + * should be taken from a seat, leaving it to other TransferPart of the + * same atomicRearrange to balance it out. For this case, the + * `[fromSeat, undefined, fromAmounts]` form is more clearly expressed as + * `fromOnly(fromSeat, fromAmounts)`. Unlike TransferPart, both arguments to + * `fromOnly` are non-optional, as otherwise it doesn't make much sense. + * + * @param {ZCFSeat} fromSeat + * @param {AmountKeywordRecord} fromAmounts + * @returns {TransferPart} + */ +export const fromOnly = (fromSeat, fromAmounts) => + harden([fromSeat, undefined, fromAmounts]); + +/** + * Sometimes a TransferPart in an atomicRearrange only expresses what amounts + * should be given to a seat, leaving it to other TransferPart of the + * same atomicRearrange to balance it out. For this case, the + * `[undefined, toSeat, undefined, toAmounts]` form is more clearly expressed as + * `toOnly(toSeat, toAmounts)`. Unlike TransferPart, both arguments to + * `toOnly` are non-optional, as otherwise it doesn't make much sense. + * + * @param {ZCFSeat} toSeat + * @param {AmountKeywordRecord} toAmounts + * @returns {TransferPart} + */ +export const toOnly = (toSeat, toAmounts) => + harden([undefined, toSeat, undefined, toAmounts]); + +/** + * Special case of atomicRearrange for a single one-way transfer + * + * @param {ZCF} zcf + * @param {ZCFSeat} [fromSeat] + * @param {ZCFSeat} [toSeat] + * @param {AmountKeywordRecord} [fromAmounts] + * @param {AmountKeywordRecord} [toAmounts] + */ +export const atomicTransfer = ( + zcf, + fromSeat = undefined, + toSeat = undefined, + fromAmounts = undefined, + toAmounts = undefined, +) => atomicRearrange(zcf, harden([[fromSeat, toSeat, fromAmounts, toAmounts]])); diff --git a/packages/zoe/src/contractSupport/index.js b/packages/zoe/src/contractSupport/index.js index 5d250474253..99b7037242c 100644 --- a/packages/zoe/src/contractSupport/index.js +++ b/packages/zoe/src/contractSupport/index.js @@ -21,6 +21,13 @@ export { makeStateMachine } from './stateMachine.js'; export * from './statistics.js'; +export { + atomicRearrange, + atomicTransfer, + fromOnly, + toOnly, +} from './atomicTransfer.js'; + export { defaultAcceptanceMsg, swap, @@ -34,7 +41,6 @@ export { withdrawFromSeat, saveAllIssuers, offerTo, - checkZCF, } from './zoeHelpers.js'; export { diff --git a/packages/zoe/src/contractSupport/zoeHelpers.js b/packages/zoe/src/contractSupport/zoeHelpers.js index 8f4be17516a..9593752cbe9 100644 --- a/packages/zoe/src/contractSupport/zoeHelpers.js +++ b/packages/zoe/src/contractSupport/zoeHelpers.js @@ -4,6 +4,12 @@ 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'; export const defaultAcceptanceMsg = `The offer has been accepted. Once the contract has been completed, please check your payout`; @@ -51,13 +57,13 @@ export const satisfies = (zcf, seat, update) => { /** @type {Swap} */ export const swap = (zcf, leftSeat, rightSeat) => { try { - rightSeat.decrementBy(harden(leftSeat.getProposal().want)); - leftSeat.incrementBy(harden(leftSeat.getProposal().want)); - - leftSeat.decrementBy(harden(rightSeat.getProposal().want)); - rightSeat.incrementBy(harden(rightSeat.getProposal().want)); - - zcf.reallocate(leftSeat, rightSeat); + atomicRearrange( + zcf, + harden([ + [rightSeat, leftSeat, leftSeat.getProposal().want], + [leftSeat, rightSeat, rightSeat.getProposal().want], + ]), + ); } catch (err) { leftSeat.fail(err); rightSeat.fail(err); @@ -72,13 +78,16 @@ export const swap = (zcf, leftSeat, rightSeat) => { /** @type {SwapExact} */ export const swapExact = (zcf, leftSeat, rightSeat) => { try { - rightSeat.decrementBy(harden(rightSeat.getProposal().give)); - leftSeat.incrementBy(harden(leftSeat.getProposal().want)); - - leftSeat.decrementBy(harden(leftSeat.getProposal().give)); - rightSeat.incrementBy(harden(rightSeat.getProposal().want)); - - zcf.reallocate(leftSeat, rightSeat); + atomicRearrange( + zcf, + harden([ + fromOnly(rightSeat, rightSeat.getProposal().give), + fromOnly(leftSeat, leftSeat.getProposal().give), + + toOnly(leftSeat, leftSeat.getProposal().want), + toOnly(rightSeat, rightSeat.getProposal().want), + ]), + ); } catch (err) { leftSeat.fail(err); rightSeat.fail(err); @@ -192,9 +201,7 @@ export const depositToSeat = async (zcf, recipientSeat, amounts, payments) => { // exit the temporary seat. Note that the offerResult is the return value of this // function, so this synchronous trade must happen before the // offerResult resolves. - tempSeat.decrementBy(harden(amounts)); - recipientSeat.incrementBy(harden(amounts)); - zcf.reallocate(tempSeat, recipientSeat); + atomicTransfer(zcf, tempSeat, recipientSeat, amounts); tempSeat.exit(); return depositToSeatSuccessMsg; }; @@ -227,9 +234,7 @@ export const depositToSeat = async (zcf, recipientSeat, amounts, payments) => { export const withdrawFromSeat = async (zcf, seat, amounts) => { assert(!seat.hasExited(), 'The seat cannot have exited.'); const { zcfSeat: tempSeat, userSeat: tempUserSeatP } = zcf.makeEmptySeatKit(); - seat.decrementBy(harden(amounts)); - tempSeat.incrementBy(harden(amounts)); - zcf.reallocate(tempSeat, seat); + atomicTransfer(zcf, seat, tempSeat, amounts); tempSeat.exit(); return E(tempUserSeatP).getPayouts(); }; @@ -374,24 +379,3 @@ export const offerTo = async ( // TODO rename return key; userSeatPromise is a remote UserSeat return harden({ userSeatPromise, deposited: depositedPromiseKit.promise }); }; - -/** - * Create a wrapped version of zcf that asserts an invariant - * before performing a reallocation. - * - * @param {ZCF} zcf - * @param {(seats: ZCFSeat[]) => void} assertFn - an assertion - * that must be true for the reallocate to occur - * @returns {ZCF} - */ -export const checkZCF = (zcf, assertFn) => { - const checkedZCF = harden({ - ...zcf, - reallocate: (...seats) => { - assertFn(seats); - // @ts-expect-error The types aren't right for spreading - zcf.reallocate(...seats); - }, - }); - return checkedZCF; -}; diff --git a/packages/zoe/src/contracts/auction/firstPriceLogic.js b/packages/zoe/src/contracts/auction/firstPriceLogic.js index 1a220d09455..4aa8cdee9b1 100644 --- a/packages/zoe/src/contracts/auction/firstPriceLogic.js +++ b/packages/zoe/src/contracts/auction/firstPriceLogic.js @@ -1,4 +1,5 @@ import { AmountMath } from '@agoric/ertp'; +import { atomicRearrange } from '../../contractSupport/index.js'; /** * @param {ZCF} zcf @@ -43,13 +44,14 @@ export const calcWinnerAndClose = (zcf, sellSeat, bidSeats) => { } // Everyone else gets a refund so their values remain the same. - highestBidSeat.decrementBy(harden({ Bid: highestBid })); - sellSeat.incrementBy(harden({ Ask: highestBid })); + atomicRearrange( + zcf, + harden([ + [highestBidSeat, sellSeat, { Bid: highestBid }, { Ask: highestBid }], + [sellSeat, highestBidSeat, { Asset: assetAmount }], + ]), + ); - sellSeat.decrementBy(harden({ Asset: assetAmount })); - highestBidSeat.incrementBy(harden({ Asset: assetAmount })); - - zcf.reallocate(sellSeat, highestBidSeat); sellSeat.exit(); bidSeats.forEach(bidSeat => { if (!bidSeat.hasExited()) { diff --git a/packages/zoe/src/contracts/auction/secondPriceLogic.js b/packages/zoe/src/contracts/auction/secondPriceLogic.js index 7ec178b81e5..654a6c1345c 100644 --- a/packages/zoe/src/contracts/auction/secondPriceLogic.js +++ b/packages/zoe/src/contracts/auction/secondPriceLogic.js @@ -1,4 +1,5 @@ import { AmountMath } from '@agoric/ertp'; +import { atomicRearrange } from '../../contractSupport/index.js'; /** * @param {ZCF} zcf @@ -49,13 +50,19 @@ export const calcWinnerAndClose = (zcf, sellSeat, bidSeats) => { } // Everyone else gets a refund so their values remain the same. - highestBidSeat.decrementBy(harden({ Bid: secondHighestBid })); - sellSeat.incrementBy(harden({ Ask: secondHighestBid })); + atomicRearrange( + zcf, + harden([ + [ + highestBidSeat, + sellSeat, + { Bid: secondHighestBid }, + { Ask: secondHighestBid }, + ], + [sellSeat, highestBidSeat, { Asset: assetAmount }], + ]), + ); - sellSeat.decrementBy(harden({ Asset: assetAmount })); - highestBidSeat.incrementBy(harden({ Asset: assetAmount })); - - zcf.reallocate(sellSeat, highestBidSeat); sellSeat.exit(); bidSeats.forEach(bidSeat => { if (!bidSeat.hasExited()) { diff --git a/packages/zoe/src/contracts/autoswap.js b/packages/zoe/src/contracts/autoswap.js index 8abacfb5533..7697c2ed749 100644 --- a/packages/zoe/src/contracts/autoswap.js +++ b/packages/zoe/src/contracts/autoswap.js @@ -11,6 +11,7 @@ import { assertProposalShape, assertNatAssetKind, calcSecondaryRequired, + atomicRearrange, } from '../contractSupport/index.js'; /** @@ -87,22 +88,24 @@ const start = async zcf => { }; function consummate(tradeAmountIn, tradeAmountOut, swapSeat) { - swapSeat.decrementBy(harden({ In: tradeAmountIn })); - poolSeat.incrementBy( - harden({ - [getPoolKeyword(tradeAmountIn.brand)]: tradeAmountIn, - }), + atomicRearrange( + zcf, + harden([ + [ + swapSeat, + poolSeat, + { In: tradeAmountIn }, + { [getPoolKeyword(tradeAmountIn.brand)]: tradeAmountIn }, + ], + [ + poolSeat, + swapSeat, + { [getPoolKeyword(tradeAmountOut.brand)]: tradeAmountOut }, + { Out: tradeAmountOut }, + ], + ]), ); - poolSeat.decrementBy( - harden({ - [getPoolKeyword(tradeAmountOut.brand)]: tradeAmountOut, - }), - ); - swapSeat.incrementBy(harden({ Out: tradeAmountOut })); - - zcf.reallocate(swapSeat, poolSeat); - swapSeat.exit(); return `Swap successfully completed.`; } @@ -201,13 +204,13 @@ const start = async zcf => { Central: AmountMath.make(brands.Central, centralIn), Secondary: secondaryAmount, }; - poolSeat.incrementBy(seat.decrementBy(harden(liquidityDeposited))); - seat.incrementBy( - poolSeat.decrementBy(harden({ Liquidity: liquidityAmountOut })), + atomicRearrange( + zcf, + harden([ + [seat, poolSeat, liquidityDeposited], + [poolSeat, seat, { Liquidity: liquidityAmountOut }], + ]), ); - - zcf.reallocate(poolSeat, seat); - seat.exit(); return 'Added liquidity.'; }; @@ -304,16 +307,14 @@ const start = async zcf => { Secondary: newUserSecondaryAmount, }; - poolSeat.incrementBy( - removeLiqSeat.decrementBy( - harden({ Liquidity: userAllocation.Liquidity }), - ), + atomicRearrange( + zcf, + harden([ + [removeLiqSeat, poolSeat, { Liquidity: userAllocation.Liquidity }], + [poolSeat, removeLiqSeat, liquidityRemoved], + ]), ); - removeLiqSeat.incrementBy(poolSeat.decrementBy(harden(liquidityRemoved))); - - zcf.reallocate(poolSeat, removeLiqSeat); - removeLiqSeat.exit(); return 'Liquidity successfully removed.'; }; diff --git a/packages/zoe/src/contracts/barterExchange.js b/packages/zoe/src/contracts/barterExchange.js index 6673653fbe1..2ed71f86de1 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 } from '../contractSupport/index.js'; +import { satisfies, atomicRearrange } from '../contractSupport/index.js'; /** * This Barter Exchange accepts offers to trade arbitrary goods for other @@ -63,13 +63,24 @@ const start = zcf => { const matchingTrade = findMatchingTrade(offerDetails, orders); if (matchingTrade) { // reallocate by giving each side what it wants - offerDetails.seat.decrementBy(harden({ In: matchingTrade.amountOut })); - matchingTrade.seat.incrementBy(harden({ Out: matchingTrade.amountOut })); - - matchingTrade.seat.decrementBy(harden({ In: offerDetails.amountOut })); - offerDetails.seat.incrementBy(harden({ Out: offerDetails.amountOut })); + atomicRearrange( + zcf, + harden([ + [ + offerDetails.seat, + matchingTrade.seat, + { In: matchingTrade.amountOut }, + { Out: matchingTrade.amountOut }, + ], + [ + matchingTrade.seat, + offerDetails.seat, + { In: offerDetails.amountOut }, + { Out: offerDetails.amountOut }, + ], + ]), + ); - zcf.reallocate(offerDetails.seat, matchingTrade.seat); removeFromOrders(matchingTrade); offerDetails.seat.exit(); matchingTrade.seat.exit(); diff --git a/packages/zoe/src/contracts/callSpread/fundedCallSpread.js b/packages/zoe/src/contracts/callSpread/fundedCallSpread.js index 46825e29b3d..cb99af8704e 100644 --- a/packages/zoe/src/contracts/callSpread/fundedCallSpread.js +++ b/packages/zoe/src/contracts/callSpread/fundedCallSpread.js @@ -7,6 +7,7 @@ import { assertProposalShape, depositToSeat, assertNatAssetKind, + atomicRearrange, } from '../../contractSupport/index.js'; import { makePayoffHandler } from './payoffHandler.js'; import { Position } from './position.js'; @@ -111,18 +112,20 @@ const start = async zcf => { give: { Collateral: null }, want: { LongOption: null, ShortOption: null }, }); - collateralSeat.incrementBy( - creatorSeat.decrementBy(harden({ Collateral: settlementAmount })), + atomicRearrange( + zcf, + harden([ + [creatorSeat, collateralSeat, { Collateral: settlementAmount }], + [ + collateralSeat, + creatorSeat, + { + LongOption: longAmount, + ShortOption: shortAmount, + }, + ], + ]), ); - creatorSeat.incrementBy( - collateralSeat.decrementBy( - harden({ - LongOption: longAmount, - ShortOption: shortAmount, - }), - ), - ); - zcf.reallocate(collateralSeat, creatorSeat); payoffHandler.schedulePayoffs(); creatorSeat.exit(); }; diff --git a/packages/zoe/src/contracts/callSpread/payoffHandler.js b/packages/zoe/src/contracts/callSpread/payoffHandler.js index 8e91944428c..001c5a543d0 100644 --- a/packages/zoe/src/contracts/callSpread/payoffHandler.js +++ b/packages/zoe/src/contracts/callSpread/payoffHandler.js @@ -2,7 +2,11 @@ import './types.js'; import { E } from '@endo/eventual-send'; import { AmountMath } from '@agoric/ertp'; -import { getAmountOut, ceilMultiplyBy } from '../../contractSupport/index.js'; +import { + getAmountOut, + ceilMultiplyBy, + atomicTransfer, +} from '../../contractSupport/index.js'; import { Position } from './position.js'; import { calculateShares } from './calculateShares.js'; @@ -36,10 +40,7 @@ function makePayoffHandler(zcf, seatPromiseKits, collateralSeat) { function reallocateToSeat(seatPromise, seatPortion) { seatPromise.then(seat => { - seat.incrementBy( - collateralSeat.decrementBy(harden({ Collateral: seatPortion })), - ); - zcf.reallocate(seat, collateralSeat); + atomicTransfer(zcf, collateralSeat, seat, { Collateral: seatPortion }); seat.exit(); seatsExited += 1; const remainder = collateralSeat.getAmountAllocated('Collateral'); diff --git a/packages/zoe/src/contracts/callSpread/pricedCallSpread.js b/packages/zoe/src/contracts/callSpread/pricedCallSpread.js index cb1397d310f..6eead911ac8 100644 --- a/packages/zoe/src/contracts/callSpread/pricedCallSpread.js +++ b/packages/zoe/src/contracts/callSpread/pricedCallSpread.js @@ -10,6 +10,7 @@ import { assertNatAssetKind, makeRatio, ceilMultiplyBy, + atomicRearrange, } from '../../contractSupport/index.js'; import { makePayoffHandler } from './payoffHandler.js'; import { Position } from './position.js'; @@ -130,12 +131,13 @@ const start = zcf => { 'wanted option not a match', ); - depositSeat.incrementBy(collateralSeat.decrementBy(harden(spreadAmount))); - collateralSeat.incrementBy( - depositSeat.decrementBy(harden({ Collateral: newCollateral })), + atomicRearrange( + zcf, + harden([ + [collateralSeat, depositSeat, spreadAmount], + [depositSeat, collateralSeat, { Collateral: newCollateral }], + ]), ); - - zcf.reallocate(collateralSeat, depositSeat); depositSeat.exit(); }; diff --git a/packages/zoe/src/contracts/loan/addCollateral.js b/packages/zoe/src/contracts/loan/addCollateral.js index 1147579f319..959cf142cc0 100644 --- a/packages/zoe/src/contracts/loan/addCollateral.js +++ b/packages/zoe/src/contracts/loan/addCollateral.js @@ -1,4 +1,7 @@ -import { assertProposalShape } from '../../contractSupport/index.js'; +import { + assertProposalShape, + atomicTransfer, +} from '../../contractSupport/index.js'; import { scheduleLiquidation } from './scheduleLiquidation.js'; @@ -16,15 +19,10 @@ export const makeAddCollateralInvitation = (zcf, config) => { want: {}, }); - collateralSeat.incrementBy( - addCollateralSeat.decrementBy( - harden({ - Collateral: addCollateralSeat.getAmountAllocated('Collateral'), - }), - ), - ); + atomicTransfer(zcf, addCollateralSeat, collateralSeat, { + Collateral: addCollateralSeat.getAmountAllocated('Collateral'), + }); - zcf.reallocate(collateralSeat, addCollateralSeat); addCollateralSeat.exit(); // Schedule the new liquidation trigger. The old one will have an diff --git a/packages/zoe/src/contracts/loan/borrow.js b/packages/zoe/src/contracts/loan/borrow.js index 29c2e287cfa..047f0e6a6b7 100644 --- a/packages/zoe/src/contracts/loan/borrow.js +++ b/packages/zoe/src/contracts/loan/borrow.js @@ -9,6 +9,7 @@ import { getAmountOut, ceilMultiplyBy, getTimestamp, + atomicRearrange, } from '../../contractSupport/index.js'; import { scheduleLiquidation } from './scheduleLiquidation.js'; @@ -75,16 +76,15 @@ export const makeBorrowInvitation = (zcf, config) => { const { zcfSeat: collateralSeat } = zcf.makeEmptySeatKit(); - // Transfer the wanted Loan amount to the borrower - borrowerSeat.incrementBy( - lenderSeat.decrementBy(harden({ Loan: loanWanted })), - ); - - // Transfer *all* collateral to the collateral seat. - collateralSeat.incrementBy( - borrowerSeat.decrementBy(harden({ Collateral: collateralGiven })), + atomicRearrange( + zcf, + harden([ + // Transfer the wanted Loan amount to the borrower + [lenderSeat, borrowerSeat, { Loan: loanWanted }], + // Transfer *all* collateral to the collateral seat. + [borrowerSeat, collateralSeat, { Collateral: collateralGiven }], + ]), ); - zcf.reallocate(lenderSeat, borrowerSeat, collateralSeat); // We now exit the borrower seat so that the borrower gets their // loan. However, the borrower gets an object as their offerResult diff --git a/packages/zoe/src/contracts/loan/close.js b/packages/zoe/src/contracts/loan/close.js index 4e8c7b9db1a..4aee7ff39a6 100644 --- a/packages/zoe/src/contracts/loan/close.js +++ b/packages/zoe/src/contracts/loan/close.js @@ -3,7 +3,10 @@ import './types.js'; import { assert, details as X } from '@agoric/assert'; import { AmountMath } from '@agoric/ertp'; -import { assertProposalShape } from '../../contractSupport/index.js'; +import { + assertProposalShape, + atomicRearrange, +} 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 @@ -39,20 +42,17 @@ export const makeCloseLoanInvitation = (zcf, config) => { // Transfer the collateral to the repaySeat and remove the // required Loan amount. Any excess Loan amount is kept by the repaySeat. // Transfer the repaid loan amount to the lender - - repaySeat.incrementBy( - collateralSeat.decrementBy( - harden({ - Collateral: collateralSeat.getAmountAllocated( - 'Collateral', - collateralBrand, - ), - }), - ), + const collateralAmount = collateralSeat.getAmountAllocated( + 'Collateral', + collateralBrand, + ); + atomicRearrange( + zcf, + harden([ + [collateralSeat, repaySeat, { Collateral: collateralAmount }], + [repaySeat, lenderSeat, { Loan: debt }], + ]), ); - lenderSeat.incrementBy(repaySeat.decrementBy(harden({ Loan: debt }))); - - zcf.reallocate(repaySeat, collateralSeat, lenderSeat); repaySeat.exit(); lenderSeat.exit(); diff --git a/packages/zoe/src/contracts/loan/scheduleLiquidation.js b/packages/zoe/src/contracts/loan/scheduleLiquidation.js index e7736416716..3c9133f8cfc 100644 --- a/packages/zoe/src/contracts/loan/scheduleLiquidation.js +++ b/packages/zoe/src/contracts/loan/scheduleLiquidation.js @@ -2,7 +2,11 @@ import { E } from '@endo/eventual-send'; import { AmountMath } from '@agoric/ertp'; import { liquidate } from './liquidate.js'; -import { getAmountIn, ceilMultiplyBy } from '../../contractSupport/index.js'; +import { + getAmountIn, + ceilMultiplyBy, + atomicTransfer, +} from '../../contractSupport/index.js'; /** @type {ScheduleLiquidation} */ export const scheduleLiquidation = (zcf, configWithBorrower) => { @@ -51,10 +55,10 @@ export const scheduleLiquidation = (zcf, configWithBorrower) => { // collateral is on the collateral seat. If an error occurs, we // reallocate the collateral to the lender and shutdown the // contract, kicking out any remaining seats. - lenderSeat.incrementBy( - collateralSeat.decrementBy(harden({ Collateral: allCollateral })), - ); - zcf.reallocate(lenderSeat, collateralSeat); + atomicTransfer(zcf, collateralSeat, lenderSeat, { + Collateral: allCollateral, + }); + zcf.shutdownWithFailure(err); throw err; }); diff --git a/packages/zoe/src/contracts/oracle.js b/packages/zoe/src/contracts/oracle.js index fce0abd9502..86993d7eecd 100644 --- a/packages/zoe/src/contracts/oracle.js +++ b/packages/zoe/src/contracts/oracle.js @@ -3,6 +3,7 @@ import { Far } from '@endo/marshal'; import { AmountMath } from '@agoric/ertp'; import { E } from '@endo/eventual-send'; +import { atomicTransfer } from '../contractSupport/index.js'; /** * This contract provides oracle queries for a fee or for free. @@ -29,8 +30,8 @@ const start = async zcf => { ? feeSeat.getCurrentAllocation() : seat.getProposal().want; - seat.incrementBy(feeSeat.decrementBy(harden(gains))); - zcf.reallocate(seat, feeSeat); + atomicTransfer(zcf, feeSeat, seat, gains); + seat.exit(); return 'Successfully withdrawn'; }, 'withdraw'); @@ -41,10 +42,8 @@ const start = async zcf => { makeShutdownInvitation: () => { const shutdown = seat => { revoked = true; - seat.incrementBy( - feeSeat.decrementBy(harden(feeSeat.getCurrentAllocation())), - ); - zcf.reallocate(seat, feeSeat); + atomicTransfer(zcf, feeSeat, seat, feeSeat.getCurrentAllocation()); + zcf.shutdown(revokedMsg); }; return zcf.makeInvitation(shutdown, 'shutdown'); @@ -82,10 +81,7 @@ const start = async zcf => { const fee = querySeat.getAmountAllocated('Fee', feeBrand); const { requiredFee, reply } = await E(handler).onQuery(query, fee); if (requiredFee) { - feeSeat.incrementBy( - querySeat.decrementBy(harden({ Fee: requiredFee })), - ); - zcf.reallocate(feeSeat, querySeat); + atomicTransfer(zcf, querySeat, feeSeat, { Fee: requiredFee }); } querySeat.exit(); E(handler).onReply(query, reply, requiredFee); diff --git a/packages/zoe/src/contracts/otcDesk.js b/packages/zoe/src/contracts/otcDesk.js index 7b6b65d11ab..f1506d5825b 100644 --- a/packages/zoe/src/contracts/otcDesk.js +++ b/packages/zoe/src/contracts/otcDesk.js @@ -5,6 +5,7 @@ import { offerTo, saveAllIssuers, assertProposalShape, + atomicTransfer, } from '../contractSupport/index.js'; /** @@ -96,10 +97,8 @@ const start = zcf => { const addInventory = seat => { assertProposalShape(seat, { want: {} }); // Take everything in this seat and add it to the marketMakerSeat - marketMakerSeat.incrementBy( - seat.decrementBy(harden(seat.getCurrentAllocation())), - ); - zcf.reallocate(marketMakerSeat, seat); + atomicTransfer(zcf, seat, marketMakerSeat, seat.getCurrentAllocation()); + seat.exit(); return 'Inventory added'; }; @@ -107,8 +106,8 @@ const start = zcf => { const removeInventory = seat => { assertProposalShape(seat, { give: {} }); const { want } = seat.getProposal(); - seat.incrementBy(marketMakerSeat.decrementBy(harden(want))); - zcf.reallocate(marketMakerSeat, seat); + atomicTransfer(zcf, marketMakerSeat, seat, want); + seat.exit(); return 'Inventory removed'; }; diff --git a/packages/zoe/src/contracts/sellItems.js b/packages/zoe/src/contracts/sellItems.js index 47838c83f28..b0609556e61 100644 --- a/packages/zoe/src/contracts/sellItems.js +++ b/packages/zoe/src/contracts/sellItems.js @@ -7,6 +7,7 @@ import { defaultAcceptanceMsg, assertProposalShape, assertNatAssetKind, + atomicRearrange, } from '../contractSupport/index.js'; const { details: X } = assert; @@ -101,13 +102,13 @@ const start = zcf => { assert.fail(X`More money (${totalCost}) is required to buy these items`); // Reallocate. - sellerSeat.incrementBy( - buyerSeat.decrementBy(harden({ Money: providedMoney })), + atomicRearrange( + zcf, + harden([ + [buyerSeat, sellerSeat, { Money: providedMoney }], + [sellerSeat, buyerSeat, { Items: wantedItems }], + ]), ); - buyerSeat.incrementBy( - sellerSeat.decrementBy(harden({ Items: wantedItems })), - ); - zcf.reallocate(buyerSeat, sellerSeat); // The buyer's offer has been processed. buyerSeat.exit();