Move OrderValidationUtils (+ tests) and ExchangeTransferSimulator to order-utils

This commit is contained in:
Fabio Berger
2018-06-11 19:21:32 +02:00
parent e4afe603f9
commit 21f7722f10
16 changed files with 786 additions and 18 deletions

View File

@@ -13,7 +13,7 @@
"pre_build": "run-s update_artifacts generate_contract_wrappers", "pre_build": "run-s update_artifacts generate_contract_wrappers",
"transpile": "tsc", "transpile": "tsc",
"copy_monorepo_scripts": "copyfiles -u 3 './lib/src/monorepo_scripts/**/*' ./scripts", "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;", "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", "test": "yarn run_mocha",
"rebuild_and_test": "run-s build test", "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" "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": { "config": {
"contracts": "IWallet IValidator Exchange", "contracts": "IWallet IValidator Exchange DummyERC20Token ERC20Proxy",
"postpublish": { "postpublish": {
"docPublishConfigs": { "docPublishConfigs": {
"extraFileIncludes": [ "extraFileIncludes": [
@@ -52,6 +52,7 @@
"homepage": "https://github.com/0xProject/0x-monorepo/packages/order-utils/README.md", "homepage": "https://github.com/0xProject/0x-monorepo/packages/order-utils/README.md",
"devDependencies": { "devDependencies": {
"@0xproject/dev-utils": "^0.4.2", "@0xproject/dev-utils": "^0.4.2",
"@0xproject/migrations": "^0.0.6",
"@0xproject/monorepo-scripts": "^0.1.20", "@0xproject/monorepo-scripts": "^0.1.20",
"@0xproject/tslint-config": "^0.4.18", "@0xproject/tslint-config": "^0.4.18",
"@types/ethereumjs-abi": "^0.6.0", "@types/ethereumjs-abi": "^0.6.0",

View File

@@ -1,6 +1,6 @@
import { BigNumber } from '@0xproject/utils'; import { BigNumber } from '@0xproject/utils';
export abstract class AbstractBalanceAndProxyAllowanceFetcher { export abstract class AbstractBalanceAndProxyAllowanceFetcher {
public abstract async getBalanceAsync(tokenAddress: string, userAddress: string): Promise<BigNumber>; public abstract async getBalanceAsync(assetData: string, userAddress: string): Promise<BigNumber>;
public abstract async getProxyAllowanceAsync(tokenAddress: string, userAddress: string): Promise<BigNumber>; public abstract async getProxyAllowanceAsync(assetData: string, userAddress: string): Promise<BigNumber>;
} }

View File

@@ -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;
}

View File

@@ -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 Exchange from './artifacts/Exchange.json';
import * as IValidator from './artifacts/IValidator.json'; import * as IValidator from './artifacts/IValidator.json';
import * as IWallet from './artifacts/IWallet.json'; import * as IWallet from './artifacts/IWallet.json';
export const artifacts = { export const artifacts = {
Exchange: (Exchange as any) as Artifact, ERC20Proxy: (ERC20Proxy as any) as ContractArtifact,
IWallet: (IWallet as any) as Artifact, DummyERC20Token: (DummyERC20Token as any) as ContractArtifact,
IValidator: (IValidator as any) as Artifact, Exchange: (Exchange as any) as ContractArtifact,
IWallet: (IWallet as any) as ContractArtifact,
IValidator: (IValidator as any) as ContractArtifact,
}; };

View File

@@ -1,3 +1,8 @@
import { BigNumber } from '@0xproject/utils';
export const constants = { export const constants = {
NULL_ADDRESS: '0x0000000000000000000000000000000000000000', 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,
}; };

View File

@@ -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));
}
}

View File

@@ -19,3 +19,5 @@ export { AbstractOrderFilledCancelledFetcher } from './abstract/abstract_order_f
export { RemainingFillableCalculator } from './remaining_fillable_calculator'; export { RemainingFillableCalculator } from './remaining_fillable_calculator';
export { OrderStateUtils } from './order_state_utils'; export { OrderStateUtils } from './order_state_utils';
export { assetProxyUtils } from './asset_proxy_utils'; export { assetProxyUtils } from './asset_proxy_utils';
export { OrderValidationUtils } from './order_validation_utils';
export { ExchangeTransferSimulator } from './exchange_transfer_simulator';

View File

@@ -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);
}
}
}

View File

@@ -90,7 +90,7 @@ export async function isValidPresignedSignatureAsync(
data: string, data: string,
signerAddress: string, signerAddress: string,
): Promise<boolean> { ): 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); const isValid = await exchangeContract.preSigned.callAsync(data, signerAddress);
return isValid; return isValid;
} }
@@ -110,7 +110,7 @@ export async function isValidWalletSignatureAsync(
): Promise<boolean> { ): Promise<boolean> {
// tslint:disable-next-line:custom-no-magic-numbers // tslint:disable-next-line:custom-no-magic-numbers
const signatureWithoutType = signature.slice(-2); 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); const isValid = await walletContract.isValidSignature.callAsync(data, signatureWithoutType);
return isValid; return isValid;
} }
@@ -129,7 +129,7 @@ export async function isValidValidatorSignatureAsync(
signerAddress: string, signerAddress: string,
): Promise<boolean> { ): Promise<boolean> {
const validatorSignature = parseValidatorSignature(signature); 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( const isValidatorApproved = await exchangeContract.allowedValidators.callAsync(
signerAddress, signerAddress,
validatorSignature.validatorAddress, validatorSignature.validatorAddress,
@@ -138,7 +138,7 @@ export async function isValidValidatorSignatureAsync(
throw new Error(`Validator ${validatorSignature.validatorAddress} was not pre-approved by ${signerAddress}.`); 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( const isValid = await validatorContract.isValidSignature.callAsync(
data, data,
signerAddress, 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)); * Parse a 0x protocol hex-encoded signature string into it's ECSignature components
return ethUtil.sha3(Buffer.concat([prefix, message])); * @param signature A hex encoded ecSignature 0x Protocol signature
} * @return An ECSignature object with r,s,v parameters
*/
function parseECSignature(signature: string): ECSignature { export function parseECSignature(signature: string): ECSignature {
const ecSignatureTypes = [SignatureType.EthSign, SignatureType.EIP712, SignatureType.Trezor]; const ecSignatureTypes = [SignatureType.EthSign, SignatureType.EIP712, SignatureType.Trezor];
assert.isOneOfExpectedSignatureTypes(signature, ecSignatureTypes); assert.isOneOfExpectedSignatureTypes(signature, ecSignatureTypes);
@@ -276,6 +276,11 @@ function parseECSignature(signature: string): ECSignature {
return 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 { function parseValidatorSignature(signature: string): ValidatorSignature {
assert.isOneOfExpectedSignatureTypes(signature, [SignatureType.Validator]); assert.isOneOfExpectedSignatureTypes(signature, [SignatureType.Validator]);
// tslint:disable:custom-no-magic-numbers // tslint:disable:custom-no-magic-numbers

View File

@@ -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 = {};
}
}

View File

@@ -23,3 +23,13 @@ export interface MessagePrefixOpts {
prefixType: MessagePrefixType; prefixType: MessagePrefixType;
shouldAddPrefixBeforeCallingEthSign: boolean; shouldAddPrefixBeforeCallingEthSign: boolean;
} }
export enum TradeSide {
Maker = 'maker',
Taker = 'taker',
}
export enum TransferType {
Trade = 'trade',
Fee = 'fee',
}

View File

@@ -1,3 +1,5 @@
import { BigNumber } from '@0xproject/utils';
export const utils = { export const utils = {
getSignatureTypeIndexIfExists(signature: string): number { getSignatureTypeIndexIfExists(signature: string): number {
// tslint:disable-next-line:custom-no-magic-numbers // tslint:disable-next-line:custom-no-magic-numbers
@@ -6,4 +8,8 @@ export const utils = {
const signatureTypeInt = parseInt(signatureTypeHex, base); const signatureTypeInt = parseInt(signatureTypeHex, base);
return signatureTypeInt; return signatureTypeInt;
}, },
getCurrentUnixTimestampSec(): BigNumber {
const milisecondsInSecond = 1000;
return new BigNumber(Date.now() / milisecondsInSecond).round();
},
}; };

View File

@@ -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;
}

View File

@@ -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);
});

View File

@@ -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();
});
});
});

View File

@@ -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;
}
}