-
Notifications
You must be signed in to change notification settings - Fork 139
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
FxPool rounding errors fixes #393
Changes from 32 commits
bf44477
37b6ef3
e9f50e1
c1f880b
1ab80f6
a44635b
f0b3516
519d418
29f16c1
7b753bd
fdeb41b
7a79166
c1f4c35
91a3076
b9d6c90
6e35042
4e736d8
ba76227
559e0bd
820bf11
adc7836
c33c58c
e464e24
19ad8be
49094af
09253c6
a965343
c578d17
4d98aa8
2576c4d
c2590b0
af3eb5c
61a9774
f62a047
9c82d4b
60071d9
090466f
5e537cc
2f8f87b
94b2616
71ab58b
ac20365
a3d7557
2673734
83cc506
b90f725
4635ed9
e5b7a8d
3471d7d
edc0b95
2350377
1e0d545
2cf04c2
1bbc298
3d347f5
7191c7b
6a15ae4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,8 @@ import { getAddress } from '@ethersproject/address'; | |
import { BigNumber, formatFixed, parseFixed } from '@ethersproject/bignumber'; | ||
import { Zero } from '@ethersproject/constants'; | ||
import { BigNumber as OldBigNumber, ZERO, bnum } from '../../utils/bignumber'; | ||
|
||
import { parseFixedCurveParam } from './parseFixedCurveParam'; | ||
import { isSameAddress } from '../../utils'; | ||
import { universalNormalizedLiquidity } from '../liquidity'; | ||
import { | ||
|
@@ -29,13 +31,15 @@ type FxPoolToken = Pick< | |
>; | ||
|
||
export type FxPoolPairData = PoolPairBase & { | ||
alpha: BigNumber; | ||
beta: BigNumber; | ||
lambda: BigNumber; | ||
delta: BigNumber; | ||
epsilon: BigNumber; | ||
alpha: OldBigNumber; | ||
beta: OldBigNumber; | ||
lambda: OldBigNumber; | ||
delta: OldBigNumber; | ||
epsilon: OldBigNumber; | ||
tokenInLatestFXPrice: OldBigNumber; | ||
tokenInfxOracleDecimals: OldBigNumber; | ||
tokenOutLatestFXPrice: OldBigNumber; | ||
tokenOutfxOracleDecimals: OldBigNumber; | ||
}; | ||
|
||
export class FxPool implements PoolBase<FxPoolPairData> { | ||
|
@@ -46,11 +50,11 @@ export class FxPool implements PoolBase<FxPoolPairData> { | |
totalShares: BigNumber; | ||
tokens: FxPoolToken[]; | ||
tokensList: string[]; | ||
alpha: BigNumber; | ||
beta: BigNumber; | ||
lambda: BigNumber; | ||
delta: BigNumber; | ||
epsilon: BigNumber; | ||
alpha: OldBigNumber; | ||
beta: OldBigNumber; | ||
lambda: OldBigNumber; | ||
delta: OldBigNumber; | ||
epsilon: OldBigNumber; | ||
|
||
static fromPool(pool: SubgraphPoolBase): FxPool { | ||
if ( | ||
|
@@ -95,11 +99,11 @@ export class FxPool implements PoolBase<FxPoolPairData> { | |
this.totalShares = parseFixed(totalShares, 18); | ||
this.tokens = tokens; | ||
this.tokensList = tokensList; | ||
this.alpha = parseFixed(alpha, 18); | ||
this.beta = parseFixed(beta, 18); | ||
this.lambda = parseFixed(lambda, 18); | ||
this.delta = parseFixed(delta, 18); | ||
this.epsilon = parseFixed(epsilon, 18); | ||
this.alpha = parseFixedCurveParam(alpha); | ||
this.beta = parseFixedCurveParam(beta); | ||
this.lambda = parseFixedCurveParam(lambda); | ||
this.delta = parseFixedCurveParam(delta); | ||
this.epsilon = parseFixedCurveParam(epsilon); | ||
} | ||
updateTotalShares: (newTotalShares: BigNumber) => void; | ||
mainIndex?: number | undefined; | ||
|
@@ -137,6 +141,8 @@ export class FxPool implements PoolBase<FxPoolPairData> { | |
|
||
if (!tO.token?.latestFXPrice || !tI.token?.latestFXPrice) | ||
throw 'FX Pool Missing LatestFxPrice'; | ||
if (!tO.token?.fxOracleDecimals || !tI.token?.fxOracleDecimals) | ||
throw 'FX Pool Missing tokenIn or tokenOut fxOracleDecimals'; | ||
|
||
const poolPairData: FxPoolPairData = { | ||
id: this.id, | ||
|
@@ -154,8 +160,20 @@ export class FxPool implements PoolBase<FxPoolPairData> { | |
lambda: this.lambda, | ||
delta: this.delta, | ||
epsilon: this.epsilon, | ||
tokenInLatestFXPrice: bnum(tI.token.latestFXPrice), // decimals is formatted from subgraph in rate we get from the chainlink oracle | ||
tokenOutLatestFXPrice: bnum(tO.token.latestFXPrice), // decimals is formatted from subgraph in rate we get from the chainlink oracle | ||
tokenInLatestFXPrice: bnum( | ||
parseFixed( | ||
tI.token.latestFXPrice, | ||
tI.token.fxOracleDecimals | ||
).toString() | ||
), // decimals is formatted from subgraph in rate we get from the chainlink oracle | ||
tokenOutLatestFXPrice: bnum( | ||
parseFixed( | ||
tO.token.latestFXPrice, | ||
tO.token.fxOracleDecimals | ||
).toString() | ||
), // decimals is formatted from subgraph in rate we get from the chainlink oracle | ||
tokenInfxOracleDecimals: bnum(tI.token.fxOracleDecimals), | ||
tokenOutfxOracleDecimals: bnum(tO.token.fxOracleDecimals), | ||
}; | ||
|
||
return poolPairData; | ||
|
@@ -181,34 +199,50 @@ export class FxPool implements PoolBase<FxPoolPairData> { | |
getLimitAmountSwap( | ||
poolPairData: FxPoolPairData, | ||
swapType: SwapTypes | ||
): OldBigNumber { | ||
return this._inHigherPrecision( | ||
this._getLimitAmountSwap, | ||
poolPairData, | ||
swapType | ||
); | ||
} | ||
|
||
_getLimitAmountSwap( | ||
poolPairData: FxPoolPairData, | ||
swapType: SwapTypes | ||
): OldBigNumber { | ||
try { | ||
const parsedReserves = poolBalancesToNumeraire(poolPairData); | ||
|
||
const alphaValue = Number(formatFixed(poolPairData.alpha, 18)); | ||
const alphaValue = poolPairData.alpha.div(bnum(10).pow(18)); | ||
|
||
const maxLimit = (1 + alphaValue) * parsedReserves._oGLiq * 0.5; | ||
const maxLimit = alphaValue | ||
.plus(1) | ||
.times(parsedReserves._oGLiq) | ||
.times(0.5); | ||
|
||
if (swapType === SwapTypes.SwapExactIn) { | ||
const maxLimitAmount = | ||
maxLimit - parsedReserves.tokenInReservesInNumeraire; | ||
|
||
return bnum( | ||
viewRawAmount( | ||
maxLimitAmount, | ||
poolPairData.tokenInLatestFXPrice.toNumber() | ||
).toString() | ||
const maxLimitAmount = maxLimit.minus( | ||
parsedReserves.tokenInReservesInNumeraire | ||
); | ||
} else { | ||
const maxLimitAmount = | ||
maxLimit - parsedReserves.tokenOutReservesInNumeraire; | ||
|
||
return bnum( | ||
viewRawAmount( | ||
maxLimitAmount, | ||
poolPairData.tokenOutLatestFXPrice.toNumber() | ||
).toString() | ||
return viewRawAmount( | ||
maxLimitAmount, | ||
bnum(poolPairData.decimalsIn), | ||
poolPairData.tokenInLatestFXPrice, | ||
poolPairData.tokenInfxOracleDecimals | ||
).div(bnum(10).pow(poolPairData.decimalsIn)); | ||
} else { | ||
const maxLimitAmount = maxLimit.minus( | ||
parsedReserves.tokenOutReservesInNumeraire | ||
); | ||
|
||
return viewRawAmount( | ||
maxLimitAmount, | ||
bnum(poolPairData.decimalsOut), | ||
poolPairData.tokenOutLatestFXPrice, | ||
poolPairData.tokenOutfxOracleDecimals | ||
).div(bnum(10).pow(poolPairData.decimalsOut)); | ||
} | ||
} catch { | ||
return ZERO; | ||
|
@@ -233,8 +267,12 @@ export class FxPool implements PoolBase<FxPoolPairData> { | |
amount: OldBigNumber | ||
): OldBigNumber { | ||
try { | ||
return _exactTokenInForTokenOut(amount, poolPairData); | ||
} catch { | ||
return this._inHigherPrecision( | ||
_exactTokenInForTokenOut, | ||
amount, | ||
poolPairData | ||
); | ||
Comment on lines
+280
to
+284
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Now that you're using fixed point math with 36 decimals on fxPoolMath, you should be able to remove this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It seems this one might have to wait until we have SOR converted to fixed-point math as well. For example, in |
||
} catch (e) { | ||
return ZERO; | ||
} | ||
} | ||
|
@@ -244,7 +282,11 @@ export class FxPool implements PoolBase<FxPoolPairData> { | |
amount: OldBigNumber | ||
): OldBigNumber { | ||
try { | ||
return _tokenInForExactTokenOut(amount, poolPairData); | ||
return this._inHigherPrecision( | ||
_tokenInForExactTokenOut, | ||
amount, | ||
poolPairData | ||
); | ||
} catch { | ||
return ZERO; | ||
} | ||
|
@@ -255,7 +297,8 @@ export class FxPool implements PoolBase<FxPoolPairData> { | |
amount: OldBigNumber | ||
): OldBigNumber { | ||
try { | ||
return _spotPriceAfterSwapExactTokenInForTokenOut( | ||
return this._inHigherPrecision( | ||
_spotPriceAfterSwapExactTokenInForTokenOut, | ||
poolPairData, | ||
amount | ||
); | ||
|
@@ -269,7 +312,8 @@ export class FxPool implements PoolBase<FxPoolPairData> { | |
amount: OldBigNumber | ||
): OldBigNumber { | ||
try { | ||
return _spotPriceAfterSwapTokenInForExactTokenOut( | ||
return this._inHigherPrecision( | ||
_spotPriceAfterSwapTokenInForExactTokenOut, | ||
poolPairData, | ||
amount | ||
); | ||
|
@@ -283,7 +327,8 @@ export class FxPool implements PoolBase<FxPoolPairData> { | |
amount: OldBigNumber | ||
): OldBigNumber { | ||
try { | ||
return _derivativeSpotPriceAfterSwapExactTokenInForTokenOut( | ||
return this._inHigherPrecision( | ||
_derivativeSpotPriceAfterSwapExactTokenInForTokenOut, | ||
amount, | ||
poolPairData | ||
); | ||
|
@@ -297,12 +342,46 @@ export class FxPool implements PoolBase<FxPoolPairData> { | |
amount: OldBigNumber | ||
): OldBigNumber { | ||
try { | ||
return _derivativeSpotPriceAfterSwapTokenInForExactTokenOut( | ||
return this._inHigherPrecision( | ||
_derivativeSpotPriceAfterSwapTokenInForExactTokenOut, | ||
amount, | ||
poolPairData | ||
); | ||
} catch { | ||
return ZERO; | ||
} | ||
} | ||
|
||
/** | ||
* Runs the given function with the BigNumber config set to 36 decimals. | ||
* This is needed since in the Solidity code we use 64.64 fixed point numbers | ||
* for the curve math operations. This makes the SOR default of 18 decimals | ||
* not enough. | ||
* | ||
* @param funcName | ||
* @param args | ||
* @returns | ||
*/ | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types | ||
_inHigherPrecision(funcName: Function, ...args: any[]): OldBigNumber { | ||
const prevDecimalPlaces = OldBigNumber.config({}).DECIMAL_PLACES; | ||
OldBigNumber.config({ | ||
DECIMAL_PLACES: 36, | ||
}); | ||
|
||
try { | ||
const val = funcName.apply(this, args); | ||
OldBigNumber.config({ | ||
DECIMAL_PLACES: prevDecimalPlaces, | ||
}); | ||
return val; | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
} catch (err: any) { | ||
// restore the original BigNumber config even in case of an exception | ||
OldBigNumber.config({ | ||
DECIMAL_PLACES: prevDecimalPlaces, | ||
}); | ||
throw err; | ||
} | ||
} | ||
} |
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.
Hey @andreiashu 👋
I'm reviewing the PR over here and I realized you're updating from
number
tobignumber.js
, but still usingfloat points
.Do you think it's possible to move to ethers
BigNumber
orbigint
and update the code to usefixed point
math?I ask that because that's what we're going to use on our new SOR version to keep consistency.
I know this would require a bigger change, but it would help a lot to migrate things later on.
Let me know if that makes sense or if you have any questions!
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.
hi @brunoguerios, I appreciate you looking over this PR!
I just pushed a commit whereby we now pass the floats as strings to
OldBigNumber
.re moving to ethers
BigNumber
orbigint
: you mean that, ideally, we shouldn't be usingOldBigNumber
anymore and just stick with EthersBigNumber
?re
fixed point math
: can you give me an example as to how you're approaching this in the new version of SOR? We're using ABDKMath64x64.sol in our contracts, and 36 decimals seemed to be enough precision on the SOR side for us (see _inHigherPrecision and parseFixedCurveParam ). We have several places where we round (down) to a specific number of decimals depending on the number of decimals that is expected (see viewRawAmount and viewNumeraireAmount). This part was somewhat more challenging to fix in the SOR code. In our smart contracts, converting from wei to fixed point 64x64 happens several times throughout the lifecycle of a swap. Each conversion operation has its own precision loss that ultimately was compounding to higher values of loss in total in our SOR code.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.
Sure thing! Thanks for getting back to me with more context! 😊
Yes, moving from
OldBigNumber
to either ethersBigNumber
orbigint
would be the ideal solution, because it forces you to usefixed point
instead offloat point
.This is how that
maxLimit
calculation would look like in fixed point math:Of course you'd already have all variables as BigNumber and those conversions wouldn't be necessary, but I added so it's easier to follow along what each of them represents.
You can also look at how other pools are handling that within the SOR.
Gyro pool for example uses 38 decimals fixed point math, which can be found on
gyroSignedFixedPoint.ts
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.
hi @brunoguerios I just pushed several commits that start to address this change.
still work in progress but wanted to get your feedback to make sure we're going in the right direction. See
calculateMicroFee
which I moved toBigNumber
. Let me know what you thinkThere 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.
ps
calculateMicroFee
is insrc/pools/xaveFxPool/fxPoolMath.ts
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.
Hey @andreiashu 👋
Thanks for sharing your progress so I can provide feedback early 😊
Yeah, based on
calculateMicroFee
, you're definitely going in the right direction 👍Thanks for taking the time and applying these changes!
They will help a lot to include FX pools on the new version of the SDK 😃
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.
hi @brunoguerios we got rid of
OldBigNumber
pretty much across our codebase. The functions that are called by the SOR (_exactTokenInForTokenOut
,_tokenInForExactTokenOut
,_spotPriceAfterSwapExactTokenInForTokenOut
and the_derivativeSpotPriceAfter[...]
) are still converting back toOldBigNumber
as expected.Let me know what you think