Skip to content

Commit

Permalink
Update starkex decimal calculations
Browse files Browse the repository at this point in the history
  • Loading branch information
krebernisak authored and boxhock committed Jan 25, 2021
1 parent 55da982 commit eb5def6
Show file tree
Hide file tree
Showing 2 changed files with 19 additions and 55 deletions.
66 changes: 15 additions & 51 deletions dydx-stark/src/endpoint/starkex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ethers, BigNumber } from 'ethers'
import * as starkwareCrypto from '@authereum/starkware-crypto'
import { AdapterError } from '@chainlink/external-adapter'
import { Decimal } from 'decimal.js'
import { logger } from '@chainlink/external-adapter'

export type PriceDataPoint = {
oracleName: string
Expand All @@ -21,22 +22,12 @@ const MAX_DECIMALS = 18

const ZERO_BN = BigNumber.from('0')
const TWO_BN = BigNumber.from('2')
const TEN_DEC = new Decimal('10')

const powOfTwo = (num: number) => TWO_BN.pow(BigNumber.from(num))
const powOfTenDec = (num: number) => TEN_DEC.pow(new Decimal(num))

const hexToBn = (hex: string): BigNumber => {
if (hex.substr(0, 2) !== '0x') {
hex = '0x' + hex
}
return BigNumber.from(hex)
}

const ERROR_MSG_PRICE_NEGATIVE = 'Price must be a positive number.'
const ERROR_MSG_PRICE_PRECISION_LOSS =
'Please use string type to avoid precision loss with very small/big numbers.'
const ERROR_MSG_PRICE_MAX_DECIMALS = 'Price has too many decimals.'

const error400 = (message: string) => new AdapterError({ message, statusCode: 400 })

Expand All @@ -56,42 +47,15 @@ export const requireNormalizedPrice = (price: number | string): string => {
}

// Check if there is any loss of precision
const msg = `${ERROR_MSG_PRICE_PRECISION_LOSS} Got: ${price}.`
if (typeof price === 'number') {
// TODO: more precision loss detection with floats
const overSafeValue = price > Number.MAX_SAFE_INTEGER
if (overSafeValue) {
throw error400(`${ERROR_MSG_PRICE_PRECISION_LOSS} Got: ${price}.`)
}
}

// Convert number to decimal string (no scientific notation)
const _toString = (n: number) => {
const nStr = n.toString()
const isScientificNotation = nStr.indexOf('e') !== -1
if (!isScientificNotation) return nStr
return (
n
.toFixed(MAX_DECIMALS)
// remove trailing zeros
.replace(/(\.\d*?[1-9])0+$/g, '$1')
// remove decimal part if all zeros (or only decimal point)
.replace(/\.0*$/g, '')
)
}

const priceStr = typeof price === 'number' ? _toString(price as number) : (price as string)
const priceStrParts = priceStr.split('.')
const priceBig = new Decimal(priceStrParts[0]).mul(powOfTenDec(MAX_DECIMALS))
const decimals = (priceStrParts[1] && priceStrParts[1].length) || 0

if (decimals === 0) return priceBig.toFixed()
// Check if too many decimals
if (decimals > MAX_DECIMALS) {
throw error400(`${ERROR_MSG_PRICE_MAX_DECIMALS} Got: ${decimals}; Max: ${MAX_DECIMALS}`)
if (overSafeValue) logger.warn(msg)
}

const decimalValBig = new Decimal(priceStrParts[1]).mul(powOfTenDec(MAX_DECIMALS - decimals))
return priceBig.add(decimalValBig).toFixed()
const _powOfTen = (num: number) => new Decimal(10).pow(num)
return new Decimal(price).mul(_powOfTen(MAX_DECIMALS)).toFixed(0)
}

/**
Expand Down Expand Up @@ -145,9 +109,9 @@ export const getKeyPair = async (
const hash = ethers.utils.keccak256(flatSig)

// 3. Cut the last 5 bits of it to get your 251-bit-long private stark key
const pk = BigNumber.from(hexToBn(hash)).shr(5).toHexString()
const pk = BigNumber.from(hash).shr(5).toHexString().substr(2)

return starkwareCrypto.getKeyPair(pk.substr(2))
return starkwareCrypto.getKeyPair(pk)
}

/**
Expand All @@ -157,13 +121,13 @@ export const getKeyPair = async (
*/
export const getPriceMessage = (data: PriceDataPoint): BigNumber => {
// padded to 40 bit
const hexOracleName = Buffer.from(data.oracleName).toString('hex').padEnd(10, '0')
const hexOracleName = '0x' + Buffer.from(data.oracleName).toString('hex').padEnd(10, '0')
// padded to 128 bit
const hexAssetName = Buffer.from(data.assetName).toString('hex').padEnd(32, '0')
const hexAssetName = '0x' + Buffer.from(data.assetName).toString('hex').padEnd(32, '0')

return getPriceMessageRaw(
hexToBn(hexOracleName),
hexToBn(hexAssetName),
BigNumber.from(hexOracleName),
BigNumber.from(hexAssetName),
BigNumber.from(data.timestamp),
BigNumber.from(data.price),
)
Expand Down Expand Up @@ -213,9 +177,9 @@ const getPriceMessageRaw = (
// The second number is timestamp in the 32 LSB, then the price.
const second_number = price.shl(32).add(timestamp)

const w1 = first_number.toHexString()
const w2 = second_number.toHexString()
const hash = starkwareCrypto.hashMessage([w1.substr(2), w2.substr(2)])
const w1 = first_number.toHexString().substr(2)
const w2 = second_number.toHexString().substr(2)
const hash = starkwareCrypto.hashMessage([w1, w2])

return hexToBn(hash)
return BigNumber.from('0x' + hash)
}
8 changes: 4 additions & 4 deletions dydx-stark/test/endpoint/starkex.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,8 @@ describe('starkex', () => {
name: 'Error: price number over max safe number',
testData: {
price: Number.MAX_SAFE_INTEGER + 1,
expected: undefined,
error: true,
expected: '9007199254740992000000000000000000',
error: false,
},
},
{
Expand Down Expand Up @@ -215,8 +215,8 @@ describe('starkex', () => {
name: 'Error: price string with more than 18 decimals',
testData: {
price: '0.0000000000000000001',
expected: undefined,
error: true,
expected: '0', // TODO: can we detect precision loss and throw?
error: false,
},
},
]
Expand Down

0 comments on commit eb5def6

Please sign in to comment.