From 918315e89f3408124d2e78bbd1acb58ed42d1766 Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Wed, 7 Jun 2017 12:40:58 +0200 Subject: Implement fillOrKill & tests --- src/0x.js.ts | 27 ++------ src/contract_wrappers/exchange_wrapper.ts | 105 +++++++++++++++++++++++++----- src/globals.d.ts | 4 +- src/types.ts | 15 +++-- src/utils/constants.ts | 1 + src/utils/utils.ts | 24 +++++++ 6 files changed, 131 insertions(+), 45 deletions(-) (limited to 'src') diff --git a/src/0x.js.ts b/src/0x.js.ts index 0f437e039..6d66c9d86 100644 --- a/src/0x.js.ts +++ b/src/0x.js.ts @@ -4,7 +4,6 @@ import {bigNumberConfigs} from './bignumber_config'; import * as ethUtil from 'ethereumjs-util'; import contract = require('truffle-contract'); import * as Web3 from 'web3'; -import * as ethABI from 'ethereumjs-abi'; import findVersions = require('find-versions'); import compareVersions = require('compare-versions'); import {Web3Wrapper} from './web3_wrapper'; @@ -16,8 +15,7 @@ import {ExchangeWrapper} from './contract_wrappers/exchange_wrapper'; import {TokenRegistryWrapper} from './contract_wrappers/token_registry_wrapper'; import {ecSignatureSchema} from './schemas/ec_signature_schema'; import {TokenWrapper} from './contract_wrappers/token_wrapper'; -import {SolidityTypes, ECSignature, ZeroExError} from './types'; -import {Order, SignedOrder} from './types'; +import {SolidityTypes, ECSignature, ZeroExError, Order, SignedOrder} from './types'; import {orderSchema} from './schemas/order_schemas'; import * as ExchangeArtifacts from './artifacts/Exchange.json'; @@ -132,30 +130,13 @@ export class ZeroEx { * Computes the orderHash for a given order and returns it as a hex encoded string. */ public async getOrderHashHexAsync(order: Order|SignedOrder): Promise { - const exchangeContractAddr = await this.getExchangeAddressAsync(); assert.doesConformToSchema('order', SchemaValidator.convertToJSONSchemaCompatibleObject(order as object), orderSchema); - const orderParts = [ - {value: exchangeContractAddr, type: SolidityTypes.address}, - {value: order.maker, type: SolidityTypes.address}, - {value: order.taker, type: SolidityTypes.address}, - {value: order.makerTokenAddress, type: SolidityTypes.address}, - {value: order.takerTokenAddress, type: SolidityTypes.address}, - {value: order.feeRecipient, type: SolidityTypes.address}, - {value: utils.bigNumberToBN(order.makerTokenAmount), type: SolidityTypes.uint256}, - {value: utils.bigNumberToBN(order.takerTokenAmount), type: SolidityTypes.uint256}, - {value: utils.bigNumberToBN(order.makerFee), type: SolidityTypes.uint256}, - {value: utils.bigNumberToBN(order.takerFee), type: SolidityTypes.uint256}, - {value: utils.bigNumberToBN(order.expirationUnixTimestampSec), type: SolidityTypes.uint256}, - {value: utils.bigNumberToBN(order.salt), type: SolidityTypes.uint256}, - ]; - const types = _.map(orderParts, o => o.type); - const values = _.map(orderParts, o => o.value); - const hashBuff = ethABI.soliditySHA3(types, values); - const hashHex = ethUtil.bufferToHex(hashBuff); - return hashHex; + const exchangeContractAddr = await this.getExchangeAddressAsync(); + const orderHash = utils.getOrderHashHex(order, exchangeContractAddr); + return orderHash; } /** * Signs an orderHash and returns it's elliptic curve signature diff --git a/src/contract_wrappers/exchange_wrapper.ts b/src/contract_wrappers/exchange_wrapper.ts index d3a53a9f7..55ff9068e 100644 --- a/src/contract_wrappers/exchange_wrapper.ts +++ b/src/contract_wrappers/exchange_wrapper.ts @@ -7,6 +7,7 @@ import { ExchangeContract, ExchangeContractErrCodes, ExchangeContractErrs, + Order, OrderValues, OrderAddresses, SignedOrder, @@ -126,21 +127,9 @@ export class ExchangeWrapper extends ContractWrapper { const exchangeInstance = await this.getExchangeContractAsync(); await this.validateFillOrderAndThrowIfInvalidAsync(signedOrder, fillTakerAmount, takerAddress); - 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 orderAddresses = this.getOrderAddresses(signedOrder); + const orderValues = this.getOrderValues(signedOrder); + const gas = await exchangeInstance.fill.estimateGas( orderAddresses, orderValues, @@ -168,6 +157,67 @@ export class ExchangeWrapper extends ContractWrapper { ); this.throwErrorLogsAsErrors(response.logs); } + /** + * Attempts to fill a specific amount of an order. If the entire amount specified cannot be filled, + * the fill order is abandoned. + */ + public async fillOrKillOrderAsync(signedOrder: SignedOrder, fillTakerAmount: BigNumber.BigNumber, + shouldCheckTransfer: boolean, takerAddress: string) { + assert.doesConformToSchema('signedOrder', + SchemaValidator.convertToJSONSchemaCompatibleObject(signedOrder as object), + signedOrderSchema); + assert.isBigNumber('fillTakerAmount', fillTakerAmount); + assert.isBoolean('shouldCheckTransfer', shouldCheckTransfer); + await assert.isSenderAddressAsync('takerAddress', takerAddress, this.web3Wrapper); + + const exchangeInstance = await this.getExchangeContractAsync(); + await this.validateFillOrderAndThrowIfInvalidAsync(signedOrder, fillTakerAmount, takerAddress); + + // Check that fillValue available >= fillTakerAmount + const orderHashHex = utils.getOrderHashHex(signedOrder, exchangeInstance.address); + const unavailableTakerAmount = await this.getUnavailableTakerAmountAsync(orderHashHex); + const remainingTakerAmount = signedOrder.takerTokenAmount.minus(unavailableTakerAmount); + if (remainingTakerAmount < fillTakerAmount) { + throw new Error(ExchangeContractErrs.INSUFFICIENT_REMAINING_FILL_AMOUNT); + } + + const orderAddresses = this.getOrderAddresses(signedOrder); + const orderValues = this.getOrderValues(signedOrder); + + const gas = await exchangeInstance.fillOrKill.estimateGas( + orderAddresses, + orderValues, + fillTakerAmount, + signedOrder.ecSignature.v, + signedOrder.ecSignature.r, + signedOrder.ecSignature.s, + { + from: takerAddress, + }, + ); + try { + const response: ContractResponse = await exchangeInstance.fillOrKill( + orderAddresses, + orderValues, + fillTakerAmount, + signedOrder.ecSignature.v, + signedOrder.ecSignature.r, + signedOrder.ecSignature.s, + { + from: takerAddress, + gas, + }, + ); + this.throwErrorLogsAsErrors(response.logs); + } catch (err) { + // There is a potential race condition where when the cancellation is broadcasted, a sufficient + // fillAmount is available, but by the time the transaction gets mined, it no longer is. Instead of + // throwing an invalid jump exception, we would rather give the user a more helpful error message. + if (_.includes(err, constants.INVALID_JUMP_IDENTIFIER)) { + throw new Error(ZeroExError.INSUFFICIENT_REMAINING_FILL_AMOUNT); + } + } + } /** * Subscribe to an event type emitted by the Exchange smart contract */ @@ -232,8 +282,8 @@ export class ExchangeWrapper extends ContractWrapper { * 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. + * TODO: Throw errors before calling the smart contract for these edge-cases in order to minimize + * the callers gas costs. */ private async validateFillOrderBalancesAndAllowancesAndThrowIfInvalidAsync(signedOrder: SignedOrder, fillTakerAmount: BigNumber.BigNumber, @@ -316,4 +366,25 @@ export class ExchangeWrapper extends ContractWrapper { const exchangeInstance = await this.getExchangeContractAsync(); return exchangeInstance.ZRX.call(); } + private getOrderAddresses(order: Order|SignedOrder) { + const orderAddresses: OrderAddresses = [ + order.maker, + order.taker, + order.makerTokenAddress, + order.takerTokenAddress, + order.feeRecipient, + ]; + return orderAddresses; + } + private getOrderValues(order: Order|SignedOrder) { + const orderValues: OrderValues = [ + order.makerTokenAmount, + order.takerTokenAmount, + order.makerFee, + order.takerFee, + order.expirationUnixTimestampSec, + order.salt, + ]; + return orderValues; + } } diff --git a/src/globals.d.ts b/src/globals.d.ts index 164fc2386..567ba016d 100644 --- a/src/globals.d.ts +++ b/src/globals.d.ts @@ -47,7 +47,9 @@ declare module 'ethereumjs-util' { } // truffle-contract declarations -declare interface ContractInstance {} +declare interface ContractInstance { + address: string; +} declare interface ContractFactory { setProvider: (providerObj: any) => void; deployed: () => ContractInstance; diff --git a/src/types.ts b/src/types.ts index a02bd0252..49d8365d1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -44,7 +44,8 @@ export interface ContractEventObj { } export type CreateContractEvent = (indexFilterValues: IndexFilterValues, subscriptionOpts: SubscriptionOpts) => ContractEventObj; -export interface ExchangeContract { + +export interface ExchangeContract extends ContractInstance { isValidSignature: { call: (signerAddressHex: string, dataHex: string, v: number, r: string, s: string, txOpts?: TxOpts) => Promise; @@ -68,6 +69,12 @@ export interface ExchangeContract { estimateGas: (orderAddresses: OrderAddresses, orderValues: OrderValues, fillAmount: BigNumber.BigNumber, shouldCheckTransfer: boolean, v: number, r: string, s: string, txOpts: TxOpts) => number; }; + fillOrKill: { + (orderAddresses: OrderAddresses, orderValues: OrderValues, fillAmount: BigNumber.BigNumber, + v: number, r: string, s: string, txOpts: TxOpts): ContractResponse; + estimateGas: (orderAddresses: OrderAddresses, orderValues: OrderValues, fillAmount: BigNumber.BigNumber, + v: number, r: string, s: string, txOpts: TxOpts) => number; + }; filled: { call: (orderHash: string) => BigNumber.BigNumber; }; @@ -76,7 +83,7 @@ export interface ExchangeContract { }; } -export interface TokenContract { +export interface TokenContract extends ContractInstance { balanceOf: { call: (address: string) => Promise; }; @@ -89,7 +96,7 @@ export interface TokenContract { approve: (proxyAddress: string, amountInBaseUnits: BigNumber.BigNumber, txOpts: TxOpts) => void; } -export interface TokenRegistryContract { +export interface TokenRegistryContract extends ContractInstance { getTokenMetaData: { call: (address: string) => Promise; }; @@ -127,7 +134,7 @@ export const ExchangeContractErrs = strEnum([ 'INSUFFICIENT_MAKER_FEE_BALANCE', 'INSUFFICIENT_MAKER_FEE_ALLOWANCE', 'TRANSACTION_SENDER_IS_NOT_FILL_ORDER_TAKER', - + 'INSUFFICIENT_REMAINING_FILL_AMOUNT', ]); export type ExchangeContractErrs = keyof typeof ExchangeContractErrs; diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 5a5ba0e0a..ebc8586f3 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -1,4 +1,5 @@ export const constants = { NULL_ADDRESS: '0x0000000000000000000000000000000000000000', TESTRPC_NETWORK_ID: 50, + INVALID_JUMP_IDENTIFIER: 'invalid JUMP at', }; diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 114b46f6c..1d2e2f908 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -1,5 +1,8 @@ import * as _ from 'lodash'; import * as BN from 'bn.js'; +import * as ethUtil from 'ethereumjs-util'; +import * as ethABI from 'ethereumjs-abi'; +import {SolidityTypes, Order} from '../types'; export const utils = { /** @@ -22,6 +25,27 @@ export const utils = { const isValid = /^0x[0-9A-F]{64}$/i.test(orderHashHex); return isValid; }, + getOrderHashHex(order: Order, exchangeContractAddr: string) { + const orderParts = [ + {value: exchangeContractAddr, type: SolidityTypes.address}, + {value: order.maker, type: SolidityTypes.address}, + {value: order.taker, type: SolidityTypes.address}, + {value: order.makerTokenAddress, type: SolidityTypes.address}, + {value: order.takerTokenAddress, type: SolidityTypes.address}, + {value: order.feeRecipient, type: SolidityTypes.address}, + {value: utils.bigNumberToBN(order.makerTokenAmount), type: SolidityTypes.uint256}, + {value: utils.bigNumberToBN(order.takerTokenAmount), type: SolidityTypes.uint256}, + {value: utils.bigNumberToBN(order.makerFee), type: SolidityTypes.uint256}, + {value: utils.bigNumberToBN(order.takerFee), type: SolidityTypes.uint256}, + {value: utils.bigNumberToBN(order.expirationUnixTimestampSec), type: SolidityTypes.uint256}, + {value: utils.bigNumberToBN(order.salt), type: SolidityTypes.uint256}, + ]; + const types = _.map(orderParts, o => o.type); + const values = _.map(orderParts, o => o.value); + const hashBuff = ethABI.soliditySHA3(types, values); + const hashHex = ethUtil.bufferToHex(hashBuff); + return hashHex; + }, spawnSwitchErr(name: string, value: any) { return new Error(`Unexpected switch value: ${value} encountered for ${name}`); }, -- cgit v1.2.3