-
Notifications
You must be signed in to change notification settings - Fork 212
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
Constant product #3771
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
59b761d
chore: make code accessible
katelynsills 968223e
chore: add old tests
katelynsills c213e7c
refactor!: tests, assertions, docs, renaming
Chris-Hibbert 2321dcf
docs: improve the README, and add some more typescript
Chris-Hibbert efcb009
chore: cleanup constantProduct calculations
Chris-Hibbert 8a04bfb
docs: improve types and documentation
Chris-Hibbert ec3fdc4
docs: clarify and correct the README; add tests
Chris-Hibbert cf3f73a
chore: more tests, improved comments, major rename
Chris-Hibbert File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
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. | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
85
packages/zoe/src/contracts/constantProduct/calcSwapPrices.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?