Commit 3249027e authored by Noah Zinsmeister's avatar Noah Zinsmeister

address edge cases around decimals and rounding

parent a56aa056
...@@ -36,18 +36,13 @@ function testMarketRates( ...@@ -36,18 +36,13 @@ function testMarketRates(
}) })
describe('manually inverted', (): void => { describe('manually inverted', (): void => {
const tradeTypeInverted = const tradeTypeInverted = tradeType === TRADE_TYPE.ETH_TO_TOKEN ? TRADE_TYPE.TOKEN_TO_ETH : TRADE_TYPE.ETH_TO_TOKEN
tradeType === TRADE_TYPE.TOKEN_TO_TOKEN
? TRADE_TYPE.TOKEN_TO_TOKEN
: tradeType === TRADE_TYPE.ETH_TO_TOKEN
? TRADE_TYPE.TOKEN_TO_ETH
: TRADE_TYPE.ETH_TO_TOKEN
test('not inverted', (): void => { test('not inverted', (): void => {
const manuallyInvertedMarketRate: BigNumber = getMarketRate( const manuallyInvertedMarketRate: BigNumber = getMarketRate(
outputTokenReserves, outputTokenReserves,
inputTokenReserves, inputTokenReserves,
tradeTypeInverted, tradeType === TRADE_TYPE.TOKEN_TO_TOKEN ? TRADE_TYPE.TOKEN_TO_TOKEN : tradeTypeInverted,
false false
) )
...@@ -58,7 +53,7 @@ function testMarketRates( ...@@ -58,7 +53,7 @@ function testMarketRates(
const manuallyInvertedInvertedMarketRate: BigNumber = getMarketRate( const manuallyInvertedInvertedMarketRate: BigNumber = getMarketRate(
outputTokenReserves, outputTokenReserves,
inputTokenReserves, inputTokenReserves,
tradeTypeInverted, tradeType === TRADE_TYPE.TOKEN_TO_TOKEN ? TRADE_TYPE.TOKEN_TO_TOKEN : tradeTypeInverted,
true true
) )
...@@ -74,11 +69,10 @@ describe('getMarketRate', (): void => { ...@@ -74,11 +69,10 @@ describe('getMarketRate', (): void => {
'4039700561005906883487', '4039700561005906883487',
'1094055210563660633471343' '1094055210563660633471343'
) )
const expectedMarketRate = '0.003692410147130181' const expectedMarketRate = '270.825818409480102284'
const expectedMarketRateInverted = '270.825818409480102284' const expectedMarketRateInverted = '0.003692410147130181'
testMarketRates(null, tokenReserves, TRADE_TYPE.ETH_TO_TOKEN, expectedMarketRate, expectedMarketRateInverted) testMarketRates(null, tokenReserves, TRADE_TYPE.ETH_TO_TOKEN, expectedMarketRate, expectedMarketRateInverted)
testMarketRates(tokenReserves, null, TRADE_TYPE.TOKEN_TO_ETH, expectedMarketRateInverted, expectedMarketRate)
}) })
describe('dummy ETH/USDC and USDC/ETH', (): void => { describe('dummy ETH/USDC and USDC/ETH', (): void => {
...@@ -86,8 +80,7 @@ describe('getMarketRate', (): void => { ...@@ -86,8 +80,7 @@ describe('getMarketRate', (): void => {
const expectedMarketRate = '0.003678674143683891' const expectedMarketRate = '0.003678674143683891'
const expectedMarketRateInverted = '271.837069808684359442' const expectedMarketRateInverted = '271.837069808684359442'
testMarketRates(null, tokenReserves, TRADE_TYPE.ETH_TO_TOKEN, expectedMarketRate, expectedMarketRateInverted) testMarketRates(tokenReserves, null, TRADE_TYPE.TOKEN_TO_ETH, expectedMarketRate, expectedMarketRateInverted)
testMarketRates(tokenReserves, null, TRADE_TYPE.TOKEN_TO_ETH, expectedMarketRateInverted, expectedMarketRate)
}) })
describe('dummy DAI/USDC and USDC/DAI', (): void => { describe('dummy DAI/USDC and USDC/DAI', (): void => {
...@@ -97,8 +90,8 @@ describe('getMarketRate', (): void => { ...@@ -97,8 +90,8 @@ describe('getMarketRate', (): void => {
'1094055210563660633471343' '1094055210563660633471343'
) )
const USDCTokenReserves: TokenReserves = constructTokenReserves(6, '1076592291503763426634', '292657693901') const USDCTokenReserves: TokenReserves = constructTokenReserves(6, '1076592291503763426634', '292657693901')
const expectedMarketRate = '0.996279935624983178' const expectedMarketRate = '1.003733954927721392'
const expectedMarketRateInverted = '1.003733954927721499' const expectedMarketRateInverted = '0.996279935624983143'
testMarketRates( testMarketRates(
DAITokenReserves, DAITokenReserves,
...@@ -107,13 +100,5 @@ describe('getMarketRate', (): void => { ...@@ -107,13 +100,5 @@ describe('getMarketRate', (): void => {
expectedMarketRate, expectedMarketRate,
expectedMarketRateInverted expectedMarketRateInverted
) )
testMarketRates(
USDCTokenReserves,
DAITokenReserves,
TRADE_TYPE.TOKEN_TO_TOKEN,
expectedMarketRateInverted,
expectedMarketRate
)
}) })
}) })
...@@ -37,14 +37,19 @@ export function getOutputPrice(outputAmount: BigNumber, inputReserve: BigNumber, ...@@ -37,14 +37,19 @@ export function getOutputPrice(outputAmount: BigNumber, inputReserve: BigNumber,
return inputAmount return inputAmount
} }
function _getMarketRate(tokenReserves: TokenReservesOptional, tradeType: TRADE_TYPE, invert: boolean): BigNumber { // returns [numerator, decimal scalar, denominator]
function getRawMarketRate(
tokenReserves: TokenReservesOptional,
tradeType: TRADE_TYPE,
invert: boolean
): [BigNumber, BigNumber, BigNumber] {
if (tokenReserves === null) { if (tokenReserves === null) {
throw Error('outputTokenReserves must be non-null.') throw Error('outputTokenReserves must be non-null.')
} else { } else {
const numerator: TokenAmount = const numerator: TokenAmount =
tradeType === TRADE_TYPE.ETH_TO_TOKEN ? tokenReserves.ethReserve : tokenReserves.tokenReserve
const denominator: TokenAmount =
tradeType === TRADE_TYPE.ETH_TO_TOKEN ? tokenReserves.tokenReserve : tokenReserves.ethReserve tradeType === TRADE_TYPE.ETH_TO_TOKEN ? tokenReserves.tokenReserve : tokenReserves.ethReserve
const denominator: TokenAmount =
tradeType === TRADE_TYPE.ETH_TO_TOKEN ? tokenReserves.ethReserve : tokenReserves.tokenReserve
const numeratorAmount: BigNumber = normalizeBigNumberish(numerator.amount) const numeratorAmount: BigNumber = normalizeBigNumberish(numerator.amount)
const denominatorAmount: BigNumber = normalizeBigNumberish(denominator.amount) const denominatorAmount: BigNumber = normalizeBigNumberish(denominator.amount)
...@@ -55,15 +60,29 @@ function _getMarketRate(tokenReserves: TokenReservesOptional, tradeType: TRADE_T ...@@ -55,15 +60,29 @@ function _getMarketRate(tokenReserves: TokenReservesOptional, tradeType: TRADE_T
ensureAllUInt8([numeratorDecimals, denominatorDecimals]) ensureAllUInt8([numeratorDecimals, denominatorDecimals])
if (!invert) { if (!invert) {
const decimalScalar = _10.exponentiatedBy(denominatorDecimals - numeratorDecimals) const decimalScalar: BigNumber = _10.exponentiatedBy(denominatorDecimals - numeratorDecimals)
return numeratorAmount.multipliedBy(decimalScalar).div(denominatorAmount) return [numeratorAmount, decimalScalar, denominatorAmount]
} else { } else {
const decimalScalar = _10.exponentiatedBy(numeratorDecimals - denominatorDecimals) const decimalScalar: BigNumber = _10.exponentiatedBy(numeratorDecimals - denominatorDecimals)
return denominatorAmount.multipliedBy(decimalScalar).div(numeratorAmount) return [denominatorAmount, decimalScalar, numeratorAmount]
} }
} }
} }
function getRawMarketRateOneSided(
tokenReserves: TokenReservesOptional,
tradeType: TRADE_TYPE,
invert: boolean
): BigNumber {
const [numerator, decimalScalar, denominator]: [BigNumber, BigNumber, BigNumber] = getRawMarketRate(
tokenReserves,
tradeType,
invert
)
return numerator.multipliedBy(decimalScalar).dividedBy(denominator)
}
// rounds output rates to 18 decimal places
export function getMarketRate( export function getMarketRate(
inputTokenReserves: TokenReservesOptional, inputTokenReserves: TokenReservesOptional,
outputTokenReserves: TokenReservesOptional, outputTokenReserves: TokenReservesOptional,
...@@ -74,12 +93,26 @@ export function getMarketRate( ...@@ -74,12 +93,26 @@ export function getMarketRate(
if (inputTokenReserves === null || outputTokenReserves === null) { if (inputTokenReserves === null || outputTokenReserves === null) {
throw Error('Both inputTokenReserves and outputTokenReserves must be non-null.') throw Error('Both inputTokenReserves and outputTokenReserves must be non-null.')
} else { } else {
const inputMarketRate: BigNumber = _getMarketRate(inputTokenReserves, TRADE_TYPE.TOKEN_TO_ETH, invert) const [inputNumerator, inputDecimalScalar, inputDenominator]: [
const outputMarketRate: BigNumber = _getMarketRate(outputTokenReserves, TRADE_TYPE.ETH_TO_TOKEN, invert) BigNumber,
return inputMarketRate.multipliedBy(outputMarketRate) BigNumber,
BigNumber
] = getRawMarketRate(inputTokenReserves, TRADE_TYPE.TOKEN_TO_ETH, invert)
const [outputNumerator, outputDecimalScalar, outputDenominator]: [
BigNumber,
BigNumber,
BigNumber
] = getRawMarketRate(outputTokenReserves, TRADE_TYPE.ETH_TO_TOKEN, invert)
return inputNumerator
.multipliedBy(inputDecimalScalar)
.multipliedBy(outputNumerator)
.multipliedBy(outputDecimalScalar)
.dividedBy(inputDenominator.multipliedBy(outputDenominator))
} }
} else { } else {
return _getMarketRate( return getRawMarketRateOneSided(
tradeType === TRADE_TYPE.ETH_TO_TOKEN ? outputTokenReserves : inputTokenReserves, tradeType === TRADE_TYPE.ETH_TO_TOKEN ? outputTokenReserves : inputTokenReserves,
tradeType, tradeType,
invert invert
......
...@@ -34,8 +34,7 @@ export enum TRADE_EXACT { ...@@ -34,8 +34,7 @@ export enum TRADE_EXACT {
export enum FIXED_UNDERFLOW_BEHAVIOR { export enum FIXED_UNDERFLOW_BEHAVIOR {
ZERO = 'ZERO', ZERO = 'ZERO',
LESS_THAN = 'LESS_THAN', LESS_THAN = 'LESS_THAN',
ONE_DIGIT = 'ONE_DIGIT', ONE_DIGIT = 'ONE_DIGIT'
MAX_DECIMAL_PLACES = 'MAX_DECIMAL_PLACES'
} }
//// constants for internal use //// constants for internal use
......
...@@ -2,7 +2,7 @@ import { ethers } from 'ethers' ...@@ -2,7 +2,7 @@ import { ethers } from 'ethers'
import { ETH, CHAIN_ID_NAME, ERC20_ABI, FACTORY_ADDRESS, FACTORY_ABI } from './constants' import { ETH, CHAIN_ID_NAME, ERC20_ABI, FACTORY_ADDRESS, FACTORY_ABI } from './constants'
import { ChainIdOrProvider, ChainIdAndProvider, Token, TokenReserves } from './types' import { ChainIdOrProvider, ChainIdAndProvider, Token, TokenReserves } from './types'
import { normalizeAddress } from './utils' import { normalizeAddress, normalizeBigNumberish } from './utils'
// abstraction to get contracts // abstraction to get contracts
function _getContract(address: string, ABI: string, provider: ethers.providers.BaseProvider): ethers.Contract { function _getContract(address: string, ABI: string, provider: ethers.providers.BaseProvider): ethers.Contract {
...@@ -80,8 +80,8 @@ export async function getTokenReserves( ...@@ -80,8 +80,8 @@ export async function getTokenReserves(
ethers.utils.BigNumber ethers.utils.BigNumber
] = await Promise.all([ethTokenPromise, tokenPromise, exchangeTokenPromise, ethBalancePromise, tokenBalancePromise]) ] = await Promise.all([ethTokenPromise, tokenPromise, exchangeTokenPromise, ethBalancePromise, tokenBalancePromise])
const ethReserve = { token: ethToken, amount: ethBalance } const ethReserve = { token: ethToken, amount: normalizeBigNumberish(ethBalance) }
const tokenReserve = { token, amount: tokenBalance } const tokenReserve = { token, amount: normalizeBigNumberish(tokenBalance) }
return { token, exchange: exchangeToken, ethReserve, tokenReserve } return { token, exchange: exchangeToken, ethReserve, tokenReserve }
} }
import BigNumber from 'bignumber.js' import BigNumber from 'bignumber.js'
import { MAX_DECIMAL_PLACES, ROUNDING_MODE, FIXED_UNDERFLOW_BEHAVIOR, _0 } from './constants' import { MAX_DECIMAL_PLACES, ROUNDING_MODE, FIXED_UNDERFLOW_BEHAVIOR, _0, _10 } from './constants'
import { BigNumberish, FlexibleFormat, FormatSignificantOptions, FormatFixedOptions } from './types' import { BigNumberish, FlexibleFormat, FormatSignificantOptions, FormatFixedOptions } from './types'
import { normalizeBigNumberish, ensureBoundedInteger } from './utils' import { normalizeBigNumberish, ensureBoundedInteger, ensureAllUInt256, ensureAllUInt8 } from './utils'
function _format(bigNumber: BigNumber, format: FlexibleFormat, decimalPlaces: number): string { function _format(
bigNumber: BigNumber,
decimalPlaces: number,
roundingMode: BigNumber.RoundingMode = ROUNDING_MODE,
format: FlexibleFormat
): string {
return typeof format === 'boolean' && format === false return typeof format === 'boolean' && format === false
? bigNumber.toFixed(decimalPlaces) ? bigNumber.toFixed(decimalPlaces, roundingMode)
: bigNumber.toFormat( : bigNumber.toFormat(
decimalPlaces, decimalPlaces,
ROUNDING_MODE, roundingMode,
typeof format === 'boolean' && format === true ? undefined : format typeof format === 'boolean' && format === true ? undefined : format
) )
} }
// bignumberish is converted to significantDigits, then cast back as a bignumber and formatted, dropping trailing 0s // bignumberish is converted to significantDigits, then cast back as a bignumber and formatted, dropping trailing 0s
export function formatSignificant( export function formatSignificant(bigNumberish: BigNumberish, options?: FormatSignificantOptions): string {
bigNumberish: BigNumberish, const { significantDigits = 6, forceIntegerSignificance = true, roundingMode = ROUNDING_MODE, format = false } =
{ significantDigits = 6, forceIntegerSignificance = false, format = false }: FormatSignificantOptions options || {}
): string {
const bigNumber: BigNumber = normalizeBigNumberish(bigNumberish) const bigNumber: BigNumber = normalizeBigNumberish(bigNumberish)
ensureBoundedInteger(significantDigits, [1, MAX_DECIMAL_PLACES]) ensureBoundedInteger(significantDigits, [1, MAX_DECIMAL_PLACES])
...@@ -27,38 +32,33 @@ export function formatSignificant( ...@@ -27,38 +32,33 @@ export function formatSignificant(
bigNumber.toPrecision(Math.max(minimumSignificantDigits, significantDigits)) bigNumber.toPrecision(Math.max(minimumSignificantDigits, significantDigits))
) )
return _format(preciseBigNumber, format, preciseBigNumber.decimalPlaces()) return _format(preciseBigNumber, preciseBigNumber.decimalPlaces(), roundingMode, format)
} }
export function formatFixed( export function formatFixed(bigNumberish: BigNumberish, options?: FormatFixedOptions): string {
bigNumberish: BigNumberish, const {
{
decimalPlaces = 4, decimalPlaces = 4,
roundingMode = ROUNDING_MODE,
dropTrailingZeros = true, dropTrailingZeros = true,
format = false, underflowBehavior = FIXED_UNDERFLOW_BEHAVIOR.ONE_DIGIT,
underflowBehavior = FIXED_UNDERFLOW_BEHAVIOR.ONE_DIGIT format = false
}: FormatFixedOptions } = options || {}
): string {
const bigNumber: BigNumber = normalizeBigNumberish(bigNumberish) const bigNumber: BigNumber = normalizeBigNumberish(bigNumberish)
ensureBoundedInteger(decimalPlaces, MAX_DECIMAL_PLACES) ensureBoundedInteger(decimalPlaces, MAX_DECIMAL_PLACES)
// this works because we've specified the rounding mode
const minimumNonZeroValue: BigNumber = new BigNumber(decimalPlaces === 0 ? '0.5' : `0.${'0'.repeat(decimalPlaces)}5`) const minimumNonZeroValue: BigNumber = new BigNumber(decimalPlaces === 0 ? '0.5' : `0.${'0'.repeat(decimalPlaces)}5`)
if (bigNumber.isLessThan(minimumNonZeroValue)) { if (bigNumber.isLessThan(minimumNonZeroValue)) {
switch (underflowBehavior) { switch (underflowBehavior) {
case FIXED_UNDERFLOW_BEHAVIOR.ZERO: { case FIXED_UNDERFLOW_BEHAVIOR.ZERO: {
return _format(_0, format, dropTrailingZeros ? 0 : decimalPlaces) return _format(_0, dropTrailingZeros ? 0 : decimalPlaces, undefined, format)
} }
case FIXED_UNDERFLOW_BEHAVIOR.LESS_THAN: { case FIXED_UNDERFLOW_BEHAVIOR.LESS_THAN: {
return `<${_format(minimumNonZeroValue, format, minimumNonZeroValue.decimalPlaces())}` return `<${_format(minimumNonZeroValue, minimumNonZeroValue.decimalPlaces(), undefined, format)}`
} }
case FIXED_UNDERFLOW_BEHAVIOR.ONE_DIGIT: { case FIXED_UNDERFLOW_BEHAVIOR.ONE_DIGIT: {
const newBigNumber = new BigNumber(bigNumber.toPrecision(1)) const newBigNumber = new BigNumber(bigNumber.toPrecision(1))
return _format(newBigNumber, format, newBigNumber.decimalPlaces()) return _format(newBigNumber, newBigNumber.decimalPlaces(), undefined, format)
}
case FIXED_UNDERFLOW_BEHAVIOR.MAX_DECIMAL_PLACES: {
const newBigNumber = new BigNumber(bigNumber.toFixed(MAX_DECIMAL_PLACES))
return _format(newBigNumber, format, dropTrailingZeros ? newBigNumber.decimalPlaces() : MAX_DECIMAL_PLACES)
} }
default: { default: {
throw Error(`Passed FIXED_UNDERFLOW_BEHAVIOR ${underflowBehavior} is not valid.`) throw Error(`Passed FIXED_UNDERFLOW_BEHAVIOR ${underflowBehavior} is not valid.`)
...@@ -66,9 +66,38 @@ export function formatFixed( ...@@ -66,9 +66,38 @@ export function formatFixed(
} }
} else { } else {
const newDecimalPlaces = dropTrailingZeros const newDecimalPlaces = dropTrailingZeros
? new BigNumber(bigNumber.toFixed(decimalPlaces)).decimalPlaces() ? new BigNumber(bigNumber.toFixed(decimalPlaces, roundingMode)).decimalPlaces()
: decimalPlaces : decimalPlaces
return _format(bigNumber, format, newDecimalPlaces) return _format(bigNumber, newDecimalPlaces, roundingMode, format)
} }
} }
function decimalize(bigNumberish: BigNumberish, decimals: number): BigNumber {
const bigNumber: BigNumber = normalizeBigNumberish(bigNumberish)
ensureAllUInt256([bigNumber])
ensureAllUInt8([decimals])
if (decimals > MAX_DECIMAL_PLACES) {
throw Error(`This function does not support decimals greater than ${MAX_DECIMAL_PLACES}.`)
}
return bigNumber.dividedBy(_10.exponentiatedBy(decimals))
}
export function formatSignificantDecimals(
bigNumberish: BigNumberish,
decimals: number,
options?: FormatSignificantOptions
): string {
return formatSignificant(decimalize(bigNumberish, decimals), options)
}
export function formatFixedDecimals(
bigNumberish: BigNumberish,
decimals: number,
options?: FormatFixedOptions
): string {
return formatFixed(decimalize(bigNumberish, decimals), options)
}
export * from './data' import BigNumber from 'bignumber.js'
export * from './computation' export { BigNumber }
export { export {
ETH, ETH,
...@@ -9,3 +9,7 @@ export { ...@@ -9,3 +9,7 @@ export {
TRADE_EXACT, TRADE_EXACT,
FIXED_UNDERFLOW_BEHAVIOR FIXED_UNDERFLOW_BEHAVIOR
} from './constants' } from './constants'
export * from './data'
export * from './computation'
export * from './format'
...@@ -46,11 +46,13 @@ export type FlexibleFormat = boolean | BigNumber.Format ...@@ -46,11 +46,13 @@ export type FlexibleFormat = boolean | BigNumber.Format
export interface FormatSignificantOptions { export interface FormatSignificantOptions {
significantDigits: number significantDigits: number
roundingMode: BigNumber.RoundingMode
forceIntegerSignificance: boolean forceIntegerSignificance: boolean
format: FlexibleFormat format: FlexibleFormat
} }
export interface FormatFixedOptions { export interface FormatFixedOptions {
decimalPlaces: number decimalPlaces: number
roundingMode: BigNumber.RoundingMode
dropTrailingZeros: boolean dropTrailingZeros: boolean
underflowBehavior: FIXED_UNDERFLOW_BEHAVIOR underflowBehavior: FIXED_UNDERFLOW_BEHAVIOR
format: FlexibleFormat format: FlexibleFormat
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment