Skip to content

Commit

Permalink
fix: revamp multipoolAutoswap: liquidity bug, in vs. out prices
Browse files Browse the repository at this point in the history
distinguish prices for stated input from prices for stated output.
provide swapIn/swapOut to distinguish stated In/Out prices
ensure proportionality is enforced when adding liquidity
re-use the test jig
match the autoswap API
Tests for all combinations of swapIn/Out to/from central pool and trading secondary currencies
Update documentation
  • Loading branch information
Chris-Hibbert committed Sep 3, 2020
1 parent 73f373b commit 92bfdd5
Show file tree
Hide file tree
Showing 10 changed files with 1,189 additions and 401 deletions.
9 changes: 5 additions & 4 deletions packages/zoe/src/contractSupport/bondingCurves.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,7 @@ export const getInputPrice = ({
const numerator = multiply(inputWithFee, outputReserve);
const denominator = add(multiply(inputReserve, 10000), inputWithFee);

const outputValue = floorDivide(numerator, denominator);
return outputValue;
return floorDivide(numerator, denominator);
};

/**
Expand Down Expand Up @@ -127,12 +126,14 @@ export const calcSecondaryRequired = ({
return secondaryIn;
}

const exact =
multiply(centralIn, secondaryPool) === multiply(secondaryIn, centralPool);
const scaledSecondary = floorDivide(
multiply(centralIn, secondaryPool),
centralPool,
);
const exact =
multiply(centralIn, secondaryPool) ===
multiply(scaledSecondary, centralPool);

// doesn't match the x-y-k.pdf paper, but more correct. When the ratios are
// exactly equal, lPrime is exactly l * (1 + alpha) and adding one is wrong
return exact ? scaledSecondary : 1 + scaledSecondary;
Expand Down
30 changes: 30 additions & 0 deletions packages/zoe/src/contracts/exported.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,36 @@
* @property {() => Record<string, Amount>} getPoolAllocation get an
* AmountKeywordRecord showing the current balances in the pool.
*/

/**
* @typedef {Object} MultipoolAutoswapPublicFacet
* @property {(issuer: Issuer, keyword: Keyword) => Pool} addPool
* add a new liquidity pool
* @property {() => Promise<Invitation>} makeSwapInvitation synonym for
* makeSwapInInvitation
* @property {() => Promise<Invitation>} makeSwapInInvitation make an invitation
* that allows one to do a swap in which the In amount is specified and the Out
* amount is calculated
* @property {() => Promise<Invitation>} makeSwapOutInvitation make an invitation
* that allows one to do a swap in which the Out amount is specified and the In
* amount is calculated
* @property {() => Promise<Invitation>} makeAddLiquidityInvitation make an
* invitation that allows one to add liquidity to the pool.
* @property {() => Promise<Invitation>} makeRemoveLiquidityInvitation make an
* invitation that allows one to remove liquidity from the pool.
* @property {() => Issuer} getLiquidityIssuer
* @property {() => number} getLiquiditySupply get the current value of
* liquidity held by investors.
* @property {(amountIn: Amount, brandOut: Brand) => Amount} getInputPrice
* calculate the amount of brandOut that will be returned if the amountIn is
* offered using makeSwapInInvitation at the current price.
* @property {(amountOut: Amount, brandIn: Brand) => Amount} getOutputPrice
* calculate the amount of brandIn that is required in order to get amountOut
* using makeSwapOutInvitation at the current price
* @property {() => Record<string, Amount>} getPoolAllocation get an
* AmountKeywordRecord showing the current balances in the pool.
*/

/**
* @typedef {Object} AutomaticRefundPublicFacet
* @property {() => number} getOffersCount
Expand Down
71 changes: 53 additions & 18 deletions packages/zoe/src/contracts/multipoolAutoswap/getCurrentPrice.js
Original file line number Diff line number Diff line change
@@ -1,47 +1,82 @@
import '../../../exported';

/**
* Build functions to calculate prices for multipoolAutoswap. Two methods are
* returned. In one the caller specifies the amount they will pay, and in the
* other they specify the amount they wish to receive.
*
* @param {(brand: Brand) => boolean} isSecondary
* @param {(brand: Brand) => boolean} isCentral
* @param {(brand: Brand) => Pool} getPool
*/

import '../../../exported';

export const makeGetCurrentPrice = (isSecondary, isCentral, getPool) => {
/**
* `getCurrentPrice` calculates the result of a trade, given a certain
* `getOutputForGivenInput` calculates the result of a trade, given a certain
* amount of digital assets in.
* @param {Amount} amountIn - the amount of digital assets to be
* sent in
* @param {Brand} brandOut - the brand of the requested payment.
* @param {Amount} amountIn - the amount of digital
* assets to be sent in
* @param {Brand} brandOut - The brand of asset desired
* @return {Amount} the amount that would be paid out at the current price.
*/
// eslint-disable-next-line consistent-return
const getCurrentPrice = (amountIn, brandOut) => {
// BrandIn could either be the central token brand, or one of
// the secondary token brands.
const getOutputForGivenInput = (amountIn, brandOut) => {
const { brand: brandIn, value: inputValue } = amountIn;

if (isCentral(brandIn) && isSecondary(brandOut)) {
return getPool(brandOut).getCurrentPrice(true, inputValue);
return getPool(brandOut).getCentralToSecondaryInputPrice(inputValue);
}

if (isSecondary(brandIn) && isCentral(brandOut)) {
return getPool(brandIn).getSecondaryToCentralInputPrice(inputValue);
}

if (isSecondary(brandIn) && isSecondary(brandOut)) {
// We must do two consecutive calls to get the price: from
// the brandIn to the central token, then from the central
// token to the brandOut.
const centralTokenAmount = getPool(
brandIn,
).getSecondaryToCentralInputPrice(inputValue);
return getPool(brandOut).getCentralToSecondaryInputPrice(
centralTokenAmount.value,
);
}

throw new Error(`brands were not recognized`);
};

/**
* `getInputForGivenOutput` calculates the amount of assets required to be
* provided in order to obtain a specified gain.
* @param {Amount} amountOut - the amount of digital assets desired
* @param {Brand} brandIn - The brand of asset desired
* @return {Amount} The amount required to be paid in order to gain amountOut
*/
const getInputForGivenOutput = (amountOut, brandIn) => {
const { brand: brandOut, value: outputValue } = amountOut;

if (isCentral(brandIn) && isSecondary(brandOut)) {
return getPool(brandOut).getCentralToSecondaryOutputPrice(outputValue);
}

if (isSecondary(brandIn) && isCentral(brandOut)) {
return getPool(brandIn).getCurrentPrice(false, inputValue);
return getPool(brandIn).getSecondaryToCentralOutputPrice(outputValue);
}

if (isSecondary(brandIn) && isSecondary(brandOut)) {
// We must do two consecutive `getCurrentPrice` calls: from
// We must do two consecutive calls to get the price: from
// the brandIn to the central token, then from the central
// token to the brandOut.
const centralTokenAmount = getPool(brandIn).getCurrentPrice(
false,
inputValue,
const centralTokenAmount = getPool(
brandIn,
).getSecondaryToCentralOutputPrice(outputValue);
return getPool(brandOut).getCentralToSecondaryOutputPrice(
centralTokenAmount.value,
);
return getPool(brandOut).getCurrentPrice(true, centralTokenAmount.value);
}

throw new Error(`brands were not recognized`);
};

return getCurrentPrice;
return { getOutputForGivenInput, getInputForGivenOutput };
};
54 changes: 39 additions & 15 deletions packages/zoe/src/contracts/multipoolAutoswap/multipoolAutoswap.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ import { makeMakeRemoveLiquidityInvitation } from './removeLiquidity';
import '../../../exported';

/**
* Autoswap is a rewrite of Uniswap. Please see the documentation for
* more
* https://agoric.com/documentation/zoe/guide/contracts/autoswap.html
* Multipool Autoswap is a rewrite of Uniswap that supports multiple liquidity
* pools, and direct exchanges across pools. Please see the documentation for
* more: https://agoric.com/documentation/zoe/guide/contracts/autoswap.html
*
* We expect that this contract will have tens to hundreds of issuers.
* Each liquidity pool is between the central token and a secondary
Expand All @@ -30,9 +30,25 @@ import '../../../exported';
*
* When the contract is instantiated, the central token is specified
* in the terms. Separate invitations are available by calling methods
* on the publicFacet for adding and removing liquidity, and for
* making trades. Other publicFacet operations support monitoring
* prices and the sizes of pools.
* on the publicFacet for adding and removing liquidity and for
* making trades. Other publicFacet operations support querying
* prices and the sizes of pools. New Pools can be created with addPool().
*
* When making trades or requesting prices, the caller must specify that either
* the input price (swapIn, getInputPrice) or the output price (swapOut,
* getOutPutPrice) is fixed. For swaps, the required keywords are `In` for the
* trader's `give` amount, and `Out` for the trader's `want` amount.
* getInputPrice and getOutPrice each take an Amount for the direction that is
* being specified, and just a brand for the desired value, which is returned as
* the appropriate amount.
*
* When adding and removing liquidity, the keywords are Central, Secondary, and
* Liquidity. adding liquidity has Central and Secondary in the `give` section,
* while removing liquidity has `want` and `give` swapped.
*
* Transactions that don't require an invitation include addPool, and the
* queries: getInputPrice, getOutputPrice, getPoolAllocation,
* getLiquidityIssuer, and getLiquiditySupply.
*
* @type {ContractStartFn}
*/
Expand All @@ -51,20 +67,23 @@ const start = zcf => {
const isSecondary = secondaryBrandToPool.has;
const isCentral = brand => brand === centralBrand;

const getLiquiditySupply = brand => getPool(brand).getLiquiditySupply();
const getLiquidityIssuer = brand => getPool(brand).getLiquidityIssuer();
const addPool = makeAddPool(zcf, isSecondary, initPool, centralBrand);
const getPoolAllocation = brand => {
return getPool(brand)
.getPoolSeat()
.getCurrentAllocation();
};
const getCurrentPrice = makeGetCurrentPrice(isSecondary, isCentral, getPool);
const makeSwapInvitation = makeMakeSwapInvitation(
zcf,
isSecondary,
isCentral,
getPool,
);

const {
getOutputForGivenInput,
getInputForGivenOutput,
} = makeGetCurrentPrice(isSecondary, isCentral, getPool);
const {
makeSwapInInvitation,
makeSwapOutInvitation,
} = makeMakeSwapInvitation(zcf, isSecondary, isCentral, getPool);
const makeAddLiquidityInvitation = makeMakeAddLiquidityInvitation(
zcf,
getPool,
Expand All @@ -75,12 +94,17 @@ const start = zcf => {
getPool,
);

/** @type {MultipoolAutoswapPublicFacet} */
const publicFacet = {
addPool,
getPoolAllocation,
getLiquidityIssuer,
getCurrentPrice,
makeSwapInvitation,
getLiquiditySupply,
getInputPrice: getOutputForGivenInput,
getOutputPrice: getInputForGivenOutput,
makeSwapInvitation: makeSwapInInvitation,
makeSwapInInvitation,
makeSwapOutInvitation,
makeAddLiquidityInvitation,
makeRemoveLiquidityInvitation,
};
Expand Down
Loading

0 comments on commit 92bfdd5

Please sign in to comment.