Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

A constant product AMM that charges pool and protocol fees #3791

Merged
merged 4 commits into from
Oct 2, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions packages/zoe/src/contracts/constantProduct/calcFees.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,9 @@ import { BASIS_POINTS } from './defaults.js';
const { details: X } = assert;

/**
* Make a ratio given a nat representing basis points
* Make a ratio given a nat representing basis points and a brand.
*
* @param {NatValue} feeBP
* @param {Brand} brandOfFee
* @returns {Ratio}
* @type {MakeFeeRatio}
*/
const makeFeeRatio = (feeBP, brandOfFee) => {
return makeRatio(feeBP, brandOfFee, BASIS_POINTS);
Expand Down
17 changes: 17 additions & 0 deletions packages/zoe/src/contracts/constantProduct/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,23 @@
* @returns {Amount}
*/

/**
* A bigint representing a number that will be divided by 10,0000. Financial
* ratios are often represented in basis points.
*
* @typedef {bigint} BASIS_POINTS
*/

/**
* Make a Ratio representing a fee expressed in Basis Points. (hundredths of a
* percent)
*
* @callback MakeFeeRatio
* @param {BASIS_POINTS} feeBP
* @param {Brand} brandOfFee
* @returns {Ratio}
*/

/**
* @callback AmountGT
* @param {Amount} left
Expand Down
30 changes: 30 additions & 0 deletions packages/zoe/src/contracts/vpool-xyk-amm/addLiquidity.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// @ts-check

import { assertProposalShape } from '../../contractSupport/index.js';

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

/**
* @param {ContractFacet} zcf
* @param {(brand: Brand) => XYKPool} getPool
*/
export const makeMakeAddLiquidityInvitation = (zcf, getPool) => {
const addLiquidity = seat => {
assertProposalShape(seat, {
give: {
Central: null,
Secondary: null,
},
want: { Liquidity: null },
});
// Get the brand of the secondary token so we can identify the liquidity pool.
const secondaryBrand = seat.getProposal().give.Secondary.brand;
const pool = getPool(secondaryBrand);
return pool.addLiquidity(seat);
};

const makeAddLiquidityInvitation = () =>
zcf.makeInvitation(addLiquidity, 'multipool autoswap add liquidity');

return makeAddLiquidityInvitation;
};
193 changes: 193 additions & 0 deletions packages/zoe/src/contracts/vpool-xyk-amm/doublePool.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
// @ts-check

import { AmountMath } from '@agoric/ertp';
import { assert, details as X } from '@agoric/assert';
import { makeFeeRatio } from '../constantProduct/calcFees';
import {
pricesForStatedInput,
pricesForStatedOutput,
} from '../constantProduct/calcSwapPrices.js';

// Price calculations and swap using a pair of pools. Both pools map between RUN
// and some collateral. We arrange the trades so collateralInPool will have
// collateral added and collateralOutPool subtracted. When traders specify an
// input price, that brand will be the inPool; when they specify the output
// price that brand is the outPool.

const publicPrices = prices => {
return { amountIn: prices.swapperGives, amountOut: prices.swapperGets };
};

/**
* @param {ContractFacet} zcf
* @param {XYKPool} collateralInPool
* @param {XYKPool} collateralOutPool
* @param {BASIS_POINTS} protocolFeeBP
* @param {BASIS_POINTS} poolFeeBP
* @param {ZCFSeat} feeSeat
* @returns {VPool}
*/
export const makeDoublePool = (
zcf,
collateralInPool,
collateralOutPool,
protocolFeeBP,
poolFeeBP,
feeSeat,
) => {
const inCentral = collateralInPool.getCentralAmount();
const inSecondary = collateralInPool.getSecondaryAmount();

const outCentral = collateralOutPool.getCentralAmount();
const outSecondary = collateralOutPool.getSecondaryAmount();

const inAllocation = { Central: inCentral, Secondary: inSecondary };
const outAllocation = { Central: outCentral, Secondary: outSecondary };

const centralBrand = inCentral.brand;
const centralFeeRatio = makeFeeRatio(poolFeeBP, centralBrand);
const emptyCentralAmount = AmountMath.makeEmpty(centralBrand);
const protocolFeeRatio = makeFeeRatio(protocolFeeBP, centralBrand);
assert(
centralBrand === outCentral.brand,
X`The central brands on the two pools must match: ${centralBrand}, ${outCentral.brand}`,
);

const allocateGainsAndLosses = (seat, prices) => {
const inPoolSeat = collateralInPool.getPoolSeat();
const outPoolSeat = collateralOutPool.getPoolSeat();

seat.decrementBy({ In: prices.swapperGives });
seat.incrementBy({ Out: prices.swapperGets });
feeSeat.incrementBy({ RUN: prices.protocolFee });
inPoolSeat.incrementBy({ Secondary: prices.inPoolIncrement });
inPoolSeat.decrementBy({ Central: prices.inPoolDecrement });
outPoolSeat.incrementBy({ Central: prices.outPoolIncrement });
outPoolSeat.decrementBy({ Secondary: prices.outpoolDecrement });

zcf.reallocate(outPoolSeat, inPoolSeat, feeSeat, seat);
seat.exit();
collateralInPool.updateState();
collateralOutPool.updateState();
return `Swap successfully completed.`;
};

const getPriceForInput = (amountIn, amountOut) => {
// We must do two consecutive swapInPrice() calls,
// followed by a call to swapOutPrice().
// 1) from amountIn to the central token, which tells us how much central
// would be provided for amountIn,
// 2) from that amount of central to brandOut, which tells us how much of
// brandOut will be provided as well as the minimum price in central
// tokens, then finally
// 3) call swapOutPrice() to see if the same proceeds can be purchased for
// less.
// Notice that in the second call, the original amountOut is used, and in
// the third call, the original amountIn is used.
const interimInpoolPrices = pricesForStatedInput(
amountIn,
inAllocation,
emptyCentralAmount,
protocolFeeRatio,
centralFeeRatio,
);
const outPoolPrices = pricesForStatedInput(
interimInpoolPrices.swapperGets,
outAllocation,
amountOut,
protocolFeeRatio,
makeFeeRatio(poolFeeBP, amountOut.brand),
);
const finalInPoolPrices = pricesForStatedOutput(
amountIn,
inAllocation,
outPoolPrices.swapperGives,
protocolFeeRatio,
centralFeeRatio,
);
return harden({
swapperGives: finalInPoolPrices.swapperGives,
swapperGets: outPoolPrices.swapperGets,
inPoolIncrement: finalInPoolPrices.xIncrement,
inPoolDecrement: finalInPoolPrices.yDecrement,
outPoolIncrement: outPoolPrices.xIncrement,
outpoolDecrement: outPoolPrices.yDecrement,
protocolFee: AmountMath.add(
finalInPoolPrices.protocolFee,
outPoolPrices.protocolFee,
),
});
};

const getInputPrice = (amountIn, amountOut) => {
return publicPrices(getPriceForInput(amountIn, amountOut));
};

const swapIn = (seat, amountIn, amountOut) => {
const prices = getPriceForInput(amountIn, amountOut);
return allocateGainsAndLosses(seat, prices);
};

const getPriceForOutput = (amountIn, amountOut) => {
// We must do two consecutive swapOutPrice() calls, followed by a call to
// swapInPrice().
// 1) from amountOut to the central token, which tells us how much central
// is required to obtain amountOut,
// 2) from that amount of central to brandIn, which tells us how much of
// brandIn is required as well as the max proceeds in central tokens, then
// finally
// 3) call swapInPrice() to see if those central proceeds could purchase
// larger amount
// Notice that the amountIn parameter to the first call to swapOutPrice
// specifies an empty amount. This is interpreted as "no limit", which is
// necessary since we can't guess a reasonable maximum of the central token.
const interimOutpoolPrices = pricesForStatedOutput(
emptyCentralAmount,
outAllocation,
amountOut,
protocolFeeRatio,
centralFeeRatio,
);
const inpoolPrices = pricesForStatedOutput(
amountIn,
inAllocation,
interimOutpoolPrices.swapperGets,
protocolFeeRatio,
makeFeeRatio(poolFeeBP, amountIn.brand),
);
const finalOutpoolPrices = pricesForStatedInput(
inpoolPrices.swapperGives,
outAllocation,
amountOut,
protocolFeeRatio,
centralFeeRatio,
);
return harden({
swapperGives: inpoolPrices.swapperGives,
swapperGets: finalOutpoolPrices.swapperGets,
inPoolIncrement: inpoolPrices.xIncrement,
inPoolDecrement: inpoolPrices.yDecrement,
outPoolIncrement: finalOutpoolPrices.xIncrement,
outpoolDecrement: finalOutpoolPrices.yDecrement,
protocolFee: AmountMath.add(
finalOutpoolPrices.protocolFee,
inpoolPrices.protocolFee,
),
});
};

const getOutputPrice = (amountIn, amountOut) => {
return publicPrices(getPriceForOutput(amountIn, amountOut));
};
const swapOut = (seat, amountIn, amountOut) => {
const prices = getPriceForOutput(amountIn, amountOut);
return allocateGainsAndLosses(seat, prices);
};

return {
getInputPrice,
getOutputPrice,
swapIn,
swapOut,
};
};
Loading