-
Notifications
You must be signed in to change notification settings - Fork 217
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Constant Product calculations for an AMM. (ref: #3791) type declarations Allow swapOut() to have an empty amountIn representing "no restriction" Drop enforcement in swapIn/swapOut that the quotes are in the forward direction doublePool will need to call them in the reverse direction extract common code in swapIn & swapOut drop reporting on price improvements reduce duplicated code in swap.js Co-authored-by: Kate Sills <[email protected]>
- Loading branch information
1 parent
4c56367
commit fc84ee0
Showing
18 changed files
with
2,376 additions
and
0 deletions.
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.