diff --git a/packages/zoe/src/contractSupport/index.js b/packages/zoe/src/contractSupport/index.js index 7fb1b2a6df5..a096590eccb 100644 --- a/packages/zoe/src/contractSupport/index.js +++ b/packages/zoe/src/contractSupport/index.js @@ -36,6 +36,7 @@ export { withdrawFromSeat, saveAllIssuers, offerTo, + checkZCF, } from './zoeHelpers'; export { diff --git a/packages/zoe/src/contractSupport/zoeHelpers.js b/packages/zoe/src/contractSupport/zoeHelpers.js index 6daa328c9d0..905f03109ad 100644 --- a/packages/zoe/src/contractSupport/zoeHelpers.js +++ b/packages/zoe/src/contractSupport/zoeHelpers.js @@ -476,3 +476,24 @@ export const offerTo = async ( return harden({ userSeatPromise, deposited: depositedPromiseKit.promise }); }; + +/** + * Create a wrapped version of zcf that asserts an invariant + * before performing a reallocation. + * + * @param {ContractFacet} zcf + * @param {(stagings: SeatStaging[]) => void} assertFn - an assertion + * that must be true for the reallocate to occur + * @returns {ContractFacet} + */ +export const checkZCF = (zcf, assertFn) => { + const checkedZCF = harden({ + ...zcf, + reallocate: (...stagings) => { + assertFn(stagings); + // @ts-ignore The types aren't right for spreading + zcf.reallocate(...stagings); + }, + }); + return checkedZCF; +}; diff --git a/packages/zoe/src/contracts/multipoolAutoswap/constantProduct.js b/packages/zoe/src/contracts/multipoolAutoswap/constantProduct.js new file mode 100644 index 00000000000..147105f22ce --- /dev/null +++ b/packages/zoe/src/contracts/multipoolAutoswap/constantProduct.js @@ -0,0 +1,38 @@ +// @ts-check + +import { assert, details as X } from '@agoric/assert'; +import { natSafeMath } from '../../contractSupport'; + +// A pool seat has Central and Secondary keywords, and a swap seat has +// In and Out keywords +const isPoolSeat = allocation => { + return allocation.Central !== undefined || allocation.Secondary !== undefined; +}; + +const calcK = allocation => { + return natSafeMath.multiply( + allocation.Secondary.value, + allocation.Central.value, + ); +}; + +/** + * + * @param {SeatStaging[]} stagings + */ +export const assertConstantProduct = stagings => { + stagings.forEach(seatStaging => { + const seat = seatStaging.getSeat(); + const priorAllocation = seat.getCurrentAllocation(); + const stagedAllocation = seatStaging.getStagedAllocation(); + if (isPoolSeat(stagedAllocation)) { + const oldK = calcK(priorAllocation); + const newK = calcK(stagedAllocation); + console.log('oldK', oldK, 'newK', newK); + assert( + newK >= oldK, + X`the product of the pool tokens must not decrease as the result of a trade. ${oldK} decreased to ${newK}`, + ); + } + }); +}; diff --git a/packages/zoe/src/contracts/multipoolAutoswap/multipoolAutoswap.js b/packages/zoe/src/contracts/multipoolAutoswap/multipoolAutoswap.js index fd9226ab7dc..909e19061e3 100644 --- a/packages/zoe/src/contracts/multipoolAutoswap/multipoolAutoswap.js +++ b/packages/zoe/src/contracts/multipoolAutoswap/multipoolAutoswap.js @@ -5,12 +5,13 @@ import { makeWeakStore } from '@agoric/store'; import { Far } from '@agoric/marshal'; import { AssetKind, makeIssuerKit, AmountMath } from '@agoric/ertp'; -import { assertIssuerKeywords } from '../../contractSupport'; +import { assertIssuerKeywords, checkZCF } from '../../contractSupport'; import { makeAddPool } from './pool'; import { makeGetCurrentPrice } from './getCurrentPrice'; import { makeMakeSwapInvitation } from './swap'; import { makeMakeAddLiquidityInvitation } from './addLiquidity'; import { makeMakeRemoveLiquidityInvitation } from './removeLiquidity'; +import { assertConstantProduct } from './constantProduct'; import '../../../exported'; @@ -126,7 +127,7 @@ const start = zcf => { makeSwapInInvitation, makeSwapOutInvitation, } = makeMakeSwapInvitation( - zcf, + checkZCF(zcf, assertConstantProduct), isSecondary, isCentral, getPool, diff --git a/packages/zoe/test/unitTests/contracts/multipoolAutoswap/test-constantProduct.js b/packages/zoe/test/unitTests/contracts/multipoolAutoswap/test-constantProduct.js new file mode 100644 index 00000000000..b4d33575799 --- /dev/null +++ b/packages/zoe/test/unitTests/contracts/multipoolAutoswap/test-constantProduct.js @@ -0,0 +1,64 @@ +// @ts-check + +// eslint-disable-next-line import/no-extraneous-dependencies +import { test } from '@agoric/zoe/tools/prepare-test-env-ava'; + +import { AmountMath } from '@agoric/ertp'; + +import { checkZCF } from '../../../../src/contractSupport'; +import { assertConstantProduct } from '../../../../src/contracts/multipoolAutoswap/constantProduct'; +import { setupZCFTest } from '../../zcf/setupZcfTest'; + +test('constantProduct invariant', async t => { + const { zcf } = await setupZCFTest(); + + const checkedZCF = checkZCF(zcf, assertConstantProduct); + + const { zcfSeat: poolSeat } = checkedZCF.makeEmptySeatKit(); + const { zcfSeat: swapSeat } = checkedZCF.makeEmptySeatKit(); + + // allocate some secondary and central to the poolSeat + const centralMint = await checkedZCF.makeZCFMint('Central'); + const { brand: centralBrand } = centralMint.getIssuerRecord(); + const secondaryMint = await checkedZCF.makeZCFMint('Secondary'); + const { brand: secondaryBrand } = secondaryMint.getIssuerRecord(); + centralMint.mintGains( + { Central: AmountMath.make(centralBrand, 10n ** 6n) }, + poolSeat, + ); + secondaryMint.mintGains( + { Secondary: AmountMath.make(secondaryBrand, 10n ** 6n) }, + poolSeat, + ); + + const poolSeatAllocation = poolSeat.getCurrentAllocation(); + t.deepEqual(poolSeatAllocation, { + Central: AmountMath.make(centralBrand, 10n ** 6n), + Secondary: AmountMath.make(secondaryBrand, 10n ** 6n), + }); + + // const oldK = + // poolSeatAllocation.Secondary.value * poolSeatAllocation.Central.value; + + // const newK = 0; + + // Let's give the swap user all the tokens and take + // nothing, a clear violation of the constant product + t.throws( + () => + checkedZCF.reallocate( + poolSeat.stage({ + Central: AmountMath.make(centralBrand, 0n), + Secondary: AmountMath.make(secondaryBrand, 0n), + }), + swapSeat.stage({ + In: poolSeatAllocation.Central, + Out: poolSeatAllocation.Secondary, + }), + ), + { + message: + 'the product of the pool tokens must not decrease as the result of a trade. "[1000000000000n]" decreased to "[0n]"', + }, + ); +}); diff --git a/packages/zoe/test/unitTests/contracts/test-multipoolAutoswap.js b/packages/zoe/test/unitTests/contracts/multipoolAutoswap/test-multipoolAutoswap.js similarity index 99% rename from packages/zoe/test/unitTests/contracts/test-multipoolAutoswap.js rename to packages/zoe/test/unitTests/contracts/multipoolAutoswap/test-multipoolAutoswap.js index f2c9e70e0ff..e7aa7153df1 100644 --- a/packages/zoe/test/unitTests/contracts/test-multipoolAutoswap.js +++ b/packages/zoe/test/unitTests/contracts/multipoolAutoswap/test-multipoolAutoswap.js @@ -6,23 +6,26 @@ import { test } from '@agoric/zoe/tools/prepare-test-env-ava'; import bundleSource from '@agoric/bundle-source'; import { makeIssuerKit, amountMath } from '@agoric/ertp'; import { E } from '@agoric/eventual-send'; -import fakeVatAdmin from '../../../tools/fakeVatAdmin'; +import fakeVatAdmin from '../../../../tools/fakeVatAdmin'; // noinspection ES6PreferShortImport -import { makeZoe } from '../../../src/zoeService/zoe'; -import { setup } from '../setupBasicMints'; +import { makeZoe } from '../../../../src/zoeService/zoe'; +import { setup } from '../../setupBasicMints'; import { makeTrader, updatePoolState, scaleForAddLiquidity, scaleForRemoveLiquidity, priceFromTargetOutput, -} from '../../autoswapJig'; -import { assertPayoutDeposit, assertAmountsEqual } from '../../zoeTestHelpers'; -import buildManualTimer from '../../../tools/manualTimer'; -import { getAmountOut } from '../../../src/contractSupport'; +} from '../../../autoswapJig'; +import { + assertPayoutDeposit, + assertAmountsEqual, +} from '../../../zoeTestHelpers'; +import buildManualTimer from '../../../../tools/manualTimer'; +import { getAmountOut } from '../../../../src/contractSupport'; -const multipoolAutoswapRoot = `${__dirname}/../../../src/contracts/multipoolAutoswap/multipoolAutoswap`; +const multipoolAutoswapRoot = `${__dirname}/../../../../src/contracts/multipoolAutoswap/multipoolAutoswap`; test('multipoolAutoSwap with valid offers', async t => { const { moolaR, simoleanR, moola, simoleans } = setup(); diff --git a/packages/zoe/test/unitTests/contracts/test-multipoolPriceAuthority.js b/packages/zoe/test/unitTests/contracts/multipoolAutoswap/test-multipoolPriceAuthority.js similarity index 88% rename from packages/zoe/test/unitTests/contracts/test-multipoolPriceAuthority.js rename to packages/zoe/test/unitTests/contracts/multipoolAutoswap/test-multipoolPriceAuthority.js index 625640dd37e..f5e9dde0180 100644 --- a/packages/zoe/test/unitTests/contracts/test-multipoolPriceAuthority.js +++ b/packages/zoe/test/unitTests/contracts/multipoolAutoswap/test-multipoolPriceAuthority.js @@ -4,9 +4,9 @@ import { test } from '@agoric/zoe/tools/prepare-test-env-ava'; import { makeNotifierKit } from '@agoric/notifier'; import { makeIssuerKit, amountMath, AssetKind } from '@agoric/ertp'; -import { makePriceAuthority } from '../../../src/contracts/multipoolAutoswap/priceAuthority'; -import { setup } from '../setupBasicMints'; -import buildManualTimer from '../../../tools/manualTimer'; +import { makePriceAuthority } from '../../../../src/contracts/multipoolAutoswap/priceAuthority'; +import { setup } from '../../setupBasicMints'; +import buildManualTimer from '../../../../tools/manualTimer'; test('multipoolAutoSwap PriceAuthority exception path', async t => { const { moolaR, simoleanR } = setup();