Skip to content

Commit

Permalink
refactor!: tests, assertions, docs, renaming
Browse files Browse the repository at this point in the history
update to current master
type declarations
renaming
  • Loading branch information
Chris-Hibbert committed Aug 27, 2021
1 parent 541e993 commit 02b65e3
Show file tree
Hide file tree
Showing 25 changed files with 867 additions and 658 deletions.
1 change: 1 addition & 0 deletions packages/zoe/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"@agoric/install-ses": "^0.5.25",
"ava": "^3.12.1",
"c8": "^7.7.2",
"jsverify": "^0.8.4",
"ses": "^0.14.1"
},
"files": [
Expand Down
97 changes: 97 additions & 0 deletions packages/zoe/src/contracts/constantProduct/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# 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 | ΔX = sGive - PoolFee
| **RUN out** | BLD | RUN | RUN | BLD | sGive | sGets + PrFee | sGive = ΔX + PoolFee
| **BLD in** | BLD | RUN | BLD | RUN | sGive | sGest + PrFee | sGets = ΔY - PoolFee
| **BLD out** | RUN | BLD | RUN | BLD | sGive - PrFee | sGets | sGive = ΔX + PoolFee

(*) The Pool Fee remains in the pool, so its impact on the calculation is
subtle.

* 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)
19 changes: 14 additions & 5 deletions packages/zoe/src/contracts/constantProduct/calcFees.js
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -12,7 +12,7 @@ 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);
};

Expand All @@ -21,6 +21,13 @@ const minimum = (left, right) => {
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(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.
Expand All @@ -31,9 +38,11 @@ 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);
};

Expand Down
4 changes: 2 additions & 2 deletions packages/zoe/src/contracts/constantProduct/checkInvariants.js
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
107 changes: 62 additions & 45 deletions packages/zoe/src/contracts/constantProduct/core.js
Original file line number Diff line number Diff line change
@@ -1,36 +1,42 @@
// @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,
);
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,
Expand All @@ -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),
});
};

Expand Down
5 changes: 2 additions & 3 deletions packages/zoe/src/contracts/constantProduct/getXY.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -20,7 +19,7 @@ export const getXY = ({ amountGiven, poolAllocation, amountWanted }) => {

const deltas = {
deltaX: amountGiven,
wantedDeltaY: amountWanted,
deltaY: amountWanted,
};

if (secondaryBrand === xBrand || centralBrand === yBrand) {
Expand Down
6 changes: 3 additions & 3 deletions packages/zoe/src/contracts/constantProduct/invariants.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading

0 comments on commit 02b65e3

Please sign in to comment.