Skip to content

Commit

Permalink
feat: public econ stats from AMM
Browse files Browse the repository at this point in the history
closes: #4648

add notifier and subscription to state
  • Loading branch information
Chris-Hibbert committed May 24, 2022
1 parent 2fedea6 commit 7e5d47b
Show file tree
Hide file tree
Showing 9 changed files with 130 additions and 15 deletions.
3 changes: 3 additions & 0 deletions packages/run-protocol/src/vpool-xyk-amm/addPool.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export const makeAddIssuer = (zcf, isInSecondaries, brandToLiquidityMint) => {
* @param {ZCFSeat} reserveLiquidityTokenSeat seat that holds liquidity tokens
* from adding pool liquidity. It is expected to be collected by the Reserve.
* @param {WeakStore<Brand,ZCFMint>} brandToLiquidityMint
* @param {() => void} updateMetrics
*/
export const makeAddPoolInvitation = (
zcf,
Expand All @@ -73,6 +74,7 @@ export const makeAddPoolInvitation = (
protocolSeat,
reserveLiquidityTokenSeat,
brandToLiquidityMint,
updateMetrics,
) => {
const makePool = definePoolKind(
zcf,
Expand All @@ -92,6 +94,7 @@ export const makeAddPoolInvitation = (
const poolFacets = makePool(liquidityZcfMint, poolSeat, secondaryBrand);

initPool(secondaryBrand, poolFacets);
updateMetrics();
return { liquidityZcfMint, poolFacets };
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import {
makeRatio,
} from '@agoric/zoe/src/contractSupport/ratio.js';

import './types.js';
import './internal-types.js';

import { BASIS_POINTS } from './defaults.js';

const { details: X } = assert;
Expand Down
30 changes: 26 additions & 4 deletions packages/run-protocol/src/vpool-xyk-amm/multipoolMarketMaker.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
// @ts-check

import { makeWeakStore } from '@agoric/store';
import { makeStore, makeWeakStore } from '@agoric/store';
import { Far } from '@endo/marshal';

import { AssetKind, makeIssuerKit } from '@agoric/ertp';
import { handleParamGovernance, ParamTypes } from '@agoric/governance';
import { makeSubscriptionKit } from '@agoric/notifier';

import { assertIssuerKeywords } from '@agoric/zoe/src/contractSupport/index.js';
import { E } from '@endo/far';
import { makeAddIssuer, makeAddPoolInvitation } from './addPool.js';
import { publicPrices } from './pool.js';
import {
makeMakeAddLiquidityInvitation,
makeMakeAddLiquidityAtRateInvitation,
makeMakeAddLiquidityInvitation,
} from './addLiquidity.js';
import { makeMakeRemoveLiquidityInvitation } from './removeLiquidity.js';

Expand All @@ -28,6 +29,11 @@ import {

const { quote: q, details: X } = assert;

/**
* @typedef {object} MetricsNotification
* @property {Brand[]} XYK brands of pools that use an X*Y=K pricing policy
*/

/**
* Multipool AMM is a rewrite of Uniswap that supports multiple liquidity pools,
* and direct exchanges across pools. Please see the documentation for more:
Expand Down Expand Up @@ -133,8 +139,8 @@ const start = async (zcf, privateArgs) => {
)}`,
);

/** @type {WeakStore<Brand,PoolFacets>} */
const secondaryBrandToPool = makeWeakStore('secondaryBrand');
/** @type {Store<Brand,PoolFacets>} */
const secondaryBrandToPool = makeStore('secondaryBrand');
const getPool = brand => secondaryBrandToPool.get(brand).pool;
const getPoolHelper = brand => secondaryBrandToPool.get(brand).helper;
const initPool = secondaryBrandToPool.init;
Expand All @@ -146,6 +152,16 @@ const start = async (zcf, privateArgs) => {

const quoteIssuerKit = makeIssuerKit('Quote', AssetKind.SET);

/** @type {SubscriptionRecord<MetricsNotification>} */
const { publication: metricsPublication, subscription: metricsSubscription } =
makeSubscriptionKit();
const updateMetrics = () => {
metricsPublication.updateState(
harden({ XYK: Array.from(secondaryBrandToPool.keys()) }),
);
};
updateMetrics();

// For now, this seat collects protocol fees. It needs to be connected to
// something that will extract the fees.
const { zcfSeat: protocolSeat } = zcf.makeEmptySeatKit();
Expand All @@ -165,6 +181,7 @@ const start = async (zcf, privateArgs) => {
protocolSeat,
reserveLiquidityTokenSeat,
secondaryBrandToLiquidityMint,
updateMetrics,
);
const addIssuer = makeAddIssuer(
zcf,
Expand All @@ -184,6 +201,9 @@ const start = async (zcf, privateArgs) => {
};
};

/** @param {Brand} brand */
const getPoolMetrics = brand => getPool(brand).getMetrics();

/**
* @param {Brand} brandIn
* @param {Brand} [brandOut]
Expand Down Expand Up @@ -266,6 +286,8 @@ const start = async (zcf, privateArgs) => {
getAllPoolBrands: () =>
Object.values(zcf.getTerms().brands).filter(isSecondary),
getProtocolPoolBalance: () => protocolSeat.getCurrentAllocation(),
getMetrics: () => metricsSubscription,
getPoolMetrics,
}),
);

Expand Down
30 changes: 29 additions & 1 deletion packages/run-protocol/src/vpool-xyk-amm/pool.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// @ts-check

import { AmountMath, isNatValue } from '@agoric/ertp';
import { makeNotifierKit } from '@agoric/notifier';
import { makeNotifierKit, makeSubscriptionKit } from '@agoric/notifier';

import {
calcLiqValueToMint,
Expand Down Expand Up @@ -42,6 +42,8 @@ export const publicPrices = prices => {
* @typedef {{
* updater: IterationObserver<any>,
* notifier: Notifier<any>,
* metricsPublication: IterationObserver<PoolMetricsNotification>,
* metricsSubscription: Subscription<PoolMetricsNotification>
* poolSeat: ZCFSeat,
* liqTokenSupply: bigint,
* }} MutableState
Expand All @@ -54,6 +56,11 @@ export const publicPrices = prices => {
* singlePool: VirtualPool,
* },
* }} MethodContext
*
* @typedef {object} PoolMetricsNotification
* @property {Amount} centralAmount
* @property {Amount} secondaryAmount
* @property {NatValue} liquidityTokens - outstanding tokens
*/

export const updateUpdaterState = (updater, pool) =>
Expand Down Expand Up @@ -131,8 +138,20 @@ const helperBehavior = {
);
zcfSeat.exit();
updateUpdaterState(updater, pool);
facets.helper.updateMetrics();
return 'Added liquidity.';
},
/** @param {MethodContext} context */
updateMetrics: context => {
const { state, facets } = context;
const payload = harden({
centralAmount: facets.pool.getCentralAmount(),
secondaryAmount: facets.pool.getSecondaryAmount(),
liquidityTokens: state.liqTokenSupply,
});

state.metricsPublication.updateState(payload);
},
};

const poolBehavior = {
Expand Down Expand Up @@ -233,6 +252,7 @@ const poolBehavior = {

userSeat.exit();
updateUpdaterState(state.updater, facets.pool);
facets.helper.updateMetrics();
return 'Liquidity successfully removed.';
},
getNotifier: ({ state: { notifier } }) => notifier,
Expand All @@ -242,6 +262,7 @@ const poolBehavior = {
getToCentralPriceAuthority: ({ state }) => state.toCentralPriceAuthority,
getFromCentralPriceAuthority: ({ state }) => state.fromCentralPriceAuthority,
getVPool: ({ facets }) => facets.singlePool,
getMetrics: ({ state }) => state.metricsSubscription,
};

/** @param {MethodContext} context */
Expand Down Expand Up @@ -290,6 +311,7 @@ const finish = context => {
context.state.toCentralPriceAuthority = toCentralPriceAuthority;
// @ts-expect-error declared read-only, set value once
context.state.fromCentralPriceAuthority = fromCentralPriceAuthority;
context.facets.helper.updateMetrics();
};

/**
Expand All @@ -312,6 +334,10 @@ export const definePoolKind = (
const { brand: liquidityBrand, issuer: liquidityIssuer } =
liquidityZcfMint.getIssuerRecord();
const { notifier, updater } = makeNotifierKit();
const {
publication: metricsPublication,
subscription: metricsSubscription,
} = makeSubscriptionKit();

// XXX why does the paramAccessor have to be repackaged as a Far object?
const params = Far('pool param accessor', {
Expand All @@ -335,6 +361,8 @@ export const definePoolKind = (
quoteIssuerKit,
timer,
paramAccessor: params,
metricsPublication,
metricsSubscription,
};
};

Expand Down
3 changes: 2 additions & 1 deletion packages/run-protocol/src/vpool-xyk-amm/singlePool.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const getPools = pool => ({

export const singlePool = {
allocateGainsAndLosses: (context, seat, prices) => {
const { pool } = context.facets;
const { pool, helper } = context.facets;
const { poolSeat, zcf, protocolSeat } = context.state;
seat.decrementBy(harden({ In: prices.swapperGives }));
seat.incrementBy(harden({ Out: prices.swapperGets }));
Expand All @@ -34,6 +34,7 @@ export const singlePool = {
zcf.reallocate(poolSeat, seat, protocolSeat);
seat.exit();
pool.updateState();
helper.updateMetrics();
return `Swap successfully completed.`;
},

Expand Down
8 changes: 8 additions & 0 deletions packages/run-protocol/src/vpool-xyk-amm/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@
* @property {Amount<'nat'>} newX
*/

/**
* @typedef {import('./multipoolMarketMaker.js').MetricsNotification} MetricsNotification
* @typedef {import('./pool.js').PoolMetricsNotification} PoolMetricsNotification
*/

/**
* @typedef {object} DoublePoolSwapResult
* @property {Amount<'nat'>} swapperGives
Expand Down Expand Up @@ -77,6 +82,7 @@
* @property {() => PriceAuthority} getToCentralPriceAuthority
* @property {() => PriceAuthority} getFromCentralPriceAuthority
* @property {() => VirtualPool} getVPool
* @property {() => Subscription<PoolMetricsNotification>} getMetrics
*/

/**
Expand Down Expand Up @@ -128,6 +134,8 @@
* Prices and notifications about changing prices.
* @property {() => Brand[]} getAllPoolBrands
* @property {() => Allocation} getProtocolPoolBalance
* @property {() => Subscription<MetricsNotification>} getMetrics
* @property {(brand: Brand) => Subscription<PoolMetricsNotification>} getPoolMetrics
*/

/**
Expand Down
45 changes: 45 additions & 0 deletions packages/run-protocol/test/amm/vpool-xyk-amm/test-xyk-amm-swap.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
import { BASIS_POINTS } from '../../../src/vpool-xyk-amm/constantProduct/defaults.js';
import { setupAmmServices } from './setup.js';
import { unsafeMakeBundleCache } from '../../bundleTool.js';
import { subscriptionTracker } from '../../metrics.js';

const { quote: q } = assert;
const { ceilDivide } = natSafeMath;
Expand Down Expand Up @@ -388,6 +389,10 @@ test('amm doubleSwap', async t => {

const ammInstance = await amm.instance;

const metricsSub = await E(amm.ammPublicFacet).getMetrics();
const m = await subscriptionTracker(t, metricsSub);
m.assertInitial({ XYK: [] });

const aliceAddLiquidityInvitation = E(
amm.ammPublicFacet,
).makeAddLiquidityInvitation();
Expand All @@ -401,6 +406,8 @@ test('amm doubleSwap', async t => {
'Moola',
);

await m.assertChange({ XYK: { 0: moolaR.brand } });

const moolaLiquidityBrand = await E(moolaLiquidityIssuer).getBrand();
const moolaLiquidity = value => AmountMath.make(moolaLiquidityBrand, value);

Expand All @@ -411,6 +418,8 @@ test('amm doubleSwap', async t => {
const simoleanLiquidity = value =>
AmountMath.make(simoleanLiquidityBrand, value);

await m.assertChange({ XYK: { 1: simoleanR.brand } });

const issuerKeywordRecord = await E(zoe).getIssuers(ammInstance);
t.deepEqual(
issuerKeywordRecord,
Expand Down Expand Up @@ -1015,6 +1024,10 @@ test('amm adding liquidity', async t => {
timer,
);

const metricsSub = await E(amm.ammPublicFacet).getMetrics();
const m = await subscriptionTracker(t, metricsSub);
await m.assertInitial({ XYK: [] });

const addInitialLiquidity = makeAddInitialLiquidity(
t,
zoe,
Expand All @@ -1029,6 +1042,18 @@ test('amm adding liquidity', async t => {
"The pool hasn't been created yet",
);

await m.assertChange({ XYK: { 0: moolaR.brand } });

const poolMetricsSub = await E(amm.ammPublicFacet).getPoolMetrics(
moolaR.brand,
);
const p = await subscriptionTracker(t, poolMetricsSub);
await p.assertInitial({
centralAmount: AmountMath.makeEmpty(centralR.brand),
secondaryAmount: moola(0n),
liquidityTokens: 0n,
});

// add initial liquidity at 10000:50000
const liquidityIssuer = await addInitialLiquidity(10000n, 50000n);
const addLiquidity = makeAddLiquidity(
Expand Down Expand Up @@ -1067,6 +1092,13 @@ test('amm adding liquidity', async t => {
payoutC: 0n,
payoutS: 11n,
};

await p.assertState({
centralAmount: AmountMath.make(centralR.brand, 50_000n),
secondaryAmount: moola(10_000n),
liquidityTokens: 5_0000n,
});

// Add liquidity. Offer 20_000:70_000.
await addLiquidity(20_000n, 70_000n, poolState1, expected1);
// After the trade, this will increase the pool by about 150%
Expand Down Expand Up @@ -1094,6 +1126,13 @@ test('amm adding liquidity', async t => {
payoutC: 0n,
payoutS: 16n,
};

await p.assertState({
centralAmount: AmountMath.make(centralR.brand, 119_996n),
secondaryAmount: moola(29_989n),
liquidityTokens: 134_115n,
});

// Add liquidity. Offer 12_000:100_000.
await addLiquidity(12_000n, 100_000n, poolState2, expected2);

Expand All @@ -1103,4 +1142,10 @@ test('amm adding liquidity', async t => {
allocation(219985n, 0n, 41973n),
`poolAllocation after initialization`,
);

await p.assertState({
centralAmount: AmountMath.make(centralR.brand, 219_985n),
secondaryAmount: moola(41_973n),
liquidityTokens: 214_795n,
});
});
Loading

0 comments on commit 7e5d47b

Please sign in to comment.