aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--packages/order-utils/package.json5
-rw-r--r--packages/order-utils/src/abstract/abstract_balance_and_proxy_allowance_fetcher.ts4
-rw-r--r--packages/order-utils/src/abstract/abstract_balance_and_proxy_allowance_lazy_store.ts11
-rw-r--r--packages/order-utils/src/artifacts.ts12
-rw-r--r--packages/order-utils/src/constants.ts5
-rw-r--r--packages/order-utils/src/exchange_transfer_simulator.ts114
-rw-r--r--packages/order-utils/src/index.ts2
-rw-r--r--packages/order-utils/src/order_validation_utils.ts223
-rw-r--r--packages/order-utils/src/signature_utils.ts25
-rw-r--r--packages/order-utils/src/store/balance_and_proxy_allowance_lazy_store.ts82
-rw-r--r--packages/order-utils/src/types.ts10
-rw-r--r--packages/order-utils/src/utils.ts6
-rw-r--r--packages/order-utils/test/exchange_transfer_simulator_test.ts164
-rw-r--r--packages/order-utils/test/global_hooks_test.ts45
-rw-r--r--packages/order-utils/test/order_validation_utils_test.ts70
-rw-r--r--packages/order-utils/test/utils/simple_erc20_balance_and_proxy_allowance_fetcher.ts26
16 files changed, 786 insertions, 18 deletions
diff --git a/packages/order-utils/package.json b/packages/order-utils/package.json
index 6c5ad6780..e19827779 100644
--- a/packages/order-utils/package.json
+++ b/packages/order-utils/package.json
@@ -13,7 +13,7 @@
"pre_build": "run-s update_artifacts generate_contract_wrappers",
"transpile": "tsc",
"copy_monorepo_scripts": "copyfiles -u 3 './lib/src/monorepo_scripts/**/*' ./scripts",
- "generate_contract_wrappers": "abi-gen --abis 'lib/src/artifacts/@(Exchange|IWallet|IValidator).json' --template ../contract_templates/contract.handlebars --partials '../contract_templates/partials/**/*.handlebars' --output src/generated_contract_wrappers --backend ethers",
+ "generate_contract_wrappers": "abi-gen --abis 'lib/src/artifacts/@(Exchange|IWallet|IValidator|DummyERC20Token|ERC20Proxy).json' --template ../contract_templates/contract.handlebars --partials '../contract_templates/partials/**/*.handlebars' --output src/generated_contract_wrappers --backend ethers",
"update_artifacts": "for i in ${npm_package_config_contracts}; do copyfiles -u 4 ../migrations/artifacts/2.0.0/$i.json lib/src/artifacts; done;",
"test": "yarn run_mocha",
"rebuild_and_test": "run-s build test",
@@ -29,7 +29,7 @@
"upload_docs_json": "aws s3 cp generated_docs/index.json $S3_URL --profile 0xproject --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers --content-type application/json"
},
"config": {
- "contracts": "IWallet IValidator Exchange",
+ "contracts": "IWallet IValidator Exchange DummyERC20Token ERC20Proxy",
"postpublish": {
"docPublishConfigs": {
"extraFileIncludes": [
@@ -52,6 +52,7 @@
"homepage": "https://github.com/0xProject/0x-monorepo/packages/order-utils/README.md",
"devDependencies": {
"@0xproject/dev-utils": "^0.4.2",
+ "@0xproject/migrations": "^0.0.6",
"@0xproject/monorepo-scripts": "^0.1.20",
"@0xproject/tslint-config": "^0.4.18",
"@types/ethereumjs-abi": "^0.6.0",
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/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..1cb132aba
--- /dev/null
+++ b/packages/order-utils/src/exchange_transfer_simulator.ts
@@ -0,0 +1,114 @@
+import { ExchangeContractErrs } from '@0xproject/types';
+import { BigNumber } from '@0xproject/utils';
+import { BlockParamLiteral } from 'ethereum-types';
+
+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 9e78c6b93..f9b37df82 100644
--- a/packages/order-utils/src/index.ts
+++ b/packages/order-utils/src/index.ts
@@ -19,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_validation_utils.ts b/packages/order-utils/src/order_validation_utils.ts
new file mode 100644
index 000000000..4280e4d97
--- /dev/null
+++ b/packages/order-utils/src/order_validation_utils.ts
@@ -0,0 +1,223 @@
+import { ExchangeContractErrs, Order, SignedOrder } from '@0xproject/types';
+import { BigNumber } from '@0xproject/utils';
+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 { isValidECSignature, parseECSignature } 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,
+ signedOrder: SignedOrder,
+ fillTakerTokenAmount: BigNumber,
+ takerAddress: string,
+ zrxTokenAddress: string,
+ ): Promise<BigNumber> {
+ if (fillTakerTokenAmount.eq(0)) {
+ throw new Error(ExchangeContractErrs.OrderFillAmountZero);
+ }
+ const orderHash = orderHashUtils.getOrderHashHex(signedOrder);
+ // TODO: Verify all signature types! To do this, we need access to a Provider...
+ const ecSignature = parseECSignature(signedOrder.signature);
+ if (!isValidECSignature(orderHash, ecSignature, signedOrder.makerAddress)) {
+ 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 = await OrderValidationUtils.isRoundingError(
+ filledTakerTokenAmount,
+ signedOrder.takerAssetAmount,
+ signedOrder.makerAssetAmount,
+ );
+ if (wouldRoundingErrorOccur) {
+ throw new Error(ExchangeContractErrs.OrderFillRoundingError);
+ }
+ return filledTakerTokenAmount;
+ }
+ public async validateFillOrKillOrderThrowIfInvalidAsync(
+ exchangeTradeEmulator: ExchangeTransferSimulator,
+ signedOrder: SignedOrder,
+ fillTakerTokenAmount: BigNumber,
+ takerAddress: string,
+ zrxTokenAddress: string,
+ ): Promise<void> {
+ const filledTakerTokenAmount = await this.validateFillOrderThrowIfInvalidAsync(
+ exchangeTradeEmulator,
+ 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..b5bd2801a
--- /dev/null
+++ b/packages/order-utils/src/store/balance_and_proxy_allowance_lazy_store.ts
@@ -0,0 +1,82 @@
+import { BigNumber } from '@0xproject/utils';
+import { BlockParamLiteral } from 'ethereum-types';
+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();
+ },
};
diff --git a/packages/order-utils/test/exchange_transfer_simulator_test.ts b/packages/order-utils/test/exchange_transfer_simulator_test.ts
new file mode 100644
index 000000000..3e35f900e
--- /dev/null
+++ b/packages/order-utils/test/exchange_transfer_simulator_test.ts
@@ -0,0 +1,164 @@
+import { BlockchainLifecycle } from '@0xproject/dev-utils';
+import { ExchangeContractErrs, Token } from '@0xproject/types';
+import { BigNumber } from '@0xproject/utils';
+import * as chai from 'chai';
+import { BlockParamLiteral } from 'ethereum-types';
+import * as _ from 'lodash';
+import 'make-promises-safe';
+
+import { artifacts } from '../src/artifacts';
+import { constants } from '../src/constants';
+import { ExchangeTransferSimulator } from '../src/exchange_transfer_simulator';
+import { DummyERC20TokenContract } from '../src/generated_contract_wrappers/dummy_e_r_c20_token';
+import { BalanceAndProxyAllowanceLazyStore } from '../src/store/balance_and_proxy_allowance_lazy_store';
+import { TradeSide, TransferType } from '../src/types';
+
+import { chaiSetup } from './utils/chai_setup';
+import { SimpleERC20BalanceAndProxyAllowanceFetcher } from './utils/simple_erc20_balance_and_proxy_allowance_fetcher';
+import { provider, web3Wrapper } from './utils/web3_wrapper';
+
+chaiSetup.configure();
+const expect = chai.expect;
+const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
+
+describe('ExchangeTransferSimulator', async () => {
+ const transferAmount = new BigNumber(5);
+ let userAddresses: string[];
+ let dummyERC20Token: DummyERC20TokenContract;
+ let coinbase: string;
+ let sender: string;
+ let recipient: string;
+ let exampleTokenAddress: string;
+ let exchangeTransferSimulator: ExchangeTransferSimulator;
+ let txHash: string;
+ let erc20ProxyAddress: string;
+ before(async () => {
+ userAddresses = await web3Wrapper.getAvailableAddressesAsync();
+ [coinbase, sender, recipient] = userAddresses;
+
+ erc20ProxyAddress = getAddressFromArtifact(artifacts.ERC20Proxy, constants.TESTRPC_NETWORK_ID);
+
+ const wethArtifact = artifacts.DummyERC20Token;
+ const wethAddress = getAddressFromArtifact(wethArtifact, constants.TESTRPC_NETWORK_ID);
+ dummyERC20Token = new DummyERC20TokenContract(
+ artifacts.DummyERC20Token.compilerOutput.abi,
+ wethAddress,
+ provider,
+ );
+ exampleTokenAddress = dummyERC20Token.address;
+ });
+ beforeEach(async () => {
+ await blockchainLifecycle.startAsync();
+ });
+ afterEach(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+ describe('#transferFromAsync', function(): void {
+ // HACK: For some reason these tests need a slightly longer timeout
+ const mochaTestTimeoutMs = 3000;
+ this.timeout(mochaTestTimeoutMs);
+
+ beforeEach(() => {
+ const simpleERC20BalanceAndProxyAllowanceFetcher = new SimpleERC20BalanceAndProxyAllowanceFetcher(
+ dummyERC20Token,
+ erc20ProxyAddress,
+ );
+ const balanceAndProxyAllowanceLazyStore = new BalanceAndProxyAllowanceLazyStore(
+ simpleERC20BalanceAndProxyAllowanceFetcher,
+ );
+ exchangeTransferSimulator = new ExchangeTransferSimulator(balanceAndProxyAllowanceLazyStore);
+ });
+ it("throws if the user doesn't have enough allowance", async () => {
+ return expect(
+ exchangeTransferSimulator.transferFromAsync(
+ exampleTokenAddress,
+ sender,
+ recipient,
+ transferAmount,
+ TradeSide.Taker,
+ TransferType.Trade,
+ ),
+ ).to.be.rejectedWith(ExchangeContractErrs.InsufficientTakerAllowance);
+ });
+ it("throws if the user doesn't have enough balance", async () => {
+ txHash = await dummyERC20Token.approve.sendTransactionAsync(erc20ProxyAddress, transferAmount, {
+ from: sender,
+ });
+ await web3Wrapper.awaitTransactionSuccessAsync(txHash);
+ return expect(
+ exchangeTransferSimulator.transferFromAsync(
+ exampleTokenAddress,
+ sender,
+ recipient,
+ transferAmount,
+ TradeSide.Maker,
+ TransferType.Trade,
+ ),
+ ).to.be.rejectedWith(ExchangeContractErrs.InsufficientMakerBalance);
+ });
+ it('updates balances and proxyAllowance after transfer', async function(): Promise<void> {
+ txHash = await dummyERC20Token.transfer.sendTransactionAsync(sender, transferAmount, {
+ from: coinbase,
+ });
+ await web3Wrapper.awaitTransactionSuccessAsync(txHash);
+
+ txHash = await dummyERC20Token.approve.sendTransactionAsync(erc20ProxyAddress, transferAmount, {
+ from: sender,
+ });
+ await web3Wrapper.awaitTransactionSuccessAsync(txHash);
+
+ await exchangeTransferSimulator.transferFromAsync(
+ exampleTokenAddress,
+ sender,
+ recipient,
+ transferAmount,
+ TradeSide.Taker,
+ TransferType.Trade,
+ );
+ const store = (exchangeTransferSimulator as any)._store;
+ const senderBalance = await store.getBalanceAsync(exampleTokenAddress, sender);
+ const recipientBalance = await store.getBalanceAsync(exampleTokenAddress, recipient);
+ const senderProxyAllowance = await store.getProxyAllowanceAsync(exampleTokenAddress, sender);
+ expect(senderBalance).to.be.bignumber.equal(0);
+ expect(recipientBalance).to.be.bignumber.equal(transferAmount);
+ expect(senderProxyAllowance).to.be.bignumber.equal(0);
+ });
+ it("doesn't update proxyAllowance after transfer if unlimited", async () => {
+ txHash = await dummyERC20Token.transfer.sendTransactionAsync(sender, transferAmount, {
+ from: coinbase,
+ });
+ await web3Wrapper.awaitTransactionSuccessAsync(txHash);
+ txHash = await dummyERC20Token.approve.sendTransactionAsync(
+ erc20ProxyAddress,
+ constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS,
+ {
+ from: sender,
+ },
+ );
+ await web3Wrapper.awaitTransactionSuccessAsync(txHash);
+ await exchangeTransferSimulator.transferFromAsync(
+ exampleTokenAddress,
+ sender,
+ recipient,
+ transferAmount,
+ TradeSide.Taker,
+ TransferType.Trade,
+ );
+ const store = (exchangeTransferSimulator as any)._store;
+ const senderBalance = await store.getBalanceAsync(exampleTokenAddress, sender);
+ const recipientBalance = await store.getBalanceAsync(exampleTokenAddress, recipient);
+ const senderProxyAllowance = await store.getProxyAllowanceAsync(exampleTokenAddress, sender);
+ expect(senderBalance).to.be.bignumber.equal(0);
+ expect(recipientBalance).to.be.bignumber.equal(transferAmount);
+ expect(senderProxyAllowance).to.be.bignumber.equal(constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS);
+ });
+ });
+});
+
+function getAddressFromArtifact(artifact: any, networkId: number): string {
+ if (_.isUndefined(artifact.networks[networkId])) {
+ throw new Error(`Contract ${artifact.contractName} not deployed to network ${networkId}`);
+ }
+ const contractAddress = artifact.networks[networkId].address.toLowerCase();
+ return contractAddress;
+}
diff --git a/packages/order-utils/test/global_hooks_test.ts b/packages/order-utils/test/global_hooks_test.ts
new file mode 100644
index 000000000..662a2cb0f
--- /dev/null
+++ b/packages/order-utils/test/global_hooks_test.ts
@@ -0,0 +1,45 @@
+import { devConstants } from '@0xproject/dev-utils';
+import { ArtifactWriter } from '@0xproject/migrations';
+import { BigNumber } from '@0xproject/utils';
+
+import { artifacts } from '../src/artifacts';
+import { constants } from '../src/constants';
+import { DummyERC20TokenContract } from '../src/generated_contract_wrappers/dummy_e_r_c20_token';
+import { ERC20ProxyContract } from '../src/generated_contract_wrappers/e_r_c20_proxy';
+
+import { provider } from './utils/web3_wrapper';
+
+before('migrate contracts', async function(): Promise<void> {
+ // HACK: Since contract migrations take longer then our global mocha timeout limit
+ // we manually increase it for this before hook.
+ const mochaTestTimeoutMs = 20000;
+ this.timeout(mochaTestTimeoutMs);
+
+ const txDefaults = {
+ gas: devConstants.GAS_LIMIT,
+ from: devConstants.TESTRPC_FIRST_ADDRESS,
+ };
+
+ const networkId = constants.TESTRPC_NETWORK_ID;
+ const artifactsDir = `lib/src/artifacts`;
+ const artifactsWriter = new ArtifactWriter(artifactsDir, networkId);
+
+ const erc20proxy = await ERC20ProxyContract.deployFrom0xArtifactAsync(artifacts.ERC20Proxy, provider, txDefaults);
+ artifactsWriter.saveArtifact(erc20proxy);
+
+ const totalSupply = new BigNumber(100000000000000000000);
+ const name = 'Test';
+ const symbol = 'TST';
+ const decimals = new BigNumber(18);
+ // tslint:disable-next-line:no-unused-variable
+ const dummyErc20Token = await DummyERC20TokenContract.deployFrom0xArtifactAsync(
+ artifacts.DummyERC20Token,
+ provider,
+ txDefaults,
+ name,
+ symbol,
+ decimals,
+ totalSupply,
+ );
+ artifactsWriter.saveArtifact(dummyErc20Token);
+});
diff --git a/packages/order-utils/test/order_validation_utils_test.ts b/packages/order-utils/test/order_validation_utils_test.ts
new file mode 100644
index 000000000..d3ff867d7
--- /dev/null
+++ b/packages/order-utils/test/order_validation_utils_test.ts
@@ -0,0 +1,70 @@
+import { BigNumber } from '@0xproject/utils';
+import * as chai from 'chai';
+import 'mocha';
+
+import { OrderValidationUtils } from '../src/order_validation_utils';
+
+import { chaiSetup } from './utils/chai_setup';
+
+chaiSetup.configure();
+const expect = chai.expect;
+
+describe('OrderValidationUtils', () => {
+ describe('#isRoundingError', () => {
+ it('should return false if there is a rounding error of 0.1%', async () => {
+ 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%
+ const isRoundingError = OrderValidationUtils.isRoundingError(numerator, denominator, target);
+ expect(isRoundingError).to.be.false();
+ });
+
+ it('should return false if there is a rounding of 0.09%', async () => {
+ const numerator = new BigNumber(20);
+ const denominator = new BigNumber(9991);
+ const target = new BigNumber(500);
+ // rounding error = ((20*500/9991) - floor(20*500/9991)) / (20*500/9991) = 0.09%
+ const isRoundingError = OrderValidationUtils.isRoundingError(numerator, denominator, target);
+ expect(isRoundingError).to.be.false();
+ });
+
+ it('should return true if there is a rounding error of 0.11%', async () => {
+ const numerator = new BigNumber(20);
+ const denominator = new BigNumber(9989);
+ const target = new BigNumber(500);
+ // rounding error = ((20*500/9989) - floor(20*500/9989)) / (20*500/9989) = 0.011%
+ const isRoundingError = OrderValidationUtils.isRoundingError(numerator, denominator, target);
+ expect(isRoundingError).to.be.true();
+ });
+
+ it('should return true if there is a rounding error > 0.1%', async () => {
+ const numerator = new BigNumber(3);
+ const denominator = new BigNumber(7);
+ const target = new BigNumber(10);
+ // rounding error = ((3*10/7) - floor(3*10/7)) / (3*10/7) = 6.67%
+ const isRoundingError = OrderValidationUtils.isRoundingError(numerator, denominator, target);
+ expect(isRoundingError).to.be.true();
+ });
+
+ it('should return false when there is no rounding error', async () => {
+ const numerator = new BigNumber(1);
+ const denominator = new BigNumber(2);
+ const target = new BigNumber(10);
+
+ const isRoundingError = OrderValidationUtils.isRoundingError(numerator, denominator, target);
+ expect(isRoundingError).to.be.false();
+ });
+
+ it('should return false when there is rounding error <= 0.1%', async () => {
+ // randomly generated numbers
+ const numerator = new BigNumber(76564);
+ const denominator = new BigNumber(676373677);
+ const target = new BigNumber(105762562);
+ // rounding error = ((76564*105762562/676373677) - floor(76564*105762562/676373677)) /
+ // (76564*105762562/676373677) = 0.0007%
+ const isRoundingError = OrderValidationUtils.isRoundingError(numerator, denominator, target);
+ expect(isRoundingError).to.be.false();
+ });
+ });
+});
diff --git a/packages/order-utils/test/utils/simple_erc20_balance_and_proxy_allowance_fetcher.ts b/packages/order-utils/test/utils/simple_erc20_balance_and_proxy_allowance_fetcher.ts
new file mode 100644
index 000000000..29b9a128b
--- /dev/null
+++ b/packages/order-utils/test/utils/simple_erc20_balance_and_proxy_allowance_fetcher.ts
@@ -0,0 +1,26 @@
+import { BigNumber } from '@0xproject/utils';
+
+import { AbstractBalanceAndProxyAllowanceFetcher } from '../../src/abstract/abstract_balance_and_proxy_allowance_fetcher';
+
+import { ERC20TokenContract } from '../../src/generated_contract_wrappers/e_r_c20_token';
+
+export class SimpleERC20BalanceAndProxyAllowanceFetcher implements AbstractBalanceAndProxyAllowanceFetcher {
+ private _erc20TokenContract: ERC20TokenContract;
+ private _erc20ProxyAddress: string;
+ constructor(erc20TokenWrapper: ERC20TokenContract, erc20ProxyAddress: string) {
+ this._erc20TokenContract = erc20TokenWrapper;
+ this._erc20ProxyAddress = erc20ProxyAddress;
+ }
+ public async getBalanceAsync(assetData: string, userAddress: string): Promise<BigNumber> {
+ // HACK: We cheat and don't pass in the userData since it's always the same token used
+ // in our tests.
+ const balance = await this._erc20TokenContract.balanceOf.callAsync(userAddress);
+ return balance;
+ }
+ public async getProxyAllowanceAsync(assetData: string, userAddress: string): Promise<BigNumber> {
+ // HACK: We cheat and don't pass in the userData since it's always the same token used
+ // in our tests.
+ const proxyAllowance = await this._erc20TokenContract.allowance.callAsync(userAddress, this._erc20ProxyAddress);
+ return proxyAllowance;
+ }
+}