Skip to content

Commit

Permalink
docs: improve the README, and add some more typescript
Browse files Browse the repository at this point in the history
  • Loading branch information
Chris-Hibbert committed Sep 23, 2021
1 parent ff01c5a commit 57e8f50
Show file tree
Hide file tree
Showing 4 changed files with 214 additions and 53 deletions.
117 changes: 78 additions & 39 deletions packages/zoe/src/contracts/constantProduct/README.md
Original file line number Diff line number Diff line change
@@ -1,32 +1,33 @@
# 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

* 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.
* 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
Expand All @@ -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

Expand Down
5 changes: 4 additions & 1 deletion packages/zoe/src/contracts/constantProduct/core.js
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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);
Expand Down
61 changes: 61 additions & 0 deletions packages/zoe/src/contracts/constantProduct/internal-types.js
Original file line number Diff line number Diff line change
@@ -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}
*/
84 changes: 71 additions & 13 deletions packages/zoe/src/contracts/constantProduct/swap.js
Original file line number Diff line number Diff line change
@@ -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)) {
Expand All @@ -15,24 +25,61 @@ 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),
poolFee,
);
};

/**
* 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);
}
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);
Expand All @@ -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}`,
Expand All @@ -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);
Expand Down Expand Up @@ -83,6 +141,7 @@ function noTransaction(amountGiven, amountWanted, poolAllocation, poolFee) {
return result;
}

/** @type {InternalSwap} */
export const swap = (
amountGiven,
poolAllocation,
Expand All @@ -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(
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand Down

0 comments on commit 57e8f50

Please sign in to comment.