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