diff --git a/contracts/exchange-libs/contracts/test/TestLibFillResults.sol b/contracts/exchange-libs/contracts/test/TestLibFillResults.sol index 501d228762..714efbdfae 100644 --- a/contracts/exchange-libs/contracts/test/TestLibFillResults.sol +++ b/contracts/exchange-libs/contracts/test/TestLibFillResults.sol @@ -25,6 +25,8 @@ import "../src/LibFillResults.sol"; contract TestLibFillResults { + using LibFillResults for *; + function calculateFillResults( LibOrder.Order memory order, uint256 takerAssetFilledAmount diff --git a/contracts/exchange-libs/test/lib_fill_results.ts b/contracts/exchange-libs/test/lib_fill_results.ts index d4b2e5d668..e56040e7e7 100644 --- a/contracts/exchange-libs/test/lib_fill_results.ts +++ b/contracts/exchange-libs/test/lib_fill_results.ts @@ -3,17 +3,26 @@ import { constants, describe, expect, + hexRandom, testCombinatoriallyWithReferenceFunc, uint256Values, } from '@0x/contracts-test-utils'; import { LibMathRevertErrors } from '@0x/order-utils'; -import { FillResults, OrderWithoutDomain as Order } from '@0x/types'; +import { FillResults, MatchedFillResults, OrderWithoutDomain as Order } from '@0x/types'; import { BigNumber, SafeMathRevertErrors } from '@0x/utils'; +import { Web3Wrapper } from '@0x/web3-wrapper'; import * as _ from 'lodash'; import { artifacts, ReferenceFunctions, TestLibFillResultsContract } from '../src'; blockchainTests('LibFillResults', env => { + interface PartialMatchedFillResults { + left: Partial; + right: Partial; + profitInLeftMakerAsset?: BigNumber; + profitInRightMakerAsset?: BigNumber; + } + const { ONE_ETHER, MAX_UINT256 } = constants; const EMPTY_ORDER: Order = { senderAddress: constants.NULL_ADDRESS, @@ -31,9 +40,59 @@ blockchainTests('LibFillResults', env => { feeRecipientAddress: constants.NULL_ADDRESS, expirationTimeSeconds: constants.ZERO_AMOUNT, }; + const EMPTY_FILL_RESULTS: FillResults = { + makerAssetFilledAmount: constants.ZERO_AMOUNT, + takerAssetFilledAmount: constants.ZERO_AMOUNT, + makerFeePaid: constants.ZERO_AMOUNT, + takerFeePaid: constants.ZERO_AMOUNT, + }; + const EMPTY_MATCHED_FILL_RESULTS: MatchedFillResults = { + left: EMPTY_FILL_RESULTS, + right: EMPTY_FILL_RESULTS, + profitInLeftMakerAsset: constants.ZERO_AMOUNT, + profitInRightMakerAsset: constants.ZERO_AMOUNT, + }; + const COMMON_MATCHED_FILL_RESULTS = { + left: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + }, + right: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(2, 18), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + }, + profitInLeftMakerAsset: Web3Wrapper.toBaseUnitAmount(3, 18), + profitInRightMakerAsset: constants.ZERO_AMOUNT, + }; + + const randomAddress = () => hexRandom(constants.ADDRESS_LENGTH); + const randomAssetData = () => hexRandom(36); + const randomUint256 = () => new BigNumber(hexRandom(constants.WORD_LENGTH)); + + function createMatchedFillResults(partialMatchedFillResults: PartialMatchedFillResults): MatchedFillResults { + const matchedFillResults = EMPTY_MATCHED_FILL_RESULTS; + matchedFillResults.left = _.assign({}, EMPTY_FILL_RESULTS, partialMatchedFillResults.left); + matchedFillResults.right = _.assign({}, EMPTY_FILL_RESULTS, partialMatchedFillResults.right); + matchedFillResults.profitInLeftMakerAsset = + partialMatchedFillResults.profitInLeftMakerAsset || constants.ZERO_AMOUNT; + matchedFillResults.profitInRightMakerAsset = + partialMatchedFillResults.profitInRightMakerAsset || constants.ZERO_AMOUNT; + return matchedFillResults; + } + let libsContract: TestLibFillResultsContract; + let makerAddressLeft: string; + let makerAddressRight: string; before(async () => { + const accounts = await env.getAccountAddressesAsync(); + makerAddressLeft = accounts[0]; + makerAddressRight = accounts[1]; + libsContract = await TestLibFillResultsContract.deployFrom0xArtifactAsync( artifacts.TestLibFillResults, env.provider, @@ -324,4 +383,1278 @@ blockchainTests('LibFillResults', env => { }); }); }); + + blockchainTests('calculateMatchedFillResults', async () => { + /** + * Asserts that the results of calling `calculateMatchedFillResults()` is consistent with the results that are expected. + */ + async function assertCalculateMatchedFillResultsAsync( + expectedMatchedFillResults: MatchedFillResults, + leftOrder: Order, + rightOrder: Order, + leftOrderTakerAssetFilledAmount: BigNumber, + rightOrderTakerAssetFilledAmount: BigNumber, + from?: string, + ): Promise { + const actualMatchedFillResults = await libsContract.calculateMatchedFillResults.callAsync( + leftOrder, + rightOrder, + leftOrderTakerAssetFilledAmount, + rightOrderTakerAssetFilledAmount, + false, + { from }, + ); + expect(actualMatchedFillResults).to.be.deep.eq(expectedMatchedFillResults); + } + + const ORDER_DEFAULTS = { + ...constants.STATIC_ORDER_PARAMS, + makerAddress: randomAddress(), + takerAddress: randomAddress(), + senderAddress: randomAddress(), + makerAssetData: randomAssetData(), + takerAssetData: randomAssetData(), + makerFeeAssetData: randomAssetData(), + takerFeeAssetData: randomAssetData(), + feeRecipientAddress: randomAddress(), + expirationTimeSeconds: randomUint256(), + salt: randomUint256(), + domain: { + verifyingContractAddress: constants.NULL_ADDRESS, + chainId: 1337, // The chain id for the isolated exchange + }, + }; + + function makeOrder(details?: Partial): Order { + return _.assign({}, ORDER_DEFAULTS, details); + } + + before(async () => { + ORDER_DEFAULTS.domain.verifyingContractAddress = libsContract.address; + }); + + it('should correctly calculate the results when only the right order is fully filled', async () => { + const leftOrder = makeOrder({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(17, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(98, 0), + }); + const rightOrder = makeOrder({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(75, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(13, 0), + }); + const expectedMatchedFillResults = createMatchedFillResults({ + left: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(13, 0), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(75, 0), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(new BigNumber('76.4705882352941176'), 16), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(new BigNumber('76.5306122448979591'), 16), + }, + right: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(75, 0), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(13, 0), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + }, + }); + await assertCalculateMatchedFillResultsAsync( + expectedMatchedFillResults, + leftOrder, + rightOrder, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + ); + }); + + it('should correctly calculate the results when only the left order is fully filled', async () => { + const leftOrder = makeOrder({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(15, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(90, 0), + }); + const rightOrder = makeOrder({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(97, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(14, 0), + }); + const expectedMatchedFillResults = createMatchedFillResults({ + left: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(15, 0), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(90, 0), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + }, + right: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(90, 0), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(13, 0), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(new BigNumber('92.7835051546391752'), 16), // 92.85% + takerFeePaid: Web3Wrapper.toBaseUnitAmount(new BigNumber('92.8571428571428571'), 16), // 92.85% + }, + profitInLeftMakerAsset: Web3Wrapper.toBaseUnitAmount(2, 0), + }); + await assertCalculateMatchedFillResultsAsync( + expectedMatchedFillResults, + leftOrder, + rightOrder, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + ); + }); + + it('should give right maker a better price when rounding', async () => { + const leftOrder = makeOrder({ + makerAddress: makerAddressLeft, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(16, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(22, 0), + }); + const rightOrder = makeOrder({ + makerAddress: makerAddressRight, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(83, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(49, 0), + }); + const expectedMatchedFillResults = createMatchedFillResults({ + left: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(16, 0), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(22, 0), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + }, + right: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(22, 0), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(13, 0), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(new BigNumber('26.5060240963855421'), 16), // 26.506% + takerFeePaid: Web3Wrapper.toBaseUnitAmount(new BigNumber('26.5306122448979591'), 16), // 26.531% + }, + profitInLeftMakerAsset: Web3Wrapper.toBaseUnitAmount(3, 0), + }); + await assertCalculateMatchedFillResultsAsync( + expectedMatchedFillResults, + leftOrder, + rightOrder, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + ); + }); + + it('should give left maker a better sell price when rounding', async () => { + const leftOrder = makeOrder({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(12, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(97, 0), + makerAddress: makerAddressLeft, + }); + const rightOrder = makeOrder({ + makerAddress: makerAddressRight, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(89, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(1, 0), + }); + const expectedMatchedFillResults = createMatchedFillResults({ + left: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(11, 0), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(89, 0), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(new BigNumber('91.6666666666666666'), 16), // 91.6% + takerFeePaid: Web3Wrapper.toBaseUnitAmount(new BigNumber('91.7525773195876288'), 16), // 91.75% + }, + right: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(89, 0), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(1, 0), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + }, + profitInLeftMakerAsset: Web3Wrapper.toBaseUnitAmount(10, 0), + }); + await assertCalculateMatchedFillResultsAsync( + expectedMatchedFillResults, + leftOrder, + rightOrder, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + ); + }); + + it('Should give right maker and right taker a favorable fee price when rounding', async () => { + const leftOrder = makeOrder({ + makerAddress: makerAddressLeft, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(16, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(22, 0), + }); + const rightOrder = makeOrder({ + makerAddress: makerAddressRight, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(83, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(49, 0), + makerFee: Web3Wrapper.toBaseUnitAmount(10000, 0), + takerFee: Web3Wrapper.toBaseUnitAmount(10000, 0), + }); + const expectedMatchedFillResults = createMatchedFillResults({ + left: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(16, 0), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(22, 0), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + }, + right: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(22, 0), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(13, 0), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(2650, 0), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(2653, 0), + }, + profitInLeftMakerAsset: Web3Wrapper.toBaseUnitAmount(3, 0), + }); + await assertCalculateMatchedFillResultsAsync( + expectedMatchedFillResults, + leftOrder, + rightOrder, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + ); + }); + + it('Should give left maker and left taker a favorable fee price when rounding', async () => { + // Create orders to match + const leftOrder = makeOrder({ + makerAddress: makerAddressLeft, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(12, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(97, 0), + makerFee: Web3Wrapper.toBaseUnitAmount(10000, 0), + takerFee: Web3Wrapper.toBaseUnitAmount(10000, 0), + }); + const rightOrder = makeOrder({ + makerAddress: makerAddressRight, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(89, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(1, 0), + }); + const expectedMatchedFillResults = createMatchedFillResults({ + left: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(11, 0), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(89, 0), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(9166, 0), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(9175, 0), + }, + right: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(89, 0), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(1, 0), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + }, + profitInLeftMakerAsset: Web3Wrapper.toBaseUnitAmount(10, 0), + }); + await assertCalculateMatchedFillResultsAsync( + expectedMatchedFillResults, + leftOrder, + rightOrder, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + ); + }); + + it('Should transfer correct amounts when right order fill amount deviates from amount derived by `Exchange.fillOrder`', async () => { + const leftOrder = makeOrder({ + makerAddress: makerAddressLeft, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(1000, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(1005, 0), + }); + const rightOrder = makeOrder({ + makerAddress: makerAddressRight, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(2126, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(1063, 0), + }); + const expectedMatchedFillResults = createMatchedFillResults({ + left: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(1000, 0), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(1005, 0), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + }, + right: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(1005, 0), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(503, 0), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(new BigNumber('47.2718720602069614'), 16), // 47.27% + takerFeePaid: Web3Wrapper.toBaseUnitAmount(new BigNumber('47.3189087488240827'), 16), // 47.31% + }, + profitInLeftMakerAsset: Web3Wrapper.toBaseUnitAmount(497, 0), + }); + await assertCalculateMatchedFillResultsAsync( + expectedMatchedFillResults, + leftOrder, + rightOrder, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + ); + }); + + it('should transfer the correct amounts when orders completely fill each other', async () => { + const leftOrder = makeOrder({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + }); + const rightOrder = makeOrder({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(2, 18), + }); + const expectedMatchedFillResults = createMatchedFillResults({ + left: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + }, + right: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(2, 18), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + }, + profitInLeftMakerAsset: Web3Wrapper.toBaseUnitAmount(3, 18), + }); + await assertCalculateMatchedFillResultsAsync( + expectedMatchedFillResults, + leftOrder, + rightOrder, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + ); + }); + + it('should transfer the correct amounts when orders completely fill each other and taker doesnt take a profit', async () => { + const leftOrder = makeOrder({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + }); + const rightOrder = makeOrder({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + }); + const expectedMatchedFillResults = createMatchedFillResults({ + left: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + }, + right: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + }, + }); + await assertCalculateMatchedFillResultsAsync( + expectedMatchedFillResults, + leftOrder, + rightOrder, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + ); + }); + + it('should transfer the correct amounts when left order is completely filled and right order is partially filled', async () => { + const leftOrder = makeOrder({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + }); + const rightOrder = makeOrder({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(20, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(4, 18), + }); + const expectedMatchedFillResults = createMatchedFillResults({ + left: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + }, + right: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(2, 18), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(50, 16), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(50, 16), + }, + profitInLeftMakerAsset: Web3Wrapper.toBaseUnitAmount(3, 18), + }); + await assertCalculateMatchedFillResultsAsync( + expectedMatchedFillResults, + leftOrder, + rightOrder, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + ); + }); + + it('should transfer the correct amounts when right order is completely filled and left order is partially filled', async () => { + const leftOrder = makeOrder({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(50, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(100, 18), + }); + const rightOrder = makeOrder({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(2, 18), + }); + const expectedMatchedFillResults = createMatchedFillResults({ + left: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(10, 16), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(10, 16), + }, + right: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(2, 18), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + }, + profitInLeftMakerAsset: Web3Wrapper.toBaseUnitAmount(3, 18), + }); + await assertCalculateMatchedFillResultsAsync( + expectedMatchedFillResults, + leftOrder, + rightOrder, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + ); + }); + it('should transfer the correct amounts if fee recipient is the same across both matched orders', async () => { + const feeRecipientAddress = randomAddress(); + const leftOrder = makeOrder({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + feeRecipientAddress, + }); + const rightOrder = makeOrder({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(2, 18), + feeRecipientAddress, + }); + await assertCalculateMatchedFillResultsAsync( + COMMON_MATCHED_FILL_RESULTS, + leftOrder, + rightOrder, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + ); + }); + + it('should transfer the correct amounts if taker == leftMaker', async () => { + const leftOrder = makeOrder({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + }); + const rightOrder = makeOrder({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(2, 18), + }); + await assertCalculateMatchedFillResultsAsync( + COMMON_MATCHED_FILL_RESULTS, + leftOrder, + rightOrder, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + leftOrder.makerAddress, + ); + }); + + it('should transfer the correct amounts if taker == leftMaker', async () => { + const leftOrder = makeOrder({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + }); + const rightOrder = makeOrder({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(2, 18), + }); + await assertCalculateMatchedFillResultsAsync( + COMMON_MATCHED_FILL_RESULTS, + leftOrder, + rightOrder, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + rightOrder.makerAddress, + ); + }); + + it('should transfer the correct amounts if taker == leftFeeRecipient', async () => { + const feeRecipientAddressLeft = randomAddress(); + const leftOrder = makeOrder({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const rightOrder = makeOrder({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(2, 18), + }); + await assertCalculateMatchedFillResultsAsync( + COMMON_MATCHED_FILL_RESULTS, + leftOrder, + rightOrder, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + feeRecipientAddressLeft, + ); + }); + + it('should transfer the correct amounts if taker == rightFeeRecipient', async () => { + const feeRecipientAddressRight = randomAddress(); + const leftOrder = makeOrder({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + }); + const rightOrder = makeOrder({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(2, 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + await assertCalculateMatchedFillResultsAsync( + COMMON_MATCHED_FILL_RESULTS, + leftOrder, + rightOrder, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + feeRecipientAddressRight, + ); + }); + + it('should transfer the correct amounts if leftMaker == leftFeeRecipient && rightMaker == rightFeeRecipient', async () => { + const leftOrder = makeOrder({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + feeRecipientAddress: makerAddressLeft, + }); + const rightOrder = makeOrder({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(2, 18), + feeRecipientAddress: makerAddressRight, + }); + await assertCalculateMatchedFillResultsAsync( + COMMON_MATCHED_FILL_RESULTS, + leftOrder, + rightOrder, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + ); + }); + + it('should transfer the correct amounts if leftMaker == leftFeeRecipient && leftMakerFeeAsset == leftTakerAsset', async () => { + const rightOrder = makeOrder({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(2, 18), + }); + const leftOrder = makeOrder({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + makerFeeAssetData: rightOrder.makerAssetData, + feeRecipientAddress: makerAddressLeft, + }); + await assertCalculateMatchedFillResultsAsync( + COMMON_MATCHED_FILL_RESULTS, + leftOrder, + rightOrder, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + ); + }); + + it('should transfer the correct amounts if rightMaker == rightFeeRecipient && rightMakerFeeAsset == rightTakerAsset', async () => { + const leftOrder = makeOrder({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + }); + const rightOrder = makeOrder({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(2, 18), + makerFeeAssetData: leftOrder.makerAssetData, + feeRecipientAddress: makerAddressRight, + }); + await assertCalculateMatchedFillResultsAsync( + COMMON_MATCHED_FILL_RESULTS, + leftOrder, + rightOrder, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + ); + }); + + it('should transfer the correct amounts if rightMaker == rightFeeRecipient && rightTakerAsset == rightMakerFeeAsset && leftMaker == leftFeeRecipient && leftTakerAsset == leftMakerFeeAsset', async () => { + const leftOrder = makeOrder({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + feeRecipientAddress: makerAddressLeft, + }); + const rightOrder = makeOrder({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(2, 18), + makerFeeAssetData: leftOrder.makerAssetData, + feeRecipientAddress: makerAddressRight, + }); + await assertCalculateMatchedFillResultsAsync( + COMMON_MATCHED_FILL_RESULTS, + leftOrder, + rightOrder, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + ); + }); + }); + + blockchainTests('calculateMatchedFillResultsWithMaximalFill', async () => { + /** + * Asserts that the results of calling `calculateMatchedFillResults()` is consistent with the results that are expected. + */ + async function assertCalculateMatchedFillResultsWithMaximalFillAsync( + expectedMatchedFillResults: MatchedFillResults, + leftOrder: Order, + rightOrder: Order, + leftOrderTakerAssetFilledAmount: BigNumber, + rightOrderTakerAssetFilledAmount: BigNumber, + from?: string, + ): Promise { + const actualMatchedFillResults = await libsContract.calculateMatchedFillResults.callAsync( + leftOrder, + rightOrder, + leftOrderTakerAssetFilledAmount, + rightOrderTakerAssetFilledAmount, + true, + { from }, + ); + expect(actualMatchedFillResults).to.be.deep.eq(expectedMatchedFillResults); + } + + const ORDER_DEFAULTS = { + ...constants.STATIC_ORDER_PARAMS, + makerAddress: randomAddress(), + takerAddress: randomAddress(), + senderAddress: randomAddress(), + makerAssetData: randomAssetData(), + takerAssetData: randomAssetData(), + makerFeeAssetData: randomAssetData(), + takerFeeAssetData: randomAssetData(), + feeRecipientAddress: randomAddress(), + expirationTimeSeconds: randomUint256(), + salt: randomUint256(), + domain: { + verifyingContractAddress: constants.NULL_ADDRESS, + chainId: 1337, // The chain id for the isolated exchange + }, + }; + + function makeOrder(details?: Partial): Order { + return _.assign({}, ORDER_DEFAULTS, details); + } + + before(async () => { + ORDER_DEFAULTS.domain.verifyingContractAddress = libsContract.address; + }); + + it('should transfer correct amounts when right order is fully filled', async () => { + const leftOrder = makeOrder({ + makerAddress: makerAddressLeft, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(17, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(98, 0), + }); + const rightOrder = makeOrder({ + makerAddress: makerAddressRight, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(75, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(13, 0), + }); + const expectedMatchedFillResults = createMatchedFillResults({ + left: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(13, 0), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(75, 0), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(new BigNumber('76.4705882352941176'), 16), // 76.47% + takerFeePaid: Web3Wrapper.toBaseUnitAmount(new BigNumber('76.5306122448979591'), 16), // 76.53% + }, + right: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(75, 0), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(13, 0), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + }, + }); + await assertCalculateMatchedFillResultsWithMaximalFillAsync( + expectedMatchedFillResults, + leftOrder, + rightOrder, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + ); + }); + + it('Should transfer correct amounts when left order is fully filled', async () => { + const leftOrder = makeOrder({ + makerAddress: makerAddressLeft, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(15, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(90, 0), + }); + const rightOrder = makeOrder({ + makerAddress: makerAddressRight, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(196, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(28, 0), + }); + const expectedMatchedFillResults = createMatchedFillResults({ + left: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(15, 0), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(90, 0), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + }, + right: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(105, 0), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(15, 0), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(new BigNumber('53.5714285714285714'), 16), // 53.57% + takerFeePaid: Web3Wrapper.toBaseUnitAmount(new BigNumber('53.5714285714285714'), 16), // 53.57% + }, + profitInLeftMakerAsset: constants.ZERO_AMOUNT, + profitInRightMakerAsset: Web3Wrapper.toBaseUnitAmount(15, 0), + }); + await assertCalculateMatchedFillResultsWithMaximalFillAsync( + expectedMatchedFillResults, + leftOrder, + rightOrder, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + ); + }); + + it('Should transfer correct amounts when left order is fully filled', async () => { + const leftOrder = makeOrder({ + makerAddress: makerAddressLeft, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(16, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(22, 0), + }); + const rightOrder = makeOrder({ + makerAddress: makerAddressRight, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(87, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(48, 0), + }); + const expectedMatchedFillResults = createMatchedFillResults({ + left: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(16, 0), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(22, 0), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + }, + right: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(29, 0), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(16, 0), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(new BigNumber('33.3333333333333333'), 16), // 33.33% + takerFeePaid: Web3Wrapper.toBaseUnitAmount(new BigNumber('33.3333333333333333'), 16), // 33.33% + }, + profitInLeftMakerAsset: constants.ZERO_AMOUNT, + profitInRightMakerAsset: Web3Wrapper.toBaseUnitAmount(7, 0), + }); + await assertCalculateMatchedFillResultsWithMaximalFillAsync( + expectedMatchedFillResults, + leftOrder, + rightOrder, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + ); + }); + + it('should fully fill both orders and pay out profit in both maker assets', async () => { + const leftOrder = makeOrder({ + makerAddress: makerAddressLeft, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(7, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(4, 0), + }); + const rightOrder = makeOrder({ + makerAddress: makerAddressRight, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(8, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(6, 0), + }); + const expectedMatchedFillResults = createMatchedFillResults({ + left: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(7, 0), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(4, 0), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + }, + right: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(8, 0), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(6, 0), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + }, + profitInLeftMakerAsset: Web3Wrapper.toBaseUnitAmount(1, 0), + profitInRightMakerAsset: Web3Wrapper.toBaseUnitAmount(4, 0), + }); + await assertCalculateMatchedFillResultsWithMaximalFillAsync( + expectedMatchedFillResults, + leftOrder, + rightOrder, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + ); + }); + + it('Should give left maker a better sell price when rounding', async () => { + const leftOrder = makeOrder({ + makerAddress: makerAddressLeft, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(12, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(97, 0), + }); + const rightOrder = makeOrder({ + makerAddress: makerAddressRight, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(89, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(1, 0), + }); + const expectedMatchedFillResults = createMatchedFillResults({ + left: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(11, 0), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(89, 0), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(new BigNumber('91.6666666666666666'), 16), // 91.6% + takerFeePaid: Web3Wrapper.toBaseUnitAmount(new BigNumber('91.7525773195876288'), 16), // 91.75% + }, + right: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(89, 0), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(1, 0), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + }, + profitInLeftMakerAsset: Web3Wrapper.toBaseUnitAmount(10, 0), + }); + await assertCalculateMatchedFillResultsWithMaximalFillAsync( + expectedMatchedFillResults, + leftOrder, + rightOrder, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + ); + }); + + it('Should give right maker and right taker a favorable fee price when rounding', async () => { + const leftOrder = makeOrder({ + makerAddress: makerAddressLeft, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(16, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(22, 0), + }); + const rightOrder = makeOrder({ + makerAddress: makerAddressRight, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(87, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(48, 0), + makerFee: Web3Wrapper.toBaseUnitAmount(10000, 0), + takerFee: Web3Wrapper.toBaseUnitAmount(10000, 0), + }); + const expectedMatchedFillResults = createMatchedFillResults({ + left: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(16, 0), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(22, 0), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + }, + right: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(29, 0), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(16, 0), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(3333, 0), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(3333, 0), + }, + profitInRightMakerAsset: Web3Wrapper.toBaseUnitAmount(7, 0), + }); + await assertCalculateMatchedFillResultsWithMaximalFillAsync( + expectedMatchedFillResults, + leftOrder, + rightOrder, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + ); + }); + + it('Should give left maker and left taker a favorable fee price when rounding', async () => { + const leftOrder = makeOrder({ + makerAddress: makerAddressLeft, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(12, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(97, 0), + makerFee: Web3Wrapper.toBaseUnitAmount(10000, 0), + takerFee: Web3Wrapper.toBaseUnitAmount(10000, 0), + }); + const rightOrder = makeOrder({ + makerAddress: makerAddressRight, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(89, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(1, 0), + }); + const expectedMatchedFillResults = createMatchedFillResults({ + left: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(11, 0), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(89, 0), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(9166, 0), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(9175, 0), + }, + right: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(89, 0), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(1, 0), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + }, + profitInLeftMakerAsset: Web3Wrapper.toBaseUnitAmount(10, 0), + }); + await assertCalculateMatchedFillResultsWithMaximalFillAsync( + expectedMatchedFillResults, + leftOrder, + rightOrder, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + ); + }); + + it('should transfer the correct amounts when consecutive calls are used to completely fill the left order', async () => { + const leftOrder = makeOrder({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(50, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(100, 18), + }); + const rightOrder = makeOrder({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(2, 18), + }); + const expectedMatchedFillResults = { + ...COMMON_MATCHED_FILL_RESULTS, + left: { + ...COMMON_MATCHED_FILL_RESULTS.left, + makerFeePaid: Web3Wrapper.toBaseUnitAmount(10, 16), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(10, 16), + }, + }; + await assertCalculateMatchedFillResultsWithMaximalFillAsync( + expectedMatchedFillResults, + leftOrder, + rightOrder, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + ); + const rightOrder2 = makeOrder({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(100, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(50, 18), + }); + const expectedMatchedFillResults2 = createMatchedFillResults({ + left: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(45, 18), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(90, 18), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(90, 16), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(90, 16), + }, + right: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(90, 18), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(45, 18), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(90, 16), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(90, 16), + }, + }); + await assertCalculateMatchedFillResultsWithMaximalFillAsync( + expectedMatchedFillResults2, + leftOrder, + rightOrder2, + Web3Wrapper.toBaseUnitAmount(10, 18), + constants.ZERO_AMOUNT, + ); + }); + + it('Should transfer correct amounts when right order fill amount deviates from amount derived by `Exchange.fillOrder`', async () => { + const leftOrder = makeOrder({ + makerAddress: makerAddressLeft, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(1000, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(1005, 0), + }); + const rightOrder = makeOrder({ + makerAddress: makerAddressRight, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(2126, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(1063, 0), + }); + const expectedMatchedFillResults = createMatchedFillResults({ + left: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(1000, 0), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(1005, 0), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + }, + right: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(2000, 0), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(1000, 0), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(new BigNumber('94.0733772342427093'), 16), // 94.07% + takerFeePaid: Web3Wrapper.toBaseUnitAmount(new BigNumber('94.0733772342427093'), 16), // 94.07% + }, + profitInRightMakerAsset: Web3Wrapper.toBaseUnitAmount(995, 0), + }); + await assertCalculateMatchedFillResultsWithMaximalFillAsync( + expectedMatchedFillResults, + leftOrder, + rightOrder, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + ); + }); + + it('should transfer the correct amounts when orders completely fill each other and taker doesnt take a profit', async () => { + const leftOrder = makeOrder({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + }); + const rightOrder = makeOrder({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + }); + const expectedMatchedFillResults = createMatchedFillResults({ + left: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + }, + right: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + }, + }); + await assertCalculateMatchedFillResultsWithMaximalFillAsync( + expectedMatchedFillResults, + leftOrder, + rightOrder, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + ); + }); + + it('should transfer the correct amounts when consecutive calls are used to completely fill the right order', async () => { + const leftOrder = makeOrder({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(2, 18), + }); + + const rightOrder = makeOrder({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(50, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(100, 18), + }); + const expectedMatchedFillResults = createMatchedFillResults({ + left: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(2, 18), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + }, + right: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(10, 16), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(10, 16), + }, + profitInRightMakerAsset: Web3Wrapper.toBaseUnitAmount(3, 18), + }); + await assertCalculateMatchedFillResultsWithMaximalFillAsync( + expectedMatchedFillResults, + leftOrder, + rightOrder, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + ); + // Create second left order + // Note: This order needs makerAssetAmount=96/takerAssetAmount=48 to fully fill the right order. + // However, we use 100/50 to ensure a partial fill as we want to go down the "right fill" + // branch in the contract twice for this test. + const leftOrder2 = makeOrder({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(100, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(50, 18), + }); + const expectedMatchedFillResults2 = createMatchedFillResults({ + left: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(90, 18), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(45, 18), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(90, 16), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(90, 16), + }, + right: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(45, 18), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(90, 18), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(90, 16), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(90, 16), + }, + }); + await assertCalculateMatchedFillResultsWithMaximalFillAsync( + expectedMatchedFillResults2, + leftOrder2, + rightOrder, + constants.ZERO_AMOUNT, + Web3Wrapper.toBaseUnitAmount(10, 18), + ); + }); + + it('should transfer the correct amounts if fee recipient is the same across both matched orders', async () => { + const feeRecipientAddress = randomAddress(); + const leftOrder = makeOrder({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + feeRecipientAddress, + }); + const rightOrder = makeOrder({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(2, 18), + feeRecipientAddress, + }); + await assertCalculateMatchedFillResultsWithMaximalFillAsync( + COMMON_MATCHED_FILL_RESULTS, + leftOrder, + rightOrder, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + ); + }); + + it('should transfer the correct amounts if taker == leftMaker', async () => { + const leftOrder = makeOrder({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + }); + const rightOrder = makeOrder({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(2, 18), + }); + await assertCalculateMatchedFillResultsWithMaximalFillAsync( + COMMON_MATCHED_FILL_RESULTS, + leftOrder, + rightOrder, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + leftOrder.makerAddress, + ); + }); + + it('should transfer the correct amounts if taker == rightMaker', async () => { + const leftOrder = makeOrder({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + }); + const rightOrder = makeOrder({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(2, 18), + }); + await assertCalculateMatchedFillResultsWithMaximalFillAsync( + COMMON_MATCHED_FILL_RESULTS, + leftOrder, + rightOrder, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + rightOrder.makerAddress, + ); + }); + + it('should transfer the correct amounts if taker == leftFeeRecipient', async () => { + const feeRecipientAddress = randomAddress(); + const leftOrder = makeOrder({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + feeRecipientAddress, + }); + const rightOrder = makeOrder({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(2, 18), + }); + await assertCalculateMatchedFillResultsWithMaximalFillAsync( + COMMON_MATCHED_FILL_RESULTS, + leftOrder, + rightOrder, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + feeRecipientAddress, + ); + }); + + it('should transfer the correct amounts if taker == rightFeeRecipient', async () => { + const feeRecipientAddress = randomAddress(); + const leftOrder = makeOrder({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + }); + const rightOrder = makeOrder({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(2, 18), + feeRecipientAddress, + }); + await assertCalculateMatchedFillResultsWithMaximalFillAsync( + COMMON_MATCHED_FILL_RESULTS, + leftOrder, + rightOrder, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + feeRecipientAddress, + ); + }); + + it('should transfer the correct amounts if leftMaker == leftFeeRecipient && rightMaker == rightFeeRecipient', async () => { + const leftOrder = makeOrder({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + feeRecipientAddress: makerAddressLeft, + }); + const rightOrder = makeOrder({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(2, 18), + feeRecipientAddress: makerAddressRight, + }); + await assertCalculateMatchedFillResultsWithMaximalFillAsync( + COMMON_MATCHED_FILL_RESULTS, + leftOrder, + rightOrder, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + ); + }); + + it('should transfer the correct amounts if leftMaker == leftFeeRecipient && leftMakerFeeAsset == leftTakerAsset', async () => { + const rightOrder = makeOrder({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(2, 18), + }); + const leftOrder = makeOrder({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + makerFeeAssetData: rightOrder.makerAssetData, + feeRecipientAddress: makerAddressLeft, + }); + await assertCalculateMatchedFillResultsWithMaximalFillAsync( + COMMON_MATCHED_FILL_RESULTS, + leftOrder, + rightOrder, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + ); + }); + + it('should transfer the correct amounts if rightMaker == rightFeeRecipient && rightMakerFeeAsset == rightTakerAsset', async () => { + const leftOrder = makeOrder({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + }); + const rightOrder = makeOrder({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(2, 18), + makerFeeAssetData: leftOrder.makerAssetData, + feeRecipientAddress: makerAddressRight, + }); + await assertCalculateMatchedFillResultsWithMaximalFillAsync( + COMMON_MATCHED_FILL_RESULTS, + leftOrder, + rightOrder, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + ); + }); + + it('should transfer the correct amounts if rightMaker == rightFeeRecipient && rightTakerAsset == rightMakerFeeAsset && leftMaker == leftFeeRecipient && leftTakerAsset == leftMakerFeeAsset', async () => { + const makerFeeAssetData = randomAssetData(); + const leftOrder = makeOrder({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + makerFeeAssetData, + feeRecipientAddress: makerAddressLeft, + }); + const rightOrder = makeOrder({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(2, 18), + makerFeeAssetData: leftOrder.makerAssetData, + feeRecipientAddress: makerAddressRight, + }); + await assertCalculateMatchedFillResultsWithMaximalFillAsync( + COMMON_MATCHED_FILL_RESULTS, + leftOrder, + rightOrder, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + ); + }); + }); }); +// tslint:disable-line:max-file-line-count diff --git a/contracts/exchange-libs/test/lib_order.ts b/contracts/exchange-libs/test/lib_order.ts index a472bf12c1..364c017305 100644 --- a/contracts/exchange-libs/test/lib_order.ts +++ b/contracts/exchange-libs/test/lib_order.ts @@ -1,72 +1,157 @@ -import { addressUtils, blockchainTests, constants, describe, expect } from '@0x/contracts-test-utils'; -import { assetDataUtils, orderHashUtils } from '@0x/order-utils'; +import { blockchainTests, constants, describe, expect, hexRandom } from '@0x/contracts-test-utils'; +import { eip712Utils, orderHashUtils } from '@0x/order-utils'; import { Order } from '@0x/types'; import { BigNumber, signTypedDataUtils } from '@0x/utils'; import * as ethUtil from 'ethereumjs-util'; +import * as _ from 'lodash'; import { artifacts, TestLibOrderContract } from '../src'; blockchainTests('LibOrder', env => { let libOrderContract: TestLibOrderContract; - let order: Order; + + const randomAddress = () => hexRandom(constants.ADDRESS_LENGTH); + const randomHash = () => hexRandom(constants.WORD_LENGTH); + const randomUint256 = () => new BigNumber(randomHash()); + const randomAssetData = () => hexRandom(36); + + const EMPTY_ORDER: Order = { + domain: { + verifyingContractAddress: constants.NULL_ADDRESS, + chainId: 0, + }, + senderAddress: constants.NULL_ADDRESS, + makerAddress: constants.NULL_ADDRESS, + takerAddress: constants.NULL_ADDRESS, + makerFee: constants.ZERO_AMOUNT, + takerFee: constants.ZERO_AMOUNT, + makerAssetAmount: constants.ZERO_AMOUNT, + takerAssetAmount: constants.ZERO_AMOUNT, + makerAssetData: constants.NULL_BYTES, + takerAssetData: constants.NULL_BYTES, + makerFeeAssetData: constants.NULL_BYTES, + takerFeeAssetData: constants.NULL_BYTES, + salt: constants.ZERO_AMOUNT, + feeRecipientAddress: constants.NULL_ADDRESS, + expirationTimeSeconds: constants.ZERO_AMOUNT, + }; + before(async () => { libOrderContract = await TestLibOrderContract.deployFrom0xArtifactAsync( artifacts.TestLibOrder, env.provider, env.txDefaults, ); - const domain = { - verifyingContractAddress: libOrderContract.address, - chainId: 1, - }; - order = { - ...constants.STATIC_ORDER_PARAMS, - makerAddress: addressUtils.generatePseudoRandomAddress(), - takerAddress: addressUtils.generatePseudoRandomAddress(), - senderAddress: addressUtils.generatePseudoRandomAddress(), - feeRecipientAddress: addressUtils.generatePseudoRandomAddress(), - makerAssetData: assetDataUtils.encodeERC20AssetData(addressUtils.generatePseudoRandomAddress()), - takerAssetData: assetDataUtils.encodeERC20AssetData(addressUtils.generatePseudoRandomAddress()), - makerFeeAssetData: assetDataUtils.encodeERC20AssetData(addressUtils.generatePseudoRandomAddress()), - takerFeeAssetData: assetDataUtils.encodeERC20AssetData(addressUtils.generatePseudoRandomAddress()), - salt: new BigNumber(0), - expirationTimeSeconds: new BigNumber(0), - domain, - }; }); - describe('LibOrder', () => { - describe('getOrderHash', () => { - it('should return the correct orderHash', async () => { - const domainHash = ethUtil.bufferToHex( - signTypedDataUtils.generateDomainHash({ - ...order.domain, - name: constants.EIP712_DOMAIN_NAME, - version: constants.EIP712_DOMAIN_VERSION, - }), - ); - const orderHashHex = await libOrderContract.getTypedDataHash.callAsync(order, domainHash); - expect(orderHashUtils.getOrderHashHex(order)).to.be.equal(orderHashHex); + /** + * Tests the `getTypedDataHash()` function against a reference hash. + */ + async function testGetTypedDataHashAsync(order: Order): Promise { + const expectedHash = orderHashUtils.getOrderHashHex(order); + const domainHash = ethUtil.bufferToHex( + signTypedDataUtils.generateDomainHash({ + ...order.domain, + name: constants.EIP712_DOMAIN_NAME, + version: constants.EIP712_DOMAIN_VERSION, + }), + ); + const actualHash = await libOrderContract.getTypedDataHash.callAsync(order, domainHash); + expect(actualHash).to.be.eq(expectedHash); + } + + describe('getTypedDataHash', () => { + it('should correctly hash an empty order', async () => { + await testGetTypedDataHashAsync({ + ...EMPTY_ORDER, + domain: { + ...EMPTY_ORDER.domain, + verifyingContractAddress: libOrderContract.address, + }, }); - it('orderHash should differ if the domain hash is different', async () => { - const domainHash1 = ethUtil.bufferToHex( - signTypedDataUtils.generateDomainHash({ - ...order.domain, - name: constants.EIP712_DOMAIN_NAME, - version: constants.EIP712_DOMAIN_VERSION, - }), - ); - const domainHash2 = ethUtil.bufferToHex( - signTypedDataUtils.generateDomainHash({ - ...order.domain, - name: constants.EIP712_DOMAIN_NAME, - version: constants.EIP712_DOMAIN_VERSION, - chainId: 1337, - }), - ); - const orderHashHex1 = await libOrderContract.getTypedDataHash.callAsync(order, domainHash1); - const orderHashHex2 = await libOrderContract.getTypedDataHash.callAsync(order, domainHash2); - expect(orderHashHex1).to.be.not.equal(orderHashHex2); + }); + + it('should correctly hash a non-empty order', async () => { + await testGetTypedDataHashAsync({ + domain: { + verifyingContractAddress: libOrderContract.address, + chainId: 1337, + }, + senderAddress: randomAddress(), + makerAddress: randomAddress(), + takerAddress: randomAddress(), + makerFee: randomUint256(), + takerFee: randomUint256(), + makerAssetAmount: randomUint256(), + takerAssetAmount: randomUint256(), + makerAssetData: randomAssetData(), + takerAssetData: randomAssetData(), + makerFeeAssetData: randomAssetData(), + takerFeeAssetData: randomAssetData(), + salt: randomUint256(), + feeRecipientAddress: randomAddress(), + expirationTimeSeconds: randomUint256(), + }); + }); + + it('orderHash should differ if the domain hash is different', async () => { + const domainHash1 = ethUtil.bufferToHex( + signTypedDataUtils.generateDomainHash({ + ...EMPTY_ORDER.domain, + name: constants.EIP712_DOMAIN_NAME, + version: constants.EIP712_DOMAIN_VERSION, + }), + ); + const domainHash2 = ethUtil.bufferToHex( + signTypedDataUtils.generateDomainHash({ + ...EMPTY_ORDER.domain, + name: constants.EIP712_DOMAIN_NAME, + version: constants.EIP712_DOMAIN_VERSION, + chainId: 1337, + }), + ); + const orderHashHex1 = await libOrderContract.getTypedDataHash.callAsync(EMPTY_ORDER, domainHash1); + const orderHashHex2 = await libOrderContract.getTypedDataHash.callAsync(EMPTY_ORDER, domainHash2); + expect(orderHashHex1).to.be.not.equal(orderHashHex2); + }); + }); + + /** + * Tests the `getStructHash()` function against a reference hash. + */ + async function testGetStructHashAsync(order: Order): Promise { + const typedData = eip712Utils.createOrderTypedData(order); + const expectedHash = ethUtil.bufferToHex(signTypedDataUtils.generateTypedDataHashWithoutDomain(typedData)); + const actualHash = await libOrderContract.getStructHash.callAsync(order); + expect(actualHash).to.be.eq(expectedHash); + } + + describe('getStructHash', () => { + it('should correctly hash an empty order', async () => { + await testGetStructHashAsync(EMPTY_ORDER); + }); + + it('should correctly hash a non-empty order', async () => { + await testGetStructHashAsync({ + // The domain is not used in this test, so it's okay if it is left empty. + domain: { + verifyingContractAddress: constants.NULL_ADDRESS, + chainId: 0, + }, + senderAddress: randomAddress(), + makerAddress: randomAddress(), + takerAddress: randomAddress(), + makerFee: randomUint256(), + takerFee: randomUint256(), + makerAssetAmount: randomUint256(), + takerAssetAmount: randomUint256(), + makerAssetData: randomAssetData(), + takerAssetData: randomAssetData(), + makerFeeAssetData: randomAssetData(), + takerFeeAssetData: randomAssetData(), + salt: randomUint256(), + feeRecipientAddress: randomAddress(), + expirationTimeSeconds: randomUint256(), }); }); }); diff --git a/contracts/exchange-libs/test/lib_zero_ex_transaction.ts b/contracts/exchange-libs/test/lib_zero_ex_transaction.ts index e5904740dc..f2b02d315d 100644 --- a/contracts/exchange-libs/test/lib_zero_ex_transaction.ts +++ b/contracts/exchange-libs/test/lib_zero_ex_transaction.ts @@ -1,74 +1,132 @@ -import { addressUtils, blockchainTests, constants, describe, expect } from '@0x/contracts-test-utils'; -import { transactionHashUtils } from '@0x/order-utils'; +import { blockchainTests, constants, describe, expect, hexRandom } from '@0x/contracts-test-utils'; +import { eip712Utils, transactionHashUtils } from '@0x/order-utils'; import { ZeroExTransaction } from '@0x/types'; import { BigNumber, signTypedDataUtils } from '@0x/utils'; import * as ethUtil from 'ethereumjs-util'; +import * as _ from 'lodash'; import { artifacts, TestLibZeroExTransactionContract } from '../src'; blockchainTests('LibZeroExTransaction', env => { let libZeroExTransactionContract: TestLibZeroExTransactionContract; - let zeroExTransaction: ZeroExTransaction; + + const randomAddress = () => hexRandom(constants.ADDRESS_LENGTH); + const randomHash = () => hexRandom(constants.WORD_LENGTH); + const randomUint256 = () => new BigNumber(randomHash()); + const randomAssetData = () => hexRandom(36); + + const EMPTY_TRANSACTION: ZeroExTransaction = { + salt: constants.ZERO_AMOUNT, + expirationTimeSeconds: constants.ZERO_AMOUNT, + signerAddress: constants.NULL_ADDRESS, + data: constants.NULL_BYTES, + domain: { + verifyingContractAddress: constants.NULL_ADDRESS, + chainId: 0, + }, + }; + before(async () => { libZeroExTransactionContract = await TestLibZeroExTransactionContract.deployFrom0xArtifactAsync( artifacts.TestLibZeroExTransaction, env.provider, env.txDefaults, ); - const domain = { - verifyingContractAddress: libZeroExTransactionContract.address, - chainId: 1, - }; - zeroExTransaction = { - signerAddress: addressUtils.generatePseudoRandomAddress(), - salt: new BigNumber(0), - expirationTimeSeconds: new BigNumber(0), - data: constants.NULL_BYTES, - domain, - }; }); - describe('LibZeroExTransaction', () => { - describe('getTransactionHash', () => { - it('should return the correct transactionHash', async () => { - const domainHash = ethUtil.bufferToHex( - signTypedDataUtils.generateDomainHash({ - ...zeroExTransaction.domain, - name: constants.EIP712_DOMAIN_NAME, - version: constants.EIP712_DOMAIN_VERSION, - }), - ); - const orderHashHex = await libZeroExTransactionContract.getTypedDataHash.callAsync( - zeroExTransaction, - domainHash, - ); - expect(transactionHashUtils.getTransactionHashHex(zeroExTransaction)).to.be.equal(orderHashHex); + /** + * Tests the `getTypedDataHash()` function against a reference hash. + */ + async function testGetTypedDataHashAsync(transaction: ZeroExTransaction): Promise { + const expectedHash = transactionHashUtils.getTransactionHashHex(transaction); + const domainHash = ethUtil.bufferToHex( + signTypedDataUtils.generateDomainHash({ + ...transaction.domain, + name: constants.EIP712_DOMAIN_NAME, + version: constants.EIP712_DOMAIN_VERSION, + }), + ); + const actualHash = await libZeroExTransactionContract.getTypedDataHash.callAsync(transaction, domainHash); + expect(actualHash).to.be.eq(expectedHash); + } + + describe('getTypedDataHash', () => { + it('should correctly hash an empty transaction', async () => { + await testGetTypedDataHashAsync({ + ...EMPTY_TRANSACTION, + domain: { + ...EMPTY_TRANSACTION.domain, + verifyingContractAddress: libZeroExTransactionContract.address, + }, }); - it('transactionHash should differ if the domain hash is different', async () => { - const domainHash1 = ethUtil.bufferToHex( - signTypedDataUtils.generateDomainHash({ - ...zeroExTransaction.domain, - name: constants.EIP712_DOMAIN_NAME, - version: constants.EIP712_DOMAIN_VERSION, - }), - ); - const domainHash2 = ethUtil.bufferToHex( - signTypedDataUtils.generateDomainHash({ - ...zeroExTransaction.domain, - name: constants.EIP712_DOMAIN_NAME, - version: constants.EIP712_DOMAIN_VERSION, - chainId: 1337, - }), - ); - const transactionHashHex1 = await libZeroExTransactionContract.getTypedDataHash.callAsync( - zeroExTransaction, - domainHash1, - ); - const transactionHashHex2 = await libZeroExTransactionContract.getTypedDataHash.callAsync( - zeroExTransaction, - domainHash2, - ); - expect(transactionHashHex1).to.be.not.equal(transactionHashHex2); + }); + + it('should correctly hash a non-empty transaction', async () => { + await testGetTypedDataHashAsync({ + salt: randomUint256(), + expirationTimeSeconds: randomUint256(), + signerAddress: randomAddress(), + data: randomAssetData(), + domain: { + ...EMPTY_TRANSACTION.domain, + verifyingContractAddress: libZeroExTransactionContract.address, + }, + }); + }); + it('transactionHash should differ if the domain hash is different', async () => { + const domainHash1 = ethUtil.bufferToHex( + signTypedDataUtils.generateDomainHash({ + ...EMPTY_TRANSACTION.domain, + name: constants.EIP712_DOMAIN_NAME, + version: constants.EIP712_DOMAIN_VERSION, + }), + ); + const domainHash2 = ethUtil.bufferToHex( + signTypedDataUtils.generateDomainHash({ + ...EMPTY_TRANSACTION.domain, + name: constants.EIP712_DOMAIN_NAME, + version: constants.EIP712_DOMAIN_VERSION, + chainId: 1337, + }), + ); + const transactionHashHex1 = await libZeroExTransactionContract.getTypedDataHash.callAsync( + EMPTY_TRANSACTION, + domainHash1, + ); + const transactionHashHex2 = await libZeroExTransactionContract.getTypedDataHash.callAsync( + EMPTY_TRANSACTION, + domainHash2, + ); + expect(transactionHashHex1).to.be.not.equal(transactionHashHex2); + }); + }); + + /** + * Tests the `getStructHash()` function against a reference hash. + */ + async function testGetStructHashAsync(transaction: ZeroExTransaction): Promise { + const typedData = eip712Utils.createZeroExTransactionTypedData(transaction); + const expectedHash = ethUtil.bufferToHex(signTypedDataUtils.generateTypedDataHashWithoutDomain(typedData)); + const actualHash = await libZeroExTransactionContract.getStructHash.callAsync(transaction); + expect(actualHash).to.be.eq(expectedHash); + } + + describe('getStructHash', () => { + it('should correctly hash an empty transaction', async () => { + await testGetStructHashAsync(EMPTY_TRANSACTION); + }); + + it('should correctly hash a non-empty transaction', async () => { + await testGetStructHashAsync({ + salt: randomUint256(), + expirationTimeSeconds: randomUint256(), + signerAddress: randomAddress(), + data: randomAssetData(), + // The domain is not used in this test, so it's okay if it is left empty. + domain: { + verifyingContractAddress: constants.NULL_ADDRESS, + chainId: 0, + }, }); }); }); diff --git a/contracts/exchange/contracts/src/MixinMatchOrders.sol b/contracts/exchange/contracts/src/MixinMatchOrders.sol index 2ed8125a13..9ec79700ac 100644 --- a/contracts/exchange/contracts/src/MixinMatchOrders.sol +++ b/contracts/exchange/contracts/src/MixinMatchOrders.sol @@ -424,7 +424,7 @@ contract MixinMatchOrders is address takerAddress, LibFillResults.MatchedFillResults memory matchedFillResults ) - private + internal { address leftFeeRecipientAddress = leftOrder.feeRecipientAddress; address rightFeeRecipientAddress = rightOrder.feeRecipientAddress; diff --git a/contracts/exchange/contracts/test/TestExchangeInternals.sol b/contracts/exchange/contracts/test/TestExchangeInternals.sol index 487b92b74c..48a657227a 100644 --- a/contracts/exchange/contracts/test/TestExchangeInternals.sol +++ b/contracts/exchange/contracts/test/TestExchangeInternals.sol @@ -41,6 +41,21 @@ contract TestExchangeInternals is Exchange(chainId) {} + function assertValidMatch( + LibOrder.Order memory leftOrder, + LibOrder.Order memory rightOrder + ) + public + view + { + _assertValidMatch( + leftOrder, + rightOrder, + getOrderInfo(leftOrder), + getOrderInfo(rightOrder) + ); + } + /// @dev Call `_updateFilledState()` but first set `filled[order]` to /// `orderTakerAssetFilledAmount`. function testUpdateFilledState( @@ -73,6 +88,26 @@ contract TestExchangeInternals is _settleOrder(orderHash, order, takerAddress, fillResults); } + function settleMatchOrders( + bytes32 leftOrderHash, + bytes32 rightOrderHash, + LibOrder.Order memory leftOrder, + LibOrder.Order memory rightOrder, + address takerAddress, + LibFillResults.MatchedFillResults memory matchedFillResults + ) + public + { + _settleMatchedOrders( + leftOrderHash, + rightOrderHash, + leftOrder, + rightOrder, + takerAddress, + matchedFillResults + ); + } + /// @dev Overidden to only log arguments so we can test `_settleOrder()`. function _dispatchTransferFrom( bytes32 orderHash, diff --git a/contracts/exchange/test/dispatcher.ts b/contracts/exchange/test/dispatcher.ts index c547d44f17..71e8ab182e 100644 --- a/contracts/exchange/test/dispatcher.ts +++ b/contracts/exchange/test/dispatcher.ts @@ -271,7 +271,7 @@ describe('AssetProxyDispatcher', () => { return expect(tx).to.revertWith(expectedError); }); - it('should should revert with the correct error when assetData length < 4 bytes', async () => { + it('should revert with the correct error when assetData length < 4 bytes', async () => { await assetProxyDispatcher.registerAssetProxy.awaitTransactionSuccessAsync(erc20Proxy.address, { from: owner, }); diff --git a/contracts/exchange/test/internal.ts b/contracts/exchange/test/internal.ts index d695a09bb6..e7fc187446 100644 --- a/contracts/exchange/test/internal.ts +++ b/contracts/exchange/test/internal.ts @@ -1,12 +1,14 @@ +import { ReferenceFunctions as LibReferenceFunctions } from '@0x/contracts-exchange-libs'; import { blockchainTests, constants, expect, hexRandom, LogDecoder } from '@0x/contracts-test-utils'; -import { OrderWithoutDomain as Order } from '@0x/types'; +import { ExchangeRevertErrors, orderHashUtils } from '@0x/order-utils'; +import { Order, OrderWithoutDomain } from '@0x/types'; import { BigNumber, SafeMathRevertErrors } from '@0x/utils'; +import { Web3Wrapper } from '@0x/web3-wrapper'; import { LogWithDecodedArgs } from 'ethereum-types'; import * as _ from 'lodash'; import { artifacts, - ReferenceFunctions, TestExchangeInternalsContract, TestExchangeInternalsDispatchTransferFromCalledEventArgs, TestExchangeInternalsFillEventArgs, @@ -23,7 +25,9 @@ blockchainTests('Exchange core internal functions', env => { let senderAddress: string; before(async () => { - [senderAddress] = await env.getAccountAddressesAsync(); + const accounts = await env.getAccountAddressesAsync(); + senderAddress = accounts[0]; + testExchange = await TestExchangeInternalsContract.deployFrom0xArtifactAsync( artifacts.TestExchangeInternals, env.provider, @@ -33,6 +37,110 @@ blockchainTests('Exchange core internal functions', env => { logDecoder = new LogDecoder(env.web3Wrapper, artifacts); }); + blockchainTests('assertValidMatch', () => { + const ORDER_DEFAULTS = { + senderAddress: randomAddress(), + makerAddress: randomAddress(), + takerAddress: randomAddress(), + makerFee: ONE_ETHER.times(0.001), + takerFee: ONE_ETHER.times(0.003), + makerAssetAmount: ONE_ETHER, + takerAssetAmount: ONE_ETHER.times(0.5), + makerAssetData: randomAssetData(), + takerAssetData: randomAssetData(), + makerFeeAssetData: randomAssetData(), + takerFeeAssetData: randomAssetData(), + salt: new BigNumber(_.random(0, 1e8)), + feeRecipientAddress: randomAddress(), + expirationTimeSeconds: new BigNumber(_.random(0, 1e8)), + domain: { + verifyingContractAddress: constants.NULL_ADDRESS, + chainId: 1337, // The chain id for the isolated exchange + }, + }; + + function makeOrder(details?: Partial): Order { + return _.assign({}, ORDER_DEFAULTS, details); + } + + before(async () => { + ORDER_DEFAULTS.domain.verifyingContractAddress = testExchange.address; + }); + + it('should revert if the maker asset multiplication should overflow', async () => { + const leftOrder = makeOrder({ + makerAssetAmount: constants.MAX_UINT256, + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(100, 18), + }); + const rightOrder = makeOrder({ + makerAssetAmount: constants.MAX_UINT256_ROOT, + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(50, 18), + }); + const expectedError = new SafeMathRevertErrors.SafeMathError( + SafeMathRevertErrors.SafeMathErrorCodes.Uint256MultiplicationOverflow, + leftOrder.makerAssetAmount, + rightOrder.makerAssetAmount, + ); + return expect(testExchange.assertValidMatch.callAsync(leftOrder, rightOrder)).to.revertWith(expectedError); + }); + + it('should revert if the taker asset multiplication should overflow', async () => { + const leftOrder = makeOrder({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(100, 18), + takerAssetAmount: constants.MAX_UINT256, + }); + const rightOrder = makeOrder({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(50, 18), + takerAssetAmount: constants.MAX_UINT256_ROOT, + }); + const expectedError = new SafeMathRevertErrors.SafeMathError( + SafeMathRevertErrors.SafeMathErrorCodes.Uint256MultiplicationOverflow, + leftOrder.takerAssetAmount, + rightOrder.takerAssetAmount, + ); + return expect(testExchange.assertValidMatch.callAsync(leftOrder, rightOrder)).to.revertWith(expectedError); + }); + + it('should revert if the prices of the left order is less than the price of the right order', async () => { + const leftOrder = makeOrder({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(49, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(100, 18), + }); + const rightOrder = makeOrder({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(100, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(50, 18), + }); + const orderHashHexLeft = orderHashUtils.getOrderHashHex(leftOrder); + const orderHashHexRight = orderHashUtils.getOrderHashHex(rightOrder); + const expectedError = new ExchangeRevertErrors.NegativeSpreadError(orderHashHexLeft, orderHashHexRight); + return expect(testExchange.assertValidMatch.callAsync(leftOrder, rightOrder)).to.revertWith(expectedError); + }); + + it('should succeed if the prices of the left and right orders are equal', async () => { + const leftOrder = makeOrder({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(50, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(100, 18), + }); + const rightOrder = makeOrder({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(100, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(50, 18), + }); + return expect(testExchange.assertValidMatch.callAsync(leftOrder, rightOrder)).to.be.fulfilled(''); + }); + + it('should succeed if the price of the left order is higher than the price of the right', async () => { + const leftOrder = makeOrder({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(50, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(100, 18), + }); + const rightOrder = makeOrder({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(100, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(50, 18), + }); + return expect(testExchange.assertValidMatch.callAsync(leftOrder, rightOrder)).to.be.fulfilled(''); + }); + }); + blockchainTests.resets('updateFilledState', async () => { const ORDER_DEFAULTS = { senderAddress: randomAddress(), @@ -51,18 +159,18 @@ blockchainTests('Exchange core internal functions', env => { expirationTimeSeconds: new BigNumber(_.random(0, 1e8)), }; - function makeOrder(details?: Partial): Order { + function makeOrder(details?: Partial): OrderWithoutDomain { return _.assign({}, ORDER_DEFAULTS, details); } async function testUpdateFilledStateAsync( - order: Order, + order: OrderWithoutDomain, orderTakerAssetFilledAmount: BigNumber, takerAddress: string, takerAssetFillAmount: BigNumber, ): Promise { const orderHash = randomHash(); - const fillResults = ReferenceFunctions.calculateFillResults(order, takerAssetFillAmount); + const fillResults = LibReferenceFunctions.calculateFillResults(order, takerAssetFillAmount); const expectedFilledState = orderTakerAssetFilledAmount.plus(takerAssetFillAmount); // CAll `testUpdateFilledState()`, which will set the `filled` // state for this order to `orderTakerAssetFilledAmount` before @@ -198,5 +306,305 @@ blockchainTests('Exchange core internal functions', env => { expect(logs[3].args.amount).to.bignumber.eq(fillResults.makerFeePaid); }); }); + + blockchainTests('settleMatchOrders', () => { + const getOrder = () => { + return { + senderAddress: randomAddress(), + makerAddress: randomAddress(), + takerAddress: randomAddress(), + makerFee: ONE_ETHER.times(0.001), + takerFee: ONE_ETHER.times(0.003), + makerAssetAmount: ONE_ETHER, + takerAssetAmount: ONE_ETHER.times(0.5), + makerAssetData: randomAssetData(), + takerAssetData: randomAssetData(), + makerFeeAssetData: randomAssetData(), + takerFeeAssetData: randomAssetData(), + salt: new BigNumber(_.random(0, 1e8)), + feeRecipientAddress: randomAddress(), + expirationTimeSeconds: new BigNumber(_.random(0, 1e8)), + }; + }; + + it('should revert if the taker fee paid fields addition overflow and left.feeRecipient == right.feeRecipient && left.takerFeeAssetData == right.takerFeeAssetData', async () => { + // Get the arguments for the call to `settleMatchOrders()`. + const leftOrder = getOrder(); + const rightOrder = getOrder(); + const leftOrderHash = randomHash(); + const rightOrderHash = randomHash(); + const takerAddress = randomAddress(); + const matchedFillResults = { + left: { + makerAssetFilledAmount: ONE_ETHER.times(2), + takerAssetFilledAmount: ONE_ETHER.times(10), + makerFeePaid: ONE_ETHER.times(0.01), + takerFeePaid: constants.MAX_UINT256, + }, + right: { + takerAssetFilledAmount: ONE_ETHER.times(20), + makerAssetFilledAmount: ONE_ETHER.times(4), + makerFeePaid: ONE_ETHER.times(0.02), + takerFeePaid: constants.MAX_UINT256_ROOT, + }, + profitInLeftMakerAsset: ONE_ETHER, + profitInRightMakerAsset: ONE_ETHER.times(2), + }; + + // Set the fee recipient addresses and the taker fee asset data fields to be the same + rightOrder.feeRecipientAddress = leftOrder.feeRecipientAddress; + rightOrder.takerFeeAssetData = leftOrder.takerFeeAssetData; + + // The expected error that should be thrown by the function. + const expectedError = new SafeMathRevertErrors.SafeMathError( + SafeMathRevertErrors.SafeMathErrorCodes.Uint256AdditionOverflow, + matchedFillResults.left.takerFeePaid, + matchedFillResults.right.takerFeePaid, + ); + + // Ensure that the call to `settleMatchOrders()` fails with the expected error. + const tx = testExchange.settleMatchOrders.sendTransactionAsync( + leftOrderHash, + rightOrderHash, + leftOrder, + rightOrder, + takerAddress, + matchedFillResults, + ); + return expect(tx).to.revertWith(expectedError); + }); + + it('should succeed if the taker fee paid fields addition overflow and left.feeRecipient != right.feeRecipient || left.takerFeeAssetData != right.takerFeeAssetData', async () => { + // Get the arguments for the call to `settleMatchOrders()`. + const leftOrder = getOrder(); + const rightOrder = getOrder(); + const leftOrderHash = randomHash(); + const rightOrderHash = randomHash(); + const takerAddress = randomAddress(); + const matchedFillResults = { + left: { + makerAssetFilledAmount: ONE_ETHER.times(2), + takerAssetFilledAmount: ONE_ETHER.times(10), + makerFeePaid: ONE_ETHER.times(0.01), + takerFeePaid: constants.MAX_UINT256, + }, + right: { + takerAssetFilledAmount: ONE_ETHER.times(20), + makerAssetFilledAmount: ONE_ETHER.times(4), + makerFeePaid: ONE_ETHER.times(0.02), + takerFeePaid: constants.MAX_UINT256_ROOT, + }, + profitInLeftMakerAsset: ONE_ETHER, + profitInRightMakerAsset: ONE_ETHER.times(2), + }; + + // The call to `settleMatchOrders()` should be successful. + return expect( + testExchange.settleMatchOrders.sendTransactionAsync( + leftOrderHash, + rightOrderHash, + leftOrder, + rightOrder, + takerAddress, + matchedFillResults, + ), + ).to.be.fulfilled(''); + }); + + it('calls `_dispatchTransferFrom()` to collect fees from the left order when left.feeRecipient == right.feeRecipient && left.takerFeeAssetData == right.takerFeeAssetData', async () => { + const leftOrder = getOrder(); + const rightOrder = getOrder(); + const leftOrderHash = randomHash(); + const rightOrderHash = randomHash(); + const takerAddress = randomAddress(); + const matchedFillResults = { + left: { + makerAssetFilledAmount: ONE_ETHER.times(2), + takerAssetFilledAmount: ONE_ETHER.times(10), + makerFeePaid: ONE_ETHER.times(0.01), + takerFeePaid: ONE_ETHER.times(0.025), + }, + right: { + takerAssetFilledAmount: ONE_ETHER.times(20), + makerAssetFilledAmount: ONE_ETHER.times(4), + makerFeePaid: ONE_ETHER.times(0.02), + takerFeePaid: ONE_ETHER.times(0.05), + }, + profitInLeftMakerAsset: ONE_ETHER, + profitInRightMakerAsset: ONE_ETHER.times(2), + }; + + // Set the fee recipient addresses and the taker fee asset data fields to be the same + rightOrder.feeRecipientAddress = leftOrder.feeRecipientAddress; + rightOrder.takerFeeAssetData = leftOrder.takerFeeAssetData; + + // Call settleMatchOrders and collect the logs + const receipt = await logDecoder.getTxWithDecodedLogsAsync( + await testExchange.settleMatchOrders.sendTransactionAsync( + leftOrderHash, + rightOrderHash, + leftOrder, + rightOrder, + takerAddress, + matchedFillResults, + ), + ); + const logs = receipt.logs as Array< + LogWithDecodedArgs + >; + + // Ensure that the logs have the correct lengths and names + expect(logs.length).to.be.eq(7); + expect(_.every(logs, log => log.event === 'DispatchTransferFromCalled')).to.be.true(); + + // Right maker asset -> left maker + expect(logs[0].args.orderHash).to.be.eq(rightOrderHash); + expect(logs[0].args.assetData).to.be.eq(rightOrder.makerAssetData); + expect(logs[0].args.from).to.be.eq(rightOrder.makerAddress); + expect(logs[0].args.to).to.be.eq(leftOrder.makerAddress); + expect(logs[0].args.amount).bignumber.to.be.eq(matchedFillResults.left.takerAssetFilledAmount); + + // Left maker asset -> right maker + expect(logs[1].args.orderHash).to.be.eq(leftOrderHash); + expect(logs[1].args.assetData).to.be.eq(leftOrder.makerAssetData); + expect(logs[1].args.from).to.be.eq(leftOrder.makerAddress); + expect(logs[1].args.to).to.be.eq(rightOrder.makerAddress); + expect(logs[1].args.amount).bignumber.to.be.eq(matchedFillResults.right.takerAssetFilledAmount); + + // Right maker fee -> right fee recipient + expect(logs[2].args.orderHash).to.be.eq(rightOrderHash); + expect(logs[2].args.assetData).to.be.eq(rightOrder.makerFeeAssetData); + expect(logs[2].args.from).to.be.eq(rightOrder.makerAddress); + expect(logs[2].args.to).to.be.eq(rightOrder.feeRecipientAddress); + expect(logs[2].args.amount).bignumber.to.be.eq(matchedFillResults.right.makerFeePaid); + + // Left maker fee -> left fee recipient + expect(logs[3].args.orderHash).to.be.eq(leftOrderHash); + expect(logs[3].args.assetData).to.be.eq(leftOrder.makerFeeAssetData); + expect(logs[3].args.from).to.be.eq(leftOrder.makerAddress); + expect(logs[3].args.to).to.be.eq(leftOrder.feeRecipientAddress); + expect(logs[3].args.amount).bignumber.to.be.eq(matchedFillResults.left.makerFeePaid); + + // Left maker -> taker profit + expect(logs[4].args.orderHash).to.be.eq(leftOrderHash); + expect(logs[4].args.assetData).to.be.eq(leftOrder.makerAssetData); + expect(logs[4].args.from).to.be.eq(leftOrder.makerAddress); + expect(logs[4].args.to).to.be.eq(takerAddress); + expect(logs[4].args.amount).bignumber.to.be.eq(matchedFillResults.profitInLeftMakerAsset); + + // right maker -> taker profit + expect(logs[5].args.orderHash).to.be.eq(rightOrderHash); + expect(logs[5].args.assetData).to.be.eq(rightOrder.makerAssetData); + expect(logs[5].args.from).to.be.eq(rightOrder.makerAddress); + expect(logs[5].args.to).to.be.eq(takerAddress); + expect(logs[5].args.amount).bignumber.to.be.eq(matchedFillResults.profitInRightMakerAsset); + + // taker fees -> fee recipient + expect(logs[6].args.orderHash).to.be.eq(leftOrderHash); + expect(logs[6].args.assetData).to.be.eq(leftOrder.takerFeeAssetData); + expect(logs[6].args.from).to.be.eq(takerAddress); + expect(logs[6].args.to).to.be.eq(leftOrder.feeRecipientAddress); + expect(logs[6].args.amount).bignumber.to.be.eq(ONE_ETHER.times(0.075)); + }); + + it('calls `_dispatchTransferFrom()` from in the right order when the fee recipients and taker fee asset data are not the same', async () => { + const leftOrder = getOrder(); + const rightOrder = getOrder(); + const leftOrderHash = randomHash(); + const rightOrderHash = randomHash(); + const takerAddress = randomAddress(); + const matchedFillResults = { + left: { + makerAssetFilledAmount: ONE_ETHER.times(2), + takerAssetFilledAmount: ONE_ETHER.times(10), + makerFeePaid: ONE_ETHER.times(0.01), + takerFeePaid: ONE_ETHER.times(0.025), + }, + right: { + takerAssetFilledAmount: ONE_ETHER.times(20), + makerAssetFilledAmount: ONE_ETHER.times(4), + makerFeePaid: ONE_ETHER.times(0.02), + takerFeePaid: ONE_ETHER.times(0.05), + }, + profitInLeftMakerAsset: ONE_ETHER, + profitInRightMakerAsset: ONE_ETHER.times(2), + }; + + // Call settleMatchOrders and collect the logs + const receipt = await logDecoder.getTxWithDecodedLogsAsync( + await testExchange.settleMatchOrders.sendTransactionAsync( + leftOrderHash, + rightOrderHash, + leftOrder, + rightOrder, + takerAddress, + matchedFillResults, + ), + ); + const logs = receipt.logs as Array< + LogWithDecodedArgs + >; + + // Ensure that the logs have the correct lengths and names + expect(logs.length).to.be.eq(8); + expect(_.every(logs, log => log.event === 'DispatchTransferFromCalled')).to.be.true(); + + // Right maker asset -> left maker + expect(logs[0].args.orderHash).to.be.eq(rightOrderHash); + expect(logs[0].args.assetData).to.be.eq(rightOrder.makerAssetData); + expect(logs[0].args.from).to.be.eq(rightOrder.makerAddress); + expect(logs[0].args.to).to.be.eq(leftOrder.makerAddress); + expect(logs[0].args.amount).bignumber.to.be.eq(matchedFillResults.left.takerAssetFilledAmount); + + // Left maker asset -> right maker + expect(logs[1].args.orderHash).to.be.eq(leftOrderHash); + expect(logs[1].args.assetData).to.be.eq(leftOrder.makerAssetData); + expect(logs[1].args.from).to.be.eq(leftOrder.makerAddress); + expect(logs[1].args.to).to.be.eq(rightOrder.makerAddress); + expect(logs[1].args.amount).bignumber.to.be.eq(matchedFillResults.right.takerAssetFilledAmount); + + // Right maker fee -> right fee recipient + expect(logs[2].args.orderHash).to.be.eq(rightOrderHash); + expect(logs[2].args.assetData).to.be.eq(rightOrder.makerFeeAssetData); + expect(logs[2].args.from).to.be.eq(rightOrder.makerAddress); + expect(logs[2].args.to).to.be.eq(rightOrder.feeRecipientAddress); + expect(logs[2].args.amount).bignumber.to.be.eq(matchedFillResults.right.makerFeePaid); + + // Left maker fee -> left fee recipient + expect(logs[3].args.orderHash).to.be.eq(leftOrderHash); + expect(logs[3].args.assetData).to.be.eq(leftOrder.makerFeeAssetData); + expect(logs[3].args.from).to.be.eq(leftOrder.makerAddress); + expect(logs[3].args.to).to.be.eq(leftOrder.feeRecipientAddress); + expect(logs[3].args.amount).bignumber.to.be.eq(matchedFillResults.left.makerFeePaid); + + // Left maker -> taker profit + expect(logs[4].args.orderHash).to.be.eq(leftOrderHash); + expect(logs[4].args.assetData).to.be.eq(leftOrder.makerAssetData); + expect(logs[4].args.from).to.be.eq(leftOrder.makerAddress); + expect(logs[4].args.to).to.be.eq(takerAddress); + expect(logs[4].args.amount).bignumber.to.be.eq(matchedFillResults.profitInLeftMakerAsset); + + // right maker -> taker profit + expect(logs[5].args.orderHash).to.be.eq(rightOrderHash); + expect(logs[5].args.assetData).to.be.eq(rightOrder.makerAssetData); + expect(logs[5].args.from).to.be.eq(rightOrder.makerAddress); + expect(logs[5].args.to).to.be.eq(takerAddress); + expect(logs[5].args.amount).bignumber.to.be.eq(matchedFillResults.profitInRightMakerAsset); + + // Right taker fee -> right fee recipient + expect(logs[6].args.orderHash).to.be.eq(rightOrderHash); + expect(logs[6].args.assetData).to.be.eq(rightOrder.takerFeeAssetData); + expect(logs[6].args.from).to.be.eq(takerAddress); + expect(logs[6].args.to).to.be.eq(rightOrder.feeRecipientAddress); + expect(logs[6].args.amount).bignumber.to.be.eq(matchedFillResults.right.takerFeePaid); + + // Right taker fee -> right fee recipient + expect(logs[7].args.orderHash).to.be.eq(leftOrderHash); + expect(logs[7].args.assetData).to.be.eq(leftOrder.takerFeeAssetData); + expect(logs[7].args.from).to.be.eq(takerAddress); + expect(logs[7].args.to).to.be.eq(leftOrder.feeRecipientAddress); + expect(logs[7].args.amount).bignumber.to.be.eq(matchedFillResults.left.takerFeePaid); + }); + }); }); // tslint:disable-line:max-file-line-count diff --git a/contracts/exchange/test/match_orders.ts b/contracts/exchange/test/match_orders.ts index c0b2077135..70e1091b8b 100644 --- a/contracts/exchange/test/match_orders.ts +++ b/contracts/exchange/test/match_orders.ts @@ -1336,7 +1336,7 @@ describe('matchOrders', () => { }); }); describe('matchOrdersWithMaximalFill', () => { - it('Should transfer correct amounts when right order is fully filled and values pass isRoundingErrorCeil but fail isRoundingErrorFloor', async () => { + it('should transfer correct amounts when right order is fully filled and values pass isRoundingErrorCeil but fail isRoundingErrorFloor', async () => { // Create orders to match const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ makerAddress: makerAddressLeft, diff --git a/contracts/utils/test/lib_eip712.ts b/contracts/utils/test/lib_eip712.ts index a2a0b7218a..812d3e35e2 100644 --- a/contracts/utils/test/lib_eip712.ts +++ b/contracts/utils/test/lib_eip712.ts @@ -11,55 +11,6 @@ chaiSetup.configure(); const expect = chai.expect; const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); -/** - * Tests a specific instance of EIP712 domain hashing. - * @param lib The LibEIP712 contract to call. - * @param name The name of the domain. - * @param version The version of the domain. - * @param chainId The chain id of the domain. - * @param verifyingContractAddress The verifying contract address of the domain. - */ -async function testHashEIP712DomainAsync( - lib: TestLibEIP712Contract, - name: string, - version: string, - chainId: number, - verifyingContractAddress: string, -): Promise { - const expectedHash = signTypedDataUtils.generateDomainHash({ - name, - version, - chainId, - verifyingContractAddress, - }); - const actualHash = await lib.externalHashEIP712DomainSeperator.callAsync( - name, - version, - new BigNumber(chainId), - verifyingContractAddress, - ); - expect(actualHash).to.be.eq(hexConcat(expectedHash)); -} - -/** - * Tests a specific instance of EIP712 message hashing. - * @param lib The LibEIP712 contract to call. - * @param domainHash The hash of the EIP712 domain of this instance. - * @param hashStruct The hash of the struct of this instance. - */ -async function testHashEIP712MessageAsync( - lib: TestLibEIP712Contract, - domainHash: string, - hashStruct: string, -): Promise { - const input = '0x1901'.concat( - domainHash.slice(2, domainHash.length).concat(hashStruct.slice(2, hashStruct.length)), - ); - const expectedHash = '0x'.concat(ethUtil.sha3(input).toString('hex')); - const actualHash = await lib.externalHashEIP712Message.callAsync(domainHash, hashStruct); - expect(actualHash).to.be.eq(expectedHash); -} - describe('LibEIP712', () => { let lib: TestLibEIP712Contract; @@ -73,28 +24,78 @@ describe('LibEIP712', () => { await blockchainLifecycle.revertAsync(); }); + /** + * Tests a specific instance of EIP712 domain hashing. + * @param lib The LibEIP712 contract to call. + * @param name The name of the domain. + * @param version The version of the domain. + * @param chainId The chain id of the domain. + * @param verifyingContractAddress The verifying contract address of the domain. + */ + async function testHashEIP712DomainAsync( + name: string, + version: string, + chainId: number, + verifyingContractAddress: string, + ): Promise { + const expectedHash = signTypedDataUtils.generateDomainHash({ + name, + version, + chainId, + verifyingContractAddress, + }); + const actualHash = await lib.externalHashEIP712DomainSeperator.callAsync( + name, + version, + new BigNumber(chainId), + verifyingContractAddress, + ); + expect(actualHash).to.be.eq(hexConcat(expectedHash)); + } + describe('_hashEIP712Domain', async () => { it('should correctly hash empty input', async () => { - await testHashEIP712DomainAsync(lib, '', '', 0, constants.NULL_ADDRESS); + await testHashEIP712DomainAsync('', '', 0, constants.NULL_ADDRESS); }); it('should correctly hash non-empty input', async () => { - await testHashEIP712DomainAsync(lib, '_hashEIP712Domain', '1.0', 62, lib.address); + await testHashEIP712DomainAsync('_hashEIP712Domain', '1.0', 62, lib.address); }); it('should correctly hash non-empty input', async () => { - await testHashEIP712DomainAsync(lib, '_hashEIP712Domain', '2.0', 0, lib.address); + await testHashEIP712DomainAsync('_hashEIP712Domain', '2.0', 0, lib.address); }); }); + /** + * Tests a specific instance of EIP712 message hashing. + * @param lib The LibEIP712 contract to call. + * @param domainHash The hash of the EIP712 domain of this instance. + * @param hashStruct The hash of the struct of this instance. + */ + async function testHashEIP712MessageAsync(domainHash: string, hashStruct: string): Promise { + // Remove the hex prefix from the domain hash and the hash struct + const unprefixedDomainHash = domainHash.slice(2, domainHash.length); + const unprefixedHashStruct = hashStruct.slice(2, hashStruct.length); + + // Hash the provided input to get the expected hash + const input = '0x1901'.concat(unprefixedDomainHash.concat(unprefixedHashStruct)); + const expectedHash = '0x'.concat(ethUtil.sha3(input).toString('hex')); + + // Get the actual hash by calling the smart contract + const actualHash = await lib.externalHashEIP712Message.callAsync(domainHash, hashStruct); + + // Verify that the actual hash matches the expected hash + expect(actualHash).to.be.eq(expectedHash); + } + describe('_hashEIP712Message', () => { it('should correctly hash empty input', async () => { - await testHashEIP712MessageAsync(lib, constants.NULL_BYTES32, constants.NULL_BYTES32); + await testHashEIP712MessageAsync(constants.NULL_BYTES32, constants.NULL_BYTES32); }); it('should correctly hash non-empty input', async () => { await testHashEIP712MessageAsync( - lib, '0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6', // keccak256(abi.encode(1)) '0x405787fa12a823e0f2b7631cc41b3ba8828b3321ca811111fa75cd3aa3bb5ace', // keccak256(abi.encode(2)) ); @@ -102,7 +103,6 @@ describe('LibEIP712', () => { it('should correctly hash non-empty input', async () => { await testHashEIP712MessageAsync( - lib, '0x405787fa12a823e0f2b7631cc41b3ba8828b3321ca811111fa75cd3aa3bb5ace', // keccak256(abi.encode(2)) '0xc2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85b', // keccak256(abi.encode(3)) ); diff --git a/packages/utils/src/sign_typed_data_utils.ts b/packages/utils/src/sign_typed_data_utils.ts index b9b9502a84..d1df23228a 100644 --- a/packages/utils/src/sign_typed_data_utils.ts +++ b/packages/utils/src/sign_typed_data_utils.ts @@ -20,6 +20,15 @@ export const signTypedDataUtils = { ]), ); }, + /** + * Generates the EIP712 Typed Data hash for a typed data object without using the domain field. This + * makes hashing easier for non-EIP712 data. + * @param typedData An object that conforms to the EIP712TypedData interface + * @return A Buffer containing the hash of the typed data. + */ + generateTypedDataHashWithoutDomain(typedData: EIP712TypedData): Buffer { + return signTypedDataUtils._structHash(typedData.primaryType, typedData.message, typedData.types); + }, /** * Generates the hash of a EIP712 Domain with the default schema * @param domain An EIP712 domain with the default schema containing a name, version, chain id,