diff options
20 files changed, 1172 insertions, 121 deletions
diff --git a/packages/contracts/src/abstract/abstract_asset_wrapper.ts b/packages/contracts/src/abstract/abstract_asset_wrapper.ts new file mode 100644 index 000000000..d7ab58fea --- /dev/null +++ b/packages/contracts/src/abstract/abstract_asset_wrapper.ts @@ -0,0 +1,10 @@ +import { BigNumber } from '@0xproject/utils'; + +export abstract class AbstractAssetWrapper { + public abstract getProxyId(): number; + public abstract async setBalancesAndAllowancesAsync(): Promise<void>; + public abstract async getBalanceAsync(owner: string, assetData: string): Promise<BigNumber>; + public abstract async getProxyAllowanceAsync(owner: string, assetData: string): Promise<BigNumber>; + public abstract getTokenOwnerAddresses(): string[]; + public abstract getTokenAddresses(): string[]; +} diff --git a/packages/contracts/src/utils/asset_wrapper.ts b/packages/contracts/src/utils/asset_wrapper.ts new file mode 100644 index 000000000..4c345aa30 --- /dev/null +++ b/packages/contracts/src/utils/asset_wrapper.ts @@ -0,0 +1,32 @@ +import { assetProxyUtils } from '@0xproject/order-utils'; +import { BigNumber } from '@0xproject/utils'; +import * as _ from 'lodash'; + +import { AbstractAssetWrapper } from '../abstract/abstract_asset_wrapper'; + +interface ProxyIdToAssetWrappers { + [proxyId: number]: AbstractAssetWrapper; +} + +export class AssetWrapper { + private _proxyIdToAssetWrappers: ProxyIdToAssetWrappers; + constructor(assetWrappers: AbstractAssetWrapper[]) { + this._proxyIdToAssetWrappers = {}; + _.each(assetWrappers, assetWrapper => { + const proxyId = assetWrapper.getProxyId(); + this._proxyIdToAssetWrappers[proxyId] = assetWrapper; + }); + } + public async getBalanceAsync(owner: string, assetData: string): Promise<BigNumber> { + const proxyId = assetProxyUtils.decodeAssetDataId(assetData); + const assetWrapper = this._proxyIdToAssetWrappers[proxyId]; + const balance = await assetWrapper.getBalanceAsync(owner, assetData); + return balance; + } + public async getProxyAllowanceAsync(owner: string, assetData: string): Promise<BigNumber> { + const proxyId = assetProxyUtils.decodeAssetDataId(assetData); + const assetWrapper = this._proxyIdToAssetWrappers[proxyId]; + const balance = await assetWrapper.getProxyAllowanceAsync(owner, assetData); + return balance; + } +} diff --git a/packages/contracts/src/utils/core_combinatorial_utils.ts b/packages/contracts/src/utils/core_combinatorial_utils.ts new file mode 100644 index 000000000..6fbd8304e --- /dev/null +++ b/packages/contracts/src/utils/core_combinatorial_utils.ts @@ -0,0 +1,445 @@ +import { + assetProxyUtils, + BalanceAndProxyAllowanceLazyStore, + ExchangeTransferSimulator, + orderHashUtils, + OrderStateUtils, + OrderValidationUtils, +} from '@0xproject/order-utils'; +import { AssetProxyId, Order, SignatureType, SignedOrder } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import { Web3Wrapper } from '@0xproject/web3-wrapper'; +import * as chai from 'chai'; +import { BlockParamLiteral, LogWithDecodedArgs, Provider, TxData } from 'ethereum-types'; +// import ethUtil = require('ethereumjs-util'); +import * as _ from 'lodash'; +import 'make-promises-safe'; + +import { ExchangeContract, FillContractEventArgs } from '../generated_contract_wrappers/exchange'; +import { artifacts } from '../utils/artifacts'; +import { expectRevertOrAlwaysFailingTransactionAsync } from '../utils/assertions'; +import { AssetWrapper } from '../utils/asset_wrapper'; +import { chaiSetup } from '../utils/chai_setup'; +import { constants } from '../utils/constants'; +import { ERC20Wrapper } from '../utils/erc20_wrapper'; +import { ERC721Wrapper } from '../utils/erc721_wrapper'; +import { ExchangeWrapper } from '../utils/exchange_wrapper'; +import { NewOrderFactory } from '../utils/new_order_factory'; +import { orderUtils } from '../utils/order_utils'; +import { signingUtils } from '../utils/signing_utils'; +import { SimpleAssetBalanceAndProxyAllowanceFetcher } from '../utils/simple_asset_balance_and_proxy_allowance_fetcher'; +import { SimpleOrderFilledCancelledFetcher } from '../utils/simple_order_filled_cancelled_fetcher'; +import { + AssetDataScenario, + ExpirationTimeSecondsScenario, + FeeRecipientAddressScenario, + OrderAmountScenario, + OrderScenario, +} from '../utils/types'; + +chaiSetup.configure(); +const expect = chai.expect; + +const ERC721_PROXY_ID = 2; + +/** + * Instantiates a new instance of CoreCombinatorialUtils. Since this method has some + * required async setup, a factory method is required. + * @param web3Wrapper Web3Wrapper instance + * @param txDefaults Default Ethereum tx options + * @return CoreCombinatorialUtils instance + */ +export async function coreCombinatorialUtilsFactoryAsync( + web3Wrapper: Web3Wrapper, + txDefaults: Partial<TxData>, +): Promise<CoreCombinatorialUtils> { + const userAddresses = await web3Wrapper.getAvailableAddressesAsync(); + const [ownerAddress, makerAddress, takerAddress] = userAddresses; + const makerPrivateKey = constants.TESTRPC_PRIVATE_KEYS[userAddresses.indexOf(makerAddress)]; + + const provider = web3Wrapper.getProvider(); + const erc20Wrapper = new ERC20Wrapper(provider, userAddresses, ownerAddress); + const erc721Wrapper = new ERC721Wrapper(provider, userAddresses, ownerAddress); + + const erc20EighteenDecimalTokenCount = 3; + const eighteenDecimals = new BigNumber(18); + const [ + erc20EighteenDecimalTokenA, + erc20EighteenDecimalTokenB, + zrxToken, + ] = await erc20Wrapper.deployDummyTokensAsync(erc20EighteenDecimalTokenCount, eighteenDecimals); + const zrxAssetData = assetProxyUtils.encodeERC20AssetData(zrxToken.address); + + const erc20FiveDecimalTokenCount = 2; + const fiveDecimals = new BigNumber(18); + const [erc20FiveDecimalTokenA, erc20FiveDecimalTokenB] = await erc20Wrapper.deployDummyTokensAsync( + erc20FiveDecimalTokenCount, + fiveDecimals, + ); + const erc20Proxy = await erc20Wrapper.deployProxyAsync(); + await erc20Wrapper.setBalancesAndAllowancesAsync(); + + const [erc721Token] = await erc721Wrapper.deployDummyTokensAsync(); + const erc721Proxy = await erc721Wrapper.deployProxyAsync(); + await erc721Wrapper.setBalancesAndAllowancesAsync(); + const erc721Balances = await erc721Wrapper.getBalancesAsync(); + + const assetWrapper = new AssetWrapper([erc20Wrapper, erc721Wrapper]); + + const exchangeContract = await ExchangeContract.deployFrom0xArtifactAsync( + artifacts.Exchange, + provider, + txDefaults, + zrxAssetData, + ); + const exchangeWrapper = new ExchangeWrapper(exchangeContract, provider); + await exchangeWrapper.registerAssetProxyAsync(AssetProxyId.ERC20, erc20Proxy.address, ownerAddress); + await exchangeWrapper.registerAssetProxyAsync(AssetProxyId.ERC721, erc721Proxy.address, ownerAddress); + + await web3Wrapper.awaitTransactionSuccessAsync( + await erc20Proxy.addAuthorizedAddress.sendTransactionAsync(exchangeContract.address, { + from: ownerAddress, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await erc721Proxy.addAuthorizedAddress.sendTransactionAsync(exchangeContract.address, { + from: ownerAddress, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + + const orderFactory = new NewOrderFactory( + userAddresses, + zrxToken.address, + [erc20EighteenDecimalTokenA.address, erc20EighteenDecimalTokenB.address], + [erc20FiveDecimalTokenA.address, erc20FiveDecimalTokenB.address], + erc721Token, + erc721Balances, + exchangeContract.address, + ); + + const coreCombinatorialUtils = new CoreCombinatorialUtils( + orderFactory, + ownerAddress, + makerAddress, + makerPrivateKey, + takerAddress, + zrxAssetData, + exchangeWrapper, + assetWrapper, + ); + return coreCombinatorialUtils; +} + +export class CoreCombinatorialUtils { + public orderFactory: NewOrderFactory; + public ownerAddress: string; + public makerAddress: string; + public makerPrivateKey: Buffer; + public takerAddress: string; + public zrxAssetData: string; + public exchangeWrapper: ExchangeWrapper; + public assetWrapper: AssetWrapper; + public static generateOrderCombinations(): OrderScenario[] { + const feeRecipientScenarios = [FeeRecipientAddressScenario.EthUserAddress]; + const makerAssetAmountScenario = [OrderAmountScenario.NonZero]; + const takerAssetAmountScenario = [OrderAmountScenario.NonZero]; + const makerFeeScenario = [OrderAmountScenario.NonZero]; + const takerFeeScenario = [OrderAmountScenario.NonZero]; + const expirationTimeSecondsScenario = [ExpirationTimeSecondsScenario.InFuture]; + const makerAssetDataScenario = [ + AssetDataScenario.ERC20FiveDecimals, + AssetDataScenario.ERC20NonZRXEighteenDecimals, + AssetDataScenario.ERC721, + AssetDataScenario.ZRXFeeToken, + ]; + const takerAssetDataScenario = [ + AssetDataScenario.ERC20FiveDecimals, + AssetDataScenario.ERC20NonZRXEighteenDecimals, + AssetDataScenario.ERC721, + AssetDataScenario.ZRXFeeToken, + ]; + const orderScenarioArrays = CoreCombinatorialUtils._allPossibleCases([ + feeRecipientScenarios, + makerAssetAmountScenario, + takerAssetAmountScenario, + makerFeeScenario, + takerFeeScenario, + expirationTimeSecondsScenario, + makerAssetDataScenario, + takerAssetDataScenario, + ]); + + const orderScenarios = _.map(orderScenarioArrays, orderScenarioArray => { + const orderScenario: OrderScenario = { + feeRecipientScenario: orderScenarioArray[0] as FeeRecipientAddressScenario, + makerAssetAmountScenario: orderScenarioArray[1] as OrderAmountScenario, + takerAssetAmountScenario: orderScenarioArray[2] as OrderAmountScenario, + makerFeeScenario: orderScenarioArray[3] as OrderAmountScenario, + takerFeeScenario: orderScenarioArray[4] as OrderAmountScenario, + expirationTimeSecondsScenario: orderScenarioArray[5] as ExpirationTimeSecondsScenario, + makerAssetDataScenario: orderScenarioArray[6] as AssetDataScenario, + takerAssetDataScenario: orderScenarioArray[7] as AssetDataScenario, + }; + return orderScenario; + }); + + return orderScenarios; + } + private static _allPossibleCases(arrays: string[][]): string[][] { + if (arrays.length === 1) { + const remainingVals = _.map(arrays[0], val => { + return [val]; + }); + return remainingVals; + } else { + const result = []; + const allCasesOfRest = CoreCombinatorialUtils._allPossibleCases(arrays.slice(1)); // recur with the rest of array + // tslint:disable:prefer-for-of + for (let i = 0; i < allCasesOfRest.length; i++) { + for (let j = 0; j < arrays[0].length; j++) { + result.push([arrays[0][j], ...allCasesOfRest[i]]); + } + } + // tslint:enable:prefer-for-of + return result; + } + } + constructor( + orderFactory: NewOrderFactory, + ownerAddress: string, + makerAddress: string, + makerPrivateKey: Buffer, + takerAddress: string, + zrxAssetData: string, + exchangeWrapper: ExchangeWrapper, + assetWrapper: AssetWrapper, + ) { + this.orderFactory = orderFactory; + this.ownerAddress = ownerAddress; + this.makerAddress = makerAddress; + this.makerPrivateKey = makerPrivateKey; + this.takerAddress = takerAddress; + this.zrxAssetData = zrxAssetData; + this.exchangeWrapper = exchangeWrapper; + this.assetWrapper = assetWrapper; + } + public async testFillOrderScenarioAsync(order: Order, provider: Provider): Promise<void> { + // 2. Sign order + const orderHashBuff = orderHashUtils.getOrderHashBuff(order); + const signature = signingUtils.signMessage(orderHashBuff, this.makerPrivateKey, SignatureType.EthSign); + const signedOrder = { + ...order, + signature: `0x${signature.toString('hex')}`, + }; + + // 3. Permutate the maker and taker balance/allowance scenarios + + // 4. Figure out fill amount OR error + const balanceAndProxyAllowanceFetcher = new SimpleAssetBalanceAndProxyAllowanceFetcher(this.assetWrapper); + const orderFilledCancelledFetcher = new SimpleOrderFilledCancelledFetcher( + this.exchangeWrapper, + this.zrxAssetData, + ); + const orderStateUtils = new OrderStateUtils(balanceAndProxyAllowanceFetcher, orderFilledCancelledFetcher); + + const fillableTakerAssetAmount = await orderStateUtils.getMaxFillableTakerAssetAmountAsync( + signedOrder, + this.takerAddress, + ); + + // If order is fillable, decide how much to fill + // TODO: Make this configurable + const takerAssetProxyId = assetProxyUtils.decodeAssetDataId(signedOrder.takerAssetData); + const makerAssetProxyId = assetProxyUtils.decodeAssetDataId(signedOrder.makerAssetData); + const isEitherAssetERC721 = takerAssetProxyId === ERC721_PROXY_ID || makerAssetProxyId === ERC721_PROXY_ID; + const takerAssetFillAmount = isEitherAssetERC721 + ? fillableTakerAssetAmount + : fillableTakerAssetAmount.div(2).floor(); + + // 5. If I fill it by X, what are the resulting balances/allowances/filled amounts exp? + const orderValidationUtils = new OrderValidationUtils(orderFilledCancelledFetcher); + const lazyStore = new BalanceAndProxyAllowanceLazyStore(balanceAndProxyAllowanceFetcher); + const exchangeTransferSimulator = new ExchangeTransferSimulator(lazyStore); + + let isFillFailureExpected = false; + try { + await orderValidationUtils.validateFillOrderThrowIfInvalidAsync( + exchangeTransferSimulator, + provider, + signedOrder, + takerAssetFillAmount, + this.takerAddress, + this.zrxAssetData, + ); + } catch (err) { + isFillFailureExpected = true; + } + + await this._fillOrderAndAssertOutcomeAsync( + signedOrder, + takerAssetFillAmount, + lazyStore, + isFillFailureExpected, + provider, + ); + } + private async _fillOrderAndAssertOutcomeAsync( + signedOrder: SignedOrder, + takerAssetFillAmount: BigNumber, + lazyStore: BalanceAndProxyAllowanceLazyStore, + isFillFailureExpected: boolean, + provider: Provider, + ): Promise<void> { + if (isFillFailureExpected) { + return expectRevertOrAlwaysFailingTransactionAsync( + this.exchangeWrapper.fillOrderAsync(signedOrder, this.takerAddress, { takerAssetFillAmount }), + ); + } + + const makerAddress = signedOrder.makerAddress; + const makerAssetData = signedOrder.makerAssetData; + const takerAssetData = signedOrder.takerAssetData; + const feeRecipient = signedOrder.feeRecipientAddress; + + const expMakerAssetBalanceOfMaker = await lazyStore.getBalanceAsync(makerAssetData, makerAddress); + const expMakerAssetAllowanceOfMaker = await lazyStore.getProxyAllowanceAsync(makerAssetData, makerAddress); + const expTakerAssetBalanceOfMaker = await lazyStore.getBalanceAsync(takerAssetData, makerAddress); + const expZRXAssetBalanceOfMaker = await lazyStore.getBalanceAsync(this.zrxAssetData, makerAddress); + const expZRXAssetAllowanceOfMaker = await lazyStore.getProxyAllowanceAsync(this.zrxAssetData, makerAddress); + const expTakerAssetBalanceOfTaker = await lazyStore.getBalanceAsync(takerAssetData, this.takerAddress); + const expTakerAssetAllowanceOfTaker = await lazyStore.getProxyAllowanceAsync(takerAssetData, this.takerAddress); + const expMakerAssetBalanceOfTaker = await lazyStore.getBalanceAsync(makerAssetData, this.takerAddress); + const expZRXAssetBalanceOfTaker = await lazyStore.getBalanceAsync(this.zrxAssetData, this.takerAddress); + const expZRXAssetAllowanceOfTaker = await lazyStore.getProxyAllowanceAsync( + this.zrxAssetData, + this.takerAddress, + ); + const expZRXAssetBalanceOfFeeRecipient = await lazyStore.getBalanceAsync(this.zrxAssetData, feeRecipient); + + const orderHash = orderHashUtils.getOrderHashHex(signedOrder); + const initialFilledTakerAmount = await this.exchangeWrapper.getTakerAssetFilledAmountAsync(orderHash); + const expFilledTakerAmount = initialFilledTakerAmount.add(takerAssetFillAmount); + + const expFilledMakerAmount = orderUtils.getPartialAmount( + takerAssetFillAmount, + signedOrder.takerAssetAmount, + signedOrder.makerAssetAmount, + ); + + // - Let's fill the order! + const txReceipt = await this.exchangeWrapper.fillOrderAsync(signedOrder, this.takerAddress, { + takerAssetFillAmount, + }); + + const actFilledTakerAmount = await this.exchangeWrapper.getTakerAssetFilledAmountAsync(orderHash); + expect(actFilledTakerAmount).to.be.bignumber.equal(expFilledTakerAmount, 'filledTakerAmount'); + + expect(txReceipt.logs.length).to.be.equal(1, 'logs length'); + // tslint:disable-next-line:no-unnecessary-type-assertion + const log = txReceipt.logs[0] as LogWithDecodedArgs<FillContractEventArgs>; + expect(log.args.makerAddress).to.be.equal(makerAddress, 'log.args.makerAddress'); + expect(log.args.takerAddress).to.be.equal(this.takerAddress, 'log.args.this.takerAddress'); + expect(log.args.feeRecipientAddress).to.be.equal(feeRecipient, 'log.args.feeRecipientAddress'); + expect(log.args.makerAssetFilledAmount).to.be.bignumber.equal( + expFilledMakerAmount, + 'log.args.makerAssetFilledAmount', + ); + expect(log.args.takerAssetFilledAmount).to.be.bignumber.equal( + takerAssetFillAmount, + 'log.args.takerAssetFilledAmount', + ); + const expMakerFeePaid = orderUtils.getPartialAmount( + expFilledTakerAmount, + signedOrder.takerAssetAmount, + signedOrder.makerFee, + ); + expect(log.args.makerFeePaid).to.be.bignumber.equal(expMakerFeePaid, 'log.args.makerFeePaid'); + const expTakerFeePaid = orderUtils.getPartialAmount( + expFilledTakerAmount, + signedOrder.takerAssetAmount, + signedOrder.takerFee, + ); + expect(log.args.takerFeePaid).to.be.bignumber.equal(expTakerFeePaid, 'logs.args.takerFeePaid'); + expect(log.args.orderHash).to.be.equal(orderHash, 'log.args.orderHash'); + expect(log.args.makerAssetData).to.be.equal(makerAssetData, 'log.args.makerAssetData'); + expect(log.args.takerAssetData).to.be.equal(takerAssetData, 'log.args.takerAssetData'); + + const actMakerAssetBalanceOfMaker = await this.assetWrapper.getBalanceAsync(makerAddress, makerAssetData); + expect(actMakerAssetBalanceOfMaker).to.be.bignumber.equal( + expMakerAssetBalanceOfMaker, + 'makerAssetBalanceOfMaker', + ); + + const actMakerAssetAllowanceOfMaker = await this.assetWrapper.getProxyAllowanceAsync( + makerAddress, + makerAssetData, + ); + expect(actMakerAssetAllowanceOfMaker).to.be.bignumber.equal( + expMakerAssetAllowanceOfMaker, + 'makerAssetAllowanceOfMaker', + ); + + const actTakerAssetBalanceOfMaker = await this.assetWrapper.getBalanceAsync(makerAddress, takerAssetData); + expect(actTakerAssetBalanceOfMaker).to.be.bignumber.equal( + expTakerAssetBalanceOfMaker, + 'takerAssetBalanceOfMaker', + ); + + const actZRXAssetBalanceOfMaker = await this.assetWrapper.getBalanceAsync(makerAddress, this.zrxAssetData); + expect(actZRXAssetBalanceOfMaker).to.be.bignumber.equal(expZRXAssetBalanceOfMaker, 'ZRXAssetBalanceOfMaker'); + + const actZRXAssetAllowanceOfMaker = await this.assetWrapper.getProxyAllowanceAsync( + makerAddress, + this.zrxAssetData, + ); + expect(actZRXAssetAllowanceOfMaker).to.be.bignumber.equal( + expZRXAssetAllowanceOfMaker, + 'ZRXAssetAllowanceOfMaker', + ); + + const actTakerAssetBalanceOfTaker = await this.assetWrapper.getBalanceAsync(this.takerAddress, takerAssetData); + expect(actTakerAssetBalanceOfTaker).to.be.bignumber.equal( + expTakerAssetBalanceOfTaker, + 'TakerAssetBalanceOfTaker', + ); + + const actTakerAssetAllowanceOfTaker = await this.assetWrapper.getProxyAllowanceAsync( + this.takerAddress, + takerAssetData, + ); + + expect(actTakerAssetAllowanceOfTaker).to.be.bignumber.equal( + expTakerAssetAllowanceOfTaker, + 'TakerAssetAllowanceOfTaker', + ); + + const actMakerAssetBalanceOfTaker = await this.assetWrapper.getBalanceAsync(this.takerAddress, makerAssetData); + expect(actMakerAssetBalanceOfTaker).to.be.bignumber.equal( + expMakerAssetBalanceOfTaker, + 'MakerAssetBalanceOfTaker', + ); + + const actZRXAssetBalanceOfTaker = await this.assetWrapper.getBalanceAsync(this.takerAddress, this.zrxAssetData); + expect(actZRXAssetBalanceOfTaker).to.be.bignumber.equal(expZRXAssetBalanceOfTaker, 'ZRXAssetBalanceOfTaker'); + + const actZRXAssetAllowanceOfTaker = await this.assetWrapper.getProxyAllowanceAsync( + this.takerAddress, + this.zrxAssetData, + ); + expect(actZRXAssetAllowanceOfTaker).to.be.bignumber.equal( + expZRXAssetAllowanceOfTaker, + 'ZRXAssetAllowanceOfTaker', + ); + + const actZRXAssetBalanceOfFeeRecipient = await this.assetWrapper.getBalanceAsync( + feeRecipient, + this.zrxAssetData, + ); + expect(actZRXAssetBalanceOfFeeRecipient).to.be.bignumber.equal( + expZRXAssetBalanceOfFeeRecipient, + 'ZRXAssetBalanceOfFeeRecipient', + ); + } +} diff --git a/packages/contracts/src/utils/erc20_wrapper.ts b/packages/contracts/src/utils/erc20_wrapper.ts index 91c9d50b7..2d367ae1c 100644 --- a/packages/contracts/src/utils/erc20_wrapper.ts +++ b/packages/contracts/src/utils/erc20_wrapper.ts @@ -1,3 +1,4 @@ +import { assetProxyUtils } from '@0xproject/order-utils'; import { BigNumber } from '@0xproject/utils'; import { Web3Wrapper } from '@0xproject/web3-wrapper'; import { Provider } from 'ethereum-types'; @@ -18,6 +19,7 @@ export class ERC20Wrapper { private _provider: Provider; private _dummyTokenContracts: DummyERC20TokenContract[]; private _proxyContract?: ERC20ProxyContract; + private _proxyIdIfExists?: number; constructor(provider: Provider, tokenOwnerAddresses: string[], contractOwnerAddress: string) { this._dummyTokenContracts = []; this._web3Wrapper = new Web3Wrapper(provider); @@ -25,8 +27,11 @@ export class ERC20Wrapper { this._tokenOwnerAddresses = tokenOwnerAddresses; this._contractOwnerAddress = contractOwnerAddress; } - public async deployDummyTokensAsync(): Promise<DummyERC20TokenContract[]> { - for (let i = 0; i < constants.NUM_DUMMY_ERC20_TO_DEPLOY; i++) { + public async deployDummyTokensAsync(num?: number, decimals?: BigNumber): Promise<DummyERC20TokenContract[]> { + // TODO(fabio): Remove and refactor all tests + const finalNum = _.isUndefined(num) ? constants.NUM_DUMMY_ERC20_TO_DEPLOY : num; + const finalDecimals = _.isUndefined(decimals) ? constants.DUMMY_TOKEN_DECIMALS : decimals; + for (let i = 0; i < finalNum; i++) { this._dummyTokenContracts.push( await DummyERC20TokenContract.deployFrom0xArtifactAsync( artifacts.DummyERC20Token, @@ -34,7 +39,7 @@ export class ERC20Wrapper { txDefaults, constants.DUMMY_TOKEN_NAME, constants.DUMMY_TOKEN_SYMBOL, - constants.DUMMY_TOKEN_DECIMALS, + finalDecimals, constants.DUMMY_TOKEN_TOTAL_SUPPLY, ), ); @@ -47,8 +52,13 @@ export class ERC20Wrapper { this._provider, txDefaults, ); + this._proxyIdIfExists = await this._proxyContract.getProxyId.callAsync(); return this._proxyContract; } + public getProxyId(): number { + this._validateProxyContractExistsOrThrow(); + return this._proxyIdIfExists as number; + } public async setBalancesAndAllowancesAsync(): Promise<void> { this._validateDummyTokenContractsExistOrThrow(); this._validateProxyContractExistsOrThrow(); @@ -73,6 +83,29 @@ export class ERC20Wrapper { } } } + public async getBalanceAsync(owner: string, assetData: string): Promise<BigNumber> { + const erc20ProxyData = assetProxyUtils.decodeERC20AssetData(assetData); + const tokenAddress = erc20ProxyData.tokenAddress; + const tokenContractIfExists = _.find(this._dummyTokenContracts, c => c.address === tokenAddress); + if (_.isUndefined(tokenContractIfExists)) { + throw new Error(`Token: ${tokenAddress} was not deployed through ERC20Wrapper`); + } + const balance = new BigNumber(await tokenContractIfExists.balanceOf.callAsync(owner)); + return balance; + } + public async getProxyAllowanceAsync(owner: string, assetData: string): Promise<BigNumber> { + const erc20ProxyData = assetProxyUtils.decodeERC20AssetData(assetData); + const tokenAddress = erc20ProxyData.tokenAddress; + this._validateProxyContractExistsOrThrow(); + const tokenContractIfExists = _.find(this._dummyTokenContracts, c => c.address === tokenAddress); + if (_.isUndefined(tokenContractIfExists)) { + throw new Error(`Token: ${tokenAddress} was not deployed through ERC20Wrapper`); + } + const allowance = new BigNumber( + await tokenContractIfExists.allowance.callAsync(owner, (this._proxyContract as ERC20ProxyContract).address), + ); + return allowance; + } public async getBalancesAsync(): Promise<ERC20BalancesByOwner> { this._validateDummyTokenContractsExistOrThrow(); const balancesByOwner: ERC20BalancesByOwner = {}; diff --git a/packages/contracts/src/utils/erc721_wrapper.ts b/packages/contracts/src/utils/erc721_wrapper.ts index b20d9acd2..6a6f10fa7 100644 --- a/packages/contracts/src/utils/erc721_wrapper.ts +++ b/packages/contracts/src/utils/erc721_wrapper.ts @@ -1,4 +1,4 @@ -import { generatePseudoRandomSalt } from '@0xproject/order-utils'; +import { assetProxyUtils, generatePseudoRandomSalt } from '@0xproject/order-utils'; import { BigNumber } from '@0xproject/utils'; import { Web3Wrapper } from '@0xproject/web3-wrapper'; import { Provider } from 'ethereum-types'; @@ -19,6 +19,7 @@ export class ERC721Wrapper { private _provider: Provider; private _dummyTokenContracts: DummyERC721TokenContract[]; private _proxyContract?: ERC721ProxyContract; + private _proxyIdIfExists?: number; private _initialTokenIdsByOwner: ERC721TokenIdsByOwner = {}; constructor(provider: Provider, tokenOwnerAddresses: string[], contractOwnerAddress: string) { this._web3Wrapper = new Web3Wrapper(provider); @@ -47,8 +48,13 @@ export class ERC721Wrapper { this._provider, txDefaults, ); + this._proxyIdIfExists = await this._proxyContract.getProxyId.callAsync(); return this._proxyContract; } + public getProxyId(): number { + this._validateProxyContractExistsOrThrow(); + return this._proxyIdIfExists as number; + } public async setBalancesAndAllowancesAsync(): Promise<void> { this._validateDummyTokenContractsExistOrThrow(); this._validateProxyContractExistsOrThrow(); @@ -85,6 +91,33 @@ export class ERC721Wrapper { } } } + public async getBalanceAsync(owner: string, assetData: string): Promise<BigNumber> { + const erc721ProxyData = assetProxyUtils.decodeERC721AssetData(assetData); + const tokenAddress = erc721ProxyData.tokenAddress; + const tokenContractIfExists = _.find(this._dummyTokenContracts, c => c.address === tokenAddress); + if (_.isUndefined(tokenContractIfExists)) { + throw new Error(`Token: ${tokenAddress} was not deployed through ERC20Wrapper`); + } + const tokenId = erc721ProxyData.tokenId; + const tokenOwner = await tokenContractIfExists.ownerOf.callAsync(tokenId); + const balance = tokenOwner === owner ? new BigNumber(1) : new BigNumber(0); + return balance; + } + public async getProxyAllowanceAsync(owner: string, assetData: string): Promise<BigNumber> { + const erc721ProxyData = assetProxyUtils.decodeERC721AssetData(assetData); + const tokenAddress = erc721ProxyData.tokenAddress; + this._validateProxyContractExistsOrThrow(); + const tokenContractIfExists = _.find(this._dummyTokenContracts, c => c.address === tokenAddress); + if (_.isUndefined(tokenContractIfExists)) { + throw new Error(`Token: ${tokenAddress} was not deployed through ERC20Wrapper`); + } + const operator = (this._proxyContract as ERC721ProxyContract).address; + const didApproveAll = await tokenContractIfExists.isApprovedForAll.callAsync(owner, operator); + const tokenId = erc721ProxyData.tokenId; + const allowedAddress = await tokenContractIfExists.getApproved.callAsync(tokenId); + const allowance = allowedAddress === operator || didApproveAll ? new BigNumber(1) : new BigNumber(0); + return allowance; + } public async getBalancesAsync(): Promise<ERC721TokenIdsByOwner> { this._validateDummyTokenContractsExistOrThrow(); this._validateBalancesAndAllowancesSetOrThrow(); diff --git a/packages/contracts/src/utils/exchange_wrapper.ts b/packages/contracts/src/utils/exchange_wrapper.ts index 4cc8f0b89..38dcb891a 100644 --- a/packages/contracts/src/utils/exchange_wrapper.ts +++ b/packages/contracts/src/utils/exchange_wrapper.ts @@ -33,8 +33,8 @@ export class ExchangeWrapper { params.signature, { from }, ); - const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); - return tx; + const txReceipt = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); + return txReceipt; } public async cancelOrderAsync(signedOrder: SignedOrder, from: string): Promise<TransactionReceiptWithDecodedLogs> { const params = orderUtils.createCancel(signedOrder); @@ -227,6 +227,10 @@ export class ExchangeWrapper { const filledAmount = new BigNumber(await this._exchange.filled.callAsync(orderHashHex)); return filledAmount; } + public async isCancelledAsync(orderHashHex: string): Promise<boolean> { + const isCancelled = await this._exchange.cancelled.callAsync(orderHashHex); + return isCancelled; + } public async getOrderInfoAsync(signedOrder: SignedOrder): Promise<OrderInfo> { const orderInfo = (await this._exchange.getOrderInfo.callAsync(signedOrder)) as OrderInfo; return orderInfo; diff --git a/packages/contracts/src/utils/new_order_factory.ts b/packages/contracts/src/utils/new_order_factory.ts new file mode 100644 index 000000000..715e38d6d --- /dev/null +++ b/packages/contracts/src/utils/new_order_factory.ts @@ -0,0 +1,218 @@ +import { assetProxyUtils, generatePseudoRandomSalt } from '@0xproject/order-utils'; +import { Order } from '@0xproject/types'; +import { BigNumber, errorUtils } from '@0xproject/utils'; + +import { DummyERC721TokenContract } from '../generated_contract_wrappers/dummy_e_r_c721_token'; + +import { constants } from './constants'; +import { + AssetDataScenario, + ERC721TokenIdsByOwner, + ExpirationTimeSecondsScenario, + FeeRecipientAddressScenario, + OrderAmountScenario, + OrderScenario, +} from './types'; + +const TEN_UNITS_EIGHTEEN_DECIMALS = new BigNumber(10000000000000000000); +const POINT_ONE_UNITS_EIGHTEEN_DECIMALS = new BigNumber(100000000000000000); +const TEN_UNITS_FIVE_DECIMALS = new BigNumber(1000000); +const ONE_NFT_UNIT = new BigNumber(1); + +export class NewOrderFactory { + private _userAddresses: string[]; + private _zrxAddress: string; + private _nonZrxERC20EighteenDecimalTokenAddresses: string[]; + private _erc20FiveDecimalTokenAddresses: string[]; + private _erc721Token: DummyERC721TokenContract; + private _erc721Balances: ERC721TokenIdsByOwner; + private _exchangeAddress: string; + constructor( + userAddresses: string[], + zrxAddress: string, + nonZrxERC20EighteenDecimalTokenAddresses: string[], + erc20FiveDecimalTokenAddresses: string[], + erc721Token: DummyERC721TokenContract, + erc721Balances: ERC721TokenIdsByOwner, + exchangeAddress: string, + ) { + this._userAddresses = userAddresses; + this._zrxAddress = zrxAddress; + this._nonZrxERC20EighteenDecimalTokenAddresses = nonZrxERC20EighteenDecimalTokenAddresses; + this._erc20FiveDecimalTokenAddresses = erc20FiveDecimalTokenAddresses; + this._erc721Token = erc721Token; + this._erc721Balances = erc721Balances; + this._exchangeAddress = exchangeAddress; + } + public generateOrder(orderScenario: OrderScenario): Order { + const makerAddress = this._userAddresses[1]; + const takerAddress = this._userAddresses[2]; + const erc721MakerAssetIds = this._erc721Balances[makerAddress][this._erc721Token.address]; + const erc721TakerAssetIds = this._erc721Balances[takerAddress][this._erc721Token.address]; + let feeRecipientAddress; + let makerAssetAmount; + let takerAssetAmount; + let makerFee; + let takerFee; + let expirationTimeSeconds; + let makerAssetData; + let takerAssetData; + + switch (orderScenario.feeRecipientScenario) { + case FeeRecipientAddressScenario.BurnAddress: + feeRecipientAddress = constants.NULL_ADDRESS; + break; + case FeeRecipientAddressScenario.EthUserAddress: + feeRecipientAddress = this._userAddresses[4]; + break; + default: + throw errorUtils.spawnSwitchErr('FeeRecipientAddressScenario', orderScenario.feeRecipientScenario); + } + + switch (orderScenario.makerAssetDataScenario) { + case AssetDataScenario.ZRXFeeToken: + makerAssetData = assetProxyUtils.encodeERC20AssetData(this._zrxAddress); + break; + case AssetDataScenario.ERC20NonZRXEighteenDecimals: + makerAssetData = assetProxyUtils.encodeERC20AssetData( + this._nonZrxERC20EighteenDecimalTokenAddresses[0], + ); + break; + case AssetDataScenario.ERC20FiveDecimals: + makerAssetData = assetProxyUtils.encodeERC20AssetData(this._erc20FiveDecimalTokenAddresses[0]); + break; + case AssetDataScenario.ERC721: + makerAssetData = assetProxyUtils.encodeERC721AssetData( + this._erc721Token.address, + erc721MakerAssetIds[0], + ); + break; + default: + throw errorUtils.spawnSwitchErr('AssetDataScenario', orderScenario.makerAssetDataScenario); + } + + switch (orderScenario.takerAssetDataScenario) { + case AssetDataScenario.ZRXFeeToken: + takerAssetData = assetProxyUtils.encodeERC20AssetData(this._zrxAddress); + break; + case AssetDataScenario.ERC20NonZRXEighteenDecimals: + takerAssetData = assetProxyUtils.encodeERC20AssetData( + this._nonZrxERC20EighteenDecimalTokenAddresses[1], + ); + break; + case AssetDataScenario.ERC20FiveDecimals: + takerAssetData = assetProxyUtils.encodeERC20AssetData(this._erc20FiveDecimalTokenAddresses[1]); + break; + case AssetDataScenario.ERC721: + takerAssetData = assetProxyUtils.encodeERC721AssetData( + this._erc721Token.address, + erc721TakerAssetIds[0], + ); + break; + default: + throw errorUtils.spawnSwitchErr('AssetDataScenario', orderScenario.takerAssetDataScenario); + } + + switch (orderScenario.makerAssetAmountScenario) { + case OrderAmountScenario.NonZero: + switch (orderScenario.makerAssetDataScenario) { + case AssetDataScenario.ZRXFeeToken: + case AssetDataScenario.ERC20NonZRXEighteenDecimals: + makerAssetAmount = TEN_UNITS_EIGHTEEN_DECIMALS; + break; + case AssetDataScenario.ERC20FiveDecimals: + makerAssetAmount = TEN_UNITS_FIVE_DECIMALS; + break; + case AssetDataScenario.ERC721: + makerAssetAmount = ONE_NFT_UNIT; + break; + default: + throw errorUtils.spawnSwitchErr('AssetDataScenario', orderScenario.makerAssetDataScenario); + } + break; + case OrderAmountScenario.Zero: + makerAssetAmount = new BigNumber(0); + break; + default: + throw errorUtils.spawnSwitchErr('OrderAmountScenario', orderScenario.makerAssetAmountScenario); + } + + switch (orderScenario.takerAssetAmountScenario) { + case OrderAmountScenario.NonZero: + switch (orderScenario.takerAssetDataScenario) { + case AssetDataScenario.ERC20NonZRXEighteenDecimals: + case AssetDataScenario.ZRXFeeToken: + takerAssetAmount = TEN_UNITS_EIGHTEEN_DECIMALS; + break; + case AssetDataScenario.ERC20FiveDecimals: + takerAssetAmount = TEN_UNITS_FIVE_DECIMALS; + break; + case AssetDataScenario.ERC721: + takerAssetAmount = ONE_NFT_UNIT; + break; + default: + throw errorUtils.spawnSwitchErr('AssetDataScenario', orderScenario.takerAssetDataScenario); + } + break; + case OrderAmountScenario.Zero: + takerAssetAmount = new BigNumber(0); + break; + default: + throw errorUtils.spawnSwitchErr('OrderAmountScenario', orderScenario.takerAssetAmountScenario); + } + + switch (orderScenario.makerFeeScenario) { + case OrderAmountScenario.NonZero: + makerFee = POINT_ONE_UNITS_EIGHTEEN_DECIMALS; + break; + case OrderAmountScenario.Zero: + makerFee = new BigNumber(0); + break; + default: + throw errorUtils.spawnSwitchErr('OrderAmountScenario', orderScenario.makerFeeScenario); + } + + switch (orderScenario.takerFeeScenario) { + case OrderAmountScenario.NonZero: + takerFee = POINT_ONE_UNITS_EIGHTEEN_DECIMALS; + break; + case OrderAmountScenario.Zero: + takerFee = new BigNumber(0); + break; + default: + throw errorUtils.spawnSwitchErr('OrderAmountScenario', orderScenario.takerFeeScenario); + } + + switch (orderScenario.expirationTimeSecondsScenario) { + case ExpirationTimeSecondsScenario.InFuture: + expirationTimeSeconds = new BigNumber(2524604400); // Close to infinite + break; + case ExpirationTimeSecondsScenario.InPast: + expirationTimeSeconds = new BigNumber(0); // Jan 1, 1970 + break; + default: + throw errorUtils.spawnSwitchErr( + 'ExpirationTimeSecondsScenario', + orderScenario.expirationTimeSecondsScenario, + ); + } + + const order: Order = { + senderAddress: constants.NULL_ADDRESS, + makerAddress, + takerAddress, + makerFee, + takerFee, + makerAssetAmount, + takerAssetAmount, + makerAssetData, + takerAssetData, + salt: generatePseudoRandomSalt(), + exchangeAddress: this._exchangeAddress, + feeRecipientAddress, + expirationTimeSeconds, + }; + + return order; + } +} diff --git a/packages/contracts/src/utils/order_utils.ts b/packages/contracts/src/utils/order_utils.ts index a9f994d80..019f6e74b 100644 --- a/packages/contracts/src/utils/order_utils.ts +++ b/packages/contracts/src/utils/order_utils.ts @@ -5,6 +5,13 @@ import { constants } from './constants'; import { CancelOrder, MatchOrder } from './types'; export const orderUtils = { + getPartialAmount(numerator: BigNumber, denominator: BigNumber, target: BigNumber): BigNumber { + const partialAmount = numerator + .mul(target) + .div(denominator) + .floor(); + return partialAmount; + }, createFill: (signedOrder: SignedOrder, takerAssetFillAmount?: BigNumber) => { const fill = { order: orderUtils.getOrderWithoutExchangeAddress(signedOrder), diff --git a/packages/contracts/src/utils/simple_asset_balance_and_proxy_allowance_fetcher.ts b/packages/contracts/src/utils/simple_asset_balance_and_proxy_allowance_fetcher.ts new file mode 100644 index 000000000..a295a40c4 --- /dev/null +++ b/packages/contracts/src/utils/simple_asset_balance_and_proxy_allowance_fetcher.ts @@ -0,0 +1,19 @@ +import { AbstractBalanceAndProxyAllowanceFetcher } from '@0xproject/order-utils'; +import { BigNumber } from '@0xproject/utils'; + +import { AssetWrapper } from './asset_wrapper'; + +export class SimpleAssetBalanceAndProxyAllowanceFetcher implements AbstractBalanceAndProxyAllowanceFetcher { + private _assetWrapper: AssetWrapper; + constructor(assetWrapper: AssetWrapper) { + this._assetWrapper = assetWrapper; + } + public async getBalanceAsync(assetData: string, userAddress: string): Promise<BigNumber> { + const balance = await this._assetWrapper.getBalanceAsync(userAddress, assetData); + return balance; + } + public async getProxyAllowanceAsync(assetData: string, userAddress: string): Promise<BigNumber> { + const proxyAllowance = await this._assetWrapper.getProxyAllowanceAsync(userAddress, assetData); + return proxyAllowance; + } +} diff --git a/packages/contracts/src/utils/simple_order_filled_cancelled_fetcher.ts b/packages/contracts/src/utils/simple_order_filled_cancelled_fetcher.ts new file mode 100644 index 000000000..24afe36b7 --- /dev/null +++ b/packages/contracts/src/utils/simple_order_filled_cancelled_fetcher.ts @@ -0,0 +1,24 @@ +import { AbstractOrderFilledCancelledFetcher } from '@0xproject/order-utils'; +import { BigNumber } from '@0xproject/utils'; + +import { ExchangeWrapper } from './exchange_wrapper'; + +export class SimpleOrderFilledCancelledFetcher implements AbstractOrderFilledCancelledFetcher { + private _exchangeWrapper: ExchangeWrapper; + private _zrxAssetData: string; + constructor(exchange: ExchangeWrapper, zrxAssetData: string) { + this._exchangeWrapper = exchange; + this._zrxAssetData = zrxAssetData; + } + public async getFilledTakerAmountAsync(orderHash: string): Promise<BigNumber> { + const filledTakerAmount = new BigNumber(await this._exchangeWrapper.getTakerAssetFilledAmountAsync(orderHash)); + return filledTakerAmount; + } + public async isOrderCancelledAsync(orderHash: string): Promise<boolean> { + const isCancelled = await this._exchangeWrapper.isCancelledAsync(orderHash); + return isCancelled; + } + public getZRXAssetData(): string { + return this._zrxAssetData; + } +} diff --git a/packages/contracts/src/utils/types.ts b/packages/contracts/src/utils/types.ts index bb8c12088..77e8d867e 100644 --- a/packages/contracts/src/utils/types.ts +++ b/packages/contracts/src/utils/types.ts @@ -150,3 +150,38 @@ export interface MatchOrder { leftSignature: string; rightSignature: string; } + +// Combinatorial testing types + +export enum FeeRecipientAddressScenario { + BurnAddress = 'BURN_ADDRESS', + EthUserAddress = 'ETH_USER_ADDRESS', +} + +export enum OrderAmountScenario { + Zero = 'ZERO', + NonZero = 'NON_ZERO', +} + +export enum ExpirationTimeSecondsScenario { + InPast = 'IN_PAST', + InFuture = 'IN_FUTURE', +} + +export enum AssetDataScenario { + ERC721 = 'ERC721', + ZRXFeeToken = 'ZRX_FEE_TOKEN', + ERC20FiveDecimals = 'ERC20_FIVE_DECIMALS', + ERC20NonZRXEighteenDecimals = 'ERC20_NON_ZRX_EIGHTEEN_DECIMALS', +} + +export interface OrderScenario { + feeRecipientScenario: FeeRecipientAddressScenario; + makerAssetAmountScenario: OrderAmountScenario; + takerAssetAmountScenario: OrderAmountScenario; + makerFeeScenario: OrderAmountScenario; + takerFeeScenario: OrderAmountScenario; + expirationTimeSecondsScenario: ExpirationTimeSecondsScenario; + makerAssetDataScenario: AssetDataScenario; + takerAssetDataScenario: AssetDataScenario; +} diff --git a/packages/contracts/test/combinatorial_tests.ts b/packages/contracts/test/combinatorial_tests.ts new file mode 100644 index 000000000..c7de8d7e3 --- /dev/null +++ b/packages/contracts/test/combinatorial_tests.ts @@ -0,0 +1,48 @@ +import { BlockchainLifecycle } from '@0xproject/dev-utils'; +import * as _ from 'lodash'; + +import { chaiSetup } from '../src/utils/chai_setup'; +import { CoreCombinatorialUtils, coreCombinatorialUtilsFactoryAsync } from '../src/utils/core_combinatorial_utils'; +import { provider, txDefaults, web3Wrapper } from '../src/utils/web3_wrapper'; + +import { OrderScenario } from '../src/utils/types'; + +chaiSetup.configure(); +const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); + +describe('Combinatorial tests', () => { + let coreCombinatorialUtils: CoreCombinatorialUtils; + + before(async () => { + await blockchainLifecycle.startAsync(); + coreCombinatorialUtils = await coreCombinatorialUtilsFactoryAsync(web3Wrapper, txDefaults); + }); + after(async () => { + await blockchainLifecycle.revertAsync(); + }); + beforeEach(async () => { + await blockchainLifecycle.startAsync(); + }); + afterEach(async () => { + await blockchainLifecycle.revertAsync(); + }); + const test = (orderScenarios: OrderScenario[]) => { + _.forEach(orderScenarios, orderScenario => { + const description = `Combinatorial OrderFill: ${orderScenario.feeRecipientScenario} ${ + orderScenario.makerAssetAmountScenario + } ${orderScenario.takerAssetAmountScenario} ${orderScenario.makerFeeScenario} ${ + orderScenario.takerFeeScenario + } ${orderScenario.expirationTimeSecondsScenario} ${orderScenario.makerAssetDataScenario} ${ + orderScenario.takerAssetDataScenario + }`; + it(description, async () => { + const order = coreCombinatorialUtils.orderFactory.generateOrder(orderScenario); + await coreCombinatorialUtils.testFillOrderScenarioAsync(order, provider); + }); + }); + }; + + const allOrderScenarios = CoreCombinatorialUtils.generateOrderCombinations(); + + describe.only('Fills orders', () => test(allOrderScenarios)); +}); diff --git a/packages/order-utils/src/abstract/abstract_order_filled_cancelled_fetcher.ts b/packages/order-utils/src/abstract/abstract_order_filled_cancelled_fetcher.ts index ec398a11e..865ea4e43 100644 --- a/packages/order-utils/src/abstract/abstract_order_filled_cancelled_fetcher.ts +++ b/packages/order-utils/src/abstract/abstract_order_filled_cancelled_fetcher.ts @@ -3,5 +3,5 @@ import { BigNumber } from '@0xproject/utils'; export abstract class AbstractOrderFilledCancelledFetcher { public abstract async getFilledTakerAmountAsync(orderHash: string): Promise<BigNumber>; public abstract async isOrderCancelledAsync(orderHash: string): Promise<boolean>; - public abstract getZRXTokenAddress(): string; + public abstract getZRXAssetData(): string; } diff --git a/packages/order-utils/src/exchange_transfer_simulator.ts b/packages/order-utils/src/exchange_transfer_simulator.ts index 32d53d6a2..cac4af243 100644 --- a/packages/order-utils/src/exchange_transfer_simulator.ts +++ b/packages/order-utils/src/exchange_transfer_simulator.ts @@ -1,7 +1,8 @@ -import { ExchangeContractErrs } from '@0xproject/types'; +import { AssetProxyId, ExchangeContractErrs } from '@0xproject/types'; import { BigNumber } from '@0xproject/utils'; import { AbstractBalanceAndProxyAllowanceLazyStore } from './abstract/abstract_balance_and_proxy_allowance_lazy_store'; +import { assetProxyUtils } from './asset_proxy_utils'; import { constants } from './constants'; import { TradeSide, TransferType } from './types'; @@ -89,8 +90,13 @@ export class ExchangeTransferSimulator { userAddress: string, amountInBaseUnits: BigNumber, ): Promise<void> { + const assetProxyId = assetProxyUtils.decodeAssetDataId(assetData); + const isERC721Asset = assetProxyId === AssetProxyId.ERC721; const proxyAllowance = await this._store.getProxyAllowanceAsync(assetData, userAddress); - if (!proxyAllowance.eq(constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS)) { + // HACK: This code assumes that all tokens with an UNLIMITED_ALLOWANCE_IN_BASE_UNITS set, + // are UnlimitedAllowanceTokens. This is however not true, it just so happens that all + // DummyERC20Tokens we use in tests are. + if (!proxyAllowance.eq(constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS) && !isERC721Asset) { this._store.setProxyAllowance(assetData, userAddress, proxyAllowance.minus(amountInBaseUnits)); } } diff --git a/packages/order-utils/src/index.ts b/packages/order-utils/src/index.ts index f9b37df82..592b9b7f6 100644 --- a/packages/order-utils/src/index.ts +++ b/packages/order-utils/src/index.ts @@ -16,6 +16,7 @@ export { generatePseudoRandomSalt } from './salt'; export { OrderError, MessagePrefixType, MessagePrefixOpts } from './types'; export { AbstractBalanceAndProxyAllowanceFetcher } from './abstract/abstract_balance_and_proxy_allowance_fetcher'; export { AbstractOrderFilledCancelledFetcher } from './abstract/abstract_order_filled_cancelled_fetcher'; +export { BalanceAndProxyAllowanceLazyStore } from './store/balance_and_proxy_allowance_lazy_store'; export { RemainingFillableCalculator } from './remaining_fillable_calculator'; export { OrderStateUtils } from './order_state_utils'; export { assetProxyUtils } from './asset_proxy_utils'; diff --git a/packages/order-utils/src/order_state_utils.ts b/packages/order-utils/src/order_state_utils.ts index 40f235da7..dbb65de59 100644 --- a/packages/order-utils/src/order_state_utils.ts +++ b/packages/order-utils/src/order_state_utils.ts @@ -7,43 +7,85 @@ import { SignedOrder, } from '@0xproject/types'; import { BigNumber } from '@0xproject/utils'; +import * as _ from 'lodash'; import { AbstractBalanceAndProxyAllowanceFetcher } from './abstract/abstract_balance_and_proxy_allowance_fetcher'; import { AbstractOrderFilledCancelledFetcher } from './abstract/abstract_order_filled_cancelled_fetcher'; -import { assetProxyUtils } from './asset_proxy_utils'; +import { constants } from './constants'; import { orderHashUtils } from './order_hash'; import { RemainingFillableCalculator } from './remaining_fillable_calculator'; +interface SidedOrderRelevantState { + isMakerSide: boolean; + traderBalance: BigNumber; + traderProxyAllowance: BigNumber; + traderFeeBalance: BigNumber; + traderFeeProxyAllowance: BigNumber; + filledTakerAssetAmount: BigNumber; + remainingFillableAssetAmount: BigNumber; +} + const ACCEPTABLE_RELATIVE_ROUNDING_ERROR = 0.0001; export class OrderStateUtils { private _balanceAndProxyAllowanceFetcher: AbstractBalanceAndProxyAllowanceFetcher; private _orderFilledCancelledFetcher: AbstractOrderFilledCancelledFetcher; - private static _validateIfOrderIsValid(signedOrder: SignedOrder, orderRelevantState: OrderRelevantState): void { - const availableTakerAssetAmount = signedOrder.takerAssetAmount.minus(orderRelevantState.filledTakerAssetAmount); + private static _validateIfOrderIsValid( + signedOrder: SignedOrder, + sidedOrderRelevantState: SidedOrderRelevantState, + ): void { + const isMakerSide = sidedOrderRelevantState.isMakerSide; + const availableTakerAssetAmount = signedOrder.takerAssetAmount.minus( + sidedOrderRelevantState.filledTakerAssetAmount, + ); if (availableTakerAssetAmount.eq(0)) { throw new Error(ExchangeContractErrs.OrderRemainingFillAmountZero); } - if (orderRelevantState.makerBalance.eq(0)) { - throw new Error(ExchangeContractErrs.InsufficientMakerBalance); + if (sidedOrderRelevantState.traderBalance.eq(0)) { + throw new Error( + isMakerSide + ? ExchangeContractErrs.InsufficientMakerBalance + : ExchangeContractErrs.InsufficientTakerBalance, + ); } - if (orderRelevantState.makerProxyAllowance.eq(0)) { - throw new Error(ExchangeContractErrs.InsufficientMakerAllowance); + if (sidedOrderRelevantState.traderProxyAllowance.eq(0)) { + throw new Error( + isMakerSide + ? ExchangeContractErrs.InsufficientMakerAllowance + : ExchangeContractErrs.InsufficientTakerAllowance, + ); } if (!signedOrder.makerFee.eq(0)) { - if (orderRelevantState.makerFeeBalance.eq(0)) { - throw new Error(ExchangeContractErrs.InsufficientMakerFeeBalance); + if (sidedOrderRelevantState.traderFeeBalance.eq(0)) { + throw new Error( + isMakerSide + ? ExchangeContractErrs.InsufficientMakerFeeBalance + : ExchangeContractErrs.InsufficientTakerFeeBalance, + ); } - if (orderRelevantState.makerFeeProxyAllowance.eq(0)) { - throw new Error(ExchangeContractErrs.InsufficientMakerFeeAllowance); + if (sidedOrderRelevantState.traderFeeProxyAllowance.eq(0)) { + throw new Error( + isMakerSide + ? ExchangeContractErrs.InsufficientMakerFeeAllowance + : ExchangeContractErrs.InsufficientTakerFeeAllowance, + ); } } - const minFillableTakerAssetAmountWithinNoRoundingErrorRange = signedOrder.takerAssetAmount - .dividedBy(ACCEPTABLE_RELATIVE_ROUNDING_ERROR) - .dividedBy(signedOrder.makerAssetAmount); + + let minFillableTakerAssetAmountWithinNoRoundingErrorRange; + if (isMakerSide) { + minFillableTakerAssetAmountWithinNoRoundingErrorRange = signedOrder.takerAssetAmount + .dividedBy(ACCEPTABLE_RELATIVE_ROUNDING_ERROR) + .dividedBy(signedOrder.makerAssetAmount); + } else { + minFillableTakerAssetAmountWithinNoRoundingErrorRange = signedOrder.makerAssetAmount + .dividedBy(ACCEPTABLE_RELATIVE_ROUNDING_ERROR) + .dividedBy(signedOrder.takerAssetAmount); + } + if ( - orderRelevantState.remainingFillableTakerAssetAmount.lessThan( + sidedOrderRelevantState.remainingFillableAssetAmount.lessThan( minFillableTakerAssetAmountWithinNoRoundingErrorRange, ) ) { @@ -57,11 +99,20 @@ export class OrderStateUtils { this._balanceAndProxyAllowanceFetcher = balanceAndProxyAllowanceFetcher; this._orderFilledCancelledFetcher = orderFilledCancelledFetcher; } - public async getOrderStateAsync(signedOrder: SignedOrder): Promise<OrderState> { - const orderRelevantState = await this.getOrderRelevantStateAsync(signedOrder); + public async getOpenOrderStateAsync(signedOrder: SignedOrder): Promise<OrderState> { + const orderRelevantState = await this.getOpenOrderRelevantStateAsync(signedOrder); const orderHash = orderHashUtils.getOrderHashHex(signedOrder); + const sidedOrderRelevantState = { + isMakerSide: true, + traderBalance: orderRelevantState.makerBalance, + traderProxyAllowance: orderRelevantState.makerProxyAllowance, + traderFeeBalance: orderRelevantState.makerFeeBalance, + traderFeeProxyAllowance: orderRelevantState.makerFeeProxyAllowance, + filledTakerAssetAmount: orderRelevantState.filledTakerAssetAmount, + remainingFillableAssetAmount: orderRelevantState.remainingFillableMakerAssetAmount, + }; try { - OrderStateUtils._validateIfOrderIsValid(signedOrder, orderRelevantState); + OrderStateUtils._validateIfOrderIsValid(signedOrder, sidedOrderRelevantState); const orderState: OrderStateValid = { isValid: true, orderHash, @@ -77,63 +128,152 @@ export class OrderStateUtils { return orderState; } } - public async getOrderRelevantStateAsync(signedOrder: SignedOrder): Promise<OrderRelevantState> { - const zrxTokenAddress = this._orderFilledCancelledFetcher.getZRXTokenAddress(); - const makerProxyData = assetProxyUtils.decodeERC20AssetData(signedOrder.makerAssetData); - const makerAssetAddress = makerProxyData.tokenAddress; - const orderHash = orderHashUtils.getOrderHashHex(signedOrder); - const makerBalance = await this._balanceAndProxyAllowanceFetcher.getBalanceAsync( - makerAssetAddress, - signedOrder.makerAddress, + public async getOpenOrderRelevantStateAsync(signedOrder: SignedOrder): Promise<OrderRelevantState> { + const isMaker = true; + const sidedOrderRelevantState = await this._getSidedOrderRelevantStateAsync( + isMaker, + signedOrder, + signedOrder.takerAddress, + ); + const remainingFillableTakerAssetAmount = sidedOrderRelevantState.remainingFillableAssetAmount + .times(signedOrder.takerAssetAmount) + .dividedToIntegerBy(signedOrder.makerAssetAmount); + + const orderRelevantState = { + makerBalance: sidedOrderRelevantState.traderBalance, + makerProxyAllowance: sidedOrderRelevantState.traderProxyAllowance, + makerFeeBalance: sidedOrderRelevantState.traderFeeBalance, + makerFeeProxyAllowance: sidedOrderRelevantState.traderFeeProxyAllowance, + filledTakerAssetAmount: sidedOrderRelevantState.filledTakerAssetAmount, + remainingFillableMakerAssetAmount: sidedOrderRelevantState.remainingFillableAssetAmount, + remainingFillableTakerAssetAmount, + }; + return orderRelevantState; + } + public async getMaxFillableTakerAssetAmountAsync( + signedOrder: SignedOrder, + takerAddress: string, + ): Promise<BigNumber> { + // Get max fillable amount for an order, considering the makers ability to fill + let isMaker = true; + const orderRelevantMakerState = await this._getSidedOrderRelevantStateAsync( + isMaker, + signedOrder, + signedOrder.takerAddress, + ); + const remainingFillableTakerAssetAmountGivenMakersStatus = orderRelevantMakerState.remainingFillableAssetAmount; + + // Get max fillable amount for an order, considering the takers ability to fill + isMaker = false; + const orderRelevantTakerState = await this._getSidedOrderRelevantStateAsync(isMaker, signedOrder, takerAddress); + const remainingFillableTakerAssetAmountGivenTakersStatus = orderRelevantTakerState.remainingFillableAssetAmount; + + // The min of these two in the actualy max fillable by either party + const fillableTakerAssetAmount = BigNumber.min([ + remainingFillableTakerAssetAmountGivenMakersStatus, + remainingFillableTakerAssetAmountGivenTakersStatus, + ]); + + return fillableTakerAssetAmount; + } + public async getMaxFillableTakerAssetAmountForFailingOrderAsync( + signedOrder: SignedOrder, + takerAddress: string, + ): Promise<BigNumber> { + // Get min of taker balance & allowance + const takerAssetBalanceOfTaker = await this._balanceAndProxyAllowanceFetcher.getBalanceAsync( + signedOrder.takerAssetData, + takerAddress, ); - const makerProxyAllowance = await this._balanceAndProxyAllowanceFetcher.getProxyAllowanceAsync( - makerAssetAddress, - signedOrder.makerAddress, + const takerAssetAllowanceOfTaker = await this._balanceAndProxyAllowanceFetcher.getProxyAllowanceAsync( + signedOrder.takerAssetData, + takerAddress, ); - const makerFeeBalance = await this._balanceAndProxyAllowanceFetcher.getBalanceAsync( - zrxTokenAddress, - signedOrder.makerAddress, + const minTakerAssetAmount = BigNumber.min([takerAssetBalanceOfTaker, takerAssetAllowanceOfTaker]); + + // get remainingFillAmount + const orderHash = orderHashUtils.getOrderHashHex(signedOrder); + const filledTakerAssetAmount = await this._orderFilledCancelledFetcher.getFilledTakerAmountAsync(orderHash); + const remainingFillTakerAssetAmount = signedOrder.takerAssetAmount.minus(filledTakerAssetAmount); + + if (minTakerAssetAmount.gte(remainingFillTakerAssetAmount)) { + return remainingFillTakerAssetAmount; + } else { + return minTakerAssetAmount; + } + } + private async _getSidedOrderRelevantStateAsync( + isMakerSide: boolean, + signedOrder: SignedOrder, + takerAddress: string, + ): Promise<SidedOrderRelevantState> { + let traderAddress; + let assetData; + let assetAmount; + let feeAmount; + if (isMakerSide) { + traderAddress = signedOrder.makerAddress; + assetData = signedOrder.makerAssetData; + assetAmount = signedOrder.makerAssetAmount; + feeAmount = signedOrder.makerFee; + } else { + traderAddress = takerAddress; + assetData = signedOrder.takerAssetData; + assetAmount = signedOrder.takerAssetAmount; + feeAmount = signedOrder.takerFee; + } + const zrxAssetData = this._orderFilledCancelledFetcher.getZRXAssetData(); + const isAssetZRX = assetData === zrxAssetData; + + const traderBalance = await this._balanceAndProxyAllowanceFetcher.getBalanceAsync(assetData, traderAddress); + const traderProxyAllowance = await this._balanceAndProxyAllowanceFetcher.getProxyAllowanceAsync( + assetData, + traderAddress, + ); + const traderFeeBalance = await this._balanceAndProxyAllowanceFetcher.getBalanceAsync( + zrxAssetData, + traderAddress, ); - const makerFeeProxyAllowance = await this._balanceAndProxyAllowanceFetcher.getProxyAllowanceAsync( - zrxTokenAddress, - signedOrder.makerAddress, + const traderFeeProxyAllowance = await this._balanceAndProxyAllowanceFetcher.getProxyAllowanceAsync( + zrxAssetData, + traderAddress, ); + + const transferrableTraderAssetAmount = BigNumber.min([traderProxyAllowance, traderBalance]); + const transferrableFeeAssetAmount = BigNumber.min([traderFeeProxyAllowance, traderFeeBalance]); + + const orderHash = orderHashUtils.getOrderHashHex(signedOrder); const filledTakerAssetAmount = await this._orderFilledCancelledFetcher.getFilledTakerAmountAsync(orderHash); - const isOrderCancelled = await this._orderFilledCancelledFetcher.isOrderCancelledAsync(orderHash); const totalMakerAssetAmount = signedOrder.makerAssetAmount; const totalTakerAssetAmount = signedOrder.takerAssetAmount; + const isOrderCancelled = await this._orderFilledCancelledFetcher.isOrderCancelledAsync(orderHash); const remainingTakerAssetAmount = isOrderCancelled ? new BigNumber(0) : totalTakerAssetAmount.minus(filledTakerAssetAmount); - const remainingMakerAssetAmount = remainingTakerAssetAmount - .times(totalMakerAssetAmount) - .dividedToIntegerBy(totalTakerAssetAmount); - const transferrableMakerAssetAmount = BigNumber.min([makerProxyAllowance, makerBalance]); - const transferrableFeeAssetAmount = BigNumber.min([makerFeeProxyAllowance, makerFeeBalance]); - - const zrxAssetData = assetProxyUtils.encodeERC20AssetData(zrxTokenAddress); - const isMakerAssetZRX = signedOrder.makerAssetData === zrxAssetData; + const remainingMakerAssetAmount = remainingTakerAssetAmount.eq(0) + ? new BigNumber(0) + : remainingTakerAssetAmount.times(totalMakerAssetAmount).dividedToIntegerBy(totalTakerAssetAmount); + const remainingAssetAmount = isMakerSide ? remainingMakerAssetAmount : remainingTakerAssetAmount; + const remainingFillableCalculator = new RemainingFillableCalculator( - signedOrder.makerFee, - signedOrder.makerAssetAmount, - isMakerAssetZRX, - transferrableMakerAssetAmount, + feeAmount, + assetAmount, + isAssetZRX, + transferrableTraderAssetAmount, transferrableFeeAssetAmount, - remainingMakerAssetAmount, + remainingAssetAmount, ); - const remainingFillableMakerAssetAmount = remainingFillableCalculator.computeRemainingFillable(); - const remainingFillableTakerAssetAmount = remainingFillableMakerAssetAmount - .times(signedOrder.takerAssetAmount) - .dividedToIntegerBy(signedOrder.makerAssetAmount); - const orderRelevantState = { - makerBalance, - makerProxyAllowance, - makerFeeBalance, - makerFeeProxyAllowance, + const remainingFillableAssetAmount = remainingFillableCalculator.computeRemainingFillable(); + + const sidedOrderRelevantState = { + isMakerSide, + traderBalance, + traderProxyAllowance, + traderFeeBalance, + traderFeeProxyAllowance, filledTakerAssetAmount, - remainingFillableMakerAssetAmount, - remainingFillableTakerAssetAmount, + remainingFillableAssetAmount, }; - return orderRelevantState; + return sidedOrderRelevantState; } } diff --git a/packages/order-utils/src/order_validation_utils.ts b/packages/order-utils/src/order_validation_utils.ts index 3a6704f26..fb96502f2 100644 --- a/packages/order-utils/src/order_validation_utils.ts +++ b/packages/order-utils/src/order_validation_utils.ts @@ -5,20 +5,15 @@ import * as _ from 'lodash'; import { OrderError, TradeSide, TransferType } from './types'; +import { AbstractOrderFilledCancelledFetcher } from './abstract/abstract_order_filled_cancelled_fetcher'; import { constants } from './constants'; import { ExchangeTransferSimulator } from './exchange_transfer_simulator'; -import { ExchangeContract } from './generated_contract_wrappers/exchange'; import { orderHashUtils } from './order_hash'; import { isValidSignatureAsync } 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% + private _orderFilledCancelledFetcher: AbstractOrderFilledCancelledFetcher; 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 @@ -55,12 +50,12 @@ export class OrderValidationUtils { public static async validateFillOrderBalancesAllowancesThrowIfInvalidAsync( exchangeTradeEmulator: ExchangeTransferSimulator, signedOrder: SignedOrder, - fillTakerTokenAmount: BigNumber, + fillTakerAssetAmount: BigNumber, senderAddress: string, - zrxTokenAddress: string, + zrxAssetData: string, ): Promise<void> { const fillMakerTokenAmount = OrderValidationUtils._getPartialAmount( - fillTakerTokenAmount, + fillTakerAssetAmount, signedOrder.takerAssetAmount, signedOrder.makerAssetAmount, ); @@ -76,17 +71,17 @@ export class OrderValidationUtils { signedOrder.takerAssetData, senderAddress, signedOrder.makerAddress, - fillTakerTokenAmount, + fillTakerAssetAmount, TradeSide.Taker, TransferType.Trade, ); const makerFeeAmount = OrderValidationUtils._getPartialAmount( - fillTakerTokenAmount, + fillTakerAssetAmount, signedOrder.takerAssetAmount, signedOrder.makerFee, ); await exchangeTradeEmulator.transferFromAsync( - zrxTokenAddress, + zrxAssetData, signedOrder.makerAddress, signedOrder.feeRecipientAddress, makerFeeAmount, @@ -94,12 +89,12 @@ export class OrderValidationUtils { TransferType.Fee, ); const takerFeeAmount = OrderValidationUtils._getPartialAmount( - fillTakerTokenAmount, + fillTakerAssetAmount, signedOrder.takerAssetAmount, signedOrder.takerFee, ); await exchangeTradeEmulator.transferFromAsync( - zrxTokenAddress, + zrxAssetData, senderAddress, signedOrder.feeRecipientAddress, takerFeeAmount, @@ -128,43 +123,43 @@ export class OrderValidationUtils { .round(0); return fillMakerTokenAmount; } - constructor(exchangeContract: ExchangeContract) { - this._exchangeContract = exchangeContract; + constructor(orderFilledCancelledFetcher: AbstractOrderFilledCancelledFetcher) { + this._orderFilledCancelledFetcher = orderFilledCancelledFetcher; } public async validateOrderFillableOrThrowAsync( exchangeTradeEmulator: ExchangeTransferSimulator, signedOrder: SignedOrder, - zrxTokenAddress: string, + zrxAssetData: string, expectedFillTakerTokenAmount?: BigNumber, ): Promise<void> { const orderHash = orderHashUtils.getOrderHashHex(signedOrder); - const filledTakerTokenAmount = await this._exchangeContract.filled.callAsync(orderHash); + const filledTakerTokenAmount = await this._orderFilledCancelledFetcher.getFilledTakerAmountAsync(orderHash); OrderValidationUtils._validateRemainingFillAmountNotZeroOrThrow( signedOrder.takerAssetAmount, filledTakerTokenAmount, ); OrderValidationUtils._validateOrderNotExpiredOrThrow(signedOrder.expirationTimeSeconds); - let fillTakerTokenAmount = signedOrder.takerAssetAmount.minus(filledTakerTokenAmount); + let fillTakerAssetAmount = signedOrder.takerAssetAmount.minus(filledTakerTokenAmount); if (!_.isUndefined(expectedFillTakerTokenAmount)) { - fillTakerTokenAmount = expectedFillTakerTokenAmount; + fillTakerAssetAmount = expectedFillTakerTokenAmount; } await OrderValidationUtils.validateFillOrderBalancesAllowancesThrowIfInvalidAsync( exchangeTradeEmulator, signedOrder, - fillTakerTokenAmount, + fillTakerAssetAmount, signedOrder.takerAddress, - zrxTokenAddress, + zrxAssetData, ); } public async validateFillOrderThrowIfInvalidAsync( exchangeTradeEmulator: ExchangeTransferSimulator, provider: Provider, signedOrder: SignedOrder, - fillTakerTokenAmount: BigNumber, + fillTakerAssetAmount: BigNumber, takerAddress: string, - zrxTokenAddress: string, + zrxAssetData: string, ): Promise<BigNumber> { - if (fillTakerTokenAmount.eq(0)) { + if (fillTakerAssetAmount.eq(0)) { throw new Error(ExchangeContractErrs.OrderFillAmountZero); } const orderHash = orderHashUtils.getOrderHashHex(signedOrder); @@ -177,7 +172,7 @@ export class OrderValidationUtils { if (!isValid) { throw new Error(OrderError.InvalidSignature); } - const filledTakerTokenAmount = await this._exchangeContract.filled.callAsync(orderHash); + const filledTakerTokenAmount = await this._orderFilledCancelledFetcher.getFilledTakerAmountAsync(orderHash); OrderValidationUtils._validateRemainingFillAmountNotZeroOrThrow( signedOrder.takerAssetAmount, filledTakerTokenAmount, @@ -187,19 +182,19 @@ export class OrderValidationUtils { } OrderValidationUtils._validateOrderNotExpiredOrThrow(signedOrder.expirationTimeSeconds); const remainingTakerTokenAmount = signedOrder.takerAssetAmount.minus(filledTakerTokenAmount); - const desiredFillTakerTokenAmount = remainingTakerTokenAmount.lessThan(fillTakerTokenAmount) + const desiredFillTakerTokenAmount = remainingTakerTokenAmount.lessThan(fillTakerAssetAmount) ? remainingTakerTokenAmount - : fillTakerTokenAmount; + : fillTakerAssetAmount; await OrderValidationUtils.validateFillOrderBalancesAllowancesThrowIfInvalidAsync( exchangeTradeEmulator, signedOrder, desiredFillTakerTokenAmount, takerAddress, - zrxTokenAddress, + zrxAssetData, ); const wouldRoundingErrorOccur = OrderValidationUtils.isRoundingError( - filledTakerTokenAmount, + desiredFillTakerTokenAmount, signedOrder.takerAssetAmount, signedOrder.makerAssetAmount, ); @@ -212,19 +207,19 @@ export class OrderValidationUtils { exchangeTradeEmulator: ExchangeTransferSimulator, provider: Provider, signedOrder: SignedOrder, - fillTakerTokenAmount: BigNumber, + fillTakerAssetAmount: BigNumber, takerAddress: string, - zrxTokenAddress: string, + zrxAssetData: string, ): Promise<void> { const filledTakerTokenAmount = await this.validateFillOrderThrowIfInvalidAsync( exchangeTradeEmulator, provider, signedOrder, - fillTakerTokenAmount, + fillTakerAssetAmount, takerAddress, - zrxTokenAddress, + zrxAssetData, ); - if (filledTakerTokenAmount !== fillTakerTokenAmount) { + if (filledTakerTokenAmount !== fillTakerAssetAmount) { throw new Error(ExchangeContractErrs.InsufficientRemainingFillAmount); } } 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 index 08d50b924..e7352119d 100644 --- 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 @@ -19,8 +19,8 @@ export class BalanceAndProxyAllowanceLazyStore implements AbstractBalanceAndProx [userAddress: string]: BigNumber; }; }; - constructor(token: AbstractBalanceAndProxyAllowanceFetcher) { - this._balanceAndProxyAllowanceFetcher = token; + constructor(balanceAndProxyAllowanceFetcher: AbstractBalanceAndProxyAllowanceFetcher) { + this._balanceAndProxyAllowanceFetcher = balanceAndProxyAllowanceFetcher; this._balance = {}; this._proxyAllowance = {}; } diff --git a/packages/order-utils/test/exchange_transfer_simulator_test.ts b/packages/order-utils/test/exchange_transfer_simulator_test.ts index eeae42698..c8fc9fbf3 100644 --- a/packages/order-utils/test/exchange_transfer_simulator_test.ts +++ b/packages/order-utils/test/exchange_transfer_simulator_test.ts @@ -5,6 +5,7 @@ import * as chai from 'chai'; import 'make-promises-safe'; import { artifacts } from '../src/artifacts'; +import { assetProxyUtils } from '../src/asset_proxy_utils'; import { constants } from '../src/constants'; import { ExchangeTransferSimulator } from '../src/exchange_transfer_simulator'; import { DummyERC20TokenContract } from '../src/generated_contract_wrappers/dummy_e_r_c20_token'; @@ -28,7 +29,7 @@ describe('ExchangeTransferSimulator', async () => { let coinbase: string; let sender: string; let recipient: string; - let exampleTokenAddress: string; + let exampleAssetData: string; let exchangeTransferSimulator: ExchangeTransferSimulator; let txHash: string; let erc20ProxyAddress: string; @@ -66,7 +67,7 @@ describe('ExchangeTransferSimulator', async () => { totalSupply, ); - exampleTokenAddress = dummyERC20Token.address; + exampleAssetData = assetProxyUtils.encodeERC20AssetData(dummyERC20Token.address); }); beforeEach(async () => { await blockchainLifecycle.startAsync(); @@ -92,7 +93,7 @@ describe('ExchangeTransferSimulator', async () => { it("throws if the user doesn't have enough allowance", async () => { return expect( exchangeTransferSimulator.transferFromAsync( - exampleTokenAddress, + exampleAssetData, sender, recipient, transferAmount, @@ -108,7 +109,7 @@ describe('ExchangeTransferSimulator', async () => { await web3Wrapper.awaitTransactionSuccessAsync(txHash); return expect( exchangeTransferSimulator.transferFromAsync( - exampleTokenAddress, + exampleAssetData, sender, recipient, transferAmount, @@ -129,7 +130,7 @@ describe('ExchangeTransferSimulator', async () => { await web3Wrapper.awaitTransactionSuccessAsync(txHash); await exchangeTransferSimulator.transferFromAsync( - exampleTokenAddress, + exampleAssetData, sender, recipient, transferAmount, @@ -137,9 +138,9 @@ describe('ExchangeTransferSimulator', async () => { 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); + const senderBalance = await store.getBalanceAsync(exampleAssetData, sender); + const recipientBalance = await store.getBalanceAsync(exampleAssetData, recipient); + const senderProxyAllowance = await store.getProxyAllowanceAsync(exampleAssetData, sender); expect(senderBalance).to.be.bignumber.equal(0); expect(recipientBalance).to.be.bignumber.equal(transferAmount); expect(senderProxyAllowance).to.be.bignumber.equal(0); @@ -158,7 +159,7 @@ describe('ExchangeTransferSimulator', async () => { ); await web3Wrapper.awaitTransactionSuccessAsync(txHash); await exchangeTransferSimulator.transferFromAsync( - exampleTokenAddress, + exampleAssetData, sender, recipient, transferAmount, @@ -166,9 +167,9 @@ describe('ExchangeTransferSimulator', async () => { 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); + const senderBalance = await store.getBalanceAsync(exampleAssetData, sender); + const recipientBalance = await store.getBalanceAsync(exampleAssetData, recipient); + const senderProxyAllowance = await store.getProxyAllowanceAsync(exampleAssetData, 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); diff --git a/packages/order-watcher/test/global_hooks.ts b/packages/order-watcher/test/global_hooks.ts index f18eef379..2018c7545 100644 --- a/packages/order-watcher/test/global_hooks.ts +++ b/packages/order-watcher/test/global_hooks.ts @@ -7,7 +7,7 @@ import { provider } from './utils/web3_wrapper'; before('migrate contracts', async function(): Promise<void> { // HACK: Since the migrations take longer then our global mocha timeout limit // we manually increase it for this before hook. - const mochaTestTimeoutMs = 20000; + const mochaTestTimeoutMs = 25000; this.timeout(mochaTestTimeoutMs); const txDefaults = { gas: devConstants.GAS_LIMIT, |