Skip to content

Commit 60a4adf

Browse files
fix: multipoolAutoswap was calculating output prices incorrectly. (#2166)
* fix: multipoolAutoswap was calculating output prices incorrectly. It effectively had the two pools swapped. We didn't notice because all our tests had the two pools at rough parity. While trying to make the staking contract work, I kept geting prices that were 2x the input rather than half when I expected the opposite. The tests were correspondingly broken. The tests called priceFromTargetOutput() to calculate expected values, and consistently had the pools reversed in the call. The correct call is `priceFromTargetOutput(deltaY, yPre, xPre, fee)`. Notice that the first two parameters are both in `Y` units. The calls from the test have been corrected so the second parameter is the pool that matches the requested output amount. In the newly added test, the pool is set up with a ratio of 500 Moola to 1000 Central tokens. When Alice asks the price for 300 Central, the price will now be 155. When she trades back, it takes roughly twice the number of central tokens to purchase the moola she wants. * refactor: Vault uses a priceAuthority to schedule liquidation getPriceGivenAvailableInput and getPriceGivenRequiredOutput added tests renamed userSeat in addLiquidityActual() to zcfSeat. That cost me time! * refactor: update getCurrentPrice update prices that changed in tests. * refactor: reduce four methods in pool for calculating prices to two And use price improvements in multipoolAutoswap * refactor: share & publish price calculations from getCurrentPrice.js * fix: add a test, fix a bug, remove redundant code * docs: add a description of the four methods for getting prices.
1 parent f8c178a commit 60a4adf

File tree

7 files changed

+871
-171
lines changed

7 files changed

+871
-171
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,118 @@
11
import '../../../exported';
22

33
/**
4-
* Build functions to calculate prices for multipoolAutoswap. Two methods are
5-
* returned. In one the caller specifies the amount they will pay, and in the
6-
* other they specify the amount they wish to receive.
4+
* Build functions to calculate prices for multipoolAutoswap. Four methods are
5+
* returned, as two complementary pairs. In one pair of methods the caller
6+
* specifies the amount they will pay, and in the other they specify the amount
7+
* they wish to receive. The two with shorter names (getOutputForGivenInput,
8+
* getInputForGivenOutput) are consistent with the uniswap interface. They each
9+
* return a single amount. The other two return { amountIn, centralAmount,
10+
* amountOut }, which specifies the best exchange consistent with the request.
11+
* centralAmount is omitted from these methods' results in the publicFacet.
712
*
813
* @param {(brand: Brand) => boolean} isSecondary
914
* @param {(brand: Brand) => boolean} isCentral
1015
* @param {(brand: Brand) => Pool} getPool
16+
* @param {Brand} centralBrand
1117
*/
1218

13-
export const makeGetCurrentPrice = (isSecondary, isCentral, getPool) => {
19+
export const makeGetCurrentPrice = (
20+
isSecondary,
21+
isCentral,
22+
getPool,
23+
centralBrand,
24+
) => {
25+
const getPriceGivenAvailableInput = (amountIn, brandOut) => {
26+
const { brand: brandIn } = amountIn;
27+
28+
if (isCentral(brandIn) && isSecondary(brandOut)) {
29+
return getPool(brandOut).getPriceGivenAvailableInput(amountIn, brandOut);
30+
} else if (isSecondary(brandIn) && isCentral(brandOut)) {
31+
return getPool(brandIn).getPriceGivenAvailableInput(amountIn, brandOut);
32+
} else if (isSecondary(brandIn) && isSecondary(brandOut)) {
33+
// We must do two consecutive getPriceGivenAvailableInput() calls,
34+
// followed by a call to getPriceGivenRequiredOutput().
35+
// 1) from amountIn to the central token, which tells us how much central
36+
// would be provided for amountIn (centralAmount)
37+
// 2) from centralAmount to brandOut, which tells us how much of brandOut
38+
// will be provided (amountOut) as well as the minimum price in central
39+
// tokens (reducedCentralAmount), then finally
40+
// 3) call getPriceGivenRequiredOutput() to see if the same proceeds can
41+
// be purchased for less (reducedAmountIn).
42+
43+
const brandInPool = getPool(brandIn);
44+
const brandOutPool = getPool(brandOut);
45+
const {
46+
amountOut: centralAmount,
47+
} = brandInPool.getPriceGivenAvailableInput(amountIn, centralBrand);
48+
const {
49+
amountIn: reducedCentralAmount,
50+
amountOut,
51+
} = brandOutPool.getPriceGivenAvailableInput(centralAmount, brandOut);
52+
53+
// propagate reduced prices back to the first pool
54+
const {
55+
amountIn: reducedAmountIn,
56+
} = brandInPool.getPriceGivenRequiredOutput(
57+
brandIn,
58+
reducedCentralAmount,
59+
);
60+
return {
61+
amountIn: reducedAmountIn,
62+
amountOut,
63+
centralAmount: reducedCentralAmount,
64+
};
65+
}
66+
67+
throw new Error(`brands were not recognized`);
68+
};
69+
70+
const getPriceGivenRequiredOutput = (brandIn, amountOut) => {
71+
const { brand: brandOut } = amountOut;
72+
73+
if (isCentral(brandIn) && isSecondary(brandOut)) {
74+
return getPool(brandOut).getPriceGivenRequiredOutput(brandIn, amountOut);
75+
} else if (isSecondary(brandIn) && isCentral(brandOut)) {
76+
return getPool(brandIn).getPriceGivenRequiredOutput(brandIn, amountOut);
77+
} else if (isSecondary(brandIn) && isSecondary(brandOut)) {
78+
// We must do two consecutive getPriceGivenRequiredOutput() calls,
79+
// followed by a call to getPriceGivenAvailableInput().
80+
// 1) from amountOut to the central token, which tells us how much central
81+
// is required to obtain amountOut (centralAmount)
82+
// 2) from centralAmount to brandIn, which tells us how much of brandIn
83+
// is required (amountIn) as well as the max proceeds in central
84+
// tokens (improvedCentralAmount), then finally
85+
// 3) call getPriceGivenAvailableInput() to see if improvedCentralAmount
86+
// produces a larger amount (improvedAmountOut)
87+
88+
const brandInPool = getPool(brandIn);
89+
const brandOutPool = getPool(brandOut);
90+
91+
const {
92+
amountIn: centralAmount,
93+
} = brandOutPool.getPriceGivenRequiredOutput(centralBrand, amountOut);
94+
const {
95+
amountIn,
96+
amountOut: improvedCentralAmount,
97+
} = brandInPool.getPriceGivenRequiredOutput(brandIn, centralAmount);
98+
99+
// propagate improved prices
100+
const {
101+
amountOut: improvedAmountOut,
102+
} = brandOutPool.getPriceGivenAvailableInput(
103+
improvedCentralAmount,
104+
brandOut,
105+
);
106+
return {
107+
amountIn,
108+
amountOut: improvedAmountOut,
109+
centralAmount: improvedCentralAmount,
110+
};
111+
}
112+
113+
throw new Error(`brands were not recognized`);
114+
};
115+
14116
/**
15117
* `getOutputForGivenInput` calculates the result of a trade, given a certain
16118
* amount of digital assets in.
@@ -21,29 +123,7 @@ export const makeGetCurrentPrice = (isSecondary, isCentral, getPool) => {
21123
* @returns {Amount} the amount that would be paid out at the current price.
22124
*/
23125
const getOutputForGivenInput = (amountIn, brandOut) => {
24-
const { brand: brandIn, value: inputValue } = amountIn;
25-
26-
if (isCentral(brandIn) && isSecondary(brandOut)) {
27-
return getPool(brandOut).getCentralToSecondaryInputPrice(inputValue);
28-
}
29-
30-
if (isSecondary(brandIn) && isCentral(brandOut)) {
31-
return getPool(brandIn).getSecondaryToCentralInputPrice(inputValue);
32-
}
33-
34-
if (isSecondary(brandIn) && isSecondary(brandOut)) {
35-
// We must do two consecutive calls to get the price: from
36-
// the brandIn to the central token, then from the central
37-
// token to the brandOut.
38-
const centralTokenAmount = getPool(
39-
brandIn,
40-
).getSecondaryToCentralInputPrice(inputValue);
41-
return getPool(brandOut).getCentralToSecondaryInputPrice(
42-
centralTokenAmount.value,
43-
);
44-
}
45-
46-
throw new Error(`brands were not recognized`);
126+
return getPriceGivenAvailableInput(amountIn, brandOut).amountOut;
47127
};
48128

49129
/**
@@ -55,30 +135,13 @@ export const makeGetCurrentPrice = (isSecondary, isCentral, getPool) => {
55135
* @returns {Amount} The amount required to be paid in order to gain amountOut
56136
*/
57137
const getInputForGivenOutput = (amountOut, brandIn) => {
58-
const { brand: brandOut, value: outputValue } = amountOut;
59-
60-
if (isCentral(brandIn) && isSecondary(brandOut)) {
61-
return getPool(brandOut).getCentralToSecondaryOutputPrice(outputValue);
62-
}
63-
64-
if (isSecondary(brandIn) && isCentral(brandOut)) {
65-
return getPool(brandIn).getSecondaryToCentralOutputPrice(outputValue);
66-
}
67-
68-
if (isSecondary(brandIn) && isSecondary(brandOut)) {
69-
// We must do two consecutive calls to get the price: from
70-
// the brandIn to the central token, then from the central
71-
// token to the brandOut.
72-
const centralTokenAmount = getPool(
73-
brandIn,
74-
).getSecondaryToCentralOutputPrice(outputValue);
75-
return getPool(brandOut).getCentralToSecondaryOutputPrice(
76-
centralTokenAmount.value,
77-
);
78-
}
79-
80-
throw new Error(`brands were not recognized`);
138+
return getPriceGivenRequiredOutput(brandIn, amountOut).amountIn;
81139
};
82140

83-
return { getOutputForGivenInput, getInputForGivenOutput };
141+
return {
142+
getOutputForGivenInput,
143+
getInputForGivenOutput,
144+
getPriceGivenRequiredOutput,
145+
getPriceGivenAvailableInput,
146+
};
84147
};

packages/zoe/src/contracts/multipoolAutoswap/multipoolAutoswap.js

+32-2
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,39 @@ const start = zcf => {
8080
const {
8181
getOutputForGivenInput,
8282
getInputForGivenOutput,
83-
} = makeGetCurrentPrice(isSecondary, isCentral, getPool);
83+
getPriceGivenAvailableInput: getInternalPriceGivenAvailableInput,
84+
getPriceGivenRequiredOutput: getInternalPriceGivenRequiredOutput,
85+
} = makeGetCurrentPrice(isSecondary, isCentral, getPool, centralBrand);
86+
87+
const getPriceGivenRequiredOutput = (brandIn, amountOutInitial) => {
88+
const { amountIn, amountOut } = getInternalPriceGivenRequiredOutput(
89+
brandIn,
90+
amountOutInitial,
91+
);
92+
// dropping centralAmount from the public method
93+
return { amountIn, amountOut };
94+
};
95+
96+
const getPriceGivenAvailableInput = (amountInInitial, brandOut) => {
97+
const { amountIn, amountOut } = getInternalPriceGivenAvailableInput(
98+
amountInInitial,
99+
brandOut,
100+
);
101+
// dropping centralAmount from the public method
102+
return { amountIn, amountOut };
103+
};
104+
84105
const {
85106
makeSwapInInvitation,
86107
makeSwapOutInvitation,
87-
} = makeMakeSwapInvitation(zcf, isSecondary, isCentral, getPool);
108+
} = makeMakeSwapInvitation(
109+
zcf,
110+
isSecondary,
111+
isCentral,
112+
getPool,
113+
getInternalPriceGivenAvailableInput,
114+
getInternalPriceGivenRequiredOutput,
115+
);
88116
const makeAddLiquidityInvitation = makeMakeAddLiquidityInvitation(
89117
zcf,
90118
getPool,
@@ -103,6 +131,8 @@ const start = zcf => {
103131
getLiquiditySupply,
104132
getInputPrice: getOutputForGivenInput,
105133
getOutputPrice: getInputForGivenOutput,
134+
getPriceGivenRequiredOutput,
135+
getPriceGivenAvailableInput,
106136
makeSwapInvitation: makeSwapInInvitation,
107137
makeSwapInInvitation,
108138
makeSwapOutInvitation,

0 commit comments

Comments
 (0)