From d2d89adbddaec435ddb65545a86fc4dc981de521 Mon Sep 17 00:00:00 2001 From: Greg Hysen Date: Sun, 25 Nov 2018 17:12:21 -0800 Subject: Abstracted out encoding/decoding of numeric values into its own utility. Could be useful elsewhere. --- .../utils/src/abi_encoder/evm_data_types/array.ts | 6 +- .../utils/src/abi_encoder/evm_data_types/int.ts | 6 +- .../utils/src/abi_encoder/evm_data_types/number.ts | 71 ++------------ .../src/abi_encoder/evm_data_types/static_bytes.ts | 8 +- .../utils/src/abi_encoder/evm_data_types/string.ts | 2 +- .../utils/src/abi_encoder/evm_data_types/tuple.ts | 2 +- .../utils/src/abi_encoder/evm_data_types/uint.ts | 6 +- packages/utils/src/abi_encoder/utils/math.ts | 103 +++++++++++++++++++++ packages/utils/src/abi_encoder/utils/queue.ts | 2 +- 9 files changed, 125 insertions(+), 81 deletions(-) create mode 100644 packages/utils/src/abi_encoder/utils/math.ts (limited to 'packages/utils') diff --git a/packages/utils/src/abi_encoder/evm_data_types/array.ts b/packages/utils/src/abi_encoder/evm_data_types/array.ts index dd8184fd0..527cdadfe 100644 --- a/packages/utils/src/abi_encoder/evm_data_types/array.ts +++ b/packages/utils/src/abi_encoder/evm_data_types/array.ts @@ -4,17 +4,17 @@ import { DataTypeFactory, MemberDataType } from '../abstract_data_types'; import * as Constants from '../utils/constants'; export class Array extends MemberDataType { - private static readonly _matcher = RegExp('^(.+)\\[([0-9]*)\\]$'); + private static readonly _MATCHER = RegExp('^(.+)\\[([0-9]*)\\]$'); private readonly _arraySignature: string; private readonly _elementType: string; public static matchType(type: string): boolean { - return Array._matcher.test(type); + return Array._MATCHER.test(type); } public constructor(dataItem: DataItem, dataTypeFactory: DataTypeFactory) { // Sanity check - const matches = Array._matcher.exec(dataItem.type); + const matches = Array._MATCHER.exec(dataItem.type); if (matches === null || matches.length !== 3) { throw new Error(`Could not parse array: ${dataItem.type}`); } else if (matches[1] === undefined) { diff --git a/packages/utils/src/abi_encoder/evm_data_types/int.ts b/packages/utils/src/abi_encoder/evm_data_types/int.ts index ec41b9cfc..457c41b28 100644 --- a/packages/utils/src/abi_encoder/evm_data_types/int.ts +++ b/packages/utils/src/abi_encoder/evm_data_types/int.ts @@ -7,16 +7,16 @@ import { DataTypeFactory } from '../abstract_data_types'; import { Number } from './number'; export class Int extends Number { - private static readonly _matcher = RegExp( + private static readonly _MATCHER = RegExp( '^int(8|16|24|32|40|48|56|64|72|88|96|104|112|120|128|136|144|152|160|168|176|184|192|200|208|216|224|232|240|248|256){0,1}$', ); public static matchType(type: string): boolean { - return Int._matcher.test(type); + return Int._MATCHER.test(type); } public constructor(dataItem: DataItem, dataTypeFactory: DataTypeFactory) { - super(dataItem, Int._matcher, dataTypeFactory); + super(dataItem, Int._MATCHER, dataTypeFactory); } public getMaxValue(): BigNumber { diff --git a/packages/utils/src/abi_encoder/evm_data_types/number.ts b/packages/utils/src/abi_encoder/evm_data_types/number.ts index 86acdce07..053a574e3 100644 --- a/packages/utils/src/abi_encoder/evm_data_types/number.ts +++ b/packages/utils/src/abi_encoder/evm_data_types/number.ts @@ -1,11 +1,11 @@ import { DataItem } from 'ethereum-types'; -import * as ethUtil from 'ethereumjs-util'; import * as _ from 'lodash'; import { BigNumber } from '../../configured_bignumber'; import { DataTypeFactory, PayloadDataType } from '../abstract_data_types'; import { RawCalldata } from '../calldata'; import * as Constants from '../utils/constants'; +import * as EncoderMath from '../utils/math'; export abstract class Number extends PayloadDataType { private static readonly _SIZE_KNOWN_AT_COMPILE_TIME: boolean = true; @@ -25,73 +25,14 @@ export abstract class Number extends PayloadDataType { : (this._width = Number._DEFAULT_WIDTH); } - public encodeValue(value_: BigNumber | string | number): Buffer { - const value = new BigNumber(value_, 10); - if (value.greaterThan(this.getMaxValue())) { - throw new Error(`Tried to assign value of ${value}, which exceeds max value of ${this.getMaxValue()}`); - } else if (value.lessThan(this.getMinValue())) { - throw new Error(`Tried to assign value of ${value}, which exceeds min value of ${this.getMinValue()}`); - } - - let valueBuf: Buffer; - if (value.greaterThanOrEqualTo(0)) { - valueBuf = ethUtil.setLengthLeft( - ethUtil.toBuffer(`0x${value.toString(Constants.HEX_BASE)}`), - Constants.EVM_WORD_WIDTH_IN_BYTES, - ); - } else { - // BigNumber can't write a negative hex value, so we use twos-complement conversion to do it ourselves. - // Step 1/3: Convert value to positive binary string - const binBase = 2; - const valueBin = value.times(-1).toString(binBase); - - // Step 2/3: Invert binary value - let invertedValueBin = '1'.repeat(Constants.EVM_WORD_WIDTH_IN_BITS - valueBin.length); - _.each(valueBin, (bit: string) => { - invertedValueBin += bit === '1' ? '0' : '1'; - }); - const invertedValue = new BigNumber(invertedValueBin, binBase); - - // Step 3/3: Add 1 to inverted value - // The result is the two's-complement represent of the input value. - const negativeValue = invertedValue.plus(1); - - // Convert the negated value to a hex string - valueBuf = ethUtil.setLengthLeft( - ethUtil.toBuffer(`0x${negativeValue.toString(Constants.HEX_BASE)}`), - Constants.EVM_WORD_WIDTH_IN_BYTES, - ); - } - - return valueBuf; + public encodeValue(value: BigNumber | string | number): Buffer { + const encodedValue = EncoderMath.safeEncodeNumericValue(value, this.getMinValue(), this.getMaxValue()); + return encodedValue; } public decodeValue(calldata: RawCalldata): BigNumber { - const paddedValueBuf = calldata.popWord(); - const paddedValueHex = ethUtil.bufferToHex(paddedValueBuf); - let value = new BigNumber(paddedValueHex, 16); - if (this.getMinValue().lessThan(0)) { - // Check if we're negative - const valueBin = value.toString(Constants.BIN_BASE); - if (valueBin.length === Constants.EVM_WORD_WIDTH_IN_BITS && valueBin[0].startsWith('1')) { - // Negative - // Step 1/3: Invert binary value - let invertedValueBin = ''; - _.each(valueBin, (bit: string) => { - invertedValueBin += bit === '1' ? '0' : '1'; - }); - const invertedValue = new BigNumber(invertedValueBin, Constants.BIN_BASE); - - // Step 2/3: Add 1 to inverted value - // The result is the two's-complement represent of the input value. - const positiveValue = invertedValue.plus(1); - - // Step 3/3: Invert positive value - const negativeValue = positiveValue.times(-1); - value = negativeValue; - } - } - + const valueBuf = calldata.popWord(); + const value = EncoderMath.safeDecodeNumericValue(valueBuf, this.getMinValue(), this.getMaxValue()); return value; } diff --git a/packages/utils/src/abi_encoder/evm_data_types/static_bytes.ts b/packages/utils/src/abi_encoder/evm_data_types/static_bytes.ts index afa9afdf2..0d01e6105 100644 --- a/packages/utils/src/abi_encoder/evm_data_types/static_bytes.ts +++ b/packages/utils/src/abi_encoder/evm_data_types/static_bytes.ts @@ -8,18 +8,18 @@ import * as Constants from '../utils/constants'; export class StaticBytes extends PayloadDataType { private static readonly _SIZE_KNOWN_AT_COMPILE_TIME: boolean = true; - private static readonly _matcher = RegExp( + private static readonly _MATCHER = RegExp( '^(byte|bytes(1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31|32))$', ); private static readonly _DEFAULT_WIDTH = 1; private readonly _width: number; public static matchType(type: string): boolean { - return StaticBytes._matcher.test(type); + return StaticBytes._MATCHER.test(type); } private static _decodeWidthFromType(type: string): number { - const matches = StaticBytes._matcher.exec(type); + const matches = StaticBytes._MATCHER.exec(type); const width = (matches !== null && matches.length === 3 && matches[2] !== undefined) ? parseInt(matches[2], Constants.DEC_BASE) : StaticBytes._DEFAULT_WIDTH; @@ -55,7 +55,7 @@ export class StaticBytes extends PayloadDataType { this._sanityCheckValue(value); return value; } - + private _sanityCheckValue(value: string | Buffer): void { if (typeof value === 'string') { if (!value.startsWith('0x')) { diff --git a/packages/utils/src/abi_encoder/evm_data_types/string.ts b/packages/utils/src/abi_encoder/evm_data_types/string.ts index 15b93e447..428ea21db 100644 --- a/packages/utils/src/abi_encoder/evm_data_types/string.ts +++ b/packages/utils/src/abi_encoder/evm_data_types/string.ts @@ -13,7 +13,7 @@ export class String extends PayloadDataType { public static matchType(type: string): boolean { return type === 'string'; } - + public constructor(dataItem: DataItem, dataTypeFactory: DataTypeFactory) { super(dataItem, dataTypeFactory, String._SIZE_KNOWN_AT_COMPILE_TIME); if (!String.matchType(dataItem.type)) { diff --git a/packages/utils/src/abi_encoder/evm_data_types/tuple.ts b/packages/utils/src/abi_encoder/evm_data_types/tuple.ts index 89dd6604d..63d9dfa9e 100644 --- a/packages/utils/src/abi_encoder/evm_data_types/tuple.ts +++ b/packages/utils/src/abi_encoder/evm_data_types/tuple.ts @@ -4,7 +4,7 @@ import { DataTypeFactory, MemberDataType } from '../abstract_data_types'; export class Tuple extends MemberDataType { private readonly _signature: string; - + public static matchType(type: string): boolean { return type === 'tuple'; } diff --git a/packages/utils/src/abi_encoder/evm_data_types/uint.ts b/packages/utils/src/abi_encoder/evm_data_types/uint.ts index ced3ef08b..c2b6e214a 100644 --- a/packages/utils/src/abi_encoder/evm_data_types/uint.ts +++ b/packages/utils/src/abi_encoder/evm_data_types/uint.ts @@ -7,16 +7,16 @@ import { DataTypeFactory } from '../abstract_data_types'; import { Number } from './number'; export class UInt extends Number { - private static readonly _matcher = RegExp( + private static readonly _MATCHER = RegExp( '^uint(8|16|24|32|40|48|56|64|72|88|96|104|112|120|128|136|144|152|160|168|176|184|192|200|208|216|224|232|240|248|256){0,1}$', ); public static matchType(type: string): boolean { - return UInt._matcher.test(type); + return UInt._MATCHER.test(type); } public constructor(dataItem: DataItem, dataTypeFactory: DataTypeFactory) { - super(dataItem, UInt._matcher, dataTypeFactory); + super(dataItem, UInt._MATCHER, dataTypeFactory); } public getMaxValue(): BigNumber { diff --git a/packages/utils/src/abi_encoder/utils/math.ts b/packages/utils/src/abi_encoder/utils/math.ts new file mode 100644 index 000000000..8d21ada0a --- /dev/null +++ b/packages/utils/src/abi_encoder/utils/math.ts @@ -0,0 +1,103 @@ +import BigNumber from 'bignumber.js'; +import * as ethUtil from 'ethereumjs-util'; +import * as _ from 'lodash'; + +import * as Constants from '../utils/constants'; + +function sanityCheckBigNumberRange(value_: BigNumber | string | number, minValue: BigNumber, maxValue: BigNumber): void { + const value = new BigNumber(value_, 10); + if (value.greaterThan(maxValue)) { + throw new Error(`Tried to assign value of ${value}, which exceeds max value of ${maxValue}`); + } else if (value.lessThan(minValue)) { + throw new Error(`Tried to assign value of ${value}, which exceeds min value of ${minValue}`); + } +} +function bigNumberToPaddedBuffer(value: BigNumber): Buffer { + const valueHex = `0x${value.toString(Constants.HEX_BASE)}`; + const valueBuf = ethUtil.toBuffer(valueHex); + const valueBufPadded = ethUtil.setLengthLeft(valueBuf, Constants.EVM_WORD_WIDTH_IN_BYTES); + return valueBufPadded; +} +/** + * Takes a numeric value and returns its ABI-encoded value + * @param value_ The value to encode. + * @return ABI Encoded value + */ +export function encodeNumericValue(value_: BigNumber | string | number): Buffer { + const value = new BigNumber(value_, 10); + // Case 1/2: value is non-negative + if (value.greaterThanOrEqualTo(0)) { + const encodedPositiveValue = bigNumberToPaddedBuffer(value); + return encodedPositiveValue; + } + // Case 2/2: Value is negative + // Use two's-complement to encode the value + // Step 1/3: Convert negative value to positive binary string + const valueBin = value.times(-1).toString(Constants.BIN_BASE); + // Step 2/3: Invert binary value + let invertedValueBin = '1'.repeat(Constants.EVM_WORD_WIDTH_IN_BITS - valueBin.length); + _.each(valueBin, (bit: string) => { + invertedValueBin += bit === '1' ? '0' : '1'; + }); + const invertedValue = new BigNumber(invertedValueBin, Constants.BIN_BASE); + // Step 3/3: Add 1 to inverted value + const negativeValue = invertedValue.plus(1); + const encodedValue = bigNumberToPaddedBuffer(negativeValue); + return encodedValue; +} +/** + * Takes a numeric value and returns its ABI-encoded value. + * Performs an additional sanity check, given the min/max allowed value. + * @param value_ The value to encode. + * @return ABI Encoded value + */ +export function safeEncodeNumericValue(value: BigNumber | string | number, minValue: BigNumber, maxValue: BigNumber): Buffer { + sanityCheckBigNumberRange(value, minValue, maxValue); + const encodedValue = encodeNumericValue(value); + return encodedValue; +} +/** + * Takes an ABI-encoded numeric value and returns its decoded value as a BigNumber. + * @param encodedValue The encoded numeric value. + * @param minValue The minimum possible decoded value. + * @return ABI Decoded value + */ +export function decodeNumericValue(encodedValue: Buffer, minValue: BigNumber): BigNumber { + const valueHex = ethUtil.bufferToHex(encodedValue); + // Case 1/3: value is definitely non-negative because of numeric boundaries + const value = new BigNumber(valueHex, Constants.HEX_BASE); + if (!minValue.lessThan(0)) { + return value; + } + // Case 2/3: value is non-negative because there is no leading 1 (encoded as two's-complement) + const valueBin = value.toString(Constants.BIN_BASE); + const valueIsNegative = valueBin.length === Constants.EVM_WORD_WIDTH_IN_BITS && valueBin[0].startsWith('1'); + if (!valueIsNegative) { + return value; + } + // Case 3/3: value is negative + // Step 1/3: Invert b inary value + let invertedValueBin = ''; + _.each(valueBin, (bit: string) => { + invertedValueBin += bit === '1' ? '0' : '1'; + }); + const invertedValue = new BigNumber(invertedValueBin, Constants.BIN_BASE); + // Step 2/3: Add 1 to inverted value + // The result is the two's-complement representation of the input value. + const positiveValue = invertedValue.plus(1); + // Step 3/3: Invert positive value to get the negative value + const negativeValue = positiveValue.times(-1); + return negativeValue; +} +/** + * Takes an ABI-encoded numeric value and returns its decoded value as a BigNumber. + * Performs an additional sanity check, given the min/max allowed value. + * @param encodedValue The encoded numeric value. + * @param minValue The minimum possible decoded value. + * @return ABI Decoded value + */ +export function safeDecodeNumericValue(encodedValue: Buffer, minValue: BigNumber, maxValue: BigNumber): BigNumber { + const value = decodeNumericValue(encodedValue, minValue); + sanityCheckBigNumberRange(value, minValue, maxValue); + return value; +} diff --git a/packages/utils/src/abi_encoder/utils/queue.ts b/packages/utils/src/abi_encoder/utils/queue.ts index 506a0b56e..53afb7e11 100644 --- a/packages/utils/src/abi_encoder/utils/queue.ts +++ b/packages/utils/src/abi_encoder/utils/queue.ts @@ -32,7 +32,7 @@ export class Queue { public getStore(): T[] { return this._store; } - + public peekFront(): T | undefined { return this._store.length >= 0 ? this._store[0] : undefined; } -- cgit v1.2.3