diff options
Diffstat (limited to 'packages/contracts/test/utils')
-rw-r--r-- | packages/contracts/test/utils/match_order_tester.ts | 353 |
1 files changed, 353 insertions, 0 deletions
diff --git a/packages/contracts/test/utils/match_order_tester.ts b/packages/contracts/test/utils/match_order_tester.ts new file mode 100644 index 000000000..91c2ab0a3 --- /dev/null +++ b/packages/contracts/test/utils/match_order_tester.ts @@ -0,0 +1,353 @@ +import { LogWithDecodedArgs, ZeroEx } from '0x.js'; +import { BlockchainLifecycle } from '@0xproject/dev-utils'; +import { BigNumber } from '@0xproject/utils'; +import * as chai from 'chai'; +import ethUtil = require('ethereumjs-util'); +import * as _ from 'lodash'; + +import { DummyERC20TokenContract } from '../../src/contract_wrappers/generated/dummy_e_r_c20_token'; +import { DummyERC721TokenContract } from '../../src/contract_wrappers/generated/dummy_e_r_c721_token'; +import { ERC20ProxyContract } from '../../src/contract_wrappers/generated/e_r_c20_proxy'; +import { ERC721ProxyContract } from '../../src/contract_wrappers/generated/e_r_c721_proxy'; +import { + CancelContractEventArgs, + ExchangeContract, + FillContractEventArgs, +} from '../../src/contract_wrappers/generated/exchange'; +import { assetProxyUtils } from '../../src/utils/asset_proxy_utils'; +import { constants } from '../../src/utils/constants'; +import { crypto } from '../../src/utils/crypto'; +import { ERC20Wrapper } from '../../src/utils/erc20_wrapper'; +import { ERC721Wrapper } from '../../src/utils/erc721_wrapper'; +import { ExchangeWrapper } from '../../src/utils/exchange_wrapper'; +import { OrderFactory } from '../../src/utils/order_factory'; +import { orderUtils } from '../../src/utils/order_utils'; +import { + AssetProxyId, + ContractName, + ERC20BalancesByOwner, + ERC721TokenIdsByOwner, + ExchangeStatus, + SignedOrder, + TransferAmountsByMatchOrders as TransferAmounts, +} from '../../src/utils/types'; +import { chaiSetup } from '../utils/chai_setup'; +import { deployer } from '../utils/deployer'; +import { provider, web3Wrapper } from '../utils/web3_wrapper'; + +chaiSetup.configure(); +const expect = chai.expect; +const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); + +export class MatchOrderTester { + private _exchangeWrapper: ExchangeWrapper; + private _erc20Wrapper: ERC20Wrapper; + private _erc721Wrapper: ERC721Wrapper; + + /// @dev Calculates the expected balances of order makers, fee recipients, and the taker, + /// as a result of matching two orders. + /// @param signedOrderLeft First matched order. + /// @param signedOrderRight Second matched order. + /// @param feeTokenAddress Address of ERC20 fee token. + /// @param takerAddress Address of taker (the address who matched the two orders) + /// @param erc20BalancesByOwner Current ERC20 balances. + /// @param erc721TokenIdsByOwner Current ERC721 token owners. + /// @param expectedTransferAmounts A struct containing the expected transfer amounts. + /// @return Expected ERC20 balances & ERC721 token owners after orders have been matched. + private static _calculateExpectedBalances( + signedOrderLeft: SignedOrder, + signedOrderRight: SignedOrder, + feeTokenAddress: string, + takerAddress: string, + erc20BalancesByOwner: ERC20BalancesByOwner, + erc721TokenIdsByOwner: ERC721TokenIdsByOwner, + expectedTransferAmounts: TransferAmounts, + ): [ERC20BalancesByOwner, ERC721TokenIdsByOwner] { + const makerAddressLeft = signedOrderLeft.makerAddress; + const makerAddressRight = signedOrderRight.makerAddress; + const feeRecipientAddressLeft = signedOrderLeft.feeRecipientAddress; + const feeRecipientAddressRight = signedOrderRight.feeRecipientAddress; + // Operations are performed on copies of the balances + const expectedNewERC20BalancesByOwner = _.cloneDeep(erc20BalancesByOwner); + const expectedNewERC721TokenIdsByOwner = _.cloneDeep(erc721TokenIdsByOwner); + // Left Maker Asset (Right Taker Asset) + const makerAssetProxyIdLeft = assetProxyUtils.decodeProxyDataId(signedOrderLeft.makerAssetData); + if (makerAssetProxyIdLeft === AssetProxyId.ERC20) { + // Decode asset data + const makerAssetAddressLeft = assetProxyUtils.decodeERC20ProxyData(signedOrderLeft.makerAssetData); + const takerAssetAddressRight = makerAssetAddressLeft; + // Left Maker + expectedNewERC20BalancesByOwner[makerAddressLeft][makerAssetAddressLeft] = expectedNewERC20BalancesByOwner[ + makerAddressLeft + ][makerAssetAddressLeft].minus(expectedTransferAmounts.amountSoldByLeftMaker); + // Right Maker + expectedNewERC20BalancesByOwner[makerAddressRight][ + takerAssetAddressRight + ] = expectedNewERC20BalancesByOwner[makerAddressRight][takerAssetAddressRight].add( + expectedTransferAmounts.amountReceivedByRightMaker, + ); + // Taker + expectedNewERC20BalancesByOwner[takerAddress][makerAssetAddressLeft] = expectedNewERC20BalancesByOwner[ + takerAddress + ][makerAssetAddressLeft].add(expectedTransferAmounts.amountReceivedByTaker); + } else if (makerAssetProxyIdLeft === AssetProxyId.ERC721) { + // Decode asset data + let makerAssetAddressLeft; + let makerAssetIdLeft; + [makerAssetAddressLeft, makerAssetIdLeft] = assetProxyUtils.decodeERC721ProxyData( + signedOrderLeft.makerAssetData, + ); + const takerAssetAddressRight = makerAssetAddressLeft; + const takerAssetIdRight = makerAssetIdLeft; + // Left Maker + _.remove(expectedNewERC721TokenIdsByOwner[makerAddressLeft][makerAssetAddressLeft], makerAssetIdLeft); + // Right Maker + expectedNewERC721TokenIdsByOwner[makerAddressRight][takerAssetAddressRight].push(takerAssetIdRight); + // Taker: Since there is only 1 asset transferred, the taker does not receive any of the left maker asset. + } + // Left Taker Asset (Right Maker Asset) + // Note: This exchange is only between the order makers: the Taker does not receive any of the left taker asset. + const takerAssetProxyIdLeft = assetProxyUtils.decodeProxyDataId(signedOrderLeft.takerAssetData); + if (takerAssetProxyIdLeft === AssetProxyId.ERC20) { + // Decode asset data + const takerAssetAddressLeft = assetProxyUtils.decodeERC20ProxyData(signedOrderLeft.takerAssetData); + const makerAssetAddressRight = takerAssetAddressLeft; + // Left Maker + expectedNewERC20BalancesByOwner[makerAddressLeft][takerAssetAddressLeft] = expectedNewERC20BalancesByOwner[ + makerAddressLeft + ][takerAssetAddressLeft].add(expectedTransferAmounts.amountReceivedByLeftMaker); + // Right Maker + expectedNewERC20BalancesByOwner[makerAddressRight][ + makerAssetAddressRight + ] = expectedNewERC20BalancesByOwner[makerAddressRight][makerAssetAddressRight].minus( + expectedTransferAmounts.amountSoldByRightMaker, + ); + } else if (takerAssetProxyIdLeft === AssetProxyId.ERC721) { + // Decode asset data + let makerAssetAddressRight; + let makerAssetIdRight; + [makerAssetAddressRight, makerAssetIdRight] = assetProxyUtils.decodeERC721ProxyData( + signedOrderRight.makerAssetData, + ); + const takerAssetAddressLeft = makerAssetAddressRight; + const takerAssetIdLeft = makerAssetIdRight; + // Right Maker + _.remove(expectedNewERC721TokenIdsByOwner[makerAddressRight][makerAssetAddressRight], makerAssetIdRight); + // Left Maker + expectedNewERC721TokenIdsByOwner[makerAddressLeft][takerAssetAddressLeft].push(takerAssetIdLeft); + } + // Left Maker Fees + expectedNewERC20BalancesByOwner[makerAddressLeft][feeTokenAddress] = expectedNewERC20BalancesByOwner[ + makerAddressLeft + ][feeTokenAddress].minus(expectedTransferAmounts.feePaidByLeftMaker); + // Right Maker Fees + expectedNewERC20BalancesByOwner[makerAddressRight][feeTokenAddress] = expectedNewERC20BalancesByOwner[ + makerAddressRight + ][feeTokenAddress].minus(expectedTransferAmounts.feePaidByRightMaker); + // Taker Fees + expectedNewERC20BalancesByOwner[takerAddress][feeTokenAddress] = expectedNewERC20BalancesByOwner[takerAddress][ + feeTokenAddress + ].minus(expectedTransferAmounts.totalFeePaidByTaker); + // Left Fee Recipient Fees + expectedNewERC20BalancesByOwner[feeRecipientAddressLeft][feeTokenAddress] = expectedNewERC20BalancesByOwner[ + feeRecipientAddressLeft + ][feeTokenAddress].add(expectedTransferAmounts.feeReceivedLeft); + // Right Fee Recipient Fees + expectedNewERC20BalancesByOwner[feeRecipientAddressRight][feeTokenAddress] = expectedNewERC20BalancesByOwner[ + feeRecipientAddressRight + ][feeTokenAddress].add(expectedTransferAmounts.feeReceivedRight); + + return [expectedNewERC20BalancesByOwner, expectedNewERC721TokenIdsByOwner]; + } + + /// @dev Compares a pair of ERC20 balances and a pair of ERC721 token owners. + /// @param expectedNewERC20BalancesByOwner Expected ERC20 balances. + /// @param realERC20BalancesByOwner Actual ERC20 balances. + /// @param expectedNewERC721TokenIdsByOwner Expected ERC721 token owners. + /// @param realERC721TokenIdsByOwner Actual ERC20 token owners. + /// @return True only if ERC20 balances match and ERC721 token owners match. + private static _compareExpectedAndRealBalances( + expectedNewERC20BalancesByOwner: ERC20BalancesByOwner, + realERC20BalancesByOwner: ERC20BalancesByOwner, + expectedNewERC721TokenIdsByOwner: ERC721TokenIdsByOwner, + realERC721TokenIdsByOwner: ERC721TokenIdsByOwner, + ) { + // ERC20 Balances + const erc20BalancesMatch = _.isEqual(expectedNewERC20BalancesByOwner, realERC20BalancesByOwner); + if (!erc20BalancesMatch) { + return false; + } + // ERC721 Token Ids + const sortedExpectedNewERC721TokenIdsByOwner = _.mapValues( + expectedNewERC721TokenIdsByOwner, + tokenIdsByOwner => { + _.mapValues(tokenIdsByOwner, tokenIds => { + _.sortBy(tokenIds); + }); + }, + ); + const sortedNewERC721TokenIdsByOwner = _.mapValues(realERC721TokenIdsByOwner, tokenIdsByOwner => { + _.mapValues(tokenIdsByOwner, tokenIds => { + _.sortBy(tokenIds); + }); + }); + const erc721TokenIdsMatch = _.isEqual(sortedExpectedNewERC721TokenIdsByOwner, sortedNewERC721TokenIdsByOwner); + return erc721TokenIdsMatch; + } + + /// @dev Constructs new MatchOrderTester. + /// @param exchangeWrapper Used to call to the Exchange. + /// @param erc20Wrapper Used to fetch ERC20 balances. + /// @param erc721Wrapper Used to fetch ERC721 token owners. + constructor(exchangeWrapper: ExchangeWrapper, erc20Wrapper: ERC20Wrapper, erc721Wrapper: ERC721Wrapper) { + this._exchangeWrapper = exchangeWrapper; + this._erc20Wrapper = erc20Wrapper; + this._erc721Wrapper = erc721Wrapper; + } + + /// @dev Matches two complementary orders and validates results. + /// Validation either succeeds or throws. + /// @param signedOrderLeft First matched order. + /// @param signedOrderRight Second matched order. + /// @param feeTokenAddress Address of ERC20 fee token. + /// @param takerAddress Address of taker (the address who matched the two orders) + /// @param erc20BalancesByOwner Current ERC20 balances. + /// @param erc721TokenIdsByOwner Current ERC721 token owners. + /// @param initialTakerAssetFilledAmountLeft Current amount the left order has been filled. + /// @param initialTakerAssetFilledAmountRight Current amount the right order has been filled. + /// @return New ERC20 balances & ERC721 token owners. + public async matchOrdersAndVerifyBalancesAsync( + signedOrderLeft: SignedOrder, + signedOrderRight: SignedOrder, + feeTokenAddress: string, + takerAddress: string, + erc20BalancesByOwner: ERC20BalancesByOwner, + erc721TokenIdsByOwner: ERC721TokenIdsByOwner, + initialTakerAssetFilledAmountLeft?: BigNumber, + initialTakerAssetFilledAmountRight?: BigNumber, + ): Promise<[ERC20BalancesByOwner, ERC721TokenIdsByOwner]> { + // Test setup & verify preconditions + const makerAddressLeft = signedOrderLeft.makerAddress; + const makerAddressRight = signedOrderRight.makerAddress; + const feeRecipientAddressLeft = signedOrderLeft.feeRecipientAddress; + const feeRecipientAddressRight = signedOrderRight.feeRecipientAddress; + // Verify Left order preconditions + const takerAssetFilledAmountBeforeLeft = await this._exchangeWrapper.getTakerAssetFilledAmountAsync( + orderUtils.getOrderHashHex(signedOrderLeft), + ); + const expectedLeftOrderFillAmoutBeforeMatch = initialTakerAssetFilledAmountLeft + ? initialTakerAssetFilledAmountLeft + : new BigNumber(0); + expect(takerAssetFilledAmountBeforeLeft).to.be.bignumber.equal(expectedLeftOrderFillAmoutBeforeMatch); + // Verify Right order preconditions + const takerAssetFilledAmountBeforeRight = await this._exchangeWrapper.getTakerAssetFilledAmountAsync( + orderUtils.getOrderHashHex(signedOrderRight), + ); + const expectedRightOrderFillAmoutBeforeMatch = initialTakerAssetFilledAmountRight + ? initialTakerAssetFilledAmountRight + : new BigNumber(0); + expect(takerAssetFilledAmountBeforeRight).to.be.bignumber.equal(expectedRightOrderFillAmoutBeforeMatch); + // Match left & right orders + await this._exchangeWrapper.matchOrdersAsync(signedOrderLeft, signedOrderRight, takerAddress); + const newERC20BalancesByOwner = await this._erc20Wrapper.getBalancesAsync(); + const newERC721TokenIdsByOwner = await this._erc721Wrapper.getBalancesAsync(); + // Calculate expected balance changes + const expectedTransferAmounts = await this._calculateExpectedTransferAmountsAsync( + signedOrderLeft, + signedOrderRight, + expectedLeftOrderFillAmoutBeforeMatch, + expectedRightOrderFillAmoutBeforeMatch, + ); + let expectedERC20BalancesByOwner: ERC20BalancesByOwner; + let expectedERC721TokenIdsByOwner: ERC721TokenIdsByOwner; + [expectedERC20BalancesByOwner, expectedERC721TokenIdsByOwner] = MatchOrderTester._calculateExpectedBalances( + signedOrderLeft, + signedOrderRight, + feeTokenAddress, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + expectedTransferAmounts, + ); + // Assert our expected balances are equal to the actual balances + const expectedBalancesMatchRealBalances = MatchOrderTester._compareExpectedAndRealBalances( + expectedERC20BalancesByOwner, + newERC20BalancesByOwner, + expectedERC721TokenIdsByOwner, + newERC721TokenIdsByOwner, + ); + expect(expectedBalancesMatchRealBalances).to.be.true(); + return [newERC20BalancesByOwner, newERC721TokenIdsByOwner]; + } + + /// @dev Calculates expected transfer amounts between order makers, fee recipients, and + /// the taker when two orders are matched. + /// @param signedOrderLeft First matched order. + /// @param signedOrderRight Second matched order. + /// @param expectedLeftOrderFillAmoutBeforeMatch How much we expect the left order has been filled, prior to matching orders. + /// @param expectedRightOrderFillAmoutBeforeMatch How much we expect the right order has been filled, prior to matching orders. + /// @return TransferAmounts A struct containing the expected transfer amounts. + private async _calculateExpectedTransferAmountsAsync( + signedOrderLeft: SignedOrder, + signedOrderRight: SignedOrder, + expectedLeftOrderFillAmoutBeforeMatch: BigNumber, + expectedRightOrderFillAmoutBeforeMatch: BigNumber, + ): Promise<TransferAmounts> { + let amountBoughtByLeftMaker = await this._exchangeWrapper.getTakerAssetFilledAmountAsync( + orderUtils.getOrderHashHex(signedOrderLeft), + ); + amountBoughtByLeftMaker = amountBoughtByLeftMaker.minus(expectedLeftOrderFillAmoutBeforeMatch); + const amountSoldByLeftMaker = amountBoughtByLeftMaker + .times(signedOrderLeft.makerAssetAmount) + .dividedToIntegerBy(signedOrderLeft.takerAssetAmount); + const amountReceivedByRightMaker = amountBoughtByLeftMaker + .times(signedOrderRight.takerAssetAmount) + .dividedToIntegerBy(signedOrderRight.makerAssetAmount); + const amountReceivedByTaker = amountSoldByLeftMaker.minus(amountReceivedByRightMaker); + let amountBoughtByRightMaker = await this._exchangeWrapper.getTakerAssetFilledAmountAsync( + orderUtils.getOrderHashHex(signedOrderRight), + ); + amountBoughtByRightMaker = amountBoughtByRightMaker.minus(expectedRightOrderFillAmoutBeforeMatch); + const amountSoldByRightMaker = amountBoughtByRightMaker + .times(signedOrderRight.makerAssetAmount) + .dividedToIntegerBy(signedOrderRight.takerAssetAmount); + const amountReceivedByLeftMaker = amountSoldByRightMaker; + const feePaidByLeftMaker = signedOrderLeft.makerFee + .times(amountSoldByLeftMaker) + .dividedToIntegerBy(signedOrderLeft.makerAssetAmount); + const feePaidByRightMaker = signedOrderRight.makerFee + .times(amountSoldByRightMaker) + .dividedToIntegerBy(signedOrderRight.makerAssetAmount); + const feePaidByTakerLeft = signedOrderLeft.takerFee + .times(amountSoldByLeftMaker) + .dividedToIntegerBy(signedOrderLeft.makerAssetAmount); + const feePaidByTakerRight = signedOrderRight.takerFee + .times(amountSoldByRightMaker) + .dividedToIntegerBy(signedOrderRight.makerAssetAmount); + const totalFeePaidByTaker = feePaidByTakerLeft.add(feePaidByTakerRight); + const feeReceivedLeft = feePaidByLeftMaker.add(feePaidByTakerLeft); + const feeReceivedRight = feePaidByRightMaker.add(feePaidByTakerRight); + // Return values + const expectedTransferAmounts = { + // Left Maker + amountBoughtByLeftMaker, + amountSoldByLeftMaker, + amountReceivedByLeftMaker, + feePaidByLeftMaker, + // Right Maker + amountBoughtByRightMaker, + amountSoldByRightMaker, + amountReceivedByRightMaker, + feePaidByRightMaker, + // Taker + amountReceivedByTaker, + feePaidByTakerLeft, + feePaidByTakerRight, + totalFeePaidByTaker, + // Fee Recipients + feeReceivedLeft, + feeReceivedRight, + }; + return expectedTransferAmounts; + } +} |