diff options
Diffstat (limited to 'packages/order-utils/src')
-rw-r--r-- | packages/order-utils/src/abstract/abstract_balance_and_proxy_allowance_fetcher.ts | 4 | ||||
-rw-r--r-- | packages/order-utils/src/abstract/abstract_balance_and_proxy_allowance_lazy_store.ts | 11 | ||||
-rw-r--r-- | packages/order-utils/src/artifacts.ts | 12 | ||||
-rw-r--r-- | packages/order-utils/src/asset_proxy_utils.ts | 137 | ||||
-rw-r--r-- | packages/order-utils/src/constants.ts | 5 | ||||
-rw-r--r-- | packages/order-utils/src/exchange_transfer_simulator.ts | 113 | ||||
-rw-r--r-- | packages/order-utils/src/index.ts | 3 | ||||
-rw-r--r-- | packages/order-utils/src/order_state_utils.ts | 4 | ||||
-rw-r--r-- | packages/order-utils/src/order_validation_utils.ts | 231 | ||||
-rw-r--r-- | packages/order-utils/src/signature_utils.ts | 25 | ||||
-rw-r--r-- | packages/order-utils/src/store/balance_and_proxy_allowance_lazy_store.ts | 81 | ||||
-rw-r--r-- | packages/order-utils/src/types.ts | 10 | ||||
-rw-r--r-- | packages/order-utils/src/utils.ts | 6 |
13 files changed, 571 insertions, 71 deletions
diff --git a/packages/order-utils/src/abstract/abstract_balance_and_proxy_allowance_fetcher.ts b/packages/order-utils/src/abstract/abstract_balance_and_proxy_allowance_fetcher.ts index 857c6167f..b2760d98e 100644 --- a/packages/order-utils/src/abstract/abstract_balance_and_proxy_allowance_fetcher.ts +++ b/packages/order-utils/src/abstract/abstract_balance_and_proxy_allowance_fetcher.ts @@ -1,6 +1,6 @@ import { BigNumber } from '@0xproject/utils'; export abstract class AbstractBalanceAndProxyAllowanceFetcher { - public abstract async getBalanceAsync(tokenAddress: string, userAddress: string): Promise<BigNumber>; - public abstract async getProxyAllowanceAsync(tokenAddress: string, userAddress: string): Promise<BigNumber>; + public abstract async getBalanceAsync(assetData: string, userAddress: string): Promise<BigNumber>; + public abstract async getProxyAllowanceAsync(assetData: string, userAddress: string): Promise<BigNumber>; } diff --git a/packages/order-utils/src/abstract/abstract_balance_and_proxy_allowance_lazy_store.ts b/packages/order-utils/src/abstract/abstract_balance_and_proxy_allowance_lazy_store.ts new file mode 100644 index 000000000..38e08b7fe --- /dev/null +++ b/packages/order-utils/src/abstract/abstract_balance_and_proxy_allowance_lazy_store.ts @@ -0,0 +1,11 @@ +import { BigNumber } from '@0xproject/utils'; + +export abstract class AbstractBalanceAndProxyAllowanceLazyStore { + public abstract async getBalanceAsync(assetData: string, userAddress: string): Promise<BigNumber>; + public abstract async getProxyAllowanceAsync(assetData: string, userAddress: string): Promise<BigNumber>; + public abstract setBalance(assetData: string, userAddress: string, balance: BigNumber): void; + public abstract deleteBalance(assetData: string, userAddress: string): void; + public abstract setProxyAllowance(assetData: string, userAddress: string, proxyAllowance: BigNumber): void; + public abstract deleteProxyAllowance(assetData: string, userAddress: string): void; + public abstract deleteAll(): void; +} diff --git a/packages/order-utils/src/artifacts.ts b/packages/order-utils/src/artifacts.ts index f6fd00472..3d2d1e953 100644 --- a/packages/order-utils/src/artifacts.ts +++ b/packages/order-utils/src/artifacts.ts @@ -1,10 +1,14 @@ -import { Artifact } from '@0xproject/types'; +import { ContractArtifact } from '@0xproject/sol-compiler'; +import * as DummyERC20Token from './artifacts/DummyERC20Token.json'; +import * as ERC20Proxy from './artifacts/ERC20Proxy.json'; import * as Exchange from './artifacts/Exchange.json'; import * as IValidator from './artifacts/IValidator.json'; import * as IWallet from './artifacts/IWallet.json'; export const artifacts = { - Exchange: (Exchange as any) as Artifact, - IWallet: (IWallet as any) as Artifact, - IValidator: (IValidator as any) as Artifact, + ERC20Proxy: (ERC20Proxy as any) as ContractArtifact, + DummyERC20Token: (DummyERC20Token as any) as ContractArtifact, + Exchange: (Exchange as any) as ContractArtifact, + IWallet: (IWallet as any) as ContractArtifact, + IValidator: (IValidator as any) as ContractArtifact, }; diff --git a/packages/order-utils/src/asset_proxy_utils.ts b/packages/order-utils/src/asset_proxy_utils.ts index 55f2d56df..915ee5032 100644 --- a/packages/order-utils/src/asset_proxy_utils.ts +++ b/packages/order-utils/src/asset_proxy_utils.ts @@ -1,10 +1,15 @@ -import { AssetProxyId, ERC20ProxyData, ERC721ProxyData, ProxyData } from '@0xproject/types'; -import { BigNumber } from '@0xproject/utils'; +import { AssetProxyId, ERC20AssetData, ERC721AssetData } from '@0xproject/types'; +import { BigNumber, NULL_BYTES } from '@0xproject/utils'; import BN = require('bn.js'); import ethUtil = require('ethereumjs-util'); +import * as _ from 'lodash'; -const ERC20_PROXY_METADATA_BYTE_LENGTH = 21; -const ERC721_PROXY_METADATA_BYTE_LENGTH = 53; +const ERC20_ASSET_DATA_BYTE_LENGTH = 21; +const ERC721_ASSET_DATA_MINIMUM_BYTE_LENGTH = 53; +const ASSET_DATA_ADDRESS_OFFSET = 0; +const ERC721_ASSET_DATA_TOKEN_ID_OFFSET = 20; +const ERC721_ASSET_DATA_RECEIVER_DATA_LENGTH_OFFSET = 52; +const ERC721_ASSET_DATA_RECEIVER_DATA_OFFSET = 84; export const assetProxyUtils = { encodeAssetProxyId(assetProxyId: AssetProxyId): Buffer { @@ -40,23 +45,24 @@ export const assetProxyUtils = { const value = new BigNumber(formattedValue, 16); return value; }, - encodeERC20ProxyData(tokenAddress: string): string { + encodeERC20AssetData(tokenAddress: string): string { const encodedAssetProxyId = assetProxyUtils.encodeAssetProxyId(AssetProxyId.ERC20); const encodedAddress = assetProxyUtils.encodeAddress(tokenAddress); - const encodedMetadata = Buffer.concat([encodedAddress, encodedAssetProxyId]); - const encodedMetadataHex = ethUtil.bufferToHex(encodedMetadata); - return encodedMetadataHex; + const encodedAssetData = Buffer.concat([encodedAddress, encodedAssetProxyId]); + const encodedAssetDataHex = ethUtil.bufferToHex(encodedAssetData); + return encodedAssetDataHex; }, - decodeERC20ProxyData(proxyData: string): ERC20ProxyData { - const encodedProxyMetadata = ethUtil.toBuffer(proxyData); - if (encodedProxyMetadata.byteLength !== ERC20_PROXY_METADATA_BYTE_LENGTH) { + decodeERC20AssetData(proxyData: string): ERC20AssetData { + const encodedAssetData = ethUtil.toBuffer(proxyData); + if (encodedAssetData.byteLength !== ERC20_ASSET_DATA_BYTE_LENGTH) { throw new Error( `Could not decode ERC20 Proxy Data. Expected length of encoded data to be 21. Got ${ - encodedProxyMetadata.byteLength + encodedAssetData.byteLength }`, ); } - const encodedAssetProxyId = encodedProxyMetadata.slice(-1); + const assetProxyIdOffset = encodedAssetData.byteLength - 1; + const encodedAssetProxyId = encodedAssetData.slice(assetProxyIdOffset); const assetProxyId = assetProxyUtils.decodeAssetProxyId(encodedAssetProxyId); if (assetProxyId !== AssetProxyId.ERC20) { throw new Error( @@ -65,33 +71,45 @@ export const assetProxyUtils = { }), but got ${assetProxyId}`, ); } - const addressOffset = ERC20_PROXY_METADATA_BYTE_LENGTH - 1; - const encodedTokenAddress = encodedProxyMetadata.slice(0, addressOffset); + const encodedTokenAddress = encodedAssetData.slice(ASSET_DATA_ADDRESS_OFFSET, assetProxyIdOffset); const tokenAddress = assetProxyUtils.decodeAddress(encodedTokenAddress); - const erc20ProxyData = { + const erc20AssetData = { assetProxyId, tokenAddress, }; - return erc20ProxyData; + return erc20AssetData; }, - encodeERC721ProxyData(tokenAddress: string, tokenId: BigNumber): string { + encodeERC721AssetData(tokenAddress: string, tokenId: BigNumber, receiverData?: string): string { const encodedAssetProxyId = assetProxyUtils.encodeAssetProxyId(AssetProxyId.ERC721); const encodedAddress = assetProxyUtils.encodeAddress(tokenAddress); const encodedTokenId = assetProxyUtils.encodeUint256(tokenId); - const encodedMetadata = Buffer.concat([encodedAddress, encodedTokenId, encodedAssetProxyId]); - const encodedMetadataHex = ethUtil.bufferToHex(encodedMetadata); - return encodedMetadataHex; + let encodedAssetData = Buffer.concat([encodedAddress, encodedTokenId]); + if (!_.isUndefined(receiverData)) { + const encodedReceiverData = ethUtil.toBuffer(receiverData); + const receiverDataLength = new BigNumber(encodedReceiverData.byteLength); + const encodedReceiverDataLength = assetProxyUtils.encodeUint256(receiverDataLength); + encodedAssetData = Buffer.concat([encodedAssetData, encodedReceiverDataLength, encodedReceiverData]); + } + encodedAssetData = Buffer.concat([encodedAssetData, encodedAssetProxyId]); + const encodedAssetDataHex = ethUtil.bufferToHex(encodedAssetData); + return encodedAssetDataHex; }, - decodeERC721ProxyData(proxyData: string): ERC721ProxyData { - const encodedProxyMetadata = ethUtil.toBuffer(proxyData); - if (encodedProxyMetadata.byteLength !== ERC721_PROXY_METADATA_BYTE_LENGTH) { + decodeERC721AssetData(assetData: string): ERC721AssetData { + const encodedAssetData = ethUtil.toBuffer(assetData); + if (encodedAssetData.byteLength < ERC721_ASSET_DATA_MINIMUM_BYTE_LENGTH) { throw new Error( - `Could not decode ERC20 Proxy Data. Expected length of encoded data to be 53. Got ${ - encodedProxyMetadata.byteLength + `Could not decode ERC20 Proxy Data. Expected length of encoded data to be at least 53. Got ${ + encodedAssetData.byteLength }`, ); } - const encodedAssetProxyId = encodedProxyMetadata.slice(-1); + + const encodedTokenAddress = encodedAssetData.slice( + ASSET_DATA_ADDRESS_OFFSET, + ERC721_ASSET_DATA_TOKEN_ID_OFFSET, + ); + const proxyIdOffset = encodedAssetData.byteLength - 1; + const encodedAssetProxyId = encodedAssetData.slice(proxyIdOffset); const assetProxyId = assetProxyUtils.decodeAssetProxyId(encodedAssetProxyId); if (assetProxyId !== AssetProxyId.ERC721) { throw new Error( @@ -100,50 +118,63 @@ export const assetProxyUtils = { }), but got ${assetProxyId}`, ); } - const addressOffset = ERC20_PROXY_METADATA_BYTE_LENGTH - 1; - const encodedTokenAddress = encodedProxyMetadata.slice(0, addressOffset); const tokenAddress = assetProxyUtils.decodeAddress(encodedTokenAddress); - const tokenIdOffset = ERC721_PROXY_METADATA_BYTE_LENGTH - 1; - const encodedTokenId = encodedProxyMetadata.slice(addressOffset, tokenIdOffset); + const encodedTokenId = encodedAssetData.slice( + ERC721_ASSET_DATA_TOKEN_ID_OFFSET, + ERC721_ASSET_DATA_RECEIVER_DATA_LENGTH_OFFSET, + ); const tokenId = assetProxyUtils.decodeUint256(encodedTokenId); - const erc721ProxyData = { + let receiverData = NULL_BYTES; + const lengthUpToReceiverDataLength = ERC721_ASSET_DATA_RECEIVER_DATA_LENGTH_OFFSET + 1; + if (encodedAssetData.byteLength > lengthUpToReceiverDataLength) { + const encodedReceiverDataLength = encodedAssetData.slice( + ERC721_ASSET_DATA_RECEIVER_DATA_LENGTH_OFFSET, + ERC721_ASSET_DATA_RECEIVER_DATA_OFFSET, + ); + const receiverDataLength = assetProxyUtils.decodeUint256(encodedReceiverDataLength); + const lengthUpToReceiverData = ERC721_ASSET_DATA_RECEIVER_DATA_OFFSET + 1; + const expectedReceiverDataLength = new BigNumber(encodedAssetData.byteLength - lengthUpToReceiverData); + if (!receiverDataLength.equals(expectedReceiverDataLength)) { + throw new Error( + `Data length (${receiverDataLength}) does not match actual length of data (${expectedReceiverDataLength})`, + ); + } + const encodedReceiverData = encodedAssetData.slice( + ERC721_ASSET_DATA_RECEIVER_DATA_OFFSET, + receiverDataLength.add(ERC721_ASSET_DATA_RECEIVER_DATA_OFFSET).toNumber(), + ); + receiverData = ethUtil.bufferToHex(encodedReceiverData); + } + const erc721AssetData: ERC721AssetData = { assetProxyId, tokenAddress, tokenId, + receiverData, }; - return erc721ProxyData; + return erc721AssetData; }, - decodeProxyDataId(proxyData: string): AssetProxyId { - const encodedProxyMetadata = ethUtil.toBuffer(proxyData); - if (encodedProxyMetadata.byteLength < 1) { + decodeAssetDataId(assetData: string): AssetProxyId { + const encodedAssetData = ethUtil.toBuffer(assetData); + if (encodedAssetData.byteLength < 1) { throw new Error( `Could not decode Proxy Data. Expected length of encoded data to be at least 1. Got ${ - encodedProxyMetadata.byteLength + encodedAssetData.byteLength }`, ); } - const encodedAssetProxyId = encodedProxyMetadata.slice(-1); + const encodedAssetProxyId = encodedAssetData.slice(-1); const assetProxyId = assetProxyUtils.decodeAssetProxyId(encodedAssetProxyId); return assetProxyId; }, - decodeProxyData(proxyData: string): ProxyData { - const assetProxyId = assetProxyUtils.decodeProxyDataId(proxyData); + decodeAssetData(assetData: string): ERC20AssetData | ERC721AssetData { + const assetProxyId = assetProxyUtils.decodeAssetDataId(assetData); switch (assetProxyId) { case AssetProxyId.ERC20: - const erc20ProxyData = assetProxyUtils.decodeERC20ProxyData(proxyData); - const generalizedERC20ProxyData = { - assetProxyId, - tokenAddress: erc20ProxyData.tokenAddress, - }; - return generalizedERC20ProxyData; + const erc20AssetData = assetProxyUtils.decodeERC20AssetData(assetData); + return erc20AssetData; case AssetProxyId.ERC721: - const erc721ProxyData = assetProxyUtils.decodeERC721ProxyData(proxyData); - const generaliedERC721ProxyData = { - assetProxyId, - tokenAddress: erc721ProxyData.tokenAddress, - data: erc721ProxyData.tokenId, - }; - return generaliedERC721ProxyData; + const erc721AssetData = assetProxyUtils.decodeERC721AssetData(assetData); + return erc721AssetData; default: throw new Error(`Unrecognized asset proxy id: ${assetProxyId}`); } diff --git a/packages/order-utils/src/constants.ts b/packages/order-utils/src/constants.ts index ec2fe744a..ed5bd8101 100644 --- a/packages/order-utils/src/constants.ts +++ b/packages/order-utils/src/constants.ts @@ -1,3 +1,8 @@ +import { BigNumber } from '@0xproject/utils'; + export const constants = { NULL_ADDRESS: '0x0000000000000000000000000000000000000000', + // tslint:disable-next-line:custom-no-magic-numbers + UNLIMITED_ALLOWANCE_IN_BASE_UNITS: new BigNumber(2).pow(256).minus(1), + TESTRPC_NETWORK_ID: 50, }; diff --git a/packages/order-utils/src/exchange_transfer_simulator.ts b/packages/order-utils/src/exchange_transfer_simulator.ts new file mode 100644 index 000000000..32d53d6a2 --- /dev/null +++ b/packages/order-utils/src/exchange_transfer_simulator.ts @@ -0,0 +1,113 @@ +import { ExchangeContractErrs } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; + +import { AbstractBalanceAndProxyAllowanceLazyStore } from './abstract/abstract_balance_and_proxy_allowance_lazy_store'; +import { constants } from './constants'; +import { TradeSide, TransferType } from './types'; + +enum FailureReason { + Balance = 'balance', + ProxyAllowance = 'proxyAllowance', +} + +const ERR_MSG_MAPPING = { + [FailureReason.Balance]: { + [TradeSide.Maker]: { + [TransferType.Trade]: ExchangeContractErrs.InsufficientMakerBalance, + [TransferType.Fee]: ExchangeContractErrs.InsufficientMakerFeeBalance, + }, + [TradeSide.Taker]: { + [TransferType.Trade]: ExchangeContractErrs.InsufficientTakerBalance, + [TransferType.Fee]: ExchangeContractErrs.InsufficientTakerFeeBalance, + }, + }, + [FailureReason.ProxyAllowance]: { + [TradeSide.Maker]: { + [TransferType.Trade]: ExchangeContractErrs.InsufficientMakerAllowance, + [TransferType.Fee]: ExchangeContractErrs.InsufficientMakerFeeAllowance, + }, + [TradeSide.Taker]: { + [TransferType.Trade]: ExchangeContractErrs.InsufficientTakerAllowance, + [TransferType.Fee]: ExchangeContractErrs.InsufficientTakerFeeAllowance, + }, + }, +}; + +export class ExchangeTransferSimulator { + private _store: AbstractBalanceAndProxyAllowanceLazyStore; + private static _throwValidationError( + failureReason: FailureReason, + tradeSide: TradeSide, + transferType: TransferType, + ): never { + const errMsg = ERR_MSG_MAPPING[failureReason][tradeSide][transferType]; + throw new Error(errMsg); + } + constructor(store: AbstractBalanceAndProxyAllowanceLazyStore) { + this._store = store; + } + /** + * Simulates transferFrom call performed by a proxy + * @param assetData Data of the asset being transferred. Includes + * it's identifying information and assetType, + * e.g address for ERC20, address & tokenId for ERC721 + * @param from Owner of the transferred tokens + * @param to Recipient of the transferred tokens + * @param amountInBaseUnits The amount of tokens being transferred + * @param tradeSide Is Maker/Taker transferring + * @param transferType Is it a fee payment or a value transfer + */ + public async transferFromAsync( + assetData: string, + from: string, + to: string, + amountInBaseUnits: BigNumber, + tradeSide: TradeSide, + transferType: TransferType, + ): Promise<void> { + // HACK: When simulating an open order (e.g taker is NULL_ADDRESS), we don't want to adjust balances/ + // allowances for the taker. We do however, want to increase the balance of the maker since the maker + // might be relying on those funds to fill subsequent orders or pay the order's fees. + if (from === constants.NULL_ADDRESS && tradeSide === TradeSide.Taker) { + await this._increaseBalanceAsync(assetData, to, amountInBaseUnits); + return; + } + const balance = await this._store.getBalanceAsync(assetData, from); + const proxyAllowance = await this._store.getProxyAllowanceAsync(assetData, from); + if (proxyAllowance.lessThan(amountInBaseUnits)) { + ExchangeTransferSimulator._throwValidationError(FailureReason.ProxyAllowance, tradeSide, transferType); + } + if (balance.lessThan(amountInBaseUnits)) { + ExchangeTransferSimulator._throwValidationError(FailureReason.Balance, tradeSide, transferType); + } + await this._decreaseProxyAllowanceAsync(assetData, from, amountInBaseUnits); + await this._decreaseBalanceAsync(assetData, from, amountInBaseUnits); + await this._increaseBalanceAsync(assetData, to, amountInBaseUnits); + } + private async _decreaseProxyAllowanceAsync( + assetData: string, + userAddress: string, + amountInBaseUnits: BigNumber, + ): Promise<void> { + const proxyAllowance = await this._store.getProxyAllowanceAsync(assetData, userAddress); + if (!proxyAllowance.eq(constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS)) { + this._store.setProxyAllowance(assetData, userAddress, proxyAllowance.minus(amountInBaseUnits)); + } + } + private async _increaseBalanceAsync( + assetData: string, + userAddress: string, + amountInBaseUnits: BigNumber, + ): Promise<void> { + const balance = await this._store.getBalanceAsync(assetData, userAddress); + this._store.setBalance(assetData, userAddress, balance.plus(amountInBaseUnits)); + } + private async _decreaseBalanceAsync( + assetData: string, + userAddress: string, + amountInBaseUnits: BigNumber, + ): Promise<void> { + const balance = await this._store.getBalanceAsync(assetData, userAddress); + this._store.setBalance(assetData, userAddress, balance.minus(amountInBaseUnits)); + } +} diff --git a/packages/order-utils/src/index.ts b/packages/order-utils/src/index.ts index cb859dcb9..f9b37df82 100644 --- a/packages/order-utils/src/index.ts +++ b/packages/order-utils/src/index.ts @@ -7,6 +7,7 @@ export { isValidECSignature, ecSignOrderHashAsync, addSignedMessagePrefix, + parseECSignature, } from './signature_utils'; export { orderFactory } from './order_factory'; export { constants } from './constants'; @@ -18,3 +19,5 @@ export { AbstractOrderFilledCancelledFetcher } from './abstract/abstract_order_f export { RemainingFillableCalculator } from './remaining_fillable_calculator'; export { OrderStateUtils } from './order_state_utils'; export { assetProxyUtils } from './asset_proxy_utils'; +export { OrderValidationUtils } from './order_validation_utils'; +export { ExchangeTransferSimulator } from './exchange_transfer_simulator'; diff --git a/packages/order-utils/src/order_state_utils.ts b/packages/order-utils/src/order_state_utils.ts index ca18097c9..40f235da7 100644 --- a/packages/order-utils/src/order_state_utils.ts +++ b/packages/order-utils/src/order_state_utils.ts @@ -79,7 +79,7 @@ export class OrderStateUtils { } public async getOrderRelevantStateAsync(signedOrder: SignedOrder): Promise<OrderRelevantState> { const zrxTokenAddress = this._orderFilledCancelledFetcher.getZRXTokenAddress(); - const makerProxyData = assetProxyUtils.decodeERC20ProxyData(signedOrder.makerAssetData); + const makerProxyData = assetProxyUtils.decodeERC20AssetData(signedOrder.makerAssetData); const makerAssetAddress = makerProxyData.tokenAddress; const orderHash = orderHashUtils.getOrderHashHex(signedOrder); const makerBalance = await this._balanceAndProxyAllowanceFetcher.getBalanceAsync( @@ -111,7 +111,7 @@ export class OrderStateUtils { const transferrableMakerAssetAmount = BigNumber.min([makerProxyAllowance, makerBalance]); const transferrableFeeAssetAmount = BigNumber.min([makerFeeProxyAllowance, makerFeeBalance]); - const zrxAssetData = assetProxyUtils.encodeERC20ProxyData(zrxTokenAddress); + const zrxAssetData = assetProxyUtils.encodeERC20AssetData(zrxTokenAddress); const isMakerAssetZRX = signedOrder.makerAssetData === zrxAssetData; const remainingFillableCalculator = new RemainingFillableCalculator( signedOrder.makerFee, diff --git a/packages/order-utils/src/order_validation_utils.ts b/packages/order-utils/src/order_validation_utils.ts new file mode 100644 index 000000000..3a6704f26 --- /dev/null +++ b/packages/order-utils/src/order_validation_utils.ts @@ -0,0 +1,231 @@ +import { ExchangeContractErrs, Order, SignedOrder } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import { Provider } from 'ethereum-types'; +import * as _ from 'lodash'; + +import { OrderError, TradeSide, TransferType } from './types'; + +import { constants } from './constants'; +import { ExchangeTransferSimulator } from './exchange_transfer_simulator'; +import { ExchangeContract } from './generated_contract_wrappers/exchange'; +import { orderHashUtils } from './order_hash'; +import { isValidSignatureAsync } from './signature_utils'; +import { utils } from './utils'; + +export class OrderValidationUtils { + private _exchangeContract: ExchangeContract; + // TODO: Write some tests for the function + // const numerator = new BigNumber(20); + // const denominator = new BigNumber(999); + // const target = new BigNumber(50); + // rounding error = ((20*50/999) - floor(20*50/999)) / (20*50/999) = 0.1% + public static isRoundingError(numerator: BigNumber, denominator: BigNumber, target: BigNumber): boolean { + // Solidity's mulmod() in JS + // Source: https://solidity.readthedocs.io/en/latest/units-and-global-variables.html#mathematical-and-cryptographic-functions + if (denominator.eq(0)) { + throw new Error('denominator cannot be 0'); + } + const remainder = target.mul(numerator).mod(denominator); + if (remainder.eq(0)) { + return false; // no rounding error + } + + // tslint:disable-next-line:custom-no-magic-numbers + const errPercentageTimes1000000 = remainder.mul(1000000).div(numerator.mul(target)); + // tslint:disable-next-line:custom-no-magic-numbers + const isError = errPercentageTimes1000000.gt(1000); + return isError; + } + public static validateCancelOrderThrowIfInvalid( + order: Order, + cancelTakerTokenAmount: BigNumber, + filledTakerTokenAmount: BigNumber, + ): void { + if (cancelTakerTokenAmount.eq(0)) { + throw new Error(ExchangeContractErrs.OrderCancelAmountZero); + } + if (order.takerAssetAmount.eq(filledTakerTokenAmount)) { + throw new Error(ExchangeContractErrs.OrderAlreadyCancelledOrFilled); + } + const currentUnixTimestampSec = utils.getCurrentUnixTimestampSec(); + if (order.expirationTimeSeconds.lessThan(currentUnixTimestampSec)) { + throw new Error(ExchangeContractErrs.OrderCancelExpired); + } + } + public static async validateFillOrderBalancesAllowancesThrowIfInvalidAsync( + exchangeTradeEmulator: ExchangeTransferSimulator, + signedOrder: SignedOrder, + fillTakerTokenAmount: BigNumber, + senderAddress: string, + zrxTokenAddress: string, + ): Promise<void> { + const fillMakerTokenAmount = OrderValidationUtils._getPartialAmount( + fillTakerTokenAmount, + signedOrder.takerAssetAmount, + signedOrder.makerAssetAmount, + ); + await exchangeTradeEmulator.transferFromAsync( + signedOrder.makerAssetData, + signedOrder.makerAddress, + senderAddress, + fillMakerTokenAmount, + TradeSide.Maker, + TransferType.Trade, + ); + await exchangeTradeEmulator.transferFromAsync( + signedOrder.takerAssetData, + senderAddress, + signedOrder.makerAddress, + fillTakerTokenAmount, + TradeSide.Taker, + TransferType.Trade, + ); + const makerFeeAmount = OrderValidationUtils._getPartialAmount( + fillTakerTokenAmount, + signedOrder.takerAssetAmount, + signedOrder.makerFee, + ); + await exchangeTradeEmulator.transferFromAsync( + zrxTokenAddress, + signedOrder.makerAddress, + signedOrder.feeRecipientAddress, + makerFeeAmount, + TradeSide.Maker, + TransferType.Fee, + ); + const takerFeeAmount = OrderValidationUtils._getPartialAmount( + fillTakerTokenAmount, + signedOrder.takerAssetAmount, + signedOrder.takerFee, + ); + await exchangeTradeEmulator.transferFromAsync( + zrxTokenAddress, + senderAddress, + signedOrder.feeRecipientAddress, + takerFeeAmount, + TradeSide.Taker, + TransferType.Fee, + ); + } + private static _validateRemainingFillAmountNotZeroOrThrow( + takerAssetAmount: BigNumber, + filledTakerTokenAmount: BigNumber, + ): void { + if (takerAssetAmount.eq(filledTakerTokenAmount)) { + throw new Error(ExchangeContractErrs.OrderRemainingFillAmountZero); + } + } + private static _validateOrderNotExpiredOrThrow(expirationTimeSeconds: BigNumber): void { + const currentUnixTimestampSec = utils.getCurrentUnixTimestampSec(); + if (expirationTimeSeconds.lessThan(currentUnixTimestampSec)) { + throw new Error(ExchangeContractErrs.OrderFillExpired); + } + } + private static _getPartialAmount(numerator: BigNumber, denominator: BigNumber, target: BigNumber): BigNumber { + const fillMakerTokenAmount = numerator + .mul(target) + .div(denominator) + .round(0); + return fillMakerTokenAmount; + } + constructor(exchangeContract: ExchangeContract) { + this._exchangeContract = exchangeContract; + } + public async validateOrderFillableOrThrowAsync( + exchangeTradeEmulator: ExchangeTransferSimulator, + signedOrder: SignedOrder, + zrxTokenAddress: string, + expectedFillTakerTokenAmount?: BigNumber, + ): Promise<void> { + const orderHash = orderHashUtils.getOrderHashHex(signedOrder); + const filledTakerTokenAmount = await this._exchangeContract.filled.callAsync(orderHash); + OrderValidationUtils._validateRemainingFillAmountNotZeroOrThrow( + signedOrder.takerAssetAmount, + filledTakerTokenAmount, + ); + OrderValidationUtils._validateOrderNotExpiredOrThrow(signedOrder.expirationTimeSeconds); + let fillTakerTokenAmount = signedOrder.takerAssetAmount.minus(filledTakerTokenAmount); + if (!_.isUndefined(expectedFillTakerTokenAmount)) { + fillTakerTokenAmount = expectedFillTakerTokenAmount; + } + await OrderValidationUtils.validateFillOrderBalancesAllowancesThrowIfInvalidAsync( + exchangeTradeEmulator, + signedOrder, + fillTakerTokenAmount, + signedOrder.takerAddress, + zrxTokenAddress, + ); + } + public async validateFillOrderThrowIfInvalidAsync( + exchangeTradeEmulator: ExchangeTransferSimulator, + provider: Provider, + signedOrder: SignedOrder, + fillTakerTokenAmount: BigNumber, + takerAddress: string, + zrxTokenAddress: string, + ): Promise<BigNumber> { + if (fillTakerTokenAmount.eq(0)) { + throw new Error(ExchangeContractErrs.OrderFillAmountZero); + } + const orderHash = orderHashUtils.getOrderHashHex(signedOrder); + const isValid = await isValidSignatureAsync( + provider, + orderHash, + signedOrder.signature, + signedOrder.makerAddress, + ); + if (!isValid) { + throw new Error(OrderError.InvalidSignature); + } + const filledTakerTokenAmount = await this._exchangeContract.filled.callAsync(orderHash); + OrderValidationUtils._validateRemainingFillAmountNotZeroOrThrow( + signedOrder.takerAssetAmount, + filledTakerTokenAmount, + ); + if (signedOrder.takerAddress !== constants.NULL_ADDRESS && signedOrder.takerAddress !== takerAddress) { + throw new Error(ExchangeContractErrs.TransactionSenderIsNotFillOrderTaker); + } + OrderValidationUtils._validateOrderNotExpiredOrThrow(signedOrder.expirationTimeSeconds); + const remainingTakerTokenAmount = signedOrder.takerAssetAmount.minus(filledTakerTokenAmount); + const desiredFillTakerTokenAmount = remainingTakerTokenAmount.lessThan(fillTakerTokenAmount) + ? remainingTakerTokenAmount + : fillTakerTokenAmount; + await OrderValidationUtils.validateFillOrderBalancesAllowancesThrowIfInvalidAsync( + exchangeTradeEmulator, + signedOrder, + desiredFillTakerTokenAmount, + takerAddress, + zrxTokenAddress, + ); + + const wouldRoundingErrorOccur = OrderValidationUtils.isRoundingError( + filledTakerTokenAmount, + signedOrder.takerAssetAmount, + signedOrder.makerAssetAmount, + ); + if (wouldRoundingErrorOccur) { + throw new Error(ExchangeContractErrs.OrderFillRoundingError); + } + return filledTakerTokenAmount; + } + public async validateFillOrKillOrderThrowIfInvalidAsync( + exchangeTradeEmulator: ExchangeTransferSimulator, + provider: Provider, + signedOrder: SignedOrder, + fillTakerTokenAmount: BigNumber, + takerAddress: string, + zrxTokenAddress: string, + ): Promise<void> { + const filledTakerTokenAmount = await this.validateFillOrderThrowIfInvalidAsync( + exchangeTradeEmulator, + provider, + signedOrder, + fillTakerTokenAmount, + takerAddress, + zrxTokenAddress, + ); + if (filledTakerTokenAmount !== fillTakerTokenAmount) { + throw new Error(ExchangeContractErrs.InsufficientRemainingFillAmount); + } + } +} diff --git a/packages/order-utils/src/signature_utils.ts b/packages/order-utils/src/signature_utils.ts index c3fa0b6a5..44a7203a0 100644 --- a/packages/order-utils/src/signature_utils.ts +++ b/packages/order-utils/src/signature_utils.ts @@ -90,7 +90,7 @@ export async function isValidPresignedSignatureAsync( data: string, signerAddress: string, ): Promise<boolean> { - const exchangeContract = new ExchangeContract(artifacts.Exchange.abi, signerAddress, provider); + const exchangeContract = new ExchangeContract(artifacts.Exchange.compilerOutput.abi, signerAddress, provider); const isValid = await exchangeContract.preSigned.callAsync(data, signerAddress); return isValid; } @@ -110,7 +110,7 @@ export async function isValidWalletSignatureAsync( ): Promise<boolean> { // tslint:disable-next-line:custom-no-magic-numbers const signatureWithoutType = signature.slice(-2); - const walletContract = new IWalletContract(artifacts.IWallet.abi, signerAddress, provider); + const walletContract = new IWalletContract(artifacts.IWallet.compilerOutput.abi, signerAddress, provider); const isValid = await walletContract.isValidSignature.callAsync(data, signatureWithoutType); return isValid; } @@ -129,7 +129,7 @@ export async function isValidValidatorSignatureAsync( signerAddress: string, ): Promise<boolean> { const validatorSignature = parseValidatorSignature(signature); - const exchangeContract = new ExchangeContract(artifacts.Exchange.abi, signerAddress, provider); + const exchangeContract = new ExchangeContract(artifacts.Exchange.compilerOutput.abi, signerAddress, provider); const isValidatorApproved = await exchangeContract.allowedValidators.callAsync( signerAddress, validatorSignature.validatorAddress, @@ -138,7 +138,7 @@ export async function isValidValidatorSignatureAsync( throw new Error(`Validator ${validatorSignature.validatorAddress} was not pre-approved by ${signerAddress}.`); } - const validatorContract = new IValidatorContract(artifacts.IValidator.abi, signerAddress, provider); + const validatorContract = new IValidatorContract(artifacts.IValidator.compilerOutput.abi, signerAddress, provider); const isValid = await validatorContract.isValidSignature.callAsync( data, signerAddress, @@ -260,12 +260,12 @@ export function addSignedMessagePrefix(message: string, messagePrefixType: Messa } } -function hashTrezorPersonalMessage(message: Buffer): Buffer { - const prefix = ethUtil.toBuffer('\x19Ethereum Signed Message:\n' + String.fromCharCode(message.length)); - return ethUtil.sha3(Buffer.concat([prefix, message])); -} - -function parseECSignature(signature: string): ECSignature { +/** + * Parse a 0x protocol hex-encoded signature string into it's ECSignature components + * @param signature A hex encoded ecSignature 0x Protocol signature + * @return An ECSignature object with r,s,v parameters + */ +export function parseECSignature(signature: string): ECSignature { const ecSignatureTypes = [SignatureType.EthSign, SignatureType.EIP712, SignatureType.Trezor]; assert.isOneOfExpectedSignatureTypes(signature, ecSignatureTypes); @@ -276,6 +276,11 @@ function parseECSignature(signature: string): ECSignature { return ecSignature; } +function hashTrezorPersonalMessage(message: Buffer): Buffer { + const prefix = ethUtil.toBuffer('\x19Ethereum Signed Message:\n' + String.fromCharCode(message.length)); + return ethUtil.sha3(Buffer.concat([prefix, message])); +} + function parseValidatorSignature(signature: string): ValidatorSignature { assert.isOneOfExpectedSignatureTypes(signature, [SignatureType.Validator]); // tslint:disable:custom-no-magic-numbers diff --git a/packages/order-utils/src/store/balance_and_proxy_allowance_lazy_store.ts b/packages/order-utils/src/store/balance_and_proxy_allowance_lazy_store.ts new file mode 100644 index 000000000..08d50b924 --- /dev/null +++ b/packages/order-utils/src/store/balance_and_proxy_allowance_lazy_store.ts @@ -0,0 +1,81 @@ +import { BigNumber } from '@0xproject/utils'; +import * as _ from 'lodash'; + +import { AbstractBalanceAndProxyAllowanceFetcher } from '../abstract/abstract_balance_and_proxy_allowance_fetcher'; +import { AbstractBalanceAndProxyAllowanceLazyStore } from '../abstract/abstract_balance_and_proxy_allowance_lazy_store'; + +/** + * Copy on read store for balances/proxyAllowances of tokens/accounts + */ +export class BalanceAndProxyAllowanceLazyStore implements AbstractBalanceAndProxyAllowanceLazyStore { + private _balanceAndProxyAllowanceFetcher: AbstractBalanceAndProxyAllowanceFetcher; + private _balance: { + [assetData: string]: { + [userAddress: string]: BigNumber; + }; + }; + private _proxyAllowance: { + [assetData: string]: { + [userAddress: string]: BigNumber; + }; + }; + constructor(token: AbstractBalanceAndProxyAllowanceFetcher) { + this._balanceAndProxyAllowanceFetcher = token; + this._balance = {}; + this._proxyAllowance = {}; + } + public async getBalanceAsync(assetData: string, userAddress: string): Promise<BigNumber> { + if (_.isUndefined(this._balance[assetData]) || _.isUndefined(this._balance[assetData][userAddress])) { + const balance = await this._balanceAndProxyAllowanceFetcher.getBalanceAsync(assetData, userAddress); + this.setBalance(assetData, userAddress, balance); + } + const cachedBalance = this._balance[assetData][userAddress]; + return cachedBalance; + } + public setBalance(assetData: string, userAddress: string, balance: BigNumber): void { + if (_.isUndefined(this._balance[assetData])) { + this._balance[assetData] = {}; + } + this._balance[assetData][userAddress] = balance; + } + public deleteBalance(assetData: string, userAddress: string): void { + if (!_.isUndefined(this._balance[assetData])) { + delete this._balance[assetData][userAddress]; + if (_.isEmpty(this._balance[assetData])) { + delete this._balance[assetData]; + } + } + } + public async getProxyAllowanceAsync(assetData: string, userAddress: string): Promise<BigNumber> { + if ( + _.isUndefined(this._proxyAllowance[assetData]) || + _.isUndefined(this._proxyAllowance[assetData][userAddress]) + ) { + const proxyAllowance = await this._balanceAndProxyAllowanceFetcher.getProxyAllowanceAsync( + assetData, + userAddress, + ); + this.setProxyAllowance(assetData, userAddress, proxyAllowance); + } + const cachedProxyAllowance = this._proxyAllowance[assetData][userAddress]; + return cachedProxyAllowance; + } + public setProxyAllowance(assetData: string, userAddress: string, proxyAllowance: BigNumber): void { + if (_.isUndefined(this._proxyAllowance[assetData])) { + this._proxyAllowance[assetData] = {}; + } + this._proxyAllowance[assetData][userAddress] = proxyAllowance; + } + public deleteProxyAllowance(assetData: string, userAddress: string): void { + if (!_.isUndefined(this._proxyAllowance[assetData])) { + delete this._proxyAllowance[assetData][userAddress]; + if (_.isEmpty(this._proxyAllowance[assetData])) { + delete this._proxyAllowance[assetData]; + } + } + } + public deleteAll(): void { + this._balance = {}; + this._proxyAllowance = {}; + } +} diff --git a/packages/order-utils/src/types.ts b/packages/order-utils/src/types.ts index db0bfb249..3f1fce66d 100644 --- a/packages/order-utils/src/types.ts +++ b/packages/order-utils/src/types.ts @@ -23,3 +23,13 @@ export interface MessagePrefixOpts { prefixType: MessagePrefixType; shouldAddPrefixBeforeCallingEthSign: boolean; } + +export enum TradeSide { + Maker = 'maker', + Taker = 'taker', +} + +export enum TransferType { + Trade = 'trade', + Fee = 'fee', +} diff --git a/packages/order-utils/src/utils.ts b/packages/order-utils/src/utils.ts index 3b465cece..6149316f6 100644 --- a/packages/order-utils/src/utils.ts +++ b/packages/order-utils/src/utils.ts @@ -1,3 +1,5 @@ +import { BigNumber } from '@0xproject/utils'; + export const utils = { getSignatureTypeIndexIfExists(signature: string): number { // tslint:disable-next-line:custom-no-magic-numbers @@ -6,4 +8,8 @@ export const utils = { const signatureTypeInt = parseInt(signatureTypeHex, base); return signatureTypeInt; }, + getCurrentUnixTimestampSec(): BigNumber { + const milisecondsInSecond = 1000; + return new BigNumber(Date.now() / milisecondsInSecond).round(); + }, }; |