From 59b761de66f549f8ba17b64da2186706bcdd5ada Mon Sep 17 00:00:00 2001 From: Kate Sills Date: Tue, 17 Aug 2021 16:49:17 -0700 Subject: [PATCH 1/8] chore: make code accessible --- .../src/contracts/constantProduct/calcFees.js | 58 +++++++++ .../constantProduct/checkInvariants.js | 44 +++++++ .../zoe/src/contracts/constantProduct/core.js | 110 ++++++++++++++++ .../src/contracts/constantProduct/defaults.js | 5 + .../src/contracts/constantProduct/getXY.js | 41 ++++++ .../contracts/constantProduct/invariants.js | 91 ++++++++++++++ .../zoe/src/contracts/constantProduct/swap.js | 117 ++++++++++++++++++ .../src/contracts/constantProduct/swapIn.js | 21 ++++ .../src/contracts/constantProduct/swapOut.js | 21 ++++ 9 files changed, 508 insertions(+) create mode 100644 packages/zoe/src/contracts/constantProduct/calcFees.js create mode 100644 packages/zoe/src/contracts/constantProduct/checkInvariants.js create mode 100644 packages/zoe/src/contracts/constantProduct/core.js create mode 100644 packages/zoe/src/contracts/constantProduct/defaults.js create mode 100644 packages/zoe/src/contracts/constantProduct/getXY.js create mode 100644 packages/zoe/src/contracts/constantProduct/invariants.js create mode 100644 packages/zoe/src/contracts/constantProduct/swap.js create mode 100644 packages/zoe/src/contracts/constantProduct/swapIn.js create mode 100644 packages/zoe/src/contracts/constantProduct/swapOut.js diff --git a/packages/zoe/src/contracts/constantProduct/calcFees.js b/packages/zoe/src/contracts/constantProduct/calcFees.js new file mode 100644 index 00000000000..1cb8afb8281 --- /dev/null +++ b/packages/zoe/src/contracts/constantProduct/calcFees.js @@ -0,0 +1,58 @@ +// @ts-check + +import { AmountMath } from '@agoric/ertp'; +import { multiplyByCeilDivide, makeRatio } from '../../contractSupport/ratio'; + +import { BASIS_POINTS } from './defaults'; + +/** + * Make a ratio given a nat representing basis points + * + * @param {NatValue} feeBP + * @param {Brand} brandOfFee + * @returns {Ratio} + */ +const makeFeeRatio = (feeBP, brandOfFee) => { + return makeRatio(feeBP, brandOfFee, BASIS_POINTS); +}; + +const minimum = (left, right) => { + // If left is greater or equal, return right. Otherwise return left. + return AmountMath.isGTE(left, right) ? right : left; +}; + +/** + * @param {{ amountIn: Amount, amountOut: Amount}} amounts - an array of two amounts in different + * brands. We must select the amount of the same brand as the feeRatio. + * @param {Ratio} feeRatio + * @returns {Amount} + */ +const calcFee = ({ amountIn, amountOut }, feeRatio) => { + const sameBrandAmount = + amountIn.brand === feeRatio.numerator.brand ? amountIn : amountOut; + // Always round fees up + const fee = multiplyByCeilDivide(sameBrandAmount, feeRatio); + + // Fee cannot be more than what exists + return minimum(fee, sameBrandAmount); +}; + +// SwapIn uses calcDeltaYSellingX +// SwapOut uses calcDeltaXSellingX + +export const calculateFees = ( + amountGiven, + poolAllocation, + amountWanted, + protocolFeeRatio, + poolFeeRatio, + swapFn, +) => { + // Get a rough estimation in both brands of the amount to be swapped + const estimation = swapFn({ amountGiven, poolAllocation, amountWanted }); + + const protocolFee = calcFee(estimation, protocolFeeRatio); + const poolFee = calcFee(estimation, poolFeeRatio); + + return harden({ protocolFee, poolFee, ...estimation }); +}; diff --git a/packages/zoe/src/contracts/constantProduct/checkInvariants.js b/packages/zoe/src/contracts/constantProduct/checkInvariants.js new file mode 100644 index 00000000000..2bd1a670d56 --- /dev/null +++ b/packages/zoe/src/contracts/constantProduct/checkInvariants.js @@ -0,0 +1,44 @@ +// @ts-check + +import { assertRightsConserved } from '../../contractFacet/rightsConservation'; + +import { + assertKInvariantSellingX, + assertPoolFee, + assertProtocolFee, +} from './invariants'; + +export const checkAllInvariants = ( + runPoolAllocation, + secondaryPoolAllocation, + runAmountIn, + protocolFeeBP, + poolFeeBP, + result, +) => { + // double check invariants + assertKInvariantSellingX( + runPoolAllocation, + secondaryPoolAllocation, + result.deltaRun, + result.deltaSecondary, + ); + + const priorAmounts = [ + runPoolAllocation, + secondaryPoolAllocation, + runAmountIn, + ]; + const newAmounts = [ + result.newRunPool, + result.protocolFee, + result.newSecondaryPool, + result.amountOut, + result.poolFee, + result.inReturnedToUser, + ]; + + assertRightsConserved(priorAmounts, newAmounts); + assertProtocolFee(result.protocolFee, result.amountIn, protocolFeeBP); + assertPoolFee(result.poolFee, result.amountOut, poolFeeBP); +}; diff --git a/packages/zoe/src/contracts/constantProduct/core.js b/packages/zoe/src/contracts/constantProduct/core.js new file mode 100644 index 00000000000..8b5b5243083 --- /dev/null +++ b/packages/zoe/src/contracts/constantProduct/core.js @@ -0,0 +1,110 @@ +// @ts-check + +import { AmountMath } from '@agoric/ertp'; + +import { natSafeMath } from '../../contractSupport'; +import { makeRatioFromAmounts } from '../../contractSupport/ratio'; +import { getXY } from './getXY'; + +// TODO: fix this up with more assertions and rename +// Used for multiplying y by a ratio with both numerators and +// denominators of brand x +/** + * @param {Amount} amount + * @param {Ratio} ratio + * @returns {Amount} + */ +const multiplyByOtherBrandFloorDivide = (amount, ratio) => { + const value = natSafeMath.floorDivide( + natSafeMath.multiply(amount.value, ratio.numerator.value), + ratio.denominator.value, + ); + return AmountMath.make(amount.brand, value); +}; + +// TODO: fix this up with more assertions and rename +// Used for multiplying y by a ratio with both numerators and +// denominators of brand x +/** + * @param {Amount} amount + * @param {Ratio} ratio + * @returns {Amount} + */ +const multiplyByOtherBrandCeilDivide = (amount, ratio) => { + const value = natSafeMath.ceilDivide( + natSafeMath.multiply(amount.value, ratio.numerator.value), + ratio.denominator.value, + ); + return AmountMath.make(amount.brand, value); +}; + +/** + * Calculate deltaY when user is selling brand X. This calculates how much of + * brand Y to give the user in return. + * + * deltaY = (deltaXToX/(1 + deltaXToX))*y + * Equivalently: (deltaX / (deltaX + x)) * y + * + * @param {Amount} x - the amount of Brand X in pool, xPoolAllocation + * @param {Amount} y - the amount of Brand Y in pool, yPoolAllocation + * @param {Amount} deltaX - the amount of Brand X to be added + * @returns {Amount} deltaY - the amount of Brand Y to be taken out + */ +export const calcDeltaYSellingX = (x, y, deltaX) => { + const deltaXPlusX = AmountMath.add(deltaX, x); + const xRatio = makeRatioFromAmounts(deltaX, deltaXPlusX); + // Result is an amount in y.brand + // We would want to err on the side of the pool, so this should be a + // floorDivide so that less deltaY is given out + return multiplyByOtherBrandFloorDivide(y, xRatio); +}; + +/** + * Calculate deltaX when user is selling brand X. This allows us to give the user a + * small refund if the amount they will as a payout could have been + * achieved by a smaller input. + * + * deltaX = (deltaYToY/(1 - deltaYToY))*x + * Equivalently: (deltaY / (y - deltaY )) * x + * + * @param {Amount} x - the amount of Brand X in pool, xPoolAllocation + * @param {Amount} y - the amount of Brand Y in pool, yPoolAllocation + * @param {Amount} deltaY - the amount of Brand Y to be taken out + * @returns {Amount} deltaX - the amount of Brand X to be added + */ +export const calcDeltaXSellingX = (x, y, deltaY) => { + const yMinusDeltaY = AmountMath.subtract(y, deltaY); + const yRatio = makeRatioFromAmounts(deltaY, yMinusDeltaY); + // Result is an amount in x.brand + // We want to err on the side of the pool, so this should be a + // ceiling divide so that more deltaX is taken + return multiplyByOtherBrandCeilDivide(x, yRatio); +}; + +const swapInReduced = ({ x, y, deltaX }) => { + const deltaY = calcDeltaYSellingX(x, y, deltaX); + const reducedDeltaX = calcDeltaXSellingX(x, y, deltaY); + return harden({ + amountIn: reducedDeltaX, + amountOut: deltaY, + }); +}; + +const swapOutImproved = ({ x, y, wantedDeltaY }) => { + const requiredDeltaX = calcDeltaXSellingX(x, y, wantedDeltaY); + const improvedDeltaY = calcDeltaYSellingX(x, y, requiredDeltaX); + return harden({ + amountIn: requiredDeltaX, + amountOut: improvedDeltaY, + }); +}; + +export const swapInNoFees = ({ amountGiven, poolAllocation }) => { + const XY = getXY({ amountGiven, poolAllocation }); + return swapInReduced(XY); +}; + +export const swapOutNoFees = ({ poolAllocation, amountWanted }) => { + const XY = getXY({ poolAllocation, amountWanted }); + return swapOutImproved(XY); +}; diff --git a/packages/zoe/src/contracts/constantProduct/defaults.js b/packages/zoe/src/contracts/constantProduct/defaults.js new file mode 100644 index 00000000000..4cddc39f4c1 --- /dev/null +++ b/packages/zoe/src/contracts/constantProduct/defaults.js @@ -0,0 +1,5 @@ +// @ts-check + +export const BASIS_POINTS = 10000n; +export const DEFAULT_POOL_FEE = 24n; // 0.0024 or .24% +export const DEFAULT_PROTOCOL_FEE = 6n; // .0006 or .06% diff --git a/packages/zoe/src/contracts/constantProduct/getXY.js b/packages/zoe/src/contracts/constantProduct/getXY.js new file mode 100644 index 00000000000..f5fcd5e48fb --- /dev/null +++ b/packages/zoe/src/contracts/constantProduct/getXY.js @@ -0,0 +1,41 @@ +// This does not support secondary to secondary. That has to happen at +// a higher abstraction + +/** + * + * @param {Object} opt + * @param {Amount=} opt.amountGiven + * @param {{ Central: Amount, Secondary: Amount }} opt.poolAllocation + * @param {Amount=} opt.amountWanted + * @returns {{ x: Amount, y: Amount, deltaX: Amount, wantedDeltaY: + * Amount }} + */ +export const getXY = ({ amountGiven, poolAllocation, amountWanted }) => { + // Regardless of whether we are specifying the amountIn or the + // amountOut, the xBrand is the brand of the amountIn. + const xBrand = amountGiven && amountGiven.brand; + const yBrand = amountWanted && amountWanted.brand; + const secondaryBrand = poolAllocation.Secondary.brand; + const centralBrand = poolAllocation.Central.brand; + + const deltas = { + deltaX: amountGiven, + wantedDeltaY: amountWanted, + }; + + if (secondaryBrand === xBrand || centralBrand === yBrand) { + return harden({ + x: poolAllocation.Secondary, + y: poolAllocation.Central, + ...deltas, + }); + } + if (centralBrand === xBrand || secondaryBrand === yBrand) { + return harden({ + x: poolAllocation.Central, + y: poolAllocation.Secondary, + ...deltas, + }); + } + assert.fail(`brand ${xBrand} was not recognized as Central or Secondary`); +}; diff --git a/packages/zoe/src/contracts/constantProduct/invariants.js b/packages/zoe/src/contracts/constantProduct/invariants.js new file mode 100644 index 00000000000..c4b84613173 --- /dev/null +++ b/packages/zoe/src/contracts/constantProduct/invariants.js @@ -0,0 +1,91 @@ +// @ts-check + +import { assert, details as X } from '@agoric/assert'; +import { AmountMath } from '@agoric/ertp'; + +import { makeRatioFromAmounts } from '../../contractSupport/ratio'; +import { natSafeMath } from '../../contractSupport'; + +import { BASIS_POINTS } from './defaults'; + +/** + * xy <= (x + deltaX)(y - deltaY) + * + * @param {Amount} x - the amount of Brand X in pool, xPoolAllocation + * @param {Amount} y - the amount of Brand Y in pool, yPoolAllocation + * @param {Amount} deltaX - the amount of Brand X to be added + * @param {Amount} deltaY - the amount of Brand Y to be taken out + */ +export const checkKInvariantSellingX = (x, y, deltaX, deltaY) => { + const oldK = natSafeMath.multiply(x.value, y.value); + const newX = AmountMath.add(x, deltaX); + const newY = AmountMath.subtract(y, deltaY); + const newK = natSafeMath.multiply(newX.value, newY.value); + return oldK <= newK; +}; + +/** + * xy <= (x + deltaX)(y - deltaY) + * + * @param {Amount} x - the amount of Brand X in pool, xPoolAllocation + * @param {Amount} y - the amount of Brand Y in pool, yPoolAllocation + * @param {Amount} deltaX - the amount of Brand X to be added + * @param {Amount} deltaY - the amount of Brand Y to be taken out + */ +export const assertKInvariantSellingX = (x, y, deltaX, deltaY) => { + const oldK = natSafeMath.multiply(x.value, y.value); + const newX = AmountMath.add(x, deltaX); + const newY = AmountMath.subtract(y, deltaY); + const newK = natSafeMath.multiply(newX.value, newY.value); + assert( + oldK <= newK, + X`the constant product invariant was violated, with x=${x}, y=${y}, deltaX=${deltaX}, deltaY=${deltaY}, oldK=${oldK}, newK=${newK}`, + ); +}; + +/** + * Assert that the protocolFee amount is greater than the specified + * basisPoints, given protocolFee as a fraction of amountIn (includes protocolFee) + * + * @param {Amount} protocolFee + * @param {Amount} amountIn + * @param {bigint} protocolFeeBP + * @returns {void} + */ +export const assertProtocolFee = (protocolFee, amountIn, protocolFeeBP) => { + const protocolFeeRatio = makeRatioFromAmounts(protocolFee, amountIn); + + const approximationBP = + (Number(protocolFeeRatio.numerator.value) * Number(BASIS_POINTS)) / + Number(protocolFeeRatio.denominator.value); + + assert( + approximationBP >= protocolFeeBP, + X`actualProtocolFeeBP was not greater: ${protocolFeeRatio}`, + ); +}; + +/** + * Assert that the poolFee amount is greater than the specified + * basisPoints, given poolFee as a fraction of amountOut + poolFee + * + * @param {Amount} poolFee + * @param {Amount} amountOut + * @param {bigint} poolFeeBP + * @returns {void} + */ +export const assertPoolFee = (poolFee, amountOut, poolFeeBP) => { + if (AmountMath.isEmpty(amountOut)) { + return; + } + const poolFeeRatio = makeRatioFromAmounts( + poolFee, + AmountMath.add(amountOut, poolFee), + ); + + const approximationBP = + (Number(poolFeeRatio.numerator.value) * Number(BASIS_POINTS)) / + Number(poolFeeRatio.denominator.value); + + assert(approximationBP >= poolFeeBP); +}; diff --git a/packages/zoe/src/contracts/constantProduct/swap.js b/packages/zoe/src/contracts/constantProduct/swap.js new file mode 100644 index 00000000000..ba8f834de6c --- /dev/null +++ b/packages/zoe/src/contracts/constantProduct/swap.js @@ -0,0 +1,117 @@ +// @ts-check + +import { assert, details as X } from '@agoric/assert'; +import { AmountMath } from '@agoric/ertp'; +import { calculateFees } from './calcFees'; + +const subtractRelevantFees = (amount, fee) => { + if (amount.brand === fee.brand) { + return AmountMath.subtract(amount, fee); + } + return amount; +}; + +const subtractFees = (amount, { poolFee, protocolFee }) => { + return subtractRelevantFees( + subtractRelevantFees(amount, protocolFee), + poolFee, + ); +}; + +const addRelevantFees = (amount, fee) => { + if (amount.brand === fee.brand) { + return AmountMath.add(amount, fee); + } + return amount; +}; + +const addFees = (amount, { poolFee, protocolFee }) => { + return addRelevantFees(addRelevantFees(amount, protocolFee), poolFee); +}; + +const addOrSubtractFromPool = (addOrSub, poolAllocation, amount) => { + if (poolAllocation.Central.brand === amount.brand) { + return addOrSub(poolAllocation.Central, amount); + } else { + return addOrSub(poolAllocation.Secondary, amount); + } +}; + +const assertGreaterThanZeroHelper = (amount, name) => { + assert( + amount && !AmountMath.isGTE(AmountMath.makeEmptyFromAmount(amount), amount), + X`${name} must be greater than 0: ${amount}`, + ); +}; + +const assertWantedAvailable = (poolAllocation, amountWanted) => { + if (amountWanted.brand === poolAllocation.Central.brand) { + assert( + AmountMath.isGTE(poolAllocation.Central, amountWanted), + X`The poolAllocation ${poolAllocation.Central} did not have enough to satisfy the wanted amountOut ${amountWanted}`, + ); + } else { + assert( + !AmountMath.isGTE(amountWanted, poolAllocation.Secondary), + X`The poolAllocation ${poolAllocation.Secondary} did not have enough to satisfy the wanted amountOut ${amountWanted}`, + ); + } +}; + +export const swap = ( + amountGiven, + poolAllocation, + amountWanted, + protocolFeeRatio, + poolFeeRatio, + swapFn, +) => { + assertGreaterThanZeroHelper(poolAllocation.Central, 'poolAllocation.Central'); + assertGreaterThanZeroHelper( + poolAllocation.Secondary, + 'poolAllocation.Secondary', + ); + assertGreaterThanZeroHelper(amountGiven, 'amountGiven'); + assertGreaterThanZeroHelper(amountWanted, 'amountWanted'); + assertWantedAvailable(poolAllocation, amountWanted); + + // The protocol fee must always be collected in RUN, but the pool + // fee is collected in the amount opposite of what is specified. + + const fees = calculateFees( + amountGiven, + poolAllocation, + amountWanted, + protocolFeeRatio, + poolFeeRatio, + swapFn, + ); + + const { amountIn, amountOut } = swapFn({ + amountGiven: subtractFees(fees.amountIn, fees), + poolAllocation, + amountWanted, + }); + + const swapperGives = addFees(amountIn, fees); + const swapperGets = subtractFees(amountOut, fees); + + // assert( + // AmountMath.isGTE(amountGiven, swapperGives), + // X`The amount provided ${amountGiven} is not enough. ${swapperGives} is required.`, + // ); + + const result = { + protocolFee: fees.protocolFee, + poolFee: fees.poolFee, + swapperGives, + swapperGets, + // swapperGiveRefund: AmountMath.subtract(amountGiven, swapperGives), + deltaX: amountIn, + deltaY: amountOut, + newX: addOrSubtractFromPool(AmountMath.add, poolAllocation, amountIn), + newY: addOrSubtractFromPool(AmountMath.subtract, poolAllocation, amountOut), + }; + + return result; +}; diff --git a/packages/zoe/src/contracts/constantProduct/swapIn.js b/packages/zoe/src/contracts/constantProduct/swapIn.js new file mode 100644 index 00000000000..b77f3d0ac38 --- /dev/null +++ b/packages/zoe/src/contracts/constantProduct/swapIn.js @@ -0,0 +1,21 @@ +// @ts-check + +import { swap } from './swap'; +import { swapInNoFees } from './core'; + +export const swapIn = ( + amountGiven, + poolAllocation, + amountWanted, + protocolFeeRatio, + poolFeeRatio, +) => { + return swap( + amountGiven, + poolAllocation, + amountWanted, + protocolFeeRatio, + poolFeeRatio, + swapInNoFees, + ); +}; diff --git a/packages/zoe/src/contracts/constantProduct/swapOut.js b/packages/zoe/src/contracts/constantProduct/swapOut.js new file mode 100644 index 00000000000..a869b331b37 --- /dev/null +++ b/packages/zoe/src/contracts/constantProduct/swapOut.js @@ -0,0 +1,21 @@ +// @ts-check + +import { swap } from './swap'; +import { swapOutNoFees } from './core'; + +export const swapOut = ( + amountGiven, + poolAllocation, + amountWanted, + protocolFeeRatio, + poolFeeRatio, +) => { + return swap( + amountGiven, + poolAllocation, + amountWanted, + protocolFeeRatio, + poolFeeRatio, + swapOutNoFees, + ); +}; From 968223e4082104ea5f8560927c37a197bf4e87e3 Mon Sep 17 00:00:00 2001 From: Kate Sills Date: Wed, 18 Aug 2021 16:05:14 -0700 Subject: [PATCH 2/8] chore: add old tests --- .../test-calcDeltaY-calcDeltaX.js | 43 +++ .../propertyBased/test-calcDeltaY-property.js | 39 +++ .../propertyBased/test-largeValues.js | 28 ++ .../propertyBased/test-reduction.js | 43 +++ .../propertyBased/test-smallValues.js | 26 ++ .../contracts/constantProduct/runTest.js | 38 +++ .../contracts/constantProduct/setupMints.js | 19 ++ .../constantProduct/test-calcDeltaY.js | 69 +++++ .../test-compareBondingCurves.js | 286 +++++++++++++++++ .../test-compareNewSwapPrice.js | 292 ++++++++++++++++++ .../constantProduct/test-edgeCases.js | 25 ++ .../contracts/constantProduct/test-getXY.js | 50 +++ .../constantProduct/test-newBondingCurve.js | 177 +++++++++++ 13 files changed, 1135 insertions(+) create mode 100644 packages/zoe/test/unitTests/contracts/constantProduct/propertyBased/test-calcDeltaY-calcDeltaX.js create mode 100644 packages/zoe/test/unitTests/contracts/constantProduct/propertyBased/test-calcDeltaY-property.js create mode 100644 packages/zoe/test/unitTests/contracts/constantProduct/propertyBased/test-largeValues.js create mode 100644 packages/zoe/test/unitTests/contracts/constantProduct/propertyBased/test-reduction.js create mode 100644 packages/zoe/test/unitTests/contracts/constantProduct/propertyBased/test-smallValues.js create mode 100644 packages/zoe/test/unitTests/contracts/constantProduct/runTest.js create mode 100644 packages/zoe/test/unitTests/contracts/constantProduct/setupMints.js create mode 100644 packages/zoe/test/unitTests/contracts/constantProduct/test-calcDeltaY.js create mode 100644 packages/zoe/test/unitTests/contracts/constantProduct/test-compareBondingCurves.js create mode 100644 packages/zoe/test/unitTests/contracts/constantProduct/test-compareNewSwapPrice.js create mode 100644 packages/zoe/test/unitTests/contracts/constantProduct/test-edgeCases.js create mode 100644 packages/zoe/test/unitTests/contracts/constantProduct/test-getXY.js create mode 100644 packages/zoe/test/unitTests/contracts/constantProduct/test-newBondingCurve.js diff --git a/packages/zoe/test/unitTests/contracts/constantProduct/propertyBased/test-calcDeltaY-calcDeltaX.js b/packages/zoe/test/unitTests/contracts/constantProduct/propertyBased/test-calcDeltaY-calcDeltaX.js new file mode 100644 index 00000000000..66d37b059fa --- /dev/null +++ b/packages/zoe/test/unitTests/contracts/constantProduct/propertyBased/test-calcDeltaY-calcDeltaX.js @@ -0,0 +1,43 @@ +// @ts-check + +// eslint-disable-next-line import/no-extraneous-dependencies +import { test } from '@agoric/zoe/tools/prepare-test-env-ava'; + +// eslint-disable-next-line import/no-extraneous-dependencies +import jsc from 'jsverify'; +import { AmountMath } from '@agoric/ertp'; + +import { + calcDeltaYSellingX, + calcDeltaXSellingX, +} from '../../../../../src/contracts/constantProduct/core'; +import { setupMintKits } from '../setupMints'; + +const doTest = (x, y, deltaX) => { + const { run, bld } = setupMintKits(); + const runX = run(x); + const bldY = bld(y); + const runDeltaX = run(deltaX); + const deltaY = calcDeltaYSellingX(runX, bldY, runDeltaX); + const newDeltaX = calcDeltaXSellingX(runX, bldY, deltaY); + + // Pass through again, should always get the same answer. + const newDeltaY = calcDeltaYSellingX(runX, bldY, newDeltaX); + + return AmountMath.isEqual(deltaY, newDeltaY); +}; + +test('jsverify constant product calcDeltaYSellingX', t => { + const runPoolAllocationArbitrary = jsc.suchthat(jsc.nat(), u => u > 1); + const secondaryPoolAllocationArbitrary = jsc.suchthat(jsc.nat(), u => u > 1); + const runValueInArbitrary = jsc.suchthat(jsc.nat(), u => u > 1); + + const zeroOut = jsc.forall( + runPoolAllocationArbitrary, + secondaryPoolAllocationArbitrary, + runValueInArbitrary, + doTest, + ); + + t.true(jsc.check(zeroOut)); +}); diff --git a/packages/zoe/test/unitTests/contracts/constantProduct/propertyBased/test-calcDeltaY-property.js b/packages/zoe/test/unitTests/contracts/constantProduct/propertyBased/test-calcDeltaY-property.js new file mode 100644 index 00000000000..83178865c8e --- /dev/null +++ b/packages/zoe/test/unitTests/contracts/constantProduct/propertyBased/test-calcDeltaY-property.js @@ -0,0 +1,39 @@ +// @ts-check + +// eslint-disable-next-line import/no-extraneous-dependencies +import { test } from '@agoric/zoe/tools/prepare-test-env-ava'; + +// eslint-disable-next-line import/no-extraneous-dependencies +import jsc from 'jsverify'; +import { AmountMath } from '@agoric/ertp'; + +import { calcDeltaYSellingX } from '../../../../../src/contracts/constantProduct/core'; +import { setupMintKits } from '../setupMints'; + +const doTest = (x, y, deltaX) => { + const { run, bld } = setupMintKits(); + const runX = run(x); + const bldY = bld(y); + const runDeltaX = run(deltaX); + const deltaY = calcDeltaYSellingX(runX, bldY, runDeltaX); + const oldK = BigInt(runX.value) * BigInt(bldY.value); + const newX = AmountMath.add(runX, runDeltaX); + const newY = AmountMath.subtract(bldY, deltaY); + const newK = BigInt(newX.value) * BigInt(newY.value); + return newK >= oldK; +}; + +test('jsverify constant product calcDeltaYSellingX', t => { + const runPoolAllocationArbitrary = jsc.suchthat(jsc.nat(), u => u > 1); + const secondaryPoolAllocationArbitrary = jsc.suchthat(jsc.nat(), u => u > 1); + const runValueInArbitrary = jsc.suchthat(jsc.nat(), u => u > 1); + + const zeroOut = jsc.forall( + runPoolAllocationArbitrary, + secondaryPoolAllocationArbitrary, + runValueInArbitrary, + doTest, + ); + + t.true(jsc.check(zeroOut)); +}); diff --git a/packages/zoe/test/unitTests/contracts/constantProduct/propertyBased/test-largeValues.js b/packages/zoe/test/unitTests/contracts/constantProduct/propertyBased/test-largeValues.js new file mode 100644 index 00000000000..4899dde63f8 --- /dev/null +++ b/packages/zoe/test/unitTests/contracts/constantProduct/propertyBased/test-largeValues.js @@ -0,0 +1,28 @@ +// @ts-check + +// eslint-disable-next-line import/no-extraneous-dependencies +import { test } from '@agoric/zoe/tools/prepare-test-env-ava'; + +import jsc from 'jsverify'; + +import { runTest } from '../runTest'; + +// larger values than this seem to take a really long time and the +// test hangs +test('jsverify constant product large values', t => { + const runPoolAllocationArbitrary = jsc.suchthat(jsc.nat(), u => u > 30468); + const secondaryPoolAllocationArbitrary = jsc.suchthat( + jsc.nat(), + u => u > 30468, + ); + const runValueInArbitrary = jsc.suchthat(jsc.nat(), u => u < 30468 && u > 0); + + const constantProduct = jsc.forall( + runPoolAllocationArbitrary, + secondaryPoolAllocationArbitrary, + runValueInArbitrary, + runTest, + ); + + t.true(jsc.check(constantProduct)); +}); diff --git a/packages/zoe/test/unitTests/contracts/constantProduct/propertyBased/test-reduction.js b/packages/zoe/test/unitTests/contracts/constantProduct/propertyBased/test-reduction.js new file mode 100644 index 00000000000..21cb0ee8078 --- /dev/null +++ b/packages/zoe/test/unitTests/contracts/constantProduct/propertyBased/test-reduction.js @@ -0,0 +1,43 @@ +// @ts-check + +// eslint-disable-next-line import/no-extraneous-dependencies +import { test } from '@agoric/zoe/tools/prepare-test-env-ava'; + +// eslint-disable-next-line import/no-extraneous-dependencies +import jsc from 'jsverify'; +import { AmountMath } from '@agoric/ertp'; + +import { + calcDeltaYSellingX, + calcDeltaXSellingX, +} from '../../../../../src/contracts/constantProduct/core'; +import { setupMintKits } from '../setupMints'; + +// Not currently functional +const doTest = (x, y, deltaX) => { + const { run, bld } = setupMintKits(); + const runX = run(x); + const bldY = bld(y); + const runDeltaX = run(deltaX); + const deltaY = calcDeltaYSellingX(runX, bldY, runDeltaX); + const newDeltaX = calcDeltaXSellingX(runX, bldY, deltaY); + + const reduction = AmountMath.subtract(runDeltaX, newDeltaX); + + return AmountMath.isGTE(run(23), reduction); +}; + +test('jsverify constant product calcDeltaYSellingX', t => { + const runPoolAllocationArbitrary = jsc.suchthat(jsc.nat(), u => u > 1); + const secondaryPoolAllocationArbitrary = jsc.suchthat(jsc.nat(), u => u > 1); + const runValueInArbitrary = jsc.suchthat(jsc.nat(), u => u > 1); + + const zeroOut = jsc.forall( + runPoolAllocationArbitrary, + secondaryPoolAllocationArbitrary, + runValueInArbitrary, + doTest, + ); + + t.true(jsc.check(zeroOut)); +}); diff --git a/packages/zoe/test/unitTests/contracts/constantProduct/propertyBased/test-smallValues.js b/packages/zoe/test/unitTests/contracts/constantProduct/propertyBased/test-smallValues.js new file mode 100644 index 00000000000..282a1730084 --- /dev/null +++ b/packages/zoe/test/unitTests/contracts/constantProduct/propertyBased/test-smallValues.js @@ -0,0 +1,26 @@ +// @ts-check + +// eslint-disable-next-line import/no-extraneous-dependencies +import { test } from '@agoric/zoe/tools/prepare-test-env-ava'; + +import jsc from 'jsverify'; + +import { runTest } from '../runTest'; + +test('jsverify constant product small values', t => { + const runPoolAllocationArbitrary = jsc.suchthat(jsc.nat(), u => u > 10000); + const secondaryPoolAllocationArbitrary = jsc.suchthat( + jsc.nat(), + u => u > 10000, + ); + const runValueInArbitrary = jsc.suchthat(jsc.nat(), u => u < 10000 && u > 0); + + const constantProduct = jsc.forall( + runPoolAllocationArbitrary, + secondaryPoolAllocationArbitrary, + runValueInArbitrary, + runTest, + ); + + t.true(jsc.check(constantProduct)); +}); diff --git a/packages/zoe/test/unitTests/contracts/constantProduct/runTest.js b/packages/zoe/test/unitTests/contracts/constantProduct/runTest.js new file mode 100644 index 00000000000..2ee879b1ed4 --- /dev/null +++ b/packages/zoe/test/unitTests/contracts/constantProduct/runTest.js @@ -0,0 +1,38 @@ +// @ts-check + +import { Nat } from '@agoric/nat'; + +import { + DEFAULT_POOL_FEE, + DEFAULT_PROTOCOL_FEE, +} from '../../../../src/contracts/constantProduct/defaults'; +import { specifyRunIn } from '../../../../src/contracts/constantProduct/specifyRunIn'; +import { checkKInvariantSellingX } from '../../../../src/contracts/constantProduct/invariants'; +import { setupMintKits } from './setupMints'; + +export const runTest = ( + runPoolAllocationNat, + secondaryPoolAllocationNat, + runValueInNat, +) => { + const { bld, run } = setupMintKits(); + const runAmountIn = run(Nat(runValueInNat)); + const runPoolAllocation = run(Nat(runPoolAllocationNat)); + const bldPoolAllocation = bld(Nat(secondaryPoolAllocationNat)); + + const result = specifyRunIn( + runAmountIn, + runPoolAllocation, + bldPoolAllocation, + DEFAULT_PROTOCOL_FEE, + DEFAULT_POOL_FEE, + ); + // console.log(result); + + return checkKInvariantSellingX( + runPoolAllocation, + bldPoolAllocation, + result.deltaRun, + result.deltaSecondary, + ); +}; diff --git a/packages/zoe/test/unitTests/contracts/constantProduct/setupMints.js b/packages/zoe/test/unitTests/contracts/constantProduct/setupMints.js new file mode 100644 index 00000000000..5b0d5ee5246 --- /dev/null +++ b/packages/zoe/test/unitTests/contracts/constantProduct/setupMints.js @@ -0,0 +1,19 @@ +// @ts-check + +import { AmountMath, makeIssuerKit, AssetKind } from '@agoric/ertp'; + +export const setupMintKits = () => { + const runKit = makeIssuerKit( + 'RUN', + AssetKind.NAT, + harden({ decimalPlaces: 6 }), + ); + const bldKit = makeIssuerKit( + 'BLD', + AssetKind.NAT, + harden({ decimalPlaces: 6 }), + ); + const run = value => AmountMath.make(runKit.brand, value); + const bld = value => AmountMath.make(bldKit.brand, value); + return { runKit, bldKit, run, bld }; +}; diff --git a/packages/zoe/test/unitTests/contracts/constantProduct/test-calcDeltaY.js b/packages/zoe/test/unitTests/contracts/constantProduct/test-calcDeltaY.js new file mode 100644 index 00000000000..6a4c8157f34 --- /dev/null +++ b/packages/zoe/test/unitTests/contracts/constantProduct/test-calcDeltaY.js @@ -0,0 +1,69 @@ +// @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 { calcDeltaYSellingX } from '../../../../src/contracts/constantProduct/core'; +import { setupMintKits } from './setupMints'; + +// the brands of x and y shouldn't matter (test this explicitly in a +// separate test) +const doTest = (t, x, y, deltaX, expectedDeltaY) => { + const { run, bld } = setupMintKits(); + const result = calcDeltaYSellingX(run(x), bld(y), run(deltaX)); + t.true( + AmountMath.isEqual(result, bld(expectedDeltaY)), + `${result.value} equals ${expectedDeltaY}`, + ); +}; + +// deltaXPlusX is 0 +test('0, 0, 0, 0', t => { + t.throws(() => doTest(t, 0, 0, 0, 0), { + message: 'No infinite ratios! Denominator was 0/"[Alleged: RUN brand]"', + }); +}); + +test('0, 0, 1, 0', t => { + doTest(t, 0, 0, 1, 0); +}); + +test('1, 0, 0, 0', t => { + doTest(t, 1, 0, 0, 0); +}); + +// deltaXPlusX is 0 +test('0, 1, 0, 0', t => { + t.throws(() => doTest(t, 0, 1, 0, 0), { + message: 'No infinite ratios! Denominator was 0/"[Alleged: RUN brand]"', + }); +}); + +test('1, 1, 0, 0', t => { + doTest(t, 1, 1, 0, 0); +}); + +test('1, 1, 1, 0', t => { + doTest(t, 1, 1, 1, 0); +}); + +test('1, 2, 1, 1', t => { + doTest(t, 1, 2, 1, 1); +}); + +test('2, 3, 4, 2', t => { + doTest(t, 2, 3, 4, 2); +}); + +test('928861206, 130870247, 746353662, 58306244', t => { + doTest(t, 928861206n, 130870247n, 746353662n, 58306244n); +}); + +test('9, 3, 17, 1', t => { + doTest(t, 9, 3, 17, 1); +}); + +test('10000, 5000, 209, 102', t => { + doTest(t, 10000, 5000, 209, 102); +}); diff --git a/packages/zoe/test/unitTests/contracts/constantProduct/test-compareBondingCurves.js b/packages/zoe/test/unitTests/contracts/constantProduct/test-compareBondingCurves.js new file mode 100644 index 00000000000..190fa120039 --- /dev/null +++ b/packages/zoe/test/unitTests/contracts/constantProduct/test-compareBondingCurves.js @@ -0,0 +1,286 @@ +// @ts-check +// eslint-disable-next-line import/no-extraneous-dependencies +import { test } from '@agoric/zoe/tools/prepare-test-env-ava'; +import { BASIS_POINTS } from '../../../../src/contracts/constantProduct/defaults'; + +import { swapIn } from '../../../../src/contracts/constantProduct/swapIn'; +import { swapOut } from '../../../../src/contracts/constantProduct/swapOut'; +import { setupMintKits } from './setupMints'; +import { makeRatio } from '../../../../src/contractSupport'; + +// This assumes run is swapped in. The test should function the same +// regardless of what brand is the amountIn, because no run fee is +// charged. +const prepareSwapInTest = ({ inputReserve, outputReserve, inputValue }) => { + const { run, bld, runKit, bldKit } = setupMintKits(); + const amountGiven = run(inputValue); + const poolAllocation = harden({ + Central: run(inputReserve), + Secondary: bld(outputReserve), + }); + const amountWanted = bld(3n); + const protocolFeeRatio = makeRatio(0n, runKit.brand, BASIS_POINTS); + const poolFeeRatio = makeRatio(3n, bldKit.brand, BASIS_POINTS); + + const args = [ + amountGiven, + poolAllocation, + amountWanted, + protocolFeeRatio, + poolFeeRatio, + ]; + return harden({ + args, + run, + bld, + }); +}; + +const testGetPrice = (t, inputs, expectedOutput) => { + const { args, bld } = prepareSwapInTest(inputs); + const result = swapIn(...args); + t.deepEqual(result.swapperGets, bld(expectedOutput)); +}; + +const getInputPriceThrows = (t, inputs, message) => { + t.throws( + _ => { + const { args } = prepareSwapInTest(inputs); + return swapIn(...args); + }, + { + message, + }, + ); +}; + +// This assumes run is swapped in. The test should function the same +// regardless of what brand is the amountIn, because no run fee is +// charged. +const prepareSwapOutTest = ({ inputReserve, outputReserve, outputValue }) => { + const { run, bld, runKit } = setupMintKits(); + const amountGiven = run(10000n); // hard-coded + const poolAllocation = harden({ + Central: run(inputReserve), + Secondary: bld(outputReserve), + }); + const amountWanted = bld(outputValue); + const protocolFeeRatio = makeRatio(0n, runKit.brand, BASIS_POINTS); + const poolFeeRatio = makeRatio(30n, runKit.brand, BASIS_POINTS); + + const args = [ + amountGiven, + poolAllocation, + amountWanted, + protocolFeeRatio, + poolFeeRatio, + ]; + return harden({ + args, + run, + bld, + }); +}; + +const testGetOutputPrice = (t, inputs, expectedInput) => { + const { args, run } = prepareSwapOutTest(inputs); + const result = swapOut(...args); + t.deepEqual(result.swapperGives, run(expectedInput)); +}; + +const getOutputPriceThrows = (t, inputs, message) => { + const { args } = prepareSwapOutTest(inputs); + t.throws(_ => swapOut(...args), { + message, + }); +}; + +// If these tests of `getInputPrice` fail, it would indicate that we have +// diverged from the calculation in the Uniswap paper. +test('getInputPrice no reserves', t => { + const input = { + inputReserve: 0n, + outputReserve: 0n, + inputValue: 1n, + }; + const message = + '"poolAllocation.Central" was not greater than 0: {"brand":"[Alleged: RUN brand]","value":"[0n]"}'; + getInputPriceThrows(t, input, message); +}); + +test('getInputPrice ok 2', t => { + const input = { + inputReserve: 5984n, + outputReserve: 3028n, + inputValue: 1398n, + }; + const expectedOutput = 572n; + testGetPrice(t, input, expectedOutput); +}); + +test('getInputPrice ok 3', t => { + const input = { + inputReserve: 8160n, + outputReserve: 7743n, + inputValue: 6635n, + }; + const expectedOutput = 3466n; + testGetPrice(t, input, expectedOutput); +}); + +test('getInputPrice ok 4', t => { + const input = { + inputReserve: 10n, + outputReserve: 10n, + inputValue: 1000n, + }; + const expectedOutput = 9n; + testGetPrice(t, input, expectedOutput); +}); + +test('getInputPrice ok 5', t => { + const input = { + inputReserve: 100n, + outputReserve: 50n, + inputValue: 17n, + }; + const expectedOutput = 7n; + testGetPrice(t, input, expectedOutput); +}); + +test('getInputPrice ok 6', t => { + const input = { + outputReserve: 117n, + inputReserve: 43n, + inputValue: 7n, + }; + const expectedOutput = 16n; + testGetPrice(t, input, expectedOutput); +}); + +test('getInputPrice negative', t => { + const input = { + outputReserve: 117n, + inputReserve: 43n, + inputValue: -7n, + }; + const message = 'value "[-7n]" must be a Nat or an array'; + getInputPriceThrows(t, input, message); +}); + +test('getInputPrice bad reserve 1', t => { + const input = { + outputReserve: 0n, + inputReserve: 43n, + inputValue: 347n, + }; + const message = + '"poolAllocation.Secondary" was not greater than 0: {"brand":"[Alleged: BLD brand]","value":"[0n]"}'; + getInputPriceThrows(t, input, message); +}); + +test('getInputPrice bad reserve 2', t => { + const input = { + outputReserve: 50n, + inputReserve: 0n, + inputValue: 828n, + }; + const message = + '"poolAllocation.Central" was not greater than 0: {"brand":"[Alleged: RUN brand]","value":"[0n]"}'; + getInputPriceThrows(t, input, message); +}); + +test('getInputPrice zero input', t => { + const input = { + outputReserve: 50n, + inputReserve: 320n, + inputValue: 0n, + }; + const message = + '"allocation.In" was not greater than 0: {"brand":"[Alleged: RUN brand]","value":"[0n]"}'; + getInputPriceThrows(t, input, message); +}); + +test('getInputPrice big product', t => { + const input = { + outputReserve: 100000000n, + inputReserve: 100000000n, + inputValue: 1000n, + }; + const expectedOutput = 996n; + testGetPrice(t, input, expectedOutput); +}); + +test('getOutputPrice ok', t => { + const input = { + outputReserve: 117n, + inputReserve: 43n, + outputValue: 37n, + }; + const expectedOutput = 20n; + testGetOutputPrice(t, input, expectedOutput); +}); + +test('getOutputPrice zero output reserve', t => { + const input = { + outputReserve: 0n, + inputReserve: 43n, + outputValue: 37n, + }; + const message = + '"poolAllocation.Secondary" was not greater than 0: {"brand":"[Alleged: BLD brand]","value":"[0n]"}'; + getOutputPriceThrows(t, input, message); +}); + +test('getOutputPrice zero input reserve', t => { + const input = { + outputReserve: 92n, + inputReserve: 0n, + outputValue: 37n, + }; + const message = + '"poolAllocation.Central" was not greater than 0: {"brand":"[Alleged: RUN brand]","value":"[0n]"}'; + getOutputPriceThrows(t, input, message); +}); + +test('getOutputPrice too much output', t => { + const input = { + outputReserve: 1024n, + inputReserve: 1132n, + outputValue: 20923n, + }; + const message = + 'The poolAllocation {"brand":"[Alleged: BLD brand]","value":"[1024n]"} did not have enough to satisfy the wanted amountOut {"brand":"[Alleged: BLD brand]","value":"[20923n]"}'; + getOutputPriceThrows(t, input, message); +}); + +test('getOutputPrice too much output 2', t => { + const input = { + outputReserve: 345n, + inputReserve: 1132n, + outputValue: 345n, + }; + const message = + 'The poolAllocation {"brand":"[Alleged: BLD brand]","value":"[345n]"} did not have enough to satisfy the wanted amountOut {"brand":"[Alleged: BLD brand]","value":"[345n]"}'; + getOutputPriceThrows(t, input, message); +}); + +test('getOutputPrice big product', t => { + const input = { + outputReserve: 100000000n, + inputReserve: 100000000n, + outputValue: 1000n, + }; + const expectedOutput = 1004n; + testGetOutputPrice(t, input, expectedOutput); +}); + +test('getOutputPrice minimum price', t => { + const input = { + outputReserve: 10n, + inputReserve: 1n, + outputValue: 1n, + }; + const expectedOutput = 1n; + testGetOutputPrice(t, input, expectedOutput); +}); diff --git a/packages/zoe/test/unitTests/contracts/constantProduct/test-compareNewSwapPrice.js b/packages/zoe/test/unitTests/contracts/constantProduct/test-compareNewSwapPrice.js new file mode 100644 index 00000000000..839eef1bde5 --- /dev/null +++ b/packages/zoe/test/unitTests/contracts/constantProduct/test-compareNewSwapPrice.js @@ -0,0 +1,292 @@ +// @ts-check + +// eslint-disable-next-line import/no-extraneous-dependencies +import { test } from '@agoric/zoe/tools/prepare-test-env-ava'; +import { AmountMath, makeIssuerKit } from '@agoric/ertp'; + +import { swapIn } from '../../../../src/contracts/constantProduct/swapIn'; +import { + calcDeltaXSellingX, + calcDeltaYSellingX, + swapInNoFees, +} from '../../../../src/contracts/constantProduct/core'; +import { makeRatio } from '../../../../src/contractSupport'; + +const BASIS_POINTS = 10000n; +const POOL_FEE = 24n; +const PROTOCOL_FEE = 6n; + +// Moola is central + +const setupMints = () => { + const moolaKit = makeIssuerKit('moola'); + const bucksKit = makeIssuerKit('bucks'); + const simoleanKit = makeIssuerKit('simolean'); + + const moola = value => AmountMath.make(moolaKit.brand, value); + const bucks = value => AmountMath.make(bucksKit.brand, value); + const simoleans = value => AmountMath.make(simoleanKit.brand, value); + + return { + moolaKit, + bucksKit, + simoleanKit, + moola, + bucks, + simoleans, + }; +}; + +function protocolFee(input) { + return floorDivide(multiply(input, 6n), BASIS_POINTS); +} + +const doTest = () => {}; + +test('newSwap getPriceGivenAvailableInput specify central', async t => { + const { moola, bucks, moolaKit, bucksKit } = setupMints(); + const poolAllocation = { + Central: moola(800000n), + Secondary: bucks(300000n), + }; + const amountGiven = moola(10000n); + const amountWanted = bucks(1n); + + const protocolFeeRatio = makeRatio( + PROTOCOL_FEE, + moolaKit.brand, + BASIS_POINTS, + ); + const poolFeeRatio = makeRatio(POOL_FEE, bucksKit.brand, BASIS_POINTS); + + // This is reduced, if any reduction occurs. + const noFeesResult = swapInNoFees({ amountGiven, poolAllocation }); + t.deepEqual(noFeesResult.amountIn, moola(9999n)); + t.deepEqual(noFeesResult.amountOut, bucks(3703n)); + + const noReductionResult = calcDeltaYSellingX( + poolAllocation.Central, + poolAllocation.Secondary, + amountGiven, + ); + t.deepEqual(noReductionResult, bucks(3703n)); + + const reduced = calcDeltaXSellingX( + poolAllocation.Central, + poolAllocation.Secondary, + noReductionResult, + ); + t.deepEqual(reduced, moola(9999n)); + + const result = swapIn( + amountGiven, + poolAllocation, + amountWanted, + protocolFeeRatio, + poolFeeRatio, + ); + // swapperGives is 9999n + // t.deepEqual(result.swapperGives, moola(9997n)); + // Same + t.deepEqual(result.swapperGets, bucks(3692n)); + // Protocol fee is 6n + // t.deepEqual(result.protocolFee, moola(5n)); +}); + +test('newSwap getPriceGivenAvailableInput secondary', async t => { + const { moola, bucks, moolaKit, bucksKit } = setupMints(); + const poolAllocation = { + Central: moola(800000n), + Secondary: bucks(500000n), + }; + const amountGiven = bucks(10000n); + const amountWanted = moola(1n); + + const protocolFeeRatio = makeRatio( + PROTOCOL_FEE, + moolaKit.brand, + BASIS_POINTS, + ); + const poolFeeRatio = makeRatio(POOL_FEE, bucksKit.brand, BASIS_POINTS); + + const result = swapIn( + amountGiven, + poolAllocation, + amountWanted, + protocolFeeRatio, + poolFeeRatio, + ); + + const newSwapResult = { + amountIn: bucks(10000n), + amountOut: moola(15640n), + protocolFee: moola(9n), + }; + + // same + t.deepEqual(result.swapperGives, newSwapResult.amountIn); + // SwapperGets one less: 15639n + // t.deepEqual(result.swapperGets, newSwapResult.amountOut); + // Swapper pays one more: 10n + // t.deepEqual(result.protocolFee, newSwapResult.protocolFee); +}); + +test('newSwap getPriceGivenRequiredOutput specify central', async t => { + const initMoola = 700000n; + const initBucks = 500000n; + const { bucks, moola, bucksBrand, pricer } = setupPricer( + initMoola, + initBucks, + ); + + const output = 10000n; + const pFeePre = protocolFee(output); + const poolChange = output + pFeePre; + const valueIn = priceFromTargetOutput(poolChange, initMoola, initBucks, 24n); + const valueOut = outputFromInputPrice(initBucks, initMoola, valueIn, 24n); + const pFee = protocolFee(valueOut); + t.deepEqual(pricer.getPriceGivenRequiredOutput(bucksBrand, moola(output)), { + amountIn: bucks(valueIn), + amountOut: moola(valueOut - pFee), + protocolFee: moola(pFee), + }); + t.truthy( + (initMoola - valueOut) * (initBucks + valueIn + pFee) > + initBucks * initMoola, + ); +}); + +test('newSwap getPriceGivenRequiredOutput specify secondary', async t => { + const initMoola = 700000n; + const initBucks = 500000n; + const { bucks, moola, moolaBrand, pricer } = setupPricer( + initMoola, + initBucks, + ); + + const output = 10000n; + const valueIn = priceFromTargetOutput(output, initBucks, initMoola, 24n); + const valueOut = outputFromInputPrice(initMoola, initBucks, valueIn, 24n); + const pFee = protocolFee(valueIn); + t.deepEqual(pricer.getPriceGivenRequiredOutput(moolaBrand, bucks(output)), { + amountIn: moola(valueIn + pFee), + amountOut: bucks(valueOut), + protocolFee: moola(pFee), + }); + t.truthy( + (initMoola - valueOut) * (initBucks + valueIn + pFee) > + initBucks * initMoola, + ); +}); + +test('newSwap getPriceGivenAvailableInput twoPools', async t => { + const initMoola = 800000n; + const initBucks = 500000n; + const initSimoleans = 300000n; + const { bucks, moola, simoleans, simoleansBrand, pricer } = setupPricer( + initMoola, + initBucks, + initSimoleans, + ); + + // get price given input from simoleans to bucks through moola, presuming + // there will be no price improvement + const input = 10000n; + const moolaOut = outputFromInputPrice(initBucks, initMoola, input, 12n); + const feeOut = floorDivide(multiply(moolaOut, 6), BASIS_POINTS); + const simOut = outputFromInputPrice( + initMoola, + initSimoleans, + moolaOut - feeOut, + 12n, + ); + t.deepEqual( + pricer.getPriceGivenAvailableInput(bucks(input), simoleansBrand), + { + amountIn: bucks(input), + amountOut: simoleans(simOut), + protocolFee: moola(feeOut), + centralAmount: moola(moolaOut), + }, + ); +}); + +test('newSwap getPriceGivenRequiredOutput twoPools', async t => { + const initMoola = 800000n; + const initBucks = 500000n; + const initSimoleans = 300000n; + const { bucks, moola, simoleans, simoleansBrand, pricer } = setupPricer( + initMoola, + initBucks, + initSimoleans, + ); + + // get price given desired output from simoleans to bucks through moola, + // choosing 10001 so there will be no price improvement + const output = 10001n; + const moolaIn = priceFromTargetOutput(output, initBucks, initMoola, 12n); + const fee = floorDivide(multiply(moolaIn, 6), BASIS_POINTS); + const simIn = priceFromTargetOutput( + moolaIn + fee, + initMoola, + initSimoleans, + 12n, + ); + t.deepEqual( + pricer.getPriceGivenRequiredOutput(simoleansBrand, bucks(output)), + { + amountIn: simoleans(simIn), + amountOut: bucks(output), + protocolFee: moola(fee), + centralAmount: moola(moolaIn), + }, + ); +}); + +test('newSwap getPriceGivenOutput central extreme', async t => { + const initMoola = 700000n; + const initBucks = 500000n; + const { bucks, moola, bucksBrand, pricer } = setupPricer( + initMoola, + initBucks, + ); + + const output = 690000n; + const pFeePre = protocolFee(output); + const poolChange = output + pFeePre; + const valueIn = priceFromTargetOutput(poolChange, initMoola, initBucks, 24n); + const valueOut = outputFromInputPrice(initBucks, initMoola, valueIn, 24n); + const pFee = protocolFee(valueOut); + t.deepEqual(pricer.getPriceGivenRequiredOutput(bucksBrand, moola(output)), { + amountIn: bucks(valueIn), + amountOut: moola(valueOut - pFee), + protocolFee: moola(pFee), + }); + + t.truthy( + (initMoola - valueOut) * (initBucks + valueIn + pFee) > + initBucks * initMoola, + ); +}); + +test('newSwap getPriceGivenInput secondary extreme', async t => { + const moolaPool = 800000n; + const bucksPool = 500000n; + const { bucks, moola, moolaBrand, pricer } = setupPricer( + moolaPool, + bucksPool, + ); + + const input = 690000n; + const valueOut = outputFromInputPrice(bucksPool, moolaPool, input, 24n); + const pFee = protocolFee(valueOut); + const valueIn = priceFromTargetOutput(valueOut, moolaPool, bucksPool, 24n); + t.deepEqual(pricer.getPriceGivenAvailableInput(bucks(input), moolaBrand), { + amountIn: bucks(valueIn), + amountOut: moola(valueOut - pFee), + protocolFee: moola(pFee), + }); + t.truthy( + (moolaPool - valueOut) * (bucksPool + valueIn) > bucksPool * moolaPool, + ); +}); diff --git a/packages/zoe/test/unitTests/contracts/constantProduct/test-edgeCases.js b/packages/zoe/test/unitTests/contracts/constantProduct/test-edgeCases.js new file mode 100644 index 00000000000..d6fb4e61262 --- /dev/null +++ b/packages/zoe/test/unitTests/contracts/constantProduct/test-edgeCases.js @@ -0,0 +1,25 @@ +// @ts-check + +// eslint-disable-next-line import/no-extraneous-dependencies +import { test } from '@agoric/zoe/tools/prepare-test-env-ava'; + +import { runTest } from './runTest'; + +// Parameters are: +// runPoolAllocationNat, +// secondaryPoolAllocationNat, +// runValueInNat, + +test('specifyRunIn 101, 101, 0', t => { + t.throws(() => runTest(101, 101, 0), { + message: 'runAmountIn cannot be empty', + }); +}); + +test('specifyRunIn 101, 101, 2', t => { + t.true(runTest(101, 101, 2)); +}); + +test('specifyRunIn 101, 101, 3', t => { + t.true(runTest(101, 101, 3)); +}); diff --git a/packages/zoe/test/unitTests/contracts/constantProduct/test-getXY.js b/packages/zoe/test/unitTests/contracts/constantProduct/test-getXY.js new file mode 100644 index 00000000000..a154628b861 --- /dev/null +++ b/packages/zoe/test/unitTests/contracts/constantProduct/test-getXY.js @@ -0,0 +1,50 @@ +// @ts-check + +// eslint-disable-next-line import/no-extraneous-dependencies +import { test } from '@agoric/zoe/tools/prepare-test-env-ava'; + +import { getXY } from '../../../../src/contracts/constantProduct/getXY'; +import { setupMintKits } from './setupMints'; + +// There's no difference between SwapIn and SwapOut for this function +test('swap Central for Secondary', t => { + const { run, bld } = setupMintKits(); + + const amountGiven = run(2000n); + const poolAllocation = { + Central: run(102902920n), + Secondary: bld(203838393n), + }; + const amountWanted = bld(2819n); + const { x, y, deltaX, wantedDeltaY } = getXY( + amountGiven, + poolAllocation, + amountWanted, + ); + + t.deepEqual(x, poolAllocation.Central); + t.deepEqual(y, poolAllocation.Secondary); + t.deepEqual(deltaX, amountGiven); + t.deepEqual(wantedDeltaY, amountWanted); +}); + +test('swap Secondary for Central', t => { + const { run, bld } = setupMintKits(); + + const amountGiven = bld(2000n); + const poolAllocation = { + Central: run(102902920n), + Secondary: bld(203838393n), + }; + const amountWanted = run(2819n); + const { x, y, deltaX, wantedDeltaY } = getXY( + amountGiven, + poolAllocation, + amountWanted, + ); + + t.deepEqual(x, poolAllocation.Secondary); + t.deepEqual(y, poolAllocation.Central); + t.deepEqual(deltaX, amountGiven); + t.deepEqual(wantedDeltaY, amountWanted); +}); diff --git a/packages/zoe/test/unitTests/contracts/constantProduct/test-newBondingCurve.js b/packages/zoe/test/unitTests/contracts/constantProduct/test-newBondingCurve.js new file mode 100644 index 00000000000..784a2ebf70a --- /dev/null +++ b/packages/zoe/test/unitTests/contracts/constantProduct/test-newBondingCurve.js @@ -0,0 +1,177 @@ +// @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 { assertAmountsEqual } from '../../../zoeTestHelpers'; +import { getCurrentPrice } from '../fixedNewSwap'; + +import { + DEFAULT_POOL_FEE, + DEFAULT_PROTOCOL_FEE, +} from '../../../../src/contracts/constantProduct/defaults'; +import { specifyRunIn } from '../../../../src/contracts/constantProduct/specifyRunIn'; +import { + assertKInvariantSellingX, + assertPoolFee, + assertProtocolFee, +} from '../../../../src/contracts/constantProduct/invariants'; +import { calcDeltaYSellingX } from '../../../../src/contracts/constantProduct/core'; +import { setupMintKits } from './setupMints'; + +const conductTestSpecifyRunIn = ( + mintKits, + runPoolAllocationValue, + bldPoolAllocationValue, + runValueIn, + expected, + t, + protocolFeeBP = DEFAULT_PROTOCOL_FEE, + poolFeeBP = DEFAULT_POOL_FEE, +) => { + const { runKit, bldKit } = mintKits; + + const bldPoolAllocation = amountMath.make( + bldKit.brand, + bldPoolAllocationValue, + ); + const runPoolAllocation = amountMath.make( + runKit.brand, + runPoolAllocationValue, + ); + + const runAmountIn = amountMath.make(runKit.brand, runValueIn); + + const result = specifyRunIn( + runAmountIn, + runPoolAllocation, + bldPoolAllocation, + protocolFeeBP, + poolFeeBP, + ); + + Object.entries(expected).forEach(([property, amount]) => { + assertAmountsEqual(t, result[property], amount, property); + }); +}; + +test('test bug scenario', async t => { + const mintKits = setupMintKits(); + const { run, bld } = mintKits; + const bldPoolAllocationValue = 2196247730468n; + const runPoolAllocationValue = 50825056949339n; + const runValueIn = 73000000n; + + const expected = { + protocolFee: run(43800n), + poolFee: bld(7571n), + amountIn: run(72999997n), // buggy newswap quotes 72999951n + amountOut: bld(3145001n), // buggy newswap quotes 3145005n + deltaRun: run(72956197n), + deltaSecondary: bld(3152572n), + newRunPool: run(50825129905536n), + newSecondaryPool: bld(2196244577896n), + inReturnedToUser: run(3n), + }; + + conductTestSpecifyRunIn( + mintKits, + runPoolAllocationValue, + bldPoolAllocationValue, + runValueIn, + expected, + t, + ); +}); + +test('test small values', async t => { + const mintKits = setupMintKits(); + const { run, bld } = mintKits; + const bldPoolAllocationValue = 40000n; + const runPoolAllocationValue = 500000n; + const runValueIn = 5839n; + + const expected = { + protocolFee: run(4n), + poolFee: bld(2n), + amountIn: run(5834n), + amountOut: bld(459n), + deltaRun: run(5830n), + deltaSecondary: bld(461n), + newRunPool: run(505830n), + newSecondaryPool: bld(39539n), + inReturnedToUser: run(5n), + }; + + conductTestSpecifyRunIn( + mintKits, + runPoolAllocationValue, + bldPoolAllocationValue, + runValueIn, + expected, + t, + ); +}); + +test.failing('test bug scenario against fixed newSwap', async t => { + const mintKits = setupMintKits(); + const { run, bld } = mintKits; + const bldPoolAllocationValue = 2196247730468n; + const runPoolAllocationValue = 50825056949339n; + const runValueIn = 73000000n; + + // const expected = { + // protocolFee: run(43800n), + // poolFee: bld(7567n), // 7566 + // amountIn: run(72999997n), // buggy newswap quotes 72999951n + // amountOut: bld(3145005n), // buggy newswap quotes 3145005n - the same + // deltaRun: run(72956197n), + // deltaSecondary: bld(3152572n), + // newRunPool: run(50825129905536n), + // newSecondaryPool: bld(2196244577896n), + // inReturnedToUser: run(3n), + // }; + + const { amountIn, amountOut, protocolFee } = getCurrentPrice( + run(runPoolAllocationValue), + bld(bldPoolAllocationValue), + run(runValueIn), + DEFAULT_PROTOCOL_FEE, + DEFAULT_POOL_FEE, + ); + + // amountIn: run(72999997n) - amountIn is the same + // amountOut: bld(3145007n) - amount out is higher + // protocolFee: run(43773n) - protocolFee is less + + const runPoolAllocation = run(runPoolAllocationValue); + const bldPoolAllocation = bld(bldPoolAllocationValue); + const deltaX = amountMath.subtract(amountIn, protocolFee); + + // This includes the pool fee so it's only checking that including + // the pool fee, k is increasing. + assertKInvariantSellingX( + run(runPoolAllocationValue), + bld(bldPoolAllocationValue), + amountMath.subtract(amountIn, protocolFee), + amountOut, + ); + + const deltaY = calcDeltaYSellingX( + runPoolAllocation, + bldPoolAllocation, + deltaX, + ); + + const poolFee = amountMath.subtract(deltaY, amountOut); + + // This is violated: 5.996 BP not 6 + t.notThrows(() => + assertProtocolFee(protocolFee, amountIn, DEFAULT_PROTOCOL_FEE), + ); + + // This is violated 23.999444263463527 not 24 + t.notThrows(() => assertPoolFee(poolFee, amountOut, DEFAULT_POOL_FEE)); +}); From c213e7cf3fc57d128ed070f217057018d11f5b33 Mon Sep 17 00:00:00 2001 From: Chris Hibbert Date: Fri, 27 Aug 2021 15:32:20 -0700 Subject: [PATCH 3/8] refactor!: tests, assertions, docs, renaming update to current master type declarations renaming Allow swapOut() to have an empty amountIn representing "no restriction" Drop enforcement in swapIn/swapOut that the quotes are in the forward direction doublePool will need to call them in the reverse direction Improved documentation in the README --- .../src/contracts/constantProduct/README.md | 100 ++++++++++ .../src/contracts/constantProduct/calcFees.js | 28 +-- .../constantProduct/checkInvariants.js | 4 +- .../zoe/src/contracts/constantProduct/core.js | 107 ++++++----- .../src/contracts/constantProduct/getXY.js | 5 +- .../contracts/constantProduct/invariants.js | 6 +- .../zoe/src/contracts/constantProduct/swap.js | 147 ++++++++++++--- .../src/contracts/constantProduct/swapIn.js | 15 +- .../src/contracts/constantProduct/swapOut.js | 15 +- .../test-calcDeltaY-calcDeltaX.js | 43 ----- .../propertyBased/test-calcDeltaY-property.js | 39 ---- .../propertyBased/test-largeValues.js | 28 --- .../propertyBased/test-reduction.js | 43 ----- .../propertyBased/test-smallValues.js | 26 --- .../contracts/constantProduct/runTest.js | 38 ---- .../constantProduct/test-calcDeltaY.js | 14 +- .../test-compareBondingCurves.js | 114 ++++++----- .../test-compareNewSwapPrice.js | 178 +----------------- .../constantProduct/test-edgeCases.js | 25 --- .../contracts/constantProduct/test-getXY.js | 18 +- .../constantProduct/test-newBondingCurve.js | 177 ----------------- .../constantProduct/test-swapScenarios.js | 138 ++++++++++++++ 22 files changed, 562 insertions(+), 746 deletions(-) create mode 100644 packages/zoe/src/contracts/constantProduct/README.md delete mode 100644 packages/zoe/test/unitTests/contracts/constantProduct/propertyBased/test-calcDeltaY-calcDeltaX.js delete mode 100644 packages/zoe/test/unitTests/contracts/constantProduct/propertyBased/test-calcDeltaY-property.js delete mode 100644 packages/zoe/test/unitTests/contracts/constantProduct/propertyBased/test-largeValues.js delete mode 100644 packages/zoe/test/unitTests/contracts/constantProduct/propertyBased/test-reduction.js delete mode 100644 packages/zoe/test/unitTests/contracts/constantProduct/propertyBased/test-smallValues.js delete mode 100644 packages/zoe/test/unitTests/contracts/constantProduct/runTest.js delete mode 100644 packages/zoe/test/unitTests/contracts/constantProduct/test-edgeCases.js delete mode 100644 packages/zoe/test/unitTests/contracts/constantProduct/test-newBondingCurve.js create mode 100644 packages/zoe/test/unitTests/contracts/constantProduct/test-swapScenarios.js diff --git a/packages/zoe/src/contracts/constantProduct/README.md b/packages/zoe/src/contracts/constantProduct/README.md new file mode 100644 index 00000000000..716602cb9fe --- /dev/null +++ b/packages/zoe/src/contracts/constantProduct/README.md @@ -0,0 +1,100 @@ +# Constant Product AMM + +A simpler constant product automatic market maker based on our Ratio library and +charges two kinds of fees. The pool fee remains in the pool to reward the +liquidity providers. The protocol fee is extracted to fund community efforts. + +This algorithm uses the x*y=k formula directly, without complicating it with +fees. Briefly, there are two pools of assets, whose values are kept roughly in +balance through the actions of arbitrageurs. At any time, a trader can come to +the pool and offer to deposit one of the two assets. They will receive an amount +of the complementary asset that will maintain the invariant that the product of +the pool values doesn't change. (Except that rounding is done in favor of the +pool.) The liquidity providers are rewarded by charging a fee. + +The user can specify a maximum amount they want to pay or a minimum amount they +want to receive. Unlike Uniswap, this approach will charge less than the user +offered or pay more than they asked for when appropriate. (By analogy, if a user +is willing to pay up to $20 when the price of soda is $3 per bottle, it would +give 6 bottles and only charge $18. Uniswap doesn't adjust the provided price, +so it charges $20. This matters whenever the values of the smallest unit of the +currencies are significantly different, which is common in defi.) + +The rules that drive the design include + +* When the user names an input (or output) price, they shouldn't pay more + (or receive less) than they said. +* The pool fee is charged against the computed side of the price. +* The protocol fee is always charged in RUN. +* The fees should be calculated based on the pool's prices before a transaction. +* Computations are rounded in favor of the pool. + +We start by estimating the exchange rate, and calculate fees based on that. Once +we know the fees, we add or subtract them directly to the amounts added to and +extracted from the pools to adhere to those rules. + +## Calculating fees + +In this table BLD represents any collateral. ΔX is always the amount contributed to the pool, and ΔY is always +the amount extracted from the pool. + +| | In (X) | Out (Y) | PoolFee | Protocol Fee | ΔX | ΔY | pool Fee * | +|---------|-----|-----|--------|-----|-----|-----|-----| +| **RUN in** | RUN | BLD | BLD | RUN | sGive - PrFee | sGets | sGet - ΔY +| **RUN out** | BLD | RUN | RUN | BLD | sGive | sGets + PrFee | ΔX - sGive +| **BLD in** | BLD | RUN | BLD | RUN | sGive | sGest + PrFee | ΔY - sGet +| **BLD out** | RUN | BLD | RUN | BLD | sGive - PrFee | sGets | sGive - ΔX + +(*) The Pool Fee remains in the pool, so its impact on the calculation is +subtle. When amountIn is specified, we add the poolFee to any minimum amountOut +from the user since the trade has to produce amoutOut plus the required fee in +order to be satisfactory. When amountOut is specified, we subtract the fee from any +amountIn max from the user since the fee has to come out of the user's deposit. + +* When the amount of RUN provided is specified, (**RUN in**) we subtract + the poolFee from the amount the user will give before using the reduced amount + in the derivation of ΔY from ΔX. +* When the amount of RUN being paid out is specified (**RUN out**), we add the + poolFee to ΔX, which was calculated from the requested payout. +* When the amount of BLD to be paid in is specified (**BLD in**), the + amount the user gets is computed by subtracting the poolFee from ΔY + which already had the protocolFee included. +* When the amount of BLD to be paid out is specified (**BLD out**), ΔX is + computed from the required payout, and the poolFee is added to that to get the + amount the user must pay. + +## Example + +For example, if the pools were at 40000 RUN and 3000 BLD and the user's offer +specifies that they want to buy BLD and are willing to spend up to 300 RUN, the +fees will be 1 RUN and 1 BLD because the amounts are low for expository +purposes. Since the user specified the input price, we calculate the output +using the constant product formula for ΔY. The protocol fee is always +charged in RUN, so the pool will only gain 299 from the user's 300 RUN. + +(3000 * 299) / (40000n + 299) = 22 + +Notice that 23 gives a product just below x*y, and 22 just above +(3000n + 23n) * (40000n + 299n) < 3000n * 40000n +3000n * 40000n < (3000n + 22n) * (40000n + 299n) + +We then calculate how much the user should actually pay for that using the +deltaX formula, which tells us that the pool would be able to maintain its +invariants if it charged 296, so the user won't have to pay the whole 300 that +was offered. We will add 1 + +(40000n * 22n) / (3000n - 22n) = 296 + +This time 295 and 296 bracket the required value. +(3000n - 22n) * (40000n + 295n) < 3000n * 40000n +3000n * 40000n < (3000n - 22n) * (40000n + 296n) + +The pool fee will be subtracted from the proceeds before paying the user, so the +result is that the user pays 297 RUN and gets 21 BLD. The pool's K changes from +120M to 120041784n reflecting the pool fee, and 1 BLD is paid to the protocol +fee. + +A withdrawal from the pool of 22 build would have maintained the invariants; +we withdrew 21 instead + +(3000n - 21n) * (40000n + 296n) diff --git a/packages/zoe/src/contracts/constantProduct/calcFees.js b/packages/zoe/src/contracts/constantProduct/calcFees.js index 1cb8afb8281..a1b02430bd3 100644 --- a/packages/zoe/src/contracts/constantProduct/calcFees.js +++ b/packages/zoe/src/contracts/constantProduct/calcFees.js @@ -1,9 +1,9 @@ // @ts-check import { AmountMath } from '@agoric/ertp'; -import { multiplyByCeilDivide, makeRatio } from '../../contractSupport/ratio'; +import { ceilMultiplyBy, makeRatio } from '../../contractSupport/ratio.js'; -import { BASIS_POINTS } from './defaults'; +import { BASIS_POINTS } from './defaults.js'; /** * Make a ratio given a nat representing basis points @@ -12,18 +12,22 @@ import { BASIS_POINTS } from './defaults'; * @param {Brand} brandOfFee * @returns {Ratio} */ -const makeFeeRatio = (feeBP, brandOfFee) => { +export const makeFeeRatio = (feeBP, brandOfFee) => { return makeRatio(feeBP, brandOfFee, BASIS_POINTS); }; -const minimum = (left, right) => { - // If left is greater or equal, return right. Otherwise return left. - return AmountMath.isGTE(left, right) ? right : left; +export const maximum = (left, right) => { + // If left is greater or equal, return left. Otherwise return right. + return AmountMath.isGTE(left, right) ? left : right; }; +export const amountGT = (left, right) => + AmountMath.isGTE(left, right) && !AmountMath.isEqual(left, right); + /** - * @param {{ amountIn: Amount, amountOut: Amount}} amounts - an array of two amounts in different - * brands. We must select the amount of the same brand as the feeRatio. + * @param {{ amountIn: Amount, amountOut: Amount}} amounts - an array of two + * amounts in different brands. We must select the amount of the same brand as + * the feeRatio. * @param {Ratio} feeRatio * @returns {Amount} */ @@ -31,10 +35,12 @@ const calcFee = ({ amountIn, amountOut }, feeRatio) => { const sameBrandAmount = amountIn.brand === feeRatio.numerator.brand ? amountIn : amountOut; // Always round fees up - const fee = multiplyByCeilDivide(sameBrandAmount, feeRatio); + const fee = ceilMultiplyBy(sameBrandAmount, feeRatio); + + // Fee cannot exceed the amount on which it is levied + assert(AmountMath.isGTE(sameBrandAmount, fee)); - // Fee cannot be more than what exists - return minimum(fee, sameBrandAmount); + return fee; }; // SwapIn uses calcDeltaYSellingX diff --git a/packages/zoe/src/contracts/constantProduct/checkInvariants.js b/packages/zoe/src/contracts/constantProduct/checkInvariants.js index 2bd1a670d56..04866d162d4 100644 --- a/packages/zoe/src/contracts/constantProduct/checkInvariants.js +++ b/packages/zoe/src/contracts/constantProduct/checkInvariants.js @@ -1,12 +1,12 @@ // @ts-check -import { assertRightsConserved } from '../../contractFacet/rightsConservation'; +import { assertRightsConserved } from '../../contractFacet/rightsConservation.js'; import { assertKInvariantSellingX, assertPoolFee, assertProtocolFee, -} from './invariants'; +} from './invariants.js'; export const checkAllInvariants = ( runPoolAllocation, diff --git a/packages/zoe/src/contracts/constantProduct/core.js b/packages/zoe/src/contracts/constantProduct/core.js index 8b5b5243083..e777d587911 100644 --- a/packages/zoe/src/contracts/constantProduct/core.js +++ b/packages/zoe/src/contracts/constantProduct/core.js @@ -1,20 +1,28 @@ // @ts-check import { AmountMath } from '@agoric/ertp'; +import { assert, details as X, q } from '@agoric/assert'; -import { natSafeMath } from '../../contractSupport'; -import { makeRatioFromAmounts } from '../../contractSupport/ratio'; -import { getXY } from './getXY'; +import { natSafeMath } from '../../contractSupport/index.js'; +import { makeRatioFromAmounts } from '../../contractSupport/ratio.js'; +import { getXY } from './getXY.js'; + +const assertSingleBrand = ratio => { + assert( + ratio.numerator.brand === ratio.denominator.brand, + X`Ratio was expected to have same brand in numerator and denominator ${q( + ratio, + )}`, + ); +}; -// TODO: fix this up with more assertions and rename -// Used for multiplying y by a ratio with both numerators and -// denominators of brand x /** * @param {Amount} amount * @param {Ratio} ratio * @returns {Amount} */ -const multiplyByOtherBrandFloorDivide = (amount, ratio) => { +const floorMultiplyKeepBrand = (amount, ratio) => { + assertSingleBrand(ratio); const value = natSafeMath.floorDivide( natSafeMath.multiply(amount.value, ratio.numerator.value), ratio.denominator.value, @@ -22,15 +30,13 @@ const multiplyByOtherBrandFloorDivide = (amount, ratio) => { return AmountMath.make(amount.brand, value); }; -// TODO: fix this up with more assertions and rename -// Used for multiplying y by a ratio with both numerators and -// denominators of brand x /** * @param {Amount} amount * @param {Ratio} ratio * @returns {Amount} */ -const multiplyByOtherBrandCeilDivide = (amount, ratio) => { +const ceilMultiplyKeepBrand = (amount, ratio) => { + assertSingleBrand(ratio); const value = natSafeMath.ceilDivide( natSafeMath.multiply(amount.value, ratio.numerator.value), ratio.denominator.value, @@ -39,63 +45,74 @@ const multiplyByOtherBrandCeilDivide = (amount, ratio) => { }; /** - * Calculate deltaY when user is selling brand X. This calculates how much of - * brand Y to give the user in return. + * Calculate the change to the shrinking pool when the user specifies how much + * they're willing to add. Also used to improve a proposed trade when the amount + * contributed would buy more than the user asked for. * - * deltaY = (deltaXToX/(1 + deltaXToX))*y + * deltaY = (deltaXOverX/(1 + deltaXOverX))*y * Equivalently: (deltaX / (deltaX + x)) * y * - * @param {Amount} x - the amount of Brand X in pool, xPoolAllocation - * @param {Amount} y - the amount of Brand Y in pool, yPoolAllocation - * @param {Amount} deltaX - the amount of Brand X to be added - * @returns {Amount} deltaY - the amount of Brand Y to be taken out + * @param {Amount} x - the amount of the growing brand in the pool + * @param {Amount} y - the amount of the shrinking brand in the pool + * @param {Amount} deltaX - the amount of the growing brand to be added + * @returns {Amount} deltaY - the amount of the shrinking brand to be taken out */ export const calcDeltaYSellingX = (x, y, deltaX) => { const deltaXPlusX = AmountMath.add(deltaX, x); const xRatio = makeRatioFromAmounts(deltaX, deltaXPlusX); - // Result is an amount in y.brand - // We would want to err on the side of the pool, so this should be a - // floorDivide so that less deltaY is given out - return multiplyByOtherBrandFloorDivide(y, xRatio); + // We want to err on the side of the pool, so this will use floorDivide to + // round down the amount paid out. + return floorMultiplyKeepBrand(y, xRatio); }; /** - * Calculate deltaX when user is selling brand X. This allows us to give the user a - * small refund if the amount they will as a payout could have been - * achieved by a smaller input. + * Calculate the change to the growing pool when the user specifies how much + * they want to receive. Also used to improve a proposed trade when the amount + * requested can be purchased for a smaller input. * - * deltaX = (deltaYToY/(1 - deltaYToY))*x - * Equivalently: (deltaY / (y - deltaY )) * x + * deltaX = (deltaYOverY/(1 - deltaYOverY))*x + * Equivalently: (deltaY / (Y - deltaY )) * x * - * @param {Amount} x - the amount of Brand X in pool, xPoolAllocation - * @param {Amount} y - the amount of Brand Y in pool, yPoolAllocation - * @param {Amount} deltaY - the amount of Brand Y to be taken out - * @returns {Amount} deltaX - the amount of Brand X to be added + * @param {Amount} x - the amount of the growing brand in the pool + * @param {Amount} y - the amount of the shrinking brand in the pool + * @param {Amount} deltaY - the amount of the shrinking brand to take out + * @returns {Amount} deltaX - the amount of the growingn brand to add */ export const calcDeltaXSellingX = (x, y, deltaY) => { const yMinusDeltaY = AmountMath.subtract(y, deltaY); const yRatio = makeRatioFromAmounts(deltaY, yMinusDeltaY); - // Result is an amount in x.brand - // We want to err on the side of the pool, so this should be a - // ceiling divide so that more deltaX is taken - return multiplyByOtherBrandCeilDivide(x, yRatio); + // We want to err on the side of the pool, so this will use ceilMultiply to + // round up the amount required. + return ceilMultiplyKeepBrand(x, yRatio); }; -const swapInReduced = ({ x, y, deltaX }) => { - const deltaY = calcDeltaYSellingX(x, y, deltaX); - const reducedDeltaX = calcDeltaXSellingX(x, y, deltaY); +const swapInReduced = ({ x: inPool, y: outPool, deltaX: offeredAmountIn }) => { + const amountOut = calcDeltaYSellingX(inPool, outPool, offeredAmountIn); + const reducedAmountIn = calcDeltaXSellingX(inPool, outPool, amountOut); + + assert(AmountMath.isGTE(offeredAmountIn, reducedAmountIn)); + return harden({ - amountIn: reducedDeltaX, - amountOut: deltaY, + amountIn: reducedAmountIn, + amountOut, + improvement: AmountMath.subtract(offeredAmountIn, reducedAmountIn), }); }; -const swapOutImproved = ({ x, y, wantedDeltaY }) => { - const requiredDeltaX = calcDeltaXSellingX(x, y, wantedDeltaY); - const improvedDeltaY = calcDeltaYSellingX(x, y, requiredDeltaX); +const swapOutImproved = ({ + x: inPool, + y: outPool, + deltaY: wantedAmountOut, +}) => { + const amountIn = calcDeltaXSellingX(inPool, outPool, wantedAmountOut); + const improvedAmountOut = calcDeltaYSellingX(inPool, outPool, amountIn); + + assert(AmountMath.isGTE(improvedAmountOut, wantedAmountOut)); + return harden({ - amountIn: requiredDeltaX, - amountOut: improvedDeltaY, + amountIn, + amountOut: improvedAmountOut, + improvement: AmountMath.subtract(improvedAmountOut, wantedAmountOut), }); }; diff --git a/packages/zoe/src/contracts/constantProduct/getXY.js b/packages/zoe/src/contracts/constantProduct/getXY.js index f5fcd5e48fb..fae48d84f66 100644 --- a/packages/zoe/src/contracts/constantProduct/getXY.js +++ b/packages/zoe/src/contracts/constantProduct/getXY.js @@ -7,8 +7,7 @@ * @param {Amount=} opt.amountGiven * @param {{ Central: Amount, Secondary: Amount }} opt.poolAllocation * @param {Amount=} opt.amountWanted - * @returns {{ x: Amount, y: Amount, deltaX: Amount, wantedDeltaY: - * Amount }} + * @returns {{ x: Amount, y: Amount, deltaX: Amount, deltaY: Amount }} */ export const getXY = ({ amountGiven, poolAllocation, amountWanted }) => { // Regardless of whether we are specifying the amountIn or the @@ -20,7 +19,7 @@ export const getXY = ({ amountGiven, poolAllocation, amountWanted }) => { const deltas = { deltaX: amountGiven, - wantedDeltaY: amountWanted, + deltaY: amountWanted, }; if (secondaryBrand === xBrand || centralBrand === yBrand) { diff --git a/packages/zoe/src/contracts/constantProduct/invariants.js b/packages/zoe/src/contracts/constantProduct/invariants.js index c4b84613173..b5cdfc5b52b 100644 --- a/packages/zoe/src/contracts/constantProduct/invariants.js +++ b/packages/zoe/src/contracts/constantProduct/invariants.js @@ -3,10 +3,10 @@ import { assert, details as X } from '@agoric/assert'; import { AmountMath } from '@agoric/ertp'; -import { makeRatioFromAmounts } from '../../contractSupport/ratio'; -import { natSafeMath } from '../../contractSupport'; +import { makeRatioFromAmounts } from '../../contractSupport/ratio.js'; +import { natSafeMath } from '../../contractSupport/index.js'; -import { BASIS_POINTS } from './defaults'; +import { BASIS_POINTS } from './defaults.js'; /** * xy <= (x + deltaX)(y - deltaY) diff --git a/packages/zoe/src/contracts/constantProduct/swap.js b/packages/zoe/src/contracts/constantProduct/swap.js index ba8f834de6c..5e0e2153f3a 100644 --- a/packages/zoe/src/contracts/constantProduct/swap.js +++ b/packages/zoe/src/contracts/constantProduct/swap.js @@ -2,10 +2,14 @@ import { assert, details as X } from '@agoric/assert'; import { AmountMath } from '@agoric/ertp'; -import { calculateFees } from './calcFees'; +import { calculateFees, amountGT, maximum } from './calcFees.js'; const subtractRelevantFees = (amount, fee) => { if (amount.brand === fee.brand) { + if (AmountMath.isGTE(fee, amount)) { + return AmountMath.makeEmptyFromAmount(amount); + } + return AmountMath.subtract(amount, fee); } return amount; @@ -44,19 +48,40 @@ const assertGreaterThanZeroHelper = (amount, name) => { ); }; -const assertWantedAvailable = (poolAllocation, amountWanted) => { - if (amountWanted.brand === poolAllocation.Central.brand) { - assert( - AmountMath.isGTE(poolAllocation.Central, amountWanted), - X`The poolAllocation ${poolAllocation.Central} did not have enough to satisfy the wanted amountOut ${amountWanted}`, - ); +const isWantedAvailable = (poolAllocation, amountWanted) => { + return amountWanted.brand === poolAllocation.Central.brand + ? !AmountMath.isGTE(amountWanted, poolAllocation.Central) + : !AmountMath.isGTE(amountWanted, poolAllocation.Secondary); +}; + +function noTransaction(amountGiven, amountWanted, poolAllocation, poolFee) { + const emptyGive = AmountMath.makeEmptyFromAmount(amountGiven); + const emptyWant = AmountMath.makeEmptyFromAmount(amountWanted); + + let newX; + let newY; + if (poolAllocation.Central.brand === amountGiven.brand) { + newX = poolAllocation.Central; + newY = poolAllocation.Secondary; } else { - assert( - !AmountMath.isGTE(amountWanted, poolAllocation.Secondary), - X`The poolAllocation ${poolAllocation.Secondary} did not have enough to satisfy the wanted amountOut ${amountWanted}`, - ); + newX = poolAllocation.Secondary; + newY = poolAllocation.Central; } -}; + + const result = { + protocolFee: AmountMath.makeEmpty(poolAllocation.Central.brand), + poolFee: AmountMath.makeEmpty(poolFee.numerator.brand), + swapperGives: emptyGive, + swapperGets: emptyWant, + // swapperGiveRefund: AmountMath.subtract(amountGiven, swapperGives), + xIncrement: emptyGive, + yDecrement: emptyWant, + newX, + newY, + improvement: emptyGive, + }; + return result; +} export const swap = ( amountGiven, @@ -71,13 +96,32 @@ export const swap = ( poolAllocation.Secondary, 'poolAllocation.Secondary', ); - assertGreaterThanZeroHelper(amountGiven, 'amountGiven'); - assertGreaterThanZeroHelper(amountWanted, 'amountWanted'); - assertWantedAvailable(poolAllocation, amountWanted); + assert( + (amountGiven && + !AmountMath.isGTE( + AmountMath.makeEmptyFromAmount(amountGiven), + amountGiven, + )) || + (amountWanted && + !AmountMath.isGTE( + AmountMath.makeEmptyFromAmount(amountWanted), + amountWanted, + )), + X`amountGiven or amountWanted must be greater than 0: ${amountWanted} ${amountGiven}`, + ); + + if (!isWantedAvailable(poolAllocation, amountWanted)) { + return noTransaction( + amountGiven, + amountWanted, + poolAllocation, + poolFeeRatio, + ); + } // The protocol fee must always be collected in RUN, but the pool // fee is collected in the amount opposite of what is specified. - + // This call gives us improved amountIn or amountOut const fees = calculateFees( amountGiven, poolAllocation, @@ -87,30 +131,77 @@ export const swap = ( swapFn, ); - const { amountIn, amountOut } = swapFn({ - amountGiven: subtractFees(fees.amountIn, fees), + if (!isWantedAvailable(poolAllocation, addFees(amountWanted, fees))) { + return noTransaction( + amountGiven, + amountWanted, + poolAllocation, + poolFeeRatio, + ); + } + + // calculate no-fee amounts. swapFn will only pay attention to the specified + // value. The pool fee is always charged on the unspecified side, so it won't + // affect the calculation. When the specified value is in RUN, the protocol + // fee will be deducted from amountGiven before adding to the pool or from + // amountOut to calculate swapperGets. + const { amountIn, amountOut, improvement } = swapFn({ + amountGiven: subtractFees(amountGiven, fees), poolAllocation, - amountWanted, + amountWanted: addFees(amountWanted, fees), }); + if (AmountMath.isEmpty(amountOut)) { + return noTransaction( + amountGiven, + amountWanted, + poolAllocation, + poolFeeRatio, + ); + } + + // The swapper pays extra or receives less to cover the fees. const swapperGives = addFees(amountIn, fees); const swapperGets = subtractFees(amountOut, fees); - // assert( - // AmountMath.isGTE(amountGiven, swapperGives), - // X`The amount provided ${amountGiven} is not enough. ${swapperGives} is required.`, - // ); + if ( + AmountMath.isEmpty(swapperGets) || + (!AmountMath.isEmpty(amountGiven) && amountGT(swapperGives, amountGiven)) || + amountGT(amountWanted, swapperGets) + ) { + return noTransaction( + amountGiven, + amountWanted, + poolAllocation, + poolFeeRatio, + ); + } + const xIncrement = addRelevantFees(amountIn, fees.poolFee); + const yDecrement = subtractRelevantFees(amountOut, fees.poolFee); + + // poolFee is the amount the pool will grow over the no-fee calculation. + // protocolFee is to be separated and sent to an external purse. + // The swapper amounts are what will we paid and received. + // xIncrement and yDecrement are what will be added and removed from the pools. + // Either xIncrement will be increased by the pool fee or yDecrement will be + // reduced by it in order to compensate the pool. + // newX and newY are the new pool balances, for comparison with start values. + // improvement is an estimate of how much the gains or losses were improved. const result = { protocolFee: fees.protocolFee, poolFee: fees.poolFee, swapperGives, swapperGets, - // swapperGiveRefund: AmountMath.subtract(amountGiven, swapperGives), - deltaX: amountIn, - deltaY: amountOut, - newX: addOrSubtractFromPool(AmountMath.add, poolAllocation, amountIn), - newY: addOrSubtractFromPool(AmountMath.subtract, poolAllocation, amountOut), + xIncrement, + yDecrement, + newX: addOrSubtractFromPool(AmountMath.add, poolAllocation, xIncrement), + newY: addOrSubtractFromPool( + AmountMath.subtract, + poolAllocation, + yDecrement, + ), + improvement: maximum(fees.improvement, improvement), }; return result; diff --git a/packages/zoe/src/contracts/constantProduct/swapIn.js b/packages/zoe/src/contracts/constantProduct/swapIn.js index b77f3d0ac38..64c63227aae 100644 --- a/packages/zoe/src/contracts/constantProduct/swapIn.js +++ b/packages/zoe/src/contracts/constantProduct/swapIn.js @@ -1,7 +1,9 @@ // @ts-check -import { swap } from './swap'; -import { swapInNoFees } from './core'; +import { swap } from './swap.js'; +import { swapInNoFees } from './core.js'; +import { assertKInvariantSellingX } from './invariants.js'; +import { getXY } from './getXY.js'; export const swapIn = ( amountGiven, @@ -10,7 +12,7 @@ export const swapIn = ( protocolFeeRatio, poolFeeRatio, ) => { - return swap( + const result = swap( amountGiven, poolAllocation, amountWanted, @@ -18,4 +20,11 @@ export const swapIn = ( poolFeeRatio, swapInNoFees, ); + const { x, y } = getXY({ + amountGiven, + poolAllocation, + amountWanted, + }); + assertKInvariantSellingX(x, y, result.xIncrement, result.yDecrement); + return result; }; diff --git a/packages/zoe/src/contracts/constantProduct/swapOut.js b/packages/zoe/src/contracts/constantProduct/swapOut.js index a869b331b37..99fd4a87720 100644 --- a/packages/zoe/src/contracts/constantProduct/swapOut.js +++ b/packages/zoe/src/contracts/constantProduct/swapOut.js @@ -1,7 +1,9 @@ // @ts-check -import { swap } from './swap'; -import { swapOutNoFees } from './core'; +import { swap } from './swap.js'; +import { swapOutNoFees } from './core.js'; +import { getXY } from './getXY.js'; +import { assertKInvariantSellingX } from './invariants.js'; export const swapOut = ( amountGiven, @@ -10,7 +12,7 @@ export const swapOut = ( protocolFeeRatio, poolFeeRatio, ) => { - return swap( + const result = swap( amountGiven, poolAllocation, amountWanted, @@ -18,4 +20,11 @@ export const swapOut = ( poolFeeRatio, swapOutNoFees, ); + const { x, y } = getXY({ + amountGiven, + poolAllocation, + amountWanted, + }); + assertKInvariantSellingX(x, y, result.xIncrement, result.yDecrement); + return result; }; diff --git a/packages/zoe/test/unitTests/contracts/constantProduct/propertyBased/test-calcDeltaY-calcDeltaX.js b/packages/zoe/test/unitTests/contracts/constantProduct/propertyBased/test-calcDeltaY-calcDeltaX.js deleted file mode 100644 index 66d37b059fa..00000000000 --- a/packages/zoe/test/unitTests/contracts/constantProduct/propertyBased/test-calcDeltaY-calcDeltaX.js +++ /dev/null @@ -1,43 +0,0 @@ -// @ts-check - -// eslint-disable-next-line import/no-extraneous-dependencies -import { test } from '@agoric/zoe/tools/prepare-test-env-ava'; - -// eslint-disable-next-line import/no-extraneous-dependencies -import jsc from 'jsverify'; -import { AmountMath } from '@agoric/ertp'; - -import { - calcDeltaYSellingX, - calcDeltaXSellingX, -} from '../../../../../src/contracts/constantProduct/core'; -import { setupMintKits } from '../setupMints'; - -const doTest = (x, y, deltaX) => { - const { run, bld } = setupMintKits(); - const runX = run(x); - const bldY = bld(y); - const runDeltaX = run(deltaX); - const deltaY = calcDeltaYSellingX(runX, bldY, runDeltaX); - const newDeltaX = calcDeltaXSellingX(runX, bldY, deltaY); - - // Pass through again, should always get the same answer. - const newDeltaY = calcDeltaYSellingX(runX, bldY, newDeltaX); - - return AmountMath.isEqual(deltaY, newDeltaY); -}; - -test('jsverify constant product calcDeltaYSellingX', t => { - const runPoolAllocationArbitrary = jsc.suchthat(jsc.nat(), u => u > 1); - const secondaryPoolAllocationArbitrary = jsc.suchthat(jsc.nat(), u => u > 1); - const runValueInArbitrary = jsc.suchthat(jsc.nat(), u => u > 1); - - const zeroOut = jsc.forall( - runPoolAllocationArbitrary, - secondaryPoolAllocationArbitrary, - runValueInArbitrary, - doTest, - ); - - t.true(jsc.check(zeroOut)); -}); diff --git a/packages/zoe/test/unitTests/contracts/constantProduct/propertyBased/test-calcDeltaY-property.js b/packages/zoe/test/unitTests/contracts/constantProduct/propertyBased/test-calcDeltaY-property.js deleted file mode 100644 index 83178865c8e..00000000000 --- a/packages/zoe/test/unitTests/contracts/constantProduct/propertyBased/test-calcDeltaY-property.js +++ /dev/null @@ -1,39 +0,0 @@ -// @ts-check - -// eslint-disable-next-line import/no-extraneous-dependencies -import { test } from '@agoric/zoe/tools/prepare-test-env-ava'; - -// eslint-disable-next-line import/no-extraneous-dependencies -import jsc from 'jsverify'; -import { AmountMath } from '@agoric/ertp'; - -import { calcDeltaYSellingX } from '../../../../../src/contracts/constantProduct/core'; -import { setupMintKits } from '../setupMints'; - -const doTest = (x, y, deltaX) => { - const { run, bld } = setupMintKits(); - const runX = run(x); - const bldY = bld(y); - const runDeltaX = run(deltaX); - const deltaY = calcDeltaYSellingX(runX, bldY, runDeltaX); - const oldK = BigInt(runX.value) * BigInt(bldY.value); - const newX = AmountMath.add(runX, runDeltaX); - const newY = AmountMath.subtract(bldY, deltaY); - const newK = BigInt(newX.value) * BigInt(newY.value); - return newK >= oldK; -}; - -test('jsverify constant product calcDeltaYSellingX', t => { - const runPoolAllocationArbitrary = jsc.suchthat(jsc.nat(), u => u > 1); - const secondaryPoolAllocationArbitrary = jsc.suchthat(jsc.nat(), u => u > 1); - const runValueInArbitrary = jsc.suchthat(jsc.nat(), u => u > 1); - - const zeroOut = jsc.forall( - runPoolAllocationArbitrary, - secondaryPoolAllocationArbitrary, - runValueInArbitrary, - doTest, - ); - - t.true(jsc.check(zeroOut)); -}); diff --git a/packages/zoe/test/unitTests/contracts/constantProduct/propertyBased/test-largeValues.js b/packages/zoe/test/unitTests/contracts/constantProduct/propertyBased/test-largeValues.js deleted file mode 100644 index 4899dde63f8..00000000000 --- a/packages/zoe/test/unitTests/contracts/constantProduct/propertyBased/test-largeValues.js +++ /dev/null @@ -1,28 +0,0 @@ -// @ts-check - -// eslint-disable-next-line import/no-extraneous-dependencies -import { test } from '@agoric/zoe/tools/prepare-test-env-ava'; - -import jsc from 'jsverify'; - -import { runTest } from '../runTest'; - -// larger values than this seem to take a really long time and the -// test hangs -test('jsverify constant product large values', t => { - const runPoolAllocationArbitrary = jsc.suchthat(jsc.nat(), u => u > 30468); - const secondaryPoolAllocationArbitrary = jsc.suchthat( - jsc.nat(), - u => u > 30468, - ); - const runValueInArbitrary = jsc.suchthat(jsc.nat(), u => u < 30468 && u > 0); - - const constantProduct = jsc.forall( - runPoolAllocationArbitrary, - secondaryPoolAllocationArbitrary, - runValueInArbitrary, - runTest, - ); - - t.true(jsc.check(constantProduct)); -}); diff --git a/packages/zoe/test/unitTests/contracts/constantProduct/propertyBased/test-reduction.js b/packages/zoe/test/unitTests/contracts/constantProduct/propertyBased/test-reduction.js deleted file mode 100644 index 21cb0ee8078..00000000000 --- a/packages/zoe/test/unitTests/contracts/constantProduct/propertyBased/test-reduction.js +++ /dev/null @@ -1,43 +0,0 @@ -// @ts-check - -// eslint-disable-next-line import/no-extraneous-dependencies -import { test } from '@agoric/zoe/tools/prepare-test-env-ava'; - -// eslint-disable-next-line import/no-extraneous-dependencies -import jsc from 'jsverify'; -import { AmountMath } from '@agoric/ertp'; - -import { - calcDeltaYSellingX, - calcDeltaXSellingX, -} from '../../../../../src/contracts/constantProduct/core'; -import { setupMintKits } from '../setupMints'; - -// Not currently functional -const doTest = (x, y, deltaX) => { - const { run, bld } = setupMintKits(); - const runX = run(x); - const bldY = bld(y); - const runDeltaX = run(deltaX); - const deltaY = calcDeltaYSellingX(runX, bldY, runDeltaX); - const newDeltaX = calcDeltaXSellingX(runX, bldY, deltaY); - - const reduction = AmountMath.subtract(runDeltaX, newDeltaX); - - return AmountMath.isGTE(run(23), reduction); -}; - -test('jsverify constant product calcDeltaYSellingX', t => { - const runPoolAllocationArbitrary = jsc.suchthat(jsc.nat(), u => u > 1); - const secondaryPoolAllocationArbitrary = jsc.suchthat(jsc.nat(), u => u > 1); - const runValueInArbitrary = jsc.suchthat(jsc.nat(), u => u > 1); - - const zeroOut = jsc.forall( - runPoolAllocationArbitrary, - secondaryPoolAllocationArbitrary, - runValueInArbitrary, - doTest, - ); - - t.true(jsc.check(zeroOut)); -}); diff --git a/packages/zoe/test/unitTests/contracts/constantProduct/propertyBased/test-smallValues.js b/packages/zoe/test/unitTests/contracts/constantProduct/propertyBased/test-smallValues.js deleted file mode 100644 index 282a1730084..00000000000 --- a/packages/zoe/test/unitTests/contracts/constantProduct/propertyBased/test-smallValues.js +++ /dev/null @@ -1,26 +0,0 @@ -// @ts-check - -// eslint-disable-next-line import/no-extraneous-dependencies -import { test } from '@agoric/zoe/tools/prepare-test-env-ava'; - -import jsc from 'jsverify'; - -import { runTest } from '../runTest'; - -test('jsverify constant product small values', t => { - const runPoolAllocationArbitrary = jsc.suchthat(jsc.nat(), u => u > 10000); - const secondaryPoolAllocationArbitrary = jsc.suchthat( - jsc.nat(), - u => u > 10000, - ); - const runValueInArbitrary = jsc.suchthat(jsc.nat(), u => u < 10000 && u > 0); - - const constantProduct = jsc.forall( - runPoolAllocationArbitrary, - secondaryPoolAllocationArbitrary, - runValueInArbitrary, - runTest, - ); - - t.true(jsc.check(constantProduct)); -}); diff --git a/packages/zoe/test/unitTests/contracts/constantProduct/runTest.js b/packages/zoe/test/unitTests/contracts/constantProduct/runTest.js deleted file mode 100644 index 2ee879b1ed4..00000000000 --- a/packages/zoe/test/unitTests/contracts/constantProduct/runTest.js +++ /dev/null @@ -1,38 +0,0 @@ -// @ts-check - -import { Nat } from '@agoric/nat'; - -import { - DEFAULT_POOL_FEE, - DEFAULT_PROTOCOL_FEE, -} from '../../../../src/contracts/constantProduct/defaults'; -import { specifyRunIn } from '../../../../src/contracts/constantProduct/specifyRunIn'; -import { checkKInvariantSellingX } from '../../../../src/contracts/constantProduct/invariants'; -import { setupMintKits } from './setupMints'; - -export const runTest = ( - runPoolAllocationNat, - secondaryPoolAllocationNat, - runValueInNat, -) => { - const { bld, run } = setupMintKits(); - const runAmountIn = run(Nat(runValueInNat)); - const runPoolAllocation = run(Nat(runPoolAllocationNat)); - const bldPoolAllocation = bld(Nat(secondaryPoolAllocationNat)); - - const result = specifyRunIn( - runAmountIn, - runPoolAllocation, - bldPoolAllocation, - DEFAULT_PROTOCOL_FEE, - DEFAULT_POOL_FEE, - ); - // console.log(result); - - return checkKInvariantSellingX( - runPoolAllocation, - bldPoolAllocation, - result.deltaRun, - result.deltaSecondary, - ); -}; diff --git a/packages/zoe/test/unitTests/contracts/constantProduct/test-calcDeltaY.js b/packages/zoe/test/unitTests/contracts/constantProduct/test-calcDeltaY.js index 6a4c8157f34..65d595ee2f0 100644 --- a/packages/zoe/test/unitTests/contracts/constantProduct/test-calcDeltaY.js +++ b/packages/zoe/test/unitTests/contracts/constantProduct/test-calcDeltaY.js @@ -1,11 +1,11 @@ // @ts-check // eslint-disable-next-line import/no-extraneous-dependencies -import { test } from '@agoric/zoe/tools/prepare-test-env-ava'; +import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; import { AmountMath } from '@agoric/ertp'; -import { calcDeltaYSellingX } from '../../../../src/contracts/constantProduct/core'; -import { setupMintKits } from './setupMints'; +import { calcDeltaYSellingX } from '../../../../src/contracts/constantProduct/core.js'; +import { setupMintKits } from './setupMints.js'; // the brands of x and y shouldn't matter (test this explicitly in a // separate test) @@ -67,3 +67,11 @@ test('9, 3, 17, 1', t => { test('10000, 5000, 209, 102', t => { doTest(t, 10000, 5000, 209, 102); }); + +test('1000000, 5000, 209, 1', t => { + doTest(t, 1000000, 5000, 209, 1); +}); + +test('5000, 1000000, 209, 1', t => { + doTest(t, 5000, 1000000, 209, 40122); +}); diff --git a/packages/zoe/test/unitTests/contracts/constantProduct/test-compareBondingCurves.js b/packages/zoe/test/unitTests/contracts/constantProduct/test-compareBondingCurves.js index 190fa120039..7ed3fbe4d66 100644 --- a/packages/zoe/test/unitTests/contracts/constantProduct/test-compareBondingCurves.js +++ b/packages/zoe/test/unitTests/contracts/constantProduct/test-compareBondingCurves.js @@ -1,12 +1,12 @@ // @ts-check // eslint-disable-next-line import/no-extraneous-dependencies -import { test } from '@agoric/zoe/tools/prepare-test-env-ava'; -import { BASIS_POINTS } from '../../../../src/contracts/constantProduct/defaults'; +import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; +import { BASIS_POINTS } from '../../../../src/contracts/constantProduct/defaults.js'; -import { swapIn } from '../../../../src/contracts/constantProduct/swapIn'; -import { swapOut } from '../../../../src/contracts/constantProduct/swapOut'; -import { setupMintKits } from './setupMints'; -import { makeRatio } from '../../../../src/contractSupport'; +import { swapIn } from '../../../../src/contracts/constantProduct/swapIn.js'; +import { swapOut } from '../../../../src/contracts/constantProduct/swapOut.js'; +import { setupMintKits } from './setupMints.js'; +import { makeRatio } from '../../../../src/contractSupport/index.js'; // This assumes run is swapped in. The test should function the same // regardless of what brand is the amountIn, because no run fee is @@ -18,7 +18,7 @@ const prepareSwapInTest = ({ inputReserve, outputReserve, inputValue }) => { Central: run(inputReserve), Secondary: bld(outputReserve), }); - const amountWanted = bld(3n); + const amountWanted = bld(0n); const protocolFeeRatio = makeRatio(0n, runKit.brand, BASIS_POINTS); const poolFeeRatio = makeRatio(3n, bldKit.brand, BASIS_POINTS); @@ -59,7 +59,7 @@ const getInputPriceThrows = (t, inputs, message) => { // charged. const prepareSwapOutTest = ({ inputReserve, outputReserve, outputValue }) => { const { run, bld, runKit } = setupMintKits(); - const amountGiven = run(10000n); // hard-coded + const amountGiven = run(0n); const poolAllocation = harden({ Central: run(inputReserve), Secondary: bld(outputReserve), @@ -95,8 +95,6 @@ const getOutputPriceThrows = (t, inputs, message) => { }); }; -// If these tests of `getInputPrice` fail, it would indicate that we have -// diverged from the calculation in the Uniswap paper. test('getInputPrice no reserves', t => { const input = { inputReserve: 0n, @@ -104,7 +102,7 @@ test('getInputPrice no reserves', t => { inputValue: 1n, }; const message = - '"poolAllocation.Central" was not greater than 0: {"brand":"[Alleged: RUN brand]","value":"[0n]"}'; + '"poolAllocation.Central" must be greater than 0: {"brand":"[Alleged: RUN brand]","value":"[0n]"}'; getInputPriceThrows(t, input, message); }); @@ -124,7 +122,12 @@ test('getInputPrice ok 3', t => { outputReserve: 7743n, inputValue: 6635n, }; - const expectedOutput = 3466n; + const expectedOutput = 3470n; + // const expected = { + // poolFee: 2n, + // swapperGives: 6634n, + // swapperGets: 3470n, + // }; testGetPrice(t, input, expectedOutput); }); @@ -134,7 +137,12 @@ test('getInputPrice ok 4', t => { outputReserve: 10n, inputValue: 1000n, }; - const expectedOutput = 9n; + const expectedOutput = 8n; + // const expected = { + // poolFee: 1n, + // swapperGives: 90n, + // swapperGets: 8n, + // }; testGetPrice(t, input, expectedOutput); }); @@ -144,24 +152,34 @@ test('getInputPrice ok 5', t => { outputReserve: 50n, inputValue: 17n, }; - const expectedOutput = 7n; + const expectedOutput = 6n; + // const expected = { + // poolFee: 1n, + // swapperGives: 17, + // swapperGets: 6, + // }; testGetPrice(t, input, expectedOutput); }); test('getInputPrice ok 6', t => { const input = { - outputReserve: 117n, inputReserve: 43n, + outputReserve: 117n, inputValue: 7n, }; - const expectedOutput = 16n; + // const expected = { + // poolFee: 1n, + // swapperGives: 7, + // swapperGets: 15, + // }; + const expectedOutput = 15n; testGetPrice(t, input, expectedOutput); }); test('getInputPrice negative', t => { const input = { - outputReserve: 117n, inputReserve: 43n, + outputReserve: 117n, inputValue: -7n, }; const message = 'value "[-7n]" must be a Nat or an array'; @@ -170,117 +188,123 @@ test('getInputPrice negative', t => { test('getInputPrice bad reserve 1', t => { const input = { - outputReserve: 0n, inputReserve: 43n, + outputReserve: 0n, inputValue: 347n, }; const message = - '"poolAllocation.Secondary" was not greater than 0: {"brand":"[Alleged: BLD brand]","value":"[0n]"}'; + '"poolAllocation.Secondary" must be greater than 0: {"brand":"[Alleged: BLD brand]","value":"[0n]"}'; getInputPriceThrows(t, input, message); }); test('getInputPrice bad reserve 2', t => { const input = { - outputReserve: 50n, inputReserve: 0n, + outputReserve: 50n, inputValue: 828n, }; const message = - '"poolAllocation.Central" was not greater than 0: {"brand":"[Alleged: RUN brand]","value":"[0n]"}'; + '"poolAllocation.Central" must be greater than 0: {"brand":"[Alleged: RUN brand]","value":"[0n]"}'; getInputPriceThrows(t, input, message); }); test('getInputPrice zero input', t => { const input = { - outputReserve: 50n, inputReserve: 320n, + outputReserve: 50n, inputValue: 0n, }; const message = - '"allocation.In" was not greater than 0: {"brand":"[Alleged: RUN brand]","value":"[0n]"}'; + 'amountGiven or amountWanted must be greater than 0: {"brand":"[Alleged: BLD brand]","value":"[0n]"} {"brand":"[Alleged: RUN brand]","value":"[0n]"}'; getInputPriceThrows(t, input, message); }); test('getInputPrice big product', t => { const input = { - outputReserve: 100000000n, - inputReserve: 100000000n, + inputReserve: 100_000_000n, + outputReserve: 100_000_000n, inputValue: 1000n, }; - const expectedOutput = 996n; + const expectedOutput = 998n; + // const expected = { + // poolFee: 1n, + // swapperGives: 1000n, + // swapperGets: 998n, + // }; testGetPrice(t, input, expectedOutput); }); test('getOutputPrice ok', t => { const input = { - outputReserve: 117n, inputReserve: 43n, + outputReserve: 117n, outputValue: 37n, }; - const expectedOutput = 20n; + const expectedOutput = 21n; testGetOutputPrice(t, input, expectedOutput); }); test('getOutputPrice zero output reserve', t => { const input = { - outputReserve: 0n, inputReserve: 43n, + outputReserve: 0n, outputValue: 37n, }; const message = - '"poolAllocation.Secondary" was not greater than 0: {"brand":"[Alleged: BLD brand]","value":"[0n]"}'; + '"poolAllocation.Secondary" must be greater than 0: {"brand":"[Alleged: BLD brand]","value":"[0n]"}'; getOutputPriceThrows(t, input, message); }); test('getOutputPrice zero input reserve', t => { const input = { - outputReserve: 92n, inputReserve: 0n, + outputReserve: 92n, outputValue: 37n, }; const message = - '"poolAllocation.Central" was not greater than 0: {"brand":"[Alleged: RUN brand]","value":"[0n]"}'; + '"poolAllocation.Central" must be greater than 0: {"brand":"[Alleged: RUN brand]","value":"[0n]"}'; getOutputPriceThrows(t, input, message); }); test('getOutputPrice too much output', t => { const input = { - outputReserve: 1024n, inputReserve: 1132n, + outputReserve: 1024n, outputValue: 20923n, }; - const message = - 'The poolAllocation {"brand":"[Alleged: BLD brand]","value":"[1024n]"} did not have enough to satisfy the wanted amountOut {"brand":"[Alleged: BLD brand]","value":"[20923n]"}'; - getOutputPriceThrows(t, input, message); + testGetOutputPrice(t, input, 0n); }); test('getOutputPrice too much output 2', t => { const input = { - outputReserve: 345n, inputReserve: 1132n, + outputReserve: 345n, outputValue: 345n, }; - const message = - 'The poolAllocation {"brand":"[Alleged: BLD brand]","value":"[345n]"} did not have enough to satisfy the wanted amountOut {"brand":"[Alleged: BLD brand]","value":"[345n]"}'; - getOutputPriceThrows(t, input, message); + testGetOutputPrice(t, input, 0n); }); test('getOutputPrice big product', t => { const input = { - outputReserve: 100000000n, - inputReserve: 100000000n, + inputReserve: 100_000_000n, + outputReserve: 100_000_000n, outputValue: 1000n, }; - const expectedOutput = 1004n; + const expectedOutput = 1005n; testGetOutputPrice(t, input, expectedOutput); }); test('getOutputPrice minimum price', t => { const input = { - outputReserve: 10n, inputReserve: 1n, + outputReserve: 10n, outputValue: 1n, }; - const expectedOutput = 1n; + // const expected = { + // poolFee: 1n, + // swapperGives: 2n, + // swapperGets: 5n, + // }; + const expectedOutput = 2n; testGetOutputPrice(t, input, expectedOutput); }); diff --git a/packages/zoe/test/unitTests/contracts/constantProduct/test-compareNewSwapPrice.js b/packages/zoe/test/unitTests/contracts/constantProduct/test-compareNewSwapPrice.js index 839eef1bde5..24282c6d60c 100644 --- a/packages/zoe/test/unitTests/contracts/constantProduct/test-compareNewSwapPrice.js +++ b/packages/zoe/test/unitTests/contracts/constantProduct/test-compareNewSwapPrice.js @@ -1,16 +1,16 @@ // @ts-check // eslint-disable-next-line import/no-extraneous-dependencies -import { test } from '@agoric/zoe/tools/prepare-test-env-ava'; +import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; import { AmountMath, makeIssuerKit } from '@agoric/ertp'; -import { swapIn } from '../../../../src/contracts/constantProduct/swapIn'; +import { swapIn } from '../../../../src/contracts/constantProduct/swapIn.js'; import { calcDeltaXSellingX, calcDeltaYSellingX, swapInNoFees, -} from '../../../../src/contracts/constantProduct/core'; -import { makeRatio } from '../../../../src/contractSupport'; +} from '../../../../src/contracts/constantProduct/core.js'; +import { makeRatio } from '../../../../src/contractSupport/index.js'; const BASIS_POINTS = 10000n; const POOL_FEE = 24n; @@ -37,12 +37,6 @@ const setupMints = () => { }; }; -function protocolFee(input) { - return floorDivide(multiply(input, 6n), BASIS_POINTS); -} - -const doTest = () => {}; - test('newSwap getPriceGivenAvailableInput specify central', async t => { const { moola, bucks, moolaKit, bucksKit } = setupMints(); const poolAllocation = { @@ -94,7 +88,7 @@ test('newSwap getPriceGivenAvailableInput specify central', async t => { }); test('newSwap getPriceGivenAvailableInput secondary', async t => { - const { moola, bucks, moolaKit, bucksKit } = setupMints(); + const { moola, bucks, moolaKit } = setupMints(); const poolAllocation = { Central: moola(800000n), Secondary: bucks(500000n), @@ -107,7 +101,7 @@ test('newSwap getPriceGivenAvailableInput secondary', async t => { moolaKit.brand, BASIS_POINTS, ); - const poolFeeRatio = makeRatio(POOL_FEE, bucksKit.brand, BASIS_POINTS); + const poolFeeRatio = makeRatio(POOL_FEE, moolaKit.brand, BASIS_POINTS); const result = swapIn( amountGiven, @@ -130,163 +124,3 @@ test('newSwap getPriceGivenAvailableInput secondary', async t => { // Swapper pays one more: 10n // t.deepEqual(result.protocolFee, newSwapResult.protocolFee); }); - -test('newSwap getPriceGivenRequiredOutput specify central', async t => { - const initMoola = 700000n; - const initBucks = 500000n; - const { bucks, moola, bucksBrand, pricer } = setupPricer( - initMoola, - initBucks, - ); - - const output = 10000n; - const pFeePre = protocolFee(output); - const poolChange = output + pFeePre; - const valueIn = priceFromTargetOutput(poolChange, initMoola, initBucks, 24n); - const valueOut = outputFromInputPrice(initBucks, initMoola, valueIn, 24n); - const pFee = protocolFee(valueOut); - t.deepEqual(pricer.getPriceGivenRequiredOutput(bucksBrand, moola(output)), { - amountIn: bucks(valueIn), - amountOut: moola(valueOut - pFee), - protocolFee: moola(pFee), - }); - t.truthy( - (initMoola - valueOut) * (initBucks + valueIn + pFee) > - initBucks * initMoola, - ); -}); - -test('newSwap getPriceGivenRequiredOutput specify secondary', async t => { - const initMoola = 700000n; - const initBucks = 500000n; - const { bucks, moola, moolaBrand, pricer } = setupPricer( - initMoola, - initBucks, - ); - - const output = 10000n; - const valueIn = priceFromTargetOutput(output, initBucks, initMoola, 24n); - const valueOut = outputFromInputPrice(initMoola, initBucks, valueIn, 24n); - const pFee = protocolFee(valueIn); - t.deepEqual(pricer.getPriceGivenRequiredOutput(moolaBrand, bucks(output)), { - amountIn: moola(valueIn + pFee), - amountOut: bucks(valueOut), - protocolFee: moola(pFee), - }); - t.truthy( - (initMoola - valueOut) * (initBucks + valueIn + pFee) > - initBucks * initMoola, - ); -}); - -test('newSwap getPriceGivenAvailableInput twoPools', async t => { - const initMoola = 800000n; - const initBucks = 500000n; - const initSimoleans = 300000n; - const { bucks, moola, simoleans, simoleansBrand, pricer } = setupPricer( - initMoola, - initBucks, - initSimoleans, - ); - - // get price given input from simoleans to bucks through moola, presuming - // there will be no price improvement - const input = 10000n; - const moolaOut = outputFromInputPrice(initBucks, initMoola, input, 12n); - const feeOut = floorDivide(multiply(moolaOut, 6), BASIS_POINTS); - const simOut = outputFromInputPrice( - initMoola, - initSimoleans, - moolaOut - feeOut, - 12n, - ); - t.deepEqual( - pricer.getPriceGivenAvailableInput(bucks(input), simoleansBrand), - { - amountIn: bucks(input), - amountOut: simoleans(simOut), - protocolFee: moola(feeOut), - centralAmount: moola(moolaOut), - }, - ); -}); - -test('newSwap getPriceGivenRequiredOutput twoPools', async t => { - const initMoola = 800000n; - const initBucks = 500000n; - const initSimoleans = 300000n; - const { bucks, moola, simoleans, simoleansBrand, pricer } = setupPricer( - initMoola, - initBucks, - initSimoleans, - ); - - // get price given desired output from simoleans to bucks through moola, - // choosing 10001 so there will be no price improvement - const output = 10001n; - const moolaIn = priceFromTargetOutput(output, initBucks, initMoola, 12n); - const fee = floorDivide(multiply(moolaIn, 6), BASIS_POINTS); - const simIn = priceFromTargetOutput( - moolaIn + fee, - initMoola, - initSimoleans, - 12n, - ); - t.deepEqual( - pricer.getPriceGivenRequiredOutput(simoleansBrand, bucks(output)), - { - amountIn: simoleans(simIn), - amountOut: bucks(output), - protocolFee: moola(fee), - centralAmount: moola(moolaIn), - }, - ); -}); - -test('newSwap getPriceGivenOutput central extreme', async t => { - const initMoola = 700000n; - const initBucks = 500000n; - const { bucks, moola, bucksBrand, pricer } = setupPricer( - initMoola, - initBucks, - ); - - const output = 690000n; - const pFeePre = protocolFee(output); - const poolChange = output + pFeePre; - const valueIn = priceFromTargetOutput(poolChange, initMoola, initBucks, 24n); - const valueOut = outputFromInputPrice(initBucks, initMoola, valueIn, 24n); - const pFee = protocolFee(valueOut); - t.deepEqual(pricer.getPriceGivenRequiredOutput(bucksBrand, moola(output)), { - amountIn: bucks(valueIn), - amountOut: moola(valueOut - pFee), - protocolFee: moola(pFee), - }); - - t.truthy( - (initMoola - valueOut) * (initBucks + valueIn + pFee) > - initBucks * initMoola, - ); -}); - -test('newSwap getPriceGivenInput secondary extreme', async t => { - const moolaPool = 800000n; - const bucksPool = 500000n; - const { bucks, moola, moolaBrand, pricer } = setupPricer( - moolaPool, - bucksPool, - ); - - const input = 690000n; - const valueOut = outputFromInputPrice(bucksPool, moolaPool, input, 24n); - const pFee = protocolFee(valueOut); - const valueIn = priceFromTargetOutput(valueOut, moolaPool, bucksPool, 24n); - t.deepEqual(pricer.getPriceGivenAvailableInput(bucks(input), moolaBrand), { - amountIn: bucks(valueIn), - amountOut: moola(valueOut - pFee), - protocolFee: moola(pFee), - }); - t.truthy( - (moolaPool - valueOut) * (bucksPool + valueIn) > bucksPool * moolaPool, - ); -}); diff --git a/packages/zoe/test/unitTests/contracts/constantProduct/test-edgeCases.js b/packages/zoe/test/unitTests/contracts/constantProduct/test-edgeCases.js deleted file mode 100644 index d6fb4e61262..00000000000 --- a/packages/zoe/test/unitTests/contracts/constantProduct/test-edgeCases.js +++ /dev/null @@ -1,25 +0,0 @@ -// @ts-check - -// eslint-disable-next-line import/no-extraneous-dependencies -import { test } from '@agoric/zoe/tools/prepare-test-env-ava'; - -import { runTest } from './runTest'; - -// Parameters are: -// runPoolAllocationNat, -// secondaryPoolAllocationNat, -// runValueInNat, - -test('specifyRunIn 101, 101, 0', t => { - t.throws(() => runTest(101, 101, 0), { - message: 'runAmountIn cannot be empty', - }); -}); - -test('specifyRunIn 101, 101, 2', t => { - t.true(runTest(101, 101, 2)); -}); - -test('specifyRunIn 101, 101, 3', t => { - t.true(runTest(101, 101, 3)); -}); diff --git a/packages/zoe/test/unitTests/contracts/constantProduct/test-getXY.js b/packages/zoe/test/unitTests/contracts/constantProduct/test-getXY.js index a154628b861..996813c08fe 100644 --- a/packages/zoe/test/unitTests/contracts/constantProduct/test-getXY.js +++ b/packages/zoe/test/unitTests/contracts/constantProduct/test-getXY.js @@ -1,10 +1,10 @@ // @ts-check // eslint-disable-next-line import/no-extraneous-dependencies -import { test } from '@agoric/zoe/tools/prepare-test-env-ava'; +import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; -import { getXY } from '../../../../src/contracts/constantProduct/getXY'; -import { setupMintKits } from './setupMints'; +import { getXY } from '../../../../src/contracts/constantProduct/getXY.js'; +import { setupMintKits } from './setupMints.js'; // There's no difference between SwapIn and SwapOut for this function test('swap Central for Secondary', t => { @@ -16,16 +16,16 @@ test('swap Central for Secondary', t => { Secondary: bld(203838393n), }; const amountWanted = bld(2819n); - const { x, y, deltaX, wantedDeltaY } = getXY( + const { x, y, deltaX, deltaY } = getXY({ amountGiven, poolAllocation, amountWanted, - ); + }); t.deepEqual(x, poolAllocation.Central); t.deepEqual(y, poolAllocation.Secondary); t.deepEqual(deltaX, amountGiven); - t.deepEqual(wantedDeltaY, amountWanted); + t.deepEqual(deltaY, amountWanted); }); test('swap Secondary for Central', t => { @@ -37,14 +37,14 @@ test('swap Secondary for Central', t => { Secondary: bld(203838393n), }; const amountWanted = run(2819n); - const { x, y, deltaX, wantedDeltaY } = getXY( + const { x, y, deltaX, deltaY } = getXY({ amountGiven, poolAllocation, amountWanted, - ); + }); t.deepEqual(x, poolAllocation.Secondary); t.deepEqual(y, poolAllocation.Central); t.deepEqual(deltaX, amountGiven); - t.deepEqual(wantedDeltaY, amountWanted); + t.deepEqual(deltaY, amountWanted); }); diff --git a/packages/zoe/test/unitTests/contracts/constantProduct/test-newBondingCurve.js b/packages/zoe/test/unitTests/contracts/constantProduct/test-newBondingCurve.js deleted file mode 100644 index 784a2ebf70a..00000000000 --- a/packages/zoe/test/unitTests/contracts/constantProduct/test-newBondingCurve.js +++ /dev/null @@ -1,177 +0,0 @@ -// @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 { assertAmountsEqual } from '../../../zoeTestHelpers'; -import { getCurrentPrice } from '../fixedNewSwap'; - -import { - DEFAULT_POOL_FEE, - DEFAULT_PROTOCOL_FEE, -} from '../../../../src/contracts/constantProduct/defaults'; -import { specifyRunIn } from '../../../../src/contracts/constantProduct/specifyRunIn'; -import { - assertKInvariantSellingX, - assertPoolFee, - assertProtocolFee, -} from '../../../../src/contracts/constantProduct/invariants'; -import { calcDeltaYSellingX } from '../../../../src/contracts/constantProduct/core'; -import { setupMintKits } from './setupMints'; - -const conductTestSpecifyRunIn = ( - mintKits, - runPoolAllocationValue, - bldPoolAllocationValue, - runValueIn, - expected, - t, - protocolFeeBP = DEFAULT_PROTOCOL_FEE, - poolFeeBP = DEFAULT_POOL_FEE, -) => { - const { runKit, bldKit } = mintKits; - - const bldPoolAllocation = amountMath.make( - bldKit.brand, - bldPoolAllocationValue, - ); - const runPoolAllocation = amountMath.make( - runKit.brand, - runPoolAllocationValue, - ); - - const runAmountIn = amountMath.make(runKit.brand, runValueIn); - - const result = specifyRunIn( - runAmountIn, - runPoolAllocation, - bldPoolAllocation, - protocolFeeBP, - poolFeeBP, - ); - - Object.entries(expected).forEach(([property, amount]) => { - assertAmountsEqual(t, result[property], amount, property); - }); -}; - -test('test bug scenario', async t => { - const mintKits = setupMintKits(); - const { run, bld } = mintKits; - const bldPoolAllocationValue = 2196247730468n; - const runPoolAllocationValue = 50825056949339n; - const runValueIn = 73000000n; - - const expected = { - protocolFee: run(43800n), - poolFee: bld(7571n), - amountIn: run(72999997n), // buggy newswap quotes 72999951n - amountOut: bld(3145001n), // buggy newswap quotes 3145005n - deltaRun: run(72956197n), - deltaSecondary: bld(3152572n), - newRunPool: run(50825129905536n), - newSecondaryPool: bld(2196244577896n), - inReturnedToUser: run(3n), - }; - - conductTestSpecifyRunIn( - mintKits, - runPoolAllocationValue, - bldPoolAllocationValue, - runValueIn, - expected, - t, - ); -}); - -test('test small values', async t => { - const mintKits = setupMintKits(); - const { run, bld } = mintKits; - const bldPoolAllocationValue = 40000n; - const runPoolAllocationValue = 500000n; - const runValueIn = 5839n; - - const expected = { - protocolFee: run(4n), - poolFee: bld(2n), - amountIn: run(5834n), - amountOut: bld(459n), - deltaRun: run(5830n), - deltaSecondary: bld(461n), - newRunPool: run(505830n), - newSecondaryPool: bld(39539n), - inReturnedToUser: run(5n), - }; - - conductTestSpecifyRunIn( - mintKits, - runPoolAllocationValue, - bldPoolAllocationValue, - runValueIn, - expected, - t, - ); -}); - -test.failing('test bug scenario against fixed newSwap', async t => { - const mintKits = setupMintKits(); - const { run, bld } = mintKits; - const bldPoolAllocationValue = 2196247730468n; - const runPoolAllocationValue = 50825056949339n; - const runValueIn = 73000000n; - - // const expected = { - // protocolFee: run(43800n), - // poolFee: bld(7567n), // 7566 - // amountIn: run(72999997n), // buggy newswap quotes 72999951n - // amountOut: bld(3145005n), // buggy newswap quotes 3145005n - the same - // deltaRun: run(72956197n), - // deltaSecondary: bld(3152572n), - // newRunPool: run(50825129905536n), - // newSecondaryPool: bld(2196244577896n), - // inReturnedToUser: run(3n), - // }; - - const { amountIn, amountOut, protocolFee } = getCurrentPrice( - run(runPoolAllocationValue), - bld(bldPoolAllocationValue), - run(runValueIn), - DEFAULT_PROTOCOL_FEE, - DEFAULT_POOL_FEE, - ); - - // amountIn: run(72999997n) - amountIn is the same - // amountOut: bld(3145007n) - amount out is higher - // protocolFee: run(43773n) - protocolFee is less - - const runPoolAllocation = run(runPoolAllocationValue); - const bldPoolAllocation = bld(bldPoolAllocationValue); - const deltaX = amountMath.subtract(amountIn, protocolFee); - - // This includes the pool fee so it's only checking that including - // the pool fee, k is increasing. - assertKInvariantSellingX( - run(runPoolAllocationValue), - bld(bldPoolAllocationValue), - amountMath.subtract(amountIn, protocolFee), - amountOut, - ); - - const deltaY = calcDeltaYSellingX( - runPoolAllocation, - bldPoolAllocation, - deltaX, - ); - - const poolFee = amountMath.subtract(deltaY, amountOut); - - // This is violated: 5.996 BP not 6 - t.notThrows(() => - assertProtocolFee(protocolFee, amountIn, DEFAULT_PROTOCOL_FEE), - ); - - // This is violated 23.999444263463527 not 24 - t.notThrows(() => assertPoolFee(poolFee, amountOut, DEFAULT_POOL_FEE)); -}); diff --git a/packages/zoe/test/unitTests/contracts/constantProduct/test-swapScenarios.js b/packages/zoe/test/unitTests/contracts/constantProduct/test-swapScenarios.js new file mode 100644 index 00000000000..0a9c1977e38 --- /dev/null +++ b/packages/zoe/test/unitTests/contracts/constantProduct/test-swapScenarios.js @@ -0,0 +1,138 @@ +// @ts-check +// eslint-disable-next-line import/no-extraneous-dependencies +import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; +import { + BASIS_POINTS, + DEFAULT_PROTOCOL_FEE, + DEFAULT_POOL_FEE, +} from '../../../../src/contracts/constantProduct/defaults.js'; + +import { swapIn } from '../../../../src/contracts/constantProduct/swapIn.js'; +import { setupMintKits } from './setupMints.js'; +import { makeRatio } from '../../../../src/contractSupport/index.js'; + +// This assumes run is swapped in. The test should function the same +// regardless of what brand is the amountIn, because no run fee is +// charged. +const prepareSwapInTest = ({ + inputPool, + outputPool, + inputValue, + outputValue, +}) => { + const { run, bld, runKit, bldKit } = setupMintKits(); + const amountGiven = run(inputValue); + const poolAllocation = harden({ + Central: run(inputPool), + Secondary: bld(outputPool), + }); + const amountWanted = bld(outputValue); + const protocolFeeRatio = makeRatio( + DEFAULT_PROTOCOL_FEE, + runKit.brand, + BASIS_POINTS, + ); + const poolFeeRatio = makeRatio(DEFAULT_POOL_FEE, bldKit.brand, BASIS_POINTS); + + const args = [ + amountGiven, + poolAllocation, + amountWanted, + protocolFeeRatio, + poolFeeRatio, + ]; + + return { args, bld, run }; +}; + +const testGetPrice = (t, inputs, expectedOutput) => { + const { args, run, bld } = prepareSwapInTest(inputs); + const result = swapIn(...args); + const expected = harden({ + protocolFee: run(expectedOutput.protocolFee), + poolFee: bld(expectedOutput.poolFee), + swapperGives: run(expectedOutput.swapperGives), + swapperGets: bld(expectedOutput.swapperGets), + xIncrement: run(expectedOutput.xIncrement), + yDecrement: bld(expectedOutput.yDecrement), + newX: run(expectedOutput.newX), + newY: bld(expectedOutput.newY), + improvement: run(expectedOutput.improvement), + }); + t.deepEqual(result, expected); +}; + +// This uses the values that provoked a bug in newSwap. +test('getInputPrice newSwap bug scenario', t => { + const input = { + inputPool: 50825056949339n, + outputPool: 2196247730468n, + inputValue: 73000000n, + outputValue: 100n, + }; + + const firstDeltaY = + (input.outputPool * input.inputValue) / + (input.inputPool + input.inputValue); + const firstImprovedDeltaX = + (input.inputPool * firstDeltaY) / (input.outputPool - firstDeltaY); + const poolFee = 1n + (DEFAULT_POOL_FEE * firstDeltaY) / BASIS_POINTS; + const protocolFee = + 1n + (DEFAULT_PROTOCOL_FEE * firstImprovedDeltaX) / BASIS_POINTS; + + const secondDeltaY = + (input.outputPool * (input.inputValue - protocolFee)) / + (input.inputPool + (input.inputValue - protocolFee)); + const secondImprovedDeltaX = + (input.inputPool * secondDeltaY) / (input.outputPool - secondDeltaY); + const yDecrement = secondDeltaY - poolFee; + const improvement = 3n; + const xIncrement = input.inputValue - protocolFee - improvement; + t.is(secondImprovedDeltaX + 1n, xIncrement); + const expectedOutput = harden({ + poolFee, + protocolFee, + swapperGives: input.inputValue - improvement, + swapperGets: yDecrement, + xIncrement, + yDecrement, + newX: input.inputPool + xIncrement, + newY: input.outputPool - yDecrement, + improvement: 18n, + }); + testGetPrice(t, input, expectedOutput); +}); + +test.only('getInputPrice newSwap example', t => { + const input = { + inputPool: 40000n, + outputPool: 3000n, + inputValue: 300n, + outputValue: 20n, + }; + + const poolFee = 1n; + const protocolFee = 1n; + + const secondDeltaY = + (input.outputPool * (input.inputValue - protocolFee)) / + (input.inputPool + (input.inputValue - protocolFee)); + const secondImprovedDeltaX = + (input.inputPool * secondDeltaY) / (input.outputPool - secondDeltaY); + const yDecrement = secondDeltaY - poolFee; + const improvement = 3n; + const xIncrement = input.inputValue - protocolFee - improvement; + t.is(secondImprovedDeltaX + 1n, xIncrement); + const expectedOutput = harden({ + poolFee, + protocolFee, + swapperGives: input.inputValue - improvement, + swapperGets: yDecrement, + xIncrement, + yDecrement, + newX: input.inputPool + xIncrement, + newY: input.outputPool - yDecrement, + improvement: 4n, + }); + testGetPrice(t, input, expectedOutput); +}); From 2321dcfdedfa101eb36180364a816369f2df313a Mon Sep 17 00:00:00 2001 From: Chris Hibbert Date: Wed, 22 Sep 2021 17:16:34 -0700 Subject: [PATCH 4/8] docs: improve the README, and add some more typescript --- .../src/contracts/constantProduct/README.md | 117 ++++++++++++------ .../zoe/src/contracts/constantProduct/core.js | 5 +- .../constantProduct/internal-types.js | 61 +++++++++ .../zoe/src/contracts/constantProduct/swap.js | 84 +++++++++++-- 4 files changed, 214 insertions(+), 53 deletions(-) create mode 100644 packages/zoe/src/contracts/constantProduct/internal-types.js diff --git a/packages/zoe/src/contracts/constantProduct/README.md b/packages/zoe/src/contracts/constantProduct/README.md index 716602cb9fe..6daeb2774f3 100644 --- a/packages/zoe/src/contracts/constantProduct/README.md +++ b/packages/zoe/src/contracts/constantProduct/README.md @@ -1,24 +1,25 @@ # Constant Product AMM -A simpler constant product automatic market maker based on our Ratio library and -charges two kinds of fees. The pool fee remains in the pool to reward the -liquidity providers. The protocol fee is extracted to fund community efforts. - -This algorithm uses the x*y=k formula directly, without complicating it with -fees. Briefly, there are two pools of assets, whose values are kept roughly in -balance through the actions of arbitrageurs. At any time, a trader can come to -the pool and offer to deposit one of the two assets. They will receive an amount +A constant product automatic market maker based on our Ratio library. It charges +two kinds of fees: a pool fee remains in the pool to reward the liquidity +providers and a protocol fee is extracted to fund the economy. + +This algorithm uses the x*y=k formula directly, without fees. Briefly, there are +two kinds of assets, whose values are kept roughly in balance through the +actions of arbitrageurs. At any time, a trader can come to the pool and offer to +deposit one of the two assets. They will receive an amount of the complementary asset that will maintain the invariant that the product of -the pool values doesn't change. (Except that rounding is done in favor of the +the balances doesn't change. (Except that rounding is done in favor of the pool.) The liquidity providers are rewarded by charging a fee. The user can specify a maximum amount they want to pay or a minimum amount they want to receive. Unlike Uniswap, this approach will charge less than the user -offered or pay more than they asked for when appropriate. (By analogy, if a user +offered or pay more than they asked for when appropriate. By analogy, if a user is willing to pay up to $20 when the price of soda is $3 per bottle, it would give 6 bottles and only charge $18. Uniswap doesn't adjust the provided price, so it charges $20. This matters whenever the values of the smallest unit of the -currencies are significantly different, which is common in defi.) +currencies are significantly different, which is common in DeFi. (We refer to +these as "improved" prices.) The rules that drive the design include @@ -26,7 +27,7 @@ The rules that drive the design include (or receive less) than they said. * The pool fee is charged against the computed side of the price. * The protocol fee is always charged in RUN. -* The fees should be calculated based on the pool's prices before a transaction. +* The fees should be calculated based on the pool balances before a transaction. * Computations are rounded in favor of the pool. We start by estimating the exchange rate, and calculate fees based on that. Once @@ -35,33 +36,71 @@ extracted from the pools to adhere to those rules. ## Calculating fees -In this table BLD represents any collateral. ΔX is always the amount contributed to the pool, and ΔY is always -the amount extracted from the pool. - -| | In (X) | Out (Y) | PoolFee | Protocol Fee | ΔX | ΔY | pool Fee * | -|---------|-----|-----|--------|-----|-----|-----|-----| -| **RUN in** | RUN | BLD | BLD | RUN | sGive - PrFee | sGets | sGet - ΔY -| **RUN out** | BLD | RUN | RUN | BLD | sGive | sGets + PrFee | ΔX - sGive -| **BLD in** | BLD | RUN | BLD | RUN | sGive | sGest + PrFee | ΔY - sGet -| **BLD out** | RUN | BLD | RUN | BLD | sGive - PrFee | sGets | sGive - ΔX - -(*) The Pool Fee remains in the pool, so its impact on the calculation is -subtle. When amountIn is specified, we add the poolFee to any minimum amountOut -from the user since the trade has to produce amoutOut plus the required fee in -order to be satisfactory. When amountOut is specified, we subtract the fee from any -amountIn max from the user since the fee has to come out of the user's deposit. - -* When the amount of RUN provided is specified, (**RUN in**) we subtract - the poolFee from the amount the user will give before using the reduced amount - in the derivation of ΔY from ΔX. -* When the amount of RUN being paid out is specified (**RUN out**), we add the - poolFee to ΔX, which was calculated from the requested payout. -* When the amount of BLD to be paid in is specified (**BLD in**), the - amount the user gets is computed by subtracting the poolFee from ΔY - which already had the protocolFee included. -* When the amount of BLD to be paid out is specified (**BLD out**), ΔX is - computed from the required payout, and the poolFee is added to that to get the - amount the user must pay. +In these tables BLD represents any collateral. The user can specify how much +they want or how much they're willing to pay. We'll call the value they +specified **sGive** or **sGet** and bold it. This table shows which brands the +amounts each have, as well as what is computed vs. given. The PoolFee is +computed based on the calculated amount (BLD in rows 1 and 2; RUN in rows 3 and +4). The Protocol fee is always in RUN. + +| | In (X) | Out (Y) | PoolFee | Protocol Fee | Specified | Computed | +|---------|-----|-----|--------|-----|------|-----| +| **RUN in** | RUN | BLD | BLD | RUN | **sGive** | sGet | +| **RUN out** | BLD | RUN | BLD | RUN | **sGet** | sGive | +| **BLD in** | BLD | RUN | RUN | RUN | **sGive** | sGet | +| **BLD out** | RUN | BLD | RUN | RUN | **sGet** | sGive | + +We'll estimate how much the pool balances would change in the no-fee, improved +price case using the constant product formulas. These estimates are δX, +and δY. The fees are based on δX, and δY. ρ is the poolFee +(e.g. .003). + +The pool fee will be ρ times whichever of δX and δY was +calculated. The protocol fee will be ρ * δX when RUN is paid in, and +ρ * δY when BLD is paid in. + +| | δX | δY | PoolFee | Protocol Fee | +|---------|-----|-----|--------|-----| +| **RUN in** | **sGive** | calc | ρ × δY | ρ × **sGive** (= ρ × δX) | +| **RUN out** | calc | **sGet** | ρ × δX | ρ × **sGet** (= ρ × δY) | +| **BLD in** | **sGive** | calc | ρ × δY | ρ × δY | +| **BLD out** | calc | **sGet** | ρ × δX | ρ × δX | + +In rows 1 and 3, **sGive** was specified and sGet will be calculated. In rows 2 +and 4, **sGet** was specified and sGive will be calculated. Once we know the +fees, we can add or subtract the fees and calculate the pool changes. + +ΔX is the incrementing side of the constant product calculation, and +ΔY is the decrementing side. If **sGive** is known, we subtract fees to +get ΔX and calculate ΔY. If **sGet** is known, we add fees to +get ΔY and calculate ΔX. ΔY and ΔX are the values +that maintain the constant product invariant. + +Notice that the ProtocolFee always affects the inputs to the constant product +calculation (because it is collected outside the pool). The PoolFee is visible +in the formulas in this table when the input to the calculation is in RUN. + +| | ΔX | ΔY | +|---------|-----|-----| +| **RUN in** | **sGive** - ProtocolFee | calc | +| **RUN out** | calc | **sGet** + ProtocolFee + PoolFee | +| **BLD in** | **sGive** - ProtocolFee - PoolFee | calc | +| **BLD out** | calc | **sGet** + ProtocolFee | + +Now we can compute the change in the pool balances, and the amount the trader +would pay and receive. + +| | xIncr | yDecr | pay In | pay Out | +|---------|-----|-----|-----|-----| +| **RUN in** | ΔX | ΔY - PoolFee | ΔX + protocolFee | ΔY - PoolFee | +| **RUN out** | ΔX | ΔY - PoolFee | ΔX | ΔY - PoolFee - ProtocolFee | +| **BLD in** | ΔX + PoolFee | ΔY | ΔX + PoolFee | ΔY - ProtocolFee | +| **BLD out** | ΔX + PoolFee | ΔY | ΔX + PoolFee + ProtocolFee | ΔY | + +In the two right columns the protocolFee is either added to the amount the +trader pays, or subtracted from the proceeds. The poolFee does the same on the +trader side, and it is either added to the amount deposited in the pool (xIncr) +or deducted from the amout removed from the pool (yDecr). ## Example diff --git a/packages/zoe/src/contracts/constantProduct/core.js b/packages/zoe/src/contracts/constantProduct/core.js index e777d587911..d399b0b4605 100644 --- a/packages/zoe/src/contracts/constantProduct/core.js +++ b/packages/zoe/src/contracts/constantProduct/core.js @@ -1,12 +1,13 @@ // @ts-check import { AmountMath } from '@agoric/ertp'; -import { assert, details as X, q } from '@agoric/assert'; import { natSafeMath } from '../../contractSupport/index.js'; import { makeRatioFromAmounts } from '../../contractSupport/ratio.js'; import { getXY } from './getXY.js'; +const { details: X, quote: q } = assert; + const assertSingleBrand = ratio => { assert( ratio.numerator.brand === ratio.denominator.brand, @@ -116,11 +117,13 @@ const swapOutImproved = ({ }); }; +/** @type {NoFeeSwapFn} */ export const swapInNoFees = ({ amountGiven, poolAllocation }) => { const XY = getXY({ amountGiven, poolAllocation }); return swapInReduced(XY); }; +/** @type {NoFeeSwapFn} */ export const swapOutNoFees = ({ poolAllocation, amountWanted }) => { const XY = getXY({ poolAllocation, amountWanted }); return swapOutImproved(XY); diff --git a/packages/zoe/src/contracts/constantProduct/internal-types.js b/packages/zoe/src/contracts/constantProduct/internal-types.js new file mode 100644 index 00000000000..1c6c95d97dd --- /dev/null +++ b/packages/zoe/src/contracts/constantProduct/internal-types.js @@ -0,0 +1,61 @@ +// @ts-check + +/** + * @typedef {Object} ImprovedNoFeeSwapResult + * @property {Amount} amountIn + * @property {Amount} amountOut + * @property {Amount} improvement + */ + +/** + * @typedef {Object} FeePair + * + * @property {Amount} poolFee + * @property {Amount} protocolFee + */ + +/** + * @typedef {Object} PoolAllocation + * + * @property {Amount} Central + * @property {Amount} Secondary + */ + +/** + * @typedef {Object} NoFeeSwapFnInput + * @property {Amount} amountGiven + * @property {Amount} amountWanted + * @property {Brand=} brand + * @property {PoolAllocation} poolAllocation + */ + +/** + * @typedef {Object} SwapResult + * + * @property {Amount} xIncrement + * @property {Amount} swapperGives + * @property {Amount} yDecrement + * @property {Amount} swapperGets + * @property {Amount} improvement + * @property {Amount} protocolFee + * @property {Amount} poolFee + * @property {Amount} newY + * @property {Amount} newX + */ + +/** + * @callback NoFeeSwapFn + * @param {NoFeeSwapFnInput} input + * @returns {ImprovedNoFeeSwapResult} + */ + +/** + * @callback InternalSwap + * @param {Amount} amountGiven + * @param {PoolAllocation} poolAllocation + * @param {Amount} amountWanted + * @param {Ratio} protocolFeeRatio + * @param {Ratio} poolFeeRatio + * @param {NoFeeSwapFn} swapFn + * @returns {SwapResult} + */ diff --git a/packages/zoe/src/contracts/constantProduct/swap.js b/packages/zoe/src/contracts/constantProduct/swap.js index 5e0e2153f3a..d2969127d19 100644 --- a/packages/zoe/src/contracts/constantProduct/swap.js +++ b/packages/zoe/src/contracts/constantProduct/swap.js @@ -1,9 +1,19 @@ // @ts-check -import { assert, details as X } from '@agoric/assert'; import { AmountMath } from '@agoric/ertp'; import { calculateFees, amountGT, maximum } from './calcFees.js'; +const { details: X } = assert; + +/** + * The fee might not be in the same brand as the amount. If they are the same, + * subtract the fee from the amount (returning empty if the fee is larger). + * Otherwise return the unadjusted amount. + * + * @param {Amount} amount + * @param {Amount} fee + * @returns {Amount} + */ const subtractRelevantFees = (amount, fee) => { if (amount.brand === fee.brand) { if (AmountMath.isGTE(fee, amount)) { @@ -15,6 +25,14 @@ const subtractRelevantFees = (amount, fee) => { return amount; }; +/** + * PoolFee and ProtocolFee each identify their brand. If either (or both) match + * the brand of the Amount, subtract it/them from the amount. + * + * @param {Amount} amount + * @param {FeePair} fee + * @returns {Amount} + */ const subtractFees = (amount, { poolFee, protocolFee }) => { return subtractRelevantFees( subtractRelevantFees(amount, protocolFee), @@ -22,6 +40,14 @@ const subtractFees = (amount, { poolFee, protocolFee }) => { ); }; +/** + * The fee might not be in the same brand as the amount. If they are the same, + * add the fee to the amount. Otherwise return the unadjusted amount. + * + * @param {Amount} amount + * @param {Amount} fee + * @returns {Amount} + */ const addRelevantFees = (amount, fee) => { if (amount.brand === fee.brand) { return AmountMath.add(amount, fee); @@ -29,10 +55,31 @@ const addRelevantFees = (amount, fee) => { return amount; }; +/** + * PoolFee and ProtocolFee each identify their brand. If either (or both) match + * the brand of the Amount, add it/them to the amount. + * + * @param {Amount} amount + * @param {FeePair} fee + * @returns {Amount} + */ const addFees = (amount, { poolFee, protocolFee }) => { return addRelevantFees(addRelevantFees(amount, protocolFee), poolFee); }; +/** + * Increment or decrement a pool balance by an amount. The amount's brand might + * match the Central or Secondary balance of the pool. Return the adjusted + * balance. The caller knows which amount they provided, so they're expecting a + * single Amount whose brand matches the amount parameter. + * + * The first parameter specifies whether we're incrementing or decrementing from the pool + * + * @param {(amountLeft: Amount, amountRight: Amount, brand?: Brand) => Amount} addOrSub + * @param {PoolAllocation} poolAllocation + * @param {Amount} amount + * @returns {Amount} + */ const addOrSubtractFromPool = (addOrSub, poolAllocation, amount) => { if (poolAllocation.Central.brand === amount.brand) { return addOrSub(poolAllocation.Central, amount); @@ -41,7 +88,7 @@ const addOrSubtractFromPool = (addOrSub, poolAllocation, amount) => { } }; -const assertGreaterThanZeroHelper = (amount, name) => { +const assertGreaterThanZero = (amount, name) => { assert( amount && !AmountMath.isGTE(AmountMath.makeEmptyFromAmount(amount), amount), X`${name} must be greater than 0: ${amount}`, @@ -54,6 +101,17 @@ const isWantedAvailable = (poolAllocation, amountWanted) => { : !AmountMath.isGTE(amountWanted, poolAllocation.Secondary); }; +/** + * We've identified a violation of constraints that means we won't be able to + * satisfy the user's request. (Not enough funds in the pool, too much was + * requested, the proceeds would be empty, etc.) Return a result that says no + * trade will take place and the pool balances won't change. + * + * @param {Amount} amountGiven + * @param {Amount} amountWanted + * @param {PoolAllocation} poolAllocation + * @param {Ratio} poolFee + */ function noTransaction(amountGiven, amountWanted, poolAllocation, poolFee) { const emptyGive = AmountMath.makeEmptyFromAmount(amountGiven); const emptyWant = AmountMath.makeEmptyFromAmount(amountWanted); @@ -83,6 +141,7 @@ function noTransaction(amountGiven, amountWanted, poolAllocation, poolFee) { return result; } +/** @type {InternalSwap} */ export const swap = ( amountGiven, poolAllocation, @@ -91,11 +150,8 @@ export const swap = ( poolFeeRatio, swapFn, ) => { - assertGreaterThanZeroHelper(poolAllocation.Central, 'poolAllocation.Central'); - assertGreaterThanZeroHelper( - poolAllocation.Secondary, - 'poolAllocation.Secondary', - ); + assertGreaterThanZero(poolAllocation.Central, 'poolAllocation.Central'); + assertGreaterThanZero(poolAllocation.Secondary, 'poolAllocation.Secondary'); assert( (amountGiven && !AmountMath.isGTE( @@ -140,11 +196,13 @@ export const swap = ( ); } - // calculate no-fee amounts. swapFn will only pay attention to the specified - // value. The pool fee is always charged on the unspecified side, so it won't - // affect the calculation. When the specified value is in RUN, the protocol - // fee will be deducted from amountGiven before adding to the pool or from - // amountOut to calculate swapperGets. + // Calculate no-fee amounts. swapFn will only pay attention to the `specified` + // value. The pool fee is always charged on the unspecified side, so it is an + // output of the calculation. When the specified value is in RUN, the protocol + // fee will be deducted from amountGiven before adding to the pool. When BLD + // was specified, we add the protocol fee to amountWanted. When the specified + // value is in RUN, the protocol fee will be deducted from amountGiven before + // adding to the pool or added to amountWanted to calculate amoutOut. const { amountIn, amountOut, improvement } = swapFn({ amountGiven: subtractFees(amountGiven, fees), poolAllocation, @@ -182,7 +240,7 @@ export const swap = ( // poolFee is the amount the pool will grow over the no-fee calculation. // protocolFee is to be separated and sent to an external purse. - // The swapper amounts are what will we paid and received. + // The swapper amounts are what will be paid and received. // xIncrement and yDecrement are what will be added and removed from the pools. // Either xIncrement will be increased by the pool fee or yDecrement will be // reduced by it in order to compensate the pool. From efcb009f5d9d9aed8b5284f46a0dac0d243ab086 Mon Sep 17 00:00:00 2001 From: Chris Hibbert Date: Tue, 28 Sep 2021 09:17:42 -0700 Subject: [PATCH 5/8] chore: cleanup constantProduct calculations Drop jsverify improve README.md add more type declarations extract common code in swapIn & swapOut drop reporting on price improvements reduce duplicated code in swap.js more tests --- .../src/contracts/constantProduct/README.md | 96 ++++++++------ .../src/contracts/constantProduct/calcFees.js | 61 +++++++-- .../constantProduct/calcSwapPrices.js | 78 ++++++++++++ .../constantProduct/checkInvariants.js | 44 ------- .../zoe/src/contracts/constantProduct/core.js | 2 - .../src/contracts/constantProduct/getXY.js | 10 +- .../constantProduct/internal-types.js | 27 +++- .../contracts/constantProduct/invariants.js | 64 ++-------- .../zoe/src/contracts/constantProduct/swap.js | 117 ++++++++++-------- .../src/contracts/constantProduct/swapIn.js | 30 ----- .../src/contracts/constantProduct/swapOut.js | 30 ----- .../src/contracts/constantProduct/types.js | 15 +++ .../test-compareBondingCurves.js | 15 +-- .../test-compareNewSwapPrice.js | 7 +- .../contracts/constantProduct/test-getXY.js | 85 ++++++++++++- .../constantProduct/test-swapScenarios.js | 61 +++++++-- 16 files changed, 449 insertions(+), 293 deletions(-) create mode 100644 packages/zoe/src/contracts/constantProduct/calcSwapPrices.js delete mode 100644 packages/zoe/src/contracts/constantProduct/checkInvariants.js delete mode 100644 packages/zoe/src/contracts/constantProduct/swapIn.js delete mode 100644 packages/zoe/src/contracts/constantProduct/swapOut.js create mode 100644 packages/zoe/src/contracts/constantProduct/types.js diff --git a/packages/zoe/src/contracts/constantProduct/README.md b/packages/zoe/src/contracts/constantProduct/README.md index 6daeb2774f3..a14c31f2f60 100644 --- a/packages/zoe/src/contracts/constantProduct/README.md +++ b/packages/zoe/src/contracts/constantProduct/README.md @@ -6,11 +6,11 @@ providers and a protocol fee is extracted to fund the economy. This algorithm uses the x*y=k formula directly, without fees. Briefly, there are two kinds of assets, whose values are kept roughly in balance through the -actions of arbitrageurs. At any time, a trader can come to the pool and offer to -deposit one of the two assets. They will receive an amount +actions of arbitrageurs. At any time a trader can trade with the pool by +offering to deposit one of the two assets. They will receive an amount of the complementary asset that will maintain the invariant that the product of -the balances doesn't change. (Except that rounding is done in favor of the -pool.) The liquidity providers are rewarded by charging a fee. +the balances doesn't decrease. (Rounding is done in favor of the +pool.) A fee is charged on the swap to reward the liquidity providers. The user can specify a maximum amount they want to pay or a minimum amount they want to receive. Unlike Uniswap, this approach will charge less than the user @@ -25,7 +25,8 @@ The rules that drive the design include * When the user names an input (or output) price, they shouldn't pay more (or receive less) than they said. -* The pool fee is charged against the computed side of the price. +* The pool fee is charged against the side not specified by the user (the + "computed side"). * The protocol fee is always charged in RUN. * The fees should be calculated based on the pool balances before a transaction. * Computations are rounded in favor of the pool. @@ -80,15 +81,16 @@ Notice that the ProtocolFee always affects the inputs to the constant product calculation (because it is collected outside the pool). The PoolFee is visible in the formulas in this table when the input to the calculation is in RUN. -| | ΔX | ΔY | +| | input estimate | output estimate | |---------|-----|-----| -| **RUN in** | **sGive** - ProtocolFee | calc | -| **RUN out** | calc | **sGet** + ProtocolFee + PoolFee | -| **BLD in** | **sGive** - ProtocolFee - PoolFee | calc | -| **BLD out** | calc | **sGet** + ProtocolFee | +| **RUN in** | **sGive** - ProtocolFee | | +| **RUN out** | | **sGet** + ProtocolFee + PoolFee | +| **BLD in** | **sGive** - ProtocolFee - PoolFee | | +| **BLD out** | | **sGet** + ProtocolFee | -Now we can compute the change in the pool balances, and the amount the trader -would pay and receive. +We use the estimate of the amount in or out to calculate the improved ΔX +and ΔY, which tells us how much the trader will pay, the changes in pool +balances, and what the trader will receive. | | xIncr | yDecr | pay In | pay Out | |---------|-----|-----|-----|-----| @@ -99,41 +101,59 @@ would pay and receive. In the two right columns the protocolFee is either added to the amount the trader pays, or subtracted from the proceeds. The poolFee does the same on the -trader side, and it is either added to the amount deposited in the pool (xIncr) +left side, and it is either added to the amount deposited in the pool (xIncr) or deducted from the amout removed from the pool (yDecr). ## Example -For example, if the pools were at 40000 RUN and 3000 BLD and the user's offer -specifies that they want to buy BLD and are willing to spend up to 300 RUN, the -fees will be 1 RUN and 1 BLD because the amounts are low for expository -purposes. Since the user specified the input price, we calculate the output -using the constant product formula for ΔY. The protocol fee is always -charged in RUN, so the pool will only gain 299 from the user's 300 RUN. +For example, let's say the pool has 40,000,000 RUN and 3,000,000 BLD. Alice +requests a swapIn with inputAmount of 30,000 RUN, and outputAmount of 2000 BLD. +(SwapIn means the inputValue is the basis of the computation, while outputAmount +is treated as a minimum). To make the numbers concrete, we'll say the pool fee +is 25 Basis Points, and the protocol fee is 5 Basis Points. -(3000 * 299) / (40000n + 299) = 22 +The first step is to compute the trade that would take place with no fees. 30K +will be added to 40M RUN. To keep the product just above 120MM, the BLD will be +reduced to 2,997,752. -Notice that 23 gives a product just below x*y, and 22 just above -(3000n + 23n) * (40000n + 299n) < 3000n * 40000n -3000n * 40000n < (3000n + 22n) * (40000n + 299n) +``` +40,030,000 * 2,997,752 > 40,000,000 * 3,000,000 > 40,030,000 * 2,997,751 + 120000012560000 > 120000000000000 > 119999972530000 +``` -We then calculate how much the user should actually pay for that using the -deltaX formula, which tells us that the pool would be able to maintain its -invariants if it charged 296, so the user won't have to pay the whole 300 that -was offered. We will add 1 +But we get an even tighter bound by reducing the amount Alice has to spend -(40000n * 22n) / (3000n - 22n) = 296 +``` +40,029,996 * 2,997,752 > 40,000,000 * 3,000,000 > 40,029,995 * 2,997,752 + 120000000568992 > 120000000000000 > 119999997571240 +``` -This time 295 and 296 bracket the required value. -(3000n - 22n) * (40000n + 295n) < 3000n * 40000n -3000n * 40000n < (3000n - 22n) * (40000n + 296n) +The initial price estimate is that 29,996 RUN would get 2249 BLD in a no-fee +pool. We base fees on this estimate, so the **protocol Fee will be 15 RUN** +(always in RUN) and the **pool fee will be 2 BLD**. The pool fee is calculated +on the output for `swapIn` and the input for `swapOut`. -The pool fee will be subtracted from the proceeds before paying the user, so the -result is that the user pays 297 RUN and gets 21 BLD. The pool's K changes from -120M to 120041784n reflecting the pool fee, and 1 BLD is paid to the protocol -fee. +Now we calculate the actual ΔX and ΔY, since the fees affect the +size of the changes to the pool. From the first row of the third table we see +that the calculation starts from ΔX of +`sGive - ProtocolFee (i.e. 29,996 - 15 = 29,981)` -A withdrawal from the pool of 22 build would have maintained the invariants; -we withdrew 21 instead +``` +40,029,981 * 2,997,754 > 40,000,000 * 3,000,000 > 40,029,981 * 2,997,753 +``` -(3000n - 21n) * (40000n + 296n) +and re-checking how much is required to produce 2,997,754, we get + +``` +40,029,970 * 2,997,754 > 40,000,000 * 3,000,000 > 40,029,969 * 2,997,754 +``` + +**ΔX is 29,970, and ΔY is 2246**. + + * Alice pays ΔX + protocolFee, which is 29970 + 15 (29985 RUN) + * Alice will receive ΔY - PoolFee which is 2246 - 2 (2244 BLD) + * The RUN in the pool will increase by ΔX (29970 RUN) + * The BLD in the pool will decrease by ΔY (2246 BLD) + +The Pool grew by 2 BLD more than was required to maintain the constant product +invariant. 15 RUN were extracted for the protocol fee. diff --git a/packages/zoe/src/contracts/constantProduct/calcFees.js b/packages/zoe/src/contracts/constantProduct/calcFees.js index a1b02430bd3..a128e79c2d6 100644 --- a/packages/zoe/src/contracts/constantProduct/calcFees.js +++ b/packages/zoe/src/contracts/constantProduct/calcFees.js @@ -5,6 +5,8 @@ import { ceilMultiplyBy, makeRatio } from '../../contractSupport/ratio.js'; import { BASIS_POINTS } from './defaults.js'; +const { details: X } = assert; + /** * Make a ratio given a nat representing basis points * @@ -12,41 +14,67 @@ import { BASIS_POINTS } from './defaults.js'; * @param {Brand} brandOfFee * @returns {Ratio} */ -export const makeFeeRatio = (feeBP, brandOfFee) => { +const makeFeeRatio = (feeBP, brandOfFee) => { return makeRatio(feeBP, brandOfFee, BASIS_POINTS); }; -export const maximum = (left, right) => { +/** @type {Maximum} */ +const maximum = (left, right) => { // If left is greater or equal, return left. Otherwise return right. return AmountMath.isGTE(left, right) ? left : right; }; -export const amountGT = (left, right) => +/** @type {AmountGT} */ +const amountGT = (left, right) => AmountMath.isGTE(left, right) && !AmountMath.isEqual(left, right); /** - * @param {{ amountIn: Amount, amountOut: Amount}} amounts - an array of two - * amounts in different brands. We must select the amount of the same brand as - * the feeRatio. + * Apply the feeRatio to the amount that has a matching brand. This used to + * calculate fees in the single pool case. + * + * @param {{ amountIn: Amount, amountOut: Amount}} amounts - a record with two + * amounts in different brands. * @param {Ratio} feeRatio * @returns {Amount} */ const calcFee = ({ amountIn, amountOut }, feeRatio) => { - const sameBrandAmount = - amountIn.brand === feeRatio.numerator.brand ? amountIn : amountOut; + assert( + feeRatio.numerator.brand === feeRatio.denominator.brand, + X`feeRatio numerator and denominator must use the same brand ${feeRatio}`, + ); + + let sameBrandAmount; + if (amountIn.brand === feeRatio.numerator.brand) { + sameBrandAmount = amountIn; + } else if (amountOut.brand === feeRatio.numerator.brand) { + sameBrandAmount = amountOut; + } else { + assert( + false, + X`feeRatio's brand (${feeRatio.numerator.brand}) must match one of the amounts [${amountIn}, ${amountOut}].`, + ); + } + // Always round fees up const fee = ceilMultiplyBy(sameBrandAmount, feeRatio); // Fee cannot exceed the amount on which it is levied - assert(AmountMath.isGTE(sameBrandAmount, fee)); + assert( + AmountMath.isGTE(sameBrandAmount, fee), + X`The feeRatio can't be greater than 1 ${feeRatio}`, + ); return fee; }; -// SwapIn uses calcDeltaYSellingX -// SwapOut uses calcDeltaXSellingX - -export const calculateFees = ( +/** + * Estimate the swap values, then calculate fees. The swapFn provided by the + * caller will be calcDeltaYSellingX for SwapIn and calcDeltaXSellingX for + * SwapOut. + * + * @type {CalculateFees} + */ +const calculateFees = ( amountGiven, poolAllocation, amountWanted, @@ -62,3 +90,10 @@ export const calculateFees = ( return harden({ protocolFee, poolFee, ...estimation }); }; + +harden(amountGT); +harden(maximum); +harden(makeFeeRatio); +harden(calculateFees); + +export { amountGT, maximum, makeFeeRatio, calculateFees }; diff --git a/packages/zoe/src/contracts/constantProduct/calcSwapPrices.js b/packages/zoe/src/contracts/constantProduct/calcSwapPrices.js new file mode 100644 index 00000000000..2de98326839 --- /dev/null +++ b/packages/zoe/src/contracts/constantProduct/calcSwapPrices.js @@ -0,0 +1,78 @@ +// @ts-check + +import { Far } from '@agoric/marshal'; + +import { swap } from './swap.js'; +import { assertKInvariantSellingX } from './invariants.js'; +import { getXY } from './getXY.js'; +import { swapInNoFees, swapOutNoFees } from './core.js'; + +const makeCalcSwapPrices = noFeesSwap => { + return Far( + 'calcSwapPrices', + ( + amountGiven, + poolAllocation, + amountWanted, + protocolFeeRatio, + poolFeeRatio, + ) => { + const result = swap( + amountGiven, + poolAllocation, + amountWanted, + protocolFeeRatio, + poolFeeRatio, + noFeesSwap, + ); + const { x, y } = getXY({ + amountGiven, + poolAllocation, + amountWanted, + }); + assertKInvariantSellingX(x, y, result.xIncrement, result.yDecrement); + return result; + }, + ); +}; + +/** @type {CalcSwapPrices} */ +const calcSwapInPrices = ( + amountGiven, + poolAllocation, + amountWanted, + protocolFeeRatio, + poolFeeRatio, +) => { + const calcSwapPrices = makeCalcSwapPrices(swapInNoFees); + return calcSwapPrices( + amountGiven, + poolAllocation, + amountWanted, + protocolFeeRatio, + poolFeeRatio, + ); +}; + +/** @type {CalcSwapPrices} */ +const calcSwapOutPrices = ( + amountGiven, + poolAllocation, + amountWanted, + protocolFeeRatio, + poolFeeRatio, +) => { + const calcSwapPrices = makeCalcSwapPrices(swapOutNoFees); + return calcSwapPrices( + amountGiven, + poolAllocation, + amountWanted, + protocolFeeRatio, + poolFeeRatio, + ); +}; + +harden(calcSwapInPrices); +harden(calcSwapOutPrices); + +export { calcSwapOutPrices, calcSwapInPrices }; diff --git a/packages/zoe/src/contracts/constantProduct/checkInvariants.js b/packages/zoe/src/contracts/constantProduct/checkInvariants.js deleted file mode 100644 index 04866d162d4..00000000000 --- a/packages/zoe/src/contracts/constantProduct/checkInvariants.js +++ /dev/null @@ -1,44 +0,0 @@ -// @ts-check - -import { assertRightsConserved } from '../../contractFacet/rightsConservation.js'; - -import { - assertKInvariantSellingX, - assertPoolFee, - assertProtocolFee, -} from './invariants.js'; - -export const checkAllInvariants = ( - runPoolAllocation, - secondaryPoolAllocation, - runAmountIn, - protocolFeeBP, - poolFeeBP, - result, -) => { - // double check invariants - assertKInvariantSellingX( - runPoolAllocation, - secondaryPoolAllocation, - result.deltaRun, - result.deltaSecondary, - ); - - const priorAmounts = [ - runPoolAllocation, - secondaryPoolAllocation, - runAmountIn, - ]; - const newAmounts = [ - result.newRunPool, - result.protocolFee, - result.newSecondaryPool, - result.amountOut, - result.poolFee, - result.inReturnedToUser, - ]; - - assertRightsConserved(priorAmounts, newAmounts); - assertProtocolFee(result.protocolFee, result.amountIn, protocolFeeBP); - assertPoolFee(result.poolFee, result.amountOut, poolFeeBP); -}; diff --git a/packages/zoe/src/contracts/constantProduct/core.js b/packages/zoe/src/contracts/constantProduct/core.js index d399b0b4605..d6cfcd4c318 100644 --- a/packages/zoe/src/contracts/constantProduct/core.js +++ b/packages/zoe/src/contracts/constantProduct/core.js @@ -96,7 +96,6 @@ const swapInReduced = ({ x: inPool, y: outPool, deltaX: offeredAmountIn }) => { return harden({ amountIn: reducedAmountIn, amountOut, - improvement: AmountMath.subtract(offeredAmountIn, reducedAmountIn), }); }; @@ -113,7 +112,6 @@ const swapOutImproved = ({ return harden({ amountIn, amountOut: improvedAmountOut, - improvement: AmountMath.subtract(improvedAmountOut, wantedAmountOut), }); }; diff --git a/packages/zoe/src/contracts/constantProduct/getXY.js b/packages/zoe/src/contracts/constantProduct/getXY.js index fae48d84f66..0cd3084b450 100644 --- a/packages/zoe/src/contracts/constantProduct/getXY.js +++ b/packages/zoe/src/contracts/constantProduct/getXY.js @@ -1,13 +1,21 @@ +// @ts-check + // This does not support secondary to secondary. That has to happen at // a higher abstraction /** + * The caller provides poolAllocation, which has balances for both Central and + * Secondary, and at least one of amountGiven and amountWanted. getXY treats + * the amountGiven as the X pool, and amountWanted as Y. It figures out which + * way to pair up X and Y with Central and Secondary, and returns the pool + * balances as X and Y and given and wanted as deltaX and deltaY + * { X, Y, deltaX, deltaY }. * * @param {Object} opt * @param {Amount=} opt.amountGiven * @param {{ Central: Amount, Secondary: Amount }} opt.poolAllocation * @param {Amount=} opt.amountWanted - * @returns {{ x: Amount, y: Amount, deltaX: Amount, deltaY: Amount }} + * @returns {{ x: Amount, y: Amount, deltaX: Amount | undefined, deltaY: Amount | undefined }} */ export const getXY = ({ amountGiven, poolAllocation, amountWanted }) => { // Regardless of whether we are specifying the amountIn or the diff --git a/packages/zoe/src/contracts/constantProduct/internal-types.js b/packages/zoe/src/contracts/constantProduct/internal-types.js index 1c6c95d97dd..8d0cde03fc1 100644 --- a/packages/zoe/src/contracts/constantProduct/internal-types.js +++ b/packages/zoe/src/contracts/constantProduct/internal-types.js @@ -4,7 +4,6 @@ * @typedef {Object} ImprovedNoFeeSwapResult * @property {Amount} amountIn * @property {Amount} amountOut - * @property {Amount} improvement */ /** @@ -36,7 +35,6 @@ * @property {Amount} swapperGives * @property {Amount} yDecrement * @property {Amount} swapperGets - * @property {Amount} improvement * @property {Amount} protocolFee * @property {Amount} poolFee * @property {Amount} newY @@ -49,6 +47,21 @@ * @returns {ImprovedNoFeeSwapResult} */ +/** + * @typedef {FeePair & ImprovedNoFeeSwapResult} FeeEstimate + */ + +/** + * @callback CalculateFees + * @param {Amount} amountGiven + * @param {PoolAllocation} poolAllocation + * @param {Amount} amountWanted + * @param {Ratio} protocolFeeRatio + * @param {Ratio} poolFeeRatio + * @param {NoFeeSwapFn} swapFn + * @returns {FeeEstimate} + */ + /** * @callback InternalSwap * @param {Amount} amountGiven @@ -59,3 +72,13 @@ * @param {NoFeeSwapFn} swapFn * @returns {SwapResult} */ + +/** + * @callback CalcSwapPrices + * @param {Amount} amountGiven + * @param {PoolAllocation} poolAllocation + * @param {Amount} amountWanted + * @param {Ratio} protocolFeeRatio + * @param {Ratio} poolFeeRatio + * @returns {SwapResult} + */ diff --git a/packages/zoe/src/contracts/constantProduct/invariants.js b/packages/zoe/src/contracts/constantProduct/invariants.js index b5cdfc5b52b..af85ff16065 100644 --- a/packages/zoe/src/contracts/constantProduct/invariants.js +++ b/packages/zoe/src/contracts/constantProduct/invariants.js @@ -3,11 +3,8 @@ import { assert, details as X } from '@agoric/assert'; import { AmountMath } from '@agoric/ertp'; -import { makeRatioFromAmounts } from '../../contractSupport/ratio.js'; import { natSafeMath } from '../../contractSupport/index.js'; -import { BASIS_POINTS } from './defaults.js'; - /** * xy <= (x + deltaX)(y - deltaY) * @@ -33,59 +30,14 @@ export const checkKInvariantSellingX = (x, y, deltaX, deltaY) => { * @param {Amount} deltaY - the amount of Brand Y to be taken out */ export const assertKInvariantSellingX = (x, y, deltaX, deltaY) => { - const oldK = natSafeMath.multiply(x.value, y.value); - const newX = AmountMath.add(x, deltaX); - const newY = AmountMath.subtract(y, deltaY); - const newK = natSafeMath.multiply(newX.value, newY.value); - assert( - oldK <= newK, - X`the constant product invariant was violated, with x=${x}, y=${y}, deltaX=${deltaX}, deltaY=${deltaY}, oldK=${oldK}, newK=${newK}`, - ); -}; - -/** - * Assert that the protocolFee amount is greater than the specified - * basisPoints, given protocolFee as a fraction of amountIn (includes protocolFee) - * - * @param {Amount} protocolFee - * @param {Amount} amountIn - * @param {bigint} protocolFeeBP - * @returns {void} - */ -export const assertProtocolFee = (protocolFee, amountIn, protocolFeeBP) => { - const protocolFeeRatio = makeRatioFromAmounts(protocolFee, amountIn); - - const approximationBP = - (Number(protocolFeeRatio.numerator.value) * Number(BASIS_POINTS)) / - Number(protocolFeeRatio.denominator.value); - assert( - approximationBP >= protocolFeeBP, - X`actualProtocolFeeBP was not greater: ${protocolFeeRatio}`, + checkKInvariantSellingX(x, y, deltaX, deltaY), + X`the constant product invariant was violated, with x=${x}, y=${y}, deltaX=${deltaX}, deltaY=${deltaY}, oldK=${natSafeMath.multiply( + x.value, + y.value, + )}, newK=${natSafeMath.multiply( + AmountMath.add(x, deltaX).value, + AmountMath.subtract(y, deltaY).value, + )}`, ); }; - -/** - * Assert that the poolFee amount is greater than the specified - * basisPoints, given poolFee as a fraction of amountOut + poolFee - * - * @param {Amount} poolFee - * @param {Amount} amountOut - * @param {bigint} poolFeeBP - * @returns {void} - */ -export const assertPoolFee = (poolFee, amountOut, poolFeeBP) => { - if (AmountMath.isEmpty(amountOut)) { - return; - } - const poolFeeRatio = makeRatioFromAmounts( - poolFee, - AmountMath.add(amountOut, poolFee), - ); - - const approximationBP = - (Number(poolFeeRatio.numerator.value) * Number(BASIS_POINTS)) / - Number(poolFeeRatio.denominator.value); - - assert(approximationBP >= poolFeeBP); -}; diff --git a/packages/zoe/src/contracts/constantProduct/swap.js b/packages/zoe/src/contracts/constantProduct/swap.js index d2969127d19..6399464133d 100644 --- a/packages/zoe/src/contracts/constantProduct/swap.js +++ b/packages/zoe/src/contracts/constantProduct/swap.js @@ -1,14 +1,16 @@ // @ts-check import { AmountMath } from '@agoric/ertp'; -import { calculateFees, amountGT, maximum } from './calcFees.js'; +import { calculateFees, amountGT } from './calcFees.js'; const { details: X } = assert; /** * The fee might not be in the same brand as the amount. If they are the same, - * subtract the fee from the amount (returning empty if the fee is larger). - * Otherwise return the unadjusted amount. + * subtract the fee from the amount. Otherwise return the unadjusted amount. + * + * We return empty if the fee is larger because an empty amount indicates that + * the trader didn't place a limit on the inputAmount. * * @param {Amount} amount * @param {Amount} fee @@ -88,14 +90,20 @@ const addOrSubtractFromPool = (addOrSub, poolAllocation, amount) => { } }; +const isGreaterThanZero = amount => { + return amount && amountGT(amount, AmountMath.makeEmptyFromAmount(amount)); +}; + const assertGreaterThanZero = (amount, name) => { assert( - amount && !AmountMath.isGTE(AmountMath.makeEmptyFromAmount(amount), amount), + amount && isGreaterThanZero(amount), X`${name} must be greater than 0: ${amount}`, ); }; const isWantedAvailable = (poolAllocation, amountWanted) => { + // The question is about a poolAllocation. If it has exactly amountWanted, + // that's not sufficient, since it would leave the pool empty. return amountWanted.brand === poolAllocation.Central.brand ? !AmountMath.isGTE(amountWanted, poolAllocation.Central) : !AmountMath.isGTE(amountWanted, poolAllocation.Secondary); @@ -112,7 +120,12 @@ const isWantedAvailable = (poolAllocation, amountWanted) => { * @param {PoolAllocation} poolAllocation * @param {Ratio} poolFee */ -function noTransaction(amountGiven, amountWanted, poolAllocation, poolFee) { +const makeNoTransaction = ( + amountGiven, + amountWanted, + poolAllocation, + poolFee, +) => { const emptyGive = AmountMath.makeEmptyFromAmount(amountGiven); const emptyWant = AmountMath.makeEmptyFromAmount(amountWanted); @@ -126,22 +139,40 @@ function noTransaction(amountGiven, amountWanted, poolAllocation, poolFee) { newY = poolAllocation.Central; } - const result = { + const result = harden({ protocolFee: AmountMath.makeEmpty(poolAllocation.Central.brand), poolFee: AmountMath.makeEmpty(poolFee.numerator.brand), swapperGives: emptyGive, swapperGets: emptyWant, - // swapperGiveRefund: AmountMath.subtract(amountGiven, swapperGives), xIncrement: emptyGive, yDecrement: emptyWant, newX, newY, - improvement: emptyGive, - }; + }); return result; -} +}; -/** @type {InternalSwap} */ +/** + * This is the heart of the calculation. See README.md for the long explanation. + * calculate how much should be added to and removed from the pool, the fees, + * and what the user will pay and receive. + * + * As soon as we detect that we won't be able to satisfy the request, we return + * noTransaction, indicating that no trade should take place. This can be due to + * a request for more assets than the pool holds, a specified price the current + * assets won't support, or the trade would requre more than the trader allowed, + * or provide less, or fees would eat up all the trader's proceeds. + * + * We start by calculating the amounts that would be traded if no fees were + * charged. The actual fees are based on these amounts. Once we know the actual + * fees, we calculate the deltaX and deltaY that will best maintain the + * constant product invariant. + * + * The amounts by which the pool will be adjusted, that the trader will pay and + * receive, and the fees are then computed based on deltaX and deltaY. + * + * @type {InternalSwap} + */ export const swap = ( amountGiven, poolAllocation, @@ -150,29 +181,21 @@ export const swap = ( poolFeeRatio, swapFn, ) => { + const noTransaction = makeNoTransaction( + amountGiven, + amountWanted, + poolAllocation, + poolFeeRatio, + ); assertGreaterThanZero(poolAllocation.Central, 'poolAllocation.Central'); assertGreaterThanZero(poolAllocation.Secondary, 'poolAllocation.Secondary'); assert( - (amountGiven && - !AmountMath.isGTE( - AmountMath.makeEmptyFromAmount(amountGiven), - amountGiven, - )) || - (amountWanted && - !AmountMath.isGTE( - AmountMath.makeEmptyFromAmount(amountWanted), - amountWanted, - )), + isGreaterThanZero(amountGiven) || isGreaterThanZero(amountWanted), X`amountGiven or amountWanted must be greater than 0: ${amountWanted} ${amountGiven}`, ); if (!isWantedAvailable(poolAllocation, amountWanted)) { - return noTransaction( - amountGiven, - amountWanted, - poolAllocation, - poolFeeRatio, - ); + return noTransaction; } // The protocol fee must always be collected in RUN, but the pool @@ -188,51 +211,38 @@ export const swap = ( ); if (!isWantedAvailable(poolAllocation, addFees(amountWanted, fees))) { - return noTransaction( - amountGiven, - amountWanted, - poolAllocation, - poolFeeRatio, - ); + return noTransaction; } // Calculate no-fee amounts. swapFn will only pay attention to the `specified` // value. The pool fee is always charged on the unspecified side, so it is an - // output of the calculation. When the specified value is in RUN, the protocol - // fee will be deducted from amountGiven before adding to the pool. When BLD - // was specified, we add the protocol fee to amountWanted. When the specified - // value is in RUN, the protocol fee will be deducted from amountGiven before - // adding to the pool or added to amountWanted to calculate amoutOut. - const { amountIn, amountOut, improvement } = swapFn({ + // output of the calculation. When BLD was specified, we add the protocol fee + // to amountWanted. When the specified value is in RUN, the protocol fee will + // be deducted from amountGiven before adding to the pool or added to + // amountWanted to calculate amoutOut. + const { amountIn, amountOut } = swapFn({ amountGiven: subtractFees(amountGiven, fees), poolAllocation, amountWanted: addFees(amountWanted, fees), }); if (AmountMath.isEmpty(amountOut)) { - return noTransaction( - amountGiven, - amountWanted, - poolAllocation, - poolFeeRatio, - ); + return noTransaction; } // The swapper pays extra or receives less to cover the fees. const swapperGives = addFees(amountIn, fees); const swapperGets = subtractFees(amountOut, fees); + // return noTransaction if fees would eat up all the trader's proceeds, + // the trader specified an amountGiven, and the trade would require mare, or + // the trade would require them to give more than they specified. if ( AmountMath.isEmpty(swapperGets) || (!AmountMath.isEmpty(amountGiven) && amountGT(swapperGives, amountGiven)) || amountGT(amountWanted, swapperGets) ) { - return noTransaction( - amountGiven, - amountWanted, - poolAllocation, - poolFeeRatio, - ); + return noTransaction; } const xIncrement = addRelevantFees(amountIn, fees.poolFee); @@ -246,7 +256,7 @@ export const swap = ( // reduced by it in order to compensate the pool. // newX and newY are the new pool balances, for comparison with start values. // improvement is an estimate of how much the gains or losses were improved. - const result = { + const result = harden({ protocolFee: fees.protocolFee, poolFee: fees.poolFee, swapperGives, @@ -259,8 +269,7 @@ export const swap = ( poolAllocation, yDecrement, ), - improvement: maximum(fees.improvement, improvement), - }; + }); return result; }; diff --git a/packages/zoe/src/contracts/constantProduct/swapIn.js b/packages/zoe/src/contracts/constantProduct/swapIn.js deleted file mode 100644 index 64c63227aae..00000000000 --- a/packages/zoe/src/contracts/constantProduct/swapIn.js +++ /dev/null @@ -1,30 +0,0 @@ -// @ts-check - -import { swap } from './swap.js'; -import { swapInNoFees } from './core.js'; -import { assertKInvariantSellingX } from './invariants.js'; -import { getXY } from './getXY.js'; - -export const swapIn = ( - amountGiven, - poolAllocation, - amountWanted, - protocolFeeRatio, - poolFeeRatio, -) => { - const result = swap( - amountGiven, - poolAllocation, - amountWanted, - protocolFeeRatio, - poolFeeRatio, - swapInNoFees, - ); - const { x, y } = getXY({ - amountGiven, - poolAllocation, - amountWanted, - }); - assertKInvariantSellingX(x, y, result.xIncrement, result.yDecrement); - return result; -}; diff --git a/packages/zoe/src/contracts/constantProduct/swapOut.js b/packages/zoe/src/contracts/constantProduct/swapOut.js deleted file mode 100644 index 99fd4a87720..00000000000 --- a/packages/zoe/src/contracts/constantProduct/swapOut.js +++ /dev/null @@ -1,30 +0,0 @@ -// @ts-check - -import { swap } from './swap.js'; -import { swapOutNoFees } from './core.js'; -import { getXY } from './getXY.js'; -import { assertKInvariantSellingX } from './invariants.js'; - -export const swapOut = ( - amountGiven, - poolAllocation, - amountWanted, - protocolFeeRatio, - poolFeeRatio, -) => { - const result = swap( - amountGiven, - poolAllocation, - amountWanted, - protocolFeeRatio, - poolFeeRatio, - swapOutNoFees, - ); - const { x, y } = getXY({ - amountGiven, - poolAllocation, - amountWanted, - }); - assertKInvariantSellingX(x, y, result.xIncrement, result.yDecrement); - return result; -}; diff --git a/packages/zoe/src/contracts/constantProduct/types.js b/packages/zoe/src/contracts/constantProduct/types.js new file mode 100644 index 00000000000..fc2bba0cb07 --- /dev/null +++ b/packages/zoe/src/contracts/constantProduct/types.js @@ -0,0 +1,15 @@ +// @ts-check + +/** + * @callback Maximum + * @param {Amount} left + * @param {Amount} right + * @returns {Amount} + */ + +/** + * @callback AmountGT + * @param {Amount} left + * @param {Amount} right + * @returns {boolean} + */ diff --git a/packages/zoe/test/unitTests/contracts/constantProduct/test-compareBondingCurves.js b/packages/zoe/test/unitTests/contracts/constantProduct/test-compareBondingCurves.js index 7ed3fbe4d66..6af2558fc5f 100644 --- a/packages/zoe/test/unitTests/contracts/constantProduct/test-compareBondingCurves.js +++ b/packages/zoe/test/unitTests/contracts/constantProduct/test-compareBondingCurves.js @@ -2,11 +2,12 @@ // eslint-disable-next-line import/no-extraneous-dependencies import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; import { BASIS_POINTS } from '../../../../src/contracts/constantProduct/defaults.js'; - -import { swapIn } from '../../../../src/contracts/constantProduct/swapIn.js'; -import { swapOut } from '../../../../src/contracts/constantProduct/swapOut.js'; import { setupMintKits } from './setupMints.js'; import { makeRatio } from '../../../../src/contractSupport/index.js'; +import { + calcSwapInPrices, + calcSwapOutPrices, +} from '../../../../src/contracts/constantProduct/calcSwapPrices.js'; // This assumes run is swapped in. The test should function the same // regardless of what brand is the amountIn, because no run fee is @@ -38,7 +39,7 @@ const prepareSwapInTest = ({ inputReserve, outputReserve, inputValue }) => { const testGetPrice = (t, inputs, expectedOutput) => { const { args, bld } = prepareSwapInTest(inputs); - const result = swapIn(...args); + const result = calcSwapInPrices(...args); t.deepEqual(result.swapperGets, bld(expectedOutput)); }; @@ -46,7 +47,7 @@ const getInputPriceThrows = (t, inputs, message) => { t.throws( _ => { const { args } = prepareSwapInTest(inputs); - return swapIn(...args); + return calcSwapInPrices(...args); }, { message, @@ -84,13 +85,13 @@ const prepareSwapOutTest = ({ inputReserve, outputReserve, outputValue }) => { const testGetOutputPrice = (t, inputs, expectedInput) => { const { args, run } = prepareSwapOutTest(inputs); - const result = swapOut(...args); + const result = calcSwapOutPrices(...args); t.deepEqual(result.swapperGives, run(expectedInput)); }; const getOutputPriceThrows = (t, inputs, message) => { const { args } = prepareSwapOutTest(inputs); - t.throws(_ => swapOut(...args), { + t.throws(_ => calcSwapOutPrices(...args), { message, }); }; diff --git a/packages/zoe/test/unitTests/contracts/constantProduct/test-compareNewSwapPrice.js b/packages/zoe/test/unitTests/contracts/constantProduct/test-compareNewSwapPrice.js index 24282c6d60c..3caeec67a73 100644 --- a/packages/zoe/test/unitTests/contracts/constantProduct/test-compareNewSwapPrice.js +++ b/packages/zoe/test/unitTests/contracts/constantProduct/test-compareNewSwapPrice.js @@ -3,14 +3,13 @@ // eslint-disable-next-line import/no-extraneous-dependencies import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; import { AmountMath, makeIssuerKit } from '@agoric/ertp'; - -import { swapIn } from '../../../../src/contracts/constantProduct/swapIn.js'; import { calcDeltaXSellingX, calcDeltaYSellingX, swapInNoFees, } from '../../../../src/contracts/constantProduct/core.js'; import { makeRatio } from '../../../../src/contractSupport/index.js'; +import { calcSwapInPrices } from '../../../../src/contracts/constantProduct/calcSwapPrices.js'; const BASIS_POINTS = 10000n; const POOL_FEE = 24n; @@ -72,7 +71,7 @@ test('newSwap getPriceGivenAvailableInput specify central', async t => { ); t.deepEqual(reduced, moola(9999n)); - const result = swapIn( + const result = calcSwapInPrices( amountGiven, poolAllocation, amountWanted, @@ -103,7 +102,7 @@ test('newSwap getPriceGivenAvailableInput secondary', async t => { ); const poolFeeRatio = makeRatio(POOL_FEE, moolaKit.brand, BASIS_POINTS); - const result = swapIn( + const result = calcSwapInPrices( amountGiven, poolAllocation, amountWanted, diff --git a/packages/zoe/test/unitTests/contracts/constantProduct/test-getXY.js b/packages/zoe/test/unitTests/contracts/constantProduct/test-getXY.js index 996813c08fe..33a630c46ed 100644 --- a/packages/zoe/test/unitTests/contracts/constantProduct/test-getXY.js +++ b/packages/zoe/test/unitTests/contracts/constantProduct/test-getXY.js @@ -6,7 +6,6 @@ import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; import { getXY } from '../../../../src/contracts/constantProduct/getXY.js'; import { setupMintKits } from './setupMints.js'; -// There's no difference between SwapIn and SwapOut for this function test('swap Central for Secondary', t => { const { run, bld } = setupMintKits(); @@ -28,6 +27,48 @@ test('swap Central for Secondary', t => { t.deepEqual(deltaY, amountWanted); }); +test('swap Central for Secondary no Give', t => { + const { run, bld } = setupMintKits(); + + const amountGiven = undefined; + const poolAllocation = { + Central: run(102902920n), + Secondary: bld(203838393n), + }; + const amountWanted = bld(2819n); + const { x, y, deltaX, deltaY } = getXY({ + amountGiven, + poolAllocation, + amountWanted, + }); + + t.deepEqual(x, poolAllocation.Central); + t.deepEqual(y, poolAllocation.Secondary); + t.deepEqual(deltaX, amountGiven); + t.deepEqual(deltaY, amountWanted); +}); + +test('swap Central for Secondary no want', t => { + const { run, bld } = setupMintKits(); + + const amountGiven = run(3000); + const poolAllocation = { + Central: run(102902920n), + Secondary: bld(203838393n), + }; + const amountWanted = undefined; + const { x, y, deltaX, deltaY } = getXY({ + amountGiven, + poolAllocation, + amountWanted, + }); + + t.deepEqual(x, poolAllocation.Central); + t.deepEqual(y, poolAllocation.Secondary); + t.deepEqual(deltaX, amountGiven); + t.deepEqual(deltaY, amountWanted); +}); + test('swap Secondary for Central', t => { const { run, bld } = setupMintKits(); @@ -48,3 +89,45 @@ test('swap Secondary for Central', t => { t.deepEqual(deltaX, amountGiven); t.deepEqual(deltaY, amountWanted); }); + +test('swap Secondary for Central no want', t => { + const { run, bld } = setupMintKits(); + + const amountGiven = bld(2000n); + const poolAllocation = { + Central: run(102902920n), + Secondary: bld(203838393n), + }; + const amountWanted = undefined; + const { x, y, deltaX, deltaY } = getXY({ + amountGiven, + poolAllocation, + amountWanted, + }); + + t.deepEqual(x, poolAllocation.Secondary); + t.deepEqual(y, poolAllocation.Central); + t.deepEqual(deltaX, amountGiven); + t.deepEqual(deltaY, amountWanted); +}); + +test('swap Secondary for Central no give', t => { + const { run, bld } = setupMintKits(); + + const amountGiven = undefined; + const poolAllocation = { + Central: run(102902920n), + Secondary: bld(203838393n), + }; + const amountWanted = run(9342193); + const { x, y, deltaX, deltaY } = getXY({ + amountGiven, + poolAllocation, + amountWanted, + }); + + t.deepEqual(x, poolAllocation.Secondary); + t.deepEqual(y, poolAllocation.Central); + t.deepEqual(deltaX, amountGiven); + t.deepEqual(deltaY, amountWanted); +}); diff --git a/packages/zoe/test/unitTests/contracts/constantProduct/test-swapScenarios.js b/packages/zoe/test/unitTests/contracts/constantProduct/test-swapScenarios.js index 0a9c1977e38..c64ed777649 100644 --- a/packages/zoe/test/unitTests/contracts/constantProduct/test-swapScenarios.js +++ b/packages/zoe/test/unitTests/contracts/constantProduct/test-swapScenarios.js @@ -6,10 +6,14 @@ import { DEFAULT_PROTOCOL_FEE, DEFAULT_POOL_FEE, } from '../../../../src/contracts/constantProduct/defaults.js'; - -import { swapIn } from '../../../../src/contracts/constantProduct/swapIn.js'; import { setupMintKits } from './setupMints.js'; -import { makeRatio } from '../../../../src/contractSupport/index.js'; +import { + makeRatio, + natSafeMath, +} from '../../../../src/contractSupport/index.js'; +import { calcSwapInPrices } from '../../../../src/contracts/constantProduct/calcSwapPrices.js'; + +const { multiply, ceilDivide } = natSafeMath; // This assumes run is swapped in. The test should function the same // regardless of what brand is the amountIn, because no run fee is @@ -47,7 +51,7 @@ const prepareSwapInTest = ({ const testGetPrice = (t, inputs, expectedOutput) => { const { args, run, bld } = prepareSwapInTest(inputs); - const result = swapIn(...args); + const result = calcSwapInPrices(...args); const expected = harden({ protocolFee: run(expectedOutput.protocolFee), poolFee: bld(expectedOutput.poolFee), @@ -57,7 +61,6 @@ const testGetPrice = (t, inputs, expectedOutput) => { yDecrement: bld(expectedOutput.yDecrement), newX: run(expectedOutput.newX), newY: bld(expectedOutput.newY), - improvement: run(expectedOutput.improvement), }); t.deepEqual(result, expected); }; @@ -76,9 +79,14 @@ test('getInputPrice newSwap bug scenario', t => { (input.inputPool + input.inputValue); const firstImprovedDeltaX = (input.inputPool * firstDeltaY) / (input.outputPool - firstDeltaY); - const poolFee = 1n + (DEFAULT_POOL_FEE * firstDeltaY) / BASIS_POINTS; - const protocolFee = - 1n + (DEFAULT_PROTOCOL_FEE * firstImprovedDeltaX) / BASIS_POINTS; + const poolFee = ceilDivide( + multiply(DEFAULT_POOL_FEE, firstDeltaY), + BASIS_POINTS, + ); + const protocolFee = ceilDivide( + multiply(DEFAULT_PROTOCOL_FEE, firstImprovedDeltaX), + BASIS_POINTS, + ); const secondDeltaY = (input.outputPool * (input.inputValue - protocolFee)) / @@ -98,12 +106,11 @@ test('getInputPrice newSwap bug scenario', t => { yDecrement, newX: input.inputPool + xIncrement, newY: input.outputPool - yDecrement, - improvement: 18n, }); testGetPrice(t, input, expectedOutput); }); -test.only('getInputPrice newSwap example', t => { +test('getInputPrice xy=k example', t => { const input = { inputPool: 40000n, outputPool: 3000n, @@ -132,7 +139,39 @@ test.only('getInputPrice newSwap example', t => { yDecrement, newX: input.inputPool + xIncrement, newY: input.outputPool - yDecrement, - improvement: 4n, + }); + testGetPrice(t, input, expectedOutput); +}); + +test('getInputPrice xy=k bigger numbers', t => { + const input = { + inputPool: 40000000n, + outputPool: 3000000n, + inputValue: 30000n, + outputValue: 2000n, + }; + + const poolFee = 6n; + const protocolFee = 18n; + + const secondDeltaY = + (input.outputPool * (input.inputValue - protocolFee)) / + (input.inputPool + (input.inputValue - protocolFee)); + const secondImprovedDeltaX = + (input.inputPool * secondDeltaY) / (input.outputPool - secondDeltaY) + 1n; + const yDecrement = secondDeltaY - poolFee; + const improvement = 12n; + const xIncrement = input.inputValue - protocolFee - improvement; + t.is(secondImprovedDeltaX, xIncrement); + const expectedOutput = harden({ + poolFee, + protocolFee, + swapperGives: input.inputValue - improvement, + swapperGets: yDecrement, + xIncrement, + yDecrement, + newX: input.inputPool + xIncrement, + newY: input.outputPool - yDecrement, }); testGetPrice(t, input, expectedOutput); }); From 8a04bfb505238a7df477f98a4acc041f458e13de Mon Sep 17 00:00:00 2001 From: Chris Hibbert Date: Wed, 29 Sep 2021 13:33:38 -0700 Subject: [PATCH 6/8] docs: improve types and documentation Don't use q() to publish prices improve internal docs and types in core.js --- .../zoe/src/contracts/constantProduct/core.js | 87 ++++++++++++------- .../src/contracts/constantProduct/getXY.js | 13 +-- .../constantProduct/internal-types.js | 33 +++++++ .../constantProduct/test-calcDeltaY.js | 4 +- 4 files changed, 99 insertions(+), 38 deletions(-) diff --git a/packages/zoe/src/contracts/constantProduct/core.js b/packages/zoe/src/contracts/constantProduct/core.js index d6cfcd4c318..1b43f88d04e 100644 --- a/packages/zoe/src/contracts/constantProduct/core.js +++ b/packages/zoe/src/contracts/constantProduct/core.js @@ -6,18 +6,20 @@ import { natSafeMath } from '../../contractSupport/index.js'; import { makeRatioFromAmounts } from '../../contractSupport/ratio.js'; import { getXY } from './getXY.js'; -const { details: X, quote: q } = assert; +const { details: X } = assert; const assertSingleBrand = ratio => { assert( ratio.numerator.brand === ratio.denominator.brand, - X`Ratio was expected to have same brand in numerator and denominator ${q( - ratio, - )}`, + X`Ratio was expected to have same brand in numerator ${ratio.numerator.brand} and denominator ${ratio.denominator.brand}`, ); }; /** + * Multiply an amount by a ratio using floorDivide, ignoring the ratio's brands + * in favor of the amount brand. This is necessary because the ratio is produced + * by dividing assets of the opposite brand. + * * @param {Amount} amount * @param {Ratio} ratio * @returns {Amount} @@ -32,6 +34,10 @@ const floorMultiplyKeepBrand = (amount, ratio) => { }; /** + * Multiply an amount and a ratio using ceilDivide, ignoring the ratio's brands + * in favor of the amount brand. This is necessary because the ratio is produced + * by dividing assets of the opposite brand. + * * @param {Amount} amount * @param {Ratio} ratio * @returns {Amount} @@ -46,17 +52,19 @@ const ceilMultiplyKeepBrand = (amount, ratio) => { }; /** - * Calculate the change to the shrinking pool when the user specifies how much - * they're willing to add. Also used to improve a proposed trade when the amount - * contributed would buy more than the user asked for. + * Calculate deltaY when the user is selling brand X. That is, whichever asset + * the user is selling, this function is used to calculate the change in the + * other asset, i.e. how much of brand Y to give the user in return. + * swapOutImproved calls this function with the calculated amountIn to find out + * if more than the wantedAmountOut can be gained for the necessary amountIn. * * deltaY = (deltaXOverX/(1 + deltaXOverX))*y * Equivalently: (deltaX / (deltaX + x)) * y * - * @param {Amount} x - the amount of the growing brand in the pool - * @param {Amount} y - the amount of the shrinking brand in the pool - * @param {Amount} deltaX - the amount of the growing brand to be added - * @returns {Amount} deltaY - the amount of the shrinking brand to be taken out + * @param {Amount} x - the amount of Brand X in pool + * @param {Amount} y - the amount of Brand Y the pool + * @param {Amount} deltaX - the amount of Brand X to be added + * @returns {Amount} deltaY - the amount of Brand Y to be taken out */ export const calcDeltaYSellingX = (x, y, deltaX) => { const deltaXPlusX = AmountMath.add(deltaX, x); @@ -67,17 +75,18 @@ export const calcDeltaYSellingX = (x, y, deltaX) => { }; /** - * Calculate the change to the growing pool when the user specifies how much - * they want to receive. Also used to improve a proposed trade when the amount - * requested can be purchased for a smaller input. + * Calculate deltaX when the user is selling brand X. That is, whichever asset + * the user is selling, this function is used to calculate the change to the + * pool for that asset. swapInReduced calls this with the calculated amountOut + * to find out if less than the offeredAmountIn would be sufficient. * * deltaX = (deltaYOverY/(1 - deltaYOverY))*x * Equivalently: (deltaY / (Y - deltaY )) * x * - * @param {Amount} x - the amount of the growing brand in the pool - * @param {Amount} y - the amount of the shrinking brand in the pool - * @param {Amount} deltaY - the amount of the shrinking brand to take out - * @returns {Amount} deltaX - the amount of the growingn brand to add + * @param {Amount} x - the amount of Brand X in the pool + * @param {Amount} y - the amount of Brand Y in the pool + * @param {Amount} deltaY - the amount of Brand Y to be taken out + * @returns {Amount} deltaX - the amount of Brand X to be added */ export const calcDeltaXSellingX = (x, y, deltaY) => { const yMinusDeltaY = AmountMath.subtract(y, deltaY); @@ -87,11 +96,22 @@ export const calcDeltaXSellingX = (x, y, deltaY) => { return ceilMultiplyKeepBrand(x, yRatio); }; -const swapInReduced = ({ x: inPool, y: outPool, deltaX: offeredAmountIn }) => { - const amountOut = calcDeltaYSellingX(inPool, outPool, offeredAmountIn); - const reducedAmountIn = calcDeltaXSellingX(inPool, outPool, amountOut); +/** + * The input contains the amounts in the pool and a maximum amount offered. + * Calculate the most beneficial trade that satisfies the constant product + * invariant. + * + * @param {GetXYResultDeltaX} obj + * @returns {ImprovedNoFeeSwapResult} + */ +const swapInReduced = ({ x, y, deltaX: offeredAmountIn }) => { + const amountOut = calcDeltaYSellingX(x, y, offeredAmountIn); + const reducedAmountIn = calcDeltaXSellingX(x, y, amountOut); - assert(AmountMath.isGTE(offeredAmountIn, reducedAmountIn)); + assert( + AmountMath.isGTE(offeredAmountIn, reducedAmountIn), + X`The trade would have required ${reducedAmountIn} more than was offered ${offeredAmountIn}`, + ); return harden({ amountIn: reducedAmountIn, @@ -99,15 +119,22 @@ const swapInReduced = ({ x: inPool, y: outPool, deltaX: offeredAmountIn }) => { }); }; -const swapOutImproved = ({ - x: inPool, - y: outPool, - deltaY: wantedAmountOut, -}) => { - const amountIn = calcDeltaXSellingX(inPool, outPool, wantedAmountOut); - const improvedAmountOut = calcDeltaYSellingX(inPool, outPool, amountIn); +/** + * The input contains the amounts in the pool and the minimum amount requested. + * Calculate the most beneficial trade that satisfies the constant product + * invariant. + * + * @param {GetXYResultDeltaY} obj + * @returns {ImprovedNoFeeSwapResult} + */ +const swapOutImproved = ({ x, y, deltaY: wantedAmountOut }) => { + const amountIn = calcDeltaXSellingX(x, y, wantedAmountOut); + const improvedAmountOut = calcDeltaYSellingX(x, y, amountIn); - assert(AmountMath.isGTE(improvedAmountOut, wantedAmountOut)); + assert( + AmountMath.isGTE(improvedAmountOut, wantedAmountOut), + X`The trade would have returned ${improvedAmountOut} less than was wanted ${wantedAmountOut}`, + ); return harden({ amountIn, diff --git a/packages/zoe/src/contracts/constantProduct/getXY.js b/packages/zoe/src/contracts/constantProduct/getXY.js index 0cd3084b450..3f13827a1fb 100644 --- a/packages/zoe/src/contracts/constantProduct/getXY.js +++ b/packages/zoe/src/contracts/constantProduct/getXY.js @@ -3,6 +3,7 @@ // This does not support secondary to secondary. That has to happen at // a higher abstraction +const { details: X } = assert; /** * The caller provides poolAllocation, which has balances for both Central and * Secondary, and at least one of amountGiven and amountWanted. getXY treats @@ -11,11 +12,7 @@ * balances as X and Y and given and wanted as deltaX and deltaY * { X, Y, deltaX, deltaY }. * - * @param {Object} opt - * @param {Amount=} opt.amountGiven - * @param {{ Central: Amount, Secondary: Amount }} opt.poolAllocation - * @param {Amount=} opt.amountWanted - * @returns {{ x: Amount, y: Amount, deltaX: Amount | undefined, deltaY: Amount | undefined }} + * @type {GetXY} */ export const getXY = ({ amountGiven, poolAllocation, amountWanted }) => { // Regardless of whether we are specifying the amountIn or the @@ -24,6 +21,10 @@ export const getXY = ({ amountGiven, poolAllocation, amountWanted }) => { const yBrand = amountWanted && amountWanted.brand; const secondaryBrand = poolAllocation.Secondary.brand; const centralBrand = poolAllocation.Central.brand; + assert( + amountGiven || amountWanted, + X`At least one of ${amountGiven} and ${amountWanted} must be specified`, + ); const deltas = { deltaX: amountGiven, @@ -31,6 +32,7 @@ export const getXY = ({ amountGiven, poolAllocation, amountWanted }) => { }; if (secondaryBrand === xBrand || centralBrand === yBrand) { + // @ts-ignore at least one of amountGiven and amountWanted is non-null return harden({ x: poolAllocation.Secondary, y: poolAllocation.Central, @@ -38,6 +40,7 @@ export const getXY = ({ amountGiven, poolAllocation, amountWanted }) => { }); } if (centralBrand === xBrand || secondaryBrand === yBrand) { + // @ts-ignore at least one of amountGiven and amountWanted is non-null return harden({ x: poolAllocation.Central, y: poolAllocation.Secondary, diff --git a/packages/zoe/src/contracts/constantProduct/internal-types.js b/packages/zoe/src/contracts/constantProduct/internal-types.js index 8d0cde03fc1..0912ec9a017 100644 --- a/packages/zoe/src/contracts/constantProduct/internal-types.js +++ b/packages/zoe/src/contracts/constantProduct/internal-types.js @@ -82,3 +82,36 @@ * @param {Ratio} poolFeeRatio * @returns {SwapResult} */ + +/** + * @typedef {Object} GetXYParam + * @property {Amount=} amountGiven + * @property {PoolAllocation} poolAllocation + * @property {Amount=} amountWanted + */ + +/** + * @typedef {Object} GetXYResultDeltaX + * @property {Amount} x + * @property {Amount} y + * @property {Amount} deltaX + * @property {Amount|undefined} deltaY + */ + +/** + * @typedef {Object} GetXYResultDeltaY + * @property {Amount} x + * @property {Amount} y + * @property {Amount} deltaY + * @property {Amount|undefined} deltaX + */ + +/** + * @typedef {GetXYResultDeltaX & GetXYResultDeltaY} GetXYResult + */ + +/** + * @callback GetXY + * @param {GetXYParam} obj + * @returns {GetXYResult} + */ diff --git a/packages/zoe/test/unitTests/contracts/constantProduct/test-calcDeltaY.js b/packages/zoe/test/unitTests/contracts/constantProduct/test-calcDeltaY.js index 65d595ee2f0..12e0cfb501b 100644 --- a/packages/zoe/test/unitTests/contracts/constantProduct/test-calcDeltaY.js +++ b/packages/zoe/test/unitTests/contracts/constantProduct/test-calcDeltaY.js @@ -7,8 +7,6 @@ import { AmountMath } from '@agoric/ertp'; import { calcDeltaYSellingX } from '../../../../src/contracts/constantProduct/core.js'; import { setupMintKits } from './setupMints.js'; -// the brands of x and y shouldn't matter (test this explicitly in a -// separate test) const doTest = (t, x, y, deltaX, expectedDeltaY) => { const { run, bld } = setupMintKits(); const result = calcDeltaYSellingX(run(x), bld(y), run(deltaX)); @@ -72,6 +70,6 @@ test('1000000, 5000, 209, 1', t => { doTest(t, 1000000, 5000, 209, 1); }); -test('5000, 1000000, 209, 1', t => { +test('5000, 1000000, 209, 40122', t => { doTest(t, 5000, 1000000, 209, 40122); }); From ec3fdc419cefcdd448f22236215478eb669f68eb Mon Sep 17 00:00:00 2001 From: Chris Hibbert Date: Wed, 29 Sep 2021 17:44:00 -0700 Subject: [PATCH 7/8] docs: clarify and correct the README; add tests --- .../src/contracts/constantProduct/README.md | 39 +- .../constantProduct/test-calcDeltaX.js | 82 ++++ .../constantProduct/test-checkInvariants.js | 406 ++++++++++++++++++ 3 files changed, 509 insertions(+), 18 deletions(-) create mode 100644 packages/zoe/test/unitTests/contracts/constantProduct/test-calcDeltaX.js create mode 100644 packages/zoe/test/unitTests/contracts/constantProduct/test-checkInvariants.js diff --git a/packages/zoe/src/contracts/constantProduct/README.md b/packages/zoe/src/contracts/constantProduct/README.md index a14c31f2f60..b30550e6545 100644 --- a/packages/zoe/src/contracts/constantProduct/README.md +++ b/packages/zoe/src/contracts/constantProduct/README.md @@ -39,7 +39,9 @@ extracted from the pools to adhere to those rules. In these tables BLD represents any collateral. The user can specify how much they want or how much they're willing to pay. We'll call the value they -specified **sGive** or **sGet** and bold it. This table shows which brands the +specified **sGive** or **sGet** and bold it. We'll always refer to the currency +being added as X (regardless of whether it's what they pay or what they receive) +and the currency the user gets as Y. This table shows which brands the amounts each have, as well as what is computed vs. given. The PoolFee is computed based on the calculated amount (BLD in rows 1 and 2; RUN in rows 3 and 4). The Protocol fee is always in RUN. @@ -52,9 +54,9 @@ computed based on the calculated amount (BLD in rows 1 and 2; RUN in rows 3 and | **BLD out** | RUN | BLD | RUN | RUN | **sGet** | sGive | We'll estimate how much the pool balances would change in the no-fee, improved -price case using the constant product formulas. These estimates are δX, -and δY. The fees are based on δX, and δY. ρ is the poolFee -(e.g. .003). +price case using the constant product formulas. We call these estimates +δX, and δY. The fees are based on δX, and δY. ρ is +the poolFee (e.g. .003). The pool fee will be ρ times whichever of δX and δY was calculated. The protocol fee will be ρ * δX when RUN is paid in, and @@ -63,20 +65,14 @@ calculated. The protocol fee will be ρ * δX when RUN is paid in, and | | δX | δY | PoolFee | Protocol Fee | |---------|-----|-----|--------|-----| | **RUN in** | **sGive** | calc | ρ × δY | ρ × **sGive** (= ρ × δX) | -| **RUN out** | calc | **sGet** | ρ × δX | ρ × **sGet** (= ρ × δY) | -| **BLD in** | **sGive** | calc | ρ × δY | ρ × δY | +| **RUN out** | calc | **sGet** | ρ × δY | ρ × **sGet** (= ρ × δY) | +| **BLD in** | **sGive** | calc | ρ × δX | ρ × δY | | **BLD out** | calc | **sGet** | ρ × δX | ρ × δX | In rows 1 and 3, **sGive** was specified and sGet will be calculated. In rows 2 and 4, **sGet** was specified and sGive will be calculated. Once we know the fees, we can add or subtract the fees and calculate the pool changes. -ΔX is the incrementing side of the constant product calculation, and -ΔY is the decrementing side. If **sGive** is known, we subtract fees to -get ΔX and calculate ΔY. If **sGet** is known, we add fees to -get ΔY and calculate ΔX. ΔY and ΔX are the values -that maintain the constant product invariant. - Notice that the ProtocolFee always affects the inputs to the constant product calculation (because it is collected outside the pool). The PoolFee is visible in the formulas in this table when the input to the calculation is in RUN. @@ -88,16 +84,23 @@ in the formulas in this table when the input to the calculation is in RUN. | **BLD in** | **sGive** - ProtocolFee - PoolFee | | | **BLD out** | | **sGet** + ProtocolFee | -We use the estimate of the amount in or out to calculate the improved ΔX -and ΔY, which tells us how much the trader will pay, the changes in pool -balances, and what the trader will receive. +We use the estimate of the amount in or out to calculate improved values of +ΔX and ΔY. These values tell us how much the trader will pay, the +changes in pool balances, and what the trader will receive. As before, ΔX +reflects a balance that will be growing, and ΔY one that will be +shrinking. If **sGive** is known, we subtract fees to get ΔX and calculate +ΔY. If **sGet** is known, we add fees to get ΔY and calculate +ΔX. ΔY and ΔX are the values that maintain the constant +product invariant. The amount paid and received by the trader and changes to the +pool are calculated relative to ΔX and ΔY so that the pool grows by +the poolFee and the protocolFee can be paid from the proceeds. | | xIncr | yDecr | pay In | pay Out | |---------|-----|-----|-----|-----| | **RUN in** | ΔX | ΔY - PoolFee | ΔX + protocolFee | ΔY - PoolFee | -| **RUN out** | ΔX | ΔY - PoolFee | ΔX | ΔY - PoolFee - ProtocolFee | -| **BLD in** | ΔX + PoolFee | ΔY | ΔX + PoolFee | ΔY - ProtocolFee | -| **BLD out** | ΔX + PoolFee | ΔY | ΔX + PoolFee + ProtocolFee | ΔY | +| **RUN out** | ΔX | ΔY - PoolFee | ΔX + protocolFee | ΔY - PoolFee | +| **BLD in** | ΔX + PoolFee | ΔY | ΔX + PoolFee + ProtocolFee | ΔY | +| **BLD out** | ΔX + PoolFee | ΔY | ΔX + PoolFee + ProtocolFee | ΔY | In the two right columns the protocolFee is either added to the amount the trader pays, or subtracted from the proceeds. The poolFee does the same on the diff --git a/packages/zoe/test/unitTests/contracts/constantProduct/test-calcDeltaX.js b/packages/zoe/test/unitTests/contracts/constantProduct/test-calcDeltaX.js new file mode 100644 index 00000000000..9e3f2b0db18 --- /dev/null +++ b/packages/zoe/test/unitTests/contracts/constantProduct/test-calcDeltaX.js @@ -0,0 +1,82 @@ +// @ts-check + +// eslint-disable-next-line import/no-extraneous-dependencies +import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; +import { AmountMath } from '@agoric/ertp'; + +import { calcDeltaXSellingX } from '../../../../src/contracts/constantProduct/core.js'; +import { setupMintKits } from './setupMints.js'; + +const doTest = (t, x, y, deltaY, expectedDeltaX) => { + const { run, bld } = setupMintKits(); + const result = calcDeltaXSellingX(run(x), bld(y), bld(deltaY)); + t.true( + AmountMath.isEqual(result, run(expectedDeltaX)), + `${result.value} should equal ${expectedDeltaX}`, + ); +}; + +test('0, 0, 0, 0', t => { + t.throws(() => doTest(t, 0, 0, 0, 0), { + message: 'No infinite ratios! Denominator was 0/"[Alleged: BLD brand]"', + }); +}); + +test('0, 0, 1, 0', t => { + t.throws(() => doTest(t, 0, 0, 1, 0), { + message: '-1 is negative', + }); +}); + +test('1, 0, 0, 0', t => { + t.throws(() => doTest(t, 1, 0, 0, 0), { + message: 'No infinite ratios! Denominator was 0/"[Alleged: BLD brand]"', + }); +}); + +test('0, 1, 0, 0', t => { + doTest(t, 0, 1, 0, 0); +}); + +test('1, 1, 0, 0', t => { + doTest(t, 1, 1, 0, 0); +}); + +test('1, 1, 1, 0', t => { + t.throws(() => doTest(t, 1, 1, 1, 0), { + message: 'No infinite ratios! Denominator was 0/"[Alleged: BLD brand]"', + }); +}); + +test('1, 2, 1, 1', t => { + doTest(t, 1, 2, 1, 1); +}); + +test('2, 3, 1, 1', t => { + doTest(t, 2, 3, 1, 1); +}); + +test('928861206, 130870247, 746353662, 158115257', t => { + doTest(t, 928_861_206n, 5_130_870_247n, 746_353_662n, 1_581_152_57n); +}); + +test('9, 17, 3, 2', t => { + doTest(t, 9, 17, 3, 2); +}); + +test('10000, 5000, 209, 437', t => { + doTest(t, 10000, 5000, 209, 437); +}); + +test('1000000, 5000, 209, 1', t => { + doTest(t, 1_000_000, 5000, 209, 43624); +}); + +test('5000, 1000000, 209, 2', t => { + doTest(t, 5000, 1000000, 209, 2); +}); + +test('500_000, 1000_000, 209 or 210', t => { + doTest(t, 500_000, 1000_000, 209, 105); + doTest(t, 500_000, 1000_000, 210, 106); +}); diff --git a/packages/zoe/test/unitTests/contracts/constantProduct/test-checkInvariants.js b/packages/zoe/test/unitTests/contracts/constantProduct/test-checkInvariants.js new file mode 100644 index 00000000000..e5c51072d96 --- /dev/null +++ b/packages/zoe/test/unitTests/contracts/constantProduct/test-checkInvariants.js @@ -0,0 +1,406 @@ +// @ts-check +// eslint-disable-next-line import/no-extraneous-dependencies +import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; +import { AmountMath } from '@agoric/ertp'; +import { BASIS_POINTS } from '../../../../src/contracts/constantProduct/defaults.js'; +import { setupMintKits } from './setupMints.js'; +import { makeRatio } from '../../../../src/contractSupport/index.js'; +import { + calcSwapInPrices, + calcSwapOutPrices, +} from '../../../../src/contracts/constantProduct/calcSwapPrices.js'; +import { checkKInvariantSellingX } from '../../../../src/contracts/constantProduct/invariants.js'; +import { getXY } from '../../../../src/contracts/constantProduct/getXY.js'; + +const prepareRUNInTest = ({ + inputReserve, + outputReserve, + inputValue, + outputValue, +}) => { + const { run, bld, runKit, bldKit } = setupMintKits(); + const amountGiven = run(inputValue || 0n); + const poolAllocation = harden({ + Central: run(inputReserve), + Secondary: bld(outputReserve), + }); + const amountWanted = bld(outputValue || 0n); + const protocolFeeRatio = makeRatio(5n, runKit.brand, BASIS_POINTS); + const poolFeeRatio = makeRatio(25n, bldKit.brand, BASIS_POINTS); + + return harden([ + amountGiven, + poolAllocation, + amountWanted, + protocolFeeRatio, + poolFeeRatio, + ]); +}; + +const prepareRUNOutTest = ({ + inputReserve, + outputReserve, + inputValue, + outputValue, +}) => { + const { run, bld, runKit, bldKit } = setupMintKits(); + const amountGiven = bld(inputValue || 0n); + const poolAllocation = harden({ + Central: run(inputReserve), + Secondary: bld(outputReserve), + }); + const amountWanted = run(outputValue || 0n); + const protocolFeeRatio = makeRatio(5n, bldKit.brand, BASIS_POINTS); + const poolFeeRatio = makeRatio(25n, runKit.brand, BASIS_POINTS); + + return harden([ + amountGiven, + poolAllocation, + amountWanted, + protocolFeeRatio, + poolFeeRatio, + ]); +}; + +function checkGetInput(t, args, result) { + t.falsy(AmountMath.isEmpty(result.swapperGets)); + t.truthy(AmountMath.isGTE(args[0], result.swapperGives)); + t.truthy(AmountMath.isGTE(result.swapperGets, args[2])); + + t.deepEqual( + AmountMath.add(result.xIncrement, result.protocolFee), + result.swapperGives, + ); + t.deepEqual(result.yDecrement, result.swapperGets); + + const xyArgs = { + amountGiven: args[0], + poolAllocation: args[1], + amountWanted: args[2], + }; + const { x, y } = getXY(xyArgs); + t.truthy(checkKInvariantSellingX(x, y, result.xIncrement, result.yDecrement)); +} + +function checkGetOutput(t, args, result) { + t.falsy(AmountMath.isEmpty(result.swapperGets)); + if (!AmountMath.isEmpty(args[0])) { + t.truthy(AmountMath.isGTE(args[0], result.swapperGives)); + } + t.truthy(AmountMath.isGTE(result.swapperGets, args[2])); + + t.deepEqual( + AmountMath.add(result.xIncrement, result.protocolFee), + result.swapperGives, + ); + t.deepEqual(result.yDecrement, result.swapperGets); + + const xyArgs = { + amountGiven: args[0], + poolAllocation: args[1], + amountWanted: args[2], + }; + const { x, y } = getXY(xyArgs); + t.truthy(checkKInvariantSellingX(x, y, result.xIncrement, result.yDecrement)); +} + +const testGetInputPrice = (t, inputs, runIn) => { + const args = runIn ? prepareRUNInTest(inputs) : prepareRUNOutTest(inputs); + const result = calcSwapInPrices(...args); + checkGetInput(t, args, result); +}; + +const testGetInputPriceThrows = (t, inputs, message, runIn) => { + t.throws( + _ => { + const args = runIn ? prepareRUNInTest(inputs) : prepareRUNOutTest(inputs); + return calcSwapInPrices(...args); + }, + { + message, + }, + ); +}; + +const testGetInputPriceNoTrade = (t, inputs, runIn) => { + const args = runIn ? prepareRUNInTest(inputs) : prepareRUNOutTest(inputs); + const result = calcSwapInPrices(...args); + t.truthy(AmountMath.isEmpty(result.swapperGets)); + t.truthy(AmountMath.isEmpty(result.swapperGives)); +}; + +const testGetOutputPrice = (t, inputs, runIn) => { + const args = runIn ? prepareRUNInTest(inputs) : prepareRUNOutTest(inputs); + const result = calcSwapOutPrices(...args); + checkGetOutput(t, args, result); +}; + +const getOutputPriceThrows = (t, inputs, message, runIn) => { + t.throws( + _ => { + const args = runIn ? prepareRUNInTest(inputs) : prepareRUNOutTest(inputs); + return calcSwapOutPrices(...args); + }, + { + message, + }, + ); +}; + +const testGetOutputPriceNoTrade = (t, inputs, runIn) => { + const args = runIn ? prepareRUNInTest(inputs) : prepareRUNOutTest(inputs); + const result = calcSwapOutPrices(...args); + t.truthy(AmountMath.isEmpty(result.swapperGets)); + t.truthy(AmountMath.isEmpty(result.swapperGives)); +}; + +test('getInputPrice no reserves', t => { + const input = { + inputReserve: 0n, + outputReserve: 0n, + inputValue: 1n, + }; + const message = + '"poolAllocation.Central" must be greater than 0: {"brand":"[Alleged: RUN brand]","value":"[0n]"}'; + testGetInputPriceThrows(t, input, message, true); + testGetInputPriceThrows(t, input, message, false); +}); + +test('getInputPrice ok 2', t => { + const input = { + inputReserve: 5984n, + outputReserve: 3028n, + inputValue: 1398n, + }; + testGetInputPrice(t, input, true); + testGetInputPrice(t, input, false); +}); + +test('getInputPrice ok 2w/output', t => { + const input = { + inputReserve: 5984n, + outputReserve: 3028n, + inputValue: 1398n, + outputValue: 572n, + }; + testGetInputPriceNoTrade(t, input, true); + testGetInputPrice(t, input, false); +}); + +test('getInputPrice outLimit', t => { + const input = { + inputReserve: 9348n, + outputReserve: 2983n, + inputValue: 828n, + outputValue: 350n, + }; + testGetInputPriceNoTrade(t, input, true); + testGetInputPrice(t, input, false); +}); + +test('getInputPrice ok 3', t => { + const input = { + inputReserve: 8160n, + outputReserve: 7743n, + inputValue: 6635n, + }; + testGetInputPrice(t, input, true); + testGetInputPrice(t, input, false); +}); + +test('getInputPrice ok 4', t => { + const input = { + inputReserve: 10n, + outputReserve: 10n, + inputValue: 1000n, + }; + testGetInputPrice(t, input, true); + testGetInputPrice(t, input, false); +}); + +test('getInputPrice ok 5', t => { + const input = { + inputReserve: 100n, + outputReserve: 50n, + inputValue: 17n, + }; + testGetInputPrice(t, input, true); + testGetInputPrice(t, input, false); +}); + +test('getInputPrice zero outputValue', t => { + const input = { + inputReserve: 100n, + outputReserve: 50n, + inputValue: 17n, + outputValue: 0n, + }; + testGetInputPrice(t, input, true); + testGetInputPrice(t, input, false); +}); + +test('getInputPrice ok 6', t => { + const input = { + inputReserve: 43n, + outputReserve: 117n, + inputValue: 7n, + }; + testGetInputPrice(t, input, true); + testGetInputPrice(t, input, false); +}); + +test('getInputPrice negative', t => { + const input = { + inputReserve: 43n, + outputReserve: 117n, + inputValue: -7n, + }; + const message = 'value "[-7n]" must be a Nat or an array'; + testGetInputPriceThrows(t, input, message, true); + testGetInputPriceThrows(t, input, message, false); +}); + +test('getInputPrice bad reserve 1', t => { + const input = { + inputReserve: 43n, + outputReserve: 0n, + inputValue: 347n, + }; + const message = + '"poolAllocation.Secondary" must be greater than 0: {"brand":"[Alleged: BLD brand]","value":"[0n]"}'; + testGetInputPriceThrows(t, input, message, true); + testGetInputPriceThrows(t, input, message, false); +}); + +test('getInputPrice bad reserve 2', t => { + const input = { + inputReserve: 0n, + outputReserve: 50n, + inputValue: 828n, + }; + const message = + '"poolAllocation.Central" must be greater than 0: {"brand":"[Alleged: RUN brand]","value":"[0n]"}'; + testGetInputPriceThrows(t, input, message, true); + testGetInputPriceThrows(t, input, message, false); +}); + +test('getInputPrice zero input', t => { + const input = { + inputReserve: 320n, + outputReserve: 50n, + inputValue: 0n, + }; + const messageA = + 'amountGiven or amountWanted must be greater than 0: {"brand":"[Alleged: BLD brand]","value":"[0n]"} {"brand":"[Alleged: RUN brand]","value":"[0n]"}'; + testGetInputPriceThrows(t, input, messageA, true); + const messageB = + 'amountGiven or amountWanted must be greater than 0: {"brand":"[Alleged: RUN brand]","value":"[0n]"} {"brand":"[Alleged: BLD brand]","value":"[0n]"}'; + testGetInputPriceThrows(t, input, messageB, false); +}); + +test('getInputPrice big product', t => { + const input = { + inputReserve: 100_000_000n, + outputReserve: 100_000_000n, + inputValue: 1000n, + outputValue: 50n, + }; + testGetInputPrice(t, input, true); + testGetInputPrice(t, input, false); +}); + +test('getOutputPrice ok', t => { + const input = { + inputReserve: 43n, + outputReserve: 117n, + outputValue: 37n, + }; + testGetOutputPrice(t, input, true); + testGetOutputPrice(t, input, false); +}); + +test('getOutputPrice zero output reserve', t => { + const input = { + inputReserve: 43n, + outputReserve: 0n, + outputValue: 37n, + }; + const message = + '"poolAllocation.Secondary" must be greater than 0: {"brand":"[Alleged: BLD brand]","value":"[0n]"}'; + getOutputPriceThrows(t, input, message, true); + getOutputPriceThrows(t, input, message, false); +}); + +test('getOutputPrice zero input reserve', t => { + const input = { + inputReserve: 0n, + outputReserve: 92n, + outputValue: 37n, + }; + const message = + '"poolAllocation.Central" must be greater than 0: {"brand":"[Alleged: RUN brand]","value":"[0n]"}'; + getOutputPriceThrows(t, input, message, true); + getOutputPriceThrows(t, input, message, false); +}); + +test('getOutputPrice too much output', t => { + const input = { + inputReserve: 1132n, + outputReserve: 1024n, + outputValue: 20923n, + }; + testGetOutputPriceNoTrade(t, input, true); + testGetOutputPriceNoTrade(t, input, false); +}); + +test('getOutputPrice too much output 2', t => { + const input = { + inputReserve: 1132n, + outputReserve: 345n, + outputValue: 345n, + }; + testGetOutputPriceNoTrade(t, input, true); + testGetOutputPrice(t, input, false); +}); + +test('getOutputPrice zero inputValue', t => { + const input = { + inputReserve: 1132n, + outputReserve: 3145n, + inputValue: 0n, + outputValue: 345n, + }; + testGetOutputPrice(t, input, true); + testGetOutputPrice(t, input, false); +}); + +test('getOutputPrice big product', t => { + const input = { + inputReserve: 100_000_000n, + outputReserve: 100_000_000n, + outputValue: 1000n, + }; + testGetOutputPrice(t, input, true); + testGetOutputPrice(t, input, false); +}); + +test('getOutputPrice minimum price', t => { + const input = { + inputReserve: 1n, + outputReserve: 10n, + outputValue: 1n, + }; + testGetOutputPrice(t, input, true); + testGetOutputPriceNoTrade(t, input, false); +}); + +test('getOutputPrice large values, in/out', t => { + const input = { + inputReserve: 1_192_432n, + outputReserve: 3_298_045n, + inputValue: 13_435n, + outputValue: 3_435n, + }; + testGetOutputPrice(t, input, true); + testGetOutputPrice(t, input, false); +}); From cf3f73a9bd5f89b54f7a03a8bb2599f69f3c1f95 Mon Sep 17 00:00:00 2001 From: Chris Hibbert Date: Thu, 30 Sep 2021 13:32:10 -0700 Subject: [PATCH 8/8] chore: more tests, improved comments, major rename rename calcSwapInPrices to pricesForStatedInput and calcSwapOutPrices to pricesForStatedOutput. --- .../src/contracts/constantProduct/README.md | 28 +++++----- .../src/contracts/constantProduct/calcFees.js | 2 +- .../constantProduct/calcSwapPrices.js | 21 ++++--- .../constantProduct/internal-types.js | 14 ++++- .../zoe/src/contracts/constantProduct/swap.js | 3 +- .../constantProduct/test-checkInvariants.js | 31 +++++++--- .../test-compareBondingCurves.js | 56 +++++-------------- .../test-compareNewSwapPrice.js | 53 ++++++++++++++++-- .../constantProduct/test-swapScenarios.js | 4 +- 9 files changed, 130 insertions(+), 82 deletions(-) diff --git a/packages/zoe/src/contracts/constantProduct/README.md b/packages/zoe/src/contracts/constantProduct/README.md index b30550e6545..dff568951c0 100644 --- a/packages/zoe/src/contracts/constantProduct/README.md +++ b/packages/zoe/src/contracts/constantProduct/README.md @@ -2,7 +2,8 @@ A constant product automatic market maker based on our Ratio library. It charges two kinds of fees: a pool fee remains in the pool to reward the liquidity -providers and a protocol fee is extracted to fund the economy. +providers and a protocol fee is extracted to fund the economy. The external +entry point is a call to `pricesForStatedInput()` or `pricesForStatedOutput()`. This algorithm uses the x*y=k formula directly, without fees. Briefly, there are two kinds of assets, whose values are kept roughly in balance through the @@ -131,32 +132,33 @@ But we get an even tighter bound by reducing the amount Alice has to spend 120000000568992 > 120000000000000 > 119999997571240 ``` -The initial price estimate is that 29,996 RUN would get 2249 BLD in a no-fee +The initial price estimate is that 29,996 RUN would get 2248 BLD in a no-fee pool. We base fees on this estimate, so the **protocol Fee will be 15 RUN** -(always in RUN) and the **pool fee will be 2 BLD**. The pool fee is calculated +(always in RUN) and the **pool fee will be 6 BLD**. The pool fee is calculated on the output for `swapIn` and the input for `swapOut`. Now we calculate the actual ΔX and ΔY, since the fees affect the size of the changes to the pool. From the first row of the third table we see that the calculation starts from ΔX of -`sGive - ProtocolFee (i.e. 29,996 - 15 = 29,981)` +`sGive - ProtocolFee (i.e. 30,000 - 15 = 29,985)` ``` -40,029,981 * 2,997,754 > 40,000,000 * 3,000,000 > 40,029,981 * 2,997,753 +40,029,985 * 2,997,7752 > 40,000,000 * 3,000,000 > 40,029,985 * 2,997,753 ``` -and re-checking how much is required to produce 2,997,754, we get +and re-checking how much is required to produce 2,997,753, we get ``` -40,029,970 * 2,997,754 > 40,000,000 * 3,000,000 > 40,029,969 * 2,997,754 +40_029_982 * 2,997,753 > 40,000,000 * 3,000,000 > 40,029,983 * 2,997,753 ``` -**ΔX is 29,970, and ΔY is 2246**. +**ΔX is 29,983, and ΔY is 2247**. - * Alice pays ΔX + protocolFee, which is 29970 + 15 (29985 RUN) - * Alice will receive ΔY - PoolFee which is 2246 - 2 (2244 BLD) - * The RUN in the pool will increase by ΔX (29970 RUN) - * The BLD in the pool will decrease by ΔY (2246 BLD) + * Alice pays ΔX + protocolFee, which is 29,983 + 15 (29998 RUN) + * Alice will receive ΔY - PoolFee which is 2247 - 6 (2241 BLD) + * The RUN in the pool will increase by ΔX (29983 RUN) + * The BLD in the pool will decrease by ΔY (2247 BLD) -The Pool grew by 2 BLD more than was required to maintain the constant product +The Pool grew by 6 BLD more than was required to maintain the constant product invariant. 15 RUN were extracted for the protocol fee. + diff --git a/packages/zoe/src/contracts/constantProduct/calcFees.js b/packages/zoe/src/contracts/constantProduct/calcFees.js index a128e79c2d6..9e3a9fc52dd 100644 --- a/packages/zoe/src/contracts/constantProduct/calcFees.js +++ b/packages/zoe/src/contracts/constantProduct/calcFees.js @@ -69,7 +69,7 @@ const calcFee = ({ amountIn, amountOut }, feeRatio) => { /** * Estimate the swap values, then calculate fees. The swapFn provided by the - * caller will be calcDeltaYSellingX for SwapIn and calcDeltaXSellingX for + * caller will be swapInNoFees or swapOutNoFees. * SwapOut. * * @type {CalculateFees} diff --git a/packages/zoe/src/contracts/constantProduct/calcSwapPrices.js b/packages/zoe/src/contracts/constantProduct/calcSwapPrices.js index 2de98326839..fd3e4db1192 100644 --- a/packages/zoe/src/contracts/constantProduct/calcSwapPrices.js +++ b/packages/zoe/src/contracts/constantProduct/calcSwapPrices.js @@ -7,6 +7,13 @@ import { assertKInvariantSellingX } from './invariants.js'; import { getXY } from './getXY.js'; import { swapInNoFees, swapOutNoFees } from './core.js'; +// pricesForStatedOutput() and pricesForStatedInput are the external entrypoints +// to the constantProduct module. The amountWanted is optional for +// pricesForStatedInput and amountgiven is optional for pricesForStatedOutput. + +// The two methods call swap, passing in different functions for noFeeSwap. +// pricesForStatedInput uses swapInNoFees, while pricesForStatedOutput uses +// swapOutNoFees. the noFeesSwap functions const makeCalcSwapPrices = noFeesSwap => { return Far( 'calcSwapPrices', @@ -36,8 +43,8 @@ const makeCalcSwapPrices = noFeesSwap => { ); }; -/** @type {CalcSwapPrices} */ -const calcSwapInPrices = ( +/** @type {CalcSwapInPrices} */ +const pricesForStatedInput = ( amountGiven, poolAllocation, amountWanted, @@ -54,8 +61,8 @@ const calcSwapInPrices = ( ); }; -/** @type {CalcSwapPrices} */ -const calcSwapOutPrices = ( +/** @type {CalcSwapOutPrices} */ +const pricesForStatedOutput = ( amountGiven, poolAllocation, amountWanted, @@ -72,7 +79,7 @@ const calcSwapOutPrices = ( ); }; -harden(calcSwapInPrices); -harden(calcSwapOutPrices); +harden(pricesForStatedInput); +harden(pricesForStatedOutput); -export { calcSwapOutPrices, calcSwapInPrices }; +export { pricesForStatedOutput, pricesForStatedInput }; diff --git a/packages/zoe/src/contracts/constantProduct/internal-types.js b/packages/zoe/src/contracts/constantProduct/internal-types.js index 0912ec9a017..2fa767d17d3 100644 --- a/packages/zoe/src/contracts/constantProduct/internal-types.js +++ b/packages/zoe/src/contracts/constantProduct/internal-types.js @@ -42,6 +42,9 @@ */ /** + * This is the type for swapInNoFees and swapOutNoFees. pricesForStatedInput() + * uses swapInNoFees, while pricesForStatedOutput() uses swapOutNoFees. + * * @callback NoFeeSwapFn * @param {NoFeeSwapFnInput} input * @returns {ImprovedNoFeeSwapResult} @@ -74,9 +77,18 @@ */ /** - * @callback CalcSwapPrices + * @callback CalcSwapInPrices * @param {Amount} amountGiven * @param {PoolAllocation} poolAllocation + * @param {Amount=} amountWanted + * @param {Ratio} protocolFeeRatio + * @param {Ratio} poolFeeRatio + * @returns {SwapResult} + */ +/** + * @callback CalcSwapOutPrices + * @param {Amount=} amountGiven + * @param {PoolAllocation} poolAllocation * @param {Amount} amountWanted * @param {Ratio} protocolFeeRatio * @param {Ratio} poolFeeRatio diff --git a/packages/zoe/src/contracts/constantProduct/swap.js b/packages/zoe/src/contracts/constantProduct/swap.js index 6399464133d..64111e0b7d1 100644 --- a/packages/zoe/src/contracts/constantProduct/swap.js +++ b/packages/zoe/src/contracts/constantProduct/swap.js @@ -235,7 +235,7 @@ export const swap = ( const swapperGets = subtractFees(amountOut, fees); // return noTransaction if fees would eat up all the trader's proceeds, - // the trader specified an amountGiven, and the trade would require mare, or + // the trader specified an amountGiven, and the trade would require more, or // the trade would require them to give more than they specified. if ( AmountMath.isEmpty(swapperGets) || @@ -255,7 +255,6 @@ export const swap = ( // Either xIncrement will be increased by the pool fee or yDecrement will be // reduced by it in order to compensate the pool. // newX and newY are the new pool balances, for comparison with start values. - // improvement is an estimate of how much the gains or losses were improved. const result = harden({ protocolFee: fees.protocolFee, poolFee: fees.poolFee, diff --git a/packages/zoe/test/unitTests/contracts/constantProduct/test-checkInvariants.js b/packages/zoe/test/unitTests/contracts/constantProduct/test-checkInvariants.js index e5c51072d96..26ec61c890d 100644 --- a/packages/zoe/test/unitTests/contracts/constantProduct/test-checkInvariants.js +++ b/packages/zoe/test/unitTests/contracts/constantProduct/test-checkInvariants.js @@ -6,8 +6,8 @@ import { BASIS_POINTS } from '../../../../src/contracts/constantProduct/defaults import { setupMintKits } from './setupMints.js'; import { makeRatio } from '../../../../src/contractSupport/index.js'; import { - calcSwapInPrices, - calcSwapOutPrices, + pricesForStatedInput, + pricesForStatedOutput, } from '../../../../src/contracts/constantProduct/calcSwapPrices.js'; import { checkKInvariantSellingX } from '../../../../src/contracts/constantProduct/invariants.js'; import { getXY } from '../../../../src/contracts/constantProduct/getXY.js'; @@ -66,6 +66,8 @@ function checkGetInput(t, args, result) { t.falsy(AmountMath.isEmpty(result.swapperGets)); t.truthy(AmountMath.isGTE(args[0], result.swapperGives)); t.truthy(AmountMath.isGTE(result.swapperGets, args[2])); + t.falsy(AmountMath.isEmpty(result.poolFee)); + t.falsy(AmountMath.isEmpty(result.protocolFee)); t.deepEqual( AmountMath.add(result.xIncrement, result.protocolFee), @@ -88,6 +90,8 @@ function checkGetOutput(t, args, result) { t.truthy(AmountMath.isGTE(args[0], result.swapperGives)); } t.truthy(AmountMath.isGTE(result.swapperGets, args[2])); + t.falsy(AmountMath.isEmpty(result.poolFee)); + t.falsy(AmountMath.isEmpty(result.protocolFee)); t.deepEqual( AmountMath.add(result.xIncrement, result.protocolFee), @@ -106,7 +110,7 @@ function checkGetOutput(t, args, result) { const testGetInputPrice = (t, inputs, runIn) => { const args = runIn ? prepareRUNInTest(inputs) : prepareRUNOutTest(inputs); - const result = calcSwapInPrices(...args); + const result = pricesForStatedInput(...args); checkGetInput(t, args, result); }; @@ -114,7 +118,7 @@ const testGetInputPriceThrows = (t, inputs, message, runIn) => { t.throws( _ => { const args = runIn ? prepareRUNInTest(inputs) : prepareRUNOutTest(inputs); - return calcSwapInPrices(...args); + return pricesForStatedInput(...args); }, { message, @@ -124,14 +128,14 @@ const testGetInputPriceThrows = (t, inputs, message, runIn) => { const testGetInputPriceNoTrade = (t, inputs, runIn) => { const args = runIn ? prepareRUNInTest(inputs) : prepareRUNOutTest(inputs); - const result = calcSwapInPrices(...args); + const result = pricesForStatedInput(...args); t.truthy(AmountMath.isEmpty(result.swapperGets)); t.truthy(AmountMath.isEmpty(result.swapperGives)); }; const testGetOutputPrice = (t, inputs, runIn) => { const args = runIn ? prepareRUNInTest(inputs) : prepareRUNOutTest(inputs); - const result = calcSwapOutPrices(...args); + const result = pricesForStatedOutput(...args); checkGetOutput(t, args, result); }; @@ -139,7 +143,7 @@ const getOutputPriceThrows = (t, inputs, message, runIn) => { t.throws( _ => { const args = runIn ? prepareRUNInTest(inputs) : prepareRUNOutTest(inputs); - return calcSwapOutPrices(...args); + return pricesForStatedOutput(...args); }, { message, @@ -149,7 +153,7 @@ const getOutputPriceThrows = (t, inputs, message, runIn) => { const testGetOutputPriceNoTrade = (t, inputs, runIn) => { const args = runIn ? prepareRUNInTest(inputs) : prepareRUNOutTest(inputs); - const result = calcSwapOutPrices(...args); + const result = pricesForStatedOutput(...args); t.truthy(AmountMath.isEmpty(result.swapperGets)); t.truthy(AmountMath.isEmpty(result.swapperGives)); }; @@ -309,6 +313,17 @@ test('getInputPrice big product', t => { testGetInputPrice(t, input, false); }); +test('getInputPrice README example', t => { + const input = { + inputReserve: 40_000_000n, + outputReserve: 3_000_000n, + inputValue: 30_000n, + outputValue: 2000n, + }; + testGetInputPrice(t, input, true); + testGetInputPrice(t, input, false); +}); + test('getOutputPrice ok', t => { const input = { inputReserve: 43n, diff --git a/packages/zoe/test/unitTests/contracts/constantProduct/test-compareBondingCurves.js b/packages/zoe/test/unitTests/contracts/constantProduct/test-compareBondingCurves.js index 6af2558fc5f..8e0801ef83a 100644 --- a/packages/zoe/test/unitTests/contracts/constantProduct/test-compareBondingCurves.js +++ b/packages/zoe/test/unitTests/contracts/constantProduct/test-compareBondingCurves.js @@ -5,8 +5,8 @@ import { BASIS_POINTS } from '../../../../src/contracts/constantProduct/defaults import { setupMintKits } from './setupMints.js'; import { makeRatio } from '../../../../src/contractSupport/index.js'; import { - calcSwapInPrices, - calcSwapOutPrices, + pricesForStatedInput, + pricesForStatedOutput, } from '../../../../src/contracts/constantProduct/calcSwapPrices.js'; // This assumes run is swapped in. The test should function the same @@ -37,9 +37,9 @@ const prepareSwapInTest = ({ inputReserve, outputReserve, inputValue }) => { }); }; -const testGetPrice = (t, inputs, expectedOutput) => { +const testInputGetPrice = (t, inputs, expectedOutput) => { const { args, bld } = prepareSwapInTest(inputs); - const result = calcSwapInPrices(...args); + const result = pricesForStatedInput(...args); t.deepEqual(result.swapperGets, bld(expectedOutput)); }; @@ -47,7 +47,7 @@ const getInputPriceThrows = (t, inputs, message) => { t.throws( _ => { const { args } = prepareSwapInTest(inputs); - return calcSwapInPrices(...args); + return pricesForStatedInput(...args); }, { message, @@ -85,13 +85,13 @@ const prepareSwapOutTest = ({ inputReserve, outputReserve, outputValue }) => { const testGetOutputPrice = (t, inputs, expectedInput) => { const { args, run } = prepareSwapOutTest(inputs); - const result = calcSwapOutPrices(...args); + const result = pricesForStatedOutput(...args); t.deepEqual(result.swapperGives, run(expectedInput)); }; const getOutputPriceThrows = (t, inputs, message) => { const { args } = prepareSwapOutTest(inputs); - t.throws(_ => calcSwapOutPrices(...args), { + t.throws(_ => pricesForStatedOutput(...args), { message, }); }; @@ -114,7 +114,7 @@ test('getInputPrice ok 2', t => { inputValue: 1398n, }; const expectedOutput = 572n; - testGetPrice(t, input, expectedOutput); + testInputGetPrice(t, input, expectedOutput); }); test('getInputPrice ok 3', t => { @@ -124,12 +124,7 @@ test('getInputPrice ok 3', t => { inputValue: 6635n, }; const expectedOutput = 3470n; - // const expected = { - // poolFee: 2n, - // swapperGives: 6634n, - // swapperGets: 3470n, - // }; - testGetPrice(t, input, expectedOutput); + testInputGetPrice(t, input, expectedOutput); }); test('getInputPrice ok 4', t => { @@ -139,12 +134,7 @@ test('getInputPrice ok 4', t => { inputValue: 1000n, }; const expectedOutput = 8n; - // const expected = { - // poolFee: 1n, - // swapperGives: 90n, - // swapperGets: 8n, - // }; - testGetPrice(t, input, expectedOutput); + testInputGetPrice(t, input, expectedOutput); }); test('getInputPrice ok 5', t => { @@ -154,12 +144,7 @@ test('getInputPrice ok 5', t => { inputValue: 17n, }; const expectedOutput = 6n; - // const expected = { - // poolFee: 1n, - // swapperGives: 17, - // swapperGets: 6, - // }; - testGetPrice(t, input, expectedOutput); + testInputGetPrice(t, input, expectedOutput); }); test('getInputPrice ok 6', t => { @@ -168,13 +153,8 @@ test('getInputPrice ok 6', t => { outputReserve: 117n, inputValue: 7n, }; - // const expected = { - // poolFee: 1n, - // swapperGives: 7, - // swapperGets: 15, - // }; const expectedOutput = 15n; - testGetPrice(t, input, expectedOutput); + testInputGetPrice(t, input, expectedOutput); }); test('getInputPrice negative', t => { @@ -227,12 +207,7 @@ test('getInputPrice big product', t => { inputValue: 1000n, }; const expectedOutput = 998n; - // const expected = { - // poolFee: 1n, - // swapperGives: 1000n, - // swapperGets: 998n, - // }; - testGetPrice(t, input, expectedOutput); + testInputGetPrice(t, input, expectedOutput); }); test('getOutputPrice ok', t => { @@ -301,11 +276,6 @@ test('getOutputPrice minimum price', t => { outputReserve: 10n, outputValue: 1n, }; - // const expected = { - // poolFee: 1n, - // swapperGives: 2n, - // swapperGets: 5n, - // }; const expectedOutput = 2n; testGetOutputPrice(t, input, expectedOutput); }); diff --git a/packages/zoe/test/unitTests/contracts/constantProduct/test-compareNewSwapPrice.js b/packages/zoe/test/unitTests/contracts/constantProduct/test-compareNewSwapPrice.js index 3caeec67a73..ed754588d80 100644 --- a/packages/zoe/test/unitTests/contracts/constantProduct/test-compareNewSwapPrice.js +++ b/packages/zoe/test/unitTests/contracts/constantProduct/test-compareNewSwapPrice.js @@ -9,7 +9,7 @@ import { swapInNoFees, } from '../../../../src/contracts/constantProduct/core.js'; import { makeRatio } from '../../../../src/contractSupport/index.js'; -import { calcSwapInPrices } from '../../../../src/contracts/constantProduct/calcSwapPrices.js'; +import { pricesForStatedInput } from '../../../../src/contracts/constantProduct/calcSwapPrices.js'; const BASIS_POINTS = 10000n; const POOL_FEE = 24n; @@ -36,7 +36,7 @@ const setupMints = () => { }; }; -test('newSwap getPriceGivenAvailableInput specify central', async t => { +test('pricesForStatedInput specify central', async t => { const { moola, bucks, moolaKit, bucksKit } = setupMints(); const poolAllocation = { Central: moola(800000n), @@ -71,7 +71,7 @@ test('newSwap getPriceGivenAvailableInput specify central', async t => { ); t.deepEqual(reduced, moola(9999n)); - const result = calcSwapInPrices( + const result = pricesForStatedInput( amountGiven, poolAllocation, amountWanted, @@ -86,7 +86,7 @@ test('newSwap getPriceGivenAvailableInput specify central', async t => { // t.deepEqual(result.protocolFee, moola(5n)); }); -test('newSwap getPriceGivenAvailableInput secondary', async t => { +test('pricesForStatedInput secondary', async t => { const { moola, bucks, moolaKit } = setupMints(); const poolAllocation = { Central: moola(800000n), @@ -102,7 +102,7 @@ test('newSwap getPriceGivenAvailableInput secondary', async t => { ); const poolFeeRatio = makeRatio(POOL_FEE, moolaKit.brand, BASIS_POINTS); - const result = calcSwapInPrices( + const result = pricesForStatedInput( amountGiven, poolAllocation, amountWanted, @@ -123,3 +123,46 @@ test('newSwap getPriceGivenAvailableInput secondary', async t => { // Swapper pays one more: 10n // t.deepEqual(result.protocolFee, newSwapResult.protocolFee); }); + +test('pricesForStatedInput README example', async t => { + const { moola, bucks, moolaKit, bucksKit } = setupMints(); + const poolAllocation = { + Central: moola(40_000_000n), + Secondary: bucks(3_000_000n), + }; + const amountGiven = moola(30_000n); + const amountWanted = bucks(2_000n); + + const protocolFeeRatio = makeRatio(5n, moolaKit.brand, BASIS_POINTS); + const poolFeeRatio = makeRatio(25n, bucksKit.brand, BASIS_POINTS); + + const noFeesResult = swapInNoFees({ amountGiven, poolAllocation }); + t.deepEqual(noFeesResult.amountIn, moola(29996n)); + t.deepEqual(noFeesResult.amountOut, bucks(2248n)); + + const noReductionResult = calcDeltaYSellingX( + poolAllocation.Central, + poolAllocation.Secondary, + amountGiven, + ); + t.deepEqual(noReductionResult, bucks(2248n)); + + const reduced = calcDeltaXSellingX( + poolAllocation.Central, + poolAllocation.Secondary, + noReductionResult, + ); + t.deepEqual(reduced, moola(29996n)); + + const result = pricesForStatedInput( + amountGiven, + poolAllocation, + amountWanted, + protocolFeeRatio, + poolFeeRatio, + ); + t.deepEqual(result.swapperGives, moola(29998n)); + t.deepEqual(result.swapperGets, bucks(2241n)); + t.deepEqual(result.protocolFee, moola(15n)); + t.deepEqual(result.poolFee, bucks(6n)); +}); diff --git a/packages/zoe/test/unitTests/contracts/constantProduct/test-swapScenarios.js b/packages/zoe/test/unitTests/contracts/constantProduct/test-swapScenarios.js index c64ed777649..9cf082cad28 100644 --- a/packages/zoe/test/unitTests/contracts/constantProduct/test-swapScenarios.js +++ b/packages/zoe/test/unitTests/contracts/constantProduct/test-swapScenarios.js @@ -11,7 +11,7 @@ import { makeRatio, natSafeMath, } from '../../../../src/contractSupport/index.js'; -import { calcSwapInPrices } from '../../../../src/contracts/constantProduct/calcSwapPrices.js'; +import { pricesForStatedInput } from '../../../../src/contracts/constantProduct/calcSwapPrices.js'; const { multiply, ceilDivide } = natSafeMath; @@ -51,7 +51,7 @@ const prepareSwapInTest = ({ const testGetPrice = (t, inputs, expectedOutput) => { const { args, run, bld } = prepareSwapInTest(inputs); - const result = calcSwapInPrices(...args); + const result = pricesForStatedInput(...args); const expected = harden({ protocolFee: run(expectedOutput.protocolFee), poolFee: bld(expectedOutput.poolFee),