aboutsummaryrefslogtreecommitdiffstats
path: root/src/contract_wrappers/exchange_wrapper.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/contract_wrappers/exchange_wrapper.ts')
-rw-r--r--src/contract_wrappers/exchange_wrapper.ts209
1 files changed, 201 insertions, 8 deletions
diff --git a/src/contract_wrappers/exchange_wrapper.ts b/src/contract_wrappers/exchange_wrapper.ts
index 3f6eb0dab..4aa532bdd 100644
--- a/src/contract_wrappers/exchange_wrapper.ts
+++ b/src/contract_wrappers/exchange_wrapper.ts
@@ -1,15 +1,39 @@
import * as _ from 'lodash';
import {Web3Wrapper} from '../web3_wrapper';
-import {ECSignature, ZeroExError, ExchangeContract} from '../types';
+import {
+ ECSignature,
+ ExchangeContract,
+ ExchangeContractErrCodes,
+ ExchangeContractErrs,
+ OrderValues,
+ OrderAddresses,
+ SignedOrder,
+ ContractEvent,
+ ContractResponse,
+} from '../types';
import {assert} from '../utils/assert';
import {ContractWrapper} from './contract_wrapper';
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 {constants} from '../utils/constants';
+import {TokenWrapper} from './token_wrapper';
export class ExchangeWrapper extends ContractWrapper {
+ private exchangeContractErrCodesToMsg = {
+ [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_FILL_ROUNDING_ERROR,
+ [ExchangeContractErrCodes.ERROR_FILL_BALANCE_ALLOWANCE]: ExchangeContractErrs.FILL_BALANCE_ALLOWANCE_ERROR,
+ };
private exchangeContractIfExists?: ExchangeContract;
- constructor(web3Wrapper: Web3Wrapper) {
+ private tokenWrapper: TokenWrapper;
+ constructor(web3Wrapper: Web3Wrapper, tokenWrapper: TokenWrapper) {
super(web3Wrapper);
+ this.tokenWrapper = tokenWrapper;
}
public invalidateContractInstance(): void {
delete this.exchangeContractIfExists;
@@ -20,23 +44,188 @@ export class ExchangeWrapper extends ContractWrapper {
assert.doesConformToSchema('ecSignature', ecSignature, ecSignatureSchema);
assert.isETHAddressHex('signerAddressHex', signerAddressHex);
- const senderAddressIfExists = await this.web3Wrapper.getSenderAddressIfExistsAsync();
- assert.assert(!_.isUndefined(senderAddressIfExists), ZeroExError.USER_HAS_NO_ASSOCIATED_ADDRESSES);
+ const senderAddress = await this.web3Wrapper.getSenderAddressOrThrowAsync();
+ const exchangeInstance = await this.getExchangeContractAsync();
- const exchangeContract = await this.getExchangeContractAsync();
-
- const isValidSignature = await exchangeContract.isValidSignature.call(
+ const isValidSignature = await exchangeInstance.isValidSignature.call(
signerAddressHex,
dataHex,
ecSignature.v,
ecSignature.r,
ecSignature.s,
{
- from: senderAddressIfExists,
+ from: senderAddress,
},
);
return isValidSignature;
}
+ /**
+ * 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, fillTakerAmount: BigNumber.BigNumber,
+ shouldCheckTransfer: boolean): Promise<void> {
+ assert.doesConformToSchema('signedOrder',
+ SchemaValidator.convertToJSONSchemaCompatibleObject(signedOrder as object),
+ signedOrderSchema);
+ assert.isBigNumber('fillTakerAmount', fillTakerAmount);
+ assert.isBoolean('shouldCheckTransfer', shouldCheckTransfer);
+
+ const senderAddress = await this.web3Wrapper.getSenderAddressOrThrowAsync();
+ const exchangeInstance = await this.getExchangeContractAsync();
+ await this.validateFillOrderAndThrowIfInvalidAsync(signedOrder, fillTakerAmount, senderAddress);
+
+ const orderAddresses: OrderAddresses = [
+ signedOrder.maker,
+ signedOrder.taker,
+ signedOrder.makerTokenAddress,
+ signedOrder.takerTokenAddress,
+ signedOrder.feeRecipient,
+ ];
+ const orderValues: OrderValues = [
+ signedOrder.makerTokenAmount,
+ signedOrder.takerTokenAmount,
+ signedOrder.makerFee,
+ signedOrder.takerFee,
+ signedOrder.expirationUnixTimestampSec,
+ signedOrder.salt,
+ ];
+ const gas = await exchangeInstance.fill.estimateGas(
+ orderAddresses,
+ orderValues,
+ fillTakerAmount,
+ shouldCheckTransfer,
+ signedOrder.ecSignature.v,
+ signedOrder.ecSignature.r,
+ signedOrder.ecSignature.s,
+ {
+ from: senderAddress,
+ },
+ );
+ const response: ContractResponse = await exchangeInstance.fill(
+ orderAddresses,
+ orderValues,
+ fillTakerAmount,
+ shouldCheckTransfer,
+ signedOrder.ecSignature.v,
+ signedOrder.ecSignature.r,
+ signedOrder.ecSignature.s,
+ {
+ from: senderAddress,
+ gas,
+ },
+ );
+ this.throwErrorLogsAsErrors(response.logs);
+ }
+ 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(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);
+ }
+ 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);
+ 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);
+
+ // exchangeRate is the price of one maker token denominated in taker tokens
+ const exchangeRate = signedOrder.takerTokenAmount.div(signedOrder.makerTokenAmount);
+ const fillMakerAmountInBaseUnits = fillTakerAmount.div(exchangeRate);
+
+ if (fillTakerAmount.greaterThan(takerBalance)) {
+ throw new Error(ExchangeContractErrs.INSUFFICIENT_TAKER_BALANCE);
+ }
+ if (fillTakerAmount.greaterThan(takerAllowance)) {
+ throw new Error(ExchangeContractErrs.INSUFFICIENT_TAKER_ALLOWANCE);
+ }
+ if (fillMakerAmountInBaseUnits.greaterThan(makerBalance)) {
+ throw new Error(ExchangeContractErrs.INSUFFICIENT_MAKER_BALANCE);
+ }
+ if (fillMakerAmountInBaseUnits.greaterThan(makerAllowance)) {
+ 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 {
+ const errEvent = _.find(logs, {event: 'LogError'});
+ if (!_.isUndefined(errEvent)) {
+ const errCode = errEvent.args.errorId.toNumber();
+ const errMessage = this.exchangeContractErrCodesToMsg[errCode];
+ 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;
@@ -45,4 +234,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();
+ }
}