aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorFabio Berger <me@fabioberger.com>2017-06-03 01:03:53 +0800
committerFabio Berger <me@fabioberger.com>2017-06-03 01:03:53 +0800
commit35c9330d6246183509a3543cdf1278b9ced191e2 (patch)
tree5cef8b37a52ef4ce258a971b1ae52c5c2ad63a63 /src
parentab5b5a881bc8f8f8d176ae65440de8fcd25ad906 (diff)
parentc83587a16d016d1efafaf31abb9b39eb54128568 (diff)
downloaddexon-sol-tools-35c9330d6246183509a3543cdf1278b9ced191e2.tar.gz
dexon-sol-tools-35c9330d6246183509a3543cdf1278b9ced191e2.tar.zst
dexon-sol-tools-35c9330d6246183509a3543cdf1278b9ced191e2.zip
Merge branch 'master' into unavailableFilledCancelled
# Conflicts: # src/contract_wrappers/exchange_wrapper.ts # src/types.ts # test/exchange_wrapper_test.ts
Diffstat (limited to 'src')
-rw-r--r--src/0x.js.ts2
-rw-r--r--src/contract_wrappers/exchange_wrapper.ts129
-rw-r--r--src/types.ts49
-rw-r--r--src/utils/schema_validator.ts2
4 files changed, 126 insertions, 56 deletions
diff --git a/src/0x.js.ts b/src/0x.js.ts
index 967c81ed8..40290467a 100644
--- a/src/0x.js.ts
+++ b/src/0x.js.ts
@@ -135,7 +135,7 @@ export class ZeroEx {
return senderAccountIfExists;
}
/**
- * Computes the orderHash given the order parameters and returns it as a hex encoded string.
+ * Computes the orderHash for a given order and returns it as a hex encoded string.
*/
public async getOrderHashHexAsync(order: Order|SignedOrder): Promise<string> {
const exchangeContractAddr = await this.getExchangeAddressAsync();
diff --git a/src/contract_wrappers/exchange_wrapper.ts b/src/contract_wrappers/exchange_wrapper.ts
index c1b310bf0..cb869b498 100644
--- a/src/contract_wrappers/exchange_wrapper.ts
+++ b/src/contract_wrappers/exchange_wrapper.ts
@@ -6,11 +6,11 @@ import {
ExchangeContract,
ExchangeContractErrCodes,
ExchangeContractErrs,
- FillOrderValidationErrs,
OrderValues,
OrderAddresses,
SignedOrder,
ContractEvent,
+ ContractResponse,
} from '../types';
import {assert} from '../utils/assert';
import {ContractWrapper} from './contract_wrapper';
@@ -18,18 +18,17 @@ import * as ExchangeArtifacts from '../artifacts/Exchange.json';
import {ecSignatureSchema} from '../schemas/ec_signature_schema';
import {signedOrderSchema} from '../schemas/order_schemas';
import {SchemaValidator} from '../utils/schema_validator';
-import {ContractResponse} from '../types';
import {constants} from '../utils/constants';
import {TokenWrapper} from './token_wrapper';
export class ExchangeWrapper extends ContractWrapper {
private exchangeContractErrCodesToMsg = {
- [ExchangeContractErrCodes.ERROR_FILL_EXPIRED]: ExchangeContractErrs.ORDER_EXPIRED,
- [ExchangeContractErrCodes.ERROR_CANCEL_EXPIRED]: ExchangeContractErrs.ORDER_EXPIRED,
+ [ExchangeContractErrCodes.ERROR_FILL_EXPIRED]: ExchangeContractErrs.ORDER_FILL_EXPIRED,
+ [ExchangeContractErrCodes.ERROR_CANCEL_EXPIRED]: ExchangeContractErrs.ORDER_FILL_EXPIRED,
[ExchangeContractErrCodes.ERROR_FILL_NO_VALUE]: ExchangeContractErrs.ORDER_REMAINING_FILL_AMOUNT_ZERO,
[ExchangeContractErrCodes.ERROR_CANCEL_NO_VALUE]: ExchangeContractErrs.ORDER_REMAINING_FILL_AMOUNT_ZERO,
- [ExchangeContractErrCodes.ERROR_FILL_TRUNCATION]: ExchangeContractErrs.ORDER_ROUNDING_ERROR,
- [ExchangeContractErrCodes.ERROR_FILL_BALANCE_ALLOWANCE]: ExchangeContractErrs.ORDER_BALANCE_ALLOWANCE_ERROR,
+ [ExchangeContractErrCodes.ERROR_FILL_TRUNCATION]: ExchangeContractErrs.ORDER_FILL_ROUNDING_ERROR,
+ [ExchangeContractErrCodes.ERROR_FILL_BALANCE_ALLOWANCE]: ExchangeContractErrs.FILL_BALANCE_ALLOWANCE_ERROR,
};
private exchangeContractIfExists?: ExchangeContract;
private tokenWrapper: TokenWrapper;
@@ -100,22 +99,24 @@ export class ExchangeWrapper extends ContractWrapper {
return cancelledAmountInBaseUnits;
}
/**
- * Fills a signed order with a fillAmount denominated in baseUnits of the taker token. The caller can
- * decide whether they want the call to throw if the balance/allowance checks fail by setting
- * shouldCheckTransfer to false. If set to true, the call will fail without throwing, preserving gas costs.
+ * Fills a signed order with a fillAmount denominated in baseUnits of the taker token.
+ * Since the order in which transactions are included in the next block is indeterminate, race-conditions
+ * could arise where a users balance or allowance changes before the fillOrder executes. Because of this,
+ * we allow you to specify `shouldCheckTransfer`. If true, the smart contract will not throw if while
+ * executing, the parties do not have sufficient balances/allowances, preserving gas costs. Setting it to
+ * false forgoes this check and causes the smart contract to throw instead.
*/
- public async fillOrderAsync(signedOrder: SignedOrder, fillTakerAmountInBaseUnits: BigNumber.BigNumber,
+ public async fillOrderAsync(signedOrder: SignedOrder, fillTakerAmount: BigNumber.BigNumber,
shouldCheckTransfer: boolean): Promise<void> {
assert.doesConformToSchema('signedOrder',
SchemaValidator.convertToJSONSchemaCompatibleObject(signedOrder as object),
signedOrderSchema);
- assert.isBigNumber('fillTakerAmountInBaseUnits', fillTakerAmountInBaseUnits);
+ assert.isBigNumber('fillTakerAmount', fillTakerAmount);
assert.isBoolean('shouldCheckTransfer', shouldCheckTransfer);
const senderAddress = await this.web3Wrapper.getSenderAddressOrThrowAsync();
- await this.validateFillOrderAsync(signedOrder, fillTakerAmountInBaseUnits, senderAddress);
-
const exchangeInstance = await this.getExchangeContractAsync();
+ await this.validateFillOrderAndThrowIfInvalidAsync(signedOrder, fillTakerAmount, senderAddress);
const orderAddresses: OrderAddresses = [
signedOrder.maker,
@@ -135,7 +136,7 @@ export class ExchangeWrapper extends ContractWrapper {
const gas = await exchangeInstance.fill.estimateGas(
orderAddresses,
orderValues,
- fillTakerAmountInBaseUnits,
+ fillTakerAmount,
shouldCheckTransfer,
signedOrder.ecSignature.v,
signedOrder.ecSignature.r,
@@ -147,7 +148,7 @@ export class ExchangeWrapper extends ContractWrapper {
const response: ContractResponse = await exchangeInstance.fill(
orderAddresses,
orderValues,
- fillTakerAmountInBaseUnits,
+ fillTakerAmount,
shouldCheckTransfer,
signedOrder.ecSignature.v,
signedOrder.ecSignature.r,
@@ -159,39 +160,89 @@ export class ExchangeWrapper extends ContractWrapper {
);
this.throwErrorLogsAsErrors(response.logs);
}
- private async validateFillOrderAsync(signedOrder: SignedOrder, fillTakerAmountInBaseUnits: BigNumber.BigNumber,
- senderAddress: string) {
- if (fillTakerAmountInBaseUnits.eq(0)) {
- throw new Error(FillOrderValidationErrs.FILL_AMOUNT_IS_ZERO);
+ private async validateFillOrderAndThrowIfInvalidAsync(signedOrder: SignedOrder,
+ fillTakerAmount: BigNumber.BigNumber,
+ senderAddress: string): Promise<void> {
+ if (fillTakerAmount.eq(0)) {
+ throw new Error(ExchangeContractErrs.ORDER_REMAINING_FILL_AMOUNT_ZERO);
}
if (signedOrder.taker !== constants.NULL_ADDRESS && signedOrder.taker !== senderAddress) {
- throw new Error(FillOrderValidationErrs.NOT_A_TAKER);
+ throw new Error(ExchangeContractErrs.TRANSACTION_SENDER_IS_NOT_FILL_ORDER_TAKER);
+ }
+ const currentUnixTimestampSec = Date.now() / 1000;
+ if (signedOrder.expirationUnixTimestampSec.lessThan(currentUnixTimestampSec)) {
+ throw new Error(ExchangeContractErrs.ORDER_FILL_EXPIRED);
}
- if (signedOrder.expirationUnixTimestampSec.lessThan(Date.now() / 1000)) {
- throw new Error(FillOrderValidationErrs.EXPIRED);
+ const zrxTokenAddress = await this.getZRXTokenAddressAsync();
+ await this.validateFillOrderBalancesAndAllowancesAndThrowIfInvalidAsync(signedOrder, fillTakerAmount,
+ senderAddress, zrxTokenAddress);
+
+ const wouldRoundingErrorOccur = await this.isRoundingErrorAsync(
+ signedOrder.takerTokenAmount, fillTakerAmount, signedOrder.makerTokenAmount,
+ );
+ if (wouldRoundingErrorOccur) {
+ throw new Error(ExchangeContractErrs.ORDER_FILL_ROUNDING_ERROR);
}
+ }
+
+ /**
+ * This method does not currently validate the edge-case where the makerToken or takerToken is also the token used
+ * to pay fees (ZRX). It is possible for them to have enough for fees and the transfer but not both.
+ * Handling the edge-cases that arise when this happens would require making sure that the user has sufficient
+ * funds to pay both the fees and the transfer amount. We decided to punt on this for now as the contracts
+ * will throw for these edge-cases.
+ * TODO: Throw errors before calling the smart contract for these edge-cases
+ * TODO: in order to minimize the callers gas costs.
+ */
+ private async validateFillOrderBalancesAndAllowancesAndThrowIfInvalidAsync(signedOrder: SignedOrder,
+ fillTakerAmount: BigNumber.BigNumber,
+ senderAddress: string,
+ zrxTokenAddress: string): Promise<void> {
+
const makerBalance = await this.tokenWrapper.getBalanceAsync(signedOrder.makerTokenAddress,
- signedOrder.maker);
+ signedOrder.maker);
const takerBalance = await this.tokenWrapper.getBalanceAsync(signedOrder.takerTokenAddress, senderAddress);
const makerAllowance = await this.tokenWrapper.getProxyAllowanceAsync(signedOrder.makerTokenAddress,
signedOrder.maker);
const takerAllowance = await this.tokenWrapper.getProxyAllowanceAsync(signedOrder.takerTokenAddress,
senderAddress);
- // How many taker tokens would you get for 1 maker token;
+
+ // exchangeRate is the price of one maker token denominated in taker tokens
const exchangeRate = signedOrder.takerTokenAmount.div(signedOrder.makerTokenAmount);
- const fillMakerAmountInBaseUnits = fillTakerAmountInBaseUnits.div(exchangeRate);
+ const fillMakerAmountInBaseUnits = fillTakerAmount.div(exchangeRate);
- if (fillTakerAmountInBaseUnits.greaterThan(takerBalance)) {
- throw new Error(FillOrderValidationErrs.NOT_ENOUGH_TAKER_BALANCE);
+ if (fillTakerAmount.greaterThan(takerBalance)) {
+ throw new Error(ExchangeContractErrs.INSUFFICIENT_TAKER_BALANCE);
}
- if (fillTakerAmountInBaseUnits.greaterThan(takerAllowance)) {
- throw new Error(FillOrderValidationErrs.NOT_ENOUGH_TAKER_ALLOWANCE);
+ if (fillTakerAmount.greaterThan(takerAllowance)) {
+ throw new Error(ExchangeContractErrs.INSUFFICIENT_TAKER_ALLOWANCE);
}
if (fillMakerAmountInBaseUnits.greaterThan(makerBalance)) {
- throw new Error(FillOrderValidationErrs.NOT_ENOUGH_MAKER_BALANCE);
+ throw new Error(ExchangeContractErrs.INSUFFICIENT_MAKER_BALANCE);
}
if (fillMakerAmountInBaseUnits.greaterThan(makerAllowance)) {
- throw new Error(FillOrderValidationErrs.NOT_ENOUGH_MAKER_ALLOWANCE);
+ throw new Error(ExchangeContractErrs.INSUFFICIENT_MAKER_ALLOWANCE);
+ }
+
+ const makerFeeBalance = await this.tokenWrapper.getBalanceAsync(zrxTokenAddress,
+ signedOrder.maker);
+ const takerFeeBalance = await this.tokenWrapper.getBalanceAsync(zrxTokenAddress, senderAddress);
+ const makerFeeAllowance = await this.tokenWrapper.getProxyAllowanceAsync(zrxTokenAddress,
+ signedOrder.maker);
+ const takerFeeAllowance = await this.tokenWrapper.getProxyAllowanceAsync(zrxTokenAddress,
+ senderAddress);
+
+ if (signedOrder.takerFee.greaterThan(takerFeeBalance)) {
+ throw new Error(ExchangeContractErrs.INSUFFICIENT_TAKER_FEE_BALANCE);
+ }
+ if (signedOrder.takerFee.greaterThan(takerFeeAllowance)) {
+ throw new Error(ExchangeContractErrs.INSUFFICIENT_TAKER_FEE_ALLOWANCE);
+ }
+ if (signedOrder.makerFee.greaterThan(makerFeeBalance)) {
+ throw new Error(ExchangeContractErrs.INSUFFICIENT_MAKER_FEE_BALANCE);
+ }
+ if (signedOrder.makerFee.greaterThan(makerFeeAllowance)) {
+ throw new Error(ExchangeContractErrs.INSUFFICIENT_MAKER_FEE_ALLOWANCE);
}
}
private throwErrorLogsAsErrors(logs: ContractEvent[]): void {
@@ -202,6 +253,18 @@ export class ExchangeWrapper extends ContractWrapper {
throw new Error(errMessage);
}
}
+ private async isRoundingErrorAsync(takerTokenAmount: BigNumber.BigNumber,
+ fillTakerAmount: BigNumber.BigNumber,
+ makerTokenAmount: BigNumber.BigNumber): Promise<boolean> {
+ const exchangeInstance = await this.getExchangeContractAsync();
+ const senderAddress = await this.web3Wrapper.getSenderAddressOrThrowAsync();
+ const isRoundingError = await exchangeInstance.isRoundingError.call(
+ takerTokenAmount, fillTakerAmount, makerTokenAmount, {
+ from: senderAddress,
+ },
+ );
+ return isRoundingError;
+ }
private async getExchangeContractAsync(): Promise<ExchangeContract> {
if (!_.isUndefined(this.exchangeContractIfExists)) {
return this.exchangeContractIfExists;
@@ -210,4 +273,8 @@ export class ExchangeWrapper extends ContractWrapper {
this.exchangeContractIfExists = contractInstance as ExchangeContract;
return this.exchangeContractIfExists;
}
+ private async getZRXTokenAddressAsync(): Promise<string> {
+ const exchangeInstance = await this.getExchangeContractAsync();
+ return exchangeInstance.ZRX.call();
+ }
}
diff --git a/src/types.ts b/src/types.ts
index 29f7e0ee4..46156b155 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -10,11 +10,12 @@ function strEnum(values: string[]): {[key: string]: string} {
}
export const ZeroExError = strEnum([
- 'CONTRACT_DOES_NOT_EXIST',
- 'UNHANDLED_ERROR',
- 'USER_HAS_NO_ASSOCIATED_ADDRESSES',
- 'INVALID_SIGNATURE',
- 'CONTRACT_NOT_DEPLOYED_ON_NETWORK',
+ 'CONTRACT_DOES_NOT_EXIST',
+ 'UNHANDLED_ERROR',
+ 'USER_HAS_NO_ASSOCIATED_ADDRESSES',
+ 'INVALID_SIGNATURE',
+ 'CONTRACT_NOT_DEPLOYED_ON_NETWORK',
+ 'ZRX_NOT_IN_TOKEN_REGISTRY',
]);
export type ZeroExError = keyof typeof ZeroExError;
@@ -37,6 +38,10 @@ export interface ExchangeContract {
getUnavailableValueT: {
call: (orderHash: string) => BigNumber.BigNumber;
};
+ isRoundingError: {
+ call: (takerTokenAmount: BigNumber.BigNumber, fillTakerAmount: BigNumber.BigNumber,
+ makerTokenAmount: BigNumber.BigNumber, txOpts: TxOpts) => Promise<boolean>;
+ };
fill: {
(orderAddresses: OrderAddresses, orderValues: OrderValues, fillAmount: BigNumber.BigNumber,
shouldCheckTransfer: boolean, v: number, r: string, s: string, txOpts: TxOpts): ContractResponse;
@@ -49,6 +54,9 @@ export interface ExchangeContract {
cancelled: {
call: (orderHash: string) => BigNumber.BigNumber;
};
+ ZRX: {
+ call: () => Promise<string>;
+ };
}
export interface TokenContract {
@@ -87,23 +95,22 @@ export enum ExchangeContractErrCodes {
}
export const ExchangeContractErrs = strEnum([
- 'ORDER_EXPIRED',
+ 'ORDER_FILL_EXPIRED',
'ORDER_REMAINING_FILL_AMOUNT_ZERO',
- 'ORDER_ROUNDING_ERROR',
- 'ORDER_BALANCE_ALLOWANCE_ERROR',
-]);
-export type ExchangeContractErrs = keyof typeof ExchangeContractErrs;
+ 'ORDER_FILL_ROUNDING_ERROR',
+ 'FILL_BALANCE_ALLOWANCE_ERROR',
+ 'INSUFFICIENT_TAKER_BALANCE',
+ 'INSUFFICIENT_TAKER_ALLOWANCE',
+ 'INSUFFICIENT_MAKER_BALANCE',
+ 'INSUFFICIENT_MAKER_ALLOWANCE',
+ 'INSUFFICIENT_TAKER_FEE_BALANCE',
+ 'INSUFFICIENT_TAKER_FEE_ALLOWANCE',
+ 'INSUFFICIENT_MAKER_FEE_BALANCE',
+ 'INSUFFICIENT_MAKER_FEE_ALLOWANCE',
+ 'TRANSACTION_SENDER_IS_NOT_FILL_ORDER_TAKER',
-export const FillOrderValidationErrs = strEnum([
- 'FILL_AMOUNT_IS_ZERO',
- 'NOT_A_TAKER',
- 'EXPIRED',
- 'NOT_ENOUGH_TAKER_BALANCE',
- 'NOT_ENOUGH_TAKER_ALLOWANCE',
- 'NOT_ENOUGH_MAKER_BALANCE',
- 'NOT_ENOUGH_MAKER_ALLOWANCE',
]);
-export type FillOrderValidationErrs = keyof typeof FillOrderValidationErrs;
+export type ExchangeContractErrs = keyof typeof ExchangeContractErrs;
export interface ContractResponse {
logs: ContractEvent[];
@@ -147,7 +154,3 @@ export interface TxOpts {
from: string;
gas?: number;
}
-
-export interface TokenAddressBySymbol {
- [symbol: string]: string;
-}
diff --git a/src/utils/schema_validator.ts b/src/utils/schema_validator.ts
index db8a960ba..932ddf62a 100644
--- a/src/utils/schema_validator.ts
+++ b/src/utils/schema_validator.ts
@@ -6,7 +6,7 @@ import {tokenSchema} from '../schemas/token_schema';
export class SchemaValidator {
private validator: Validator;
// In order to validate a complex JS object using jsonschema, we must replace any complex
- // sub-types (e.g BigNumber) with a simpler string represenation. Since BigNumber and other
+ // sub-types (e.g BigNumber) with a simpler string representation. Since BigNumber and other
// complex types implement the `toString` method, we can stringify the object and
// then parse it. The resultant object can then be checked using jsonschema.
public static convertToJSONSchemaCompatibleObject(obj: object): object {