From 6241ea4c253c29c6f884cc33ebe0ab61ac66dfd7 Mon Sep 17 00:00:00 2001 From: Alex Soong Date: Fri, 29 Jun 2018 14:23:59 -0700 Subject: [PATCH 1/2] Adding lib bytes and removing coverage of it. (#77) * Adding lib bytes and removing coverage of it. * Add slice * Add content address --- contracts/external/LibBytes.sol | 194 ++++++++++++++++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 contracts/external/LibBytes.sol diff --git a/contracts/external/LibBytes.sol b/contracts/external/LibBytes.sol new file mode 100644 index 000000000..dad0a92f9 --- /dev/null +++ b/contracts/external/LibBytes.sol @@ -0,0 +1,194 @@ +/* + Copyright 2018 ZeroEx Intl. + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +pragma solidity ^0.4.24; + +library LibBytes { + + using LibBytes for bytes; + + /// @dev Gets the memory address for the contents of a byte array. + /// @param input Byte array to lookup. + /// @return memoryAddress Memory address of the contents of the byte array. + function contentAddress(bytes memory input) + internal + pure + returns (uint256 memoryAddress) + { + assembly { + memoryAddress := add(input, 32) + } + return memoryAddress; + } + + + /// @dev Reads a bytes32 value from a position in a byte array. + /// @param b Byte array containing a bytes32 value. + /// @param index Index in byte array of bytes32 value. + /// @return bytes32 value from byte array. + function readBytes32( + bytes memory b, + uint256 index + ) + internal + pure + returns (bytes32 result) + { + require( + b.length >= index + 32, + "GREATER_OR_EQUAL_TO_32_LENGTH_REQUIRED" + ); + + // Arrays are prefixed by a 256 bit length parameter + index += 32; + + // Read the bytes32 from array memory + assembly { + result := mload(add(b, index)) + } + return result; + } + + /// @dev Copies `length` bytes from memory location `source` to `dest`. + /// @param dest memory address to copy bytes to. + /// @param source memory address to copy bytes from. + /// @param length number of bytes to copy. + function memCopy( + uint256 dest, + uint256 source, + uint256 length + ) + internal + pure + { + if (length < 32) { + // Handle a partial word by reading destination and masking + // off the bits we are interested in. + // This correctly handles overlap, zero lengths and source == dest + assembly { + let mask := sub(exp(256, sub(32, length)), 1) + let s := and(mload(source), not(mask)) + let d := and(mload(dest), mask) + mstore(dest, or(s, d)) + } + } else { + // Skip the O(length) loop when source == dest. + if (source == dest) { + return; + } + + // For large copies we copy whole words at a time. The final + // word is aligned to the end of the range (instead of after the + // previous) to handle partial words. So a copy will look like this: + // + // #### + // #### + // #### + // #### + // + // We handle overlap in the source and destination range by + // changing the copying direction. This prevents us from + // overwriting parts of source that we still need to copy. + // + // This correctly handles source == dest + // + if (source > dest) { + assembly { + // We subtract 32 from `sEnd` and `dEnd` because it + // is easier to compare with in the loop, and these + // are also the addresses we need for copying the + // last bytes. + length := sub(length, 32) + let sEnd := add(source, length) + let dEnd := add(dest, length) + + // Remember the last 32 bytes of source + // This needs to be done here and not after the loop + // because we may have overwritten the last bytes in + // source already due to overlap. + let last := mload(sEnd) + + // Copy whole words front to back + // Note: the first check is always true, + // this could have been a do-while loop. + for {} lt(source, sEnd) {} { + mstore(dest, mload(source)) + source := add(source, 32) + dest := add(dest, 32) + } + + // Write the last 32 bytes + mstore(dEnd, last) + } + } else { + assembly { + // We subtract 32 from `sEnd` and `dEnd` because those + // are the starting points when copying a word at the end. + length := sub(length, 32) + let sEnd := add(source, length) + let dEnd := add(dest, length) + + // Remember the first 32 bytes of source + // This needs to be done here and not after the loop + // because we may have overwritten the first bytes in + // source already due to overlap. + let first := mload(source) + + // Copy whole words back to front + // We use a signed comparisson here to allow dEnd to become + // negative (happens when source and dest < 32). Valid + // addresses in local memory will never be larger than + // 2**255, so they can be safely re-interpreted as signed. + // Note: the first check is always true, + // this could have been a do-while loop. + for {} slt(dest, dEnd) {} { + mstore(dEnd, mload(sEnd)) + sEnd := sub(sEnd, 32) + dEnd := sub(dEnd, 32) + } + + // Write the first 32 bytes + mstore(dest, first) + } + } + } + } + + /// @dev Returns a slices from a byte array. + /// @param b The byte array to take a slice from. + /// @param from The starting index for the slice (inclusive). + /// @param to The final index for the slice (exclusive). + /// @return result The slice containing bytes at indices [from, to) + function slice(bytes memory b, uint256 from, uint256 to) + internal + pure + returns (bytes memory result) + { + require( + from <= to, + "FROM_LESS_THAN_TO_REQUIRED" + ); + require( + to < b.length, + "TO_LESS_THAN_LENGTH_REQUIRED" + ); + + // Create a new bytes structure and copy contents + result = new bytes(to - from); + memCopy( + result.contentAddress(), + b.contentAddress() + from, + result.length); + return result; + } +} \ No newline at end of file From 5e0f1682f47b447e042120f4739703c340580665 Mon Sep 17 00:00:00 2001 From: Alexander Soong Date: Mon, 2 Jul 2018 00:26:34 -0700 Subject: [PATCH 2/2] Parse exchange orders data --- contracts/core/Core.sol | 2 + .../extensions/CoreExchangeDispatcher.sol | 67 ++++++++++ .../core/extensions/CoreIssuanceOrder.sol | 82 +++++++++++-- contracts/core/interfaces/IExchange.sol | 39 ++++++ contracts/core/lib/CoreState.sol | 11 ++ contracts/core/lib/ExchangeHandler.sol | 63 ++++++++++ contracts/core/lib/ExchangeOrderHandler.sol | 104 ---------------- contracts/external/LibBytes.sol | 2 +- .../test/lib/ExchangeOrderHandlerLibrary.sol | 44 ------- .../extensions/coreExchangeDispatcher.spec.ts | 114 ++++++++++++++++++ .../core/extensions/coreIssuanceOrder.spec.ts | 30 ++++- test/core/lib/exchangeOrderHandler.spec.ts | 74 ------------ test/logs/contracts/core.ts | 17 ++- test/utils/constants.ts | 9 +- test/utils/coreWrapper.ts | 27 ++++- test/utils/orderWrapper.ts | 64 +++++----- 16 files changed, 473 insertions(+), 276 deletions(-) create mode 100644 contracts/core/extensions/CoreExchangeDispatcher.sol create mode 100644 contracts/core/interfaces/IExchange.sol create mode 100644 contracts/core/lib/ExchangeHandler.sol delete mode 100644 contracts/core/lib/ExchangeOrderHandler.sol delete mode 100644 contracts/test/lib/ExchangeOrderHandlerLibrary.sol create mode 100644 test/core/extensions/coreExchangeDispatcher.spec.ts delete mode 100644 test/core/lib/exchangeOrderHandler.spec.ts diff --git a/contracts/core/Core.sol b/contracts/core/Core.sol index 0e4f56ffd..61166ca83 100644 --- a/contracts/core/Core.sol +++ b/contracts/core/Core.sol @@ -17,6 +17,7 @@ pragma solidity 0.4.24; import { CoreAccounting } from "./extensions/CoreAccounting.sol"; +import { CoreExchangeDispatcher } from "./extensions/CoreExchangeDispatcher.sol"; import { CoreFactory } from "./extensions/CoreFactory.sol"; import { CoreInternal } from "./extensions/CoreInternal.sol"; import { CoreIssuance } from "./extensions/CoreIssuance.sol"; @@ -32,6 +33,7 @@ import { CoreIssuanceOrder } from "./extensions/CoreIssuanceOrder.sol"; * creating Sets, as well as all collateral flows throughout the system. */ contract Core is + CoreExchangeDispatcher, CoreIssuanceOrder, CoreAccounting, CoreInternal, diff --git a/contracts/core/extensions/CoreExchangeDispatcher.sol b/contracts/core/extensions/CoreExchangeDispatcher.sol new file mode 100644 index 000000000..007b512bc --- /dev/null +++ b/contracts/core/extensions/CoreExchangeDispatcher.sol @@ -0,0 +1,67 @@ +/* + Copyright 2018 Set Labs Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +pragma solidity 0.4.24; + +import { Ownable } from "zeppelin-solidity/contracts/ownership/Ownable.sol"; +import { CoreState } from "../lib/CoreState.sol"; + + +/** + * @title Core Exchange Dispatcher + * @author Set Protocol + * + * The CoreExchangeDispatcher factilitates updating permissible exchanges + * that are used in filling issuance orders. See CoreState.State.exchanges + */ +contract CoreExchangeDispatcher is + Ownable, + CoreState +{ + + /* ============ Events ============ */ + + // Logs registration of new exchange + event ExchangeRegistered( + uint8 _exchangeId, + address _exchange + ); + + /* ============ Setter Functions ============ */ + + /** + * Register exchange address into mapping of exchanges + * + * @param _exchangeId Enumeration of exchange + * @param _exchange Exchange address to set + */ + function registerExchange( + uint8 _exchangeId, + address _exchange + ) + external + onlyOwner + { + // Add asset proxy and log registration. + state.exchanges[_exchangeId] = _exchange; + + // Add asset proxy and log registration. + emit ExchangeRegistered( + _exchangeId, + _exchange + ); + } +} diff --git a/contracts/core/extensions/CoreIssuanceOrder.sol b/contracts/core/extensions/CoreIssuanceOrder.sol index bc5e5e06c..c0329b876 100644 --- a/contracts/core/extensions/CoreIssuanceOrder.sol +++ b/contracts/core/extensions/CoreIssuanceOrder.sol @@ -15,11 +15,15 @@ */ pragma solidity 0.4.24; +pragma experimental "ABIEncoderV2"; import { SafeMath } from "zeppelin-solidity/contracts/math/SafeMath.sol"; import { CoreModifiers } from "../lib/CoreSharedModifiers.sol"; +import { CoreState } from "../lib/CoreState.sol"; +import { ExchangeHandler } from "../lib/ExchangeHandler.sol"; import { ICoreIssuance } from "../interfaces/ICoreIssuance.sol"; +import { LibBytes } from "../../external/LibBytes.sol"; import { OrderLibrary } from "../lib/OrderLibrary.sol"; @@ -32,13 +36,17 @@ import { OrderLibrary } from "../lib/OrderLibrary.sol"; * */ contract CoreIssuanceOrder is - CoreModifiers, - ICoreIssuance + ICoreIssuance, + CoreState, + CoreModifiers { using SafeMath for uint256; /* ============ Constants ============ */ + uint256 constant HEADER_LENGTH = 64; + + string constant INVALID_EXCHANGE = "Exchange does not exist."; string constant INVALID_SIGNATURE = "Invalid order signature."; string constant INVALID_TOKEN_AMOUNTS = "Quantity and makerTokenAmount should be greater than 0."; string constant ORDER_EXPIRED = "This order has expired."; @@ -48,12 +56,13 @@ contract CoreIssuanceOrder is /** * Fill an issuance order * - * @param _addresses [setAddress, makerAddress, makerToken, relayerToken] - * @param _values [quantity, makerTokenAmount, expiration, relayerTokenAmount, salt] - * @param _fillQuantity Quantity of set to be filled - * @param _v v element of ECDSA signature - * @param _r r element of ECDSA signature - * @param _s s element of ECDSA signature + * @param _addresses [setAddress, makerAddress, makerToken, relayerToken] + * @param _values [quantity, makerTokenAmount, expiration, relayerTokenAmount, salt] + * @param _fillQuantity Quantity of set to be filled + * @param _v v element of ECDSA signature + * @param _r r element of ECDSA signature + * @param _s s element of ECDSA signature + * @param _orderData Bytes array containing the exchange orders to execute */ function fillOrder( address[4] _addresses, @@ -61,14 +70,14 @@ contract CoreIssuanceOrder is uint _fillQuantity, uint8 _v, bytes32 _r, - bytes32 _s + bytes32 _s, + bytes _orderData ) external isValidSet(_addresses[0]) isPositiveQuantity(_fillQuantity) isNaturalUnitMultiple(_fillQuantity, _addresses[0]) { - OrderLibrary.IssuanceOrder memory order = OrderLibrary.IssuanceOrder({ setAddress: _addresses[0], quantity: _values[0], @@ -103,6 +112,9 @@ contract CoreIssuanceOrder is INVALID_SIGNATURE ); + // Execute exchange orders + executeExchangeOrders(_orderData); + //Issue Set issueInternal( order.makerAddress, @@ -111,7 +123,53 @@ contract CoreIssuanceOrder is ); } - /* ============ Internal Functions ============ */ + /* ============ Private Functions ============ */ + + /** + * Execute the exchange orders by parsing the order data and facilitating the transfers + * + * @param _orderData Bytes array containing the exchange orders to execute + */ + function executeExchangeOrders( + bytes _orderData + ) + private + { + uint256 scannedBytes; + while (scannedBytes < _orderData.length) { + // Read the next exchange order header + bytes memory headerData = LibBytes.slice( + _orderData, + scannedBytes, + scannedBytes.add(HEADER_LENGTH) + ); + ExchangeHandler.OrderHeader memory header = ExchangeHandler.parseOrderHeader( + headerData + ); + + // Get exchange address from state mapping based on header exchange info + address exchange = state.exchanges[header.exchange]; + + // Verify exchange address is registered + require( + exchange != address(0), + INVALID_EXCHANGE + ); + + // Read the order body based on header order length info + uint256 orderLength = header.orderLength.add(HEADER_LENGTH); + bytes memory orderBody = LibBytes.slice( + _orderData, + scannedBytes.add(HEADER_LENGTH), + scannedBytes.add(orderLength) + ); + + // TODO: Call Exchange + + // Update scanned bytes with header and body lengths + scannedBytes = scannedBytes.add(orderLength); + } + } /** * Validate order params are still valid @@ -123,7 +181,7 @@ contract CoreIssuanceOrder is OrderLibrary.IssuanceOrder _order, uint _fillQuantity ) - internal + private view { // Make sure makerTokenAmount and Set Token to issue is greater than 0. diff --git a/contracts/core/interfaces/IExchange.sol b/contracts/core/interfaces/IExchange.sol new file mode 100644 index 000000000..1e227b0ba --- /dev/null +++ b/contracts/core/interfaces/IExchange.sol @@ -0,0 +1,39 @@ +/* + Copyright 2018 Set Labs Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +pragma solidity 0.4.24; + + +/** + * @title IExchange + * @author Set Protocol + * + * Interface for executing an order with an exchange + */ +interface IExchange { + + /** + * Exchange some amount of takerToken for makerToken. + * + * @param _orderData Arbitrary bytes data for any information to pass to the exchange + * @return uint256 The amount of makerToken received + */ + function exchange( + bytes _orderData + ) + external + returns (uint256); +} diff --git a/contracts/core/lib/CoreState.sol b/contracts/core/lib/CoreState.sol index befbaaebc..798a110e9 100644 --- a/contracts/core/lib/CoreState.sol +++ b/contracts/core/lib/CoreState.sol @@ -29,6 +29,9 @@ contract CoreState { /* ============ Structs ============ */ struct State { + // Mapping of exchange enumeration to address + mapping(uint8 => address) exchanges; + // Address of the TransferProxy contract address transferProxyAddress; @@ -48,6 +51,14 @@ contract CoreState { /* ============ Public Getters ============ */ + function exchanges(uint8 _exchangeId) + public + view + returns(address) + { + return state.exchanges[_exchangeId]; + } + function transferProxyAddress() public view diff --git a/contracts/core/lib/ExchangeHandler.sol b/contracts/core/lib/ExchangeHandler.sol new file mode 100644 index 000000000..f2f7da3c2 --- /dev/null +++ b/contracts/core/lib/ExchangeHandler.sol @@ -0,0 +1,63 @@ +/* + Copyright 2018 Set Labs Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +pragma solidity 0.4.24; +pragma experimental "ABIEncoderV2"; + + +/** + * @title ExchangeHandler + * @author Set Protocol + * + * This library contains functions and structs to assist with parsing exchange orders data + */ +library ExchangeHandler { + + // ============ Structs ============ + + struct OrderHeader { + uint8 exchange; + uint256 orderLength; + } + + // ============ Internal Functions ============ + + /** + * Function to convert bytes into OrderHeader + * + * This will always trail an ExchangeOrderHeader, so we don't need to skip + * the first 32. See Notes in parseExchangeOrdersHeader + * + * @param _headerData Bytes representing the order body information + * @return OrderHeader Struct containing exchange order body data + */ + function parseOrderHeader( + bytes _headerData + ) + internal + pure + returns (OrderHeader memory) + { + OrderHeader memory header; + + assembly { + mstore(header, mload(add(_headerData, 32))) // exchange + mstore(add(header, 32), mload(add(_headerData, 64))) // orderLength + } + + return header; + } +} diff --git a/contracts/core/lib/ExchangeOrderHandler.sol b/contracts/core/lib/ExchangeOrderHandler.sol deleted file mode 100644 index 394eb7c7c..000000000 --- a/contracts/core/lib/ExchangeOrderHandler.sol +++ /dev/null @@ -1,104 +0,0 @@ -/* - Copyright 2018 Set Labs Inc. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -pragma solidity 0.4.24; -pragma experimental "ABIEncoderV2"; - - -/** - * @title ExchangeOrderHandler - * @author Set Protocol - * - * This library contains functions and structs to assist with parsing exchange orders data - */ -library ExchangeOrderHandler { - - // ============ Structs ============ - - struct IssuanceOrder { - address setToken; - uint256 quantity; - address makerToken; - uint256 makerTokenAmount; - uint256 expiration; - address feeToken; - uint256 feeAmount; - uint8 v; - bytes32 r; - bytes32 s; - } - - struct ExchangeOrdersHeader { - uint8 orderCount; - } - - struct ExchangeOrderBody { - uint8 exchange; - uint8 orderLength; - } - - // ============ Internal Functions ============ - - /** - * Function to convert bytes into ExchangeOrdersHeader - * - * Skipping the first 32 bytes in an array which stores the length - * - * @param _orderHeader Bytes representing the order header information - * @return ExchangeOrdersHeader Struct containing exchange order header data - */ - function parseExchangeOrdersHeader( - bytes _orderHeader - ) - internal - pure - returns (ExchangeOrdersHeader memory) - { - ExchangeOrdersHeader memory header; - - assembly { - mstore(header, mload(add(_orderHeader, 32))) // orderCount - } - - return header; - } - - /** - * Function to convert bytes into ExchangeOrderBody - * - * This will always trail an ExchangeOrderHeader, so we don't need to skip - * the first 32. See Notes in parseExchangeOrdersHeader - * - * @param _orderBody Bytes representing the order body information - * @return ExchangeOrderBody Struct containing exchange order body data - */ - function parseExchangeOrderBody( - bytes _orderBody - ) - internal - pure - returns (ExchangeOrderBody memory) - { - ExchangeOrderBody memory body; - - assembly { - mstore(body, mload(add(_orderBody, 32))) // exchange - mstore(add(body, 32), mload(add(_orderBody, 64))) // orderLength - } - - return body; - } -} diff --git a/contracts/external/LibBytes.sol b/contracts/external/LibBytes.sol index dad0a92f9..0c6be6f23 100644 --- a/contracts/external/LibBytes.sol +++ b/contracts/external/LibBytes.sol @@ -179,7 +179,7 @@ library LibBytes { "FROM_LESS_THAN_TO_REQUIRED" ); require( - to < b.length, + to <= b.length, "TO_LESS_THAN_LENGTH_REQUIRED" ); diff --git a/contracts/test/lib/ExchangeOrderHandlerLibrary.sol b/contracts/test/lib/ExchangeOrderHandlerLibrary.sol deleted file mode 100644 index b4f1629af..000000000 --- a/contracts/test/lib/ExchangeOrderHandlerLibrary.sol +++ /dev/null @@ -1,44 +0,0 @@ -pragma solidity 0.4.24; -pragma experimental "ABIEncoderV2"; - -import { ExchangeOrderHandler } from "../../core/lib/ExchangeOrderHandler.sol"; - - -// Mock class implementing internal OrderHandler methods -contract MockExchangeOrderHandlerLibrary { - function testExchangeOrdersHeader( - bytes _data - ) - public - pure - returns (uint8) - { - ExchangeOrderHandler.ExchangeOrdersHeader memory order; - order = ExchangeOrderHandler.parseExchangeOrdersHeader(_data); - return order.orderCount; - } - - function testExchangeOrdersBodyExchange( - bytes _body - ) - public - pure - returns (uint8) - { - ExchangeOrderHandler.ExchangeOrderBody memory order; - order = ExchangeOrderHandler.parseExchangeOrderBody(_body); - return order.exchange; - } - - function testExchangeOrdersBodyOrderLength( - bytes _body - ) - public - pure - returns (uint8) - { - ExchangeOrderHandler.ExchangeOrderBody memory order; - order = ExchangeOrderHandler.parseExchangeOrderBody(_body); - return order.orderLength; - } -} diff --git a/test/core/extensions/coreExchangeDispatcher.spec.ts b/test/core/extensions/coreExchangeDispatcher.spec.ts new file mode 100644 index 000000000..c8bc8f61b --- /dev/null +++ b/test/core/extensions/coreExchangeDispatcher.spec.ts @@ -0,0 +1,114 @@ +import * as chai from "chai"; +import * as _ from "lodash"; + +import * as ABIDecoder from "abi-decoder"; +import { BigNumber } from "bignumber.js"; +import { ether } from "../../utils/units"; + +// Types +import { Address, Log, UInt } from "../../../types/common.js"; + +// Contract types +import { CoreContract } from "../../../types/generated/core"; + +// Artifacts +const Core = artifacts.require("Core"); + +// Core wrapper +import { CoreWrapper } from "../../utils/coreWrapper"; +import { ERC20Wrapper } from "../../utils/erc20Wrapper"; + +// Testing Set up +import { BigNumberSetup } from "../../config/bigNumberSetup"; +import ChaiSetup from "../../config/chaiSetup"; +BigNumberSetup.configure(); +ChaiSetup.configure(); +const { expect } = chai; + +import { + EXCHANGES, +} from "../../utils/constants"; + +import { + expectRevertError, +} from "../../utils/tokenAssertions"; + +import { getFormattedLogsFromTxHash } from "../../logs/logUtils"; +import { assertLogEquivalence } from "../../logs/logAssertions"; +import { ExchangeRegistered } from "../../logs/contracts/core"; + +contract("CoreExchangeDispatcher", (accounts) => { + const [ + ownerAccount, + notOwnerAccount, + zeroExWrapperAddress, + ] = accounts; + + let core: CoreContract; + + const coreWrapper = new CoreWrapper(ownerAccount, ownerAccount); + + before(async () => { + ABIDecoder.addABI(Core.abi); + }); + + after(async () => { + ABIDecoder.removeABI(Core.abi); + }); + + beforeEach(async () => { + core = await coreWrapper.deployCoreAsync(); + }); + + describe("#registerExchange", async () => { + let subjectCaller: Address; + let subjectExchangeId: UInt; + let subjectExchangeAddress: Address; + + beforeEach(async () => { + subjectCaller = ownerAccount; + subjectExchangeId = EXCHANGES.ZERO_EX; + subjectExchangeAddress = zeroExWrapperAddress; + }); + + async function subject(): Promise { + return core.registerExchange.sendTransactionAsync( + subjectExchangeId, + subjectExchangeAddress, + { from: subjectCaller }, + ); + } + + it("sets exchange address correctly", async () => { + await subject(); + + const exchangeAddress = await core.exchanges.callAsync(subjectExchangeId); + expect(exchangeAddress).to.eql(subjectExchangeAddress); + }); + + it("emits a IssuanceComponentDeposited even for each component deposited", async () => { + const txHash = await subject(); + const formattedLogs = await getFormattedLogsFromTxHash(txHash); + + const expectedLogs: Log[] = [ + ExchangeRegistered( + core.address, + new BigNumber(subjectExchangeId), + subjectExchangeAddress, + ) + ]; + + await assertLogEquivalence(expectedLogs, formattedLogs); + }); + + describe("when the caller is not the owner of the contract", async () => { + beforeEach(async () => { + subjectCaller = notOwnerAccount; + }); + + it("should revert", async () => { + await expectRevertError(subject()); + }); + }); + }); +}); diff --git a/test/core/extensions/coreIssuanceOrder.spec.ts b/test/core/extensions/coreIssuanceOrder.spec.ts index b44e7c120..076544683 100644 --- a/test/core/extensions/coreIssuanceOrder.spec.ts +++ b/test/core/extensions/coreIssuanceOrder.spec.ts @@ -6,7 +6,7 @@ import { BigNumber } from "bignumber.js"; import { ether } from "../../utils/units"; // Types -import { Address, IssuanceOrder } from "../../../types/common.js"; +import { Address, Bytes32, IssuanceOrder } from "../../../types/common.js"; // Contract types import { CoreContract } from "../../../types/generated/core"; @@ -24,6 +24,8 @@ import { CoreWrapper } from "../../utils/coreWrapper"; import { ERC20Wrapper } from "../../utils/erc20Wrapper"; import { generateFillOrderParameters, + generateOrdersDataForOrderCount, + generateOrdersDataWithIncorrectExchange, } from "../../utils/orderWrapper"; // Testing Set up @@ -85,6 +87,7 @@ contract("CoreIssuanceOrder", (accounts) => { describe("#fillOrder", async () => { let subjectCaller: Address; let subjectQuantityToIssue: BigNumber; + let subjectExchangeOrdersData: Bytes32; const naturalUnit: BigNumber = ether(2); let components: StandardTokenMockContract[] = []; @@ -111,10 +114,12 @@ contract("CoreIssuanceOrder", (accounts) => { naturalUnit, ); + await coreWrapper.registerDefaultExchanges(core); + subjectCaller = takerAccount; subjectQuantityToIssue = ether(2); - issuanceOrderParams = await generateFillOrderParameters(setToken.address, signerAddress, signerAddress, componentAddresses[0]); + subjectExchangeOrdersData = generateOrdersDataForOrderCount(3); }); async function subject(): Promise { @@ -125,6 +130,7 @@ contract("CoreIssuanceOrder", (accounts) => { issuanceOrderParams.signature.v, issuanceOrderParams.signature.r, issuanceOrderParams.signature.s, + subjectExchangeOrdersData, { from: subjectCaller }, ); } @@ -141,6 +147,7 @@ contract("CoreIssuanceOrder", (accounts) => { const expectedNewBalance = existingBalance.sub(subjectQuantityToIssue.div(naturalUnit).mul(unit)); expect(newBalance).to.be.bignumber.equal(expectedNewBalance); }); + it("mints the correct quantity of the set for the user", async () => { const existingBalance = await setToken.balanceOf.callAsync(signerAddress); @@ -148,6 +155,7 @@ contract("CoreIssuanceOrder", (accounts) => { assertTokenBalance(setToken, existingBalance.add(subjectQuantityToIssue), signerAddress); }); + describe("when the quantity to issue is not positive", async () => { beforeEach(async () => { subjectQuantityToIssue = ZERO; @@ -157,6 +165,7 @@ contract("CoreIssuanceOrder", (accounts) => { await expectRevertError(subject()); }); }); + describe("when the set was not created through core", async () => { beforeEach(async () => { issuanceOrderParams = await generateFillOrderParameters(NULL_ADDRESS, signerAddress, signerAddress, componentAddresses[0]) @@ -166,6 +175,7 @@ contract("CoreIssuanceOrder", (accounts) => { await expectRevertError(subject()); }); }); + describe("when the quantity is not a multiple of the natural unit of the set", async () => { beforeEach(async () => { subjectQuantityToIssue = ether(3); @@ -175,6 +185,7 @@ contract("CoreIssuanceOrder", (accounts) => { await expectRevertError(subject()); }); }); + describe("when the order has expired", async () => { beforeEach(async () => { issuanceOrderParams = await generateFillOrderParameters(setToken.address, signerAddress, signerAddress, componentAddresses[0], undefined, undefined, -1) @@ -184,6 +195,7 @@ contract("CoreIssuanceOrder", (accounts) => { await expectRevertError(subject()); }); }); + describe("when invalid Set Token quantity in Issuance Order", async () => { beforeEach(async () => { issuanceOrderParams = await generateFillOrderParameters(setToken.address, signerAddress, signerAddress, componentAddresses[0], ZERO) @@ -193,6 +205,7 @@ contract("CoreIssuanceOrder", (accounts) => { await expectRevertError(subject()); }); }); + describe("when invalid makerTokenAmount in Issuance Order", async () => { beforeEach(async () => { issuanceOrderParams = await generateFillOrderParameters(setToken.address, signerAddress, signerAddress, componentAddresses[0], undefined, ZERO) @@ -202,9 +215,20 @@ contract("CoreIssuanceOrder", (accounts) => { await expectRevertError(subject()); }); }); + describe("when the message is not signed by the maker", async () => { beforeEach(async () => { - issuanceOrderParams = await generateFillOrderParameters(setToken.address, signerAddress, makerAccount, componentAddresses[0]) + issuanceOrderParams = await generateFillOrderParameters(setToken.address, signerAddress, makerAccount, componentAddresses[0]); + }); + + it("should revert", async () => { + await expectRevertError(subject()); + }); + }); + + describe("when an encoded exchangeId is invalid", async () => { + beforeEach(async () => { + subjectExchangeOrdersData = generateOrdersDataWithIncorrectExchange(); }); it("should revert", async () => { diff --git a/test/core/lib/exchangeOrderHandler.spec.ts b/test/core/lib/exchangeOrderHandler.spec.ts deleted file mode 100644 index 25d91420b..000000000 --- a/test/core/lib/exchangeOrderHandler.spec.ts +++ /dev/null @@ -1,74 +0,0 @@ -import * as chai from "chai"; -import * as _ from "lodash"; -import * as ethUtil from "ethereumjs-util"; - -import * as ABIDecoder from "abi-decoder"; -import { BigNumber } from "bignumber.js"; - -// Types -import { Address, Bytes32, Log } from "../../../types/common.js"; - -// Contract types -import { MockExchangeOrderHandlerLibraryContract } from "../../../types/generated/mock_exchange_order_handler_library"; - -// Artifacts -const MockExchangeOrderHandlerLibrary = artifacts.require("MockExchangeOrderHandlerLibrary"); - -import { - generateExchangeOrdersHeader, - generateExchangeOrdersBody -} from "../../utils/orderWrapper"; - -// Testing Set up -import { BigNumberSetup } from "../../config/bigNumberSetup"; -import ChaiSetup from "../../config/chaiSetup"; -BigNumberSetup.configure(); -ChaiSetup.configure(); -const { expect, assert } = chai; - -import { - DEFAULT_GAS, -} from "../../utils/constants"; - -contract("orderHandler", (accounts) => { - const [ownerAccount] = accounts; - let orderHandlerLibrary: MockExchangeOrderHandlerLibraryContract; - - beforeEach(async () => { - const orderHandlerInstance = await MockExchangeOrderHandlerLibrary.new( - { from: ownerAccount, gas: DEFAULT_GAS }, - ); - - orderHandlerLibrary = new MockExchangeOrderHandlerLibraryContract( - web3.eth.contract(orderHandlerInstance.abi).at(orderHandlerInstance.address), - { from: ownerAccount }, - ); - }); - - describe("#testParseExchangeOrdersHeader", async () => { - const subjectOrderData: Bytes32 = generateExchangeOrdersHeader(3); - - it("works", async () => { - const result = await orderHandlerLibrary.testExchangeOrdersHeader.callAsync(subjectOrderData); - expect(result).to.be.bignumber.equal(3); - }); - }); - - describe("#testParseExchangeOrdersBodyExchange", async () => { - const subjectExchangeOrdersBody: Bytes32 = generateExchangeOrdersBody(0, 64); - - it("works", async () => { - const result = await orderHandlerLibrary.testExchangeOrdersBodyExchange.callAsync(subjectExchangeOrdersBody); - expect(result).to.be.bignumber.equal(0); - }); - }); - - describe("#testParseExchangeOrdersBodyOrderLength", async () => { - const subjectExchangeOrdersBody: Bytes32 = generateExchangeOrdersBody(0, 64); - - it("works", async () => { - const result = await orderHandlerLibrary.testExchangeOrdersBodyOrderLength.callAsync(subjectExchangeOrdersBody); - expect(result).to.be.bignumber.equal(64); - }); - }); -}); diff --git a/test/logs/contracts/core.ts b/test/logs/contracts/core.ts index eb50cc93f..626077285 100644 --- a/test/logs/contracts/core.ts +++ b/test/logs/contracts/core.ts @@ -2,7 +2,7 @@ import * as _ from "lodash"; import * as LogUtils from "../logUtils"; import { BigNumber } from "bignumber.js"; -import { Address, Log } from "../../../types/common"; +import { Address, Log, UInt } from "../../../types/common"; interface CreateLogArgs { _setTokenAddress: Address; @@ -56,6 +56,21 @@ export function IssuanceComponentDeposited( } } +export function ExchangeRegistered( + _coreAddress: Address, + _exchangeId: UInt, + _exchange: Address, +): Log { + return { + event: "ExchangeRegistered", + address: _coreAddress, + args: { + _exchangeId, + _exchange + }, + } +} + export function extractNewSetTokenAddressFromLogs( logs: Log[], ): Address { diff --git a/test/utils/constants.ts b/test/utils/constants.ts index a06bb6149..857bea936 100644 --- a/test/utils/constants.ts +++ b/test/utils/constants.ts @@ -1,5 +1,6 @@ import { BigNumber } from "bignumber.js"; import { ether, gWei } from "../utils/units"; +import { Address, UInt } from "../../types/common.js"; export const DEFAULT_GAS = 10000000; export const DEFAULT_MOCK_TOKEN_DECIMALS = 18; @@ -16,10 +17,10 @@ export const MAX_DIGITS_IN_UNSIGNED_256_INT = 78; export const ZERO: BigNumber = new BigNumber(0); export const ONE: BigNumber = new BigNumber(1); -export const ORDER_TYPE = { - ZERO_EX: "zeroEx", - KYBER: "kyber", - MAKER_WALLET: "makerWallet", +export const EXCHANGES = { + ZERO_EX: 1, + KYBER: 2, + TAKER_WALLET: 3, } export const PRIVATE_KEYS = [ diff --git a/test/utils/coreWrapper.ts b/test/utils/coreWrapper.ts index 0306f7f6e..eb5fd51d2 100644 --- a/test/utils/coreWrapper.ts +++ b/test/utils/coreWrapper.ts @@ -11,7 +11,7 @@ import { VaultContract } from "../../types/generated/vault"; import { BigNumber } from "bignumber.js"; import { Address } from "../../types/common.js"; -import { DEFAULT_GAS } from "../utils/constants"; +import { DEFAULT_GAS, EXCHANGES } from "../utils/constants"; import { getFormattedLogsFromTxHash } from "../logs/logUtils"; import { extractNewSetTokenAddressFromLogs } from "../logs/contracts/core"; @@ -302,4 +302,29 @@ export class CoreWrapper { { from }, ); } + + // ExchangeDispatcher + + public async registerDefaultExchanges( + core: CoreContract, + from: Address = this._contractOwnerAddress, + ) { + const approvePromises = _.map(_.values(EXCHANGES), (exchangeId) => + this.registerExchange(core, exchangeId, this._tokenOwnerAddress, from) + ); + await Promise.all(approvePromises); + } + + public async registerExchange( + core: CoreContract, + exchangeId: number, + exchangeAddress: Address, + from: Address = this._contractOwnerAddress, + ) { + await core.registerExchange.sendTransactionAsync( + exchangeId, + exchangeAddress, + { from }, + ); + } } diff --git a/test/utils/orderWrapper.ts b/test/utils/orderWrapper.ts index 9277934d9..93a7f9a2e 100644 --- a/test/utils/orderWrapper.ts +++ b/test/utils/orderWrapper.ts @@ -7,7 +7,7 @@ import BN = require('bn.js'); import { Address, Bytes32, UInt, IssuanceOrder, SolidityTypes } from "../../types/common.js"; import { - ORDER_TYPE, + EXCHANGES, MAX_DIGITS_IN_UNSIGNED_256_INT, } from "../utils/constants"; @@ -28,45 +28,45 @@ function parseSigHexAsRSV(sigHex: string): any { return ecSig } -export function generateExchangeOrdersHeader( - orderCount: UInt +export function generateOrdersDataForOrderCount( + orderCount: number ): Bytes32 { - return ethUtil.bufferToHex( - ethUtil.setLengthLeft( - ethUtil.toBuffer(orderCount), 32) - ); + const exchangeOrderDatum: Buffer[] = []; + _.times(orderCount, (index) => { + const exchange = _.sample(EXCHANGES); + exchangeOrderDatum.push(paddedBufferForData(exchange)); + + const orderLength = _.random(120, 160) + exchangeOrderDatum.push(paddedBufferForData(orderLength)); + exchangeOrderDatum.push(randomBufferOfLength(orderLength)); + }); + + return ethUtil.bufferToHex(Buffer.concat(exchangeOrderDatum)); } -export function generateExchangeOrdersBody( - exchange: UInt, - orderLength: UInt, -): Bytes32 { +export function generateOrdersDataWithIncorrectExchange(): Bytes32 { + const invalidExchangeId = 4; + const orderLength = _.random(120, 160); - const buffer = Buffer.concat( - [ - ethUtil.setLengthLeft(ethUtil.toBuffer(exchange), 32), - ethUtil.setLengthLeft(ethUtil.toBuffer(orderLength), 32), - ] - ); + const exchangeOrderDatum: Buffer[] = [ + paddedBufferForData(invalidExchangeId), + paddedBufferForData(orderLength), + randomBufferOfLength(orderLength), + ]; - return ethUtil.bufferToHex(buffer); + return ethUtil.bufferToHex(Buffer.concat(exchangeOrderDatum)); } -export function generateExchangeOrdersData( - orderCount: UInt, - exchange: UInt, - orderLength: UInt, -): Bytes32 { - - const buffer = Buffer.concat( - [ - ethUtil.setLengthLeft(ethUtil.toBuffer(orderCount), 32), - ethUtil.setLengthLeft(ethUtil.toBuffer(exchange), 32), - ethUtil.setLengthLeft(ethUtil.toBuffer(orderLength), 32), - ] - ); +function paddedBufferForData( + data: any +): Buffer { + return ethUtil.setLengthLeft(ethUtil.toBuffer(data), 32); +} - return ethUtil.bufferToHex(buffer); +function randomBufferOfLength( + length: number +): Buffer { + return ethUtil.setLengthLeft(ethUtil.toBuffer(0), length); } export function generateSalt(): BigNumber {