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

Constant product #3771

Merged
merged 8 commits into from
Sep 30, 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
164 changes: 164 additions & 0 deletions packages/zoe/src/contracts/constantProduct/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
# Constant Product AMM

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. The external
entry point is a call to `pricesForStatedInput()` or `pricesForStatedOutput()`.

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 trade with the pool by
offering 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 balances doesn't decrease. (Rounding is done in favor of the
pool.) A fee is charged on the swap to reward the liquidity providers.

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. (We refer to
Comment on lines +21 to +22
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't understand this sentence. Do you mean, when the two kinds of tokens have very different valuations?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we're saying the same thing. Dollars and Pounds are at the same order of magnitude, but if we were representing them in cents and nano-Pounds, then the smallest units would be very different.

What's a clearer way to say this?

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 side not specified by the user (the
"computed side").
* The protocol fee is always charged in RUN.
* 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
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 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. We'll always refer to the currency
being added as X (regardless of whether it's what they pay or what they receive)
and the currency the user gets as Y. 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. We call these estimates
δ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** | ρ × δY | ρ × **sGet** (= ρ × δY) |
| **BLD in** | **sGive** | calc | ρ × δX | ρ × δ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.

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.

| | input estimate | output estimate |
|---------|-----|-----|
| **RUN in** | **sGive** - ProtocolFee | |
| **RUN out** | | **sGet** + ProtocolFee + PoolFee |
| **BLD in** | **sGive** - ProtocolFee - PoolFee | |
| **BLD out** | | **sGet** + ProtocolFee |

We use the estimate of the amount in or out to calculate improved values of
ΔX and ΔY. These values tell us how much the trader will pay, the
changes in pool balances, and what the trader will receive. As before, ΔX
reflects a balance that will be growing, and ΔY one that will be
shrinking. 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. The amount paid and received by the trader and changes to the
pool are calculated relative to ΔX and ΔY so that the pool grows by
the poolFee and the protocolFee can be paid from the proceeds.

| | xIncr | yDecr | pay In | pay Out |
|---------|-----|-----|-----|-----|
| **RUN in** | ΔX | ΔY - PoolFee | ΔX + protocolFee | ΔY - PoolFee |
| **RUN out** | ΔX | ΔY - PoolFee | ΔX + protocolFee | ΔY - PoolFee |
| **BLD in** | ΔX + PoolFee | ΔY | ΔX + PoolFee + ProtocolFee | ΔY |
| **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
left 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

For example, let's say the pool has 40,000,000 RUN and 3,000,000 BLD. Alice
requests a swapIn with inputAmount of 30,000 RUN, and outputAmount of 2000 BLD.
(SwapIn means the inputValue is the basis of the computation, while outputAmount
is treated as a minimum). To make the numbers concrete, we'll say the pool fee
is 25 Basis Points, and the protocol fee is 5 Basis Points.

The first step is to compute the trade that would take place with no fees. 30K
will be added to 40M RUN. To keep the product just above 120MM, the BLD will be
reduced to 2,997,752.

```
40,030,000 * 2,997,752 > 40,000,000 * 3,000,000 > 40,030,000 * 2,997,751
120000012560000 > 120000000000000 > 119999972530000
```

But we get an even tighter bound by reducing the amount Alice has to spend

```
40,029,996 * 2,997,752 > 40,000,000 * 3,000,000 > 40,029,995 * 2,997,752
120000000568992 > 120000000000000 > 119999997571240
```

The initial price estimate is that 29,996 RUN would get 2248 BLD in a no-fee
pool. We base fees on this estimate, so the **protocol Fee will be 15 RUN**
(always in RUN) and the **pool fee will be 6 BLD**. The pool fee is calculated
on the output for `swapIn` and the input for `swapOut`.

Now we calculate the actual ΔX and ΔY, since the fees affect the
size of the changes to the pool. From the first row of the third table we see
that the calculation starts from ΔX of
`sGive - ProtocolFee (i.e. 30,000 - 15 = 29,985)`

```
40,029,985 * 2,997,7752 > 40,000,000 * 3,000,000 > 40,029,985 * 2,997,753
```

and re-checking how much is required to produce 2,997,753, we get

```
40_029_982 * 2,997,753 > 40,000,000 * 3,000,000 > 40,029,983 * 2,997,753
```

**ΔX is 29,983, and ΔY is 2247**.

* Alice pays ΔX + protocolFee, which is 29,983 + 15 (29998 RUN)
* Alice will receive ΔY - PoolFee which is 2247 - 6 (2241 BLD)
* The RUN in the pool will increase by ΔX (29983 RUN)
* The BLD in the pool will decrease by ΔY (2247 BLD)

The Pool grew by 6 BLD more than was required to maintain the constant product
invariant. 15 RUN were extracted for the protocol fee.

99 changes: 99 additions & 0 deletions packages/zoe/src/contracts/constantProduct/calcFees.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// @ts-check

import { AmountMath } from '@agoric/ertp';
import { ceilMultiplyBy, makeRatio } from '../../contractSupport/ratio.js';

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

const { details: X } = assert;

/**
* Make a ratio given a nat representing basis points
*
* @param {NatValue} feeBP
* @param {Brand} brandOfFee
* @returns {Ratio}
*/
const makeFeeRatio = (feeBP, brandOfFee) => {
return makeRatio(feeBP, brandOfFee, BASIS_POINTS);
};

/** @type {Maximum} */
const maximum = (left, right) => {
// If left is greater or equal, return left. Otherwise return right.
return AmountMath.isGTE(left, right) ? left : right;
};

/** @type {AmountGT} */
const amountGT = (left, right) =>
AmountMath.isGTE(left, right) && !AmountMath.isEqual(left, right);

/**
* Apply the feeRatio to the amount that has a matching brand. This used to
* calculate fees in the single pool case.
*
* @param {{ amountIn: Amount, amountOut: Amount}} amounts - a record with two
* amounts in different brands.
* @param {Ratio} feeRatio
* @returns {Amount}
*/
const calcFee = ({ amountIn, amountOut }, feeRatio) => {
assert(
feeRatio.numerator.brand === feeRatio.denominator.brand,
X`feeRatio numerator and denominator must use the same brand ${feeRatio}`,
);

let sameBrandAmount;
if (amountIn.brand === feeRatio.numerator.brand) {
sameBrandAmount = amountIn;
} else if (amountOut.brand === feeRatio.numerator.brand) {
sameBrandAmount = amountOut;
} else {
assert(
false,
X`feeRatio's brand (${feeRatio.numerator.brand}) must match one of the amounts [${amountIn}, ${amountOut}].`,
);
}

// Always round fees up
const fee = ceilMultiplyBy(sameBrandAmount, feeRatio);

// Fee cannot exceed the amount on which it is levied
assert(
AmountMath.isGTE(sameBrandAmount, fee),
X`The feeRatio can't be greater than 1 ${feeRatio}`,
);

return fee;
};

/**
* Estimate the swap values, then calculate fees. The swapFn provided by the
* caller will be swapInNoFees or swapOutNoFees.
* SwapOut.
*
* @type {CalculateFees}
*/
const calculateFees = (
amountGiven,
poolAllocation,
amountWanted,
protocolFeeRatio,
poolFeeRatio,
swapFn,
) => {
// Get a rough estimation in both brands of the amount to be swapped
const estimation = swapFn({ amountGiven, poolAllocation, amountWanted });

const protocolFee = calcFee(estimation, protocolFeeRatio);
const poolFee = calcFee(estimation, poolFeeRatio);

return harden({ protocolFee, poolFee, ...estimation });
};

harden(amountGT);
harden(maximum);
harden(makeFeeRatio);
harden(calculateFees);

export { amountGT, maximum, makeFeeRatio, calculateFees };
85 changes: 85 additions & 0 deletions packages/zoe/src/contracts/constantProduct/calcSwapPrices.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// @ts-check

import { Far } from '@agoric/marshal';

import { swap } from './swap.js';
import { assertKInvariantSellingX } from './invariants.js';
import { getXY } from './getXY.js';
import { swapInNoFees, swapOutNoFees } from './core.js';

// pricesForStatedOutput() and pricesForStatedInput are the external entrypoints
// to the constantProduct module. The amountWanted is optional for
// pricesForStatedInput and amountgiven is optional for pricesForStatedOutput.

// The two methods call swap, passing in different functions for noFeeSwap.
// pricesForStatedInput uses swapInNoFees, while pricesForStatedOutput uses
// swapOutNoFees. the noFeesSwap functions
const makeCalcSwapPrices = noFeesSwap => {
return Far(
'calcSwapPrices',
(
amountGiven,
poolAllocation,
amountWanted,
protocolFeeRatio,
poolFeeRatio,
) => {
const result = swap(
amountGiven,
poolAllocation,
amountWanted,
protocolFeeRatio,
poolFeeRatio,
noFeesSwap,
);
const { x, y } = getXY({
amountGiven,
poolAllocation,
amountWanted,
});
assertKInvariantSellingX(x, y, result.xIncrement, result.yDecrement);
return result;
},
);
};

/** @type {CalcSwapInPrices} */
const pricesForStatedInput = (
amountGiven,
poolAllocation,
amountWanted,
protocolFeeRatio,
poolFeeRatio,
) => {
const calcSwapPrices = makeCalcSwapPrices(swapInNoFees);
return calcSwapPrices(
amountGiven,
poolAllocation,
amountWanted,
protocolFeeRatio,
poolFeeRatio,
);
};

/** @type {CalcSwapOutPrices} */
const pricesForStatedOutput = (
amountGiven,
poolAllocation,
amountWanted,
protocolFeeRatio,
poolFeeRatio,
) => {
const calcSwapPrices = makeCalcSwapPrices(swapOutNoFees);
return calcSwapPrices(
amountGiven,
poolAllocation,
amountWanted,
protocolFeeRatio,
poolFeeRatio,
);
};

harden(pricesForStatedInput);
harden(pricesForStatedOutput);

export { pricesForStatedOutput, pricesForStatedInput };
Loading